mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[MG_Remote] (P6/c6): the normative contract, and the data-plane seam declared but not wired
c6 is P6's contract package. It lands CONTRACT-P6.md plus two headers and changes nothing: no caller moves, no byte moves on any wire. THE CONTRACT supersedes P6-CONTRACT-DRAFT.md, which was written before its own audit and is wrong in four places - the ABI row, "the latch is implemented unchanged", "cp is transport only", and "a transport swap and nothing else". a6 measured all four. It carries 21 rulings, of which the load-bearing one is D1: the spawned child keeps Transport == Spawn and anti-recursion becomes a separate Dial axis. ARCHITECTURE.md:488's literal instruction is amended, because 226 live lines test `Transport != Monolith` to select the SERVER arm and forcing Monolith flips 148 MG_Backend lines to frontend glue in the one process that has no frontend. That ruling can be stated normatively without changing behaviour because of something a6 found and the contract exploits: TransportMode::Spawn is UNREACHABLE at this head. ConfigLoader.cpp:344 is the only writer for the string "spawn" and it writes Monolith. So every rule conditioned on Transport == Spawn constrains a state no process can enter - and the same fact is why ConfigLoader's named refusal cannot currently falsify itself, which is sm's to fix and §9.5's arm-proof gate to catch. THE SEAM. ITransport says in its own header that the hot path bypasses it; below it there was no interface of any kind, because Ring.h, ReplySlot.h and EventRing.h each take a raw void* plus a capacity. ILink.h is that missing thing. It is setup-time, not hot-path: no method is called per record or inside a spin predicate, because Doorbell::Wait's predicate runs 1e5-1e6 times a frame reading watermarks straight off the page, and because the only lane that could measure a virtual there has four times the dispersion of the effect - a gate that can only print "within noise" is ID-124 one level down. StreamLink.h is declared and implemented by nobody, and that is its job: ILink's adequacy is a claim only a compile can settle. Writing it forced three methods the brief did not name - ResolveSpan, FlushProgress, and the reply pair - so it has already earned its keep. Delete one ILink method it declares and the build goes red. Deliberately NOT here, each for a reason the contract records: Watermark::Reached is not redefined (SessionRings.h owns it); Ring.cpp's reserve/pop bodies are not relocated into a header that would then need the logger; RingControl is not regrouped, because that rename is 109 production sites with five behind a non-Android #if, so it is lk's with a published itemisation; and the schema rows are specified but left to hs, because deprecating SurfaceReply.defaultFb while appending `result` changes CreateSurfaceReply's arity and the existing six-argument call would silently bind its defaultFb 0 to result. INERTNESS IS STRUCTURAL, not measured. Zero object dependency files reference either header, zero build rules do, and nothing in the tree includes them. Verified anyway on both flavours: pull and split each have 0 symbols added, 0 removed, and .text byte-identical (10822147 / 11681395). check_doc_citations resolves all 61 citations.
This commit is contained in:
@@ -0,0 +1,627 @@
|
||||
# CONTRACT-P6 — the backend runs in a second process
|
||||
|
||||
Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17),
|
||||
`CONTRACT-P5B.md` (the class-C slots), `CONTRACT-P5C.md` (rule E, the two named exemptions, SEG_EVENT,
|
||||
the guards), `CONTRACT-P5E.md` (rule F, the barriered predicate, the wait rule) and
|
||||
`CONTRACT-MAGMA-RUNAHEAD.md`. Where it disagrees with any of them this file is newer and wins; §10
|
||||
lists every such place. Base: `feat/disaggregated @ 785fed0b`. Every `file:line` was read at that
|
||||
commit; paths are under `MobileGL/` unless they start with `docs/`.
|
||||
|
||||
**How to change it.** Package `c6`'s file, edited by the integrator first. A P6 package
|
||||
(`lk` / `hs` / `so` / `sm` / `cp` / `dl` / `st` / `t6`) that needs a row changed goes through the
|
||||
integrator; the packages compile against the rows below from day one.
|
||||
|
||||
**What this file is written from.** [`a6-audit-v1.md`](../../docs/Disaggregated/notes/p6/a6-audit-v1.md)
|
||||
(198 rows, seven items, each adversarially re-resolved) and
|
||||
[`a6-link-experiment.md`](../../docs/Disaggregated/notes/p6/a6-link-experiment.md) (the 184 symbols).
|
||||
It supersedes [`P6-CONTRACT-DRAFT.md`](../../docs/Disaggregated/P6-CONTRACT-DRAFT.md), which was
|
||||
written before its own audit and is **wrong in four places** (§10.1). The re-scoping against the
|
||||
eventual client-in-a-VM end state is
|
||||
[`P6-ENDSTATE-REVIEW.md`](../../docs/Disaggregated/P6-ENDSTATE-REVIEW.md); this file takes from it
|
||||
only what P6 must not preclude, and §11 says what P6 explicitly does not take on.
|
||||
|
||||
---
|
||||
|
||||
## §0 What c6 is, and the rule above every row
|
||||
|
||||
`c6` lands **this file and two headers**. It changes no behaviour, adds no caller, and moves no
|
||||
byte on any wire. Every rule below that describes an action names the package that implements it.
|
||||
A rule with no package named is a statement about the tree as it already is.
|
||||
|
||||
**Rule G — a role may not name a process-local handle.** Rule E forbade naming the other role's
|
||||
*memory*; rule G extends it to *identity*. A value whose meaning comes from the owning process's
|
||||
kernel or loader state — `EGLDisplay`, `EGLSurface`, `EGLContext`, `ANativeWindow*`, any `void*`
|
||||
window handle, an fd number, a pid, a mapped address — either crosses as a **token** defined in §2,
|
||||
or does not cross. Descriptors cross only by a mechanism the transport **declares it supports**, and
|
||||
where the transport declares none they do not cross at all and the bytes are transferred instead;
|
||||
a transport may not invent a third answer.
|
||||
|
||||
> The draft hard-coded `SCM_RIGHTS` inside rule G. That is one transport's implementation of one
|
||||
> clause, not the rule, and writing it into the rule is what would make the rule wrong the first
|
||||
> time a transport has no descriptor passing at all.
|
||||
|
||||
**Rule H — a value whose meaning comes from the peer's window system never crosses. Permanently.**
|
||||
Not "until P12". P12 gives the *server* its own display; it does not give the client's window a way
|
||||
across. §6 is rule H's whole enforcement.
|
||||
|
||||
**Rule G's teeth** are §9's arm-proof gate: a spawn lane that silently ran monolith satisfies every
|
||||
other gate in this file.
|
||||
|
||||
---
|
||||
|
||||
## §1 The one fact that makes c6 possible: `Spawn` is unreachable today
|
||||
|
||||
`ConfigLoader.cpp:339-346` is the only writer of `MG_Config::Transport` for the string `spawn`, and
|
||||
it writes **`TransportMode::Monolith`** at `:344`, with a log line saying P5 does not implement it.
|
||||
|
||||
Two consequences, and the second is why this file can exist without changing behaviour:
|
||||
|
||||
1. **`ConfigLoader`'s named refusal cannot falsify itself.** It lands on the same value a server's
|
||||
resting state would have, so "the child was forced to monolith" and "the parent fell back" are
|
||||
one state. `sm` fixes this; until then, no lane can prove it ran spawn (§9.5).
|
||||
2. **Every rule in this file conditioned on `Transport == Spawn` is vacuous in c6's own tree.** It
|
||||
constrains a state no process can enter. That is how D1 (§3.1) is stated normatively while
|
||||
changing nothing, and `c6`'s gate pins the vacuity so the `ConfigLoader` change cannot land early.
|
||||
|
||||
---
|
||||
|
||||
## §2 Table 0 additions
|
||||
|
||||
| row | encoding | invalid | consumer | package |
|
||||
|---|---|---|---|---|
|
||||
| `DisplayToken` / `SurfaceToken` / `ContextToken` | `Uint64`, minted by the client, dense from 1, never reused in a session. Server keeps token → native handle; it never sees the client's EGL values. `protocol.fbs`'s `display` / `surface` / `context` are already `ulong` and are these. **The client already mints them** (`MG_State/EGLState/Core.h`'s `EncodeHandle`), contrary to `Server/SurfaceControlFrame.h:26-29`, which says inproc bit-casts the handle — that comment is corrected by `cp`. | `0` on any op but `ReleaseCurrent` → `Fatal{ProtocolCorruption}` | the server's surface map | `cp` |
|
||||
| `SurfaceOpKind::InitCapabilities = 11` | **Appended.** The draft claimed `ServerInitCapabilities` needs no wire form because "`CapsSnapshot` answers it". The *answer* is a `CapsSnapshot`; the **request** has a live client caller (`Client/BackendObject_Remote.cpp:126`) and the codec **refuses to encode it** (`Protocol/SurfaceOpCodec.cpp:119-122`, `InprocOnlyOpOnTheWire`). A spawn client cannot ask. | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | the server's control pump | `cp` |
|
||||
| `SurfaceReply.result: int` | **Appended, id 5.** Carries the `MobileGLResult` that `ServerApplyWireSurfaceOp` already computes and drops. Without it a client cannot tell a server that died mid-op from a legitimate refusal, and therefore cannot choose between arming the device-lost latch and returning `EGL_FALSE`. | — | the client's control demultiplexer | `hs` |
|
||||
| `SurfaceReply.defaultFb` | **`(deprecated)`.** The encoder passes a literal `0` offset (`SurfaceOpCodec.cpp:184`) and the decoder never reads it; `kEventSurfaceChanged` is the landed carrier (P5f `fc`). The slot is **burned, never deleted** — a deleted field frees its vtable slot for the next append. | — | nobody, by construction | `hs` |
|
||||
| `LinkTerms{dataPlane, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` | Appended to `Hello`/`Welcome`. `dataPlane` has exactly **one** legal value in P6, `SharedSegments`; `wireForm` exactly one, `StructImage`. Anything else is refused **by name**. The four sizes are **stated by the SERVER**. | any other value → `Refuse{ProtocolMismatch}` | the handshake | `hs` |
|
||||
| `Refuse{code, detail, peer values}` | Appended `CtrlMsg` union tag. A handshake disagreement is an **answer**, not an abort (§5.1). | — | both sides | `hs` |
|
||||
| `wireFingerprint` / `buildFingerprint` | The single `abiFingerprint` splits. §4. | mismatch → `Refuse`, never `Fatal` | the handshake | `hs` |
|
||||
| `FatalCode` | **Unchanged**, 7 values (`protocol.fbs:248-256`). `table Fatal` appends `family: string`. §5.2. | — | — | `dl` |
|
||||
|
||||
All schema edits are **append-only**. A FlatBuffers table field's id is its vtable slot and an enum
|
||||
value **is** the wire value; retired fields become `(deprecated)`, never disappear. `CapsSnapshot`'s
|
||||
four deprecated fields are the worked precedent and their reasoning is in `protocol.fbs` itself.
|
||||
|
||||
> **`hs` implementation note, from c6's review.** `flatc` drops deprecated fields from the generated
|
||||
> `Create*` helper. Deprecating `defaultFb` while appending `result` therefore changes
|
||||
> `CreateSurfaceReply`'s arity, and the existing 6-argument call at `SurfaceOpCodec.cpp:183-184`
|
||||
> would **silently bind its `/*defaultFb=*/0` to `result`**. `hs` converts that call site to
|
||||
> `SurfaceReplyBuilder` with explicit `add_*` in the same commit, so a positional mistake is
|
||||
> impossible rather than merely unlikely.
|
||||
|
||||
---
|
||||
|
||||
## §3 Role, Dial, and the six predicates
|
||||
|
||||
### 3.1 D1 — what `Transport` reads as in the server process
|
||||
|
||||
**`TransportMode::Spawn`. The child does not force `Monolith`.**
|
||||
|
||||
`ARCHITECTURE.md:488`'s literal instruction ("`mobilegl_server_main` hard-sets `MG_Config::Transport`
|
||||
to `Monolith` before reaching `Init()`") is **amended by this file**. a6 measured why: 226 live lines
|
||||
test `Transport != Monolith` to select the **server** arm, 148 of them in `MG_Backend`. Forcing
|
||||
`Monolith` flips all of them to frontend glue in the one process with no frontend. The sharpest
|
||||
instance is `PipeInputs::IsLive()` (`MG_Impl/Pipe/PipeFill.cpp:2135`): forced monolith, it reads
|
||||
`MG_State::pGLContext`, which is null there, so **58 `MGB_CTX_LIVE` guards all answer "not live"**.
|
||||
|
||||
Anti-recursion moves to a **separate, non-inherited axis**: `MG_Config::Dial == DialMode::No` in the
|
||||
child. The envp scrub stays as the second, independent catch, and S3 (§9) still falsifies it —
|
||||
and the diagnostic must **name which of the two fired**, or the double safety is untestable as two
|
||||
things. Package: `sm`.
|
||||
|
||||
### 3.2 D1c — there are six predicates, not three
|
||||
|
||||
a6 proposed three. Three do not cover the tree.
|
||||
|
||||
| predicate | asks | pull-build shape |
|
||||
|---|---|---|
|
||||
| `MGPipeSplitActive()` | do records cross a role boundary at all | `#else` returns `false` |
|
||||
| `MGPipeServerArm()` | am I executing as the server **right now** | `#else` returns `false` |
|
||||
| `MGPipeShouldDial()` | should this process connect out | `#else` returns `false` |
|
||||
| `MGPipeSessionLive()` | is a peer session live | role-defined; `#else` `false` |
|
||||
| `MGPipeBlocksAreDistinct()` | do the two roles share one storage object | `#else` `false` |
|
||||
| `MGPipeBackendIsLocal()` | is the backend in **this** process | `#else` `true` |
|
||||
|
||||
`ContextEpoch::Server` and `ActiveXfbState`'s role index are **`MGPipeSplitActive()`** sites, not
|
||||
`MGPipeServerArm()` sites: they are partition keys, and the `ServerArm` disjunct would make a
|
||||
partition key thread-dependent.
|
||||
|
||||
**D14 — all six must fold away in the pull build**, and the required shape is `SlotCaps.h`'s: the
|
||||
non-disaggregated build gets a **different definition** under `#else`, not a runtime-false predicate.
|
||||
`MGPipeSplitActive()` must additionally remain exactly one load of a process global and must never
|
||||
be latched into a `static Bool`. G1 is 0/0/0/0 and `.text` unchanged; anything else is a c6 defect.
|
||||
Package: `sm`.
|
||||
|
||||
### 3.3 D13 — rename before introducing
|
||||
|
||||
`MGPipeRoleSplitActive()` (`MG_Backend/MGPipe/PipeInputs.cpp:292`) means
|
||||
`Ipc.RoleSplitState && Transport != Monolith` — the **rehearsal knob**, default off. It is renamed
|
||||
to **`MGPipeRoleSplitRehearsalActive()`**, which is what it has always meant, and
|
||||
`MGPipeBlocksAreDistinct()` is introduced beside it as the wider predicate. a6's shape — rename, then
|
||||
widen the same function — is **overturned**: a widened function under a narrow name is a name that lies.
|
||||
|
||||
Blast radius, measured at this head: **12 sites**, of which `DualBlockScenario.cpp`'s assertion
|
||||
*message* embeds the old name and must be updated with it. Package: `sm`.
|
||||
|
||||
### 3.4 D10 — the four guards that cannot arm in a server-only process
|
||||
|
||||
| guard | today | becomes | package |
|
||||
|---|---|---|---|
|
||||
| `MGPipeApplierReset` layer 2 (`MG_Pipe/PipeApply.cpp:1353-1355`) | needs `ClientSession::Active() != nullptr`, permanently null in a server process | middle conjunct → `MGPipeSessionLive()`; third → `!MGPipeServerArm()` | `sm` |
|
||||
| `MGPipeServerBlockNoteIdentity` (`PipeInputs.cpp:314`) | early-returns unless the rehearsal knob is on | gate → `MGPipeBlocksAreDistinct()` | `sm` |
|
||||
| `CountBarrierPull`'s unconditional Fatal arm (`PipeInputs.cpp:174`) | knob-gated | rides `MGPipeBlocksAreDistinct()` | `sm` |
|
||||
| the two `RefuseFromApplyThread` helpers (`MG_Impl/Pipe/SlotAllocator.cpp:29-57`) | `ServerLoop::OnApplyThread()` | OR'd with a process-role fact set once by `ServerMain` | `sm` |
|
||||
|
||||
**The one that crashes rather than merely failing to arm.** With `MGPipeServerBlockNoteIdentity`
|
||||
early-returning, a spawn server's `gPipeInputs.ContextIdentity()` stays `nullptr` for the life of the
|
||||
process, and `DirectGLES.cpp:199-213`'s fb-slot memo does a **raw pointer compare with no generation**
|
||||
against it — so `nullptr == nullptr` reads as a **cache hit** and the first call uses an
|
||||
uninitialised slot. `PipeInputs.h:878-884`'s own comment says a named Fatal was intended there.
|
||||
`sm` lands the gate change; `st` lands the named Fatal.
|
||||
|
||||
---
|
||||
|
||||
## §4 The handshake (`hs`)
|
||||
|
||||
### 4.1 One equality test answers three different questions today
|
||||
|
||||
`AbiFingerprintInputs` (`Transport/SessionRings.h:544-566`) mixes wire properties, struct-layout
|
||||
properties and pure build identity into one `Uint64`. It splits:
|
||||
|
||||
**`wireFingerprint` — always compared.** Inputs, in order:
|
||||
an ordered **catalogue digest** over `PipeCalls.def`'s `(opcode, payload struct name, call-flag row,
|
||||
WaitClass)`; a **payload layout digest** derived from `PipeFields.def` (per member: name, `offsetof`,
|
||||
`sizeof`); the record-header layout; both blob-codec version stamps; the format-capability table's
|
||||
two extents; `kOpCount`; `MOBILEGL_ABI_VERSION`; endianness; pointer width; **and**
|
||||
`sizeof(MGPCaps)` and `sizeof(DynamicBackendParameters)`.
|
||||
|
||||
Those last two **stay compared** because they are wire facts, not build facts: `MGPWireRec_GetCaps`
|
||||
is composed from `sizeof(MGPCaps)` (`MG_Pipe/generated/PipeWire.inc`). `sizeof(GLFunctionsTable)` is
|
||||
**deleted** from the mix: it never crosses the wire, so adding one backend slot today invalidates
|
||||
every peer for no wire reason.
|
||||
|
||||
> **Why the layout digest is not optional.** `SessionRings.h:561-565`'s own comment says the git
|
||||
> stamp is in the mix because *two builds of the same sizes can still disagree about a FIELD ORDER,
|
||||
> which no sizeof can see*. A catalogue digest over `(opcode, struct name, flags, WaitClass)` sees
|
||||
> neither a member reorder inside a payload struct nor a width change. So the widely-proposed "drop
|
||||
> the git stamp, add a catalogue hash" is **wrong as usually stated** — it removes a guard and does
|
||||
> not replace it. The layout digest is the replacement, and `gen_pipe.py` already CI-enforces that
|
||||
> `PipeFields.def` names every member, so it is a generator addition and not a toolchain.
|
||||
|
||||
**`buildFingerprint` — compared only while `Dial == Fork`.** Under fork the child *is* the same
|
||||
binary, so comparing it catches a stale `MOBILEGL_IPC_SERVER_PATH` pointing at an old
|
||||
`libMobileGLServer.so` — a real P6 failure mode. **P6's policy is therefore strictly stronger than
|
||||
today's, not looser**; the end state is a policy flip, not a redesign.
|
||||
|
||||
### 4.2 The missing build stamp
|
||||
|
||||
a6 found `CMakeLists.txt:188-195` takes the git hash with **no `RESULT_VARIABLE` and no fallback**, so
|
||||
a failed `git rev-parse` silently yields `""`. Verified live: building this worktree from WSL
|
||||
produces `GIT_COMMIT_HASH_SHORT ""`, because the worktree's `.git` file holds a Windows path WSL's
|
||||
git cannot resolve. Two builds from different commits then get **identical** fingerprints and the
|
||||
stamp silently stops doing its documented job.
|
||||
|
||||
**The fix may not touch `GIT_COMMIT_HASH_SHORT`.** That macro is baked into
|
||||
`glGetString(GL_VERSION)` (`MG_Impl/GLImpl/Getter/GL_Getter.cpp:614`), so redefining it changes an
|
||||
**application-visible GL string** and the pull build's `.rodata` — a behaviour leak and a G1 break
|
||||
at once. Instead `MGGitHash.h.in` gains a **second** pair, `MOBILEGL_BUILD_STAMP_VALUE` and
|
||||
`MOBILEGL_BUILD_STAMP_PRESENT`, consumed only by the fingerprint, and the CMake step gains a
|
||||
`RESULT_VARIABLE`. A build with no stamp sets `PRESENT` to 0, and a `Dial == Fork` handshake whose
|
||||
peer reports `PRESENT == 0` is a **named refusal**: under fork the two peers are supposed to be the
|
||||
same binary, and without a stamp that premise cannot be checked at all. Package: `hs`.
|
||||
|
||||
### 4.3 Two verified asymmetries `hs` also fixes
|
||||
|
||||
- The client checks only `abiFingerprint` and **never `abiMajor`/`abiMinor`** (`ClientSession.cpp:566-568`).
|
||||
- `Hello::pid` is hard-coded `0` and `Welcome::serverPid` echoes the client's, so it is always 0
|
||||
(`ClientSession.cpp:519-521`, `ServerSession.cpp:532-534`). §9.5's arm-proof gate wants the child
|
||||
pid; `serverPid` is its natural carrier and must start carrying it.
|
||||
|
||||
### 4.4 Segment geometry (D-size)
|
||||
|
||||
`MOBILEGL_IPC_RING_MB` / `STAGE_MB` are read from the **client** process's env with ceilings of
|
||||
1024 / 4096 MiB (`ConfigLoader.cpp:363-364`), while the **server** creates the segments. Under spawn
|
||||
that is a guest environment variable sizing an allocation inside the renderer process. The four sizes
|
||||
become **server-stated** `LinkTerms` fields; the client's values become a *request* the server may
|
||||
clamp, and the clamped values are reported in `Welcome`.
|
||||
|
||||
`ShmSegment::Adopt`'s `fstat` "do not trust the peer's declared size" check
|
||||
(`Transport/ShmSegmentPosix.cpp:127-133`) is today the **only** bound, and it has no analogue on a
|
||||
link with no fd. The compile-time ceiling that refuses an oversized announcement by name is
|
||||
**P6.5's** to define; P6 does not pretend to have answered it.
|
||||
|
||||
---
|
||||
|
||||
## §5 Death, refusal, and what P5e changed
|
||||
|
||||
### 5.1 The handshake never aborts
|
||||
|
||||
A version or terms disagreement is a `Refuse{code, detail, peer values}` control message plus
|
||||
`MOBILEGL_ERR_PROTOCOL_MISMATCH` on **both** sides: the server sends `Refuse` and exits 0, the client
|
||||
reaps the child and refuses **by name**, never falling back to monolith. Both `FatalAbiMismatch` call
|
||||
sites go (`ServerSession.cpp:471-483`, `ClientSession.cpp:565-568`). The worked shape is eight lines
|
||||
away in the same function: `ServerSession.cpp:456-466` already returns
|
||||
`MOBILEGL_ERR_PROTOCOL_MISMATCH` for a malformed frame, with the correct `msg_as_Hello() != nullptr`
|
||||
guard. Package: `hs`.
|
||||
|
||||
### 5.2 `Fatal{}` keeps aborting in P6, and gains one funnel
|
||||
|
||||
a6 censused **92 abort sites** under `MG_Remote/` carrying **30 distinct family words**, against a
|
||||
**7-value** wire `FatalCode`. So `Session::Fail(FatalCode, detail)` as usually proposed **cannot carry
|
||||
today's vocabulary in its first argument**.
|
||||
|
||||
**D2 ruling.** The first argument is an internal `MGFatalFamily`, generated from a new
|
||||
`MG_Remote/FatalFamilies.def` — one row per family word, each row carrying its `FatalCode`
|
||||
projection. The **wire** keeps `FatalCode` unchanged; `table Fatal` appends `family: string`. The
|
||||
projection is a **total table**, not a switch with a `default:` arm — this build has **no `-Werror`
|
||||
anywhere** (verified: `CMakeLists.txt`, every `*.cmake`, both workflows), so a gate premised on
|
||||
`-Wswitch` failing the build is fiction.
|
||||
|
||||
`Session::Fail` stays `[[noreturn]]` in P6 and still ends in `abort()`. Log lines, CI greps and every
|
||||
red-once are byte-identical. What the funnel buys, and what makes it worth `dl`'s two days:
|
||||
|
||||
- **one** place that publishes a `SessionFault{family, detail, seq, op}` frame to the peer **before**
|
||||
dying, so the guest's log names which record killed the session instead of reading a bare EOF;
|
||||
- one telemetry point;
|
||||
- a CI census gate refusing any **new** bare abort outside the funnel.
|
||||
|
||||
**The policy flip is not P6's.** Making `Fail` return needs a real return path invented at every site
|
||||
whose callers rely on `[[noreturn]]`, and a6 counted how many do. That is `Ph`'s (§11).
|
||||
|
||||
Two sites carry **no `Fatal{` marker at all** (`Client/PersistentMapTracker.cpp:1052-1058`,
|
||||
`Transport/WireLog.cpp:41-50`), so "the log stays verbatim" is not true of them and the family grep
|
||||
cannot see them. `dl` gives each a family tag.
|
||||
|
||||
### 5.3 The device-lost latch does not exist, and P6 writes it
|
||||
|
||||
`P6-CONTRACT-DRAFT §5.1` says the latch "is implemented unchanged". It is not implemented at all:
|
||||
`FatalCode::DeviceLost`/`ServerCrashed` have zero producers and zero consumers,
|
||||
`glGetGraphicsResetStatus` returns `GL_NO_ERROR` unconditionally
|
||||
(`MG_Impl/GLImpl/Getter/GL_Getter.cpp:2855-2861`), and `MOBILEGL_IPC_RESPAWN` / `IDLE_EXIT_S` are
|
||||
parsed **nowhere** (only a comment at `Config.h:466`). Today a dead server leaves the client
|
||||
silently `DECLINE`-ing every verb forever with `glGetError` clean.
|
||||
|
||||
**It is a package (`dl`), not carriage.** The latch is session-scoped and set from
|
||||
`Doorbell::Dead()`, **never from a timeout**. `MOBILEGL_IPC_RESPAWN=1` is a named refusal until some
|
||||
stage implements the re-push.
|
||||
|
||||
### 5.4 Slow, dead, and frozen are three states
|
||||
|
||||
- A published, never-applied record is **discarded at the latch, not waited on**.
|
||||
- **Slow and dead are distinguished without a timeout threshold.** A server one frame behind is
|
||||
P5e's intended steady state; a server that is gone is `Fatal{ServerCrashed}`. `SocketDoorbell`
|
||||
already latches `m_dead` on EOF — take the fact from there.
|
||||
- **A frozen peer produces no hangup**, and a6 found the tree has no defence: the control-reply wait
|
||||
`m_controlDone.wait(lock, pred)` (`Server/ServerLoop.cpp:724`) has **no deadline**, while the ring
|
||||
side has `kBarrierTimeoutMs`. A SIGSTOP'd or freezer'd server wedges the GL thread inside
|
||||
`eglMakeCurrent` with no diagnostic.
|
||||
|
||||
**D5 ruling — two waits, two answers.** (a) The server's *in-process* mailbox wait gains a bound and
|
||||
its expiry is `Fatal{ControlReplyTimeout, "<op>@<seq>"}`. (b) The client's *cross-process* reply wait
|
||||
is bounded by `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` (default 5000) and its expiry is **not** fatal: the
|
||||
client consults the doorbell's death latch — dead latches device-lost, alive-but-silent is a named
|
||||
diagnostic. Packages: `cp` (a), `dl` (b).
|
||||
|
||||
---
|
||||
|
||||
## §6 The control plane (`cp`)
|
||||
|
||||
### 6.1 What `fc` landed, and what is left
|
||||
|
||||
P5f package `fc` framed the twelve `Server*` forwarders as value-only `SurfaceControlFrame`s and
|
||||
landed the wire codec. The draft concluded `cp` is "transport only". **Three things are not
|
||||
transport**, and a6 found all three:
|
||||
|
||||
1. The `InitCapabilities` **request** has no wire form (§2).
|
||||
2. `SurfaceReply` cannot carry a `MobileGLResult` (§2).
|
||||
3. **Neither side of the control plane is wired to a socket.** The apply thread's `ready` predicate
|
||||
has exactly three terms — stop, `ControlIsPending()`, `cmdHead != LocalTail()` — and a socket is
|
||||
not among them (`ServerLoop.cpp:398-402`), so a `SurfaceOp` arriving on the wire is read by
|
||||
**nobody**. The client's pump consumes only `CapsSnapshot` and drops everything else with a WARN
|
||||
(`ClientSession.cpp:1425-1432`).
|
||||
|
||||
### 6.2 D4 — who reads the control socket
|
||||
|
||||
A dedicated **`mgl-srv-io` thread**: `ReceiveFrame` → `DecodeWireSurfaceOp` → post into the
|
||||
**existing** one-slot mailbox as an ordinary non-apply-thread poster. `ServerLoop.cpp:673-727` is not
|
||||
changed. The io thread is additionally the server's **only** sender on the control transport:
|
||||
`PublishCapsSnapshot` enqueues and the io thread drains one outbound FIFO — which is what fixes the
|
||||
ordering hazard that `CapsSnapshot` is pushed from **inside the dispatch, on the apply thread,
|
||||
mid-op** (`ServerLoop.cpp:959-969`).
|
||||
|
||||
Rejected: adding a fourth term to the apply thread's `ready` predicate. That puts frame decode on the
|
||||
thread holding the native context and makes every control arrival a wake of the applier.
|
||||
|
||||
### 6.3 D7 — `seq`
|
||||
|
||||
The **client** mints, always, dense from 1, never reused in a session. `seq == 0` arriving on the
|
||||
wire is `Fatal{ProtocolCorruption, "SurfaceOp.seq"}` at the **decoder**, not at
|
||||
`RunSurfaceControlFrame`, whose local mint (`ServerLoop.cpp:659-662`) stays and becomes reachable only
|
||||
for a local, non-wire post. Correlation lives in the client's control demultiplexer;
|
||||
`DecodeWireSurfaceReply` keeps its void signature and stops being the place correlation was expected.
|
||||
|
||||
### 6.4 `ForgetCurrentTuple`
|
||||
|
||||
The N-3 rule holds identically on the frame path: the forgetting calls are inside the dispatch
|
||||
(`ApplySurfaceControlFrame`) and `ServerLoopTest`'s C7/N-3 controls drive them through framed posts
|
||||
unchanged. No change.
|
||||
|
||||
---
|
||||
|
||||
## §7 Windows — rule H's enforcement (`cp`)
|
||||
|
||||
### 7.1 The blacklist is open, and the end state's first guest walks through it
|
||||
|
||||
`WindowBackendForWireWindowKind` (`Protocol/SurfaceOpCodec.cpp:104-113`) **accepts**
|
||||
`WindowKind::X11 → WindowBackend::X11` and `Win32Hwnd → Win32`. `DecodeWireSurfaceOp` (`:141-179`)
|
||||
refuses only `AndroidNativeWindow` and `MetalLayer`. An accepted kind's `nativeToken` is copied
|
||||
through and `UnpackWindowHandle` (`Server/ServerLoop.cpp:875-882`) casts it to `void*`
|
||||
**unconditionally**, whence it reaches `eglCreateWindowSurface` in the server process.
|
||||
`DetectWindowBackend()` is a compile-time `#if` chain (`MG_Impl/EGLImpl/EGLImpl.cpp:59-70`), so a
|
||||
guest cannot avoid announcing its own window system.
|
||||
|
||||
### 7.2 D8 — whitelist, and in P6 the accept set for a window op is empty
|
||||
|
||||
- `X11` → `Fatal{UnmigratedSurface, "X11@P12"}`; `Win32Hwnd` → `"Win32Hwnd@P12"`.
|
||||
- `None` on a **window** op → `Fatal{ProtocolCorruption, "SurfaceOp.windowKind"}`. Today it is
|
||||
accepted, maps to `WindowBackend::Unknown`, and the raw token is copied through anyway.
|
||||
- Separately and independently: `nativeToken != 0` on **any** op →
|
||||
`Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}`.
|
||||
- `Pbuffer` / `Surfaceless` are surface **shapes**, not window backends, and remain the only things
|
||||
P6 lands.
|
||||
|
||||
**Do not append `WindowKind::ServerOwned` in P6.** The schema is append-only, so an enumerator
|
||||
appended now and re-meant later is a wire value burned; P6 has no consumer for it. P12 appends it.
|
||||
|
||||
### 7.3 The client emits unconditionally, and P6 must decide what that costs
|
||||
|
||||
`Client/BackendObject_Remote.cpp:214-216` posts `SetWindowHandle` with no split-arm check, so on a
|
||||
real Android app the **first `eglCreateWindowSurface` aborts the server process** and the client sees
|
||||
only a dead peer. `cp` mirrors the refusal on the **client** side, so the diagnostic names the cause
|
||||
in the process that caused it. This is P6's, not P12's, because P6's device lanes run on a phone.
|
||||
|
||||
### 7.4 `m_windowHandle` has four stores, not one
|
||||
|
||||
a6's precise shape, correcting the corpus (and `P6-ENDSTATE-REVIEW §2.5`): four stores
|
||||
(`MG_Backend/BackendObject.cpp:475`, `:365`, `:206`, `:207`), five reads, and `:283` is a **call**,
|
||||
not a store. `.Width`/`.Height` have **no readers**. The smallest latch point for a server-owned
|
||||
window is `RegisterEGLWindowSurface` (`:232-238`), and the non-obvious fourth edit is the
|
||||
`sameHandle` dedup key (`BackendObject_DirectGLES.cpp:962-967`), which under server ownership never
|
||||
matches and would rebuild the native context on every `eglCreateWindowSurface`. Recorded here for
|
||||
P12; P6 changes none of it.
|
||||
|
||||
---
|
||||
|
||||
## §8 The data plane (`lk` implements, `c6` declares)
|
||||
|
||||
### 8.1 Why a seam at all
|
||||
|
||||
`ITransport.h:16-20` says in its own header that the hot path bypasses it: it is the **control**
|
||||
plane, and that scope is already correct for a socket. Below it there is **no interface of any kind** —
|
||||
`Ring.h`, `ReplySlot.h` and `EventRing.h` take raw `void* base + capacity`. So "swap the transport"
|
||||
has nothing to swap.
|
||||
|
||||
`c6` declares `Transport/ILink.h` and `Transport/StreamLink.h`. `lk` implements `ShmLink` by moving
|
||||
today's `SessionSegments` / `Ring` / `ReplySlotPool` / `EventRing` behind the interface, line for
|
||||
line, with **zero behaviour change**.
|
||||
|
||||
### 8.2 The seam is a setup-time interface, not a hot-path one
|
||||
|
||||
`ILink` hands out POD handles **once at attach** and then stays out of the way. No `ILink` virtual is
|
||||
called per record, per publish, or inside any spin predicate. Three grounds, and the second is
|
||||
decisive:
|
||||
|
||||
1. Two virtuals per record at the 4000 records/frame that `SEG_CMD` is sized against is ~8000
|
||||
indirect calls/frame — about 0.5% of the measured 15.72 ms client frame. Affordable, but the only
|
||||
lane that could measure it has ±4% run-to-run dispersion, **eight times the effect**, so the gate
|
||||
`P6-ENDSTATE-REVIEW` demands could only ever print "within noise" — a green that proves nothing.
|
||||
2. **The spin predicate forbids it outright.** `Doorbell::Wait` is a non-virtual template whose spin
|
||||
loop calls `ready()` up to `SpinItersPerUs() * spinUs` times; at 916 waits/frame with 99.8%
|
||||
resolved inside the spin, the predicate runs 10⁵–10⁶ times per frame and reads the watermarks
|
||||
straight off the page. That is the 31.58%-of-the-GL-thread path. A virtual there is not a rounding
|
||||
question.
|
||||
3. The encode path already depends on four inline accessors around its single `Reserve`, and
|
||||
virtualising `Reserve` alone would force either four more virtuals or moving R-10's wrap counters
|
||||
behind the seam — and those are deliberately encoder-owned.
|
||||
|
||||
**The gate is therefore an `objdump` call-count of the encode path, not a wall clock**: after `lk` the
|
||||
path must have no more out-of-line calls than today. That is falsifiable; "within noise" is not.
|
||||
|
||||
### 8.3 `RingControl`'s regroup is `lk`'s, and it is a wire break the day a second build exists
|
||||
|
||||
The watermark cache line is `{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial,
|
||||
presentAckSerial}`; `eventRingFull`/`eventDropped` sit in a **separate** `alignas(64)` group with
|
||||
`serverEpoch`/`ringGeneration`/the two park flags. And `submittedSeq` is **producer-written**
|
||||
(`Ring.h:50`) while the other four are consumer-written — so the group has **mixed writers**, and any
|
||||
"single-writer progress block aliased over the watermark line" is unsound.
|
||||
|
||||
`lk` therefore **regroups**: `submittedSeq` moves to the producer's own line (`Ring.h:45-50` states
|
||||
it is diagnostic and nobody waits on it), `eventRingFull`/`eventDropped` are promoted into the
|
||||
watermark group, and the group becomes a named `LinkProgress` member with per-field `offsetof`
|
||||
static_asserts.
|
||||
|
||||
It is free **today** — both peers are the same binary, enforced by the fingerprint — and a wire break
|
||||
the day a second build exists. It is **`lk`'s and not `c6`'s** because the rename blast radius is
|
||||
**109 sites in production alone**, not the 39 an earlier draft assumed, and because five of them sit
|
||||
behind `#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)` — an Android configure
|
||||
compiles them out, so a missed rename is green on device and red only on a desktop split-peek build.
|
||||
`lk` publishes the per-file itemisation before it starts.
|
||||
|
||||
### 8.4 What `StreamLink` is for
|
||||
|
||||
Every method is a named refusal, `Fatal{UnmigratedDataPlane, "StreamLink@P6.5"}`. It exists so the
|
||||
seam's adequacy is a **compile**, not an argument: if `ILink` cannot name what a byte stream needs,
|
||||
`StreamLink` cannot be declared against it. Writing it is what revealed that the seam needs
|
||||
`FlushProgress()`, `ResolveSpan()` and a reply pair — three methods the brief's own list
|
||||
(progress transmission, send window, flush) did not name.
|
||||
|
||||
`c6`'s red-once: delete one `ILink` method `StreamLink` declares and the build goes red.
|
||||
|
||||
### 8.5 The purity gate (`lk`)
|
||||
|
||||
After `lk`, the identifiers `RingControl`, `RingProducer`, `RingConsumer`, `ReplySlotPool` and
|
||||
`EventRingConsumer`, and any `m_shm.` expression, appear **only** inside `Transport/ShmLink.*` and
|
||||
the transport's own tests. Same shape as the existing A/B/C purity gates. The baseline is recomputed
|
||||
mechanically at `lk`'s head and published per file — the earlier draft's ratchet number was wrong by
|
||||
a factor of three, and a ratchet installed at the wrong number is red on day one for reasons that
|
||||
have nothing to do with a leak.
|
||||
|
||||
### 8.6 `SEG_STAGE`'s cursors are deliberately dead
|
||||
|
||||
`Ring.h:105-129` states it and a test pins it: `SEG_STAGE` is **not a ring**; staging is an
|
||||
encoder-local linear allocator reclaiming on `retiredSeq`, and all three stage cursors stay zero for
|
||||
the whole of P5. A stream link's send window either revives them **completely** or does not use them;
|
||||
the header names the middle state as *a guaranteed hang rather than a slow path*. P6.5's, recorded
|
||||
here so it is not rediscovered.
|
||||
|
||||
---
|
||||
|
||||
## §9 The gate
|
||||
|
||||
Beyond the five-part gate (`docs/Disaggregated/ARCHITECTURE.md` §13.2):
|
||||
|
||||
1. **G1** — pull build 0/0/0/0, `.text` unchanged. Every predicate in §3.2 folds away.
|
||||
2. **G2/G14** — `integration-spawn`'s case-name set identical to `integration-split`'s.
|
||||
3. `integration-spawn` green at the same count as `integration-split`.
|
||||
4. **Process tree, mechanical** — exactly one extra child while running, zero after; `HeadlessGL`'s
|
||||
fork pre-check leaves no orphan.
|
||||
5. **Arm proof (ID-124)** — every spawn entry records the child pid and `transport=spawn` in its own
|
||||
private log. A spawn lane that fell back to monolith passes items 1–4. §1 is why this gate is not
|
||||
optional: today the fallback and the intended state are **the same value**.
|
||||
6. **Red-once per package (R-16)**, one named pair each.
|
||||
7. **Device** — Redmi `2f7cbe2e`, reboot-clean, paired A/B in one thermal window.
|
||||
8. **Performance recorded, not gated**, but three numbers are mandatory: the socket doorbell's cost
|
||||
against `inproc`'s condvar; `SEG_STAGE` bytes/frame; and records/frame **post-chunking**. The last
|
||||
two are takeable on the existing inproc lane today and `MEASUREMENTS.md:342` says in as many words
|
||||
that no post-chunking distribution exists.
|
||||
|
||||
> Item 8's wording must be able to **accept a negative regression without looking suspicious**: today
|
||||
> the client spins ~1600 times/frame on a shared cache line with 99.8% of waits resolved there, and a
|
||||
> socket link has no such line. Converting that into ~4 blocking reads/frame may be **cheaper**.
|
||||
|
||||
Negative controls, each run red once:
|
||||
|
||||
| # | knob | what it must falsify | package |
|
||||
|---|---|---|---|
|
||||
| S1 | `MOBILEGL_IPC_SERVER_PATH=/nonexistent` | named refusal, not a silent monolith fallback | `sm` |
|
||||
| S2 | `kill -9` the server mid-frame | the latch; the client never hangs and never keeps submitting. Red-once the **wrong implementation** too: set the latch from a timeout and the paired control "a server one frame behind is the normal state" must go red | `dl` |
|
||||
| S3 | child envp not scrubbed | `Dial = No` catches it **and the diagnostic names which of the two catches fired** | `sm` |
|
||||
| S4 | inherited P5f epoch invalidation disabled | a replaced context inherits stale synced state; a still-live XFB lifetime retains its paused span | `st` |
|
||||
| S5 | `WindowKind::X11` with a plausible XID reaches the server | named Fatal, **and the gate falsifies that the token reached `UnpackWindowHandle`**. Today's S5 tests only `AndroidNativeWindow` and stays green with §7.1's hole open | `cp` |
|
||||
| S6 | a peer offers `LinkTerms.dataPlane = StreamOnly` | refused by name, **and the server survives to serve the next connection** | `hs` |
|
||||
| S7 | a bare `abort()` added outside `Session::Fail` | the census gate goes red | `dl` |
|
||||
| S8 | — | `integration-spawn` asserts `SessionFaultCount() == 0` over a whole run: the **good-path** assertion E1/ID-122 wanted and could not express, and which S5/S6 can falsify | `t6` |
|
||||
|
||||
---
|
||||
|
||||
## §10 Amendments
|
||||
|
||||
### 10.1 To `P6-CONTRACT-DRAFT.md`, which this file supersedes
|
||||
|
||||
1. §1's "ABI fingerprint | Unchanged … same-machine, same-binary" → §4.
|
||||
2. §5.1's "The latch … is implemented unchanged" → **false**; §5.3.
|
||||
3. §4's "P6's remainder is TRANSPORT ONLY" → **false in three ways**; §6.1.
|
||||
4. §7's "a transport swap and nothing else" → **no longer true**. This file adds `lk`, `hs` and `dl`
|
||||
to the package list and says so here rather than smuggling it.
|
||||
|
||||
### 10.2 To the earlier contracts
|
||||
|
||||
1. `CONTRACT-P5C.md:537-539` — EGL value framing was discharged by P5f `fc`, the unpack/render-state
|
||||
epoch work by P5f `fs`. P6 carries the existing frames over the socket and verifies the inherited
|
||||
lifecycle rules in the second process.
|
||||
2. `CONTRACT-P5C.md:56` — P6's multi-context shape. **Declined**: P6 stays single-context. §11.
|
||||
3. `Transport/ReplySlot.h`'s "P6 debt" for the 2 MiB reply cap and chunked readback. **Declined**:
|
||||
P9's. Note `P6-SPAWN-PLAN §8.5` already reassigned it and the header was never updated — two
|
||||
documents in the tree disagreed; this file settles it.
|
||||
4. `Ring.h:71-77` — `appliedSeq` is excluded from lazy publication **by name**. P6 does not change
|
||||
the behaviour; it records that any future stream link **amends** this rule rather than inferring
|
||||
around it. Several designs have argued the late-never-early invariant licenses lazy `appliedSeq`;
|
||||
it explicitly carves `appliedSeq` out.
|
||||
5. `ARCHITECTURE.md:488` — the child-forces-monolith rule. **Amended**; §3.1.
|
||||
6. `ARCHITECTURE.md:9` and `MG_Backend/MGPipe/PipeInputs.h:281`, `:744`, `:957`, and
|
||||
`P5F-WIRE-COMPLETENESS §1.4` — "the server does not link `MG_Impl`". **False at this head.** §12.
|
||||
|
||||
---
|
||||
|
||||
## §11 Not P6
|
||||
|
||||
Real window arrival and the `android:process=":mgl"` Service (P12); multi-context (P12); chunked
|
||||
readback and the reply-slot pool (P9); `DynamicBackendParameters`' fixed-width rewrite (P7);
|
||||
`MOBILEGL_IPC_POLL_ESCALATE` (P10); remaining monolith-only frontend-object / twin-registry glue
|
||||
(P3b/P4b) and the named P7 functionality debts; Windows `pipe:` / `unix:`; the 511 class-C entries.
|
||||
|
||||
**The stream data plane is P6.5**, and it must land before P11 and before P9 picks a mechanism.
|
||||
**Untrusted-guest hardening is `Ph`**, and it must land before any shipping server app accepts a
|
||||
connection from a guest it did not produce — including the `Fatal`-policy flip (§5.2), the
|
||||
handle-slot budget, and the caps in §12.
|
||||
|
||||
E1's re-specification (ID-122) is **not** discharged by P6 and does not become P6's because P6 starts.
|
||||
|
||||
---
|
||||
|
||||
## §12 Recorded debt
|
||||
|
||||
### 12.1 The server image links `MG_Impl` in P6 (D12)
|
||||
|
||||
**P6's server image is the whole `libMobileGL.so`** — which is what `ARCHITECTURE.md:496`'s "one
|
||||
shared library, two roles" always meant. The assertions that the server does not link `MG_Impl` are
|
||||
false and become debt with named owners.
|
||||
|
||||
Measured: **184 symbols**, 177 of them real function calls, that the server set needs and only
|
||||
`MG_Impl` / `MG_State` / `MG_Remote/Client` define. Owners: **P7 101**, **P3b/P4b 14**, both backends
|
||||
**60**, **P6's own 6**. There is additionally **no module target to link** — `SOURCE_FILES` is one
|
||||
flat list feeding both library targets — so establishing module boundaries is itself named work,
|
||||
owed to P13. Method and full symbol list:
|
||||
[`a6-link-experiment.md`](../../docs/Disaggregated/notes/p6/a6-link-experiment.md).
|
||||
|
||||
**P6's own six** are not P7's and are `sm`/`lk`'s to clear: `Server/PipeApplier.cpp` calls
|
||||
`Client::ClientSession::NoteApplyThreadEnteredApplier()` / `NoteApplyThreadLeftApplier()`;
|
||||
`Wire/PipeWireCodec.cpp` needs `MG_State::GLState::DecodeProgramArchive` and
|
||||
`Client::AdoptTierIsEmulate()`; `MG_Pipe/PipeApply.cpp` needs `Client::ClientSession::Active()` and
|
||||
`AdoptTierIsEmulate()`.
|
||||
|
||||
**Gate**: a ratchet recomputes the 184 in CI and asserts it only ever falls.
|
||||
|
||||
### 12.2 D11 — five unbounded peer-driven allocations, capped, with no `try`/`catch`
|
||||
|
||||
Every peer-declared count is bounded **before** it reaches an allocator and the bound is a named
|
||||
refusal. `MG_Remote` gets no `try`/`catch`: a cap that is checked is better than an exception that is
|
||||
caught, and the funnel (§5.2) is where the diagnostic lives.
|
||||
|
||||
| site | today | package |
|
||||
|---|---|---|
|
||||
| `MG_Pipe/PipeApply.cpp:1563-1569` | resizes to a peer-supplied `Cso.Slot + 1` with **no slot limit**; every sibling create-family goes through `RecordAt`'s bounded path. `Slot = 0xFFFFFFFE` asks for ~1.7 TB | `dl` |
|
||||
| `Server/PipeApplier.cpp:416-441` | readback scratch resized from `Box.W * Box.H * bpp` **before** any size gate. 65535² RGBA8 asks for ~17 GB | `dl` |
|
||||
| `MG_State/.../ProgramArtifactsCodec.cpp:166-175`, `:216-223` | count bounded by remaining **bytes**, then multiplied by `sizeof(element)` | `dl` |
|
||||
| `Server/StagedTextureStore.h` | resize not bounded against the level's declared extent | `Ph` |
|
||||
| `SEG_EVENT` overflow | a guest that merely stops draining kills the host renderer in 60 s | `Ph` |
|
||||
|
||||
### 12.3 One guest per server process (D9)
|
||||
|
||||
For the whole of P6, one server process serves exactly one client session, and it is a **structural
|
||||
fact, not a preference**: `g_Display`/`g_Context`/`g_Surface`/`g_Config` are per-process file-statics
|
||||
and `InitDisplayAndContext` opens with an unconditional `DestroyEGLContext()` whose last act is
|
||||
`eglTerminate` — a **process-wide** EGL teardown. `ServerSession::Accept`'s same-object arm
|
||||
(`ServerSession.cpp:425`) returns `MOBILEGL_ERR_INVALID_ARGUMENT` **with no log line at all**; `sm`
|
||||
gives it one. Under `Dial == Fork` a second guest is unreachable by construction, so the invariant
|
||||
becomes falsifiable only under `Dial == Connect`, which P6 does not land.
|
||||
|
||||
### 12.4 Settled by ruling rather than by experiment
|
||||
|
||||
- **Who runs the applier in the server process**: `ServerMain` **must** drive it through
|
||||
`ServerLoop::Start()`, so `g_applyThreadKey` is published and the dozen `OnApplyThread()` guards
|
||||
keep meaning what they mean. Owner `sm`; red-once drives the applier without `Start()`.
|
||||
- **The server's exit primitive**: `_exit`, after an explicit log flush. It decides whether
|
||||
`g_processTeardown` is set and therefore whether seven twin destructors take their early-out.
|
||||
Owner `sm`.
|
||||
- **The handshake asserts the resolved subsystem mask**; a mismatch is `Refuse`, not `Fatal`. The
|
||||
envp scrub list does **not** include `MOBILEGL_PIPE_*`, so the two processes can resolve different
|
||||
masks from their own environments. Owner `hs`.
|
||||
- **P6 maps no segment read-only.** `ShmSegment.h:62-63`'s stated design — the peer gets
|
||||
`SEG_CMD`/`SEG_STAGE` read-only on the server side — **cannot be implemented as written**, and the
|
||||
comment is corrected rather than left to mislead. Owner `lk`.
|
||||
- **`DualBlockScenario`'s `serverIdentity != nullptr`** is an in-process fact, and G2/G14 requires
|
||||
the spawn lane to keep the case name. Its assertions are **re-homed**, not weakened: `DualBlockPeek`
|
||||
becomes a control-plane query answered by the server process. The declared fallback — assert
|
||||
`MGPipeBlocksAreDistinct()` on the client side and carry the identity half by §3.4's server-side
|
||||
Fatal — must be **declared here** if taken, not discovered in review. Owner `t6`.
|
||||
@@ -0,0 +1,277 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/ILink.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// The DATA-plane seam. Declared by package c6, implemented by package lk.
|
||||
// Authority: CONTRACT-P6.md §8.
|
||||
//
|
||||
// WHY THIS EXISTS AND WHY IT IS NOT ITransport. ITransport.h:16-20 says in its
|
||||
// own header that everything on the hot path bypasses it: it carries the
|
||||
// handshake, surface ops, resync, aux requests and fatals, and that scope is
|
||||
// already correct for a socket. Below it there was NO interface of any kind -
|
||||
// Ring.h, ReplySlot.h and EventRing.h each take a raw `void* base` plus a
|
||||
// capacity, so "swap the transport" had nothing to swap. This header is that
|
||||
// missing thing, and ITransport is neither extended nor changed by it.
|
||||
//
|
||||
// NOTHING INCLUDES THIS FILE IN c6. That is the whole of c6's behaviour
|
||||
// guarantee: the seam is declared, StreamLink.h is declared against it (which
|
||||
// is the adequacy proof - see below), and not one call site moves. Package lk
|
||||
// implements ShmLink by relocating today's SessionSegments / Ring /
|
||||
// ReplySlotPool / EventRing behind it, line for line, and only then rewrites
|
||||
// the call sites.
|
||||
//
|
||||
// THE SEAM IS SETUP-TIME, NOT HOT-PATH. No method here is called per record,
|
||||
// per publish, or inside a spin predicate. The producer takes a LinkArena once
|
||||
// at attach and reserves through it non-virtually; every wait predicate reads
|
||||
// LinkProgress through a raw pointer taken once. Three grounds, the second
|
||||
// decisive (CONTRACT-P6.md §8.2):
|
||||
//
|
||||
// 1. Two virtuals per record at the 4000 records/frame SEG_CMD is sized
|
||||
// against is ~8000 indirect calls/frame, about 0.5% of the measured
|
||||
// 15.72 ms client frame - affordable, but the only lane that could
|
||||
// measure it has +/-4% dispersion, EIGHT TIMES the effect. A gate that
|
||||
// can only ever print "within noise" is ID-124's failure mode one level
|
||||
// down.
|
||||
// 2. Doorbell::Wait is a non-virtual template whose spin loop calls ready()
|
||||
// up to SpinItersPerUs() * spinUs times; at 916 waits/frame with 99.8%
|
||||
// resolved inside the spin, the predicate runs 1e5-1e6 times per frame
|
||||
// and reads the watermarks straight off the shared page. That is the
|
||||
// 31.58%-of-the-GL-thread path. A virtual there is not a rounding
|
||||
// question.
|
||||
// 3. The encode path already depends on four inline accessors around its one
|
||||
// Reserve; virtualising Reserve alone forces either four more virtuals or
|
||||
// moving R-10's wrap counters behind the seam, and those are deliberately
|
||||
// encoder-owned.
|
||||
//
|
||||
// So lk's gate is an objdump call-count of the encode path, not a wall clock:
|
||||
// after lk that path has no more out-of-line calls than before. That is
|
||||
// falsifiable; "within noise" is not.
|
||||
//
|
||||
// WHAT THIS HEADER DELIBERATELY DOES NOT DO.
|
||||
// - It does not define Watermark::Reached or any other predicate that
|
||||
// SessionRings.h already defines. One definition, one owner.
|
||||
// - It does not relocate the bodies of RingProducer::Reserve or
|
||||
// RingConsumer::Pop. Those log (MGLOG_E / MGLOG_D) and use Align8 and
|
||||
// <cstring>; no header under Transport/ pulls the logger in, and this one
|
||||
// does not start. They stay out of line and move to ShmLink.cpp in lk.
|
||||
// - It declares no function it does not define. Every entry point here is
|
||||
// pure virtual or a POD accessor.
|
||||
// - It does not regroup RingControl. That regroup is real and necessary
|
||||
// (CONTRACT-P6.md §8.3) but its rename blast radius is 109 production
|
||||
// sites, five of them behind a non-Android #if, so it is lk's with a
|
||||
// published per-file itemisation - not a side effect of declaring a seam.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Protocol/mg_protocol_base.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
class Doorbell;
|
||||
|
||||
// Which end of the link this instance is. Deliberately NOT ITransport's
|
||||
// TransportRole: that enum's InProcess value names a control-plane
|
||||
// hand-off, and a data plane's two ends are producer and consumer whatever
|
||||
// the control plane did.
|
||||
enum class TransportRoleTag : std::uint32_t {
|
||||
ClientProducer = 1,
|
||||
ServerConsumer = 2,
|
||||
};
|
||||
|
||||
// ---- the progress block -------------------------------------------------
|
||||
//
|
||||
// The watermarks every wait predicate reads. On ShmLink this aliases the
|
||||
// shared control page and the loads are cross-process acquire loads; on
|
||||
// StreamLink it is process-local and a reader thread stores into it from
|
||||
// decoded progress messages.
|
||||
//
|
||||
// THE RULES SURVIVE THE MECHANISM VERBATIM, and that is the point of naming
|
||||
// the block rather than the page: late-never-early (Ring.h:71-77), `>=`
|
||||
// never `==` (SessionRings.h's Watermark::Reached), monotone with a
|
||||
// backwards move Fatal (Ring.cpp's AdvanceMonotonic). A stream link changes
|
||||
// only HOW a value arrives, never what it means.
|
||||
//
|
||||
// WHY THESE SIX AND NOT THE WHOLE PAGE. RingControl's watermark cache line
|
||||
// today holds {appliedSeq, submittedSeq, retiredSeq, completedFrameSerial,
|
||||
// presentAckSerial}, and submittedSeq is PRODUCER-written (Ring.h:50,
|
||||
// "Advanced by the PRODUCER after it publishes") while the other four are
|
||||
// consumer-written - so the line has MIXED WRITERS and no single-writer
|
||||
// block can be aliased over it as it stands. eventRingFull and
|
||||
// eventDropped, which every park predicate on both sides must also see,
|
||||
// sit a full cache line away in the doorbell/generation group. lk's regroup
|
||||
// moves submittedSeq out to the producer's own line and promotes the two
|
||||
// event flags in, so THIS struct becomes a named member of RingControl with
|
||||
// per-field offsetof static_asserts. Free today, because both peers are the
|
||||
// same binary and the fingerprint enforces it; a wire break the day a
|
||||
// second build exists, which is why it is booked now rather than later.
|
||||
struct LinkProgress {
|
||||
std::atomic<std::uint64_t> appliedSeq; // records applied
|
||||
std::atomic<std::uint64_t> retiredSeq; // GPU finished
|
||||
std::atomic<std::uint64_t> completedFrameSerial; // releases *RetiredTail / SEG_ADOPT
|
||||
std::atomic<std::uint64_t> presentAckSerial; // returns present credit
|
||||
std::atomic<std::uint32_t> eventRingFull; // SEG_EVENT full, apply stopped
|
||||
std::atomic<std::uint32_t> eventDropped; // dropped lossy events
|
||||
};
|
||||
|
||||
// ---- the producer's handle ----------------------------------------------
|
||||
//
|
||||
// POD state, taken ONCE at attach and used non-virtually thereafter. The
|
||||
// reserve/commit algorithm is NOT inlined here (see the header comment):
|
||||
// ShmLink keeps today's out-of-line body, StreamLink writes its own.
|
||||
// `Base` is null on a link that has no mapping, and a link that publishes
|
||||
// that shape must answer every span through ResolveSpan.
|
||||
struct LinkArena {
|
||||
void* Base = nullptr;
|
||||
std::uint64_t Capacity = 0;
|
||||
std::uint64_t Mask = 0;
|
||||
std::uint64_t LocalHead = 0;
|
||||
};
|
||||
|
||||
// The consumer's mirror of the same.
|
||||
struct LinkCursor {
|
||||
const void* Base = nullptr;
|
||||
std::uint64_t Capacity = 0;
|
||||
std::uint64_t Mask = 0;
|
||||
std::uint64_t LocalTail = 0;
|
||||
};
|
||||
|
||||
// A run of bulk bytes named on the wire. On ShmLink `Offset` indexes the
|
||||
// shared SEG_STAGE mapping; on StreamLink it indexes the trailing staged
|
||||
// region of the batch that carried the record. MGPBlobRef's spelling and
|
||||
// every honesty check over it (R-2) are unchanged either way - which is the
|
||||
// property that lets the whole record catalogue cross a boundary with no
|
||||
// mapping without a single record layout changing.
|
||||
struct LinkSpan {
|
||||
std::uint64_t Offset = 0;
|
||||
std::uint64_t Size = 0;
|
||||
};
|
||||
|
||||
// Which segment a span or a view belongs to. Mirrors SegmentKind's wire
|
||||
// values; declared here so ILink does not pull the protocol headers in.
|
||||
enum class LinkSegment : std::uint32_t {
|
||||
Cmd = 1,
|
||||
Stage = 2,
|
||||
Reply = 3,
|
||||
Event = 4,
|
||||
};
|
||||
|
||||
// What a link can do. A caller may not infer any of these from the
|
||||
// transport's role, and a link may not answer "sometimes" - rule G's
|
||||
// second clause is that a transport DECLARES what it supports and may not
|
||||
// invent a third answer.
|
||||
struct LinkCapabilities {
|
||||
bool SharedMapping = false; // peers see one set of bytes; no copy on read
|
||||
bool DescriptorPassing = false;
|
||||
bool PublishIsDelivery = false; // false => Flush() is mandatory before any wait
|
||||
std::uint64_t MaxRecordBytes = 0;
|
||||
std::uint64_t MaxReplyBytes = 0;
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// The seam itself.
|
||||
//
|
||||
// Nineteen entry points, every one taken at attach, at a park boundary, or
|
||||
// once per control operation. None on the hot path.
|
||||
// -------------------------------------------------------------------------
|
||||
class ILink {
|
||||
public:
|
||||
virtual ~ILink() = default;
|
||||
|
||||
ILink(const ILink&) = delete;
|
||||
ILink& operator=(const ILink&) = delete;
|
||||
|
||||
// ---- setup ----------------------------------------------------------
|
||||
|
||||
virtual LinkCapabilities Capabilities() const = 0;
|
||||
|
||||
// The watermark block. Taken once; every wait predicate reads through
|
||||
// the returned pointer with no further virtual dispatch. Never null on
|
||||
// an attached link.
|
||||
virtual LinkProgress* Progress() = 0;
|
||||
|
||||
// The command-record arena (producer side) and cursor (consumer side).
|
||||
// Exactly one of the two is valid on a given peer.
|
||||
virtual LinkArena* RecordArena() = 0;
|
||||
virtual LinkCursor* RecordCursor() = 0;
|
||||
|
||||
// ---- bulk bytes -----------------------------------------------------
|
||||
|
||||
// Reserve `bytes` of staging and return where the record should name
|
||||
// it. MOBILEGL_ERR_RING_OVERRUN when the request cannot be satisfied at
|
||||
// any occupancy; the caller's existing back-pressure path is unchanged.
|
||||
virtual MobileGLResult StageBytes(std::uint64_t bytes, LinkSpan* outSpan,
|
||||
void** outWritePtr) = 0;
|
||||
|
||||
// Resolve a span the peer named. On a shared mapping this is pointer
|
||||
// arithmetic over the peer's segment; on a stream it points into the
|
||||
// reassembly buffer of the batch that carried the record, and the
|
||||
// lifetime rule ("valid until retiredSeq passes the record") is
|
||||
// restated against that buffer rather than against the mapping.
|
||||
//
|
||||
// THIS IS THE METHOD WRITING StreamLink REVEALED. The brief's list -
|
||||
// progress transmission, send window, flush - did not name it, and
|
||||
// without it a stream link has no way to answer the eleven
|
||||
// ResolveOrFatal call sites in the decoder.
|
||||
virtual MobileGLResult ResolveSpan(LinkSegment segment, LinkSpan span,
|
||||
const void** outPtr) = 0;
|
||||
|
||||
// ---- replies --------------------------------------------------------
|
||||
|
||||
virtual MobileGLResult PostReply(std::uint64_t seq, std::int32_t status,
|
||||
const void* payload, std::uint64_t size) = 0;
|
||||
virtual MobileGLResult ReadReply(std::uint64_t seq, std::int32_t* outStatus,
|
||||
const void** outPayload, std::uint64_t* outSize) = 0;
|
||||
|
||||
// ---- the reverse channel --------------------------------------------
|
||||
|
||||
virtual LinkArena* EventArena() = 0;
|
||||
virtual LinkCursor* EventCursor() = 0;
|
||||
|
||||
// ---- wakeups --------------------------------------------------------
|
||||
//
|
||||
// THESE TWO RETIRE CONTRACT-P5 §3.9. That ruling kept the doorbell
|
||||
// accessors on the session "so that this stays one switch in one file
|
||||
// rather than two virtuals every transport has to invent a home for"
|
||||
// (ServerSession.cpp:709-724) - and the switch it produced aborts for
|
||||
// every role but InProcess, so a spawn apply thread dies in
|
||||
// ConsumerDoorbell before it pops its first record. The seam is the
|
||||
// home the ruling said did not exist. lk deletes both accessors rather
|
||||
// than growing them a socket arm.
|
||||
virtual Doorbell& ConsumerBell() = 0;
|
||||
virtual Doorbell& ProducerBell() = 0;
|
||||
|
||||
// ---- publication -----------------------------------------------------
|
||||
|
||||
// Make everything published so far visible to the peer. On a shared
|
||||
// mapping publish IS delivery and this is a no-op; on a stream it is a
|
||||
// write(), and CONTRACT-P6.md requires every transition into a wait to
|
||||
// call it first, or a kWaitReply record sitting in a send buffer while
|
||||
// the client parks is a hang rather than a slowdown.
|
||||
virtual MobileGLResult Flush() = 0;
|
||||
|
||||
// Push the local progress block to the peer. A no-op on a shared
|
||||
// mapping, where a release-store IS the transmission. Named separately
|
||||
// from Flush because the two have different triggers: Flush is about
|
||||
// records the peer has not seen, this is about watermarks the peer is
|
||||
// waiting on. The second method writing StreamLink revealed.
|
||||
virtual MobileGLResult FlushProgress() = 0;
|
||||
|
||||
// ---- lifecycle -------------------------------------------------------
|
||||
|
||||
virtual bool Attached() const = 0;
|
||||
virtual void Detach() = 0;
|
||||
virtual TransportRoleTag Role() const = 0;
|
||||
|
||||
protected:
|
||||
ILink() = default;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -0,0 +1,102 @@
|
||||
// MobileGL - MobileGL/MG_Remote/Transport/StreamLink.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// ILink's second implementation, DECLARED BY c6 AND IMPLEMENTED BY NOBODY.
|
||||
// Authority: CONTRACT-P6.md §8.4. Its phase is P6.5.
|
||||
//
|
||||
// WHY A HEADER FOR SOMETHING THAT DOES NOT EXIST. ILink's adequacy - "can this
|
||||
// seam name what a byte stream needs?" - is the one claim c6 makes that an
|
||||
// argument cannot settle and a compile can. If ILink cannot express progress
|
||||
// transmission, a send window, a flush, span resolution without a mapping, and
|
||||
// replies that are messages rather than slots, then StreamLink cannot be
|
||||
// declared against it, and the build says so. That is c6's red-once: delete
|
||||
// one ILink method this class declares and the build goes red.
|
||||
//
|
||||
// It has already earned its keep. Writing it forced THREE methods that the
|
||||
// original brief's list - progress transmission, send window, flush - did not
|
||||
// name:
|
||||
//
|
||||
// ResolveSpan - a stream has no mapping to do pointer arithmetic over, so
|
||||
// the eleven ResolveOrFatal sites in the decoder need the
|
||||
// link to answer, not a segment table. Without it the seam
|
||||
// was unimplementable for the case it exists for.
|
||||
// FlushProgress - separate from Flush because the triggers differ: Flush is
|
||||
// about records the peer has not seen, FlushProgress is
|
||||
// about watermarks the peer is WAITING on. On a shared page
|
||||
// a release-store is both; on a stream, conflating them
|
||||
// either sends too much or hangs a waiter.
|
||||
// PostReply / - SEG_REPLY carries no message today: the server memcpys
|
||||
// ReadReply into a slot addressed `seq % slotCount` and the client
|
||||
// reads it after observing appliedSeq. A stream turns that
|
||||
// into a real message carrying its own seq, which DELETES
|
||||
// the slot pool, the modulus addressing and the stamp
|
||||
// self-check - so the seam has to own the operation rather
|
||||
// than hand out the pool.
|
||||
//
|
||||
// WHAT P6.5 STILL HAS TO SETTLE, recorded here so it is not rediscovered:
|
||||
// - SEG_STAGE's three ring cursors are DELIBERATELY DEAD (Ring.h:105-129,
|
||||
// pinned by a test). Staging is an encoder-local linear allocator that
|
||||
// reclaims on retiredSeq. A send window either revives them COMPLETELY or
|
||||
// does not use them; the header names the middle state as a guaranteed
|
||||
// hang rather than a slow path.
|
||||
// - appliedSeq is excluded BY NAME from lazy publication (Ring.h:71-77).
|
||||
// P6.5 AMENDS that rule with a flush-on-idle discipline; it does not infer
|
||||
// around it.
|
||||
// - one dedicated reader thread per direction per process, or the event-ring
|
||||
// flow-control deadlock returns as write() blocked against write().
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ILink.h"
|
||||
|
||||
namespace MobileGL::MG_Remote::Transport {
|
||||
|
||||
// Every entry point is a named refusal. There is no partial implementation
|
||||
// and there may not be one: a half-wired stream link is the shape Ring.h
|
||||
// warns about, where FreeBytes() falls to zero on the first lap and never
|
||||
// recovers.
|
||||
//
|
||||
// The refusals live in StreamLink.cpp (package P6.5) rather than inline,
|
||||
// for the reason ILink.h gives: no header under Transport/ pulls the logger
|
||||
// in, and this one does not start.
|
||||
class StreamLink final : public ILink {
|
||||
public:
|
||||
StreamLink();
|
||||
~StreamLink() override;
|
||||
|
||||
LinkCapabilities Capabilities() const override;
|
||||
|
||||
LinkProgress* Progress() override;
|
||||
LinkArena* RecordArena() override;
|
||||
LinkCursor* RecordCursor() override;
|
||||
|
||||
MobileGLResult StageBytes(std::uint64_t bytes, LinkSpan* outSpan,
|
||||
void** outWritePtr) override;
|
||||
MobileGLResult ResolveSpan(LinkSegment segment, LinkSpan span,
|
||||
const void** outPtr) override;
|
||||
|
||||
MobileGLResult PostReply(std::uint64_t seq, std::int32_t status,
|
||||
const void* payload, std::uint64_t size) override;
|
||||
MobileGLResult ReadReply(std::uint64_t seq, std::int32_t* outStatus,
|
||||
const void** outPayload, std::uint64_t* outSize) override;
|
||||
|
||||
LinkArena* EventArena() override;
|
||||
LinkCursor* EventCursor() override;
|
||||
|
||||
Doorbell& ConsumerBell() override;
|
||||
Doorbell& ProducerBell() override;
|
||||
|
||||
MobileGLResult Flush() override;
|
||||
MobileGLResult FlushProgress() override;
|
||||
|
||||
bool Attached() const override;
|
||||
void Detach() override;
|
||||
TransportRoleTag Role() const override;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Remote::Transport
|
||||
@@ -13,7 +13,7 @@
|
||||
| **P5d** `inproc` 性能专项 | **已收官(2026-09-18,三轮)** | `cb06538c`、`56a77348`、`1f8de61b`;报告 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md);`MEASUREMENTS.md` §9 |
|
||||
| **P5e** 退役 Espryt draw path 的 lockstep | **已收官(2026-09-19)**,附一条具名未决(E1 对照,ID-122) | 契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`;计划 `~/w7/notes/p5e/BRIEF-P5E.md`、裁定 `~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`(**ID-80..136**);**十二个包已全部落地合并**(§2.7),`kMGPipeP5eRunAheadReady` 与 `kMGPipeP5eClientWaitRuleLanded` 均已翻。strict 车道硬绿 179/179、`integration-gpu` 1357/1357、三个构建 flavour 全绿;设备上 VD32 已与 monolith 齐平。报告 [`P5E-RUNAHEAD.md`](P5E-RUNAHEAD.md),未完成项见 §5 |
|
||||
| **P5f** 一切状态上 wire | **已收官(2026-09-20)** | f0 / f1 / fc / fe / fm / fs / fr / fv 及分类收口均已完成;零 BARRIER_PULLED、两份 marker 空表、逐帧 rsp=0。主机全门、Claude 异族终审两项修复及 red-once、Redmi 六 clean-boot 臂通过。见 [`close-report`](notes/p5f/close-report.md)、[`close-review`](notes/p5f/close-review.md)、[`device-report`](notes/p5f/device-report.md) |
|
||||
| **P6** spawn transport | **a6 已收官(2026-09-21)**,c6 开工中 | 计划 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md)、草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md);a6 见 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md) 与 [`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)。传输原语与 P5f 值控制帧、静态世代/角色隔离已在树上;待做的是进程装配、socket 传输、握手/EOF/device-lost 等,不再重做已落地的控制帧与静态量 |
|
||||
| **P6** spawn transport | **a6、c6 已收官(2026-09-21)**;下一个包 `lk`(数据面缝的实现) | 计划 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md)、草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md);a6 见 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md) 与 [`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)。传输原语与 P5f 值控制帧、静态世代/角色隔离已在树上;待做的是进程装配、socket 传输、握手/EOF/device-lost 等,不再重做已落地的控制帧与静态量 |
|
||||
|
||||
## 2. P5f 最终验收
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
| **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 |
|
||||
| **P5e** 退役 Espryt draw path 的 lockstep | ✅ **已收官(2026-09-19)**,附一条具名未决:E1 对照需按 ID-114 重新定义 | 契约 `MG_Remote/CONTRACT-P5E.md`(规则 F:**未设障**记录的 apply 不得读任何 client 内存;ID-84 限定它只约束未设障记录);裁定 **ID-80..136**(`~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`)。**十二个包,三波**:wave 1 **c0e**(契约 + 线上行:`PipeCalls.def` 第五列 `WaitClass`、`set_program_bindings` opcode 80、`kCapRunAheadApply` 位 10 仅 DirectGLES 臂、`kDrawClientArrays`、子系统位 13、两个旋钮)→ **id**(registry 按 `{slot, gen}` 重键、by-handle resolver、分配器守卫);wave 2 **vi / sb / pg / tx2 / fb** 五个 per-draw 家族 ∥ **ra**(等待规则、present credit、`gPipeInputs` 变 server 角色内存),加 **fix1**(设备发现的 monolith 臂回归,ID-107);wave 3 **pa**(program 家族离开前端)、**mv**(multi-draw + 五个档位的门禁条目)、**gl**(门禁机制本身 + 两处潜伏崩溃)、**ra2**(翻开关后暴露的竞态)。**`kMGPipeP5eRunAheadReady` 与 `kMGPipeP5eClientWaitRuleLanded` 均已翻**。最大的那次迁移(逐 draw 的 `GetProgramForDraw`)**没有改动任何线上结构**:记录早就带着逐 link 的 `ProgramArchive`,缺的是仍去问前端对象的读者。报告 [`P5E-RUNAHEAD.md`](P5E-RUNAHEAD.md) | 出口门已逐条跑过(BRIEF-P5E §3,2026-09-19 在集成树上,run-ahead 已武装):**item 1** unit 在 strict 两臂均 **2256/2256**;**item 2/3** `integration-split` **179/179**、逐条普查零 `Fatal{`、`Admitted{` 恰为八行;**item 4(a)** `RUN_AHEAD=0` 作为配对对照 **179/179 绿**;**item 4(b)** `VERB_BARRIER=0` **变红**(车道 62/179);**item 4(c)** Magma 逐字记下 `run-ahead requested, server does not publish kCapRunAheadApply - running lockstep`,而 Espryt 对照记下 `run-ahead ARMED`;**item 5** 在集成树上重跑 red-once(ID-121 重排后的形式):倒掉 pa 的臂得 `GetProgramForDraw@DrawArrays`、倒掉 mv 的臂得 `GetBoundVertexArray@DrawArrays`,**恰好各一个具名对**,还原后各自转绿;**item 7** `MOBILEGL_IPC_AUDIT=1` **179/179**。**唯一未过的是 E1 对照脚本本身**(ID-122):已修掉它两个遮蔽性缺陷(选中了一个在本车道永远跑不起来的阳性对照用例;ctest 与日志助手对“谁被选中”意见不一致),但它索要的 `Fatal{BarrierViolation}` 在整次运行里 **出现 0 次**:`BATCH_WAITS` 默认值 1 下那个重叠不发生,而 run-ahead 武装后 `VERB_BARRIER=0` 还会顺手关掉 run-ahead(ID-114),它产生的那条臂**既不是 lockstep 也不是 run-ahead**。E1 需要重新定义它要证伪什么,不是再调阈值;在想清楚之前不动它的计数规则,否则只是换一种伪绿。G1 仍由 CI 断言(ID-123) | P5d |
|
||||
| **P5f** 一切状态上 wire | ✅ **已收官(2026-09-20)**;验收代码 Windows `c42577a4` / WSL `cfca93c7885f` | f0 普查、f1 双块、fc 控制帧、fe Espryt 字段、fm Magma record consumers、fs 静态世代/角色及 liveness、fr registry、fv 反向回调均已合并。字段归属 **41 RECORD_SUPPLIED / 6 APPLIER_DERIVED / 0 BARRIER_PULLED / 16 FATAL**;strict / dualblock 预期 marker 集合均空。控制帧与静态量工作已完成,不再列为 P6 待实现项 | unit **2310 = 2300 PASS + 10 skip**;GPU **1377** 零失败;ordinary / strict split **180 = 176 PASS + 4 skip**;双块 **212 = 206 PASS + 6 精确具名 skip**;双后端 RSP 门 **2 PASS / 0 skip**、逐帧 rsp=0。G1 `.text` 10806051、symbols 27815 恒等且 0/0/0/0;G2 3016 同名;G14 3646→3744 无删名。逐包与收官修复均有 red-once;Claude 异族终审 P1/P2 已修;Redmi 六 clean-boot 臂 **60 PASS / 6 skip / 0 failed**、36 个窗口 rsp=0。详见 [`close-report`](notes/p5f/close-report.md) |
|
||||
| **P6** spawn transport | **a6 已收官(2026-09-21)**,c6 开工中 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch。消费 P5f 已落地的值控制帧与 server-owned 状态,不重做 EGL 帧化或静态世代隔离。依赖序见 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md):**a6**(核验进程装配/链接边界)→ **c6**(契约)→ **so / sm / cp / st / t6**。契约草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md)。**a6 产出**:[`a6-audit-v1.md`](notes/p6/a6-audit-v1.md)(198 行,七项各带对抗复审)、[`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)(**server 不能不链 `MG_Impl`**:184 个符号,债主 P7 101 / P3b-P4b 14 / 共有 60 / P6 自己 6);终局重审见 [`P6-ENDSTATE-REVIEW.md`](P6-ENDSTATE-REVIEW.md)。a6 推翻计划四处陈述:`spawn` 今天不是可观察状态、`cp` 不止传输、device-lost 闩锁不存在、"server 不链 MG_Impl" 为假。边界仍为 pbuffer / surfaceless / 离屏;真窗口到达归 P12。需区分不等待的 client 与已死亡的 server,不以超时阈值代替 device-lost 判定 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99;`integration-spawn` 与 `integration-split` 名集合一致;每条私有日志证明子进程 pid 与 `transport=spawn`;socket 门铃代价实际测量,性能只记录 |
|
||||
| **P6** spawn transport | **a6、c6 已收官(2026-09-21)**;下一个包 `lk` | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch。消费 P5f 已落地的值控制帧与 server-owned 状态,不重做 EGL 帧化或静态世代隔离。依赖序见 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md):**a6**(核验进程装配/链接边界)→ **c6**(契约)→ **so / sm / cp / st / t6**。契约草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md)。**a6 产出**:[`a6-audit-v1.md`](notes/p6/a6-audit-v1.md)(198 行,七项各带对抗复审)、[`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)(**server 不能不链 `MG_Impl`**:184 个符号,债主 P7 101 / P3b-P4b 14 / 共有 60 / P6 自己 6);终局重审见 [`P6-ENDSTATE-REVIEW.md`](P6-ENDSTATE-REVIEW.md)。**c6 产出**:规范契约 [`MG_Remote/CONTRACT-P6.md`](../../MobileGL/MG_Remote/CONTRACT-P6.md)(21 条裁定,取代草稿)+ 数据面缝的惰性声明 `Transport/ILink.h` 与 `Transport/StreamLink.h`;零 TU 依赖它们、零构建规则、pull 与 split 两侧 `.text` 与符号集逐字不变。c6 指定但留给 `hs` 落地的 schema 行见契约 §2。a6 推翻计划四处陈述:`spawn` 今天不是可观察状态、`cp` 不止传输、device-lost 闩锁不存在、"server 不链 MG_Impl" 为假。边界仍为 pbuffer / surfaceless / 离屏;真窗口到达归 P12。需区分不等待的 client 与已死亡的 server,不以超时阈值代替 device-lost 判定 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99;`integration-spawn` 与 `integration-split` 名集合一致;每条私有日志证明子进程 pid 与 `transport=spawn`;socket 门铃代价实际测量,性能只记录 |
|
||||
| **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 的 monolith 清理(transport 原生臂已由 P5f fs 落地);回读 / 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)全量迁移 | 部分已提前落地,阶段未收官 | `38919d45` 已补实际游戏的 server buffer consumers 和 Android 旋转 blit([报告](notes/p5f/magma-inproc-fix.md));`194382c9` 又完成 Magma run-ahead、GPU 延迟退休与 VAO 身份碰撞修复([报告](notes/p5f/magma-runahead.md));其余工作:其余子系统:`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` 逐用例发布 |
|
||||
|
||||
Reference in New Issue
Block a user