[Docs] (Disaggregated): P6 contract draft rewritten in concise wording, 269 -> 181 lines, same rows

This commit is contained in:
2026-09-19 04:39:12 -04:00
parent e125b75655
commit 7f1d2134c3
+131 -219
View File
@@ -1,269 +1,181 @@
# CONTRACT-P6 (DRAFT) — the backend runs in a second process
> **THIS IS A DRAFT, AND IT IS WRITTEN BEFORE ITS OWN AUDIT.** Package `a6` (`P6-SPAWN-PLAN.md`
> §4) is a read-only audit whose output is this file's input. Every row below that depends on
> what `a6` finds is marked **`PENDING a6`** and states what it is waiting for. A contract that
> pretended to know those rows today would be the same defect this project has recorded three
> times: evidence asserted ahead of the thing it describes (ID-124, ID-132, ID-133).
>
> On landing, package `c6` moves this file to `MobileGL/MG_Remote/CONTRACT-P6.md`, beside its
> four siblings, and it becomes normative there. Until then it is a proposal.
> **Draft, written before its own audit.** Package `a6` ([`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md) §5)
> is a read-only audit whose output is this file's input. Rows that depend on it are marked
> **`PENDING a6`**. `c6` moves this file to `MobileGL/MG_Remote/CONTRACT-P6.md`, where it becomes
> normative.
Authority once landed: 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) and `CONTRACT-P5E.md` (rule F, the barriered predicate,
the wait rule). Where it disagrees with any of them this file is newer and wins; §9 lists every
such place. Base: `feat/disaggregated @ f23fbc1b`. Every `file:line` was read at that commit and
must be re-resolved at `c6`'s head; paths are under `MobileGL/` unless they start with `docs/`.
Authority once landed: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, R-1…R-17),
`CONTRACT-P5B.md` (class-C slots), `CONTRACT-P5C.md` (rule E, SEG_EVENT, the guards) and
`CONTRACT-P5E.md` (rule F, the barriered predicate, the wait rule). Disagreements: this file is
newer and wins; §9 lists them. Base `feat/disaggregated @ f23fbc1b`; every `file:line` re-resolves
at `c6`'s head. Paths under `MobileGL/` unless they start with `docs/`.
**How to change it.** Package `c6`'s file, edited by the integrator first. A P6 package
(`so` / `sm` / `cp` / `st` / `t6`) that needs a row changed goes through the integrator; the
packages compile against the rows below from day one.
Changes go through the integrator, in `c6`'s copy. Packages compile against these rows from day one.
---
## §0 What P6 is, and the rule above every row
## §0 Rule G
P5c made `inproc` honest — between the two roles nothing crosses except `SEG_CMD` / `SEG_STAGE` /
`SEG_REPLY` / `SEG_EVENT` / the caps snapshot / control frames — *so that P6 would be a transport
swap and nothing else* (`MG_Remote/CONTRACT-P5C.md:23-27`). P5e then retired the lockstep on the
Espryt draw path: the client publishes an unbarriered record and does not wait.
Rules A–F bind unchanged. P6 adds:
**P6 puts the server in a second process.** The rings, the codec, the applier, the sinks, the
backends and the wait rule are unchanged. What changes is that the two roles no longer share an
address space at all — so every assumption that survived P5c *because* they did is now load-bearing
for the first time.
**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 below,
or does not cross. Descriptors cross only by `SCM_RIGHTS` (`Transport/FdPassing.h`), where the
kernel renumbers them.
Rules A–F bind unchanged. P6 adds one rule:
Rule G's teeth are §8's arm-proof gate: a spawn lane that silently ran monolith satisfies every
other gate here.
**Rule G — a role may not name a process-local handle.** Rule E (P5c) forbade a role from naming
the other role's *memory*. Rule G extends it to *identity*: a value whose meaning is supplied by
the owning process's kernel or loader state — `EGLDisplay`, `EGLSurface`, `EGLContext`,
`ANativeWindow*`, any `void*` window handle, a file descriptor number, a `pid`, a mapped address —
either crosses as a **token** this contract defines, or it does not cross. A descriptor crosses
only through `SCM_RIGHTS` (`Transport/FdPassing.h`), where the *kernel* re-numbers it, never as an
integer in a frame.
Rule G's teeth are §8's arm-proof gate, not a reviewer's attention: a spawn lane that silently ran
monolith satisfies every other gate in this document.
---
## §1 Table 0 additions — the encodings P6 introduces
## §1 Table 0 additions
| row | encoding | invalid | consumer |
|---|---|---|---|
| **`DisplayToken` / `SurfaceToken` / `ContextToken`** | `Uint64`, **minted by the client**, dense from 1, never reused within a session. The server keeps a token → native-handle map and never sees the client's `EGLDisplay`/`EGLSurface`/`EGLContext` values. `protocol.fbs:186-196`'s `SurfaceOp.display` / `.surface` are already `ulong` and are these. | `0` on any op other than `ReleaseCurrent` is `Fatal{ProtocolCorruption}` | the server's surface map |
| **`SurfaceOpKind` additions** | Append-only to `protocol.fbs:166-175`. Expected: `SetSwapInterval`, `ReleaseResources`, `SetWindowHandle`. **`PENDING a6`** — the count is the audit's item 4, not a guess this file may make. | an unknown tag is `Fatal{ProtocolCorruption}`, never ignored | `ServerMain`'s control pump |
| **`WindowKind::AndroidNativeWindow` arriving at the server** | Not an encoding but a refusal: `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`. `protocol.fbs:186-196`'s own comment already says Android transfers the window out of band; P12 is that band. | — | §4.3 |
| **`FatalCode::ServerCrashed` / `DeviceLost`** | Already allocated (`protocol.fbs:234-242`). P6 is the first phase that can produce either. | — | §5 |
| **ABI fingerprint** | Unchanged. `CapsCodec.h:23-29` states the ruling: P6's spawn is same-machine and same-binary, so the existing `sizeof(DynamicBackendParameters)` / `sizeof(MGPCaps)` / build-fingerprint handshake is inherited as-is and `Fatal{AbiMismatch}` keeps its meaning. **The fixed-width rewrite stays P7's account.** | mismatch → `Fatal{AbiMismatch}` | `SessionHandshakeTest.cpp:115` |
---
| `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:186-196`'s `display` / `surface` are already `ulong` and are these. | `0` on any op but `ReleaseCurrent` → `Fatal{ProtocolCorruption}` | the server's surface map |
| `SurfaceOpKind` additions | Append-only to `protocol.fbs:166-175`. Expected `SetSwapInterval`, `ReleaseResources`, `SetWindowHandle` — **`PENDING a6`** (audit item 4). | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | `ServerMain`'s control pump |
| `WindowKind::AndroidNativeWindow` at the server | Refusal: `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`. | — | §4.3 |
| `FatalCode::ServerCrashed` / `DeviceLost` | Already allocated (`protocol.fbs:234-242`); P6 is the first phase that can produce either. | — | §5 |
| ABI fingerprint | Unchanged. `CapsCodec.h:23-29`: spawn is same-machine, same-binary, so the existing `sizeof(DynamicBackendParameters)` / `sizeof(MGPCaps)` / build-fingerprint handshake is inherited. Fixed-width rewrite stays P7's. | mismatch → `Fatal{AbiMismatch}` | `SessionHandshakeTest.cpp:115` |
## §2 The transport (`so`)
**2.1 `SocketTransport` implements `ITransport` and adds nothing to it.** The two doorbell
accessors stay on the session, not on the interface — that ruling was made in P5's s1 precisely so
P6 would not discover it (`Transport/SessionRings.h:279-285`, `Server/ServerSession.cpp:670-690`).
**2.2 `ReceiveFrame`'s `BUFFER_TOO_SMALL` keeps the message queued.** This is not an optimization;
`ITransport.h` records that the earlier branch's transport failed the call *and dropped the
message*, which wedges the stream permanently the first time a message exceeds the reader's guess.
`SocketTransport` must pass the same assertion `InProcessTransport` passes.
**2.3 `SocketTransport` must be green before a second process exists.** One `socketpair()`, two
threads, the whole `InProcessTransportTest` suite. This is the same discipline that made `inproc`
use `ShmSegment` rather than `new[]` (`Transport/SessionRings.h:22-28`): a code path first executed
on the day the second process appears is the shape of every "it was green in CI" failure this phase
exists to avoid.
**2.4 Segment delivery.** The server owns and creates the four segments (`SessionRings.h:120-130`);
the client adopts them by `SCM_RIGHTS`. `ShmSegment::Adopt` (`Transport/ShmSegment.h:57`,
`ShmSegmentPosix.cpp:120`) and the dedicated `AF_UNIX SOCK_DGRAM` aux socket
(`Transport/FdPassing.cpp:92`) already exist and are tested (`MG_Test/Wire/FdPassingTest.cpp:114`,
`:187`). The client's `AttachInProcess` path (`SessionRings.h:147-159`) is replaced by an adopt on
the received descriptors — the same `Adopt` + `Map` calls it already makes.
**2.5 Doorbells are `SocketDoorbell`, and this is a deliberate cost.** `Doorbell.h:385-420` chose a
socket byte over a futex so that peer death arrives as `POLLIN|POLLHUP` with `recv()==0` rather
than as a hang. Implemented at `Doorbell.cpp:211-360`, four cases at
`MG_Test/Wire/FdPassingTest.cpp:197`, `:227`, `:260`, `:289`. **One syscall per wake is the price
of death detection**; §8.8 makes it a measurement, not an argument.
**2.6 Windows.** `ShmSegment::Adopt` is POSIX-only. `TransportMode::UnixSocket` and `NamedPipe`
stay **named refusals** (`ConfigLoader.cpp:310-348`). P6 lands POSIX only.
---
1. `SocketTransport` implements `ITransport` and adds nothing to it. The two doorbell accessors
stay on the session (`Transport/SessionRings.h:279-285`, `Server/ServerSession.cpp:670-690`).
2. `ReceiveFrame`'s `BUFFER_TOO_SMALL` keeps the message queued. `ITransport.h` records that the
earlier branch dropped it, wedging the stream at the first oversized message.
3. **`SocketTransport` must be green before a second process exists**: one `socketpair()`, two
threads, the whole `InProcessTransportTest` suite. Same discipline as `Transport/SessionRings.h:22-28`.
4. The server creates the four segments (`SessionRings.h:120-130`); the client adopts them by
`SCM_RIGHTS`. `ShmSegment::Adopt` (`Transport/ShmSegment.h:57`, `ShmSegmentPosix.cpp:120`) and
the aux socket (`Transport/FdPassing.cpp:92`) exist and are tested (`MG_Test/Wire/FdPassingTest.cpp:114`,
`:187`). `AttachInProcess` (`SessionRings.h:147-159`) is replaced by an adopt on the received fds.
5. Doorbells are `SocketDoorbell`. `Doorbell.h:385-420` chose a socket byte over a futex so peer
death arrives as `POLLIN|POLLHUP` with `recv()==0` rather than as a hang. One syscall per wake
is the price; §8.8 measures it.
6. Windows: `Adopt` is POSIX-only. `UnixSocket` and `NamedPipe` stay named refusals
(`ConfigLoader.cpp:310-348`). P6 lands POSIX only.
## §3 The process (`sm`)
**3.1 The child must not be able to become a client.** Two independent mechanisms, both required:
(a) `MOBILEGL_TRANSPORT` and every `MOBILEGL_IPC_*` that selects a role is **removed from the
child's envp**; (b) the child's own config is **forced to `Monolith`** regardless of what it reads.
Either alone is a single point of failure whose failure mode is a fork bomb.
**3.2 Finding the server image.** `MOBILEGL_IPC_SERVER_PATH` first (already parsed,
`Config.h`'s `IpcTable::ServerPath`), then `dladdr` on a symbol in the loaded library to locate
`libMobileGLServer.so` beside it. **A path that does not resolve is a named refusal and never a
fallback to monolith** — `ConfigLoader.cpp:315-317` names this exact accident as the thing its own
refusal exists to prevent.
**3.3 `ServerMain`.** Today's `MobileGLServer` (`CMakeLists.txt:923`) is the P0 spike stub
(`tools/spikes/server_stub/main.cpp`), Android-only and off by default; it writes a marker and
exits. `c6` lands the real target. **`PENDING a6`** — whether the server image can be linked
without `MG_Impl` (`MG_Backend/MGPipe/PipeInputs.h` asserts "P6, where MG_Impl is not in the
server"). If it cannot, this file says so and the debt is recorded against P13 rather than left as
a comment promising something no phase owns.
**3.4 Handshake.** Bounded retry, never unbounded. `Hello` → `Welcome` → segment descriptors →
first `CapsSnapshot` (`protocol.fbs:59-153`). A retry budget that is exhausted is a named refusal.
**3.5 No orphans.** The child exits on EOF of the control socket. The parent reaps. §8.4 makes this
mechanical rather than observed.
---
1. **The child must not be able to become a client.** Both required: (a) `MOBILEGL_TRANSPORT` and
every role-selecting `MOBILEGL_IPC_*` removed from the child's envp; (b) the child's config
forced to `Monolith` regardless. Either alone fails as a fork bomb.
2. Image lookup: `MOBILEGL_IPC_SERVER_PATH` (already parsed, `Config.h` `IpcTable::ServerPath`),
then `dladdr` to find `libMobileGLServer.so` beside the loaded library. **An unresolvable path
is a named refusal, never a monolith fallback** — `ConfigLoader.cpp:315-317` names that accident.
3. `ServerMain`. Today's `MobileGLServer` (`CMakeLists.txt:923`) is the P0 spike stub
(`tools/spikes/server_stub/main.cpp`). **`PENDING a6`**: whether the server image can link
without `MG_Impl` (`MG_Backend/MGPipe/PipeInputs.h` claims it). If not, this file says so and the
debt is recorded against P13 rather than left as a comment.
4. Handshake: `Hello` → `Welcome` → segment descriptors → first `CapsSnapshot`
(`protocol.fbs:59-153`). Bounded retry; an exhausted budget is a named refusal.
5. Child exits on EOF; parent reaps. §8.4 makes this mechanical.
## §4 The control plane (`cp`)
**4.1 The twelve forwarders become frames.** `Server/ServerLoop.cpp:802-1022` holds twelve `Server*`
EGL forwarders; each posts a **function pointer plus a stack-local `void*`** to a one-slot mailbox
(`ServerLoop.cpp:533-568`, `RunOnApplyThread` at `:645`). This is P5c audit row G4, recorded there
as P6's (`CONTRACT-P5C.md:537`). Across a process boundary neither the pointer nor the `void*` has
meaning, so each becomes a `SurfaceOp` / `SurfaceReply` pair.
1. `Server/ServerLoop.cpp:802-1022`'s twelve `Server*` forwarders each post a function pointer plus
a stack-local `void*` to a one-slot mailbox (`ServerLoop.cpp:533-568`, `:645`) — P5c audit row
G4, recorded as P6's (`CONTRACT-P5C.md:537`). Neither has meaning across a process, so each
becomes a `SurfaceOp` / `SurfaceReply` pair.
2. Reconciliation against the seven existing enumerators: `ServerSwapEGLBuffers` stays a record with
its own present credit (P5e ruling, `Client/BackendObject_Remote.cpp`); `ServerInitCapabilities`
is answered by `CapsSnapshot`; `ServerInitWindowSurface` is a client-side no-op. The remaining
nine map onto the seven plus §1's additions. **`PENDING a6`**.
3. **Real windows are refused, by name.** `WindowHandle::Handle` (`MG_Backend/BackendObject.h:561-566`)
is an `ANativeWindow*` on Android. P6 lands pbuffer / surfaceless / offscreen — what `HeadlessGL`,
the split-equivalent lane and trace replay need, and what P6's exit gate measures. **Real window
arrival is P12.**
4. `ForgetCurrentTuple`'s N-3 rule (`ServerLoop.cpp:318-330`) is a backend property, not a transport
one, and must hold identically on the frame path.
**4.2 The reconciliation.** Twelve forwarders against seven existing `SurfaceOpKind` enumerators:
- `ServerSwapEGLBuffers` does **not** become a frame. It travels as a record, in order, with its
own present credit — `Client/BackendObject_Remote.cpp`'s own comment states this and P5e ruled it.
- `ServerInitCapabilities` is answered by `CapsSnapshot`, which already exists.
- `ServerInitWindowSurface` is a client-side no-op and crosses nothing.
- The remaining nine map onto the seven enumerators plus the additions of §1. **`PENDING a6`.**
## §5 Death, and what P5e changed
**4.3 Real windows are refused, by name.** `WindowHandle::Handle` (`MG_Backend/BackendObject.h:561-566`)
is an `ANativeWindow*` on Android and means nothing in the child. P6 lands pbuffer / surfaceless /
offscreen — enough for `HeadlessGL`, the whole `integration-split`-equivalent lane and trace replay,
which is what P6's own exit gate measures. **The arrival of a real window is P12.** This row exists
because "P6 is done" will otherwise be read as "the game runs on the phone now". It will not.
**4.4 `ForgetCurrentTuple` semantics survive verbatim.** `ServerLoop.cpp:318-330`'s N-3 rule —
creating a different surface destroys and recreates the native context, so the bound tuple names a
dead context — is a property of the backend, not of the transport, and must hold identically on the
frame path.
---
## §5 Death, and what P5e changed about it
**5.1 The latch is already designed; P6 implements it unchanged.** `docs/Disaggregated/ARCHITECTURE.md:504`:
server dies → client reads EOF/EPIPE → device-lost latch (GL calls no-op, `eglSwapBuffers` returns
`EGL_FALSE` + `EGL_CONTEXT_LOST`, `glGetGraphicsResetStatus` returns `GL_UNKNOWN_CONTEXT_RESET`);
`MOBILEGL_IPC_RESPAWN=1` restarts and re-pushes (default off). Client dies → server reads EOF and
exits immediately; `MOBILEGL_IPC_IDLE_EXIT_S` (default 30) is a last resort only.
**5.2 What P5e added, and what no existing document covers.** Under lockstep, an apply thread that
died took the process with it, so "the client kept drawing" was not a reachable state. Under
run-ahead the client is several records ahead by construction. Therefore:
- **A published, never-applied record is discarded at the latch, not waited on.** The client must
not block for a watermark that can no longer move.
- **Slow and dead must be distinguishable without a timeout threshold.** A server that is merely a
frame behind is P5e's intended steady state; a server that is gone is `Fatal{ServerCrashed}`.
`SocketDoorbell` already latches `m_dead` on EOF (`Doorbell.cpp`'s `Drain`) — the distinction is
available as a *fact* and must be taken from there rather than inferred from elapsed time.
- **A frozen peer produces no hangup.** Android's cached-app freezer suspends the child without
closing anything. P6 must give a named, bounded diagnostic instead of relying on "it will come
back". Full handling is P12's; the diagnostic is P6's.
**5.3 This is P6's only new design.** Everything else in this contract is carriage. It is also the
nearest neighbour of the E1 control's outstanding re-specification (ID-122), which needs exactly the
same predicate: a test that can tell a slow apply from a dead one. **The two should be designed
together, and neither should be settled by choosing a threshold.**
---
1. The latch is already designed and is implemented unchanged: `docs/Disaggregated/ARCHITECTURE.md:504`
— server dies → client reads EOF/EPIPE → device-lost latch (GL calls no-op, `eglSwapBuffers`
returns `EGL_FALSE` + `EGL_CONTEXT_LOST`, `glGetGraphicsResetStatus` returns
`GL_UNKNOWN_CONTEXT_RESET`); `MOBILEGL_IPC_RESPAWN=1` restarts and re-pushes (default off).
Client dies → server reads EOF and exits; `MOBILEGL_IPC_IDLE_EXIT_S` (default 30) is a last resort.
2. What P5e added, and no existing document covers. Under lockstep a dead apply thread took the
process with it, so "the client kept drawing" was unreachable. Under run-ahead the client is
several records ahead by construction. Therefore:
- 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 (`Doorbell.cpp`'s `Drain`) — take the fact from there.
- **A frozen peer produces no hangup.** Android's cached-app freezer suspends the child without
closing anything. P6 gives a named, bounded diagnostic; full handling is P12's.
3. This is P6's only new design; everything else is carriage. It shares a predicate with E1's
outstanding re-specification (ID-122) — design the two together, and settle neither by choosing a
threshold.
## §6 Process-lifetime statics (`st`)
`g_syncedRenderStateParameters` (`MG_Backend/DirectGLES/DirectGLES.cpp:4586`) and
`ScopedDefaultUnpackState::s_synced` are **process-level statics whose semantics are per-context**.
P5c recorded both as P6's (`CONTRACT-P5C.md:538-539`). In one process they happen to track the
process; in a server process that outlives or is reused across client contexts they hand the next
context the previous one's synced state.
`ScopedDefaultUnpackState::s_synced` are process-level statics with per-context semantics, recorded
as P6's by `CONTRACT-P5C.md:538-539`. In a server process reused across client contexts they hand
the next context the previous one's synced state.
**The rule: any static whose meaning is per-context or per-session is keyed by the context
generation and reset at `applier_reset`.** **`PENDING a6`** — the two above are the known members;
the audit's item 2 is to establish the complete list. This class of defect is structurally invisible
to `inproc`, which is why it is being looked for rather than waited for.
**Rule: any static whose meaning is per-context or per-session is keyed by the context generation
and reset at `applier_reset`.** **`PENDING a6`** for the complete list (audit item 2); the two above
are the known members. This class is structurally invisible to `inproc`.
---
## §7 Unchanged by P6
## §7 What P6 does not change
The rings, the codec, the record layout, the applier, both verb sinks, both backends, the barriered
predicate and the wait rule (`CONTRACT-P5E.md` §2) are untouched. The reduced path is the same one
`integration-split` runs today: P6's lane must produce the **same test-name set and the same
numbers**, not more (§8.2). The 511 `UnmigratedVerb` class-C entries are not P6's account.
---
Rings, codec, record layout, applier, both verb sinks, both backends, the barriered predicate and
the wait rule (`CONTRACT-P5E.md` §2). P6's lane runs the same reduced path as `integration-split`
and must produce the same name set and the same numbers, not more (§8.2). The 511 `UnmigratedVerb`
class-C entries are not P6's.
## §8 The gate
Beyond the standard five-part gate (`docs/Disaggregated/ARCHITECTURE.md` §13.2):
Beyond the five-part gate (`docs/Disaggregated/ARCHITECTURE.md` §13.2):
1. **G1** — pull build 0/0/0/0, `.text` unchanged.
2. **G2/G14** — `integration-spawn`'s test-name set is **identical** to `integration-split`'s. A
differing name set means the spawn lane is quietly running less.
3. `integration-spawn` fully green, at the same count as `integration-split` (today 179/179).
4. **Process-tree gate, mechanical** — child count sampled before and after every entry: exactly
one extra while running, zero after. `HeadlessGL`'s fork pre-check leaves no orphan.
5. **Arm-proof gate (ID-124)** — every spawn entry records the child pid and `transport=spawn` in
**its own private log**. ID-124 established that a lane can report a green arm it never ran; a
spawn lane that fell back to monolith would pass items 1-4.
6. **Red-once per package** (R-16), one named pair each.
7. **Device** — Redmi `2f7cbe2e`, reboot-clean, paired A/B in one thermal window; OpenRA on
Adreno 830 at split SSIM ≥ 0.99.
8. **Performance is recorded, not gated** (roadmap discipline), but one number is mandatory: **the
socket doorbell's cost against `inproc`'s condvar.** P5d took the client thread from 15.0 to
9.2 ms/frame and a real part of that was the doorbell path. This is P6's only expected
regression and it gets its own paired measurement rather than a footnote.
1. G1 — pull build 0/0/0/0, `.text` unchanged.
2. G2/G14 — `integration-spawn`'s name set identical to `integration-split`'s.
3. `integration-spawn` green at the same count as `integration-split` (today 179/179).
4. Process tree, mechanical — child count before and after every entry: exactly one 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.
6. Red-once per package (R-16), one named pair each.
7. Device — Redmi `2f7cbe2e`, reboot-clean, paired A/B in one thermal window; OpenRA on Adreno 830
at split SSIM ≥ 0.99.
8. Performance recorded, not gated, but one number is mandatory: **the socket doorbell's cost against
`inproc`'s condvar.** P5d took the client thread 15.0 → 9.2 ms/frame and part of that was the
doorbell path. P6's only expected regression.
**Negative controls**, each run red once:
Negative controls, each run red once:
| # | knob | what it must falsify |
|---|---|---|
| S1 | `MOBILEGL_IPC_SERVER_PATH=/nonexistent` | a named refusal, **not** a silent monolith fallback (§3.2) |
| S2 | `kill -9` the server mid-frame | the §5.1 latch; the client never hangs and never keeps submitting |
| S3 | child envp not scrubbed | the second safety of §3.1 catches it, by name |
| S1 | `MOBILEGL_IPC_SERVER_PATH=/nonexistent` | named refusal, not a silent monolith fallback (§3.2) |
| S2 | `kill -9` the server mid-frame | §5.1's latch; the client never hangs and never keeps submitting |
| S3 | child envp not scrubbed | §3.1's second safety catches it, by name |
| S4 | `st`'s generation reset disabled | a second context inherits the first's synced state (§6) |
| S5 | `WindowKind::AndroidNativeWindow` reaches the server | named `Fatal`, not a black screen (§4.3) |
**S2's shape must be decided before it is written.** E1 is the cautionary case: it demanded a
`Fatal` that occurs zero times in the entire run (ID-122). S2 must falsify *"the client can tell
slow from dead"*, not *"some Fatal appeared"*.
---
S2's shape is settled before it is written. E1 demanded a `Fatal` that occurs zero times in the
entire run (ID-122); S2 must falsify *"the client can tell slow from dead"*.
## §9 Amendments to the earlier contracts
1. **`CONTRACT-P5C.md:537-539`** — the EGL forwarders' control plane and the `s_synced` /
`g_syncedRenderStateParameters` generation resets are discharged here, §4 and §6.
2. **`CONTRACT-P5C.md:56`** — `MGPApplierReset::ContextSerial` is asserted equal, not dispatched
on, because P5c has one context per session. It says P6's multi-context shape reads it for real.
**This contract declines that**: P6 stays single-context and multi-context moves to P12, where
the Service lifecycle makes it concrete. The assertion stands unchanged.
3. **`Transport/ReplySlot.h`** — the 2 MiB single-slot reply cap and chunked readback are marked
"P6 debt" there. **This contract declines them too**: they belong to P9's reverse channel.
Taking them here would make "a transport swap and nothing else" false a second time.
4. **`CapsCodec.h:23-29`** — inherited unchanged; recorded in §1 so it is not re-litigated.
1. `CONTRACT-P5C.md:537-539` — the EGL forwarders' control plane and the `s_synced` /
`g_syncedRenderStateParameters` resets are discharged here, §4 and §6.
2. `CONTRACT-P5C.md:56` says P6's multi-context shape reads `MGPApplierReset::ContextSerial` for
real. **Declined**: P6 stays single-context, multi-context moves to P12 where the Service
lifecycle makes it concrete, and the assertion stands unchanged.
3. `Transport/ReplySlot.h` marks the 2 MiB reply cap and chunked readback "P6 debt". **Declined**:
they are P9's. Taking them here would make "a transport swap and nothing else" false a second time.
4. `CapsCodec.h:23-29` — inherited unchanged; recorded in §1 so it is not re-litigated.
---
## §10 Explicitly not P6
## §10 Not P6
Real window arrival and the `android:process=":mgl"` Service (P12); multi-context (P12, §9.2);
chunked readback and the reply-slot pool (P9, §9.3); the `DynamicBackendParameters` fixed-width
rewrite (P7); `MOBILEGL_IPC_POLL_ESCALATE` and the polling-entry escalation (P10); object-class
BARRIER-PULLED rows and the frontend-keyed twin registry (P3b/P4b, P7); Windows `pipe:` /
`unix:` (§2.6); the 511 class-C `UnmigratedVerb` entries (§7).
chunked readback and the reply-slot pool (P9, §9.3); `DynamicBackendParameters` fixed-width rewrite
(P7); `MOBILEGL_IPC_POLL_ESCALATE` (P10); object-class BARRIER-PULLED rows and the frontend-keyed
twin registry (P3b/P4b, P7); Windows `pipe:` / `unix:` (§2.6); the 511 class-C entries (§7).
The E1 control's re-specification (ID-122) is **not** discharged by P6 and does not become P6's
merely because P6 starts. It is named here only because §5.3 argues it shares a predicate.
E1's re-specification (ID-122) is **not** discharged by P6 and does not become P6's because P6
starts. It appears here only because §5.3 argues it shares a predicate.