Files
MobileGL/docs/Disaggregated/notes/p5c/p5c-audit-v1.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

42 KiB
Raw Blame History

p5c-audit-v1 — where the inproc split still reads the other role's memory

Read-only static audit of feat/disaggregated @ a79a0af69de8c6c53ef985dd23be7eab8cd17641 (git rev-parse HEAD, 2026-09-17). No builds, no tests, no git mutation, no adb. Every file:line below was re-resolved on this head (the contracts' line numbers are from older heads and drifted; §5 lists the drift found while auditing).

Direction legend: S→C read = server/apply-thread reads client memory · S→C write · C→S read · C→S write · shared = process global both roles touch.

Frequency shorthand: draw = per draw/dispatch, upload = per upload/respecify, verb = per emitted record, rare = per EGL call / make-current / bring-up, once.

Phases: P5c = worth fixing now, inproc, before P6 · P6 = spawn transport must replace it · P7/P8/P9/P10 = the phase the field tables / ROADMAP already assign.


1. Inventory

1a. Textures and texture sync — the largest un-instrumented family

# dir what memory site (function) owner when why it works today breaks under spawn guard natural carrier phase
1 S→C read client MipmapStorage texel bytes MG_Backend/DirectGLES/Managers.cpp:7315 (BackendTextureObject::SyncMipmapsToBackend), also :6940, :7129, :7198 — textureMipmapObject->MapMipmapData(...) client TextureObjectMipmap per level needing upload, per validate/draw verb barrier parks GL thread; same address space pointer into other process; staged SEG_STAGE bytes exist but are dropped none — not in FieldOwnership.def, invisible to rsp/strict the already-staged resource_subdata SEG_STAGE run, adopted into a server-owned level shadow (mirror of buffer StagedShadowStore) P5c now — the bytes already cross; ApplyTextureUpload just has to keep them. Biggest single finding
2 S→C read frontend texture-object shape/state (GetMipmapTexelSize, GetTarget, GetFormat, GetMipmapByteSize, GetUploadTargets) Managers.cpp:7314, :7332, :6878-6880 (same fn); DirectGLES.cpp:8528-8529, :8502-8507 (mip gen) client ITextureObject per upload / per mip gen barrier method calls on a dead pointer none (only the MGB_STORAGE_* macros are descriptor-routed, Managers.cpp:6643-6661) applier MGPipeResourceRecord::Desc (already server-side) P5c — same fix as #1: the descriptor + staged bytes answer all of these
3 S→C read client dirty-region set Managers.cpp:7360, :7371 (GetStorageDirtyRegion, legacy arm when no pendingUpload) client per level upload barrier dead pointer none PendingUpload::UnionBox/regions already on the wire (PipeApply.cpp:824-849) P5c — falls out of #1
4 S→C read client slot allocator + frontend GetLifetimeId() (mip-descriptor registry lookup, named inproc dependency #2) DirectGLES.cpp:8087 (EnsureGenerateMipmapStorageAllocated re-derived arm) → SlotTables.h:392-400 HandleOf → MGPipeSlots().FindByLifetimeId; also Managers.cpp:5627, DirectGLES.cpp:8185, :8564, :8659, :6706 client MGPipeSlots() per GenerateMipmap / CopyTex / re-mint barrier; Find-not-mint so no allocator mutation client allocator does not exist server-side none (MEASUREMENTS.md:378 calls it barrier debt) MGPMipPlan::Res / MGPCopyFromFramebuffer::Dst handles already on the records → GetOrCreate(MGPipeHandle) (SlotTables.h:278) P5c now — handle is already on the record; the lookup is redundant even inproc
5 S→C write client level shadows re-dirtied so server can re-read texels it never held Managers.cpp:5638-5645 (RequireImageBindableStorage, MarkStorageDirty(...,true)) then Fatal at :5650 client MipmapStorage per texture re-mint (texture-remint-pull) barrier dead pointer, then the Fatal anyway Fatal{UnmigratedEmulation,"texture-remint-pull"} (PipeApply.cpp:2947) — reached: iris-photon/derivative/create-indirect first blocker server-owned level shadow (#1) removes the need to re-pull P8 per ROADMAP; unblocked by #1
6 S→C read every live frontend FBO's attachment list DirectGLES.cpp:8177-8256 (ScopedDetachedTextureFramebufferAttachments: ForEachLive → stateFBO->GetAllAttachmentObjects() :8207-8243) client FramebufferObjects per GenerateMipmap/Blit/CopyTexImage2D/SubImage2D barrier walk over another process's objects none server-side FBO twin attachment table (P3b/P4b) P3b/P4b — keyed on the twin tables, not a wire question
7 S→C read client texture-unit array walk + GetBoundObject() DirectGLES.cpp:2204-2215 (SyncNeccessaryTextures); keys from CaptureDrawTextureSyncKeys :2021-2040 client GLContext units per draw on memo miss barrier dead pointer rsp-counted only for the MGB_CTX accessor half (GetMaxTouchedTextureUnit etc.) server-side binding table from set_sampler_views/set_texture_bindings (P3b/P4b) P3b/P4b per FieldOwnership.def
8 shared raw pointers into client slot arrays held across calls DirectGLES.cpp:1958-1987 (g_unitTextureSyncList/g_fboTextureSyncList/g_readFboTextureSyncList: const SharedPtr<ITextureObject>* slot) client arrays, server-held persistent, read per draw (:2119-2128, :2183-2193, :2270-2279) barrier + PairingsIntact memo keys stored cross-process pointers memo keys only lifetime-id keyed entries (the handle-arm shape already used by UnitBindingsSnapshot) P3b/P4b (ROADMAP.md:30 names the re-keying)
9 S→C read client read/draw FBO binding slots during named blit (named inproc dependency #1) client: MG_Remote/Client/EmitTables.cpp:741-762 (ScopedBlitBindings), emit :764-786; server: DirectGLES.cpp:7782-7785 (BlitFramebuffer → GetFramebufferBindingSlot(...).GetBoundObject()) via sink PipeApplier.cpp:228-244 client GLContext binding slots per BlitNamedFramebuffer client rebinds its own shadow, barrier holds it, RAII restores server reads a binding that was never told to it rsp (GetFramebufferBindingSlot, BARRIER_PULLED); strict knob MGPBlit.ReadFbo/DrawFbo handles already on the record → server handle-keyed FBO resolution P6 must (docs say so); P5c can already switch the sink to the handles since they cross today
10 S→C read client bound texture via binding slot (CopyTex paths) DirectGLES.cpp:8561-8563, :8656-8658 (CopyTexImage2D/CopyTexSubImage2D → GetTextureUnitObject(...).GetBindingSlot(...).GetBoundObject()) client unit array per CopyTex call barrier dead pointer rsp (BARRIER_PULLED GetTextureUnitObject) MGPCopyFromFramebuffer::Dst handle on the record P5c — same shape as #9, handle already crosses
11 — CopyTex-copied texels land only in the ES driver; client shadow left allocated-but-empty DirectGLES.cpp:8590, :8621, :8681; frontend GL_Texture.cpp:4542 (TexImage2D_State(..., nullptr)) — per CopyTex — wrong readback data later bounded by GetTexImage/texture-remint-pull Fatals (22 of the 27 inproc wrong answers are this class, CURRENT_STAGE_PROGRESS §5) server→client texture writeback (P9) P9
12 — copy-image-shadow-mirror skipped under split; destination client shadow goes stale DirectGLES.cpp:9042 (early return) — :9050 Fatal is unreachable under transport; body :9053-9099 is monolith-only — per CopyImageSubData with CPU shadows — — by design (CONTRACT-P5B §6.8), bounded by two Fatals client-side mirror (ROADMAP P8) P8
13 S→C read get-tex-image-shadow body reads client shadow DirectGLES.cpp:10676, :10684 client not reached under split — client refuses first: EmitTables.cpp:1581-1582, :1614-1615 Fatal{UnmigratedVerb,"GetTexImage"} — — named refusal ResourceReadback/readback carrier P9
14 S→C write Magma mip-gen writes client texture level storage directly DirectVulkan/Renderer/VulkanRenderer.cpp:1562-1590, called :11317 client level shadows per glGenerateMipmap on Magma server barrier dead pointer none (DirectGLES twin is the re-derived #4 arm; Magma has no equivalent) OnMipLevelsGenerated (declared, never installed) or server-derived levels from Desc.Levels P5c — Magma half of the GLES fix that already landed
15 S→C read Magma texture-object lookups via sticky forward / registry DirectVulkan/Renderer/VkTextureManager.cpp:810, UniformManager.cpp:1190, :2014, VulkanRenderer.cpp:11527; DirectVulkan.cpp:271, :274 client objects per draw/dispatch on Magma barrier dead pointer rsp sticky-forward counting server-side texture handle table P7 per FieldOwnership.def
16 — OnTexturePullRequest declared, never installed/called; ResourceSubDataComplete terminator unimplemented MG_Pipe/MGPipeCallbacks.h:34-38; install sites only cover 2 of 10 callbacks (ResourceTracker.h:606-613); thunk generated/PipeThunks.inc:216 — never — the pull protocol does not exist none itself — it is the designed carrier P8/P9 (ROADMAP.md:33,63)
17 S→C write Fatal{UnmigratedEmulation,"generate-mipmap-cpu-fallback"} body would filter/re-dirty client level shadows DirectGLES.cpp:8734 (Fatal), body :8736-8789 client per RGB16F/RGB32F mip gen — reached (open item, CURRENT_STAGE_PROGRESS:59) — — named Fatal needs OnTextureWriteback (P8/P9) P8/P9

1b. Buffers, persistent maps, XFB

# dir what memory site (function) owner when why it works today breaks under spawn guard natural carrier phase
18 S→C read frontend BufferObject::HasDefinedContent() Managers.cpp:3147-3148 (EnsureBufferResourceForHandle) client BufferObject every draw that ensures a buffer barrier dead pointer none — declared debt at :3141-3146 live-content bit beside MGPResourceDesc ("client publishes a live content flag") P5c now — cheapest per-draw kill on the list
19 S→C read frontend GetLifetimeId() per ensure Managers.cpp:2705-2716 (HandleOfBuffer) → SlotTables.h:392-397 client object per ensure / VAO memo rebuild / SSBO mark barrier; memoised dead pointer + client allocator none handles on the records P3b/P4b
20 shared raw BufferObject* held in server per-VAO memo Managers.h:1185 (ResolvedDrawBuffers::Entry::frontend), written DirectGLES.cpp:716 client object, server-held stored once per VAO config; read on memo hit client SharedPtr pins it stored cross-process pointer only reader under split is monolith-gated (IsBufferDrawCleanByHandle → IsMapped(), Managers.cpp:2914-2916) handle-keyed memo entry P3b/P4b
21 shared SharedPtr<BufferObject> held by server for a whole XFB span DirectGLES.cpp:950 (XfbCaptureTarget::buffer), pushed :1260, read :1060,:1077,:1172,:1185 client object, server-held per XFB span GL forbids rebinding mid-span; barrier strong ref into another process none server buffer twin handle P9 (XFB scatter)
22 S→C write client buffer shadow written at XFB end DirectGLES.cpp:1077 (ReadbackCapturedRanges → WritebackFromBackend), epoch bump :1081 client BufferObject per EndTransformFeedback barrier dead pointer none OnBufferWriteback-style event / readback carrier P9
23 S→C read+write client shadow read-modify-write in XFB scatter DirectGLES.cpp:1172 (ScatterCapturedRecords: target.buffer->MappedData() + target.start), :1185 client BufferObject per XFB scatter barrier dead pointer none — no transport guard at all OnXfbScatterReady (declared MGPipeCallbacks.h:50, never implemented) P9
24 S→C read XFB six fields (GetTransformFeedbackProgram → client ProgramObject dereferenced :1248-1340; binding points :1250; counters :1135) DirectGLES.cpp:1224-1343 (StartPendingTransformFeedback, from PrepareForDraw :4545) client every draw barrier dead pointers rsp BARRIER_PULLED (6 rows, FieldOwnership.def:119-130) server-side XFB span state P3b/P4b, P7 per table
25 — MGPDrawInfo::XfbCpuCapturedVertices / kDrawHasXfbCount written 0 at EmitTables.cpp:402, :1873; read by nobody — — — — dead field was meant to carry the accounting (CONTRACT-P5B:163-164) P9 or delete
26 S→C read legacy buffer arm reads client freely (MappedData(), IsMapped(), GetChangeSerial()) Managers.cpp:1012-1022, :1119-1121, :1901, :2944-2949, :3281-3327, :5239, :5274, :1740-1917 client reachable in a split build whenever MOBILEGL_PIPE_PUSH bit 7 is clear same process everything none — only an MGLOG_D names the arm (Managers.cpp:2588-2619) n/a — arm should be refused under split like Fatal{PipeLegacyMemosDisabled} P5c — one transport check at the arm resolver
27 — hostBytes rule-C violation — fixed on this head: server-owned copy under split Managers.cpp:2165 (Ops_H_SubData), :2225 (Ops_H_FlushRange), store StagedShadow.h:66-82 server per upload/flush — — Fatal{StageSnapshotTooNarrow} six named sites + 0xDD audit wire-clean done

1c. Reverse channel: callbacks, SEG_EVENT, error/surface/caps writes

# dir what memory site (function) owner when why it works today breaks under spawn guard natural carrier phase
28 S→C write client buffer CPU shadow bytes, via a raw host pointer producer Managers.cpp:2339-2343 (Ops_H_Readback: MGPBlobRef{(Uint64)mapped, size, kMGHostSpanSegNone, 0}); consumer MG_Impl/Pipe/ResourceTracker.h:553-572 (MGPipeClientOnBufferWriteback casts Offset back to a pointer :569) → BufferObject.cpp:492-511 client BufferObject per blocking SyncGpuWrites/readback same address space + barrier dangling foreign pointer; and the consumer rejects any real segment (:560-564) none on the pointer SEG_EVENT kEventBufferWriteback — consumer half exists (ClientSession.cpp:188-213), no producer anywhere P5c now — wire the producer; fix the consumer's segment arm in the same commit
29 S→C write client m_hasDefinedContent/m_gpuWritePending via direct callback into client tracker producer Managers.cpp:2729-2752 (MarkBufferGpuWritten); consumer ResourceTracker.h:579-601 → BufferObject::MarkGpuWritten :514-517; backend feeders DirectGLES.cpp:568, :616, :2601 client BufferObject per draw binding SSBO/atomic/writable-image same address space call into the other process MGLOG_E_ONCE only (:2729-2739, :596) SEG_EVENT kEventGpuWritten — consumer exists (ClientSession.cpp:227-236), no producer P5c now — same producer wiring as #28
30 S→C write Magma MarkGpuWritten writes client objects directly, bypassing the callback DirectVulkan/Renderer/UniformManager.cpp:1075, :1231; VulkanRenderer.cpp:11618 client BufferObject per Magma draw with SSBO/texel-buffer/XFB same address space direct foreign write none (Magma registers no MGPipeResourceOps, so it is outside the handle arm) same kEventGpuWritten P5c — route through the same callback/event as Espryt
31 S→C write client GLContext error queue MG_Impl/Pipe/PipeFill.cpp:1764-1772 (PipeInputs::RecordError sticky forward → ctx->RecordError); producers DirectGLES.cpp:8155, Managers.cpp:13128, DirectVulkan.cpp:717, VulkanRenderer.cpp:1339,:1343,:1399 client GLContext per backend-detected GL error barrier + sticky-forward accounting write into the other process MGP_STICKY_FORWARD_PULL → rsp; Fatal under MOBILEGL_IPC_STRICT_ERRORS OnGlError event (declared, never installed); ordering is P9's P9 per table
32 S→C write client compile env / advertised extensions PipeFill.cpp:1753-1756 (InvalidateCompileEnv forward); producers DirectVulkan/BackendObject_DirectVulkan.cpp:390, :788 (Magma only) client GLContext per backend caps re-run barrier; no-op outside a stamped verb (PipeInputs.h:265-278) — silent outside it write into the other process sticky pull counting R-12 re-arriving caps snapshot — client half already landed (CapsMirror.cpp:78-80) P5c — delete the forward; the snapshot already does the job on the client
33 S→C write client default-framebuffer attachments (pDefaultFramebufferInfo depth/stencil/colour) DirectGLES.cpp:11519-11620 (PublishDefaultFramebufferDepthStencilFormat, from surface creation :11885, :11902); Magma twin DirectVulkan/Renderer/SwapchainObject.cpp:276-335 client MG_Impl placeholder per surface/swapchain (re)create barrier; rare write into the other process none SEG_EVENT kEventSurfaceChanged — consumer stub exists (ClientSession.cpp:242-253), no producer, no handler installed P6 — surface events must cross anyway; P5c can add the producer
34 S→C write server's apply-thread-inside flag lives in client TU statics PipeApplier.cpp:934 (ScopedApplierEntry) → ClientSession.cpp:884-889 writes g_applyThreadInsideApplier (:113); read by client ClientSession.cpp:748 (Fatal{BarrierViolation}) client statics, server-written per applied record same process write into the other process none (it is the handshake) session-local flag under spawn; fine to keep inproc P6
35 — SEG_EVENT ring: created, mapped, announced, consumer drained after every reply verb — zero producers segments ServerSession.cpp:285-301; consumer ClientSession.cpp:176-267, called :836; producer API ServerSession::PublishEvents :484-492 has no callers — — — the ring exists but carries nothing overflow latch + eventDropped counter built, unused itself P5c — rows 28/29/33 are its first three producers

1d. Caps, format tables, program artifacts

# dir what memory site (function) owner when why it works today breaks under spawn guard natural carrier phase
36 S→C read client caps mirror's FormatCapabilityCache MG_Backend/DirectGLES/Utils.cpp:50 (ActiveBackendFormatCaps fallback arm when ServerLoopInstance().Backend() is null — bring-up/teardown window); main arm :46-48 is server-routed client BackendObject_Remote rare (server backend null) values are a copy of the server's own caps read of client mirror null test only server's own Backend()->GetFormatCapabilities(); refuse when null P5c — one-line arm tightening; pinned by ServerLoopTest.cpp:848-868
37 S→C read client mirror's DynamicBackendParameters copy, four Magma sites, unrouted DirectVulkan/DirectVulkan.cpp:713-715 (MaxShaderStorageBufferBindings); VulkanRenderer.cpp:667-676 (line-width range, per state change); VertexInputStateFactory.cpp:249-250, :502-503 (SupportsFloat64VertexAttributes, per pipeline build) client BackendObject_Remote per draw/pipeline build on Magma mirror holds a copy of the snapshot read of client mirror none — no #if, no Fatal, no ServerLoopInstance() consult server's own backend GetDynamicParameters() (same values, same process role) P5c now — mechanical re-pointing; Espryt already shows the shape
38 — GetCaps record's two blobrefs — record is dead; the snapshot carries the data instead decoder declines PipeWireCodec.cpp:1493-1506; no client emitter exists; snapshot path ServerSession.cpp:366-408 → ClientSession.cpp:275-330 → CapsMirror.cpp:55-87 (blobrefs cleared :63-64) — — — — open item (ROADMAP.md:58) the CapsSnapshot control frame (already the carrier) P6 (delete or wire the record)
39 — CreateShaderState program archive — wire-clean: one SEG_STAGE blob, server decodes into apply-locals emit MG_Remote/Client/WireTables.cpp:470-502 (Fatal{ArtefactsMissing} :478-485, Fatal{ArchiveEmpty} :491-495, six Spirv[] refs zeroed :496-497); decode PipeWireCodec.cpp:1676-1714; applier stores descriptor only (PipeApply.cpp:2780-2784) — per program link — — both Fatals are client-side pre-emission SEG_STAGE (in use) done

1e. Process globals and cross-role control

# dir what memory site (function) owner when why it works today breaks under spawn guard natural carrier phase
40 shared gPipeInputs (~20 KB, 63 fields + 7 sticky forwards): client residual fill writes, applier mirrors write, server BARRIER-PULLED reads (36 rows) definition MG_Backend/MGPipe/PipeInputs.h:806; fill PipeFill.cpp:2798 (MGPipeValidateForVerb); applier writes PipeApply.cpp:1415-1560; server stamp PipeInputs.cpp:103-132; rsp increment PipeInputs.cpp:72 co-owned under the barrier every verb only the verb barrier makes one writer at a time the whole block rsp + MOBILEGL_IPC_STRICT_ERRORS Fatal (PipeInputs.cpp:61-77) + poison (generated/PipeFilled.inc:408-422) per-field rows: P3b/P4b (12), P7/P8 (objects), P9 (writes) — FieldOwnership.def per table
41 C→S write server g_applier wholesale reset from the GL thread PipeFill.cpp:2654-2656 (MGPipeValidateForVerb FreshlyPrimed arm → MGPipeApplierReset(), body PipeApply.cpp:1202-1299) server per freshly-primed context / make-current edge barrier post-condition: nothing in flight at a validate; reset advances serials GL thread writes server state; no record names it none — no role check, no barrier check a reset control record / mailbox frame at the make-current edge (R-12 edge already exists) P6 must; P5c can add the control record
42 S→C read+write client slot allocator MGPipeSlots() driven from the server backend: Acquire (mint!), FindByLifetimeId, Free MG_Backend/DirectGLES/SlotTables.h:232 (GetOrCreate), :397 (HandleOf), :418, :428 (OnFrontendObjectDestroyed); call sites Managers.cpp:2708, :4293 client allocator per twin creation/death/probe barrier server minting into a client allocator none (MOBILEGL_ASSERT at :233 inert at INFO); self-declared at SlotTables.h:70-77 GetOrCreate(MGPipeHandle) overload (SlotTables.h:278) — the handle already arrives in verb payloads P3b/P4b, P7
43 shared eight Espryt twin registries keyed by frontend object address, holding weak/raw refs Managers.h:309-377 (StateBackendObjectRegistry), SlotTables.h:138-161 (BackendSlotTable), eight globals Managers.h:858,:1385,:1780,:1897,:2494,:2632,:2718,:2773; legacy memos DirectGLES.cpp:95-140 client-keyed per object lookup barrier; weak refs expire cleanly keys are foreign addresses arm resolver Fatal{PipeLegacyMemosDisabled} only when bit 5 clear AND LEGACY_MEMOS=0 lifetime-id/handle keying (handle arm already the default) P3b/P4b
44 shared pDefaultFramebufferInfo — frontend default-FBO placeholder, read and mutated by the server storage MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp:3457; server readers DirectGLES.cpp:2846,:3137,:4370,:4414; writer :11520; Magma SwapchainObject.cpp:276 client MG_Impl per FBO compare / surface event barrier dead pointer none reserved default-FBO handle {0,1} + IsDefault bit (D-C1, DirectGLES.cpp:2838-2849) P3b/P4b (D-C1)
45 shared g_rawDepthFetchSamplerState — server constructs, owns and mutates a frontend SamplerObject DirectGLES.cpp:61-62, created :269-274, used :277 server-held frontend-type object per raw depth fetch same process a client-type object server-side none; self-declared "largest surviving MG_State-type usage under MG_Backend" sampler CSO record fields the applier already reads P3b/P4b (comment assigns it)
46 shared ScopedDefaultUnpackState::s_synced + six value shadows, never reset on context death Managers.cpp:5806-5812, writes :5773,:5790-5795; callers :6925,:7120,:7183,:7287 server-private memo per texture upload server-private today (client runs no GL) latent: a server re-attaching to a moved context skips an owed glPixelStorei none reset keyed on the context-generation edge P6 — register it now (CONTRACT-P5 table 3 already says so)
47 — g_syncedRenderStateParameters — server-private driver-state memo DirectGLES.cpp:3185, writes :3274-3878, reads :6895,:7061,:7630,:7946 server per render-state sync server-exclusive not context-generation keyed (latent, same class as #46) none — P6 housekeeping
48 — gBackendFunctionsTable — client = emit table, server holds its own table privately client install MG_Backend/Init.cpp:63 via BackendObject_Remote::GetBackendFunctions() (BackendObject_Remote.cpp:162); server PipeApplier.cpp:58-72 — — — — re-emission loop documented PipeApplier.h:94-98 wire-clean done
49 — gMGPipeSegmentResolver — server installs, client never resolves; only production resolve is the user-index span install PipeWireCodec.cpp:425-437 (Fatal on double install), ServerSession.cpp:336; resolve PipeApplier.cpp:559; client writes Ptr=nullptr EmitTables.cpp:503; honesty gate PipeWireCodec.cpp:521-536 — per user-index draw — — Fatal{ProtocolCorruption} four arms wire-clean done
50 C→S read ServerLoop::m_running/m_applyThreadId read per routed record MG_Remote/Client/WireTables.cpp:71 (RunsAsTheServerRole), PersistentMapTracker.cpp:38, BufferObject.cpp:385,:410,:440, PipeFill.cpp:757,:877 server singleton per verb (hottest C→S read) same-process atomics singleton absent in the client process none a client-local role flag set at session start P6 — trivially local; no carrier needed
51 C→S ctrl twelve Server* EGL forwarders → server's private backend via the one-slot control mailbox; payload is a raw fn-ptr + void* to a caller stack local callers BackendObject_Remote.cpp:125-264; mailbox ServerLoop.h:216-226; mechanism ServerLoop.cpp:543-598 server backend rare (per EGL call) blocking; same address space unserializable by construction Fatal{ApplyThreadNotRunning} (ServerLoop.cpp:568) transport control messages (P6 SocketTransport control plane) P6 must
52 C→S write FBO/object death posted as a stack struct host pointer into the mailbox (named inproc dependency #3) Managers.cpp:206-221 (OnFrontendStateObjectDestroyed split arm) → apply-thread DestroyByLifetimeId :223-228 server twins per frontend object death (6 kinds) blocking; same address space no framebuffer delete opcode exists ("Death notices have no framebuffer wire opcode", :209) none on the carrier a framebuffer_destroy/object-death wire record P6 must; P5c can add the record
53 C→S read ServerLoop::Backend() read on the app thread at bring-up (kCapBackendOwnsXfbCapture derivation, SetBackend) MG_Backend/Init.cpp:164-170 server backend once same process direct read #if MOBILEGL_BUILD_DISAGGREGATED the caps snapshot already carries the answer (cap bits) P6 — read it from the snapshot
54 C→S read MGPipeGetResourceOps() at bootstrap honesty checks MG_Backend/Init.cpp:100,:145,:247 (AssertConsumerMaskIsHonest, Fatal{ConsumerMaskLie}) server-registered table once ×3 same process — honesty check only consumer bits in CallMask (already the wire form) P6 — keep as a check against the snapshot
55 C→S read Windows inproc attach aliases the server's ShmSegment objects (m_owns=false); client ring/reply/event views are the server's members MG_Remote/Transport/ShmSegment.cpp:352-359; client reads ClientSession.cpp:454,:459,:477,:502-506 server mappings once same process — none (POSIX does a real dup+Map) SCM_RIGHTS/fd adopt (P6) P6 by design; note the Windows lane never exercises the POSIX attach path
56 — barrier client-half assertion gap: InBarrierWait() has zero call sites; only ApplyThreadIsInsideApplier is enforced (at emit) declaration ClientSession.h:128, definition ClientSession.cpp:880; enforced half :748-753 — — — client gPipeInputs writes outside a barrier are unchecked (CONTRACT-P5 §4 claims the check exists) — finish the assertion or delete the claim P5c — cheap, catches #40-class regressions

1f. Test-only cross-role access (listed separately, per the brief)

# dir what site
57 C→S read MGPipeApplier() (server g_applier) records/windows MG_IntegrationTest/Harness/PipeApplyPeek.cpp:79,:176; P4aSeamPeek.cpp:52,:63; P4aFinalFixPeek.cpp:30
58 C→S write ServerLoopInstance().SetBeforeRetireHookForTesting(...) MG_IntegrationTest/Harness/SplitRuntimePeek.cpp:31
59 client-local pActiveBackendObject (client mirror), MGPipeSlots(), PersistentMapTracker BackendCapsPeek.cpp:29, PipeSlotPeek.cpp, PersistentMapPeek.cpp:62,:76 — no cross-role read

2. Summary counts

By direction (58 inventory rows, #57–59 counted once as test-only):

direction rows the weight
S→C read 19 texture sync (#1–#8, #10, #15, #19), XFB pulls (#24), legacy arm (#26), caps mirrors (#36, #37), slot allocator probes (#42, #43)
S→C write 12 reverse channel (#28–#34), re-dirty (#5), XFB writebacks (#22, #23), Magma mip (#14), surface (#33, #44-adjacent)
C→S read 6 role flags (#50), bring-up (#53, #54), shm alias (#55)
C→S write 3 applier reset (#41), death mailbox (#52), test hook (#58)
C→S ctrl 1 EGL forwarders (#51)
shared global 8 gPipeInputs (#40), twin registries (#43), memo pointers (#8, #20, #21), pDefaultFramebufferInfo (#44), depth-fetch sampler (#45), unpack shadows (#46)
wire-clean / dead / latent confirmations 9 #11, #12, #13, #16, #25, #27, #38, #39, #47, #48, #49

By natural carrier:

carrier rows
server-owned level shadow / adopt the already-staged SEG_STAGE run (texture) #1, #2, #3, #5
SEG_EVENT (producer exists in shape; consumer live) #28, #29, #30, #31, #33 (and #16, #23 designed-for)
handles already on the record → server-side resolution #4, #9, #10, #19, #20, #42, #43, #44
server's own backend / caps snapshot (no new carrier) #32, #36, #37, #53, #54
new/extended control record (reset, death, EGL control plane) #41, #51, #52
applier descriptor / server-derived — no carrier #6, #7, #24, #45
client-local role fact — no carrier #50
P3b/P4b twin-table re-keying (not a wire question) #8, #20, #21, #43

Top 10 by frequency (per-draw first):

rank row site frequency
1 #40 gPipeInputs BARRIER-PULLED reads (GetBoundVertexArray DirectGLES.cpp:4486, GetProgramForDraw :4497 unconditional per draw) per draw
2 #24 XFB six pulls in StartPendingTransformFeedback (DirectGLES.cpp:1224-1343) per draw
3 #18 HasDefinedContent() (Managers.cpp:3148) per draw that ensures a buffer
4 #29 OnGpuWritten direct callback (Managers.cpp:2752) per draw with SSBO/atomic/writable image
5 #50 RunsAsTheServerRole() (WireTables.cpp:71) per routed record
6 #1 client MipmapStorage texel reads (Managers.cpp:7315 etc.) per uploaded level, per validate
7 #7/#8 texture-unit sync walk + sync-list replay (DirectGLES.cpp:2204-2215, :2119-2279) per draw on memo miss / per draw
8 #19 HandleOfBuffer → GetLifetimeId() (Managers.cpp:2705-2716) per ensure / memo rebuild
9 #37 Magma SupportsFloat64VertexAttributes (VertexInputStateFactory.cpp:249,:502) per pipeline build
10 #6 FBO attachment walk (DirectGLES.cpp:8177-8256) per blit/mip/copy

3. Already wire-clean (verified on this head)

  • Buffers, whole family: all ten Ops_H_* handlers take handle + record + SEG_STAGE bytes only; StagedShadowStore::Adopt copies on apply (Managers.cpp:2165, :2225); Fatal{StageSnapshotTooNarrow} six named sites (StagedShadow.h:106); hostBytes is server-owned under split (rule C's one named violation is fixed). resource_subdata declares a real size, cross-checked (PipeApply.cpp:715). resource_flush_range carries no bytes, transport-gated (PipeApply.cpp:1885-1888). buffer_subdata_resident copies, never retains (Managers.cpp:2199-2207).
  • resource_respecify: initialBytes == nullptr under split; Fatal{UncarriedInitialBytes} (WireTables.cpp:367-374) / Fatal{InitialBytesNotCarried} (PipeFill.cpp:768-775) guard both directions.
  • Persistent maps: tier-2 emulated push goes block-wise through SEG_STAGE (PersistentMapTracker.cpp:76-127 → BufferObject.cpp:404-434 → WireTables.cpp:314-330); Fatal{RoleViolation} bars the apply thread from the producer; tiers 0/1 are named Fatals; liveHostBase() cannot fall back to MappedData() under a transport (Managers.cpp:2986-2988, ID-50/52).
  • CreateShaderState: archive crosses as one SEG_STAGE blob; server decodes into apply-locals; six Spirv[] refs zeroed and Fatal if declared (#39).
  • Caps: FormatCapabilities + RendererInfo cross as serialized blobs in the CapsSnapshot control frame (ServerSession.cpp:366-408 → CapsMirror.cpp:55-87); DirectGLES format-cap lookups are routed to the server's backend (Utils.cpp:40-51 main arm, BackendObject_DirectGLES.cpp:866-874).
  • ReadPixels: tight wire form, neutral pack server-side, scatter client-side; oversized reads refused at the client before emission (ReplySlot.h:191-207); PACK-PBO destination named-refused (ID-57).
  • DrawVbo user indices: staged into SEG_STAGE, Ptr=nullptr, four honesty arms, sink resolves via MGPipeHostBytes (PipeApplier.cpp:559, PipeWireCodec.cpp:521-536).
  • Fence/sync family: client mints {slot,gen}, only the handle crosses; native fences live on the apply thread (wave-3, CONTRACT-P5B §9).
  • Persistent-map / XFB counters: MGPDrawInfo::XfbCpuCapturedVertices has no reader, so its deadness leaks nothing.
  • Roles: gMGPipeSegmentResolver server-only with double-install Fatal; gBackendFunctionsTable split correctly; the three MGPipeGetResourceOps() liveness reads in PipeFill.cpp:642/655/1069 short-circuit to CapsMirror::ServerConsumes under a transport; no MG_Remote/Server or MG_Pipe TU reads any MG_Impl/Pipe singleton except the SlotTables.h backend arm (#42).

4. Surprises / disagreements with the contracts and ARCHITECTURE.md §17

  1. The texture texel path is the real rule-C violation, and no table names it. CONTRACT-P5 §0 calls hostBytes "the tree's exactly one violation" — fixed on this head. But texture uploads stage the level into SEG_STAGE and ApplyTextureUpload then drops the pointer ("DISPATCHES TO NOBODY", PipeApply.cpp:989-1008); Espryt re-reads the texels from the client MipmapStorage (Managers.cpp:7315). Every texture-object method the server calls on a client object has no row in FieldOwnership.def, so the whole family is invisible to rsp and to MOBILEGL_IPC_STRICT_ERRORS, and MOBILEGL_IPC_AUDIT=1's 0xDD fill cannot observe it (textures never adopt staged bytes). rsp is a lower bound by a wide margin — exactly as r2-v1.md:79 warned, but the unnamed set is larger than "sticky/non-verb forwards": it is the texture hot path.
  2. SEG_EVENT is fully plumbed and silent. Segments created, mapped, announced in Welcome, consumer drained after every reply verb (ClientSession.cpp:836) — and zero producers repo-wide. Worse: the one live writeback producer bypasses the ring with a raw host pointer (Managers.cpp:2339-2343), and the installed consumer would reject the ring's own blobref convention (ResourceTracker.h:560-564 rejects any real Seg). A producer added tomorrow would be dropped, not delivered.
  3. 8 of 10 MGPipeCallbacks are never installed — only OnBufferWriteback/OnGpuWritten have assignment sites (ResourceTracker.h:606-613), and both are invoked in-line on the apply thread straight into client MG_State. OnGlError, OnSurfaceChanged, OnCapsInvalidated, OnMipLevelsGenerated, OnXfbScatterReady, OnTexturePullRequest, OnTextureWriteback, OnLog are declaration-only.
  4. MGPDrawInfo::XfbCpuCapturedVertices / kDrawHasXfbCount ride nothing. CONTRACT-P5B:163-164 says the field "rides when the client's XFB accounting has a value"; on this head it is written 0 (EmitTables.cpp:402, :1873) and read by nobody.
  5. The barrier's client-half assertion is half-implemented. CONTRACT-P5 §4 says "the client checks [InBarrierWait] when it touches gPipeInputs outside a barrier" — the function has zero call sites (ClientSession.cpp:880). Only ApplyThreadIsInsideApplier is enforced, and only at emit (ClientSession.cpp:748). The two client writes that run outside the barrier (PipeFill.cpp:2798 into gPipeInputs, :2656 into g_applier) are covered by the barrier's post-condition, not by the advertised check.
  6. GetCaps's two blobrefs are answered by the snapshot, not the record. The GetCaps wire record is dead (decoder declines, no emitter); ARCHITECTURE §17's "GetCaps 两个 blobref 的载体" open item is in practice the CapsSnapshot frame, which is wire-clean. The open item is record hygiene, not a missing carrier.
  7. Magma was left out of three fixes Espryt got. pActiveBackendObject routing (#37 vs Utils.cpp), mip-level re-derivation (#14 vs DirectGLES.cpp:8080-8111), and the MarkGpuWritten callback (#30) all have a DirectGLES arm and an untouched DirectVulkan twin. CURRENT_STAGE_PROGRESS §5's "Magma split compute/image: 82 of 89 wrong answers" is consistent with this.
  8. Windows inproc does not exercise the POSIX attach path. AttachInProcess aliases the server's ShmSegment objects on _WIN32 (ShmSegment.cpp:352-359); the dup+Adopt+Map arm that P6 generalises is POSIX-only. Windows-lane green says nothing about segment lifetime discipline.
  9. Stale doc/code citations found while auditing (all content still true, only the numbers drifted): StagedShadow.h:11-14 and PipeApplier.h:33-37 repeat CONTRACT-P5's Managers.cpp:1980-1983/:2035/:2000/... (now :2165,:2225,:2183,:2271,:2279,:2315,:3179,:5393-5396); PipeWireCodec.cpp:1872 cites PipeApply.cpp:702 for the subdata length check (now :715); CONTRACT-P5 table 3: PipeInputs.h:706→:806, PipeApply.cpp:396/402→:409/:415, Managers.cpp:5490-5496→:5806-5812, Init.cpp:44→:63, Managers.cpp:2594→:2769; EmitTables.cpp:21-22 header comment says B=11/C=58 while its own static_asserts pin B=54/C=15 (:1643-1667).
  10. EmitBlitNamedFramebuffer already carries the two FBO handles (EmitTables.cpp:776-777) — the "scoped client binding" inproc dependency #1 is therefore removable inproc (P5c), not only at P6: the sink can resolve ReadFbo/DrawFbo server-side today. Same for the mip registry lookup (#4, MGPMipPlan::Res already crosses).

5. Existing numbers (no runs performed; cited from notes)

  • rsp=35 for Triangle and PersistentCoherentMap entries (pinned table, ~/w7/notes/p5/p5-results/joint-v1.md:271, log lines :1482-1493; reproduced x2-v1.md:137). rsp=2690 for OpenRA over a 5-frame window (x2-v1.md:138).
  • rsp=35 is repeatedly stated to be a lower bound, not a census: ~/w7/notes/p5b/p5b-results/r2-v1.md:64, :79; p5-close-codex-review.md:220; docs-codex-v1.md:11. This audit confirms the bound is low by at least the whole texture family (§4.1).
  • rsp per Minecraft frame is unmeasured — stated at ~/w7/notes/p5b/p5b-results/c0b-v1.md:263; no P5b-close rsp sum exists in the notes. The only numeric rsp= in the whole p5b notes tree is the forwarded 35.
  • P5b final census lane logs (~/w7/p5b-final-census-348d22a4/lane-stage32/, 17 logs, 51 stats lines): rsp histogram — 0×29, 260×4, 271×3, 80/133×2, 50/110/131/138/139/143/150/167/171/185/2472×1 (e.g. …StorageBufferRegrowScenario…log:580 rsp=2472; …TextureUploadShape…log:580 rsp=150 with maxrec=1360).
  • maxrec=784 (SetVertexAttribDefaults) dominates 979 of 1206 lane logs; cap 4 MiB of an 8 MiB SEG_CMD (x2-v1.md:118-125 table; SmallRing ledger x2m-v1.md:55). Largest observed maxrec=4632.
  • Class-C first-blocker census (~/w7/notes/p6/census-classC.md): BindImageTexture 138, BeginTransformFeedback 95, DrawElements 56, DispatchCompute 49, PatchParameteri 43, CopyImageSubData 30, MemoryBarrier 18, MultiDrawElementsBaseVertex 18, ClearBufferiv 12, ClearBufferuiv 8 — all now migrated; 505 UnmigratedVerb + 6 StageSnapshotTooNarrow aborts in the baseline.
  • Device A/B lanes (p5-results/ab-v1.md, 193 rsp= lines): all rsp=0/rsp=None — the stats line is not published on Android device runs, so no device-side rsp exists.

6. What this sizes for P5c (suggested, one line each)

  1. Adopt staged texture bytes server-side (#1–#3): the bytes already cross; add the texture half of StagedShadowStore and route SyncMipmapsToBackend to it. Retires the largest un-instrumented family and makes MOBILEGL_IPC_AUDIT meaningful for textures.
  2. Wire three SEG_EVENT producers (#28, #29, #33) and fix the writeback consumer's segment arm: the ring, consumer and counters already exist.
  3. Resolve records by their carried handles (#4, #9, #10): named blit, mip descriptor, CopyTex — all three already carry the handle the lookup re-derives.
  4. Re-point Magma's four mirror reads + Espryt's fallback (#36, #37) at the server's own backend.
  5. Kill the unguarded per-draw read (#18) with a descriptor live-content bit.
  6. Transport-refuse the legacy buffer arm (#26); implement or delete InBarrierWait (#56).
  7. Leave #41/#51/#52/#55 to P6 (they are its stated job), but #41 and #52 can get their control records in P5c since the mailbox already serialises the call shape.