diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 8584fb0d..748ea872 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -10169,7 +10169,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // that second one is single-sample on both sides, where ES does allow conversion. static Bool ResolveThenBlit(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLenum filter) { - static Uint s_resolveFramebuffer = 0; + static NativeContextName s_resolveFramebuffer; // per context: a framebuffer is never shared static Uint s_resolveRenderbuffer = 0; static GLenum s_resolveFormat = 0; static GLsizei s_resolveWidth = 0; @@ -10204,8 +10204,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } if (s_resolveFramebuffer == 0) { g_GLESFuncs.glGenFramebuffers(1, &s_resolveFramebuffer); + if (s_resolveFramebuffer == 0) return false; + } + if (s_resolveRenderbuffer == 0) { g_GLESFuncs.glGenRenderbuffers(1, &s_resolveRenderbuffer); - if (s_resolveFramebuffer == 0 || s_resolveRenderbuffer == 0) return false; + if (s_resolveRenderbuffer == 0) return false; s_resolveFormat = 0; } @@ -10485,12 +10488,12 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace ReplicateBlitImpl { static Uint s_contextGeneration = ~0u; - static GLuint s_framebuffer = 0; + static NativeContextName s_framebuffer; // per context, as is the vertex array below static GLuint s_texture = 0; static GLenum s_textureFormat = 0; static GLsizei s_textureWidth = 0; static GLsizei s_textureHeight = 0; - static GLuint s_vertexArray = 0; + static NativeContextName s_vertexArray; static GLuint s_depthProgram = 0; static GLuint s_stencilProgram = 0; static GLint s_depthUvTransform = -1; @@ -14138,11 +14141,12 @@ namespace MobileGL::MG_Backend::DirectGLES { // caller. No flip anywhere. namespace DepthStencilSamplingReadImpl { static Uint s_contextGeneration = ~0u; - static GLuint s_colorFramebuffer = 0; + static NativeContextName s_colorFramebuffer; // per context, as are the vertex array and the + // slots' framebuffers below static GLuint s_colorTexture = 0; static GLsizei s_colorWidth = 0; static GLsizei s_colorHeight = 0; - static GLuint s_vertexArray = 0; + static NativeContextName s_vertexArray; static GLuint s_depthProgram = 0; static GLuint s_stencilProgram = 0; static GLint s_depthUvTransform = -1; @@ -14162,7 +14166,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // other aspect attaches its own (differently formatted) texture to its own point // and the framebuffer is then incomplete - which reads as "no candidate format // worked" and writes nothing at all. - GLuint framebuffer = 0; GLuint texture = 0; GLenum format = 0; GLsizei width = 0; @@ -14173,6 +14176,9 @@ namespace MobileGL::MG_Backend::DirectGLES { GLenum defaultFramebufferFormat = 0; }; static StageSlot s_slots[2]; // [0] depth, [1] stencil + // Each slot's own framebuffer (see StageSlot), one per native context. + static NativeContextName s_slotFramebuffers[2]; + static NativeContextName& SlotFramebuffer(const StageSlot& slot) { return s_slotFramebuffers[&slot - s_slots]; } // `precision highp int` is not decoration: ESSL 3.00 defaults integers to mediump in // the fragment language, which is allowed to be 16 bits - it would saw the top half @@ -14214,6 +14220,8 @@ namespace MobileGL::MG_Backend::DirectGLES { s_programsFailed = false; s_slots[0] = StageSlot{}; s_slots[1] = StageSlot{}; + s_slotFramebuffers[0] = 0; + s_slotFramebuffers[1] = 0; s_contextGeneration = g_backendContextGeneration; } if (s_programsFailed) { @@ -14233,10 +14241,10 @@ namespace MobileGL::MG_Backend::DirectGLES { s_depthUvTransform = g_GLESFuncs.glGetUniformLocation(s_depthProgram, "uUvTransform"); s_stencilUvTransform = g_GLESFuncs.glGetUniformLocation(s_stencilProgram, "uUvTransform"); } - for (StageSlot& slot : s_slots) { - if (slot.framebuffer == 0) { - g_GLESFuncs.glGenFramebuffers(1, &slot.framebuffer); - if (slot.framebuffer == 0) return false; + for (NativeContextName& framebuffer : s_slotFramebuffers) { + if (framebuffer == 0) { + g_GLESFuncs.glGenFramebuffers(1, &framebuffer); + if (framebuffer == 0) return false; } } if (s_colorFramebuffer == 0) { @@ -14519,7 +14527,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } StageSlot& slot = s_slots[stencilAspect ? 1 : 0]; - FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, slot.framebuffer); + FramebufferImpl::BindFramebufferId(GL_DRAW_FRAMEBUFFER, SlotFramebuffer(slot)); if (!StageAspect(slot, candidates, stencilAspect, isDefault, x, y, width, height)) { MGLOG_E_ONCE("ReadPixels: no ES-compatible scratch format for the %s source", stencilAspect ? "stencil" : "depth"); @@ -16221,6 +16229,8 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 ContextToken = 0; Uint64 ShareGroupToken = 0; EGLContext Context = EGL_NO_CONTEXT; + // Context's lifetime serial (CurrentNativeContextSerial); a new one per eglCreateContext. + Uint64 Serial = 0; EGLConfig Config = nullptr; // The session surface this context draws to / reads from, as last made current for it. EGLSurface Draw = EGL_NO_SURFACE; @@ -16330,8 +16340,45 @@ namespace MobileGL::MG_Backend::DirectGLES { // And the surface it was made current with: a bind to the same context on a different // surface (one client context drawing to several windows) is a real switch. thread_local EGLSurface t_boundNativeDraw = EGL_NO_SURFACE; + // ...and that context's lifetime serial, the key of its container objects. + thread_local Uint64 t_boundNativeSerial = 0; + std::atomic g_nativeContextSerials{0}; + + std::mutex& PerNativeContextRegistryMutex() { + static auto* mutex = new std::mutex(); + return *mutex; + } + Vector& PerNativeContextRegistry() { + static auto* registry = new Vector(); + return *registry; + } } // namespace + Uint64 CurrentNativeContextSerial() { return t_boundNativeSerial; } + + namespace { + std::atomic g_perNativeContextEpoch{1}; + } + Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); } + void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); } + + PerNativeContextBase::PerNativeContextBase() { + const std::lock_guard lock(PerNativeContextRegistryMutex()); + PerNativeContextRegistry().push_back(this); + } + + PerNativeContextBase::~PerNativeContextBase() { + const std::lock_guard lock(PerNativeContextRegistryMutex()); + auto& registry = PerNativeContextRegistry(); + registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end()); + } + + void ForgetNativeContextObjects(Uint64 serial) { + if (serial == 0) return; + const std::lock_guard lock(PerNativeContextRegistryMutex()); + for (PerNativeContextBase* holder : PerNativeContextRegistry()) holder->ForgetContext(serial); + } + static NativeSessionState& AcquireNativeSession(Uint64 sessionKey) { for (auto* session : NativeSessions()) { if (session->SessionKey == sessionKey) return *session; @@ -16910,6 +16957,7 @@ namespace MobileGL::MG_Backend::DirectGLES { EGL_NONE}; tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, contextAttribs); if (tuple.Context != EGL_NO_CONTEXT) { + tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; ++g_displayUsers; return true; } @@ -16918,6 +16966,7 @@ namespace MobileGL::MG_Backend::DirectGLES { const EGLint legacyContextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE}; tuple.Context = g_EGLFuncs.eglCreateContext(g_Display, tuple.Config, share, legacyContextAttribs); if (tuple.Context == EGL_NO_CONTEXT) return false; + tuple.Serial = g_nativeContextSerials.fetch_add(1, std::memory_order_relaxed) + 1; ++g_displayUsers; return true; } @@ -16963,6 +17012,15 @@ namespace MobileGL::MG_Backend::DirectGLES { } g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); } + // This thread no longer has it current either way; a context created later at the same + // address must not read as "already bound". + if (t_boundNativeContext == tuple->Context) { + t_boundNativeContext = EGL_NO_CONTEXT; + t_boundNativeDraw = EGL_NO_SURFACE; + t_boundNativeSerial = 0; + } + ForgetNativeContextObjects(tuple->Serial); + tuple->Serial = 0; tuple->Context = EGL_NO_CONTEXT; tuple->Config = nullptr; tuple->Draw = EGL_NO_SURFACE; @@ -17241,7 +17299,9 @@ namespace MobileGL::MG_Backend::DirectGLES { // the allocate path. std::atomic g_currentFrameSerial{0}; std::atomic g_completedFrameSerial{0}; - constexpr int kFrameFenceRingDepth = 4; + // Deep enough for several sessions presenting in turn: a slot is only reused once its + // fence has signaled (InsertFrameFence waits for it rather than dropping it). + constexpr int kFrameFenceRingDepth = 32; struct FrameFence { GLsync sync = nullptr; Uint contextGeneration = 0; @@ -17331,6 +17391,7 @@ namespace MobileGL::MG_Backend::DirectGLES { InvalidateEglVerifiedStamp(); t_boundNativeContext = tuple.Context; t_boundNativeDraw = session.Draw; + t_boundNativeSerial = tuple.Serial; g_backendContextOwnerThread.store(std::this_thread::get_id(), std::memory_order_release); // The ops table may have been unregistered when a previous ES context was // destroyed (e.g. a probe context); re-register now that GL is usable. @@ -17375,6 +17436,7 @@ namespace MobileGL::MG_Backend::DirectGLES { InvalidateEglVerifiedStamp(); t_boundNativeContext = EGL_NO_CONTEXT; t_boundNativeDraw = EGL_NO_SURFACE; + t_boundNativeSerial = 0; g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } @@ -17800,31 +17862,63 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 CurrentFrameSerial() { return g_currentFrameSerial.load(std::memory_order_relaxed); } Uint64 CompletedFrameSerial() { return g_completedFrameSerial.load(std::memory_order_relaxed); } + namespace { + // THE WATERMARK IS CONTIGUOUS. Fences signal in submission order only within ONE context, + // and the serials are handed out to every native context of every session in turn, so the + // highest signaled serial says nothing about an older fence of another context still + // pending. A serial is complete when no live fence at or below it is pending. + void PollFrameFences() { + if (!g_GLESFuncs.glGetSynciv) return; + Uint64 lowestPending = ~Uint64{0}; + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync) continue; + if (slot.contextGeneration != g_syncContextGeneration) { + slot.sync = nullptr; // its context reclaimed it, and its work is over + continue; + } + GLint status = GL_SIGNALED; + GLsizei length = 0; + g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status); + if (status == GL_SIGNALED) { + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); + slot.sync = nullptr; + } else if (slot.serial < lowestPending) { + lowestPending = slot.serial; + } + } + const Uint64 completed = + lowestPending == ~Uint64{0} ? g_currentFrameSerial.load(std::memory_order_relaxed) : lowestPending - 1; + if (completed > g_completedFrameSerial.load(std::memory_order_relaxed)) { + g_completedFrameSerial.store(completed, std::memory_order_relaxed); + } + } + + FrameFence* OldestPendingFrameFence() { + FrameFence* oldest = nullptr; + for (FrameFence& slot : g_frameFenceRing) { + if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; + if (!oldest || slot.serial < oldest->serial) oldest = &slot; + } + return oldest; + } + } // namespace + Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs) { if (CompletedFrameSerial() >= serial) return true; if (!IsBackendContextCurrentOnThisThread() || !g_GLESFuncs.glClientWaitSync) return false; - // Fences signal in submission order, so the live fence with the SMALLEST - // serial at or past the target is the earliest event that proves the - // target frame retired. A recycled slot (GPU more than ring-depth frames - // behind) leaves no usable fence; report failure and let the caller pick - // its own fallback rather than draining the whole queue here. - FrameFence* best = nullptr; - for (FrameFence& slot : g_frameFenceRing) { - if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; - if (slot.serial < serial) continue; - if (!best || slot.serial < best->serial) best = &slot; + // Oldest pending fence first: the watermark can only pass a serial once every older fence + // (of any context) has signaled. Each fence was flushed when it was made (Present's swap), + // so waiting from this context needs no flush of the other. + for (int guard = 0; guard < kFrameFenceRingDepth + 1; ++guard) { + PollFrameFences(); + if (CompletedFrameSerial() >= serial) return true; + FrameFence* oldest = OldestPendingFrameFence(); + if (!oldest) return false; + const GLenum status = g_GLESFuncs.glClientWaitSync(oldest->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs); + if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false; } - if (!best) return false; - const GLenum status = - g_GLESFuncs.glClientWaitSync(best->sync, GL_SYNC_FLUSH_COMMANDS_BIT, timeoutNs); - if (status != GL_ALREADY_SIGNALED && status != GL_CONDITION_SATISFIED) return false; - Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed); - if (best->serial > completed) { - g_completedFrameSerial.store(best->serial, std::memory_order_relaxed); - } - if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(best->sync); - best->sync = nullptr; - return true; + PollFrameFences(); + return CompletedFrameSerial() >= serial; } void Present() { @@ -17835,8 +17929,14 @@ namespace MobileGL::MG_Backend::DirectGLES { if (canFence) { const Uint64 serial = g_currentFrameSerial.fetch_add(1, std::memory_order_relaxed) + 1; FrameFence& slot = g_frameFenceRing[serial % kFrameFenceRingDepth]; - if (slot.sync && slot.contextGeneration == g_syncContextGeneration && g_GLESFuncs.glDeleteSync) { - g_GLESFuncs.glDeleteSync(slot.sync); + if (slot.sync && slot.contextGeneration == g_syncContextGeneration) { + // The slot's fence is a ring-depth of serials old and may be another context's: + // dropping it unsignaled would let the watermark pass work nobody waited for. + if (g_GLESFuncs.glClientWaitSync) { + constexpr Uint64 kOldFenceWaitNs = 100ull * 1000 * 1000; + (void)g_GLESFuncs.glClientWaitSync(slot.sync, GL_SYNC_FLUSH_COMMANDS_BIT, kOldFenceWaitNs); + } + if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); } slot = {g_GLESFuncs.glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0), g_syncContextGeneration, serial}; } @@ -17857,23 +17957,7 @@ namespace MobileGL::MG_Backend::DirectGLES { g_EGLFuncs.eglSwapBuffers(g_Display, drawSurface); } - if (canFence && g_GLESFuncs.glGetSynciv) { - // Fences signal in submission order within one context, so the highest - // signaled serial is a valid contiguous completion watermark. - Uint64 completed = g_completedFrameSerial.load(std::memory_order_relaxed); - for (FrameFence& slot : g_frameFenceRing) { - if (!slot.sync || slot.contextGeneration != g_syncContextGeneration) continue; - GLint status = GL_SIGNALED; - GLsizei length = 0; - g_GLESFuncs.glGetSynciv(slot.sync, GL_SYNC_STATUS, 1, &length, &status); - if (status == GL_SIGNALED) { - if (slot.serial > completed) completed = slot.serial; - if (g_GLESFuncs.glDeleteSync) g_GLESFuncs.glDeleteSync(slot.sync); - slot.sync = nullptr; - } - } - g_completedFrameSerial.store(completed, std::memory_order_relaxed); - } + if (canFence) PollFrameFences(); // After the watermark advanced: retire grown-away ring stores and record the // frame's ring high-water marks for slot reclamation. @@ -17969,8 +18053,11 @@ namespace MobileGL::MG_Backend::DirectGLES { } t_boundNativeContext = EGL_NO_CONTEXT; t_boundNativeDraw = EGL_NO_SURFACE; + t_boundNativeSerial = 0; for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; + ForgetNativeContextObjects(tuple->Serial); + tuple->Serial = 0; if (g_EGLFuncs.eglDestroyContext) g_EGLFuncs.eglDestroyContext(g_Display, tuple->Context); tuple->Context = EGL_NO_CONTEXT; tuple->Config = nullptr; diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index 94b2441f..3cc6ed63 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -293,6 +293,98 @@ namespace MobileGL::MG_Backend::DirectGLES { // has gone. void DestroyNativeContextFor(Uint64 contextToken); + // ---- objects that belong to ONE native context ---- + // + // Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never + // shares them, not even between contexts of one share group, and every context numbers its own + // from 1. A process-wide scratch FBO id minted in one context and bound in another therefore + // names THAT context's own framebuffer - often an application's - and the scratch path's + // attach/scrub then rewires it onto staging textures. With one process serving several + // sessions (each with its own native contexts) that silently corrupted other clients' frames. + // + // The calling thread's current native context as a lifetime serial: assigned when the context + // is created and never reused (a recycled EGLContext address is a new serial). 0 when none was + // made current through MakeCurrent. + Uint64 CurrentNativeContextSerial(); + + class PerNativeContextBase { + public: + PerNativeContextBase(); + PerNativeContextBase(const PerNativeContextBase&) = delete; + PerNativeContextBase& operator=(const PerNativeContextBase&) = delete; + // The context died, and its names with it: forget them (no GL call - there is no context + // left to make one in). + virtual void ForgetContext(Uint64 serial) = 0; + + protected: + ~PerNativeContextBase(); + }; + + // Called where a native context is destroyed. + void ForgetNativeContextObjects(Uint64 serial); + // Moves whenever any per-context entry is dropped; a thread's cached entry is valid while + // it has not moved. + Uint64 PerNativeContextEpoch(); + void BumpPerNativeContextEpoch(); + + // One T per native context, created on first use from that context. + template + class PerNativeContext final : public PerNativeContextBase { + public: + T& Current() { + const Uint64 serial = CurrentNativeContextSerial(); + // The draw path asks for the same entry over and over from one thread: a one-entry + // thread-local cache, invalidated by any drop anywhere (rare). + thread_local CurrentCache cache; + const Uint64 epoch = PerNativeContextEpoch(); + if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry; + const std::lock_guard lock(m_mutex); + T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts + cache = {this, serial, epoch, &entry}; + return entry; + } + template + void ForEach(F&& visit) { + const std::lock_guard lock(m_mutex); + for (auto& entry : m_bySerial) visit(entry.second); + } + void Clear() { + const std::lock_guard lock(m_mutex); + BumpPerNativeContextEpoch(); + m_bySerial.clear(); + } + void ForgetContext(Uint64 serial) override { + const std::lock_guard lock(m_mutex); + BumpPerNativeContextEpoch(); + m_bySerial.erase(serial); + } + + private: + struct CurrentCache { + const PerNativeContext* Owner = nullptr; + Uint64 Serial = 0; + Uint64 Epoch = 0; + T* Entry = nullptr; + }; + std::mutex m_mutex; + std::unordered_map m_bySerial; + }; + + // The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads + // as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it. + class NativeContextName { + public: + operator GLuint() const { return m_names.Current(); } + NativeContextName& operator=(GLuint name) { + m_names.Current() = name; + return *this; + } + GLuint* operator&() { return &m_names.Current(); } + + private: + mutable PerNativeContext m_names; + }; + // P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's // records belong to; this makes the calling thread's native tuple follow it, binding the // session's current surface to the newly named context's native EGLContext. A no-op when the diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b77853d4..d4cda8ee 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1083,14 +1083,14 @@ namespace MobileGL::MG_Backend::DirectGLES { const char* label = ""; }; - PersistentRing g_uboRing{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"}; - PersistentRing g_unpackRing{{}, - {}, - {}, - kUnpackRingInitialBytes, - kUnpackRingMaxBytes, - kUnpackRingAlignment, - "Texture unpack ring"}; + // THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at + // Present, which say "everything this context wrote before its fence N is done once N is". + // One process-wide set used to be shared by every session's contexts: a fence from one + // context retired bytes another context's not-yet-executed copies still had to read + // (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's + // staging overwrote them - its pixels landed, at the wrong offsets, in the first one's + // textures. With one set per context, a context's bytes wait for that context's own + // fence. (The buffers themselves are shared objects; only who stages into them is split.) // Staging ring for app buffer updates whose destination store may still be // referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR // hazard by BLOCKING in the call (osup_sync_object_wait) until every @@ -1099,13 +1099,67 @@ namespace MobileGL::MG_Backend::DirectGLES { // chunks stream in). Staging the bytes here and issuing a // glCopyBufferSubData instead keeps the hazard on the GPU timeline where it // is just job ordering, and the CPU never waits. - PersistentRing g_uploadRing{{}, - {}, - {}, - kUploadRingInitialBytes, - kUploadRingMaxBytes, - kUploadRingAlignment, - "Buffer upload ring"}; + struct RingSet { + PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"}; + PersistentRing unpack{{}, + {}, + {}, + kUnpackRingInitialBytes, + kUnpackRingMaxBytes, + kUnpackRingAlignment, + "Texture unpack ring"}; + PersistentRing upload{{}, + {}, + {}, + kUploadRingInitialBytes, + kUploadRingMaxBytes, + kUploadRingAlignment, + "Buffer upload ring"}; + RingSet(); + ~RingSet(); + RingSet(const RingSet&) = delete; + RingSet& operator=(const RingSet&) = delete; + }; + + // A dead context's ring buffers: shared objects, so they outlive it; deleted at the next + // Present of any context of the same share-group generation. + struct OrphanRingBuffer { + Uint id = 0; + Uint contextGeneration = 0; + }; + std::mutex g_orphanRingBuffersMutex; + Vector& OrphanRingBuffers() { + static auto* orphans = new Vector(); + return *orphans; + } + + // Each set's generations start in a range of their own: a per-program slot cache keyed + // by (generation, offset) must never take one context's ring for another's. + std::atomic g_ringGenerationBases{0}; + + RingSet::RingSet() { + const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12; + ubo.store.generation = base; + unpack.store.generation = base; + upload.store.generation = base; + } + + RingSet::~RingSet() { + const std::lock_guard lock(g_orphanRingBuffersMutex); + for (const PersistentRing* ring : {&ubo, &unpack, &upload}) { + if (ring->store.id != 0) { + OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration}); + } + for (const auto& retired : ring->retired) { + if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration}); + } + } + } + + DirectGLES::PerNativeContext g_ringSets; + PersistentRing& UboRing() { return g_ringSets.Current().ubo; } + PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; } + PersistentRing& UploadRing() { return g_ringSets.Current().upload; } // The ES context the ring's id/map belonged to is gone (or was never // seen): drop every handle without GL calls and re-arm creation. The @@ -1383,7 +1437,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool UploadRingUsableNow() { if (MG_Config::Features.EsprytDisableUploadRing) return false; if (!g_GLESFuncs.glCopyBufferSubData) return false; - return RingAvailable(g_uploadRing); + return RingAvailable(UploadRing()); } // A partial range below this goes through the staging ring instead of a @@ -1532,9 +1586,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(g_uploadRing, size, ringOffset)) { - Memcpy(g_uploadRing.store.mappedPtr + ringOffset, hostBase + start, size); - BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id); + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size); @@ -1611,9 +1665,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(g_uploadRing, size, ringOffset)) { - Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size); - BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id); + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset, (GLintptr)write.offset, @@ -1714,7 +1768,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool UploadRingUsableNow() { if (MG_Config::Features.EsprytDisableUploadRing) return false; if (!g_GLESFuncs.glCopyBufferSubData) return false; - return RingAvailable(g_uploadRing); + return RingAvailable(UploadRing()); } // A partial range below this goes through the staging ring instead of a @@ -1820,9 +1874,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(g_uploadRing, size, ringOffset)) { - Memcpy(g_uploadRing.store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size); - BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id); + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size); @@ -1866,9 +1920,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } SizeT ringOffset = 0; if (ringUsable && size <= kUploadRingMaxBytes && - RingAllocate(g_uploadRing, size, ringOffset)) { - Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size); - BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id); + RingAllocate(UploadRing(), size, ringOffset)) { + Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size); + BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id); BindBufferId(GL_COPY_WRITE_BUFFER, resource.id); g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, (GLintptr)ringOffset, (GLintptr)write.offset, @@ -3895,8 +3949,8 @@ namespace MobileGL::MG_Backend::DirectGLES { InvalidatePixelBufferBindingCaches(); // The rings' ids and persistent maps died with the context; drop the // handles (no GL) and let the next draw / texture upload recreate them. - ResetRingForNewContext(g_uboRing); - ResetRingForNewContext(g_unpackRing); + ResetRingForNewContext(UboRing()); + ResetRingForNewContext(UnpackRing()); #if MOBILEGL_PIPE_PUSH // R-11's server copies die with the context for the same reason the rings' ids do: // the resource twins they are keyed by are about to be rebuilt against a new @@ -4905,9 +4959,23 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } + void DeleteOrphanRingBuffers() { + const std::lock_guard lock(g_orphanRingBuffersMutex); + auto& orphans = OrphanRingBuffers(); + if (orphans.empty()) return; + for (const OrphanRingBuffer& orphan : orphans) { + // A buffer of a share group that is gone was reclaimed with it. + if (orphan.contextGeneration != g_bufferContextGeneration) continue; + ScrubBufferBindingShadowsForId(orphan.id); + g_GLESFuncs.glDeleteBuffers(1, &orphan.id); + } + orphans.clear(); + } + // Present()-time upkeep shared by both rings. void RingOnPresent(PersistentRing& ring) { if (!CanTouchGLNow()) return; + DeleteOrphanRingBuffers(); // Delete grown-away stores the GPU is provably done with. const Uint64 completed = DirectGLES::CompletedFrameSerial(); @@ -4952,36 +5020,36 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool UboRingAvailable() { if (MG_Config::Features.EsprytDisableUboRing) return false; - return RingAvailable(g_uboRing); + return RingAvailable(UboRing()); } - Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(g_uboRing, size, outOffset); } + Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); } - void* UboRingMappedPtr() { return g_uboRing.store.mappedPtr; } - Uint UboRingBufferId() { return g_uboRing.store.id; } - Uint32 UboRingGeneration() { return g_uboRing.store.generation; } + void* UboRingMappedPtr() { return UboRing().store.mappedPtr; } + Uint UboRingBufferId() { return UboRing().store.id; } + Uint32 UboRingGeneration() { return UboRing().store.generation; } - void UboRingOnPresent() { RingOnPresent(g_uboRing); } + void UboRingOnPresent() { RingOnPresent(UboRing()); } Bool UnpackRingAvailable() { if (MG_Config::Features.EsprytDisableUnpackRing) return false; - return RingAvailable(g_unpackRing); + return RingAvailable(UnpackRing()); } Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) { // A request the ring could never satisfy even empty would otherwise walk // the whole grow/drain ladder before failing. if (size > kUnpackRingMaxBytes) return false; - return RingAllocate(g_unpackRing, size, outOffset); + return RingAllocate(UnpackRing(), size, outOffset); } - void* UnpackRingMappedPtr() { return g_unpackRing.store.mappedPtr; } - Uint UnpackRingBufferId() { return g_unpackRing.store.id; } + void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; } + Uint UnpackRingBufferId() { return UnpackRing().store.id; } SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; } - void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); } + void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); } - void UploadRingOnPresent() { RingOnPresent(g_uploadRing); } + void UploadRingOnPresent() { RingOnPresent(UploadRing()); } } // namespace BufferImpl #if MOBILEGL_PIPE_PUSH @@ -12891,11 +12959,16 @@ namespace MobileGL::MG_Backend::DirectGLES { namespace ScratchFBOImpl { namespace { - ScratchFramebuffer g_tempFramebuffer; - ScratchFramebuffer g_blitReadFramebuffer; - ScratchFramebuffer g_blitDrawFramebuffer; - Uint g_completeTinyFBOId = 0; - Uint g_completeTinyRBOId = 0; + // ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it + // (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it. + struct ScratchSet { + ScratchFramebuffer temp; + ScratchFramebuffer blitRead; + ScratchFramebuffer blitDraw; + Uint completeTinyFBOId = 0; + Uint completeTinyRBOId = 0; + }; + PerNativeContext g_scratchSets; // Detach every point the shadow no longer vouches for. Used when the // shadow is unknown (context reset, texture id deleted while attached). @@ -12950,13 +13023,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } // namespace ScratchFramebuffer& TempFramebuffer() { - return g_tempFramebuffer; + return g_scratchSets.Current().temp; } ScratchFramebuffer& BlitReadFramebuffer() { - return g_blitReadFramebuffer; + return g_scratchSets.Current().blitRead; } ScratchFramebuffer& BlitDrawFramebuffer() { - return g_blitDrawFramebuffer; + return g_scratchSets.Current().blitDraw; } Uint EnsureId(ScratchFramebuffer& fb) { @@ -13125,6 +13198,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } Uint EnsureCompleteTinyFramebufferId() { + ScratchSet& set = g_scratchSets.Current(); + Uint& g_completeTinyFBOId = set.completeTinyFBOId; + Uint& g_completeTinyRBOId = set.completeTinyRBOId; if (g_completeTinyFBOId != 0) { return g_completeTinyFBOId; } @@ -13152,19 +13228,18 @@ namespace MobileGL::MG_Backend::DirectGLES { if (textureId == 0) { return; } - for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) { - if (fb->colorTex == textureId || fb->depthTex == textureId) { - fb->attachmentsKnown = false; + // Textures ARE shared, so the deleted id may sit in any context's scratch FBO. + g_scratchSets.ForEach([textureId](ScratchSet& set) { + for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) { + if (fb->colorTex == textureId || fb->depthTex == textureId) { + fb->attachmentsKnown = false; + } } - } + }); } void OnBackendContextDestroyed() { - g_tempFramebuffer = {}; - g_blitReadFramebuffer = {}; - g_blitDrawFramebuffer = {}; - g_completeTinyFBOId = 0; - g_completeTinyRBOId = 0; + g_scratchSets.Clear(); } } // namespace ScratchFBOImpl diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index c146aa95..ca1f093c 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -224,6 +224,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return found->second; } SharedPtr fresh = MakeShared
(); + fresh->SetOwnerSession(key.SessionKey); m_buckets.emplace(key, fresh); m_memoKey = key; m_memoTable = fresh; @@ -458,6 +459,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // ENTRIES and the memo; the links are the table's own and are never copied. BackendSlotTable() { LinkHolder(); } BackendSlotTable(const BackendSlotTable& other): + m_ownerSession(other.m_ownerSession), m_slots(other.m_slots), m_band(other.m_band), m_nullTwin(other.m_nullTwin), @@ -466,6 +468,7 @@ namespace MobileGL::MG_Backend::DirectGLES { LinkHolder(); } BackendSlotTable(BackendSlotTable&& other) noexcept: + m_ownerSession(other.m_ownerSession), m_slots(std::move(other.m_slots)), m_band(std::move(other.m_band)), m_nullTwin(std::move(other.m_nullTwin)), @@ -501,6 +504,12 @@ namespace MobileGL::MG_Backend::DirectGLES { } ~BackendSlotTable() { UnlinkHolder(); } + // The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a + // table that belongs to none (the process-wide registry on a sessionless thread, a + // fixture's copy of one). + void SetOwnerSession(Uint64 session) { m_ownerSession = session; } + Uint64 OwnerSession() const { return m_ownerSession; } + // Resolve-or-create. The handle comes from the client allocator keyed on the frontend // object's lifetime id, so two calls for the same live object always land on the same // slot, and a successor object at the same heap address never does. @@ -802,12 +811,24 @@ namespace MobileGL::MG_Backend::DirectGLES { // double Free would be refused by generation anyway. Idempotent against the kind's // own delete opcode, exactly as the notice arm is: a twin the delete already released // fails ReleaseTwinAt's generation check and the walk moves on. + // + // ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own + // client's allocator, so with several clients served by one process the same {slot, gen} + // names a live, unrelated object in every other session: walking every holder released + // THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's + // driver texture with the same handle, and its windows sampled freed memory. On a thread + // that belongs to a session only that session's buckets answer; a sessionless thread + // (the in-library backend, a unit fixture) keeps the process-wide walk. static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) { if (MG_Pipe::MGPipeHandleIsNull(handle)) return false; + TwinKey key; + const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0; Bool released = false; for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) { BackendSlotTable* const next = holder->m_nextHolder; - released = holder->ReleaseTwinAt(handle) || released; + if (!sessionScoped || holder->m_ownerSession == key.SessionKey) { + released = holder->ReleaseTwinAt(handle) || released; + } holder = next; } return released; @@ -1012,6 +1033,7 @@ namespace MobileGL::MG_Backend::DirectGLES { static inline BackendSlotTable* s_firstHolder = nullptr; BackendSlotTable* m_prevHolder = nullptr; BackendSlotTable* m_nextHolder = nullptr; + Uint64 m_ownerSession = 0; // Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live. Vector m_slots; diff --git a/MobileGL/MG_Test/Wire/TwinOwnershipTest.cpp b/MobileGL/MG_Test/Wire/TwinOwnershipTest.cpp index 34855e60..907417ef 100644 --- a/MobileGL/MG_Test/Wire/TwinOwnershipTest.cpp +++ b/MobileGL/MG_Test/Wire/TwinOwnershipTest.cpp @@ -288,4 +288,47 @@ namespace { EXPECT_EQ(table.BucketCount(), 0u); } + // AN OBJECT'S DEATH IS ITS OWN SESSION'S. Every client numbers its objects with its own + // allocator, so the same {slot, gen} is live in other sessions at the same time and names + // their objects. The object_death release used to walk every holder of the kind: a client + // deleting a texture released the compositor's twin with the same handle - its driver + // texture was deleted while it was still drawn from (the lock screen exiting turned the + // desktop's windows to noise). + TEST_F(TwinOwnershipTest, AnObjectDeathReleasesOnlyItsOwnSessionsTwins) { + RegistryShapedTable table; + const P::MGPipeHandle handle{23u, 1u}; + const auto mint = [&](Uint64 session, Uint64 group, Uint64 stamp) { + BindKey(session, group); + MobileGL::SharedPtr* slot = table.GetOrCreateByHandle(handle); + ASSERT_NE(slot, nullptr); + *slot = MobileGL::MakeShared(); + (*slot)->SyncedSerial = stamp; + }; + mint(kSessionA, kGroupOne, 1u); + mint(kSessionA, kGroupTwo, 2u); + mint(kSessionB, kGroupOne, 3u); + + // Session A's client deleted its object; the death is applied on A's thread. + BindKey(kSessionA, kGroupOne); + EXPECT_TRUE(RegistryShapedTable::ReleaseByHandle(handle)); + EXPECT_EQ(table.FindByHandle(handle), nullptr) << "the dying object's twin survived its death"; + // A handle is unique across one client's share groups, so the session's other group lets go too. + BindKey(kSessionA, kGroupTwo); + EXPECT_EQ(table.FindByHandle(handle), nullptr); + + BindKey(kSessionB, kGroupOne); + const MobileGL::SharedPtr* const survivor = table.FindByHandle(handle); + ASSERT_NE(survivor, nullptr) << "another session's live object was released by this session's death"; + EXPECT_EQ((*survivor)->SyncedSerial, 3u); + + // A thread that belongs to no session (the in-library backend) keeps the process-wide walk. + BindKey(0, 0); + EXPECT_TRUE(RegistryShapedTable::ReleaseByHandle(handle)); + BindKey(kSessionB, kGroupOne); + EXPECT_EQ(table.FindByHandle(handle), nullptr); + + table.DropBucketsOfSession(kSessionA); + table.DropBucketsOfSession(kSessionB); + } + } // namespace