# fc — 控制面帧契约草稿(P5f 包 fc;CONTRACT-P5F.md 本体留给集成收口) > Status: DRAFT, landed in code by package `fc` on branch `p5f/fc`. Authority once integrated: this > feeds CONTRACT-P5F.md. Until then it supplements CONTRACT-P5C (audit row G4, discharged here) and > CONTRACT-P5E §2.5 (the wait-before-forwarder discipline, unchanged), and it answers the > control-plane rows of `docs/Disaggregated/P6-CONTRACT-DRAFT.md` §4 ahead of schedule. > Code anchors: `MobileGL/MG_Remote/Server/SurfaceControlFrame.h` (the frame), > `MobileGL/MG_Remote/Protocol/SurfaceOpCodec.{h,cpp}` (the wire codec and the named refusals), > `MobileGL/MG_Remote/Server/ServerLoop.cpp` (`RunSurfaceControlFrame` / > `ApplySurfaceControlFrame`). --- ## 1. Ownership 1. A **SurfaceControlFrame is a value**. The poster fills the request fields; ownership of the slot's content passes to the channel at publish; the dispatch on the apply thread fills the reply half (`ok`, `eglMajor`, `eglMinor`) into the same value; the poster's copy-out happens after the blocking handshake returns. No field is a pointer and none may become one — the exhaustive constexpr structured binding in `SurfaceControlFrame.h` checks every member as an integer or enum. Adding a member without extending the binding fails compilation; changing a checked member to a pointer fails its static assertion. Trivially-copyable / standard-layout remain separate copyability checks: neither trait rejects pointers. 2. An unnumbered local request (`seq == 0`) is numbered by `RunSurfaceControlFrame`, from 1 per session. An already numbered wire request retains its client-minted sequence through dispatch and reply (P6-CONTRACT-DRAFT table 0); the server never renumbers it. 3. `display` / `surface` / `readSurface` / `context` are Uint64 **tokens**. Inproc they are the client's EGL handle values bit-cast — legal only because both roles share a process and a driver, and the N-3 tuple bookkeeping compares them by value. Under spawn they are client-minted dense tokens (P6-CONTRACT-DRAFT table 0, unchanged). No code may dereference a token as a pointer on a path a non-inproc transport can reach. 4. `windowBackend` + `nativeToken` + `width`/`height` carry a `WindowHandle` as values. The frame names the **backend** enum, never the wire's `WindowKind`; the two enums meet only inside `SurfaceOpCodec.cpp`'s explicit tables (`Surfaceless`/`Pbuffer` are surface shapes and answer "no backend" there). A `nativeToken` whose backend is Android is an `ANativeWindow*` — an address in the client's process — and may never cross (§3.5). MetalLayer carries a `CAMetalLayer*` and has the same restriction. ## 2. Ordering 1. The channel keeps its P5 semantics: one blocking slot, `m_callerMutex` serialising posters, pumped by the apply thread **between drain batches**, publish-then-ring. Frames are FIFO against each other; they are NOT ordered against SEG_CMD records by the channel itself. 2. The client-side discipline that orders frames against records is unchanged and remains mandatory under spawn: `WaitForApplyBeforeEglForwarder` waits `appliedSeq` to `LastPublishedSeq` before every forwarder (CONTRACT-P5E §2.5), because the channel is pumped between drain batches and a control frame would otherwise overtake in-flight records. 3. **All twelve ops stay blocking in fc**, including the three f0-egl §4.3 marked relaxable (`SetSwapInterval`, `ReleaseSurface`, `SetWindowHandle`). Relaxing them to fire-and-forget is a P6 decision and must preserve: a `SetWindowHandle` is in effect before any later `CreateWindowSurface` (one ordered channel suffices), and `ReleaseResources` NEVER relaxes — `MobileGL::Destroy()` walks on the moment it answers (ServerLoop.h's header note). 4. Re-entrancy is unchanged: a post from the apply thread dispatches inline (the `~BackendObject_DirectGLES` → `ReleaseEGLResources` path). This is thread semantics, not wire semantics; under spawn the destructor path runs server-side natively and never posts. ## 3. Error semantics 1. Loop down, backend alive → `Fatal{ApplyThreadNotRunning}` (M-7, unchanged). 2. Loop down, no backend → `MOBILEGL_ERR_NOT_INITIALIZED`, nothing runs on the caller (C2, unchanged). 3. Backend null at dispatch → `MOBILEGL_ERR_NOT_INITIALIZED` with `ok=false`; the forwarder's answer is the old `false`/void-drop, unchanged. 4. Successful void operations (`SetSwapInterval`, `ReleaseSurface`, `ReleaseResources`, `SetWindowHandle`) reply `ok=true` after completing on the apply thread; a null backend replies `ok=false`. The wire therefore preserves the success/failure distinction even though the inproc forwarder returns void. 5. Wire entry (`ServerApplyWireSurfaceOp`, the future P6 control pump's entry point; P5f drives it from tests only): a decode failure is **`Fatal{ProtocolCorruption, "SurfaceOp"}`**, never a dropped frame (a dropped control frame is a client waiting on a reply that never comes); a decoded `WindowKind::AndroidNativeWindow` is **`Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}`** — the window kind is legal schema, the token is the part that means nothing in the server's process, and real window arrival is P12. A decoded `MetalLayer` is similarly **`Fatal{UnmigratedSurface, "MetalLayer@P12"}`**, because its `CAMetalLayer*` also belongs to the client process. Inproc-only kinds (`InitCapabilitiesInprocOnly`, `SwapBuffersInprocOnly`, `InitWindowSurfaceInprocOnly`, `ProbeForTesting`) cannot arrive: the encoder refuses them (`SurfaceWireError::InprocOnlyOpOnTheWire`). 6. A frame that reaches the inproc dispatch with `kind == None` or a kind the switch does not know is `Fatal{ProtocolCorruption, "SurfaceOp.kind"}`; a `windowBackend` tag outside the enum is `Fatal{ProtocolCorruption, "SurfaceOp.windowBackend"}`. Inproc these are corruption shapes, not bad input — same discipline as the ring's header check. ## 4. Reply carriers — and the deliberate non-use of `SurfaceReply.defaultFb` 1. The default framebuffer's shape already crosses as `kEventSurfaceChanged` on SEG_EVENT (P5c ev), and the client drains it at the RPC return (`BackendObject_Remote.cpp`'s create/resize arms). fc rules that `SurfaceReply.defaultFb` **stays unset**: two spellings of one fact is how the two sides come to disagree about it (the `CapsSnapshot` deprecated-fields lesson, `protocol.fbs`). Enabling it later is a schema amendment, not a silent flip. 2. The caps republish side effects are unchanged and are server-spontaneous frames on the wire: a real bind under `MakeCurrent` republishes (R-12 arm (a)); `InitCapabilities` publishes. The client's absorption point — pump the control plane after the RPC returns — is the same in both worlds: "after this op's reply, before the next op" is a total order the client can rely on. ## 5. What fc deliberately did NOT do - No transport: no socket, no spawn pump, no client-minted token table (the frame fields are token-width already, so P6 is transport-only here). - The two dead forwarders (`ServerSwapEGLBuffers`, `ServerInitWindowSurface`) and `ServerInitCapabilities` were NOT given wire ops (f0-egl F8/§4.2): they ride the inproc channel as inproc-only kinds because the function-pointer mailbox is gone entirely, and G14 forbids deleting their pinning tests. - Blocking semantics, the shadow, the doorbell order and the C2 exit block: unchanged.