Files
MobileGL/docs/Disaggregated/notes/p5/BRIEF-P5.md
T
swung0x48 b16531c3a8 [Docs] record the landed stage chunking in the R-10 texts
Every place that stated "P5 does not chunk" as current behaviour except the
Fatal texts (previous commit) now records the stage chunk budget and what it
does not cover.

- CONTRACT-P5.md section 5: the STAGE_MB row names MGPipeStageChunkBytes() =
  clamp(segment/4, 4096, segment), the two buffer walks (1e7c372e) and one
  texture level's whole-width slabs (9469d48e, assembled by
  StagedTextureStore::AdoptRun), and the Fatal that stays the backstop for a row
  with no cut; the RING_MB row says the record bound is about the record's own
  bytes.
- BRIEF-P5.md's R-10 section is no longer headed "does not chunk": it keeps the
  record bound and the maxrec proof obligation unchanged and records that
  content is cut, with the encoder Fatal as the fallback.
- ROADMAP.md's staging debt row and open question 11, ARCHITECTURE.md's
  record-bound bullet and SEG_STAGE table note, README.md's knob row,
  MEASUREMENTS.md section 6.3 and CURRENT_STAGE_PROGRESS.md's open item: the
  explicit MOBILEGL_IPC_STAGE_MB=256 profile is no longer needed for the two cut
  paths, and the carrier design for the rest stays P8.
- Historical results (notes/**/p5-results/, p5b/) are left exactly as recorded.
2026-09-20 19:39:56 -04:00

69 KiB
Raw Blame History

BRIEF-P5 — 传输 + inproc applier + 发射表

基线:feat/disaggregated @ a29807cc(P4a 收官 8c458cd5 + docs D.5 a29807cc)。 路线图行:docs/Disaggregated/ROADMAP.md:21(估 12 天,里程碑「第 99 天:首个 IPC 帧(缩减路径)」)。 侦察产物:~/w7/notes/p5/scout-{wire-codec,transport,backend-install-and-thread,caps-reply-reverse, unmigrated-census,gpu-writes-and-persistent-map-push,test-ci-plumbing,premortem}.md(8 份,4494 行, 全部 file:line 可复核)。本 brief 引用它们时写 scout-<slug>:§n。


§0 这一阶段要交付什么

一句话:让 MOBILEGL_TRANSPORT=inproc 下的一帧,真正从客户端编码进 ring、由另一条线程解码并应用到后端, 而且那条 ring 上跑的字节与 P6 的 spawn 完全相同。

范围内(P5 owns):

  1. G3 编解码器的真实实现(今天只有骨架,见 §1.1)。
  2. MG_Remote/Client/ 与 MG_Remote/Server/:会话对象、发射表、applier、ServerLoop 与 apply 线程。
  3. MG_Backend/Init.cpp 的单一 hook + MG_Config::Transport + MOBILEGL_TRANSPORT 解析 + CMake 布线。
  4. MGPCaps 快照(客户端只能从快照答 56 个 caps 读点)。
  5. 阻塞 read_pixels、客户端保守 MarkGpuWritten、块粒度 persistent-map 推送 + persistent-map-push 出数。
  6. 两个新场景(TriangleScenario、PersistentCoherentMapScenario)、DirectGLES.Split. 车道、 trace-replay 的 SPLIT 臂、build-split 构建目录与门脚本第五部分。
  7. 未迁移输入的可见化:字段归属表 + 生成器检查 + 严格模式对照。

范围外(明确不做,写进 brief 免得包去建):

  • spawn / SocketTransport / ServerMain / HeadlessGL fork 预检 → P6。
  • G3 分块协议、HostResolve.cpp、MGHostSpan 的 split 填法、IndexHostMirror → P8(见 R-10)。
  • SEG_REPLY 的通用异步池、OnGpuWritten 收窄、纹理拉取四缓解 + 终止符、OnLog、OnXfbScatterReady → P9。
  • client 铸造 sync/query handle、轮询入口门铃化、present credit 叠加公式 → P10。
  • T0/T1 采纳档(P5 恒定跑 T2,见 R-6)→ P11。
  • 71 条目录里的 34 条无 applier 无发射器的行,只做 §3 verb 普查点名的那一小撮(R-4)。

§1 侦察结论:十条必须先知道的事实

