[Feat] (Espryt): give the backend a dense {slot, gen} twin table beside the address-keyed registry

- SlotTables.h: BackendSlotTable<StateObject, BackendObject, kKind>, indexed by
  MGPipeHandle::Slot and validated by Gen, with the handle minted by the client
  MGPipeSlotAllocator off the frontend object GetLifetimeId(). A lookup is one bounds
  check plus one array index, and unlike the registry Find it never mutates the table,
  so a returned BackendPtr* is not invalidated by the next call on it.
- StateBackendObjectRegistry keeps its name, its signature and all ~40 call sites, and
  becomes the two-arm facade ARCHITECTURE.md 9.6 asks for: the pre-handle map under
  MOBILEGL_PIPE_LEGACY_MEMOS, the slot table under MOBILEGL_PIPE_PUSH, chosen once per
  process by EsprytSlotTablesEnabled() off kMGPipeSubsystemEsprytSlots. A clear bit with
  MOBILEGL_PIPE_LEGACY_MEMOS=0 leaves no arm at all and is Fatal{PipeLegacyMemosDisabled}.
- The kind is a template parameter only in the push build (MGB_TWIN_KIND_ARG): a third
  template argument would rename every instantiation and G1 wants the pull build byte
  identical. Pull-build symbol report is 0 added / 0 removed / 0 renamed and adds no
  resize beyond the three RenderState symbols the contract commit already moved.
- ForEachLive replaces begin()/end() under push and hands the callee a strong reference
  to the state object instead of the map key, which was the raw frontend address.
- Nothing switches over yet: the tables are built and reachable, and the twins still go
  through whichever arm the bit selects.
This commit is contained in:
Swung0x48
2026-09-07 23:18:09 -04:00
committed by swung0x48
parent 842af23331
commit f4dbea2300
3 changed files with 334 additions and 22 deletions
+37 -6
View File
@@ -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<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::VertexElementsCso)>
g_backendVertexArrayObjects;
} // namespace VertexArrayImpl
@@ -5072,7 +5103,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject> g_backendTextureObjects;
StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Texture)> g_backendTextureObjects;
} // namespace TextureImpl
namespace FramebufferImpl {
@@ -5825,7 +5856,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return m_backendColorSlots[index];
}
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Framebuffer)>
g_backendFramebufferObjects;
Array<Uint16, SizeT(FramebufferTarget::FramebufferTargetCount)> 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<MG_State::GLState::ProgramObject, BackendProgramObjectImpl> g_backendProgramObjects;
StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::ShaderCso)> g_backendProgramObjects;
BackendProgramObjectImpl::BackendProgramObjectImpl() {
#ifdef TRACY_ENABLE
@@ -8654,7 +8685,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
Array<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS> g_boundSamplersCache;
StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject> g_backendSamplerObjects;
StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::SamplerCso)> 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<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject>
StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Renderbuffer)>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+109 -16
View File
@@ -16,6 +16,7 @@
#include <MG_State/GLState/TextureState/TextureObject.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/Converters/MGToGL/TextureEnumConverter.h>
#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 <typename StateObject, typename BackendObject>
// 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<StateObject*, Entry> 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 <typename StateObject, typename BackendObject MGB_TWIN_KIND_PARAM>
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 <typename Fn>
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<StateObject, BackendObject, kKind> m_slotTable;
#endif
};
namespace BufferImpl {
@@ -800,7 +893,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint64 m_syncedBufferIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject>
extern StateBackendObjectRegistry<MG_State::GLState::VertexArrayObject, BackendVertexArrayObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::VertexElementsCso)>
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<MG_State::GLState::ITextureObject, BackendTextureObject>
extern StateBackendObjectRegistry<MG_State::GLState::ITextureObject, BackendTextureObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Texture)>
g_backendTextureObjects;
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
@@ -1212,7 +1305,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint64 m_syncedBackendIdGeneration = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject>
extern StateBackendObjectRegistry<MG_State::GLState::FramebufferObject, BackendFramebufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Framebuffer)>
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<MG_State::GLState::ProgramObject, BackendProgramObjectImpl>
extern StateBackendObjectRegistry<MG_State::GLState::ProgramObject, BackendProgramObjectImpl MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::ShaderCso)>
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<BackendSamplerObject*, MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundSamplersCache;
extern StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject>
extern StateBackendObjectRegistry<MG_State::GLState::SamplerObject, BackendSamplerObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::SamplerCso)>
g_backendSamplerObjects;
} // namespace SamplerImpl
@@ -1857,7 +1950,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
Int m_cacheSamples = 0;
};
extern StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject>
extern StateBackendObjectRegistry<MG_State::GLState::RenderbufferObject, BackendRenderbufferObject MGB_TWIN_KIND_ARG(MG_Pipe::MGPipeKind::Renderbuffer)>
g_backendRenderbufferObjects;
} // namespace RenderbufferImpl
} // namespace MobileGL::MG_Backend::DirectGLES
+188
View File
@@ -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 <Includes.h>
#include <MG_Pipe/MGPipeHandles.h>
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
// Espryt 0b, the first Track H slice: the DENSE, {slot, gen}-keyed twin table that replaces
// StateBackendObjectRegistry's UnorderedMap<StateObject*, Entry>.
//
// 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 <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable {
public:
using StatePtr = SharedPtr<StateObject>;
using StateWeakPtr = std::weak_ptr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
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<Uint32>(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<BackendSlotTable*>(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<Uint32>(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 <typename Fn>
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<SizeT>(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<Entry> m_slots;
Uint32 m_gcTick = 0;
Bool m_isCollecting = false;
};
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectGLES