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.
69 KiB
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):
- G3 编解码器的真实实现(今天只有骨架,见 §1.1)。
MG_Remote/Client/与MG_Remote/Server/:会话对象、发射表、applier、ServerLoop与 apply 线程。MG_Backend/Init.cpp的单一 hook +MG_Config::Transport+MOBILEGL_TRANSPORT解析 + CMake 布线。MGPCaps快照(客户端只能从快照答 56 个 caps 读点)。- 阻塞
read_pixels、客户端保守MarkGpuWritten、块粒度 persistent-map 推送 +persistent-map-push出数。 - 两个新场景(
TriangleScenario、PersistentCoherentMapScenario)、DirectGLES.Split.车道、 trace-replay 的SPLIT臂、build-split构建目录与门脚本第五部分。 - 未迁移输入的可见化:字段归属表 + 生成器检查 + 严格模式对照。
范围外(明确不做,写进 brief 免得包去建):
spawn/SocketTransport/ServerMain/HeadlessGLfork 预检 → 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 时:
- 编码器必须把
MGHostSpan::Ptr置nullptr,Seg/Offset指向SEG_STAGE; - 编码器必须为每一条携带内容的
MGPBlobRef填真实的Seg/ 段内Offset/ 非零Size; - 解码/应用侧必须
Fatal{ProtocolCorruption}:Ptr != nullptr、内容记录的Blob.Size == 0、Size != 0 && Seg == kMGHostSpanSegNone、以及段内Offset+Size越过段边界; MGPipeApplyMapPersistent在 split 下必须返回nullptr(见 R-6);- 负面对照:
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、四个Boolacceptance(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。三条机械化要求:
- 63 个字段 + 7 个 sticky forward,每一个恰好落在
RECORD-SUPPLIED(call) | APPLIER-DERIVED(record) | BARRIER-PULLED(retiring phase) | FATAL四类之一; 表由生成器产出并--check(照gen_pipe_dirty_surface.py --check的形状),不在任何一类里 = 构建失败。 - split 下读一个
BARRIER-PULLED字段:递增新的PipeStats::CallClass::ResidualPulls(短名rsp,放进#if MOBILEGL_PIPE_PUSH块内,G1 安全),逐帧发布。 它的值就是 P6/P7/P8 欠债的大小,P5 结束时进 MEASUREMENTS。 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 — 目录与编码的四处不一致,契约包一次性改掉
GetCaps/MGPCaps与CreateSamplerState/MGPSamplerDesc带MGPBlobRef却无kHasBlob→ 补标志 (改标志不移动 opcode,PipeCalls.def:26-29的编号规则不受影响)。ResourceFlushRange是kNone却带内容指针 → 或补 blob 成员 + 标志,或在编码表里写明 "长度由MGPFlushRange的 range 决定,字节走SEG_STAGE,无MGPBlobRef"。二选一,写下来。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。- 生成一张逐 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 的正式措辞):
- split 下,payload 带
MGPBlobRef的记录若Blob.Size == 0→Fatal{ProtocolCorruption}; - split 下 apply 侧
MGHostSpan::Ptr != nullptr→Fatal; - 任何 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;串行,其余包等它)
交付
MobileGL/MG_Remote/CONTRACT-P5.md:§3 的表 0/1/2/3 全文(表 2 的权威副本由 p1 的生成器产出, c0 先落规格与四类定义)。PipeCalls.def标志修正(R-13.1/.2)+ResourceRespecify初始字节的归宿裁定(R-13.3)。 不得移动任何 opcode;PipeCatalogueTest.cpp:418-422钉着最后四个 opcode。gen_pipe.py:逐 opcode 标志表kMGPipeCallFlags[kOpCount](R-13.4);--check+--self-test保持全绿,生成物提交。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)的形状。- CMake:
MOBILEGL_BUILD_DISAGGREGATED_INPROC选项(蕴含DISAGGREGATED)、MG_Remote/{Wire,Client,Server}/的源文件列表、MOBILEGL_PIPE_PUSH源列表 (CMakeLists.txt:484-499)里追加新文件。 - 头文件与桩,让七个包当天就能并行编译:
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 的桩体。 - verb 普查(R-4):用全 Fatal 的发射表跑 A/B/D 三条路径,产
~/w7/notes/p5/verb-census.md。 这是唯一一件 c0 必须"先跑起来一点东西"的事——如果 c1/v1 还没有,普查可以用一个最小的--gtest_filter桩程序 + 手工 trace 解析做,或推迟到 c1 落地后由集成者补跑并广播。 Ring.h头注释补 R-9 的五句 +kRecPad不计 seq 的规则 + 五条单元用例。- 纯度:新头文件若从
ITransport.h可达,不得 includeMG_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)
rsp逐帧/逐用例——P6/P7/P8 欠债的大小,P5 结束时的数字是后续阶段的基线。pmap与mpr的对照(两者反相关:T2 下每次MapPersistent都 decline,mpr不变、pmap从 0 起来)。- 最大记录字节(R-10 的证明义务)。
- 双角色峰值 RSS + 段台账(
SEG_CMD8 MiB /SEG_STAGE32 MiB /SEG_REPLY16 MiB(ID-47 起;原 8 MiB) /SEG_EVENT256 KiB 是ProtocolSmokeTest.cpp:72钉住的规范尺寸)。要 N 帧上的斜率,不只是一个峰值—— present 1:1 的批处理/credit bug 表现为慢泄漏,SSIM 看不见,两个场景的车道也够不着。 - 栅栏成本:inproc vs monolith 的配对帧时(红米,定频,尾 200 帧 p50/p99)。 预期变慢,这是 R-1 的已知税,写清它随栅栏退役而消失。
- 桌面
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 给复审者的检查清单(第一轮就照这个打)
- 有没有任何一处在 split 下仍然使用宿主指针 /
Blob.Size == 0/Seg == 0携带内容?(R-2) - 有没有第二份 acceptance 谓词、第二份 D-K2 依赖表、第二份程序归档编解码器?(R-5、表 1)
appliedSeq有没有在栅栏期被批处理?kRecPad有没有被一侧计进 seq?(R-9)- 有没有 applier 入口把指针留过返回?
hostBytes改干净了吗?(R-11) - 客户端还有没有在读
MGPipeGetResourceOps()来决定发不发?(R-8) - 发射表里有没有 null 槽,或者穿透去调驱动的槽?(R-4)
- 表 2 里有没有字段落在四类之外?sticky 的豁免在 split 下取消了吗?(R-7)
- 新的 ctest
ENVIRONMENT有没有漏掉${MGL_ITEST_COMMON_ENV}?;转义了吗?SPLIT臂的输出目录带变体了吗?(t1) - 新计数器有没有掉到
#if MOBILEGL_PIPE_PUSH块外面去(G1)? - 有没有任何 P5 的 CI 步骤挂在
feat/disaggregated这个临时触发上?(R-14) - (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。