Files
MobileGL/docs/Disaggregated/notes/p5e/reports/gl-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

20 KiB
Raw Blame History

gl-v1 — the gate mechanism, and two latent crashes

Branch p5e/gl, base 7c6f6886, head e5b74028. Ten commits; all eight brief items landed, plus ID-125 and ID-128 which arrived mid-task.

# item commit
1 barriered stamp asks the server's capability (ID-111) 8df7861f
2 compile-time trip wire on the residual fill (ID-112) f94752c0
3 admission predicate, generated (ID-116) a211756a
— retiring-phase disjunct (ID-125) faf19a6a
— escalation disjunct (ID-128) a0836568
4 strict knob becomes admission-aware (ID-117) 4f492edb
5 ResourceCopyRegion becomes barriered (ID-118) a6ddeb08
6 lane reads every log, counts, positive control (ID-119/115) bc6e24ff
7 Magma's entries get their own lane 664cbf3e
8 amendment 8 marked UNLANDED (ID-120) e5b74028

Gate

step result delta and why
build rc=0
unit 2254/2254 +4 split-only cases (one per red-once: ID-111, ID-116, ID-117, ID-128). One rename, no deletions.
isplit 117/117 +3 for the new TriangleScenario case (plain split, SmallRing, and its own StrictArming block); −2 for the DirectVulkan NamedBlit pair moving to integration-magma-split.
gpu 1299/1299 +5: the same case is discovered by five gpu-labelled blocks and skips loudly in four.
strict 49/117, 68 abort was 7/116. The 68 are exactly pa's and mv's rows — table below.
integration-magma-split 0/2 under strict, 2/2 without the expected-red step, asserted red and asserted on its marker.
gens all rc=0 incl. --check and --self-test (18 negative + 13 derivation controls). The one check_doc_citations line (CONTRACT-P5.md:524) is pre-existing in a file I did not touch.
flavours rc=1, not mine breaks in DirectGLES/DirectGLES.cpp:5575,6716 and Managers.cpp:9081,9344 on keysMatch, ComputeShaderStorageBlockBindingSignatureOf, StagedTextureTargetForPipeTarget. My diff contains no DirectGLES file; git log -S puts those symbols in 49aab57f / 72e79020, both ancestors of my base. ID-124's finding, unchanged.

The marker table (this head — base 7c6f6886, so before pa and mv)

