Files
MobileGL/docs/Disaggregated/notes/recovered/wf4/scout-docs-spec.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

429 lines
58 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.
# P1 specification, extracted from the MGPipe design docs
Scout report. Worktree: `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg` (branch `feat/disaggregated`). Read-only; nothing was edited.
Docs read in full: `docs/Disaggregated/README.md` (57 lines), `ARCHITECTURE.md` (601 lines), `ROADMAP.md` (90 lines), `MEASUREMENTS.md` (96 lines). Every code claim below carries a path + line that I actually opened in this tree.
Quoting convention: `>` blocks are **verbatim** from the doc (Chinese as written). Everything else is my summary or my own measurement.
---
## 0. Where P1 sits
- `docs/Disaggregated/README.md:3`: `> 状态:**P0、P0.5 已落地**(`feat/disaggregated@5d99ee43`,基线 `dev@50fb1343`)。下一步 P1 → P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`。`
- Track: monolith lane. `ROADMAP.md:9` — `> 两条跑道:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止且 monolith 严格好于起点;**IPC 跑道** P5 → P6 → P9 → P10 → P11 → P12。`
- Milestone: `ROADMAP.md:38` — `> - **第 25 天(P1 出口)**:verify harness 逐 draw 逐字段证明"推送等价于拉取"。零产品风险,**不是** GO/NO-GO。`
- Cumulative day count: `ROADMAP.md:32` — P0 9 → P0.5 15 → **P1 25** → P2 43 …
- Sunk-cost framing: `ROADMAP.md:58` — `> 真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天,NO-GO 分支下仍留下上述产物。`
- Under NO-GO, P1's output is explicitly **not rolled back** (`ROADMAP.md:57`): `> **不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev``.
---
## 1. The P1 row of the phase table (verbatim), `ROADMAP.md:17`
Columns are: 阶段 | 天 | 落地什么 | 验收门 | 依赖.
**阶段**: `**P1** `PipeInputs` 替换与 verify harness`
**天**: `10–13`
**落地什么 (deliverables), verbatim:**
> `MG_Backend/MGPipe/PipeInputs.h`(Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表
Broken out — six deliverables:
1. `MG_Backend/MGPipe/PipeInputs.h`, Espryt 32 accessors / Magma 55 accessors.
2. The mechanical `sed` of 293 arrow sites **plus** a per-line conversion of the 58-line non-arrow list — the doc calls this an **explicit deliverable** (i.e. the list itself is a produced artifact, not a side effect).
3. Per-verb-class fill points: the G5 table, ~93 boundary sites.
4. Per-verb generation poison.
5. G4 shadow comparator + **a third CI mode**.
6. A per-site ownership table for the 20 `SyncPersistentMappedRange` and 6 `SyncGpuWrites` sites.
**验收门 (acceptance gate), verbatim:**
> pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal
Four gate clauses:
1. On the **pull** build, `nm --defined-only` unchanged; `.text` differences attributable line by line. Empty-guard / ternary rewrites are **deferred to P2** — do not do them in P1, they would break this gate's attributability.
2. 40 traces + **all** integration tests, run under `MOBILEGL_PIPE_VERIFY=1`, **zero divergence**.
3. Negative control A: deliberately corrupting one snapshot field must turn verify red.
4. Negative control B: deliberately omitting one field from `glGenerateMipmap`'s fill table must raise the poison Fatal **on that verb** (not on a later draw). This is what makes the poison a per-verb *generation* rather than a bitmap.
**依赖**: `P0.5`. And from the P0.5 row (`ROADMAP.md:16`, last cell): `> P0;**P1 与 P7 的硬前置**` — P0.5 is a hard prerequisite for P1 and P7.
### Every other P1 mention in the docs
| Where | Verbatim / content |
|---|---|
| `ARCHITECTURE.md:172` | `> 完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举 `MG_Impl/GLImpl` 里每个 mutator → 必须 bump 的聚合世代,CI 重生成 + `git diff --exit-code`,未映射即失败(P1 起成为门)。` Also gives the measured surface: 926 mutator calls over 73 distinct mutators; 92 of them (7 mutators, mostly `RecordError`) sit at "即时发布点"; the other 834 are published by the following verb. `> 映射表是 73 条目的问题。` |
| `ARCHITECTURE.md:323` | Section title: `### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1)` — §2 below. |
| `ARCHITECTURE.md:503` (gate 13.2-③) | `> 逐名功能基线是"P1 出口的重构后 monolith"(P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。` **P1's exit becomes the by-name functional baseline for every later phase.** |
| `ARCHITECTURE.md:553` | Build layout: `MobileGL/MG_Backend/MGPipe/ PipeInputs.h + MGPipeImpl_DirectGLES/DirectVulkan.cpp [P1+]` — i.e. P1 creates that directory with **three** files: the header plus one impl TU per backend. (The directory does not exist yet: `MobileGL/MG_Backend/` currently holds only `BackendObject.cpp`, `BackendObject.h`, `BackendObjects.h`, `Init.cpp`, `DirectGLES/`, `DirectVulkan/`.) |
| `ARCHITECTURE.md:568` | CI: `> `gen_pipe_dirty_surface.py --summary`(信息性,P1 成门)` |
| `ARCHITECTURE.md:583` (附 A) | `> `MOBILEGL_PIPE_VERIFY` \| OFF \| 计划(P1;构建期开关,编译进 `SnapshotFromGLContext()` 与 G4 比对器,P13 后保留)` — note **CMake-level** switch, distinct from the runtime env var of the same name. |
| `ROADMAP.md:46` (GO/NO-GO) | `> - [ ] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧)` |
| `ROADMAP.md:18` | P2's 依赖 cell is `P1`. |
In-tree code comments that name P1 (these are contracts the P1 implementer inherits):
- `MobileGL/MG_Pipe/generated/PipeFilled.inc:26` / `scripts/gen_pipe.py:462`: `// P0 is the skeleton: the enum, the tables and the assertion helper exist, PipeInputs // itself lands in P1.`
- `MobileGL/MG_Pipe/generated/PipeFilled.inc:161` / `scripts/gen_pipe.py:479`: `// true has to be argued for in P1 when the fillers land: a sticky field is a field // the poison cannot protect.`
- `MobileGL/MG_Pipe/PipeFields.def:16`: `// here makes the comparator blind to it; that gap closes in P1, when the verify harness // goes live and the comparator's coverage is itself asserted.`
- `scripts/gen_pipe.py:165` and `MobileGL/MG_Pipe/generated/PipeVerify.inc:236`: the memcmp fallback types (`RenderStateParameters`, `PixelStoreParameters`, `DynamicBackendParameters`, `MGHostSpan`, listed at `scripts/gen_pipe.py:167-172`) `// P1 gives them field lists of their own, at which point this branch stops being reachable from any payload.`
- `MobileGL/MG_Pipe/MGPipeTypes.h:32`: `// needs fixed-width members before it can move (a type change, not a move): P1/P7.` — about `DynamicBackendParameters` inside `MGPCaps`, which is what still keeps purity gate A off `MGPipeTypes.h`.
- `MobileGL/MG_Pipe/Coverage.def:36-40`: `GetBufferBindingSlot` is polymorphic over `BufferTarget`; `/* inventory carries the target argument (P1) */` — its rows must split across `set_vertex_buffers` / `set_index_buffer` / `set_indirect_buffers` / `set_shader_buffers` in P1.
- `scripts/gen_pipe_dirty_surface.py:21-22`: `P0 is the skeleton: it reports. P1 adds the mapping file and CI regenerates it with `git diff --exit-code` and zero unmapped mutators, the same shape as gen_pipe.py's G6.`
- `scripts/gen_pipe_dirty_surface.py:193-194`: `dirty-surface: every mutator above is UNMAPPED - the aggregate-generation mapping file lands in P1, and this report is what it has to cover.`
- `scripts/check_include_closure.py:15` and `:86` describe the P0.5 closure gate that P1 must not regress.
---
## 2. The strangler scaffold (ARCHITECTURE §9.2, lines 323–351)
### 2.1 `PipeInputs` + `MGB_CTX` — verbatim code block (`ARCHITECTURE.md:325-340`)
> ```cpp
> // MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织(~20 KB,字段集全迁移期稳定)
> struct PipeInputs {
> const RenderStateParameters& GetRenderStateParameters() const; // 阶段 A:类型与后端今天读到的完全一致
> // … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 55)
> #if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
> Uint64 m_filledGen[kFieldCount]; // 逐字段"上次填充的 verb 序号"
> Uint64 m_currentVerbSerial;
> #endif
> };
> #if MOBILEGL_PIPE_PUSH
> # define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
> #else
> # define MGB_CTX (::MG_State::pGLContext)
> #endif
> ```
Load-bearing details in that block:
- **Organized by memo key, not by read point.** That is why the field set is small (61) and stable across the whole migration. `MobileGL/MG_Pipe/generated/PipeFilled.inc:15-17` repeats this: `// PipeInputs is organized by MEMO KEY, not by read point, which is why the field set is // small and stable across the whole migration).`
- Size budget: `~20 KB`.
- **Phase A rule**: the accessor return types are *exactly* what the backends read today (`const RenderStateParameters&`, etc.), so the `sed` is type-neutral.
- The poison members are compiled only under `MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED`.
- `MGB_CTX` is a **compile-time** macro switched on `MOBILEGL_PIPE_PUSH`. Note the asymmetry with the runtime knob: `MOBILEGL_PIPE_PUSH` is *also* a runtime per-subsystem bitmask (`MobileGL/Config.h:320-326`, `Uint64 PipePush = 0;` at `Config.h:326`, parsed at `MobileGL/ConfigLoader.cpp:245`). The docs use the same name for both; the P1 implementer must decide/record how the macro arm and the runtime bitmask relate (see §7, Risk R3).
- `gPipeInputs` is a **single global instance** in `MobileGL::MG_Pipe` (matching `gMGPipeScreen`/`gMGPipeContext` at `MobileGL/MG_Pipe/MGPipe.h:72-73`).
### 2.2 The three-phase table (`ARCHITECTURE.md:342-347`) — verbatim
> | 阶段 | 改什么 | 证明 |
> |---|---|---|
> | A 别名 | 机械 `sed`:`MG_State::pGLContext->` → `MGB_CTX->`(293 处)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) |
> | B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 |
> | C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) |
**P1 is phase A plus the scaffolding for B** (the ROADMAP P1 row's `verify harness`). Phase B's per-field yielding by the `MOBILEGL_PIPE_PUSH` bitmap and phase C's handle-ification belong to P2+ (P2 lands the tracker + the first push subsystems; P3a/P4a land Track H).
The 58 non-arrow lines break down (per the doc) into: ~34 `MOBILEGL_ASSERT` deletions, 7 empty guards, 3 ternaries, `.get()` raw-pointer captures + `decltype` aliases, 14 `!= nullptr`, 1 comment. **The empty-guard and ternary rewrites are explicitly deferred to P2** so the `.text` diff stays attributable.
### 2.3 Fill points: the G5 table and the ~93 boundary sites (`ARCHITECTURE.md:348`) — verbatim
> - 填充点逐 verb 类,不只 `PrepareForDraw`/`SetupDraw` 两处:G5 从 `PipeCalls.def` 生成"每个 `kCtxVerb`/`kCtxObject` 调用可能读哪些字段"的表,在 `MG_Impl` 的 ~93 个边界站点生成 validate/fill 调用。
Cross-references:
- G2 (`ARCHITECTURE.md:157`): `> monolith 直调 thunk `MGP_<Name>()`,`MG_Impl` 的约 93 个 `gBackendFunctionsTable.GL.*` 站点逐名改到它上面` — the same ~93 sites are where the fill calls go.
- G5 (`ARCHITECTURE.md:160`): `> `PipeFilled.inc` \| `PipeInputs` 字段 id(61 个)与逐 verb 世代 poison`
- The eight validate entry points (`ARCHITECTURE.md:153`, §5.1): `> 八个 validate 入口,由 `PipeCalls.def` 的 `kCtxVerb`/`kCtxObject` 条目生成:`ValidateForDraw`(20 个 draw 入口)、`ValidateForDispatch`、`ValidateForClear`、`ValidateForBlitOrCopy`、`ValidateForTextureOp`(GenerateMipmap / CopyTex* / BindImageTexture)、`ValidateForReadback`、`ValidateForXfbSpan`、`ValidateForQuery`。八个而不是四个,因为 `MG_Impl` 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、query/sync)很多自己就读 `pGLContext`。`
- **P1 relevance**: the tracker itself lands in P2, but the *fill points* land in P1 at these same eight shapes. `glGenerateMipmap` — the P1 negative-control verb — is under `ValidateForTextureOp`.
- Class counts (`ARCHITECTURE.md:171-172` region, §3.2 table at `ARCHITECTURE.md:165-172`): `kCtxObject` = 9 calls, `kCtxVerb` = 13 calls. Those 22 rows are the ones G5 has to enumerate fields for.
- `ARCHITECTURE.md:155` (§5.1, still binding on where a push happens): `> **只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。`
**Measured in this tree** (my count, for the mechanical work estimate):
- `grep -rc "gBackendFunctionsTable" MobileGL/MG_Impl/` → 91 lines across 8 files: `GLImpl/Drawing/GL_Drawing.cpp` 37, `GLImpl/Framebuffer/GL_Framebuffer.cpp` 11, `GLImpl/Getter/GL_Getter.cpp` 3, `GLImpl/Program/GL_Program.cpp` 1, `GLImpl/Program/ProgramInterface.h` 1, `GLImpl/Query/GL_Query.cpp` 22, `GLImpl/Sync/GL_Sync.cpp` 6, `GLImpl/Texture/GL_Texture.cpp` 10. (Doc says "~93"; 91 lines here — a line may hold more than one site.)
### 2.4 Per-verb generation poison (`ARCHITECTURE.md:349`) — verbatim
> - poison 是**逐 verb 世代**不是位图:每次 verb 递增 `m_currentVerbSerial`,字段被填时记下序号,读取时断言相等(跨 verb 有效的字段显式标 sticky)。位图看不见"上一个 draw 填过、紧随的 `glTexSubImage` 读到陈旧值"。debug 与 disaggregated 构建里读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}`。纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`。
So, precisely:
- **What is poisoned**: every one of the 61 `PipeInputs` fields, individually.
- **When**: `m_currentVerbSerial` is bumped once per verb; a fill stamps the field with the current serial; a read asserts `stamp == currentVerbSerial` unless the field is marked *sticky*.
- **Where it is live**: `MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED` builds only.
- **The Fatal**: `Fatal{UnmigratedPipeInput, "<FieldName>@<Verb>"}` — the message names the field *and* the verb.
- **Last sentence is a separate rule**: purity gate C greps for `pGLContext`, not `pGLContext->` — so an `MGB_CTX` alias that still spells `pGLContext` in the `#else` arm inside `MG_Backend` will trip gate C. (Gate C: `ARCHITECTURE.md:502` — `> **C 门未声明**——`grep -c 'pGLContext' MG_Backend/` == 0`.) The `MGB_CTX` macro definition therefore cannot live under `MG_Backend/` at P13; in P1 gate C is not yet green anyway (it turns green with the pull path's retirement, `ARCHITECTURE.md:369`).
**The skeleton already in the tree** — P1 fills it in, it does not invent it. `MobileGL/MG_Pipe/generated/PipeFilled.inc` (308 lines, generated by `scripts/gen_pipe.py` from `Coverage.def` + `PipeCalls.def`, included from `MGPipe.h:86`):
- `PipeFilled.inc:19-26` states the poison contract verbatim in code: `// The poison is a per-verb GENERATION, not a bit. A bitmap cannot see the dangerous case: // a field filled by the previous DRAW and then read by the glTexSubImage that follows is // stale, and its bit is already set. So every verb bumps CurrentVerbSerial, filling a // field stamps it with that serial, and reading a non-sticky field whose stamp is older is // Fatal{UnmigratedPipeInput} (section 6.2.2).`
- `PipeFilled.inc:28-91`: `enum class MGPipeInputField : Uint16 { … kFieldCount }` — the 61 field ids, in this order:
`GetActiveTextureUnit, GetBlendColor, GetBlendEquationIndexed, GetBlendFuncIndexed, GetBoundTransformFeedbackName, GetBoundVertexArray, GetBufferBindingSlot, GetBufferBindingPoint, GetBufferBindingPointCount, GetTouchedBufferBindingPointCount, GetClampReadColor, GetClearColor, GetClearDepth, GetClearStencil, GetColorMaskIndexed, GetCullFaceMode, GetCurrentVertexAttribute, GetDepthFunc, GetDepthMask, GetDepthRangeIndexed, GetFramebufferBindingSlot, GetImageTextureBinding, GetLineWidth, GetLogicOp, GetMaxTouchedTextureUnit, GetMinSampleShadingValue, GetPatchDefaultInnerLevel, GetPatchDefaultOuterLevel, GetPatchVertices, GetPipelineStateVersion, GetPixelStoreParameters, GetPolygonModeFront, GetPolygonOffsetFactor, GetPolygonOffsetUnits, GetPrimitiveRestartIndex, GetProgramForDispatch, GetProgramForDraw, GetProgramObject, GetProvokingVertexMode, GetRenderStateParameters, GetRenderStateParametersVersion, GetSamplingResolutionGeneration, GetScissorBox, GetStencilState, GetTextureBindGeneration, GetTextureContextId, GetTextureObject, GetTextureUnitObject, GetTransformFeedbackCapturedVertices, GetTransformFeedbackGeneration, GetTransformFeedbackPausedPrimitiveCounter, GetTransformFeedbackProgram, GetViewport, GetViewportIndexed, IsCapabilityEnabled, IsCapabilityEnabledIndexed, IsTransformFeedbackActive, IsTransformFeedbackPaused, InvalidateCompileEnv, ValidateProgramName, RecordError`
- `PipeFilled.inc:93-94`: `static_assert(kMGPipeInputFieldCount == 61, "the PipeInputs field set moved");`
- `PipeFilled.inc:96-155`: `kMGPipeInputFieldNames[]`.
- `PipeFilled.inc:158-225`: `kMGPipeInputFieldSticky[]` — **every entry is currently `false`**, and each `true` P1 introduces has to be argued for (`PipeFilled.inc:160-162`).
- `PipeFilled.inc:227-291`: `kMGPipeInputFieldFilledBy[]` — the call that must have filled each field by the time a verb reads it; names come from `Coverage.def` so `PipeCoverage.inc` and `PipeFilled.inc` cannot disagree (`PipeFilled.inc:227-228`). The last three entries are pseudo-calls: `"kClientResolved"` (×2: `InvalidateCompileEnv`, `ValidateProgramName`) and `"kReverseChannel"` (`RecordError`).
- `PipeFilled.inc:293-296`: `struct MGPipeFilledState { Uint64 CurrentVerbSerial; Uint64 FilledGen[kMGPipeInputFieldCount]; };`
- `PipeFilled.inc:298-302`: `[[noreturn]] inline void MGPipeInputPoisonFatal(MGPipeInputField field, const char* verb)` → `MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"}", …); std::abort();`
- `PipeFilled.inc:304-308`: `MGPipeInputFieldIsFresh(state, field)` → sticky fields pass when `FilledGen != 0`; non-sticky require `FilledGen == CurrentVerbSerial`.
So P1's poison work is: instantiate `MGPipeFilledState` inside `PipeInputs` (the doc's `m_filledGen[kFieldCount]` / `m_currentVerbSerial`), stamp it from the G5-generated fill points, check it in each of the 61 accessors, and argue each `sticky = true`.
The source of truth behind the field list is `MobileGL/MG_Pipe/Coverage.def:29-97` (`MGP_COVERAGE_ACCESSOR_LIST`) — one `X(Accessor, PipeCall)` row per field, which is what generates both `kMGPipeInputFieldFilledBy` and the coverage table.
### 2.5 G4 shadow comparator / `MOBILEGL_PIPE_VERIFY`
The definitive statement is gate 13.2-②, `ARCHITECTURE.md:501` — verbatim:
> 2. **语义影子比对 `MOBILEGL_PIPE_VERIFY=1`**——决定性的一条:两套状态模型活在同一地址空间,tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 比对器逐字段、每 draw 比对,打印第一个分歧字段与 draw 序号。抓 tracker 忘推的字段、**dirty 位触发得太少**(危险方向)、两条路径变换不一致的值。第三种 CI 模式,40 个 trace + 全部集成测试,~5–10× 慢,永不出货。逐字段而非 `memcmp`(padding 会 false-DIFFER)。**保留模式**:消费即清的组(纹理 dirty rect)发射后无法重算,verify 时 tracker 保留清除前的集合并比对发射出去的 `(UnionBox, RegionCount, Regions[])`。**活过 P13**。
Decomposed:
- **What is compared**: field by field, the pushed `PipeInputs` against a second `PipeInputs` filled by `SnapshotFromGLContext()` — both models live in the same address space.
- **When**: once per draw.
- **Output on failure**: the *first* differing field name + the draw serial.
- **What it is designed to catch**: (a) fields the tracker forgot to push; (b) **a dirty bit that fires too rarely** — the dangerous direction, which no purity gate can see; (c) values that the two paths transform inconsistently.
- **"Zero divergence"** means: across the 40 trace fixtures and the entire integration suite, the comparator never reports a differing field. That is exactly the ROADMAP gate and exactly GO/NO-GO item 1.
- **"The third CI mode"**: a build/run configuration alongside the existing ones, running the 40 traces + all integration tests, ~5–10× slower, **never shipped**.
- **Field-wise, never memcmp** — padding would false-DIFFER.
- **Retention mode (保留模式)**: for consume-and-clear groups (texture dirty rects) that cannot be recomputed after emission, the tracker under verify keeps the pre-clear set and compares the emitted `(UnionBox, RegionCount, Regions[])`. (P1 only needs the mechanism to the extent texture ops are in scope; the shape gold-standard `TextureUploadShapeScenario` is a P3b/P4b gate, `ROADMAP.md:23`.)
- **Survives P13**: `ARCHITECTURE.md:369` — `> P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()` 与 `MG_State` include**(D-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。` So P1 must build `SnapshotFromGLContext()` as a *permanent* verify-only facility, not a throwaway.
- **CMake vs env**: `ARCHITECTURE.md:583` lists `MOBILEGL_PIPE_VERIFY` as a CMake option (`OFF`, `计划(P1;构建期开关,编译进 SnapshotFromGLContext() 与 G4 比对器,P13 后保留)`); `ARCHITECTURE.md:589` lists the runtime env `MOBILEGL_PIPE_VERIFY` default `0`, `逐 draw 逐字段影子比对`. Both exist; P1 must wire both. Runtime side already landed: `MobileGL/Config.h:327-332` (`Bool PipeVerify = false;` at `Config.h:332`) and `MobileGL/ConfigLoader.cpp:246`.
- **G4 in the catalogue table** (`ARCHITECTURE.md:159`): `> G4 \| `PipeVerify.inc` \| `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表来自 `PipeFields.def`;浮点按位比较,NaN patch level 不会误报)`
**Already in the tree**: `MobileGL/MG_Pipe/generated/PipeVerify.inc` (573 lines, included at `MGPipe.h:83`).
- `PipeVerify.inc:9-23` restates the contract, including `// Floating-point fields are compared by BITS, so a NaN patch level - which // glPatchParameterfv accepts and ComputePipelineStateHash already hashes bitwise - equals // itself instead of tripping every draw.`
- `PipeVerify.inc:30-…` declares one `MGPipeVerify(const X&, const X&, const char** outField)` per payload (≈63 payloads; `MEASUREMENTS.md:82` says `63 个 verify payload`).
- `PipeVerify.inc:222-239`: the dispatch template — float → bitwise memcmp; scalar/enum → `==`; else `==` if available; **else the memcmp fallback**, reachable today only from `RenderStateParameters`, `PixelStoreParameters`, `DynamicBackendParameters` and `MGHostSpan`. `PipeVerify.inc:233-237`: `// P0.5 moved the first two into MGPipeValueTypes.h; P1 gives them field lists of their own, at which point this branch stops being reachable from any payload.` → **P1 deliverable (implied but explicit in code): add `MGP_FIELDS_*` lists for those value structs to `MobileGL/MG_Pipe/PipeFields.def`** (238 lines, format shown at `PipeFields.def:21-39`) and remove them from `MEMCMP_FALLBACK_TYPES` (`scripts/gen_pipe.py:167-172`).
- `PipeFields.def:9-17` states the zero-false-positive requirement: `// MOBILEGL_PIPE_VERIFY has to have ZERO false positives and a padding byte is exactly what makes a memcmp of RenderStateParameters false-DIFFER`.
### 2.6 Track V / Track H (`ARCHITECTURE.md:353-357`, §9.3) — verbatim
> - Track V(值类型:`GetRenderStateParameters`、`GetPixelStoreParameters`、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。
> - Track H(对象类型:167 个 `SharedPtr<MG_State…>` 点):真活。
> - 读点分类实测(静态):A 探测变化 ~35(12%)、B 翻译输入 ~216(74%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"bump 一个版本让 server 自己拉"行不通,值本身必须过去。
Glossary (`README.md:52`): `> **Track V / Track H**:值类读点的迁移(整块 POD 过线)/ 对象类读点的迁移(`SharedPtr<前端对象>` → 句柄)。`
P1 does neither migration — P1 makes both *addressable*: every read point, V or H, goes through a `PipeInputs` accessor. Track V's bulk moves in P2 via the residual value block; Track H's first slice moves in P2 (Espryt 0b + Magma subsystem 4) and the waves in P3a/P4a.
### 2.7 The residual value block (`ARCHITECTURE.md:359-360`, §9.4) — verbatim
> Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)`,payload `ResidualValueBlock{RenderStateParameters, PixelStoreParameters, CapabilityBits, patch 三字段}`。三条纪律:退役是编译错误(`MGL_RESIDUAL_BLOCK_SIZE` 只降不升,`MobileGL/MG_Pipe/MGPipeTypes.h:535`,P13 变成 `static_assert(sizeof == 0)`);布局逐成员 `offsetof` 断言且 split 下逐字段序列化(异质 POD 并集的 padding 差异 monolith verify 看不见);只在 P2..P13 存在,`MOBILEGL_PIPE_STATS` 单独计一类字节(`ResidualValueBlock`,P0 已占位)。
**It is a P2 deliverable, not P1** — but P1 constrains it: the `PipeInputs` accessors for `GetRenderStateParameters` / `GetPixelStoreParameters` / capability bits / the three patch fields are the ones the residual block will feed in P2, so their P1 signatures must be the whole-struct-by-const-ref shape the block can satisfy.
Verified in tree: `MobileGL/MG_Pipe/MGPipeTypes.h:534` `#define MGL_RESIDUAL_BLOCK_SIZE 1248` with the ratchet comment at `MGPipeTypes.h:525-533` (`// This number only ever goes DOWN … P13 replaces it with static_assert(sizeof(ResidualValueBlock) == 0)`), the `static_assert` at `MGPipeTypes.h:535-537`, `static_assert(std::is_trivially_copyable_v<ResidualValueBlock>)` at `MGPipeTypes.h:525`(region), and `struct MGPResidualValueState { Uint32 Version; Uint32 Pad0; MGPBlobRef Blob; }` at `MGPipeTypes.h:539-543`. Size 1248 (of which `RenderStateParameters` is 1168) is confirmed at `MEASUREMENTS.md:86`.
### 2.8 `SnapshotFromGLContext()`
Mentions (there is no other definition in the docs; the function does not yet exist in the tree — `grep -rn SnapshotFromGLContext` over the source returns nothing outside `docs/`):
- `ARCHITECTURE.md:501` (gate 13.2-②): the tracker uses it to fill a second `PipeInputs` for the comparator.
- `ARCHITECTURE.md:367` (§9.6): `> `MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。`
- `ARCHITECTURE.md:369` (P13): delete its non-verify branch, keep the verify branch **plus the `MG_State` include it needs**.
- `ROADMAP.md:30` (P13 row): `删 `SnapshotFromGLContext()` 非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;保留 `MOBILEGL_PIPE_VERIFY``.
So `SnapshotFromGLContext()` has **two branches from birth**: a non-verify branch (the pull path — what phase A's `MGB_CTX` `#else` arm effectively is) and a verify branch. P1 authors both.
### 2.9 The `SyncPersistentMappedRange` / `SyncGpuWrites` per-site ownership table
The rule the table has to encode is the stale-index discipline, `ARCHITECTURE.md:233-238` (§5.7) — verbatim:
> **陈旧索引纪律是逐站点表,不是一条笼统规则**(client 侧扫描/解析之前要做的 reconcile 必须逐字复现 monolith 的集合):
>
> | client 侧动作 | 必须做的 reconcile |
> |---|---|
> | client 顶点数组范围计算 + 暂存 | 无(应用内存,无 GPU 写者) |
> | 最大索引扫描(EBO 源) | `SyncPersistentMappedRange()` **+** `SyncGpuWrites()` |
> | 最大索引扫描(client 指针源) | 无 |
> | `*IndirectCount` 计数解析 | **只** `SyncPersistentMappedRange()`,不加 `SyncGpuWrites()`——monolith 今天就只做这一个,加了会给 Create/Flywheel 的每 batch 平白加一次 publish-and-wait |
> | server 侧 restart 重写 / multi-draw 展平 | server 从镜像读;GPU 写者可见性由 `OnGpuWritten` 收窄集在 server 本地判定 |
And `ARCHITECTURE.md:294` (§8.1, last sentence): `> 20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payload(D-B8)。`
Also relevant to the same table: `ARCHITECTURE.md:239` — `> 前两条 client reconcile 的形态:publish → 等 `appliedSeq` → 排空事件 → 再碰 shadow。门:`ClientArrayAfterComputeWriteScenario`(去掉等待必须看到几何缺失);`create-indirect` fixture 上 `roundtrips-per-frame` 必须读零(P8)。`
**The 20 + 6 sites, enumerated from this tree** (I grepped `MobileGL/MG_Backend/`; comment-only hits excluded and listed separately). The doc's counts match exactly.
`SyncPersistentMappedRange()` — 20 call sites:
| # | Site | Backend |
|---|---|---|
| 1 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:263` `drawBuffer->` | Espryt |
| 2 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4468` `indexBuffer->` | Espryt |
| 3 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4722` `drawBuffer->` | Espryt |
| 4 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4723` `parameterBuffer->` | Espryt |
| 5 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4824` `drawBuffer->` | Espryt |
| 6 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4825` `parameterBuffer->` | Espryt |
| 7 | `MobileGL/MG_Backend/DirectGLES/Managers.cpp:1569` `bufferObject->` | Espryt |
| 8 | `MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp:510` `indexBuffer->` | Espryt |
| 9 | `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:280` `drawBuffer->` | Magma |
| 10 | `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:471` `parameterBuffer->` | Magma |
| 11 | `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:795` `indexBufferShared->` | Magma |
| 12 | `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:2023` `bufferObject->` | Magma (**the immovable consumer**: `ResolveUniformBufferPayload` / D-B8) |
| 13 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp:628` `bufferObject->` | Magma |
| 14 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp:679` `bufferObject->` | Magma |
| 15 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3434` `indexBufferShared->` | Magma |
| 16 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3513` `bufferObject->` | Magma |
| 17 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3828` `sourceBufferShared->` | Magma |
| 18 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:7137` `indirectBuffer->` | Magma |
| 19 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:12132` `drawBuffer->` | Magma |
| 20 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:12133` `parameterBuffer->` | Magma |
Comment-only mentions to keep in view while writing the table (they carry the reasoning): `DirectGLES/Managers.cpp:1461` (`// A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push`), `DirectGLES/Managers.cpp:2681`, `DirectVulkan/Renderer/VulkanRenderer.h:1214` (`// SyncPersistentMappedRange is the push-down. A map taken AFTER the record …`).
`SyncGpuWrites()` — 6 call sites:
| # | Site | Backend |
|---|---|---|
| 1 | `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:4469` `indexBuffer->` (pairs with #2 above) | Espryt |
| 2 | `MobileGL/MG_Backend/DirectGLES/Managers.cpp:2643` `bufferObject->` | Espryt |
| 3 | `MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp:511` `indexBuffer->` (pairs with #8 above) | Espryt |
| 4 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3433` `indexBufferShared->` (pairs with #15) | Magma |
| 5 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3827` `sourceBufferShared->` (pairs with #17) | Magma |
| 6 | `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:4161` `indexBufferShared->` | Magma |
Comment-only: `DirectGLES/DirectGLES.cpp:459`, `DirectGLES/DirectGLES.cpp:509`.
Note the pattern the table has to preserve: the EBO/max-index sites pair the two calls (rows 1/3/4/5 of the `SyncGpuWrites` list sit immediately after a `SyncPersistentMappedRange` on the same object), whereas the `*IndirectCount` parameter-buffer sites (`DirectGLES.cpp:4722-4723`, `:4824-4825`, `DirectVulkan.cpp:471`, `VulkanRenderer.cpp:12132-12133`) deliberately do **not** call `SyncGpuWrites` — exactly the row `> `*IndirectCount` 计数解析 \| **只** `SyncPersistentMappedRange()`` in the §5.7 table, and the subject of open question 15 (§6 below).
### 2.10 The five-part verification gates (ARCHITECTURE §13.2, `ARCHITECTURE.md:498-506`)
Preamble, `ARCHITECTURE.md:499`: `> "改前改后 `nm --defined-only` 与 `.text` size 完全相等"的门在本方案里按构造死亡(不存在能让旧字节回来的配置);替换是:`
**① Interface purity, three gates** (`ARCHITECTURE.md:500`) — verbatim:
> 1. **接口纯度三道门**(只跑非 verify 构建):**A 门 include 图**——disaggregated 配置编译 `MG_Backend` 时把 `MG_State/GLState` 从 include 搜索路径移除(`nm --undefined-only` 对"只 include 不调用"是瞎的,而 `RenderState.h → FramebufferObject.h → TextureObject.h` 正是这种耦合),依赖 P0.5;**B 门符号**——`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**C 门未声明**——`grep -c 'pGLContext' MG_Backend/` == 0。外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`,永不是裸前端指针",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。
All three turn green only in P13 (`ARCHITECTURE.md:369`). P1 must not make them *further* from green: in particular gate C greps the bare token `pGLContext`, so the `MGB_CTX` `#else` arm must be defined outside `MG_Backend/`.
**②** — the verify comparator, quoted in full in §2.5 above (`ARCHITECTURE.md:501`). **This is P1's gate.**
**③ Behavioural A/B** (`ARCHITECTURE.md:503`) — verbatim:
> 3. **行为 A/B**:40 个 trace 在 `{monolith-pull, monolith-push, split}` 下 SSIM ≥ 0.99(默认阈值);`ctest -L integration-gpu` 在 `DirectGLES.` 与 `DirectGLES.Pipe.`/`DirectGLES.Split.`(DirectVulkan 同)之间逐名相同;单元测试全绿;CTS 逐后端 conformance 在 0.5 pp 内(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母)。`TextureUploadShapeScenario` 把逐纹理逐帧的上传形状(box vs N region、作业数)录金标比对——+6 ms 悬崖由形状相等把关,SSIM 对它完全不敏感。逐名功能基线是"P1 出口的重构后 monolith"(P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。
**④ Monolith performance non-regression** (`ARCHITECTURE.md:504`) — verbatim:
> 4. **monolith 性能不回归**:两台设备 reboot-clean、同热窗口、配对 A/B,`tools/bench.sh` + trace replay `--benchmark` 逐帧 JSON;**指标是逐线程 CPU 时间**,p50 与 p99;**绝对阈值**——tracker 每 draw 的 ns 公布并设上限(真实拉取基线只有每 draw 6.5–9.3 次 accessor,相对噪声阈值会平凡通过);Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。
**⑤ Coverage + poison + handle discipline** (`ARCHITECTURE.md:505`) — verbatim:
> 5. **覆盖 + poison + 句柄纪律**:G6 重生成 0 UNMAPPED;`gen_pipe_dirty_surface.py` 重生成 0 未映射 mutator;逐 verb 世代 poison;G7 setter 一致性测试;`ResidualValueBlock` 的 `offsetof` 断言与 P13 的 `sizeof == 0`。
Of ⑤, P1 owns: the poison, and the `gen_pipe_dirty_surface.py` mapping-with-zero-unmapped gate (`ARCHITECTURE.md:172`, `ARCHITECTURE.md:568`, `scripts/gen_pipe_dirty_surface.py:21-22`) — see risk R2.
Two surviving byte-level equalities (`ARCHITECTURE.md:506`) — verbatim: `> `MOBILEGL_BUILD_DISAGGREGATED=OFF` 时 `nm --defined-only libMobileGL.so | grep MG_Remote` 为空且链接行不增加库;`nm -D libMobileGL.so | grep mobilegl_server_main` 在 RelWithDebInfo 里命中。符号与 `.text` 漂移每阶段作为信息性指标发布。`
---
## 3. What P2 expects P1 to have left in place
P2 row, `ROADMAP.md:18`, deliverables verbatim:
> `MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门;`MGPipeRenderStateSpans` + G7 setter 一致性测试;`CsoCache`(64 项,键 = pipeline 子集);`create/bind_render_state` + `set_dynamic_state`(Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state`、`set_patch_state`、`set_vertex_attrib_defaults`;`set_residual_value_state` + `ResidualValueBlock` 绊线;**第一片 Track H**:Espryt 0b(`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4(`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针);`MOBILEGL_PIPE_LEGACY_MEMOS`;补 `FramebufferSrgb`/`DepthClamp` 存储
P2's acceptance gate, verbatim:
> 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照
Concretely, P1 must not paint P2 into a corner on:
1. **A `{pull, push}` A/B arm that is real.** P2 compares integration × 2 backends × `{pull, push}` by name. That requires the `MOBILEGL_PIPE_PUSH` bitmap to genuinely select per subsystem, per field (phase B: `> 填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位`, `ARCHITECTURE.md:345`). P1's accessors and fill points must be structured so a *field-granular* yield is possible, not just a whole-build switch. Note the honesty caveat P1 inherits (`ARCHITECTURE.md:367`): after phase C the bitmap stops being a true A/B, which is why P2 also introduces the compile-time `MOBILEGL_PIPE_LEGACY_MEMOS` (default ON) arm.
2. **`SyncRenderState` untouched.** `ARCHITECTURE.md:348`-region §5.3, `ARCHITECTURE.md:347`: `> **Espryt 的 `SyncRenderState`(693 行)拿到的仍是 `const RenderStateParameters&`,单 `Uint16` 早退、三段 memcmp 一行不动**`. GO/NO-GO item 2 (`ROADMAP.md:47`) restates it: `> 两个后端上都已推送的渲染状态,`SyncRenderState` 693 行一行未动`. **P1's `PipeInputs::GetRenderStateParameters()` must therefore return `const RenderStateParameters&`** — a by-value or restructured return would force a `SyncRenderState` edit and break GO/NO-GO item 2. The doc's phase-A comment says exactly this: `// 阶段 A:类型与后端今天读到的完全一致` (`ARCHITECTURE.md:328`).
3. **Server-side working `RenderStateParameters`.** `ARCHITECTURE.md:347`: `> server 侧:每 context 一份 working `RenderStateParameters`(1168 B),`bind` 与 `set_dynamic_state` 各把自己的 chunk 散射进去。` P1's `PipeInputs` is where that working copy will live in P2. Size it for that (`~20 KB`, `ARCHITECTURE.md:326`).
4. **The five aggregate generations are new frontend fields P2 adds** (`ARCHITECTURE.md:164-169`, §5.2 table): `VertexArrayState::m_anyVaoAttributeGeneration`, `FramebufferState::m_anyAttachmentGeneration`, `TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration`, `BufferState::m_anyBufferChangeGeneration` — all marked 新增, landing "on existing bump points (~20 lines)" (`ARCHITECTURE.md:170`). P1 must not restructure those mutators in ways that scatter the bump points.
5. **The dirty-surface mapping file.** P1 creates it (73 entries); P2 turns the first mapping into a gate (`ROADMAP.md:18`) — see risk R2 for the phase disagreement.
6. **Residual value block shape.** See §2.7: `GetRenderStateParameters`, `GetPixelStoreParameters`, capability bits and the three patch fields must be reachable as whole-struct values.
7. **CSO cache and content addressing.** `ARCHITECTURE.md:63` (§2.3): the client-side content-addressed CSO maps with LRU caps (render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader tied to `ProgramObject` lifetime). P2 also needs the negative control bit inside `MOBILEGL_PIPE_PUSH` that disables CSO content addressing (`Config.h:320-325`, `ARCHITECTURE.md:367`, `ROADMAP.md:52`), so P1's use of the bitmap must leave that bit free and meaningful.
8. **Track H's first slice needs handles to be a `PipeInputs` field type.** Phase C (`ARCHITECTURE.md:346`) turns `SharedPtr<frontend object>` fields into `MGPipeHandle` + POD descriptors. Handles already exist (`MobileGL/MG_Pipe/MGPipeHandles.h`; composite-shader band at `MGPipeHandles.h:88-90`: `kMGPipeShaderCsoSlotLimit = 1u << 20`, `kMGPipeShaderCsoCompositeSlotBase = limit - (limit >> 4)`, predicate `MGPipeIsCompositeShaderSlot` at `:92-94`). P1's object-typed accessors should be shaped so the return type can be swapped without re-touching all 167 read points a second time.
9. **The eight validate entry points** are the frame P2's tracker hangs on (`ARCHITECTURE.md:153`). P1's fill points should be those same eight shapes, generated, not hand-placed ad hoc.
10. **G7 chunk table is frozen in P2, not P1** (`ARCHITECTURE.md:161`: `带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2`), and `Coverage.def:72-74` warns `GetProvokingVertexMode` is not in `ComputePipelineStateHash` today `/* recorded here so the G7 chunk table has to answer for it before it freezes */`. P1 must keep that accessor visible in the field list (it is, field #39).
---
## 4. Commit / CI discipline (通用纪律) and the GO/NO-GO items P1 delivers
### 4.1 通用纪律, `ROADMAP.md:7` — verbatim (applies to **every commit**)
> 默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门);**每个门必须能因它存在的理由变红**;Windows 机器不是正确性门;设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议;每阶段出口跑一次五部分门;每阶段性能判据是**逐线程 CPU 时间**。
Enforcement in the tree (CI job `pipe-gates`, `.github/workflows/test.yml:835-887`, deliberately independent of `build-linux` per `test.yml:838-839`):
- `test.yml:850-853` — `python3 scripts/gen_pipe.py` then `git diff --exit-code -- MobileGL/MG_Pipe/generated`.
- `test.yml:862-869` — stdio-instrumentation grep gate over `MobileGL/MG_Backend` and `MobileGL/MG_State`: `fprintf(stderr|stdout`, `printf(`, `puts(`, `std::cout|cerr`. `test.yml:857-861` says the gate `starts with no exceptions, and any addition to it needs a reason in the pull request rather than a quiet whitelist entry`.
- `test.yml:871-874` — `python3 scripts/gen_pipe_dirty_surface.py --summary`, with the comment `# Informational: … It becomes a gate in P1, when the mapping file exists to diff against.`
- `test.yml:876-887` — `check_doc_citations.py`, currently `|| true` (warning only), `# It becomes --strict when the documents settle.` **P1 edits these docs, so keep `file:line` citations correct** — the lint checks exactly the `file:line` references in `docs/Disaggregated/*.md`.
- Separate job `include-graph-check` (`test.yml:332-349`): `python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test --require-all`. `test.yml:330-331` restates the discipline in English: `The script's own --self-test is always on: a negative control that stopped tripping fails the job, because a gate that cannot go red is not a gate (ROADMAP.md:7).`
Also binding on P1 commits (from repo memory / project convention, consistent with the docs): single-line `[Type] (Scope): description` messages; no `Co-Authored-By`.
CTS turnaround (`ROADMAP.md:34`) — verbatim: `> **CTS 周转单独计价**:`gl44to46` 约 56,271 例。逐阶段只跑该阶段可能影响的具名块(P4a `packed_pixels`、P3b/P4b `texture_*`/`shader_image_*`、P9 `transform_feedback*`);完整 caselist 只在五个架构边界(P0.5、P3a、P4a、P3b/P4b、P13)与每次合并 `dev` 之前跑,放 CI 不放关键路径。若周转仍主导排期,加宽估时而不是削弱门。` — **P1 is not one of the five full-caselist boundaries**, but a merge to `dev` still triggers a full run.
### 4.2 The day-43 GO/NO-GO list, `ROADMAP.md:44-52` — verbatim, P1's contribution marked
> 手上必须有:
>
> - [ ] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧) ← **P1 delivers this one**
> - [ ] 两个后端上都已推送的渲染状态,`SyncRenderState` 693 行一行未动
> - [ ] 两片 Track H 的实测单位成本(Espryt 0b、Magma 子系统 4)
> - [ ] 两台设备(Adreno 830 `35d0befa`、Mali `3B159D009VZ00000`)reboot-clean 配对的逐线程 CPU 时间增量,p50 与 p99
> - [ ] tracker 每 draw 的**绝对 ns**(上限从设备基线定:稳态每 draw 6.5–9.3 次 accessor + memo 探测,见 `MEASUREMENTS.md`)
> - [ ] Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)
> - [ ] 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开
Verdict criteria, `ROADMAP.md:56` — verbatim: `> - **继续**:两台设备 p50 与 p99 逐线程 CPU 增量都不为负;tracker 绝对 ns 在上限内;Track H 单位成本不超出估计的 50%。按两条跑道推进。`
**The per-draw per-field semantic equivalence proof** = gate 13.2-② run to zero divergence over 40 traces + the whole integration suite, with both negative controls (corrupt a snapshot field → red; drop a field from `glGenerateMipmap`'s fill table → poison Fatal on that verb). It is P1's single most important artifact and it is the first line of the GO/NO-GO list.
The absolute-ns ceiling P1's work is later measured against comes from `MEASUREMENTS.md:50-60` (the four-trace table) and `MEASUREMENTS.md:64`: `> **真机稳态动态 accessor 成本是每 draw 6.5–9.3 次**(预测区间 10–25 的下沿;llvmpipe 的 15.5/20.7 是 memo 冷的)。推送要打败的是 ~8 次 accessor + memo 探测,不是 124/169 的静态调用点数。GO/NO-GO 的 tracker 绝对 ns 上限从这里定。` The measured per-draw accessor counts: Espryt 9.28 / Magma 8.56 on `minecraft-1.21.4-in-world` (`MEASUREMENTS.md:52-53`); Espryt 8.44 / Magma 6.53 on `improved-transparency-minecraft-26.3` (`MEASUREMENTS.md:56-57`). `MEASUREMENTS.md:60` warns `accessor-calls` is a **lower bound** (static counting at ~10 hot entry points; site list is a contract at `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`).
---
## 5. Open questions and caveats that touch P1
From `ROADMAP.md:70-90` ("仍然开放的问题"), the ones that bear on P1:
- **#1** (`ROADMAP.md:74`) — `> **client 侧 dirty 走查的真实每 draw CPU 代价。** 拉取基线已实测为每 draw 6.5–9.3 次 accessor + memo 探测;推送要在这个数字下净减少。P2 的头号数字,逐线程 CPU + 绝对 ns,两台设备。` P1's accessor design determines whether this is measurable at all; P1 should keep the accessor call path cheap enough that the P2 number is not dominated by P1 scaffolding.
- **#5** (`ROADMAP.md:78`) — `> **`FramebufferSrgb` / `DepthClamp` 的拍板。** 事实已清(无存储、`glEnable` 静默吞掉、六个读点恒 false、41 个 fixture 无一开启);建议在 chunk 表冻结前补真存储并把 `FramebufferSrgb` 划进 pipeline 半边。由计划所有者拍板,**拍板前不冻结 chunk 表**。` The six `FramebufferSrgb` read points are inside `MG_Backend` and are read points P1's `sed` will touch; today they consume a compile-time `false` (`MEASUREMENTS.md:84`). P1 must alias them without silently changing them, and must not freeze the chunk table.
- **#7** (`ROADMAP.md:80`) — `> **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross pass 流水线、ESSL 转译缓存、格式处理器、POST 探针;client 需要 glslang phase A/B 与反射层。P0.5 解决了 `ProgramObject.h` 一处,`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。` Unaudited; may surface as an unexpected include when P1 touches backend TUs.
- **#14** (`ROADMAP.md:87`) — `> **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。`
- **#15** (`ROADMAP.md:88`) — `> **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口**(compute 写的 indirect buffer)。独立 `dev` 问题,拆分不得借机顺手修。` **Directly aimed at P1's per-site ownership table**: the `*IndirectCount` rows must record today's behaviour verbatim, bug included. Do not "fix" it in P1.
- **#17** (`ROADMAP.md:89-90`) — `> **create-indirect fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;它是 P3a/P8 验收清单里的用例,需要先在 `dev` 上修。` P1's "40 trace" gate inherits this: `MEASUREMENTS.md:96` — `minecraft-1.21.1-neoforge-create-indirect-in-world` fails on **both** devices (Adreno: Espryt black frame after ~4.5 min, Magma `VK_ERROR_DEVICE_LOST` on texture upload submit; Mali: SSIM 0.85 / 0.45). Expect it to be excluded or waived when running the 40-trace verify sweep.
Other caveats in the docs that touch P1:
- `ARCHITECTURE.md:3` — `> 落地状态以 `feat/disaggregated@458ccde1` 为准` while `README.md:3` says P0.5 landed at `5d99ee43`; `ROADMAP.md:3` still says `P0 已落地(feat/disaggregated@458ccde1)`. The docs' own commit anchors are inconsistent; the ROADMAP table cells are the current truth.
- `ARCHITECTURE.md:172` measured surface: `> 926 次 mutator 调用落在 73 个不同 mutator 上` vs `MEASUREMENTS.md:81` which says `73 个不同 mutator(`RecordError` 一项就占 836 次)` and `92 次(36 个即时发布点、7 个 mutator …)`. `ARCHITECTURE.md:172` says 92 calls / 7 mutators without the 36. Consistent enough; use `MEASUREMENTS.md:81` for the full shape. Scanner limits are stated at `scripts/gen_pipe_dirty_surface.py:196-198`: it matches braced bodies textually, a mutator inside a lambda is attributed to the enclosing function, and a mutation published through a helper reads as deferred.
- `ARCHITECTURE.md:33` (§1.3) — the `Init.cpp` hook branch lands in P5, **not** P1: `> (分支在 P5 落地;P0 的 `Init.cpp` 尚未含它。)`
- `MobileGL/MG_Pipe/Coverage.def:16-17` says UNMAPPED rows are `allowed in P0 and merely counted, ZERO from P5 onward`, while gate 13.2-⑤ (`ARCHITECTURE.md:505`) says `G6 重生成 0 UNMAPPED` as a standing gate and `MEASUREMENTS.md:82` reports **0 UNMAPPED already**. No action for P1 beyond keeping it at zero when `GetBufferBindingSlot` is split by target (`Coverage.def:36-40`).
- `MEASUREMENTS.md:92-95` harness traps that will bite a P1 device run: the trace app never reaches `MobileGL::DestroyImpl` so `MOBILEGL_PIPE_STATS_FILE` JSON never lands on device (only the periodic `MGPipe stats:` line in `mobilegl.log`); `run_android_retrace_local.py` `rmtree`s one shared result root per tree, so two devices must run **serially** from one tree; `--env` values containing `/data/...` need `MSYS_NO_PATHCONV=1`.
---
## 6. Risks / discrepancies I found (read these before starting)
**R1 — the "293 sites" figure does not match this tree.** `ROADMAP.md:17` and `ARCHITECTURE.md:344` both say `sed` 293 sites. Measured here: `grep -rno "MG_State::pGLContext->" MobileGL/MG_Backend/` → **277** occurrences (274 distinct lines), distributed: `DirectGLES/DirectGLES.cpp` 91, `DirectGLES/Managers.cpp` 15, `DirectGLES/MultiDraw.cpp` 5, `DirectGLES/Utils.cpp` 2, `DirectVulkan/BackendObject_DirectVulkan.cpp` 2, `DirectVulkan/DirectVulkan.cpp` 12, `DirectVulkan/Renderer/UniformManager.cpp` 14, `DirectVulkan/Renderer/VkClearManager.cpp` 1, `DirectVulkan/Renderer/VkRenderPassManager.cpp` 3, `DirectVulkan/Renderer/VkTextureManager.cpp` 2, `DirectVulkan/Renderer/VulkanRenderer.cpp` 127. The **58 non-arrow lines matches exactly** (`DirectGLES/DirectGLES.cpp` 1, `DirectGLES/Managers.cpp` 8, `DirectVulkan/BackendObject_DirectVulkan.cpp` 2, `DirectVulkan/DirectVulkan.cpp` 35, `DirectVulkan/Renderer/UniformManager.cpp` 9, `DirectVulkan/Renderer/VertexInputStateFactory.h` 1, `DirectVulkan/Renderer/VulkanRenderer.cpp` 2). Likely the 293 was counted at a different commit or with a different spelling. **Re-count and update `ROADMAP.md:17` / `ARCHITECTURE.md:344` as part of P1**, since these are cited numbers under the doc-citation lint.
**R2 — the dirty-surface mapping gate is assigned to two different phases.** `ARCHITECTURE.md:172` (`P1 起成为门`), `ARCHITECTURE.md:568` (`信息性,P1 成门`), `scripts/gen_pipe_dirty_surface.py:21-22` and `.github/workflows/test.yml:872-873` all say **P1**. `ROADMAP.md:18` puts `gen_pipe_dirty_surface.py 首轮映射成门` in the **P2** deliverables and the P1 row does not mention it. Resolve before planning P1's day budget — 73 mapping entries is real work and the P1 row's 10–13 days does not obviously include it.
**R3 — `MOBILEGL_PIPE_PUSH` is simultaneously a preprocessor condition and a runtime `Uint64` bitmask.** `ARCHITECTURE.md:334` uses `#if MOBILEGL_PIPE_PUSH` to switch `MGB_CTX`; `MobileGL/Config.h:326` defines `Uint64 PipePush = 0;` and `MobileGL/ConfigLoader.cpp:245` reads it from the environment; `ARCHITECTURE.md:345` says the fillers yield per field **according to the bitmap**. These cannot both be the same symbol. P1 must pick the spelling (e.g. a CMake `MOBILEGL_PIPE_PUSH` selecting the alias arm, with `MG_Config::Features.PipePush` selecting subsystems at run time) and write it down; leaving it ambiguous is how P2's `{pull, push}` by-name A/B silently degenerates into a single arm.
**R4 — `MGB_CTX`'s `#else` arm spells `pGLContext`.** Purity gate C greps the bare token under `MG_Backend/` (`ARCHITECTURE.md:502`, and `ARCHITECTURE.md:349`'s last sentence says so explicitly). Define the macro outside `MG_Backend/` (e.g. in `MG_Pipe/`) so the pull arm's spelling does not live where the gate looks. Gate C is not required green until P13, but placing it wrong makes P13 a second migration.
**R5 — sticky fields are unaudited.** All 61 entries of `kMGPipeInputFieldSticky[]` are `false` today (`PipeFilled.inc:163-225`). Any `true` P1 introduces is a field the poison stops protecting (`PipeFilled.inc:160-162`). Candidates that will *look* sticky and probably are not: `GetTextureContextId`, `GetTextureBindGeneration`, `GetSamplingResolutionGeneration`, `GetTransformFeedbackGeneration`, `GetPipelineStateVersion`, `GetRenderStateParametersVersion`. Each `true` needs a written argument in the commit.
**R6 — `MGPipeTypes.h` still includes `MG_Backend/BackendObject.h`.** `MobileGL/MG_Pipe/MGPipeTypes.h:25-34` documents this as P0.5 debt: `MGPCaps` embeds `DynamicBackendParameters`, so purity gate A asserts `MGPipeValueTypes.h` instead of `MGPipeTypes.h`, and `// The caps block needs fixed-width members before it can move (a type change, not a move): P1/P7.` (`MGPipeTypes.h:32`). P1 may be expected to do the fixed-width conversion; it is **not** in the ROADMAP P1 row. Confirm scope with the plan owner rather than discovering it when gate A is re-tightened.
**R7 — the four memcmp-fallback types.** `scripts/gen_pipe.py:167-172` (`MEMCMP_FALLBACK_TYPES = {RenderStateParameters, PixelStoreParameters, DynamicBackendParameters, MGHostSpan}`) and `PipeVerify.inc:232-237` both say P1 gives them field lists. Not listed in the ROADMAP P1 row either, but it is a *precondition of "zero false positives"* for the P1 verify gate — `RenderStateParameters` is precisely the struct documented to false-DIFFER under memcmp (`PipeFields.def:11-13`). Treat it as part of deliverable 5.
**R8 — verify's cost.** 5–10× slowdown over 40 traces + the whole integration suite, on two contended devices, with the serial-run constraint from `MEASUREMENTS.md:93`. Budget device time; the P1 row's 10–13 days does not price CTS turnaround (`ROADMAP.md:34`).
---
## 7. Quick reference: files P1 touches or creates
| Path | Status | Role in P1 |
|---|---|---|
| `MobileGL/MG_Backend/MGPipe/PipeInputs.h` | **create** | The 61-field struct, Espryt 32 / Magma 55 accessors, poison state (`ARCHITECTURE.md:325-340`, `:553`) |
| `MobileGL/MG_Backend/MGPipe/MGPipeImpl_DirectGLES.cpp` | **create** | `ARCHITECTURE.md:553` |
| `MobileGL/MG_Backend/MGPipe/MGPipeImpl_DirectVulkan.cpp` | **create** | `ARCHITECTURE.md:553` |
| `MobileGL/MG_Backend/DirectGLES/*`, `DirectVulkan/*` | edit | 277 arrow sites + 58 non-arrow lines (R1) |
| `MobileGL/MG_Impl/GLImpl/**` | edit | ~93 (measured 91-line) boundary sites get generated validate/fill calls |
| `MobileGL/MG_Pipe/PipeFields.def` | edit | field lists for the four memcmp-fallback types (R7) |
| `MobileGL/MG_Pipe/Coverage.def` | edit | split `GetBufferBindingSlot` by `BufferTarget` (`Coverage.def:36-40`) |
| `MobileGL/MG_Pipe/generated/*.inc` | regenerate + commit | CI diffs them (`test.yml:850-853`) |
| `scripts/gen_pipe.py` | edit | G5 fill-table emission, drop fallback types |
| `scripts/gen_pipe_dirty_surface.py` + new mapping file | edit / create | 73-entry mapping; gate (R2) |
| `.github/workflows/test.yml` | edit | third CI mode (`MOBILEGL_PIPE_VERIFY`); promote dirty-surface to a gate |
| `CMakeLists.txt` | edit | `MOBILEGL_PIPE_VERIFY` build option (`ARCHITECTURE.md:583`) |
| `MobileGL/Config.h` / `ConfigLoader.cpp` | already landed | runtime `PipePush` / `PipeVerify` (`Config.h:320-332`, `ConfigLoader.cpp:245-246`) |
| `docs/Disaggregated/*.md` | edit | keep `file:line` citations valid (`check_doc_citations.py`, `test.yml:876-887`) |