mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-10 19:43:23 +09:00
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.
208 lines
14 KiB
Markdown
208 lines
14 KiB
Markdown
# 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 |
|