mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-14 21:43:24 +09:00
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.
905 lines
69 KiB
Markdown
905 lines
69 KiB
Markdown
# 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**。
|