mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
P6's premise is that it is "only a transport swap". That premise was bought by P5c and has
expired. Measured on this head, the two roles still share state directly, so a spawned server
would have no source for it. P5f is the phase that makes the premise true, and P6 does not
start before it.
WHAT THE MEASUREMENT FOUND
- FieldOwnership.def still carries 21 BARRIER_PULLED rows over 15 distinct fields. That class
is defined as "the server answers by reading a value the client's residual fill left in THE
SINGLE SHARED gPipeInputs while the verb barrier holds both threads apart". Eight of those
fields were retired by P5e for Espryt's unbarriered path only - Magma has not moved at all.
The strict lane measures 10 live <field>@<verb> pairs today.
- gPipeInputs (MG_Backend/MGPipe/PipeInputs.h:835) is a plain process global,
`inline PipeInputs& gPipeInputs = *new PipeInputs();` - not a shm segment. The client fills
it (MG_Impl/Pipe/PipeFill.cpp), the backend reads it. Under spawn the two processes have
DIFFERENT objects, the 15 BARRIER_PULLED fields have no filler in the server's copy, and
every read is Fatal{UnmigratedPipeInput}. MG_Impl is not in the server, so "let the server
fill it" is not available either. The header says this itself at :279 and :902.
- The twelve Server* EGL forwarders do not use the wire at all: they post a function pointer
plus a stack-local void* through a one-slot mailbox (ServerLoop.cpp:533-568, :645). The
SurfaceOp / SurfaceReply / WindowKind schema is defined in protocol.fbs and unused.
- Process-level statics with per-context semantics (g_syncedRenderStateParameters,
ScopedDefaultUnpackState::s_synced) - and the full list is unknown, because this defect class
is structurally invisible to inproc.
- MGPipeFrontendKeyedRegistryScope is still live at ~12 sites in DirectGLES.cpp, carrying
P3b/P4b's debt: the registry is still keyed by frontend address / lifetime id.
THE SCOPE RULE
P5f takes the cross-process-blocking subset of P3b/P4b/P7/P8/P9 and nothing else. One test:
does it still hold in another address space? No -> P5f. Yes but slow or ugly -> it stays with
the phase that owns it. So Magma's nine fields and its direct writes into client storage come
here; content-addressed CSO, internal shader baking and the fixed-width rewrite stay P7's. The
reverse channel's asynchrony and ordering stay P9's; only the parts that touch client memory
move. Class C is NOT P5f's - a named refusal behaves identically in two processes.
THE CENTRAL MECHANISM: A TWO-BLOCK REHEARSAL
P5c set the same rule (rule E) and checked it by AUDIT - a human reading 59 lines. P5f checks
it by MACHINE: give each role its own PipeInputs under inproc, and its own copy of every
per-context static, behind a knob. Anything that depends on the two being the same object
becomes Fatal{UnmigratedPipeInput, "<Field>@<Verb>"} with the field and verb named.
That rehearses the process split's SEMANTICS without a second process. It matters because
today "the server read the value the client filled" and "the server read the value a record
carried" are indistinguishable at runtime - which is exactly what P6 would discover on day
one. The package order puts the mechanism (f1) BEFORE the migrations deliberately: let the red
appear first and let it be the worklist, rather than having each package declare itself done
and then looking for a criterion.
Exit gate: the two-block lane green; strict-expected-markers.txt EMPTY with its two-sided
ratchet still armed; zero BARRIER_PULLED rows; rsp reading zero per frame; Magma judged by the
same criteria as Espryt. The negative control is that turning the two-block knob OFF must move
at least one named case from green to red - otherwise the mechanism proves nothing, which is
ID-122's lesson.
ALSO IN THIS COMMIT
- ROADMAP gains the P5f row and the IPC track becomes P5e -> P5f -> P6; P6's status cell
becomes "blocked on P5f".
- README and CURRENT_STAGE_PROGRESS gain the same, plus the file-map entry.
- Both P6 documents gain a blocked banner naming what is not true yet.
Doc citation lint: 124 citations across docs/Disaggregated/*.md, 0 problems.
187 lines
12 KiB
Markdown
187 lines
12 KiB
Markdown
# CONTRACT-P6 (DRAFT) — the backend runs in a second process
|
||
|
||
> **⏸ Blocked on P5f** (2026-09-19): this draft assumes P6 is only a transport
|
||
> swap, which is not true today - 15 `BARRIER_PULLED` fields are still filled by the
|
||
> client into a shared `gPipeInputs`, and Magma has not moved at all. See
|
||
> [`P5F-WIRE-COMPLETENESS.md`](P5F-WIRE-COMPLETENESS.md).
|
||
>
|
||
> **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, 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/`.
|
||
|
||
Changes go through the integrator, in `c6`'s copy. Packages compile against these rows from day one.
|
||
|
||
---
|
||
|
||
## §0 Rule G
|
||
|
||
Rules A–F bind unchanged. P6 adds:
|
||
|
||
**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.
|
||
|
||
Rule G's teeth are §8's arm-proof gate: a spawn lane that silently ran monolith satisfies every
|
||
other gate here.
|
||
|
||
## §1 Table 0 additions
|
||
|
||
| row | encoding | invalid | consumer |
|
||
|---|---|---|---|
|
||
| `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`)
|
||
|
||
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`)
|
||
|
||
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`)
|
||
|
||
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.
|
||
|
||
## §5 Death, and what P5e changed
|
||
|
||
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 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.
|
||
|
||
**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
|
||
|
||
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 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 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:
|
||
|
||
| # | knob | what it must falsify |
|
||
|---|---|---|
|
||
| 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 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` 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 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); `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).
|
||
|
||
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.
|