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

15 KiB

pg — programs (P5e wave 2), v1

Branch p5e/pg, head 4d1499ef, base f6cfcbd3. 3 commits, 18 files, +2071/-316. Gate: build 0 · unit 2210/2210 · integration-split 111/111 · gens all rc=0 · one on the six named scenarios 212/212. Strict lane run before AND after (§5).

1 What changed, per file

file region what
ProgramArtifactsCodec.{h,cpp} header note, :242 note, +55 tail names the skipped member + reads the libc++ caveat (§2); ProgramArchive, kProgramArchiveMaxStages, Encode/DecodeProgramArchive — the FRAME [stageCount][stages][codec stream], bounded before it multiplies, refusing a stage count that disagrees with the module count
MG_Pipe/PipeApply.h :41-52, :384-420, :1327-1345 ProgramArchive fwd-decl; MGPipeShaderCsoRecord::Archive; MGPipeApplyCreateShaderState's 4th argument
MG_Pipe/PipeApply.cpp :2854-2918, :3000-3060 MGPipeApplySetProgramBindings's body; the create ADOPTS the archive and clears the three tails + Signature, advancing BindingsSerial
MG_Pipe/PipeRoute.{h,cpp} escape table + 2 sites the FIFTH escape (§7.3); CreateShaderState grows the stage list; MGPipeRouteSetProgramBindings
MG_Remote/Client/WireTables.cpp :488-600 the create frames the archive and counts its bytes into PipeStats; new Wire_Escape_SetProgramBindings (three tails, per-name SEG_STAGE staging)
MG_Remote/Wire/PipeWireCodec.cpp the two arms decodes onto the heap into the record's own archive; op 80 resolves and NUL-checks every name span and calls the applier
MG_Impl/Pipe/ProgramEmit.h :90-110, :106-180, :215-330 MGPipeStorageOverrideSignatureEntry; EmitProgramBindings + latch; Desc.LinkStatus; the stage list; the bindings latch cleared on re-issue
Managers.h :2341-2480 new + 6 signatures ProgramArchiveSource, ProgramBuildSource, ProgramHandleArm(), m_syncedBindingsSerial, ProgramBlockBindingFromRecord / ProgramSamplerUnitFromRecord
Managers.cpp :745, :10913-11060, :11123-11370, :11907-13400 the source's two constructors and its accessors; every helper of the build re-typed; SyncToBackendFromSource + the two public heads; the seam stub deleted
DirectGLES.cpp :4097, :4230-4620, :4800-4830, :5460-5800, :5900-6030, :6400-6430 the handle stash; ResolveGlobalConstantsRecordForHandle + the MapUBO refusal; ResolveFragColorBroadcastCount's policy; SyncCurrentProgramByHandle; both Prepare lines; BindCurrentProgramWithResources; GetCurrentBackendProgram; CurrentProgramMayNeedPerSubDrawBuiltins
tests ProgramEmitTest +4 cases; 5 files mechanical §7.5

The one-body decision (G1). SyncToBackend's ~1070-line body is written ONCE against a reference src whose TYPE is PrgramImpl::ProgramBuildSource — ProgramArchiveSource in a push build, ProgramObject in a pull build. The view's accessors carry ProgramObject's names one for one, so the pull build compiles the same text through the same accessors. The alternative was an #if-duplicated 1100-line body. Exact pull deltas: §6.

2 Codec completeness (CONTRACT-P5E §5.5)

The skipped member is LinkArtifacts::program, the live SharedPtr<glslang::TProgram> that VisitFields omits (57 of 58). Proven unread by either twin, by closed grep: every read in the tree is inside ProgramLinkTask.cpp (DoReflection) plus the three reset() sites; MG_Backend/ contains none, and ProgramTranslationCache asserts it null at insert. It could not be carried in any case — it points into a glslang arena no archived instance owns. ProgramArtifactsCodec.cpp:242's "libc++ caveat" is about the struct-size ECHO, not a member: under the NDK's unpinned branch the word is 0, which is exact for an in-process split (one build writes and reads) and becomes a real check at P6's process boundary. Both verdicts are written where they were asked for.

3 Kimi audit rows 91-110

Retired (record/handle answers them now): 93 (Desc.LinkStatus/SpirvStatus), 94 (ResolveProgramTwin), 96 (record.Serial/BindingsSerial), 97 (record.Signature), 98 (SyncToBackendByHandle), 99 (ResolveGlobalConstantsRecordForHandle), 101, 102 (Desc.GlobalUboSize/GlobalConstantsVersion), 103 (MapUBO is Fatal now, gap G-C), 104 (ProgramBlockBindingFromRecord), 105 (BindingsSerial + ProgramSamplerUnitFromRecord), 107, 108, 110 (handle arm; kept as the monolith stamp).

