From 9951961d9c11ec74123177ff056534c09227bbe6 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 8 Sep 2026 02:26:55 -0400 Subject: [PATCH] [Refactor] (Espryt): give the buffer resource its own {slot, gen} table instead of hanging it off the frontend object --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 42 +++++++++++++ MobileGL/MG_Backend/DirectGLES/Managers.h | 68 +++++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/SlotTables.h | 26 ++++++++ 3 files changed, 136 insertions(+) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 741b0d3c..82a8ee16 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -1501,6 +1501,48 @@ namespace MobileGL::MG_Backend::DirectGLES { g_bufferMutationEpoch.fetch_add(1, std::memory_order_release); } +#if MOBILEGL_PIPE_PUSH + // The seventh slot table. A process-lifetime global, like the other six, so it links + // itself into its own holder list from its own constructor with no initialisation + // order question to answer (SlotTables.h). + BackendBufferResourceTable g_backendBufferResources; + + Bool ResolveResourceSubsystemArm() { + const Bool enabled = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemResources) != 0; + MGLOG_D("MGPipe: Espryt resource family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + Bool ResolveVertexInputSubsystemArm() { + const Bool enabled = (MG_Config::Features.PipePush & MG_Pipe::kMGPipeSubsystemVertexInput) != 0; + MGLOG_D("MGPipe: Espryt vertex-input family runs the %s arm", enabled ? "handle" : "legacy"); + return enabled; + } + + GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { + if (MG_Pipe::MGPipeHandleIsNull(res)) return nullptr; + auto& twin = g_backendBufferResources.GetOrCreate(res); + if (!twin) { + twin = MakeShared(); + // Same seed the lazy legacy path gives a resource it has just minted: nothing + // has defined storage yet, so the first sync owes a full (re)specification. + twin->pendingRespecify = true; + } + return twin.get(); + } + + GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res) { + auto* twin = g_backendBufferResources.FindByHandle(res); + return twin ? twin->get() : nullptr; + } + + MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject) { + if (bufferObject == nullptr) return MG_Pipe::kMGPipeNullHandle; + return MG_Pipe::MGPipeSlots().FindByLifetimeId(MG_Pipe::MGPipeKind::Buffer, + bufferObject->GetLifetimeId()); + } +#endif + // See the declaration: re-mints of a live resource's driver id. Written only on // the context thread (all re-mint sites run there), read only by the VAO sync. Uint64 g_bufferBackendIdGeneration = 0; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index a061774b..b5b72d19 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -644,8 +644,76 @@ namespace MobileGL::MG_Backend::DirectGLES { // no map, and a respecification then has to retire the id rather than hand it // to glBufferData, which the driver would silently refuse. Bool immutableStorage = false; +#if MOBILEGL_PIPE_PUSH + // P3a: the client's shadow base as the last content-carrying resource call left + // it. The handle-shaped ops carry `shadow + offset` beside their record, so the + // base is recovered by subtracting the record's own offset once, here, instead of + // asking a frontend object for MappedData() at every later drain. Everything the + // legacy arm reads through bufferObject.MappedData() - the three-tier range + // flush, the pool reseed, the full re-upload - reads this on the handle arm. + // + // RAW, and it may not be dereferenced except while the client's shadow is known + // to be live: the client owns the allocation and hands the base over per call. + // Null until the first respecify/subdata/flush that carries content, which is + // exactly when nothing has bytes to move yet. + const Uint8* hostBytes = nullptr; +#endif }; +#if MOBILEGL_PIPE_PUSH + // P3a (D-A4): the SEVENTH Espryt slot table, and the first one keyed by a handle the + // CALL carried rather than one this backend minted off a frontend object's lifetime + // id. That is what discharges, for this kind, the debt SlotTables.h records against + // itself: GLESBufferResource stops hanging off PipeResource::m_backend and lives here + // instead, so the resource table is the server's own and a frontend heap reference is + // no longer part of resolving it. + // + // The StateObject parameter is BufferObject only because the template names one; not + // one member that touches it is instantiated on this table (no Find(StateObject*), no + // HandleOf, no ForEachLive), and the handle overloads never look at it. Death is + // announced by the family's own ResourceDestroy call, not by the shared death notice + // (D-L), and the slot is freed by the CLIENT after that call returns. + using BackendBufferResourceTable = + BackendSlotTable; + extern BackendBufferResourceTable g_backendBufferResources; + + // Resolved once per process and latched, exactly like EsprytSlotTablesEnabled() and + // for the same reason: the two arms hold GLESBufferResource in DIFFERENT containers - + // the legacy arm in the frontend object's PipeResource::m_backend, the handle arm in + // the table above - so an answer that changed mid-run would strand every resource + // already built and leak the driver ids they own. + Bool ResolveResourceSubsystemArm(); + // Same shape for the vertex-input family (bit 8). Kept separate because the two bits + // are separately clearable and the A/B has to be able to run either one alone. + Bool ResolveVertexInputSubsystemArm(); + + // INLINE for the reason SlotTables.h spells out at EsprytSlotTablesEnabled: both are + // consulted on the per-draw path (the VAO sync's gate, EnsureBufferResource, every + // buffer op), and out-of-line they would be a call through the PLT per consult. + inline Bool ResourceSubsystemEnabled() { + static const Bool enabled = ResolveResourceSubsystemArm(); + return enabled; + } + inline Bool VertexInputSubsystemEnabled() { + static const Bool enabled = ResolveVertexInputSubsystemArm(); + return enabled; + } + + // Resolve-or-create / resolve-only, by the handle the call carried. Neither touches + // MGPipeSlots(): the handle ARRIVED already minted by the side that owns minting. + GLESBufferResource* GetOrCreateBufferResourceForHandle(MG_Pipe::MGPipeHandle res); + GLESBufferResource* FindBufferResourceForHandle(MG_Pipe::MGPipeHandle res); + + // MONOLITH GLUE, and named as such: the handle of a resource this backend is looking + // at through a frontend object, resolved through the client allocator's lifetime-id + // index. Every caller is a site P3a deliberately does NOT migrate - the SSBO / UBO / + // indirect / pack-PBO binding walks are dirty bits 15-17 and P4b's, and the index + // host mirror is P8's - so they still arrive holding the object. Under a real split + // neither the object nor its lifetime id exists on this side and every one of them + // has to receive the handle in a payload instead. + MG_Pipe::MGPipeHandle HandleOfBuffer(const MG_State::GLState::BufferObject* bufferObject); +#endif + // Registered as the frontend's BufferBackendOps at backend init and on // every MakeCurrent (the ES context can be destroyed and recreated, e.g. // by the trace replayer's probe context). diff --git a/MobileGL/MG_Backend/DirectGLES/SlotTables.h b/MobileGL/MG_Backend/DirectGLES/SlotTables.h index 759848ae..9d4c0137 100644 --- a/MobileGL/MG_Backend/DirectGLES/SlotTables.h +++ b/MobileGL/MG_Backend/DirectGLES/SlotTables.h @@ -285,6 +285,32 @@ namespace MobileGL::MG_Backend::DirectGLES { return entry.backend; } + // P3a: the death half of the overload above, for a kind whose announcement is its own + // destroy CALL rather than the shared death notice (D-L). Hands the twin OUT rather + // than destroying it in place, because the caller may still have to decide what + // happens to the driver id it owns - Espryt pools it, deletes it, or parks it on the + // deferred-release list when no context is current on this thread - and every one of + // those outcomes has to be reached with the entry already retired, so a re-entrant + // GetOrCreate from a twin destructor cannot resurrect it. + // + // The slot itself is NOT freed here: it belongs to the kind, and for a handle-keyed + // kind the CLIENT frees it after the destroy call returns (SlotAllocator.h:60 - the + // Gen bump rides the next handout, so a double free cannot skip a generation). An + // entry whose Gen no longer matches is a twin of the slot's previous owner and is + // left alone: the successor's own GetOrCreate resets it. + BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) { + if (MG_Pipe::MGPipeHandleIsNull(handle)) return BackendPtr{}; + if (m_memoHandle.Slot == handle.Slot) ForgetHandle(); + if (handle.Slot >= m_slots.size()) return BackendPtr{}; + Entry& entry = m_slots[handle.Slot]; + if (!entry.Live || entry.Gen != handle.Gen) return BackendPtr{}; + BackendPtr dead = std::move(entry.backend); + entry.backend.reset(); + entry.stateRef.reset(); + entry.Live = false; + return dead; + } + // 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 on it; only a // GetOrCreate that grows the vector can move it, and callers that hold one across a