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

58 KiB
Raw Blame History

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)

// 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_(),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:

  1. 语义影子比对 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-indirectfixture 上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:

  1. 行为 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:

  1. 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:

  1. 覆盖 + 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 例。逐阶段只跑该阶段可能影响的具名块(P4apacked_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 rmtrees 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)