1.1 G3 没有编解码器。 scripts/gen_pipe.py:605 gen_wire 只产 8 字节头、MGPWireOp 枚举、71 个 MGPWireRec_* 结构 + 尺寸断言、以及一个每条臂都 return false 的 bounds gate (PipeWire.inc:708-933;生成器自称 "SKELETON",gen_pipe.py:666)。没有 encoder,没有 decoder。 G1 的两张表(gMGPipeScreen/gMGPipeContext,MGPipe.h:137-138)与 G2 的 71 个 thunk 同样零调用者。 今天真正的边界是 MG_Impl/Pipe/* 直接调 ~45 个 MGPipeApply*。scout-wire-codec:§1、scout-install:§1.4。

1.2 Transport 层写完了但从未被编译过。 Ring/Doorbell/ShmSegment/FdPassing/Framing/InProcessTransport 共 2747 行、52 个测试;但 .github/workflows/test.yml 里没有任何一次 cmake 传 -DMOBILEGL_BUILD_DISAGGREGATED=ON,wsl_p4a_gate.sh 也没有。P5 的第一次 ON 配置可能是这些文件 在本分支 HEAD 上第一次见到编译器。给它留预算。scout-test-ci:§5.2。

1.3 InProcessTransport 是消息队列,不是 ring。 两个 deque<vector<uint8_t>> + 两个 condvar 门铃 (InProcessTransport.cpp:38-97),既不碰 ring 也不碰任何 codec。"inproc 走与 spawn 相同的 G3 编解码" 不能靠改这个类实现,要在它之上加一个持有 ring 的会话对象(ARCHITECTURE.md:573-576 把 Client/、Server/ 标为 [P5+],Transport/ 下 P6 只加 SocketTransport)。scout-transport:§3。

1.4 五个水位线和两个事件标志是声明了但没人写的。 appliedSeq/submittedSeq/retiredSeq/ completedFrameSerial/presentAckSerial、eventRingFull/eventDropped(Ring.h:69-81)全树只有 InitRingControl 把它们清零。§11.6 的整个 credit 协议是 P5 的。scout-transport:§1。

1.5 blob 规则今天是"反着的"。 每一条带 MGPBlobRef 的记录都写 Seg=None, Size=0, Offset=<宿主地址>,字节作为伴随指针旁路传递;三条甚至传的是有类型的前端指针 (const SamplerParameters*;const LinkArtifacts* + const SpirvArtifacts*)。 另有两个载体带 MGPBlobRef 却没有 kHasBlob 标志:MGPCaps(两个)与 MGPSamplerDesc。 scout-wire-codec:§2.1/§4、scout-premortem:§3。

1.6 63 个 PipeInputs 字段里,31 个没有任何推送记录供给(24 个 V/O + 7 个 sticky F)。 缩减路径(clear + triangle + readPixels,再加 OpenRA)实打实要读其中 21 个,其中 PrepareForDraw 无条件读两个前端指针(DirectGLES.cpp:4486 GetBoundVertexArray、 :4497 GetProgramForDraw),退役它们的是 P8,不是 P5。scout-unmigrated-census:§3。 并且:服务端没有任何东西 stamp 毒化世代(applier 故意不 stamp,PipeInputs.h:612-618), 所以一个"纯粹的"服务端会在 SyncRenderState 的第一次读上就 Fatal{…@<none>}。

1.7 七个 sticky(F 类)字段对毒化免疫(PipeFilled.inc:421 无条件返回 fresh),而它们恰恰是 把前端对象或前端写入直接交给后端的那七个。P5 的退出门"未迁移字段读 = Fatal"在最危险的七个字段上 结构性失明。scout-premortem:§0(B)。

1.8 acceptance 是同步返回值。 四个返回 Bool 的 applier 入口 (ResourceCreate/Respecify/ResourceSubData/SetTextureParams)与 MapPersistent 的 void* 决定客户端破坏性状态变更(清 dirty、latch 参数、采纳指针)。队列一插进去,答案在调用点就没了。 scout-premortem:§0(C)。

1.9 inproc 天然会作弊。 同一地址空间里 MGHostSpan::Ptr、Blob.Offset、 MGPipeApplyMapPersistent 返回的指针全都能解析,于是 P5 的六条退出门可以在 "一个字节都没离开地址空间"的实现上全绿。scout-premortem:§0(A)。这条决定了 R-2。

1.10 客户端的消费者门读的是服务端的事实。 P4aFamilyHasItsConsumer() / 资源子系统门读 MGPipeGetResourceOps()——那是后端注册的表。inproc 下碰巧答对;spawn 下客户端进程根本没有后端, 四个 P4a 族 + P3a 的 buffer 族一条记录都不发,静默全灭。scout-premortem:§1 class 4。


§2 裁定(R-1…R-14)。包不得推翻,只能引用。

R-1 — 目标形态:lockstep inproc

P5 交付的 inproc 是:

  • 一条真正的 apply 线程 mgl-srv-apply,启动时把原生 context 变成自己的并终身持有 (ARCHITECTURE.md:535 的形状,P6 直接继承),于是 DirectGLES.cpp 的 16 个 IsBackendContextCurrentOnThisThread() 站点与 Managers.cpp 的 19 个 CanTouchGLNow() 站点 在服务端答 true,off-thread 降级消失(scout-install:§3.3)。
  • 客户端在每个 verb 边界阻塞等 appliedSeq == emitSeq(下称verb 栅栏)。

栅栏的理由是 §1.6,不是保守:31/63 个字段仍由客户端的残余填充从活 GLContext 里拉, 一条自由奔跑的队列会让后端读到未来的字段值(scout-premortem:H1)。栅栏在的时候, 任一瞬间两条线程里只有一条可运行,于是 gPipeInputs 保持单实例是合法的。

配套裁定:

  • 开关 MOBILEGL_IPC_VERB_BARRIER,P5 默认 1;=0 是负面对照臂(预期红,允许红,必须被跑过一次并记录 它以什么方式红)。
  • 栅栏是可退役对象,不是永久设计:它随 §3 表 2 里 BARRIER-PULLED 那一栏被 P3b/P4b/P7/P8 清空而逐族打开。 每个包在自己的结论里写"我这一族还剩几行 BARRIER-PULLED"。
  • P5 不产生 CPU 收益,这是预期而非回归。 性能按用户 2026-09-08 规则只记录不设门; MEASUREMENTS 里要明写"栅栏成本 = 每 verb 一次 spin/park 往返,这是 P5 的已知税,随栅栏退役而消失"。
  • 契约包必须把"栅栏臂下 apply 线程与 GL 线程的可运行性互斥"写成一条运行期断言 (debug/verify 构建:apply 线程进入 applier 时置标志,客户端在栅栏外触碰 gPipeInputs 时检查), 否则这条不变量只活在 brief 里。

退路(必须提前写下来,不许临场发明):若 EGL context 迁移在集成时证明是多日窟窿,允许先落 inproc-inline(客户端线程自己 drain ring,不建线程,context 不迁移)作为中间态并立刻开第二个包补线程臂。 触发条件:server 包在第 4 个工作日结束时 context 迁移仍未在 ClearThenReadPixels 上跑通。 由集成者宣布,包不得自行降级。

R-2 — 诚实 inproc 规则(杀掉 §1.9)

MG_Config::Transport != Monolith 时:

  1. 编码器必须把 MGHostSpan::Ptr 置 nullptr,Seg/Offset 指向 SEG_STAGE;
  2. 编码器必须为每一条携带内容的 MGPBlobRef 填真实的 Seg / 段内 Offset / 非零 Size;
  3. 解码/应用侧必须 Fatal{ProtocolCorruption}:Ptr != nullptr、内容记录的 Blob.Size == 0、 Size != 0 && Seg == kMGHostSpanSegNone、以及段内 Offset+Size 越过段边界;
  4. MGPipeApplyMapPersistent 在 split 下必须返回 nullptr(见 R-6);
  5. 负面对照:MOBILEGL_IPC_AUDIT=1 时,服务端在记录 retire 之后把它占的 SEG_STAGE 字节填 0xDD; 任何"应用完还留着指针"的实现会在下一帧读到 0xDD 并红。这是 §1.9 与 class 3 的唯一机械对照。

这五条合起来就是 ROADMAP "inproc 走与 spawn 相同的 G3 编解码" 的可检验定义。 在 MEASUREMENTS 里以「inproc 诚实性五条」的名字登记,每条注明它被观察到红过一次的方式。

R-3 — 回复槽的 Id 就是记录序号

十条 kReplySlot 调用的 payload 今天一个都不含 MGPReplySlot(scout-premortem:§1 class 2)。 不新增字段、不新增分配器:

  • 记录序号(m_emitSeq/m_applySeq,ARCHITECTURE.md:124:wire 上没有逐记录序号字段,seq 就是序数) 就是回复槽 id;服务端把答案写进 SEG_REPLY[seq % slots] 并把 seq 回填进槽头做自检。
  • 这样 S-1 那一类"两个家族各铸一套 id、查表永远查不到"在 P5 结构上不可能发生。
  • 栅栏在的时候,等 appliedSeq >= mySeq 与"等我的回复到了"是同一次等待, 所以阻塞 read_pixels、MapPersistent 的 decline、四个 Bool acceptance(R-5)零额外往返。
  • P9 把它推广成异步池时,这条规则升级为"seq 是 id 的初值",不是推翻。

pad 记录不推进 seq(见 R-9)。

R-4 — 发射表:69 个槽,未实现的槽是具名 Fatal

BackendObject_Remote::GetBackendFunctions() 返回一张 GlobalBackendFunctionsTable, 每个槽是一个发射器。没有任何槽允许为 null(91 个 MG_Impl/GLImpl 站点直接调它, scout-install:§1.2),也不允许穿透去调驱动。P5 未实现的槽装 Fatal{UnmigratedVerb, "<slot>"}(MGLOG_F + abort,与 MGPipeInputPoisonFatal 同形)。

实现哪些槽由 verb 普查决定,不由 brief 猜。 契约包在第 1 天跑一次普查: 把两个场景 + OpenRA trace 跑在"全 Fatal 表"上,按 Fatal{UnmigratedVerb 收集点名, 输出 ~/w7/notes/p5/verb-census.md。预期最小集(猜测,供排期用,不是承诺): Clear、DrawArrays/DrawElements(→ DrawVbo)、ReadPixels、Present、Flush、Finish, OpenRA 再加 blit/纹理/程序若干;总计 6–15 个。其余 ~25 条目录行明确不做。

R-5 — acceptance 不在客户端重新推导

四个 Bool 入口 + MapPersistent 的答案,走 R-3 的回复槽,在栅栏的那次等待里取回。 禁止的两条路(写下来是为了让复审可以直接判红):

  • ✗ 客户端复制一份 acceptance 谓词(那正是 c0f/c0g 两侧各一份 D-K2 依赖表的形状,P4a 为此付了两轮契约修);
  • ✗ "总是接受"(那是 ID-39 的 66 条 DirectVulkan 用例丢上传,中间再加一条线)。

R-6 — split 下恒定 T2,且 LargeArenaAdoptionScenario 必须变成档位感知

MGPipeApplyMapPersistent 在 split 下恒返回 nullptr(R-2.4),前端三处已经容忍 (BufferObject.cpp:238、:603-606、:657-660)。理由:persistent-map-push 在采纳存活时结构性为零 (PipeStats.cpp:68-70),而它是退出门。MOBILEGL_IPC_ADOPT_TIER 作为负面对照开关落地 (默认 2;=0 在 P5 下不实现,置为 Fatal "P11")。 LargeArenaAdoptionScenario(断言发生了采纳)在 split 车道下改成断言decline + mpr 不变—— 改断言不改测试名(G14 安全)。scout-gpu-writes:§2.3。

R-7 — 字段归属表是生成的,栅栏拉取是被计数的

见 §3 表 2。三条机械化要求:

  1. 63 个字段 + 7 个 sticky forward,每一个恰好落在 RECORD-SUPPLIED(call) | APPLIER-DERIVED(record) | BARRIER-PULLED(retiring phase) | FATAL 四类之一; 表由生成器产出并 --check(照 gen_pipe_dirty_surface.py --check 的形状),不在任何一类里 = 构建失败。
  2. split 下读一个 BARRIER-PULLED 字段:递增新的 PipeStats::CallClass::ResidualPulls (短名 rsp,放进 #if MOBILEGL_PIPE_PUSH 块内,G1 安全),逐帧发布。 它的值就是 P6/P7/P8 欠债的大小,P5 结束时进 MEASUREMENTS。
  3. MOBILEGL_IPC_STRICT_ERRORS=1 把 BARRIER-PULLED 提升为 FATAL; 一条具名测试断言它确实 abort(证明这套插桩不是装饰)。 同一开关把七个 sticky forward 也提升为 FATAL——sticky 豁免在 split + strict 下取消。

R-8 — 消费者/存活门移到握手,即使 inproc 下它"碰巧对"

MGPipeResourceSubsystemEnabled() 与 P4aFamilyHasItsConsumer() 在 split 下必须读 MGPCaps::CallMask 的客户端镜像,不得读 MGPipeGetResourceOps()。 理由是 §1.10:inproc 下读错来源会答对,spawn 下会静默灭掉整条推送路径。 门:一条单元用例,给客户端一份缺少资源族的 caps 掩码,断言它一条记录都不发、且计数器点名该族。

CallMask 与 protocol.fbs:94 的 tableSlotMask 是同一个字段(ARCHITECTURE.md:114: "CallMask 取代『槽位是否为 null』这个隐式能力探测")。契约包删掉 tableSlotMask 这个名字 或把它改名成 callMask 并在 protocol.fbs 注释里写清;同时 maxComputeWorkGroupCount/Size 与 prefersCpuXfbPrimitiveAccounting 三个字段与 dynamicParameters 里的同名成员冗余, 契约包二选一并写进编码表(§3 表 0)。scout-caps-reply:§1.4(6)(7)(8)。

R-9 — 五条水位线,每条一句话

契约包把这五句写进 Ring.h 的头注释并各配一条单元用例:

水位线 谁推进 谁可以等 比较 批处理
submittedSeq 生产者 publish 后 无(诊断用) >= 允许延迟
appliedSeq 消费者每条记录(P5 栅栏期禁止 64 条批处理) 客户端栅栏、回复等待 >= P5 禁止
retiredSeq 消费者 drain 完 SEG_STAGE 引用后 暂存分配器 >= 允许延迟
completedFrameSerial 服务端 present 完成 回收/老化 >= 允许延迟
presentAckSerial 服务端 present credit 归还 客户端 present 节流 >= 允许延迟
通用规则:批处理只允许让水位线变晚,绝不允许让等待者看到"比实际做的更多"的值。
pad 记录(kRecPad)不推进 seq——两侧都必须跳过它再计数,否则每一次回绕都会让 seq 永久错位且无校验和。

R-10 — blob 走 SEG_STAGE,记录上界,以及内容侧的分块

规则:所有 blob 走 SEG_STAGE,记录本身只带 {Seg, Offset, Size},于是 CreateShaderState 的记录是 192+8 字节而不是整个 archive。var-tail 的长度由 GL 上限或发射器自己的 切分(MGPipeForEachSubDataRecordRange,PipeFill.cpp:694-713)界定。因此没有一条记录会超过 RingProducer::MaxRecordBytes() == Capacity()/2,超过即 Fatal{RingOverrun}——记录本身永不分块。 内容侧分块已落地(本节原写"P5 不做分块…分块留 P8"):SEG_STAGE 仍是"一条记录一个整 blob"的 线性 arena,但会超出它的内容由发射侧按 stage chunk 预算切开——预算 MGPipeStageChunkBytes() = clamp(segment/4, 4096, segment),默认 MOBILEGL_IPC_STAGE_MB=32 → 8 MiB (MG_Remote/Client/GpuWritePending.h:169,无 session / monolith / server 角色自身返回 0 = 不切)。 已接入的两条内容路径:buffer 内容走查(PipeFill.cpp 的 MGPipeContentChunkCap,1e7c372e) 与纹理一级的整宽 slab(TextureEmit.h 的 slab 切分,服务端由 StagedTextureStore::AdoptRun 按 run 拼回整级、覆盖不全即 Fatal{StageSnapshotTooNarrow},9469d48e)。未接入分片的 record 类型、以及单片仍大于 arena 的情形,仍由编码器的 Fatal{RingOverrun, "SEG_STAGE"} 兜底;编码器 上界(一条记录的 header + payload + 内联尾)与这条 Fatal 都不因分块而放松。 证明义务(不变):wire 包加一个 PipeStats 最大记录字节的 max 计数器,在缩减路径 + OpenRA 上出数, 写进 MEASUREMENTS。若实测有记录逼近 Capacity()/2(默认 MOBILEGL_IPC_RING_MB=8 → 4 MiB), 立刻上报集成者,由集成者决定是为该 record 类型加分片还是调大默认 ring。

R-11 — 暂存字节的生命期:应用侧不得跨返回持有指针

SEG_STAGE 的一段字节从 publish 到 retiredSeq 越过该记录为止有效。 任何 applier 入口不得把指针留到返回之后——Espryt 的 GLESBufferResource::hostBytes (Managers.h:839,写于 Managers.cpp:1980-1983/:2035,读于 :2000/:2062/:2080/:2111/:2741/:2843) 是今天唯一的违例,它在 split 下必须改成把字节拷进服务端自有存储。 对照见 R-2.5 的 0xDD 毒化。这是 P4a 分类学 class 3 在 P5 的复发点。

R-12 — 反向通道:P5 只接三条半

MGPipeCallbacks 十条里 P5 接:OnGpuWritten(#2)、OnBufferWriteback(#3)、OnSurfaceChanged(#7), 外加 DirectVulkan 车道需要的 OnCapsInvalidated(#8)。 OnGlError(#1) 在 inproc 下先走 RecordError 的 sticky forward(计入 rsp), 其有序化留 P9。其余六条留 P7/P9。不新增第十一条回调(MGPipeCallbacks.h:56-58 的 static_assert 存在就是为了让这个代价可见);服务端 context 死亡的再开事件折进 OnCapsInvalidated (scout-gpu-writes:§5.2)。 OnCapsInvalidated 的 DirectGLES 缺口按 scout-caps-reply:§1.5(1) 的方案 (a) 解决: 每次 InitCapabilities 重跑就重发一次 caps 快照,客户端把"再次到达"当作失效,不改 dev 形状的后端代码。

R-13 — 目录与编码的四处不一致,契约包一次性改掉

  1. GetCaps/MGPCaps 与 CreateSamplerState/MGPSamplerDesc 带 MGPBlobRef 却无 kHasBlob → 补标志 (改标志不移动 opcode,PipeCalls.def:26-29 的编号规则不受影响)。
  2. ResourceFlushRange 是 kNone 却带内容指针 → 或补 blob 成员 + 标志,或在编码表里写明 "长度由 MGPFlushRange 的 range 决定,字节走 SEG_STAGE,无 MGPBlobRef"。二选一,写下来。
  3. ResourceRespecify 的 initialBytes(每一次 glBufferData(size,data) / glTexImage*(…,data)) 今天没有任何载体(MGPResourceDesc 没有 MGPBlobRef 成员,PipeApply.h:76-79 明确说这是故意的) → 必须在编码表里给它一个归宿:用 pad 位加一个 MGPBlobRef(MGPResourceDesc 有 Pad0/Pad1, 但 24 字节的 blobref 放不进 pad,需要扩结构并改 MGP_ASSERT_POD(…, 88)),或者规定 "初始字节永远以一条紧随其后的 ResourceSubData 送达"。推荐后者:零结构变更、零 POD 断言移动、 复用已经走通的路径;代价是多一条记录。由契约包定,写进 §3 表 1。
  4. 生成一张逐 opcode 的标志表(kMGPipeCallFlags[kOpCount])——今天没有任何生成物导出标志, 六个包会各硬编码一份(scout-wire-codec:§2.7)。一行 gen_pipe.py 改动。

R-14 — 不要把任何 P5 产物挂在临时 CI 触发分支上

test.yml:9-12 的 feat/disaggregated 触发与四个带同样分支守卫的 job 步骤(:1694/:1698/:1727/:1731) 是要在并入 dev 前删掉的。P5 的新 job / 新步骤要么无条件跑,要么 workflow_dispatch, 不得写 github.ref == 'refs/heads/feat/disaggregated'。scout-test-ci:§4.1。

§3 三张必须在任何包分叉之前冻结的表

P4a 的教训是:契约改了七次(c0b…c0g),根因表在 fable-seam-audit.md:359-398 与 MEASUREMENTS.md:557-573。 这三张表各杀掉那份清单的一整列。契约包(c0)的产物就是这三张表 + 让它们编译得过的头文件与桩。 表的权威副本落在 MobileGL/MG_Remote/CONTRACT-P5.md(随代码走,不是只在 notes 里), 本节是它的规格与已知行。

表 0 — 编码表(杀 class 1)

一行一个不是句柄的 wire 字段:位布局、零的含义、读者。已知必须在表里的行:

字段 规定 备注
段 id 空间 SEG_CMD=1, SEG_STAGE=2, SEG_REPLY=3, SEG_EVENT=4, SEG_SHADOW=5, SEG_ADOPT=6,与 protocol.fbs:36-44 的 SegmentKind 同值;0 永远是「无段」,不得作为真实段 id kMGHostSpanSegNone = 0 已定;kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF 保留给 P8
MGPBlobRef{Seg,Offset,Size} Seg 用上表;Offset 是段内字节偏移,不是宿主地址;Size 非零即"本记录声明了它的 blob",split 下必须非零(R-2) 今天全树是 {0, <地址>, 0}
MGHostSpan{Ptr,Seg,Size,Offset} split 下 Ptr 恒 nullptr;32 字节布局不得重排(MGPipeHostSpan.h:29-31) P5 的缩减路径应当一条都不产生(见 §4 的 cap 位裁定)
caps 载体 只有一个:MGPCaps。protocol.fbs 的 CapsSnapshot 是它的运输形,三个 blob 是 POD 字节像;tableSlotMask 改名 callMask 或删除(R-8);maxComputeWorkGroupCount/Size、prefersCpuXfbPrimitiveAccounting 与 Dynamic 冗余,二选一 MGPCaps 只有组合式尺寸断言,因为 DynamicBackendParameters 还带 SizeT(P0.5 没做)。P5 的裁定:不重写定宽,改为在 Hello/Welcome 里断言双方 sizeof(DynamicBackendParameters)、sizeof(MGPCaps) 与 buildFingerprint 相等,不等即 Fatal{AbiMismatch}。P6 的 spawn 同机同 ABI,继承这条即可;定宽重写记到 P7 的账上
MGPSubData::Target 低字节 = 资源目标,高字节 = 上传目标(P4a ID-12 的编码) 已定,抄进表
MGPImageView::Access ImageEmit.h:146-159 今天只活在包头里(P4a R-3),本表是它的第一个 wire 读者,必须收进契约
MGPSamplerView::Target SamplerEmit.h:900 同上
MGPFramebufferState::DrawBuffers[8] 的 −1 / default-token 收窄 FramebufferEmit.h:146-163 同上
MGPReplySlot::Id = 记录序号(R-3);服务端把 seq 回填进槽头自检
槽头 {Uint64 Seq; Int32 Status; Uint32 Size;},Status 0=OK、1=DECLINED、2=ERROR;DECLINED 是真答案不是失败(MapPersistent 的 nullptr 语义) 新增,契约包定义

表 1 — 字节载体表(杀 class 1/3/5、H2、H3)

19 行,一行一个"带内容或带尾巴"的调用。列: PipeCalls.def 标志 · payload 里的 blob 成员(或无) · 今天的伴随指针 · 字节住哪个段 · 谁拥有这块内存 · 槽什么时候 retire(apply / submit / GPU-complete) · 谁声明长度、谁交叉校验 · kReplySlot 行的回复名。

表头之上先写三条规则(R-2、R-11 的正式措辞):

  1. split 下,payload 带 MGPBlobRef 的记录若 Blob.Size == 0 → Fatal{ProtocolCorruption};
  2. split 下 apply 侧 MGHostSpan::Ptr != nullptr → Fatal;
  3. 任何 applier 入口不得跨返回持有指针,除非记录标了 borrowed;Espryt 的 hostBytes 队列是唯一具名例外, 且 P5 必须把它改成服务端自有拷贝。

已知的 19 行(来源 scout-premortem:§3 + scout-wire-codec:§2): CreateRenderState、CreateVertexElements、CreateShaderState、SetDynamicState、SetGlobalConstants、 SetResidualValueState、ResourceSubData、BufferSubDataResident(以上 8 条 kHasBlob); SetVertexBuffers、SetSamplerViews、BindSamplerStates、SetShaderImages、SetShaderBuffers、 SetStreamOutputTargets、SetVertexAttribDefaults、DrawVbo(以上 kVarTail,其中后两条另带 host span); ResourceRespecify(R-13.3)、CreateSamplerState(R-13.1)、ResourceFlushRange(R-13.2)。

免费的一块:CreateShaderState 的序列化已经存在且已被验证—— EncodeProgramArtifacts/DecodeProgramArtifacts(MG_State/GLState/ProgramState/ProgramArtifactsCodec.{h:53,60,cpp:252,264}), 有专门测试套件,并且 verify 构建已经在每一个真实程序上跑过往返 (PinProgramArchiveRoundTrip,PipeApply.cpp:1056-1087,调用点 :2638)。 任何包都不得再写第二份程序归档编解码器。 CreateSamplerState 的序列化 = memcpy(sizeof(SamplerParameters))(POD,borderColorForm 必须逐字节存活)。 MGPCaps 的两个 blob 的序列化器不存在,是 P5 的新活(MGPipeTypes.h:135-136 明说"随传输在 P5 落地")。

表 2 — PipeInputs 字段归属表(杀 class 6、裁定 B/§1.6/§1.7)

63 个字段 + 7 个 sticky forward,每个恰好一类,生成 + --check(R-7)。 四类的定义与已知归属:

RECORD-SUPPLIED(40 条,已有) — kMGPipeFieldEmittedBy / kMGPipeEmittedFieldCount = 40 (generated/PipeFilled.inc:336-402),减去 EmittedCallSuppliesTheWholeField 里返回 false 的 9 条。

APPLIER-DERIVED — applier 自己写的:render-state 镜像、m_pixelStore[0]、capability、 当前顶点属性、patch,加 MGPipeDeriveRenderStateFields(PipeApply.cpp:2157)派生的那批。

BARRIER-PULLED(P5 的债,逐条点名退役阶段) — 缩减路径实际会读的 21 个 (scout-unmigrated-census:§3 的并集):

GetActiveTextureUnit              GetPixelStoreParameters(unpack 半)   GetTextureUnitObject
GetBoundVertexArray               GetProgramForDraw                    GetTransformFeedbackCapturedVertices
GetBufferBindingSlot              GetSamplingResolutionGeneration      GetTransformFeedbackGeneration
GetBufferBindingPoint             GetTextureBindGeneration             GetTransformFeedbackProgram
GetTouchedBufferBindingPointCount GetTextureContextId                  GetBoundTransformFeedbackLifetimeId
GetCurrentVertexAttribute         GetMaxTouchedTextureUnit             IsTransformFeedbackActive
GetFramebufferBindingSlot         GetImageTextureBinding               IsTransformFeedbackPaused

退役阶段:GetBoundVertexArray / GetBufferBindingSlot(indirect 半) → P8; framebuffer / texture-unit / image / program / XFB 一族 → P3b/P4b(Espryt)与 P7(Magma); 三个纹理快门(GetTextureContextId/GetTextureBindGeneration/GetSamplingResolutionGeneration) 不是"迁移值"而是"服务端用自己的 Serial 回答"(Coverage.def:220-224),P3b/P4b。 GetPixelStoreParameters 必须在本阶段拆成 pack / unpack 两个字段,否则 readback 路径会因为 unpack 半没有载体而整字段 Fatal(scout-unmigrated-census:§3 的两种读法)——这是 P5 自己的一条小迁移。

FATAL — 剩下的 3 个非 sticky 未迁移字段(GetBoundTransformFeedbackName 已死、 GetTransformFeedbackPausedPrimitiveCounter 只在 kQuery、GetProgramForDispatch 只在 kDispatch) 加上 MGPipeUnmigratedEmulation 的五个站点(Managers.cpp:5334、DirectGLES.cpp:8051/:8702/:8997/:10623)—— 后者在 split 下从 (void)name; 变成 Fatal,这是 P5 改一个函数就给五个站点装上牙齿 (PipeApply.cpp:2812-2817 自己这么写的)。

七个 sticky forward 单列一节:GetBufferBindingPointCount、GetProgramObject、GetTextureObject、 HasOpenTransformFeedbackSpan、ValidateProgramName、InvalidateCompileEnv、RecordError。 P5 的归属:前五个 = BARRIER-PULLED(计入 rsp,strict 下 FATAL); InvalidateCompileEnv → OnCapsInvalidated;RecordError → 记 rsp,有序化留 P9(R-12)。 PipeFilled.inc:421 的无条件 fresh 在 split 构建下必须取消,否则这七个永远不进计数。

同一张表的形状再延伸到两个没有字段 id 的集合:

  • 客户端保守 GPU-write 集:一行一个后端 MarkGpuWritten 站点(6 个:DirectGLES.cpp:570/:618/:2603、 UniformManager.cpp:1075/:1231、VulkanRenderer.cpp:11618),映射到客户端必须触发的谓词,每行一条单元用例; 外加 P5 的两个新生产者(glReadPixels→pack PBO、glEndTransformFeedback,ARCHITECTURE.md:508)。
  • persistent-map 可达集:一行一个 SyncPersistentMappedRange 站点(文档记 20,侦察实点 21, 差异见 scout-gpu-writes:§2.4,契约包负责裁定并写进表)。

表 3 — 角色/线程所有权表(杀 class 4、class 7、H1/H5/H6)

ARCHITECTURE.md:581 说 MGPipe 把需要按角色复制的进程全局从四个降到两个。这个普查至少少算了五个 (scout-premortem:§1 class 4 + scout-gpu-writes:§5.3):

全局 位置 谁写 P5 裁定
gPipeInputs(~20 KB) PipeInputs.h:706 客户端残余填充 + applier 栅栏臂下单实例合法(R-1 的互斥不变量);栅栏退役前不得引入第二个写者
g_applier PipeApply.cpp:396 applier 服务端独占;头注释已写"split 下每个被服务的 context 一个"
g_resourceOps PipeApply.cpp:402 后端注册 服务端独占;客户端不得读(R-8)
gMGPipeSegmentResolver MGPipeHostSpan.h:47(非原子 inline 变量) MG_Remote 安装 每角色一份,在 apply 线程启动之前安装;inproc 下两角色的解析器必须指向各自的段视图
MG_Impl/Pipe 的十个 *Instance() fable-seam-audit.md:122-124 客户端 客户端独占;纹理 drain list m_drain 是进程级,审计已经把"每个 client context 一份"记成 P5 项
ScopedDefaultUnpackState::s_synced + 五个值影子 Managers.cpp:5490-5496 后端 服务端独占;第六个漏网的进程全局,登记即可(P5 缩减路径里客户端角色不碰 GL)
pActiveBackendObject GlobalObjects.cpp:23 MG_Backend::Init() 客户端装 BackendObject_Remote;服务端的 BackendObject_DirectGLES 由 ServerLoop 私有持有。唯一从后端内部读它的站点是 DirectGLES.cpp:12446(ClampSamplesToBackendSupport)→ 改成把 format cache 传下去,读法 (i) 成立,不需要 MOBILEGL_BUILD_DISAGGREGATED_INPROC 的线程键控 shim
gBackendFunctionsTable GlobalObjects.cpp:24 Init.cpp:44 客户端 = 发射表(R-4);服务端直接持有真表,不经这个全局

表 3 的每一行还要写第四列:make-current 与 teardown 对它做什么。 teardown 顺序按 ARCHITECTURE.md:537,并补上 ARCHITECTURE.md 没写的那一句: Doorbell::Kill() 是唯一能把 kWaitForever 上的 apply 线程叫醒的东西 (Doorbell.h:211-221,InProcessTransportTest.cpp:344 已经钉住这个形状); 先 Kill 再 join,join 之前客户端不得释放任何发射器拥有的 Vector。 ID-8 的规矩(没有前端析构函数可以从退出处理器跑进 pipe/后端状态)对两套角色局部单例各生效一次; 所有新的 MG_Remote/Client/*、MG_Remote/Server/* 单例按 ID-8 一律 leak-at-exit,不是新判断。 证明配方继承:两条车道都跑 GLIBC_TUNABLES=glibc.malloc.tcache_count=0。


§4 缩减路径的精确定义

P5 的门只对下面这条路径负责,包不得越界实现:

A. DirectGLES.Split.ClearThenReadPixelsScenario.*(场景已存在,5 个用例,默认 FBO,不走 PBO) → 需要的目录行:GetCaps(1)、Clear(57)、ReadPixels(58)、Present(67)。

B. DirectGLES.Split.TriangleScenario.*(新场景) → 再加 DrawVbo(59)、Flush(66),程序/VAO/buffer 侧复用 P2/P3a/P4a 已接的记录。 用 VBO 后端的 draw,不要 client-array 索引,这样 kDrawHasUserIndices 的 MGHostSpan 一条都不产生 (P8 才做 host span 的 split 填法)。相应地,MGPCaps 里 kCapNeedsHostIndexBytes 与 kCapNeedsHostUboBytes 两位在 P5 恒为 0——这是把 MGHostSpan 挡在首个 IPC 帧之外最便宜的办法。 写进表 0。

C. PersistentCoherentMapScenario(新场景,规格全文在 ARCHITECTURE.md:500) → map PERSISTENT|WRITE|COHERENT、透过指针写、不发任何别的 GL 调用、draw、回读校验。 第一天就要写进去的陷阱:场景大小的 buffer 远低于 16 MiB 的 kLargeBufferAdoptBytes, 所以 TryAdoptLargeStorage 不会触发,但 AcquireMemoryRange 自己的采纳会触发 (BufferObject.cpp:655-661),于是同一个测试在不同驱动/构建上落进采纳臂或模拟臂, 两臂代码完全不同。MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION 分不开它们。 场景必须断言自己在哪条臂上。(R-6 让 split 恒定落模拟臂。)

D. OpenRA trace,SSIM ≥ 0.99(trace_cases.json:14-28,31249 次调用,640×480,阈值 0.99) → verb 集由 R-4 的普查决定。OpenRA 不会加宽字段集(仍是表 2 的那 21 个), 它加宽的是站点集,并且是第一个够到读附件(Managers.cpp:8603/:8966)与 maxTouchedUnit >= 0 的纹理单元遍历的东西。

不在缩减路径上的、明确留给后续阶段的:fence 全族、query 全族、compute、XFB、 indirect、copy-region、GetTextureImage、GenerateMipmap、SetShaderBuffers、 SetStreamOutputTargets、ResourceSubDataComplete。

§5 包划分

一个契约包先落地(其余七个都从它之后分叉),然后七个并行包。 每个包在自己的 worktree ~/w7/p5-<slug> 上工作,结论写 ~/w7/notes/p5/p5-results/<slug>-v1.md, 中间日志自己清(临时文件纪律:只留当前阶段)。

文件所有权(防 P4a 那种同点追加冲突)

区域 属主
MG_Pipe/PipeCalls.def、PipeFields.def、Coverage.def、scripts/gen_pipe.py、MG_Remote/CONTRACT-P5.md c0 独占。任何包要改,走集成者。
MG_Pipe/generated/*.inc 由生成器产出;只有 c0 与 p1 可以触发重生成并提交
MG_Remote/Wire/*(新目录)、MG_Pipe/MGPipeHostSpan.h 的解析器安装点 w1
MG_Remote/Transport/*、MG_Remote/Protocol/protocol.fbs + 生成头 s1
MG_Remote/Server/*、MG_Backend/Init.cpp v1
MG_Remote/Client/*、MG_Backend/BackendObject*.{h,cpp} 的 remote 侧 c1
MG_State/GLState/BufferState/BufferObject.{h,cpp}、MG_Backend/DirectGLES/Managers.{h,cpp}、MG_Util/Metrics/PipeStats.* b1
MG_Backend/MGPipe/PipeInputs.{h,cpp}、MG_Impl/Pipe/PipeFill.{h,cpp} 的毒化/残余部分、scripts/gen_pipe_dirty_surface.py p1
MG_IntegrationTest/**、tools/trace_replay/**、.github/workflows/test.yml、~/w7/notes/tools/wsl_p5_*.sh t1
MobileGL/Config.h、ConfigLoader.cpp、根 CMakeLists.txt c0 先落全部新旋钮与构建布线,之后任何包新增旋钮走集成者追加

c0 — 契约(模型:fable;串行,其余包等它)

交付

  1. MobileGL/MG_Remote/CONTRACT-P5.md:§3 的表 0/1/2/3 全文(表 2 的权威副本由 p1 的生成器产出, c0 先落规格与四类定义)。
  2. PipeCalls.def 标志修正(R-13.1/.2)+ ResourceRespecify 初始字节的归宿裁定(R-13.3)。 不得移动任何 opcode;PipeCatalogueTest.cpp:418-422 钉着最后四个 opcode。
  3. gen_pipe.py:逐 opcode 标志表 kMGPipeCallFlags[kOpCount](R-13.4); --check + --self-test 保持全绿,生成物提交。
  4. MG_Config::Transport(Config.h,MOBILEGL_BUILD_DISAGGREGATED=OFF 时是 constexpr Monolith, ARCHITECTURE.md:576 的 #if 形状)+ ConfigLoader 解析 MOBILEGL_TRANSPORT=monolith|inproc (spawn|unix:|pipe: 解析出来但返回"P6 未实现"的具名错误)+ MOBILEGL_IPC_* 全家旋钮的解析与默认值 (_RING_MB=8、_STAGE_MB=32、_SPIN_US=50、_PERSISTENT_BLOCK_KB=64、_ADOPT_TIER=2、 _VERB_BARRIER=1、_STRICT_ERRORS=0、_AUDIT=0、_SERVER_AFFINITY=auto)。 解析器抄 InitBackendType()(ConfigLoader.cpp:284-300)的形状。
  5. CMake:MOBILEGL_BUILD_DISAGGREGATED_INPROC 选项(蕴含 DISAGGREGATED)、 MG_Remote/{Wire,Client,Server}/ 的源文件列表、MOBILEGL_PIPE_PUSH 源列表 (CMakeLists.txt:484-499)里追加新文件。
  6. 头文件与桩,让七个包当天就能并行编译: MG_Remote/Wire/PipeWireCodec.h(EncodeRecord/DecodeAndApply/StageBytes 的签名)、 MG_Remote/Client/ClientSession.h、MG_Remote/Server/ServerSession.h、 MG_Remote/Client/EmitTables.h、MG_Remote/Server/PipeApplier.h、 MG_Remote/CapsCodec.h。全部带 MGLOG_F+abort 的桩体。
  7. verb 普查(R-4):用全 Fatal 的发射表跑 A/B/D 三条路径,产 ~/w7/notes/p5/verb-census.md。 这是唯一一件 c0 必须"先跑起来一点东西"的事——如果 c1/v1 还没有,普查可以用一个最小的 --gtest_filter 桩程序 + 手工 trace 解析做,或推迟到 c1 落地后由集成者补跑并广播。
  8. Ring.h 头注释补 R-9 的五句 + kRecPad 不计 seq 的规则 + 五条单元用例。
  9. 纯度:新头文件若从 ITransport.h 可达,不得 include MG_Util/Debug/Log.h 或任何够到 MobileGL/Includes.h 的东西(check_include_closure.py:136-144 的 wire-header 探针),用 WireLog.h。

验收:gen_pipe.py --check/--self-test 绿;三个构建目录(pull/push/split)都配得起来并编译过; nm --defined-only build-linux/libMobileGL.so | grep -i MG_Remote 仍为空(G1 不动); MOBILEGL_TRANSPORT=monolith 下 ctest -L integration-gpu 与今天逐名相同且全绿。


w1 — G3 编解码器(opus)

交付

  • MG_Remote/Wire/PipeWireCodec.{h,cpp}:71 条的 encoder(MGPWireRec_* 写入 + var-tail 写入 + blob 暂存)与 decoder(bounds + tail 算术 + 段解析 + 调 MGPipeApply*)。 decoder 不自己实现语义:它解码完就调今天的 applier 自由函数,PipeApply.cpp 一行不改。
  • MGPipeApplyWireRecord(PipeWire.inc 的骨架)改成把工作转给上面这个 decoder; 生成器里那段 "SKELETON … until P5" 的注释换成真话。
  • gMGPipeSegmentResolver 的安装(每角色一份,R-表 3)。
  • SEG_STAGE 的线性分配器 + retiredSeq 回收;MOBILEGL_IPC_AUDIT=1 的 0xDD 毒化(R-2.5)。
  • MGPCaps 的两个 blob 序列化器(FormatCapabilityCache 与 RendererInfo)+ 往返单元测试。 CreateShaderState 用已有的 EncodeProgramArtifacts/DecodeProgramArtifacts,禁止重写。
  • R-2 的四条 Fatal 臂 + R-10 的最大记录字节计数器。
  • var-tail 的长度交叉校验:MGP_WIRE_CHECK_BOUNDS 今天只证明 size >= sizeof(rec),看不见尾巴; 一条声明 Count=4000 只带 8 字节的记录今天能过。decoder 必须按 count 重算总长并与 MGPWireRecHeader.Size 对齐校验。

验收:新单元套件 MG_Test/Wire/PipeWireCodecTest.cpp——71 条里至少覆盖 {每个标志类至少一条、两条双尾巴(SetShaderBuffers/SetStreamOutputTargets)、 DrawVbo 的条件尾巴、七 blob 的 CreateShaderState},加四条 Fatal 的死亡测试。


s1 — 会话与传输(opus)

交付

  • MG_Remote/Client/ClientSession.{h,cpp} + MG_Remote/Server/ServerSession.{h,cpp}: 在 ShmSegment 上建 RingControl + RingProducer/RingConsumer(inproc 也用 ShmSegment,不用 new, 这是"同一条代码路径"的一半),拿 InProcessTransport 的两个门铃。
  • 握手:Hello/Welcome/CapsSnapshot/SurfaceOp(今天零调用者); Welcome 的四个 SegmentRef 尺寸已被 ProtocolSmokeTest.cpp:72 钉住,照抄。 ABI 断言按表 0(sizeof(DynamicBackendParameters)、sizeof(MGPCaps)、buildFingerprint)。
  • 五条水位线 + 两个事件标志的真实写入(R-9),含 publish→NotifyIfParked 的顺序 (RingTest.cpp:446 已经钉住这个调用序)。
  • SEG_REPLY 的最小槽池 + 槽头(表 0)+ seq % slots 寻址(R-3); SEG_EVENT 只做到"能承载 OnBufferWriteback / OnGpuWritten / OnSurfaceChanged",溢出策略留 P9。
  • 两个门铃访问器今天挂在具体类 InProcessTransport 上而不是 ITransport (InProcessTransport.h:64-68)——在 P5 里决定:要么提到 ITransport,要么在会话层封一层。 P6 才发现的话要重铺一个包的调用点。
  • 双角色峰值 RSS 的记账钩子(/proc/self/status:VmHWM 采样 + 段大小台账),给 t1 用。

验收:MG_Test/Wire/SessionTest.cpp——两条真线程把 20000 条记录跑通、 水位线的五条规则各一条用例、kRecPad 不计 seq 的用例、Kill()+join 的关闭用例 (抄 InProcessTransportTest.cpp:344 的 5 秒有界 join,免得回归变成 CI 挂死)。


v1 — 服务端与线程(模型:fable;本阶段最高风险)

交付

  • MG_Remote/Server/ServerLoop.{h,cpp}:mgl-srv-apply 线程(P5 不建 mgl-srv-io, inproc 下控制面就在同进程;P6 再拆),park 走 Doorbell::Wait(consumerParked, …, kWaitForever), Wait==false && Dead() 即关闭。
  • EGL 所有权迁移:服务端角色私有持有 BackendObject_DirectGLES; eglMakeCurrent 在 apply 线程上跑一次并终身持有(DirectGLES.cpp:11925 那一串 + :11933-11953 的六个缓存失效变成启动一次性)。客户端的 EGL 生命周期调用(InitializeEGLDisplay、 CreateEGLWindowSurface、MakeEGLCurrent、SwapEGLBuffers、SetEGLSwapInterval、 ReleaseEGLSurface、ReleaseEGLResources)作为阻塞控制请求转给服务端,在 apply 线程上执行。 ReleaseEGLResources / ~BackendObject_DirectGLES 必须是阻塞的,否则 MobileGL::Destroy() 会在服务端还持着 context 的时候往下走(scout-install:§5.3)。
  • MG_Backend/Init.cpp:50 的单一 hook(#if MOBILEGL_BUILD_DISAGGREGATED + Transport != Monolith)。 注意同文件 :43-44 还要给 gBackendFunctionsTable 喂东西——那是 c1 的发射表。
  • Server/PipeApplier:把 w1 的 decoder 接到 MGPipeApply*;服务端的毒化 stamp (§1.6 的前置条件:applier 今天故意不 stamp,split 下必须有人在 verb 边界 stamp,否则第一次读就 Fatal)。 与 p1 协同:p1 定规则,v1 放调用点。
  • R-11 的落地:Ops_H_SubData/Ops_H_FlushRange 在 split 下把字节拷进服务端自有存储, GLESBufferResource::hostBytes 不再指向客户端影子。
  • teardown 顺序(ARCHITECTURE.md:537 + 表 3 的 Kill/join 补充)。

验收:ClearThenReadPixels 在 MOBILEGL_TRANSPORT=inproc 下绿; 0xDD 审计臂下跑同一场景不红;关闭路径在 GLIBC_TUNABLES=glibc.malloc.tcache_count=0 下无 UAF; apply 线程的亲和(MOBILEGL_IPC_SERVER_AFFINITY,抄 ShaderCompilePool 的大核探测)落地并把 解析出的 mask 打日志。


c1 — 客户端与发射表(opus)

交付

  • MG_Remote/Client/BackendObject_Remote.{h,cpp}:BackendObject 的 8 个纯虚 + 9 个 EGL 虚函数。 GetRendererInfo() 返回引用,所以远程对象要自己持有一份 RendererInfo; GetFormatCapabilities() 是非虚的,必须填 m_formatCapabilities 而不是重写访问器。 GetBackendType() 返回服务端的后端类型,不是新枚举值。 LogBackendInfo() 在 caps 到达之前就会读 GetRendererInfo()(Init.cpp:21)—— 按 scout-caps-reply:§1.2 的 (a):返回占位,接受启动时一行日志不准,不重构 MG_Backend::Init()。
  • MG_Remote/Client/EmitTables.{h,cpp}:69 个槽,未实现 = Fatal{UnmigratedVerb}(R-4)。
  • MG_Remote/Client/CapsMirror.{h,cpp}:caps 快照的客户端副本;R-12 的"再次到达即失效"。
  • verb 栅栏(R-1):发射后等 appliedSeq >= emitSeq,spin(_SPIN_US)→park。 栅栏的那次等待同时取回复(R-3/R-5)。
  • R-8 的存活门改读 caps 镜像。
  • 阻塞 read_pixels:把像素从 SEG_REPLY 拷回应用指针; PBO 目的地的那一半(fire-and-forget + 客户端 MarkGpuWritten)属于 b1。

验收:Triangle 场景在 inproc 下绿;发射表的 Fatal 臂有一条死亡测试; caps 镜像的单元测试覆盖 glGetString/glGetIntegerv/glGetStringi 三条读路径。


b1 — buffer 侧(opus)

交付

  • 客户端保守 GPU-write 集(MG_Remote/Client/GpuWritePending):按表 2 的 6+2 行站点映射建集; SyncGpuWrites 的第三状态("已发回读、答案未到")→ 在栅栏下退化成阻塞等 OnBufferWriteback。 不做收窄(ResourceTracker.h:587-592 的 rangeCount == 1 断言保持,收窄是 P8/P9)。
  • persistent-map 块粒度推送:m_livePersistentMaps 集合(= SyncPersistentMappedRange 早退链读作成员测试)、 64 KiB 块、键 {MGPipeHandle, blockIndex}、走已有的 resource_subdata 记录,不新增记录类型; Phase 1 = 保守(整个 mapped span 按块推)。
  • hasLiveHostWrites 位上线:MGPResourceDesc/MGPSubData 的 pad 位(:303/:1004), 并且必须登记进 MGPipeResourceRespecifyNeedsAck 旁边的元数据字段清单(P4a ID-18 M4), 否则第一次推这个位就要一次假重分配。
  • 抬起四个 pin:PinNoLiveHostWrites(PipeApply.cpp:839-850 的五个调用点)、 Managers.cpp:2669-2674 的断言、:2675-2681 的 frontend->IsMapped() 最后一处前端读、 SanityTest.cpp:4583 的单元门(它记录的正是"这条搞错了会 draw-clean forever 且无诊断")。
  • persistent-map-push 接线(PipeStats.h:72,短名 pmap;同一个提交里必须改 PipeStats.cpp:68-70 那段"P0 未接线"的清单文字,否则清单在说谎)。
  • R-6 的 T2 强制 + MOBILEGL_IPC_ADOPT_TIER 负面对照。
  • flush 阶梯(Managers.cpp:1047-1076):hostBase 的"每次使用重求值"在 split 下变成 "发射时快照进 SEG_STAGE";tier 1 的 INVALIDATE_RANGE 是"旧字节已死"的断言, 晚到或加宽的快照会静默打烂 GPU 写过的数据(Managers.cpp:1126-1129 已经流过一次血)—— 这条要有单元用例。

验收:PersistentCoherentMapScenario 在 inproc 下绿且断言自己在模拟臂; pmap 出数非零、mpr 与 monolith 臂相同;MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 下同场景变红(负面对照); MG_Test 的 buffer 族零回归。


p1 — 毒化、字段归属与可见性(opus)

交付

  • 表 2 的生成器:63 字段 + 7 sticky 的四类归属,产 generated/PipeFieldOwnership.inc + --check (抄 gen_pipe_dirty_surface.py 的 check/self-test 形状),不在任何一类里 = 构建失败。
  • PipeStats::CallClass::ResidualPulls(短名 rsp,放进 #if MOBILEGL_PIPE_PUSH 块内)。
  • split 下的 stamp 规则(与 v1 协同):谁在服务端 verb 边界 stamp、stamp 什么。
  • PipeFilled.inc:421 的 sticky 无条件 fresh 在 split 构建下取消;七个 forward 走 R-7.3。
  • MOBILEGL_IPC_STRICT_ERRORS=1 车道 + 一条具名的 abort 断言测试。
  • MGPipeUnmigratedEmulation 的 split Fatal 臂(PipeApply.cpp:2820 一个函数,五个站点)。
  • GetPixelStoreParameters 拆成 pack / unpack 两个字段 id(表 2 的小迁移)。

验收:--check/--self-test 绿且负面对照(把一个字段从表里删掉)按名变红; strict 车道在缩减路径上按预期 abort 并点名;默认车道 rsp 逐帧出数。


t1 — 测试与 CI 布线(opus)

交付

  • Scenarios/TriangleScenario.cpp、Scenarios/PersistentCoherentMapScenario.cpp (+ MG_IntegrationTest/CMakeLists.txt 源列表两行)。
  • DirectGLES.Split. 车道:一个场景一个 gtest_discover_tests 块 (抄 DirectGLES.MapPersistentRoundtrips. 的 :1377-1394), mgl_itest_join_environment 拼环境并必须追加 ${MGL_ITEST_COMMON_ENV}, 标签写 "integration-gpu\;integration-split"(私有标签让 ctest -L integration-split --no-tests=error 在忘了开构建选项时变红而不是绿跑零个)。
  • trace-replay 的 SPLIT:走 run_trace_case.cmake 的环境读法 (set(ENV{MOBILEGL_TRANSPORT} ...),抄它已有的 $ENV{MOBILEGL_PIPE_VERIFY} 处理,:153-190), add_trace_replay_test 加变体后缀参数(add_test 重名是硬错误), set_tests_properties 里带 MOBILEGL_IPC_SERVER_PATH。 TRACE_OUTPUT_DIR/TRACE_ARTIFACT_DIR 必须也带变体,否则两条臂写同一个 output/mobilegl.log,而那个文件是唯一有效的拒绝普查来源(ctest -V 的普查是假零)。
  • build-split 构建目录 + ~/w7/notes/tools/wsl_p5_gate.sh(五部分,§6)。
  • CI:build-linux-split(克隆 build-linux-verify,带 nm 证明库里真有 MG_Remote 符号—— 这一步的缺席正是"split 车道跑着 monolith 还全绿"的成因)、integration-split、retrace-split (克隆 retrace-verify,把 MOBILEGL_PIPE_VERIFY=1 换成 MOBILEGL_TRANSPORT=inproc)。 不挂临时分支触发(R-14)。
  • 双角色峰值 RSS 的采集(/proc/<pid>/status:VmHWM 采样 + s1 的段台账),记录不设门。
  • 读日志的用例(pmap 出数那条)要自己一条车道 + 自己的 MOBILEGL_LOG_FILE_PATH + RESOURCE_LOCK (不加锁实测 3 次全标签 -j 8 会出 4/0/2 次伪失败)。
  • ~/w7/p5-before-ctest-names.txt 与 ~/w7/p5-before-libMobileGL.so 快照(G14/G1 的基线)。

验收:ctest -L integration-split 在 build-split 里匹配到东西且绿; 在忘开选项的构建里变红;retrace_gate.py 能跑 OpenRA 的 inproc 臂并出 SSIM。 (retrace_gate.py 读的是 ~/mgl/MobileGL/build-retrace 的外来 CTestTestfile.cmake, 名字正则不认 SPLIT 后缀——零代码改动的路子是用现有名字 + 进程环境导出 MOBILEGL_TRANSPORT=inproc, 与 wsl_p4a_gate.sh:69 对 MOBILEGL_PIPE_VERIFY=1 的做法逐字相同。先走这条。)

§6 门(五部分,沿用 P4a 的结构 + 第五个 split 臂)

~/w7/notes/tools/wsl_p5_gate.sh,四个构建目录: build-linux(pull)、build-push、build-verify、build-split ($COMMON -DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON)。 全部 fail-fast,GLIBC_TUNABLES=glibc.malloc.tcache_count=0。

第 1 部分 — 接口纯度(继承 P4a 全部)加:

  • nm --defined-only build-linux/libMobileGL.so | grep -i MG_Remote 为空(G1 的 split 版);
  • nm --defined-only build-split/libMobileGL.so | grep -i MG_Remote 非空(否则 split 车道在跑 monolith);
  • G1 本体不变:pull 符号 0 增 / 0 删 / 0 重命名 / 0 resize,.text 字节不变。 P5 碰了 PipeInputs.h、BufferObject.cpp、Managers.cpp、PipeStats.*——这些都在 pull 构建里, G1 是本阶段最容易破的门,每个包自测时就要跑。

第 5 部分 — 覆盖与生成器加:gen_pipe.py --check/--self-test、 gen_pipe_dirty_surface.py --check/--self-test、新的字段归属 --check/--self-test(p1)。

第 3 部分 — 行为 A/B加:

  • ctest -L unit 在 build-split 里——这是五个 MG_Test/Wire 套件的第一次真实运行,加上 w1/s1 的新套件;
  • ctest -L integration-gpu 在 build-split 下分别以 MOBILEGL_TRANSPORT=monolith 与 =inproc 跑, 逐名相同(ARCHITECTURE.md:521 原话要求的就是这个三臂形状);
  • ctest -L integration-split 必须匹配到东西且绿;
  • G2 不变(pull vs push 逐名相同)、G14 不变(0 删除,新增记数)。

第 2 部分 — 语义对照加:

  • MOBILEGL_TRANSPORT=inproc python3 ~/w7/retrace_gate.py --tree ~/w7/pipe --lib ~/w7/pipe/build-split/libMobileGL.so --out ~/w7/retrace-out/p5-split -j 4 --only 'OpenRA' → SSIM ≥ 0.99;
  • 79 例 retrace 的 push / verify 两臂仍然 79/79(P5 不许把 monolith 弄坏)。

第 4 部分 — 设备(wsl_p5_bench.sh):只用红米 2f7cbe2e,其余 adb 设备一律不碰。 风扇按已记录的档位(只按 real_speed 判断状态,101–104 会锁进调试模式)。 P5 的设备任务是记录,不是门:inproc 臂的帧时与 monolith 臂配对、双角色峰值 RSS、 rsp/pmap/mpr 三个计数器逐用例。


§7 退出门(把 ROADMAP:21 那一行改写成可以变红的形式)

ROADMAP 原文六项里有两项是记录不是门("两个角色峰值 RSS 在案"、"persistent-map-push 出数"), 按 ROADMAP.md:57 对性能的处理方式标注清楚。真正的门,逐条带负面对照:

E1 DirectGLES.Split.ClearThenReadPixelsScenario.* 与 DirectGLES.Split.TriangleScenario.* 在 inproc 下绿。负面对照:MOBILEGL_IPC_VERB_BARRIER=0 下必须红,且红的方式被记录一次 (证明栅栏是承重的,不是摆设)。

E2 OpenRA trace 在 inproc 下 SSIM ≥ 0.99。负面对照:把 Clear 的发射器改成丢弃一次 (临时补丁)必须让 SSIM 掉下阈值——证明这条臂真的走了 split 路径。

E3 PersistentCoherentMapScenario 绿,且(premortem §4 的五条加强版): (a) MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 下同场景红,红过一次并记录; (b) 场景里有一次没有任何 GL 调用宣告的写(map、写、draw、再写、再 draw、回读), 这样"每个 validate 点推送"是必需的而不是被某次 glBufferSubData 顺带满足; (c) split 臂断言 pmap > 0 且 mpr 与 monolith 臂相等; (d) 断言 split 下 MGPipeApplyMapPersistent 从不返回非空指针——这是抓 inproc 地址泄漏的那条, 也是让这个门在 inproc 与 spawn 下含义相同的唯一一行; (e) 以上五条再跑一遍 ring 小到足以至少发生一次背压等待的配置。

E4 未迁移输入的门(加强版,替掉原来那一句):

split 下,63 个 PipeInputs 字段 + 7 个 sticky forward 每一个都在 RECORD-SUPPLIED | APPLIER-DERIVED | BARRIER-PULLED | FATAL 四类之一,表是生成的且 --check 在 CI 里; 不在任何一类 = 构建失败。FATAL 类的读确实 abort 并点名(现有毒化 + 五个 emulation 站点的新 split 臂)。 BARRIER-PULLED 类的读被 rsp 计数并逐帧发布;MOBILEGL_IPC_STRICT_ERRORS=1 把它提升为 FATAL, 一条具名测试断言 abort 发生。sticky 的毒化豁免在 split + strict 下取消。 负面对照:把一个字段从表里挪走(supplied → FATAL)必须让一条具名测试变红。

E5 inproc 诚实性五条(R-2)各自被观察到红过一次并记录。 这是把 premortem 裁定 A("inproc 什么都证明不了")变成可检验命题的唯一办法。

E6 全门第 1/2/3/5 部分绿;G1 0/0/0/0;G2 逐名相同;G14 0 删除; integration-verify 零 Fatal{;79 例 retrace 的 push/verify 两臂不退。


§8 记录(不设门,进 MEASUREMENTS)

  1. rsp 逐帧/逐用例——P6/P7/P8 欠债的大小,P5 结束时的数字是后续阶段的基线。
  2. pmap 与 mpr 的对照(两者反相关:T2 下每次 MapPersistent 都 decline,mpr 不变、pmap 从 0 起来)。
  3. 最大记录字节(R-10 的证明义务)。
  4. 双角色峰值 RSS + 段台账(SEG_CMD 8 MiB / SEG_STAGE 32 MiB / SEG_REPLY 16 MiB(ID-47 起;原 8 MiB) / SEG_EVENT 256 KiB 是 ProtocolSmokeTest.cpp:72 钉住的规范尺寸)。要 N 帧上的斜率,不只是一个峰值—— present 1:1 的批处理/credit bug 表现为慢泄漏,SSIM 看不见,两个场景的车道也够不着。
  5. 栅栏成本:inproc vs monolith 的配对帧时(红米,定频,尾 200 帧 p50/p99)。 预期变慢,这是 R-1 的已知税,写清它随栅栏退役而消失。
  6. 桌面 DriverBench:P4a 已经证明它分辨不出单阶段增量(T1−T2 在两次运行里 133.9 / 11.4 ns 而每臂自己漂 ~200 ns),P5 不单独引用它。

§9 风险登记(按"会不会让阶段停摆"排序)

# 风险 证据 缓解
1 EGL context 迁移到 apply 线程跑不通 16+19 个 off-thread 降级站点;ReleaseEGLResources/析构在 app 线程 R-1 的退路(inproc-inline 中间态)+ v1 交给 fable + 第 4 个工作日的硬检查点
2 Transport/ 从未被编译过 scout-test-ci:§5.2,全 CI 零 =ON 配置 c0 第一天就把四个构建目录配起来并编译,编译错误由 c0 就地修(这属于契约包,不是 s1 的账)
3 G1 被打破(pull 符号动了) P5 要改 PipeInputs.h/BufferObject.cpp/Managers.cpp/PipeStats.* 每个包自测跑 G1;新计数器一律放进 #if MOBILEGL_PIPE_PUSH 块内(PersistentMapPush 例外——它已经在两个构建里,安全)
4 acceptance 被某个包自行"客户端重推导" P4a 的 c0f/c0g 就是这个形状,代价两轮契约修 R-5 写死;复审第一条检查项
5 inproc 偷偷用宿主指针 MGPipeHostBytes 优先 Ptr 分支;五个发射器今天都写 Size=0 R-2 的五条 + 0xDD 审计 + E5
6 一条记录超过 Capacity()/2 CreateShaderState 的归档 R-10(blob 走 SEG_STAGE,记录恒小)+ 最大字节计数器兜底
7 SPLIT 臂的日志/目录撞车导致假零普查 TRACE_OUTPUT_DIR 不带变体;ctest ENVIRONMENT 是替换不是追加 t1 的目录变体 + mgl_itest_join_environment + 每车道自己的日志 + RESOURCE_LOCK
8 teardown 挂死或 UAF 只有 Doorbell::Kill() 能叫醒 kWaitForever;ID-8 的退出顺序规矩 表 3 第四列 + 有界 join 的用例 + tcache_count=0
9 Magma 车道空转 DirectVulkan 不注册 MGPipeResourceOps(P4a 的消费者门) P5 的 split 车道只跑 DirectGLES;DirectVulkan 的 split 是 P7。写进 brief,免得有人去修一个不该在这一阶段修的东西
10 OpenRA 需要的 verb 比预期多 R-4 的普查还没跑 普查在第 1 天出结果;若 >15 个 verb,集成者裁剪 E2 的范围并在 ROADMAP 上记账

§10 流程(沿用,写明差异)

  • 契约包 c0 先落地并推送;七个并行包从 c0 之后分叉,各自 worktree。
  • 每包一轮对抗性复审。同一个包返工超过两轮就交给 fable(研究根因/同类缺陷,或直接做那一轮)。
  • 集成按依赖序:c0 → w1 → s1 → v1 → c1 → b1 → p1 → t1;冲突按文件所有权表判, 共享测试文件取该包的最终版本再重放另一边的改动(P4a 的教训:盲目 theirs-first union 会重复整块)。
  • 验证轮 → 终审 → 全门 → APK → 设备记录 → docs(README/ROADMAP/ARCHITECTURE/MEASUREMENTS 四件)→ push。
  • 决策全部写 ~/w7/notes/p5/INTEGRATOR-DECISIONS.md(ID-1…),每条带证据与推翻它需要什么。
  • 提交信息:[Type] (Scope): description 单行,不带任何署名行。
  • 顺带修正(各包在自己的文件里做掉,不单独开包): ARCHITECTURE.md:83 的 "61 个" → 63;ARCHITECTURE.md:492 引的 PipeStats.h:126 → :141; ROADMAP P4a 行的 emulation 站点数 6 → 5 次调用 + 1 处注释; ARCHITECTURE.md:578/581 的"四 → 二"普查按表 3 补到至少七项。

§11 给复审者的检查清单(第一轮就照这个打)

  1. 有没有任何一处在 split 下仍然使用宿主指针 / Blob.Size == 0 / Seg == 0 携带内容?(R-2)
  2. 有没有第二份 acceptance 谓词、第二份 D-K2 依赖表、第二份程序归档编解码器?(R-5、表 1)
  3. appliedSeq 有没有在栅栏期被批处理?kRecPad 有没有被一侧计进 seq?(R-9)
  4. 有没有 applier 入口把指针留过返回?hostBytes 改干净了吗?(R-11)
  5. 客户端还有没有在读 MGPipeGetResourceOps() 来决定发不发?(R-8)
  6. 发射表里有没有 null 槽,或者穿透去调驱动的槽?(R-4)
  7. 表 2 里有没有字段落在四类之外?sticky 的豁免在 split 下取消了吗?(R-7)
  8. 新的 ctest ENVIRONMENT 有没有漏掉 ${MGL_ITEST_COMMON_ENV}?; 转义了吗? SPLIT 臂的输出目录带变体了吗?(t1)
  9. 新计数器有没有掉到 #if MOBILEGL_PIPE_PUSH 块外面去(G1)?
  10. 有没有任何 P5 的 CI 步骤挂在 feat/disaggregated 这个临时触发上?(R-14)
  11. (R-16,第一波实测出来的本阶段头号缺陷类)测试有没有自己构造它本该观察的那个状态? 三个包在同一轮里各犯了一次:p1 的自测 harness 抓任何 SystemExit,于是 4 号对照 因为转义写坏而跳去触发 1 号对照的消息,"11 条全红"实为 10;b1 的 persistent-map 用例 手写记录字段,于是把生产者删掉它照样绿;t1 只看符号的探针对着契约桩武装, 实测 11 条 Split. 条目全绿而跑的是 monolith。 规则:每一条门/对照都必须由生产路径驱动,并且必须实际观察到它因自己的理由变红一次。 做法:把它要抓的东西真的破坏掉,看它红,再恢复;对照必须断言自己的失败串, 不能只断言"失败了"。报告里要写下这次观察。

§13 R-16 — 门的自证义务(2026-09-11 第一波之后追加)

上面第 11 条的正式形式,对第二波(v1/c1)与终审同样生效:

  • 一条断言不得构造它要观察的状态。 手写记录字段、手动置位、直接调内部函数去"准备"被测状态, 都会把"生产者不存在"这个失败模式变成不可见。
  • 一条负面对照必须断言它自己的失败原因(问题串/测试名/Fatal 的措辞),不能只断言进程失败了。
  • 一条探针不得对着桩武装。 符号存在 ≠ 实现存在;与"不再残留 Fatal{Unimplemented"之类的 实现性条件取合取。
  • 每条门在报告里都要带一行"我把它弄红过一次,方式是 X"。 没有这一行的门按未验证处理。
  • 已知的反例清单(三条,见第 11 条)进 MEASUREMENTS 的缝分类学,作为 P4a 九类之外的第十类: 「一条不会因自己的理由变红的门」的三种具体长相。

§12 契约包落地后的更正(2026-09-11,c0 41b4f8df..b4bbcc11)

冲突时 MobileGL/MG_Remote/CONTRACT-P5.md 优先于本 brief。 完整清单见 ~/w7/notes/p5/p5-results/c0-v1.md §6(14 条)与 ~/w7/notes/p5/verb-census.md。 下面只列会改变某个包做什么的那些。

C-1(最重要)ResourceRespecify 有第二个没有载体的伴随物,本 brief 漏了。 const MGPRespecifiedLevel* level(PipeApply.h:792-795)是重定义的作用域, MGPResourceDesc 表达不了它。没有载体的话,OpenRA 里每一次逐 level 的 glTexImage2D 都会静默走整资源臂并丢掉全部 pending 上传。c0 已裁定用现有 pad 位(Pad1 → 两个 Uint16, Pad0 的一个字节 → HasRespecifiedLevel;尺寸不变、POD 断言不动)并在后续轮里落地。 w1 编码这条记录之前必须先有它。

C-2 发射表是 71 个指针,不是 69。 Present 与 SetSwapInterval 在 MG_Impl 里 零调用点,只从 BackendObject.cpp:396/:403 经 EGL 路径到达——照着 GLImpl 的调用点镜像出来的 表两个都盖不到,而 Present 在三个目标里的两个上都在缩减路径。它们与 v1 正在改道的 EGL 那条缝是同一件事。

C-3 verb 普查的答案是 6 个槽,不是 6–15(OpenRA 是实测不是估算,fixture 本来就 hydrate 着, 没有任何 LFS 拉取):Clear、DrawArrays、ReadPixels、BlitFramebuffer、GetIntegeri_v、Present。 其余 65 个槽 Fatal{UnmigratedVerb}。三个陷阱:

  • Flush 根本不是一个槽,而且 glFlush/glFinish 是空函数体 (Definitions.cpp:111-112)——R-4 预测集里的 Flush 作废;TriangleScenario 若想用 glFlush 给回读排序,它排的是空气。
  • GetIntegeri_v 由一个上下文的第一次 glCompileShader 到达(CompileEnv.cpp:134-138), 不由任何 verb 到达;全 Fatal 的表会在每个场景的第一次着色器编译上 abort。
  • OpenRA 是三个目标里最薄的那个,不是最宽的:它加宽的是站点集与记录集,verb 集一点没加宽。

R-15(新裁定,因 C-3 而生)getter 形状的 GLFunctionsTable 槽由 caps 镜像在客户端就地回答, 不发射记录。 GetIntegeri_v 属于这一类(它携带的六个 compute 上限本来就在 MGPCaps::Dynamic 里), 现成的门是 AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer。 71 个槽分成「就地回答 / 发射 / UnmigratedVerbFatal」三类,清单在 CONTRACT-P5.md,c1 不要自己重推。

C-4 R-8 按原文不可实现:CallMask 只有九个 MGPCapBit 特性位,没有逐族位。 c0 裁定 bits 32..47 = (serverConsumedSubsystems & 0xFFFF) << 32,常量与折叠/测试 helper 在 MG_Remote/CapsCodec.h。protocol.fbs 的 tableSlotMask 删除(GLFunctionsTable 有 69 个槽, ulong 是 64 位,这个字段从来就寻址不了它自称的那张表),maxComputeWorkGroupCount/Size 与 prefersCpuXfbPrimitiveAccounting 一并删。

C-5 表 1 是 23 行不是 19,而且 brief 的 19 与 scout-premortem:§3 的 19 是两个不同的清单。 多出来的四行是服务端→客户端方向的(MapPersistent、ResourceReadback、ReadPixels、GetTextureImage)。 ReadPixels 的目的地裁定为在 P5 里永远是 SEG_REPLY,所以 MGPReadbackInfo 不加 Seg 字段。 另外 SetResidualValueState 是第四个有类型的伴随物(applier 收 const ResidualValueBlock&, 而 MGPResidualValueState 在活路径上从未被实例化过)——按 w1 最硬的一行排期,不是简单八行之一。

C-6 ResourceFlushRange 不携带任何字节(R-13.2 的第三个答案):它驱动的阶梯是 「从权威影子重写自己的 range」,而 R-11 之下权威影子归服务端,所以 resource_subdata 已经是字节 到达那里的唯一途径;再给一个 blobref 等于给同一件事第二种可伪造的说法。 栅栏为 buffer 族退役时重新审视。

C-7 那 18 个 abort 与毒化无关,本 brief 的诊断是错的。 它们是测试故意触发并期待的 Fatal{ProtocolCorruption} 绊线;七个 MG_Test/Pipe TU 在 #if 里测 MOBILEGL_PIPE_POISON 却没 include 唯一定义它的头,于是编了不 abort 的那条臂,而 PipeApply.cpp 编了 abort 的那条。 MOBILEGL_BUILD_DISAGGREGATED 是唯一住在那个头后面的 arming 条件,所以 split 是第一个能暴露它的构建。 c0 已修(每文件一个 include + 一处潜伏的 guard 更正),p1 的机制是清白的。

C-8 杂项数字更正(照 c0-v1 §6 的 9/10/11/12/13/14 条): CanTouchGLNow() 守 16 个站点不是 19;ScopedDefaultUnpackState 有六个值影子不是五个; DirectGLES.cpp:12446 不是 ClampSamplesToBackendSupport(真正的是 BackendObject_DirectGLES.cpp:807-828,后端内部读 pActiveBackendObject 是七处跨六个函数, 不是一处——表 3 的结论不变,diff 变);MGPipeApply* 是 37 个入口 / 41 个调用点不是 ~45; persistent-map 普查是 21 个站点不是 20,且 ARCHITECTURE.md:290 引的那张 §5.7 归属表 在树里不存在(b1 别去找);MGHostSpan 不是 MGPHostSpan。

C-9 所有包第一天都会撞到的一件事:worktree 的 submodule 是指向 ~/w7/pipe 的符号链接 (git worktree add 不带 submodule 内容,递归 init 要联网)。 git 在它们是符号链接时会拒绝运行,用 ~/w7/p5-c0-submodlinks.sh off|on 切换: 跑任何 git 命令之前 off,跑构建之前 on。

C-10 脚本自检的教训:一个早期 bring-up 脚本报了四个"绿"的构建而它们根本没发生 (丢了 $COMMON、构建循环没传目录,cmake --build -j 24 只打印了用法而 $?=0)。 任何 P5 脚本报构建结果之前,先确认 CTestTestfile.cmake 存在。

文件所有权补充:MG_Pipe/MGPipeTypes.h(payload 结构形状)归 c0。