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
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::Adoptcopies on apply (Managers.cpp:2165,:2225);Fatal{StageSnapshotTooNarrow}six named sites (StagedShadow.h:106);hostBytesis server-owned under split (rule C's one named violation is fixed).resource_subdatadeclares a real size, cross-checked (PipeApply.cpp:715).resource_flush_rangecarries no bytes, transport-gated (PipeApply.cpp:1885-1888).buffer_subdata_residentcopies, never retains (Managers.cpp:2199-2207). - resource_respecify:
initialBytes == nullptrunder 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 toMappedData()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+RendererInfocross as serialized blobs in theCapsSnapshotcontrol frame (ServerSession.cpp:366-408→CapsMirror.cpp:55-87); DirectGLES format-cap lookups are routed to the server's backend (Utils.cpp:40-51main 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 viaMGPipeHostBytes(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::XfbCpuCapturedVerticeshas no reader, so its deadness leaks nothing. - Roles:
gMGPipeSegmentResolverserver-only with double-install Fatal;gBackendFunctionsTablesplit correctly; the threeMGPipeGetResourceOps()liveness reads inPipeFill.cpp:642/655/1069short-circuit toCapsMirror::ServerConsumesunder a transport; noMG_Remote/ServerorMG_PipeTU reads anyMG_Impl/Pipesingleton except theSlotTables.hbackend arm (#42).
4. Surprises / disagreements with the contracts and ARCHITECTURE.md §17
- 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 andApplyTextureUploadthen drops the pointer ("DISPATCHES TO NOBODY",PipeApply.cpp:989-1008); Espryt re-reads the texels from the clientMipmapStorage(Managers.cpp:7315). Every texture-object method the server calls on a client object has no row inFieldOwnership.def, so the whole family is invisible torspand toMOBILEGL_IPC_STRICT_ERRORS, andMOBILEGL_IPC_AUDIT=1's0xDDfill cannot observe it (textures never adopt staged bytes).rspis a lower bound by a wide margin — exactly asr2-v1.md:79warned, but the unnamed set is larger than "sticky/non-verb forwards": it is the texture hot path. - 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-564rejects any realSeg). A producer added tomorrow would be dropped, not delivered. - 8 of 10
MGPipeCallbacksare never installed — onlyOnBufferWriteback/OnGpuWrittenhave assignment sites (ResourceTracker.h:606-613), and both are invoked in-line on the apply thread straight into clientMG_State.OnGlError,OnSurfaceChanged,OnCapsInvalidated,OnMipLevelsGenerated,OnXfbScatterReady,OnTexturePullRequest,OnTextureWriteback,OnLogare declaration-only. MGPDrawInfo::XfbCpuCapturedVertices/kDrawHasXfbCountride 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.- The barrier's client-half assertion is half-implemented. CONTRACT-P5 §4 says "the client checks [
InBarrierWait] when it touchesgPipeInputsoutside a barrier" — the function has zero call sites (ClientSession.cpp:880). OnlyApplyThreadIsInsideApplieris enforced, and only at emit (ClientSession.cpp:748). The two client writes that run outside the barrier (PipeFill.cpp:2798intogPipeInputs,:2656intog_applier) are covered by the barrier's post-condition, not by the advertised check. GetCaps's two blobrefs are answered by the snapshot, not the record. TheGetCapswire record is dead (decoder declines, no emitter); ARCHITECTURE §17's "GetCaps 两个 blobref 的载体" open item is in practice theCapsSnapshotframe, which is wire-clean. The open item is record hygiene, not a missing carrier.- Magma was left out of three fixes Espryt got.
pActiveBackendObjectrouting (#37 vsUtils.cpp), mip-level re-derivation (#14 vsDirectGLES.cpp:8080-8111), and theMarkGpuWrittencallback (#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. - Windows inproc does not exercise the POSIX attach path.
AttachInProcessaliases the server'sShmSegmentobjects on_WIN32(ShmSegment.cpp:352-359); the dup+Adopt+Maparm that P6 generalises is POSIX-only. Windows-lane green says nothing about segment lifetime discipline. - Stale doc/code citations found while auditing (all content still true, only the numbers drifted):
StagedShadow.h:11-14andPipeApplier.h:33-37repeat CONTRACT-P5'sManagers.cpp:1980-1983/:2035/:2000/...(now:2165,:2225,:2183,:2271,:2279,:2315,:3179,:5393-5396);PipeWireCodec.cpp:1872citesPipeApply.cpp:702for 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-22header comment says B=11/C=58 while its ownstatic_asserts pin B=54/C=15 (:1643-1667). EmitBlitNamedFramebufferalready 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 resolveReadFbo/DrawFboserver-side today. Same for the mip registry lookup (#4,MGPMipPlan::Resalready crosses).
5. Existing numbers (no runs performed; cited from notes)
rsp=35for Triangle and PersistentCoherentMap entries (pinned table,~/w7/notes/p5/p5-results/joint-v1.md:271, log lines:1482-1493; reproducedx2-v1.md:137).rsp=2690for OpenRA over a 5-frame window (x2-v1.md:138).rsp=35is 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).rspper Minecraft frame is unmeasured — stated at~/w7/notes/p5b/p5b-results/c0b-v1.md:263; no P5b-closerspsum exists in the notes. The only numericrsp=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):rsphistogram —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:580rsp=2472;…TextureUploadShape…log:580rsp=150withmaxrec=1360). maxrec=784(SetVertexAttribDefaults) dominates 979 of 1206 lane logs; cap 4 MiB of an 8 MiB SEG_CMD (x2-v1.md:118-125table; SmallRing ledgerx2m-v1.md:55). Largest observedmaxrec=4632.- Class-C first-blocker census (
~/w7/notes/p6/census-classC.md):BindImageTexture138,BeginTransformFeedback95,DrawElements56,DispatchCompute49,PatchParameteri43,CopyImageSubData30,MemoryBarrier18,MultiDrawElementsBaseVertex18,ClearBufferiv12,ClearBufferuiv8 — all now migrated; 505UnmigratedVerb+ 6StageSnapshotTooNarrowaborts in the baseline. - Device A/B lanes (
p5-results/ab-v1.md, 193rsp=lines): allrsp=0/rsp=None— the stats line is not published on Android device runs, so no device-siderspexists.
6. What this sizes for P5c (suggested, one line each)
- Adopt staged texture bytes server-side (#1–#3): the bytes already cross; add the texture half of
StagedShadowStoreand routeSyncMipmapsToBackendto it. Retires the largest un-instrumented family and makesMOBILEGL_IPC_AUDITmeaningful for textures. - Wire three SEG_EVENT producers (#28, #29, #33) and fix the writeback consumer's segment arm: the ring, consumer and counters already exist.
- Resolve records by their carried handles (#4, #9, #10): named blit, mip descriptor, CopyTex — all three already carry the handle the lookup re-derives.
- Re-point Magma's four mirror reads + Espryt's fallback (#36, #37) at the server's own backend.
- Kill the unguarded per-draw read (#18) with a descriptor live-content bit.
- Transport-refuse the legacy buffer arm (#26); implement or delete
InBarrierWait(#56). - 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.