Two independent instruments agreeing exactly: my in-lane split_log_paths.py markers (reads the entries' own private logs) and the integrator's strict_census3.py (re-runs each entry under its own command and environment). 113 of 113 log-declaring entries read; 117 lane entries.

Fatal{ — must be only what pa and mv owe. It is: GetProgramForDraw@DrawArrays 52 (pa), GetBoundVertexArray@DrawArrays 9 (mv), GetProgramForDispatch@DispatchCompute 7 (pa). Nothing else. GetFramebufferBindingSlot@Clear (Magma, 2) has left this table — item 7.

Admitted{, by the generated table:

pair entries admitted by
GetBufferBindingSlot@ReadPixels 29 disjunct 2 (P8/P9/P13)
GetFramebufferBindingSlot@ReadPixels 29 disjunct 1 (read_pixels is kWaitReply)
GetTransformFeedbackProgram@DrawArrays 11 disjunct 2 (P3b/P4b), also 3
GetTextureObject@CopyImageSubData 6 disjuncts 1 and 2, independently
GetProgramObject@ShaderStorageBlockBinding 2 disjunct 2 (P9)
GetTextureUnitObject@CopyTexImage2D 2 disjunct 1 (kWaitApplied)
ValidateProgramName@ShaderStorageBlockBinding 2 disjuncts 1 and 2

ADMITTED-ESCALATED (disjunct 3 only): GetProgramForDraw@DrawArrays 13, GetBufferBindingPoint@DrawArrays 11, GetBoundVertexArray@DrawArrays 2.

The first is not a contradiction with the Fatal table: the same pair is Fatal in 52 entries and admitted in 13. The 13 are draws the client is parked behind because the payload escalated them (open XFB span, or client arrays); the 52 are ordinary draws. That is exactly the distinction ID-128 exists to draw, and no table indexed by (field, verb) could have drawn it.

Reconciling with ID-128's census of the merged tree

ID-128 measured 180 entries at 3c13fa4b; I measure 117 at 7c6f6886. Differences, all accounted for, none of them a rule I bent:

  • pa/mv are not in my base, so their three Fatal rows are still here.
  • GetFramebufferBindingSlot@Clear is absent from my Espryt table by item 7, not by admission (0/2 red in integration-magma-split with its marker asserted).
  • GetBufferBindingSlot@ReadPixels, GetProgramObject@ShaderStorageBlockBinding and GetBufferBindingPoint@DrawArrays appear in mine and not in ID-128's table. I believe this is instrument, not disagreement — both of mine record every marker per entry, a first-fault census records one — but I cannot check that from here. Flagging rather than assuming.

Item by item

1 · ID-111 (PipeApplier.h/.cpp). The stamp is now MGPipeApplierStampsBarriered(waitRuleLanded, serverPublishesRunAhead, wireSaysBarriered), a pure function; both predicates still computed unconditionally, as arguments rather than short-circuit arms (ID-103 extended to the capability probe). ServerPublishesRunAhead promoted out of the anonymous namespace, body unchanged, so the red-once can say "this session lacks bit 10" in its own words (ID-102). It is a pure function because the brief's red-once needs it to be: kMGPipeP5eClientWaitRuleLanded is false today, so a case that only drove ApplyOne would pass with or without the conjunct — and keep passing until the integration commit, at which point Magma SIGSEGVs with the lane green. So the case drives a real DrawVbo through ApplyOne on a SetCapabilityBits(0) session and asserts the stamp at the post-flip value of the constant. Red-once, deleting serverPublishesRunAhead &&: ../MobileGL/MG_Test/Wire/RemoteClientControls.inc:143: Failure / Expected: true / exited 124.

2 · ID-112 (PipeFill.cpp, +45 lines after the existing assertion; I did not touch EmittedCallSuppliesTheWholeField itself). The old assertion is keyed on the emitter's subsystem, so it covered five of seven; GetBoundVertexArray and GetBufferBindingPoint are outside kMGPipeP4aFamilySubsystems. All seven are now named in one list with their own assertion, a size assertion, and a comment saying where a fired assertion sends you (P7/P8, not this table). Red-once, deleting the GetBoundVertexArray case: PipeFill.cpp:2720:23: error: static assertion failed due to requirement 'NoPointerBackedRowIsWhollySupplied()': a pointer-backed residual row stopped being pulled (ID-112). The pre-existing NoP4aFamilyFieldIsWhollySupplied stayed silent on that same edit — the gap ID-112 names, now measured.

3 · ID-116 (generator +394, .inc +126, test.yml hand-kept 13 rows deleted, one unit case + skip twin). The generator reads kMGPipeWaitClasses, kMGPipeVerbClass and kMGPipeClassFieldMask and emits kMGPipeAdmittedPullMask + MGPipeBarrierPullAdmitted, with a static_assert that every admitted bit is a BARRIER-PULLED field of its verb's class; --print-admitted hands CI the same table. Final rule after ID-125/128: BARRIER_PULLED ∧ the verb has a stamp row ∧ in the class mask ∧ (statically barriered ∨ retiring phase ≠ P5e ∨ escalated at runtime). Static part: 174 pairs across 23 stamped verbs. Three new negative controls (each source renamed away must stop the script — a missing source yields a silently empty allowlist, which reads as rigour and is blindness) and nine derivation controls. Red-once, dropping the wait-class conjunct: self-test: 3 admission-derivation control(s) did not hold: unbarriering ReadPixels' op removes every @ReadPixels pair; … plus the unit case's GetBoundVertexArray@DrawArrays is admitted on a verb no barriered op stamps.

ID-125. Implemented as ruled; the phase column is matched as a token (\bP\d+[a-z]*\b), not a substring, so a P5 or a future P5e2 cannot answer for P5e. 111 → 174 pairs. All six survivors and all three rejections verified (§Rulings for the sixth). One consequence you should know: not one of the seven sticky forwards names P5e (P7, P7/P9, P7/P13, P9), so after ID-125 no sticky forward can produce a Fatal{ on a stamped verb — they are all admitted. StrictErrorsAlsoPromotesTheStickyForwards therefore stopped going red. That is the "check, do not assume" instruction in item 4 paying off. I renamed it StrictErrorsReachTheStickyForwardsAndNameThemByField and restated it around the claim that still has teeth — the detector reaches them (asserted by rsp moving and by the marker naming field and phase), so "did not abort" cannot pass for "never detected". The Fatal half keeps its own case next door on GetBoundVertexArray, a P5e row; StrictErrorsTurnsABarrierPulledReadIntoANamedAbort still goes red, unchanged.

ID-128. PipeApply.h/.cpp gain MGPipeApplierCurrentRecordIsBarrieredByEscalation() and its setter — a second thread_local defaulting false, the answer that admits nothing; ApplyOne stamps it as wireSaysBarriered && MGPipeWaitClassFor(op) == kWaitNone; the strict arm reads it as disjunct 3. The marker says which disjunct (ADMITTED vs ADMITTED-ESCALATED), in the slot the grammar already had for the reason, so Fatal{UnmigratedPipeInput filters are untouched. That is what keeps the lane exact: an ADMITTED pair is checked against --print-admitted, an escalated one is not — it is a runtime fact about the payload, and widening the static list to hold it would forgive the ordinary draw path too. Red-once, dropping && !escalated: FieldOwnershipTest.cpp:938: Failure / Actual: false / signal 6 with Fatal{UnmigratedPipeInput, "GetBoundVertexArray@DrawArrays"} [BARRIER-PULLED, MOBILEGL_IPC_STRICT_ERRORS=1, retires in P5e (Espryt unbarriered), P7 (Magma)]. Its control is StrictErrorsTurnsABarrierPulledReadIntoANamedAbort, the identical pull with the flag at default.

4 · ID-117 (PipeInputs.cpp). Third state exactly as sketched, both existing arms untouched; AdmittedBarrierPullOnce deduped per (field, verb) in a static bitset — plain, not atomic, for the file's existing reason (reachable only on the apply thread; g_residualPulls beside it is a plain Uint64). Red-once, making the strict arm unconditional again: FieldOwnershipTest.cpp:867: Failure / Actual: false / signal 6.

5 · ID-118. PipeCalls.def: kWaitNone → kWaitApplied. Chosen because the row posts no reply slot (so there is nothing to wait for but the apply, and gen_pipe.py refuses a kWaitReply row without a slot); because copy_framebuffer_to_texture — same kBlitOrCopy class — already carries that wait; and because glCopyImageSubData is zero per frame in the measured scene. PipeCatalogueTest's census moves in the same commit (9 → 10 applied, offset 24 → 25). Red-once, reverting the row: the allowlist loses @CopyImageSubData and PipeCatalogueTest goes red on three counts. After ID-125 the pair is admitted by disjunct 2 as well, so the allowlist alone no longer sees the revert — I restated the unit assertion to check the wait class itself beside the admission. Both disjuncts hold independently, as ID-128 asked me to confirm.

6 · ID-119 / ID-115. (a) split_log_paths.py gains marker_log_paths() and a markers mode driven from ctest's own map: 113 of 113 log-declaring entries read, against 90 for the old split-logs/ grep — the missing 26 were the F1. block, both NamedBlit pairs, the four Ct. entries and PersistentMapArm, i.e. the readback population the allowlist is about. The CI step resets those logs first (a stale file reads as this run's evidence) and runs the census in both arms. The two-sided ratchet is markers + Harness/strict-expected-markers.txt: unadmitted marker fails; ADMITTED outside --print-admitted fails; a pair not in the expected set fails; and an expected pair that no longer appears fails, so the lane cannot rot green. (b) The old rsp check grepped for a counter no entry in that directory emits, testing a sentence true of every possible implementation (the unbarriered arm is [[noreturn]] and runs before ++g_residualPulls). Replaced by an assertion where the counter is: DirectGLES.Split.StrictArming. (MOBILEGL_PIPE_STATS=1, PERIOD=1, own log path, RESOURCE_LOCK), asserting rsp == 0 || rsp >= draws — the device evidence's shape. Measured window: draws=1 … rsp=3 vbs=3. (c) New push-only counter vbs (ServerVerbBoundaries) at MGPipeServerStampVerbBoundary behind Enabled(); the same entry asserts vbs > 0. Deliberately not rsp: rsp reaches zero when the phase succeeds, so a control built on it would fail on the day the debt is paid. The entry skips loudly in the monolith arms and in any lane that is not the counting one (MGITEST_STRICT_ARMING_LANE=1); without that second guard it is discovered by TriangleScenario.* in three other blocks and asserts on a summary line nobody enabled. The CI step also fails if the arming entry did not run — a positive control that can be skipped away is not one.

7 · Magma's lane. Why the split and not admitting the row: FieldOwnership.def carries one retiring-phase string for both backends, so any rule admitting GetFramebufferBindingSlot@Clear for Magma admits it for Espryt, where it is this phase's debt — turning exactly the regression fb's handle arm prevents into a warning line. The split costs two entries; the admission costs the gate. A per-backend phase column is a FieldOwnership.def schema change, far outside this package. Finding: ctest -L takes a regex, so -L integration-split matches a label spelled integration-split-magma. I built the briefed spelling first and measured it — ctest -N -L integration-split still listed both DirectVulkan entries, i.e. the split looked done in every listing and had not happened. The label is integration-magma-split, which no existing -L matches. The step asserts red and the named marker, so "expected red" cannot decay into "did not run".

8 · ID-120. Struck through in place with a blockquote naming P7/P13 and the argument; code untouched. Measured, not inherited: ID-120 says 10 pairs would change; on this head it is 23 (--print-admitted | grep -c '^GetBufferBindingPointCount@'). The 10 predates ID-125's widening; the note records the command as well as the number.

G1 / monolith impact

vbs and its line are inside #if MOBILEGL_PIPE_PUSH; PipeInputs.cpp and PipeApply.cpp are compiled only under MOBILEGL_PIPE_PUSH (the header gains declarations only, which emit no symbol); PipeFieldOwnership.inc is included only from PipeInputs.h, which its banner keeps out of the pull build's include closure, and nothing there odr-uses the new table; PipeFill.cpp's trip wire is constexpr + static_assert. The one shared-code delta is PipeCalls.def's wait class, and that column is read only on a server publishing kCapRunAheadApply — inert until the integration commit. The flavours step could not serve as the empirical G1 check because it is already red at my base (above), so this argument is by construction rather than by build.

G2/G14: the one new non-fork unit case has its skip twin beside the other seven; the new integration case skips loudly in the monolith arms with its reason recorded; the Wire/ suite is registered only under MOBILEGL_BUILD_DISAGGREGATED, so its case needs none.

Kimi audit rows: not applicable — I migrate no field family and retired no apply-thread read of client memory. Every row I touched is left barriered; that is the whole of items 3-5.

Seams, assumptions, rulings I need

  1. The expected-marker file must be refreshed by the wave-3 integration commit. It carries three ADMITTED-ESCALATED rows that pa/mv may retire; the ratchet will name them. That failure is the ratchet working, not breaking.
  2. MG_IntegrationTest/CMakeLists.txt merge (per your note): my new block is at the very end of the file, after the NamedBlit loop, and is the only thing there; my other change in that file is inside the NamedBlit loop (the LABELS variable). pa's StrictProgramArm and mv's tiers are in the big disaggregated section above, so the three should not overlap.
  3. test.yml: I rewrote the strict step's whole body from ready= down, and added one step before "Upload strict lane logs".
  4. RULING WANTED — "a verb with no stamp row admits nothing". That restriction is mine, not ID-125's, and it is the difference between 174 and 491 pairs. The table is read only from CountBarrierPull, reachable only after MGPipeServerStampVerbBoundary, and the server stamps only MGP_VERB_OP_LIST's ops — so a bit for any other verb is an allowlist row nothing can exercise and nobody can retire. I believe it is right; please confirm.
  5. RULING WANTED — the lane count. ID-128 defines done as 180/180. With item 7 the Espryt lane cannot hold the two Magma entries, so the merged number is 180 − 2 + 1 = 179 on integration-split plus 2 on integration-magma-split. I did not adjust the admission rule to reach 180; wanting 180 on one label means admitting GetFramebufferBindingSlot@Clear globally, which I have argued against in item 7.
  6. I assumed pa does not touch EmittedCallSuppliesTheWholeField (ID-112 forbids it); if it did, item 2's assertion is what says so.
  7. Housekeeping: the flavours step left a gitignored build-push/ in my tree. git status is clean. I ran the census to ~/w7/p5e-logs/strict-census-gl/ and strict-census3-gl/; the shared strict-census/ is untouched.

Is "the strict lane is hard green" now a statement a script can evaluate?

Yes — the mechanism is complete, and every part of it is exercised on this branch. One step now decides all five things the phrase has to mean: (1) every entry completed (ctest rc); (2) no Fatal{ marker anywhere in the lane — over the log set ctest itself declares, so the 26 entries the old check could not see are in it; (3) every Admitted{ marker is one the tables admit, compared against the same table the C++ answers from, so shell and runtime cannot drift; (4) no admitted pair appeared and none silently vanished — the two-sided ratchet; (5) strict was actually armed on something that draws — vbs > 0 on an inproc drawing entry, and the step fails if that entry did not run.

What is missing is not mechanism. It is two rows of debt and one flip:

  • On my head the script evaluates to RED, correctly, naming three rows in three lines: GetProgramForDraw@DrawArrays (52), GetBoundVertexArray@DrawArrays (9), GetProgramForDispatch@DispatchCompute (7) — pa's and mv's, both now merged upstream.
  • The post-flip arm has never executed. It is gated on kMGPipeP5eRunAheadReady, still false, so the no-fatal call is code no CI run has taken. I exercised it by hand; CI has not.
  • The expected-marker set is a measurement, and it is mine, not the merged tree's. The first integration run will fail the ratchet on the rows the merge retires. That is the design — but it means "hard green" is not evaluable by a fresh clone until that refresh lands.
  • One place I read a ruling as intent rather than text. ID-119 says rsp "must equal the admitted-marker tally". It cannot: markers are deduped per (field, verb) per process and rsp counts every pull, so count-equality is not a true statement about either number. I implemented the set equality (which pairs rsp counts — that is what the ratchet checks) and the shape (rsp == 0 || rsp >= draws). If you meant literal count equality, the dedupe has to go, and an 852-draw frame writes 852 lines.