[Docs, MG_Remote] (Disaggregated): P5e c0e - the contract lands, and the roadmap row

- MG_Remote/CONTRACT-P5E.md: the draft edited into its landed form. The nineteen rulings are
  as the integrator decided them (ID-80..98) and are restated here rather than referenced,
  because a package reads this file and not the brief. The "draft" wording is gone and every
  file:line was re-resolved at this head.
- FOUR PLACES WHERE THE PLAN WAS WRONG AND THE CODE IS RIGHT, each argued where it sits: the
  descriptor has no spare byte so MGPProgramDesc grew 192 -> 200; the applier's per-class
  windows are ShaderBufferStart/Count/WritableMask, not bare Start/Count/WritableMask; the
  three suppressor slots are named per Class; and the wire codec's layout carries three tails
  now, because set_program_bindings has three and the third one's offset is derived from the
  first two.
- One finding the contract now carries rather than leaves for sb to discover: MGPShaderBuffers'
  WritableMask is a Uint32 and the window is 84, so the mask can only describe the first 32
  points of a class. sb asks for the ruling; it is a wire field and not a package's to pick.
- A new section 10 lists, by name, every symbol c0e put in the tree and what each package
  compiles against - plus the three red-once evidence lines.
- ROADMAP.md gains the P5e row after P5d (status, the eight packages in dependency order, the
  integration gate and the device exit) and the two lines that said P6 was next now say P5e is.
  CURRENT_STAGE_PROGRESS.md section 1 gains its row and section 6 its next-step entry.
