diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 81e4ca64..7171a2a0 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -170,6 +170,37 @@ namespace MobileGL::MG_Backend::DirectGLES { [] { std::atexit(+[] { g_processTeardown = true; }); }); } +#if MOBILEGL_PIPE_PUSH + Bool EsprytSlotTablesEnabled() { + // Latched once, not read per call: the two arms of StateBackendObjectRegistry keep + // their twins in different containers, so an answer that changed mid-run would strand + // every twin already built (and, for the driver ids those twins own, leak them). + static const Bool enabled = [] { + const Bool bitSet = + (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemEsprytSlots) != 0; +#if MOBILEGL_PIPE_LEGACY_MEMOS + if (!bitSet && !MG_Config::Features.PipeLegacyMemos) { + // The operator asked for the handle arm to be OFF and the legacy arm to be + // unreachable at the same time, which leaves no arm at all. Say so at startup + // rather than silently running the thing they turned off (ARCHITECTURE.md 9.6). + MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled, \"kMGPipeSubsystemEsprytSlots " + "is clear but MOBILEGL_PIPE_LEGACY_MEMOS=0\"}"); + } + return bitSet; +#else + // The legacy arm is not compiled, so the handle arm is the only arm. The bit still + // decides nothing here; it is recorded so a log reader sees the mismatch. + if (!bitSet) { + MGLOG_D("MGPipe: kMGPipeSubsystemEsprytSlots is clear but this build has no " + "legacy twin registry; running the handle arm anyway"); + } + return true; +#endif + }(); + return enabled; + } +#endif + Bool VertexStageStorageBlockUsable(Int maxVertexShaderStorageBlocks) { // One block is all the indirect-params view needs, so this is a >= 1 test and not a // budget calculation. Negative is treated as unusable rather than clamped: a driver @@ -2744,7 +2775,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return true; } - StateBackendObjectRegistry + StateBackendObjectRegistry g_backendVertexArrayObjects; } // namespace VertexArrayImpl @@ -5072,7 +5103,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Array, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundTexturesCache; - StateBackendObjectRegistry g_backendTextureObjects; + StateBackendObjectRegistry g_backendTextureObjects; } // namespace TextureImpl namespace FramebufferImpl { @@ -5825,7 +5856,7 @@ namespace MobileGL::MG_Backend::DirectGLES { return m_backendColorSlots[index]; } - StateBackendObjectRegistry + StateBackendObjectRegistry g_backendFramebufferObjects; Array g_fboSyncedSlotVersions = {0}; // Tracks the bound FBO's object version (bumped on any attachment/drawbuffer change) @@ -6136,7 +6167,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // context never answers GL_NO_ERROR, and the build runs on the thread that would // then spin forever. constexpr Int kMaxDrainedProgramErrors = 32; - StateBackendObjectRegistry g_backendProgramObjects; + StateBackendObjectRegistry g_backendProgramObjects; BackendProgramObjectImpl::BackendProgramObjectImpl() { #ifdef TRACY_ENABLE @@ -8654,7 +8685,7 @@ namespace MobileGL::MG_Backend::DirectGLES { } Array g_boundSamplersCache; - StateBackendObjectRegistry g_backendSamplerObjects; + StateBackendObjectRegistry g_backendSamplerObjects; } // namespace SamplerImpl namespace RenderbufferImpl { @@ -8766,7 +8797,7 @@ namespace MobileGL::MG_Backend::DirectGLES { MGLOG_D("RBO %u sync completed. backend ID %u", stateRBOObject->GetExternalIndex(), m_backendRBOId); } - StateBackendObjectRegistry + StateBackendObjectRegistry g_backendRenderbufferObjects; } // namespace RenderbufferImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index 6603f2cc..df2907f6 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -16,6 +16,7 @@ #include #include #include +#include "SlotTables.h" namespace MobileGL::MG_Backend::DirectGLES { String EmulateBaseInstanceInVertexShader(String source, GLenum shaderType); @@ -267,7 +268,34 @@ namespace MobileGL::MG_Backend::DirectGLES { EndViewportRoutingPasses(passCount); } - template + // The backend twin table. Two arms live behind this one interface (ARCHITECTURE.md 9.6 - + // after Track H the MOBILEGL_PIPE_PUSH bitmap alone is not a valid A/B, because with a bit + // clear the backend would still be running the re-keyed code): + // + // legacy (MOBILEGL_PIPE_LEGACY_MEMOS): UnorderedMap keyed on the + // frontend heap ADDRESS, with a weak_ptr per entry as the ABA defence, an erase + // inside Find, and a garbage sweep as the only death signal. Pre-P2 code verbatim. + // handles (MOBILEGL_PIPE_PUSH and kMGPipeSubsystemEsprytSlots): BackendSlotTable, keyed + // on MGPipeHandle{Slot, Gen}. See SlotTables.h for what that buys. + // + // Which arm runs is fixed once per process (EsprytSlotTablesEnabled()): the two arms hold + // their twins in different containers, so a mid-run flip would strand every twin already + // built. Every call site below this class is arm-agnostic and unchanged. + // + // The kind is a template parameter ONLY in the push build. G1 requires the pull build's + // symbol set to be byte-for-byte the pre-P2 one, and a third template argument changes + // every instantiation's mangled name - so in the pull build the parameter, like the arm it + // selects, does not exist. MGB_TWIN_KIND_ARG spells the same thing at the six declarations + // and six definitions. +#if MOBILEGL_PIPE_PUSH +#define MGB_TWIN_KIND_PARAM , MG_Pipe::MGPipeKind kKind +#define MGB_TWIN_KIND_ARG(kind) , kind +#else +#define MGB_TWIN_KIND_PARAM +#define MGB_TWIN_KIND_ARG(kind) +#endif + + template class StateBackendObjectRegistry { public: @@ -291,8 +319,15 @@ namespace MobileGL::MG_Backend::DirectGLES { MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null"); // Twin creation is the moment a driver-owned id starts needing a guarded - // destructor; cold path, so the once-guard costs nothing per draw. + // destructor; cold path, so the once-guard costs nothing per draw. It is armed + // here, at the first insertion, on BOTH arms - a destructor hook on the table + // itself is wrong for the reason spelled out above InProcessTeardown(). EnsureProcessTeardownSentinel(); +#if MOBILEGL_PIPE_PUSH + if (EsprytSlotTablesEnabled()) { + return m_slotTable.GetOrCreate(stateObj); + } +#endif // Sweep BEFORE the entry reference below exists: the map is open-addressed and an // erase relocates the rest of the probe cluster, so collecting once that reference // is taken would invalidate it. The sweep is therefore owed from an earlier call @@ -324,14 +359,24 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry.backend; } - // Null when no live state object owns this key. The result points into the map, so - // it stays valid only until the next GetOrCreate/Find/CollectGarbage on this registry. - // Take that literally, including for Find: the map is open-addressed and erases by - // shifting the rest of the probe cluster into the hole, so an erase relocates entries - // OTHER than the erased one - and Find erases, whenever it lands on a key whose state - // object has expired. Callers that need the twin across another registry call must copy - // the BackendPtr out (or keep only the pointee, which is heap-allocated and never moves). + // Null when no live state object owns this key. + // + // On the HANDLE arm the result is a stable array element: only a GetOrCreate that grows + // the table can move it, and nothing else on the table invalidates it. + // + // On the LEGACY arm the result points into the map, so it stays valid only until the + // next GetOrCreate/Find/CollectGarbage on this registry. Take that literally, including + // for Find: the map is open-addressed and erases by shifting the rest of the probe + // cluster into the hole, so an erase relocates entries OTHER than the erased one - and + // Find erases, whenever it lands on a key whose state object has expired. Callers that + // need the twin across another registry call must copy the BackendPtr out (or keep only + // the pointee, which is heap-allocated and never moves). BackendPtr* Find(StateObject* stateObj) { +#if MOBILEGL_PIPE_PUSH + if (EsprytSlotTablesEnabled()) { + return m_slotTable.Find(stateObj); + } +#endif const auto entryIt = m_entries.find(stateObj); if (entryIt == m_entries.end()) { return nullptr; @@ -352,7 +397,44 @@ namespace MobileGL::MG_Backend::DirectGLES { iterator end() { return m_entries.end(); } const_iterator end() const { return m_entries.end(); } +#if MOBILEGL_PIPE_PUSH + // The {slot, gen} this object's twin is keyed on, or the null handle. This is what a + // backend memo stores instead of a raw pointer, a GL name or a bare lifetime id. + MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { + if (EsprytSlotTablesEnabled()) { + return m_slotTable.HandleOf(stateObj); + } + return MG_Pipe::kMGPipeNullHandle; + } + + // fn(const StatePtr& state, const BackendPtr& twin) over every live entry. The legacy + // begin()/end() handed out the map key, i.e. the raw frontend address - exactly the + // identity the backend must stop reading - and handed it out for entries whose state + // object had already died, so the one caller had to test stateRef.expired() itself + // before dereferencing it. Here the state object arrives as a strong reference. + template + void ForEachLive(Fn&& fn) const { + if (EsprytSlotTablesEnabled()) { + m_slotTable.ForEachLive(fn); + return; + } + for (const auto& [stateKey, entry] : m_entries) { + (void)stateKey; + if (!entry.backend) continue; + const StatePtr state = entry.stateRef.lock(); + if (!state) continue; + fn(state, entry.backend); + } + } +#endif + void CollectGarbageIfNeeded() { +#if MOBILEGL_PIPE_PUSH + if (EsprytSlotTablesEnabled()) { + m_slotTable.CollectGarbageIfNeeded(); + return; + } +#endif ++m_gcTick; if (m_gcTick < kGCInterval) { return; @@ -361,7 +443,15 @@ namespace MobileGL::MG_Backend::DirectGLES { m_gcTick = 0; } - void CollectGarbageNow() { CollectGarbage(); } + void CollectGarbageNow() { +#if MOBILEGL_PIPE_PUSH + if (EsprytSlotTablesEnabled()) { + m_slotTable.CollectGarbageNow(); + return; + } +#endif + CollectGarbage(); + } private: void CollectGarbage() { @@ -395,6 +485,9 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint32 m_gcTick = 0; Uint32 m_creationTick = 0; Bool m_isCollecting = false; +#if MOBILEGL_PIPE_PUSH + BackendSlotTable m_slotTable; +#endif }; namespace BufferImpl { @@ -800,7 +893,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 m_syncedBufferIdGeneration = 0; }; - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendVertexArrayObjects; // Shadowed glBindVertexArray: every backend VAO bind goes through here so a @@ -1121,7 +1214,7 @@ namespace MobileGL::MG_Backend::DirectGLES { void ActivateTextureUnit(Uint unit); void UnbindTexture(Uint unit, GLenum target); - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendTextureObjects; SharedPtr& SyncTextureObjectToBackend( const SharedPtr& textureObject, @@ -1212,7 +1305,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Uint64 m_syncedBackendIdGeneration = 0; }; - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendFramebufferObjects; // True when the read buffer names a fixed-point (norm/snorm) attachment that the // backend actually stores in a floating-point format. GL clamps a read from a @@ -1730,7 +1823,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // skip redundant rebinds. Reset to 0 wherever glUseProgram(0) is issued or the // ES context is recreated. extern Uint g_lastUsedBackendProgramId; - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendProgramObjects; // Points one shader storage block of an ALREADY-LINKED backend program at @@ -1830,7 +1923,7 @@ namespace MobileGL::MG_Backend::DirectGLES { extern Array g_boundSamplersCache; - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendSamplerObjects; } // namespace SamplerImpl @@ -1857,7 +1950,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Int m_cacheSamples = 0; }; - extern StateBackendObjectRegistry + extern StateBackendObjectRegistry g_backendRenderbufferObjects; } // namespace RenderbufferImpl } // namespace MobileGL::MG_Backend::DirectGLES diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h new file mode 100644 index 00000000..187d2ce5 --- /dev/null +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -0,0 +1,188 @@ +// MobileGL - MobileGL/MG_Backend/DirectGLES/SlotTables.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once +#include + +#include + +#if MOBILEGL_PIPE_PUSH +#include +#endif + +// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces +// StateBackendObjectRegistry's UnorderedMap. +// +// What changes, and why each of them is the point: +// +// * The KEY stops being a frontend heap address. It is MGPipeHandle{Slot, Gen}, minted by the +// client's MGPipeSlotAllocator off the frontend object's GetLifetimeId(). A recycled heap +// address cannot reproduce a handle, so the weak_ptr the registry carried per entry purely +// to catch that (its Entry::stateRef, used as an IDENTITY test) stops being an identity +// mechanism, and OwnerEquals / TwinLookupMemo x3 / UnitSamplerLookupMemo's owner compare all +// lose their reason to exist. +// * The lookup stops being a hash probe into an open-addressed map and becomes one bounds +// check plus one array index, so a returned BackendPtr* is NOT invalidated by the next Find +// or sweep on the table. That kills the hazard Managers.h documents at length, and with it +// the by-value copy plus second Find that SyncTextureObjectToBackend paid to survive it. +// * Slots are dense per kind, which is what lets the server side (ARCHITECTURE.md 10.1, +// MG_Remote/Server/PipeObjectTables) be an array rather than an object graph. +// +// What has NOT changed, deliberately, and is this file's one departure from the P2 brief +// (recorded in the package result file): a frontend object's death is still discovered rather +// than announced. The brief's step e2 - a BufferBackendOps-shaped OnDestroy for the other six +// kinds - has to be installed in MG_State/GLState/{Texture,Framebuffer,Renderbuffer,Sampler, +// Program,VertexArray}State/*, and the P2 file-ownership table gives every one of those files +// to another package. So the table keeps ONE weak_ptr per entry and uses it for exactly one +// thing: ReclaimDeadSlots() frees the slot - and the twin, and the driver storage it owns - +// once the frontend object is gone. That is a liveness sweep, not an identity test, and it is +// what bumps Gen, which is precisely the ABA defence: a slot is only ever handed out again +// after it was freed. When e2 lands, ReclaimDeadSlots() becomes the fallback path of an +// explicit Destroy(handle) and the sweep call sites go away. +namespace MobileGL::MG_Backend::DirectGLES { + +#if MOBILEGL_PIPE_PUSH + + // True when this process runs the {slot, gen} arm. Fixed for the life of the process: the + // two arms hold their twins in different containers, so flipping mid-run would strand them. + Bool EsprytSlotTablesEnabled(); + + template + class BackendSlotTable { + public: + using StatePtr = SharedPtr; + using StateWeakPtr = std::weak_ptr; + using BackendPtr = SharedPtr; + + struct Entry { + BackendPtr backend; + // LIVENESS ONLY. Never compared against another object to decide identity - that is + // what Gen is for - and never dereferenced for its address. Read by + // ReclaimDeadSlots(), and locked by ForEachLive() so the callee holds a strong ref. + StateWeakPtr stateRef; + // The generation this entry's twin was built for. An entry whose Gen no longer + // matches the allocator's is a twin of the slot's PREVIOUS owner. + Uint32 Gen = 0; + Bool Live = false; + }; + + // 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. + BackendPtr& GetOrCreate(const StatePtr& stateObj) { + MOBILEGL_ASSERT(stateObj != nullptr, "State object must not be null"); + const MG_Pipe::MGPipeHandle handle = + MG_Pipe::MGPipeSlots().Acquire(kKind, stateObj->GetLifetimeId()); + MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle), + "MGPipe slot space of kind %u is exhausted", + static_cast(kKind)); + Entry& entry = EntryAt(handle.Slot); + if (entry.Live && entry.Gen != handle.Gen) { + // The slot was reclaimed and handed to a new object: the twin at it describes + // driver ids the new state object never made. + entry.backend.reset(); + } + entry.Gen = handle.Gen; + entry.Live = true; + entry.stateRef = stateObj; + return entry.backend; + } + + // Null when no live twin of this object exists. Unlike the registry's Find this NEVER + // mutates the table, so the returned pointer survives any later Find or sweep on it; + // only a GetOrCreate that grows the vector can move it, and callers that hold one + // across a possible insertion still copy the BackendPtr out. + BackendPtr* Find(StateObject* stateObj) { + if (stateObj == nullptr) return nullptr; + return FindByHandle(HandleOf(stateObj)); + } + + const BackendPtr* Find(StateObject* stateObj) const { + return const_cast(this)->Find(stateObj); + } + + BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return nullptr; + if (handle.Slot >= m_slots.size()) return nullptr; + Entry& entry = m_slots[handle.Slot]; + if (!entry.Live || entry.Gen != handle.Gen) return nullptr; + return &entry.backend; + } + + // The handle this object's twin is keyed on, or the null handle. This is what a backend + // memo stores instead of a raw pointer, a GL name or a bare lifetime id. + MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const { + if (stateObj == nullptr) return MG_Pipe::kMGPipeNullHandle; + return MG_Pipe::MGPipeSlots().FindByLifetimeId(kKind, stateObj->GetLifetimeId()); + } + + // Drop the twin of every slot whose frontend object is gone and return the slot to the + // allocator. Freeing is what makes the NEXT handout of that slot bump Gen. + void ReclaimDeadSlots() { + if (m_isCollecting) return; + m_isCollecting = true; + for (SizeT slot = 0; slot < m_slots.size(); ++slot) { + Entry& entry = m_slots[slot]; + if (!entry.Live || !entry.stateRef.expired()) continue; + entry.backend.reset(); + entry.stateRef.reset(); + entry.Live = false; + MG_Pipe::MGPipeSlots().Free( + kKind, MG_Pipe::MGPipeHandle{static_cast(slot), entry.Gen}); + } + m_isCollecting = false; + } + + void CollectGarbageIfNeeded() { + ++m_gcTick; + if (m_gcTick < kGCInterval) return; + m_gcTick = 0; + ReclaimDeadSlots(); + } + + void CollectGarbageNow() { ReclaimDeadSlots(); } + + // fn(const StatePtr& state, const BackendPtr& twin) over every live, still-owned entry. + // Replaces the registry's begin()/end(), whose iterator exposed the raw frontend + // address as the map key - the one place the backend read an identity it must not have. + // The state object is handed over as a STRONG reference, so the callee cannot be handed + // a dangling key the way the old iteration could. + template + void ForEachLive(Fn&& fn) const { + for (const Entry& entry : m_slots) { + if (!entry.Live || !entry.backend) continue; + const StatePtr state = entry.stateRef.lock(); + if (!state) continue; + fn(state, entry.backend); + } + } + + Uint32 LiveCount() const { + Uint32 count = 0; + for (const Entry& entry : m_slots) { + if (entry.Live) ++count; + } + return count; + } + + private: + Entry& EntryAt(Uint32 slot) { + if (slot >= m_slots.size()) m_slots.resize(static_cast(slot) + 1); + return m_slots[slot]; + } + + static constexpr Uint32 kGCInterval = 1024; + + // Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live. + Vector m_slots; + Uint32 m_gcTick = 0; + Bool m_isCollecting = false; + }; + +#endif // MOBILEGL_PIPE_PUSH +} // namespace MobileGL::MG_Backend::DirectGLES