Commit Graph
3210 Commits
Author SHA1 Message Date
swung0x48 fec49a54d8 [MG_Backend, MG_IntegrationTest] (Disaggregated): P5f fm - apply bufferless draws and compute from server records 2026-09-19 22:34:28 -04:00
swung0x48 a6223c6509 [MG_Backend] (Disaggregated): P5f fm - order wire image writes and preserve implicit alpha 2026-09-19 22:34:04 -04:00
swung0x48 d9fc45bf4c [MG_Backend] (Disaggregated): P5f fm - use shared image access constant 2026-09-19 22:32:57 -04:00
swung0x48 f37380d0dc [MG_Backend] (Disaggregated): P5f fm - resolve uniform descriptors from server records 2026-09-19 22:32:10 -04:00
swung0x48 2abc561721 p5f fm: cover GPU texture writes and view subresources 2026-09-19 22:29:36 -04:00
swung0x48 277d1a07f0 [MG_Test] (Disaggregated): P5f fm - pin server program overlays and identities 2026-09-19 22:29:33 -04:00
swung0x48 867c5c5efd [MG_Backend] (Disaggregated): P5f fm - expose archive stage and capture-mode queries 2026-09-19 22:27:51 -04:00
swung0x48 31795675f1 p5f fm: build Vulkan samplers from server-owned values 2026-09-19 22:27:43 -04:00
swung0x48 62bf26bf8c [MG_Pipe] (Disaggregated): P5f fm - admit registered server object consumers independently of buffer ops 2026-09-19 22:27:03 -04:00
swung0x48 6ae5944831 [MG_Backend] (Disaggregated): P5f fm - build Magma programs from server archive views 2026-09-19 22:26:23 -04:00
swung0x48 a6283f40f2 [MG_Backend, MG_Impl] (Disaggregated): P5f fm - map texture view attachments through wire identities 2026-09-19 22:23:05 -04:00
swung0x48 4b3afe7840 [MG_Backend] (Disaggregated): P5f fm - retain blit scissor clipping across wire arm 2026-09-19 22:20:53 -04:00
swung0x48 911cc74a57 [MG_Backend, MG_Impl, MG_Remote, CI] (Disaggregated): P5f fm - connect family records and preserve GPU texture writes 2026-09-19 22:18:24 -04:00
swung0x48 20bbf9647e [MG_Backend, MG_IntegrationTest] (Disaggregated): P5f fm - carry framebuffer copies and exercise Magma wire verbs 2026-09-19 22:11:38 -04:00
swung0x48 4593719a3c [MG_Backend] (Disaggregated): P5f fm - share native depth readback and retain masked clear behavior 2026-09-19 22:01:44 -04:00
swung0x48 036c3a3d5e [MG_Backend, MG_Pipe, MG_Remote] (Disaggregated): P5f fm - route framebuffer verbs through server records 2026-09-19 22:00:14 -04:00
swung0x48 418c765c58 [Docs, MG_IntegrationTest] (Disaggregated): P5f f1 report - the census ratchet's first version (6 pairs), CONTRACT-P5E section 3.2 marked landed, and the gate numbers 2026-09-19 10:39:40 -04:00
swung0x48 cfcc12f73a [MG_Backend, MG_Impl, MG_Remote, MG_Test, MG_IntegrationTest, CI] (Disaggregated): P5f f1 - the dual-block rehearsal: per-role PipeInputs, the MOBILEGL_IPC_ROLE_SPLIT_STATE knob, and the named-Fatal arm that makes the red a census 2026-09-19 09:59:33 -04:00
swung0x48 e75e00cb7a [Docs] (Disaggregated): P5f f0 - the eight-way read-only census reports
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.
2026-09-19 09:08:56 -04:00
swung0x48 8b68b92c76 [Docs] (Disaggregated): P5f section 1 - the pointer fact, and one draw's full lifecycle through gPipeInputs
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.
2026-09-19 06:02:28 -04:00
swung0x48 465656843b [Docs] (Disaggregated): P5f - every piece of state on the wire, and P6 is blocked until it is
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.
2026-09-19 05:32:38 -04:00
swung0x48 7f1d2134c3 [Docs] (Disaggregated): P6 contract draft rewritten in concise wording, 269 -> 181 lines, same rows 2026-09-19 04:39:12 -04:00
swung0x48 e125b75655 [Docs] (Disaggregated): P6 plan rewritten in concise wording, 282 -> 124 lines, same content 2026-09-18 23:40:26 -04:00
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
swung0x48 edd0fef5cd [Docs] (Disaggregated): P5e closed - the exit gate run item by item, with one named outstanding item
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.
2026-09-18 21:54:11 -04:00
swung0x48 a6c51ca73c [CI, MG_Test] (Disaggregated): P5e ID-122 - E1's two masking defects, and what is actually wrong with it
BRIEF-P5E section 3 item 4 calls for three negative controls. Running them showed E1
(MOBILEGL_IPC_VERB_BARRIER=0) had THREE problems stacked, with the first two hiding the third.
This fixes the first two and narrows the third into something executable.

1. It selected a case that can never run in this lane.
   E1's selection regex is (Triangle|ClearThenReadPixels), and the positive control gl added
   under ID-115, TriangleScenario.TheServerStampedAVerbBoundaryOnThisDrawingFrame, matches it.
   That case reads a PipeStats window out of the lane's OWN private log, so it runs only in the
   strict-arming lane and skips everywhere else BY DESIGN, whatever this knob is set to. ID-62
   makes a SKIPPED SELECTED entry a hard failure - correctly, because a control that silently
   loses its subjects proves nothing. So E1 reported "the knob killed the pre-flight, not the
   entry" while the knob was in fact working: four other selected entries went red with the
   abort in the same run.
   The fix excludes a case that can never run here, rather than teaching E1 to tolerate skips,
   which would have thrown ID-62 away. ctest -R is POSIX ERE with no negative lookahead, so
   run_control gains a fifth positional: a real ctest -E.

2. ctest and the log helper disagreed about who was selected.
   run_control passed -E to ctest, but split_log_paths.py still filtered on -R alone, so it
   counted the excluded entry as selected and then reported it as "did not run". The helper now
   reads the same exclusion (SPLIT_LOG_EXCLUDE). An env var rather than another positional
   because the helper's positional layout differs per mode - and a control whose two halves
   disagree about its own subject set is exactly the class of bug this script exists to
   prevent.

3. The real defect. ID-122 was right, and there is now a sharper statement of it.
   With the first two fixed E1 is still red, and red on "10 selected entries did not fail": 4
   of 14 went red, AND Fatal{BarrierViolation} appears ZERO times in the entire run - those 4
   are red for other reasons. The evidence E1 demands has never appeared. The Fatal at
   ClientSession.cpp:878 asserts an overlap OBSERVED BY THE APPLY THREAD, and at
   MOBILEGL_IPC_BATCH_WAITS's default of 1 the barrier is taken at the next residual-reading
   verb, so that overlap does not happen.
   Further: with run-ahead armed, VERB_BARRIER=0 ALSO disarms run-ahead (it is the first
   conjunct of RunAheadArmed(), ID-114), so the arm this knob produces is neither lockstep nor
   run-ahead - it cannot serve as the negative control for either side.
   E1 needs its falsification target REDEFINED, not its threshold adjusted. Left as a named
   outstanding item, recorded in ID-122 and the open items.