This commit is contained in:
2026-09-18 06:49:46 -04:00
parent c06878ef73
commit b4cf9b0527
3 changed files with 576 additions and 8 deletions
+565
View File
@@ -0,0 +1,565 @@
# CONTRACT-P5E — the client runs ahead of apply on the Espryt draw path
Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17),
`CONTRACT-P5B.md` (the class-C slots) and `CONTRACT-P5C.md` (rule E, the two named exemptions,
SEG_EVENT, the guards). Where it disagrees with any of them this file is newer and wins; §8 lists
every such place. Base: `feat/disaggregated @ 2fde7034` (code head `1f8de61b`). Every `file:line`
was read at that commit and re-resolved at c0e's head; paths are under `MobileGL/` unless they
start with `docs/`.
**How to change it.** Package c0e's file, edited by the integrator first. A P5e package (id / vi /
sb / pg / tx2 / fb / ra) that needs a row changed goes through the integrator; the packages compile
against the rows below from day one.
**What c0e actually landed, where it differs from the plan this file was written as.** Four places,
each argued where it sits: `MGPProgramDesc` GREW 192 → 200 bytes because it has no spare byte (§1);
the applier's per-class binding-point windows are spelled `ShaderBufferStart/Count/WritableMask`
rather than bare `Start/Count/WritableMask` (§5.6); the suppressor's three slots are named
`SetShaderBuffersUniform/ShaderStorage/AtomicCounter` (§1); and the wire codec's record layout now
carries THREE tails rather than two, because `set_program_bindings` has three (§1). Everything else
below is the plan as written, and the nineteen rulings of §9 are as the integrator decided them
(ID-80…98).
---
## §0 What P5e is, and the rule above every row
P5d made `inproc` CPU-bound on the client thread with the frame still `client work + (per draw)
wait for apply + handoff` (R-1, `MG_Remote/Client/ClientSession.cpp:932-959`). The wait exists
because the apply of a draw still dereferences client-owned memory: the object-class BARRIER_PULLED
rows of `MG_Pipe/FieldOwnership.def:72-106` and the frontend-keyed twin registries
(`MG_Backend/DirectGLES/SlotTables.h:411-449`) inside the two P5C §3.1 exemption scopes. **P5e
retires the lockstep for every op on the Espryt draw path: the client publishes and moves on; the
server resolves every object from a handle a record carried, and the only client memory it may read
is the residual fill of a record the client is provably blocked behind.**
Rules A–E bind unchanged. P5e adds one definition and one rule:
**Definition — a BARRIERED record.** A record is barriered iff `MGPipeBarriered(op, payload,
applierState)` is true, a pure function of wire-visible data computed identically by the emit
table and by the sink (§2.1). The client blocks in `WaitForApplied(seq)` after publishing a
barriered record; it never blocks after an unbarriered one.
**Rule F — an unbarriered record's apply names no client memory.** With a live wire on a
run-ahead server (§6), the apply of an unbarriered record reads NOTHING from client-owned memory:
no frontend object dereference, no `gPipeInputs` BARRIER_PULLED row, no `MGPipeSlots()` probe or
mint, no `SharedPtr` to a frontend object taken or held. Every object is resolved from a handle the
record or the applier state carries, through a handle-keyed twin table (§4). A violation is
`Fatal{UnmigratedPipeInput, "<field>@<verb>"}` (a pulled row, §3.3), `Fatal{RoleViolation,
"MGPipeSlots"}` (an allocator probe, §4.4) or `Fatal{RoleViolation, "<surface>"}` (P5C §6 layer 1),
**regardless of `MOBILEGL_IPC_STRICT_ERRORS`** — the value would be torn or stale by construction,
so there is no "count it" arm. A barriered record's apply keeps P5C's rules exactly: its pulls count
into `rsp`, its probes are legal inside a scope, and the client's wait is what makes them legal.
Refusal vocabulary is P5C's, plus: `Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` (§5.1),
`Fatal{UnmigratedVerb, "set_global_constants"}` (§5.5), `Fatal{UnmigratedEmulation,
"image-bindable-redirty"}` (§5.2), `Fatal{ProtocolCorruption, "SetSamplerViews.Count"}` /
`"BindSamplerStates.Count"` / `"SetShaderBuffers.Class"` / `"SetShaderBuffers.Count"` /
`"SetProgramBindings.<field>"` / `"Present.FrameSerial"` / `"ImageView.Access"` (§1, §5),
`Fatal{PresentCreditTimeout}` (§2.4), `Fatal{RoleViolation, "gPipeInputs"}` (§3.5).
`Fatal{EventRingOverflow}` is RETIRED on a run-ahead server (§2.6).
---
## §1 Table 0 additions — encodings the P5e rows introduce
| field | the ruling | zero means | who reads it |
|---|---|---|---|
| **`MGPCapBit::kCapRunAheadApply = 1ull << 10`** (`MGPipeTypes.h`, bits 0..9 used) | Set in the DirectGLES arm of the server's `CallMask` (`MG_Backend/Init.cpp`), NEVER in the DirectVulkan arm. THE ARM IS A PURE FUNCTION, `MGPipeRunAheadCapBitsFor(backendType, ready)` in `MGPipeTypes.h`, so "Magma never" is a unit case (`MG_Test/Pipe/MagmaPipeIdentityTest.cpp`) rather than a line only a live split session reaches; `ready` is `MG_Backend/Init.cpp`'s `kMGPipeP5eRunAheadReady`. It says "this server applies unbarriered records without reading client memory". The client latches `RunAheadArmed()` = `m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply)` at the first caps adoption after `Start`; a later snapshot may only turn it OFF. | absent = lockstep (Magma; and Espryt until the integration commit flips `kMGPipeP5eRunAheadReady`, §6.2) | `ClientSession::RunAheadArmed`, `MGPipeBarriered` on both sides |
| **`MGPipeWaitClass`** — a FIFTH column of `PipeCalls.def`'s X-macro (`MGPipe.h`'s enum, spelled as a bare token like the class column), generated beside `kMGPipeCallFlags` into `generated/PipeWire.inc` as `MGPipeWaitClassFor(op)` | `kWaitReply` (every `kReplySlot` row), `kWaitPresent` (`Present`), `kWaitApplied` (the static barriered rows of §2.2), `kWaitNone` (everything else). Opcode order and the flag column are untouched: the column is a value, not a new op. `gen_pipe.py` refuses a token that is not an `MGPipeWaitClass` enumerator AND a row where the flag column and this one disagree in either direction — a `kReplySlot` row that did not wait is a waiter that hangs, a `kWaitReply` row with no slot blocks on an answer nobody posts. Three negative controls in `--self-test` (twelve now, up from nine). | — | `EmitAndWaitTails`, `ServerVerbSink::ApplyOne`, `MGPipeBarriered` |
| **`MGPDrawInfo::Flags` gains `kDrawClientArrays`** = `1u << 6`, the next free bit beside `kDrawIsIndirect` (`MGPipeTypes.h`'s `MGPDrawFlagBit`; `MGPDrawInfo` stays 56 bytes, the flag byte had two bits left) | Set by the client emit arm when any ENABLED attribute of the bound VAO has no buffer. §2.1 escalation (ii). `PipeCatalogueTest` pins the bit's value and its disjointness from the five before it. | no client array in this draw | `MGPipeBarriered`; `EmitDrawRecord`'s refusal (§5.1) |
| **`MGPPresent::FrameSerial`** | Minted by the CLIENT, `++m_presentsSent`, 1-based — no longer 0 (`EmitTables.cpp:1040`). The sink calls `ServerSession::ReturnPresentCredit(FrameSerial)` after `table->Present()` returns (today `ReturnPresentCredit` has no production caller, `ServerSession.cpp:677-682`), one credit per swap. | 0 arriving on a run-ahead server is `Fatal{ProtocolCorruption, "Present.FrameSerial"}` | `OnPresent`, `WaitForPresentAck` |
| **`MGPShaderBuffers` + `MGPBufferRange[]`** (`MGPipeTypes.h`, wire op 38, catalogued since P4a, no route/sink/emitter today) | One record per **Class** ∈ {Uniform=0, ShaderStorage=1, AtomicCounter=2} — the three values are `kMGPipeShaderBufferClassUniform/ShaderStorage/AtomicCounter` in `MGPipeTypes.h`, which is the ONLY spelling of them: the payload's comment had named them since P4a and nothing numbered them, so the emitter and the applier were one literal each away from disagreeing. `Start = 0` always; `Count = TouchedBufferBindingPointCount[target]` (the rv value already carried) clamped to `kMGPipeMaxBufferBindingPoints = 84` (pinned against `BufferState.h:29` in `PipeFill.cpp`); per entry `Res` = the buffer handle or null, `Offset/Size` from the point's range, **`Size = kMGPipeWholeBuffer` (`MGPipeTypes.h:406`) for a base binding** so the server re-resolves the extent against ITS descriptor at use (a `glBufferData` between emit and apply then binds the new extent, as GL does); `WritableMask` bit per entry for classes 1/2. `HostSpanCount = 0` on Espryt for the whole of P5e (`kCapNeedsHostUboBytes` is 0, `CONTRACT-P5.md:64`); the codec's honesty pass (`PipeWireCodec.cpp:1783-1799`) is the guard. `Class >= 3` → `Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}`; `Start + Count > 84` → `"SetShaderBuffers.Count"`. XFB targets do NOT ride this record (`set_stream_output_targets` keeps its row, still unemitted: XFB stays lockstep, §5.7). | `Res` null = nothing bound at that point | `MGPipeApplySetShaderBuffers`, `SyncBufferBindingPointsByRecord`, the UBO loop, `SyncAtomicCounterBuffers` |
| **`set_program_bindings` — opcode 80, appended.** `X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail\|kHostSpan, kWaitNone)` | Head `MGPProgramBindings { MGPipeHandle Cso; Uint64 Signature; Uint32 BlockBindingCount, SamplerUnitCount, StorageOverrideCount, Pad0; }` (32 B, `MGP_ASSERT_POD`); three tails in order: `Int32 BlockBindings[BlockBindingCount]` (dense, GL uniform-block index order), `MGPProgramSamplerUnit {Uint32 Location; Int32 Unit;}[SamplerUnitCount]` (8 B, sparse, ascending Location), `MGPProgramStorageOverride {MGHostSpan Name; Int32 Binding; Uint32 Pad0;}[StorageOverrideCount]` (40 B; the storage-block override map is name-keyed BY DESIGN, `ProgramObject.h:1030-1034`; the name travels as a host span staged whole). `Signature` = the client's commutative hash of the override map, the same function as `Managers.cpp:10832-10841`, so the server's rebuild clause is one `Uint64` compare. Bounds: `kMGPipeMaxProgramBlockBindings` 64, `kMGPipeMaxProgramSamplerUnits` 256, `kMGPipeMaxProgramStorageOverrides` 64, else `Fatal{ProtocolCorruption, "SetProgramBindings.<field>"}` — a larger sampler array is a counted refusal, not a truncation. **THE RECORD ALWAYS DECLARES THREE TAILS**, empty ones included: the tails are positional and the third one's offset is derived from the first two, so a record that declared "one tail" when only the block bindings were present would put the same bytes at a different offset than one that declared three, and the two halves of the wire would disagree about where the overrides start. The wire codec's `WireRecordLayout` grew from two tails to three for it (`MG_Remote/Wire/PipeWireCodec.h`), and the override names are walked through `CheckHostSpanIsHonest` in the decode arm rather than by the encoder's blanket second-tail pass — they are MEMBERS of a 40-byte element, and reading that tail as bare spans would see one span and a quarter of garbage. Emitted BEFORE `create_shader_state` at the same validate point (a rebuild inside the verb must already see the bindings). Dirty rule: bit `NewShaderBindings` (`Tracker.h:364-371, 419-448`) already mixes the three counters; the emitter latches on `(Cso, backendStateVersion, blockBindingVersion)`. A re-issued create clears the record's tails (`GlobalConstants`' rule, `PipeApply.cpp:2903-2909`). | all three counts 0 = "defaults" | `MGPipeApplySetProgramBindings`, the program twin's sync and memo keys |
| **`MGPProgramDesc::LinkStatus`** (a `Uint8`; **the descriptor grows 192 → 200**, `PipeCatalogueTest` pins the new size) | **THE DRAFT SAID "in the descriptor's existing pad, size unchanged" AND THAT WAS WRONG.** `MGPProgramDesc`'s four trailing `Uint8`s end at offset 24 and `MGPBlobRef` is 8-aligned, so the fixed head is exactly full and a fifth byte costs the alignment slack behind it: 20 bytes of handle and counters + 5 bytes of flags, padded to 32, plus 168 of blob refs. It is paid once, on a record that fires at most once per link, and it is confined to the push build — this payload is only ever instantiated under `MOBILEGL_PIPE_PUSH` (`PipeApply.cpp`, `PipeRoute.cpp` and the emitters are all push-only sources), which is why G1 does not see it. 1 = the link the archive describes succeeded. `create_shader_state` is re-issued on the same handle at every link that changes `GetLinkVersion` (today's rule) and the record's `Serial` moves with it; a failed link re-issues with `LinkStatus = 0` and empty blobs IFF the frontend reports the program unlinked afterwards (§9 ruling 9 pins which). A draw on a `LinkStatus == 0` program DECLINES on the server, as the frontend raises `INVALID_OPERATION`. | not linked | the program twin (`SyncCurrentProgram`) |
| **`MGPProgramDesc::Spirv[6]` / `Reflection` blobs are NON-EMPTY** on a live wire | The client encodes the link artefacts once per link with `ProgramArtifactsCodec` into `SEG_STAGE`; the applier decodes into RECORD-OWNED `LinkArtifacts`/`SpirvArtifacts` and the companion pointers (`ProgramEmit.h:230-260`) are deleted under a transport. `Size 0` with no companion is the existing trip wire `Fatal{ProtocolCorruption}` (`PipeApply.cpp:2858-2871`). A `SharedPtr` pin of the client's artefacts was considered and REFUSED: rule B, and `Link()` replaces them in place (`ProgramObject.cpp:348-356`). | — | the program twin's rebuild |
| **`MGPImageView::Access`** | 0 = `GL_READ_ONLY`, 1 = `GL_WRITE_ONLY`, 2 = `GL_READ_WRITE` — the encoding `MGPipeEncodeImageAccess` already writes (`MG_Impl/Pipe/ImageEmit.h`). The NUMBERS move to `MG_Pipe/MGPipeValueTypes.h` as `enum class MGPipeImageAccess : Uint8` with `MGPipeDecodeImageAccess` / `MGPipeImageAccessIsValid` / `MGPipeImageAccessReads` / `MGPipeImageAccessWrites` beside them, and the client encode NAMES those enumerators instead of literal 0/1/2 — one table, both roles, moved rather than duplicated. The encode also stops being a private static so the unit case can reach both halves. The server comment at `DirectGLES.cpp:2662-2669` is deleted as stale (fb). Any other value is `Fatal{ProtocolCorruption, "ImageView.Access"}` AT THE READER: the value header may not log, so it answers the question and the caller owns the refusal. | read-only | `SyncImageTextureBinding` |
| **`kMGPipeSubsystemBufferBindings = 1ull << 13`** (`MG_Pipe/MGPipe.h`, bits 14..62 now reserved) | Owns dirty bits 15/16/17 (`NewConstBuffers/NewShaderBuffers/NewSoTargets`, `Tracker.h:97-99`) and the `SetShaderBuffers` emitter. Caps-required under a transport like bits 7–12; the default push mask becomes `kMGPipeSubsystemsMigratedAtP5e` = `0x3fff`, and `0x1fff` survives as P5e's "everything P4a had and nothing of mine" control. Bit 13 requires bit 7 (a range names a Buffer handle). `NewSoTargets` rides the bit even though `set_stream_output_targets` stays unemitted for the whole of P5e: the bit answers "which A/B switch owns this family's legacy arm", and an operator clearing it has to get the whole binding-point family's frontend walk back rather than two thirds of it. `TrackerTest.cpp` is rewritten, not deleted, and now also pins that the phase constant is the WHOLE dirty-bit set — after P5e every bit names a subsystem. | bit clear = the frontend walk (monolith / A/B only) | `MGPipeSubsystemForDirty`, `SubsystemForEmitter` |
| **`SetHashSuppressor` slots** (`MG_Impl/Pipe/SetHashSuppressor.h`) | `SetShaderBuffers` becomes THREE slots (`SetShaderBuffersUniform` / `SetShaderBuffersShaderStorage` / `SetShaderBuffersAtomicCounter`) — one per Class, so an emission of one class never cancels another's; `SetProgramBindings` gets one. `SetStreamOutputTargets` stays "P4b" (unemitted). | — | the emitters |
| **`IpcTable::RunAhead`** = `MOBILEGL_IPC_RUN_AHEAD` (default 1, range 0..1); **`IpcTable::PresentCredit`** = `MOBILEGL_IPC_PRESENT_CREDIT` (default **1**, range 1..8) | Parsed unconditionally like every IPC knob (`ConfigLoader.cpp:356-403`), and both are in the IPC log line, because a knob that silently means nothing on one arm of an A/B is how an A/B stops being one. `PipeVerify` forces `RunAhead = 0` beside `BatchWaits = 0`. `RunAhead=1` on a server without the caps bit logs once "run-ahead requested, server does not publish kCapRunAheadApply" and runs lockstep. The stats line gains `credit-waits`. | `RunAhead=0` is the A/B control: identical server code, only the client's wait differs | `RunAheadArmed`, `EmitPresent` |
| **`BufferState` bind-point generations** (client-only, `#if MOBILEGL_PIPE_PUSH`) | One `Uint64` per `BufferBindPointTargets` entry beside `NoteBufferChanged` (`BufferState.h:69-75`), bumped by `glBindBufferBase/Range` (`GL_Buffer.cpp:1501-1520, 1624-1650`) and `SetNamedTransformFeedbackBinding`. Not a wire field; listed because it is the emit rule's input and the pull build's object must not resize (the licence `TextureState::NoteImageUnitTouched` took at P5d r3). | — | bits 15/16/17's shutters |
---
## §2 ra — the wait rule
### 2.1 The barriered predicate (one function, two callers)
```
MGPipeBarriered(op, payload, st) =
MGPipeWaitClassFor(op) != kWaitNone // static column, §2.2
|| (MGPipeCallClassFor(op) == kCtxVerb && st.IsTransformFeedbackActive) // (i) XFB stays lockstep
|| (op == DrawVbo && (payload.Flags & kDrawClientArrays)) // (ii) client arrays
```
`st.IsTransformFeedbackActive` is `MGPContextValues`' value (`MGPipeTypes.h:1698`): on the client
it is `ctx.IsTransformFeedbackActive()` at emit, on the server the applied value — `set_context_values`
precedes the verb on the ring, so the two agree. **c0e LANDED THE FUNCTION, all three clauses, in
`MG_Pipe/PipeApply.cpp`**, and gave `MGPipeApplierState` its own `IsTransformFeedbackActive` mirror
written by `MGPipeApplySetContextValues`: the predicate has to be computable on the server from
APPLIER state, and `gPipeInputs` is the block a run-ahead client stops filling, so reading the
answer out of there would be reading the very memory rule F exists to stop it reading. `payload`
may be null, which makes clause 3 fall through — a caller holding an opcode but not yet the bytes
must not be told a draw is unbarriered on that account. Escalation (ii) is a refusal under run-ahead
(§5.1), so on a run-ahead server it never reaches the sink; it is listed so the predicate is total.
**No other runtime escalation exists**; adding one is an integrator ruling and a row here.
### 2.2 The static column, row by row
| class | rows | why |
|---|---|---|
| `kWaitReply` | every `kReplySlot` row (`PipeCalls.def:115-202, 206, 221`): GetCaps, ResourceCreate/Respecify, MapPersistent, FenceStatus/Wait, QueryAvailable/Result/Timestamp, SetTextureParams, ResourceSubData (texture half, §2.5), ResourceReadback, GetTextureImage, ReadPixels | the answer is not derivable (R-5) |
| `kWaitPresent` | `Present` | the credit, §2.4 |
| `kWaitApplied` | `ApplierReset`; `GenerateMipmap` (`kCtxObject`, waits today for the reason at `ClientSession.cpp:900-914` — until tx2 lands its `VerbMipRes` arm, then `kWaitNone`); `SetStorageBlockBinding` (`kCtxVerb`, resolves through `GetProgramObject(GlName)`, `PipeApplier.cpp:805-808`; trailing: pg resolves through `MGPStorageBlockBinding::ShaderCso` and flips it); `BeginStreamOutput`, `EndStreamOutput`, `PauseStreamOutput`, `ResumeStreamOutput`, `BindStreamOutput` (§5.7); `CopyFramebufferToTexture` (the CopyTex endpoint still syncs the frontend texture, `DirectGLES.cpp:9714-9735`; trailing, tx2) | each still pulls a row or probes; the wait keeps P5C's semantics for it |
| `kWaitNone` | `DrawVbo`, `LaunchGrid`, `Clear`, `Blit`, `MemoryBarrier`, `Flush`, `SetSwapInterval`, `BindShaderImage`, `PatchParameter`; every `kCtxState` / `kCtxCso` / `kCtxObject` row without a reply (incl. `ObjectDeath`, `ResourceSubData` buffer half); `ResourceDestroy`, `UnmapPersistent`, `FenceCreate/Destroy`, `FenceWaitServer`; `QueryCreate/Begin/End/Destroy/Counter` | rule F holds for their sinks after §4/§5 |
The column is inert until the integration commit publishes the caps bit (§6.2), after vi/sb/pg/tx2/fb.
### 2.3 `EmitAndWaitTails` after `PublishAndNotify` (`ClientSession.cpp:877`)
```
if (ownsReplySlot) -> WaitForAppliedBudget + DrainEventRing + ReadReply (unchanged)
else if (RunAheadArmed())
switch (class) kWaitNone: return seq;
kWaitApplied: WaitForAppliedBudget + DrainEventRing + ReleaseFillPins(); return seq;
kWaitPresent: return seq; // the credit wait ran BEFORE encode, §2.4
else -> today's :915-959 shape, byte for byte (lockstep, Magma, verify)
```
`ReleaseFillPins()` clears the four O-class `SharedPtr` rows of `gPipeInputs` (`PipeInputs.h:801-809`)
on the GL thread once the barriered apply returned — under run-ahead the next fill may be a frame
away and a pinned VAO/program must not make the apply thread its last owner. `VerbBarrier==0`
keeps its lockstep meaning (nothing but reply rows waits); under run-ahead it removes the
`kWaitApplied` waits too and is the run-ahead NEGATIVE CONTROL: expected red is
`Fatal{UnmigratedPipeInput, ...}` at the first barriered row's stale pull.
### 2.4 Present pacing
`EmitPresent` (`EmitTables.cpp:977-1053`): `BeforeReadOnlyVerb(); if (m_presentsSent >= PresentCredit)
WaitForPresentAck(m_presentsSent + 1 - PresentCredit, 30 s)` — `ShutDown` returns, `TimedOut` is
`Fatal{PresentCreditTimeout}`; then `DrainEventRing`, encode, publish, `PumpControlPlane`. Credit 1
= the client publishes frame N+1 while the server applies and swaps frame N: one frame of overlap,
at most one frame of added latency. Budget at ~850 draws + ~0.36 MB pmap/frame: SEG_CMD ~70 KB +
set/CSO records per frame against 8 MiB, SEG_STAGE ~1 MB/frame against 32 MiB, SEG_REPLY unchanged.
The credit, never the bytes, is the steady-state backpressure; `WaitForCmdSpace` and the stage bell
(`PipeWireCodec.cpp:836`) are the safety net and both drain events on exit (§2.6).
### 2.5 Drains and forced waits
Drain points: every wait exit in `ClientSession` (reply, applied, present-ack, cmd-space, stage
bell), the four EGL RPC returns (`BackendObject_Remote.cpp:200-245`), `Stop`. Forced waits:
`MapBuffer(READ)` / `GetBufferSubData` / a `CopyBufferSubData` source → `SyncGpuWrites` (reply
row, unchanged); `glFinish` → `WaitForApplied(LastPublishedSeq)` + drain (a new client `Finish`
slot; `Flush`/`Finish` stay no-ops on the wire, ARCHITECTURE:415); every `Server*` EGL forwarder
waits `WaitForApplied(LastPublishedSeq)` BEFORE the RPC (the mailbox is pumped between drain
batches, `ServerLoop.cpp:385-390`, so a make-current would otherwise land between two run-ahead
records); `ServerSwapEGLBuffers` is not on that list — present is the swap. **`glGetError`
relaxation:** a run-ahead verb's `kEventGlError` is observed at the next drain point, i.e. at most
one present credit later than the call after it; P5C §4.2 already assigns ordering to P9 and P5e
names the window. **`resource_subdata`:** the BUFFER half passes `wantReply = false` through
`MGPipeRouteResourceSubData` (the Bool is discarded today, `PipeFill.cpp:839`; the server still
posts, the slot is never read — legal, `ReplySlot.h:16, 105`), which is what makes the persistent-map
push (`PersistentMapTracker.cpp:735, 850`) fire-and-forget; the TEXTURE half keeps its wait because
`DrainTextureSubData` clears the level's dirty flag on the accepted reply (D-D5; trailing).
### 2.6 Event-ring flow control (ARCHITECTURE §11.7 made real)
On a run-ahead server `Reserve == nullptr` is NOT `Fatal{EventRingOverflow}`: the producer latches
`eventRingFull` (exists, `EventRing.h:135-143`), publishes, rings, and `ApplyThreadMain` parks at
the record boundary with `eventRingFull == 0` added to `ready` (`ServerLoop.cpp:380-383`). The
client re-checks `m_events.RingIsFull()` at every park exit and drains before re-parking. Deadlock
argument: the server blocks only on the event ring; the client blocks only on watermarks the
server advances; every client wait drains; so at most one side is parked at any instant. The
lockstep arm keeps the Fatal (P5C §4.4).
### 2.7 Object lifetime under run-ahead
The client frees a slot only after its death record went out (`PipeFill.cpp:1623-1626`), the ring
is in order, every record naming `{s,g}` precedes `destroy{s,g}` which precedes `create{s,g+1}`,
and every server twin table Gen-checks (§4.3). **No wait is added at any delete.** Persistent maps:
`Forget()` (`PersistentMapTracker.cpp:599-606`) drops tracker state only; pushed bytes were copied
into SEG_STAGE at `StageOptional` and reclaim waits on `retiredSeq`, so a map released with blocks
in flight is safe. The P5d deferred-destroy queue (`PipeMutation.h:216-236`) STAYS LIVE: a
barriered record's apply may still pin a frontend object (its fill's `SharedPtr` rows, XFB
`targets`), so the apply thread can still be a last owner. Under `RunAheadArmed` an enqueue from an
UNBARRIERED apply is a finding: `MGLOG_E_ONCE` with the kind, Fatal under strict.
---
## §3 `gPipeInputs` — server-role memory for unbarriered records
1. **The client fills only for barriered records.** In `MGPipeValidateForVerb`
(`PipeFill.cpp:2962-3306`), when `RunAheadArmed()` and `!MGPipeBarriered(...)`: step 2 (tracker
walk) and step 3 (emitters) run unchanged; `:2989` (`++CurrentVerbSerial`), `:2999-3001`
(`SetVerb/SetIdentity`), step 4 (`:3252-3270`, `CopyField` + `FilledGen`) and `MGPipeLeaveVerb`'s
writes are SKIPPED; `MGPipeNoteFrontendMutation` returns at `:554`. The residual-value trip
wire (`:3284-3294`) still emits — it compares on the server against the applier mirror.
`RunAhead=0` on a caps-publishing server uses the SAME fill decision (the A/B changes one
variable: the wait). `PipeVerify` forces lockstep, a fill for every verb, and every record
barriered (both roles read the in-process flag; the comparator `:3304` is inproc-only).
2. **The server's stamp is server-private.** `MGPipeServerStampVerbBoundary` writes
`m_filled/m_currentVerb/m_serverStampedVerb` into applier-owned storage, never into the shared
block; `MGPipeServerClearVerbBoundary()` from the GL thread (`:2997`) is DELETED under a
transport. For a barriered record the server reads the client's fill (the client is parked) and
its own stamp; for an unbarriered one it does not touch the block. `SetIdentity` (`:3001`) moves
to `ApplyOne`, once per session. This removes the E note's hazard (b.4).
3. **The detector.** `MGPipeInputUnfreshRead` (`PipeInputs.cpp:191-224`) under an unbarriered
record takes `StrictBarrierPullFatal` (`:60-66`) regardless of `StrictErrors`; the seven sticky
forwards (`MGPipeStickyForwardPull`, `:251-257`) the same; `CountBarrierPull` stays for barriered
records. This is what turns `integration-split-strict` into a hard green lane (§7).
4. `VertexAttribDefaultsOf` (`PipeFill.cpp:84-86`) diffs against the emitter's own last header
(`g_attribDefaultLastHeader`, `:2949`), never against the block.
5. `RefusePipeInputsTouchWhileApplierOwnsIt` (`ClientSession.cpp:1015-1056`) is re-derived: the
`BatchWaits` early return at `:1037` goes; under `RunAheadArmed` any GL-thread write to the block
outside a barriered fill is `Fatal{RoleViolation, "gPipeInputs"}` by name.
`Fatal{BarrierViolation}` (`:840`) stays for `BatchWaits==0 && !RunAhead`.
---
## §4 id — handle-keyed twins
### 4.1 The rule
With a live wire, every twin on the apply thread is resolved BY THE HANDLE the applier state or
the record carries, never by a frontend object: `GetOrCreate(MGPipeHandle)` (`SlotTables.h:294-342`),
`FindByHandle` (`:420-426`), `ReleaseByHandle` (`:394-408`). `HandleOf(stateObj)`,
`Find(StateObject*)`, `GetOrCreate(const StatePtr&)`, `NoteStateForHandle/StateForHandle` and
`Entry::stateRef` survive as the MONOLITH-GLUE half (§5.8) and are unreachable from an
unbarriered apply. The twin sync bodies take `(MGPipeHandle, const <Record>&)` and store
`m_handle` (the `m_pushedSyncHandle` pattern, `Managers.h:1857`); the object parameter is deleted
from the transport overload, not defaulted to null. The E note's "content-addressed VAO CSO" is
corrected: every kind mints its slot off the object's own lifetime id (`VertexInputEmit.h:170-172`,
`TextureEmit.h:422`, `FramebufferEmit.h:257`, `ProgramEmit.h:411-422`), so the rekey is a
replacement of the lookup ARGUMENT, not of the table.
### 4.2 Per kind
| kind / table | key on the server | the record that names the handle before any verb reads it | twin created | twin dies |
|---|---|---|---|---|
| Buffer `g_backendBufferResources` (already handle-keyed) | `Res` from `VertexBuffers[]`, `IndexBuffer`, `BoundShaderBuffers[c][i]`, `Desc.BufferForTexBuffer`, `VerbIndirect*` | `resource_create/respecify` | `EnsureBufferResourceForHandle(nullptr, h)` | `resource_destroy` |
| VAO `g_backendVertexArrayObjects` | `st.BoundVertexElements` (`PipeApply.h:594`; one CSO per VAO, re-created on the same handle when the config moves) | `create/bind_vertex_elements` | first `ResolveVaoTwin(handle)` at a draw | `object_death{VertexElementsCso}` / `delete_vertex_elements` |
| Texture `g_backendTextureObjects` | `BoundSamplerViews[u].Texture`, `BoundShaderImages[u].Res`, `FramebufferRecords[h].State.{Color[i],Depth,Stencil}.Res`, `VerbMipRes`, `VerbCopyTexDst`, `MGPCopyImage` src/dst | `resource_create/respecify` (Texture) | first `SyncTextureToBackendByHandle(h)` | `object_death{Texture}` / `resource_destroy` |
| Renderbuffer `g_backendRenderbufferObjects` | `MGPSurface.Res` with `Kind == Renderbuffer` | `resource_create/respecify` (Renderbuffer) | first attach sync | `object_death{Renderbuffer}` |
| Framebuffer `g_backendFramebufferObjects` | `st.BoundFramebuffer[t]`, `record.Fbo`, `VerbBlitReadFbo/DrawFbo`; `kMGPipeDefaultFramebuffer` is never a twin | `set_framebuffer_state` | `GetOrCreateByHandle(record.Fbo)` (already, `DirectGLES.cpp:3197`) | `object_death{Framebuffer}` |
| Program `g_backendProgramObjects` | `st.DrawProgram` / `DispatchProgram` / `BoundShaderCso`; composites at the band | `create_shader_state` (+ `set_program_bindings`) | first `SyncCurrentProgram(handle)` | `object_death{ShaderCso}` / `delete_shader_state` |
| Sampler `g_backendSamplerObjects` | the CSO family ONLY: `st.BoundSamplerStates[u]` or `TextureResources[h].Params.BuiltinSampler` | `create_sampler_state` | `ResolveSamplerCsoTwin` (already, `Managers.cpp:13457-13486`) | `object_death{SamplerCso}` |
| SamplerView `g_backendSamplerViews` | `BoundSamplerViews[u].View` (no draw-path resolver today; unchanged) | `create_sampler_view` | — | `object_death{SamplerViewCso}` |
The identity family of samplers (`ResolveUnitSamplerBackend` `DirectGLES.cpp:4985-5026`, the mint
at `:5623-5630`, `Managers.cpp:13264-13280`) is deleted under a transport; a unit whose
`BoundSamplerStates[u]` is null while the frontend held a sampler is a missing record and §5.3's
window rule makes it unrepresentable. The creation moment is LAZY everywhere (no twin at
`create_*`); the applier's Gen check refuses a bind or verb naming an unknown or stale handle
(`PipeApply.cpp:2266, 2549, 2625, 2683, 2896`), so a twin is never minted for an object the ring has
not introduced. `applier_reset` touches neither twins nor object records (`PipeApply.h:800-832`).
### 4.3 ABA
`Gen` moves only on reuse (`SlotAllocator.cpp:410-419`). At the server every record naming `{s,g}`
is applied before `object_death{s,g}`, which is applied before `create{s,g+1}`. `GetOrCreate({s,g+1})`
on a live `{s,g}` entry resets it (`SlotTables.h:338`); `FindByHandle({s,g})` after the recycle
answers null (`:424`); a backward `{s,g-1}` is refused (`:331`). Even with a death skipped (an
object that never crossed emits none, `WireTables.cpp:595-596`) the forward-Gen reset answers.
Twin memos key on `{slot,gen}` plus server serials, never on a pointer. `BackendSlotTable` gains
the composite band (`m_band`, indexed `slot - base`, as `SlotAllocator.h:144` / `PipeApply.h:511`)
so a pipeline program (slot >= 983040) does not grow the ordinary table to `kMaxHandleSlot` entries
— a P5e prerequisite, not a follow-up, because the rekey makes it the ordinary path.
### 4.4 The scopes and the guard
`MGPipeRefuseAllocatorFromApplyThread` (`SlotAllocator.cpp:22-38`) exempts a probe iff a scope is
active AND `MGPipeApplierCurrentRecordIsBarriered()` (server-private, set by `ApplyOne` from
§2.1; `true` for every record on a non-run-ahead server, §6). **c0e landed the storage and the
writer** (`MGPipeApplierSetCurrentRecordBarriered`, `MG_Pipe/PipeApply.cpp`) with `true` as the
default — the only seam in this phase whose body is implemented rather than aborting, because
there the safe answer and the landed lockstep answer are the same answer, and an abort would take
down the monolith and the lockstep split alike. id owns the `ApplyOne` call site. **`MGPipeFrontendKeyedRegistryScope`
is deleted at its draw-path sites** — `DirectGLES.cpp:1609, 1734, 2588, 3303, 4236, 4329, 4585, 4926,
4993, 5328, 5689, 6729, 6775, 9695` and `Managers.cpp:256, 2938 (draw-path callers), 4544, 5882,
7087, 8481, 8717, 9256, 9310, 9709, 12949, 13273, 13570` — with the frontend probes they wrap; the
class survives only around the barriered-row sites (`DirectGLES.cpp:6166, 8837, 9161, 9216, 10029,
11884`, the verify arms `Managers.cpp:10382, 10403`) with their P8/P9 phases re-annotated.
`MGPipeReverseAnnouncementScope` is renamed `MagmaP7AllocatorDebtScope`, compiled for the Magma
sites only (`VulkanRenderer.cpp:4554, 4634, 8986`, `ResourceTracker.h:723`), exempting only when
the server backend is DirectVulkan. The renderbuffer cross-check `Managers.cpp:9709-9720` (not
transport-gated today, unlike `:9628-9632`) is deleted by the rekey, not gated. The NoSession
server-only fixture hop (`Managers.cpp:224-246`) is deleted; such a fixture's twins die at Stop.
---
## §5 Per family — the clean gate, the records, the windows
### 5.1 VAO / vertex input / index (vi)
Twin from `st.BoundVertexElements`; memo key `{BoundVertexElements, rec->ContentSerial,
VertexBuffersSerial, IndexBufferSerial}` (all server-owned); the buffer walk is over
`st.VertexBuffers[Start..+Count).Res` (dedupe on `{slot,gen}`) with
`EnsureBufferResourceForHandle(nullptr, Res)` and `IsBufferDrawCleanByHandle(h, res, nullptr)`;
`ResolvedDrawBuffers::Entry::frontend` leaves the split arm; the IBO arm reads `st.IndexBuffer.Res`
+ `IndexBufferSerial` (also for the restart substitution, `DirectGLES.cpp:6447-6461`);
`SyncCurrentVertexAttributeValues` keys on `{elementsHandle, ContentSerial, activeMask}` and reads
`rec->Attributes[i].Enabled`; the dead `GetConfigVersion` read at `:4775` is deleted. **Client-memory
vertex arrays are NOT refused under split today** (only `CLIENT_INDICES/CLIENT_COMMANDS/
UNBOUND_PARAMETER`, `EmitTables.cpp:651-685`; the server dereferences `attrib.Offset` as a raw
pointer, `Managers.cpp:5382`): under `RunAheadArmed` a draw with `kDrawClientArrays` is
`Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` raised on the GL thread in `EmitDrawRecord`'s
array arm; the lockstep arm is unchanged; the staged form (a per-attribute `{BindingIndex,
MGHostSpan}` tail like `kDrawHasUserIndices`) is P8's. Two pins by test: `CurrentBufferMutationEpoch()`
(`Managers.cpp:794`) is apply-thread-only under a transport; the VAO twin key is 1:1 only while the
vertex-elements CSO stays identity-addressed.
### 5.2 Textures (tx2)
Every draw-path entry is `SyncTextureToBackendByHandle(h, imageBindable)`: record first, twin by
`GetOrCreateByHandle`, three syncs `(h, *rec)`; the by-value twin copy + second `Find`
(`DirectGLES.cpp:1745-1755, 1781-1805`) go with the map arm. Clean = `m_isInitialized &&
m_syncedResourceSerial == rec->Serial && rec->PendingUploads.empty() && m_syncedParamsSerial ==
rec->ParamsSerial && !rec->Params.ForceResync && !m_forceTextureParamsResync &&
m_syncedBuiltinSampler == rec->Params.BuiltinSampler && m_syncedBuiltinSamplerSerial ==
SamplerCso(rec->Params.BuiltinSampler)->Serial && !rec->Params.SamplerResync && !m_forceSamplerResync
&& rec->Desc.StorageKind == Mipmap` — no new state. The four in-body live reads (`GetTarget` at
`Managers.cpp:8876-8877, 8620-8621`; `IsTextureView` `:7060`; the TexBuffer backing `:8223-8231`)
read `Desc.Target / Desc.ViewOf / Desc.BufferForTexBuffer`; `GetExternalIndex` in logs becomes
`Desc.GlNameForDiag`. The unit work list is `{Res, backend}` keyed `(ContextSerial,
SamplerViewsSerial, SamplerViewStart, SamplerViewCount, g_backendContextGeneration)`;
`PairingsIntact` is deleted; `ResolveAndBindUnitTextures` is one pass over the window with the
target from `SamplerViewCsos[View].View.Target`, then an unbind of every target the window did not
claim driven by `g_boundTexturesCache`; the alias arbiter dies (the client arbitrated,
`SamplerEmit.h:751-757`). **Named delta (P5e-5):** the unit list syncs the program-resolved texture
per unit, not every slot of every touched unit; coverage is unchanged (views ∪ image units ∪
attachment lists ∪ the waited texture ops). `RequireImageBindableStorage`'s re-dirty arm
(`Managers.cpp:5930-5962`, already aborting three frames deep) becomes a NAMED refusal at its
entry under a transport, `Fatal{UnmigratedEmulation, "image-bindable-redirty"}`; `ImageBindableHint`
is the prevention, P9 owns the pull. `GenerateMipmap(target)` under a transport resolves the
texture from `VerbMipRes` (as `EnsureGenerateMipmapStorageAllocated` already does, `DirectGLES.cpp:8736`),
never the active unit (`:9563-9566`), and its row flips to `kWaitNone`. `m_force*Resync` are
server-set and never cleared by the wire; the wire bits are never cleared by the server (D-E2, now
load-bearing for a gate). The pinned guard list (`MipmapStorage.cpp:39-51`, `RemoteClientTest.cpp:1938-1978`)
gains the six shape accessors it exempts today plus `GetSamplerObject/GetTextureParamsVersion/GetContentVersion`.
### 5.3 Samplers and the two unit windows (tx2; the rule binds the emitters)
The effective sampler of unit u is `st.BoundSamplerStates[u]` if non-null else
`TextureResources[view.Texture].Params.BuiltinSampler`; its values are the CSO record's
(`MGPipeValueTypes.h:462-486`); the raw-depth-fetch substitution stays on the server
(ARCHITECTURE §5.5); `texture2D->GetFormat()` becomes `SamplerViewCsos[View].View.InternalFormat`.
**Window rule (A8 closed):** `set_sampler_views` and `bind_sampler_states` MUST carry `Start == 0`
and `Count >= MaxTouchedTextureUnit + 1` (0 when none touched), as `SamplerEmit.h:733-737` emits
today; at every draw/dispatch apply the sink checks the two counts against the applied
`MGPContextValues::MaxTouchedTextureUnit` and a narrower window is `Fatal{ProtocolCorruption,
"SetSamplerViews.Count"}` / `"BindSamplerStates.Count"` — a server-side re-derivation is refused.
The `BindCurrentTextures` memo key is `{DrawProgram.{Slot,Gen}, ShaderCso.Serial, BindingsSerial,
SamplerViewsSerial}` (ruling 6). The decline arms `DirectGLES.cpp:5152-5169` and `:2073-2098` become
named refusals under a live wire; `CaptureUnitBindings/UnitBindingsUnchanged/g_observedUnitBindings`
(`:1867-1957`) are monolith-only text.
### 5.4 Framebuffers, attachments, images (fb)
`SyncCurrentFBO` is `SyncCurrentFBOByRecord` only; a decline under a transport is
`Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"}`; `FramebufferRecordMatchesBinding`
(`:3017-3033`) is monolith-only. `BackendFramebufferObject::SyncToBackend(record, target)`: the
attachment walk is over `Color[0..7] + Depth + Stencil` (`Color[0]` alone when `IsDefault`) — the
record's 11 surfaces ARE the point set (the emitter refuses a point at or above the wire width,
`FramebufferEmit.h:489-501`; FRONT/BACK tokens exist only on the default framebuffer); the empty
point is `Kind == None || Res null` for the detach (`Managers.cpp:10310`) and the N-6 refusal
(`:9601-9606`) alike; `m_syncedFrontendAttachmentVersions` is replaced by the record hash;
`g_attachmentBackendIdGeneration` stays. `SyncAttachmentSurface(target, surface, attachment)` takes
no object; storage through `SyncMipmapsToBackendByHandle` / RBO `SyncToBackendByHandle(surface.Res)`.
The two attachment texture lists key `(record.Fbo, ContentHash, ContextSerial,
g_backendContextGeneration)` with entries `{Res, backend}` and `PairingsIntact` = `entry.Res ==
surface.Res`. `ForceBindCurrentFBO` is handle-only and stamps nothing frontend (the stamps at
`:4734-4738` and the legacy memo are skipped together under the transport test). The named blit
takes two `const MGPFramebufferState&` (draw buffers, `ReadSurface`, `Kind`, layer/level/layered/
format from the surface, `Samples` from the record, `kMGPipeDefaultFramebuffer` for the default)
and retires `StateForHandle`; the bound arm reads `FramebufferRecordFor(st.BoundFramebuffer[t])`.
`ScopedDetachedTextureFramebufferAttachments` walks a reverse index texture → FBO slots maintained
at `set_framebuffer_state` apply. `MGPFramebufferState::Complete` is never a "may I draw" gate.
`SyncReadFramebufferTextureAttachments` is not gated on the framebuffer bit (`:2227-2230`), so its
retirement moves the object A/B baseline in BOTH arms (re-baselined, not a regression).
**Default-framebuffer resize:** the surface-changed consumer's `AllocateStorage` (`ClientSession.cpp:210-221`)
reaches `MGP_NOTE_AGGREGATE(FramebufferAttachment)` (`TextureObject.cpp:84-102`), so bit
`NewFramebuffer` (`Tracker.h:594`) re-emits the record — verified at this head, pinned by a test in fb.
Images: `SyncImageTextureBinding(const MGPImageView&)`: twin by `FindByHandle(Res)`, `Access`
decoded, target and storage format off the TWIN (no new wire field; `MGPImageView` is 24 B, no
pad); `ResolveShaderImageRecord`'s identity test and I5 seam log go. The sweep gate is
`(ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration)` — never a
backend re-mint counter, so the "re-minted inside the sweep" property (`:2890-2901`) holds; the walk
stays the UNION of the record window and `g_imageUnitHighWaterMark` (`:2811-2821`), never narrowed
to the window. `MarkWritableImageBufferTexturesGpuWritten` is DELETED under a transport (the client
owns the GPU-write set, `GpuWritePending.h:9-55`, `:83-115`); `WritableMask` survives as the record
of which points P9's narrowing will name.
### 5.5 Programs (pg)
Twin from the ShaderCso handle; `SyncToBackend(h, const MGPipeShaderCsoRecord&)` answers every
reflection question from the record-owned archive and the bindings tails (`GetLinkedShaderSnapshot`
`:12049` is a debug log, deleted on this arm). The nine-clause clean condition (`DirectGLES.cpp:4437-4507`)
keeps its clause count and moves its inputs: `record.Serial` vs `m_syncedShaderCsoSerial` (exists,
stamped at `Managers.cpp:12944-12966`, unread today — P5e is its first reader), `BindingsSerial`,
`record.Signature`, `Desc.SpirvStatus`, `Desc.LinkStatus`; `ImageUnitFormatsStillMatch` and the
patch clauses are fb's / value rows. The stash `g_currentDrawFrontendProgram` (`:4097-4098`)
becomes `{MGPipeHandle, twin*}` compared against `st.DrawProgram`; `PrepareForCompute` reads
`st.DispatchProgram`. The sampler-pass memo (`:5522`) keys on `BindingsSerial`;
`sampledTargetForUnit` reads the sampler-unit tail and the archive's uniform types. **`MapUBO()`
has no run-ahead answer:** when `ResolveGlobalConstantsRecord` declines under a transport it is
`Fatal{UnmigratedVerb, "set_global_constants"}`, not a torn upload. `GetProgramObject/
ValidateProgramName` keep their P9 rows: `GetBackendProgramId(name)` (`:6151`) is a monolith entry;
`ShaderStorageBlockBinding` (`:10020`) is reached from the barriered `set_storage_block_binding`
(§2.2) until pg's trailing item resolves it by `MGPStorageBlockBinding::ShaderCso`. Codec
completeness: `ProgramArtifactsCodec.cpp:242`'s skipped member is named and either encoded or
proven unread by the twin before pg claims "every reflection question is answered".
### 5.6 Buffer binding points (sb)
Applier: `BoundShaderBuffers[3][84]`, `ShaderBufferStart[3]` / `ShaderBufferCount[3]` /
`ShaderBufferWritableMask[3]` (the draft said bare `Start/Count/WritableMask`; they sit in
`MGPipeApplierState` beside `SamplerViewStart`/`SamplerViewCount` and take the same shape), ONE
`ShaderBuffersSerial` advanced on every applied record and ADVANCED (never zeroed) by
`MGPipeApplierReset` (`VertexBuffersSerial`'s rule, `PipeApply.h`); the emitter's latch resets with
it (`PipeFill.cpp:3129-3143`'s list). c0e declares all of it, with
`MGPipeApplySetShaderBuffers` declared and aborting by name until sb writes the body.
**`WritableMask` IS A `Uint32` AND THE WINDOW IS 84** (`MGPShaderBuffers::WritableMask`, unchanged
since P4a): the mask can only describe the first 32 points of a class, so sb either narrows the
window it sets bits for, widens the field, or states the bound — an integrator ruling sb must ask
for rather than pick, because the field is on the wire. `SyncBufferBindingPointsByRecord(Class, glTarget)`: per entry
`EnsureBufferResourceForHandle(nullptr, Res)`; whole-vs-range is `Offset == 0 && Size ==
kMGPipeWholeBuffer`; clean = `resource->syncedChangeSerial` vs the resource record's `Serial`
(`Managers.cpp:3151`) plus `ShaderBuffersSerial`; no twin key is introduced (a point names an object,
it is not one). The UBO loop (`DirectGLES.cpp:5447-5497`) reads `BoundShaderBuffers[Uniform][binding]`
where `binding` is the program's block binding (pg's tail); `binding >= Count` = nothing bound;
`GetBufferResource(obj)` `:5472` becomes `FindBufferResourceForHandle`. `SyncAtomicCounterBuffers`
reads `Count[AtomicCounter]`; `GetBufferBindingPointCount`'s sticky forward becomes FATAL under
split (`InvalidateCompileEnv`'s shape, `FieldOwnership.def:163-165`). The three backend GPU-write
marks (`DirectGLES.cpp:606, 658, 2775`) are deleted under a transport in ONE edit with fb's.
**Client shutter (the P4b hole, verified: bits 15/16/17 read the content aggregate, `Tracker.h:525,
636-639`, and `glBindBufferBase` moves only `BindingSlot::m_version`):** bit 15 =
`Mix(bindPointGen[Uniform], programIdentity)`, bit 16 = `Mix(gen[ShaderStorage], gen[AtomicCounter])`,
bit 17 = `Mix(gen[TransformFeedback], TransformFeedbackGeneration)`; the content aggregate leaves all
three. `Touch` joins `gen_pipe_dirty_surface.py`'s `MUTATOR_PREFIXES` (`:87-90`) and `DirtySurface.def`
gains the rows the generator then demands. `Coverage.def`'s EMITTED list gains `GetBufferBindingPoint
→ SetShaderBuffers` as SHAPE-ONLY — **landed by c0e, with BOTH of its `PipeFill.cpp` switch arms
(`SubsystemForEmitter` and `EmittedCallSuppliesTheWholeField`) and the pairing `static_assert`s**,
because that file belongs to the contract package for the whole phase and an emitter enumerator
whose dispatch arm lived in the family's own worktree is exactly the merge trap the file's own
comment was written about. It is inert until the family's wired constant leaves 0.
`FieldOwnership.def:80-81` flips to RECORD_SUPPLIED when the four Espryt consumers are gone. `GetBufferBindingSlot`'s 15-pointer copy (`PipeFill.cpp:116-124`) needs no
special case: an unbarriered record has no fill at all.
### 5.7 XFB — stays lockstep
`StartPendingTransformFeedback` (`:1381-1510`), `SyncTransformFeedbackBindingPoints` (`:562-592`),
`ReadbackCapturedRanges` (`:1118-1160`) and the `xfb.targets` pins are not migrated
(`GetTransformFeedbackProgram` is an object row no package retires; the targets hold `SharedPtr`s
across two verbs, which rule C forbids an applier entry to do without a barrier). Escalation (i)
makes every verb inside an active span barriered; the cost on the Espryt steady path is one bool
test per draw (`:1386`).
### 5.8 Monolith / G1 / the push build under `Transport=monolith` (ruling 1)
Every server-side handle arm is selected by `MG_Config::Transport != Monolith` (the idiom at
`DirectGLES.cpp:3226, 4725, 6534`, `Managers.cpp:9629`) AND the family bit; it runs under ANY
active transport, lockstep or run-ahead, so `RUN_AHEAD=0` is a pure wait-rule A/B on identical
server code. The push build under `Transport=monolith` keeps its frontend arms token for token
(behaviour and bytes), which the verify comparator needs; the pull build compiles none of it (every
new symbol sits under `MOBILEGL_PIPE_PUSH` / `MOBILEGL_BUILD_DISAGGREGATED`; the `TwinRegistry`
alias keeps its mangled name, `Managers.h:626-640`; `BackendTextureObject`'s pull-build layout does
not move — the admitted-resize set stays empty). Two overloads, not an `#if` inside one body — the
frontend one is visibly the monolith-glue half, as `GetOrCreate(const StatePtr&)` is
(`SlotTables.h:236-244`). G1 0/0/0/0, G2/G14 name-set rules kept; the new public names are listed in
BRIEF-P5E per package. `MOBILEGL_PIPE_VERIFY` forces lockstep (`ConfigLoader.cpp:377`, confirmed).
---
## §6 Magma keeps the lockstep
1. The DirectVulkan arm of `CallMask` never sets `kCapRunAheadApply`; `RunAheadArmed()` is false;
the client runs today's `:915-959` path; `MGPipeApplierCurrentRecordIsBarriered()` answers `true`
for every record on a server that does not publish the bit, so Magma's probes inside
`MagmaP7AllocatorDebtScope` and its BARRIER_PULLED reads keep P5C's semantics and `rsp`
accounting. P7 retires them.
2. On Espryt the bit is published by `Init.cpp`'s DirectGLES arm gated on one constant
(`kMGPipeP5eRunAheadReady`, false until the integration commit), so every package lands with the
column inert and nothing changes behaviour until the integrator flips it.
3. Negative controls: `MOBILEGL_IPC_RUN_AHEAD=1` on Magma logs once and runs lockstep;
`MagmaPipeIdentityTest`/`ServerLoopTest` pin that a DirectVulkan `CallMask` has no bit 10.
---
## §7 The strict gate
`integration-split-strict` (`.github/workflows/test.yml:1140-1206`) becomes a HARD GREEN lane for
the Espryt run-ahead server: with `MOBILEGL_IPC_STRICT_ERRORS=1` and the caps bit published, every
scenario must complete and every `[BARRIER-PULLED, ...]` marker in a private log is red. Because
§3.3 aborts on an unbarriered pull regardless of the knob, strict adds only the barriered rows'
pulls (readbacks, GenerateMipmap until tx2, XFB, CopyTex, set_storage_block_binding) — the lane's
step lists those rows as the ONLY admitted markers (an allowlist by `<field>@<verb>`) and fails on
any other. A revert of any handle arm in vi/sb/pg/tx2/fb goes red HERE, by field and verb, not in
a picture. Magma keeps the expected-red step. `rsp` on unbarriered records is 0 in every scenario
summary (pinned: a non-zero count would be a Fatal that did not fire). The `PipeSlotPeek` harness
asserts zero scope entries per frame on the draw path.
---
## §8 Amendments to CONTRACT-P5C (the complete list)
1. **§3.1's two named exemptions** are superseded by §4.4: the registry scope is deleted at every
draw-path site and exempts only a barriered record elsewhere; the announcement scope is Magma's.
2. **§4.4 overflow**: lossless stays, but a run-ahead server parks on `Reserve == nullptr` (§2.6).
3. **§5.2 `object_death`**: the ring keeps the ordering; the row is `kWaitNone`.
4. **§5.1 `applier_reset`** is `kWaitApplied`; its `ContextSerial` assertion stands.
5. **§6 CI lane**: the two-sided lane becomes one-sided green for Espryt (§7).
6. **§6 layer 2** gains `Fatal{RoleViolation, "gPipeInputs"}` (§3.5); `InBarrierWait()` is
re-derived as `MGPipeBarriered`. 7. **§3.3's state note** is retired by fb (§5.4).
8. **`FieldOwnership.def`**: `GetBoundVertexArray`, `GetTextureUnitObject`, `GetImageTextureBinding`,
`GetFramebufferBindingSlot`, `GetProgramForDraw/Dispatch`, `GetBufferBindingPoint` gain retiring
phase "P5e (Espryt unbarriered), P7 (Magma)"; `GetBufferBindingPointCount`'s forward → FATAL under
split; `GetTransformFeedbackProgram`, `GetProgramObject`, `GetTextureObject`, `ValidateProgramName`,
`HasOpenTransformFeedbackSpan`, `RecordError` keep their rows (barriered-only readers).
9. **Table 1** gains `set_program_bindings` (80), appended; `set_shader_buffers` (38) gains its
route, sink and emitter (`PipeCatalogueTest.cpp:170-171, 242-243` invert). **Table 0** gains §1's
thirteen rows. **Table 3** gains `IpcTable::RunAhead/PresentCredit`.
10. **P5C §6's `rsp` paragraph**: the measured object-class pulls (948.5/frame on bsl) go to 0 on
unbarriered records; what remains is the barriered allowlist of §7.
## §9 The rulings, as made (ID-80…98; BRIEF-P5E §6 has the conflicts they resolve)
All nineteen are ACCEPTED AS WRITTEN. They are restated here rather than referenced, because a
package reads this file and not the brief.
1. Monolith arm selection: transport check + family bit, frontend arms kept for monolith (§5.8) —
NOT S7's "handle arm under monolith too" (it would make the monolith push build depend on the
new emissions and lose the verify comparator's frontend arm).
2. Client vertex arrays: refuse under run-ahead (§5.1), stage in P8; wait-escalation rejected (it
would need `Find(vao)` on the apply thread, which §4.4 forbids for a draw).
3. XFB / atomic dispatch waits: the wire-derived predicate (§2.1); atomic-counter dispatch needs no
escalation because sb lands 4.5.
4. Present credit default 1, range 1..8; credit 2 is a measurement arm only.
5. `resource_subdata`: buffer half `kWaitNone`, texture half keeps the reply (trailing D-D5).
6. `BindCurrentTextures` memo key: both S2's and S4's rows (§5.3).
7. Image sweep gate: applier serials, never a backend counter (§5.4); keep the union walk.
8. Program archive: encode per link into SEG_STAGE (§1), measured on MC + a shader pack before pg
is sized; the `SharedPtr` pin is refused; lazy encode at first bind is the fallback.
9. Failed relink: re-issue with `LinkStatus = 0` iff the frontend reports the program unlinked
afterwards; pg verifies which at `ProgramObject.cpp:348-356, 518-522`. Never an `object_death`.
10. `set_shader_buffers` window: the touched high-water count per target; capacity 84 on the wire,
the backend clamps (`DirectGLES.cpp:500-504`). 11. Suppressor: three class slots.
12. Magma scope: one renamed scope, backend-kind-keyed exemption (§4.4).
13. Deferred-destroy queue: stays live (§2.7). 14. `ResolvedDrawBuffers`: measured after vi.
15. `Blit`'s `kWaitNone` is fb's landing, not a separate package; `Clear`'s is tx2+fb.
---
## §10 What c0e landed, by name — the surface every other package compiles against
Inert by construction: the caps bit is not published (`kMGPipeP5eRunAheadReady` is `false`), the
new opcode has no route, the new subsystem bit is not in `kMGPipeWiredSubsystems`, and every new
applier entry point and by-handle backend signature has a body that aborts by name. Unit,
`integration-split` and the generator gates are green with all of it in the tree.
| where | the names |
|---|---|
| `MG_Pipe/MGPipeTypes.h` | `kCapRunAheadApply`, `MGPipeRunAheadCapBitsFor(BackendType, Bool)`, `kMGPipeMaxBufferBindingPoints`, `kMGPipeShaderBufferClass{Uniform,ShaderStorage,AtomicCounter,Count}`, `MGPProgramBindings`, `MGPProgramSamplerUnit`, `MGPProgramStorageOverride`, `kMGPipeMaxProgram{BlockBindings,SamplerUnits,StorageOverrides}`, `MGPProgramDesc::LinkStatus`, `kDrawClientArrays` |
| `MG_Pipe/MGPipeValueTypes.h` | `MGPipeImageAccess`, `kMGPipeImageAccess{ReadOnly,WriteOnly,ReadWrite}`, `MGPipeImageAccessIsValid`, `MGPipeDecodeImageAccess`, `MGPipeImageAccessReads/Writes` |
| `MG_Pipe/MGPipe.h` | `MGPipeWaitClass` (`kWaitNone/kWaitApplied/kWaitReply/kWaitPresent/kWaitClassCount`), `kMGPipeSubsystemBufferBindings`, `kMGPipeSubsystemsMigratedAtP5e` |
| `MG_Pipe/generated/PipeWire.inc` | `MGPWireOp::SetProgramBindings` (80), `kMGPipeWaitClasses[]`, `MGPipeWaitClassFor(op)` |
| `MG_Pipe/PipeApply.h` | `MGPipeBarriered(op, payload, st)` (implemented), `MGPipeApplierCurrentRecordIsBarriered()` / `MGPipeApplierSetCurrentRecordBarriered()` (implemented, default `true`), `MGPipeApplySetShaderBuffers` and `MGPipeApplySetProgramBindings` (declared, aborting), `MGPipeApplierState::{BoundShaderBuffers, ShaderBufferStart, ShaderBufferCount, ShaderBufferWritableMask, ShaderBuffersSerial, IsTransformFeedbackActive}`, `MGPipeShaderCsoRecord::{BlockBindings, SamplerUnits, StorageOverrides, Signature, BindingsSerial}`, `MGPipeProgramStorageOverride` |
| `MG_Impl/Pipe/SetHashSuppressor.h` | `MGPipeSuppressorSlot::{SetShaderBuffersUniform, SetShaderBuffersShaderStorage, SetShaderBuffersAtomicCounter, SetProgramBindings}` |
| `MG_Impl/Pipe/Tracker.h` | `kMGPipeDirtyEmittedAtP5e`; bits 15/16/17 mapped onto bit 13 |
| `MG_Impl/Pipe/ImageEmit.h` | `MGPipeEncodeImageAccess` promoted to a free function over the shared table |
| `MG_Backend/DirectGLES/Managers.h` | `VertexArrayImpl::ResolveVaoTwin(handle)`, `TextureImpl::SyncTextureToBackendByHandle`, `TextureImpl::ResolveTextureTwin`, `BackendTextureObject::SyncMipmapsToBackendByHandle`, `BackendFramebufferObject::SyncToBackendByHandle`, `BackendRenderbufferObject::SyncToBackendByHandle`, `BackendProgramObjectImpl::SyncToBackendByHandle`, `PrgramImpl::ResolveProgramTwin` — all declared, all aborting by name, one block at the end of `Managers.cpp` |
| `Config.h` / `ConfigLoader.cpp` | `IpcTable::RunAhead`, `IpcTable::PresentCredit`; the push default is `kMGPipeSubsystemsMigratedAtP5e` |
| `MG_Backend/Init.cpp` | `kMGPipeP5eRunAheadReady` (`false`) and the caps arm through `MGPipeRunAheadCapBitsFor` |
**The three red-onces c0e ran, and what they name.** (1) flipping one row's `WaitClass` without
regenerating → `gen_pipe: OUT OF DATE: MobileGL/MG_Pipe/generated/PipeWire.inc`, rc 1; with the
regeneration, `PipeCatalogue.EveryRowCarriesTheWaitClassTheContractGivesIt` fails naming
`GenerateMipmap`. (2) `sizeof(MGPProgramBindings)` 32 → 40 →
`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` fails "Which is: 40 / 32". (3) making
the caps arm ignore the backend type →
`MagmaPipeIdentityTest.AMagmaServerNeverPublishesTheRunAheadCapBit` fails "Which is: 1024 / 0".
+7 -5
View File
@@ -11,7 +11,8 @@
| **P5b** inproc 下的 verb 迁移(Minecraft 优先) | **已收官(2026-09-16)** | `37fc4fdb..82683d4a`;`MEASUREMENTS.md` §7 |
| **P5c** `inproc` 共享内存读点归零 | **已收官(2026-09-17)** | `11ac3de6..b88e8487` + triage 修复;契约 `MobileGL/MG_Remote/CONTRACT-P5C.md`;审计 `~/w7/notes/p5c/p5c-audit-v1.md` |
| **P5d** `inproc` 性能专项 | **已收官(2026-09-18,三轮)** | `cb06538c`、`56a77348`、`1f8de61b`;报告 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md);`MEASUREMENTS.md` §9 |
| P6 spawn transport | **下一个** | 届时只是传输替换 |
| **P5e** 退役 Espryt draw path 的 lockstep | **进行中** | 契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`(c0e 落地);计划 `~/w7/notes/p5e/BRIEF-P5E.md`、裁定 `~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`(ID-80..98);包序 c0e → id → {vi, sb, pg, tx2, fb} ∥ ra,集成 commit 翻 `kMGPipeP5eRunAheadReady` |
| P6 spawn transport | P5e 之后 | 届时只是传输替换 |
## 2. 当前头实测
@@ -85,10 +86,11 @@
## 6. 下一步
0. P5d 的遗留(`P5D-INPROC-PERFORMANCE.md` "什么没完成"):R-1 序列化留给 P3b/P4b → P11(`gPipeInputs` 版本化已写进 P11 行);线程放置记录;小项随 P3b/P4b 顺手。
1. **P6 spawn transport**:`SocketTransport` + `ServerMain` + 握手 / 退出语义 + EGL forwarder 的控制面帧;P5c 之后这只是传输替换。注意 P5c 留下的:`s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置;两个豁免 scope 里的探测在 spawn 下根本不存在对应内存,P6 第一天的红就是它们的清单。
2. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、`texture-remint-pull` 仿真槽)。
3. P6 出口门:P5b 的完整渲染路径在 `spawn` 下绿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。
4. Redmi 四臂复测(P5c 的记录项,需设备窗口);79 trace 普查重跑(需全集语料)。
1. **P5e 退役 draw path 的 lockstep**(进行中):c0e 已落地契约与线上行——`PipeCalls.def` 的第五列 `WaitClass` + 生成的 `MGPipeWaitClassFor(op)`、`set_program_bindings`(opcode 80,空路由)、`kCapRunAheadApply`(位 10,只在 DirectGLES 臂且只在 `kMGPipeP5eRunAheadReady` 为真时发布)、`kDrawClientArrays`、子系统位 13 与 push 默认 `0x3fff`、`MGPipeImageAccess` 一张表、`MOBILEGL_IPC_RUN_AHEAD` / `MOBILEGL_IPC_PRESENT_CREDIT`、以及跨包 seam(`MGPipeBarriered`、两个 applier 入口、六个 by-handle 后端签名)。全部 inert:caps 位未发布前 client 跑今天的 lockstep 路径。下一个是 **id**(registry 重键 + by-handle resolver + 分配器守卫),然后 {vi, sb, pg, tx2, fb} 并行、ra 从第一天起并行。
2. **P6 spawn transport**:`SocketTransport` + `ServerMain` + 握手 / 退出语义 + EGL forwarder 的控制面帧;P5c 之后这只是传输替换。注意 P5c 留下的:`s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置;两个豁免 scope 里的探测在 spawn 下根本不存在对应内存,P6 第一天的红就是它们的清单。
3. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、`texture-remint-pull` 仿真槽)。
4. P5e 出口门(`~/w7/notes/p5e/BRIEF-P5E.md` §3 / §4):`integration-split-strict` 转硬绿车道、三条阴性对照、Redmi 四臂(monolith / lockstep / credit 1 / credit 2)。P6 出口门:P5b 的完整渲染路径在 `spawn` 下绿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。
5. Redmi 四臂复测(P5c 的记录项,需设备窗口);79 trace 普查重跑(需全集语料)。
## 7. 记录位置
+4 -3
View File
@@ -1,6 +1,6 @@
# MGPipe 路线图
> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b、P5c、P5d 已收官**(P5c:2026-09-17,代码头 `b88e8487`;P5d:三轮,2026-09-18,代码头 `1f8de61b`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**;设计见 `ARCHITECTURE.md`;逐阶段实测见 `MEASUREMENTS.md`;P5d 报告见 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md)。**下一个是 P6 spawn transport**(此时只是传输替换)。
> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b、P5c、P5d 已收官**(P5c:2026-09-17,代码头 `b88e8487`;P5d:三轮,2026-09-18,代码头 `1f8de61b`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**;设计见 `ARCHITECTURE.md`;逐阶段实测见 `MEASUREMENTS.md`;P5d 报告见 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md)。**当前在 P5e**(退役 Espryt draw path 的 lockstep,契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`);之后才是 P6 spawn transport(此时只是传输替换)。
## 通用纪律(每个 commit)
@@ -12,7 +12,7 @@
- **拆分不借机顺手修 `dev` 的 bug**:句柄化过程中发现的 `dev` 侧缺陷记成独立条目、独立 PR(开放问题 15、17)。
- 过程(用户 2026-09-16,ID-66):不过度验证;轮次间不做对抗性审查,每个 P 阶段收官由 Codex 审一次,发现进入下一阶段首轮;只读 / 非核心任务派给 Codex;实现方与审查方错开模型族。
两条跑道分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b → **P5c** → P6 → P9 → P10 → P11 → P12。目标顺序:先完整的 separate-thread rendering(P5b,已达成),再让 `inproc` 变成只经 wire 交换的诚实两角色(P5c),最后才是 separate-process transport(P6,此时只是传输替换)。
两条跑道分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b → P5c → P5d → **P5e** → P6 → P9 → P10 → P11 → P12。目标顺序:先完整的 separate-thread rendering(P5b,已达成),再让 `inproc` 变成只经 wire 交换的诚实两角色(P5c),再把它的 CPU 代价压到可接受(P5d),然后退役 draw path 上的 lockstep(P5e:client 发了就走,server 只从记录带来的句柄解析对象),最后才是 separate-process transport(P6,此时只是传输替换)。
## 阶段
@@ -28,7 +28,8 @@
| **P5b** `inproc` verb migration | ✅ `37fc4fdb..82683d4a` | class-C 普查决定顺序;d1 19 / i1 7 / t2 6 / f1 11 槽迁移;sync 五槽;具名 blit;GLES mip descriptor;r1 / r2;收官审查三项修复。发射表 A=2 / B=54 / C=15 | 主机全门 `348d22a4` complete;`integration-split` 107/107;合并普查零回退、79 trace 72/6/1;**Redmi 正确性 8/8 + 四臂 A/B、barrier tax 首测**。§7 |
| **P5c** `inproc` 共享内存读点归零 | ✅ `11ac3de6..b88e8487` | server 端纹理 staged shadow(`StagedTextureStore`,句柄为键、整段覆盖、defined-ness 追踪);`SEG_EVENT` 三个 producer + `kEventGlError`(反向通道零裸指针);sink / twin 按记录句柄解析(blit 具名臂经 verb 句柄工作区,Magma 走 G6 消费臂);`applier_reset` / `object_death` 控制记录(opcode 77/78,framebuffer 首个 wire delete);`set_context_values`(79)退役全部值类 BARRIER-PULLED 行,三 shutter 自答;双层 `Fatal{RoleViolation}` 守卫 + `InBarrierWait` 接线;两个具名豁免 scope(通告家族 P4b/P7、G6 registry 家族 P3b/P4b)承接未到期的债 | 角色守卫开启下 unit 2187/2187(strict 下同绿)、`integration-split` 111/111、每层守卫 red-once 按名变红;纹理 `0xDD` audit 在 bsl / complementary 两条 in-world trace 零 Fatal;rsp 按帧实测(bsl 948.5/帧,残留全为钉住的对象类 15 行);G1 三个认定 resize(SwapchainObject::Create / CopyTexSubImage2D / ScopedRestartIndexSubstitution,均已具名)+ `.text` −16 B、pull 构建零 MG_Remote 符号;G5 保护区字节一致;双生成器 --check/--self-test 绿 | P5b |
| **P5d** `inproc` 性能专项 | ✅ `cb06538c`、`56a77348`、`1f8de61b` | 一二轮:persistent-map 推送三级臂(mprotect 脏页追踪 + 哈希抑制 + 绑定过滤 `PushDrawConsumers`)、barrier 批处理 `MOBILEGL_IPC_BATCH_WAITS` + 延期销毁队列、自旋时钟 64 合 1、零页注册 + shadow 页对齐分配。三轮(按符号化 profile 逐项切,四包 + 一份 lockstep 可行性研究):apply 空转轮询零锁(控制邮箱原子影子位)、`Doorbell::Wait` 校准自旋稳态不读时钟、wait/park 计数进 stats;`OnApplyThread()` 内联为线程指针比较、三个 scope 深度计数器退掉 `thread_local`;shadow 分配尺寸补页、追踪范围向外对齐、边块哈希消失、脏页排空修复;image unit 高水位、残余填充 memo、server stamp 常量表;`generate_mipmap` 不再免等(apply 读残余输入)、death 测试围栏 | 三轮设备(Redmi,CPU 定频):inproc **64.3 → 103-106 fps**(p50),client 线程 CPU 15.0 → 9.2 ms/帧、apply 12.4 → 7.0-7.4;monolith 同场景 115(120 Hz vsync 封顶的运行)/ 206(一次未封顶,渲染线程 5.0 ms/帧 大核);配对比较以逐线程 CPU/帧为主,见报告"数字"节;unit 2203/2203、`integration-split` 111/111(两条曾 flaky 的用例复跑 6/6);双生成器、include 闭包、dirty-surface、doc 引用绿。未完成:R-1 序列化(研究结论:留给 P3b/P4b 后的 P11 `gPipeInputs` 版本化)、线程放置(内核忽略亲和)、`SetHashSuppressor` 哈希等小项。报告:[`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md) | P5b |
| **P6** spawn transport | P5c 之后 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch;EGL forwarder 的控制面帧;P5c 留下的 `s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 |
| **P5e** 退役 Espryt draw path 的 lockstep | 🚧 进行中 | 契约 `MG_Remote/CONTRACT-P5E.md`(规则 F:未设障记录的 apply 不得读任何 client 内存;19 条裁定见 §9)。八包:**c0e** 契约 + 线上行(`PipeCalls.def` 第五列 `WaitClass` + `MGPipeWaitClassFor`、`set_program_bindings` opcode 80 空路由、`kCapRunAheadApply` 位 10、`kDrawClientArrays`、子系统位 13、`MGPipeImageAccess` 一张表、两个旋钮、跨包 seam 声明)→ **id** 身份(registry 按 `{slot, gen}` 重键、by-handle resolver、分配器守卫)→ **vi / sb / pg / tx2 / fb** 五个 per-draw 家族并行(VAO / 缓冲绑定点 / program 归档 + 绑定 / 纹理 + sampler 窗口 / FBO + image)∥ **ra** 传输机制(等待规则 `EmitAndWaitTails`、present credit、`gPipeInputs` 变 server 角色内存、事件环流控)。集成 commit 翻 `kMGPipeP5eRunAheadReady` | 集成门(§3):unit 两臂绿;`integration-split` 111+ 绿 + inproc 普查零回退 + 79 trace 无新首阻塞;`integration-split-strict` 转**硬绿车道**(只允许 §7 白名单的 `[BARRIER-PULLED]` 标记,未设障记录 `rsp` = 0);三条阴性对照(`MOBILEGL_IPC_RUN_AHEAD=0` SSIM 一致、`VERB_BARRIER=0` 在设障行变红、Magma 上 `RUN_AHEAD=1` 记一条日志后跑 lockstep);每包 red-once 在集成树复跑;G1 0/0/0/0、G2、G14、G5;monolith push 四条 A/B trace SSIM 不变。设备出口(§4,Redmi 30 s 在世界窗口,四臂):fps ≥ lockstep、client 线程 CPU ms/帧 < lockstep、SSIM 不变,且逐 op 等待清单里**没有 `kWaitApplied` 行、没有逐 draw 的 reply 行** | P5d |
| **P6** spawn transport | P5e 之后 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch;EGL forwarder 的控制面帧;P5c 留下的 `s_synced` / `g_syncedRenderStateParameters` 按 context 世代重置 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 |
| **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 原生化;回读 / pack state;P4a 记下的 R-3 / R-4 / R-5 / R-7 / R-10 / R-11;`ProgramArtifacts.h` 的 NDK 尺寸钉 | 纹理 / program 场景;CTS `texture_*` / `shader_image_*` / `packed_pixels` 在 0.5 pp 内;每一个 Iris trace;`TextureUploadShapeScenario` 升级成门 |
| **P7** DirectVulkan(Magma)全量迁移 | 待排 | 其余 10 个子系统:`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化;具名 UBO host payload(D-B8);内部 shader 烘焙;`VertexInputStateFactory` 内容寻址 CSO;D18 容器纪律保留 | 集成 + trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;`nm -D libMobileGLServer.so \| grep glslang` 为空;CTS 0.5 pp 内。**再基线检查点:中点完成子系统 < 40% 立即重定基线** |
| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 待排 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,逐站点 reconcile);`Server/IndexHostMirror`;CopyImage 镜像搬到 client;viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;大 blob carrier(开放问题 11);无 present fence tick;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同;trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` fixture;`ClientArrayAfterComputeWriteScenario`;`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes` 逐用例发布 |