Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.
1. P6's plan and contract draft (neither started; not one line of code)
docs/Disaggregated/P6-SPAWN-PLAN.md the package plan
docs/Disaggregated/P6-CONTRACT-DRAFT.md the contract draft (moves to
MG_Remote/CONTRACT-P6.md when c6 lands)
The three findings that mattered, all written into the documents:
a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
the tree and already tested: SocketDoorbell including hangup detection and the two
lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
assembly plus a control plane plus a process, not "write a socket transport".
b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
the tree again. So the first package, a6, is a read-only audit that writes no code, and
every contract row that depends on it is marked PENDING a6 rather than decided in
advance.
c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
and protocol.fbs:186-196's own comment says Android transfers the window out of band -
that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
gate measures. A real window reaching the server is a named refusal. This gets its own
section because "P6 is done" would otherwise read as "the game runs now".
The contract adds rule G: a role may not name a process-local handle. It extends P5c's
rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
a token this contract defines or they do not cross, and a descriptor crosses only through
SCM_RIGHTS, where the kernel renumbers it.
What P5e left for P6 gets its own section. The latch semantics are already designed
(ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
client no longer waits while the peer process can DIE: the client holds records that were
published and will never be applied, and Android's freezer suspends the child without
closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
That is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122), and the two should be designed together.
Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
match integration-split's name for name, because a differing set means the lane is quietly
running less; and the arm-proof gate requires every entry to record the child pid and
transport=spawn in its OWN private log - ID-124 established that this kind of false green
really happens, and a spawn lane that actually ran monolith would pass every other gate.
S2's shape must be settled before it is written, and the document says why: E1 is the
cautionary case.
2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)
ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
is under no version control. A record cited hundreds of times that lives only locally is not
a record, so it moves in whole, with its directory structure intact.
Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
stay where they are: they are evidence DATA rather than documents - large, regenerated per
run, and mostly carrying absolute paths and device serials that mean something only on the
machine that produced them. The mapping and this split are written in
docs/Disaggregated/notes/README.md.
Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
material salvaged from failed workflow runs, which may duplicate the like-named phase
directories and is kept because a few of those were the only complete version at the time.
CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
deliberately outside it: those notes' file:line citations were written against their own
code heads, and resolving them against today's HEAD would fail wholesale - which is not an
error, it is their correct state as a historical record. To check one, use the revision it
declares (--rev).
3. Inconsistencies fixed in passing
- README.md's status line still said P5e was in progress while the stage table already said
closed.
- CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
(a) and (b) were run in the previous commit; rewritten to closed plus the one item that
actually remains (E1).
- ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
package order, the scope boundary and the two new gates.
Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
42 KiB
Track H, slice 1 — scout report
Worktree: C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg, branch feat/disaggregated @ e7a6a72f.
All paths below are relative to that root unless spelled absolutely. Every line number was opened.
Scope per ROADMAP P2 (docs/Disaggregated/ROADMAP.md:18): "第一片 Track H:Espryt 0b(SlotAllocator + 6 个 registry → slot 数组 + 删 TwinLookupMemo×3/OwnerEquals/g_fbSlotCache/GC)与 Magma 子系统 4(VertexInputStateFactory/VaoDrawMemo 重键,删前端 VAO 里的后端裸指针);MOBILEGL_PIPE_LEGACY_MEMOS".
1. Espryt 0b — StateBackendObjectRegistry and the memos layered on it
1.1 The template itself
MobileGL/MG_Backend/DirectGLES/Managers.h:271-395 — template <typename StateObject, typename BackendObject> class StateBackendObjectRegistry.
- Key type: raw
StateObject*(the frontend heap address).using BackendMap = UnorderedMap<StateObject*, Entry>;atManagers.h:285.UnorderedMaphere is the ska/flat open-addressed map (see the erase-relocation warnings quoted below), notstd::unordered_map. struct Entry { BackendPtr backend; StateWeakPtr stateRef; }—Managers.h:281-284. Theweak_ptris the ABA defence: a recycled heap address is caught because the snapshot pins the dead object's control block. Comment atManagers.h:277-280says a separate liveness map was tried and removed (second hash lookup on a path the draw runs ~10×).GetOrCreate(const StatePtr&)—Managers.h:289-323. CallsEnsureProcessTeardownSentinel()(Managers.h:295), runs a deferredCollectGarbage()whenm_creationTick >= kCreationGCIntervalbefore taking the entry reference (Managers.h:300-303; the comment at296-299explains that erasing after taking the reference would invalidate it because the map is open-addressed), thenm_entries[stateObj.get()], ticksm_creationTickonly on a genuinely new key (Managers.h:305-318), resetsentry.backendwhenentry.stateRef.expired()(Managers.h:319-322), re-seedsentry.stateRef = stateObj, returnsBackendPtr&.Find(StateObject*)—Managers.h:334-345. Find erases: on an expiredstateRefit callsm_entries.erase(entryIt)and returns null (Managers.h:339-342). The doc comment atManagers.h:326-333is explicit that an erase relocates the rest of the probe cluster, so aBackendPtr*returned byFindis invalidated by any later registry call — callers must copy theSharedPtrout.constoverload atManagers.h:347-349const_casts and therefore also erases.- Iteration:
begin()/end()atManagers.h:351-354— used by the FBO scan in §1.3. - GC:
CollectGarbageIfNeeded()Managers.h:356-364(ticksm_gcTick, collects everykGCInterval = 1024),CollectGarbageNow()Managers.h:366, privateCollectGarbage()Managers.h:369-391(two-pass: gatherstaleKeyswherestateRef.expired(), then erase; re-entrancy guarded bym_isCollecting). - Constants/members:
kGCInterval = 1024(Managers.h:394),kCreationGCInterval = 64(Managers.h:396),m_entries,m_gcTick,m_creationTick,m_isCollecting(Managers.h:397-400). - Teardown interlock:
InProcessTeardown()/EnsureProcessTeardownSentinel()declaredManagers.h:59-60, documentedManagers.h:44-58, definedManagers.cpp:161-170(astd::atexithandler latched viastd::call_once; a registry destructor hook was tried and is explicitly wrong because tests destroy temporary registry instances mid-run). Twin destructors also compareg_backendContextGeneration(Managers.h:64-70) so a twin outliving its ES context does notglDelete*a recycled name.
1.2 The six instances
| # | Instance | Declared | Defined | Maps |
|---|---|---|---|---|
| 1 | VertexArrayImpl::g_backendVertexArrayObjects |
Managers.h:803-804 |
Managers.cpp:2747-2748 |
MG_State::GLState::VertexArrayObject* → SharedPtr<BackendVertexArrayObject> |
| 2 | TextureImpl::g_backendTextureObjects |
Managers.h:1124-1125 |
Managers.cpp:5075 |
ITextureObject* → SharedPtr<BackendTextureObject> |
| 3 | FramebufferImpl::g_backendFramebufferObjects |
Managers.h:1215-1216 |
Managers.cpp:5828-5829 |
FramebufferObject* → SharedPtr<BackendFramebufferObject> |
| 4 | PrgramImpl::g_backendProgramObjects |
Managers.h:1733-1734 |
Managers.cpp:6139 |
ProgramObject* → SharedPtr<BackendProgramObjectImpl> |
| 5 | SamplerImpl::g_backendSamplerObjects |
Managers.h:1833-1834 |
Managers.cpp:8657 |
SamplerObject* → SharedPtr<BackendSamplerObject> |
| 6 | RenderbufferImpl::g_backendRenderbufferObjects |
Managers.h:1860-1861 |
Managers.cpp:8769-8770 |
RenderbufferObject* → SharedPtr<BackendRenderbufferObject> |
Note: the key of #2 is the polymorphic base ITextureObject, so texture views (TextureObjectView) and ordinary textures share one keyspace — the handle kind Texture must cover both (MGPipeHandles.h:27).
1.3 Every lookup / insert / erase / GC site
DirectGLES.cpp (all sites, with enclosing function):
| Line | Enclosing function | Op |
|---|---|---|
| 1209-1210 | VertexArrayImpl::ResolveVaoTwin (1202) |
VAO Find then GetOrCreate, creates twin if slot null, stores into g_vaoTwinLookupMemo (1214) |
| 1223 | VertexArrayImpl::SyncCurrentVAO (1216) |
VAO CollectGarbageIfNeeded |
| 1306-1308 | TextureImpl::SyncTextureObjectToBackend (1300) |
texture Find/GetOrCreate; copies the twin out by value (1321) because the sync can re-enter via SyncTextureViewToBackend and relocate the map |
| 1340-1342 | same function, tail | re-resolve after the nested sync; restores the twin if a collection took the slot |
| 1533 | TextureImpl::SyncNeccessaryTextures (1529) |
texture CollectGarbageIfNeeded |
| 1898-1900 | FramebufferImpl::SyncCurrentFBO (1894) |
GC ×3: FBO, texture, renderbuffer |
| 1960 | SyncCurrentFBO, draw==read fast path |
FBO Find → SyncReadBufferToBackend |
| 1969-1971 | SyncCurrentFBO, main path |
FBO Find/GetOrCreate + create |
| 2728-2729 | PrgramImpl::SyncCurrentProgram (2724) |
GC ×2: program, sampler |
| 2771-2777 | SyncCurrentProgram |
program Find/GetOrCreate + create + g_programTwinLookupMemo.Store |
| 2876-2879 | BindCurrentFBO (2856) |
FBO Find (memo-miss fallback) + g_fboTwinLookupMemo.Store |
| 2917-2922 | SyncAndBindFramebufferObject (2897) |
FBO Find/GetOrCreate + create |
| 3112 | ResolveAndBindUnitTextures (3025) |
texture Find |
| 3170 | ResolveUnitSamplerBackend (3164) |
sampler Find, memoised (see §1.5) |
| 3387 | BindCurrentProgramWithResources (3373) |
program Find |
| 3622 | BindCurrentProgramWithResources |
sampler GetOrCreate (this is where a unit's sampler twin is first created — the reason ResolveUnitSamplerBackend must never latch a miss) |
| 3671 | GetCurrentBackendProgram (3663) |
program Find |
| 4101-4103 | GetBackendProgramId (4089) |
program Find/GetOrCreate |
| 4574 | DrawArrays (4566) |
VAO Find |
| 4615 | MultiDrawArrays (4599) |
VAO Find |
| 6127-6130 | UpdateTextureBindingAtTarget (6105) |
texture Find/GetOrCreate |
| 6407 | ScopedDetachedTextureFramebufferAttachments ctor (6401) |
texture Find |
| 6413-6425 | same ctor | full iteration over g_backendFramebufferObjects, dereferencing it->first (the raw key) only after checking it->second.stateRef.expired() — the one site that reads the registry's internals directly |
| 6705 | CopyTexImage2D (6678) |
texture Find |
| 6800 | CopyTexSubImage2D (6771) |
texture Find |
| 7064-7067 | SyncRenderbufferObjectToBackend (7060) |
renderbuffer Find/GetOrCreate + create |
| 7363 | ShaderStorageBlockBinding (7357) |
program Find |
| 9206 | GetTexImage (9168) |
texture Find |
Managers.cpp:
| Line | Context | Op |
|---|---|---|
| 5222-5228 | BackendFramebufferObject::SyncAttachmentObject (5216) |
texture Find/GetOrCreate + create |
| 5273-5279 | same function, renderbuffer branch | renderbuffer Find/GetOrCreate + create |
| 5771 | attachment verification asserts (debug) | texture Find |
| 5788 | same | renderbuffer Find |
MG_Test/SanityTest.cpp (test-only, but it copies and restores whole registry instances, so a slot-array rewrite has to keep that possible):
160 (copy-construct previousRegistry), 164 (= {} reset), 178 (restore), 195 (decltype member) — the ScopedDirectGLESTextureBindings fixture at 145-197; 321, 365 GetOrCreate; 3031, 3051 Find in the sampled-set staleness test.
1.4 TwinLookupMemo ×3 and OwnerEquals
OwnerEquals—DirectGLES.cpp:63-66, a free template:!snapshot.owner_before(current) && !current.owner_before(snapshot). Rationale comment60-62(raw addresses lie; a heldweak_ptrpins the control block).TwinLookupMemo<StateObject, BackendObject, kIndexBits>—DirectGLES.cpp:83-116.Lookup(86-92) probesm_slots[IndexFor(ptr)], requiresslot.key == stateObj.get() && slot.twin && OwnerEquals(slot.owner, stateObj).Store(94-99).struct Slot { StateObject* key; WeakPtr<StateObject> owner; BackendObject* twin; }(102-106).IndexForis Fibonacci hashing of the address, topkIndexBits(108-113). Backing storeArray<Slot, 1<<kIndexBits>(115). The 20-line safety argument at68-82is the thing a handle rewrite must reproduce: it rests entirely on "a live state object's entry is never erased nor its twin replaced".- Three instances:
g_vaoTwinLookupMemo, 12 bits = 4096 slots,DirectGLES.cpp:123-124(sizing rationale118-122: Minecraft cycles hundreds of section VAOs/frame).g_programTwinLookupMemo, 8 bits = 256 slots,DirectGLES.cpp:125-126.g_fboTwinLookupMemo, 6 bits = 64 slots,DirectGLES.cpp:131-132(rationale127-130).
- Use sites:
1206+1214(VAO),2769+2777(program),2874+2879(FBO). - Other
OwnerEqualscallers, outside the three memos — these die with it and must be re-expressed:DirectGLES.cpp:1392and1394—UnitBindingsUnchanged, comparing theUnitBindingsSnapshot(1366-1370:Array<WeakPtr<ITextureObject>, TextureTargetCount> slotObjects+WeakPtr<SamplerObject> samplerObject) captured byCaptureUnitBindings(1372-1384) against the live units. Global snapshot storeg_observedUnitBindings/g_observedUnitBindingsContextIdat1399-1400.DirectGLES.cpp:3167—ResolveUnitSamplerBackend(§1.5).
1.5 UnitSamplerLookupMemo (the fourth weak-ptr memo; ARCHITECTURE §9.5 lists it among the 11 deletions)
DirectGLES.cpp:3157-3161 — struct UnitSamplerLookupMemo { WeakPtr<SamplerObject> frontend; BackendSamplerObject* backend; }, array g_unitSamplerLookupMemos per texture image unit (3162-3163). ResolveUnitSamplerBackend at 3165-3177. Contract comment 3148-3156: a miss is never cached, because the sampler twin is created later in the same draw by the program pass (DirectGLES.cpp:3622). The downstream walk memo lives at 3195-3200 (g_unitSamplerWalkContextId, …Epoch, …MaxUnit, …ContextGeneration, …Valid, …Rows).
1.6 g_fbSlotCache / g_fbSlotCacheContext and the P1-accessor bypass
DirectGLES.cpp:134-157:
143 using FbBindingSlot =
144 std::remove_reference_t<decltype(MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw))>;
145 static const void* g_fbSlotCacheContext = nullptr;
146 static Array<FbBindingSlot*, SizeT(FramebufferTarget::FramebufferTargetCount)> g_fbSlotCache = {};
147 static inline FbBindingSlot& GetFramebufferBindingSlotFast(FramebufferTarget target) {
148 const void* ctx = MGB_CTX_IDENTITY;
149 if (ctx != g_fbSlotCacheContext) { ... refill all slots from *MGB_CTX ... }
156 return *g_fbSlotCache[SizeT(target)];
157 }
Why it exists (comment 134-142): the frontend getter linear-scans, and the draw path asks several times per draw. The frontend getter it replaces is GLContext::GetFramebufferBindingSlot at MG_State/GLState/Core.cpp:1180, which forwards to FramebufferState::GetFramebufferBindingSlot — a linear scan over m_bindingSlots at MG_State/GLState/FramebufferState/FramebufferState.cpp:48-57.
The P1 bypass, precisely. Under the push arm MGB_CTX is &gPipeInputs (MG_Pipe/PipeInputsSwitch.h:19) and MGB_CTX_IDENTITY is gPipeInputs.ContextIdentity() (PipeInputsSwitch.h:21 → MG_Backend/MGPipe/PipeInputs.h:191, backed by m_contextIdentity at PipeInputs.h:614, written in PipeFill.cpp:270). The accessor PipeInputs::GetFramebufferBindingSlot at PipeInputs.h:508-517 carries MGP_INPUT_CHECK (per-verb poison, PipeInputs.h:44-49) and MGP_INPUT_VERIFY_READ (PipeInputs.h:62-63) and returns *m_framebufferBindingSlot[index] — a pointer into the live GLContext, filled per verb at MG_Impl/Pipe/PipeFill.cpp:123-128 from FillPoints.def rows 156, 175, 188, 203, 247, 252, 284 (kDraw, kDispatch, kClear, kBlitOrCopy, kTextureOp, kReadback, kProgramOp).
The fast getter runs the checked accessor once per context change and then hands out the cached raw pointer forever, so on every later call the per-verb poison stamp and the verify read-hook are skipped. A verb that legitimately never fills GetFramebufferBindingSlot cannot be caught at these five call sites, and a verify build compares the field only when the slow accessor is used.
Its five callers (all in DirectGLES.cpp):
1613— insideTextureImpl::SyncNeccessaryTextures(1529), reads the Draw slot.1907—FramebufferImpl::SyncCurrentFBO(1894), loop over {Draw, Read}.2745—PrgramImpl::SyncCurrentProgram(2724), Draw slot.2860—BindCurrentFBO(2856).2935—ForceBindCurrentFBO(2931).
Remaining uncached MGB_CTX->GetFramebufferBindingSlot reads (they keep the poison and stay correct, but a handle rewrite touches them too): DirectGLES.cpp:4217, 6046, 6047; Managers.cpp:5356, 5461; Magma: UniformManager.cpp:556, VulkanRenderer.cpp:2989, 5188, 5408, 6020, 6111, 6465, 7261, 7629, 8587, 8588, 9223, 9934.
1.7 The FBO-identity comparisons the reserved handle {0,1} retires
DirectGLES.cpp:2874 (currentFBO != pDefaultFramebufferInfo->defaultFBO), 2898 (same in SyncAndBindFramebufferObject), 1939 (currentFBO == …->defaultFBO in SyncCurrentFBO), plus stateFBO->IsDefaultFramebuffer() in the FBO scan at 6420. MGPipeHandles.h:67-73 reserves kMGPipeDefaultFramebuffer{0,1} exactly for these.
1.8 What the replacement has to provide
A SlotAllocator (planned home MobileGL/MG_Impl/Pipe/, ARCHITECTURE.md:552) plus per-kind dense arrays must supply, at minimum:
- Client-minted
{slot, gen}per kind with free list + high-water mark,gen++on slot reuse only (MGPipeHandles.h:19-22, 44-48;ARCHITECTURE.md:36-37), first allocatable slot 1 (MGPipeHandles.h:81), debug assert ongenwrap. lifetimeId → slotmap on the client (ARCHITECTURE.md:40) — the frontend keepsGetLifetimeId()as its own identity; GL names never enter a key (GlNameForDiagonly).- Twin storage indexed by slot replacing all six
BackendMaps — this is what makesFindO(1) with no probe cluster, and removes the "reference into the map is invalidated by the next call" hazard thatSyncTextureObjectToBackend(DirectGLES.cpp:1310-1345) currently pays a by-value copy + a secondFindto survive. - Explicit destroy so the six GCs (
CollectGarbageIfNeededatDirectGLES.cpp:1223, 1533, 1898, 1899, 1900, 2728, 2729) and theweak_ptrliveness probe disappear. Today nothing tells the backend an object died except buffers (§4). - An iterable live set per kind for the one scan site (
DirectGLES.cpp:6413-6425) — a dense array iterates naturally, but a "which slots are live" bit needs to exist. - Default-framebuffer identity as
{0,1}to retire the fourpDefaultFramebufferInfo->defaultFBOcompares (§1.7). - A slot-indexed framebuffer binding table replacing
g_fbSlotCache, soGetFramebufferBindingSlotFastbecomes an ordinary indexed read of pushed state rather than a cached pointer into frontend storage. This is where the P1 bypass gets closed rather than preserved. - Test-fixture support for save/reset/restore of a whole kind's table (
SanityTest.cpp:160-195). MOBILEGL_PIPE_LEGACY_MEMOS(defaulttrue,MobileGL/Config.h:356-360) keeps the registries andTwinLookupMemos compiled alongside the new path for the A/B window; both arms must live behind the samePipeInputsinterface (ARCHITECTURE.md:367).
Every file that changes for Espryt 0b: MobileGL/MG_Backend/DirectGLES/Managers.h (registry template, 6 externs, twin classes' destructor/context-generation contract), MobileGL/MG_Backend/DirectGLES/Managers.cpp (6 definitions + 4 use sites), MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp (~40 sites above, plus OwnerEquals, 3 TwinLookupMemos, UnitSamplerLookupMemo, UnitBindingsSnapshot, g_fbSlotCache), new MobileGL/MG_Impl/Pipe/SlotAllocator.*, MobileGL/MG_Pipe/MGPipeHandles.h (if kinds/reserved bands move), MobileGL/Config.h (PipeLegacyMemos gating), MobileGL/MG_Test/SanityTest.cpp (fixture).
2. Magma subsystem 4 — VertexInputStateFactory, VaoDrawMemo, and the frontend VAO's backend pointers
2.1 VertexInputStateFactory
Header MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.h; class at :18, using HashType = Uint64 at :20, struct BackendVertexInputState at :31 (hash :32, layoutHash :40), cache UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache at :123, m_frameBoundaryCounter :125, m_evictionEpoch = ++s_evictionEpochSource :141. Owned by the renderer: VulkanRenderer.h:889, constructed VulkanRenderer.cpp:3197-3198.
- Key = a content hash of the VAO, not a pointer.
ComputeHashatVertexInputStateFactory.cpp:15-53:XXH64overCacheVersionthen, per enabled attribute,Enabled/Size/Type/Normalized/Stride/Offset/IsInteger/IsLong/IsBgra/Divisorand — crucially —const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;at:48. The 10-line comment at:37-47records that this used to be the buffer's heap address and that the recycled address is what let a transform-feedback capture read a destroyed VAO's slice.ARCHITECTURE.md:363calls for exactly thislifetimeIdto be replaced by the handle'sgenmixed into every server-side content hash. - Where the key comes from: the
VertexArrayObject&is the context's bound VAO —MGB_CTX->GetBoundVertexArray()(e.g.DirectGLES.cpp:2947on the Espryt side; on Magma it arrives as thevaoargument ofUploadAndBindVertexBuffers,VulkanRenderer.cpp:3581).attr.Bufferis aSharedPtr<BufferObject>held by the frontend VAO. GetOrComputeHashVertexInputStateFactory.cpp:55-63— reads/writes the VAO's own hash memo.GetOrCreateVertexInputState(vao):65-86— VAO state-memo fast path (:71-76), elseGetOrCreateVertexInputState(vao, hash)and then stamps all three VAO memos (:78state,:83-84aux).GetOrCreateVertexInputState(vao, hash):88-…—m_cache.find(hash):89, builds otherwise.- Eviction:
OnFrameBoundary():298-324— sweeps every 256 boundaries, retires entries idle > 1024 boundaries, and on every erase doesm_evictionEpoch = ++s_evictionEpochSource(:318), which is what invalidates every VAO'sSetBackendStateMemopointer. Erasure is deliberately confined to the frame boundary (:303-307).
2.2 VaoDrawMemo
VulkanRenderer.h:1244-1264. Fields: vaoKey (raw const VertexArrayObject*, :1245), vaoLifetimeId (:1252, with the 6-line comment :1246-1251 saying the pointer alone is not an identity), contentHash (:1255), layoutFactsValid (:1256), layoutHash (:1261), layoutAuxMasks (:1262), bindings (ResolvedVertexBindings, :1263). Table m_vaoDrawMemoTable sized kVaoDrawMemoSlotCount = 2048 (:1271-1272), never rehashed or swept (:1265-1270); m_currentDrawResolvedEntry :1279 is a pointer into it valid only within one draw (:1275-1278).
LookupVaoDrawMemo — VulkanRenderer.cpp:3540-3579. Key = (address, lifetimeId): the address (>>4, Fibonacci-mixed, :3547-3548) picks a 2-way slot pair index / index^1; the match requires vaoKey == vao && vaoLifetimeId == vao->GetLifetimeId() (:3553-3561); a miss recycles the slot whose bindings.frameSerial is older (:3564-3568) and resets contentHash, layoutFactsValid, bindings.frameSerial/indexFrameSerial/indexBuffer (:3569-3577).
Readers/writers: VulkanRenderer.cpp:3619-3647 (UploadAndBindVertexBuffers: probes the memo before the factory resolve, :3622 reads the VAO hash memo, :3624/:3640 lookup), :3649-3652 (stamps layoutAuxMasks, contentHash, layoutHash, layoutFactsValid), :6197-6220 (the TrySetupDrawFastPath layout-facts read; note the comment :6190-6193 that the slot "only ever answers for THIS object" because of the lifetime id).
The related snapshot SetupDrawSnapshot (VulkanRenderer.h:958-…) already carries the same doubled identities that handles collapse: programLifetimeId :963, vao (raw pointer) :965 + vaoLifetimeId :971 + vaoConfigVersion :972, drawFbo :973 + drawFboLifetimeId :978 + fboVersion :979. ARCHITECTURE.md:363 names this as one of the 7 re-keys ("~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty mask").
2.3 The backend raw pointers stored in the frontend VertexArrayObject
MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h:
| API | Lines | Storage |
|---|---|---|
GetBackendHashMemo(Uint64&) |
106-110 |
m_backendHashMemo :190, m_backendHashMemoVersion :191 |
SetBackendHashMemo(Uint64) |
111-114 |
same |
GetBackendStateMemo(const void*&, Uint64&) |
121-126 |
m_backendStateMemo (const void* into the factory's heap entry) :192, m_backendStateMemoEpoch :193, m_backendStateMemoVersion :194 |
SetBackendStateMemo(const void*, Uint64) |
127-131 |
same |
GetBackendAuxMemo(Uint64&, Uint64&) |
140-145 |
m_backendAuxMemo0/1 :195-196, m_backendAuxMemoVersion :197 |
SetBackendAuxMemo(Uint64, Uint64) |
146-150 |
same |
All six are const methods over mutable members, all guarded by m_configVersion (:189, exposed at :101, bumped in VertexArrayObject.cpp:299, 305, 311 and elsewhere). GetLifetimeId() :66, m_lifetimeId :176, allocator VertexArrayObject.cpp:19-21 (atomic, starts at 1 — :14-17 explains a zero-initialized memo slot must never match a live id).
Every writer and reader across both backends (this is the complete set; grep over MobileGL/ finds no others):
SetBackendHashMemo/GetBackendHashMemoon the VAO — readerVertexInputStateFactory.cpp:58, writer:60; readersVulkanRenderer.cpp:3622,6197,6214.SetBackendStateMemo/GetBackendStateMemo— readerVertexInputStateFactory.cpp:72, writer:78. No other site. This is the oneARCHITECTURE.md:284says is deleted outright ("SetBackendStateMemo(前端 VAO 里存后端堆裸指针)直接删除").SetBackendAuxMemo— writerVertexInputStateFactory.cpp:83-84. There is no live reader left: the values were moved intoVaoDrawMemo::layoutHash/layoutAuxMasks(see the commentVulkanRenderer.h:1257-1259, "the exact valuesGetBackendAuxMemoused to serve, moved here"). TheSetis dead weight today;ARCHITECTURE.md:284routesSetBackendHashMemo/AuxMemoto "server 侧 per-slot 字段".- DirectGLES/Espryt uses none of the three. The Espryt VAO twin tracks its own
m_syncedBufferIdGenerationetc. (Managers.h:795-800) instead. - Adjacent but out of this slice:
ProgramObject::GetBackendHashMemo(Uint flags, Uint64&)/SetBackendHashMemo—ProgramObject.h:798-826, 4-slot flag-keyed ring (kBackendHashMemoSlotCount = 4:1408,m_backendHashMemoSlots:1414, round-robin:824); its only user isProgramFactory.cpp:3446-3448.
2.4 What the re-key must produce
VertexInputStateFactory::ComputeHash'sbufferKeybecomes the buffer handle's{slot, gen}(ARCHITECTURE.md:363), so the hash is meaningful across the wire and no frontendSharedPtr<BufferObject>is dereferenced in the backend.VaoDrawMemokeys onvaoHandle.slot(direct index, no Fibonacci mix, no 2-way probe) withvaoHandle.genas the validity compare —vaoKeyandvaoLifetimeIdboth go away.SetBackendStateMemodeleted; the eviction-epoch dance (VertexInputStateFactory.cpp:78,:318,VertexInputStateFactory.h:141) goes with it, since a slot-indexed table has stable entries.SetBackendAuxMemo/SetBackendHashMemobecome server-side per-slot fields; the frontend VAO losesm_backendHashMemo*,m_backendStateMemo*,m_backendAuxMemo*(VertexArrayObject.h:190-197).- Files that change:
VertexInputStateFactory.{h,cpp},VulkanRenderer.{h,cpp}(memo struct +LookupVaoDrawMemo+UploadAndBindVertexBuffers+TrySetupDrawFastPath+SetupDrawSnapshot),MG_State/GLState/VertexArrayState/VertexArrayObject.h, and — becauseMOBILEGL_PIPE_LEGACY_MEMOSmust keep both arms —MobileGL/Config.h:356-360.
3. The handle scheme the design wants
MobileGL/MG_Pipe/MGPipeHandles.h exists and is P0-landed (98 lines total):
enum class MGPipeKind : Uint8—:24-40:None=0, Buffer=1, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context, KindCount. MirrorsARCHITECTURE.md:38.struct MGPipeHandle { Uint32 Slot; Uint32 Gen; }—:54-61, withoperator==:58-60;static_asserts for size 8 / align 4 / trivially copyable at:63-65.- Semantics comment
:44-53:Genbumps only on slot reuse, never on respecify;2^32recycles to wrap, documented not defended in release, debug allocator asserts. Two generations exist and are strictly separate —MGPipeHandle::Genis the client's "is this still the same GL object",MGGenis the server's "did I recast my driver object", and no MGPipe call may require the client to knowMGGen(ARCHITECTURE.md:44-49). - Reserved handles
:67-77:kMGPipeNullHandle{0,0},kMGPipeDefaultFramebuffer{0,1}(explicitly to retire the fourpDefaultFramebufferInfo->defaultFBOcompares),MGPipeHandleIsNull. First allocatable slotkMGPipeFirstAllocatableSlot = 1at:81. - ShaderCso composite band
:83-97:kMGPipeShaderCsoSlotLimit = 1<<20,kMGPipeShaderCsoCompositeSlotBase = limit - limit/16,MGPipeIsCompositeShaderSlot, plus astatic_assertthat the band does not swallow ordinary slots. - Not yet present anywhere in the tree: any
SlotAllocator, any handle table, any use ofMGPipeHandleoutside this header —grep SlotAllocatorhits onlyARCHITECTURE.md:552andROADMAP.md:18.
Design points from docs/Disaggregated/ARCHITECTURE.md:
- §2.1
:33-40— 8-byte POD in a register pair; client mints, server never returns a handle, hence zero creation round trips (deviation D1 from gallium); slots dense per kind so the server table is an array not a hash map; explicitly unrelated toIndexGenerator, whose LIFO name reuse is the problem handles close; GL name survives only asGlNameForDiaginMGPResourceDescand never enters a memo key or content hash;GetLifetimeId()stays client-side and the client maintainslifetimeId → slot. - §2.3
:53-63— which existing backend cache each CSO kind corresponds to; notablyVertexElementsCso↔VertexInputStateFactory::m_cache,SamplerCso↔BackendSamplerObject,Framebuffer/Xfb↔BackendFramebufferObject. CSOs are content-addressed client-side with per-classska::flat_hash_map<xxHash, MGPipeHandle>and LRU caps (render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096; shader followsProgramObjectlifetime). - §9.5
:363— the accounting this slice pays into: 11 memos deleted (the 6 registries' same-addressweak_ptr+ GC,TwinLookupMemo×3 +OwnerEquals,UnitSamplerLookupMemo'sWeakPtrtest,SetBackendStateMemo,VkTextureManager::TextureIdentityliveness probe,ConvertedVertexStreamKey::sourcePin), 2 moved to client debounce, 7 re-keyed (incl.StampSyncedFBO's 4-tuple →ContentHash+ server-privateattachmentRemintEpoch, andVertexInputStateFactory::ComputeHash'slifetimeId→gen), 1 unchanged (D18). Two latent bugs already landed separately:m_xfbCounterSlotByObjectkeyed on the raw GL name (bd2b4158) andRenderbufferObjectmissingGetLifetimeId()(9c7339b2). - §10.1
:375— server side isMG_Remote/Server/PipeObjectTables: per-kind slot arrays, not an object graph; the server holds no buffer copy, no texels, no frontend object graph.PipeApplierdecodes → updates object tables +PipeInputs→ calls backend function pointers, with a debug assert that noSharedPtror raw frontend pointer crosses the applier boundary. Directory placeholder for the client half:ARCHITECTURE.md:552(MG_Impl/Pipe/— Tracker, SlotAllocator, CsoCache, HostResolve, CompositeResolver, all[P2+]). - §13 gate
:501— beyond the three purity gates there is a debug assertion "every backend memo key is{slot, gen}, never a raw frontend pointer", backed byHandleRecycleScenario, which must be RED on at least one backend before the re-key. - Coverage plumbing that already anticipates handles:
MobileGL/MG_Pipe/Coverage.def:23-24defineskStructuralHandle("the row is a SIGNATURE carryingSharedPtr<MG_State...>, which becomes anMGPipeHandleparameter"), andCoverage.def:128maps the inventory deltahandle-ify (wire handle)to it.ARCHITECTURE.md:80counts 167 structural-handle rows out of 477.
4. Lifetime hooks available today
4.1 GetLifetimeId() per frontend class
| Class | Declaration | Allocator |
|---|---|---|
BufferObject |
MG_State/GLState/BufferState/BufferObject.h:215 (rationale :210-214) |
BufferObject.cpp:22 |
FramebufferObject |
FramebufferState/FramebufferObject.h:159 (rationale :153-158) |
FramebufferObject.cpp:20 |
ProgramObject |
ProgramState/ProgramObject.h:1262 |
ProgramObject.cpp:27 |
RenderbufferObject |
RenderbufferState/RenderbufferObject.h:52 (rationale :46-51) |
RenderbufferObject.cpp:23 |
SamplerObject |
SamplerState/SamplerObject.h:57, defined SamplerObject.cpp:253; id assigned in the ctor init-list SamplerObject.cpp:25 |
SamplerObject.cpp:20 |
ITextureObject (pure virtual) |
TextureState/TextureObject.h:83; override on TextureObjectBase :161, defined TextureObject.cpp:306; ctor TextureObject.cpp:41 |
TextureObject.cpp:24 |
VertexArrayObject |
VertexArrayState/VertexArrayObject.h:66 (rationale :57-65) |
VertexArrayObject.cpp:19-21 |
Each class has its own counter starting at 1, so ids are only comparable within a type (stated at MG_Test/State/ObjectLifetimeIdTest.cpp:52-55). Consumer example outside the backends: ProgramPipelineObject.h:101 builds a pipeline signature from stage programs' lifetime ids.
Gaps for a per-kind slot allocator: the transform-feedback object is a plain struct in a map and has no heap identity — its recycled-name hazard was fixed differently (see MG_Test/State/TransformFeedbackLifetimeIdTest.cpp:10-23, D21, commit bd2b4158). Query and fence objects have no GetLifetimeId() either, though MGPipeKind reserves Xfb, Fence, Query.
4.2 Deletion paths
Frontend GL entry points → GLContext::Mark*ForDeletion:
| GL entry | MG_Impl call |
GLContext |
State container |
|---|---|---|---|
glDeleteBuffers |
GL_Buffer.cpp:532 |
Core.cpp:116-140 |
BufferState.cpp:53 |
glDeleteVertexArrays |
GL_VertexArray.cpp:392 |
Core.cpp:167-169 |
VertexArrayState.cpp:78-104 |
glDeleteTextures |
GL_Texture.cpp:3849 |
Core.cpp:279-307 |
TextureState.cpp:118-155 |
glDeleteProgram |
GL_Program.cpp:611 |
Core.cpp:346-348 |
ProgramState.cpp:28 |
glDeleteShader |
GL_Program.cpp:618 |
Core.cpp:350-352 |
ProgramState.cpp:132 |
glDeleteFramebuffers |
GL_Framebuffer.cpp:2261 |
Core.cpp:1188-1190 |
FramebufferState.cpp:59-71 |
glDeleteRenderbuffers |
GL_Framebuffer.cpp:2236 |
Core.cpp:1252-1254 |
RenderbufferState.cpp:51 |
glDeleteSamplers |
GL_Sampler.cpp:329 |
Core.cpp:1213-1225 |
SamplerState.cpp:31 |
glDeleteProgramPipelines |
GL_ProgramPipeline.cpp:97 |
Core.cpp:1399 |
— |
glDeleteTransformFeedbacks |
GL_Drawing.cpp:1609 |
Core.cpp:1437 |
— |
What deletion actually does (representative bodies): TextureState::MarkTextureObjectForDeletion (TextureState.cpp:118-155) unbinds from every touched unit's binding slot and image binding, calls BumpTextureBindGeneration() (:142, with the comment that a cached sampled set may hold the raw pointer), erases from m_textureObjects and returns the name to m_indexGenerator. FramebufferState::MarkFramebufferObjectForDeletion (FramebufferState.cpp:59-71) rebinds each slot to FBO 0 and erases. VertexArrayState::MarkVertexArrayForDeletion (VertexArrayState.cpp:78-104) handles the detached-bound case and nulls the slot.
Crucially: dropping the container's SharedPtr is the only signal. A still-bound object keeps living, and nothing calls into the backend. The backend learns of the death lazily, when the registry's weak_ptr is found expired by Find (Managers.h:339-342) or by a CollectGarbage sweep. The GetOrCreate comment at Managers.h:306-314 says this out loud: "Nothing tells the backend that a texture or renderbuffer was DELETED — the twin, and the driver storage it owns, lives until a collection", and quantifies the resulting ~100 CTS cases' worth of dead gigabyte-sized objects held at once.
4.3 The one existing explicit death notification
Buffers only: BufferBackendOps (MG_State/GLState/BufferState/BufferObject.h:76-103) with void (*OnDestroy)(SharedPtr<BackendBufferResource>&&) at :103; installed via SetBufferBackendOps (BufferObject.h:126, BufferObject.cpp:26-28); fired from BufferObject's destructor path at BufferObject.cpp:39-41. The other ops in the same table (Respecify :46, SubData :55, FlushMappedRange :64, AcquirePersistentMap :186/499/546, ReadbackFromGpu :320, ResidentSubData :365/397) are the model an object-death callback would follow. SetBackendResource (BufferObject.cpp:607) is slated for deletion once the server owns the resource table (ARCHITECTURE.md:284).
Secondary "the driver object went away" signals the backends already use, which handles do not replace and which must survive: g_backendContextGeneration (Managers.h:64-70, bumped once per DestroyEGLContext), g_attachmentBackendIdGeneration (used in StampSyncedFBO, DirectGLES.cpp:1891), BufferImpl::g_bufferBackendIdGeneration (Managers.h:800), and InProcessTeardown().
5. Tests that would catch a mistake
5.1 What the docs name vs. what exists
HandleRecycleScenario (ROADMAP.md:18, ARCHITECTURE.md:501) does not exist yet — grep finds it only in those two documents. The gate is stated as "green after, and red before the re-key on at least one backend", so it must be written as a genuine reproducer first. Integration scenarios live in MobileGL/MG_IntegrationTest/Scenarios/ (77 files), are gtest fixtures deriving from MGITest::ScenarioTest (Harness/ScenarioFixture.h:117, with the Ready() / GPU-skip contract documented at :120-135), and must be added to the source list in MobileGL/MG_IntegrationTest/CMakeLists.txt (scenario entries start at :55).
Likewise share groups are not implemented at all: the only mention in code is MobileGL/MG_Backend/DirectGLES/Managers.h:1080 ("a texture is owned by exactly one context (share groups are not implemented)"), and ARCHITECTURE.md:18 confirms v1 has one screen / one context / one flow. A "share-group" test can only be a multi-context / context-recreation test today (§5.3).
5.2 Existing unit tests that pin the invariants this slice rewrites
MobileGL/MG_Test/State/ObjectLifetimeIdTest.cpp— the canonical "an address is not an identity" test.ProbeLifetimeIdAcrossAddressReuse:57-…allocates/frees 64 times through avoid* volatilesink (:44, to defeat new/delete elision) and asserts the id differs at a repeated address;:109distinct-id assertion; cases at:115(VAO),:125(Buffer),:135,:139. It skips rather than passes when the allocator never repeats (:120-121, :130). A handle allocator'sgenneeds the analogous test.MobileGL/MG_Test/State/TransformFeedbackLifetimeIdTest.cpp— the name-recycling analogue (D21);context.MarkTransformFeedbackObjectForDeletion(name)at:72and:135; header comment:10-23describes exactly the failure mode a slot table must not reintroduce, plus the "open span vs closed span vs gone" contract a bounded slot table must answer.MobileGL/MG_Test/SanityTest.cpp:ScopedDirectGLESTextureBindings:153-197— saves/clears/restoresg_backendTextureObjectswholesale; a slot-array rewrite must keep this shape or the fixture must be rewritten with it.:2575-2596DirectGLESStateGuards.ScratchFBOTextureDeletionForcesFullScrub— "attached texture id dies → must scrub and re-attach", the ES-does-not-auto-detach rule.:2650-2665— a texture twin whose context died must not delete a foreign recycled name (bumpsg_backendContextGenerationaround the reset).:2765-2773and:2786-2790— the same for framebuffer and renderbuffer twins;:2757-2762also pins that the bind shadow follows ES reverting deleted-framebuffer bindings to 0.:3020-3062— the sampled-set staleness test: syncsresident, silently swapsforeignonto unit 0's binding slot, re-runs the draw's fill (ScopedPipeVerb draw:3029,draw.Renew():3046), and assertsforeigngot its own twin (:3051-3055) and that nothing re-specifiedresident's storage. This is the closest existing analogue of a handle-reuse scenario and it exercises the texture registryFinddirectly.
MobileGL/MG_Test/Buffer/BufferTest.cpp:137-171,MG_Test/Framebuffer/FramebufferTest.cpp:154-227— name recycling at theIndexGeneratorlevel (GenBuffers/GenFramebuffers/GenRenderbuffersreturn the recycled name). These are the frontend behaviours the handle allocator must not inherit (MGPipeHandles.h:20-22).MG_Test/Program/ProgramPipelineCompositeTest.cpp:449-459— composite program identity is stable across units viaGetLifetimeId(); relevant to the reserved ShaderCso composite band.MG_Test/Program/XfbFrontendOrderInvarianceTest.cpp:27— references the recycled-heap-address binding memo as its motivation.
5.3 Existing integration scenarios closest to the four named cases
- Handle reuse / deletion-while-bound:
Scenarios/XfbCaptureBufferReuseScenario.cpp(header:9-27: one capture buffer reused across spans, with respecify-between-spans and immutable-storage variants, plus a fresh-buffer-per-span negative control) andScenarios/CrossFrameBufferScenario.cpp(header:9-30: the shipped stale-slice bug thatVaoDrawMemo/the EBO memo caused — mutate a buffer after a frame boundary, prove the pixels show the new content). - Framebuffer slot:
Scenarios/CrossFrameBufferScenario.cpp(draw/read FBO interplay) and, on the unit side,SanityTest.cpp:2575scratch-FBO scrub. The read-buffer-shared-FBO defect class the design says handles kill by construction is described atARCHITECTURE.md:134. - Pipe machinery negative controls already wired:
Scenarios/PipeVerifyArmingScenario.cppandScenarios/PoisonOmissionScenario.cpp(the latter forks a child process,:61-69, to observe aFatal{UnmigratedPipeInput}) — these are the templates for aHandleRecycleScenariothat must be red before the re-key. - VAO-shaped coverage that the Magma re-key can regress:
Scenarios/VertexArrayEnableDisableScenario.cpp,Scenarios/VertexAttribBindingScenario.cpp,Scenarios/MultiDrawScenario.cpp,Scenarios/ResidentIndexScenario.cpp,Scenarios/SampledSetStalenessScenario.cpp.
5.4 Suggested shape of the missing HandleRecycleScenario
To be red before the re-key it has to reproduce, through public GL, the ABA the current keys only paper over: delete a VAO (or texture/FBO) that a backend memo slot holds, immediately create a replacement that the allocator is very likely to place at the same address and give a byte-identical configuration (so the content hash matches too), then draw and assert the pixels come from the new object. The ObjectLifetimeIdTest volatile-sink trick (:44) and the CrossFrameBufferScenario red/green quad readback are the two halves to borrow. Because the current code defends this with lifetimeId + weak_ptr, the "red before" arm needs those defences disabled — which is exactly what MOBILEGL_PIPE_LEGACY_MEMOS=0 (Config.h:356-360) is for.
6. Sequencing notes for the implementer
MOBILEGL_PIPE_LEGACY_MEMOS(Config.h:360, defaulttrue) is a compile-time switch by design (ARCHITECTURE.md:367): after phase C theMOBILEGL_PIPE_PUSHbitmap is not a valid A/B any more, because a re-key bug is present in both arms. Both implementations must sit under the samePipeInputsinterface.- The registry's "reference into the map dies on the next call" hazard (
Managers.h:326-333) has already produced one real defect fix inSyncTextureObjectToBackend(DirectGLES.cpp:1310-1345). A dense slot array removes the hazard; the by-value copy and secondFindshould be deleted in the same change, not left as harmless. Finderasing on expiry (Managers.h:339-342) means the current code has no read-only lookup. If the new table keeps aFindthat mutates, the FBO scan atDirectGLES.cpp:6413(which iterates while other code may run) needs re-auditing.EnsureProcessTeardownSentinel()is called fromGetOrCreate(Managers.h:295) — i.e. the first registry insertion is what arms the atexit guard. If insertion moves to aSlotAllocator, the arming site moves with it; the comment atManagers.h:52-57explains why a destructor hook is wrong.- Purity gate C greps
MG_Backend/for the tokenpGLContext(ARCHITECTURE.md:501,PipeInputsSwitch.h:15-16); nothing in this slice may reintroduce it, and theg_fbSlotCacherewrite is the one place tempted to.