Files
MobileGL/docs/Disaggregated/notes/p5e/scout-S7.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
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.
2026-09-18 23:31:39 -04:00

227 lines
26 KiB
Markdown

# S7 - identity: the five twin registries and the allocator (head `2fde7034`)
Paths under `MobileGL/`. `DG` = `MG_Backend/DirectGLES/DirectGLES.cpp`, `MG` = `MG_Backend/DirectGLES/Managers.cpp`,
`MH` = `Managers.h`, `ST` = `MG_Backend/DirectGLES/SlotTables.h`, `SA` = `MG_Impl/Pipe/SlotAllocator.{h,cpp}`,
`PF` = `MG_Impl/Pipe/PipeFill.cpp`. Kinds: **id** = `GetLifetimeId()` + `MGPipeSlots().FindByLifetimeId`
(`ST:436-449`, through `MGPipeRefuseAllocatorFromApplyThread` `SA.cpp:257-273`); **mint** = `GetOrCreate(const StatePtr&)`
-> `MGPipeSlots().Acquire` (`ST:215-268`), a WRITE into the client allocator; **live** = a dereference of the frontend
object beyond its lifetime id; **rec** = a handle-keyed arm that already exists beside the site.
## 1. The exact live reads of the family (per kind: today's key, every apply-thread site, hit/miss)
**The mechanism common to all five** (`MH:309-620` `StateBackendObjectRegistry` wrapping `BackendSlotTable`):
`Find(StateObject*)` `ST:411-414` = `HandleOf` (id) + `FindByHandle`; `GetOrCreate(StatePtr)` `ST:215-268` = `Acquire`
(mint) + `RememberHandle` + `entry.stateRef = stateObj` (a frontend `SharedPtr` OWNED BY THE SERVER TABLE, `ST:162,260`);
the per-table memo `m_memoLifetimeId/m_memoHandle` (`ST:649-650`, `mutable`, written from the apply thread on every id);
`OnFrontendObjectDestroyed` `ST:465-485` = id + `MGPipeSlots().Free` (monolith / NoSession fixture only). Every id and
mint on the apply thread is legal today only through `MGPipeFrontendKeyedRegistryScope` / `MGPipeReverseAnnouncementScope`
(`SA.cpp:263-264`); the two depth counters `SA.cpp:303,324`.
| kind / table (key today) | apply-thread sites at `2fde7034` | kind; when |
|---|---|---|
| **Buffer** `g_backendBufferResources` `MG:2757`, `BackendSlotTable<..., Buffer>` `MH:886-888` - ALREADY handle-keyed; the only frontend probe is `HandleOfBuffer` `MG:2925-2941` (scope `:2938`) | callers: `EnsureBufferResource(obj)` `MG:3444-3455`; `IsBufferDrawClean(frontend,res)` `MG:3159-3162` -> `IsBufferDrawCleanByHandle(HandleOfBuffer(frontend),...)`; `MarkBufferGpuWritten` `MG:2945-2951`. Reached per draw from S1 (VAO ensure walk, IBO arm), S5 (UBO/SSBO/atomic/XFB points), image buffer textures | id; memo HIT and miss (IsBufferDrawClean runs on the hit path) |
| **VAO** `g_backendVertexArrayObjects` `MH:1423-1424`, `TwinRegistry<VertexArrayObject, BackendVertexArrayObject, VertexElementsCso>`; key = VAO lifetime id -> VertexElementsCso slot | `ResolveVaoTwin` `DG:1602-1633` (scope `:1609`, `Find` `:1617`, `GetOrCreate` `:1618`) from `PrepareForDraw` `:4772` EVERY draw; `DrawArrays` `:6729-6731`, `MultiDrawArrays` `:6775-6777` (`Find`) | id every draw; mint on first draw of a VAO |
| **Texture** `g_backendTextureObjects` `MH:1818-1819`, key = texture lifetime id -> Texture slot | `SyncTextureObjectToBackend` `DG:1723-1802` (scope `:1734`, `Find` `:1736`, `GetOrCreate` `:1738`, re-resolve `:1781/:1790`) called from the three sync-list rebuilds `:2308,:2381,:2457` (memo MISS), `SyncImageTextureBinding` `:2660` (every image unit), `CopyImageSubData` `:8294-8295`, `GenerateMipmap` `:9575-9578`, CopyTex endpoint `:9735`; `ResolveShaderImageRecord` `:2588/:2634` `HandleOf`; `ResolveAndBindUnitTextures` `:4926-4928` `Find` (memo miss); `ScopedDetachedTextureFramebufferAttachments` `:8837-8839` `Find`; `GetTexImage` `:11884-11886`; mip shape `:9161-9162, :9216-9217` `HandleOf` (transport arm!); CopyTex monolith else-arm `:8512-8515`. `MG`: `:5882-5884` `HandleOf` (image-bindable re-arm), **`:7087-7089` `HandleOf` inside `SyncMipmapsToBackend` - on EVERY texture sync on the handle arm**, `:8481-8483` `ResolvePushedBuiltinSampler`, `:8717-8719` `ResolvePushedTextureParams`, legacy attach `:9256-9261` `Find/GetOrCreate`, `:9630` (monolith-gated), verify arm `:10382-10384` | id on every texture sync (hit or miss of the unit memo); mint on first sync |
| **Renderbuffer** `g_backendRenderbufferObjects` `MH:2819-2820`, key = rb lifetime id | `SyncRenderbufferObjectToBackend` `DG:9688-9710` (scope `:9695`, `Find/GetOrCreate` `:9698-9701`) from CopyTex endpoint `:9714` and attach; `MG:9310-9318` legacy attach; **`MG:9709-9720` `HandleOf` cross-check - NOT monolith-gated, unlike the texture twin at `:9628-9632`**; `MG:13570-13574` `HandleOf` inside `BackendRenderbufferObject::SyncToBackend` (every rb sync); verify arm `:10403-10406` | id per sync; mint on first attach |
| **Framebuffer** `g_backendFramebufferObjects` `MH:1943-1944`, key = FBO lifetime id; default FBO = `kMGPipeDefaultFramebuffer{0,1}` (`MGPipeHandles.h:73`), never a twin | record arm `SyncCurrentFBOByRecord` `DG:3197` `GetOrCreateByHandle` (rec) **but `:3228` `NoteStateForHandle(record.Fbo, currentFBO)` writes a frontend `SharedPtr` into the table**; decline arm `:3303/:3358/:3367-3369` `Find/GetOrCreate`; `:3032` `HandleOf` (monolith-gated `:3031`); `BindCurrentFBO` decline `:4585/:4592/:4602`; `SyncAndBindFramebufferObject` `:4646-4647`; `SyncAndBindFramebufferByHandle` `:4687` (rec) + `:4700` `StateForHandle` (live: the note); named blit `:8361` `StateForHandle`; `ForEachLive` `:8904` (locks every `stateRef`); `MG:9862,:10101` `HandleOf` monolith else-arms (`m_pushedSyncHandle` `MH:1857` under transport) | rec on the steady draw; the note is a live frontend read on every named blit / by-handle bind |
| **Program / ShaderCso** `g_backendProgramObjects` `MH:2540-2541`, key = program lifetime id -> ShaderCso slot (composites: the band, `ProgramEmit.h:411-422`) | `SyncCurrentProgram` `DG:4320-4432` (scope `:4329`, `Find/GetOrCreate` `:4415-4416`) EVERY draw; `ResolveGlobalConstantsRecord` `:4236/:4242` `HandleOf` every draw with UBO bytes; `BindCurrentProgramWithResources` `:5328/:5337` `Find` when the raw-pointer stash `g_currentDrawFrontendProgram` (`:4097,:4509,:5333`) misses; `GetCurrentBackendProgram` `:5689-5691` `Find` (per sub-draw via `:5716-5827`); `GetProgramBackendId` `:6166-6170` `Find/GetOrCreate`; `SetStorageBlockBinding` `:10029-10031`; `MG:12949-12954` `HandleOf` inside `BackendProgramObjectImpl::SyncToBackend` | id every draw; mint on first use |
| **Sampler** `g_backendSamplerObjects` `MH:2678-2679`, `TwinRegistry<SamplerObject, BackendSamplerObject, SamplerCso>` - TWO disjoint families in one table (`MH:2681-2700`): identity twins minted off the SamplerObject's lifetime id, and CSO twins at content-addressed handles (`SamplerEmit.h:427` `Allocate` with no lifetime id) | identity family: `ResolveUnitSamplerBackend` `DG:4989-5013` (scope `:4993`, `HandleOf` `:4999`, `g_unitSamplerLookupMemos`), the sampler-pass decline arm `:5623-5630` **`GetOrCreate(samplerObject)` = a MINT from the apply thread on every unit the record arm does not cover**, `MG:13264-13280` `HandleOf` inside `BackendSamplerObject::SyncToBackend(object)`. CSO family: `ResolveSamplerCsoTwin` `MG:13460-13486` `GetOrCreateByHandle(cso)` from `BindCurrentUnitSamplers` `:5146` and the program pass `:5616` | rec (CSO); id + mint (identity) |
| **SamplerView** `g_backendSamplerViews` `MH:2761-2764` handle-only `BackendSlotTable`; key = texture lifetime id -> SamplerViewCso slot | no backend caller of `HandleOfSamplerViewForTexture` (`MG:4535-4546`, scope `:4544`); its only caller is the harness `MG_IntegrationTest/Harness/PipeApplyPeek.cpp:165` | none on the draw path |
Outside DirectGLES: `MG_Impl/Pipe/ResourceTracker.h:723` (`MGPipeAnnounceBufferGpuWritten`, Magma), `VulkanRenderer.cpp:4554,
:4634, :8986` (Magma hidden resources' destructors + `MGPipeSlots().IsLive` in the blit endpoint resolve). Scope-site census:
20 in `DG` (`:1609 :1734 :2588 :3303 :4236 :4329 :4585 :4926 :4993 :5328 :5689 :6166 :6729 :6775 :8837 :9161 :9216 :9695
:10029 :11884`), 15 in `MG` (`:256 :2938 :4544 :5882 :7087 :8481 :8717 :9256 :9310 :9709 :10382 :10403 :12949 :13273 :13570`),
1 in `ResourceTracker.h`, 3 in Magma, 3 in `MG_Test/Wire/RemoteClientTest.cpp:1883-1903`.
**Corrections to the E note.** (i) The VAO twin key is NOT content-addressed: `VertexInputEmit.h:170-172` mints the
VertexElementsCso slot with `Acquire(VertexElementsCso, vao->GetLifetimeId())`, one CSO per VAO, re-created on the SAME
handle when `ConfigVersion` moves (`:178-179`), so the twin key already equals `MGPipeApplier().BoundVertexElements`.
(ii) The same holds for every kind: Texture `TextureEmit.h:422` / `PF:1495`, Renderbuffer `PF:1499`, Framebuffer
`FramebufferEmit.h:257` / `PF:1503`, ShaderCso `ProgramEmit.h:411-422` / `PF:1507`, SamplerView `SamplerEmit.h:886-887`
all mint off the object's own lifetime id, so `HandleOf(obj)` on the server always answers the handle the records carry.
The rekey is therefore a REPLACEMENT OF THE LOOKUP ARGUMENT, not a re-keying of the table.
## 2. What the records and applier state already carry
- Handle-indexed record tables, Gen-checked on every read (`MG_Pipe/PipeApply.h:486-524`; readers `MG:4325-4360`
`PipeTextureRecordForHandle` / `PipeRenderbufferRecordForHandle` / `PipeSamplerCsoRecordForHandle` /
`PipeSamplerViewRecordForHandle` / `PipeShaderCsoRecordForHandle` (band-aware), `PushedFramebufferRecord` `MH:1963`,
`FindShaderCsoRecord` `DG:4243`; `PipeApply.cpp:444,481,488,505` `!Live || Gen != handle.Gen -> nullptr`).
- The bound handles: `BoundVertexElements` `:594`, `BoundFramebuffer[2]` `:646`, `BoundSamplerViews[u].Texture` `:673`,
`BoundSamplerStates[u]`, `BoundShaderImages[u].Res`, `DrawProgram/DispatchProgram/BoundShaderCso` `:691-693`,
`VertexBuffers[].Res`, `IndexBuffer.Res`; the verb workspace `VerbBlitReadFbo/DrawFbo`, `VerbCopyTexDst`, `VerbMipRes`,
`VerbIndirect*` `:733-755`; `MGPSurface.Res` in `FramebufferRecords`.
- Twin creation by handle already exists per kind: `GetOrCreate(handle)` `ST:294-342` (forward Gen = recycle & reset `:338`,
backward Gen = refused `:331`), `GetOrCreateByHandle` `MH:407-414`, `AdoptTwinByHandle` `MG:4148-4152`,
`GetOrCreateBufferResourceForHandle` `MG:2887-2908`, `GetOrCreateSamplerViewForHandle` `MG:4505-4528`,
`ResolveSamplerCsoTwin` `MG:13474`, `SyncAndBindFramebufferByHandle` `DG:4677-4712`, CopyTex split arm `DG:8487`.
- Twin death by handle already exists for all seven kinds: `object_death` (opcode 78, `MGPHandleOnly` `MGPipeTypes.h:93-98`,
`PipeCalls.def:265`, `kCtxObject`) emitted by `EmitObjectDeathRecord` `MG_Remote/Client/WireTables.cpp:569-608` (GL thread,
the client's own allocator) -> `ServerVerbSink::OnObjectDeath` `PipeApplier.cpp:988-1013` -> `ReleaseTwinsForWireObjectDeath`
`MG:328-372` -> `ReleaseTwinByHandle` `ST:498-507`; Buffer via `resource_destroy` -> `Ops_H_Destroy` `MG:2424-2444`
`ReleaseByHandle`. Order on the client is fixed (`PF:1623-1626` `NotifyAndFree`: notice -> record out -> `Free`), so the
death record precedes the slot's next handout on the ring. Under `BatchWaits=1` (default, `Config.h:493`) `object_death`,
every `create_*/delete_*` (kCtxCso) and every `set_*` (kCtxState) already publish WITHOUT waiting
(`ClientSession.cpp:915-920`); `EmitAndWait` at `WireTables.cpp:604` is a name only.
- Client-side lifetime-id -> handle maps that STAY client: `MGPipeSlotAllocator::ByLifetimeId` `SA.h:146`,
`MGPipeResourceTracker::m_bySlot` `ResourceTracker.h:535` (slot -> `BufferObject*` inverse for writebacks), the emitters'
latches (`VertexInputEmit.h:174`, `SamplerEmit.h:889`, `TextureEmit.h:455-470` `ResolveTexture`), `EmitTables.cpp:1076-1082`
`PublishedTextureHandle`. All GL-thread; none is touched by this design.
## 3. The gap (what does not exist yet)
No new wire record and no new field is needed for identity. The gaps are server-side shapes:
1. **Per-kind by-handle resolvers on the draw path** are missing for VAO, Program and Texture-at-draw: `ResolveVaoTwin(handle)`,
`ResolveProgramTwin(handle)`, `ResolveTextureTwin(handle)` (the shape of `ResolveSamplerCsoTwin`). `AdoptTwinByHandle`
is the body; the release-build voice of the refusal is per kind (`MG:2893-2898`'s shape).
2. **The twin sync bodies take the frontend object** (`MG:4881` VAO, `:7049` mips, `:8802` params, `:10044` FBO, `:11922`
program, `:13184/:13188` sampler, `:13550` rb) and resolve their own record through `HandleOf(obj)` (`MG:7089, :12954,
:13574`). Identity's requirement on S1-S5: every body becomes `SyncToBackend(MGPipeHandle, const <Record>&)` and stores
`m_handle` (the `m_pushedSyncHandle` pattern, `MH:1857`) - the object parameter is deleted, not defaulted to null.
3. **The state note** (`NoteStateForHandle` writer `DG:3228`; readers `:4700`, `:8361`) and **`ForEachLive`'s `stateRef`
walk** (`DG:8904`, the one direct iteration) are frontend `SharedPtr`s in server memory. S3 owns the replacement: the
by-handle FBO sync reads `FramebufferRecords[h].State` only; `ScopedDetachedTextureFramebufferAttachments` walks
`FramebufferRecords` for surfaces naming texture handle T (an O(records) walk, or a reverse index texture -> FBO slots
maintained at `set_framebuffer_state`).
4. **Raw-pointer memos**: `g_currentDrawFrontendProgram` (`DG:4097`) -> a handle stash; `g_unitSamplerLookupMemos`
(`DG:4985`) and the sampler identity family (`:4989-5013`, `:5623-5630`, `MG:13264-13280`) are DELETED - a unit whose
`BoundSamplerStates[u]` is null while the frontend holds a sampler is a missing `bind_sampler_states`, which under
run-ahead is `Fatal{UnmigratedPipeInput,"SamplerObject@draw"}` (strict), never a mint (S2 to ratify).
5. **`BackendSlotTable` has no composite band.** `EntryAt(handle.Slot)` `ST:581-584` resizes to the slot; a program-pipeline
composite (slot >= 983040, `SA.cpp:457`, `kMaxHandleSlot = 1<<20` `ST:154`) grows `g_backendProgramObjects` to ~983k
entries (~40 B each). Today reachable through `GetOrCreate(StatePtr)` for a composite program; after the rekey it is the
ordinary path. The table gets a `m_band` vector indexed by `slot - base`, as the allocator (`SA.h:144`) and the applier
(`PipeApply.h:511`) already do.
6. **`MG:9709-9720`** (renderbuffer attach cross-check) lacks the `Transport == Monolith` gate its texture sibling has
(`MG:9628-9632`): a live id probe under transport today. Fixed by the rekey (the check is deleted; `AdoptTwinByHandle`
is the whole identity test) - noted so its disappearance is not mistaken for a lost check.
7. The client-side rule that lets the server never miss: the record naming a handle is ON THE RING BEFORE the verb that
reads it. Already true - `create_*` from the validate point / birth hooks (`PipeMutation.h:996-1184`), `set_*` from the
tracker walk, `object_death` from the destructor - and the applier's Gen check turns any violation into the loud
refusal (`PipeApply.cpp:1740-1750`), not a wrong twin.
## 4. The retirement design (server shape after P5e)
**ST (BackendSlotTable), handle-only under `MOBILEGL_PIPE_PUSH`:** delete `GetOrCreate(const StatePtr&)`, `HandleOf`,
`Find(StateObject*)`, `OnFrontendObjectDestroyed`, `NoteStateForHandle/StateForHandle`, `Entry::stateRef`, the memo pair
and `RememberHandle/ForgetHandle`; keep `GetOrCreate(handle)`, `FindByHandle`, `ReleaseByHandle`, `ReleaseTwinByHandle`,
`LiveGenAt`, the holder list, `ForEachLive` re-typed as `fn(MGPipeHandle, const BackendPtr&)`; add the composite band.
`Entry` becomes `{backend, Gen, Live}`. The `StateObject` template parameter survives only as a name. The pull build
never compiles this header (`ST:14-16, :86`), so G1 holds by construction.
**MH (StateBackendObjectRegistry):** the slot arm forwards only the handle members; the legacy `UnorderedMap<StateObject*>`
arm stays exactly as it is under `MOBILEGL_PIPE_LEGACY_MEMOS` (the A/B of ARCHITECTURE 9.6; `CMakeLists.txt:43` default ON).
`DestroyByLifetimeId` becomes legacy-arm-only; the notice consumer `OnFrontendStateObjectDestroyed` `MG:206-320` keeps the
split arm (`EmitObjectDeathRecord`), keeps the monolith switch for the LEGACY arm, and the slot arm's monolith death is the
same `object_death` route applied synchronously (`MGPipeRouteObjectDeath` under `Transport=monolith` = direct apply) - one
death path per arm, the scope at `MG:256` deleted. The NoSession fixture hop (`MG:224-246`) is deleted: a server-role
fixture with no client session gets no deaths (its twins die with the backend at Stop, which is what happens today past
`Running()==false` anyway).
**Twin key = the record's handle, resolved from applier state, never from an object:**
- VAO: `ResolveVaoTwin(st.BoundVertexElements)` in `PrepareForDraw`; `DrawArrays/MultiDrawArrays` client-side attribute paths
are refused under split already (S1 confirms) - their `Find` becomes the monolith legacy arm only.
- Program: `SyncCurrentProgram(st.DrawProgram)`, `PrepareForCompute(st.DispatchProgram)`, `ResolveGlobalConstantsRecord(handle)`;
`g_currentDrawBackendProgram` keyed by `{slot,gen}`; `GetProgramBackendId(name)` / `SetStorageBlockBinding(name)` are
monolith sticky entries (`DG:6152`, `:10022`) - under split they are unreachable (P5b class-C refused or carried as
`MGPSetStorageBlockBinding.ShaderCso`, `EmitTables.cpp:1267`).
- Texture: `SyncTextureTwin(handle)` called with `BoundSamplerViews[u].Texture`, `BoundShaderImages[u].Res`,
`FramebufferRecords[h].State.Color[i].Res`, `VerbCopyTexDst`, `VerbMipRes`, `MGPCopyImage` src/dst; `SyncMipmapsToBackend`
reads the staged store by handle (tx already does, `CONTRACT-P5C.md §2.2`).
- Renderbuffer: `AdoptTwinByHandle(surface.Res)` (already, `MG:9719`) + by-handle `SyncToBackend(handle, record)`.
- Framebuffer: `GetOrCreateByHandle(BoundFramebuffer[t])` / `record.Fbo` (already); the decline arms (`DG:3303-3370`,
`:4568-4602`) become monolith-legacy-only; `g_fboRecordsTrusted=false` under a transport is `Fatal{ProtocolCorruption}`.
- Sampler: the CSO family only (`ResolveSamplerCsoTwin(st.BoundSamplerStates[u])`); the table is renamed to what it is.
- Buffer: `EnsureBufferResourceForHandle(nullptr, h)` / `IsBufferDrawCleanByHandle(h, res, nullptr)` from every consumer
that holds `h` (S1/S5 name them); `HandleOfBuffer` and `MarkBufferGpuWritten(obj)` become monolith-only, and the split
GPU-written announcement takes the handle (`MarkBufferGpuWrittenByHandle`).
**"Is it clean / does it need re-sync" from server-owned serials:** each twin stores `m_handle` and the record serial(s)
it last consumed (`m_syncedResourceSerial`, `m_syncedParamsSerial`, `m_syncedShaderCsoSerial`, `m_syncedRecordHashes`
already exist: `MH:1400-1410, :1800-1812, :2750, :2820`). `FindByHandle` answering null or a different `Gen` = a different
object = full sync; equal handle + equal serials = clean. No frontend version is read anywhere. (S1-S5 own the per-kind
serial sets; the identity invariant is only: the compare is `{handle, serial}`, never `{pointer, version}`.)
**Twin creation moment:** the first apply that resolves the handle through `GetOrCreate(handle)` - a verb, a bind, an
attach - AFTER the record naming it was applied (the applier refuses a bind/verb naming an unknown or stale handle,
`PipeApply.cpp:2266, :2549, :2625, :2683, :2896`). Creation is lazy, exactly as today; no twin is minted at `create_*`.
**Twin death moment:** the apply of `object_death` / `delete_*` / `resource_destroy` (in ring order, after every verb that
named the generation), or the server backend's own destruction at Stop (`MG:188-212` `InProcessTeardown`). `applier_reset`
does not touch twins or object records (`PipeApply.h:800-832`); `MGPipeApplierReleaseObjectRecords` `:841` is wired to
nothing outside tests and stays so.
**The ABA answer ("recycled slot, new gen"):** `Gen` moves only on reuse (`SA.cpp:410-419`); the client frees a slot only
after its death record went out (`PF:1623-1626`); the ring is in order; so at the server every record naming `{s,g}`
is applied before `object_death{s,g}`, which is applied before `create{s,g+1}`. Even with the death skipped (an object
that never crossed emits none, `WireTables.cpp:595-596`), `GetOrCreate({s,g+1})` finds `Live && Gen != g+1` and resets
(`ST:338`); `FindByHandle({s,g})` after the recycle answers null (`:424`); a backward `{s,g-1}` is refused (`:331`).
No wait is involved and none is added: run-ahead changes nothing here. What the client must NOT do is hold a server-side
`SharedPtr` to a frontend object - after P5e the server holds none (no `stateRef`, no state note, no object rows in
`gPipeInputs` under run-ahead), so the P5d deferred-destroy queue (`PipeMutation.h:957-975`, `PF:999-1046`) loses its
producer; it stays as a guard that logs once if it ever enqueues under run-ahead.
**The two scopes:** `MGPipeFrontendKeyedRegistryScope` is deleted (class, 35 Espryt sites, the `SA.cpp:264` exemption, the
`ResourceTracker.h:723` site with it once S5 carries the handle). `MGPipeRefuseAllocatorFromApplyThread` becomes
`Transport != Monolith && OnApplyThread -> Fatal`, unconditional for DirectGLES. `MGPipeReverseAnnouncementScope` is the
Magma question (part 7).
## 5. Monolith / G1
- Pull build: untouched - every symbol above is under `MOBILEGL_PIPE_PUSH` or `MOBILEGL_BUILD_DISAGGREGATED`; the
`TwinRegistry` alias keeps the two-argument mangled name (`MH:626-634`); G2/G14 unchanged.
- Push build under `Transport=monolith`: the applier is in-process and populated synchronously by the same records
(`MGPipeRoute* == MGPipeApply*` under monolith), so the handle bodies run under monolith too and read
`MGPipeApplier()` state that is current at the verb. Behaviour identical; bytes differ. The selection is the FAMILY BIT
(`XSubsystemEnabled()`, the `0x1fff` mask), not a transport check: bit clear -> the legacy `UnorderedMap` arm (its
`HandleOf` on the GL thread is legal), bit set -> handle arm. Under a transport the bits are caps-required, so only the
handle arm is reachable and a legacy-arm entry from the apply thread is the Fatal. The monolith entry points that reach
the backend WITHOUT a record - `GetProgramBackendId(name)`, `GetTexImage`, `CopyImageSubData`, the sticky forwards -
keep their object-taking monolith bodies and resolve the handle on the GL thread through the emitter-side maps
(`PublishedTextureHandle`'s shape), i.e. the resolution moves to the SAME side the mint lives on.
- If the integrator rules "monolith bytes identical" instead, the alternative is `#if MOBILEGL_BUILD_DISAGGREGATED` +
`Transport != Monolith` around each handle body with the frontend body kept beside it; it keeps 35 sites alive and the
scope classes with them, and is not recommended.
## 6. Red-once
1. Revert `ResolveVaoTwin`'s handle arm (put `Find(vao.get())` back) with the scope gone: the first draw under
`integration-split` aborts `Fatal{RoleViolation, "MGPipeSlots"}` naming `HandleOf` (`SA.cpp:265-272`); the same for
`SyncCurrentProgram` and `SyncTextureTwin`.
2. `RemoteGuards.AnAllocatorProbeInsideAnExemptionScopeOnTheApplyThreadIsAllowed` (`RemoteClientTest.cpp:1881-1895`)
is deleted with the scope; a new `...FromTheApplyThreadIsFatalEvenAtTheOldDebtSites` posts `HandleOf` through
`RunOnApplyThread` and expects the abort. `:1866-1879` and `:1901-1913` stay as the control.
3. ABA under run-ahead: `CtWireScenario.TextureDeathCrossesAndTheRecycledSlotAnswersTheNewObject` /
`FramebufferDeath...` (`CtWireScenario.cpp:120-215`) and `HandleRecycleScenario`'s `aba` arm (`:140-149`) run with the
draw wait removed; skipping the `ReleaseTwinByHandle` in `OnObjectDeath` must render the dead twin's colour (red).
4. Unit: `DirectGLESSlotTable.AGenerationBehindTheLiveTwinIsRefusedRatherThanAdopted` (`SanityTest.cpp:4898`) kept; a new
`ACompositeHandleDoesNotGrowTheOrdinaryTable` pins the band; the cases that drive `GetOrCreate(StatePtr)` / the death
notice (`:3499-3960, :4061, :4241, :4378`) are moved onto the legacy arm or rewritten by handle; `ScopedDirectGLESTextureBindings`
(`:230-262`) keeps its by-value registry copy (holder list unchanged).
5. Strict lane: `integration-split-strict` green for the migrated verbs is the gate that a frontend-keyed resolution cannot
hide behind a memo hit (P5e-3).
## 7. Risks and what S7 could not settle
- **Magma.** `VulkanRenderer.cpp:4554, :4634, :8986` and `ResourceTracker.h:723` are apply-thread allocator touches P7
retires; with the guard unconditional they abort on the first Magma frame under `inproc`. Ruling needed: keep ONE scope,
renamed and compiled for DirectVulkan only (`MagmaP7AllocatorDebtScope`), with the guard's exemption keyed on the
backend kind; Espryt has no exemption. (P5e-4 keeps Magma lockstep either way.)
- **Sampler identity family deletion** changes what a draw does for a unit the record arm does not cover (`DG:5601-5630`):
today it mints a twin off the object; after P5e it is a Fatal under strict. S2 must confirm `bind_sampler_states`
covers `[0, maxTouchedUnit]` (E's caveat A8) before the arm is removed.
- **`ForEachLive` / the detach walk** (`DG:8893-8935`) needs S3's reverse index; without it the by-handle FBO table cannot
answer "which twins attach T" and the walk silently does nothing (the handle-keyed entries are already invisible to
`ForEachLive`, `ST:282-285`).
- **NoSession server-only fixtures** (`MG:224-246`, `CONTRACT-P5C.md §5.2` amendment) lose their death delivery; the
affected fixtures are `ServerLoop` unit cases with no client - to be enumerated by the integrator (grep `RunOnApplyThread`
users in `MG_Test/Wire`).
- **Composite band in the twin table** (part 3.5) is a pre-existing ~40 MB growth on any program pipeline; it becomes
the ordinary path after the rekey, so it is a P5e prerequisite, not a follow-up.
- **Ordering of packages.** Every other family's design calls a by-handle resolver; the ST/MH rekey + the per-kind
resolvers + the band land FIRST (one worktree, no behaviour change under lockstep), then S1-S5 swap their arguments, then
S6 removes the wait. The guard flip (delete the scope) is the LAST commit of the identity package, because it is the
red-once for every site that was missed.
- Not verified: whether any `MGPipeApplier()` bound handle can be null at a verb that today resolved a non-null frontend
object (a family bit clear under transport). The caps-required-bits rule (P5e-4) is what makes that unreachable; S6 to pin.