Files
MobileGL/docs/Disaggregated/notes/p5f/fc-contract-draft.md
T
swung0x48 d5ed139cc5 [MG_Remote, MG_Test] (Disaggregated): P5f fc - the EGL control plane crosses as value frames, not function pointers
The twelve Server* forwarders' one-slot mailbox carried a raw function
pointer plus a void* to a stack-local Args struct (P5c audit row G4);
neither means anything across a process boundary. The slot now carries
one SurfaceControlFrame by value: kind is the op, scalars are the
arguments, and ok/eglMajor/eglMinor come back in the same value. The
blocking handshake, the shadow, the doorbell order and the C2 exit
block are unchanged.

- protocol.fbs: append-only - SurfaceOpKind gains SetSwapInterval=8,
  ReleaseResources=9, SetWindowHandle=10; WindowKind gains MetalLayer=6;
  SurfaceOp gains readSurface and context (the MakeCurrent four-tuple).
  protocol_generated.h regenerated with the pinned flatc.
- Server/SurfaceControlFrame.{h,cpp}: the frame (trivially copyable by
  static_assert) and the op enum - ten wire kinds plus four inproc-only
  kinds (InitCapabilities is answered by CapsSnapshot on the wire,
  SwapBuffers/InitWindowSurface have no production caller - f0-egl F8,
  kept for the inproc test lane).
- Protocol/SurfaceOpCodec.{h,cpp}: the frame <-> SurfaceOp/SurfaceReply
  codec, the explicit WindowBackend <-> WindowKind table, and the wire
  entry ServerApplyWireSurfaceOp whose refusals are named:
  Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} and
  Fatal{ProtocolCorruption, "SurfaceOp"}.
- ServerLoop: RunOnApplyThread(fn, void*) -> RunSurfaceControlFrame(frame)
  plus the dispatch ApplySurfaceControlFrame; the old per-forwarder Args
  bodies moved into the dispatch unchanged (C7/ID-54, N-3, R-12 included).
  RunProbeOnApplyThreadForTesting is the test seam through the same
  channel (the hook lives in a member, never in the slot).
- Tests: RemoteClientTest/ServerLoopTest migrated to the probe seam; new
  SurfaceControlFrameTest suite (mapping tables, wire round-trips, the
  two named refusals) plus ServerLoopTest's AVoidForwarderCrossesAsOne-
  DispatchedFrame (the frame channel's red-once handle) and
  ServerLoopEglTest's TheInitializeDisplayReplyArrivesThroughTheFrame.
2026-09-19 20:30:21 -04:00

6.6 KiB
Raw Blame History

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 static_assert(std::is_trivially_copyable_v<...>) in SurfaceControlFrame.h is the enforcement, not a courtesy.
  2. seq is minted by the channel's poster side (RunSurfaceControlFrame), monotonically from 1 per session, and echoed in the reply. Under spawn the client mints (P6-CONTRACT-DRAFT table 0); the field does not change shape.
  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.4).

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. 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. Inproc-only kinds (InitCapabilitiesInprocOnly, SwapBuffersInprocOnly, InitWindowSurfaceInprocOnly, ProbeForTesting) cannot arrive: the encoder refuses them (SurfaceWireError::InprocOnlyOpOnTheWire).
  5. 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.