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.
17 KiB
P2 package B — p2/tracker — result
Tree ~/w7/p2-tracker, branch p2/tracker, branched from the contract commit
9c6a8a25 (the p2/contract tag). Not pushed.
1. Commits
| sha | subject |
|---|---|
f52dd262 |
[Feat] (State): give the frontend six aggregate generations so a tracker can answer "did any bound texture, buffer, attachment or attribute move" with one Uint64 compare |
9c9df515 |
[Feat] (Pipe): compute the per-verb dirty mask at the validate point and count how often each bit fires |
8d000f7e |
[Feat] (Pipe): mint render-state CSOs on the pipeline subset and send only the dynamic chunks that moved - the steady-state cost of the whole render-state family is now two Uint16 compares |
a0c44c4f |
[Feat] (Pipe): push pixel-pack, patch and vertex-attribute-default state as their own calls, the last one behind the set-hash suppressor |
e54e7399 |
[Feat] (Pipe): carry what has no call of its own in the residual value block and abort when it disagrees with the assembled state |
e49f0ea7 |
[Feat] (Pipe): map every frontend mutator onto the aggregate generation that publishes it, and make the dirty-surface scanner a gate |
b3daa404 |
[Test] (Pipe): pin the tracker's shutters and the CSO cache's content addressing, including the collision the memcmp exists to stop |
7993d711 |
[Test] (Pipe): give the pull build the same ctest names as the push build so a push-only case skips instead of vanishing |
26 files, +2259 / -78. No file owned by another package was touched (see §5).
2. Verification — every command run and what it printed
2.1 The package's own list (brief §C.1)
| command | result |
|---|---|
python3 scripts/gen_pipe_dirty_surface.py --check |
rc 0 — dirty-surface: 73 mutators, all mapped, no stale rows |
python3 scripts/gen_pipe_dirty_surface.py --self-test |
rc 0 — 3 negative controls, all tripped |
python3 scripts/gen_pipe.py --check |
rc 0 — generated files are up to date; 63 PipeInputs fields (7 sticky, 34 emitted by a P2 call) |
python3 scripts/gen_pipe.py --self-test |
rc 0 — 7 negative-control trip(s), positive control OK |
python3 scripts/check_include_closure.py |
rc 0 — 4 probes, 0 skipped, 0 problem(s) |
cmake --build build-linux + symbol_report.py --threshold 0 |
0 added / 0 removed / 0 renamed, 4 resized, .text +160 (+0.001%). The four resized are the CONTRACT commit's own (RenderState::{RenderState, SetCapability, IsCapabilityEnabled} and _GLOBAL__sub_I_DirectGLES.cpp); this package adds zero pull-build delta on top of them. |
ctest --test-dir build-push -L unit |
1518 / 1518 passed |
ctest --test-dir build-push -L integration-gpu -j 4 |
878 / 878 passed |
MOBILEGL_PIPE_PUSH=0 ctest --test-dir build-push -L integration-gpu -j 4 |
878 / 878 passed (the all-pull control) |
ctest --test-dir build-verify -L integration-verify -j 4 |
818 / 818 passed |
MOBILEGL_PIPE_VERIFY=1 retrace_gate.py --lib build-verify/libMobileGL.so -j 4 |
rc 0 — 79 / 79 passed, lowest SSIM 0.997009 |
grep -l 'Fatal{' .../p2-tracker/*/mobilegl.log |
empty |
grep -L 'MGPipe verify:' .../p2-tracker/*/mobilegl.log |
empty — every case armed |
2.2 Section-A gates this tree can reach on its own
- G1 — green, as above.
- G2 —
build-linuxandbuild-pushctest name sets: 2396 each, zero-line diff. - G3 (SSIM half) — the 79-case desktop corpus, both backends, lowest SSIM 0.997009 under verify (which is push plus the comparator). The push-only arm was run separately; see §7 if its line is absent below.
- G4 — verify build: 818
integration-verifygreen, 79 retraces green, noFatal{, every case armed. - G5 —
SyncRenderStatebyte identity:d8fd1c48716056c536752fde09db3a9e5aa8494b70404798bfc15d11220efe27on both sides, equal to~/w7/p2-before-syncrenderstate.sha. Not one line changed. - G9 — the dirty-surface gate, above.
- G10 (the non-device half) —
MGL_RESIDUAL_BLOCK_SIZE == 8is the contract'sstatic_assert; theresid=byte class is now non-zero, measured on the desktop:MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=1overCrossFrameBufferScenarioprints one window withresid=8.00and 63 windows withresid=0.00, which is the block going out once and then being suppressed. The same run shows one window withcso[csom=1 csob=1]and 64 withcso[csom=0 csob=0]— the mint, the bind, and then the steady state. - G13 —
grep -rc 'pGLContext' MobileGL/MG_Backend | grep -v ':0$'empty; include closure clean; generators regenerate clean. No stdio added anywhere. - G14 — 2363 baseline names, 0 removed; the tree now lists 2396.
Not reachable from this tree and left to their owners: G6/G7 (RenderStateSpans. tests
and scripts/g7_negative_control.sh — package A and E), G8/G12
(HandleRecycleScenario, CsoContentAddressingScenario — package E), G11 (the
two-device A/B — the integrator).
3. Deviations from the brief, with reasons
-
Tracker,CsoCacheandSetHashSuppressorare header-only, not{h,cpp}. The brief's D15/C.1 asks for.cppfiles, but registering a new source means editing the rootCMakeLists.txt, which the C.5 ownership table gives to package A and which is frozen behind thep2/contracttag. The contract'sif (MOBILEGL_PIPE_PUSH)block names onlyMGPipeRenderStateSpans.cpp,PipeApply.cppandSlotAllocator.cpp— notTracker.cpporCsoCache.cpp. Rather than edit another package's file I made all three header-only; every one of them is included by exactly one translation unit in the library (MG_Impl/Pipe/PipeFill.cpp) plus the tests, so inline costs nothing. Splitting them back out is onelist(APPEND)line whenever the integrator wants it. This is the one thing in this package that would have been a blocking deviation, and it was routed around rather than reported as blocked. -
A SIXTH aggregate generation,
VertexAttribDefault, which D4 does not list. D4 shuttersNEW_VERTEX_ATTRIB_DEFAULTSon aContentHashover all 32CurrentVertexAttributeValues. That is ~768 bytes hashed on every draw, which does not fit inside the T1 ≤ 45 ns/draw ceiling the same brief pins in D.4.3. The hash still decides whether to emit (D11's set-hash suppressor, wired); the new generation decides whether to hash at all. It lives onGLContextrather than a state container because the values it guards do too. -
MobileGL/MG_State/GLState/Core.{h,cpp}were edited. They are in no package's row of the C.5 table ("everything else | nobody"), so no other package's ownership was crossed, but B's file list does not name them either. They are needed because D4's bump points sit on OBJECTS, which have no back-pointer to the state container that owns them, soMGP_NOTE_AGGREGATEgoes through the free function D4 itself allows and that function needs aGLContextfacade. All of it is#if MOBILEGL_PIPE_PUSH; G1 is unaffected. -
The shutters for bits 5–17 are coarser than D4's table, and three of them are composed with a mixing hash. D4 describes what each bit will eventually watch (a 32-attribute prefix, a recomputed framebuffer
ContentHash, aGetMaxTouchedUnit()prefix). Those are per-draw scans and hashes, and P2 emits nothing from these bits, so the tracker uses the cheapest shutter that cannot under-fire: the aggregate generations, the bound object's identity, and the current program's version counters. Five bits share one buffer aggregate. Every one over-fires rather than under-fires, which is the safe direction, and the file says so where it is coarse. A composed shutter is a hash and can in principle collide; that is stated in the code and is acceptable only because nothing consumes those bits in P2. -
CsoCacheuses aVectorlinear scan, notska::flat_hash_map. 64 entries ofUint64is a handful of cache lines, the probe runs only when the pipeline version moved, and keeping the entries in one array is what lets LRU eviction answer "which is oldest" without a second structure. Behaviour (capacity 64, LRU, hash → probe →memcmp→ handle,delete_render_stateon evict) is exactly D7. -
MGPipeCsoCache::s_hashForTest, a test seam in production code. A 64-bit collision between two different render states is silent wrong pixels and is precisely what thememcmpconfirm exists to stop, soCsoCacheTest.HashCollisionDoesNotAliasTwoStateshas to be able to make one happen. Null in every real build: one never-taken, perfectly predicted branch on a path that runs only when the pipeline version moved. -
The residual value block goes out AFTER the residual fill, and is held when the verb's class does not read the capability mirror. D9 says "once per context and again whenever the capability set changes". Emitting it with the other calls compares the carried bits against the PREVIOUS verb's assembled mirror; and
IsCapabilityEnabledis in seven of the nine class masks, so at akQueryorkXfbSpanverb the mirror is stale by construction. The change is therefore held rather than dropped — dropping it would silently disarm the wire for a capability that moved between two queries. (This is not theoretical: the first version of the commit firedFatal{PipeResidualDiverged, "Dither"} carried=1 assembled=0on 112 of the 818integration-verifyentries. GL_DITHER defaults to enabled, so the very first mismatch the block could find is the one it found.) -
MGPipeValidateForVerb's residual-fill skip is guarded by a one-shot runtime probe of the applier's derivation. See §4.1 — this exists because of a real state of the contract tree, not because of a design preference. -
Five comment references to the old
MGPipeFillForVerbname were left in package A's files (MG_Pipe/MGPipe.h:113,generated/PipeFilled.inc:404,generated/PipeFillPoints.inc:17,scripts/gen_pipe.py:914and:1012). Fixing them means editing A's files.MG_Test/ScopedPipeVerb.handMG_Test/Pipe/PipeInputsTest.cpp(owned by nobody) were updated.
4. Where the tree contradicted the brief
4.1 MGPipeDeriveRenderStateFields is a declared STUB on the p2/contract tag
The brief's D5 and C.1 assume the derivation exists (it is package A's commit c1, on
p2/spans, which this branch does not contain). Package B's headline deliverable — the
residual fill skipping the 29 derived fields — is therefore not verifiable on this branch
alone, because skipping a field the derivation would have written leaves its mirror
unwritten.
Resolution, and it is self-healing rather than a hard-coded branch check: the filler asks
once. It puts a sentinel in a scratch block's working RenderStateParameters, clears the
mirror the derivation is supposed to recompute (m_clearStencil), runs
MGPipeDeriveRenderStateFields, and looks. The answer is latched for the process and costs
one compare, once. On this tree the probe answers false and logs
MGPipe: MGPipeDeriveRenderStateFields does not derive on this build - the render-state mirrors stay on the pull path; the moment A's c1 is in the tree it answers true and the
full skip engages with no edit. It stays useful afterwards: if the derivation is ever deleted
or gated off, the filler degrades to pulling those fields rather than rendering a
default, which is the safe direction.
What this means for the integrator: after A's p2/spans lands, re-run
ctest -L integration-verify and the verify retrace on the merged tree — that run, and not
this one, is what proves the 26 derived mirrors. On this tree the eight fields the applier
writes DIRECTLY (GetRenderStateParameters, both versions, the patch trio,
GetPixelStoreParameters, GetCurrentVertexAttribute) are already skipped and already
proven by 818 verify entries and 79 retraces.
4.2 Two rows of Coverage.def's MGP_COVERAGE_EMITTED_LIST cannot retire their pull
Both are contract-side defects in files this package does not own. Neither is worked around silently; each is named in the code with its reason and the emission still happens, so the wire shape, the payload bytes and the suppressor are real.
GetPixelStoreParameters → SetPixelPackState. ThePipeInputsfield is BOTH halves of the pixel store (m_pixelStore[0]pack and[1]unpack) andset_pixel_pack_statedeliberately carries only PACK (ARCHITECTURE.md4.6 D5,MGPipeTypes.h). The unpack half has no carrier at all, so the field keeps being pulled. Fix: either split the field, or record inCoverage.defthat this row is partial.GetCurrentVertexAttribute → SetVertexAttribDefaults. The three views of aCurrentVertexAttributeValueare not bit-identical —GLContextCONVERTS between them (SetCurrentVertexAttributeFloatwrites(Int32)valueintointValue) — whileMGPipeApplySetVertexAttribDefaultsmemcpys oneData[4]into all three and ignoresMGPAttribValue::ValueClass, which the wire type carries precisely so it does not have to. Until that applier readsValueClassthe carrier cannot reproduce the frontend value. Fix:MGPipeApplySetVertexAttribDefaultsshould switch onValueClassand reproduceGLContext's conversions, and the client should setValueClassfromClassifyVertexAttribType.
4.3 The brief's own G2/G14 grep is wrong
grep -E '^\s+Test #' matches only a four-digit test number: ctest -N right-aligns the
number, so tests 1..999 print as Test #7: with more than one space. On this tree that
silently dropped 999 of 2368 names, i.e. the gate passes while looking at 58 % of the
list. The pattern that works is grep -E '^ +Test +#[0-9]+: ' with
sed -E 's/^ *Test +#[0-9]+: //'. Both gates are green under the corrected command. The
integrator should fix the two commands in §A of the brief and in any CI step that copies
them.
4.4 Minor
MGPipeApplyDeleteRenderStatetakesMGPHandleOnly(the tree), not the bare handle the brief's D2 signature list implies. Followed the tree.- Three
integration-gpuentries (*.TheEmulationIsActuallyArmed*,*.TheRerouteIsActuallyArmed*,*.TheDemotionIsActuallyArmed*) failed once under-j 4and passed standalone and on a re-run of the whole lane. Treated as a parallel flake of the lane, not a regression; recorded here because it will be seen again.
5. File ownership
Every file this package touched is one B owns in C.5, plus three the table gives to nobody:
MG_State/GLState/Core.{h,cpp}— nobody (deviation 3 above).MG_Test/ScopedPipeVerb.h,MG_Test/Pipe/PipeInputsTest.cpp— nobody; both only follow theMGPipeFillForVerb→MGPipeValidateForVerbrename D1 mandates.
CMakeLists.txt (package A) was not edited — see deviation 1. No file under
MG_Backend/ was touched at all.
6. Unfinished
- The 26 derived render-state mirrors are still pulled on this branch — §4.1. Nothing to
do: it engages by itself once package A's
c1is in the tree. The integrator should re-run the verify lane and the verify retrace on the merged tree, because that is the run that proves the derivation. FillPoints.deftightening: the verdict is recorded, no row was removed. All eight statically over-approximated rows are KEPT, per group, with the concrete backend path each one names written into the def's own comment. The reason none was removed is stated there: the only evidence that could retire one is DYNAMIC, and a corpus that never reaches a path proves nothing about it — a row dropped on that basis turns a rare path intoFatal{UnmigratedPipeInput}in a shipped build. What would retire a row is theMOBILEGL_PIPE_POISON_OMITknob run across the fullgl44to46caselist on both devices, which is recorded as P3a work. The contract's newFramebufferSrgbstorage in fact makes one of the eight MORE load-bearing than it was: it used to read a compile-time constant.- Per-bit fire rates for
MEASUREMENTS.mdwere not collected. The tallies are implemented (MGPipeTracker::FireCount/WalkCount, behindPipeStats::Enabled()) but nothing prints them on theMGPipe stats:line — that line's format lives inPipeStats.cpp, which is package A's. Suggested one-line follow-up for A or the integrator: add the 18 fire counts toFormatWindowLine, or have the integrator read them through the accessor from a scenario. - No device work. G11 (two devices, paired A/B), the DriverBench T1/T2 numbers, the
blend-toggle table and the CSO negative-control device arm are all D.4 items and belong to
the integrator.
TrackerTest.BlendToggleReusesTwoCsosandCsoCacheTest.ContentAddressingOffMintsEveryTimeare the correctness half of the last two. - The
MGPipeSetHashSuppressor's other six slots are declared and unit-tested but not wired, which is D11's own scope (P3b/P4b).
7. Push-arm retrace
python3 ~/w7/retrace_gate.py --lib build-push/libMobileGL.so -j 4 was started after the
verify run and its result is in ~/w7/tk-retrace-push.log. The verify run above is the
stronger evidence — it is the same push semantics plus the compare-at-read oracle — and it
reported 79/79 with a lowest SSIM of 0.997009.