# P5e — the authoritative fact sheet for the doc update Everything below is measured or landed. Do not restate anything that is not here; if a doc you are editing asserts something this sheet contradicts, this sheet wins and you say so in your report. Head: **`5c007b22`** on `feat/disaggregated`, pushed to origin. Tree `~/w7/p5e-int`. Rulings: `~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`, **ID-80 … ID-136**. Contract: `MobileGL/MG_Remote/CONTRACT-P5E.md` (landed by c0e, amended by gl and ra2). ## 1 What the phase set out to do, and where it stands Retire the lockstep on the Espryt draw path: the client publishes an unbarriered record and walks away instead of waiting for the apply thread. **Run-ahead is ARMED** — both `kMGPipeP5eRunAheadReady` (`MG_Backend/Init.cpp`) and `kMGPipeP5eClientWaitRuleLanded` (`MG_Pipe/PipeApply.h`) are `true`. The phase is NOT closed: see §7. ## 2 The twelve packages | wave | package | what it did | |---|---|---| | 1 | **c0e** | the contract and the wire: `PipeCalls.def`'s fifth column `WaitClass` + `MGPipeWaitClassFor`, `set_program_bindings` (opcode 80), `kCapRunAheadApply` (bit 10, DirectGLES arm only), `kDrawClientArrays`, subsystem bit 13, `MGPipeImageAccess`, the two knobs, the cross-package seams | | 1 | **id** | identity: registries rekeyed on `{slot, gen}`, by-handle resolvers, the allocator guard; ID-96's answer was the empty set | | 2 | **vi** | vertex input / VAO / index | | 2 | **sb** | buffer binding points; `MGPShaderBuffers::WritableMask` widened (ID-104) | | 2 | **pg** | programs: the per-link archive into SEG_STAGE, `set_program_bindings`' three tails | | 2 | **tx2** | textures and samplers, the staged store's by-handle reads | | 2 | **fb** | framebuffers, attachments, images, blit | | 2 | **ra** | the transport mechanics: the wait rule, present credit, `gPipeInputs` as server-role memory | | 2.5 | **fix1** | the monolith-arm regression the device found (ID-107); see §5 | | 3 | **pa** | the program family leaves the frontend on the draw and dispatch paths | | 3 | **mv** | multi-draw's frontend VAO read, plus gated entries for all five multi-draw tiers | | 3 | **gl** | the gate mechanism itself, plus two latent crashes | | 3 | **ra2** | the race the flip exposed, and the indirect-binding pull | ## 3 The strict lane became a hard gate `GetProgramForDraw@DrawArrays` 62 → 0 (**no wire change**: the record already carried the per-link `ProgramArchive`; what was missing were readers that still asked the frontend object), `GetProgramForDispatch@DispatchCompute` 7 → 0, `GetBoundVertexArray@DrawArrays` 9 → 0. The admission rule is **three disjuncts**, and none was designed up front — each was forced by a real entry failing: 1. the verb's wire op is statically barriered (ID-116); 2. the field's retiring phase does not name this phase (ID-125); 3. the record was barriered by ESCALATION rather than by its class (ID-128) — XFB-active draws and client-array draws, both of which the contract puts outside P5e. The allowlist is **generated** by `scripts/gen_pipe_field_ownership.py`, not hand-kept: the 13-row copy `ra` had landed in CI was wrong in BOTH directions (it omitted `GetFramebufferBindingSlot@ReadPixels`, 21 entries, and carried `GetTextureObject@CopyImageSubData`, which was not admissible). An admitted pull is **loud but not fatal** (ID-117): same marker grammar, tag `Admitted{` instead of `Fatal{`, deduped per pair. The lane is a **two-sided ratchet** — a pair that appears and is not expected fails, and an expected pair that stops appearing fails too, so the lane cannot rot green. The ratchet earned itself on the merge: it named `GetProgramForDraw@DrawArrays` as no longer appearing (pa retired it) and `GetBufferBindingSlot@DrawArrays` as newly appearing (mv's indirect tiers forced a row mv had itself NAMED as latent before it could fire; it retires in P8). ## 4 Gate at the head | gate | result | |---|---| | `build` | rc=0 | | `unit` | **2256 / 2256** | | `integration-split` | **179 / 179** | | `integration-gpu` | **1357 / 1357** | | `flavours` (ID-124, new) | rc=0 — pull 915 TUs (push define absent), push 929 TUs (push=1) | | generators / include-closure / dirty-surface / doc citations | all rc=0 (the `CONTRACT-P5.md:524` ambiguity is the standing pre-existing one) | | per-entry strict census | **179/179 green, zero `Fatal{`**, `Admitted{` exactly the eight expected rows | | expected-red lanes | `integration-clientarrays-split` 2 entries on `(Multi)DrawArrays+CLIENT_ARRAYS`; `integration-magma-split` 2 on `GetFramebufferBindingSlot@Clear` — both asserted by name | Coverage grew: unit 2250 → 2256, `integration-split` 116 → 179, `integration-gpu` 1294 → 1357, and build flavours from **one** (split) to **three** (pull, push, split). The 65 new lane entries all cover arms or tiers that previously had **no gated entry at all**. ## 5 Three defect classes the phase found, and what each teaches **(a) An ungated runtime arm (ID-107/109/110).** The push build has two server arms selected at runtime; every `integration-split` entry exported `MOBILEGL_TRANSPORT=inproc`, and split-only scenarios skip themselves off the split runtime, so the arm the phone ships on had zero gated entries. `44f91c74` was **1157/1294 on `integration-gpu`: 137 SEGFAULTs across 38 scenarios**, while unit and the split lane were green. Fixed by `66621767`. `gpu` is now a standing gate step. The code-shape lesson: **"a handle was noted" is not the statement "the record arm is selected"**. **(b) Ungated build flavours (ID-124).** The gate configured only the split flavour. Three of four flavours were broken. And the first version of the new `flavours` step reported a clean 982-TU "pull" build that was a **second copy of the split build** — `-DMOBILEGL_PIPE_PUSH=OFF` beside an option that implies it is silently overridden, and the CMake cache still read OFF. The step now asserts the **effective compile define** out of `compile_commands.json`. Lesson: **the evidence for an arm must include proof that it was that arm.** **(c) A guard that could never fire (ID-132/135).** Throwing the flip dropped `isplit` to 112/181 and `gpu` to 1290/1359, **flaky** — 70 red on four fields across eleven verbs. One field at one verb consistently is a missing migration; four fields scattered and flaky is a race. Root cause: `gPipeInputs`' **field values** are already partitioned by `SuppliedFieldMask` and do not race; the **metadata scalars** do. It stayed invisible because `RefusePipeInputsTouchWhileApplierOwnsIt`'s run-ahead arm exempted any `isBarrieredFill` on a claim about the FUTURE ("the client is about to park") while the real order is fill → emit → park. The fix makes the claim a tested fact. A second defect fell out: `MGP_VERB_OP_LIST` joins the whole draw family through one row, so nineteen indexed-draw verbs answered "barriered", filled, and never parked. Contract §3.2 turns out to have **specified per-role stamp storage and never been landed**; that gap is what made the race possible. **The methodological finding (ID-132), which is the most reusable thing in the phase:** a hard-green strict lane is necessary and **structurally not sufficient**. That lane runs under lockstep, where `ApplyOne` stamps every record barriered, so it cannot see anything that only happens once the client stops waiting. It measures remaining debt, not whether run-ahead is correct. The phase exit needs a separate post-flip lane. ## 6 Device measurements (Redmi `2f7cbe2e`, MC 26.3-rc-3, world `test`, DirectGLES) ### 6.1 The measurement correction that has to be stated first The first matrix was **not CPU-clock-pinned** and is void: the **monolith arm's own** per-frame CPU moved 4.43 → 7.07 ms between two runs of the same arm, and the slow run was **cooler** (53.5 °C vs 58.9). The `walt` governor had taken policy0 from 2745 to 748 MHz. `tools/device_bench/pin_device.sh` does not know this serial and **refuses to guess** — correct, and why it did not half-apply silently. `~/w7/notes/p5e/pin_redmi.sh` pins this device's own nodes, write order **min→hw floor, max→target, min→target**, and **reads back `scaling_cur_freq`** to verify; checked again after every session. ### 6.2 Render distance 12 — both arms hit the panel Clocks pinned, GPU pinned, fan on, arms interleaved, 30 s windows, two runs each: | arm | fps p50 | fps max | client ms/frame | apply ms/frame | |---|---|---|---|---| | monolith | 115.4 / 115.6 | 117.5 / 117.2 | 6.25 / 6.45 | — | | inproc run-ahead | 118.0 / 117.6 | 118.6 / 118.5 | 6.87 / 6.92 | 4.35 / 4.43 | | inproc lockstep (`RUN_AHEAD=0`) | 111.6 | 113.7 | 8.16 | 6.53 | Monolith and run-ahead both reach the **120 Hz panel**; lockstep is the only arm that cannot. So this distance answers "can it reach the ceiling", not "which is faster". ### 6.3 Render distance 32 — the comparable round `renderDistance:32` (backed up, restored afterwards). **~3550 draws/frame**, 4.2× the VD12 load, and **nobody near a ceiling** (the game's own `enableVsync:false`, `maxFps:260`; measured max 37–71). Settle raised to 75–150 s because the world streams far longer at this distance. Five runs per arm. | arm | n | fps p50 median | p50 range | client ms/frame median | apply ms/frame | |---|---|---|---|---|---| | monolith | 5 | 58.5 | 56.3 – 67.9 (±10%) | 16.19 | — | | **inproc run-ahead** | 5 | **59.9** | 56.8 – 61.1 (±4%) | **15.72** | 11.1 – 12.1 | | inproc lockstep | 2 | 35.5 | 34.5 – 36.5 | 26.17 | 19.7 – 20.5 | **Parity.** Run-ahead is marginally ahead on both medians and the difference sits well inside monolith's own spread; what IS outside the noise is that the split arm is **half as variable**. `mono1`'s 67.9 is the only outlier in the set (first run after install). **Against the lockstep it replaces: p50 +69%, client CPU −40%, apply CPU −43%.** It is worth far more under heavy load, as expected — the rendezvous count tracks the draw count, about 900 per frame at VD12 and about 3600 at VD32. Both arms are **client-thread saturated (~94%)**, so that column is each one's real bottleneck and they compare directly. ### 6.4 Why run-ahead was worth doing, from the profile (taken at `7c6f6886`, pre-flip) Symbolized `simpleperf cpu-cycles`, client GL thread: `SessionProducer::WaitForAppliedOrEventBacklog` **31.58% SELF time**; the next symbol is 3.72%. The counters showed the client entering the doorbell wait about **916 times per frame** against ~849 draws, 99.8% of them resolved by spinning. `MOBILEGL_IPC_SPIN_US=0` (park every time) collapses the frame rate to **20.9**. So the cost is the **rendezvous itself**, not the spin tuning — which is why the fix is "stop having one per draw" rather than "make each one cheaper". After the wait, the client thread is a flat tail of 1–3% functions: there is no second lever. ## 7 What the phase has NOT done - **BRIEF-P5E §3 items 4 and 5** — the three negative controls and the per-package red-once re-runs. Item 5's evidence form was **not executable** while every candidate was red before the revert, so ID-121 re-sequenced it to after the lane went green. It has not been run since. - `scripts/ci/split_negative_controls.sh`'s E1 control **fails its own check** and demands a `Fatal{BarrierViolation}` that cannot fire at `MOBILEGL_IPC_BATCH_WAITS`'s default of 1 (ID-122). - **G1** is still CI's to assert; the phase gate builds the flavours but does not do symbol identity (ID-123). - Contract §8 amendment 8 (`GetBufferBindingPointCount`'s sticky forward) is marked **UNLANDED** (ID-120); contract §3.2's per-role stamp storage is likewise unlanded and is what let (c) happen. - **Above the panel ceiling at VD12, which arm is faster** is unanswered: this device cannot hold a higher clock. VD32 answers the general question instead. ## 8 The next opportunity, which is not a defect At VD32 the apply thread sits at **11–12 ms** while the client is at **15.7 ms**. The two sides are **unbalanced**, and moving work from client to apply would lower the bottleneck directly. This was meaningless under lockstep, where the two costs simply added; run-ahead is what makes it a lever. It belongs to a later phase. ## 9 Rulings worth indexing (ID-80 … ID-136; the full text is in the notes) | ID | one line | |---|---| | 81 | handle arms only under `Transport != Monolith`; the push-monolith build keeps its frontend arms | | 84 | rule F is scoped to UNBARRIERED records; barriered ones keep P5C semantics | | 90 | Magma stays lockstep for all of P5e; `kCapRunAheadApply` is never published on DirectVulkan | | 103 | the barriered stamp is forced `true` until the client's wait rule lands; the predicate runs anyway | | 107/109/110 | the ungated monolith runtime arm, the `gpu` gate step, and stating the record-arm selector instead of inferring it | | 111 | the stamp must ask the server's OWN capability, or flipping the flag kills Magma | | 112 | a compile-time trip wire on the residual fill's seven rows | | 116/117/125/128 | the admission rule: generated, three disjuncts, admitted pulls loud but not fatal | | 119/115 | the `rsp` pin reworded to something checkable; the lane needs a positive control | | 121 | §3.5's red-once evidence form re-sequenced to after the lane is green | | 124 | three of four build flavours were broken; the arm check on the effective compile define | | 129 | a verb with no stamp row admits nothing | | 131 | `ctest -L` is a REGEX — a new label must not contain an existing one, and prove it by counting | | 132/135 | strict hard green is necessary and structurally not sufficient; right about the object, wrong about the mechanism | | 136 | a wait added to satisfy a lane is a real cost on a real path: ask **which arm pays**, not which lane goes green |