E1's counting rule ("every selected entry must go red") is deliberately NOT relaxed here: it
was set to block the "any non-zero exit counts" false green, and loosening it before knowing
what E1 should falsify would only trade one false green for another.
2026-09-18 21:52:59 -04:00
swung0x48 0d747da470 [Docs] (Disaggregated): ROADMAP's P5e row had only its status cell updated; the other three were still the pre-phase plan
The previous commit changed the status cell to "twelve packages landed, run-ahead armed" and
left the scope and exit-gate cells saying eight packages, 19 rulings, "the integration commit
flips kMGPipeP5eRunAheadReady", and an exit gate drafted BEFORE the phase started - several of
whose items later proved unexecutable as written. All three cells are rewritten:

- Scope cell: twelve packages in three waves (including fix1 / pa / mv / gl / ra2), rulings
  ID-80..136, both constants flipped. It also names the largest migration (the per-draw
  GetProgramForDraw) as having changed NO wire structure at all - the record already carried
  the per-link ProgramArchive, and what was missing was a reader that still asked the frontend
  object. Links to P5E-RUNAHEAD.md.
- Exit-gate cell: rewritten into what was actually measured (unit 2256, integration-split
  179/179, integration-gpu 1357/1357, three flavours with each arm asserting its EFFECTIVE
  compile define, strict hard green with zero Fatal, the device at VD32 level with monolith and
  +69% p50 against lockstep), plus what has not been executed (the three negative controls, the
  per-package red-once reruns, E1's control failing its own self-check, G1 still asserted by
  CI), plus the criteria that are now VOID (rsp = 0 is empty by construction, rewritten by
  ID-119; VERB_BARRIER=0 may not serve as the paired control, ID-114, because it also disarms
  run-ahead).
- Debt table, rsp row: P5e rewrote what that number means. It now reads as the hard-green lane
  plus eight Admitted rows, and says why the original criterion was empty - CountBarrierPull's
  unbarriered arm sits before ++g_residualPulls and is [[noreturn]].
- Debt table, unmigrated row: client vertex arrays are a named refusal under run-ahead
  (ID-82) and have their own expected-red lane, integration-clientarrays-split (ID-134);
  staging stays P8's.
- Open question 18 added: the two sides of split are unbalanced (at VD32, apply 11-12 ms
  against client 15.7 ms, with the client ~94% saturated). Moving work from client to apply
  lowers the bottleneck directly - meaningless under lockstep, where the two add serially, and
  a lever only because of run-ahead. It needs a fresh profile first: with the wait gone, every
  tail item's denominator has changed.
- The header status line gains the P5E-RUNAHEAD.md link.

Doc citation check: seven files under docs/Disaggregated, 58 citations, 0 problems.
2026-09-18 21:35:38 -04:00
swung0x48 be345d711c [Docs] (Disaggregated): P5e progress, in full - the landed-content table, open items, ruling index and phase report
- New P5E-RUNAHEAD.md: P5e's phase report, a sibling of P5D-INPROC-PERFORMANCE.md. It carries
  the twelve-package list, the three defect classes and the lesson each one cost (a runtime arm
  no gate covered / build flavours no gate covered, where the FIRST gate added for them was
  itself a false green / a guard that could never fire), the profile and both device tiers, the
  unfinished items, and the next opportunity run-ahead unlocked.
- CURRENT_STAGE_PROGRESS.md: the stage table's P5e row becomes twelve packages + run-ahead
  armed + ID-80..136; new section 2.7, the P5e landed-content table, in the same shape as
  P5c's and P5b's; section 2.5 retitled as historical; two sections both numbered 4 fixed; five
  P5e entries added to the open items; next-step item 1 rewritten from c0e to the evidence the
  close still lacks; the ruling index extended from ID-79 to the load-bearing entries of
  ID-136.
- ROADMAP.md: the P11 row gains P5e's correction - retiring the draw lockstep did NOT need
  gPipeInputs versioning, because that race was in the metadata scalars. The row still holds
  for the general case, and contract section 3.2's per-role stamp storage is still unlanded.
- README.md: the status line gains P5e and run-ahead, and "next is P6" becomes "P6 comes after
  P5e".

