mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 20:28:32 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user