[Refactor] (Espryt): give the buffer resource its own {slot, gen} table instead of hanging it off the frontend object

This commit is contained in:
2026-09-08 04:52:15 -04:00
parent 31e370bed5
commit 9951961d9c
3 changed files with 136 additions and 0 deletions
@@ -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<GLESBufferResource>();
// 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;
+68
View File
@@ -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<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
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).
@@ -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