From fb0502886b8cb400cab5dbe449aa7b740c43459e Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 9 Oct 2026 20:59:10 -0400 Subject: [PATCH] TLS: Espryt's bound context serial and per-context lookup cache share one thread_local block. --- MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 21 ++++++---- MobileGL/MG_Backend/DirectGLES/DirectGLES.h | 39 +++++++++++++------ 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 65c4c457..46b44439 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -12039,7 +12039,6 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 activeToken = 0; EGLContext boundContext = EGL_NO_CONTEXT; EGLSurface boundDraw = EGL_NO_SURFACE; - Uint64 boundSerial = 0; // QueryCurrentSurfaceSize's memo (P15): the last answer, good while the session, the // registry epoch and the surface-size epoch all hold. Bool sizeMemoValid = false; @@ -12105,7 +12104,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // surface (one client context drawing to several windows) is a real switch. // (t_native.boundDraw) // ...and that context's lifetime serial, the key of its container objects. - // (t_native.boundSerial) + // (t_nativeContextThread.BoundSerial, DirectGLES.h) std::atomic g_nativeContextSerials{0}; std::mutex& PerNativeContextRegistryMutex() { @@ -12118,7 +12117,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } } // namespace - Uint64 CurrentNativeContextSerial() { return t_native.boundSerial; } namespace { std::atomic g_perNativeContextEpoch{1}; @@ -12126,12 +12124,19 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 PerNativeContextEpoch() { return g_perNativeContextEpoch.load(std::memory_order_acquire); } void BumpPerNativeContextEpoch() { g_perNativeContextEpoch.fetch_add(1, std::memory_order_acq_rel); } - PerNativeContextBase::PerNativeContextBase() { + namespace { + std::atomic g_perNativeContextCacheSlots{0}; + } + + PerNativeContextBase::PerNativeContextBase() + : m_cacheSlot(g_perNativeContextCacheSlots.fetch_add(1, std::memory_order_relaxed) % + NativeContextThreadState::kCacheEntries) { const std::lock_guard lock(PerNativeContextRegistryMutex()); PerNativeContextRegistry().push_back(this); } PerNativeContextBase::~PerNativeContextBase() { + BumpPerNativeContextEpoch(); const std::lock_guard lock(PerNativeContextRegistryMutex()); auto& registry = PerNativeContextRegistry(); registry.erase(std::remove(registry.begin(), registry.end(), this), registry.end()); @@ -12872,7 +12877,7 @@ namespace MobileGL::MG_Backend::DirectGLES { if (t_native.boundContext == tuple->Context) { t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; - t_native.boundSerial = 0; + t_nativeContextThread.BoundSerial = 0; } ForgetNativeContextObjects(tuple->Serial); tuple->Serial = 0; @@ -13476,7 +13481,7 @@ namespace MobileGL::MG_Backend::DirectGLES { InvalidateEglVerifiedStamp(); t_native.boundContext = tuple.Context; t_native.boundDraw = session.Draw; - t_native.boundSerial = tuple.Serial; + t_nativeContextThread.BoundSerial = 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. @@ -13522,7 +13527,7 @@ namespace MobileGL::MG_Backend::DirectGLES { InvalidateEglVerifiedStamp(); t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; - t_native.boundSerial = 0; + t_nativeContextThread.BoundSerial = 0; g_backendContextOwnerThread.store(std::thread::id{}, std::memory_order_release); return true; } @@ -14248,7 +14253,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } t_native.boundContext = EGL_NO_CONTEXT; t_native.boundDraw = EGL_NO_SURFACE; - t_native.boundSerial = 0; + t_nativeContextThread.BoundSerial = 0; for (auto* tuple : NativeContexts()) { if (tuple->SessionKey != sessionKey || tuple->Context == EGL_NO_CONTEXT) continue; ForgetNativeContextObjects(tuple->Serial); diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h index b66bfe96..343eb7bd 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.h @@ -327,7 +327,24 @@ namespace MobileGL::MG_Backend::DirectGLES { // 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(); + // + // P15: it lives in one constant-initialized thread_local with the PerNativeContext lookup + // cache below, so the draw path's "this context's entry" question is one TLS address + // computation, not two calls each paying their own. + struct NativeContextThreadState { + struct CacheEntry { + const void* Owner = nullptr; + Uint64 Serial = 0; + Uint64 Epoch = 0; + void* Entry = nullptr; + }; + static constexpr Uint32 kCacheEntries = 8; + Uint64 BoundSerial = 0; + // One entry per PerNativeContext instance slot (instances sharing a slot evict each other). + CacheEntry Cache[kCacheEntries]{}; + }; + inline constinit thread_local NativeContextThreadState t_nativeContextThread; + inline Uint64 CurrentNativeContextSerial() { return t_nativeContextThread.BoundSerial; } class PerNativeContextBase { public: @@ -340,6 +357,8 @@ namespace MobileGL::MG_Backend::DirectGLES { protected: ~PerNativeContextBase(); + // This instance's entry in NativeContextThreadState::Cache. + const Uint32 m_cacheSlot; }; // Called where a native context is destroyed. @@ -354,12 +373,16 @@ namespace MobileGL::MG_Backend::DirectGLES { class PerNativeContext final : public PerNativeContextBase { public: T& Current() { - const Uint64 serial = CurrentNativeContextSerial(); + NativeContextThreadState& thread = t_nativeContextThread; + const Uint64 serial = thread.BoundSerial; // 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; + // thread-local cache per instance, invalidated by any drop anywhere (rare) and by an + // instance's destruction (so a later instance at the same address cannot hit). + NativeContextThreadState::CacheEntry& cache = thread.Cache[m_cacheSlot]; const Uint64 epoch = PerNativeContextEpoch(); - if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry; + if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) { + return *static_cast(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}; @@ -382,12 +405,6 @@ namespace MobileGL::MG_Backend::DirectGLES { } 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; };