Files
MobileGL/docs/Disaggregated/notes/p5/scout-caps-reply-reverse.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.

1. P6's plan and contract draft (neither started; not one line of code)

     docs/Disaggregated/P6-SPAWN-PLAN.md      the package plan
     docs/Disaggregated/P6-CONTRACT-DRAFT.md  the contract draft (moves to
                                              MG_Remote/CONTRACT-P6.md when c6 lands)

   The three findings that mattered, all written into the documents:

   a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
      the tree and already tested: SocketDoorbell including hangup detection and the two
      lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
      passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
      protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
      SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
      assembly plus a control plane plus a process, not "write a socket transport".

   b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
      audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
      the tree again. So the first package, a6, is a read-only audit that writes no code, and
      every contract row that depends on it is marked PENDING a6 rather than decided in
      advance.

   c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
      and protocol.fbs:186-196's own comment says Android transfers the window out of band -
      that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
      the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
      gate measures. A real window reaching the server is a named refusal. This gets its own
      section because "P6 is done" would otherwise read as "the game runs now".

   The contract adds rule G: a role may not name a process-local handle. It extends P5c's
   rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
   EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
   a token this contract defines or they do not cross, and a descriptor crosses only through
   SCM_RIGHTS, where the kernel renumbers it.

   What P5e left for P6 gets its own section. The latch semantics are already designed
   (ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
   client no longer waits while the peer process can DIE: the client holds records that were
   published and will never be applied, and Android's freezer suspends the child without
   closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
   That is P6's only new design; everything else is carriage. It shares a predicate with E1's
   outstanding re-specification (ID-122), and the two should be designed together.

   Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
   match integration-split's name for name, because a differing set means the lane is quietly
   running less; and the arm-proof gate requires every entry to record the child pid and
   transport=spawn in its OWN private log - ID-124 established that this kind of false green
   really happens, and a spawn lane that actually ran monolith would pass every other gate.
   S2's shape must be settled before it is written, and the document says why: E1 is the
   cautionary case.

2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)

   ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
   the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
   is under no version control. A record cited hundreds of times that lives only locally is not
   a record, so it moves in whole, with its directory structure intact.

   Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
   stay where they are: they are evidence DATA rather than documents - large, regenerated per
   run, and mostly carrying absolute paths and device serials that mean something only on the
   machine that produced them. The mapping and this split are written in
   docs/Disaggregated/notes/README.md.

   Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
   document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
   material salvaged from failed workflow runs, which may duplicate the like-named phase
   directories and is kept because a few of those were the only complete version at the time.

   CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
   deliberately outside it: those notes' file:line citations were written against their own
   code heads, and resolving them against today's HEAD would fail wholesale - which is not an
   error, it is their correct state as a historical record. To check one, use the revision it
   declares (--rev).

3. Inconsistencies fixed in passing

   - README.md's status line still said P5e was in progress while the stage table already said
     closed.
   - CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
     (a) and (b) were run in the previous commit; rewritten to closed plus the one item that
     actually remains (E1).
   - ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
     package order, the scope boundary and the two new gates.

Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
2026-09-18 23:31:39 -04:00