Doc citation check: 3 problems, all pre-existing entries in CONTRACT-P5.md, none related to
this change.
2026-09-18 21:30:29 -04:00
swung0x48 45664725f8 [Docs] (Disaggregated): P5e - the render-distance 32 comparison, where inproc draws level with monolith
Section 11's matrix ran at VD12, where BOTH monolith and run-ahead hit the 120 Hz panel
ceiling - so that section answers "can it reach the ceiling", not "which is faster". At render
distance 32 the load rises to ~3550 draws/frame (4.2x) and neither arm is near any cap (the
game's own enableVsync:false / maxFps:260, and the measured max is only 37-71). That is the
comparable round.

- Five runs per arm, interleaved, CPU clocks pinned (checked before AND after, both PINNED),
  GPU held at pwrlevel 0, fan on; 75-150 s in-world settle before a 40 s window - VD32's chunk
  streaming runs far longer than VD12's and reusing the 20 s settle would have counted loading
  as steady state.
- monolith p50 median 58.5 (range 56.3-67.9), inproc run-ahead p50 median 59.9 (range
  56.8-61.1), client CPU 16.19 vs 15.72 ms/frame. Level, run-ahead marginally ahead, and with
  half monolith's spread.
- Against the lockstep it replaces: p50 +69%, client CPU -40%, apply CPU -43%. The heavier the
  load the more it is worth, which is what the mechanism predicts: rendezvous count follows
  draw count, ~900 per frame at VD12 and ~3600 at VD32.
- Both arms are client-thread saturated (~94%), so the client ms/frame column is each arm's
  real bottleneck and the two are directly comparable.

New section 11.1 records an OPPORTUNITY rather than a defect: apply spends 11-12 ms against
the client's 15.7 ms, so the two sides are unbalanced and moving work from client to apply
lowers the bottleneck directly. Under lockstep that was meaningless (the two sides add
serially); run-ahead is what turns it into a lever. Left to a later phase.

options.txt restored from .p5e-backup to renderDistance:12; the device is unpinned, fan off,
GPU back to governed.
2026-09-18 16:00:46 -04:00
swung0x48 8943af73e6 [Docs] (Disaggregated): P5e wave 3 - the strict lane goes hard green, run-ahead is armed, the device matrix is written back
- CURRENT_STAGE_PROGRESS.md gains section 2.6: the four packages (pa / mv / gl / ra2), the
  admission rule's three disjuncts and which real entry forced each one, coverage going from
  116 to 179 entries plus 1357, the race the flip exposed and its root cause, and the finding
  that a hard-green strict lane is necessary and NOT sufficient (ID-132). Code head is now
  25fba0d5.
- MEASUREMENTS.md gains section 11: the device matrix. The section leads with clock pinning
  because the first matrix was run without it and had to be thrown away - monolith's OWN arm
  moved 4.43 -> 7.07 ms/frame between two runs, and the SLOWER run was the COOLER one: the
  governor had dropped policy0 from 2745 to 748 MHz. It records pin_redmi.sh's write order and
  read-back verification, and why pin_device.sh refusing to guess this machine's serial is
  correct rather than a gap. Conclusion: at both pinned tiers monolith and run-ahead each hit
  the 120 Hz panel ceiling and only lockstep cannot reach it.
- ROADMAP.md's P5e row and status line now say twelve packages landed, run-ahead armed.

The phase is NOT closed: which arm is faster ABOVE the panel ceiling is unanswered (this
device cannot hold a higher clock), and BRIEF-P5E section 3's three negative controls and
section 5's per-package red-once reruns have been reordered by ID-121 to after the lane goes
green, so they have not been executed yet.
2026-09-18 14:57:36 -04:00
swung0x48 25fba0d5ef Merge branch p5e/flip into feat/disaggregated (P5e: run-ahead armed) 2026-09-18 14:14:11 -04:00
swung0x48 efe58eac14 [MG_Backend, MG_Pipe, MG_Impl, Docs] (Disaggregated): P5e ra2 ID-136 - retire the indirect-binding pull instead of escalating for it, and withdraw escalation (iii)
- TOUCHING mv's FILE UNDER ID-136, deliberately and by ruling: MG_Backend/DirectGLES/
  MultiDraw.cpp is mv's per ID-113, and the integrator overrode that boundary for this one
  change because the two halves must not exist apart on any head. The boundary has not drifted.
- BoundDrawIndirectBufferId (MultiDraw.cpp) gets the handle arm its neighbour
  ResolveBoundIndexBuffer already had, in mv's own idiom rather than a second one beside it:
  the arm is `MG_Config::Transport != Monolith`, STATED (ID-81) and never inferred from a null
  handle (ID-110); a null handle is "this verb bound no indirect buffer", the same 0 the
  monolith arm returns for an empty slot; a missing backend resource is the named refusal
  Fatal{RoleViolation, "multidraw-indirect-buffer-arm"}, a sibling of mv's
  RefuseMissingIndexBufferRecord, never a quiet fall-back to the frontend. The monolith body
  below it is unchanged token for token. The function moved down beside BoundIndexBufferId,
  which is the same question about the other target.
- WHY A HANDLE ARM AND NOT A MIGRATION: the tier never reads a byte of the client's indirect
  buffer. It binds its OWN scratch command buffer and wants to put back the name that was
  there - a save/restore. With a transport the only other writer of this process's
  GL_DRAW_INDIRECT_BUFFER is DirectGLES.cpp's DrawSyncBit::IndirectBuffer arm, which binds from
  MGPipeApplier().VerbIndirectBuffer and nothing else, so "what was bound" IS "what the verb's
  record named".
- Escalation (iii) withdrawn in the same commit - MGPipeBarriered's clause, the client's
  verb-keyed half in ClientVerbIsBarriered, and the contract row. It was never wrong, it was
  the wrong instrument: there is one draw opcode and the tier is chosen server-side per batch,
  so the narrowest key both roles could compute charged EVERY plain glMultiDraw* on the
  auto -> ext default - a per-batch rendezvous on the arm the phone ships, added to make a lane
  green. THE CHECK ON AN ESCALATION IS "WHICH ARM PAYS FOR IT", NOT "WHICH LANE GOES GREEN";
  the rule is recorded in PipeApply.h beside the three that remain and in CONTRACT-P5E §2.1.
- Red-once D: `if (false && ...)` on the new arm and the entry dies 3/3, deterministically, on
  `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"} [BARRIER-PULLED, UNBARRIERED,
  the client did not fill it, retires in P8 (indirect), P9 (readback), P13 (transfer)]`.
  Its counterpart is this head itself: the escalation is GONE and the lane is green, so nothing
  needed it - and the strict census's admitted set drops from nine rows back to ID-128's eight,
  because the pull no longer happens at all rather than being legalised.
- Gate on the flipped head: unit 2256/2256, isplit 179/179, gens rc=0, lane counts unchanged
  (179 / 1357 / 2 / 2).
2026-09-18 14:10:04 -04:00
swung0x48 f286548010 [MG_Pipe, MG_Impl, MG_IntegrationTest, CI, Docs] (Disaggregated): P5e ra2 - ID-133's escalation, ID-134's lane, and the contract sentence that could not be checked
- ID-133: escalation (iii) in MGPipeBarriered - a draw_vbo with NumDraws > 1 that is NOT
  kDrawIsIndirect is barriered. Espryt's indirect multi-draw tiers reach
  MultiDrawImpl::RunIndirect, whose apply reads the client's GL_DRAW_INDIRECT_BUFFER binding
  (BoundDrawIndirectBufferId, MultiDraw.cpp:87) - GetBufferBindingSlot, retiring in P8 - and
  unbarriered that is Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"} on 18 lane
  entries. Backtrace of the aborting thread, verbatim frames: CountBarrierPull <-
  MultiDrawImpl::RunIndirect <- MultiDrawImpl::DrawElementsBatch <- ServerVerbSink::OnDrawVbo
  <- PipeWireDecoder::DecodeAndApply <- PipeApplier::ApplyOne.
- TWO PREMISE CORRECTIONS, both measured, both in the report and in the contract. (a) There is
  no indirect multi-draw OP: all twenty draw entry points collapse onto draw_vbo, and the tier
  is resolved on the server per batch (ResolveTierForBatch), so the escalation cannot be keyed
  on the tier and its cost is NOT confined to the opt-in tier lanes - a plain glMultiDraw* now
  waits on the auto -> ext default too. NumDraws > 1 is the narrowest wire fact that contains
  the reaching set; kDrawIsIndirect is excluded so Sodium's indirect draw, which resolves by
  handle and pulls nothing, keeps running ahead. (b) The read is a SAVE/RESTORE of a GL binding
  name around the tier's own scratch buffer, not a data dependency - so it can be retired
  outright by giving BoundDrawIndirectBufferId the handle arm its neighbour
  ResolveBoundIndexBuffer already has, and this escalation withdrawn. That is three lines in
  MultiDraw.cpp, which ID-113 gives to mv, so it is reported rather than edited here.
- The client's half is verb-keyed because the fill decision is asked before the record exists:
  {MultiDrawArrays, MultiDrawElements, MultiDrawElementsBaseVertex} is exactly the set whose
  record can carry NumDraws > 1 without kDrawIsIndirect. The inclusion goes that way round on
  purpose - a record the server calls barriered whose fields the client did not fill would be
  an ADMITTED pull of a stale value, which is worse than the abort.
- ID-134: the two ClientVertexArrayScenario entries get the label integration-clientarrays-split
  and an expected-red CI step that asserts the MARKER, shaped like gl's Magma lane. ID-131 heeded
  by COUNTING, not by assuming: -L integration-split 181 -> 179, -L integration-gpu 1359 -> 1357,
  the new label selects exactly 2. The step branches on whether run-ahead is actually ARMED,
  because kMGPipeP5eRunAheadReady is a BUILD constant: on a head where it is false the refusal is
  unreachable and the entries are legitimately green, so an unconditional demand for red would
  fail for the one reason that is not a defect. The client says which world it is in in its own
  log line; delete the green branch when the flip lands for good.
- CONTRACT-P5E §3.5 amended to the code: "outside a barriered fill" rested on the caller's claim
  about the FUTURE, and a guard whose exemption cannot be false on the class it exists for is not
  a guard - it never fired on any of the 70 red entries. The landed rule is "not while the applier
  is inside, barriered fill or not", with the fill sites taking §2.5's forced wait first, twice
  per filling verb because the block is written either side of the emitters.
- AND §3.2 IS MARKED UNLANDED, which nobody had noticed: "the server's stamp is server-private
  … never into the shared block" describes storage the applier owns, and
  MGPipeServerStampVerbBoundary still writes m_filled / m_currentVerb / m_serverStampedVerb into
  gPipeInputs itself. That gap is what made the race possible. Splitting the stamp is the P11
  item - but per-role stamps WITHOUT versioning the BARRIER_PULLED values would be worse than
  today, because it turns a loud Fatal into a silent stale read.
- §2.1 carries escalation (iii) as a row, per its own rule that a new escalation is a ruling and
  a row in the contract rather than a condition at a call site.
- Gate on the flipped head: unit 2256/2256, isplit 179/179 HARD GREEN, gens rc=0, doc citations
  rc=0 (the three remaining are CONTRACT-P5.md's and pre-date this branch).
2026-09-18 13:56:51 -04:00
swung0x48 2d03565f87 [MG_Impl, MG_Remote, MG_Test] (Disaggregated): P5e ra2 - a barriered fill must be quiescent, not merely about to park
- ID-132's finding, diagnosed: with the flip thrown the 70 red lane entries are ONE race
  and two debts. The race is the client's residual fill writing gPipeInputs while the apply
  thread is inside an earlier UNBARRIERED record. Measured interleaving, verbatim:
  `[MobileGLIntegra/ERROR] RA2-PROBE fill verb=DrawElements wireop=81 waitclass=-1
  applierInside=1` then `[mgl-srv-apply/ERROR] RA2-PROBE unfresh field=IsCapabilityEnabled
  verb=DrawElements serverStamped=0 ownership=RECORD-SUPPLIED` then
  `[mgl-srv-apply/FATAL] MGPipe: Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@DrawElements"}`.
  The GL thread's MGPipeServerClearVerbBoundary withdrew the applier's own stamp mid-record.
- The marker's verb is the CLIENT's verb, which is what identifies the writer without a
  probe at all: the applier can only ever put a verb of MGP_VERB_OP_LIST into the block, and
  40 of the 54 unbracketed aborts named a verb that is not in it.
- A/B control on the same binary and the same records: MOBILEGL_IPC_RUN_AHEAD=0 is 8/8 green
  where the default arm is 2/6 red, so the defect is in the wait rule and not in a record.
- RefusePipeInputsTouchWhileApplierOwnsIt could not fire on any of it: its run-ahead arm read
  `if (isBarrieredFill) return;`, and that sentence is a claim about the FUTURE ("this thread
  is about to park behind the record"). The order at the validate point is fill, then emit,
  then park. So the arm now tests the fact - `isBarrieredFill && !ApplyThreadIsInsideApplier()`
  - and the fill sites MAKE it true first with §2.5's forced wait (QuiesceApplierBeforeFill).
- TWICE PER VERB, because the block is written in two phases that straddle publication: the
  serial bump / stamp withdrawal / verb rename run before the fourteen emitters and the
  63-field walk of step 4 runs after them, and the records those emitters published are
  records whose apply reads the block.
- ClientVerbIsBarriered: MGP_VERB_OP_LIST joins the whole draw family through one row
  (DrawVbo -> DrawArrays), so inverting it verb-first answered kOpCount for DrawElements and
  eighteen other draw verbs, and the "unknown verb answers barriered" default filled for each
  while the record it published was a kWaitNone draw_vbo it never parked behind. The family's
  row is applied to the family; every other unknown verb keeps the conservative answer, which
  the wait above now makes honest. Measured alone it is worth 112 -> 133 isplit; with the wait
  in place the lane is 161 either way, so this half is the PERFORMANCE half - without it every
  indexed draw takes a forced drain and run-ahead is lockstep by another name on the draw path.
- Red-once A: put the guard's arm back to `if (isBarrieredFill) return;` and
  RemoteGuards.ClientBarrieredFillWhileTheApplierIsInsideUnderRunAheadIsFatalByName exits 0
  instead of aborting (`Value of: DiedOfAbort(child) Actual: false`), while its green control
  keeps passing. Red-once B: delete the kDraw fallback and
  RemoteRunAhead.AnIndexedDrawVerbIsUnbarrieredAndTouchesPipeInputsNotAtAll dies of signal 6
  with `Fatal{RoleViolation, "gPipeInputs"} - the GL thread touched gPipeInputs
  (MGPipeValidateForVerb) on a RUN-AHEAD session in a barriered fill that did not first wait
  for the applier to catch up`.
- Both new cases run in the ordinary unit lane with run-ahead armed by the fixture, which is
  what ID-132(1) asks for: the strict lane runs under lockstep and cannot see this class.
- Gate on the flipped head: unit 2256/2256, isplit 161/181 (was 112), gpu 1339/1359 (was 1290),
  gens rc=0, flavours rc=0. The remaining 20 are two named debts, not this race: 18 are
  `GetBufferBindingSlot@DrawArrays [UNBARRIERED]` on the two indirect multi-draw tiers, whose
  retiring phase is P8, and 2 are ID-82's own refusal of client vertex arrays.
- The arm is stated and not inferred (ID-81): WaitForApplyToCatchUp's first test is
  `m_runAheadArmed && m_started`, which reduces to Transport != Monolith. Under lockstep,
  under monolith and under MOBILEGL_IPC_RUN_AHEAD=0 every line added here is a call that
  returns, so those arms are unchanged.
2026-09-18 13:38:40 -04:00
swung0x48 985ae62dfa [MG_Backend, MG_Pipe] (Disaggregated): P5e - throw both run-ahead constants (NOT FOR MERGE YET)
kMGPipeP5eRunAheadReady (MG_Backend/Init.cpp) and kMGPipeP5eClientWaitRuleLanded
(MG_Pipe/PipeApply.h) both to true. They are ONE switch with two spellings and the integration
commit throws both.

THIS BRANCH IS NOT MERGEABLE AS IT STANDS and is parked so the defect it exposes can be worked on
against a reproducible head. Measured on this commit:

  build rc=0 · unit 2254/2254 · gens rc=0 · flavours rc=0 (pull 915 TUs, push 929 TUs)
  integration-split   112 / 181   (69 failed)
  integration-gpu    1290 / 1359  (69 failed)

and the failures are FLAKY: 71 failed at -j 4, 68 at -j 1, and a single entry run alone passes and
then aborts on a repeat. A census over all 181 entries under each entry's own command and
ENVIRONMENT gives 70 red on 17 distinct <field>@<verb> pairs, over just FOUR fields:

  27  GetRenderStateParametersVersion
  25  GetTextureContextId
  16  GetBufferBindingSlot
   2  IsCapabilityEnabled

spread across eleven verbs - Clear, DrawArrays, DrawElements, DrawElementsBaseVertex,
DrawElementsIndirect, DrawElementsInstancedBaseVertex, EndTransformFeedback, GetIntegeri_v,
GetSyncStatus, MultiDrawElementsBaseVertex, ReadPixels. Several of those verbs are BARRIERED, which
is what makes this interesting: 16 of the 70 abort on the UNBARRIERED arm
(why=UNBARRIERED) and the other 54 are the plain unfresh-read abort with no [BARRIER-PULLED]
bracket at all, i.e. a RECORD-SUPPLIED field that the freshness stamp says was never filled.

One field at one verb, consistently, would be a missing migration. Four fields scattered over
eleven verbs, flaky run to run, including barriered ones, is a RACE - and the obvious candidate is
that gPipeInputs is shared mutable state whose mutual exclusion WAS the lockstep. The client still
fills it for barriered verbs while the apply thread is draining earlier unbarriered records, so the
client's fill and the server's verb-boundary stamp can now overlap in time. That is consistent with
P5d's own research conclusion, recorded in the roadmap, that retiring this barrier needs
gPipeInputs VERSIONING or double-buffering, deferred to P11 after P3b/P4b's handle-keyed twin
tables.

The strict lane being hard green (181/181, zero Fatal) is necessary and NOT sufficient, and this
commit is the proof: strict runs under lockstep, where ApplyOne stamps every record barriered, so it
cannot see anything that only happens when the client stops waiting. The lane measures remaining
DEBT; it does not measure whether run-ahead is correct.
2026-09-18 12:58:58 -04:00
swung0x48 72bc4268ff [MG_Test] (Disaggregated): P5e wave 3 - the strict lane's expected-marker set, re-measured on the merge
The file carried gl's base measurement (7c6f6886, before pa and mv landed) and said so rather than
hiding it. The merge discharged that note exactly as written, and the two-sided ratchet named both
deltas by itself rather than being told:

- GetProgramForDraw@DrawArrays no longer appears - pa retired it, 62 entries to 0, and it was the
  read that fired once per draw and kept every draw barriered. Removed here, in the wave that
  retired it, which is the whole point of the other side of the ratchet: an expected set that keeps
  rows nothing writes any more is how a lane rots green.
- GetBufferBindingSlot@DrawArrays is new, 18 entries, all of them the two indirect multi-draw
  tiers. mv NAMED it as a latent row before it could fire - BoundDrawIndirectBufferId's
  GetBufferBindingSlot(DrawIndirect), which "would surface as GetBufferBindingSlot@DrawArrays if an
  indirect tier were ever forced under strict" - and then gated the five tiers, which forced it.
  It retires in P8 (FieldOwnership.def:78), so it is a debt owed by a later phase and not by this
  one. Added with that reason.

Measured on the integration tree, two independent instruments agreeing (Harness/split_log_paths.py
markers, and a separate per-entry census that drives each entry's own command and ENVIRONMENT from
ctest --show-only=json-v1):

  integration-split      181 / 181 green
  Fatal{UnmigratedPipeInput   0 distinct pairs
  Admitted{                  10 distinct pairs - 8 by the generated table, 2 by escalation

So "the strict lane is hard green" is now a statement a script evaluates, not one a reader argues:
every entry completes, no Fatal marker over the log set ctest itself declares, every Admitted marker
checked against the same table the C++ answers from, and the ratchet fails in BOTH directions.

Gate on this head: build rc=0, unit 2254/2254, integration-split 181/181, integration-gpu 1359/1359,
gens all rc=0, flavours rc=0 (pull 915 TUs push-absent, push 929 TUs push=1).
2026-09-18 12:50:06 -04:00
swung0x48 97a6a46c0d Merge branch p5e/gl into feat/disaggregated (P5e gl) 2026-09-18 12:42:54 -04:00
swung0x48 e5b74028b5 [Docs] (Disaggregated): P5e ID-120 - contract amendment 8's sticky-forward clause is marked UNLANDED
- CONTRACT-P5E section 8 item 8 says GetBufferBindingPointCount's sticky forward becomes FATAL
  under split. It is not in the code and must not be put there this phase: FieldOwnership.def:147
  (field row) and :179 (forward row) both say BARRIER_PULLED, and the generator refuses a
  field/forward pair that disagrees - pinned by
  FieldOwnershipTest.TheSevenStickyForwardsAgreeWithTheirFieldRows.
- Landing it as written would either trip the generator or drag the FIELD row to FATAL with it,
  and that second reading moves 23 pairs of the derived admitted set: the row's retiring phase is
  "P7/P13", which is exactly what admits it on every stamped verb under ID-125, and a FATAL row is
  admitted nowhere - so 23 reads this phase never promised to migrate would start aborting.
- MEASURED, NOT INHERITED: ID-120 said 10 pairs; that count predates ID-125's retiring-phase
  disjunct. `--print-admitted | grep -c '^GetBufferBindingPointCount@'` says 23 on this head, and
  the note records the command as well as the number.
- Annotated in place, struck through with the phase that lands it (P7/P13), for ID-105's reason:
  a contract clause that is arithmetically impossible against the code is a clause that changes,
  and the divergence is better stated than discovered from a red gate two phases from now.
2026-09-18 12:33:04 -04:00
swung0x48 664cbf3ef8 [MG_IntegrationTest, CI, Docs] (Disaggregated): P5e - Magma's named-blit entries get a lane of their own
- Both DirectVulkan.Split.NamedBlit entries carried LABELS "integration-split" and abort under
  strict on GetFramebufferBindingSlot@Clear (VulkanRenderer.cpp:7648). P5e does not unbarrier that
  field on Magma - it retires there in P7 - so the Espryt lane could not go hard green with them
  in it.
- ARGUED RATHER THAN ASSUMED, because the brief allowed either reading: admitting the row instead
  is the WORSE option. FieldOwnership.def carries ONE retiring-phase string for both backends
  ("P5e (Espryt unbarriered), P7 (Magma)"), so a rule that admitted the pair for Magma would admit
  it for Espryt too - and an Espryt Clear reading the frontend's binding slot again is exactly the
  regression fb's handle arm exists to prevent. The lane split costs two entries; the admission
  would cost the gate.
- THE LABEL IS `integration-magma-split`, NOT `integration-split-magma`, and the spelling is
  load-bearing: `ctest -L` takes a REGEX, so `-L integration-split` matches a label spelled
  `integration-split-magma` as a substring. Measured with the obvious name first: both DirectVulkan
  entries were still listed by `ctest -N -L integration-split`, i.e. the split looked done in every
  listing and had not happened.
- Its CI step asserts the run is RED **and** that a private log carries the named marker, so
  "Magma is expected red" cannot decay into "Magma did not run". Measured: 0/2 under strict with
  two logs carrying Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}, and 2/2 green
  without the knob. integration-split is 117 and integration-magma-split is 2.
- CONTRACT-P5E section 7 states the split and the reason it is not an allowlist entry.
2026-09-18 12:32:39 -04:00
swung0x48 bc6e24ff30 [MG_IntegrationTest, MG_Util, MG_Backend, CI, Docs] (Disaggregated): P5e ID-119/ID-115 - the lane reads every log, counts, and has a positive control
Three measured defects in the strict lane, one commit each in spirit and one in fact because
they are the same step.

1. THE LOG SET CAME FROM A DIRECTORY. The CI step grepped MG_IntegrationTest/split-logs/, which
   holds 90 of the lane's entries. The ones it could not see were the F1. readback block, both
   NamedBlit pairs, the four Ct. entries and PersistentMapArm - precisely the readback population
   the allowlist is ABOUT, plus the only DirectVulkan entries in the lane. split_log_paths.py
   gains a `markers` mode that walks ctest's own {entry: MOBILEGL_LOG_FILE_PATH} map and
   classifies every Fatal{ and Admitted{ line. Measured on this head: 113 of 113 entries read.
   The lane is now a TWO-SIDED RATCHET - an unadmitted marker fails, an admitted pair not in
   Harness/strict-expected-markers.txt fails, AND an expected pair that no longer appears fails,
   so the lane cannot rot green by outliving its own debts.

2. THE `rsp` CHECK GREPPED FOR A COUNTER NO SPLIT ENTRY EMITS, and the pin behind it was vacuous:
   CountBarrierPull's unbarriered arm is [[noreturn]] and runs BEFORE ++g_residualPulls, so
   "rsp = 0 on unbarriered records" was true of every possible implementation. Replaced by an
   assertion where the counter actually is - one entry, DirectGLES.Split.StrictArming., with
   MOBILEGL_PIPE_STATS=1, MOBILEGL_PIPE_STATS_PERIOD=1 and a private log path of its own (a
   whole-lane env flip races under -j: the library opens that path "w"). It asserts
   rsp == 0 || rsp >= draws, which is the device evidence's shape - rsp ~= the draw count under
   inproc, 0 under monolith - so the draw path's retirement is visible as rsp ceasing to scale.

3. THE LANE HAD NO POSITIVE CONTROL (ID-115). Of the seven entries that passed strict, three were
   monolith transport - where nothing stamps a verb boundary, so the whole mechanism is
   structurally unreachable - two self-skipped, one was a death test and one was a Python check.
   Not one was a record-carrying split GL scenario, so "green" and "strict was never armed" were
   the same observation. New push-only counter `vbs` (PipeStats::ServerVerbBoundaries), counted
   at MGPipeServerStampVerbBoundary behind Enabled(), and the same entry asserts vbs > 0 on a
   frame that drew. It is deliberately NOT rsp: rsp reaches zero when the phase SUCCEEDS, so a
   control built on it would start failing on the day the debt is paid.

- Measured on this head (base 7c6f6886, before pa and mv): isplit 119/119; under strict the
  census is Fatal{ GetProgramForDraw@DrawArrays 52, GetBoundVertexArray@DrawArrays 9,
  GetProgramForDispatch@DispatchCompute 7 - exactly what pa and mv owe - and Admitted{ on ten
  pairs, three of them ADMITTED-ESCALATED. The expected-marker file carries all ten and says in
  its header that the wave-3 merge retires three of them.
- CONTRACT-P5E §7 rewritten around what is now checkable: the derived allowlist and its three
  disjuncts, the two-sided ratchet, the restated rsp pin, and the positive control.
2026-09-18 12:30:10 -04:00
swung0x48 a0836568ac [MG_Pipe, MG_Remote, MG_Backend, MG_Test] (Disaggregated): P5e ID-128 - admission gains the escalation disjunct
- The integrator's census over the merged tree left two markers nothing admitted, both
  DirectGLES.Split.ClientVertexArrayScenario: GetBoundVertexArray@DrawArrays. Disjunct 1 fails
  (draw_vbo is kWaitNone) and disjunct 2 fails (that row's retiring phase names P5e, correctly,
  for the ORDINARY draw path vi and mv retired). But these two entries pull the pair on a record
  the client IS parked behind: MGPipeBarriered escalates a draw carrying client vertex arrays
  (ID-83), ID-82 puts client arrays outside P5e entirely, and the read site
  (SyncClientSideVertexArraysForDrawArrays) aborts by its own name if it is ever applied
  unbarriered. The pull is legal; no static table could see it.
- Third disjunct: the record was barriered BY ESCALATION, i.e. wireSaysBarriered(op, payload,
  applierState) && MGPipeWaitClassFor(op) == kWaitNone. Both halves were already computed on
  every record - ApplyOne computes the predicate unconditionally for ID-103's reason - so this is
  one more bit stamped beside the barriered stamp, not new plumbing.
- It is a SEPARATE flag rather than a refinement of the barriered stamp, because the two answer
  different questions: the stamp answers "is the client parked" (the allocator and registry
  guards ask that), this answers "and for a reason the static table cannot see" (only the strict
  knob asks). Its default is false - the answer that admits nothing - which is the opposite of
  the stamp's default and deliberately so.
- The marker says WHICH disjunct: ADMITTED when the generated table answered, ADMITTED-ESCALATED
  when only the record did. That keeps the lane's comparison against --print-admitted exact
  instead of widening the static list with every pair that could ever escalate - and the tag sits
  in the slot the grammar already has for the reason, so Fatal{UnmigratedPipeInput filters are
  untouched.
- Escalation also happens to be the truer justification for GetTransformFeedbackProgram@DrawArrays,
  which ID-125 admits by phase. Both now hold; ID-125's disjunct is kept, as ruled.
- Red-once: AnEscalatedRecordsPullIsAdmittedAndSaysWhy. Dropping `&& !escalated` from the strict
  arm kills it - "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, which drives the identical pull with the
  flag at its default and must keep aborting.
2026-09-18 12:19:14 -04:00
swung0x48 faf19a6aaa [MG_Pipe, MG_Test, CI] (Disaggregated): P5e ID-125 - admission gains the retiring-phase disjunct
- ID-116's static rule failed on a row no package in this phase may touch. pa retired the
  per-draw program pull, which unmasked GetTransformFeedbackProgram@DrawArrays (11 entries):
  BARRIER_PULLED, on kWaitNone draw_vbo, and ruled out of P5e entirely by CONTRACT-P5E section
  5.7, with a retiring phase of "P3b/P4b (Espryt), P7 (Magma)". The rule was wrong, not the code.
- A pull is now admitted if the field is BARRIER_PULLED, the verb has a stamp row, the field is
  in the verb class's may-read mask, and EITHER the verb's op is statically barriered OR the
  field's retiring phase does not name P5e. This table is only ever consulted on a record the
  server already stamped BARRIERED - CountBarrierPull's unbarriered arm is [[noreturn]] and
  fires first - so the pull is legal by construction and the only open question is whose debt it
  is. The retiring-phase column is exactly that answer and was already in the table.
- A verb with NO stamp row still admits nothing under either disjunct: the server stamps only
  MGP_VERB_OP_LIST's ops, so any other bit would be a row nothing can exercise. 111 -> 174 pairs.
- The phase column is matched as a TOKEN, not a substring, so a "P5" or a future "P5e2" cannot
  answer for "P5e".
- All six of ID-125's survivors checked: GetFramebufferBindingSlot@ReadPixels and
  GetTextureUnitObject@CopyTexImage2D by disjunct 1; GetTransformFeedbackProgram@DrawArrays
  (P3b/P4b), GetTextureObject@CopyImageSubData (P7) and ValidateProgramName@ShaderStorageBlockBinding
  (P9) by disjunct 2. GetTextureObject@CopyImageSubData holds by BOTH, asserted separately.
  GetProgramForDraw@DrawArrays, GetBoundVertexArray@DrawArrays and
  GetProgramForDispatch@DispatchCompute stay rejected.
- Magma's GetFramebufferBindingSlot@Clear is NOT admitted and cannot be: the retiring-phase
  column is one string for both backends and it names P5e for Espryt. The DirectVulkan entries
  get their own lane label instead (item 7), which keeps the Espryt allowlist about Espryt.
- StrictErrorsAlsoPromotesTheStickyForwards is RENAMED to
  StrictErrorsReachTheStickyForwardsAndNameThemByField and restated. Not one of the seven sticky
  forwards names P5e (P7, P7/P9, P7/P13, P9), so on a stamped verb they are all admitted now.
  The claim that still has teeth is that the detector REACHES them - asserted by rsp moving and
  by the marker naming the field and its phase, so "did not abort" cannot pass for "never
  detected". The Fatal half keeps its own case next door on GetBoundVertexArray, a P5e row.
- Six new derivation controls, including one that makes GetTransformFeedbackProgram this phase's
  debt and requires it to leave the unbarriered verb's allowlist - without which disjunct 2
  could be a constant `true` and every other control would still hold.
2026-09-18 12:12:08 -04:00
swung0x48 3c13fa4b4a [MG_Backend] (Disaggregated): P5e ID-124 - the pull and push flavours compile again
Three sites, all pre-existing at 7c6f6886, all the same disease: something guarded by
MOBILEGL_BUILD_DISAGGREGATED used from code that is not. Nothing in the gate built either
flavour, so nobody knew.

- Managers.cpp: StagedTextureTargetForPipeTarget is a pure MGPipeResourceTarget -> TextureTarget
  switch with no wire, no record and no role in it, but it sat inside a nested
  #if MOBILEGL_BUILD_DISAGGREGATED while its only caller MGB_TEXPARAM_TARGET is guarded by
  #if MOBILEGL_PIPE_PUSH alone. Let out of the inner block without moving any text. The narrower
  fix - guarding the macro block too - is WRONG and was tried: that block also carries
  TextureDiagName and MGB_TEXTURE_RECORD_ARM_SELECTED, which the push flavour does need, and
  removing them broke it differently.
- DirectGLES.cpp:5575: the storage-block-signature clause called
  ComputeShaderStorageBlockBindingSignatureOf, declared only under MOBILEGL_PIPE_PUSH, from an
  UNGUARDED condition list. The clause is push-only in substance too - the signature it compares
  is over the override map the wire carries.
- DirectGLES.cpp BindCurrentTextures: keysMatch was declared inside
  #if MOBILEGL_BUILD_DISAGGREGATED while the ternary's else-arm and every use sat outside it.
  The legacy arm becomes a LAZY lambda, not an eager Bool: it reads currentProgram, which is
  null on the handle arm after pa, so the laziness the ternary gave for free is load-bearing
  (ID-81 / ID-110 - the arm decides, and the other arm's reads must not happen at all).

Gate: flavours rc=0 - pull 915 TUs (push define absent), push 929 TUs (push=1), both arms
asserted against compile_commands.json rather than against the flags, because the first version
of this step passed -DMOBILEGL_PIPE_PUSH=OFF beside an option that implies it and reported a
clean 982-TU "pull" build that was a second copy of the split build. The cache entry still read
OFF. Split build unaffected.
2026-09-18 12:07:45 -04:00
swung0x48 d8b9848003 Merge branch p5e/pa into feat/disaggregated (P5e pa) 2026-09-18 12:07:22 -04:00
swung0x48 b4789e8ed4 Merge branch p5e/mv into feat/disaggregated (P5e mv) 2026-09-18 12:07:22 -04:00
swung0x48 a6ddeb0816 [MG_Pipe, MG_Test] (Disaggregated): P5e ID-118 - resource_copy_region becomes barriered
- CopyImageSubData's apply reads GetTextureObject, a BARRIER_PULLED row that retires in P7, not
  in P5e. resource_copy_region was kWaitNone, so after the flip that is a genuinely UNBARRIERED
  record reading client memory: an unconditional Fatal, no knob, standing for two phases.
- The wait class moves rather than the field being special-cased into the allowlist. The
  allowlist is derived (ID-116), so an exception written into it would be a real defect kept
  quiet instead of a debt stated - and with the class moved the derivation admits the pair with
  no exception at all: 101 -> 111 pairs, CopyImageSubData joins the 12 barriered verbs.
- kWaitApplied AND NOT kWaitReply: the row posts no reply slot, so the client parks until the
  record applies. That is exactly what copy_framebuffer_to_texture - the two framebuffer-sourced
  copies, same kBlitOrCopy verb class - already does. THE COST IS MEASURED: glCopyImageSubData
  is zero per frame in the Minecraft 26.3-rc-3 scene, so the wait is on no per-frame path.
- PipeCatalogue's wait-class census moves with it, both halves in this commit: the named
  kWaitApplied set gains the row, the count goes 9 -> 10, and the kWaitNone offset 24 -> 25.
- Red-once: putting the row back to kWaitNone drops CopyImageSubData out of the derived
  allowlist (111 -> 101 pairs, 0 @CopyImageSubData) and fails
  "FieldOwnershipTest.cpp:422: Failure / resource_copy_region is unbarriered again: after the
  flip, a copy record reading the client's texture object is an unconditional Fatal and P7 is
  two phases away", with PipeCatalogueTest red on three counts beside it.
2026-09-18 12:02:02 -04:00
swung0x48 5c777b49e8 [MG_Test] (Disaggregated): P5e mv - the multi-draw tiers get gated entries of their own
- The resolved multi-draw tier is ONE process-wide read of MOBILEGL_ESPRYT_MULTIDRAW_MODE
  (MultiDraw.cpp's ResolveTierOnce) and the d1 block leaves it at "auto", which on this
  driver lands on "ext" - the one tier that asks the bound index buffer nothing. Three of
  the five sites the previous commit migrated therefore had no entry in the set the gate
  runs: ID-107's shape one level down, with the env knob in place of MG_Config::Transport.
- Five blocks, one per tier "auto" cannot reach (basevertex, multiindirect, indirect,
  drawelements, compute), prefix DirectGLES.Split.MultiDrawTier<Tier>., both labels, and
  MOBILEGL_ESPRYT_MULTIDRAW_MODE pinned as a ctest ENVIRONMENT property rather than a
  job-level export - review finding N-6's reason, and the PushMonolithArm block's precedent.
- "ext" is deliberately NOT registered: the d1 block already IS the ext lane. Every other
  tier was confirmed to resolve to ITSELF here rather than falling back to ext, so no entry
  in this set is a green that silently means "ext again".
- The filter is the tier-sensitive workload: MultiDrawScenario (minus the client-indices
  case d1 already excludes by name) plus IndexedDrawFamilyScenario's three MultiDraw cases.
  12 entries per tier, 60 in total.
- SplitLogPaths.cmake.in gives each of them a private log path. That is the rule every
  DirectGLES.Split. entry follows (SplitLogPaths.PrivateAndDistinct), and for these it also
  repairs the census blind spot: a third name segment means strict_census2.sh would re-run
  them without the very pin under test, so the marker is read where the lane wrote it.
- integration-split 116 -> 176, integration-gpu 1294 -> 1354, both fully green. Under the
  strict lane all 60 stop on GetProgramForDraw@DrawArrays, which is pa's row: PrepareForDraw
  runs before the tier dispatch, so no new marker class appears and mv's row stays at 0.
2026-09-18 11:59:58 -04:00
swung0x48 4f492edbf3 [MG_Backend, MG_Test] (Disaggregated): P5e ID-117 - the strict knob gains an admitted state
- CountBarrierPull aborted under MOBILEGL_IPC_STRICT_ERRORS on EVERY barrier-pulled read,
  admitted or not. The run's rc is therefore non-zero before CI ever reaches its allowlist
  comparison, so that comparison was unreachable code and "hard green" was impossible with the
  knob as written - the lane could not tell a debt this phase leaves standing from a defect.
- Third state, with the other two untouched: unbarriered stays an unconditional Fatal; barriered
  and NOT admitted stays a Fatal under strict; barriered and admitted logs one MGLOG_W and the
  entry completes. Admission is MGPipeBarrierPullAdmitted, generated from the three tables
  (ID-116), so there is no list here to drift.
- The marker keeps the grammar and changes only the tag: Admitted{UnmigratedPipeInput,
  "<F>@<V>"} [BARRIER-PULLED, ADMITTED, retires in <phase>]. Every filter written since P5c
  matches Fatal{UnmigratedPipeInput and therefore still means exactly "red".
- Deduped per (field, verb) per process. An 852-draw frame reaches an admitted readback row once
  per draw; undeduped, one frame writes 852 identical lines and the marker census is unreadable.
- CHECKED, NOT ASSUMED (the brief's instruction): StrictErrorsTurnsABarrierPulledReadIntoANamedAbort
  and StrictErrorsAlsoPromotesTheStickyForwards both still go red. Both stamp DrawArrays, whose
  op is kWaitNone, so neither pair is admitted.
- ID-119: the old "rsp = 0 on unbarriered records" comment at this function is withdrawn as
  vacuous - the unbarriered arm is [[noreturn]] and runs BEFORE ++g_residualPulls, so the
  sentence was true of every possible implementation.
- Red-once: AnAdmittedBarrierPullIsLoudOnceAndNotFatal is the same sticky forward as the case
  above it under a barriered verb; making the strict arm unconditional again kills it -
  "FieldOwnershipTest.cpp:867: Failure / Actual: false / signal 6".
2026-09-18 11:58:19 -04:00
swung0x48 dd89951062 [MG_Test] (Disaggregated): P5e pa - the strict lane gets four entries of its own
- MOBILEGL_IPC_STRICT_ERRORS=1 had no registered ctest entry anywhere: the phase's
  gate driver sets the knob by hand and pg's declared red-once for
  GetProgramForDraw never landed, so the two rows pa retires could come back and
  every gate would stay green. ID-109's finding, one scale down: the gap is a
  missing gated entry, not a missing case.
- DirectGLES.Split.StrictProgramArm. registers the only two scenarios that are
  strict-CLEAN after pa - one per half. MultiDrawScenario.BaseVertexDrawsReject-
  MalformedArguments prepares a draw and asserts on GL errors rather than pixels;
  AtomicCounterScenario's three cases are the dispatch half. Everything else in the
  lane still ends in a readback, whose GetFramebufferBindingSlot@ReadPixels is an
  allowlisted row only P7 retires.
- integration-split alone, and the prefix says which arm: the monolith arm of these
  scenarios is registered under DirectGLES. already and the knob is a no-op there,
  so labelling them integration-gpu would add entries that cannot fail for the
  reason they exist (R-16).
- Negative control run: with the arm reverted all four abort, two with
  GetProgramForDraw@DrawArrays and two with GetProgramForDispatch@DispatchCompute.
- Each entry takes a private log path through SplitLogPaths, for vi's reason: an
  entry that exists to carry a marker cannot share the file the marker is in.
2026-09-18 11:55:01 -04:00
swung0x48 a211756aec [MG_Pipe, MG_Test, CI] (Disaggregated): P5e ID-116 - the section 7 allowlist is generated, not hand-kept
- ra landed a hand-kept 13-row copy of CONTRACT-P5E section 7 in .github/workflows/test.yml, and
  it was wrong in BOTH directions at once: it omitted GetFramebufferBindingSlot@ReadPixels (21
  entries, the lane's largest survivor) and carried GetTextureObject@CopyImageSubData, which
  resource_copy_region's kWaitNone makes a genuine unbarriered read of client memory. Neither
  error is visible by reading the list. Both are obvious once the list is derived.
- gen_pipe_field_ownership.py now joins the three tables ID-84 already states the rule in -
  kMGPipeFieldOwnership, kMGPipeWaitClasses (generated/PipeWire.inc) and kMGPipeClassFieldMask /
  kMGPipeVerbClass (generated/PipeFillPoints.inc) - and emits kMGPipeAdmittedPullMask plus
  MGPipeBarrierPullAdmitted(field, verb): 101 admitted pairs across 12 barriered verbs.
- The three inputs are READ, never restated, so the allowlist moves with the code: retiring a
  verb's wait class narrows it with no edit anywhere, and a new BARRIER_PULLED row widens it only
  on verbs that wait. A verb with no stamp row admits nothing - only MGP_VERB_OP_LIST's ops can
  ever be the record a pull is reported against.
- --print-admitted gives the CI shell the SAME table the C++ answers from, which is what makes
  the hand copy unnecessary rather than merely corrected.
- 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 five derivation
  controls that move an input and read the allowlist back.
- Red-once: dropping the wait-class conjunct from build_admitted fails the self-test by name -
  "3 admission-derivation control(s) did not hold: unbarriering ReadPixels' op removes every
  @ReadPixels pair; unbarriering ResourceCopyRegion removes every @CopyImageSubData pair; no pair
  is admitted on an unbarriered verb (DrawArrays)" - and the new unit case fails with
  "GetBoundVertexArray@DrawArrays is admitted on a verb no barriered op stamps".
2026-09-18 11:54:28 -04:00