- BackendSlotTable links every instance into a per-type holder list; OnFrontendObjectDestroyed resolves the handle once, drops the twin in each holder by handle and frees the slot once, last - a notice delivered to one registry left the fixture's saved copy holding a dead twin, and its driver storage, for the life of the process (review v4 minor 2)
- ReclaimDeadSlots and CollectGarbageNow leave the slot table: nothing called them, so the header's "backstop" was a claim without a caller; the registry's pre-P2 CollectGarbageNow is a no-op on the handle arm (minor 1)
- HandleOf no longer memoises a null answer, which a second holder's acquire could never refresh (minor 8)
- EnsureProcessTeardownSentinel is armed by the slot table's first insertion, as D13 says; the registry arms it only on the legacy arm (minor 9)
- new SanityTest cases: OneDeathNoticeDropsTheTwinInEveryHolderOfTheKind, ASavedCopyOfARealRegistryDropsTheTwinOnTheSameNotice, ANegativeLookupIsNotCachedAcrossAnotherHoldersAcquire, and EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping, which runs InitPbufferSurface under the pair in a forked child and fails naming both knobs if InitDisplayAndContext ever stops there again (minor 4)
- AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane writes a per-process, per-case log path and restores MOBILEGL_LOG_FILE_PATH and MG_Config::Features through RAII guards on every exit path (minor 5)
- e2 was landed for two of six kinds, so Texture, Framebuffer, SamplerCso and
VertexElementsCso still discovered death in a sweep and ROADMAP.md:18's "delete the GC"
was undelivered. TextureObjectBase (the one base every concrete texture derives from),
FramebufferObject, SamplerObject and VertexArrayObject now raise
NotifyStateObjectDestroyed from their destructor, on RenderbufferObject's pattern: out of
line, declared only under MOBILEGL_PIPE_PUSH, so the pull build keeps its implicit
destructor and its symbol set (G1 still 0 added / 0 removed / 0 renamed and 0 resized
against p2/contract).
- With all six announcing, BackendSlotTable loses BOTH sweep drivers: no draw tick, no
creation tick, no kGCInterval / kCreationGCInterval / m_gcTick / m_creationTick.
CollectGarbageIfNeeded() is empty on this arm; CollectGarbageNow() stays as an EXPLICIT
collection and is the backstop for a notice that InProcessTeardown() drops.
- The seven CollectGarbageIfNeeded call sites in DirectGLES.cpp keep their spelling because
they are the legacy registry's driver and that arm is still compiled beside this one; the
registry's body is now guarded on MOBILEGL_PIPE_LEGACY_MEMOS, so a build without the
legacy arm has no collector at all. On the handle arm each site is a predicted branch.
- The weak_ptr per entry stays for exactly two jobs it is honest about: ForEachLive()'s
strong hand-over to ScopedDetachedTextureFramebufferAttachments, and the explicit
collection. It is never an identity test; Gen is.
- Tests: AProgramAndARenderbufferAnnounceTheirOwnDeath becomes
EveryReKeyedObjectClassAnnouncesItsOwnDeath and drives all six classes, by membership
rather than count because every texture owns a private sampler that also announces;
ObjectChurnAloneDrivesTheSweep becomes
AnnouncedDeathKeepsObjectChurnFromAccumulatingWithoutASweep and pins that 256 churned
objects hold one live twin at a time with no CollectGarbage* call anywhere.
- P2 step e2, as far as the file-ownership table lets one package take it. New frontend
header MG_State/GLState/StateObjectDeathNotice.h carries BufferBackendOps' shape for the
other six kinds: an ops table the backend fills in, and one entry point that takes
{kind, lifetimeId} rather than the object, because by the time the last SharedPtr has
dropped there is no object left to pass and the lifetime id is exactly what the client
slot allocator resolves a handle from. Declared only under MOBILEGL_PIPE_PUSH, so the
pull build's symbol set is untouched.
- BackendSlotTable::DestroyByLifetimeId drops the twin and returns the slot at the moment
the object goes, instead of at the next sweep - which for a renderbuffer or a texture
atlas is the difference between freeing the driver allocation now and freeing it 64
creations from now. It returns the slot only when THIS table holds it: two holders of one
kind already exist (the ScopedDirectGLESTextureBindings fixture; Magma's subsystem-4
table shares the VertexElementsCso kind), and a table that never twinned the object must
not free a slot the other one still names. The legacy arm keys on the frontend heap
address, cannot answer a notice at all, and keeps the sweep - which is the announced-
versus-discovered half of the A/B the compile-time arm exists for.
- Managers.cpp registers one dispatcher for all six kinds from ResolveEsprytSlotTablesArm(),
i.e. exactly when the arm that can answer a notice is the arm that runs, and drops a
notice that arrives after exit() has begun.
- FIRING it needs a destructor per class, and the P2 ownership table gives
{Texture,Framebuffer,Sampler,VertexArray}State/* to other packages, so only ProgramObject
and RenderbufferObject raise it here. The other four still rely on the sweep; their four
one-line calls retire it entirely.
- Three cases pin the three halves: the slot comes back with no sweep and the notice is
idempotent and does not free another holder's slot; a program and a renderbuffer announce
their own death when the last SharedPtr drops and not before; and the handle arm actually
installs a consumer, rather than the two halves each being fine on their own.
- The handle arm took only ONE of the registry's two sweep drivers. The map arm sweeps
every 64 first-time insertions BECAUSE object churn, not draw count, is what makes the
sweep urgent: a CTS-shaped case runs ~10 per-draw ticks, so the 1024-tick draw-path
driver alone spans ~100 cases' worth of dead, gigabyte-sized twins. BackendSlotTable now
carries the same kCreationGCInterval = 64 creation tick, swept before the entry reference
exists for the same reason the map arm sweeps there. Without this the slice REGRESSED the
memory it was supposed to leave unchanged.
- Fatal{PipeLegacyMemosDisabled} now aborts. It logged and then returned false, which fell
straight into the legacy arm the operator had just made unreachable: a green run measured
on the wrong arm, and the exact lever HandleRecycleScenario's arms are selected with. It
is also resolved at backend context creation now, not on the first twin lookup, so a
process that twins nothing still learns its knobs leave it with no arm at all.
- EsprytSlotTablesEnabled() becomes an inline latch over an out-of-line resolver. It is
consulted on every Find/GetOrCreate/HandleOf/ForEachLive/CollectGarbage*, i.e. several
times per draw, and as a cross-TU call with no LTO that was a PLT call per lookup.
- HandleOf keeps a one-entry lifetimeId -> handle memo, so the three per-draw resolution
paths whose TwinLookupMemos this slice deleted go back to an integer compare plus an
array index instead of the allocator's ByLifetimeId hash - which is the "direct slot
indexing" the memo removal was traded for. It cannot serve a stale answer: a lifetime id
is never handed out twice, and FindByHandle compares Gen anyway.
- GetOrCreate(nullptr) returns a parking slot instead of dereferencing null in a release
build; the map arm inserted a null key and SyncTextureObjectToBackend documents relying
on that tolerance.
- ReclaimDeadSlots moves the twin out before it writes the entry, so a twin destructor that
re-entered GetOrCreate and grew m_slots could not make the writes land in freed memory.
- The framebuffer binding-slot cache is gated on kMGPipeSubsystemEsprytSlots rather than on
the compile-time MOBILEGL_PIPE_PUSH, so MOBILEGL_PIPE_PUSH=0 stays the faithful
all-subsystems-pull control ConfigLoader.cpp documents. The poison bypass stays closed on
the arm that ships.
- MGB_TWIN_KIND_PARAM/ARG stop leaking into every TU that includes Managers.h: the twelve
declaration and definition sites name a TwinRegistry alias template that swallows the
kind in the pull build, and the one remaining macro is #undef'd after the class.
- The twin lookup inside BindCurrentFBO stops shadowing the framebuffer binding slot in a
function whose whole subject is which "slot" is meant.
- ResolveVaoTwin, SyncCurrentProgram and BindCurrentFBO stop consulting the three
TwinLookupMemos on the handle arm. The memo existed to replace the registry hash probe
with an array index, and the slot table Find already IS that array index; its safety
argument - owner-equality of a weak snapshot against a recycled heap address - is
answered by the generation instead of re-derived per lookup. OwnerEquals, the memo
template and its three instances are now compiled only under MOBILEGL_PIPE_LEGACY_MEMOS.
- UnitSamplerLookupMemo compares {slot, gen} instead of owner-equality, and keeps its "a
miss is never cached" contract verbatim: the sampler twin is created later in the same
draw by the program pass.
- UnitBindingsSnapshot holds lifetime ids rather than weak_ptrs under push. It cannot hold
handles: a bound-but-never-synced texture has no twin and so no handle, and two of those
would read as equal. A lifetime id exists before the twin does and is never handed out
twice, which is the property the weak_ptr was there for.
- SyncTextureObjectToBackend keeps its by-value copy only to hold the twin alive across the
nested glTextureView sync; the second Find-or-create and the put-the-twin-back repair are
gone on the handle arm, because nothing there erases a live entry.
- The one direct-iteration site walks ForEachLive, which hands over a strong reference to
the framebuffer instead of the map key - the raw frontend address it had to null- and
expiry-check by hand before dereferencing.
- GetFramebufferBindingSlotFast becomes GetFramebufferBindingSlotChecked and, under push,
reads MGB_CTX->GetFramebufferBindingSlot(target) every time. This closes the P1 accessor
bypass: the cached raw pointer ran the checked accessor once per context change and then
handed out the pointee forever, so the per-verb poison stamp and the verify read-hook
were skipped at all five call sites.
- Every one of these is a push-build arm; the pull build compiles the pre-P2 text and its
symbol report stays 0 added / 0 removed / 0 renamed with no new resize.
- 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.
Audit of every memoization implementation; sixteen verified defects fixed:
DirectGLES backend:
- Broadcast draw-buffer memo: cleared at MakeCurrent/DestroyEGLContext like its
sibling shadows; its identity+version key is only monotonic within one GLContext,
so a library teardown + re-init could false-hit on a recycled FBO address.
- Backend texture id re-mint (RecreateBackendTexture) now bumps an attachment
generation that the SyncCurrentFBO gate and every FBO twin compare, so driver
FBOs re-attach instead of keeping the deleted texture name; the attachment walk
re-enters until the generation is quiescent (a walk itself can re-mint).
- Buffer id re-mint (persistent-map adoption, immutable-store retire) now bumps a
generation the VAO twin sync compares, forcing a full re-emit of the baked
glVertexAttribPointer / element-array bindings that frontend versions cannot see.
- VAO element-array sync memo: bound-object identity joins the wrapping Uint16
slot version (same pairing the ResolvedDrawBuffers IBO memo already uses).
DirectVulkan backend:
- EBO slice memo gains the mapped-buffer guard its vertex-binding sibling has: a
shadow-backed persistent map mutates with no epoch bump, so a hit must decline.
- VkClearManager::MergeClearPayload keeps colorEncoding/colorInt/colorUint with
the color, so deferred glClearBufferiv/uiv no longer degrade to all-zero float.
- GetOrCreateComputePipeline no longer memoizes a failed creation (same contract
as PipelineFactory): a transient driver failure was permanently disabling every
dispatch of that program.
- Explicit-LOD-0 verdict memo keys on the sampling-resolution generation; sampler
filter/aniso/LOD setters bump only that counter, so the old key served a stale
verdict (wrong SPIR-V variant) after glTexParameter/glSamplerParameter changes.
- SetupDraw fast path declines instead of re-arming on a moved sampling-resolution
generation (the snapshot bakes the LOD verdict into its pipeline), and
recomputes the XfbCapture bit so the first draw after glBeginTransformFeedback
cannot bind the undecorated variant and silently capture nothing.
- VertexInputStateFactory eviction epoch is drawn from a process-wide source: VAO
state-pointer memos outlive the factory across renderer recreation, and a fresh
factory restarting at epoch 1 would dereference a dead factory's entry.
- Cached render passes re-read the live renderbuffer clear payload at begin (the
clear VALUE is not in the pass hash; the entry's inline snapshot replayed the
creation-time color and dropped the newly queued one).
- FramebufferObject gains a never-reused lifetime id, keyed into the render-pass
fast-path memo and the SetupDraw snapshot beside the raw pointer + Uint16
version pair, which address reuse plus fresh version counts could equal.
- SyncTextureResource's preserved-content image goes through the deferred-release
ring on both failure paths instead of a synchronous destructor under the GPU.
MG_State frontend:
- Layer-1 compile memo is env-disciplined like layers 2/3: a node computed against
a dead CompileEnv (e.g. pre-capability fallback limits) no longer answers
glCompileShader forever once the environment's content changes.
- Pipeline composite cache rebuilds from each stage program's last-link shader
snapshot (new LinkedShaderRef list + pinned link inputs) instead of the live
attach list and current compile nodes: post-link glAttachShader/glCompileShader
must not leak into the composite while the (lifetimeId, linkVersion) signature
still hits - GL's "as last linked" rule.