Files
MobileGL/docs/Disaggregated/notes/p2/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

433 lines
54 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.
# P2 specification, extracted from `docs/Disaggregated/` (read-only scout)
Worktree: `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`, branch `feat/disaggregated @ e7a6a72f`
(`git log --oneline -3` → `e7a6a72f [Docs] (Disaggregated): record the P1 landing and what its verify lane found`).
Documents opened in full:
| file | lines | size |
|---|---|---|
| `docs/Disaggregated/README.md` | 57 | 5,048 B |
| `docs/Disaggregated/ARCHITECTURE.md` | 601 | 90,120 B |
| `docs/Disaggregated/ROADMAP.md` | 90 | 21,462 B |
| `docs/Disaggregated/MEASUREMENTS.md` | 143 | 16,695 B |
All docs are in Chinese; requirement wording is quoted verbatim (Chinese) with a translation only where it aids the implementer. Every claim below carries the `file:line` I actually opened.
Status line, `README.md:3`:
> 状态:**P0、P0.5、P1 已落地**(`feat/disaggregated`,基线 `dev@50fb1343`)。下一步 P2,第 43 天 GO/NO-GO。见 `ROADMAP.md`。
(Note the baseline drift across docs: `README.md:3` says baseline `dev@50fb1343`; `ROADMAP.md:3` and `ARCHITECTURE.md:3` still say "P0 已落地(`feat/disaggregated@458ccde1`)"; `81b17c0b` is used only as the *performance* anchor, `ARCHITECTURE.md:504`, `ROADMAP.md:30`.)
---
## 1. The P2 row of the ROADMAP phase table, and every other P2 mention
### 1.1 P2 row — verbatim (`ROADMAP.md:18`)
Table header is `ROADMAP.md:13`: `| 阶段 | 天 | 落地什么 | 验收门 | 依赖 |`.
**阶段**: `**P2** 渲染状态 CSO + 第一片 Track H + 残余值块`
**天**: `18–26`
**落地什么 (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` 存储
Itemised (11 deliverables):
1. `MG_Impl/Pipe/Tracker` — dirty bits, 5 aggregate generations, suppressor skeleton.
2. `gen_pipe_dirty_surface.py` first-round mapping **promoted to a gate**.
3. `MGPipeRenderStateSpans` + the G7 setter-consistency test.
4. `CsoCache` — 64 entries, key = the pipeline subset.
5. `create/bind_render_state` + `set_dynamic_state`; **Espryt `SyncRenderState` not one line changed**; Magma `ComputePipelineStateHash` / `GetOrCreatePipeline` / `ApplyDynamicDrawStateTail` re-sourced from the CSO and the dynamic payload.
6. `set_pixel_pack_state`
7. `set_patch_state`
8. `set_vertex_attrib_defaults`
9. `set_residual_value_state` + the `ResidualValueBlock` tripwire.
10. First Track H slice — Espryt 0b and Magma subsystem 4 (detail in §2.7 below).
11. `MOBILEGL_PIPE_LEGACY_MEMOS`; add real storage for `FramebufferSrgb` / `DepthClamp`.
**验收门 (acceptance gate), verbatim:**
> 集成 × 2 后端 × {pull, push} 逐名相同;40 trace push 下 SSIM ≥ 0.99 双后端;verify 零分歧;`HandleRecycleScenario` 绿且重键前红;G7 测试绿且拿掉一个字段能红;两台设备配对逐线程 CPU p50/p99 不差且 tracker 绝对 ns 在上限内;Blaze3D blend-toggle 微基准;CSO 内容寻址关闭的负面对照
Itemised (7 gate clauses):
1. integration × 2 backends × {pull, push} — **identical test-name-by-test-name**.
2. 40 traces under push: SSIM ≥ 0.99, both backends.
3. verify mode: zero divergence.
4. `HandleRecycleScenario` green **and red before the rekey**.
5. G7 test green **and removable-field negative control turns it red**.
6. Two devices, paired: per-thread CPU p50/p99 not worse **and** tracker absolute ns within the ceiling.
7. Blaze3D blend-toggle microbenchmark; negative control with CSO content addressing switched off.
**依赖**: `P1`.
### 1.2 Every other P2 mention
- `ROADMAP.md:9` — track membership: `两条跑道:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止且 monolith 严格好于起点;**IPC 跑道** P5 → P6 → P9 → P10 → P11 → P12。`
- `ROADMAP.md:17` (P1 row) — two rewrites were **deferred into P2**: `pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2)`. Same deferral restated at `ARCHITECTURE.md:344` (Phase A proof column: `(空守卫/三元的重写推迟到 P2)`). Concretely, the P1 mechanical pass left ~7 empty guards (`空守卫`) and 3 ternaries (`三元`) among the 58 non-arrow conversions; P2 rewrites them.
- `ROADMAP.md:19` (P3a) — `依赖 | P2`.
- `ROADMAP.md:24` (P7) — `§5.5 其余 10 个子系统(子系统 1、4 已在 P2)`; dependencies `P0.5、P2;可与 P5/P6/P8 并行`. **Doc discrepancy:** `ARCHITECTURE.md:202` §5.5 is "sampler view 在 client 侧解析" and contains no 12-subsystem list — the "§5.5 subsystem" numbering is a dangling reference to the deleted `PLAN.md` (see Appendix A).
- `ROADMAP.md:32` — cumulative low-end day count: `P1 25 → P2 43 → P3a 61`. P2 therefore spans days 26–43 (low-end).
- `ROADMAP.md:39` — `- **第 43 天(P2 出口):GO/NO-GO**。`
- `ROADMAP.md:57` — NO-GO branch: `**不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev``.
- `ROADMAP.md:58` — `真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天`.
- `ROADMAP.md:74` (open question 1) — `P2 的头号数字,逐线程 CPU + 绝对 ns,两台设备。`
- `ARCHITECTURE.md:81` (G7 row of the generator table) — `G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2`.
- `ARCHITECTURE.md:147` — the whole of §5 is scoped `## 5. 前端 state tracker(`MG_Impl/Pipe/Tracker`,P2 起)`.
- `ARCHITECTURE.md:187` — `MG_Pipe/MGPipeRenderStateSpans.{h,cpp}`(P2).
- `ARCHITECTURE.md:359` — `所以 P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)``, and `只在 P2..P13 存在`.
- `ARCHITECTURE.md:552` — build layout: `MobileGL/MG_Impl/Pipe/ Tracker、SlotAllocator、CsoCache、HostResolve、CompositeResolver [P2+]`. (Only Tracker/SlotAllocator/CsoCache are P2; HostResolve is P8 per `ROADMAP.md:25`, CompositeResolver is P4a per `ROADMAP.md:20`.)
- `ARCHITECTURE.md:580` — switch table: `| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON(P2..P13) | 计划(编译期臂) |`.
- `ARCHITECTURE.md:367` — why that switch exists (quoted in §2.7.3 below).
- `MEASUREMENTS.md:115` — `其中 8 行是静态过近似(代码路径可达但通道未跑到),过近似会让那对 (类, 字段) 的 poison 永久失效,**P2 收紧填充表时先复查这 8 行**。` — **an explicit P2 work item not in the ROADMAP row**: when P2 tightens the fill table, first re-check those 8 statically over-approximated (verb-class, field) rows.
- `MEASUREMENTS.md:117` — push-on-mutation (`MGP_NOTE_MUTATION`, `MG_Pipe/PipeMutation.h`) `…"推送块在每次读取时都等于活上下文"这条不变式因此字面成立,也正是 P2 tracker 需要的形状。` — the P1 solution is stated to be exactly the shape the P2 tracker needs.
---
## 2. What ARCHITECTURE.md says (the P2 design surface)
### 2.1 §5.1 Push happens at validate, before the verb — not in the GL setter (`ARCHITECTURE.md:149-155`)
`ARCHITECTURE.md:151` (the reason, verbatim):
> Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住(Espryt 代码自己标它为最热路径),per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium `st_validate_state`。
`ARCHITECTURE.md:153` — **eight validate entry points**, generated from the `kCtxVerb`/`kCtxObject` entries of `PipeCalls.def`:
`ValidateForDraw`(20 个 draw 入口)、`ValidateForDispatch`、`ValidateForClear`、`ValidateForBlitOrCopy`、`ValidateForTextureOp`(GenerateMipmap / CopyTex* / BindImageTexture)、`ValidateForReadback`、`ValidateForXfbSpan`、`ValidateForQuery`.
Why eight and not four: `MG_Impl` 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、query/sync)很多自己就读 `pGLContext`.
`ARCHITECTURE.md:155` (verbatim, a hard rule):
> **只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。
### 2.2 §5.2 Dirty bits: zero new bookkeeping for value class, five new aggregate generations for object class (`ARCHITECTURE.md:157-174`)
Table at `ARCHITECTURE.md:161-169` (dirty bit | class | shutter source):
| dirty 位 | 类 | 快门来源 | line |
|---|---|---|---|
| `NEW_RENDER_STATE` / `NEW_PIPELINE_STATE` | 值 | `m_version` / `m_pipelineStateVersion` | 161 |
| `NEW_PIXEL_PACK`、`NEW_PATCH_STATE`(`BitwiseEqual`,NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS`、`NEW_VERTEX_ELEMENTS`(VAO config version) | 值 | 既有计数器 | 162 |
| `NEW_SHADER`、`NEW_SHADER_BINDINGS`、`NEW_GLOBAL_CONSTANTS` | 值 | link/image-unit/backend-state/block-binding/uniform-write-set/UBO-content 版本 | 163 |
| `NEW_VERTEX_BUFFERS` | 对象 | **`VertexArrayState::m_anyVaoAttributeGeneration`**(新增)→ 命中后走 32 属性前缀 | 164 |
| `NEW_INDEX_BUFFER` | 对象 | 索引 slot 版本 + 绑定对象 `{slot,gen}` | 165 |
| `NEW_FRAMEBUFFER` | 对象 | **`FramebufferState::m_anyAttachmentGeneration`**(新增)+ 对象/slot 版本 → 重算 `ContentHash` | 166 |
| `NEW_SAMPLER_VIEWS`、`NEW_SAMPLERS`、`NEW_SHADER_IMAGES` | 对象 | **`TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration`**(新增)+ bind/sampling-resolution generation → 走 `GetMaxTouchedUnit()` 前缀、重算集合 hash | 167 |
| `NEW_CONST_BUFFERS` / `NEW_SHADER_BUFFERS` / `NEW_SO_TARGETS` | 对象 | **`BufferState::m_anyBufferChangeGeneration`**(新增)→ 走 `GetTouchedBindPointCount()` 前缀 | 168 |
**The five aggregate generations** are therefore: `VertexArrayState::m_anyVaoAttributeGeneration`, `FramebufferState::m_anyAttachmentGeneration`, `TextureState::m_anyTextureContentGeneration`, `TextureState::m_anyTextureParamsGeneration`, `BufferState::m_anyBufferChangeGeneration` (two of the five live in `TextureState`).
`ARCHITECTURE.md:170` (verbatim, the rationale + the cost claim):
> 五个聚合世代全部落在既有 bump 点上(约 20 行),把对象类组的快门从"每 validate 走查 192 单元 / 84×4 绑定点 / 32 属性 / 40 attachment"降成一次 `Uint64` 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了",这正是 Magma 不得不用有损 `sampledContentSum` 的原因)。
`ARCHITECTURE.md:174` — wrap-around discipline:
> 三个回绕 `Uint16` 在 tracker 边界加宽(`m_lastPushed[]` 是 tracker 自己的字段,不改 `MG_State`);回绕在 tracker 本地无害(多一次重推,永不漏推),且被集合 hash 抑制器吞掉。
### 2.3 §5.3 Render state design D-B1 (`ARCHITECTURE.md:176-190`)
Interface shape, verbatim (`ARCHITECTURE.md:178-182`):
```
create_render_state(cso, MGPBlobRef pipelineSubsetChunks) // 只带 pipeline 子集
bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B
set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化 chunk
```
- **Whole blob on the wire** (`ARCHITECTURE.md:184`): `整块的理由:`RenderStateParameters` 是平凡可复制 POD,Espryt 自己 `static_assert` 并做 head/blend/tail 三段 memcmp,**字段顺序承重**(`ScissorBoxWrittenMask`、`ClipDistanceEnabledMask` 故意放在 tail 段);拆成 blend/depth-stencil/rasterizer 三个 CSO 要手工维护 ~150 字段划分表且无绊线。`
- **CSO identity = the pipeline subset only** (`ARCHITECTURE.md:185`): `子集身份的理由:整块内容寻址会让 `glViewport`/`glScissor`/`glBlendColor`/`glClearColor` 每次铸造新 CSO、冲掉 server 的 pipeline memo——`RenderState.h` 记录的那次回归。`RenderState.cpp` 里 viewport/scissor/line-width 族只 `++m_version`,`SET_CAPABILITY` 与 pipeline 相关 setter 才 `BumpVersions()`。`
- **The dynamic subset** (`ARCHITECTURE.md:186`, verbatim, this is the enumeration to implement): `动态子集:viewport、scissor、depth range、blend color、line width、polygon offset、stencil ref/write mask、clear 值、sample coverage、hints、point-size 族。`
- **`MGPipeRenderStateSpans` — the split is written in exactly one place** (`ARCHITECTURE.md:187`, verbatim): `划分只写在一处:`MG_Pipe/MGPipeRenderStateSpans.{h,cpp}`(P2)的 chunk 表 + `MGPipeComputePipelineSubsetHash()`,从 Magma 的 `ComputePipelineStateHash` 搬来,client 与两个后端共用;G7 的 `MG_Test` 遍历每个 `RenderState` public setter,断言 `pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变`。`
- The generated member-name table already exists: G7 → `PipeSpanTable.inc`, `render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2` (`ARCHITECTURE.md:81`). So P2 adds the `offsetof`-carrying chunk table on top of the 24 names, plus the test.
- **Server side** (`ARCHITECTURE.md:188`, verbatim — contains the "not one line" requirement): `server 侧:每 context 一份 working `RenderStateParameters`(1168 B),`bind` 与 `set_dynamic_state` 各把自己的 chunk 散射进去。**Espryt 的 `SyncRenderState`(693 行)拿到的仍是 `const RenderStateParameters&`,单 `Uint16` 早退、三段 memcmp 一行不动**;Magma 的 pipeline memo 键是 `cso.slot`,动态尾巴仍走 `ApplyDynamicDrawStateTail`。Espryt 的 head/blend/tail 划分(驱动侧增量)与 pipeline/dynamic 划分(线上与身份)是两回事,并存、各有绊线。` (The per-context working copy is restated at `ARCHITECTURE.md:378`, §10.1.)
- **Client value-fetch order** (`ARCHITECTURE.md:189`, verbatim — this is the CsoCache lookup algorithm): `client 取值顺序:`m_pipelineStateVersion` 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 pipeline 子集算 xxHash(~25-30 字,Magma 今天就在算)→ CSO map 探测 → 命中发 12 B bind,未命中发变化 chunk 的 create 再 bind;`m_version` 变而子集未变 → 只发 `set_dynamic_state`(~200 B)。`
- **`FramebufferSrgb` / `DepthClamp`** (`ARCHITECTURE.md:190`, verbatim): ``FramebufferSrgb` 与 `DepthClamp` 今天**没有存储**(`glEnable` 被静默吞掉且不报错,六个后端读点恒为 false);chunk 表冻结前要补真存储并把 `FramebufferSrgb` 划进 pipeline 半边(它改变 attachment/blend 的解释)——待拍板,见 `ROADMAP.md`。`
Payload sizes (`ARCHITECTURE.md:70` = §4.1 row): `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` = `48 / **12** / 32`, "§5.3". Confirmed by the measured `static_assert` list, `MEASUREMENTS.md:86`: ``MGPBindRenderState` **12**` and `ResidualValueBlock` **1248**(其中 `RenderStateParameters` 1168)`.
Call-catalogue placement: `SetDynamicState`(B) is the first of the 17 `kCtxState` entries (`ARCHITECTURE.md:95`); `CreateRenderState`/`DeleteRenderState`/`BindRenderState` are among the 13 `kCtxCso` entries (`ARCHITECTURE.md:94`). Flag `B` = `kHasBlob` (`ARCHITECTURE.md:99`).
CSO cache capacities for all five classes (`ARCHITECTURE.md:63`, §2.3, verbatim):
> CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
CSO handle kinds and reserved handles: `ARCHITECTURE.md:38-39` (`RenderStateCso` is one of the 13 kinds; `{0,0}` = null; `{0,1}` of `Framebuffer` = default FB; top 1/16 of `ShaderCso` slot space reserved for program-pipeline composites, `MobileGL/MG_Pipe/MGPipeHandles.h:88-90`).
### 2.4 §5.4 Validation invariant, coalescing and the suppressors (`ARCHITECTURE.md:192-200`)
`ARCHITECTURE.md:194` — the normative invariant (D-B3), verbatim:
> 规范(D-B3):**一条 verb 的全部 `set_*`/`bind_*` 必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态惰性特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,`set_*` 之间没有顺序要求。** 推荐实现顺序(framebuffer → program → 纹理/sampler/image/buffer/global constants → render state/dynamic → vertex elements/buffers/index/attrib defaults → patch/XFB → verb)只是代码组织,不是契约。
`ARCHITECTURE.md:196` — `create_shader_state` 从编译池的终止 continuation 发出(不是从 draw).
`ARCHITECTURE.md:198` — **the four coalescing rules**, verbatim (rule 4 is the suppressor P2 must scaffold):
> 四条合并规则:整块结构优于逐字段;高水位标记(`GetTouchedBindPointCount`、`GetMaxTouchedUnit`)留在 tracker 走查里,直接就是 `count` 实参;只发 program 解析过的集合(`uniformSamplerOrImageUnitIndex`);**集合 hash 抑制器**——每条 `kVarTail` `set_*` 在 client 算已解析集合的 xxHash,未变不发。最后一条是从后端搬到 client 的 ~175 行去抖(`UnitBindingsSnapshot`/`PairingsIntact`/`g_fboTextureSyncList` 族)的载体:`GetTextureBindGeneration()` 在冗余重绑时也 bump(MC 26.2 每次纹理单元切换都重绑同一个 sampler),没有抑制器每个 batch 都会重发一条几百字节的变长记录并冲掉 server 的两个 memo。
Which calls the suppressor covers (`ARCHITECTURE.md:145`, §4.1 tail): `每条 `kVarTail` 的 `set_*`(`SetVertexBuffers`、`SetSamplerViews`、`BindSamplerStates`、`SetShaderImages`、`SetShaderBuffers`、`SetStreamOutputTargets`)都带 `ContentHash`——与 `MGPFramebufferState` 同一模式,hash 未变就不发(§5.4)。` The `MGPFramebufferState.ContentHash` doubles as server render-pass memo key and client emission suppressor (`ARCHITECTURE.md:134`).
`ARCHITECTURE.md:200` — 索引绑定范围在 validate 时刻实时解析(`glBindBufferBase` 之后再 `glBufferData` 是普通应用代码).
Note: the P2 ROADMAP row asks only for the suppressor **skeleton** (`抑制器骨架`, `ROADMAP.md:18`); the full landing of the debounce移动 from backend to client is P3b/P4b (`ROADMAP.md:23`: `同时**在 Tracker 落地集合 hash 抑制器`).
### 2.5 `set_pixel_pack_state` / `set_patch_state` / `set_vertex_attrib_defaults`
- All three are `kCtxState` entries (`ARCHITECTURE.md:95`): `SetVertexAttribDefaults`(V)、`SetPixelPackState`、`SetPatchState`. `V` = `kVarTail` (`ARCHITECTURE.md:99`) — so `SetVertexAttribDefaults` carries a variable-length tail (and per `ARCHITECTURE.md:145` therefore also a `ContentHash` suppressor).
- `MGPPixelPackState` = 28 B, `只有 PACK 方向(D5)` (`ARCHITECTURE.md:138`); measured `static_assert` 28 (`MEASUREMENTS.md:86`).
- There is deliberately **no** `set_pixel_unpack_state` (`ARCHITECTURE.md:107`, verbatim): `set_pixel_unpack_state`(不存在:前端已在 `glTexImage` 时解析压缩格式、强制默认 unpack).
- `MGPPatchState` = 40 B, `同时是 shader variant 输入` (`ARCHITECTURE.md:139`).
- Dirty bits for these three are value-class, driven by existing counters — `NEW_PIXEL_PACK`、`NEW_PATCH_STATE`(`BitwiseEqual`,NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS` (`ARCHITECTURE.md:162`). The `BitwiseEqual`/NaN-legal note is a requirement on the patch-state comparison.
- Vertex attribute default values are part of the `Track V` value class (`ARCHITECTURE.md:353`: `stencil/colormask/depthmask/scissor/patch/attrib 默认值`), and `PixelStoreParameters` + `VertexAttribute` types were already extracted to `MG_Pipe/MGPipeValueTypes.h` in P0.5 (`ARCHITECTURE.md:261`, `ROADMAP.md:16`).
### 2.6 §9.4 The residual value block and its tripwire (`ARCHITECTURE.md:357-359`)
`ARCHITECTURE.md:359`, verbatim (the whole spec):
> 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 已占位)。
Three disciplines, restated: (a) retirement is a **compile error** — `MGL_RESIDUAL_BLOCK_SIZE` may only ratchet **down**, never up, and becomes `static_assert(sizeof == 0)` at P13; (b) per-member `offsetof` asserts **and** field-by-field serialisation under split (monolith verify cannot see padding differences in a heterogeneous POD union); (c) it exists only during P2..P13, and `MOBILEGL_PIPE_STATS` counts its bytes as their own class.
Supporting facts:
- Catalogue entry: `迁移期临时的 `SetResidualValueState`(B)` — the 17th `kCtxState` call (`ARCHITECTURE.md:95`).
- Size: `ResidualValueBlock` = **1248** B (`ARCHITECTURE.md:140`, `MEASUREMENTS.md:86`; of which `RenderStateParameters` is 1168 — same 1168 as the server-side working copy, `ARCHITECTURE.md:188`).
- Counter slot already reserved in P0: `residual-value-block`(占位) (`ARCHITECTURE.md:601`).
- P13 removes it: `删 `set_residual_value_state`` and `static_assert(sizeof(ResidualValueBlock) == 0)` 编译通过 (`ROADMAP.md:30`).
- Gate part 5 lists it: ``ResidualValueBlock` 的 `offsetof` 断言与 P13 的 `sizeof == 0`` (`ARCHITECTURE.md:505`).
### 2.7 Track V and Track H, and the first Track H slice
#### 2.7.1 Definitions
- Terminology (`README.md:52`, verbatim): `**Track V / Track H**:值类读点的迁移(整块 POD 过线)/ 对象类读点的迁移(`SharedPtr<前端对象>` → 句柄)。`
- `ARCHITECTURE.md:353` (§9.3), verbatim: `Track V(值类型:`GetRenderStateParameters`、`GetPixelStoreParameters`、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。`
- `ARCHITECTURE.md:354`: `Track H(对象类型:167 个 `SharedPtr<MG_State…>` 点):真活。` ("real work")
- `ARCHITECTURE.md:355` — read-point census: `A 探测变化 ~35(12%)、B 翻译输入 ~216(74%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"bump 一个版本让 server 自己拉"行不通,值本身必须过去。`
- The three-stage strangler (`ARCHITECTURE.md:343-346`): A 别名 (done, P1) / B 推送 / C 句柄化. Stage C's proof column is `全套门(§13)`.
#### 2.7.2 The first Track H slice — Espryt 0b and Magma subsystem 4
`ROADMAP.md:18`, verbatim: `**第一片 Track H**:Espryt 0b(`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4(`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针)`
Espryt 0b components:
1. `SlotAllocator` (lives in `MG_Impl/Pipe/`, `ARCHITECTURE.md:552`).
2. 6 registries → slot arrays.
3. Delete `TwinLookupMemo` ×3.
4. Delete `OwnerEquals`.
5. Delete `g_fbSlotCache`.
6. Delete the GC (`ARCHITECTURE.md:363` calls it `GC ×6` alongside the same-address `weak_ptr`; the deleted PLAN says `2 个 GC 扫描`, Appendix A).
Magma subsystem 4 components: rekey `VertexInputStateFactory` and `VaoDrawMemo`; **delete the backend raw pointers stored in the frontend VAO**.
Backing detail from §9.5 (`ARCHITECTURE.md:363`, verbatim — the 21-memo rekey census that Espryt 0b/Magma 4 draw from):
> 统一事实:每个进入 memo 键的版本计数器要么是回绕 `Uint16`,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。`{slot, gen}` + 显式 destroy 让 **11 条直接删除**(registry 的同址 `weak_ptr` + GC ×6、`TwinLookupMemo` ×3 + `OwnerEquals`、`UnitSamplerLookupMemo` 的 `WeakPtr` 测试、`SetBackendStateMemo`、`VkTextureManager::TextureIdentity` 存活探测、`ConvertedVertexStreamKey` 的 `sourcePin`……),**2 条** server 删除但去抖搬到 client(§5.4),**7 条重键**成更便宜的比较(`StampSyncedFBO` 四元组 → `ContentHash` + server 私有 `attachmentRemintEpoch`;`ResolvedTextureBindingMemo` 9 键 → `(shaderCso.slot, viewSetSerial)`;`SetupDrawSnapshot` 的 ~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty mask;`VertexInputStateFactory::ComputeHash` 里的 lifetimeId → `gen` **混进** server 侧每个 content hash),**1 条**(D18)原样不动。
So the Magma-4 rekey rule is precisely: `VertexInputStateFactory::ComputeHash` stops using `lifetimeId` and instead mixes the handle `gen` into every server-side content hash. The corresponding frontend-VAO raw pointer is `SetBackendStateMemo` — `ARCHITECTURE.md:284` (§8.1 tail): `**`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除**` (verbatim: ``SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除`); also `SetBackendResource` 删除(server 拥有资源表)and `SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段.
Handle/slot rules the SlotAllocator must obey (`ARCHITECTURE.md:35-40`, §2.1):
- 8-byte POD, passed by value in a register pair; **client mints, server never returns a handle** → zero create round trips (deviation D1) (`:35`).
- `slot 稠密、**按 kind 分配**(free list + 高水位),server 对象表是数组而非哈希表。与 `IndexGenerator` 无关——后者的 LIFO 名字复用正是句柄要关掉的问题。` (`:36`)
- ``gen` 只在 slot 复用时 ++,不在 respecify 时 ++;同一 slot 复用 2³² 次才回绕(1000 fps 逐帧复用约 50 天),debug 分配器断言回绕。` (`:37`)
- 13 kinds (`:38`); reserved handles (`:39`); `GL name 只以 `GlNameForDiag` 出现…永不做身份、永不进 memo 键或 content hash;`GetLifetimeId()` 留在 client 作 tracker 自己的身份,client 维护 `lifetimeId → slot`。` (`:40`)
- Two generation spaces stay strictly separate (`ARCHITECTURE.md:44-49`): client `MGPipeHandle::Gen` crosses the wire, server `MGGen` **never** does.
- Lifetime ordering constraints expressed by payload (`ARCHITECTURE.md:208`): view destroyed before its storage owner (`ViewOf` + server keep-alive), FBO attachment pins the texture, buffer texture pins the buffer.
#### 2.7.3 `MOBILEGL_PIPE_LEGACY_MEMOS` (§9.6, `ARCHITECTURE.md:365-369`)
`ARCHITECTURE.md:367`, verbatim (why it must be a compile-time arm):
> `MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)在 P3a/P4a 期间保留 registry / `TwinLookupMemo` 实现活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役(各阶段 +1 天维护)。
Switch table: `| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON(P2..P13) | 计划(编译期臂) |` (`ARCHITECTURE.md:580`) and the runtime row `| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON | 三态读取,只有显式 falsy 才关 |` (`ARCHITECTURE.md:591`). Maintenance cost is explicitly budgeted at +1 day per phase.
#### 2.7.4 The `HandleRecycleScenario` gate
`ARCHITECTURE.md:501` (purity gate block, verbatim tail): `外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`,永不是裸前端指针",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。`
`ROADMAP.md:18` restates: ``HandleRecycleScenario` 绿且重键前红`.
### 2.8 The dirty-surface gate `gen_pipe_dirty_surface.py` — and which phase owns it
**The docs assign the gate promotion to P1, and the first-round mapping to P2.** Both statements exist; they are consistent only if read as: the script becomes a CI gate at P1, and P2 lands the first real mapping table behind it.
- `ARCHITECTURE.md:172` (§5.2 tail), verbatim: `完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举 `MG_Impl/GLImpl` 里每个 mutator → 必须 bump 的聚合世代,CI 重生成 + `git diff --exit-code`,未映射即失败(**P1 起成为门**)。**实测规模**:926 次 mutator 调用落在 73 个不同 mutator 上,其中 92 次(7 个 mutator,绝大多数 `RecordError`)位于同函数内也会到达后端的"即时发布点",其余 834 次由紧随其后的 verb 发布——映射表是 73 条目的问题。`
- `ARCHITECTURE.md:568` (§16 CI bullet): ``gen_pipe_dirty_surface.py --summary`(信息性,**P1 成门**)` — i.e. informational in P0, a gate from P1.
- `ROADMAP.md:15` (P0 row): the script itself is listed as already landed (✅) in P0.
- `ROADMAP.md:18` (P2 row): ``gen_pipe_dirty_surface.py` 首轮映射成门` — **P2 delivers the first-round mapping, promoted to a gate.**
- `ARCHITECTURE.md:505` (gate part 5): ``gen_pipe_dirty_surface.py` 重生成 0 未映射 mutator`.
- Measured surface (`MEASUREMENTS.md:81`, verbatim): `**dirty-surface 面**(`python3 scripts/gen_pipe_dirty_surface.py --summary`,本树):`MG_Impl/GLImpl` 41 个文件,926 次 mutator 调用,73 个不同 mutator(`RecordError` 一项就占 836 次);92 次(36 个即时发布点、7 个 mutator,绝大多数是 `RecordError`)位于同函数内也到达后端的入口,其余 834 次由紧随的 verb 发布。映射表是 73 条目的问题。`
(Numeric discrepancy to be aware of: `ARCHITECTURE.md:172` attributes "绝大多数 `RecordError`" to the 92 immediate-publish sites; `MEASUREMENTS.md:81` says `RecordError` alone accounts for 836 of the 926 calls and names 36 immediate-publish points across 7 mutators.)
### 2.9 Other P2-relevant architecture the row does not spell out
- Sampler-view resolution stays on the client with its own memo, ~40 lines moved (`ARCHITECTURE.md:204`, §5.5) — that is P4a work but constrains the tracker shape.
- Composite program pipeline (`ARCHITECTURE.md:210`): tracker pushes **one** handle, slot from the reserved high segment of `ShaderCso`, `gen++` and `delete_shader_state` on pipeline-cache eviction.
- Emulation ownership rule (`ARCHITECTURE.md:214`, verbatim): `**驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。**`
- The `PipeInputs`/`MGB_CTX` scaffolding P2 builds on: `ARCHITECTURE.md:325-340` (code block), fill points per verb class (`ARCHITECTURE.md:348`), per-verb generation poison and its `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}` shape (`ARCHITECTURE.md:349`), and `纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`` (same line).
- Push-on-mutation, already landed in P1 and named as the shape P2's tracker needs (`MEASUREMENTS.md:117-124`): `MGP_NOTE_MUTATION(Field)` in `MG_Pipe/PipeMutation.h`, hooked on **three counters**, not on forty write sites — `GetSamplingResolutionGeneration` ← `TextureState::BumpSamplingResolutionGeneration()`; `GetTextureBindGeneration` ← `TextureState::BumpTextureBindGeneration()` and the `bindingChanged` branch of `NoteUnitTouched()`; `GetMaxTouchedTextureUnit` ← the high-water branch of `NoteUnitTouched()` (`MEASUREMENTS.md:121-123`).
- P1 scale numbers P2 inherits (`MEASUREMENTS.md:105-112`): 277 arrow sites (Espryt 113 / Magma 164), 58 non-arrow lines (Espryt 9 / Magma 49), 63 `PipeInputs` fields, 62 distinct accessors (Espryt 32 / Magma 56), **83 `MGP_FILL` fill points covering 69 verbs across 9 classes** (`MG_Pipe/FillPoints.def`), 20 `SyncPersistentMappedRange` / 6 `SyncGpuWrites`.
---
## 3. The day-43 GO/NO-GO checklist, in full (`ROADMAP.md:42-58`)
Heading `ROADMAP.md:42`: `## 第 43 天 GO/NO-GO 清单`; `ROADMAP.md:44`: `手上必须有:` ("you must have in hand").
Verbatim checklist (`ROADMAP.md:46-52`):
- [ ] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧)
- [ ] 两个后端上都已推送的渲染状态,`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/disable,MC batch 速率) ← *doc reads* `(enable/draw/disable/draw,MC batch 速率)`
- [ ] 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开
(Item 6 verbatim is `Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)`, `ROADMAP.md:51`.)
**Which items P2 must produce** (item 1 is P1's; items 2–7 are all P2 deliverables):
| # | item | produced by |
|---|---|---|
| 1 | per-draw per-field semantic equivalence proof, 40 traces + all integration tests, zero divergence | **P1** (already landed — `MEASUREMENTS.md:126-135`: 79/79 retrace verify, 818 integration-verify, 1485 unit ×3, zero `Fatal{`) |
| 2 | pushed render state on both backends, `SyncRenderState` 693 lines untouched | **P2** (deliverable 5) |
| 3 | measured unit cost of the two Track H slices | **P2** (deliverable 10) |
| 4 | two-device reboot-clean paired per-thread CPU delta, p50 and p99 | **P2** (gate clause 6) |
| 5 | tracker absolute ns per draw, ceiling derived from the device baseline of 6.5–9.3 accessors + memo probes per draw | **P2** (gate clause 6) |
| 6 | Blaze3D blend-toggle microbenchmark | **P2** (gate clause 7) |
| 7 | negative control with CSO content addressing off | **P2** (gate clause 7) |
Verdict criteria and exits (`ROADMAP.md:54-58`), verbatim:
> 判据与出口:
> - **继续**:两台设备 p50 与 p99 逐线程 CPU 增量都不为负;tracker 绝对 ns 在上限内;Track H 单位成本不超出估计的 50%。按两条跑道推进。
> - **收缩为 headless 工装用途或重新评估**:任一判据落空。**不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev`;MGPipe 收缩为 `MG_Test` mock 后端 → MGPipe recorder(给 trace_replay 一种记录已解析状态的录制格式)+ `inproc` 渲染线程实验;IPC 跑道搁置到出现新判据。
> - 沉没成本:P0 与 P0.5 无论走哪条路都要花(后者本身是 monolith 净收益);真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天,NO-GO 分支下仍留下上述产物。
Note the GO criterion wording `两台设备 p50 与 p99 逐线程 CPU 增量都不为负` — the *delta must not be negative*, i.e. push must not cost more than pull, on both devices, at both percentiles.
Related milestone lines (`ROADMAP.md:38-40`):
> - **第 25 天(P1 出口)**:verify harness 逐 draw 逐字段证明"推送等价于拉取"。零产品风险,**不是** GO/NO-GO。
> - **第 43 天(P2 出口):GO/NO-GO**。
Where the absolute-ns ceiling comes from (`MEASUREMENTS.md:64`, verbatim):
> **真机稳态动态 accessor 成本是每 draw 6.5–9.3 次**(预测区间 10–25 的下沿;llvmpipe 的 15.5/20.7 是 memo 冷的)。推送要打败的是 ~8 次 accessor + memo 探测,不是 124/169 的静态调用点数。GO/NO-GO 的 tracker 绝对 ns 上限从这里定。
Per-device, per-backend accessor baselines to compare against (`MEASUREMENTS.md:50-58`, acc/draw column): MC 1.21.4 in-world Espryt **9.28** / Magma **8.56** (91.6 draws/frame); Iris BSL (memo cold) Espryt 21.04 / Magma 11.26 (23.2 draws/frame); improved-transparency 26.3 Espryt **8.44** / Magma **6.53** (1320 draws/frame). `accessor-calls` 是约 10 个热入口的静态计数,是每 draw accessor 数的**下界** (`MEASUREMENTS.md:60`). Memo-gate hit/miss abbreviations at `MEASUREMENTS.md:60`: `ers = EsprytRenderState、etl = EsprytTextureSyncList、eub = EsprytUnitBindingsEpoch、mfp = MagmaDrawFastPath、mpm = MagmaPipelineMemo、mdt = MagmaDynamicTail`(`MobileGL/MG_Util/Metrics/PipeStats.h:46-122`).
Devices (`MEASUREMENTS.md:3`): `35d0befa` = Xiaomi 24129PN74C, Adreno 830, Android 16; `3B159D009VZ00000` = Oppo PLG110, Mali, Android 16 (ColorOS).
Gate part 4 of the five-part validation gate defines *how* to measure (`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 内容寻址的负面对照。
And gate part 3 defines the name-for-name baseline (`ARCHITECTURE.md:503`): `逐名功能基线是"P1 出口的重构后 monolith"(P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。` SSIM default threshold 0.99; CTS conformance within 0.5 pp, `行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母`.
Re-baseline checkpoints table (`ROADMAP.md:60-68`) — none fires inside P2, but the first (`P3a > 27 天`) is the immediate successor; `任一触发,先跑 `inproc` 的证伪数字再决定是否继续。` (`ROADMAP.md:68`).
---
## 4. Open questions that touch P2 (`ROADMAP.md:70-89`)
Preamble `ROADMAP.md:72`: `P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。`
**Q1 — the headline P2 number** (`ROADMAP.md:74`, verbatim):
> 1. **client 侧 dirty 走查的真实每 draw CPU 代价。** 拉取基线已实测为每 draw 6.5–9.3 次 accessor + memo 探测;推送要在这个数字下净减少。P2 的头号数字,逐线程 CPU + 绝对 ns,两台设备。
**Q4 — CSO LRU capacity and `set_dynamic_state` chunk granularity** (`ROADMAP.md:77`, verbatim):
> 4. **渲染状态的 wire 粒度。** chunk 划分定下来后,CSO LRU 容量(暂定 64)与 `set_dynamic_state` 的 chunk 粒度由计数器定。
Reading: 64 is provisional (`暂定`); both the LRU capacity and the dynamic-chunk granularity are to be **decided by the boundary counters**, after the chunk split is settled. The P2 row nevertheless specifies `CsoCache`(64 项)(`ROADMAP.md:18`) and §2.3 fixes 64 for render state (`ARCHITECTURE.md:63`) — so build to 64 and let the counters retune it. P13 re-tunes surviving cache capacities generally with counters live (`ROADMAP.md:30`), and open question 14 says `**推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。` (`ROADMAP.md:87`).
**Q5 — `FramebufferSrgb` / `DepthClamp` decision** (`ROADMAP.md:78`, verbatim — note the freeze prohibition):
> 5. **`FramebufferSrgb` / `DepthClamp` 的拍板。** 事实已清(无存储、`glEnable` 静默吞掉、六个读点恒 false、41 个 fixture 无一开启);建议在 chunk 表冻结前补真存储并把 `FramebufferSrgb` 划进 pipeline 半边。由计划所有者拍板,**拍板前不冻结 chunk 表**。
Supporting measurement (`MEASUREMENTS.md:84`, verbatim):
> - **`FramebufferSrgb` / `DepthClamp`**:`FramebufferSrgb` 的六个后端读点全部消费一个编译期常量 `false`,`DepthClamp` 零读点;两者的 `glEnable` 落到 `RenderState.cpp` 的 `default:` 分支既不存储也不报 `GL_INVALID_ENUM`;41 个 fixture 无一开启任一项(补真存储不会改动任何既有 fixture 的输出)。
And the design-side statement at `ARCHITECTURE.md:190` (quoted in §2.3): add real storage before freezing the chunk table and put `FramebufferSrgb` on the **pipeline** half, because it changes the interpretation of attachments/blend. `DepthClamp` has no stated half — it has zero read points, so it is a pure storage addition; the pipeline/dynamic assignment for it is not decided in the docs.
**Other open questions with P2 relevance (secondary):**
- Q2 (`ROADMAP.md:75`) — texture-remint pull rate; touches `MOBILEGL_PIPE_TEXEL_RETAIN_MB` default 0, P8/P9-era but the `ImageBindableHint` prevention is set at resource-create time.
- Q9 (`ROADMAP.md:82`) — `**viewport-array 回放能否塞进一次 `draw_vbo`**:`EndViewportRoutingPasses` 会 `InvalidateSyncedRenderState`,各遍之间观察到的状态是否与今天一致未验证。` — this is a render-state-adjacent unknown that P2's CSO/dynamic split can perturb.
- Q14 (`ROADMAP.md:87`) — cache hit rates changed by the push model; retuned at P13.
- Q17 (`ROADMAP.md:89`) — `create-indirect` fixture fails on Adreno 830 **on the `dev@81b17c0b` baseline itself**, must be fixed on `dev` first; corroborated at `MEASUREMENTS.md:96` (Adreno 830: Espryt black frame after ~4.5 min, Magma `VK_ERROR_DEVICE_LOST` during texture upload submit; Mali SSIM 0.85 / 0.45). Relevant because it is one of the 40 traces the P2 SSIM gate runs over.
---
## 5. Commit / CI discipline
### 5.1 Per-commit discipline (`ROADMAP.md:5-9`)
Heading `ROADMAP.md:5`: `## 通用纪律(每个 commit)`. Verbatim (`ROADMAP.md:7`):
> 默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门);**每个门必须能因它存在的理由变红**;Windows 机器不是正确性门;设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议;每阶段出口跑一次五部分门;每阶段性能判据是**逐线程 CPU 时间**。
Seven rules: (1) the default ALL target must build completely; (2) no hot-path instrumentation may be committed (CI grep gate); (3) **every gate must be able to go red for the reason it exists**; (4) the Windows machine is not a correctness gate; (5) device comparisons are reboot-clean + same-thermal-window paired A/B with CPU frequency pinned per project protocol; (6) run the five-part gate once at each phase exit; (7) the per-phase performance criterion is per-thread CPU time.
Track rule (`ROADMAP.md:9`): every monolith-track segment must be independently deliverable, abortable at any time, and leave the monolith **strictly better than the starting point**.
CTS turnaround is priced separately (`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 不放关键路径。若周转仍主导排期,加宽估时而不是削弱门。
**P2 is not one of the five architecture boundaries** requiring a full caselist, and no named CTS block is assigned to P2.
### 5.2 CI jobs (`ARCHITECTURE.md:568`, verbatim)
> CI(`.github/workflows/test.yml:809` `pipe-gates`,P0 已落地):`gen_pipe.py` 重生成 + diff;`MG_Backend`/`MG_State` 下禁止 stdio 插桩的 grep 门;`gen_pipe_dirty_surface.py --summary`(信息性,P1 成门);`check_doc_citations.py`(警告级,文档定稿后 `--strict`)。独立 job `flatc-check`。后续:`include-graph-check`(P0.5)、`monolith-symbol-report`。
Generated artifacts are committed into the tree and CI regenerates + `git diff --exit-code` (`ARCHITECTURE.md:69`): `七个生成器(`scripts/gen_pipe.py`,产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`)`. `PipeCalls.def` opcodes are file positions, so `新调用只能**追加**到文件末尾、退役的调用保留槽位` (`ARCHITECTURE.md:75`); `MGP_CALL_LIST_DOCUMENTED_COUNT = 71` is pinned by `MG_Test/Pipe/PipeCatalogueTest.cpp` (`ARCHITECTURE.md:75`, citing `MobileGL/MG_Pipe/PipeCalls.def:69`).
Doc-citation lint: `scripts/check_doc_citations.py` lints `file:line` citations in this doc directory (`README.md:39`) — every `file:line` a P2 doc edit adds must be true.
### 5.3 The five-part validation gate to run at P2 exit (`ARCHITECTURE.md:497-507`)
Heading `ARCHITECTURE.md:497`: `### 13.2 五部分验证门(取代 monolith 的字节一致门)`; `ARCHITECTURE.md:499` explains the byte-identity gate dies by construction.
1. **Interface purity, three gates** (non-verify builds only) (`ARCHITECTURE.md:501`): A = include graph (remove `MG_State/GLState` from the include search path when compiling `MG_Backend` in the disaggregated config; depends on P0.5); B = symbols (`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` empty); C = undeclared (`grep -c 'pGLContext' MG_Backend/` == 0). Plus the `{slot, gen}` memo-key debug assertion backed by `HandleRecycleScenario`.
2. **Semantic shadow comparison `MOBILEGL_PIPE_VERIFY=1`** (`ARCHITECTURE.md:502`) — the decisive one; per-field, per-draw; catches fields the tracker forgot to push and **dirty bits that fire too rarely(危险方向)**; third CI mode, 40 traces + all integration tests, ~5–10× slower, never shipped; per-field not `memcmp` (padding false-DIFFERs); **保留模式** for consume-and-clear groups (texture dirty rects) compares the emitted `(UnionBox, RegionCount, Regions[])`; survives P13.
3. **Behavioural A/B** (`ARCHITECTURE.md:503`) — 40 traces under `{monolith-pull, monolith-push, split}` SSIM ≥ 0.99; `ctest -L integration-gpu` name-for-name identical between `DirectGLES.` and `DirectGLES.Pipe.`/`DirectGLES.Split.` (same for DirectVulkan); unit tests all green; CTS per-backend conformance within 0.5 pp; `TextureUploadShapeScenario` golden shape comparison.
4. **monolith performance non-regression** — quoted in §3 above.
5. **Coverage + poison + handle discipline** (`ARCHITECTURE.md:505`): G6 regenerates with 0 UNMAPPED; `gen_pipe_dirty_surface.py` regenerates with 0 unmapped mutators; per-verb generation poison; G7 setter-consistency test; `ResidualValueBlock` `offsetof` asserts and the P13 `sizeof == 0`.
Two surviving byte-level equalities (`ARCHITECTURE.md:507`): with `MOBILEGL_BUILD_DISAGGREGATED=OFF`, `nm --defined-only libMobileGL.so | grep MG_Remote` is empty and the link line gains no library; `nm -D libMobileGL.so | grep mobilegl_server_main` hits in RelWithDebInfo. Symbol/`.text` drift is published per phase as an informational metric.
### 5.4 Harness traps that will bite P2's measurement runs (`MEASUREMENTS.md:98-104`)
1. `MEASUREMENTS.md:98` — the trace app never reaches `MobileGL::DestroyImpl`, so `MOBILEGL_PIPE_STATS_FILE` JSON is never written on device; only the periodic summary lines in `mobilegl.log`; traces shorter than one period produce nothing. Set `MOBILEGL_PIPE_STATS_PERIOD` small enough.
2. `MEASUREMENTS.md:99` — `run_android_retrace_local.py` shares one `.trace-work/android-retrace-result` root per tree and `rmtree`s it per invocation, so **two devices must be run serially from one tree**.
3. `MEASUREMENTS.md:100` — `/data/...` inside `--env` values gets MSYS path-converted; run with `MSYS_NO_PATHCONV=1`.
4. `MEASUREMENTS.md:101` — `coherent_as_flush` goes through `--ez coherent_as_flush true`, independent of `--env`.
5. `MEASUREMENTS.md:102` — `minecraft-1.21.1-neoforge-create-indirect-in-world` fails on both devices, reproducible on the `dev@81b17c0b` baseline APK; not caused by this branch.
Reproduction command (`MEASUREMENTS.md:71-75`):
```sh
ANDROID_SERIAL=<serial> MSYS_NO_PATHCONV=1 \
python3 tools/trace_replay/run_android_retrace_local.py \
--case minecraft-1.21.4-in-world --backend DirectGLES \
--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
# 数字在结果目录的 mobilegl.log 里,grep 'MGPipe stats:',取最后一个完整窗口
```
ColorOS install trap (`MEASUREMENTS.md:18`): first `adb install` of a not-yet-installed package hangs on `com.oplus.appdetail InstallGuideActivity` until "继续安装" is tapped (`input tap 353 2349` on a 1272×2772 panel); same-signature reinstalls pass silently.
### 5.5 Switches P2 touches (`ARCHITECTURE.md:576-597`)
CMake (`ARCHITECTURE.md:578-583`): `MOBILEGL_PIPE_VERIFY` OFF (build-time, compiles in `SnapshotFromGLContext()` + the G4 comparator, retained past P13); `MOBILEGL_PIPE_LEGACY_MEMOS` ON (P2..P13, compile-time arm).
Runtime, landed in P0 (`MobileGL/Config.h:319-358`, `MobileGL/ConfigLoader.cpp:245-256`, `ARCHITECTURE.md:586`): `MOBILEGL_PIPE_PUSH` default 0 — `子系统位图(0 = 全 pull),含一位关闭 CSO 内容寻址;十进制或 `0x`` (`ARCHITECTURE.md:588`) — **that bit is the P2 negative control**; `MOBILEGL_PIPE_VERIFY`, `MOBILEGL_PIPE_STATS`, `MOBILEGL_PIPE_LEGACY_MEMOS`(三态读取,只有显式 falsy 才关), `MOBILEGL_PIPE_TEXEL_RETAIN_MB` 0, `MOBILEGL_PIPE_INDEX_MIRROR_MB` 64, `MOBILEGL_PIPE_STATS_PERIOD` 120, `MOBILEGL_PIPE_STATS_FILE`.
Counters available for the P2 numbers (`ARCHITECTURE.md:601`, §附 B): byte classes incl. `residual-value-block`(占位); call classes `draws`、`accessor-calls`、`texture-upload-emissions/box/rect/jobs`; six memo gates each hit/miss (`SyncRenderState` 早退、`SyncNeccessaryTextures` 键比较、`CurrentUnitBindingsEpoch` 快门、`TrySetupDrawFastPath`、pipeline memo、`ApplyDynamicDrawStateTail`); a 24-bucket per-draw payload histogram already implemented, waiting for the first emitter; `站点清单——哪些路径**没有**接线——写在 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份清单是契约。`
### 5.6 Commit message convention (project standing rule, not in these docs)
Single-line `[Type] (Scope): description`; never add `Co-Authored-By` or other attribution lines. (User memory rule, applies to every commit on this branch; the branch's own history matches, e.g. `e7a6a72f [Docs] (Disaggregated): record the P1 landing and what its verify lane found`.)
---
## Appendix A — supplementary P2 detail from the deleted `PLAN.md` (git history, **not** current doctrine)
`README.md:57` says the 328 KB `PLAN.md` and 135 KB `REVIEW.md` were deleted and live only in git history (`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`). `ROADMAP.md:24`'s "§5.5 subsystem" numbering and the names "Espryt 0b" / "Magma 子系统 4" originate there. `git show 87ee17c6:docs/Disaggregated/PLAN.md` yields detail the current ROADMAP compresses away — useful to the implementer, but treat the current four docs as authoritative where they differ:
- P2 day breakdown (PLAN §, line 2261 of that blob): `P2 | 18-26 | 43 | Espryt 1(3-5) + Magma 1(3-4) + Espryt 0b(5-7) + Magma 4(2-3) + tracker/CSO/G7(4-6) + 聚合世代(1)`.
- Espryt subsystem 0b row (line 1170): `handle 基建;6 个 registry → slot 数组;删 `TwinLookupMemo`×3 / `OwnerEquals` / `g_fbSlotCache` / **2 个 GC 扫描**`, estimated `5-7` days, risk `低`.
- Render-state deliverable (line ~2145): the Espryt side keeps `RenderStateImpl`'s 693-line body, the single-`Uint16` early-out, the three-segment memcmp, `g_syncedColorMaskAlphaWidenMask` and the dual-source decline **unchanged** (eliminating 4 read points); the Magma side re-sources `ComputePipelineStateHash` / `GetOrCreatePipeline` / `ApplyDynamicDrawStateTail` from the CSO and the dynamic payload (**eliminating ~55 read points**); **both version numbers cross the wire**.
- Chunk table provenance: `从 `VulkanRenderer.cpp:4826-4906` 原样搬来` (line ~2143).
- `CsoCache`: `64 项 LRU,键是 **pipeline 子集**的 xxHash(**不是整块**,D-B1 v2)`.
- Additional named integration scenarios the PLAN's P2 acceptance called out beyond the ROADMAP row: `ClipDistance`、`SampleMaskScope`、`SampleVariables`、`DualSourceBlend`、`ViewportArray`、`PrimitiveRestart` — worth running, since they are exactly the render-state-sensitive ones.
- The PLAN's P2 acceptance also states the paired-device criterion as `monolith-push 在 p50 与 p99 逐线程 CPU 上落在 monolith-pull 噪声内或更好,**并且 tracker 每 draw 的绝对 ns 落在预设上限内**(相对阈值不够)`.
---
## Appendix B — discrepancies / cautions for the implementers
1. **`ROADMAP.md:24`'s "§5.5 其余 10 个子系统" is a dangling cross-reference.** `ARCHITECTURE.md:202` §5.5 is "sampler view 在 client 侧解析". The 12-subsystem Magma decomposition (from which "subsystem 1" = pipeline+dynamic state and "subsystem 4" = VertexInput/VaoDrawMemo come) exists only in the deleted `PLAN.md`.
2. **Baseline commit differs across docs**: `README.md:3` `dev@50fb1343`; `ROADMAP.md:3` and `ARCHITECTURE.md:3` both still say `feat/disaggregated@458ccde1`; `81b17c0b` is the performance anchor only.
3. **`PipeInputs` field count**: `ARCHITECTURE.md:79` (G5 row) says 61; `MEASUREMENTS.md:107` says the landed count is **63** (`计划写 61;GetBoundTransformFeedbackLifetimeId、HasOpenTransformFeedbackSpan 是 D21 之后新增的读点`). Same for accessors: plan said Espryt 32 / Magma 55 (`ARCHITECTURE.md:328`), landed is Espryt 32 / Magma 56 (`ROADMAP.md:17`, `MEASUREMENTS.md:108`, which also notes `GetBoundTransformFeedbackName` is a dead annotated line).
4. **Fill points**: `ROADMAP.md:17` says `~93 个边界站点` (also `ARCHITECTURE.md:348`); the landed measurement is **83 `MGP_FILL`, 69 verbs, 9 classes** in `MG_Pipe/FillPoints.def` (`MEASUREMENTS.md:110`).
5. **The dirty-surface gate's phase**: promoted to a gate at P1 per `ARCHITECTURE.md:172` and `:568`, while the *first-round mapping* is a P2 deliverable per `ROADMAP.md:18`. Build P2 assuming the CI job already fails on unmapped mutators and that P2 supplies the 73-entry table.
6. **The 8 over-approximated poison rows** (`MEASUREMENTS.md:115`) are a P2 obligation the ROADMAP row does not mention: a (verb class, field) pair that is statically over-approximated has its poison permanently disabled, so re-check those 8 before tightening the fill table.
7. `ROADMAP.md:18` says the Espryt 0b deletion list includes "GC" (singular); `ARCHITECTURE.md:363` counts `GC ×6` together with the same-address `weak_ptr`; the deleted PLAN says `2 个 GC 扫描`. Resolve against the code.