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.
15 KiB
ra — the wait rule, present credit, server-owned gPipeInputs (P5e)
Branch p5e/ra, head 48b2f56e, based on f6cfcbd3 (landed wave 1). Tree ~/w7/p5e-ra.
Five commits; the mechanism is complete and INERT — kMGPipeP5eRunAheadReady is still false,
so RunAheadArmed() is false everywhere and every gate is exactly as green as the base.
1 Per deliverable: what is wired, and what it does when the bit flips
| # | deliverable | wired | at the flip |
|---|---|---|---|
| 1 | the predicate | c0e's MGPipeBarriered (all three clauses) is the server's; ra adds the CLIENT's spelling, ClientVerbIsBarriered(verb, ctx*) in PipeFill.cpp |
both roles answer the same for the same record: static WaitClass column, XFB-active, (client arrays: vi refuses at emit) |
| 2 | the client wait rule | EmitAndWaitTails switches on MGPipeWaitClassFor(op) under RunAheadArmed(), replacing the BatchWaits skip; ReleaseFillPins() after a barriered apply; present credit in EmitPresent→AcquirePresentCredit; OnPresent→ReturnPresentCredit; forced waits before every Server* EGL forwarder and in glFinish; drains at every wait exit incl. the stage bell |
kWaitNone rows publish and return; the steady draw path stops waiting; presents pace on a credit of 1 |
| 3 | resource_subdata buffer half |
MGPipeSubDataWantsItsReply(record) — ONE predicate, read by MGPipeRouteResourceSubData and by Wire_ResourceSubData; the row keeps kReplySlot, the server still posts |
the 64 KB persistent-map push stops being a hidden round trip per block |
| 4 | gPipeInputs ownership |
fillOwed = barriered in MGPipeValidateForVerb gates the serial bump, SetVerb, MGPipeServerClearVerbBoundary and the whole of step 4; MGPipeNoteFrontendMutation/MGPipeLeaveVerb return; CountBarrierPull aborts unconditionally under an unbarriered record; the layer-2 guard is re-derived |
the block is server-role memory for unbarriered records; a missed migration is a named abort, not a wrong picture |
| 5 | event-ring flow control | ReserveEventOrBlock (all four event kinds), Drained() clears and rings, WaitFor{Applied,PresentAck}OrEventBacklog, ServerLoop's park gains "not while the ring is full" |
a full 256 KiB ring is backpressure, not Fatal{EventRingOverflow}; two named refusals remain for the impossible cases |
| 6 | lifetime | the deferred-destroy queue STAYS; an enqueue from an unbarriered apply logs once and is Fatal under strict | ruling 13 (ID-91) pinned; slot reuse needs no wait (the ring orders {s,g} → death → {s,g+1}) |
| 7 | the strict lane | ONE step, TWO arms, and the arm is chosen by grepping kMGPipeP5eRunAheadReady out of Init.cpp |
the lane becomes a hard green gate with §7's <field>@<verb> allowlist, in the same commit the runtime flips |
| 8 | docs | ARCHITECTURE §11.6, §11.7, §14, new §17.7, 附 A's knob table | — |
2 Files touched (regions, not whole files)
MG_Remote/Client/ClientSession.{h,cpp}— the wait switch inEmitAndWaitTails;RunAheadArmed/LatchRunAheadFromCaps(latched inPumpControlPlane, demote-only);ReleaseFillPins,WaitForApplyToCatchUp,Finish,AcquirePresentCredit,PresentCreditWaits,LastPublishedSeq; the re-derivedRefusePipeInputsTouchWhileApplierOwnsIt(surface, isBarrieredFill); drains at the cmd-space wait and the stage bell;credit-waitson the wire ledger; the latch reset inStop.MG_Remote/Client/EmitTables.cpp—EmitPresentmintsFrameSerialthroughAcquirePresentCredit.MG_Remote/Client/BackendObject_Remote.cpp—WaitForApplyBeforeEglForwarderbefore the fiveServer*forwarders (NOTServerSwapEGLBuffers).MG_Remote/Client/WireTables.cpp—Wire_ResourceSubDatatakeswantReplyand posts accept-by-construction for the buffer half.MG_Pipe/PipeRoute.h—MGPipeSubDataWantsItsReply+ the route's use of it (inside#if MOBILEGL_PIPE_PUSH).MG_Remote/Server/PipeApplier.cpp—OnPresentreturns the credit and refusesFrameSerial == 0on a run-ahead server;ServerPublishesRunAheadhelper;#include "ServerSession.h".MG_Remote/Server/ServerSession.cpp—ReserveEventOrBlockreplaces the fourReserve+ Fatal sites.MG_Remote/Server/ServerLoop.cpp— the park predicate's fourth clause and theDrainRinggate.MG_Remote/Transport/{EventRing.h,SessionRings.h,Ring.cpp}—SetServerDoorbell, the ring-and-clearDrained(), the two…OrEventBacklogwaits,EventRingIsFull().MG_Remote/Wire/PipeWireCodec.{h,cpp}—SetStageWaitHook, called around the SEG_STAGE retirement park (named delta, see §5).MG_Impl/Pipe/PipeFill.cpp—ClientRunsAhead,WireOpForVerb,ClientVerbIsBarriered,g_lastFillWasBarriered, thefillOwedgate,MGPipeReleaseResidualFillPins,MGPipeClientFinish, the deferred-destroy finding, the vertex-attrib-defaults mirror read skipped under run-ahead (§3.4).MG_Backend/MGPipe/PipeInputs.cpp—CountBarrierPull's unconditional arm;StrictBarrierPullFatalgains a reason so the lane can tell the two markers apart.MG_Pipe/PipeMutation.h(declarations + the ruling-13 comment),MG_Pipe/PipeApply.h(the two-constants ruling, §6).MG_Impl/GLImpl/Exporting/Definitions.cpp—glFinish(named delta, §5).MG_Test/Wire/{RemoteClientTest,SessionTest}.cpp,.github/workflows/test.yml,docs/Disaggregated/ARCHITECTURE.md.
3 Red-once evidence (executed, reverted, verbatim)
1. Present without the credit wait (if (m_presentsSent >= credit) → if (false)),
RemoteRunAhead.ACreditOneClientPaysTheCreditAtEveryPresentAfterTheFirst:
0% tests passed, 1 tests failed out of 1
../MobileGL/MG_Test/Wire/RemoteClientControls.inc:143: Failure
Actual: false
exited 103
(103 = PresentCreditWaits() != 2 — red by count, which is the only way to test a wait that is
usually already satisfied.)
2. Leave the fill in for an unbarriered record (const Bool fillOwed = barriered; → = true),
RemoteRunAhead.AnUnbarrieredVerbDoesNotFillPipeInputsAndIsAllowed:
signal 6
[08:09:45] [Linux RemoteClientTes/FATAL]: MGPipe: Fatal{RoleViolation, "gPipeInputs"} - the GL thread touched gPipeInputs (MGPipeValidateForVerb) on a RUN-AHEAD session outside a barriered fill. With the client running a
3. Count instead of Fatal under an unbarriered record (drop CountBarrierPull's first
clause), RemoteGuards.AResidualPullUnderAnUnbarrieredRecordIsFatalWithoutTheStrictKnob:
0% tests passed, 1 tests failed out of 1
../MobileGL/MG_Test/Wire/RemoteClientControls.inc:139: Failure
Actual: false
exited 0
(the probe stops aborting; on the lane this is every still-pulling scenario going from a named abort to a wrong picture with a number beside it.)
4. Keep Fatal{EventRingOverflow} (if (!ServerPublishesRunAhead(session)) Fatal… → always),
RemoteRunAhead.AnEventBurstBehindAnUnwaitedSequenceFlowControlsInsteadOfAborting:
signal 6
[08:11:31] [Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{EventRingOverflow} - the producer of kEventGpuWritten could not reserve 1040 bytes on SEG_EVENT. P5c's events are lossless and a full ring under lockstep is a produce
4b. The drain stops ringing the server (Drained()'s NotifyIfParked → (void)wasFull;),
SessionTest.DrainingAFullEventRingClearsTheLatchAndRingsTheServer:
../MobileGL/MG_Test/Wire/SessionTest.cpp:661: Failure
Which is: 0
Which is: 1
4c. The applied park stops breaking on the ring (drop the eventRingFull clause),
SessionTest.AFullEventRingWakesAClientParkedOnAppliedSeq:
../MobileGL/MG_Test/Wire/SessionTest.cpp:586: Failure
Which is: 4-byte object <02-00 00-00> (TimedOut)
Which is: 4-byte object <00-00 00-00> (Reached)
5. Bit 10 for Magma — c0e already ran and recorded this
(MagmaPipeIdentityTest.AMagmaServerNeverPublishesTheRunAheadCapBit fails "Which is: 1024 / 0").
Re-run here green: MagmaPipeIdentity 5/5.
6. NOT RUN, and why. MOBILEGL_IPC_RUN_AHEAD=0 renders identically is TRIVIALLY true on this
tree — the caps bit is unpublished, so both arms are the same code path — and the SSIM arm needs
the device. MOBILEGL_IPC_VERB_BARRIER=0 under run-ahead is likewise unreachable before the flip.
Both belong to the integrator's device exit (BRIEF §4).
4 Gate (this tree vs base f6cfcbd3, measured both)
| step | base f6cfcbd3 |
p5e/ra 48b2f56e |
|---|---|---|
| build | rc=0 | rc=0 |
| unit | 2206/2206 | 2222/2222 (+16 cases, all new) |
integration-split |
111/111 | 111/111 |
integration-split-strict |
3% passed, 108 failed of 111 | identical |
| gens | 7 rc=0 + the pre-existing CONTRACT-P5.md:524 Core.cpp:39 ambiguity |
identical |
Strict marker table, from the 88 per-entry private logs, before and after are the same table
(measured by checking out f6cfcbd3, rebuilding and re-running):
| marker | count (base) | count (ra) | owner |
|---|---|---|---|
GetFramebufferBindingSlot@Clear |
39 | 39 | fb |
GetBoundVertexArray@DrawArrays |
14 | 14 | vi |
GetImageTextureBinding@BindImageTexture |
13 | 13 | fb |
GetTextureUnitObject@Clear |
9 | 9 | tx2 |
GetTextureObject@CopyImageSubData |
6 | 6 | P5b debt (allowlisted) |
GetProgramForDispatch@DispatchCompute |
3 | 3 | pg |
ValidateProgramName@ShaderStorageBlockBinding |
2 | 2 | pg (allowlisted) |
G1: 0 delta. Every edit to shared code is under MOBILEGL_BUILD_DISAGGREGATED or
MOBILEGL_PIPE_PUSH (PipeRoute.h's route is at line 270, inside the file's push guard).
5 Kimi audit cross-check (my family is list (b), not an object family)
- RETIRED:
PI:807, PI:812-814— the four O-classSharedPtrrows (m_boundVertexArray, the three programs).ReleaseFillPinsdrops them after every barriered apply, and an unbarriered verb never re-fills them, so the apply thread can no longer become a frontend object's last owner through this block. - LEFT BARRIERED:
PI:808-811, PI:815— the raw pointer bases into the frontend tables. They are written only by a barriered fill; an unbarriered apply that reads through them takes §3.3's abort by field and verb. Their retirement is the family packages' (list (c)). - NOT FOUND / not mine: every row of list (c) is an
MGB_CTX->accessor whose retirement belongs to vi/sb/pg/tx2/fb. ra changes not who reads them but what a read costs.
6 Rulings I need from the integrator
kMGPipeP5eClientWaitRuleLandedflips withkMGPipeP5eRunAheadReady, not when ra lands. ID-103 says "ra flips it in the same commit that lands the wait rule". That cannot be right:RunAheadArmed()needs the server's caps bit, so between ra landing and the bit being published the client still blocks after every record — and afalsestamp there withdraws §4.4's exemptions from probes that wait still makes safe, which is the "refusal with no defect behind it" ID-103 itself refused. I left BOTH constantsfalseand amended the comment at the constant. The integration commit throws both. (Had I flipped it,integration-splitwould be near-totally red on this tree: the family packages have not landed.)- §3.2's "the stamp moves to applier-owned storage" is implemented as an OWNERSHIP rule, not
as moved storage. Physically moving
m_filled/m_currentVerb/m_serverStampedVerbout ofgPipeInputsmeans re-pointing the freshness check in all 63 generated accessors (generated/PipeFilled.inc), which is c0e's file and a much larger blast radius. What ra landed is the rule the move existed for: under run-ahead the GL thread writes those fields only inside a barriered fill, so the E-note hazard (b.4) — a GL thread withdrawing the server's stamp while an apply depends on it — cannot occur. Confirm, or schedule the move. SetIdentityis NOT skipped (§3.1 lists it).m_live/m_contextIdentityare not fields of the residual model — no ownership row owns them, no record carries them, no server stamp can answer them — they are "does this process still have a GL context", which every null-context guard reads and only the GL thread knows. Two stores per verb, no stamp.- The §7 allowlist is my best reading, not a measurement. It cannot be measured before the flip (every entry still aborts on an unmigrated row). The integrator should re-derive it from the first green run and delete rows rather than widen them.
7 Seams assumed from other packages
- vi: a draw with a client-memory array is refused at emit under run-ahead (
ruling 2/ §5.1).ClientVerbIsBarrieredtherefore does NOT re-computekDrawClientArrays— duplicating vi's "does any enabled attribute lack a buffer" walk is exactly the second spelling ruling 3 replaced. If vi's refusal does not land, a client-array draw would be judged unbarriered and its fill skipped. Named in the code comment. - tx2:
GenerateMipmapstayskWaitApplieduntil theVerbMipResarm lands; the wait rule reads the column, so tx2 flipping the row is all that is needed. - pg/sb/fb/tx2: the strict allowlist's rows are theirs to delete.
- c0e (landed):
MGPipeWaitClassFor,kCapRunAheadApply,MGPipeRunAheadCapBitsFor,IpcTable::RunAhead/PresentCredit,MGPipeBarriered, the barriered-record flag + setter. - id (landed):
ApplyOne's stamp line — untouched by ra, as the brief assigns.
8 Named deltas and what I did not do
PipeWireCodec.{h,cpp}(not in my scope list): the SEG_STAGE retirement park is a CLIENT wait and the deadlock argument needs every client wait to drain. The encoder cannot drain SEG_EVENT itself, so it gained aStageWaitHookthe session installs. Null in every standalone codec; the park is otherwise byte-identical.MG_Impl/GLImpl/Exporting/Definitions.cpp(not in my scope list):glFinishwas a pure no-op. Under run-ahead "the commands issued so far have completed" is a promise the queue can break, and this is the only place an application can ask for it. Two lines, under#if MOBILEGL_BUILD_DISAGGREGATED, callingMG_Pipe::MGPipeClientFinish.- The server half of the present credit is not unit-pinned. The
RemoteClientfixture has no backend object, soServerVerbSink::OnPresentdeclines before it can reachReturnPresentCredit; the cases substitute a peer sink and assert the CLIENT's half. That the server returns exactly one credit per swap afterPresent()returns is three lines inPipeApplier::OnPresentand the device exit'scredit-waitsreading. - No device numbers. The measurement arms of BRIEF §4 (fps, per-thread ms/frame, simpleperf, SSIM) are the integrator's; no adb from here.
MOBILEGL_IPC_EVENT_KBstays a trailing item: flow control makes 256 KiB correct rather than fast, and the ring size is a measurement, not a correctness input.- G2/G14 skip twins: the new public names are all in split-only translation units
(
MG_Remote/*, the push-only half ofPipeFill.cpp) and the new cases live inMG_Test/Wire/*, which is a split-only binary. No monolith twin is possible or needed; unit count moves 2206 → 2222 on the split build only.