662 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P5 scout — the caps snapshot, the reply slot, and the reverse channel
Tree read: `/home/swung/w7/pipe` (WSL), branch `feat/disaggregated` @ `a29807cc` (P4a landed), read over
`//wsl.localhost/Arch/...`. **Nothing was written, built, or run in that tree.** Every line number below was
opened. Repo-relative paths are under `MobileGL/` unless the path starts with `docs/`, `tools/`, `scripts/`
or `~/w7/`.
Background read first: `docs/Disaggregated/ARCHITECTURE.md` §3.3, §4, §8, §10, §11, §13.1, §15.1;
`docs/Disaggregated/ROADMAP.md:21` (the P5 row) and open questions 1-2 (`:80-85`);
`~/w7/notes/p4a/INTEGRATOR-DECISIONS.md` ID-1..57.
**One-sentence summary of the whole subject.** Of the five things P5's row names in my area, exactly one
has a live producer *and* a live consumer today (`OnGpuWritten` / `OnBufferWriteback`, DirectGLES-only);
`MGPCaps`, `MGPReplySlot` and the other eight callbacks are declared-and-asserted **but have zero call
sites in the whole tree** — P5 is their first implementation, not their migration.
---
# PART 1 — `MGPCaps`: the startup snapshot
## 1.1 Where it is defined
| thing | file:line | notes |
|---|---|---|
| `enum MGPCapBit : Uint64` | `MG_Pipe/MGPipeTypes.h:108-124` | 9 bits: `kCapNone`, `kCapViewportArray`, `kCapFloat64VertexAttrib`, `kCapResidentSubData`, `kCapCpuXfbPrimitiveAccounting`, `kCapTimerQuery`, `kCapOcclusionQuery`, `kCapXfbPrimitivesQuery`, `kCapNeedsHostIndexBytes`, `kCapNeedsHostUboBytes`. (That is 8 real caps + `kCapNone`; `kCapNeedsHostUboBytes` is `1<<8`.) |
| `struct MGPCaps` | `MG_Pipe/MGPipeTypes.h:126-139` | four members: `DynamicBackendParameters Dynamic` (`:132`), `Uint64 CallMask` (`:133`), `MGPBlobRef FormatCapabilities` (`:137`), `MGPBlobRef RendererInfo` (`:138`) |
| size assert | `MGPipeTypes.h:140`, `:145-146` | stated as a **composition**, not a literal: `sizeof(MGPCaps) == sizeof(DynamicBackendParameters) + 8 + 24 + 24`, because `DynamicBackendParameters` still carries `SizeT` members and is ABI-dependent until P0.5's fixed-width move (which has **not** happened — see 1.6/risk R1) |
| field list for G4/verify | `MG_Pipe/PipeFields.def:41-42` (`MGP_FIELDS_MGPCaps(F) = F(Dynamic) F(CallMask) F(FormatCapabilities) F(RendererInfo)`), registered in the POD list at `PipeFields.def:338` |
| catalogue row | `MG_Pipe/PipeCalls.def:80` — `X(GetCaps, MGPCaps, kScreen, kReplySlot)`, **opcode 1** (the wire opcode is the 1-based position in `MGP_CALL_LIST`, `PipeCalls.def:29`) |
| generated table slot | `MG_Pipe/generated/PipeTables.inc:18` — `void (*GetCaps)(const MGPCaps* payload, MGPReplySlot* reply);` |
| generated monolith thunk | `MG_Pipe/generated/PipeThunks.inc:20-22` — `gMGPipeScreen.GetCaps(payload, reply)` |
| generated wire record | `MG_Pipe/generated/PipeWire.inc:123-126` |
| generated verify | `MG_Pipe/generated/PipeVerify.inc:53`, `:133`, `:340-341` |
`DynamicBackendParameters` itself is `MG_Backend/BackendObject.h:316-549` (the struct ends at `:549`;
`GpuVendorKind GpuVendor` at `:548` is the last member, `enum class WindowBackend` follows at `:551`).
That is ~90 scalars plus the six per-axis compute limits `MaxComputeWorkGroupCount[3]` /
`MaxComputeWorkGroupSize[3]` and one method pair (`PerLayerFramebufferAttachmentBit` /
`SupportsPerLayerFramebufferAttachment`) — POD-safe, methods are `constexpr`/const.
`MGPipeTypes.h:28` records the deliberate coupling: `MGPCaps` embeds `DynamicBackendParameters`
**by inclusion**, so that a caps field added in `BackendObject.h` needs no second edit.
## 1.2 Who fills it, and when
**Today nobody fills it.** `MGPCaps` has **no producer and no consumer** anywhere:
`gMGPipeScreen` / `gMGPipeContext` are declared at `MG_Pipe/MGPipe.h:137-138` and are read by exactly one
place in the tree, `MG_Test/Pipe/PipeCatalogueTest.cpp:107` and `:111` (which walk them as arrays of
function pointers to assert the table arity). No backend installs either table; the landed P3a/P4a wiring
goes through the free functions `MGPipeApply*` in `MG_Pipe/PipeApply.h` instead, and `PipeApply.h`
declares **no** `MGPipeApplyGetCaps`. (Full list of the 37 `MGPipeApply*` entry points that do exist:
`PipeApply.h:740-1052`; no caps, no read_pixels, no fence, no query.)
What *does* fill the underlying state, and when:
- `BackendObject::InitCapabilities()` is pure virtual at `BackendObject.h:573` and is called **lazily,
from inside `BackendObject::MakeEGLCurrent`** at `MG_Backend/BackendObject.cpp:341-347`, guarded by
`m_backendCapabilitiesInitialized` (`:341`, set at `:346`). It is **not** called from
`MG_Backend::Init()`.
- The latch is cleared by `BackendObject::ResetEGLRuntimeState()` (`BackendObject.cpp:359`, clear at
`:362`), which runs on surface release/teardown from three sites (`:429`, `:461`, `:467`). So
**`InitCapabilities` can run more than once per process** — at most once per surface lifetime, exactly
as `ARCHITECTURE.md` §13.1 says ("surface 生命周期与首次 `MakeCurrent`+`InitCapabilities` 每 surface 至多一次").
- DirectGLES: `BackendObject_DirectGLES::InitCapabilities()` at
`MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:853-874` — `FillInGLESCapabilities` →
`SetGLESCapabilities` → `UpdateAdvertisedCapabilityExtensions` → `UpdateDynamicBackendParameters` →
`PopulateFormatCapabilities` → `PrintFormatCapabilities`.
- DirectVulkan: `BackendObject_DirectVulkan::InitCapabilities()` at
`MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:371-395` — `QueryVulkanCapabilities` →
`UpdateDynamicBackendParameters` → `UpdateAdvertisedExtensions` → **`MGB_CTX->InvalidateCompileEnv()`
(`:390`, guarded by `MGB_CTX_LIVE`)** → `PopulateFormatCapabilities`.
**The ordering trap P5 must design around** (`BackendObject_DirectGLES.cpp:780-792`, verbatim in the
comment): `LogBackendInfo()` (`MG_Backend/Init.cpp:15`) reads
`pActiveBackendObject->GetRendererInfo()` at `MG_Backend/Init.cpp:21` **during `MG_Backend::Init()`**
(`Init.cpp:48`, `LogBackendInfo()` called at `:69`) — i.e. **before any ES/Vulkan context exists and
before `InitCapabilities` has run**. Both backends therefore build a *provisional* extension list in
their static initializer and reconcile it at the end of `InitCapabilities`
(`UpdateAdvertisedCapabilityExtensions`, `BackendObject_DirectGLES.cpp:793-799`;
`BackendObject_DirectVulkan::UpdateAdvertisedExtensions`, `:793`). Under split, `MG_Backend::Init()` is
the single hook where `BackendObject_Remote` is installed (`MG_Backend/Init.cpp:48-70`), and
`LogBackendInfo()` will call `GetRendererInfo()` on it *before the server has a surface*. Two readings,
and the brief must pick one:
(a) `BackendObject_Remote::GetRendererInfo()` returns an empty/placeholder `RendererInfo` until the
first `CapsSnapshot` arrives, and P5 accepts one wrong log line at startup; or
(b) `MG_Backend::Init()` is restructured so `LogBackendInfo()` is deferred to first-MakeCurrent.
**Evidence that would settle it:** whether anything other than `MGLOG_I` reads `GetRendererInfo()`
before the first `eglMakeCurrent`. I checked: in non-test code the only pre-MakeCurrent reader is
`LogBackendInfo` (`Init.cpp:21`); every other reader is behind a GL entry point (list in 1.3), so (a)
is cheap. **Guess:** (a) is right, but I did not trace the FCL/launcher startup order.
## 1.3 Who reads caps on the client today — the exact enumeration
Counts are of **non-test, non-`MG_Backend`** sites (`MG_Impl` + `MG_State` + `MG_Util` + `Init.cpp`):
| accessor | sites | where |
|---|---|---|
| `GetDynamicParameters()` | **40** | `GLImpl/Getter/GL_Getter.cpp` 19 · `GLImpl/Texture/GL_Texture.cpp` 6 · `GLImpl/Framebuffer/GL_Framebuffer.cpp` 4 · `GLImpl/Program/GL_Program.cpp` 4 · `GLImpl/{Buffer,Framebuffer,Texture,VertexArray}/Validators.cpp` 1 each · `GLImpl/VertexArray/GL_VertexArray.cpp` 1 · `MG_Util/ShaderTranspiler/CompileEnv.cpp` 1 · `MG_Util/ShaderTranspiler/ShaderCompiler.cpp` 1 |
| `GetRendererInfo()` | **7** | `GL_Getter.cpp:596`, `:654`, `:2400` · `GL_Program.cpp:1424` · `GL_Query.cpp:77` · `GL_Texture.cpp:5903` · `CompileEnv.cpp:124` |
| `GetFormatCapabilities()` | **4** | `GL_Framebuffer.cpp:127`, `:784` · `GL_Texture.cpp:570`, `:6855` |
| `GetBackendType()` | **3** | `GL_Framebuffer.cpp:47` · `GL_Texture.cpp:6536` · `CompileEnv.cpp:122` |
| `GetBackendAPIVersionString()` | **2** | `GL_Getter.cpp` 1 · `MG_Util/SelfTest/DriverPost.cpp` 1 (the POST probe — **server-side** under split, `ARCHITECTURE.md` open question 7) |
That is **56 client caps read points** (the doc's "40 `pActiveBackendObject->` 站点 + 89 caps 读点" in
`ARCHITECTURE.md:116` counts differently — it counts *field* reads inside those sites, e.g. `GL_Getter.cpp:867`
binds one `dynamicParameters` reference and then reads many members. Do not use 40+89 as a work estimate;
**40 is the number of `GetDynamicParameters()` call expressions**, and it coincides with the doc's 40 by
accident of arithmetic. Guess: the doc's 40 means the `pActiveBackendObject->` *expressions* and the 89
means individual field reads; I did not count the 89.)
Two hot call sites are worth naming because they bind a reference and read many fields at once:
`GL_Getter.cpp:2400-2401` (`glGetFloatv`/`glGetIntegerv` scalar family; binds **both** `rendererInfo`
and `dynamicParameters`) and `MG_Util/ShaderTranspiler/CompileEnv.cpp:120-124`
(`env->backend = GetBackendType()`, `env->params = GetDynamicParameters()` — a **whole-struct copy** —
and `env->advertisedExtensions = GetRendererInfo().RendererGLInfo.Extensions`).
## 1.4 What MUST be in the snapshot (a split client cannot call the driver)
Everything below is client-consumed and device-answered. I list the carrier that already exists.
1. **`DynamicBackendParameters` (whole struct, by value).** `MGPCaps::Dynamic`
(`MGPipeTypes.h:132`). Consumers: the 40 sites above. Non-negotiable — `CompileEnv.cpp:123` copies
the whole struct into the compile env, so a partial snapshot is not an option.
2. **`RendererInfo`** = `RendererName`, `BackendName`, `Optional<String> ExtraVendor`,
`GLInfo RendererGLInfo{TargetGLVersion, TargetGLSLVersion, Vector<GLExtension> Extensions,
IsCompatibilityProfile}`, `StaticBackendCapabilityT{AllowVSOnlyPrograms}`
(`MG_Util/Types.h:317`, `:321`, `:328-334`). Not POD (three `String`s + a `Vector`) → travels as the
`MGPCaps::RendererInfo` blob (`MGPipeTypes.h:138`). Consumers: `glGetString` (`GL_Getter.cpp:596`),
`glGetStringi(GL_EXTENSIONS)` (`GL_Getter.cpp:654`), `glGetIntegerv` scalar family (`:2400`),
`AllowVSOnlyPrograms` at link (`GL_Program.cpp:1424`), timer-query gating in
`glGenQueries`/`glQueryCounter` (`GL_Query.cpp:77`), anisotropy/`GL_EXT_texture_filter_anisotropic`
in `glTexParameter` (`GL_Texture.cpp:5903`), and the **compile env's advertised extension list**
(`CompileEnv.cpp:124`) — which feeds glslang phase A/B, which stays on the client
(`ARCHITECTURE.md` open question 7).
3. **`FormatCapabilityCache`** (`BackendObject.h:93-98`) = `FullCaps` + `CaveatCaps`
(`FormatCapabilityTable`, `BackendObject.h:88`) + `SampleCounts` (`FormatSampleCountTable`,
`BackendObject.h:90` — a `Vector<Int>` per (target, format) pair, which is why
`MGPipeTypes.h:134-136` says it must be a blob). Dimensions:
`kFormatCapabilityTargetCount = TextureTargetCount + 1` (`BackendObject.h:81-83`) ×
`kFormatCapabilityFormatCount = TextureInternalFormatCount` (`:84`). Consumers:
`GL_Framebuffer.cpp:127` (renderable-format check in `CheckCompleteness`'s helper),
`GL_Framebuffer.cpp:784` and `GL_Texture.cpp:570` (`GL_SAMPLES`/`GL_NUM_SAMPLE_COUNTS` answers),
`GL_Texture.cpp:6855`. **Serialization shape is P5's to invent** — `MGPipeTypes.h:136` says in as many
words "Their serializers land with the transport (P5)".
4. **`BackendType`** (3 sites). Already implied by `Hello.backendType` (`protocol.fbs:57-64`) but the
*client* reads it through `pActiveBackendObject->GetBackendType()`; `BackendObject_Remote` must answer
it locally. `GL_Texture.cpp:6536` uses it to pick the `GetTextureImage` path, `GL_Framebuffer.cpp:47`
to pick the distinct-depth-stencil path, `CompileEnv.cpp:122` to key the compile env.
5. **`GetBackendAPIVersionString()`** (1 client site in `GL_Getter.cpp`). Already in the control-plane
schema as `CapsSnapshot.apiVersion` (`protocol.fbs:91`).
6. **`CallMask`** (`MGPipeTypes.h:133`) — replaces "is this table slot null" as the feature probe
(`ARCHITECTURE.md:114`). The schema already reserves `CapsSnapshot.tableSlotMask: ulong`
(`protocol.fbs:94`) with the comment "which GLFunctionsTable slots the peer registered".
**Note the mismatch:** `CallMask` is documented as `MGPCapBit` bits (8 of them), but `tableSlotMask`
is documented as *table slots*. Two readings: (i) they are the same field and the schema comment is
stale; (ii) they are two different masks and both are needed (cap bits + registered-slot bitmap).
**Evidence that would settle it:** `GL_Drawing.cpp:283`'s live use of "slot non-null" as a capability
probe, and `ARCHITECTURE.md:114`'s sentence "`CallMask` 取代『槽位是否为 null』这个隐式能力探测" — which
reads as (i). **Guess: (i)**, and the brief should say so explicitly, because 71 catalogue rows do not
fit in the 8 cap bits and someone will otherwise build both.
7. **The six compute limits** ride inside `Dynamic` (`BackendObject.h:381-392`); the schema *also* carries
`maxComputeWorkGroupCount`/`Size` as separate `[int]` arrays (`protocol.fbs:92-93`) — redundant with
(1), and `AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer`
(`MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp:580-623`, harness
`MG_IntegrationTest/Harness/BackendCapsPeek.{h,cpp}`) is the existing gate that pins the caps copy
and `glGetIntegeri_v` to one number. **Reuse it as P5's caps gate** — it already runs on every lane.
8. **`prefersCpuXfbPrimitiveAccounting`** (`protocol.fbs:95`) — the schema's spelling of
`kCapCpuXfbPrimitiveAccounting`; same redundancy question as (6).
**Already present in the control plane (do not re-invent):** `protocol.fbs:86-96`
`table CapsSnapshot { dynamicParameters: [ubyte]; rendererInfo: [ubyte]; formatCaps: [ubyte];
extensions: [string]; apiVersion: string; maxComputeWorkGroupCount: [int]; maxComputeWorkGroupSize: [int];
tableSlotMask: ulong; prefersCpuXfbPrimitiveAccounting: bool; }`, with the header comment at
`protocol.fbs:83-85` ("The three blobs are byte-for-byte images of the corresponding POD structs; they are
versioned by structSize-first discipline, not by this schema"). It is a `CtrlMsg` union member
(`protocol.fbs:220`) — i.e. **the snapshot travels on the CTRL socket, not in `SEG_CMD`**, and the
`GetCaps` catalogue row (`PipeCalls.def:80`) is the *in-process/monolith* spelling of the same answer.
`protocol.fbs:98-104` also already has `table DefaultFramebufferInfo` and `SurfaceReply.defaultFb`
(`:142-148`) — that is the `OnSurfaceChanged` payload's control-plane twin.
## 1.5 What a caps snapshot CANNOT answer
1. **Caps *invalidation* is an event, not a value.** `GLContext::GetCompileEnv()`
(`MG_State/GLState/Core.cpp:33-43`) memoises the compile env and re-captures it when
**the raw pointer `MG_Backend::pActiveBackendObject.get()` changes** (`Core.cpp:34`,
`m_compileEnvBackend`). Under split that pointer is the client's single long-lived
`BackendObject_Remote` and **never changes**, so a server-side `InitCapabilities` re-run (surface
recreate, `ResetEGLRuntimeState` → next `MakeEGLCurrent`) would leave the client's compile env, its
preprocess memos and its advertised-extension list stale with nothing saying so. This is exactly what
`OnCapsInvalidated` is for (`MG_Pipe/MGPipeCallbacks.h:45`), and its two producers today are
`BackendObject_DirectVulkan.cpp:390` and `:788`. **DirectGLES has zero producers** — it re-runs
`UpdateAdvertisedCapabilityExtensions` + `UpdateDynamicBackendParameters` at
`BackendObject_DirectGLES.cpp:865-871` and tells the frontend nothing, because in monolith the
frontend reads the same object. **Under split that silence becomes a bug.** P5 either (a) ships the
caps snapshot again on every `InitCapabilities` and treats re-arrival as invalidation, or (b) adds a
DirectGLES `InvalidateCompileEnv` call to match DirectVulkan. **(a) is the smaller diff and does not
touch `dev`-shaped backend code** — recommend (a).
2. **Default-framebuffer geometry is not caps.** `pDefaultFramebufferInfo` is written by the *backends*
into `MG_Impl` (`DirectGLES.cpp:11467` `PublishDefaultFramebufferDepthStencilFormat`;
`DirectVulkan/Renderer/SwapchainObject.cpp:276-295`, which even calls
`TextureObject2D::AllocateStorage` on a frontend object). It changes per surface/resize, so it is
`OnSurfaceChanged` (`MGPipeCallbacks.h:43`, payload `MGPSurfaceInfo` at `MGPipeTypes.h:1321-1328`) /
`SurfaceReply.defaultFb` — never the snapshot. Note `kMGPipeDefaultFramebuffer = {0,1}`
(`MG_Pipe/MGPipeHandles.h:73`) already exists to retire the four
`pDefaultFramebufferInfo->defaultFBO` identity compares in DirectGLES (`DirectGLES.cpp:2846`,
`:3137`, `:4370`, `:4414`).
3. **Per-format sample counts have a second, non-caps source.**
`ClampSamplesToBackendSupport` (`BackendObject_DirectGLES.cpp:806-828`) falls back to
`GetGLESFormatMaxSamples(g_GLESCapabilities, ...)` when the probed `SampleCounts` vector is empty
(`:820-826`). That fallback reads a **backend-private** capabilities struct. It is a backend-side
function so it stays server-side, but if any client path ever needs the clamped answer it cannot get
it from `MGPCaps` alone.
4. **Live GL state dressed as a "cap".** `GL_Getter.cpp:531-545` floors backend limits against frontend
constants, and `GL_Getter.cpp:267-269` only consults the backend when the *currently bound* buffer
target is `ShaderStorage`. These are fine (the flooring is client-side arithmetic over snapshot
values) — flagged only so a reviewer does not mistake them for driver round trips.
5. **`GetBackendFunctions()`** (`BackendObject.h:592`) has **zero** client-side call sites outside
`MG_Backend` (verified). The client's copy `gBackendFunctionsTable` is assigned once at
`MG_Backend/Init.cpp:44`; under split it must be either a table of emitters or all-null with
`CallMask` answering instead.
## 1.6 Risks for the brief
- **R1 (real, blocks the wire).** `MGPCaps`'s size assert is a *composition* precisely because
`DynamicBackendParameters` contains `SizeT` (`BackendObject.h:317`, `:321`, `:546`) and `GLenum`
(`:445-446`). `ARCHITECTURE.md` said P0.5 would move the caps block into `MGPipeValueTypes.h` with
fixed-width members; **that has not happened** (`MGPipeTypes.h:141-144` still says "until P0.5 moves…").
A byte-for-byte blob of a struct with `SizeT` members is not portable client↔server if the two ever
differ in word size. For `inproc` (P5) this is harmless; for `spawn` (P6) on the same machine it is
also harmless. **Recommend: P5 asserts same-`sizeof` in the `Hello`/`Welcome` handshake
(`protocol.fbs:57-75`, `abiMajor/abiMinor/buildFingerprint`) and defers the fixed-width rewrite.**
- **R2.** `MGPCaps` is `kScreen` + `kReplySlot` in the catalogue but the real snapshot is a CTRL-socket
`CapsSnapshot` message. Two mechanisms for one answer; the brief should state which one `inproc` uses
(**guess:** the CTRL one, because `InProcessTransport` must run the *same* G3 codec as spawn and the
handshake is the same code path).
---
# PART 2 — `kReplySlot` and `MGPReplySlot`
## 2.1 The flag and the type
- `kReplySlot = 1u << 4` (`MG_Pipe/MGPipe.h:50`), documented as **"Answers into an `MGPReplySlot`; never
blocks."** The legend in the catalogue header is `PipeCalls.def:18`.
- `struct MGPReplySlot { Uint64 Id; }`, `MGP_ASSERT_POD(MGPReplySlot, 8)` —
`MG_Pipe/MGPipeTypes.h:76-81`. The comment at `:76-77`: *"Where an asynchronous answer lands. Every
server query in this catalogue is async-with-handle; none of them blocks (section 4.4.6, 'the total
rule')."* Field list `PipeFields.def:32-33`.
- It is **8 bytes and one opaque `Id`** — there is no status, no size, no segment. Everything about where
the bytes are is P5's to define. The obvious home is `SEG_REPLY` (server-owned, 8 MiB, 4 KiB slots —
`ARCHITECTURE.md` §11.1; `SegmentKind.Reply = 3` at `protocol.fbs:40`; `Welcome.replyPool` at
`protocol.fbs:73`).
## 2.2 The ten calls that carry it
All from `PipeCalls.def`; opcode = 1-based position in `MGP_CALL_LIST` starting at `:80`.
| op | call | payload | class | flags | line |
|---|---|---|---|---|---|
| 1 | `GetCaps` | `MGPCaps` | `kScreen` | `kReplySlot` | `:80` |
| 5 | `MapPersistent` | `MGPHandleOnly` | `kScreen` | `kReplySlot\|kOptional` | `:84` |
| 8 | `FenceStatus` | `MGPHandleOnly` | `kScreen` | `kReplySlot` | `:87` |
| 9 | `FenceWait` | `MGPFenceWait` | `kScreen` | `kReplySlot` | `:88` |
| 15 | `QueryAvailable` | `MGPHandleOnly` | `kCtxQuery` | `kReplySlot` | `:94` |
| 16 | `QueryResult` | `MGPQueryResultRequest` | `kCtxQuery` | `kReplySlot` | `:95` |
| 55 | `ResourceReadback` | `MGPReadback` | `kCtxObject` | `kReplySlot` | `:138` |
| 58 | `GetTextureImage` | `MGPReadbackInfo` | `kCtxObject` | `kReplySlot` | `:141` |
| 61 | `ReadPixels` | `MGPReadbackInfo` | `kCtxVerb` | `kReplySlot` | `:145` |
| 69 | `QueryTimestamp` | `MGPTimestampRequest` | `kCtxQuery` | `kReplySlot` | `:160` |
(I derived the opcodes by position; the brief should re-derive rather than trust me on the exact
numbers — the *set* of ten rows is what matters and is exact.)
Generated artefacts for each: `PipeTables.inc:18,22,25,26,37,38,75,78,81,92`;
`PipeThunks.inc:20,36,48,52,72,76,224,236,248,292`.
## 2.3 How the answer gets back in monolith today — **it does not use the reply slot at all**
`MGPReplySlot` has **zero producers and zero consumers** outside the generated tables and the field-list
macro. The two reply-slot calls that are actually implemented today return their answers as **ordinary
C++ return values from the free-function applier**:
- `void* MGPipeApplyMapPersistent(const MGPHandleOnly& handle, Uint64 size, const void* seedBytes);`
— `PipeApply.h:917`, defined `PipeApply.cpp:1893-1921`. Returns the coherent host pointer, **or
`nullptr` for a DECLINE, which is a real answer** (`PipeApply.cpp:1913-1919`, and the same sentence at
`PipeApply.h:914-916`: *"a real answer and the reason the call is `kOptional` as well as
`kReplySlot`"*). Client caller: `MGPipeEmitMapPersistent`, `MG_Impl/Pipe/PipeFill.cpp:772-789`
(returns the pointer straight up the stack).
- `void MGPipeApplyResourceReadback(const MGPReadback& record);` — `PipeApply.h:908`. Returns nothing;
**the bytes come back through the reverse channel instead**, via `OnBufferWriteback`
(`PipeApply.h:905-907`, and the producer at `MG_Backend/DirectGLES/Managers.cpp:2135-2139`). The
client emitter `MGPipeEmitResourceReadback` (`PipeFill.cpp:754-768`) says so at `:764-766`.
The other eight reply-slot calls (`GetCaps`, `FenceStatus`, `FenceWait`, `QueryAvailable`,
`QueryResult`, `GetTextureImage`, `ReadPixels`, `QueryTimestamp`) have **no applier at all** — they are
still served by `MG_Backend::gBackendFunctionsTable.GL.*` directly from `MG_Impl`. Example:
`ReadPixels_Backend` at `MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp:3093-3096` is
`MGP_FILL(ReadPixels); gBackendFunctionsTable.GL.ReadPixels(...)` — a synchronous call with an out
pointer into the application's memory.
**Consequence for the brief.** P5 is the first phase that has to give `MGPReplySlot` a meaning, and it
has to answer three questions that nothing in the tree answers today:
1. **Who allocates the `Id`?** The slot pool is server-owned (`SegmentKind.Reply`), but the *record*
carrying the request is client-produced. Most likely the client mints a monotonic `Id` and the server
writes into `SEG_REPLY[Id % slots]`; nothing in the tree says so. **Guess.**
2. **How does the client know the answer landed?** `ARCHITECTURE.md` §11.7 names `EvReadbackDone`,
`EvQueryResult`, `EvFenceSignaled` as `SEG_EVENT` records — i.e. the reply slot carries the *payload*
and the event ring carries the *completion*. No code exists for either.
3. **`MapPersistent`'s decline** must remain expressible: the answer is a pointer **or `nullptr`**, and
`nullptr` means "declined", not "failed" (`PipeApply.cpp:1913-1916`). A reply-slot encoding that
cannot distinguish "not yet" from "declined" will hang `MGPipeEmitMapPersistent`, which returns its
value synchronously into `BufferObject` storage definition. **`MapPersistent` is the one reply-slot
call whose client caller genuinely cannot proceed without the answer**, which is why
`ARCHITECTURE.md` §13.1 lists `MapPersistent` under "不可避免" (T1 tier, once per storage definition)
and why `MOBILEGL_IPC_ADOPT_TIER=2` (decline) is the permanent correct fallback (§12).
**Also note:** `kNeedsAck` is a *different* mechanism from `kReplySlot`. Only `ResourceRespecify`
carries `kNeedsAck` (`PipeCalls.def:82`), and only when the per-record predicate
`MGPipeResourceRespecifyNeedsAck(desc)` says so — `MG_Pipe/MGPipeTypes.h:680`, pinned by
`MG_Test/Pipe/PipeCatalogueTest.cpp:552-585`
(`ResourceRespecifyAcksOnlyImmutableStorage`). `ARCHITECTURE.md:298` says in as many words: **"monolith
下 ack 是 `((void)0)`… P5 把门铃接到这个谓词上"**, and P4a narrowed the predicate to *buffer* targets
(D-A2). So P5 owns: the doorbell on that predicate, and the reply-slot mechanism, and they are separate.
---
# PART 3 — Blocking `read_pixels`
## 3.1 The current path, end to end
```
glReadPixels (MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp:3099)
└─ ReadPixels_State(...) :2924-3091 pure frontend validation
└─ ReadPixels_Backend(...) :3093-3096
├─ MGP_FILL(ReadPixels) :3094 fills the kReadback class (18 fields)
└─ gBackendFunctionsTable.GL.ReadPixels :3095 -> the backend, synchronous
```
`glReadnPixels` is the same two calls with a size bound (`GL_Framebuffer.cpp:3135-3152`; validation at
`:3143`, backend at `:3151`).
The DirectGLES implementation is `DirectGLES.cpp:10851-11000`:
- `:10858-10859` picks native vs. converted readback;
- `:10926-10929` depth/stencil special paths;
- **`:10936-10937`** `MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack).GetBoundObject()` — the PACK
PBO, read *from the context* inside the backend;
- `:10952` `ScopedPixelPackBuffer packBufferBinding(packBufferId)` — the driver binding is scoped so it
returns to 0 on every exit;
- `:10955` `glReadPixels`;
- **`:10982-10997` — the blocking half.** When a PBO was used, the backend immediately
`glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, size, GL_MAP_READ_BIT)` (`:10985-10986`),
`pixelPackBufferObject->WritebackFromBackend({pboMappedPtr, size}, 0)` (`:10989`),
`BufferImpl::BumpBufferMutationEpoch()` (`:10991`), `glUnmapBuffer` (`:10993`).
There are **five** `MGB_CTX->GetBufferBindingSlot(BufferTarget::PixelPack)` reads in DirectGLES:
`:9407` (inside `StoreReadbackRowsToClient`, `DirectGLES.cpp:9397` — the **per-scanline** writeback loop,
`:9418`), `:10389` (`ReadPixelsViaFormatConversion`, `:10371`), `:10626`
(`GetTexImageViaShadowConversion`, `:10604`), `:10936` (`ReadPixels`), `:11362` (`GetTextureImage`).
DirectVulkan has its own at `Renderer/VulkanRenderer.cpp:10988-11004` (`ReadDepthStencilPixels`, also a
per-row `WritebackFromBackend` loop at `:11000`). DirectGLES `Utils.cpp:2343` is a sixth per-row
writeback.
`MGPReadbackInfo` (`MGPipeTypes.h:1197-1205`, 64 B) is the payload already minted for this:
`MGPipeHandle Res` — **`:1198` says "null for read_pixels: the bound read surface answers"** — plus
`MGPBox Box`, `Format`, `Type`, `Target`, `Level`, `DstOffset`, `DstSize`.
## 3.2 What the split version must block on
**It must block on exactly one thing: the pixels, and only when the destination is client memory.**
`ARCHITECTURE.md` §13.1 puts it in the "不可避免" list in those words: *"`glReadPixels` → 客户内存(像素进
`SEG_REPLY`,逐行写回循环留在 server 内按操作级批成一段)"*.
Concretely, the blocking version is:
1. publish the command stream up to and including the `ReadPixels` record (doorbell — `ARCHITECTURE.md`
§11.6 lists `kNeedsAck` and the polling entry points as explicit doorbell points; a blocking readback
is the same shape);
2. wait on the reply slot / `EvReadbackDone`;
3. copy the pixels out of `SEG_REPLY` into the application's pointer.
Two things it must **NOT** block on, both stated in `ARCHITECTURE.md` §13.1's zero-round-trip list:
- **`glReadPixels` into a PACK PBO must NOT block.** It becomes fire-and-forget: emit the record, and
**the client marks the PBO GPU-written itself** ("`glReadPixels` → pack PBO(fire-and-forget +
client 侧 `MarkGpuWritten`,严格优于 monolith 的无条件停等)"). This is the *only* place in P5 where the
split path is strictly **better** than monolith — today `DirectGLES.cpp:10982-10997` stalls on
`glMapBufferRange(READ)` for every PBO readback, and the whole point of a PBO is not to. The
client-side conservative `MarkGpuWritten` P5's row names is exactly this: the existing client callback
`MGPipeClientOnGpuWritten` (`MG_Impl/Pipe/ResourceTracker.h:579-603`) already sets
`m_gpuWritePending` + `m_hasDefinedContent` via `BufferObject::MarkGpuWritten()`
(`MG_State/GLState/BufferState/BufferObject.cpp:364`), and the client just has to *call it itself*
at the emission point instead of waiting for the server to announce. The contract note at
`Managers.cpp:2533-2538` is the one to obey: **both** flags, or the next `ResourceRespecify` carries
`HasDefinedContent = 0` and the shader-written contents are dropped silently.
- **The per-scanline writeback loop must NOT become per-scanline IPC.** `MGPipeCallbacks.h:32`'s
`OnBufferWriteback` comment and `ARCHITECTURE.md` §8.1 both say it: *"按操作级批处理(今天两处逐行循环绝不能
变成每扫描线一次 IPC)"*. The two loops are `DirectGLES.cpp:9397-9424`
(`StoreReadbackRowsToClient`, writeback at `:9418`) and `Utils.cpp:2337-2351`; DirectVulkan's third is
`VulkanRenderer.cpp:10996-11004`. Under split those loops stay **inside the server** and produce one
`SEG_REPLY` region.
- It must also **not** block on `glGetError` (always local), on `glFinish`/`glFlush` (no-ops), or on the
framebuffer-completeness check — `ReadPixels_State` (`GL_Framebuffer.cpp:2924-3091`) is 100% frontend
and answers locally, including `CheckCompleteness()` (`:2963`).
## 3.3 The seam that will bite
`MGP_FILL(ReadPixels)` maps to fill class `kReadback` (`MG_Pipe/FillPoints.def:115`), whose field set is
`FillPoints.def:293-313` — **18 fields**, and the first three are
`GetPixelStoreParameters`, **`GetBufferBindingSlot`**, `GetFramebufferBindingSlot`. `GetBufferBindingSlot`
returns `BindingSlot<BufferObject>&` (`MG_Backend/MGPipe/PipeInputs.h:485-492`), i.e. **a pointer into
client memory** — `PipeFill.cpp:98-105` fills it as `dst.m_bufferBindingSlot[target] =
&ctx.GetBufferBindingSlot(target)`. That is fine in `inproc` (one address space) and **impossible** in
`spawn`. Its declared coverage owner is `SetIndirectBuffers` (`MG_Pipe/Coverage.def:70`,
`generated/PipeCoverage.inc:40`), and `Coverage.def:184` notes it is polymorphic over a target set P3a
only partly covers.
So: **P5's `inproc` milestone can ship with the backend still reading the PACK PBO out of `PipeInputs`;
P6's `spawn` cannot.** Two readings of the P5 charter: (i) P5 only has to make `inproc` work, so the PBO
binding can stay a `PipeInputs` pointer read and `MGPReadbackInfo.Res`/`DstOffset`/`DstSize` stay unused;
(ii) P5 should already route the PBO destination through `MGPReadbackInfo.Res` + `DstOffset` because
`MGPReadbackInfo:1198` was minted for it and P6 is only 5 days later. **Evidence that would settle it:**
whether `DirectGLES.Split.ClearThenReadPixels` (the P5 gate) uses a PBO — it does **not** (see 3.4), so
(i) passes the gate. **Recommendation: (ii) anyway**, because the P5 exit gate explicitly includes
"未迁移字段读 = `Fatal{UnmigratedPipeInput}`", and under `MOBILEGL_BUILD_DISAGGREGATED` the poison is
armed (`MG_Backend/MGPipe/PipeInputs.h:21-26`) — a split build that reads a client pointer through
`PipeInputs` is precisely what that gate exists to catch, and leaving it for P6 means P5's own gate is
only half-armed.
## 3.4 The P5 gate targets, as they exist today
`DirectGLES.Split.ClearThenReadPixels` — the scenario file exists:
`MG_IntegrationTest/Scenarios/ClearThenReadPixelsScenario.cpp`, five cases at `:80`, `:143`, `:191`,
`:241`, `:308`. All five read the **default framebuffer into client memory** (no PBO). It is registered
in `MG_IntegrationTest/CMakeLists.txt:76`; the `TEST_PREFIX` mechanism that makes lane names
(`DirectGLES.`, `DirectGLES.AsyncOn.`, `DirectGLES.HandleRecycle.Handles.`, …) is at
`CMakeLists.txt:739`, `:868`, `:1157` etc. — **P5 adds a `DirectGLES.Split.` prefixed lane the same way.**
`DirectGLES.Split.Triangle` — **there is no Triangle scenario in the tree.** Full scenario list (85
files) checked; nothing matching `Triangle`. P5 must write it.
`PersistentCoherentMapScenario` — **does not exist either.** The nearest existing relatives are
`StorageBufferRegrowScenario.cpp` (`:255`,
`NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw`) and `LargeArenaAdoptionScenario.cpp`
(`:450`, `AnAdoptionCostsExactlyOneMapPersistentRoundtrip`) — the two `map-persistent-roundtrips` gates
(`ARCHITECTURE.md` §12). P5 must write `PersistentCoherentMapScenario` from the §12 recipe (map
`PERSISTENT|WRITE|COHERENT`, write, **no other GL call**, draw, readback-verify).
`MOBILEGL_TRANSPORT` — **no parsing exists.** `grep` over `Config.h`, `MG_Util/` finds no `Transport`
and no `MOBILEGL_IPC_*`; the `MOBILEGL_PIPE_*` knobs are `Config.h:320-415`. `ARCHITECTURE.md` §15.1
requires `MG_Config::Transport` to exist and to be force-set to `Monolith` in `mobilegl_server_main`.
`add_trace_replay_test` is `tools/trace_replay/CMakeLists.txt:311`, with
`add_trace_replay_test_for_backends` at `:384-387`. There is no `SPLIT` argument and no
`TRACE_TRANSPORT`; P5 adds both.
---
# PART 4 — `MGPipeCallbacks`: ten reverse callbacks + one forward terminator
## 4.1 The table
`MG_Pipe/MGPipeCallbacks.h:27-52`. `kMGPipeCallbackCount = 10` at `:56`, asserted at `:57-58` ("an
eleventh cannot be added without touching the transport's reverse-channel record table"), pinned by
`MG_Test/Pipe/PipeCatalogueTest.cpp:684`. The single global instance `gMGPipeCallbacks` is
null-initialised at `MGPipeCallbacks.h:62`.
| # | callback | decl | producers today | consumers today | needed by P5's reduced path? |
|---|---|---|---|---|---|
| 1 | `OnGlError(code)` | `:29` | **none** (the 6 sites still call `MGB_CTX->RecordError`, forwarded) | none | **no** for Clear/Triangle/ReadPixels; **yes** if the gate asserts `glGetError()==0` |
| 2 | `OnGpuWritten(res, rangeCount, ranges[])` | `:31` | `MG_Backend/DirectGLES/Managers.cpp:2516-2539` (`MarkBufferGpuWritten`, push arm only) | `MGPipeClientOnGpuWritten`, `MG_Impl/Pipe/ResourceTracker.h:579-603` | **yes** — and P5 *narrows* it (client-side conservative set) |
| 3 | `OnBufferWriteback(res, offset, bytes)` | `:32` | `Managers.cpp:2135-2139` (`Ops_H_Readback`) | `MGPipeClientOnBufferWriteback`, `ResourceTracker.h:553-572` | **yes** for `resource_readback`; **not** for the two gate scenarios |
| 4 | `OnTextureWriteback(res, box, bytes)` | `:33` | **none** | none | no (CPU-fallback mip generation only) |
| 5 | `OnTexturePullRequest(res, target, firstLevel, levelCount, pullSerial)` | `:35-36` | **none** — the site is only *named*, `Managers.cpp:5334` raises `MGPipeUnmigratedEmulation("texture-remint-pull")` | none | **no** — see Part 5 |
| 6 | `OnMipLevelsGenerated(res, base, count)` | `:39` | **none** (`DirectGLES.cpp:8051` raises `MGPipeUnmigratedEmulation("generate-mipmap-storage")` instead) | none | no |
| 7 | `OnSurfaceChanged(info)` | `:43` | **none** — the layering inversion is still live: `DirectGLES.cpp:11467` and `SwapchainObject.cpp:276-295` write `MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo` directly | none | **yes** — nothing renders without a default FB |
| 8 | `OnCapsInvalidated()` | `:45` | **none** — `BackendObject_DirectVulkan.cpp:390`, `:788` call `MGB_CTX->InvalidateCompileEnv()` instead | none | **probably yes** on DirectVulkan; see 1.5(1) |
| 9 | `OnLog(level, text)` | `:47` | **none** | none | **yes if the server is a second process** (P6); for `inproc` the logger is shared, so **no** |
| 10 | `OnXfbScatterReady(scratch, packedStride, vertices)` | `:50` | **none** | none | no |
| — | `ResourceSubDataComplete` (forward terminator, **not** in this struct) | `PipeCalls.def:136`, payload `MGPSubDataComplete` `MGPipeTypes.h:1135-1140` (24 B) | **none** — `PipeCatalogueTest.cpp:953` says in as many words that the terminator is a later phase's and "P4a must not build half a terminator" | none | no |
So: **2 of 10 wired, 8 stubs.** Installation is `MGPipeInstallClientResourceCallbacks()`,
`ResourceTracker.h:605-613`, which installs #2 and #3 **only if the entry is still null** — the comment
at `:605-607` states the ownership rule: *"the backend installs the rest of the table and these two
answer for it."* Under split that rule inverts (the server has no client pointers at all), and the brief
should say so explicitly.
## 4.2 What P5's reduced path actually needs — and the ordering rule
`ARCHITECTURE.md` §8.2 is the one non-negotiable: **the reverse channel needs the same ordering guarantee
as the forward one.** The concrete instance is spelled out at `Managers.cpp:2120-2136`: writeback →
unmap → serial stamp → **epoch bump strictly last** ("The bump must NEVER move earlier"). The
mirror-image note on the client side is `ResourceTracker.h:550-552`.
For P5's milestone ("first inproc IPC frame, reduced path") the minimum is **#2, #3, #7**, plus **#1** if
the gate checks `glGetError`, plus **#8** on the DirectVulkan lane. #4, #5, #6, #10 and the terminator
are P7/P9 (texture and XFB families). #9 is P6.
## 4.3 The group the split will break first — `PipeInputs`'s "forwarded to the live context"
`MG_Backend/MGPipe/PipeInputs.h:558` opens a group of **six accessors + `RecordError`** that are *not*
snapshot fields: they forward to `MG_State::pGLContext` through
`MG_Impl/Pipe/PipeFill.cpp`'s `LiveContext()` (`PipeFill.cpp:313`,
`GLContext* LiveContext() { return MG_State::pGLContext.get(); }`). **In the server process
`LiveContext()` is null**, and every one of them then silently degrades:
| accessor | decl | impl | backend callers | split answer |
|---|---|---|---|---|
| `GetBufferBindingPointCount(target)` | `PipeInputs.h:569` | `PipeFill.cpp:1553-1557` | `DirectGLES.cpp:579` (`SyncAtomicCounterBuffers`), `UniformManager.cpp:1190`, `:2014` | server-side binding tables (P7/P9) |
| `GetProgramObject(index)` | `:570` | `PipeFill.cpp:1559-1562` | `DirectGLES.cpp:5810`, `:9203`, `DirectVulkan.cpp:274` | handle tables |
| `GetTextureObject(index)` | `:571` | `PipeFill.cpp:1564-1566` | `VkTextureManager.cpp:810` | handle tables |
| `HasOpenTransformFeedbackSpan(lifetimeId)` | `:572` | `PipeFill.cpp:1569-1572` | `VulkanRenderer.cpp:11527` | handle tables |
| `InvalidateCompileEnv()` | `:573` | `PipeFill.cpp:1574-1576` | `BackendObject_DirectVulkan.cpp:390`, `:788` | **→ `OnCapsInvalidated` (#8)** |
| `ValidateProgramName(index)` | `:574` | `PipeFill.cpp:1578-1581` | `DirectGLES.cpp:5805`, `:9202`, `DirectVulkan.cpp:271` | handle tables |
| `RecordError(code, info)` | `:577` | `PipeFill.cpp:1583-1590` | `Managers.cpp:12812`, `DirectGLES.cpp:8123`, `DirectVulkan.cpp:717`, `VulkanRenderer.cpp:1339`, `:1343`, `:1399` | **→ `OnGlError` (#1)** |
`PipeInputs.h:563-568` states that this group deliberately carries **no** `MGP_INPUT_CHECK`, i.e. **no
`Fatal{UnmigratedPipeInput}`** — *"a forward is a live call, not a stored value, and
`InvalidateCompileEnv` is reached from backend initialisation before any verb has filled, where a check
would be `Fatal{...@<none>}` on every start."* **This is the hole in P5's exit gate.** The gate says "a
read of an unmigrated field = `Fatal{UnmigratedPipeInput}`", but these seven are the very reads a split
build most needs to catch and they are exempt by construction. `RecordError` degrades to
`MGLOG_E_ONCE` + drop (`PipeFill.cpp:1585-1588`); the other six degrade to null/false/zero **silently**.
**Recommendation for the brief:** P5 should add a split-only arm to those seven — under
`MOBILEGL_BUILD_DISAGGREGATED` with no live context, `Fatal{UnmigratedPipeInput}` (or the callback where
one exists), not a silent null. Cheap (7 sites), and it is the difference between P5's gate being armed
and being decorative. The precedent for a split-only Fatal arm is `MGPipeUnmigratedEmulation`
(`PipeApply.h:1068`, monolith body `PipeApply.cpp:2820` = `(void)name;`).
**Good news for the reduced path:** of the seven, a Clear + Triangle + ReadPixels sequence on DirectGLES
hits **none**. `SyncAtomicCounterBuffers` (`DirectGLES.cpp:574-620`) only runs when a program declares
atomic counters; `MarkShaderStorageBuffersGpuWritten` (`DirectGLES.cpp:562-573`) uses
`GetTouchedBufferBindingPointCount` / `GetBufferBindingPoint`, which are **migrated** fields, not
forwarded ones. `ValidateProgramName`/`GetProgramObject` are on `glGetProgramBinary`-shaped paths
(`DirectGLES.cpp:5805`, `:9202`), not on draw.
## 4.4 The `Fatal{UnmigratedPipeInput}` machinery P5 inherits
- Armed by `MOBILEGL_PIPE_POISON`, `PipeInputs.h:21-26` — **and `MOBILEGL_BUILD_DISAGGREGATED` is one of
the three arming conditions**, so a split build has the poison on by construction.
- The check macro `MGP_INPUT_CHECK` is `PipeInputs.h:44-53`; the abort is
`MGPipeInputPoisonFatalForVerb` (`PipeInputs.h:31`), `MGLOG_F` + `std::abort()`, **live at every log
level** (`:29-31`).
- Existing gates that assert the message shape: `MG_IntegrationTest/Scenarios/PoisonOmissionScenario.cpp`
(prefix at `:88`, pair assertion at `:341`) and `PipeVerifyArmingScenario.cpp:78`; unit side
`MG_Test/Pipe/PipeInputsTest.cpp:93`, `:292-296`.
- The knob that injects an omission is `MOBILEGL_PIPE_POISON_OMIT=<Verb>:<FieldName>` (`Config.h:370`).
---
# PART 5 — D-B6, the one new stall class (server-initiated texture remint pull)
## 5.1 The design, summarised
`ARCHITECTURE.md` §8.4 (`docs/Disaggregated/ARCHITECTURE.md:303-312`). The server keeps no texels
(§10.1). Three things make it ask for a level it was already sent:
`RequireImageBindableStorage`'s re-dirty, a whole-format re-creation, and a texture-view source remint.
Four mitigations run at once:
1. **Prevention (the main one):** the client stamps `everImageBound` and carries
`MGPResourceDesc::ImageBindableHint` on every `ResourceCreate`/`ResourceRespecify`, so
image-bindable storage is allocated up front.
2. **The pull is async:** the server emits `OnTexturePullRequest` and marks the twin not-ready; the
client re-sends on its next publish. **What blocks is `mgl-srv-apply`, never the app thread.**
3. **Bounded retention, default off:** `MOBILEGL_PIPE_TEXEL_RETAIN_MB` default 0 (`Config.h:397`) —
`MipmapStorage` holds a full CPU shadow per level, so a pull can always be served; the cache buys
latency, not correctness.
4. **Explicit terminator:** `ResourceSubDataComplete(res, target, firstLevel, levelCount, pullSerial)`,
**may carry zero regions** (a level whose content came only from rendering, from a copy
`CanMirrorCopyImageShadow` refused, or from GPU-side mip generation — the client has no bytes). Zero
regions means the server proceeds with allocated-but-empty storage and logs `MGLOG_W`.
**Without the terminator the apply thread parks forever.**
Gate: `TextureRemintPullScenario` (must include an unservable case, and must be **red** before the
terminator lands). Pull counts are published per trace case.
## 5.2 What is actually in the tree
- The site is **named, not built**: `MG_Backend/DirectGLES/Managers.cpp:5275-5345`. The comment at
`:5275-5284` says it exactly: *"P4a supplies mitigation 1 — the prevention half… and NAMES the site.
Mitigations 2-4 (the async pull, the bounded retention and the `ResourceSubDataComplete` terminator)
and `TextureRemintPullScenario` are P9's, and P4a must not build half a terminator."*
- The marker is `MG_Pipe::MGPipeUnmigratedEmulation("texture-remint-pull")` at `Managers.cpp:5334`,
raised **once per transition** and **only where a level is actually replayed** (review N-4,
`Managers.cpp:5289-5297`). Monolith body is a no-op (`PipeApply.cpp:2820`).
- The counter is `PipeStats::CallClass::TextureRemintPulls` (`MG_Util/Metrics/PipeStats.h:166`,
documented `:161-166`), incremented at `Managers.cpp:5339` **only when `hadBackendStorage`**; name
`"tex-remint-pulls"` (`PipeStats.cpp:182`), summary token `trp=` (`PipeStats.cpp:459`). Test-side
peek: `MG_IntegrationTest/Harness/P4aFinalFixPeek.{h,cpp}` (`P4aFinalFixPeek.h:35`,
`.cpp:50-55`), used by `Scenarios/P4aFinalFixScenario.cpp:518`, `:579-582`, `:602`.
- There are **three sibling unmigrated-emulation markers** P5 will meet on the same path:
`"generate-mipmap-storage"` (`DirectGLES.cpp:8051`), `"generate-mipmap-cpu-fallback"` (`:8702`),
`"copy-image-shadow-mirror"` (`:8997`), `"get-tex-image-shadow"` (`:10623`) — four, in fact. **All four
are no-ops today and all four become split-only Fatals or callbacks.** `"get-tex-image-shadow"` is the
one that touches P5's readback subject.
## 5.3 The measured rate, and the answer for P5
`ROADMAP.md:85` (open question 2) is **answered**, by P4a:
> **P4a 已答:可以忽略,保留 LRU 维持默认 0。** … 在 79 例 retrace × 两后端、`MOBILEGL_PIPE_STATS_PERIOD=60`、
> push 构建(`8c458cd5`)下:**780 个统计窗口里总共 2 次**,分布在 **2 个用例**
> (`iris-photon-v1.3b-in-world`、`iris-derivative-main-d24.4.14-in-world`,都只在 DirectGLES 侧,各 1 次),
> 其余 77 例恒 0。
Corroborated in `~/w7/notes/p4a/INTEGRATOR-DECISIONS.md:770` (the final-fix round: *"`tex-remint-pulls`
= `ROADMAP:85` 的数字"*) and `:726` (the review finding that made the counter real —
before it, the hint was always 0 and the number was unmeasurable). `ImageBindableHint` got its first real
producers in P4a (`glBindImageTexture` and sampler-view resolution each set a bit), so **2 is the residue
after prevention is already working**.
**Can P5's reduced path avoid it? Yes, and it does so by construction, three times over:**
1. The path is entered only from `RequireImageBindableStorage` — i.e. only a texture that gets
`glBindImageTexture`'d after its storage was already minted non-image-bindable. P5's two gate
scenarios (`ClearThenReadPixels`, a new `Triangle`) use no image loads/stores at all.
2. Both fixtures where it fired are Iris shader-pack in-world traces; the P5 SSIM gate is **OpenRA**,
not Iris (`ROADMAP.md:21`).
3. Even when it fires, monolith behaviour is unchanged — `Managers.cpp:5286-5288`: *"In monolith the code
below keeps running exactly as it does today: the Fatal is a split-only arm."*
**Recommendation:** P5 should (a) leave mitigations 2-4 to P9, exactly as `Managers.cpp:5281-5284` and
`PipeCatalogueTest.cpp:953` instruct, and (b) make the **split-only arm of `MGPipeUnmigratedEmulation`
a `Fatal`** so that if a P5 trace does reach it, the phase finds out loudly instead of rendering a wrong
mip. That single edit (`PipeApply.cpp:2820` gains a `#if MOBILEGL_BUILD_DISAGGREGATED` arm) is also what
half-arms the P5 exit gate "a read of an unmigrated field = `Fatal{UnmigratedPipeInput}`" for the
*emulation* family, complementing the `PipeInputs` half in 4.3.
---
# PART 6 — Consolidated list of things P5 must invent (nothing exists today)
| item | evidence it does not exist |
|---|---|
| `MGPCaps` producer and consumer | zero call sites; `gMGPipeScreen` read only by `PipeCatalogueTest.cpp:107` |
| `BackendObject_Remote` | `grep BackendObject_Remote` over the tree: 0 hits outside `ROADMAP.md:21` |
| `MG_Remote/Client/*` (emitter tables) | `MG_Remote/` contains only `Protocol/` (3 files) and `Transport/` (12 files) |
| `MG_Remote/Server/*` (`PipeApplier`, `ServerLoop`, `PipeObjectTables`, `IndexHostMirror`) | same |
| `MGPReplySlot` semantics | zero producers/consumers |
| `MG_Config::Transport` / `MOBILEGL_TRANSPORT` / `MOBILEGL_IPC_*` | zero hits in `Config.h`, `MG_Util/` |
| `SEG_REPLY` / `SEG_EVENT` runtime code | named in `ShmSegment.h:9`, `Ring.h:80` (`eventRingFull`), `protocol.fbs:40-41`; no implementation |
| `DirectGLES.Split.Triangle` | no `Triangle*` scenario in the 85-file scenario list |
| `PersistentCoherentMapScenario` | not in the scenario list |
| `add_trace_replay_test(... SPLIT)` / `-DTRACE_TRANSPORT=` | `tools/trace_replay/CMakeLists.txt:311`, `:384-387` have neither |
| a round-trip counter | `PipeStats::CallClass` (`PipeStats.h:100-168`) has `MapPersistentRoundtrips` and `TextureRemintPulls` but no general round-trip class. **Any new class must be inside the `#if MOBILEGL_PIPE_PUSH` block (`PipeStats.h:118-167`)** — G1 forbids resizing the pull build's counter arrays. |
| peak-RSS recording for both roles | no harness; `tools/device_bench/bench.sh` records frame times (`ARCHITECTURE.md` §13.2-④) |
---
# Appendix — three claims I could not verify, flagged as guesses
1. **Opcode numbers in Part 2's table** are derived by counting `MGP_CALL_LIST` positions, not read off
a generated artefact. The *set* of ten `kReplySlot` rows and their `PipeCalls.def` line numbers are
exact; the opcodes are a **guess** and should be re-derived from
`MG_Pipe/generated/PipeWire.inc` before anyone pins a wire value.
2. **`CallMask` vs `CapsSnapshot.tableSlotMask`** — argued in 1.4(6); my reading is that they are one
field and the schema comment is stale, but I did not find a line that settles it.
3. **Startup ordering of `LogBackendInfo()`** relative to the first `eglMakeCurrent` in the real
launcher — argued in 1.2; I verified it in library code only, not in FCL.