The twelve Server* forwarders' one-slot mailbox carried a raw function
pointer plus a void* to a stack-local Args struct (P5c audit row G4);
neither means anything across a process boundary. The slot now carries
one SurfaceControlFrame by value: kind is the op, scalars are the
arguments, and ok/eglMajor/eglMinor come back in the same value. The
blocking handshake, the shadow, the doorbell order and the C2 exit
block are unchanged.
- protocol.fbs: append-only - SurfaceOpKind gains SetSwapInterval=8,
ReleaseResources=9, SetWindowHandle=10; WindowKind gains MetalLayer=6;
SurfaceOp gains readSurface and context (the MakeCurrent four-tuple).
protocol_generated.h regenerated with the pinned flatc.
- Server/SurfaceControlFrame.{h,cpp}: the frame (trivially copyable by
static_assert) and the op enum - ten wire kinds plus four inproc-only
kinds (InitCapabilities is answered by CapsSnapshot on the wire,
SwapBuffers/InitWindowSurface have no production caller - f0-egl F8,
kept for the inproc test lane).
- Protocol/SurfaceOpCodec.{h,cpp}: the frame <-> SurfaceOp/SurfaceReply
codec, the explicit WindowBackend <-> WindowKind table, and the wire
entry ServerApplyWireSurfaceOp whose refusals are named:
Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} and
Fatal{ProtocolCorruption, "SurfaceOp"}.
- ServerLoop: RunOnApplyThread(fn, void*) -> RunSurfaceControlFrame(frame)
plus the dispatch ApplySurfaceControlFrame; the old per-forwarder Args
bodies moved into the dispatch unchanged (C7/ID-54, N-3, R-12 included).
RunProbeOnApplyThreadForTesting is the test seam through the same
channel (the hook lives in a member, never in the slot).
- Tests: RemoteClientTest/ServerLoopTest migrated to the probe seam; new
SurfaceControlFrameTest suite (mapping tables, wire round-trips, the
two named refusals) plus ServerLoopTest's AVoidForwarderCrossesAsOne-
DispatchedFrame (the frame channel's red-once handle) and
ServerLoopEglTest's TheInitializeDisplayReplyArrivesThroughTheFrame.
Statics, reverse channel, Magma cross-role sites, EGL control plane,
registry rekey, build/gate mechanics, the 15 BARRIER_PULLED fields'
carrier rulings, and the dual-block mechanism's design recon.
The section explained what gPipeInputs IS and what breaks under spawn, but not the one fact
that decides the shape of the fix, and not how a call actually reaches the driver through it.
Both are now in the document rather than only in the conversation that found them.
- THE POINTER FACT, now stated first because everything else follows from it: several
BARRIER_PULLED fields do not hold values, they hold pointers into client-owned objects.
PipeFill.cpp:274 fills m_textureUnitBase with `&ctx.GetTextureUnitObject(0)` - a raw base
into the frontend's array - and :245 fills m_programForDraw with a SharedPtr<ProgramObject>.
PipeInputs.h:669 dereferences it as `m_textureUnitBase[unit]`. PipeFill.cpp:2591-2592 says
the same of the shader-buffer field: "four raw bases into the frontend's binding-point
table".
The consequence gets its own paragraph in 1.4, because it is the step most likely to be got
wrong: this CANNOT be fixed by moving the block into shared memory. Mapping the same pages
into both processes does not make a client heap address meaningful in the server, and a
SharedPtr's refcount does not cross at all. The 15 fields each need a different SOURCE, not
a shared location.
- NEW 1.2, one draw end to end: glBindTexture marks a tracker bit and sends nothing, because
push only happens at a verb boundary; glDrawArrays enters MGPipeValidateForVerb
(PipeFill.cpp:3347) whose four steps are barrier decision, dirty walk, emission (records,
carrying handles), and the residual fill - which is where BARRIER_PULLED happens; the record
goes on SEG_CMD; the apply thread dispatches to the backend; PrepareForDraw reads either a
record-carried handle against the server's own tables, or gPipeInputs; then the real driver
call. Plus the field lifetime: stamped with verb N's serial, read during verb N's apply,
Fatal{UnmigratedPipeInput} if read at verb N+1 without a refill - one verb, never longer.
- The same example carries the Espryt/Magma split concretely: the texture binding reaches
Espryt as a record with a handle (P5e retired GetTextureUnitObject for it) and reaches Magma
through gPipeInputs into the client's array. That is FieldOwnership.def's own
"P5e (Espryt unbarriered), P7 (Magma)" made legible, and it is why section 2.1's table has
eight rows in that state.
Sections renumbered 1.1-1.4. Doc citation lint: 129 citations, 0 problems.
P6's premise is that it is "only a transport swap". That premise was bought by P5c and has
expired. Measured on this head, the two roles still share state directly, so a spawned server
would have no source for it. P5f is the phase that makes the premise true, and P6 does not
start before it.
WHAT THE MEASUREMENT FOUND
- FieldOwnership.def still carries 21 BARRIER_PULLED rows over 15 distinct fields. That class
is defined as "the server answers by reading a value the client's residual fill left in THE
SINGLE SHARED gPipeInputs while the verb barrier holds both threads apart". Eight of those
fields were retired by P5e for Espryt's unbarriered path only - Magma has not moved at all.
The strict lane measures 10 live <field>@<verb> pairs today.
- gPipeInputs (MG_Backend/MGPipe/PipeInputs.h:835) is a plain process global,
`inline PipeInputs& gPipeInputs = *new PipeInputs();` - not a shm segment. The client fills
it (MG_Impl/Pipe/PipeFill.cpp), the backend reads it. Under spawn the two processes have
DIFFERENT objects, the 15 BARRIER_PULLED fields have no filler in the server's copy, and
every read is Fatal{UnmigratedPipeInput}. MG_Impl is not in the server, so "let the server
fill it" is not available either. The header says this itself at :279 and :902.
- The twelve Server* EGL forwarders do not use the wire at all: they post a function pointer
plus a stack-local void* through a one-slot mailbox (ServerLoop.cpp:533-568, :645). The
SurfaceOp / SurfaceReply / WindowKind schema is defined in protocol.fbs and unused.
- Process-level statics with per-context semantics (g_syncedRenderStateParameters,
ScopedDefaultUnpackState::s_synced) - and the full list is unknown, because this defect class
is structurally invisible to inproc.
- MGPipeFrontendKeyedRegistryScope is still live at ~12 sites in DirectGLES.cpp, carrying
P3b/P4b's debt: the registry is still keyed by frontend address / lifetime id.
THE SCOPE RULE
P5f takes the cross-process-blocking subset of P3b/P4b/P7/P8/P9 and nothing else. One test:
does it still hold in another address space? No -> P5f. Yes but slow or ugly -> it stays with
the phase that owns it. So Magma's nine fields and its direct writes into client storage come
here; content-addressed CSO, internal shader baking and the fixed-width rewrite stay P7's. The
reverse channel's asynchrony and ordering stay P9's; only the parts that touch client memory
move. Class C is NOT P5f's - a named refusal behaves identically in two processes.
THE CENTRAL MECHANISM: A TWO-BLOCK REHEARSAL
P5c set the same rule (rule E) and checked it by AUDIT - a human reading 59 lines. P5f checks
it by MACHINE: give each role its own PipeInputs under inproc, and its own copy of every
per-context static, behind a knob. Anything that depends on the two being the same object
becomes Fatal{UnmigratedPipeInput, "<Field>@<Verb>"} with the field and verb named.
That rehearses the process split's SEMANTICS without a second process. It matters because
today "the server read the value the client filled" and "the server read the value a record
carried" are indistinguishable at runtime - which is exactly what P6 would discover on day
one. The package order puts the mechanism (f1) BEFORE the migrations deliberately: let the red
appear first and let it be the worklist, rather than having each package declare itself done
and then looking for a criterion.
Exit gate: the two-block lane green; strict-expected-markers.txt EMPTY with its two-sided
ratchet still armed; zero BARRIER_PULLED rows; rsp reading zero per frame; Magma judged by the
same criteria as Espryt. The negative control is that turning the two-block knob OFF must move
at least one named case from green to red - otherwise the mechanism proves nothing, which is
ID-122's lesson.
ALSO IN THIS COMMIT
- ROADMAP gains the P5f row and the IPC track becomes P5e -> P5f -> P6; P6's status cell
becomes "blocked on P5f".
- README and CURRENT_STAGE_PROGRESS gain the same, plus the file-map entry.
- Both P6 documents gain a blocked banner naming what is not true yet.
Doc citation lint: 124 citations across docs/Disaggregated/*.md, 0 problems.
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.
It said "in progress" because BRIEF-P5E section 3's exit gate had never been RUN, not because
the work was unfinished. It has now been run, item by item, recorded in ROADMAP's P5e
exit-gate cell:
- item 1: unit 2256/2256 under both strict arms.
- item 2/3: integration-split 179/179; the per-entry census has zero Fatal{ and exactly eight
Admitted{ rows.
- item 4(a): RUN_AHEAD=0 as the PAIRED control, 179/179 green - same server code, same
records, the only difference is whether the client waits.
- item 4(b): VERB_BARRIER=0 goes red (62/179 in the lane).
- item 4(c): Magma logs, verbatim, "run-ahead requested, server does not publish
kCapRunAheadApply - running lockstep", and the Espryt control logs "run-ahead ARMED". Both
taken from each entry's own private log.
- item 5: red-once rerun on the integrated tree, in the form ID-121 reordered it into -
reverting pa's arm yields GetProgramForDraw@DrawArrays, reverting mv's yields
GetBoundVertexArray@DrawArrays, EXACTLY ONE NAMED PAIR EACH, and each restores green.
- item 7: MOBILEGL_IPC_AUDIT=1, 179/179.
- item 6's G1 remains CI's to assert (ID-123).
The only item that does not pass is E1's control script itself (ID-122), and the diagnosis is
sharper than before: with its two masking defects fixed it is still red, and the
Fatal{BarrierViolation} it demands appears ZERO times in the whole run - at BATCH_WAITS's
default of 1 that overlap does not happen, and with run-ahead armed VERB_BARRIER=0 also
disarms run-ahead (ID-114), so the arm it produces is neither lockstep nor run-ahead. E1 needs
its falsification target redefined, not its threshold adjusted.
So the status is "closed, with one named outstanding item" rather than an unqualified close:
hiding a control that never passed behind the word "closed" is precisely the class of mistake
this phase kept recording.