Left pulled, and why: 91/92 (GetProgramForDraw/ForDispatch — my sync no longer reads them, but the accessor CALL stays for vi's and tx2's consumers, §4.3); 100 (BindCurrentTextures' memo — tx2's line range, §4.1); 106 (vi's); 95 (MOBILEGL_PIPE_LEGACY_MEMOS, unreachable under a transport); 109 left barriered BY CONTRACT §5.5 — GetBackendProgramId is a GL-thread entry point (P9) and ShaderStorageBlockBinding is reached from the barriered set_storage_block_binding (ID-85, pg's trailing item).

Not found: none — all twenty rows exist at this head.

The five scope sites → three retired, two kept. ResolveGlobalConstantsRecord, SyncCurrentProgram and BindCurrentProgramWithResources are unreachable under a transport (the scopes stay as the monolith glue's, ruling 1); the other two are row 109's contract exceptions.

4 Red-once evidence (executed, quoted verbatim, reverted)

(1) Empty the applier's block-binding tail. The two named scenarios are NOT in integration-split (they are DirectGLES.*, the monolith lane), and with the tail emptied the split lane stays 111/111 green — no split scenario binds a named uniform block to a non-zero point. The statement moved to the unit case the brief also names:

../MobileGL/MG_Test/Pipe/ProgramEmitTest.cpp:894: Failure
    Which is: 0
    Which is: 3
[  FAILED  ] ProgramEmit.TheThreeBindingTailsAreWholeSetsAndAReIssuedCreateDropsThem

(2) Blob refs at Size 0, companion pointers removed (decoder passes nullptr,nullptr,nullptr); Split.TriangleScenario.AVboBackedTriangleReachesReadPixels aborts:

[Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{ProtocolCorruption} create_shader_state {slot=1, gen=0}: the record declares no blobs and carries no artefacts (stages=0x11, globalUboSize=0, bound 16777216)

(3) The frontend-keyed resolution back, id's hook forcing "unbarriered". PrepareForDraw routed to SyncCurrentProgram + MGPipeApplierSetCurrentRecordBarriered(false) at its top — ID-102 kept: the probed action IS the registry lookup, not an object built on the apply thread.

[Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{RoleViolation, "MGPipeSlots"} - the apply thread called MGPipeSlots().HandleOf. With an active transport the client slot allocator is client-only memory (CONTRACT-P5C §3.1, rule E; CONTRACT-P5E §4.4): a handle arrives already minted in a record, and a server that resolves or mints one off a frontend object's lifetime id is reading memory that will not exist on its side of a real split. A named exemption scope admits it only while the record being applied is BARRIERED (barriered=0) and, for Magma's P7 debt, only on a DirectVulkan server

(4) strict names GetProgramForDraw@draw_vbo — NOT DEMONSTRABLE AT THIS HEAD, and the reason is structural rather than a miss: see §5. The marker is in NEITHER table. (5) The global-constants record declines under run-ahead (what forcing bit 12 off does to the record arm); 8 split entries abort:

[Linux mgl-srv-apply/FATAL]: MGPipe: Fatal{UnmigratedVerb, "set_global_constants"} - the ShaderCso record {1, 0} does not answer the default uniform block for a draw of a program that declares 16 bytes of it, and MapUBO() is the client's live scratch array

ID-87's measurement — numbers, not an argument

EncodeProgramArchive instrumented at the one place the bytes exist; whole split lane, 80 links, then reverted:

us:    min=3.2  p50=4.5  p90=5.4  max=6.4  mean=4.6
bytes: mean=2516, max observed 6070

PipeStats over TriangleScenario.* independently: cso-blob-bytes = 4538 for 2 links. Links per steady frame = 0 — the re-issue latch is GetLinkVersion(), which only a real glLinkProgram moves, so the cost lands on loading frames. Slope ≈ 1 µs per KB, so ruling 8's 0.2 ms/frame trigger needs ~200 KB of archive linked in ONE frame (~33 of the largest measured).

The half ID-87 actually asked for — MC + a shader pack on the Redmi — IS NOT DONE: no device here and adb is forbidden by the package instructions. These are lavapipe with the lane's small programs, and I will not extrapolate a ruling trigger from a 6 KB sample. Blocker for declaring ruling 8 settled: run the device arm at the phase gate, or land lazy-encode as a precaution.

5 The strict lane, before and after

Run on this tree at f6cfcbd3 and at 4d1499ef. Both: 108/111 failed, 3 passed; 88 of 111 entries keep a private log (the ID-53 baseline saw 22 — the per-entry log path now covers the whole lane, so this is a wider slice than STRICT-BASELINE-2fde7034.md's).

marker before after 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 tx2/fb (trailing)
GetProgramForDispatch@DispatchCompute 3 3 pg, blocked on tx2 (§4.3/§7)
ValidateProgramName@ShaderStorageBlockBinding 2 2 pg, allowlisted trailing (ID-85)

IDENTICAL, and that is the honest result. Each entry aborts on its FIRST pulled row, and in every entry of this lane a framebuffer, VAO, texture or image row precedes any program read — so no program field of mine was ever first, none could disappear, and none surfaced behind one. GetProgramForDraw@draw_vbo appears in neither. This lane will only measure this family once fb, vi and tx2 have landed; what pg can show is the three executed Fatals above.

6 G1 / monolith impact — the deltas, named

Every new symbol is under MOBILEGL_PIPE_PUSH / MOBILEGL_BUILD_DISAGGREGATED. I could not run the comparator (symbol_report.py needs pull builds of base and head; the gate script has no G1 step). The deltas the pull build WILL see, from the diff:

  1. SyncToBackend(const SharedPtr<ProgramObject>&) keeps name and signature; its body gains const ProgramBuildSource& src = *stateProgramObject; and every read becomes src.X(). Same loads, same inlining expected — an expectation, not a measurement.
  2. CacheResourceLocations: (const SharedPtr<ProgramObject>&) → (const ProgramObject&) — mangled-name change + a dropped SharedPtr deref.
  3. RebindImageUniformsToFrontendUnits: likewise, and it loses one null test the caller already made.
  4. The other six helpers keep their signatures exactly (ProgramBuildSource IS ProgramObject there).

Expected pull report: 2 renamed + 2 resized, 0 added, 0 removed, all in one function family. Ruling wanted: accept those four, or ask for the #if-duplicated body. I recommend accepting — the duplicate is 1100 lines two arms would have to be kept in step by hand for ever. Push + Transport=monolith keeps its frontend arms through ProgramHandleArm() = Transport != Monolith && ProgramSubsystemEnabled().

7 Seams, assumptions and rulings needed

  1. tx2 — BindCurrentTextures' memo key (ruling 6 / ID-86). Its range, so I left a comment on ResolvedTextureBindingMemo instead of editing. What I expect: the four program fields replaced on the handle arm by {DrawProgram.Slot, .Gen, record Serial, BindingsSerial}, AND-ed with tx2's SamplerViewsSerial. Both serials are needed — a relink with unchanged texture resolution still hands the twin new unit assignments.
  2. tx2 — ResolveAndBindUnitTextures/sampledTargetForUnit (:4834-4948). BRIEF gives the range to tx2, §5.5 gives the function to pg; I left it to tx2. PrgramImpl::ProgramSamplerUnitFromRecord(record, location) and ProgramArchiveSource's uniform-type accessors are public for it.
  3. vi + tx2 — GetProgramForDraw/ForDispatch. :4781 and :6126 are my lines but their consumers are vi's SyncCurrentVertexAttributeValues and tx2's BindCurrentTextures; the call can only become conditional once both stop taking a SharedPtr<ProgramObject>.
  4. sb — the UBO loop. I own :5463 (the block binding, from the record's first tail) and left :5464-5497; binding is still a Uint in the same index space.
  5. Files touched outside my listed scope, all mergeable: PipeWireCodec.cpp (op 80's own case — PipeApplier.cpp has no OnSetProgramBindings; every non-verb row's dispatch lives in ApplyChecked, so the task file's pointer is an estimate); PipeCatalogueTest.cpp and RemoteClientTest.cpp (escapes 4→5, total 38→39 — op 80's gMGPipeContext slot STAYS null, so those pins are unchanged); PipeWireCodecTest.cpp (the create stages the FRAMED archive); ResourceEmitTest.cpp/CompositeResolverTest.cpp (the create's 4th argument, nullptr).
  6. RULING: the emission ORDER is the opposite of the draft's note. MGPipeTypes.h's MGPProgramBindings comment says "Emitted BEFORE create_shader_state". That is wrong against the applier the same contract specifies: a re-issued create CLEARS all three tails, so bindings published ahead of it are wiped. I emit them AFTER the create at the same validate point and said so in both files. That c0e comment still says the wrong thing and needs a one-line fix.
  7. RULING: one signature authority per arm — the handle arm stamps and compares the CLIENT's record.Signature verbatim, so the two commutative hashes never have to stay bit-identical. Confirm, or the equality needs a test that can reach both (nothing in MG_Test can today: MG_Backend includes nothing from MG_Impl).
  8. MGPipeApplyCreateShaderState grew a fourth argument, undefaulted, on PipeRoute.h's stated reasoning; five call sites moved, three in other packages' test files.
  9. ID-87 is not settled (§4).

8 What I did not do

  • set_storage_block_binding by ShaderCso — the TRAILING item, explicitly not this package (ID-85); ShaderStorageBlockBinding's monolith body is kept verbatim.
  • BindCurrentTextures' memo and ResolveAndBindUnitTextures — tx2's ranges (§7.1-2).
  • The two GetProgramFor* accessor calls — blocked on vi and tx2 (§7.3).
  • The composite uniform-mirror fire rate (BRIEF §5) — still unmeasured. The mechanism is the latch on (backendStateVersion, blockBindingVersion) and both setters' equality bail-outs, so a mirror re-setting unchanged values emits nothing; that is the mechanism, not a measurement.
  • The G1 comparator run (§6) and ID-87's device arm (§4).