mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Docs] (.claude/skills, .github/workflows, CMakeLists.txt, docs/archive, docs/Disaggregated, docs/MobileVA, docs/Performance, MG_Backend, MG_Backend/DirectGLES, MG_Backend/DirectVulkan, MG_Backend/Record, MG_Gbm, MG_Impl, MG_Impl/EGLImpl, MG_Impl/GLImpl, MG_Impl/GLXImpl, MG_Impl/Pipe, MG_IntegrationTest, MG_IntegrationTest/Harness, MG_IntegrationTest/Scenarios, MG_Pipe, MG_Pipe/generated, MG_Remote, MG_Remote/Client, MG_Remote/Server, MG_Remote/Transport, MG_Remote/Wire, MG_State/GLState, MG_Test/Pipe, MG_Test/Util, MG_Test/Wire, MobileGL, scripts, scripts/data, tools/device_bench, userguide): archive the planning notes and agent contracts under docs/archive
- DEBTS.md and OPEN-QUESTIONS.md stay maintained, now in docs/Disaggregated.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# 当前阶段:P13 已收官(2026-10-07);下一阶段 P14
|
||||
|
||||
> **更新 2026-10-07**。这一页只有摘要;计划与裁定在 [`notes/p13/PLAN-P13.md`](notes/p13/PLAN-P13.md)、[`notes/p13/INTEGRATOR-DECISIONS-P13.md`](notes/p13/INTEGRATOR-DECISIONS-P13.md),逐波记录在 [`notes/p13/README.md`](notes/p13/README.md)。上一阶段 P8 于 09-30 收官,见 [`notes/p8/README.md`](notes/p8/README.md)。
|
||||
> **更新 2026-10-07**。这一页只有摘要;计划与裁定在 [`docs/archive/notes/p13/PLAN-P13.md`](../archive/notes/p13/PLAN-P13.md)、[`docs/archive/notes/p13/INTEGRATOR-DECISIONS-P13.md`](../archive/notes/p13/INTEGRATOR-DECISIONS-P13.md),逐波记录在 [`docs/archive/notes/p13/README.md`](../archive/notes/p13/README.md)。上一阶段 P8 于 09-30 收官,见 [`docs/archive/notes/p8/README.md`](../archive/notes/p8/README.md)。
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -23,4 +23,4 @@
|
||||
|
||||
**P14**:状态归属(同进程多 session / 多 context / share group),设计见 [`design/11-state-ownership.md`](design/11-state-ownership.md)。性能轮(基线 `dev` 构建,定频,FCL MC + anland glmark2,两后端)也排在 P13 之后。
|
||||
|
||||
**待用户决策**:[`DEBTS.md`](notes/DEBTS.md) 中「需裁定」各项;FCL 无声死亡留给用户(`notes/p13/fcl-soak/README.md`)。
|
||||
**待用户决策**:[`DEBTS.md`](DEBTS.md) 中「需裁定」各项;FCL 无声死亡留给用户(`docs/archive/notes/p13/fcl-soak/README.md`)。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
> **2026-10-07(P13 收官)关闭三行**:ID-P8-13(9 条 monolith 缺陷——monolith 换到记录臂后对应 monolith 登记在集成车道上全过,旧的 monolith 前端臂在 W6 删除)、ID-P8-14(`SyncClientSideVertexArraysForIndirectFetch` 的整 buffer 回读与 Magma monolith 深度 mip 的 view 类型都属于已删的 monolith 前端臂)、`MOBILEGL_PIPE_TEXEL_RETAIN_MB`(W3b 随 pull 构建删除)。新增两行:W9 缓存容量重调与 Magma monolith 性能,都排在 P13 之后的性能轮,基线是 `dev` 构建。
|
||||
>
|
||||
> 路线图只列"有债务、看这里"。每条写明去向;已关闭的历史债与当时的完整债务表见 [`p5b/README.md`](p5b/README.md) 末节。开放问题另见 [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)。更新:2026-09-30(P8 第二波;按债务审查重排。同日:create-indirect 换成派生 fixture `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024`(不是原抓取,16 字节对齐的 SSBO 范围绑定挪到 1024 对齐),ID-P8-14、ID-P8-19 两条关闭——红米两后端 monolith / spawn 全过,Magma spawn 不再丢设备)。去向「需裁定」= 等用户定;「可进行」= 随时可开的小修,不依赖别的阶段。
|
||||
> 路线图只列"有债务、看这里"。每条写明去向;已关闭的历史债与当时的完整债务表见 [`docs/archive/notes/p5b/README.md`](../archive/notes/p5b/README.md) 末节。开放问题另见 [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)。更新:2026-09-30(P8 第二波;按债务审查重排。同日:create-indirect 换成派生 fixture `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024`(不是原抓取,16 字节对齐的 SSBO 范围绑定挪到 1024 对齐),ID-P8-14、ID-P8-19 两条关闭——红米两后端 monolith / spawn 全过,Magma spawn 不再丢设备)。去向「需裁定」= 等用户定;「可进行」= 随时可开的小修,不依赖别的阶段。
|
||||
|
||||
| 债务 | 去向 |
|
||||
|---|---|
|
||||
@@ -26,12 +26,12 @@
|
||||
| P8 转出(E):driver-written 标按整张纹理打(`GenerateMipmap` 也打),驱动拒读时连基级也具名拒绝,偏保守;跨格式拷贝 store 不跟随;拒绝仍是会话 Fatal(要改回包状态才能答 `GL_INVALID_OPERATION`) | 小修(按 level 打标);其余按需 |
|
||||
| P8 转出(B):`PipeCalls.def:260` `GenerateMipmap` 仍是 `kWaitApplied`,裁定 14 的理由(CPU 滤波碰 client 影子)已不成立,可改 `kWaitNone` | 性能工作(协议行为变更,需单独裁定) |
|
||||
| P8 转出(C):server SSBO 标记过近似(窗口内每个绑定的 SSBO 都标);一个留在 SSBO 绑定上又当 restart EBO 用的 buffer 每次 draw 都回读 | 性能工作 |
|
||||
| P13 转出(ID-P13-2,P14 更新):Magma monolith 记录臂对 dev(pull)——trace 上已基本追平:rd12 83.8 对 87.0 fps(−4%),openra 747 对 293(dev 在那里等 GPU);P14 最大一项是 `553eb86d`(纹理层上传不再在 CPU 上等 GPU 排空,rd12 70→83)。但 **FCL MC 上仍差**:monolith 171 对 dev 232(−26%),inproc 195;呈现线程 CPU 只占约 2.8 / 5.8 ms 帧,其余是等待——与 trace 不同的另一处等待 | 下一阶段第一项:FCL Magma monolith 做 off-CPU profile(`--trace-offcpu`,同找到上传等待的方法;P14 收官时两次采集都没抓到游戏帧),找出那 3 ms 等待;数字见 `perf-p14/README.md` |
|
||||
| ~~P13 转出(W9):缓存容量重调~~ **P14 关闭(读数)**:`MOBILEGL_PIPE_STATS` 在 rd12 / openra 上 render-state CSO 稳态每帧新铸 0 个,Magma 管线 memo 命中 99.0% / 92.7%,没有缓存抖动,P8 容量保留 | 已关闭(`perf-p14/BENCH.md` W9 节) |
|
||||
| P14 转出:Espryt monolith 记录臂对 dev(pull)rd12 91.1 对 115.2 fps(−21%);FCL MC 198 对 229(−13%)。渲染线程每帧 CPU 11.56 对 9.07 ms:client half +1.39(validate 0.92:EmitGlobalConstants 0.17、EmitVertexElements 0.13、Tracker::Update 0.12、CopyField 0.10、validate 自身 0.10、EmitVertexBuffers 0.07;verb port 约 0.2)、backend +0.45(SyncNeccessaryTextures +0.14、SyncNeccessaryBuffers +0.08、AdoptUnchangedBufferSets +0.09、BindCurrentProgramWithResources +0.07、SyncCurrentProgramByHandle +0.05)、driver +0.33、applier +0.07。每项都在帧时间 2% 以下;合起来是记录臂的结构成本:pull 的后端在 draw 时按指针读前端对象,push 要检测变化(tracker)、序列化(emitter、uniform span)、应用(applier)、再由后端按句柄经 slot 表重新解析,并且因为后端看不到前端脏位而对每个绑定对象做 clean 检查——每步每 draw 几十 ns、各自是不相干的冷 cache 行,乘以 1471 draw/帧 | 下一阶段:per-(VAO, program) draw-packet 缓存(设计草稿 `perf-p14/NEXT-draw-packet-cache.md`,估计 rd12 Espryt monolith 回收 1.0–1.6 ms/帧,约 91→100–106 fps);先只做 client half 原型量 |
|
||||
| P13 转出(ID-P13-2,P14 更新):Magma monolith 记录臂对 dev(pull)——trace 上已基本追平:rd12 83.8 对 87.0 fps(−4%),openra 747 对 293(dev 在那里等 GPU);P14 最大一项是 `553eb86d`(纹理层上传不再在 CPU 上等 GPU 排空,rd12 70→83)。但 **FCL MC 上仍差**:monolith 171 对 dev 232(−26%),inproc 195;呈现线程 CPU 只占约 2.8 / 5.8 ms 帧,其余是等待——与 trace 不同的另一处等待 | 下一阶段第一项:FCL Magma monolith 做 off-CPU profile(`--trace-offcpu`,同找到上传等待的方法;P14 收官时两次采集都没抓到游戏帧),找出那 3 ms 等待;数字见 `docs/archive/notes/perf-p14/README.md` |
|
||||
| ~~P13 转出(W9):缓存容量重调~~ **P14 关闭(读数)**:`MOBILEGL_PIPE_STATS` 在 rd12 / openra 上 render-state CSO 稳态每帧新铸 0 个,Magma 管线 memo 命中 99.0% / 92.7%,没有缓存抖动,P8 容量保留 | 已关闭(`docs/archive/notes/perf-p14/BENCH.md` W9 节) |
|
||||
| P14 转出:Espryt monolith 记录臂对 dev(pull)rd12 91.1 对 115.2 fps(−21%);FCL MC 198 对 229(−13%)。渲染线程每帧 CPU 11.56 对 9.07 ms:client half +1.39(validate 0.92:EmitGlobalConstants 0.17、EmitVertexElements 0.13、Tracker::Update 0.12、CopyField 0.10、validate 自身 0.10、EmitVertexBuffers 0.07;verb port 约 0.2)、backend +0.45(SyncNeccessaryTextures +0.14、SyncNeccessaryBuffers +0.08、AdoptUnchangedBufferSets +0.09、BindCurrentProgramWithResources +0.07、SyncCurrentProgramByHandle +0.05)、driver +0.33、applier +0.07。每项都在帧时间 2% 以下;合起来是记录臂的结构成本:pull 的后端在 draw 时按指针读前端对象,push 要检测变化(tracker)、序列化(emitter、uniform span)、应用(applier)、再由后端按句柄经 slot 表重新解析,并且因为后端看不到前端脏位而对每个绑定对象做 clean 检查——每步每 draw 几十 ns、各自是不相干的冷 cache 行,乘以 1471 draw/帧 | 下一阶段:per-(VAO, program) draw-packet 缓存(设计草稿 `docs/archive/notes/perf-p14/NEXT-draw-packet-cache.md`,估计 rd12 Espryt monolith 回收 1.0–1.6 ms/帧,约 91→100–106 fps);先只做 client half 原型量 |
|
||||
| P14 转出:Espryt monolith 的 driver 项 +0.33 ms/帧(2.9%),全在 `glDrawElements` 内部,GL 调用组合与 dev 相同。`simpleperf stat`(整次运行、渲染线程):feat 比 dev 多执行 16.6% 指令、L1D refill 多 56%、L2D 多 23%(IPC 3.73 对 3.86)。推断:记录臂每 draw 更大的工作集(record、applier 状态、slot 表、twin)把驱动的状态挤出 cache——这是计数器推断,没有在驱动内部测量 | 随 draw-packet 缓存一起验证:落地前后量 L1D refill 与 driver ms/帧;如果 driver 项不降,推断不成立,再单独查 |
|
||||
| P14 转出(测过未做):Tracker / residual 的前端变更 epoch 快路径——在每个 `DECLARE_GL_FUNCTION_END` 入口(除 draw、`glBindVertexArray`、浮点 `glUniform*` 外)以及手写入口和 EGL 入口里 bump 一个上下文 epoch;epoch 未动时 validate 只重算 VAO / program 派生的 shutter(上限约 0.07 ms),再经审计哪些 residual 字段依赖 VAO / program 绑定后跳过 residual copy(合计 0.15–0.25 ms,1.5–2%) | 各自低于 2% 门槛,且漏 bump 一个入口就渲染陈旧状态;并入 draw-packet 设计(其 §3 的上下文 epoch 就是它),不单独做 |
|
||||
| P13 转出(FCL 浸泡,用户裁定:FCL 暂不改、MobileGL 不做规避):FCL game process can die silently (exit SIGNALED status=34, SIGRTMIN+2): the JVM signal reset after renderer selection drops libcore's AsynchronousCloseMonitor handler, and FCL's okhttp/Okio watchdog then kills the process; renderer-independent (reproduced on P12 and P13 libraries, both backends; untested with other renderers)。浸泡:P13 库 30 次(Magma 22 / Espryt 8)死 18 次、P12 库 10 次死 2 次;P12 / P13 逐次交替的对照(同场景同网络时段)P12 4/10、P13 2/10,时间线(`Renderer:` 行到 MobileGL 首行约 6.2 s、晚死固定在其后约 9.4 s)两库相同,无证据表明 MobileGL 拉高概率;全部 status=34,无 tombstone、无 kgsl;证据与机理见 [`p13/fcl-soak/README.md`](p13/fcl-soak/README.md) | fix belongs in FCL (save/restore libcore's SIGRTMIN+2 sigaction around JVM creation); left for the user |
|
||||
| P13 转出(FCL 浸泡,用户裁定:FCL 暂不改、MobileGL 不做规避):FCL game process can die silently (exit SIGNALED status=34, SIGRTMIN+2): the JVM signal reset after renderer selection drops libcore's AsynchronousCloseMonitor handler, and FCL's okhttp/Okio watchdog then kills the process; renderer-independent (reproduced on P12 and P13 libraries, both backends; untested with other renderers)。浸泡:P13 库 30 次(Magma 22 / Espryt 8)死 18 次、P12 库 10 次死 2 次;P12 / P13 逐次交替的对照(同场景同网络时段)P12 4/10、P13 2/10,时间线(`Renderer:` 行到 MobileGL 首行约 6.2 s、晚死固定在其后约 9.4 s)两库相同,无证据表明 MobileGL 拉高概率;全部 status=34,无 tombstone、无 kgsl;证据与机理见 [`docs/archive/notes/p13/fcl-soak/README.md`](../archive/notes/p13/fcl-soak/README.md) | fix belongs in FCL (save/restore libcore's SIGRTMIN+2 sigaction around JVM creation); left for the user |
|
||||
| P8 转出(MD):dev 的 `RingAllocateSlow` 在出错的上下文上无上限地排水(设备上挂在 `glFinish`) | 小修(dev) |
|
||||
| P8 转出:`LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime` 在负载下超时(两次),单跑必过 | 小修(放宽时限或隔离) |
|
||||
| P8 转出(SV):`gen_pipe_dirty_surface.py` 扫描集成测试源,测试成员名与 GLContext 成员同名(如 `m_parameters`)会静默弄坏它的自检;`.Mip3DValidation.` 在没装校验层的车道上是失败不是跳过 | 小修 / CI 记录 |
|
||||
@@ -42,11 +42,11 @@
|
||||
| P11 转出(M2,给 dev):dev 的 `DirectVulkan.SplitRecording.` 车道没有 `RESOURCE_LOCK`、其用例读字面日志路径——`-j` 下可能互相截断;P11 线的副本已修(Magma 空 uniform 的重复修复已在 ID-P8-17 合成一条) | 下次向 dev 回流时带上 |
|
||||
| P11 转出(M2):超过 stage 分块预算的大层按分片发出,没有写入框覆盖的分片仍带整框(server 要靠它拼层图),会把影子盖到 GPU 写过的地方;修法要一个两个 server 都跳过的「只定位」区域写法,控制协议修订 +1(split 专属,早于 P11) | 小包或 P8 |
|
||||
| P11 转出(M2):上传仍有两种有损形状,各记一次日志——一层的精确写入框超过 4096 个时按存储的矩形表覆盖发出;applier 一个条目累计超过 4096 个区域时并成并集框。纹理带 GPU 写时两者都会把影子盖进空隙 | 小修,无触发报告 |
|
||||
| P10 转出(性能):设备上 tcp loopback 的 rd12 每帧 19.4 ms 中 ~11.2 ms 是生产者在 stream 传输里的停车(发送 + 环 / stage / ack 等待),credit 等待只有 ~0.2 ms(`notes/p10/C-MEASUREMENTS.md` §2,ID-P10-13) | 路线图推完后的性能工作 |
|
||||
| P11 转出(性能):设备上 MC 26.3,split T2 相对单进程采纳的 p99 Espryt +37–42%、Magma 约 2.5×;其中一部分是线程落位——server apply 线程钉在 6–7 核,client GL 线程多半落到慢核,每帧 CPU 约 2.3×(`notes/p11/A-DEVICE.md`)。RSS 一半已由 B2(T0)交付:峰值内存 Espryt −41–42%、Magma −18–26%(ID-P11-17) | 路线图推完后的性能工作(p99 与线程落位) |
|
||||
| P10 转出(性能):设备上 tcp loopback 的 rd12 每帧 19.4 ms 中 ~11.2 ms 是生产者在 stream 传输里的停车(发送 + 环 / stage / ack 等待),credit 等待只有 ~0.2 ms(`docs/archive/notes/p10/C-MEASUREMENTS.md` §2,ID-P10-13) | 路线图推完后的性能工作 |
|
||||
| P11 转出(性能):设备上 MC 26.3,split T2 相对单进程采纳的 p99 Espryt +37–42%、Magma 约 2.5×;其中一部分是线程落位——server apply 线程钉在 6–7 核,client GL 线程多半落到慢核,每帧 CPU 约 2.3×(`docs/archive/notes/p11/A-DEVICE.md`)。RSS 一半已由 B2(T0)交付:峰值内存 Espryt −41–42%、Magma −18–26%(ID-P11-17) | 路线图推完后的性能工作(p99 与线程落位) |
|
||||
| P11 转出(B2,性能):`glBufferStorage(NULL)` 的采纳先把整块零存储当 `resource_subdata` 发一遍(T2 与 T0 都是;MC 26.3 8 个存储、441.5 MiB) | 路线图推完后的性能工作 |
|
||||
| P12 未做:DirectGLES `g_Display` / `g_Surface` / `g_Context` 仍是全局(每进程一个会话时不需要);cached-app freezer 的完整处理(文档从未定义);多 context;server 窗口不转发输入 | 等出现第二个会话 / 第二个 context 的需求 |
|
||||
| P9 转出:Magma 每次 draw 对可写 SSBO / texel buffer / XFB 目标发整 buffer 的 `OnGpuWritten`(create-indirect 首帧 7,093 条),client 已自标同一批,冗余无害;`OnMipLevelsGenerated`、`OnCapsInvalidated` 仍无生产者。`OnTextureWriteback` 也无生产者,但别删:CPU 三通道 mip 退路要它(`CONTRACT-P5B.md:255`) | 以后的收窄 / 清理候选(ID-P9-7) |
|
||||
| 树外脚本:普查跑器、`wsl_p5_gate.sh` 等在 `~/w7/notes/`,git merge 不会传播;`~/w7/notes/p11/gate.sh` 是事实上的门 | 需要时入库 |
|
||||
| 树外脚本:普查跑器、`wsl_p5_gate.sh` 等在 `~/w7/notes/`,git merge 不会传播;`docs/archive/notes/p11/gate.sh` 是事实上的门 | 需要时入库 |
|
||||
|
||||
已关闭的历史债(P5 的 27 个 wrong-answer、`rsp` 残余读、P5b 的 inproc 依赖、ABI 指纹、默认 staging 装不下 128 MiB 上传、184 符号棘轮 CI 门)及当时的完整债务表见 [`notes/p5b/README.md`](p5b/README.md) 末节。
|
||||
已关闭的历史债(P5 的 27 个 wrong-answer、`rsp` 残余读、P5b 的 inproc 依赖、ABI 指纹、默认 staging 装不下 128 MiB 上传、184 符号棘轮 CI 门)及当时的完整债务表见 [`docs/archive/notes/p5b/README.md`](../archive/notes/p5b/README.md) 末节。
|
||||
@@ -25,29 +25,29 @@
|
||||
这解释了 handoff §3.6「换快 3.4 倍的链路墙钟不变」:钱花在设备每次唤醒上;handoff §3.2 那 249 s 是在**未定频**的机器上取的,含这份空闲税。
|
||||
**同一个诊断给出一条可移植修法**:把服务端的自旋预算调大(Activity 的 `env` extra → `MOBILEGL_IPC_SPIN_US=2000`,不需要 root、不需要改代码),
|
||||
那一帧 **221.5 s → 182.8 s(−17%)**;设备定频+关深度空闲则是 **166.5 s(−25%)**。服务端日志会打印 `spin 2000 us` 自证。
|
||||
见 [`notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md`](notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md) §2.6–2.10。
|
||||
见 [`docs/archive/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md`](../archive/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md) §2.6–2.10。
|
||||
- **画面正确**:Vulkan 后端真机画面检查 36/36 通过,CTS 五块相对基线没有超过 0.5 个百分点的退步、没有新崩溃(P7);server 自有窗口上屏两后端 SSIM 1.0(P12)。
|
||||
- **五臂传输对比(2026-09-28,未定频交错 A/B)**:重负载(rd12)下 inproc ≈ spawn+shm 比 monolith **快约 1/3**(run-ahead 流水线并行 + applier 路径探针更少),轻负载(openra)下 split 只付固定开销;**tcp localhost 慢 ~8 倍,瓶颈是 present credit=1 的串行等待(~74%)+ futex 唤醒税**,`MOBILEGL_IPC_SPIN_US=2000` 单项实测 21.7→63.6 fps;跨主机 TCP 的「`eglMakeCurrent` hang」**是测试环境造成的**(主机 v2rayN `xray_tun` 代理终结 TCP 后中继卡住,不是源码 bug);绕开后 P12 出口门 (b) 通过:WSL client 经 Wi-Fi 重放 rd12 世界内 trace,ssim 0.999883,稳态 17–22 fps(`PRESENT_CREDIT` 1/2/3 = 16.9 / 21.3 / 22.1),openra 稳态 105–133 fps;加载帧 2.7 GB 受带宽约束(server 实测 52 MB/s),稳态受往返延迟(20–24 MB/s)。全文 [`notes/perf-five-arm-20260928`](notes/perf-five-arm-20260928/README.md)。
|
||||
- **五臂传输对比(2026-09-28,未定频交错 A/B)**:重负载(rd12)下 inproc ≈ spawn+shm 比 monolith **快约 1/3**(run-ahead 流水线并行 + applier 路径探针更少),轻负载(openra)下 split 只付固定开销;**tcp localhost 慢 ~8 倍,瓶颈是 present credit=1 的串行等待(~74%)+ futex 唤醒税**,`MOBILEGL_IPC_SPIN_US=2000` 单项实测 21.7→63.6 fps;跨主机 TCP 的「`eglMakeCurrent` hang」**是测试环境造成的**(主机 v2rayN `xray_tun` 代理终结 TCP 后中继卡住,不是源码 bug);绕开后 P12 出口门 (b) 通过:WSL client 经 Wi-Fi 重放 rd12 世界内 trace,ssim 0.999883,稳态 17–22 fps(`PRESENT_CREDIT` 1/2/3 = 16.9 / 21.3 / 22.1),openra 稳态 105–133 fps;加载帧 2.7 GB 受带宽约束(server 实测 52 MB/s),稳态受往返延迟(20–24 MB/s)。全文 [`docs/archive/notes/perf-five-arm-20260928`](../archive/notes/perf-five-arm-20260928/README.md)。
|
||||
|
||||
## 按阶段
|
||||
|
||||
| § | 阶段 | 一句结论 | 全文 |
|
||||
|---|---|---|---|
|
||||
| 1 | P0 | 能从应用进程拉起第二个进程;跨进程共享 GPU 内存只有一条路在所有设备上可用 | [`notes/p0`](notes/p0/README.md) |
|
||||
| 2 | P1 | 逐 draw 核对推送状态,79 条 trace 零分歧 | [`notes/p1`](notes/p1/README.md) |
|
||||
| 3 | P2 | 桌面基准:新 tracker 比它替掉的旧填充更便宜 | [`notes/p2`](notes/p2/README.md) |
|
||||
| 4 | P3a | 两机真机 Release:接口代价 +6–12%,VAO 切换密集的场景 +27–30% | [`notes/p3a`](notes/p3a/README.md) |
|
||||
| 5 | P4a | 换到 Redmi;P4a 自己只加 3–6 个百分点 | [`notes/p4a`](notes/p4a/README.md) |
|
||||
| 6 | P5 | 两线程第一帧画对;真机两线程臂还跑不完 | [`notes/p5`](notes/p5/README.md) |
|
||||
| 7 | P5b | 真机两线程跑通四条游戏 trace;两线程额外代价 +6–18% | [`notes/p5b`](notes/p5b/README.md) |
|
||||
| 8 | P5c | 两线程之间零共享内存后,全部测试仍绿 | [`notes/p5c`](notes/p5c/README.md) |
|
||||
| 9 | P5d | 游戏内 7–13 → 103–106 fps,client CPU 15.0 → 9.2 ms/帧 | [`notes/p5d`](notes/p5d/README.md) |
|
||||
| 10–11 | P5e | 发完就走比每条都等快 69%;重负载下与单线程持平 | [`notes/p5e`](notes/p5e/README.md) |
|
||||
| 12 | P6 | 两进程与两线程持平;每帧数据量与记录数首次实测 | [`notes/p6`](notes/p6/README.md) |
|
||||
| 13 | P3b/P4b | 纹理上传形状在四种拓扑下完全一致 | [`notes/p34b`](notes/p34b/README.md) |
|
||||
| 14 | P6.5 | 跨机 TCP 102/102;断线约 5 s 检测到 | [`notes/p65`](notes/p65/README.md) |
|
||||
| 15 | P7 | 真机画面 36/36;server 内存无界增长收住(主机 825 → 546 MiB) | [`notes/p7`](notes/p7/README.md) |
|
||||
| 16 | P12 | 审查后真机复测:Espryt SSIM 1.0;Magma 1.0 / 0.999999511;P12 定向 CTest 46/46;纹理上传回包等待 1→0(主机机制门,真机墙钟待测);**P12 已收官(09-29)**:跨机 TCP 门 (b) 通过(rd12 ssim 0.999883、稳态 17–22 fps);FCL 门 (a) 通过(Minecraft 上屏到 render server 窗口,杀 server 后 33–54 ms 闩住 device-lost) | [`notes/p12`](notes/p12/README.md) |
|
||||
| 17 | 五臂传输测量(2026-09-28,非阶段) | inproc/shm 重负载比 monolith 快 ~1/3;tcp localhost 瓶颈是 credit=1 + 唤醒税(SPIN_US=2000 → 2.9×);跨主机 hang 实为主机 xray TUN 代理造成;直连 Wi-Fi 稳态 rd12 17–22 fps、openra 105–133 fps | [`notes/perf-five-arm-20260928`](notes/perf-five-arm-20260928/README.md) |
|
||||
| 18 | P11 | 同机零拷贝(T0)MC 26.3 峰值内存 Espryt −41–42%、Magma −18–26%,ssim / 帧时 / CPU 不变;外部 client 走共享内存 rd12 Espryt 约 119 fps(TCP 40–53);split T2 相对单进程采纳 p99 Espryt +37–42%、Magma 约 2.5×。未量:M2 后 split server 上纹理无 GPU 写时 Espryt 自己按存储阈值取舍精确写入框,非 ring 路径可能多发几个小框 | [`notes/p11`](notes/p11/README.md) |
|
||||
| 19 | P8 | 真实内容只 Magma wire indirect 命中(MC 26.3 × Magma inproc 1,300 万 draw CPU 展开)→ 原生后 create-indirect 每次整 GPU 等待 321 → 0;红米 create-instancing × Espryt × monolith 0.870 → 0.99998(Adreno 丢排队环形拷贝,dev + feat 两条阶梯);最大 program archive 355 KB = arena/4 的 4.2 %(不分片);出口设备矩阵 77 格 / 臂无一变差 | [`notes/p8`](notes/p8/README.md) |
|
||||
| 1 | P0 | 能从应用进程拉起第二个进程;跨进程共享 GPU 内存只有一条路在所有设备上可用 | [`docs/archive/notes/p0`](../archive/notes/p0/README.md) |
|
||||
| 2 | P1 | 逐 draw 核对推送状态,79 条 trace 零分歧 | [`docs/archive/notes/p1`](../archive/notes/p1/README.md) |
|
||||
| 3 | P2 | 桌面基准:新 tracker 比它替掉的旧填充更便宜 | [`docs/archive/notes/p2`](../archive/notes/p2/README.md) |
|
||||
| 4 | P3a | 两机真机 Release:接口代价 +6–12%,VAO 切换密集的场景 +27–30% | [`docs/archive/notes/p3a`](../archive/notes/p3a/README.md) |
|
||||
| 5 | P4a | 换到 Redmi;P4a 自己只加 3–6 个百分点 | [`docs/archive/notes/p4a`](../archive/notes/p4a/README.md) |
|
||||
| 6 | P5 | 两线程第一帧画对;真机两线程臂还跑不完 | [`docs/archive/notes/p5`](../archive/notes/p5/README.md) |
|
||||
| 7 | P5b | 真机两线程跑通四条游戏 trace;两线程额外代价 +6–18% | [`docs/archive/notes/p5b`](../archive/notes/p5b/README.md) |
|
||||
| 8 | P5c | 两线程之间零共享内存后,全部测试仍绿 | [`docs/archive/notes/p5c`](../archive/notes/p5c/README.md) |
|
||||
| 9 | P5d | 游戏内 7–13 → 103–106 fps,client CPU 15.0 → 9.2 ms/帧 | [`docs/archive/notes/p5d`](../archive/notes/p5d/README.md) |
|
||||
| 10–11 | P5e | 发完就走比每条都等快 69%;重负载下与单线程持平 | [`docs/archive/notes/p5e`](../archive/notes/p5e/README.md) |
|
||||
| 12 | P6 | 两进程与两线程持平;每帧数据量与记录数首次实测 | [`docs/archive/notes/p6`](../archive/notes/p6/README.md) |
|
||||
| 13 | P3b/P4b | 纹理上传形状在四种拓扑下完全一致 | [`docs/archive/notes/p34b`](../archive/notes/p34b/README.md) |
|
||||
| 14 | P6.5 | 跨机 TCP 102/102;断线约 5 s 检测到 | [`docs/archive/notes/p65`](../archive/notes/p65/README.md) |
|
||||
| 15 | P7 | 真机画面 36/36;server 内存无界增长收住(主机 825 → 546 MiB) | [`docs/archive/notes/p7`](../archive/notes/p7/README.md) |
|
||||
| 16 | P12 | 审查后真机复测:Espryt SSIM 1.0;Magma 1.0 / 0.999999511;P12 定向 CTest 46/46;纹理上传回包等待 1→0(主机机制门,真机墙钟待测);**P12 已收官(09-29)**:跨机 TCP 门 (b) 通过(rd12 ssim 0.999883、稳态 17–22 fps);FCL 门 (a) 通过(Minecraft 上屏到 render server 窗口,杀 server 后 33–54 ms 闩住 device-lost) | [`docs/archive/notes/p12`](../archive/notes/p12/README.md) |
|
||||
| 17 | 五臂传输测量(2026-09-28,非阶段) | inproc/shm 重负载比 monolith 快 ~1/3;tcp localhost 瓶颈是 credit=1 + 唤醒税(SPIN_US=2000 → 2.9×);跨主机 hang 实为主机 xray TUN 代理造成;直连 Wi-Fi 稳态 rd12 17–22 fps、openra 105–133 fps | [`docs/archive/notes/perf-five-arm-20260928`](../archive/notes/perf-five-arm-20260928/README.md) |
|
||||
| 18 | P11 | 同机零拷贝(T0)MC 26.3 峰值内存 Espryt −41–42%、Magma −18–26%,ssim / 帧时 / CPU 不变;外部 client 走共享内存 rd12 Espryt 约 119 fps(TCP 40–53);split T2 相对单进程采纳 p99 Espryt +37–42%、Magma 约 2.5×。未量:M2 后 split server 上纹理无 GPU 写时 Espryt 自己按存储阈值取舍精确写入框,非 ring 路径可能多发几个小框 | [`docs/archive/notes/p11`](../archive/notes/p11/README.md) |
|
||||
| 19 | P8 | 真实内容只 Magma wire indirect 命中(MC 26.3 × Magma inproc 1,300 万 draw CPU 展开)→ 原生后 create-indirect 每次整 GPU 等待 321 → 0;红米 create-instancing × Espryt × monolith 0.870 → 0.99998(Adreno 丢排队环形拷贝,dev + feat 两条阶梯);最大 program archive 355 KB = arena/4 的 4.2 %(不分片);出口设备矩阵 77 格 / 臂无一变差 | [`docs/archive/notes/p8`](../archive/notes/p8/README.md) |
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# 开放问题全文(原 `ROADMAP.md`「开放问题」,2026-09-24 移入)
|
||||
|
||||
> [`ROADMAP.md`](../ROADMAP.md) 只留每题一行的状态;这里是每一题的完整陈述、已答部分的出处与未答部分的判据。编号稳定,引用写 "开放问题 N"。
|
||||
> [`ROADMAP.md`](ROADMAP.md) 只留每题一行的状态;这里是每一题的完整陈述、已答部分的出处与未答部分的判据。编号稳定,引用写 "开放问题 N"。
|
||||
|
||||
P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。
|
||||
|
||||
1. **client 侧 dirty 走查的真实每 draw CPU 代价。** 已答(P2–P4a):推送没有在拉取基线之下净减少;Release 下 P2 边界 +6–12%,P3a 在 VAO 切换密的 rd12 上再加 +17–19 pt,P4a 在 Espryt 上 +3.4–5.7 pt(`MEASUREMENTS.md` §3–§5)。用户 2026-09-08 接受,性能自此只记录。
|
||||
2. **真实语料上纹理重铸拉取的发生率。** 已答(P4a):可忽略,保留 LRU 维持默认 0——79 例 × 两后端 780 个统计窗口里共 2 次(`iris-photon`、`iris-derivative` 各 1,只在 DirectGLES)。但 P5b 下这条路径是具名 Fatal,正是这两条 trace 加 `create-indirect` GLES 的首阻塞(P9)。**P9 W2 关闭**:重铸不需要拉取——server 读 GPU 层、store 补 pending 盒(Espryt),GPU→GPU 拷贝(Magma);77 行 × {monolith, inproc} 普查仍是同两次(都是 store 无字节的渲染目标),`OnTexturePullRequest` 已删([`p9/W2-REMINT.md`](p9/W2-REMINT.md))。
|
||||
2. **真实语料上纹理重铸拉取的发生率。** 已答(P4a):可忽略,保留 LRU 维持默认 0——79 例 × 两后端 780 个统计窗口里共 2 次(`iris-photon`、`iris-derivative` 各 1,只在 DirectGLES)。但 P5b 下这条路径是具名 Fatal,正是这两条 trace 加 `create-indirect` GLES 的首阻塞(P9)。**P9 W2 关闭**:重铸不需要拉取——server 读 GPU 层、store 补 pending 盒(Espryt),GPU→GPU 拷贝(Magma);77 行 × {monolith, inproc} 普查仍是同两次(都是 store 无字节的渲染目标),`OnTexturePullRequest` 已删([`docs/archive/notes/p9/W2-REMINT.md`](../archive/notes/p9/W2-REMINT.md))。
|
||||
3. **spike B 的 `untrusted_app` 域复核。** 两台设备的分档在 `shell` 域测得;从应用进程再跑一次 `extmem_probe`(spike A 的 exec 钩子已可用)。P11 前做。**2026-09-22 降级**:只对同机臂的 T0 / T1 有意义,stream 数据面不需要。
|
||||
4. **渲染状态的 wire 粒度。** 已答(P2):16 个边界 / 15 个 chunk,7 pipeline 396 B + 8 dynamic 772 B;CSO LRU 64 暂定,P13 重调。
|
||||
5. **无存储的 capability。** 已答(P2):`FramebufferSrgb`、`DepthClamp`、`TextureCubeMapSeamless` 三个都补了真存储,`sizeof(RenderStateParameters)` 仍 1168。
|
||||
@@ -21,10 +21,10 @@
|
||||
|---|---|
|
||||
| 现在做到哪、下一步 | [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md) |
|
||||
| 设计要点与各部分设计 | [`ARCHITECTURE.md`](ARCHITECTURE.md) → [`design/`](design/) |
|
||||
| 分几个阶段、每个阶段做了什么 | [`ROADMAP.md`](ROADMAP.md) → [`notes/<阶段>/`](notes/README.md) |
|
||||
| 分几个阶段、每个阶段做了什么 | [`ROADMAP.md`](ROADMAP.md) → [`notes/<阶段>/`](../archive/notes/README.md) |
|
||||
| 性能与验证的关键数字 | [`MEASUREMENTS.md`](MEASUREMENTS.md) |
|
||||
| 怎么构建、运行、测试 | [`guide/build-and-run.md`](guide/build-and-run.md) |
|
||||
| 代码在哪 / 术语 / 工程纪律 | [`guide/code-map.md`](guide/code-map.md)、[`guide/glossary.md`](guide/glossary.md)、[`guide/discipline.md`](guide/discipline.md) |
|
||||
| 各阶段的原始计划、裁定、报告、证据 | [`notes/`](notes/README.md) |
|
||||
| 各阶段的原始计划、裁定、报告、证据 | [`notes/`](../archive/notes/README.md) |
|
||||
|
||||
整理记录:2026-09-24 两次整理——顶层只留索引,设计正文拆到 `design/`,上手与参考放 `guide/`,阶段细节放 `notes/<阶段>/`;整理前的版本见提交 `1fb18d9`。更早的设计竞赛与评审稿在 git 历史(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`)。
|
||||
|
||||
@@ -15,32 +15,32 @@ client(跑应用的一方)与 server(跑驱动的一方)可以在**不
|
||||
|
||||
| 阶段 | 做什么 | 状态 |
|
||||
|---|---|---|
|
||||
| [P0](notes/p0/README.md) | 打地基:计数器、调用目录与生成器、CI 门;验证 Android 能拉起第二个进程 | ✅ 09-05 |
|
||||
| [P0.5](notes/p05/README.md) | 抽出前后端共用的数据类型头,切断后端对前端头文件的依赖 | ✅ |
|
||||
| [P1](notes/p1/README.md) | 后端改读一个"输入块",并能逐 draw 核对它与前端真实状态一致 | ✅ |
|
||||
| [P2](notes/p2/README.md) | 前端开始主动推送渲染状态;接口代价实测 +6–12%,决定继续 | ✅ 09-08 |
|
||||
| [P3a](notes/p3a/README.md) | GLES 后端的 buffer、顶点数组改用句柄 | ✅ 09-08 |
|
||||
| [P4a](notes/p4a/README.md) | GLES 后端的帧缓冲、纹理、sampler、program 改用句柄 | ✅ 09-08 |
|
||||
| [P5](notes/p5/README.md) | 第一次拆成两线程:命令队列 + 后端线程(每条命令都等) | ✅ 09-16 |
|
||||
| [P5b](notes/p5b/README.md) | 把剩余命令迁到队列上;真机首次以独立后端线程跑通目标游戏 | ✅ 09-16 |
|
||||
| [P5c](notes/p5c/README.md) | 两线程之间不再偷偷共享内存,只经队列交换 | ✅ 09-17 |
|
||||
| [P5d](notes/p5d/README.md) | 两线程版本的性能:游戏内 7–13 fps → 103–106 fps | ✅ 09-18 |
|
||||
| [P5e](notes/p5e/README.md) | client 发完就走,不再每条命令等后端;重负载下与单线程持平 | ✅ 09-19 |
|
||||
| [P5f](notes/p5f/README.md) | 所有状态都经队列传递,为拆进程扫清最后障碍;Vulkan 后端也能发完就走 | ✅ 09-20 |
|
||||
| [P6](notes/p6/README.md) | 拆成两个进程;真机上两进程与两线程成本持平 | ✅ 09-22 |
|
||||
| [P6.5](notes/p65/README.md) | 传输分控制面 / 数据面;电脑当 client、手机当 server 经 TCP 跑通 | 第一波 ✅,余项交 CI |
|
||||
| [Ph](notes/p7/README.md) | server 能安全地接受外来连接:令牌、限额、坏数据不致崩溃 | 必需项 ✅(随 P7) |
|
||||
| [P3b / P4b](notes/p34b/README.md) | GLES 后端的深化与收尾 | 大部分 ✅,余项并行 |
|
||||
| [P7](notes/p7/README.md) | Vulkan 后端完整迁移;真机画面检查 36/36 通过 | ✅ 09-23 |
|
||||
| [**P12**](notes/p12/README.md) | **server 自己开窗口上屏,client 不需要窗口** | ✅ 09-29:出口门 B(跨机 TCP)✅ 09-28、A(FCL + 杀 server)✅ 09-29;契约 `MG_Remote/CONTRACT-P12.md`;G1 成立;余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [**P8**](notes/p8/README.md) | ~~把剩余的仿真路径挪到正确的一侧,补齐协议~~ → **补齐 server 侧仿真缺口与 split 覆盖** | ✅ 09-30:重定界(原 10 项里 6 项前序已完成);Espryt 生成 mip / 暂存副本与 GPU 写 / 驱动拒读、Magma wire 原生 indirect 与 dispatch;246 例登上 split 三臂(`split_coverage.py`);Create 在 Adreno 上的丢拷贝修到 dev 与 feat;契约 `MG_Remote/CONTRACT-P8.md`;余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [**P9**](notes/p9/README.md) | **反向通道:回读、日志、重铸不再同步等待 client** | ✅ 09-29:PACK-PBO 回读 fire-and-forget(两后端)、日志分级转发、重铸无拉取(`OnLog` / `OnTexturePullRequest` 删,回调 9 → 7),F1 / F2;契约 `MG_Remote/CONTRACT-P9.md`;G1 成立;事件量批处理无实测需求不做;余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [**P10**](notes/p10/README.md) | **同步对象、查询与帧节奏** | ✅ 09-29:fence 轮询由 server 报告本地作答(`kEventFenceSignaled`、`MOBILEGL_IPC_POLL_ESCALATE`;主机 Magma·tcp −37%,Adreno 830 上两后端 0 次往返);`SetSwapInterval` 转发、class C 清零;query / XFB / AsyncCompile 登记 split 各臂;credit 1/2/3 设备 loopback 在噪声内;契约 `MG_Remote/CONTRACT-P10.md`;G1 成立;余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [**P11**](notes/p11/README.md) | **同机大缓冲零拷贝共享** | ✅ 09-29:T0 零拷贝导入(两后端,默认开,自测不过或 Stream 上静默退回 T2);同机外部 client 经 helper + 令牌 broker 走共享内存(B1);连接按 `PairBind` nonce 配对(修订 5);采纳档位在握手时定;契约 `MG_Remote/CONTRACT-P11.md`;余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [**P13**](notes/p13/README.md) | **删掉旧的"后端直接读前端"路径** | ✅ 10-07:单进程只走记录臂;pull / push 构建与 `PIPE_PUSH` / `LEGACY_MEMOS` 编译期开关、残余值块、后端前端对象臂删除;七个 OBJECT 模块与 server-only 真链接;link ratchet 172 → 27 具名债务;recorder 金标;G1 退役;FCL 真机两后端冒烟通过;性能轮(基线 `dev`)与余项进 [`DEBTS.md`](notes/DEBTS.md) |
|
||||
| [P0](../archive/notes/p0/README.md) | 打地基:计数器、调用目录与生成器、CI 门;验证 Android 能拉起第二个进程 | ✅ 09-05 |
|
||||
| [P0.5](../archive/notes/p05/README.md) | 抽出前后端共用的数据类型头,切断后端对前端头文件的依赖 | ✅ |
|
||||
| [P1](../archive/notes/p1/README.md) | 后端改读一个"输入块",并能逐 draw 核对它与前端真实状态一致 | ✅ |
|
||||
| [P2](../archive/notes/p2/README.md) | 前端开始主动推送渲染状态;接口代价实测 +6–12%,决定继续 | ✅ 09-08 |
|
||||
| [P3a](../archive/notes/p3a/README.md) | GLES 后端的 buffer、顶点数组改用句柄 | ✅ 09-08 |
|
||||
| [P4a](../archive/notes/p4a/README.md) | GLES 后端的帧缓冲、纹理、sampler、program 改用句柄 | ✅ 09-08 |
|
||||
| [P5](../archive/notes/p5/README.md) | 第一次拆成两线程:命令队列 + 后端线程(每条命令都等) | ✅ 09-16 |
|
||||
| [P5b](../archive/notes/p5b/README.md) | 把剩余命令迁到队列上;真机首次以独立后端线程跑通目标游戏 | ✅ 09-16 |
|
||||
| [P5c](../archive/notes/p5c/README.md) | 两线程之间不再偷偷共享内存,只经队列交换 | ✅ 09-17 |
|
||||
| [P5d](../archive/notes/p5d/README.md) | 两线程版本的性能:游戏内 7–13 fps → 103–106 fps | ✅ 09-18 |
|
||||
| [P5e](../archive/notes/p5e/README.md) | client 发完就走,不再每条命令等后端;重负载下与单线程持平 | ✅ 09-19 |
|
||||
| [P5f](../archive/notes/p5f/README.md) | 所有状态都经队列传递,为拆进程扫清最后障碍;Vulkan 后端也能发完就走 | ✅ 09-20 |
|
||||
| [P6](../archive/notes/p6/README.md) | 拆成两个进程;真机上两进程与两线程成本持平 | ✅ 09-22 |
|
||||
| [P6.5](../archive/notes/p65/README.md) | 传输分控制面 / 数据面;电脑当 client、手机当 server 经 TCP 跑通 | 第一波 ✅,余项交 CI |
|
||||
| [Ph](../archive/notes/p7/README.md) | server 能安全地接受外来连接:令牌、限额、坏数据不致崩溃 | 必需项 ✅(随 P7) |
|
||||
| [P3b / P4b](../archive/notes/p34b/README.md) | GLES 后端的深化与收尾 | 大部分 ✅,余项并行 |
|
||||
| [P7](../archive/notes/p7/README.md) | Vulkan 后端完整迁移;真机画面检查 36/36 通过 | ✅ 09-23 |
|
||||
| [**P12**](../archive/notes/p12/README.md) | **server 自己开窗口上屏,client 不需要窗口** | ✅ 09-29:出口门 B(跨机 TCP)✅ 09-28、A(FCL + 杀 server)✅ 09-29;契约 `docs/archive/contracts/CONTRACT-P12.md`;G1 成立;余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| [**P8**](../archive/notes/p8/README.md) | ~~把剩余的仿真路径挪到正确的一侧,补齐协议~~ → **补齐 server 侧仿真缺口与 split 覆盖** | ✅ 09-30:重定界(原 10 项里 6 项前序已完成);Espryt 生成 mip / 暂存副本与 GPU 写 / 驱动拒读、Magma wire 原生 indirect 与 dispatch;246 例登上 split 三臂(`split_coverage.py`);Create 在 Adreno 上的丢拷贝修到 dev 与 feat;契约 `docs/archive/contracts/CONTRACT-P8.md`;余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| [**P9**](../archive/notes/p9/README.md) | **反向通道:回读、日志、重铸不再同步等待 client** | ✅ 09-29:PACK-PBO 回读 fire-and-forget(两后端)、日志分级转发、重铸无拉取(`OnLog` / `OnTexturePullRequest` 删,回调 9 → 7),F1 / F2;契约 `docs/archive/contracts/CONTRACT-P9.md`;G1 成立;事件量批处理无实测需求不做;余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| [**P10**](../archive/notes/p10/README.md) | **同步对象、查询与帧节奏** | ✅ 09-29:fence 轮询由 server 报告本地作答(`kEventFenceSignaled`、`MOBILEGL_IPC_POLL_ESCALATE`;主机 Magma·tcp −37%,Adreno 830 上两后端 0 次往返);`SetSwapInterval` 转发、class C 清零;query / XFB / AsyncCompile 登记 split 各臂;credit 1/2/3 设备 loopback 在噪声内;契约 `docs/archive/contracts/CONTRACT-P10.md`;G1 成立;余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| [**P11**](../archive/notes/p11/README.md) | **同机大缓冲零拷贝共享** | ✅ 09-29:T0 零拷贝导入(两后端,默认开,自测不过或 Stream 上静默退回 T2);同机外部 client 经 helper + 令牌 broker 走共享内存(B1);连接按 `PairBind` nonce 配对(修订 5);采纳档位在握手时定;契约 `docs/archive/contracts/CONTRACT-P11.md`;余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| [**P13**](../archive/notes/p13/README.md) | **删掉旧的"后端直接读前端"路径** | ✅ 10-07:单进程只走记录臂;pull / push 构建与 `PIPE_PUSH` / `LEGACY_MEMOS` 编译期开关、残余值块、后端前端对象臂删除;七个 OBJECT 模块与 server-only 真链接;link ratchet 172 → 27 具名债务;recorder 金标;G1 退役;FCL 真机两后端冒烟通过;性能轮(基线 `dev`)与余项进 [`DEBTS.md`](DEBTS.md) |
|
||||
| **P14** | **状态归属:同进程多 session / 多 context / share group**(统一 server 的前提,anland 内置渲染器不做按进程隔离) | 设计 ✅ [`design/11-state-ownership.md`](design/11-state-ownership.md),实现待做 |
|
||||
|
||||
## 另见
|
||||
|
||||
- 工程纪律与验证门:[`guide/discipline.md`](guide/discipline.md)
|
||||
- 仍开放的债务:[`notes/DEBTS.md`](notes/DEBTS.md);开放问题:[`notes/OPEN-QUESTIONS.md`](notes/OPEN-QUESTIONS.md)
|
||||
- 仍开放的债务:[`docs/Disaggregated/DEBTS.md`](DEBTS.md);开放问题:[`docs/Disaggregated/OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
### 3.3 能力位(`MGPCapBit`)
|
||||
|
||||
`CallMask` 取代"槽位是否为 null"这一隐式能力探测;`MGPCaps` = `DynamicBackendParameters` + `CallMask` + 格式能力表与 renderer 字符串两个 blob,握手后快照、`MakeCurrent` / `InitCapabilities` 后按 generation 重发布。位包括 viewport array、fp64 顶点、`kCapResidentSubData`、XFB / query 家族、`kCapNeedsHostIndexBytes`、`kCapNeedsHostUboBytes`、`kCapBackendOwnsXfbCapture`、`kCapRunAheadApply`(`MGPipeRunAheadCapBitsFor`:Espryt 自 P5e、Magma 自 P5f 之后的 Magma run-ahead 起发布,后者契约 `MG_Remote/CONTRACT-MAGMA-RUNAHEAD.md`)。**不存在"归属开关"(D-B7)**:multi-draw 分档与 restart 重写永远由 server 拥有,client 只在 caps 要求时提供索引字节。
|
||||
`CallMask` 取代"槽位是否为 null"这一隐式能力探测;`MGPCaps` = `DynamicBackendParameters` + `CallMask` + 格式能力表与 renderer 字符串两个 blob,握手后快照、`MakeCurrent` / `InitCapabilities` 后按 generation 重发布。位包括 viewport array、fp64 顶点、`kCapResidentSubData`、XFB / query 家族、`kCapNeedsHostIndexBytes`、`kCapNeedsHostUboBytes`、`kCapBackendOwnsXfbCapture`、`kCapRunAheadApply`(`MGPipeRunAheadCapBitsFor`:Espryt 自 P5e、Magma 自 P5f 之后的 Magma run-ahead 起发布,后者契约 `docs/archive/contracts/CONTRACT-MAGMA-RUNAHEAD.md`)。**不存在"归属开关"(D-B7)**:multi-draw 分档与 restart 重写永远由 server 拥有,client 只在 caps 要求时提供索引字节。
|
||||
|
||||
## 4. 记录与 payload 约定(`MG_Pipe/MGPipeTypes.h`)
|
||||
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
- 删除:`OnTexturePullRequest`(与 W3 删除的 `OnLog` 合计,回调 9 → 7)、`texture-remint-pull` 标记。op 50 `ResourceSubDataComplete` 只追加不删,留作退役行(解码拒收)。`MOBILEGL_PIPE_TEXEL_RETAIN_MB` 已无消费者,但在 pull 构建里,随 P13 删(G1)。
|
||||
- `ImageBindableHint` 仍是预防:hint 在第一次 sync 前到达就直接按 image-bindable 分配,省掉一次 server 回读 + 重传。
|
||||
- 已知 dev 缺陷(非 P9):monolith Espryt 的前端臂重放 client 影子,覆盖 GPU 写过的纹素。
|
||||
- 普查、覆盖矩阵、门与 red-once:[`notes/p9/W2-REMINT.md`](../notes/p9/W2-REMINT.md)。
|
||||
- 普查、覆盖矩阵、门与 red-once:[`docs/archive/notes/p9/W2-REMINT.md`](../../archive/notes/p9/W2-REMINT.md)。
|
||||
|
||||
### 8.5 XFB scatter 留在 server
|
||||
|
||||
补丁循环需要"捕获前的字节",而拆分后权威影子归 server(`StagedShadow`),所以 scatter 在 server 原地跑完;回程只有一条 `OnBufferWriteback`,载补好的整段,按 `SEG_EVENT` 容量的四分之一切片(`MGPipeBufferWritebackSliceBytes`),最后一片落地即蕴含之前每一片。孤儿目标(`HasDefinedContent == 0`)零填充后照常散射。`OnXfbScatterReady` 已删除(回调 10 → 9)。落地论证见 [`notes/p34b/README.md`](../notes/p34b/README.md)(espryt D1)。
|
||||
补丁循环需要"捕获前的字节",而拆分后权威影子归 server(`StagedShadow`),所以 scatter 在 server 原地跑完;回程只有一条 `OnBufferWriteback`,载补好的整段,按 `SEG_EVENT` 容量的四分之一切片(`MGPipeBufferWritebackSliceBytes`),最后一片落地即蕴含之前每一片。孤儿目标(`HasDefinedContent == 0`)零填充后照常散射。`OnXfbScatterReady` 已删除(回调 10 → 9)。落地论证见 [`docs/archive/notes/p34b/README.md`](../../archive/notes/p34b/README.md)(espryt D1)。
|
||||
|
||||
@@ -51,4 +51,4 @@ Track V(值类型:render state、pixel store、capability 位、标量)是
|
||||
|
||||
### 10.3 索引宿主镜像(`Server/IndexHostMirror`,P8,待重裁)
|
||||
|
||||
设计:`kCapNeedsHostIndexBytes` 下,server 从本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护 element-array buffer 的宿主镜像,零额外流量;预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),超预算退化为逐 draw 经 `MGHostSpan` 传送。**P6 的 a6 审计实测宿主索引 span 无 producer、client 索引已是 owned buffer**,所以 P8 要按真实消费者重新裁定,不照抄本节([`notes/p8/README.md`](../notes/p8/README.md))。
|
||||
设计:`kCapNeedsHostIndexBytes` 下,server 从本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护 element-array buffer 的宿主镜像,零额外流量;预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),超预算退化为逐 draw 经 `MGHostSpan` 传送。**P6 的 a6 审计实测宿主索引 span 无 producer、client 索引已是 owned buffer**,所以 P8 要按真实消费者重新裁定,不照抄本节([`docs/archive/notes/p8/README.md`](../../archive/notes/p8/README.md))。
|
||||
|
||||
@@ -64,4 +64,4 @@ fence 完成度必须来自真的逐 fence 退休,不是 present 水位(Dire
|
||||
- 两端是不同二进制:`wireFingerprint`(`PipeFields.def` 派生的逐成员布局摘要 + 目录摘要 + 字节序 / 指针宽度)永远比较;`buildFingerprint` 只在 `Dial == Fork`(或 `MOBILEGL_IPC_REQUIRE_SAME_BUILD=1`)下比较;不一致是 `Refuse`,不是 abort。
|
||||
- TCP 上每条 `kWaitReply` 就是一个 RTT、`SEG_STAGE` 字节对的是 30–100 MB/s 的链路:这两列从"记录项"变成可行性数(开放问题 19、20)。
|
||||
|
||||
第一波(全 TCP 控制 + stream 数据)已落地;同机混搭(nd `auto` / sd)是第二波。细节 [`notes/p65/README.md`](../notes/p65/README.md),契约 `MobileGL/MG_Remote/CONTRACT-P65.md`。
|
||||
第一波(全 TCP 控制 + stream 数据)已落地;同机混搭(nd `auto` / sd)是第二波。细节 [`docs/archive/notes/p65/README.md`](../../archive/notes/p65/README.md),契约 `docs/archive/contracts/CONTRACT-P65.md`。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|---|---|---|
|
||||
| T0 | client 分配 `AHardwareBuffer` BLOB,server 导入(Vulkan / GLES) | 唯一在两台设备、两个后端上都完整读写的档(P11 主攻) |
|
||||
| T1 | server 导出 opaque fd | 仅 Adreno 的 Vulkan 路径 |
|
||||
| T2 | 拒绝,client 侧推送 | 永久正确回退;**今天 split 用的档**(`MOBILEGL_IPC_ADOPT_TIER=2`),stream 数据面上强制(P11 A1:两端握手时 `Transport::SettleAdoptTierAtHandshake` 对 0/1 打一行 `Refuse{AdoptTierOnStream}` 并走 T2;共享段上 client 握手期 `Fatal{UnimplementedAdoptTier}`,[`CONTRACT-P11.md`](../../../MobileGL/MG_Remote/CONTRACT-P11.md) §1) |
|
||||
| T2 | 拒绝,client 侧推送 | 永久正确回退;**今天 split 用的档**(`MOBILEGL_IPC_ADOPT_TIER=2`),stream 数据面上强制(P11 A1:两端握手时 `Transport::SettleAdoptTierAtHandshake` 对 0/1 打一行 `Refuse{AdoptTierOnStream}` 并走 T2;共享段上 client 握手期 `Fatal{UnimplementedAdoptTier}`,[`CONTRACT-P11.md`](../../archive/contracts/CONTRACT-P11.md) §1) |
|
||||
|
||||
T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `hasLiveHostWrites`);在每个 validate 点按块(`MOBILEGL_IPC_PERSISTENT_BLOCK_KB`,默认 64)推送可达的已映射 buffer,脏页追踪 + 哈希抑制只发变化的块(P5d);门 `PersistentCoherentMapScenario`。`mpr` 数的是每一次 `MapPersistent` 发射(铸成或拒绝都算),所以在 monolith 下也可断言。
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
- 交付链(spike A 已证):APK 唯一可 exec 的位置是 `lib/<abi>/`,server 以 `add_executable` + `lib*.so` 命名构建;从 `untrusted_app` 进程 `fork`+`execve` 同域、零 avc denial;`posix_spawn` 在 minSdk 26 不可用。一份共享库两个角色(stub `dlopen` + `dlsym("mobilegl_server_main")`)。
|
||||
- 离屏 server:前台 `MobileGLServerService`(`:mglsrv`),从 `nativeLibraryDir` exec,fork-per-session,EGL 是 pbuffer / surfaceless。
|
||||
- **上屏 server(P12 子集)**:窗口归 **server 自己**(client 的窗口不跨进程)。`MobileGLDisplayActivity`(`:mglwin`)持全屏 SurfaceView,在自己的进程里用一个线程跑 TCP server(会话串行、会话间重置闩、后端按进程钉住);`ServerDisplay` 以 acquire / release / 几何三个钩子持有窗口,apply 线程以**租约**使用它。client 设 `MOBILEGL_IPC_SURFACE=server` 后**无头**:`eglCreateWindowSurface(NULL)` 发一帧 `WindowKind::ServerOwned`(控制修订 3),不发 `SetWindowHandle`,尺寸由 reply 带回,resize 变成对 server 窗口的几何请求;client 没给 `EGL_WIDTH/HEIGHT`(SDL)时取 native window 的大小作请求,server 把窗口缓冲固定成它并等比留黑边(不然横屏游戏在竖屏窗口里被裁)。FCL 只能接外部 server(自带 spawn 会把窗口指针发过来,被 Rule H 拒),且要开版本设置「游戏退到后台时不暂停」,否则 FCL 一退后台游戏就暂停(`MG_Remote/CONTRACT-P12.md`)。一台设备同一时刻一个 server;一个会话一种表面模式(`SurfaceModeMismatch`);无显示的 server 对 ServerOwned 具名拒绝(`NoServerDisplay`)。
|
||||
- **上屏 server(P12 子集)**:窗口归 **server 自己**(client 的窗口不跨进程)。`MobileGLDisplayActivity`(`:mglwin`)持全屏 SurfaceView,在自己的进程里用一个线程跑 TCP server(会话串行、会话间重置闩、后端按进程钉住);`ServerDisplay` 以 acquire / release / 几何三个钩子持有窗口,apply 线程以**租约**使用它。client 设 `MOBILEGL_IPC_SURFACE=server` 后**无头**:`eglCreateWindowSurface(NULL)` 发一帧 `WindowKind::ServerOwned`(控制修订 3),不发 `SetWindowHandle`,尺寸由 reply 带回,resize 变成对 server 窗口的几何请求;client 没给 `EGL_WIDTH/HEIGHT`(SDL)时取 native window 的大小作请求,server 把窗口缓冲固定成它并等比留黑边(不然横屏游戏在竖屏窗口里被裁)。FCL 只能接外部 server(自带 spawn 会把窗口指针发过来,被 Rule H 拒),且要开版本设置「游戏退到后台时不暂停」,否则 FCL 一退后台游戏就暂停(`docs/archive/contracts/CONTRACT-P12.md`)。一台设备同一时刻一个 server;一个会话一种表面模式(`SurfaceModeMismatch`);无显示的 server 对 ServerOwned 具名拒绝(`NoServerDisplay`)。
|
||||
- 失窗:`surfaceDestroyed` 有界(3 s)阻塞,直到 apply 线程(批内每条记录前也检查)放掉后端表面并以 `Fatal{ServerWindowLost}` 闩住会话;server 继续监听。进程内会话结束时手动做进程退出本来白给的清理(Espryt twin 与单元影子、swap interval、Magma renderer)。
|
||||
- `HeadlessGL` 的 fork 预检会产生孤儿 server:server 的 EOF 检测即时且无条件退出。
|
||||
|
||||
|
||||
@@ -70,4 +70,4 @@ CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_
|
||||
|
||||
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`)
|
||||
|
||||
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`notes/p6/README.md`](../notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
|
||||
关闭时每站点一次全局 load + 一条永不命中的分支。字节类(stage buffer / texture / UBO / client 数组、`pmap`、`resid`、`csob-blob`、`seg` = 写进 `SEG_STAGE` 的字节)、调用类(`draws`、`accessor-calls`、纹理上传 emit / box / rect / jobs、`csom` / `csob`、`mpr`、`trp`、`rsp`、`wrec`、`maxrec` / `ringwraps` / `ringpads` / `ringwaits`、门铃 `srv` / `srvpark` / `cli` / `clipark`)与六个 memo 门的 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行,按角色写日志。读法陷阱:`accessor-calls` 是静态下界,判性能只看 CPU 时间;spawn 下 client 不推进帧窗口(`frames=0`),窗口化字段在 spawn client 上无效,run total 仍有效([`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
| 原节 | 主题 | 仍生效的规则 | 全文 |
|
||||
|---|---|---|---|
|
||||
| 17.1 | verb barrier 与诚实的同地址空间传输(P5) | 同地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`;reply 以记录序号为 slot id(R-3);等待看门狗 120 s 只判死锁,不是性能门 | [`notes/p5/README.md`](../notes/p5/README.md) |
|
||||
| 17.1 | verb barrier 与诚实的同地址空间传输(P5) | 同地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`;reply 以记录序号为 slot id(R-3);等待看门狗 120 s 只判死锁,不是性能门 | [`docs/archive/notes/p5/README.md`](../../archive/notes/p5/README.md) |
|
||||
| 17.2 | 后端槽三类、caps、tight readback(P5) | A 本地答 / B 发射 / C 具名拒绝,永不回落 monolith(R-4);能力只读 `CallMask`(R-8);`ReadPixels` 线上恒 tight,client 按自己的 pack state 散射 | 同上 |
|
||||
| 17.3 | server 角色、shadow 与退出(P5) | apply 线程终身持有 native context,EGL 罕见操作经控制邮箱进 apply 线程;staged shadow 只交 server-owned 拷贝;有界退出顺序 | 同上 |
|
||||
| 17.4 | lane 隔离(P5) | 每条 split 条目私有日志;E1 / E3 控制把"选中条目被跳过"判为失败 | 同上 |
|
||||
| 17.5 | class-C verb 迁移(P5b) | 迁移顺序由动态普查决定;一次迁移 = 一个发射器 + 一个 sink 体;记录逐字带 GL 调用参数,未测量的形式按名拒绝(规则 D) | [`notes/p5b/README.md`](../notes/p5b/README.md) |
|
||||
| 17.6 | 共享地址空间访问归零(P5c) | server 端纹理 staged shadow;`SEG_EVENT` 是唯一反向通道;sink / twin 按记录句柄解析;`applier_reset` / `object_death` 控制记录;角色守卫 `Fatal{RoleViolation}` | [`notes/p5c/README.md`](../notes/p5c/README.md) |
|
||||
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照,`VERB_BARRIER=0` 会连带关掉 run-ahead | [`notes/p5e/README.md`](../notes/p5e/README.md) |
|
||||
| 17.5 | class-C verb 迁移(P5b) | 迁移顺序由动态普查决定;一次迁移 = 一个发射器 + 一个 sink 体;记录逐字带 GL 调用参数,未测量的形式按名拒绝(规则 D) | [`docs/archive/notes/p5b/README.md`](../../archive/notes/p5b/README.md) |
|
||||
| 17.6 | 共享地址空间访问归零(P5c) | server 端纹理 staged shadow;`SEG_EVENT` 是唯一反向通道;sink / twin 按记录句柄解析;`applier_reset` / `object_death` 控制记录;角色守卫 `Fatal{RoleViolation}` | [`docs/archive/notes/p5c/README.md`](../../archive/notes/p5c/README.md) |
|
||||
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照,`VERB_BARRIER=0` 会连带关掉 run-ahead | [`docs/archive/notes/p5e/README.md`](../../archive/notes/p5e/README.md) |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 状态归属:session / context / share group / thread current(P14 设计)
|
||||
|
||||
> 为 anland 内置统一 server 而做。动因与否定项见 [`../notes/anland/handoff-legacy-mobilegl-unified.md`](../notes/anland/handoff-legacy-mobilegl-unified.md):不许用「每应用 fork 一个渲染进程」掩盖全局状态归属。本文行号对 `2f64ede8`(+ `4156ca6e` 文档提交),代码移动后以符号名检索。
|
||||
> 为 anland 内置统一 server 而做。动因与否定项见 [`docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md`](../../archive/notes/anland/handoff-legacy-mobilegl-unified.md):不许用「每应用 fork 一个渲染进程」掩盖全局状态归属。本文行号对 `2f64ede8`(+ `4156ca6e` 文档提交),代码移动后以符号名检索。
|
||||
|
||||
## 结论
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@ ctest --test-dir build-split -L 'integration-(spawn|tcp)' --output-on-failure
|
||||
|
||||
- 两份 APK flavour:单进程 / split(Gradle 属性 `mobilegl.buildDisaggregated`、`mobilegl.buildDisaggregatedInproc`;独立 plugin 构建默认 split,FCL 内嵌默认单进程)。split APK 同样要设 `MOBILEGL_TRANSPORT`,不设就是单进程对照。
|
||||
- 设备侧 server 有两种形态:离屏的前台 Service(`MobileGLServerService`)与上屏的 `MobileGLDisplayActivity`;同一时刻只有一个。plugin 与 trace 两个 flavour 的 APK 都带(standalone 构建默认开 `mobilegl.buildDisaggregated`),入口是 APK 底部导航栏的 “Render Server” 页;plugin flavour 的这三个组件不导出,只有 trace 允许 adb 直接按组件名拉起。
|
||||
- 电脑当 client、手机当 server 的跨机配置(起 server、设令牌、`adb forward` 备用路线)见 [`../notes/p65/README.md`](../notes/p65/README.md) 的"两边怎么配"。
|
||||
- 电脑当 client、手机当 server 的跨机配置(起 server、设令牌、`adb forward` 备用路线)见 [`docs/archive/notes/p65/README.md`](../../archive/notes/p65/README.md) 的"两边怎么配"。
|
||||
- 性能对比只用 Redmi `2f7cbe2e`,按 [`pin-verification-2026-09-07.md`](pin-verification-2026-09-07.md) 定频,reboot-clean、同热窗口、配对 A/B。P13 之后的性能工作以 `dev` 分支构建为「之前」基线(用户 2026-10-07)。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|---|---|---|---|---|---|
|
||||
| Redmi M332BF `2f7cbe2e` | SM8750 / Adreno 830v2 | policy6 → 1958400(stock 1017600–3072000) | policy0 → 1555200(stock 556800–2745600) | kgsl `min/max_pwrlevel = 0`;2026-09-11 实测 1050 MHz(厂商 `thermal_pwrlevel` 锁 1),2026-09-16 起实测 1100 MHz | 主动风扇 `/sys/class/xm_power/hw_monitor/pwm_fan` 恒 level 2,用例间降温 <1 min;温度门 `cpuss-0-0` |
|
||||
|
||||
与小米 `35d0befa` 同 SoC、同 OPP 表,两台的钉频数值可比;跨钉频口径(1050 vs 1100 MHz)的活动不可比钟频,只有同场配对可比。Redmi 的行已于 P6 收尾(2026-09-22)并入树内 `tools/device_bench/pin_device.sh`(`2f7cbe2e` 条目;stock 范围与小米不同,GPU 实测钳在 1050 MHz:`thermal_pwrlevel` 写 0 读回 1,见 [`../notes/p6/README.md`](../notes/p6/README.md) §12.7);此前它只在树外的 `~/w7/notes/p2/devices/pin_device.sh`。
|
||||
与小米 `35d0befa` 同 SoC、同 OPP 表,两台的钉频数值可比;跨钉频口径(1050 vs 1100 MHz)的活动不可比钟频,只有同场配对可比。Redmi 的行已于 P6 收尾(2026-09-22)并入树内 `tools/device_bench/pin_device.sh`(`2f7cbe2e` 条目;stock 范围与小米不同,GPU 实测钳在 1050 MHz:`thermal_pwrlevel` 写 0 读回 1,见 [`docs/archive/notes/p6/README.md`](../../archive/notes/p6/README.md) §12.7);此前它只在树外的 `docs/archive/notes/p2/devices/pin_device.sh`。
|
||||
|
||||
## 2026-09-07 核验:小米 `35d0befa` 与 Oppo `3B159D009VZ00000`(P2 / P3a 使用)
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ with a 165 Hz display (6.06 ms per vsync). Espryt = DirectGLES, Magma = DirectVu
|
||||
thread records and applies. Inproc: the GL thread records and `mgl-srv-apply` applies and presents.
|
||||
|
||||
The timelines and latency below come from one 4.9 s Perfetto run per arm (`pwlat`, with frametimeline).
|
||||
The fps numbers come from the 3-rep A/B in [SCOREBOARD.md](../notes/perf-p15/SCOREBOARD.md) (section pwab).
|
||||
The mechanism is analysed in depth in [PRESENT-WAIT.md](../notes/perf-p15/PRESENT-WAIT.md).
|
||||
The fps numbers come from the 3-rep A/B in [SCOREBOARD.md](../../archive/notes/perf-p15/SCOREBOARD.md) (section pwab).
|
||||
The mechanism is analysed in depth in [PRESENT-WAIT.md](../../archive/notes/perf-p15/PRESENT-WAIT.md).
|
||||
|
||||
## Summary
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
> source named) or **[unverified]** (expected from documentation or memory of the client sources;
|
||||
> each one has a check in the M1 spikes). Anything that says "will", "proposed" or "TODO" is design.
|
||||
>
|
||||
> Context: [`../Disaggregated/notes/anland/plan-ahb-dmabuf.md`](../Disaggregated/notes/anland/plan-ahb-dmabuf.md)
|
||||
> Context: [`docs/archive/notes/anland/plan-ahb-dmabuf.md`](../archive/notes/anland/plan-ahb-dmabuf.md)
|
||||
> (shared images), the YUV import work in MobileGL `7218bacb` / `18a5dba0`, and the skill
|
||||
> `.claude/skills/anland-mobilegl-plasma/` (build, deploy, probes).
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Measured 2026-09-26 on the split stack (client and server in separate processes). These are the numbers
|
||||
that used to live in the comment above `IpcTable::CreateWindow` in `MobileGL/Config.h`. The longer record,
|
||||
with the logs and the attempts that failed, is
|
||||
[`notes/p12/CREATE-WINDOW-MEASURED.md`](../Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md) (in Chinese).
|
||||
[`docs/archive/notes/p12/CREATE-WINDOW-MEASURED.md`](../archive/notes/p12/CREATE-WINDOW-MEASURED.md) (in Chinese).
|
||||
|
||||
## What the knob does
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Archive
|
||||
|
||||
The planning notes and agent contracts of the disaggregation work, kept as they were when the work moved on. Nothing here is maintained. The directory is frozen, and the text check (`scripts/check_text_hygiene.py`) rejects any change to it.
|
||||
|
||||
| Directory | Moved from | Contents |
|
||||
|---|---|---|
|
||||
| `notes/` | `docs/Disaggregated/notes/` | Per-phase plans, briefs, reviews, measurements and logs |
|
||||
| `contracts/` | `MobileGL/MG_Remote/` | The per-phase agent contracts (`CONTRACT-P5.md` and so on) |
|
||||
|
||||
Links inside these files were adjusted once, for the move. Links that were already broken before it are left as they were.
|
||||
|
||||
The maintained documents are in `docs/Disaggregated/`: the design and guide directories, `DEBTS.md`, `OPEN-QUESTIONS.md` and the `ARCHITECTURE.md`, `ROADMAP.md` and `MEASUREMENTS.md` indexes.
|
||||
@@ -0,0 +1,63 @@
|
||||
# CONTRACT — Magma run-ahead
|
||||
|
||||
This extends CONTRACT-P5E to DirectVulkan after P5f and the Magma application-buffer
|
||||
migration. The P5e rule F, wait-class table, reply ownership, event flow control,
|
||||
present credit and one-way run-ahead latch remain the protocol. This document
|
||||
supersedes the historical requirement that Magma never publish `kCapRunAheadApply`.
|
||||
It does not make Magma a multi-process transport or complete every P7/P8 GL shape.
|
||||
|
||||
## Readiness and waits
|
||||
|
||||
Magma has an independent implementation-readiness constant in `MG_Backend/Init.cpp`.
|
||||
Only the integration commit may enable it after the queued-state tests pass. A
|
||||
ready server advertises the existing bit 10; `MOBILEGL_IPC_RUN_AHEAD=0` remains the
|
||||
paired lockstep control. Unsupported backends/readiness=false advertise no bit.
|
||||
The knob alone never arms run-ahead. A later caps snapshot may disarm, not arm it.
|
||||
|
||||
Unbarriered apply reads only records, server storage and immutable capabilities.
|
||||
No frontend object/registry or legacy pointer getter is admitted by a barrier,
|
||||
scope, or backend choice. Unsupported GL shapes keep named refusals. Reply rows,
|
||||
explicit synchronization, control operations and present-credit exhaustion retain
|
||||
their existing waits; a test that removes all waits would violate the protocol.
|
||||
|
||||
## GPU resource and submission order
|
||||
|
||||
1. Wire buffer updates use exact owned bytes. A busy store is updated by ordered
|
||||
GPU copy or a justified host-access wait; CPU readback does not revoke a slice
|
||||
already reserved for a following draw. Store identity includes generation.
|
||||
2. Lazy texture upload/preserve submissions are ordered after earlier renderer
|
||||
recordings. Descriptor resolution prepares all active texture/image resources
|
||||
and framebuffer attachments before capturing the current command buffer; a
|
||||
later lazy upload cannot silently move bindings into an already submitted one.
|
||||
3. CPU draw preparation can own future transient slices even before a draw is
|
||||
recorded. Idle-drain/arena rewind must not reclaim them during that interval.
|
||||
4. Wire draw, clear and color blit retain every native framebuffer, render pass,
|
||||
view and descriptor pool until the submission that names it completes. Retirement
|
||||
is keyed to submit indices, not an assumed number of frames or client progress.
|
||||
Swapchain replacement/shutdown drains these after device idle, including abandoned
|
||||
recordings. Independent blits never overwrite a descriptor set still in flight.
|
||||
Wire sampled views and draw framebuffer objects may be reused within a frame slot:
|
||||
exact keys include native storage identity/generation and every view/window value,
|
||||
and the slot is cleared only after its actual fence completes. Overflow still uses
|
||||
submit-index retirement. A cache hit never skips the image dependencies or pass boundaries.
|
||||
Pipelines use renderer-lifetime, non-recycled identities interned from exact render-pass
|
||||
compatibility values. A creator's temporary native render-pass handle is not a wire
|
||||
pipeline cache key or an eviction proof for another compatible pass using that pipeline.
|
||||
5. GPU writes retain their reverse notifications; readback waits for actual GPU
|
||||
completion and returns owned bytes. Removing a per-draw queue idle does not
|
||||
remove explicit GL synchronization or image/buffer memory dependencies.
|
||||
|
||||
## Evidence required before activation
|
||||
|
||||
Real Magma tests must hold apply at a known boundary while the client publishes
|
||||
later commands, observe real sequence/ack counters, then verify pixels/bytes from
|
||||
each queued state. Include resource rewrite/respecify/death-reuse, program/uniform
|
||||
rebinding, GPU write/readback, texture ordering/promotion, and present credits 1/3.
|
||||
The dedicated discovery set must execute completely without skips. A real
|
||||
run-ahead-off control must fail the queued-progress assertion, then restore green.
|
||||
|
||||
Keep strict/dual-block and zero-residual gates; preserve pull G1 and test-name
|
||||
ratchets. On Redmi, verify the installed library identity, actual `run-ahead ARMED`,
|
||||
both roles, world rendering and liveness, and record a paired run-ahead-off arm.
|
||||
Performance numbers are recorded with their workload/clock limits, not substituted
|
||||
for correctness or inferred from the capability log alone.
|
||||
@@ -0,0 +1,54 @@
|
||||
# P10:sync / query / present 节奏
|
||||
|
||||
2026-09-29。依据 `docs/Disaggregated/notes/p10/`(计划与范围重划 [`PLAN-P10.md`](../notes/p10/PLAN-P10.md)、裁定 `INTEGRATOR-DECISIONS-P10.md`)。本文只记实现所需的协议与行为;验证结果以 notes 里的运行证据为准。
|
||||
|
||||
## 范围
|
||||
|
||||
fence 轮询不再是往返(§1);最后一个 class-C 与 split 覆盖(§2);credit 与往返的测量记录(§3)。路线图原文里已做完或前提已变的项(client 铸造 query handle、present 1:1、credit 默认 1、无 present 负载的 `SEG_STAGE` 饥饿)见计划 §1。
|
||||
|
||||
## 1. fence 轮询从 server 的报告作答(A)
|
||||
|
||||
### 线上格式
|
||||
|
||||
- `EventKind` 追加 `kEventFenceSignaled = 5`,载荷 `EventFenceSignaledHead { EventHandle Fence; }`(8 字节):server 看到该 client fence 已 signaled,每个 fence 至多一条。opcode 与控制协议修订号不变。
|
||||
|
||||
### server
|
||||
|
||||
- `ServerVerbSink` 按**创建顺序**记下尚未报告的 fence(`FenceCreate` 入队;被删的出队时跳过)。`ReportSignaledFences(flush)` 从最老的一个开始查,查到第一个未完成的就停(同一 context 的 GPU 按提交顺序完成),每个已完成的发一条 `kEventFenceSignaled` 并立即发布。
|
||||
- `OnPresent` 在归还 present credit **之前**调用一次(状态查询,不提交):credit 放行的 client 会立刻去等一两帧前的 fence,报告必须先于放行。
|
||||
- apply 线程在**每个 drain 批次之后**调用(后端表 `GL.GetSyncStatus`,不提交);**将要空闲**且还有未报告的 fence 时先调一次 `flush=true`——最老的 pending fence 改用 `GL.ClientWaitSync(GL_SYNC_FLUSH_COMMANDS_BIT, 0)`,这是两个后端都会提交所持命令的唯一后端无关方式——仍有未报告的就**限时停车 1 ms**(`kFenceIdlePollMs`)后再查,否则照旧无限期停车。`SEG_EVENT` 满时不报告(与既有"环满不 apply"同一规矩)。
|
||||
- 与后端无关:两个后端都实现 `GL.FenceSync / GetSyncStatus / ClientWaitSync`。没有原生 fence 的(`FenceSync` 为空或返回空)视为已 signaled。
|
||||
|
||||
### client
|
||||
|
||||
- 事件排空时记下已报告的 fence(`{slot → gen}`,独立互斥锁:排空发生在持有 `g_fenceMutex` 的发射函数内部);往返答"已 signaled"的也记下;`glDeleteSync` 时忘掉。
|
||||
- `glClientWaitSync` / `glGetSynciv(GL_SYNC_STATUS)` 先做一次**轮询入口**(`ClientSession::PollEntry`:刷出已发布的记录、排空反向通道),然后:
|
||||
- 已知 signaled → 本地答 `GL_ALREADY_SIGNALED` / `GL_SIGNALED`;
|
||||
- 未知且调用不能等(timeout = 0、`glGetSynciv`)→ 本地答 `GL_TIMEOUT_EXPIRED` / `GL_UNSIGNALED`;同一 fence 连续第 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)改为一次真往返,计数归零;
|
||||
- 未知且调用可以等(timeout > 0)→ 真往返,语义不变。
|
||||
- `MOBILEGL_IPC_POLL_ESCALATE=0`:每次轮询都往返,即 P10 之前的行为(A/B 臂)。设备丢失后一律走往返臂(其 DECLINED 答复是既有的 no-op)。
|
||||
- 计数:`ReadFencePollCounters()`(本地作答、升级、往返、server 报告),经 `SplitRuntimePeek` 给车道。
|
||||
|
||||
### 门
|
||||
|
||||
- `FencePollScenario`(两后端 × 单进程 / Split / Spawn / Tcp):零超时轮询循环有界结束(带 / 不带 flush 位,后者仅 split);已 signaled 的 fence 轮询不产生回复槽记录;只轮询不发记录时,空闲 server 仍会报告后来完成的 fence(无升级)。单元控制 `RemoteClientControls.AnUnreportedFencePollCrossesOnlyEveryPollEscalateTimes` 钉住"第 N 次才过线"。
|
||||
- red-once:`POLL_ESCALATE=0` → 200 条回复槽记录 / 200 次往返;去掉空闲复查 → Espryt 需 1 次升级、Magma 升级 15 次仍等不到(Magma 的 fence 不会自己提交);client 关掉本地作答 → 单元控制 exit 163。
|
||||
|
||||
### 测量
|
||||
|
||||
`PacedFrameFenceWaitsBench`(手动开启,不设门)的配对结果在 [`A-FENCE-POLL.md`](../notes/p10/A-FENCE-POLL.md):Magma 走 tcp 时每帧耗时降 37%、fence 往返为 0;Espryt 在主机 llvmpipe 上没有收益,因为只有阻塞等待才能让 fence 可见。在 Adreno 830 上两后端都是 0 次往返、598 / 598 次本地作答([`C-MEASUREMENTS.md`](../notes/p10/C-MEASUREMENTS.md) §1)。
|
||||
|
||||
## 2. 最后一个 class-C 与 split 覆盖(B)
|
||||
|
||||
- 发射表的 `SetSwapInterval` 槽 = server 转发器(与 `eglSwapInterval` 经 `SurfaceControlOp::SetSwapInterval` 的同一路);`SetSwapInterval_Unmigrated` 与 `BackendObject_Remote` 的覆盖删除。class C 为空:69 已实现 / 0 未迁移。单元控制:`ServerLoopTest.TheEmitTableSwapIntervalSlotCrossesAsOneDispatchedFrame`(经表调用,恰好一个控制帧到达)。
|
||||
- caps mirror 采纳 server 快照时,`GL_KHR_parallel_shader_compile` 按 **client** 的 `MOBILEGL_ASYNC_SHADER_COMPILE` 重算:编译池与 `GL_MAX_SHADER_COMPILER_THREADS_KHR` 都在 client。red-once:去掉重算 → 两后端 Tcp 的 `AsyncOff` 扩展串用例红。
|
||||
- 登记(全部进门):`AsyncCompileScenario`(`AsyncOn.` / `AsyncOff.` / `OptimisticShaderStatus.`)与 `XfbPrimitiveQueryScenario` × 两后端 × Split / Spawn / Tcp;`PrimitivesGeneratedNoXfbScenario` 只在 Magma(`PrimGen.` 三臂,`PrimGenReroute.` 仅 Split / Spawn:tcp 的共享 server 读不到改道开关)。red-once:client 查询结果加一 → 101 条里 49 条红(全部 XfbQuery / PrimGen / PrimGenReroute),同名 monolith 条目仍绿。
|
||||
|
||||
## 3. 测量(只记录)
|
||||
|
||||
见 [`C-MEASUREMENTS.md`](../notes/p10/C-MEASUREMENTS.md),包括真机 fence bench、设备上 TCP loopback 的 `PRESENT_CREDIT` 1/2/3、以及主机 retrace 往返普查。在 loopback 上 credit 没有噪声以外的影响(rd12 51.4 / 51.6 / 52.4 fps),默认值保持 1。
|
||||
|
||||
## 不变量
|
||||
|
||||
- G1:新代码全在 `MOBILEGL_BUILD_DISAGGREGATED` 下(`MG_Remote`、`Config` 的 IPC 段)。
|
||||
- `EventKind` 只追加;控制协议修订号不变(3)。
|
||||
@@ -0,0 +1,354 @@
|
||||
# P11:persistent map 采纳档位诚实化(A 包主机半)
|
||||
|
||||
2026-09-29。依据 [`PLAN-P11.md`](../notes/p11/PLAN-P11.md) §2 的 A1、A2 与 C 的文档修正。档位名按 `design/07`:T0 = client 分配 `AHardwareBuffer`、server 导入;T1 = server 导出 opaque fd;T2 = 拒绝,client 保留 shadow 并推送。本文只记实现所需的行为与门;A3 / A4(设备)与 B(T0)不在本文。
|
||||
|
||||
## 范围
|
||||
|
||||
档位在握手时定下(§1);断言 arena 落在哪一档、三臂登记(§2);文档漂移(§3)。
|
||||
|
||||
## 1. 档位由知道数据面的一侧在握手时定下(A1)
|
||||
|
||||
### 线上格式
|
||||
|
||||
- 无变化。LinkTerms、`RefuseCode`、控制协议修订号(3)都不动:每一侧只判自己的旋钮,不需要把档位带过线。
|
||||
|
||||
### 规则(`Transport/AdoptTier.cpp` `SettleAdoptTierAtHandshake`)
|
||||
|
||||
| 数据面 | 侧 | `MOBILEGL_IPC_ADOPT_TIER=0/1` |
|
||||
|---|---|---|
|
||||
| Stream(tcp) | client / server | 一行 W `Refuse{AdoptTierOnStream, "T<n>"}`,本会话 T2;从不 Fatal |
|
||||
| SharedSegments(inproc、fork / unix spawn) | client | `Fatal{UnimplementedAdoptTier, "T<n>"}`,握手期、任何记录之前 |
|
||||
| SharedSegments | server | 一行 W(档位由 client 定),T2 |
|
||||
|
||||
- **B2 起,SharedSegments 两行被 B2 的档位表取代**(0 = 问 T0、1 = 具名拒绝后 T2,都不再 Fatal),见 B2 节。
|
||||
|
||||
- 调用点:client `StartOverTransportPair`(Welcome 校验后)与 `StartOverSocket`(link terms 定下 `stream` 后、开数据连接前);server `ServerSession::Accept`(后端检查后、Welcome 前)。
|
||||
- 使用处 `Client::AdoptTierIsEmulate`(`PipeApply.cpp`、`PipeWireCodec.cpp` 仍问它):旋钮 2 → true;本进程已有握手定档 → true,不再打印;没有握手定过档的调用者 → 仍 `Fatal{UnimplementedAdoptTier}`("no handshake settled")。旋钮 > 2(解析器不收)→ Fatal。
|
||||
- `ServerSpawn` 从它拉起的子进程环境剔除 `MOBILEGL_IPC_*`(五个 server 自有旋钮除外),所以 fork / unix spawn 的 server 看不到 client 的旋钮;tcp 的 server 读自己的环境。
|
||||
|
||||
### 普查
|
||||
|
||||
新的死亡走 `SessionFail`:abort 站点仍 79,family 词仍 45(复用 `UnimplementedAdoptTier`);refusal 词 3 → 4,`AdoptTierOnStream` 进 `fatal_census.py` 的 `LOCAL_REFUSAL_WORDS`(本地拒绝,不成为 Refuse 帧)。
|
||||
|
||||
### 门
|
||||
|
||||
- 单元:`AdoptTierStream.*`(真 tcp 会话:client 0/1 → 会话起、一行拒绝、`map_persistent` 得 DECLINED、之后仍 apply;进程内显示 server 0 → 每会话一行、不死);`RemoteClientControls.AdoptTier{Zero,One}OverSharedSegmentsDiesAtTheHandshakeBeforeAnyMapPersistent`(inproc 真握手,子进程在 `Start` 返回前死、具名);`SplitBufferSet.OnlyAdoptTierTwoIsImplemented`(函数级矩阵);`PipeWireCodecTest.AdoptTier{Zero,One}AfterAStreamHandshakeIsADeclineOnTheWirePath` 与 `AnUnsettledAdoptTierStillDiesByNameOnTheWirePath`。
|
||||
- 集成:`{DirectGLES,DirectVulkan}.Tcp.AdoptTier0.LargeArenaAdoptionScenario.*`(`MOBILEGL_IPC_ADOPT_TIER=0`;`integration-tcp` / `integration-magma-tcp`),`AStreamSessionRefusesTheAdoptTierOnceByName` 读 client 日志恰一行,其余用例绿。tcp 专属:共享段上同一旋钮在握手期死(`spawn_lane_parity.py` 的 `MAGMA_TCP_ONLY`)。
|
||||
- red-once:删 Stream 分支 → 两后端 tcp 条目 client 握手期 `Fatal{UnimplementedAdoptTier}`、无拒绝行,单元 `AdoptTierStream` 两例、codec 两例、`SplitBufferSet` 一例红;把检查挪回使用处 → inproc 会话起来、`map_persistent` 发出后才在 apply 线程死(子进程写出 `S`),spawn 臂则静默跑 T2(server 环境被剔除)。
|
||||
|
||||
## 2. arena 落在哪一档(A2)
|
||||
|
||||
- `LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares`:`PersistentMapPeek` 读 arena 的 `IsBackendPersistentMapped`,与车道的 `MGITEST_PERSISTENT_MAP_ARM` 比;问两次(NULL 定义后;一帧绘制 + SSBO 绑定 + dispatch 后)。未声明的车道跳过。
|
||||
- 声明:monolith = adopted(`DirectGLES.ResourceSubsystemOn/Off.`、新 `DirectVulkan.ArenaArm.`,都钉 monolith);split = emulated(Espryt 三臂经 `SplitLogPaths` 按条目声明;Magma `Arena.` 块与 Buffers 车道)。
|
||||
- 登记:`mgl_itest_register_split_arms("LargeArenaAdoptionScenario.*")`(Espryt 进门、Magma 进信息层);Magma 进门:`DirectVulkan.<arm>.Arena.`(band 口味,六例)与 Buffers 车道(加六例,`test.yml` 的 require-green 名单同步)。
|
||||
- red-once:monolith 车道加 `MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION=1` → 三条红;split 车道改声明 adopted → 两后端 8 条红。删 `PipeApply.cpp` 的 R-6 拒绝**不会**让 Espryt inproc 采纳(实测仍 emulated):split 下 client 的采纳门经线路走,codec 不调 applier 就答 DECLINED;applier 只在 server 角色自己发射时被问到(只有 Magma 的 `EnsureGpuResidentStorage`),而 Magma 的线路表 `MapPersistent` 恒返回空。
|
||||
|
||||
## 3. 文档漂移(C)
|
||||
|
||||
- `Config.h`:大 buffer 采纳下的 SubData 自 `0ee3384b` 起经 resident-subdata GPU 有序落地,不是"plain memcpy"。
|
||||
- `MG_Backend/Init.cpp`:DirectVulkan 在非 monolith 下注册线路资源表(`VkBufferManager::RegisterWireResourceOps`)并声明 bit 7。
|
||||
- 档位命名按 `design/07`:`PersistentMapTracker.h` 注释已改。`protocol.fbs` 的 `Adopt = 6 // server-owned adopted store` 注释**没改**:`protocol_revision_pin.py` 对整文件取 sha256,改注释要 bump 修订号。
|
||||
- `CONTRACT-P6.md`、`design/06`、`design/07` 的"stream 上具名拒绝"现为真,已指向本节代码。
|
||||
|
||||
## 不变量
|
||||
|
||||
- G1:新代码全在 `MOBILEGL_BUILD_DISAGGREGATED` 下(`MG_Remote`)。
|
||||
- 线上格式与控制协议修订号不变(3)。(A 包;PAIR 包把修订号升到 4,见下节;B2 升到 5。)
|
||||
|
||||
## PAIR:socket client 的两条连接按身份配对(B0 F2)
|
||||
|
||||
依据 [`B0-CROSS-APP.md`](../notes/p11/B0-CROSS-APP.md) F2:server 按到达顺序配对(`AcceptPair`,"先到是控制、后到是 aux"),设备上 server app 的就绪探测(一次空 `connect()` + `close()`)成了下一个真 client 的控制连接,会话读到探测的套接字后退出(`control peer closed before sending a first frame`),client `no Welcome … (rc=6)`;两个 client 同时连也会互换半边。
|
||||
|
||||
### 线上格式
|
||||
|
||||
- `protocol.fbs` 追加 `table PairBind { nonce: [ubyte]; aux: bool; }` 与 `CtrlMsg` 末尾的 `PairBind`。**控制协议修订号 3 → 4**(`mg_protocol_base.h`,`protocol_revision_pins.json` 已钉),`wireFingerprint` 随之变。
|
||||
- `SocketTransport::ConnectTo`(`unix:` 与 fork spawn 两条 client 路径都走它;`tcp://` 上只有测试用):CSPRNG 取 16 字节 nonce,控制、aux 两条连接各以一帧 `PairBind{nonce, aux}` 开头;控制连接上 Hello 在它之后。编解码在 `Transport/PairBind.h`。
|
||||
- TCP 产品路径不变:`ConnectControl` 单连接,数据连接凭 server 发的 `Welcome.dataNonce` 绑定(`DataBind`,PH-7 (4)),`TcpSupervisor` 的预认证门本来就丢探测。
|
||||
|
||||
### 规则(`Server/PairAcceptor.{h,cpp}`,取代已删的 `SocketTransport::AcceptPair`)
|
||||
|
||||
| 连接的首帧 / 状态 | 处置 |
|
||||
|---|---|
|
||||
| 一字节未发就关(就绪探测) | 关闭,D 级日志 |
|
||||
| `PairBind`,另一角色同 nonce 已在等 | 成对交出(控制作流、aux 作描述符通道) |
|
||||
| `PairBind`,同 nonce 同角色已在等 | `Refuse{Authentication}`,关新来者 |
|
||||
| `Hello`(修订号 < 4 的 client) | 按 `RunSession` 的顺序问:token → 线上格式;旧 client 得 `Refuse{WireFingerprint}`,都过的仍 `Refuse{MalformedHello}`;从不配对 |
|
||||
| `DataBind` / 其它 | `Refuse{Authentication}` "data connection names no live session" / `Refuse{MalformedHello}` + 形状细节 |
|
||||
| 自 accept 起 2000 ms 内未发 `PairBind`,或伙伴未到 | 控制与未识别者 `Refuse{Authentication}` 后关;aux 直接关(其 client 在那里只收描述符);W 日志 |
|
||||
| 同时未配对的超过 16 条 | 丢最老的未识别者(没有则最老者),`Refuse{Busy}` |
|
||||
|
||||
- 从不读过 `PairBind` 一个字节(`FirstFrameAssembler`):控制连接后面的 Hello 归会话读,aux 后面是 `SCM_RIGHTS`。
|
||||
- 有状态:A-control、B-control、B-aux、A-aux 时先交出 B,A-control 留给下一次 `Accept`。`Accept(timeoutMs)` 超时返回 `TIMEOUT`,未配对的留着。
|
||||
- 调用点:`UnixInProcessSupervisor`(成员)与 `mobilegl_server_main` 的 unix 分支。`--serve` 的会话子进程 `CloseInForkedChild`(只 `close`,不 `shutdown`);单会话形状配对后即析构,其余等待者得 `Refuse{Busy}`。`RefuseBusy` 不变(Hello 仍在套接字里)。
|
||||
|
||||
### 版本互通
|
||||
|
||||
| | 修订 3 server | 修订 4 server |
|
||||
|---|---|---|
|
||||
| 修订 3 client | — | `PairAcceptor` 代答 `Refuse{WireFingerprint}`,从不配对 |
|
||||
| 修订 4 client | 旧 server 按顺序配对后首帧是 `PairBind` → `Refuse{MalformedHello}` "first control frame is not a verifiable Hello";旧会话退出若抢在 client 发 Hello 之前,client 只见发送失败 | 正常 |
|
||||
|
||||
### 门
|
||||
|
||||
- 单元 `Endpoints/PairAcceptorTest.*/{unix,tcp}`(8 例):(1) 探测后紧跟 client;(2) 四条交错;(3) 孤儿 aux 过预算被关、下一个 client 成对;(4) Hello 先到的旧 client 具名拒绝。每例用 `ServesExactly` 证身份(控制双向各一帧 + 该 client 的 aux 字节是 server aux 上的头几个字节)。
|
||||
- 端到端 `ServerSpawnTest.AStrayConnectionAheadOfTheClientDoesNotTakeItsSession`:单会话 unix server,探测之后真 client 握手成功、server 退出 0。
|
||||
- red-once:`PairAcceptor::Accept` 换回到达顺序配对(仍读掉首帧,只差配对规则)→ 单元 8/8 红((1) 控制连接是已关的探测;(2) aux 读到的不是本 client 的字节),端到端握手 rc=6(与设备上 `no Welcome … (rc=6)` 同);对照 `StartsAServerProcessAndHandshakesAcrossIt` 仍绿。拷回原文件、`cmp` 字节相同后复绿。
|
||||
|
||||
### 不变量
|
||||
|
||||
- G1:新代码全在 `MG_Remote`(`MOBILEGL_BUILD_DISAGGREGATED` 下)。
|
||||
- 未改:`TcpSupervisor`、预认证门、`DataBind`、会话读 Hello 的路径。
|
||||
|
||||
## B1:同机外部 client 经 app_process helper 与令牌 broker 走共享内存
|
||||
|
||||
依据 [`PLAN-P11.md`](../notes/p11/PLAN-P11.md) §2 B、[`B0-CROSS-APP.md`](../notes/p11/B0-CROSS-APP.md)、[`HSPIKE.md`](../notes/p11/HSPIKE.md)(ID-P11-13)。消费者:同一台手机上从另一个 app(Termux、adb shell)启动、没有 Java `Context` 的原生 GL 程序。证据根 `~/w7/notes/p11/evidence/b1/`(下文的 `evidence/b1/…`、`m-probe*`、`freeze-*` 都在它下面),报告 `~/w7/notes/p11/b1-report.md`。
|
||||
|
||||
### 链路
|
||||
|
||||
| 步 | 谁 | 做什么 |
|
||||
|---|---|---|
|
||||
| 1 | 用户 | `MOBILEGL_IPC_TOKEN=<令牌> CLASSPATH=<server APK> app_process / top.mobilegl.plugin.ExternalClientHelper [--server <包名>] [--timeout-ms <n>] [--keep-affinity] -- <程序> [参数…]`(server 界面给出可复制的整行:APK 路径取 `getApplicationInfo().sourceDir`,不靠 `pm path`——包可见性会过滤 app uid 的查询) |
|
||||
| 2 | helper(`app_process`,client 自己的 uid) | 令牌取自 `MOBILEGL_IPC_TOKEN`,否则取 `MOBILEGL_IPC_TOKEN_FILE` 指的文件;**从不取 argv**(`--token` 具名拒绝)。`--server` 缺省为 `BuildConfig.APPLICATION_ID`(加载它的那个 APK 的包名) |
|
||||
| 3 | helper | 经隐藏的 `IActivityManager.broadcastIntentWithFeature`(null caller,参数按类型填,末位 int = user id)发显式广播:组件 `<包名>/top.mobilegl.plugin.ExternalClientBroker`,action `top.mobilegl.plugin.EXTERNAL_CLIENT_CONNECT`,`FLAG_RECEIVER_FOREGROUND \| FLAG_INCLUDE_STOPPED_PACKAGES`,extra `mobilegl_broker` = Bundle{`callback`: helper 自建的 Binder,`token`,`version`=2} |
|
||||
| 4 | broker(导出的 receiver,两个口味都有,跑在 `:mglsrv`) | 按下表逐条检查;通过则连两次 server 的 unix 端点(`@name` 或路径;相对路径按 `filesDir` 解析)。server 只听 `tcp://` 时,`MobileGLServerService` 在旁边多起一个 supervisor,听私有抽象名 `@<包名>.broker.<8 位 hex>`(同一二进制、同一环境与令牌),broker 连它。连上后**立即写 PairBind 对**(ID-P11-12:谁开连接谁出示;新 nonce,控制 `aux=false`、辅助 `aux=true`;字节见下「交接时配对」) |
|
||||
| 5 | broker → helper | **每个请求都回话**,单向 `transact`:`FIRST_CALL_TRANSACTION` = int version(2)+ 控制 PFD + 辅助 PFD + long applyCore(在跑的 supervisor 启动时宣布的保留核,0 = 无)+ int flags(bit 0 = 已在交接时配对);`FIRST_CALL_TRANSACTION+1` = String 拒绝名 + String 说明 |
|
||||
| 6 | helper | 两个 fd 清 `FD_CLOEXEC`,`execve` 程序(applyCore ≠ 0 且没给 `--keep-affinity` 时经 `/system/bin/taskset <在线 & ~applyCore>`);环境 = 原环境 − `CLASSPATH` + `MOBILEGL_TRANSPORT=spawn`、`MOBILEGL_IPC_CONTROL=fd:<控制>,<辅助>`、`MOBILEGL_IPC_DATA=shm`、`MOBILEGL_IPC_TOKEN=<令牌>`,flags bit 0 时再加 `MOBILEGL_IPC_FD_PAIRED=1` |
|
||||
| 7 | client(`fd:` 端点) | 见下;之后与 `unix:` 完全相同:Hello(dial = Connect)、Welcome、7 个 fd 经 `SCM_RIGHTS`、SharedSegments |
|
||||
|
||||
### `fd:<控制>,<辅助>` 端点(`ClientSession::StartSpawned`,`SocketTransport::AdoptConnectedPair`)
|
||||
|
||||
- 文本严格:两个十进制数、一个逗号,别的都不收;`MOBILEGL_IPC_DATA` 只收 `auto` / `shm`。违者 `Refuse{ProtocolMismatch}` 具名、`MOBILEGL_ERR_UNSUPPORTED`。
|
||||
- 两个描述符必须互不相同、在本进程打开、是 `AF_UNIX` + `SOCK_STREAM`、已连接(`getpeername`);否则 `Refuse{ProtocolMismatch}` 说明哪一条,`MOBILEGL_ERR_INVALID_ARGUMENT`,**一个字节不写**。TCP 套接字被拒(没有描述符通道,用 `tcp://`)。
|
||||
- 收养后两个 fd 设 `FD_CLOEXEC`(程序自己的子进程不继承)。
|
||||
- `MOBILEGL_IPC_FD_PAIRED=1`(helper 按 broker 的 flags 设):PairBind 已由 broker 在交接时出示,本端点**不再写**(`SocketTransport.cpp:492`),直接 Hello。没有这个标记时本端点自己出示一对(CSPRNG 16 字节 nonce,控制 `aux=false`、辅助 `aux=true`,然后 Hello——与 `ConnectTo` 同),供裸连接交来的 fd(测试、旧 broker)。
|
||||
- 自己出示时发送失败(server 已拒绝并关掉这一对):两个 fd 留给调用方,client 读控制连接上待读的拒绝帧并具名报出(如 `peer Refuse{Authentication} no PairBind within the pairing budget`),不只是一个发送错误。
|
||||
- 一对 fd 只够一个会话:会话结束(或同一进程再起会话)时 fd 已关,第二次收养具名失败。
|
||||
|
||||
### 交接时配对与等 Hello 的时限(ID-P11-12)
|
||||
|
||||
- broker 连上两条连接后立即写 PairBind 对(`PairBindFrames.java`:帧头 magic `0x464C474D` + 长度,小端;负载是原生 `EncodePairBind` 的输出,16 字节 nonce 在最后)。两个前缀是编码器的字节;`ServerSpawnTest.TheBrokersPairBindTemplatesAreTheEncoders` 从 Java 源文件读出前缀、对 32 个随机 nonce 与 `AppendFrame(EncodePairBind(…))` 逐字节比较,`protocol.fbs` 一改就红。`PairAcceptor` 的 2000 ms 配对预算(`PairAcceptor.h:63`)因此在 broker 手里满足,不再等程序跑到第一个 EGL 调用。
|
||||
- **配对之后等 Hello:没有时限,靠 EOF。** 之前是 10 s(`RunSession` 的 `helloWaitMs`,`ServerMain.cpp:385` 的读);现在 unix 端点的 `--serve` 会话子进程与单会话形状都用 `kPairedUnixHelloWaitMs = kWaitForever`(`ServerMain.cpp:136`,调用点 `:1537`、`:1562`)。理由:配对时身份已定(nonce),unix 端点只有本用户 / 本 app 能连(Android 上 SELinux,路径上 0600;开发机 Linux 上一个 `@abstract` 名同一 network namespace 内谁都能连——这是产品设备上的论证),而一个先加载资源的程序要多久没有上界;程序退出或关掉描述符 → EOF → `control peer closed before sending a first frame`,会话子进程干净退出。代价:程序活着但迟迟不 Hello 时,它占着唯一的会话槽(与一个正在渲染的会话相同):别的 client 在配对后 2 s 内说 Hello 时得 `Refuse{Busy}`;更慢的那个只见到 Hello 发送失败——`RefuseBusy` 写完 Busy 就关了连接,而 Hello 发送失败的路径不读待读的拒绝帧(`AdoptConnectedPair` 那条路径会读);未修,见 B1 报告附录。TCP 保留预认证时限(`PreAuthKnobs`);进程内显示 server(`:mglwin`,`ServerMain.cpp:1395`)保留 10 s(它停止时要等会话线程)。
|
||||
|
||||
### 令牌
|
||||
|
||||
- broker 比对的是**正在运行的 server 自己的** `MOBILEGL_IPC_TOKEN`(启动 server 时界面上的 Auth token),常量时间比较。
|
||||
- server 的令牌不足 16 字节(含空)→ `server-no-token`:不接同机外部 client。unix 端点对同 app client 仍然不要令牌(行为不变)。
|
||||
- helper 把令牌放进被 exec 程序的环境,Hello 也带它:server 配了令牌时 `AuthenticatePeerToken` 在 unix 会话上照样核对(`Refuse{Authentication}`)。
|
||||
- 令牌只经环境、令牌文件、Binder 事务传;不进任何进程的 argv。
|
||||
|
||||
### 拒绝
|
||||
|
||||
| 名 | 条件 | helper 退出码 |
|
||||
|---|---|---|
|
||||
| `bad-request` | 没有 `mobilegl_broker` Bundle / callback(只能记日志)或 version ≠ 2 | 64 |
|
||||
| `token-missing` | 未出示令牌 | 65 |
|
||||
| `token-wrong` | 令牌不符;不连、不给 fd | 66 |
|
||||
| `server-no-token` | server 令牌 < 16 字节 | 67 |
|
||||
| `server-not-running` | `:mglsrv` 里没有在跑的 server(广播可以把 `:mglsrv` 进程拉起来——server app 被强停后也一样,设备实测——但 Android 12+ 不许后台 receiver 起前台服务,所以答"先在 MobileGL 的 server 界面启动 server",不挂起)。只认离屏服务;`:mglwin` 显示 server 在跑时也答它 | 68 |
|
||||
| `broker-busy` | 另一个请求正在 broker 里连接,或 server 的监听 3000 ms 内没接受两条连接 | 69 |
|
||||
| `connect-failed` | server 在跑但它的端点拒绝连接,或 PairBind 写不进去 | 70 |
|
||||
| (helper 本地)无应答 | `--timeout-ms`(缺省 10000)内 broker 没回话:包没装,或系统没把停止的 app 拉起来(自启动管控) | 71 |
|
||||
| (helper 本地)广播发不出 | 反射拿不到 `IActivityManager` / 调用抛异常(见"依赖 ROM") | 72 |
|
||||
| (helper 本地)exec 失败 | | 73 |
|
||||
| 用法错 | 无程序、未知选项、`--token` | 2 |
|
||||
|
||||
### apply 线程的亲和 / 自旋(`Server/ApplyThreadPolicy.h`)
|
||||
|
||||
`ServerSession::Accept` 记下 Hello 的 dial mode 与数据面;`ServerLoop` 的 apply 线程按它们解析 `MOBILEGL_IPC_SERVER_AFFINITY=auto`:
|
||||
|
||||
| peer | 数据面 | `auto` |
|
||||
|---|---|---|
|
||||
| in-process(inproc,dial No) | — | 全部大核、配置的自旋(不变) |
|
||||
| forked(同 app fork spawn,dial Fork) | 共享段 | 同上(不变) |
|
||||
| connected(dial Connect:`tcp://` / `unix:` / `fd:`) | stream | 同上(不变:io 线程喂 apply 线程,B0 实测 apply 7.9 ms/帧 vs shm 26.8) |
|
||||
| connected | 共享段 | 一个核,`ReservedApplyCore(big, prime, online)`(`ApplyThreadPolicy.h:120`):≥ 2 个 prime 核(峰值主频)→ 最低编号的 prime 核;只有 1 个 prime 核 → 它旁边最低编号的其他大核;只有 1 个 prime 核且无其他大核 → 不设亲和;无非对称(全是大核或读不到拓扑)→ 不设亲和。**从不把唯一的 prime 核给自旋的 apply 线程**。自旋 `kConnectedPeerSpinUs` = 50 µs(`MOBILEGL_IPC_SPIN_US` 显式设了则用它) |
|
||||
|
||||
- **一个来源**:保留核只由原生规则算。supervisor 启动时(`ServerMain.cpp:1485`)按本机拓扑与 `MOBILEGL_IPC_SERVER_AFFINITY`(显式掩码 / `off` → 0)算出 `ReservedApplyCoreForThisProcess`(`ServerLoop.cpp:210`),在 stdout 打印 `MG_Remote server: apply core reserved for dialled-in shared-segment clients: 0x40 (<规则>)`;`MobileGLServerService` 逐行读 supervisor 输出(`ServerEnvironment.parseReservedApplyCore`),broker 把它放进答复;helper 经 `/system/bin/taskset <在线 & ~applyCore>` exec 程序(亲和跨 exec 继承);`--keep-affinity` 不设。Java 不再重算规则。代价:整个程序少用一个核。
|
||||
- 显式掩码或 `off` 对所有 peer 生效(与以前相同);`MOBILEGL_IPC_SPIN_US` 设了就用设的值。
|
||||
- 启动日志 `mgl-srv-apply started … RESOLVED mask 0x…, spin N us; peer <in-process|forked|connected> on <shared segments|stream>, policy <规则>`,规则名写明是哪一种(最低 prime / 唯一 prime 旁的大核 / 唯一 prime 留给 client 而不设亲和 / 无非对称)。
|
||||
- P12 的进程内显示 server(`:mglwin`)的 client 也是 dial Connect:它们走 stream 时不变,走 unix 共享段时得到本规则。
|
||||
|
||||
选择依据(红米,rd12 稳态 fps,helper 路线,默认落位、定频;`evidence/b1/m-policy.tables.md`):
|
||||
|
||||
| 候选(apply 掩码, 自旋) | a shm Espryt | a tcp Espryt | b shm Espryt | b tcp Espryt | a shm Magma | a tcp Magma |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 旧 auto(0xc0, 50) | 21.5 | — | 62.5 | — | 22.1 | — |
|
||||
| 最高大核(0x80, 50) | 26.7 | — | **6.5** | — | — | — |
|
||||
| 最低大核(0x40, 50)= q1 | 26.5 | 51.1 | **119.9** | 40.3 | 21.9 | 20.3 |
|
||||
| 0x40, 不自旋 | 16.9 | — | 73.6 | — | 22.5 | — |
|
||||
| 不设亲和, 50 | 21.7 | — | 64.9 | — | 23.3 | — |
|
||||
| 不设亲和, 不自旋 | 20.2 | — | 12.8 | — | 20.7 | — |
|
||||
| 非大核(0x3f, 50)= q3 | 49.0 | 27.0 | 50.3 | 29.2 | 18.4 | 13.9 |
|
||||
|
||||
- 前台 app 的 GL 线程被调度器放在**最高**大核(cpu7,44 次采样中 43 次):把 apply 线程钉在 cpu7 就与它同核(6.5 fps);钉在 cpu6 得到 B0 手工定核的布局(120)。
|
||||
- 不自旋(0)在每一格都输:保留 50 µs。
|
||||
- adb shell 的 client 被放在 cpu6(与 apply 线程同核):q1 下路线 a 的 shm 26.5 < tcp 51.1。q3(apply 放非大核)能让 shm ≥ tcp,是因为它**同样**把 tcp 会话的 apply 线程放到慢核(tcp 51 → 27),会拖慢所有同机 TCP client(FCL loopback、`:mglwin`),所以不用;改为数据面分流(stream 不变)+ helper 让程序避开保留核。
|
||||
|
||||
### 保活
|
||||
|
||||
- server 仍是前台服务(`dataSync`,带 partial wake lock);broker 从不启动它。前台服务本身挡不住 HyperOS 的冻结(greeze / SmartPower,B0 F1)。
|
||||
- **用户必须设**:server app 与 client 所在 app 的"省电策略"都选"**无限制**"(HyperOS:设置 → 应用 → 应用管理 → <app> → 省电策略 → 无限制;powerkeeper 记为 `bgControl=noRestrict`,并进 deviceidle 白名单),再在 server 界面设 Auth token(≥ 16 字节)、以离屏模式启动。
|
||||
- 设了(红米,`evidence/b1/freeze-{a,b}`):路线 a、b 各熄屏 180 s,在飞会话(rd12 + 200 s 保持)**存活**(正常结束,0 个 Fatal),熄屏期间每 45 s 新起的 3 个路线 a 会话都通过(ssim 1.0);两 app 所有进程的 cgroup.freeze 采样 0 / 103 与 0 / 143,无 `do_freezer_trap`;系统日志 `PolicyMaker: uid=… pkg=<server> reason=NO_RESTRICT_APP`。
|
||||
- 不设(缺省"智能限制后台运行")时具名失败的样子:client app 在前、server app 在后,greeze 对 server uid `setUidState allow=false`、停用其 wake lock,路线 b 的会话 `no Welcome from the spawned server within 5000 ms (rc=7)`(`m-probe2-defaultbattery`,屏亮时就发生,非必现);熄屏约 7 s 后冻结整个 server uid,在飞会话 120 s 后 `Fatal{BarrierTimeout}`(B0 F1、HSPIKE §7)。
|
||||
- 已知:这台机上旧包 `top.mobilegl.plugin.trace` 的"省电策略"页不显示选项(原因未查),门改用新装的 `top.mobilegl.plugin.b1.trace`(像用户新装一样)。
|
||||
|
||||
### 依赖 ROM
|
||||
|
||||
- 直接跑的 `app_process` 不是 zygote fork 出来的,不受 app 的隐藏 API 黑名单约束,`IActivityManager.broadcastIntentWithFeature` 才能反射调用(HyperOS / Android 16 上 `hiddenapi` 日志 0 条,shell 与 app uid 都通)。若某 ROM 对它也强制执行,helper 以退出码 72 具名失败;没有替代(`am broadcast` 带不了 Binder)。
|
||||
- HyperOS 的冻结(greeze / SmartPower)不看前台服务:见"保活"。
|
||||
|
||||
### 线上格式
|
||||
|
||||
无变化(控制协议修订 4,PAIR)。`fd:` 只是 client 端点的一种写法;broker 协议是 Android Binder 上的 Bundle / Parcel,不属于 MobileGL 线上格式。
|
||||
|
||||
### 门
|
||||
|
||||
- 单元:`ApplyThreadPolicy.*`(8 例:每个 peer × 数据面的 `auto`、最低大核与非连续大核集、对称 / 单大核 / 读不到拓扑、显式掩码 / `off` / 显式自旋、认不出的文本);`ServerSpawnTest.AnFdPairTheBrokerConnectedBringsASharedSegmentSessionUp`(真 server 进程,两次裸连接交给 `fd:`,会话起、fd 为 CLOEXEC、server 退出 0)、`…AnAdoptedFdPairPresentsOnePairBindPairBeforeAnythingElse`(在 server 侧读:每条连接恰一帧 PairBind,同 nonce,aux 标志正确)、`…AnFdEndpointThatIsNotAConnectedUnixPairIsRefusedByNameAndWritesNothing`(管道、同一 fd 两次、未打开、未连接、八种坏文本、`stream`,都具名拒绝且一字节不写);Java 镜像 `ServerEnvironmentTest`(`reservedApplyCore` 7 条,`test_android_lifecycle.py`)。
|
||||
- red-once:`AdoptConnectedPair` 不写 PairBind → 端到端例红,client 日志 `peer Refuse{MalformedHello} the control connection's first frame is a Hello, not a PairBind (control revision 4 pairs by nonce)`,PairBind 例红(控制连接上无帧);保留核取最高大核(设备否决的规则)→ 3 例红;策略不看数据面 → stream 例红。每次拷回原文件、`cmp` 相同后复绿。
|
||||
- 跟进(交接时配对、唯一 prime 核、单一来源):`ServerSpawnTest.TheBrokersPairBindTemplatesAreTheEncoders`(Java 前缀 = 编码器字节,32 个随机 nonce × 两个角色)、`…APairTheBrokerPresentedAtHandoffWaitsForAHelloThatComesSecondsLater`(broker 的字节在交接时写出,Hello 5 s 后才来,会话起)、`…APairPresentedOnlyWhenTheProgramStartsIsRefusedByNameOnceThePairingBudgetIsGone`(旧流程:3 s 后才出示 → client 日志 `peer Refuse{Authentication} no PairBind within the pairing budget`);`ApplyThreadPolicy.*` 10 例(加唯一 prime 核旁有 / 无其他大核、无非对称、`ReservedApplyCoreForConfig`);Java `PairBindFramesTest`、`ServerEnvironmentTest`(宣布行解析 5 条)。red-once:交接步骤拿掉(今天的 client 侧配对)→ 交接例红,`Refuse{Authentication} no PairBind within the pairing budget`;Hello 时限改回有限的 3 s → 红,`Refuse{Authentication} no authenticated first frame within the pre-auth deadline`;client 不看标记、再出示一对 → 红,`Refuse{MalformedHello} first control frame is not a verifiable Hello`;保留核不看 prime 集 → 唯一 prime 的两例红;Java 前缀改一个字节 → 模板例红。每次拷回、`cmp` 相同后复绿(`~/w7/logs/p11/b1/redonce4.out`)。
|
||||
- 设备门(红米 `2f7cbe2e`,HEAD `3519a21b` 的 APK,两 app "无限制",默认环境:不手工定核、无解冻守护;CPU 定频并回读只为可重复,`evidence/b1/m-gate.summary.md`):16 个 ssim 格全过(rd12 0.99988 / 0.999884,openra 1.0);shm(helper)对 tcp 的稳态 fps 中位数:
|
||||
|
||||
| 路线 | 后端 | rd12 shm / tcp | openra shm / tcp |
|
||||
|---|---|---|---|
|
||||
| a(shell uid) | Espryt | 119.2 / 53.1(2.25×) | 927.6 / 427.5(2.17×) |
|
||||
| a | Magma | 36.8 / 20.2(1.82×) | 281.2 / 278.6(1.01×,8 次) |
|
||||
| b(另一 app,在前) | Espryt | 119.9 / 39.0(3.07×) | 923.4 / 377.4(2.45×) |
|
||||
| b | Magma | 31.5 / 20.2(1.56×) | **285.3 / 286.6(1.00×,8 次,差 0.5%)** |
|
||||
|
||||
15 / 16 格 shm ≥ tcp;Magma openra 路线 b 按合并中位数差 0.5%(逐次配对 shm 赢 6 / 8,区间完全重叠):该负载两个数据面都受 server 端限制,shm 不增益。拒绝(token-wrong a/b、token-missing、server-not-running a/b 与强停后、server-no-token)都按名、退出码对、无会话。同 app spawn+shm / inproc 的 rd12 p50 与 A4 相同(策略对 Forked / InProcess 不变,日志 `mask 0xc0`)。
|
||||
|
||||
- 重定基后的设备复测(`df826f42` 的 APK,两 app "无限制",默认环境;`evidence/b1/m-pass2.summary.md`、`m-openra3.summary.md`):rd12 八格 ssim 全过;shm / tcp 中位数(3 次交错)路线 a Espryt 119.3 / 53.2、Magma 36.4 / 20.3,路线 b Espryt 118.7 / 39.8、Magma 32.5 / 20.4;22 个 shm 运行全部"交接时配对"。Magma openra 在干净设备上重测:路线 a 296.4 / 284.4、路线 b 292.7 / 281.6(都 1.04×)——首轮门那一格的 5 次补测与一个失控的采样子进程(root `tr`,占满一个核约 65 分钟)同时跑过,不作数。慢启动:helper exec 一个先睡 10 s 的包装再 exec retrace,broker 交接后 10.2 s 才 Hello,两后端会话都起、ssim 过。plugin 口味(`.b1p`,界面上 Generate token + Start server):helper 路线 openra ssim 1.0、rd12 0.99988,HyperOS 省电策略页显示四个选项。
|
||||
|
||||
### 不变量
|
||||
|
||||
- G1:原生改动全在 `MG_Remote`(`MOBILEGL_BUILD_DISAGGREGATED` 下);pull 构建符号增 0 减 0。
|
||||
- 线上格式与控制协议修订号不变(4)。
|
||||
|
||||
## B2:T0——client 的 AHardwareBuffer 作 persistent map 的存储
|
||||
|
||||
依据 [`PLAN-P11.md`](../notes/p11/PLAN-P11.md) §2 B、`design/07`。证据根 `~/w7/notes/p11/evidence/b2/`,报告 `~/w7/notes/p11/b2-report.md`。T0 只换**存储**:采纳门(≥ 16 MiB 的 NULL 定义、persistent 写映射等,与 monolith 同一组门)问到的那个 store 由 client 分配的 AHB 承载,server 导入它;GPU 有序的写(`glBufferSubData` 进已采纳 store = resident subdata)仍走 SEG_STAGE——**T0 不减少 MC 26.3 每帧的 stage 字节**:红米实测 SEG_STAGE 1071 → 1052 KiB/帧(只少了 T2 的 persistent-map 推送 19.4 KiB/帧),省的是 client shadow 与 server 暂存副本(内存)。monolith 不变。
|
||||
|
||||
### 档位(取代 §1 表里 SharedSegments 的 client 行)
|
||||
|
||||
| 数据面 | `MOBILEGL_IPC_ADOPT_TIER` | client | server |
|
||||
|---|---|---|---|
|
||||
| SharedSegments | **未设(缺省,2026-09-29 起)** | Hello 问 `0x80`(`kAdoptAskT0Default`:T0,回退安静)。有 `kCapAdoptT0` → T0(I 行同 0);没有 → **只 MGLOG_D**,本会话 T2,计入 `T0Fallbacks` | 授予同 0(`T0 granted` I 行);不授予 → **只 MGLOG_D**(POST 结果行同样降为 D),计入 `T0Refusals`;会话总计行降为 D |
|
||||
| Stream | 未设 | Hello 问 2;**只 MGLOG_D**(不是 A1 的 W 行),T2,计入 `T0Fallbacks` | server 自己的旋钮未设时同样只 D |
|
||||
| 任意 | 2 | 今天的 T2,逐字节不变;Hello 的 `adoptTier` = 2 | 不变 |
|
||||
| Stream | 0 / 1 | A1 不变:一行 W `Refuse{AdoptTierOnStream, "T<n>"}`,T2 | A1 不变 |
|
||||
| SharedSegments | 1 | 一行 W `Refuse{AdoptTierClosed, "T1"}`,问 2,T2 | — |
|
||||
| SharedSegments | 0 | Hello 问 T0。第一次 `map_persistent` 时档 = min(旋钮, caps, 数据面):有 `kCapAdoptT0` → T0(I 行);没有 → 一行 W `Refuse{AdoptT0Unavailable, "no kCapAdoptT0"}`,本会话 T2 | 第一次原生 bind 时定(`ServerSession::SettleAdoptT0AtBind`,`ServerSession.cpp:1076`):允许开关 → 平台有 AHB → 后端登记了导入 op → POST(进程内缓存)。授予:I `T0 granted`,post-bind 的 caps 快照带 `kCapAdoptT0`;否则一行 W `Refuse{AdoptT0Unavailable, "disallowed" \| "no AHardwareBuffer" \| "POST"}`,不发布 |
|
||||
| 任意 | 解析不了的值 | 一行 W(Config 忽略该值),按**未设**处理 | 同 |
|
||||
|
||||
- **缺省是 T0(用户 2026-09-29 决定)**:旋钮未设 = 能用就用 T0,不能用就安静回退 T2——stream、没有 AHB、POST 失败、server 不允许这四种都只打 MGLOG_D、没有 W / I 行,但照样计数(client `T0Fallbacks`、server `T0Refusals`,以及两侧的会话总计)。否则每个 tcp 会话和每个主机会话都会多一行回退。**显式 `=0` 与未设的区别只在日志**:`=0` 保留全部具名行(A1 的 `Refuse{AdoptTierOnStream}`、`Refuse{AdoptT0Unavailable, …}`),线上问 `0`;未设问 `0x80`,这样 server 才知道要安静(拒绝的原因只有 server 知道)。`=2` 仍是 T2,`MOBILEGL_IPC_ALLOW_ADOPT_T0` 不变。Config 行记为 `adopt-tier=unset(T0)`。
|
||||
- **修订 5 的 schema 补丁**:`protocol.fbs` 里 `adoptTier` 的注释加上 `0x80`,修订 5 按 `protocol_revision_pin.py --write --force` 重钉(`4632750f…`)。依据是脚本自己写的「修订号提交本身的罕见补丁」这一例外:修订 5 只在这条未落地的分支里出现过,没有外部对端。e44d6ac7 之前的修订 5 server 收到 `0x80` 时按名 `Refuse{LinkTerms}`,不会静默误读。生成的头文件不变。
|
||||
- **行为变化(A1 → B2)**:SharedSegments + 0 / 1 不再在握手期 `Fatal{UnimplementedAdoptTier}`。A1 的两个死亡用例改写为 `RemoteClientAdoptT0.*` 的具名回退用例。从不 Fatal:T0 不可用 → T2 + 具名行。
|
||||
- server 允许开关 `MOBILEGL_IPC_ALLOW_ADOPT_T0`(缺省 1;0 = 从不授予),进 `ServerSpawn` 的 server 自有名单(环境剔除后仍在),Config 行 `allow-t0=`。
|
||||
- 门(缺省 T0):`RemoteClientAdoptT0.AnUnsetKnobFallsBackToT2QuietlyAndCountsIt`(主机无 AHB 的共享段)、`AdoptTierStream.AnUnsetKnobRunsT2OnAStreamQuietlyAndCountsTheFallback`(真 tcp);未设 → T2,没有 W / I 回退行,计数为 1。red-once:把未设路径的三处 D 改成 W → 两例都红。`KnobTwoIsTodaysT2WithNoT0Line` 显式设 2。`SessionHandshakeTest` 的问值表加上 `0x80`(合法且安静)和 `0x81`(`Refuse{LinkTerms}`)。
|
||||
- 单个 store 的拒绝(server 已授予,这一条没导入)→ 该 store DECLINED = T2,client W 行,会话继续:Offer 未到(E `Refuse{AdoptT0NoStore, "record N"}`)、client 分配失败(Offer flags 0)、hop 读不出、后端导入失败;client 建不了 hop → W `Refuse{AdoptT0NoStore, "hop"}`、不发记录。
|
||||
- refusal 词 4 → 7(`AdoptTierClosed`、`AdoptT0Unavailable`、`AdoptT0NoStore` 进 `LOCAL_REFUSAL_WORDS`);abort 站点仍 79、family 仍 45。
|
||||
|
||||
### 线上格式(控制协议修订 4 → 5)
|
||||
|
||||
- `LinkTerms` 末尾追加 `adoptTier: ubyte = 2`:Hello 里是 client 的问,Welcome 里 server 回显。server 收到既非 2、也非(共享段上的)0 的问 → `Refuse{LinkTerms}`;client 见回显 ≠ 问 → `Refuse{LinkTerms}`。
|
||||
- 修订号 5(`mg_protocol_base.h`,`protocol_revision_pins.json` 第 5 行 `f83f4c5b…`);`wireFingerprint` 含修订号,修订 4 的对端得 `Refuse{WireFingerprint}`,从不配对。
|
||||
- `kCapAdoptT0` = `MGPCapBit` 1 << 11(`MGPipeTypes.h`),只在 post-bind 的 `CapsSnapshot.callMask` 里(POST 在第一次原生 bind 之后才能跑;client 在 make-current 返回前采用 post-bind caps)。
|
||||
- `map_persistent` 记录与回复不变:OK = 已导入、client 采纳 AHB 映射;DECLINED = 该 store T2;ERROR → `Fatal{ReplyError}`;OK 而 client 没给 AHB → `Fatal{UnexpectedMapAccept}`。
|
||||
- **Offer**(aux 套接字,`SCM_RIGHTS`,`Transport/AdoptT0.h:36`):32 字节旁带 {magic `'MGT0'` = `0x3054474D`,version 1,seq = 该 `map_persistent` 记录序号,size,flags bit 0 = HasBuffer} + 一个 fd = **hop**:client 建的 socketpair 一端,client 已在另一端 `AHardwareBuffer_sendHandleToUnixSocket`;server `recvHandleFromUnixSocket`(校验 BLOB 宽 = size)。hop 的理由:AHB 自带的 fd 集不定长,aux 上一条消息只带一个 fd。
|
||||
|
||||
### 顺序、线程、为什么不死锁
|
||||
|
||||
- client(app 的 GL 线程,`ClientSession::AdoptPersistentT0`,`ClientSession.cpp:2739`):分配 AHB(BLOB,CPU_READ/WRITE_OFTEN \| GPU_DATA_BUFFER)、lock(整个 store 生命期持有)、用 shadow 播种、建 hop → 编码 `map_persistent` → pre-publish 钩子在 `EncodeRecord` 与 `PublishAndNotify` 之间把 Offer 排上 aux → 发布 → 阻塞等回复。
|
||||
- server:**apply 线程**(`mgl-srv-apply`)在应用该记录时取 Offer:codec 的 `MapPersistent` 分支先问 `WireVerbSink::OnMapPersistent`(`PipeWireCodec.cpp:1834`)→ `ServerSession::AdoptStoreT0`(`:1118`)→ `AdoptInbox::Take(seq, 2000 ms)`。
|
||||
- 不死锁:(1) fd 在记录可见之前已进内核队列;(2) 握手后只有 apply 线程读 server 的 aux;(3) 每会话至多一条 T0 `map_persistent` 在飞(client 阻塞等回复),aux 队列里至多一个有效 Offer,排不满;(4) 等待有界(`kOfferWaitMs` = 2000)→ 具名 DECLINED,不挂。晚到 / 乱序:按 seq 暂存(`AdoptInbox`);seq 小于当前的旧 Offer 关闭并计数;畸形的跳过并计数。
|
||||
|
||||
### POST(`kCapAdoptT0` 的前提)
|
||||
|
||||
持续持锁形状:分配 64 KiB AHB,**整个测试期间 CPU lock 不放**,导入,GPU 读 CPU 写的图案、GPU 写一段,经持有的指针读回比较。**两轮**:
|
||||
- 第一轮读的是导入**之前**写进去的图案。导入会顺手清 CPU 缓存,所以只有这一轮的 POST 在「导入之后经持有指针的写 GPU 永远看不见」的设备上照样通过——而 T0 的真实流量全是导入之后的写。
|
||||
- 第二轮(steady,2026-09-30 补):导入且 GPU 用过之后,CPU 才经持有的指针写 region 1、把 region 4(第一轮的 GPU 写)读进缓存;再一次 GPU 工作把 region 1 拷出、改写 region 4,两个方向都比对。两轮都全对才 PASSED。
|
||||
- 实测:Mali-G1-Ultra(天玑,`3B159D009VZ00000`)第一轮全对、第二轮 Magma 读到 0/1024 词、写回 16/1024(import 内存 non-coherent),Espryt 48/1024、160/1024 → FAILED,T0 回退 T2。改之前该机 T0 被授予,`create-indirect-in-world-align1024` × Magma 的 inproc / spawn 丢掉全部 Flywheel 实例物体(0.867 / 0.848),同 trace 的 T2 0.99998;改之后两臂 0.999984。红米(Adreno 830)两后端两轮全对(Magma coherent),T0 照旧授予。
|
||||
- Espryt(`Ops_H_SelfTestExternal`,第二轮 `SelfTestExternalSteady`):一次性 ES 3.1 pbuffer 上下文(不碰 Espryt 的绑定缓存),`glBufferStorageExternalEXT` + 持久 coherent 映射,映射读一遍,compute shader 把 region 0 拷到输出、在 word 4096 起写 `0x6C0FFEE0 ^ i`;第二轮另一个 compute program 拷 word 1024 起、在 word 4096 起写 `0x3D5EA11E ^ i`。
|
||||
- Magma(`VkBufferManager::SelfTestWireImport`,`VkBufferManager.cpp:768`):renderer 的设备,`vkCmdCopyBuffer` region 0 → staging、`vkCmdFillBuffer` region 4,栅栏等 2 s;第二轮同一命令池重录:region 1 → staging、region 4 填 `0x3D5EA11E`。
|
||||
- 结果一行 I `T0 POST (<后端>, sustained-lock pattern) PASSED|FAILED - …`,两轮的计数都在这一行。红米(Adreno 830)两后端 PASSED;Mali-G1-Ultra 两后端 FAILED(缺省旋钮下这行降为 D,见上表)。
|
||||
|
||||
### server 端存储
|
||||
|
||||
- Espryt:`Ops_H_ImportExternal` → `ImportAhbAsBuffer`(`eglGetNativeClientBufferANDROID` + `glBufferStorageExternalEXT`,flags READ \| WRITE \| PERSISTENT \| COHERENT \| DYNAMIC_STORAGE,持久 coherent 映射);**先导入、再退役旧存储**(导入失败则 T2 存储原样);孪生体 = adopted 形状(`InstallT0Store`:immutable、persistentMapped、无 pendingRanges、无 hostBytes → 不进 buffer pool、不走 R-11 暂存)。上下文丢失后下一次 ensure 在新上下文重导入。
|
||||
- Magma:`VkBufferManager::ImportWireBuffer`(`:714`)经 `VK_ANDROID_external_memory_android_hardware_buffer`(Android 线路设备上有就启用,连同 `VK_EXT_queue_family_foreign`):要求 host-visible,优先 coherent,dedicated 导入;`VkBufferObject` 外部模式;`stagedCoverage` = 整个 store(`:742`)。
|
||||
|
||||
暂存副本(R-11 的 server shadow)读者审计——T0 孪生体没有暂存副本:
|
||||
|
||||
| 读者 | T0 下 |
|
||||
|---|---|
|
||||
| CPU 回读 `Ops_H_Readback`(Espryt `Managers.cpp:2540`) | persistentMapped 分支:`DrainResidentWritesNow` + `glFinish`,不回字节 |
|
||||
| CPU 回读 `ReadbackWireBuffer`(Magma `:560`) | imported:只 `WaitForWireBufferHostAccess`,不回字节 |
|
||||
| P9 W1 pack-buffer 落地(`PipeApplier.cpp:648` → `Ops_H_SubData`) | imported 分支排进 `pendingResidentWrites`(`Managers.cpp:2378`,GPU 有序);B2 前 adopted 的提前返回会丢这批像素 |
|
||||
| GPU 写跟踪 | client 自有(`m_gpuWritePending`);XFB 捕获回读跳过 persistentMapped(`DirectGLES.cpp:1806`) |
|
||||
| R-11 排水(`FlushPendingRangesFrom`、`MGL_SERVER_STAGED_REQUIRE_PENDING`) | 不可达:T0 孪生体无 pendingRanges |
|
||||
| handle 臂的 CPU 读者(间接命令、primitive restart 替换、multi-draw 索引重定基:`DirectGLES.cpp:486`、`:9224`、`:9541-9542`、`:9695-9696`、`MultiDraw.cpp:202`) | `SplitHostBytes`(`Managers.h:1121`)→ 导入的 coherent 映射,同 monolith 读 adopted 映射 |
|
||||
| Magma `WriteWireBuffer`(`:329`) | 不变:store 忙 → staged copy(GPU 有序),闲 → host 写进导入内存 |
|
||||
|
||||
### 生命周期
|
||||
|
||||
- client:从采纳到 respecify / destroy(`WireTables.cpp:214`、`:907` → `ReleaseT0Store`)或会话结束(`ResetT0`)持有 AHB 与 lock;之后 unlock + release。
|
||||
- server 在自己的栅栏之后放手,不依赖 link watermark,不需要 `completedFrameSerial` 的生产者:
|
||||
- Espryt:导入时 `AcquireImported`;`glDeleteBuffers` 之后 `RetireT0Import` 插栅栏,栅栏 signal 后才 release(`SweepT0Retired`:每次导入、ensure、以及两个同步点在延迟释放排水之后的 `ProcessDeferredT0Retires`);无上下文时的 destroy 进延迟列表并留第二个引用,排水删 id 后再退役;上下文丢失 → 全部释放。`ProcessDeferredBufferReleases` 逐字不动(G5)。
|
||||
- Magma:respecify / destroy 一律 `DeferWireRelease(T0ReleaseSerial(lastUse))`,按 serial 回收、从不"立即";外部 `VkBufferObject::Destroy` 释放 buffer / memory 后 `ReleaseImported`。
|
||||
|
||||
### CPU 读
|
||||
|
||||
client `BufferObject::SyncGpuWrites` 的 T0 分支:发 `ResourceReadback` 往返("完成、无字节"),清标记,读持有的映射;server 先落地排队的 resident 写、等 GPU(Espryt `glFinish`,Magma host-access 等待)再答。
|
||||
|
||||
### 计数与日志
|
||||
|
||||
client 与 server 每会话:导入 store 数、字节、拒绝数(I `T0 store {…} adopted - N bytes` / `T0 imported store {…} - N bytes (record R); session total S stores / B bytes`;会话结束 I `T0 session totals - …`)。
|
||||
|
||||
### 门
|
||||
|
||||
主机(`~/w7/notes/p11/evidence/b2/redonce-host*`;每次拷回原文件、`cmp` 相同、touch 后复绿):
|
||||
|
||||
| 门 | 用例 | red-once(变异 → 红) |
|
||||
|---|---|---|
|
||||
| 旋钮 2 = 今天的 T2 | `RemoteClientAdoptT0.KnobTwoIsTodaysT2WithNoT0Line`;全门禁缺省旋钮全绿 | Hello 不看旋钮一律问 0 → 红(client `Refuse{LinkTerms}` 回显 ≠ 问) |
|
||||
| 旋钮 0、主机无 AHB → T2 + 具名行 | `RemoteClientAdoptT0.{KnobZero…,NoAhardwareBuffer…}`、`DirectGLES.{Split,Spawn}.AdoptTier0.*`、`DirectVulkan.{Split,Spawn,Tcp}.AdoptTier0.*`(`ASessionThatCannotRunT0FallsBackToT2OnceByName` 读两行) | 恢复 A1 的握手 Fatal → 单元 2 例 + 集成例红(`e869d18c` 起 Espryt split / spawn 各条目的 ENVIRONMENT 自带 `MOBILEGL_ITEST_REQUIRE_GPU=1`,预检 abort 是失败而不是被 ctest 算作通过的 SKIP;之前只有门禁的 integration-gpu 两遍能抓到);删 client 的具名行 → 单元 1 例 + 两臂集成例红 |
|
||||
| 允许开关关 → T2 + 具名行 | `…TheServersAllowSwitchOffIsT2AndANamedLine`、`DirectGLES.Spawn.AdoptT0Disallowed.*` | 开关不进 `ServerSpawn` 白名单 → spawn 例红(server 说 "no AHardwareBuffer" 不是 "disallowed") |
|
||||
| 修订不符 → 具名拒绝 | `SessionHandshakeTest.ARevisionFourPeerIsRefusedByNameAtRevisionFive`、`protocol_revision_pin.py` | 修订号留 4 → 编译期 `static_assert` 红;去掉它 → 运行期该例红(两指纹相等,Welcome 发出) |
|
||||
| Stream + 0 → A1 行不变 | `{DirectGLES,DirectVulkan}.Tcp.AdoptTier0.*` | stream 上也问 0 → 19 / 21 例在 bring-up abort(握手不成,client 随后 `Fatal{CapsBeforeFirstSnapshot}`) |
|
||||
| 授予 + 存储落地 | `…AGrantedStoreLandsWithTheClientsBytes`(替身平台:memfd 当 AHB) | 不发布 `kCapAdoptT0` → 3 例红 |
|
||||
| Offer 按 seq 配对 | `Channels/AdoptInboxTest.*`(6 例 × inprocess / socket) | 按到达顺序配对 → 4 例红 |
|
||||
| Offer 晚到 / 不到 | `…AnOfferThatArrivesAfterItsRecordStillLands`(晚 300 ms)、`…AnOfferThatNeverArrivesIsANamedDeclineNotAHang`、`AdoptInboxTest.AnOfferThatNeverComesTimesOutWithinItsBound` | 等待不设界 → 3 例红(2 例超时 90 s) |
|
||||
|
||||
设备(红米 `2f7cbe2e`,Adreno 830;Android 集成测试静态 peek 载具 inproc 臂,每个用例一个进程,同主机 ctest;`evidence/b2/itest/`):对照载具旋钮 0 两后端 arena 落 T0(`arena_arm = adopted`,18 次导入),旋钮 2 落 T2(`emulated`),11 例(8 过、3 条车道专属跳过)全绿。
|
||||
|
||||
| 变异(只改一次性源码拷贝) | 载具 / 用例 | Espryt | Magma |
|
||||
|---|---|---|---|
|
||||
| M1 resident 写就地落(client 把 T0 store 的 SubData 直接写进 AHB) | `ADrawQueuedBeforeASubDataKeepsItsOwnBytes` | 红 | 红 |
|
||||
| M2 respecify 时立即释放导入 | `ARespecifyWithADrawQueuedKeepsThatDrawsStore` | **绿**:与"GL 导入自持内存引用、早放我们的 AHB 引用不释放页"一致,未直接验证;扩展不保证这一引用,栅栏后释放保留 | 红(先排队的绘制读到被释放的存储) |
|
||||
| M3 不发布 `kCapAdoptT0` | `TheArenaLandsInTheTierItsLaneDeclares` | 红(emulated) | 红 |
|
||||
| M4 CPU 读不做回读往返 | `ReadbackSeesTheLatestCpuWrite`、`GpuWriteIntoTheArenaIsReadBack` | 红 | 红 |
|
||||
|
||||
brief 点名的 `SubDataAfterAnInFlightDrawReachesTheNextDraw`(M1)与 `RespecifiedVertexArenaKeepsVaoBindings`(M2)在变异下**仍绿**:两者在每次写 / respecify 前都 `glReadPixels`,GPU 已退役,就地写与早释放都看不出来;所以加了上表两个"两次绘制之间不回读"的用例(主机各车道全绿)。
|
||||
|
||||
MC 26.3(同 app,spawn+shm 与 inproc × 两后端 × 旋钮 0 / 2)ssim 全过、旋钮 0 时 8 个 store(441.5 MiB)全部 T0;create-instancing 1 个 16 MiB store T0、ssim 过;B1 外部路线 b(helper)旋钮 0 同样导入。内存与帧时间见报告。
|
||||
|
||||
### 不变量
|
||||
|
||||
- G1:pull 构建符号增 0 减 0,`.text` 不变;原生改动在 `MG_Remote` 与两后端的线路路径(`MOBILEGL_BUILD_DISAGGREGATED` 下);Espryt 的 T0 导入只在 `__ANDROID__`。
|
||||
- G5:`ProcessDeferredBufferReleases` 等 P3a / P4a 名单上的函数逐字不动(T0 退役放在它之后的 `ProcessDeferredT0Retires`)。
|
||||
- 线上格式:控制协议修订 5(`LinkTerms.adoptTier`);Offer 走 aux 描述符通道,不是新帧。
|
||||
|
||||
## B2 追加:删除仍绑定在索引绑定点上的 buffer
|
||||
|
||||
- **缺陷(B2 之前就有,`b9b14c47` 同样复现)**:`BufferState::MarkBufferObjectForDeletion` 解绑索引绑定点,但不动绑定点代次(`NoteBindPointChanged`)。dirty 位 15 / 16 / 17(`Tracker.h`)只看代次,所以窗口不重发,server 一直指着死 buffer 的句柄;槽位以新代次被复用后,下一次遍历窗口的 draw / dispatch 就去要旧代次。
|
||||
- Espryt:`Fatal{ProtocolCorruption, "BackendSlotTable.Generation"}`(`Managers.cpp:3507`),整个会话死掉。
|
||||
- Magma:不 Fatal,句柄解析不到 wire buffer,声明了该点的 draw / dispatch 被具名拒绝后丢掉(`Magma wire decline [ComputeUniformBufferBinding]`)。
|
||||
- 触发面:storage / atomic 在 Espryt 的每次 draw(storage 窗口整窗遍历)和声明该点的程序;uniform 在 compute(整窗遍历)和声明该点的程序,且只在程序没换时才触发,因为 uniform 窗口的 shutter 混入了程序身份;transform feedback 目前没有消费者(位 17 只计算、不发射)。
|
||||
- **修法(`4aec5621`)**:删除时解绑的每个索引目标都调用 `NoteBindPointChanged`;transform-feedback 对象切换(`RestoreBoundTransformFeedbackState`,写绑定点的第四处)也调用。都在 `MOBILEGL_PIPE_PUSH` 下:代次计数器本身只存在于 push 构建。其余写入口(`BindBuffer{Base,Range}_State`、`SetNamedTransformFeedbackBinding`)原本就会动代次。
|
||||
- **Magma 单体(`7784d6bb`,新用例暴露的旧崩溃)**:程序声明的 uniform block 所在点为空时,`ResolveUniformBufferPayload` 解引用空指针。现在改给一个清零的块,与线路臂一致。
|
||||
- **门**:`DeletedBoundBufferScenario` 覆盖 storage / uniform / atomic counter / transform feedback 四个目标。
|
||||
- 注册:单体用 ambient `DirectGLES.` / `DirectVulkan.`;Espryt 用 `Split` / `Spawn` / `Tcp`(`mgl_itest_register_split_arms`);Magma 用 `DirectVulkan.<arm>.DeletedBound.`;另有同进程两例的回归 `…LargeArenaAdoptionScenario.GpuWriteThenTheTierCaseInOneProcess`,两后端 × 三臂。
|
||||
- red-once(去掉修法):前三个目标在 Espryt 三臂和 inproc 都 abort,报上面那条 Fatal;在 Magma 三臂和 inproc 都失败,dispatch 被丢;同进程对在 Espryt 三臂都 abort。transform feedback 在两后端都保持绿(没有消费者);单体在两后端都保持绿(没有窗口)。
|
||||
- red-once(去掉 Magma 空 UBO 分支):`DirectVulkan.DeletedBoundBufferScenario.AUniformBuffer…` 单体 SegFault。
|
||||
- 每次拷回原文件、`cmp` 相同、touch 后复绿。
|
||||
- **G1**:符号增 0 减 0;`.text` a5b933 → a5ba13(+224 字节),全部来自 `UniformManager::ResolveUniformBufferPayload`(Magma 修复:单体属于 pull 构建,这里改的是真实行为,没有加守卫)。`BufferState.cpp` / `Core.cpp` 按 pull 构建自己的参数预处理,新旧逐字相同。
|
||||
@@ -0,0 +1,64 @@
|
||||
# P12:server 自有屏幕窗口
|
||||
|
||||
2026-09-29。依据 `docs/Disaggregated/notes/p12/`([`PLAN-P12.md`](../notes/p12/PLAN-P12.md)、裁定 `INTEGRATOR-DECISIONS-P12.md` ID-P12-1..15、验收 [`CROSSHOST-ACCEPTANCE.md`](../notes/p12/CROSSHOST-ACCEPTANCE.md) / [`FCL-ACCEPTANCE.md`](../notes/p12/FCL-ACCEPTANCE.md))。本文只记实现所需的协议与行为;验证结果以那些文件里的运行证据为准。
|
||||
|
||||
## 范围
|
||||
|
||||
server 应用自己拥有一个窗口,把 client 渲染的帧直接上屏;client 不需要窗口。**离屏(pbuffer)路径不变**,两条路径**每会话只有一条活跃**。不在本阶段:X11 / Win32 / None 窗口种类的白名单(D8)、cached-app freezer 的完整处理、多 context、TLS、把触摸转发给 client。
|
||||
|
||||
## 配置
|
||||
|
||||
- client:`MOBILEGL_IPC_SURFACE=offscreen|server`(默认 `offscreen`)。`server` 让 `eglCreateWindowSurface` 向远端 server 要它自己的窗口;没有远端 server 时忽略并说一次。
|
||||
- server:进程是否拥有显示由宿主决定(`ServerDisplay` 钩子已安装 = 拥有)。fork 出的离屏 supervisor 不拥有。
|
||||
|
||||
## 线上格式(控制面修订 3)
|
||||
|
||||
- `WindowKind.ServerOwned = 7`,只在 `CreateWindowSurface` 上合法;`nativeToken` 恒为 0,**没有任何指针过线**(Rule G / H)。`SetWindowHandle` 不得指名它(`SurfaceRefusal::ServerOwnedOnSetWindowHandle`)。
|
||||
- `SurfaceReply` 追加 `width`、`height`(server 窗口实际尺寸)与 `refusal`。`SurfaceRefusal`:`NoServerDisplay`(server 没有显示)、`NoServerWindow`(有显示但等不到窗口)、`SurfaceModeMismatch`(会话已被另一种表面模式闩定)、`ServerOwnedOnSetWindowHandle`。拒绝是具名的 `ok=false` 应答,**不闩会话**。
|
||||
- 修订号 2 → 3,`wireFingerprint` 随之移动:client 与 server 一起重装。
|
||||
- client 的 `SetWindowHandle` 不再无条件发送(server-owned 模式下不发)。
|
||||
|
||||
## 表面模式
|
||||
|
||||
会话的表面模式在**第一次成功的表面创建**时闩定:ServerOwned 窗口 → 上屏,pbuffer → 离屏;之后另一种具名拒绝(`SurfaceModeMismatch`)。client 自己命名的窗口(X11 / Win32 / None)既不闩也不拒(D8 未关)。
|
||||
|
||||
## 窗口租约与失窗
|
||||
|
||||
- `ServerDisplay` 持有窗口引用与租约;创建表面时 apply 线程按需取租约。窗口没有时等(有界),同时按 `SurfaceProgress` 心跳,client 读作"还在跑"而不是"活着但沉默"。
|
||||
- **失窗的顺序即契约**(`surfaceDestroyed` → `Detach`):apply 线程先让后端放掉窗口(Espryt 销毁 EGL surface / context,Magma 销毁 swapchain / VkSurface),再以 `Fatal{ServerWindowLost}` 闩会话,**最后**才结束租约、让回调返回。`Detach` 有界 3 s,超时不抽走窗口。失窗请求在 `DrainRing` 每次 pop 之前也检查。
|
||||
- 窗口属于显示进程,不属于会话:会话结束后窗口还在,下一个会话接着用;Espryt 的孪生对象与单元影子随会话清空(`DropEveryTwinForEndedServerSession`),下一会话从空表开始。
|
||||
|
||||
## 几何
|
||||
|
||||
- client 请求 `W×H`:`W,H > 0` → server 显示 `SurfaceHolder.setFixedSize(W,H)`,视图**等比缩放、留黑边**放进屏幕;`0/0` → `setSizeFromLayout`(窗口自己的大小)。请求是异步应答,server 等布局回报(ID-P12-9)。
|
||||
- **请求尺寸的来源**:`eglCreateWindowSurface` 的 `EGL_WIDTH/EGL_HEIGHT`;两者缺省(SDL 就是这样)且传入了 native window 时,取 `ANativeWindow_getWidth/Height`(Android、split 构建、server-owned)。缺这一步时游戏按自己的窗口渲染、server 窗口取屏幕尺寸,横屏游戏在竖屏窗口里被裁成左下角。
|
||||
- `ResizeEGLWindowSurface` 走同一条几何请求,后端表面随窗口**真实**尺寸,reply 带回该尺寸,client 采用它(ID-P12-10)。`kEventSurfaceChanged` 携带宽高(Espryt 以前只发格式,client 的默认帧缓冲会停在 512×512 占位值)。
|
||||
|
||||
## 死亡与 device-lost
|
||||
|
||||
- device-lost 只由对端**挂断**闩住,从不由超时闩住(CONTRACT-P6 §5.4 不变)。杀 server 进程后 client 在毫秒级闩住,随后每个 verb 被 DECLINED,`glGetGraphicsResetStatus` 报 `GL_UNKNOWN_CONTEXT_RESET`。
|
||||
- 闩锁的 `DEVICE LOST` 说一次;闩住之后 DECLINED 的 verb **不再逐条打 ERROR**(一个 FCL 游戏几千次/秒)。
|
||||
|
||||
## server 的形态
|
||||
|
||||
- **进程内显示 server**(`MobileGLDisplayActivity`,进程 `:mglwin`):TCP / unix 监听线程 + 会话线程(`TcpSupervisor::Handoff::Thread`),预认证 / Busy / DataBind 路由与 fork 形状是同一份代码;会话串行;后端**按进程钉住**(无 `MOBILEGL_BACKEND_TYPE` 时取第一个会话的);会话间 `ResetSessionLatch`。以崩溃隔离换窗口可达:会话里一个未武装的 `SessionFail` 会带走整个显示进程。
|
||||
- **离屏 supervisor**(`MobileGLServerService`):每会话 fork 一个子进程,子进程 `PR_SET_PDEATHSIG` 随 supervisor 去。
|
||||
- **同一设备一个 server**:显示 Activity 与离屏 service 互相替换(谁后启谁拥有端口)。Render Server 屏(`ServerControlActivity`)是两种形态的入口,字段与 extras:`listen` / `token` / `env`(`KEY=VALUE;KEY`)/ `onscreen` / `start` / `stop`。
|
||||
- 停止推流中的进程内 server 时,会话在 `loop.Stop()` 前放弃队列里剩余记录(`AbandonQueuedRecords`);正常的 client EOF 仍 drain 完。
|
||||
- 转发日志:进程内 server 只有会话线程与选择加入的线程转发,且在释放日志互斥之后、在独立转发互斥下发送。
|
||||
|
||||
## FCL 集成(FCL fork,不在本仓库)
|
||||
|
||||
- FCL 的界面一退后台就把游戏暂停;server 窗口要在屏幕上,FCL 必然在后台。版本设置 **`keepRunningInBackground`**("游戏退到后台时不暂停",默认关):强制 TextureView 并保留它的 SurfaceTexture,界面离开前台时不通知 SDL 暂停、不撤销 GLFW focused / visible。
|
||||
- FCL 默认的 spawn(游戏进程派生 server 子进程)把自己的窗口指针发给 server,按 Rule H 被拒(`UnmigratedSurface`):**只能接外部 render server**,`MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:<port>`、`MOBILEGL_IPC_DATA=stream`、`MOBILEGL_IPC_SURFACE=server`;后端由 `mg_env.txt` 里的 `MOBILEGL_BACKEND_TYPE` 选。
|
||||
- 时序:FCL 的游戏界面出现后要再等约 10 s 才能把 server 窗口切到前台。
|
||||
|
||||
## 不变量
|
||||
|
||||
- G1:pull 构建符号 0/0/0/0,`.text` 不变(全部新代码在 `MOBILEGL_BUILD_DISAGGREGATED` 下;`EGLImpl` 的新分支还要 `__ANDROID__`)。
|
||||
- 离屏路径仍是门(gate 3 跑 `--use-pbuffer`)。`RefuseCode` 只追加。
|
||||
- 会话里 server-owned 表面之外的表面(`SurfaceModeMismatch`)具名拒绝,会话继续。
|
||||
|
||||
## 仍开放
|
||||
|
||||
D8 窗口种类白名单(X11 / Win32 / None 仍被接受并把 token 强转为指针);freezer 的完整处理;多 context;TLS;server 窗口不转发输入;DirectGLES 的 `g_Display/g_Surface/g_Context` 仍是全局(每进程一个会话时不需要);修后的横屏 server 窗口没有重测。
|
||||
@@ -0,0 +1,642 @@
|
||||
# CONTRACT-P5 — the four tables every P5 package is held to
|
||||
|
||||
Authority: this file. `~/w7/notes/p5/BRIEF-P5.md` states the rulings R-1…R-14; this file is
|
||||
where they become implementable, and where the rulings the brief left to the contract package
|
||||
are made. Where the two disagree, this file is newer and this file wins — and §6 lists every
|
||||
place they disagree, so nobody has to diff them.
|
||||
|
||||
**How to change it.** `MG_Remote/CONTRACT-P5.md` is c0's file. A package that needs a row
|
||||
changed goes through the integrator, who edits here first and only then lets the package land.
|
||||
P4a's contract was corrected seven times and each correction cost a package a rework round;
|
||||
the point of this file existing at all is that a correction is a one-line diff here rather than
|
||||
a rediscovery in six places.
|
||||
|
||||
It lives beside the code, not in `~/w7/notes/`, because it describes bytes on a wire and has to
|
||||
move with the branch that defines them.
|
||||
|
||||
Base: `feat/disaggregated @ a29807cc`. Every `file:line` below was read at that commit.
|
||||
|
||||
---
|
||||
|
||||
## §0 The three rules that sit above every table
|
||||
|
||||
These are R-2 and R-11 in their formal wording. They apply to every row of table 1 without
|
||||
restatement, and every reviewer's first three questions (BRIEF §11.1, §11.4) are these three.
|
||||
|
||||
**Rule A — a content record must declare its bytes.** Under
|
||||
`MG_Config::Transport != Monolith`, a record whose payload owns an `MGPBlobRef` and which
|
||||
carries content must set `Seg` to a real segment, `Offset` to a byte offset **within that
|
||||
segment**, and `Size` to a **non-zero** byte count. `Blob.Size == 0` on such a record is
|
||||
`Fatal{ProtocolCorruption}`.
|
||||
|
||||
This inverts today's legal state, and deliberately. `MGPipeTypes.h:398-410` says in so many
|
||||
words that a zero `Blob.Size` means *"this record does not declare its blob"* and *"is not a
|
||||
fault"* — which is right for monolith, where the bytes travel beside the record as a companion
|
||||
pointer. Under split there is no beside.
|
||||
|
||||
**Rule B — no host pointer crosses.** Under split, `MGHostSpan::Ptr` is written `nullptr` by
|
||||
the encoder and any non-null `Ptr` seen by the apply side is `Fatal{ProtocolCorruption}`.
|
||||
Today `Ptr` is the fast path (`MGPipeHostSpan.h:51`), which is exactly why this needs saying.
|
||||
P5's reduced path should produce **zero** host spans at all — see table 0's cap-bit row.
|
||||
|
||||
**Rule C — an applier entry point may not hold a pointer past its return.** A `SEG_STAGE` run
|
||||
is valid from publish until `retiredSeq` passes the record that named it. The tree has exactly
|
||||
one violation and it is named rather than tolerated: Espryt's `GLESBufferResource::hostBytes`
|
||||
(`Managers.h:839`), written by `Ops_H_SubData` (`Managers.cpp:1980-1983`) and `Ops_H_FlushRange`
|
||||
(`:2035`), read by six later drains (`:2000`, `:2062`, `:2080`, `:2111`, `:2741`, `:2843`).
|
||||
Under split those two writes must **copy into server-owned storage**. `MOBILEGL_IPC_AUDIT=1`'s
|
||||
`0xDD` fill over retired staging bytes (R-2.5) is the mechanical control that says whether they
|
||||
did; without it, an `inproc` implementation that kept the pointer is indistinguishable from one
|
||||
that copied.
|
||||
|
||||
---
|
||||
|
||||
## §1 Table 0 — the encoding table
|
||||
|
||||
One row per wire field that is **not a handle**. Handles are `{slot, gen}` and are settled by
|
||||
P2/P3a; everything else that is not a plain scalar is here.
|
||||
|
||||
| field | the ruling | zero means | who reads it | evidence / note |
|
||||
|---|---|---|---|---|
|
||||
| **segment id space** | `SEG_CMD=1, SEG_STAGE=2, SEG_REPLY=3, SEG_EVENT=4, SEG_SHADOW=5, SEG_ADOPT=6`. Identical values to `Protocol::SegmentKind`. | **"no segment", always.** 0 is never a real segment id. | encoder, decoder, `gMGPipeSegmentResolver` | `protocol.fbs:36-44`; `kMGHostSpanSegNone = 0` at `MGPipeHostSpan.h:21`. The two are tied by `static_assert` in `Wire/PipeWireCodec.cpp`, which is the only place the flatbuffers header and the codec enum meet. `kMGHostSpanSegFromServerIndexMirror = 0xFFFFFFFF` (`:26`) stays reserved for P8. |
|
||||
| **`MGPBlobRef{Offset, Size, Seg, Pad0}`** (24 B, `MGPipeTypes.h:55-61`) | `Seg` from the row above. `Offset` is a **byte offset inside that segment**, never a host address. `Size != 0` ⇔ "this record declares its blob", and under split a content record **must** declare it (rule A). | `Size == 0` = "no blob declared" — legal in monolith, `Fatal` under split for a content record. | decoder, every bounds cross-check | Today every emitter writes `{Seg=None, Offset=<host address>, Size=0 or real}`. Five of the eleven blob members do declare a real size today (`CsoCache.h:156`, `VertexInputEmit.h:398`, `ResourceTracker.h:216`, `PipeFill.cpp:2259`); the rest write 0. See table 1's "length" column for which. |
|
||||
| **`MGHostSpan{Ptr, Seg, Pad0, Size, Offset}`** (32 B, `MGPipeHostSpan.h:28-37`) | Split: `Ptr == nullptr` always (rule B). **The 32-byte layout must not be reordered** (`:29-31`). | `Seg == 0` with `Size != 0` is `Fatal`. | decoder | **P5's reduced path must produce none at all.** See the cap-bit row below. |
|
||||
| **`kCapNeedsHostIndexBytes` (1<<7), `kCapNeedsHostUboBytes` (1<<8)** | **Both are 0 for the whole of P5**, by ruling. | — | client emitters | `MGPipeTypes.h:120-123`. This is the cheapest way to keep every `MGHostSpan` out of the first IPC frame: the two bits are the only things that ask for one. `TriangleScenario` must therefore use a **VBO-backed draw and not client-array indices**, or `kDrawHasUserIndices` (`MGPipeTypes.h:1212`) produces a span the split filling for which is P8's. |
|
||||
| **`MGPCaps`** (4 members, `MGPipeTypes.h:126-139`) | **One carrier, not two.** `MGPCaps` is the model; `protocol.fbs`'s `CapsSnapshot` is its transport form. `Dynamic` and `CallMask` cross as POD bytes; `FormatCapabilities` and `RendererInfo` cross as the two blobs whose serializers are P5's new work (`MG_Remote/CapsCodec.h`). | — | `CapsMirror` (client), `ServerSession` (server) | `MGPCaps` has only a **compositional** size assertion (`MGPipeTypes.h:145-146`) because `DynamicBackendParameters` still carries `SizeT` and `GLenum`; P0.5's fixed-width rewrite never happened. **P5 does not rewrite it** — see the ABI row. |
|
||||
| **`CapsSnapshot` redundancy** | `tableSlotMask` (`protocol.fbs:94`) is **DELETED**, not renamed. `maxComputeWorkGroupCount` / `maxComputeWorkGroupSize` (`:92-93`) and `prefersCpuXfbPrimitiveAccounting` (`:95`) are **deleted** too: the first two ride inside `Dynamic` already (`BackendObject.h:392-393`), and the third is answered by `kCapCpuXfbPrimitiveAccounting`. | — | s1 (the schema), c1 (the mirror) | R-8 offered rename-or-delete for `tableSlotMask`; **delete**, for two reasons and the second is decisive. `ARCHITECTURE.md:114` says `CallMask` *replaces* "is this table slot null" as the capability probe, so a field whose comment is "which `GLFunctionsTable` slots the peer registered" re-introduces precisely what it replaced. And `GLFunctionsTable` has **69** function-pointer slots (`BackendObject.h:117-292`), so a `ulong` mask cannot address it and never could — it is five bits short on day one. |
|
||||
| **`MGPCaps::CallMask` layout** | bits **0..8** = `MGPCapBit`, unchanged. bits **9..31** reserved. bits **32..47** = the **consumer mask**: bit `32+n` means "the server has a consumer for MGPipe subsystem bit `n`". bits **48..63** reserved. | a clear consumer bit = "this server does not consume that family; emit nothing for it". | `CapsMirror::ServerConsumes` — the **only** legal client-side source | c0's ruling, and the thing that makes R-8 implementable at all: R-8 says the client's liveness gates must read the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no per-family bit. Constants and the two fold/test helpers are in `MG_Remote/CapsCodec.h`; `CapsCodec.cpp` asserts the block does not collide with `MGPCapBit` and that P4a's `0x1fff` fits sixteen bits. |
|
||||
| **ABI agreement** | `Hello`/`Welcome` assert both peers agree on `sizeof(DynamicBackendParameters)`, `sizeof(MGPCaps)`, `sizeof(GLFunctionsTable)` and `buildFingerprint`. A mismatch is `Fatal{AbiMismatch}` and **never** a downgrade. | — | s1 | The compositional assertion above means the caps block's literal size is ABI-dependent. P6's spawn is same-machine, same-binary and inherits this unchanged. A fixed-width rewrite of `DynamicBackendParameters` is **P7's** account, not P5's. |
|
||||
| **`MGPSubData::Target`** | Packed: **low byte = `MGPipeResourceTarget`, high byte = the cube-face upload target**. Read only through `MGPipeSubDataResourceTargetOf` / `MGPipeSubDataUploadTargetOf`. Whole field `== 0` is the **buffer** half; a low byte naming `Buffer`, `Renderbuffer` or `>= MGPipeResourceTarget::Count` is `Fatal{ProtocolCorruption}`. | whole field 0 = buffer upload | applier | P4a ID-12. Already settled; copied here because a decoder that open-codes it is the class-1 defect. |
|
||||
| **`MGPImageView::Access`** | `0 = GL_READ_ONLY`, `1 = GL_WRITE_ONLY`, `2 = GL_READ_WRITE`. Named by `kMGPipeImageAccess{ReadOnly,WriteOnly,ReadWrite}` and validated by `MGPipeImageAccessIsValid` / `MGPipeDecodeImageAccess`, all in `MG_Pipe/MGPipeValueTypes.h`. Any other value is `Fatal{ProtocolCorruption, "ImageView.Access"}` at the READER. **Not** `MGPImageBind::Access`, which carries the raw GL token (`0x88B8`/`0x88B9`/`0x88BA`) and is a different field. | — | decoder, applier | **This encoding only ever lived in a package header** (P4a R-3); P5e ruling 16 moved it to `MGPipeValueTypes.h`, which is where both roles now read it. P3b/P4b R-3 replaced this row's line-range citation with the ENUMERATOR NAMES: the old pointer (`ImageEmit.h:146-159`) had drifted off the encoding entirely, which is the failure mode this table's own `MGPSubData::Target` row warns about. |
|
||||
| **`MGPSamplerView::Target`** | `MobileGL::TextureTarget` cast to `Uint8` (`SamplerEmit.h`'s `m_lastView.Target = static_cast<Uint8>(texture.GetTarget())`), 11 values. **Not** `SamplerEmit.h`'s `SamplerTarget[]`, which is a DIFFERENT, biased encoding of the same enum (`TextureTarget + 1`, `0` = "this program samples nothing here") living 200 lines away and client-internal. | — | decoder, applier | P3b/P4b R-3: the old line citation had drifted by six lines. **Known gap, stated rather than closed here**: unlike `MGPipeResourceTarget` this encoding has no `Count` bound, no `…IsValid` predicate and no `Fatal{ProtocolCorruption}` for an out-of-range value — the decoders at `DirectGLES.cpp` cast straight back. Giving it the `MGPipeImageAccess` treatment in `MGPipeValueTypes.h` is the remaining half of R-3 and is not this package's file. |
|
||||
| **`MGPFramebufferState::DrawBuffers[8]`** | An index INTO THIS RECORD'S OWN `Color[]` array; `-1` = NONE. The four default-framebuffer tokens map to `0`. Narrowed by `MGPipeDrawBufferIndex` and bounded by `MGPipeDrawBufferIsInsideTheWireWidth` (`MG_Impl/Pipe/FramebufferEmit.h`); the applier re-checks `< -1 \|\| >= kMGPipeMaxColorAttachments` and raises `Fatal{ProtocolCorruption}` (`MG_Pipe/PipeApply.cpp`). `kMGPipeMaxColorAttachments == MobileGL::kMGMaxDrawBuffers` is `static_assert`ed. | `-1` = "no attachment" | decoder, applier | P3b/P4b R-3: the old line citation (`FramebufferEmit.h:146-163`) was off by one and is replaced by the FUNCTION NAMES, which move with the code. |
|
||||
| **`MGPReplySlot::Id`** | **= the record's sequence number** (R-3). No new id space, no allocator. The server writes the answer into `SEG_REPLY[seq % slots]` and **stamps `seq` back into the slot header** so a wrong-slot read is detectable rather than plausible. | seq is 1-based; `0` = "no record / not encoded" | client barrier wait | `ARCHITECTURE.md:124`: the wire carries no per-record seq field, so seq *is* the ordinal. `MGPReplySlot` exists (`MGPipeTypes.h:78-81`) and **no payload of the ten `kReplySlot` calls contains one** — which is exactly why the id must be derived rather than carried. P9 generalises this to "seq is the id's initial value", which extends the rule rather than overturning it. |
|
||||
| **reply slot header** | `{Uint64 Seq; Int32 Status; Uint32 Size;}` — 16 bytes, then the payload. `Status`: **0 = OK, 1 = DECLINED, 2 = ERROR**. | — | client | **`DECLINED` is a real answer, not a failure.** It is how `MapPersistent` says `nullptr` (R-6) and how the four `Bool` acceptance entry points say `false` (R-5). A client that treats DECLINED as an error re-creates ID-39's 66 lost uploads from the other side. |
|
||||
| **which rows own a reply slot** | **14, and `kMGPipeCallFlags` is the single source of truth** (R-16). The ten always-declared answers plus the **four acceptance rows** — `ResourceCreate`, `ResourceRespecify`, `ResourceSubData`, `SetTextureParams`. | a row without `kReplySlot` has no slot and must never be posted to | s1 sizes the pool from it; w1 posts against it | The four are exactly the `MGPipeApply*` entry points returning `Bool` (`PipeApply.h:820`, `:868`, `:897`, `:1023`); `MapPersistent`'s `void*` is the fifth answer and was already declared. They carried no flag because in monolith the answer is a direct call's return value and there was nothing to declare — but this table already said DECLINED is how they answer, so the catalogue and the contract could not both stand. Pinned by `PipeCatalogueTest`: 14 by count **and** the four by name, so a row cannot lose the flag while another gains one. |
|
||||
| **`kReplySlot` vs "never blocks"** | Both hold, and the barrier is why. | — | reviewers | `MGPipe.h:49` says a reply-slot call "never blocks", and R-5 says the four acceptance answers are read synchronously. That reads as a contradiction and is not one: **under the verb barrier the client is already waiting for `appliedSeq >= mySeq` at this verb boundary**, and the reply is read inside that wait. A `kReplySlot` row therefore adds **no** block — it adds a read to a wait that was already happening (R-3). The flag keeps its literal meaning: the CALL does not block; the barrier does, and the barrier is a retiring object. When it retires per family, these four become genuinely asynchronous and the acceptance answer becomes a real latency question — which is P9's account, not P5's. |
|
||||
| **`kRecPad` and seq** | A wrap filler **does not advance seq**, on either side. | — | both | R-9. `RingConsumer::Pop` already skips fillers; the rule is stated because the *counter* is the caller's, not `Pop`'s. A side that counts pads drifts by one per wrap, for ever — and since seq is the reply-slot id, a drifted seq reads another call's answer instead of failing. Pinned by `RingTest.AWrapFillerDoesNotAdvanceTheRecordSequence`. |
|
||||
| **per-opcode flags** | `kMGPipeCallFlags[MGPWireOp::kOpCount]` in `generated/PipeWire.inc`, read only through `MGPipeCallFlagsFor(op)`. Index 0 (`kInvalid`) is `kNone`. | `kNone` = no flags | every package | R-13.4. Before this table existed nothing generated exported the flags, so six packages were each about to hard-code `PipeCalls.def`'s fourth column — which is how `GetCaps` and `CreateSamplerState` came to own an `MGPBlobRef` with no `kHasBlob` on their line. `gen_pipe.py` now also refuses a flag token that is not an `MGPipeCallFlags` enumerator, with two negative controls in `--self-test`. |
|
||||
| **`kHasBlob`'s meaning** | **Exactly "the payload owns an `MGPBlobRef` member"** — nothing weaker. | — | decoder | `PipeApply.h:78-79` already says so. Three calls carry bytes with **no** `MGPBlobRef`; they are table 1 rows 19–21 and are deliberately unflagged, because a decoder that trusts `kHasBlob` has to find a member to read. |
|
||||
|
||||
---
|
||||
|
||||
## §2 Table 1 — the byte carriers
|
||||
|
||||
**23 rows, not 19.** BRIEF §3 lists 19 and `scout-premortem:§3` lists a different 19; the
|
||||
union is 23 and the four that only the premortem lists — `MapPersistent`, `ResourceReadback`,
|
||||
`ReadPixels`, `GetTextureImage` — are precisely the ones whose bytes travel **server → client**.
|
||||
Leaving them out of the byte-carrier table is how a phase discovers in week three that it never
|
||||
decided where readback pixels land. They are rows 20–23 and are marked with their owning phase.
|
||||
|
||||
Columns: **flags** · **blob member** · **companion pointer today** · **which segment the bytes
|
||||
live in** · **who owns that memory** · **when the slot retires** · **who declares the length,
|
||||
who cross-checks it** · **reply name** (for `kReplySlot` rows).
|
||||
|
||||
`apply` = retires when `DecodeAndApply` returns. `submit` = when the server has handed the bytes
|
||||
to the driver. `gpu` = `completedFrameSerial`.
|
||||
|
||||
### Group A — `kHasBlob`, the payload owns an `MGPBlobRef`
|
||||
|
||||
| # | call (op) | flags | blob member | companion today | segment | owner | retires | length declared / cross-checked |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 1 | `CreateRenderState` (17) | `kHasBlob` | `Blob` (`MGPipeTypes.h:364`) | `const void* chunkBytes` (`PipeApply.h:740`), passed `CsoCache.h:157` | `SEG_STAGE` | client stages, server copies on apply | **apply** | declared real (`CsoCache.h:156`, `= kMGPipePipelineChunkBytes`); **nothing reads it today** — `MGPipeApplyCreateRenderState` (`PipeApply.cpp:1280`) never touches `Blob.Size`. Decoder must cross-check against `ChunkMask`. |
|
||||
| 2 | `CreateVertexElements` (20) | `kHasBlob` | `Blob` (`:419`) | `const void* blobBytes` (`PipeApply.h:932`), passed `VertexInputEmit.h:399` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`VertexInputEmit.h:398`); **cross-checked, and this is the model for every other row**: `PipeApply.cpp:1990-1999` recomputes `AttributeCount*sizeof(MGPVertexAttribWire) + BindingPointCount*sizeof(MGPVertexBindingPointWire)` and refuses a disagreement; both counts bounded by `kMGPipeMaxVertexAttribs`. |
|
||||
| 3 | `CreateShaderState` (27) | `kHasBlob` | **seven**: `Spirv[6]` (`:507`) + `Reflection` (`:508`) | **two typed frontend pointers** — `const LinkArtifacts*` + `const SpirvArtifacts*` (`PipeApply.h:1039-1041`), passed `ProgramEmit.h:257` | `SEG_STAGE`, seven independent runs | client stages, server copies | **apply** | all seven declare `Size = 0` today and `Reflection.Offset` is literally `(Uint64)&link` (`ProgramEmit.h:254`). **The serializer already exists and no package may write a second one**: `EncodeProgramArtifacts`/`DecodeProgramArtifacts` (`ProgramArtifactsCodec.{h:53,60,cpp:252,264}`), its own suite, and the verify build already round-trips **every real program it links** (`PinProgramArchiveRoundTrip`, `PipeApply.cpp:1056-1087`, called `:2638`). |
|
||||
| 4 | `SetDynamicState` (30) | `kHasBlob` | `Blob` (`:389`) | `const void* chunkBytes` (`PipeApply.h:748`), passed `PipeFill.cpp:2260` | `SEG_STAGE` | client stages, server copies | **apply** | declared real (`PipeFill.cpp:2259`); **nothing reads it** (`PipeApply.cpp:1363-1370` scatters by `ChunkMask`). Same fix as row 1. |
|
||||
| 5 | `SetGlobalConstants` (40) | `kHasBlob` | `Blob` (`:848`) | `const void* bytes` = `MapUBO()`'s image (`PipeApply.h:1052`), passed `ProgramEmit.h:195` | `SEG_STAGE` | client stages, server copies | **apply** | declares **0** (`ProgramEmit.h:194`); cross-check exists at `PipeApply.cpp:2784` against `Desc.GlobalUboSize` but is **inert while Size is 0**. Under rule A it becomes live. Per program per frame, unbounded length — the row most worth watching against R-10's max-record counter. |
|
||||
| 6 | `SetResidualValueState` (46) | `kHasBlob` | `Blob` (`:932`) | **none, and no record either**: the entry point is `MGPipeApplySetResidualValueState(const ResidualValueBlock&)` (`PipeApply.h:760`), passed `PipeFill.cpp:2184`. **`MGPResidualValueState` is never instantiated on the live path.** | `SEG_STAGE` | client | **apply** | nothing declares it. The encoder must invent **both** the record fill and the blob fill. `sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE == 8` is statically asserted (`MGPipeTypes.h:924-927`) and only ever ratchets **down**. **This is the hardest row in the table** and neither scout flagged it; see §6. |
|
||||
| 7 | `ResourceSubData` (48) | `kHasBlob\|kVarTail` | `Blob` (`:1009`) | `const void* bytes` + `const MGPSubRegion* regions` (`PipeApply.h:897-899`); buffer half `PipeFill.cpp:705`, texture half `TextureEmit.h:1278-1280` | `SEG_STAGE` | client stages, **server must copy** (rule C names this call) | **apply** | **the two halves disagree today**: buffer declares real (`ResourceTracker.h:216`), cross-checked at `PipeApply.cpp:702`; texture declares **0** (`TextureEmit.h:1265-1267`) on the grounds that the byte count is *"the server's to compute once it has picked box-or-rects"* — which cannot be a bounds check. **Under rule A the texture half must declare too.** Tail: `MGPSubRegion[RegionCount]` (`:1007`). |
|
||||
| 8 | `BufferSubDataResident` (49) | `kHasBlob\|kOptional` | `Blob` (`:1009`, same payload) | `const void* bytes` = application staging, *"valid for the duration of the call only"* (`PipeApply.h:90`, `:901`); one caller, `PipeFill.cpp:727` | `SEG_STAGE` | client stages, server copies | **apply** | declared real via `MGPipeBuildSubDataRecord`, cross-checked at `PipeApply.cpp:702`. `kOptional` is a **capability** question under split, not a null-pointer question: the client must gate on `kCapResidentSubData` through the caps mirror, never on a table slot (R-8). |
|
||||
|
||||
### Group B — `kVarTail`, a tail and no blob member
|
||||
|
||||
Every row here declares its tail by a **count**, and `MGP_WIRE_CHECK_BOUNDS` **cannot see the
|
||||
tail at all** — it only proves `size >= sizeof(MGPWireRec_X)`, so a record declaring
|
||||
`Count = 4000` while carrying 8 bytes passes today. The decoder must recompute the total from
|
||||
the declared count(s) and require it to **equal** `MGPWireRecHeader::Size`.
|
||||
|
||||
| # | call (op) | flags | tail element × count | companion today | segment | owner | retires | length |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| 9 | `SetVertexBuffers` (32) | `kVarTail` | `MGPVertexBuffer` × `Count` (`:730`) | `const MGPVertexBuffer*` (`PipeApply.h:941`), `VertexInputEmit.h:245` | `SEG_STAGE` (tail follows the payload in `SEG_CMD` only if it fits the record bound) | emitter-owned `Vector`, **reused next emission** | **apply** | `Count`; no tail cross-check today |
|
||||
| 10 | `SetSamplerViews` (35) | `kVarTail` | `MGPBoundView` × `Count` (`:778`) | `const MGPBoundView*` (`PipeApply.h:1028`), `SamplerEmit.h:786` | as above | as above | **apply** | `Count`; `Start+Count` past the unit bound is already `Fatal` (`PipeApply.h:1024-1026`) — a *slot* bound, not a byte-length check |
|
||||
| 11 | `BindSamplerStates` (36) | `kVarTail` | `MGPipeHandle` × `Count` (`:785`) | `const MGPipeHandle*` (`PipeApply.h:1029`), `SamplerEmit.h:860` | as above | as above | **apply** | as above |
|
||||
| 12 | `SetShaderImages` (37) | `kVarTail` | `MGPImageView` × `Count` (`:802`) | `const MGPImageView*` (`PipeApply.h:1030`), `ImageEmit.h:129` | as above | as above | **apply** | as above |
|
||||
| 13 | `SetShaderBuffers` (38) | `kVarTail\|kHostSpan` | **two tails**: `MGPBufferRange` × `Count` (`:826`), then `MGHostSpan` × `HostSpanCount` (`:828`) | **none — no applier entry point exists.** P5 writes the first producer *and* the first consumer. | `SEG_STAGE` | — | **apply** | `HostSpanCount` is 0 **or** `Count`, never anything else (`MGPipeTypes.h:820-823`), so the two arrays stay index-aligned. `kCapNeedsHostUboBytes` is 0 for all of P5, so the second tail is **always absent** in this phase. |
|
||||
| 14 | `SetStreamOutputTargets` (39) | `kVarTail` | **two tails**: `MGPBufferRange` × `Count`, then `Uint32` × `Count` (`:836-838`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | one `Count` sizes both tails; off the reduced path in P5 |
|
||||
| 15 | `SetVertexAttribDefaults` (41) | `kVarTail` | `MGPAttribValue` × `Count` (`:864`) | `const MGPAttribValue*` (`PipeApply.h:756`), `PipeFill.cpp:2115` | as above | as above | **apply** | **two declarants that must agree**: `Count` and `popcount(Mask)` (`:863`, contract at `PipeApply.h:754-755`). A disagreement is a wire fault nothing checks today; the decoder must. |
|
||||
| 16 | `DrawVbo` (59) | `kHostSpan\|kVarTail` | `MGPDrawRange` × `NumDraws` (`:1239`), then a **conditional** `MGHostSpan` when `Flags & kDrawHasUserIndices` (`:1212`, `:1231`) | **none — no applier entry point exists.** | `SEG_STAGE` | — | **apply** | `NumDraws`; the span carries its own `Size`. **The only `kHostSpan` on the hot path, and P5 must produce none of them** — `TriangleScenario` uses a VBO-backed draw precisely so this tail never appears. `MGPipeTypes.h:1221-1224` defers the fixed-head-versus-tail question to this phase: **P5 keeps it in the tail, unchanged**; there are no per-draw byte histograms yet to justify moving it, and moving it would be a wire-format change with no measurement behind it. |
|
||||
|
||||
### Group C — carries content with **no** `MGPBlobRef` and **no** `kHasBlob`
|
||||
|
||||
These three are the reason `kHasBlob` had to be given an exact meaning (table 0).
|
||||
|
||||
| # | call (op) | flags | ruling | evidence |
|
||||
|---|---|---|---|---|
|
||||
| 17 | `CreateSamplerState` (23) | **now `kHasBlob`** (R-13.1) | `MGPSamplerDesc` **does** own an `MGPBlobRef Parameters` (`:429`) and the flag was simply missing. The blob is `memcpy(sizeof(SamplerParameters))` — a POD, and **`borderColorForm` must survive byte for byte** (`MGPipeTypes.h:425-428`), because all three colour representations are always numerically populated and it is the only thing that says which one the backend must use. | companion today is a **typed frontend pointer**, `const SamplerParameters*` (`PipeApply.h:1000`), passed `SamplerEmit.h:458`. Declares `Size = 0` (`SamplerEmit.h:433-435`); cross-check at `PipeApply.cpp:2353` is inert until rule A arms it. **Padding trap:** `SamplerEmit.h:437-445` — assignment leaves three trailing padding bytes stale, and the bytes staged must be the bytes a later `memcmp` compares. |
|
||||
| 18 | `GetCaps` (1) | **now `kReplySlot\|kHasBlob`** (R-13.1) | `MGPCaps` owns **two** `MGPBlobRef`s, `FormatCapabilities` and `RendererInfo` (`:137-138`), and carried no `kHasBlob` at all. | `PipeCalls.def:80` before the fix. Serializers are P5's new work (`MG_Remote/CapsCodec.h`); the header itself defers them to this phase (`MGPipeTypes.h:134-136`). |
|
||||
| 19 | `ResourceRespecify` (3) | stays `kNeedsAck`, **no `kHasBlob`** (R-13.3) | **`initialBytes` is always `nullptr` under split. Initial content arrives as `ResourceSubData` records immediately after this one.** `MGPResourceDesc` owns no `MGPBlobRef` and gains none. | The alternative was costed and rejected: `MGPBlobRef` is 24 bytes, `MGPResourceDesc`'s two pads are `Uint16 Pad0` (`:303`) + `Uint32 Pad1` (`:315`) = **6 bytes**, so a blob member takes the struct 88 → 112 and moves `MGP_ASSERT_POD(MGPResourceDesc, 88)` (`:320`). The chosen route reuses a path that is already chunked (`MGPipeForEachSubDataRecordRange`, `PipeFill.cpp:694-713`) and already acceptance-gated; it costs one extra record. `HasDefinedContent` (`:301`) is the field the encoder branches on, and it already exists. **The texture path already does exactly this** — `TextureEmit.h:1137` passes `nullptr` and relies on a following upload — so this generalises today's texture behaviour to buffers rather than inventing anything. |
|
||||
| 19b | `ResourceRespecify`'s **second** uncarried companion | — | **`const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`, 4 bytes: `Uint16 UploadTarget; Uint16 Level;`) has no wire carrier either, and it is not bytes — R-13.3 does not cover it.** Ruling: it rides in `MGPResourceDesc`'s existing pads — `Pad1` (4 B, `:315`) becomes `{Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;}` and one byte of `Pad0` (`:303`) becomes `Uint8 HasRespecifiedLevel`. **Zero size change, `MGP_ASSERT_POD(..., 88)` does not move**, and `PipeFields.def`'s `MGP_FIELDS_MGPResourceDesc` gains the two named members (pads are excluded from field lists, so this is required, not optional). | Null means "this respecify redefines the **whole** resource" and drops every pending upload; non-null names the single `(uploadTarget, level)` and drops **only** that key. Clearing the whole set for a per-level `glTexImage2D` loses exactly the texels the server-side set exists to protect (`PipeApply.h:805-820`). Without a carrier, every OpenRA per-level respecify would silently take the whole-resource arm. **LANDED** (integrator ruling A made `MGPipeTypes.h` c0's file): `Uint8 HasRespecifiedLevel` in Pad0's high byte, `Uint16 RespecifiedUploadTarget; Uint16 RespecifiedLevel;` in Pad1, `MGP_ASSERT_POD(MGPResourceDesc, 88)` unmoved, plus an `offsetof` assertion that the pair stays adjacent and in `MGPRespecifiedLevel`'s order. **Read it only through `MGPipeRespecifyIsWholeResource` / `MGPipeRespecifiedUploadTargetOf` / `MGPipeRespecifiedLevelOf`, and write it only through `MGPipeSetRespecifiedLevel` / `MGPipeClearRespecifiedLevel`**: three fields are one value, and an open-coded reader that forgets the presence byte reads level 0 of upload target 0 as a real scope. **The carrier has no producer** — P5 builds only whole-resource descriptors, and a verify build pins that (`PinWholeResourceRespecifyScope`, `PipeApply.cpp`, the `PinNoLiveHostWrites` shape) so the phase that wires it cannot arrive unannounced. |
|
||||
| 20 | `ResourceFlushRange` (51) | stays `kNone` (R-13.2) | **It carries no bytes at all under split.** It is a `{range, AccessFlags}` control record; the bytes of `[Offset, Offset+Size)` arrive **ahead of it** as `ResourceSubData` records covering exactly that range. | R-13.2 offered "add a blobref" or "write the convention down". Neither, and for a reason: the ladder this record drives rewrites its range *"from the authoritative shadow"* (`Managers.cpp:1047-1076`), and under split the authoritative shadow is **server-owned** by rule C — so `resource_subdata` is already the only way bytes reach it, and a blobref here would be a second, forgeable way to say the same thing. `AccessFlags` must still cross **verbatim**, not normalised (`PipeApply.h:902-903`). **Overturn condition:** if the tier-1 `INVALIDATE_RANGE` arm turns out to need the bytes and the range in the *same* record — i.e. an intervening record could stale the subdata — this needs its own blobref. It cannot happen while the verb barrier holds, because nothing interleaves; **revisit when the barrier retires for the buffer family.** |
|
||||
|
||||
**Row 18 invariant (added 2026-09-29, debt review).** The two `GetCaps` blobrefs never cross the wire.
|
||||
The reply carries the decoded objects; `CapsMirror::Adopt` clears both refs (`CapsMirror.cpp:107-112`)
|
||||
because they name SEG_STAGE runs of the server's encoder. A change that transports them needs its own
|
||||
server → client carrier rule; it cannot reuse `SEG_STAGE`.
|
||||
|
||||
### Group D — the four server → client rows the brief's list omitted
|
||||
|
||||
| # | call (op) | flags | ruling | reply name |
|
||||
|---|---|---|---|---|
|
||||
| 21 | `MapPersistent` (5) | `kReplySlot\|kOptional` | **Returns `nullptr` under split, always** (R-6/R-2.4). Its `const void* seedBytes` companion (`PipeApply.h:917`) therefore never crosses in P5 and needs no carrier. The three frontend sites already tolerate a decline (`BufferObject.cpp:238`, `:603-606`, `:657-660`). Answer travels as `Status = DECLINED` with a zero-length payload. | `map_persistent.decline` |
|
||||
| 22 | `ResourceReadback` (52) | `kReplySlot` | Bytes go **server → client** in `SEG_EVENT` via `OnBufferWriteback` (#3), not in the reply slot: the destination is the client's shadow and the size is the resource's, not a fixed slot's. The reply slot carries only completion. **The ordering rule is load-bearing:** the writeback is applied **before** the mutation epoch bumps, never after (`ARCHITECTURE.md:292-294`, `Managers.cpp:2120-2136`). | `resource_readback.done` |
|
||||
| 23 | `ReadPixels` (58) / `GetTextureImage` (55) | `kReplySlot` | **`ReadPixels` blocks in P5** and its pixels come back in the reply slot, which is why `ReplyPool::SlotBytes()` is sized from the scenario's largest read rather than guessed. **ID-47: `SEG_REPLY` is 16 MiB, eight slots of 2 MiB, `MaxReplyBytes = 2 MiB − 16 = 2,097,136`** — the canonical sizes are 8/32/16 MiB + 256 KiB (`ProtocolSmokeTest` pins them on the wire, `SessionTest` on the mapping); the largest P5 read is E2's full-surface 640×480 RGBA8 snapshot, 1,228,800 bytes, which the previous 8 MiB pool refused. **A read whose answer would exceed `MaxReplyBytes` is refused at the CLIENT before emission** with `Fatal{ReplyTooLarge, "ReadPixels <w>x<h> <format> <bytes> > <cap>"}` (`ReplySlotPool::RequireReadPixelsFits`, forwarded by `ClientSession::RequireReadPixelsReplyFits`, called once by c1's `OnReadPixels` emitter) — never truncated, never a server-side abort the client cannot name; `Post`'s own refusal stays as the last line of defence. Reads larger than 2 MiB (a 2400×1080 RGBA8 device surface) are a P6 debt, not a bigger pool; §5 has no knob for this segment by design. `MGPReadbackInfo` has `DstOffset`/`DstSize` but **no `Seg`** (`MGPipeTypes.h:1197-1206`): ruling — the destination is **always `SEG_REPLY`** in P5, so no `Seg` field is added; the PBO destination (fire-and-forget plus a client-side `MarkGpuWritten`) is b1's and also needs none, because a PBO destination is a resource handle rather than a segment. `GetTextureImage` is **not on P5's reduced path** and its slot stays `Fatal{UnmigratedVerb}`. | `read_pixels.pixels` **P7 更正(ID-P7-58)**:超出一个回复槽的 ReadPixels 由客户端切成行带(单行超槽切行内片),每带各过一次 `RequireReadPixelsReplyFits`;`Fatal{ReplyTooLarge}` 只剩「一个像素都放不下」。 |
|
||||
|
||||
---
|
||||
|
||||
## §3 Table 2 — `PipeInputs` field ownership
|
||||
|
||||
**This section is the SPEC and the four class definitions. The authoritative instance is
|
||||
generated**: package p1 writes the generator that emits `generated/PipeFieldOwnership.inc` plus
|
||||
a `--check`, in the shape of `gen_pipe_dirty_surface.py`. **A field in none of the four classes
|
||||
is a build failure** (R-7.1) — that is the whole mechanism, and a hand-maintained table would
|
||||
be wrong within a week.
|
||||
|
||||
The domain is **63 fields** (`kMGPipeInputFieldCount`, asserted `generated/PipeFilled.inc:96`)
|
||||
**plus the 7 sticky forwards**, which are among those 63 but are exempted from the poison and
|
||||
so need their own row. 70 rows, each in exactly one class.
|
||||
|
||||
### The four classes
|
||||
|
||||
**RECORD-SUPPLIED** — a pushed record supplies the **whole** field, so the server never needs
|
||||
the client for it.
|
||||
Membership is `kMGPipeFieldEmittedBy` (`generated/PipeFilled.inc:336-402`,
|
||||
`kMGPipeEmittedFieldCount = 40`) **minus** the nine for which
|
||||
`EmittedCallSuppliesTheWholeField` returns false (`MG_Impl/Pipe/PipeFill.cpp:1924-1939`;
|
||||
reasons `:1875-1923`). **31 fields today.**
|
||||
The nine excluded, with the generator's own reason: `GetPixelStoreParameters` ("only the PACK
|
||||
half has a carrier"); `GetCurrentVertexAttribute` ("the applier cannot reproduce GLContext's
|
||||
cross-view conversion"); `GetMaxTouchedTextureUnit` ("the set is hash-suppressed while the
|
||||
high-water mark still moves"); and six sharing one reason — "the storage is a frontend heap
|
||||
reference and the record carries an 8-byte `{slot, gen}`" — `GetBoundVertexArray`,
|
||||
`GetFramebufferBindingSlot`, `GetImageTextureBinding`, `GetTextureUnitObject`,
|
||||
`GetProgramForDraw`, `GetProgramForDispatch`.
|
||||
|
||||
**APPLIER-DERIVED** — the applier writes it from the records it already applies; no client
|
||||
participation at all. Today: the render-state mirrors, `m_pixelStore[0]` (the **pack** half),
|
||||
capability bits, the current vertex attribute, and the patch fields — written at
|
||||
`PipeApply.cpp:177-184`, `:1338-1437`, plus everything `MGPipeDeriveRenderStateFields`
|
||||
(`PipeApply.h:1084`, `PipeApply.cpp:2157`) derives.
|
||||
|
||||
**BARRIER-PULLED** — **P5's debt, and every row names the phase that retires it.** The server
|
||||
answers by reading a value the client's residual fill put into the single shared `gPipeInputs`
|
||||
while the verb barrier holds both threads apart. It is correct only because of that barrier,
|
||||
which is why the barrier is load-bearing rather than cautious.
|
||||
Each read increments `PipeStats::CallClass::ResidualPulls` (short name `rsp`, inside the
|
||||
`#if MOBILEGL_PIPE_PUSH` block so G1 holds), published per frame. **`rsp`'s value at the end of
|
||||
P5 is the size of the P6/P7/P8 debt** and goes into MEASUREMENTS.
|
||||
`MOBILEGL_IPC_STRICT_ERRORS=1` promotes every read in this class to `Fatal`, and a named test
|
||||
asserts the abort actually happens — an instrumentation that cannot go red is decoration.
|
||||
|
||||
**FATAL** — no carrier, and the reduced path never reads it, so a read is a real defect.
|
||||
`Fatal{UnmigratedPipeInput, "<Field>@<verb>"}` (`generated/PipeFilled.inc:407-413`), live at
|
||||
every log level on purpose (`PipeInputs.h:29-31`: *"this is not `MOBILEGL_ASSERT`, which is
|
||||
inert in INFO builds"*).
|
||||
|
||||
### The known BARRIER-PULLED rows — the 21 the reduced path actually reads
|
||||
|
||||
Union of `kClear` (7 of its 18 own fields), `kDraw` (19 of 47) and `kReadback` (12 of 17).
|
||||
OpenRA adds no field to this set — it widens the **site** set, not the field set, and is the
|
||||
first thing to reach the read-attachment sites (`Managers.cpp:8603`, `:8966`) and the
|
||||
`maxTouchedUnit >= 0` texture-unit walks.
|
||||
|
||||
| field | class | retires in | note |
|
||||
|---|---|---|---|
|
||||
| `GetBoundVertexArray` | O | **P8** | `DirectGLES.cpp:4486`, `PrepareForDraw`, **unconditional on every draw**. `PipeFill.cpp:1902-1905` says the pull retires at P8, not here. |
|
||||
| `GetProgramForDraw` | O | **P8** (Espryt), P7 (Magma) | `DirectGLES.cpp:4497`, same site, also unconditional. |
|
||||
| `GetBufferBindingSlot` | O | P8 (indirect half), P9 (readback), P13 (transfer) | 18 Espryt sites; the 7 of 15 `BufferTarget`s no call covers (`Coverage.def:37-70`). |
|
||||
| `GetBufferBindingPoint` | O | P3b/P4b + P7 | |
|
||||
| `GetTouchedBufferBindingPointCount` | V | P3b/P4b | |
|
||||
| `GetFramebufferBindingSlot` | O | P3b/P4b (Espryt), **P7** (Magma) | 8 Espryt sites through `GetFramebufferBindingSlotChecked`; `SyncCurrentFBO` (`:2995`) is self-declared monolith glue (`DirectGLES.cpp:2961-2965`) while `BindCurrentFBO` (`:4303-4353`) is already split-clean. |
|
||||
| `GetTextureUnitObject` | O | P3b/P4b, P7 | 13 Espryt + 8 Magma sites. |
|
||||
| `GetImageTextureBinding` | O | P3b/P4b, P7 | |
|
||||
| `GetActiveTextureUnit` | V | P3b/P4b | server answers from its own state (`Coverage.def:215-219`). |
|
||||
| `GetMaxTouchedTextureUnit` | V | P3b/P4b | hash-suppressed set, high-water mark still moves. |
|
||||
| `GetTextureContextId` | V | P3b/P4b | **not a value to migrate**: the server answers from its own `Serial`. `Coverage.def:220-224` is explicit that no call carries these three and none should. |
|
||||
| `GetTextureBindGeneration` | V | P3b/P4b | as above |
|
||||
| `GetSamplingResolutionGeneration` | V | P3b/P4b | as above |
|
||||
| `GetCurrentVertexAttribute` | V | P3b/P4b | the applier cannot reproduce GLContext's cross-view conversion. |
|
||||
| `GetPixelStoreParameters` | V | **split in P5 — see below** | |
|
||||
| `IsTransformFeedbackActive`, `IsTransformFeedbackPaused`, `GetTransformFeedbackProgram`, `GetTransformFeedbackGeneration`, `GetBoundTransformFeedbackLifetimeId`, `GetTransformFeedbackCapturedVertices` | V/O | P3b/P4b (Espryt XFB scatter), P7 (Magma) | read on `kDraw`; XFB itself is off the reduced path but these are read on the draw walk regardless. |
|
||||
|
||||
**`GetPixelStoreParameters` is split into pack and unpack in this phase** (R-7's one small
|
||||
migration, and it is c0's ruling to keep rather than soften). The field is `m_pixelStore[2]` and
|
||||
only `[0]` (pack) has a carrier — which the applier does write (`PipeApply.cpp:1373`) — so the
|
||||
whole field reads as unmigrated while the half a readback needs is fine. Splitting it is what
|
||||
stops the readback path from taking a whole-field `Fatal` for a half it never touches. The five
|
||||
Espryt read sites whose `isUnpack` argument decides which half they want are
|
||||
`DirectGLES.cpp:7924`, `:9399`, `:10893`, `:11272` and `Utils.cpp:2302`; **the scout named them
|
||||
and did not open them**, so p1 reads the argument at each before it writes the two rows.
|
||||
|
||||
### The FATAL rows
|
||||
|
||||
Three non-sticky fields, each off the reduced path for a different, checkable reason:
|
||||
|
||||
| field | why it is FATAL rather than pulled |
|
||||
|---|---|
|
||||
| `GetBoundTransformFeedbackName` | **dead** — read by no backend since D21 (`PipeInputs.h:232-234`). |
|
||||
| `GetTransformFeedbackPausedPrimitiveCounter` | reachable only from class `kQuery`, which the reduced path never enters. |
|
||||
| `GetProgramForDispatch` | reachable only from `kDispatch`; there is no compute on the reduced path. Sites: `DirectGLES.cpp:5779`, `VulkanRenderer.cpp:7327`, `:7379`. |
|
||||
|
||||
Plus **`MGPipeUnmigratedEmulation`'s five call sites**, which in a split build stop being
|
||||
`(void)name;` and become `Fatal`: `Managers.cpp:5334` ("texture-remint-pull"),
|
||||
`DirectGLES.cpp:8051` ("generate-mipmap-storage"), `:8702` ("generate-mipmap-cpu-fallback"),
|
||||
`:8997` ("copy-image-shadow-mirror"), `:10623` ("get-tex-image-shadow"). **One function grows
|
||||
teeth and five sites get them** — `PipeApply.cpp:2820`, today a no-op, which
|
||||
`PipeApply.h:1055-1066` and `PipeApply.cpp:2812-2817` both already say is waiting for this
|
||||
phase. None of the five is on the reduced path. (ROADMAP's P4a row says six sites; there are
|
||||
**five calls and one comment** — `Managers.cpp:5287` is the comment.)
|
||||
|
||||
### The seven sticky forwards
|
||||
|
||||
`GetBufferBindingPointCount`, `GetProgramObject`, `GetTextureObject`,
|
||||
`HasOpenTransformFeedbackSpan`, `ValidateProgramName`, `InvalidateCompileEnv`, `RecordError`
|
||||
(`PipeInputs.h:569-577`; identity asserted `:135-137`; argued `Coverage.def:138-150`).
|
||||
|
||||
P5's assignment:
|
||||
|
||||
- the first five → **BARRIER-PULLED** (counted in `rsp`, `Fatal` under strict);
|
||||
- `InvalidateCompileEnv` → **`OnCapsInvalidated`**, i.e. the re-arriving caps snapshot (R-12);
|
||||
- `RecordError` → **BARRIER-PULLED**, counted in `rsp`; its *ordering* is P9's (`OnGlError`).
|
||||
|
||||
**And their exemption is cancelled in a split build.** Today F-class accessors carry no
|
||||
`MGP_INPUT_CHECK` at all (`PipeInputs.h:563-568`) and `MGPipeInputFieldIsFresh`
|
||||
(`generated/PipeFilled.inc:418-426`) answers "fresh" for a sticky field regardless of
|
||||
generation. That is exactly backwards for this phase: these seven are the ones that hand a
|
||||
**frontend object or a frontend write** straight to the backend, so the exit gate "an
|
||||
unmigrated field read is Fatal" is structurally blind on the seven most dangerous fields.
|
||||
Under `MOBILEGL_BUILD_DISAGGREGATED` the exemption is lifted so they enter `rsp`, and under
|
||||
`MOBILEGL_IPC_STRICT_ERRORS=1` they become `Fatal` like any other BARRIER-PULLED row.
|
||||
|
||||
### The prerequisite nobody else owns: someone must stamp
|
||||
|
||||
`MGPipeApplyAccess` **deliberately does not stamp** the poison generations
|
||||
(`PipeInputs.h:612-618`): *"a stamp says the filler published this for THIS verb, which is the
|
||||
walk's statement, not the applier's"*. Under split the filler is in the other role, so nothing
|
||||
stamps, every `FilledGen[]` stays 0, `MGPipeInputFieldIsFresh` returns false for **everything**,
|
||||
and a purely-server-side read aborts on the **first** field inside `SyncRenderState` —
|
||||
`Fatal{UnmigratedPipeInput, "GetRenderStateParameters@<none>"}` — long before any interesting
|
||||
case. **The server stamps at the verb boundary**: p1 defines what is stamped and for which
|
||||
verb, v1 places the call (`Server/PipeApplier::StampVerbBoundary`). Neither half works alone,
|
||||
and this is not in the ROADMAP row.
|
||||
|
||||
### Two sets with no field ids
|
||||
|
||||
**The conservative client GPU-write set.** One row per backend `MarkGpuWritten` site, mapped to
|
||||
the client predicate that must fire, one unit case each:
|
||||
|
||||
| backend site | what it marks | when |
|
||||
|---|---|---|
|
||||
| `DirectGLES.cpp:570` | every SSBO binding point | draw/dispatch about to go out |
|
||||
| `DirectGLES.cpp:618` | every bound atomic counter | every conformance case reads the increment back |
|
||||
| `DirectGLES.cpp:2603` | buffer textures on image units, **only when `Access != GL_READ_ONLY`** | from draw preparation, deliberately not from `glBindImageTexture` |
|
||||
| `UniformManager.cpp:1075` | storage texel buffer, `Access != GL_READ_ONLY` | after `EnsureGpuResidentStorage()` |
|
||||
| `UniformManager.cpp:1231` | SSBO block, unconditional | after `EnsureGpuResidentStorage()` |
|
||||
| `VulkanRenderer.cpp:11618` | the XFB capture targets | "the capture is a GPU write like any shader's" |
|
||||
|
||||
Plus **two new producers P5 adds**, both client-side with no server participation
|
||||
(`ARCHITECTURE.md:508`): `glReadPixels` into a pack PBO becomes fire-and-forget plus a client
|
||||
`MarkGpuWritten` (strictly better than monolith's unconditional stall), and
|
||||
`glEndTransformFeedback` drops its unbounded fence wait and marks the capture targets instead.
|
||||
|
||||
`SyncGpuWrites` gains a **third state** it cannot express today — *emitted, answer not back* —
|
||||
and under split it must **block until `OnBufferWriteback` lands** rather than clear the flag
|
||||
optimistically (`BufferObject.cpp:372-374` clears unconditionally; `ARCHITECTURE.md:509` lists
|
||||
this among the unavoidable blocking points, because monolith already `glFinish()`es here).
|
||||
**No narrowing in P5**: `ResourceTracker.h:587-592`'s `rangeCount == 1` assertion **stays**.
|
||||
|
||||
**The persistent-map reachable set: the census is 21 sites, not 20.** `MEASUREMENTS.md:111`
|
||||
records 20 and `Managers.cpp:5047-5048` speaks of "the eleven Espryt" sites; the actual count
|
||||
is **9 Espryt + 12 Magma = 21**, and Espryt's own count is 12 (9 + 3 `SyncGpuWrites`), so both
|
||||
published numbers are exactly one low and the missing one is an Espryt line. **Ruling: 21 is
|
||||
the number, and `DirectGLES.cpp:361` (`ResolveIndirectCommandBytes`) is in the set.** It is a
|
||||
shared helper rather than a draw-path site, which is the most likely reason it was excluded,
|
||||
but a helper that reaches a persistently-mapped range is exactly as able to read stale bytes as
|
||||
a draw site is; excluding it would be a shutter that cannot see its own subject. The
|
||||
per-site attribution table `ARCHITECTURE.md:290` refers to as "§5.7" **does not exist in the
|
||||
tree** — b1 should not go looking for it. The nine Espryt sites: `DirectGLES.cpp:361`, `:6185`,
|
||||
`:6439`, `:6440`, `:6541`, `:6542`, `Managers.cpp:2817`, `:2994`, `MultiDraw.cpp:511`. The
|
||||
twelve Magma: `DirectVulkan.cpp:281`, `:472`, `:796`, `UniformManager.cpp:2024`,
|
||||
`VulkanRenderer.cpp:3542`, `:3621`, `:4013`, `:7428`, `:12423`, `:12424`,
|
||||
`VkBufferManager.cpp:628`, `:679`. **Correcting `MEASUREMENTS.md:111` from 20 to 21 is b1's
|
||||
line to write**, in the same commit that lands the tracker.
|
||||
|
||||
`m_livePersistentMaps` is defined by `SyncPersistentMappedRange`'s own early-out chain read as
|
||||
a membership test (`BufferObject.cpp:346-349`): persistent, write, **not** flush-explicit,
|
||||
**not** GPU-resident.
|
||||
|
||||
---
|
||||
|
||||
## §4 Table 3 — role and thread ownership of every process global
|
||||
|
||||
`ARCHITECTURE.md:578-581` claims MGPipe brings the globals a role split must duplicate down
|
||||
from four to two. **That census is at least seven short.** Every row below also answers what
|
||||
make-current and teardown do to it — the column `ARCHITECTURE.md` does not have.
|
||||
|
||||
| global | where | who writes | P5 ruling | make-current / teardown |
|
||||
|---|---|---|---|---|
|
||||
| `gPipeInputs` (~20 KB) | `PipeInputs.h:706` | client residual fill (`PipeFill.cpp:530`, `:2131`, `:2604`) + applier (`PipeApply.cpp:1336`, `:1364`, `:1373`, `:1377`, `:1436`, `:1512`) | **One instance is legal, but only under the verb barrier.** The barrier makes at most one of {GL thread, apply thread} runnable at a time, so there is exactly one writer at any instant. **No second writer may be introduced before the barrier retires.** The invariant is a runtime assertion in debug/verify builds, not only a sentence here: the apply thread raises a flag on entering the applier and the client checks it when it touches `gPipeInputs` outside a barrier (`ClientSession::InBarrierWait` / `ApplyThreadIsInsideApplier`). | make-current: unchanged. teardown: nothing — it is POD in the image. |
|
||||
| `g_applier` | `PipeApply.cpp:396` | applier | **Server-exclusive.** Its own header already says "under split there is one per served context" (`PipeApply.h:684`). | It is `*new MGPipeApplierState{}` and never destroyed, deliberately (`PipeApply.cpp:392-395`): `resource_destroy` is raised from `~BufferObject`, which runs from exit handlers after this TU's globals are gone. |
|
||||
| `g_resourceOps` | `PipeApply.cpp:402` | the backend, at register time | **Server-exclusive, and the client must NEVER read it** (R-8). Under `inproc` a client reading it is right *by accident*; under spawn it is null and the four P4a families plus P3a's buffers emit **nothing at all**, silently. The client asks `CapsMirror::ServerConsumes` instead. | registered around `DirectGLES.cpp:11933`, nulled from `OnBackendContextDestroyed` (`Managers.cpp:2584`) — so it moves on every context loss, which is another reason the client cannot key on it. |
|
||||
| `gMGPipeSegmentResolver` | `MGPipeHostSpan.h:47` — a **plain non-atomic inline variable** | `MG_Remote` installs | **One process-wide slot, so it cannot be per-role.** Ruling: **the server role installs it and the client never resolves a span at all** — the client only ever *writes* `Ptr = nullptr`. `SegmentTable::InstallProcessResolver()` asserts if one is already installed, so two roles racing on it is loud rather than silent. Install **before** the apply thread starts. | teardown: uninstall after the join, never before — a record still in flight can still resolve. |
|
||||
| the ten `MG_Impl/Pipe` `*Instance()` singletons | `fable-seam-audit.md:120-135` | client | **Client-exclusive.** One correction carried forward: the texture **drain list `m_drain` is process-wide**, not per-context as D-D4 claims; the audit already booked "one drain per client context" as a P5 item. | `FreshlyPrimed` (`PipeFill.cpp:2414-2440`) resets them on make-current; leak at exit (ID-8). |
|
||||
| `ScopedDefaultUnpackState::s_synced` + **six** value shadows | `Managers.cpp:5490-5496` | backend | **Server-exclusive — the sixth global the four→two census missed.** Latent rather than live in P5 only because the client role never touches GL on the reduced path. (`Managers.cpp:5491-5496` is six `GLint`s, not five: `s_skipImages` at `:5496` is the one usually dropped.) | **Never reset on context death** — `OnBackendContextDestroyed` resets the rings and the binding caches and not this. Benign while a lost context returns the driver to GL defaults; not benign the day a server re-attaches to a context something else moved. Register it now. |
|
||||
| `pActiveBackendObject` | `GlobalObjects.cpp:23` | `MG_Backend::Init()` | **Client installs `BackendObject_Remote`; the server's `BackendObject_DirectGLES` is held privately by `ServerLoop`.** No thread-keyed shim, and therefore `MOBILEGL_BUILD_DISAGGREGATED_INPROC` needs none — but the cost is **seven** backend-internal reads across **six** functions, not the one the scout reported: `BackendObject_DirectGLES.cpp:815`, `:819` (`ClampSamplesToBackendSupport`) and `Utils.cpp:74`, `:82`, `:126`, `:220`, `:260`. All seven are format-capability lookups, so "pass the format cache down" still works. **`DirectGLES.cpp:12446` is NOT `ClampSamplesToBackendSupport`** — it is `Present()`'s fence poll, and `DirectGLES.cpp` contains no `pActiveBackendObject` reference at all. | `GetFormatCapabilities()` is **non-virtual** (`BackendObject.h:594`), so the remote object must **fill** `m_formatCapabilities` rather than override the accessor. Teardown: `pActiveBackendObject.reset()` (`MobileGL/Init.cpp:68`) runs `~BackendObject_DirectGLES` → `DestroyEGLContext()`, so under split it must be a **blocking** request onto the apply thread. |
|
||||
| `gBackendFunctionsTable` | `GlobalObjects.cpp:24`, assigned `MG_Backend/Init.cpp:63` | `MG_Backend::Init()` | **Client = the emit table (R-4); the server holds its real table directly and never goes through this global.** | cleared at `MobileGL/Init.cpp:91`. |
|
||||
|
||||
**Teardown order**, `ARCHITECTURE.md:537` plus the sentence it omits:
|
||||
|
||||
1. client publishes and waits for the server to drain and acknowledge;
|
||||
2. **`Doorbell::Kill()`** — *the only thing that can wake an apply thread parked on
|
||||
`kWaitForever`* (`CondVarDoorbell::Kill` in `Doorbell.h`; the shape is already pinned by
|
||||
`InProcessTransportTest.cpp:344`);
|
||||
3. **join**, bounded (that test uses 5 s) so a regression is a red test and not a hung CI job;
|
||||
4. only then may the client free anything an emitter owns — a tail still referenced by an
|
||||
unapplied record is a use-after-free the join is what prevents;
|
||||
5. then the existing order (`MobileGL/Init.cpp:38-98`).
|
||||
|
||||
**ID-8 applies once per role-local singleton, not once overall**: every new
|
||||
`MG_Remote/Client/*` and `MG_Remote/Server/*` singleton leaks at exit. The proof recipe is
|
||||
inherited: both lanes run `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`.
|
||||
|
||||
**Known open item, flagged not resolved.** `ARCHITECTURE.md:537`'s required order puts the
|
||||
client's sync/query handle release **after** the transport closes, while today
|
||||
`DestroyAllSyncObjects` / `DestroyAllQueryObjects` (`MobileGL/Init.cpp:62`, `:67`) deliberately
|
||||
run **before** `pActiveBackendObject.reset()` (`:68`). The two are only reconcilable if a split
|
||||
sync handle is client-minted and needs no backend call — which is P10's, not P5's. **P5 keeps
|
||||
today's order** and v1 records which way it went.
|
||||
|
||||
---
|
||||
|
||||
## §5 The knobs
|
||||
|
||||
Parsed in `ConfigLoader.cpp`, declared in `Config.h`. All of them live behind
|
||||
`#if MOBILEGL_BUILD_DISAGGREGATED` — including the parser — because `MG_ConfigLoader::Init()`
|
||||
is a pull-build symbol and G1 admits **no resize**, which is the same reason the
|
||||
`MOBILEGL_PIPE_VERIFY` knobs sit behind their own `#if`.
|
||||
|
||||
| knob | default | notes |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_TRANSPORT` | `monolith` | `monolith\|inproc\|spawn\|unix:<path>\|pipe:<name>`. The three P6 forms **parse and are then refused by name**, staying on monolith: a P6 lane that set `spawn`, fell back silently and went green on the wrong arm is the failure this wording avoids. |
|
||||
| `MOBILEGL_IPC_SERVER_PATH` | `""` | P6 consumes it; P5 parses it because t1's ctest `ENVIRONMENT` blocks and `add_trace_replay_test`'s SPLIT variant already carry it, and an unparsed variable is indistinguishable from a parsed-and-ignored one. |
|
||||
| `MOBILEGL_IPC_RING_MB` | 8 | SEG_CMD. **One record may be at most half of this** (`RingProducer::MaxRecordBytes`), so 8 MiB caps a record at 4 MiB. R-10 makes the codec publish a max-record-bytes counter rather than assume that is enough. The stage chunk budget below cuts a record's BLOB and never the record's own bytes, so this bound is the one thing no emitter cuts. |
|
||||
| `MOBILEGL_IPC_STAGE_MB` | 32 | SEG_STAGE. Every blob and every var-tail's bytes, **one record's blob at a time**, and **content chunking is landed**: the rows that can outgrow the segment cut themselves at `MGPipeStageChunkBytes()` = `clamp(segment/4, 4096, segment)` — the buffer content walks (`PipeFill.cpp`'s two `MGPipeContentChunkCap` sites, `1e7c372e`) and one texture level's whole-width slabs (`TextureEmit.h`'s slab split, assembled server-side by `StagedTextureStore::AdoptRun`, `9469d48e`). A record type with no such cut, or a single piece still larger than the arena, stays `Fatal{RingOverrun, "SEG_STAGE"}` at the encoder — the backstop, not the normal path. |
|
||||
| `MOBILEGL_IPC_SPIN_US` | 50 | spin before parking, either direction. |
|
||||
| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | 64 | **0 is the E3(a) negative control, not "unlimited"**: it turns the push off and `PersistentCoherentMapScenario` must go red. |
|
||||
| `MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS` | 1 | 1 = the push ships only blocks whose hash changed since the last push (tracked buffers: the mprotect fault bitmap; untracked: the content scan). 0 restores the whole-range push (A/B control). |
|
||||
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | 1 = value-class records (kCtxState / kCtxCso / kCtxObject with no reply slot) publish without waiting for their own apply; the barrier is taken at the next pull-reading verb, the only place BARRIER-PULLED fields are read. 0 restores R-1's per-record barrier, and `MOBILEGL_PIPE_VERIFY` forces 0 (the shadow compare reads the pulled fields at every record). |
|
||||
| `MOBILEGL_IPC_ADOPT_TIER` | 2 | 2 = emulate, the only tier P5 implements. 0 and 1 parse and are `Fatal` at use, naming P11. |
|
||||
| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 0 is R-1's negative control and is **expected** to be red. |
|
||||
| `MOBILEGL_IPC_STRICT_ERRORS` | 0 | promotes BARRIER-PULLED reads — and, in a split build, the seven sticky forwards — to `Fatal`. |
|
||||
| `MOBILEGL_IPC_AUDIT` | 0 | `0xDD` over retired staging bytes (rule C's mechanical control). |
|
||||
| `MOBILEGL_IPC_SERVER_AFFINITY` | `auto` | kept as the raw string; whoever starts the apply thread logs the **resolved mask**, because an affinity that silently did nothing looks exactly like one that worked. |
|
||||
| `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` | 5000 | (P6 D5b, parsed from P7) a spawn/tcp client's wait for one surface-control `SurfaceReply` once the server's backend is up. Expiry is **not** fatal: dead latches device-lost, alive-but-silent is a named diagnostic. Range 100..600000. Since p7/spawnhang it bounds **silence**, not the op: each `SurfaceProgress` the server sends while its apply thread runs the op (every 250 ms, `ServerLoop::kControlProgressIntervalMs`) restarts it, up to `kBarrierTimeoutMs` (120 s) from the send. |
|
||||
| `MOBILEGL_IPC_COLD_START_MS` | 20000 | (P7) the same wait for `CreatePbufferSurface` / `CreateWindowSurface` / `MakeCurrent` until the session's first `MakeCurrent` is answered ok - the ops a server brings its native backend up inside, lazily. Never shorter than `CONTROL_TIMEOUT_MS`. Measured need: retrace-split spawn legs outlasted 5 s on loaded CI runners, and one outlasted 20 s (run 35912252677, `CreatePbufferSurface` seq 2) - which is why, since p7/spawnhang, this too is a silence budget that the server's `SurfaceProgress` reports restart (row above). The test lever is the server's `MOBILEGL_TEST_DELAY_FIRST_BRINGUP_MS`. |
|
||||
|
||||
**One consequence, stated so it is not rediscovered.** In a build *without*
|
||||
`MOBILEGL_BUILD_DISAGGREGATED`, `MOBILEGL_TRANSPORT=inproc` is accepted by the environment and
|
||||
**silently ignored** — the parser does not exist there, and putting a complaint in the
|
||||
unconditional part of `ConfigLoader` would move a pull-build symbol and break G1. That is
|
||||
precisely the shape of "the split lane ran monolith and went green", so the guard against it is
|
||||
a **build-level** check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||
MG_Remote` must be non-empty in `build-split`, and it is t1's CI job to assert that.
|
||||
|
||||
CMake gained `MOBILEGL_BUILD_DISAGGREGATED_INPROC` (implies `DISAGGREGATED`) and, new here,
|
||||
**`MOBILEGL_BUILD_DISAGGREGATED` now implies `MOBILEGL_PIPE_PUSH`**: the split path decodes
|
||||
into the MGPipe applier and `MOBILEGL_PIPE_PUSH` is what compiles the applier, so
|
||||
`-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone used to configure cleanly and then fail to link — a
|
||||
shape indistinguishable at the CMake level from a legitimate transport-only build.
|
||||
|
||||
---
|
||||
|
||||
## §6 Rulings this file makes that the brief did not, and where the brief is wrong
|
||||
|
||||
Each entry says what would overturn it.
|
||||
|
||||
1. **`CallMask` bits 32..47 are the consumer mask.** R-8 says the client's liveness gates read
|
||||
the `CallMask` mirror, but `CallMask` as declared has only nine feature bits and no
|
||||
per-family bit, so R-8 was not implementable as written. Overturned by: a decision to carry
|
||||
a second mask field in `CapsSnapshot` instead — which costs a schema field and gains
|
||||
nothing, since 16 bits is enough through P8.
|
||||
|
||||
2. **`tableSlotMask` is deleted, not renamed** (R-8 allowed either). Decisive evidence:
|
||||
`GLFunctionsTable` has **69** slots and `ulong` is 64 bits, so the field cannot address the
|
||||
table its own comment names. Overturned by: widening the schema field *and* a reason to
|
||||
keep an explicit slot probe after `ARCHITECTURE.md:114` retired the concept.
|
||||
|
||||
3. **`ResourceFlushRange` carries no bytes at all** (R-13.2 offered "add a blobref" or "write
|
||||
the convention down"; this is a third answer, and a stronger one). The ladder it drives
|
||||
rewrites from the authoritative shadow, which under rule C is server-owned, so
|
||||
`resource_subdata` is already the only path bytes take. Overturned by: evidence that the
|
||||
tier-1 `INVALIDATE_RANGE` arm needs bytes and range in one record. Cannot arise while the
|
||||
verb barrier holds; **revisit when the barrier retires for the buffer family.**
|
||||
|
||||
4. **`ResourceRespecify` also has a SECOND uncarried companion, and the brief does not mention
|
||||
it.** `const MGPRespecifiedLevel* level` (`PipeApply.h:792-795`) is the *scope* of the
|
||||
redefinition and `MGPResourceDesc` cannot express it. Without a carrier every per-level
|
||||
`glTexImage2D` in OpenRA silently takes the whole-resource arm and drops every pending
|
||||
upload — the exact texel loss the server-side set exists to prevent. Ruling: two named
|
||||
fields in the existing pads, zero size change. **c0 rules and specifies; the integrator
|
||||
lands the `MGPipeTypes.h` + `PipeFields.def` edit before w1 encodes this record.**
|
||||
|
||||
5. **Table 1 is 23 rows, not 19.** The brief's 19 and `scout-premortem:§3`'s 19 are different
|
||||
lists; the four only the premortem carries are the server → client ones, and a phase that
|
||||
omits them discovers in week three that it never decided where readback pixels land.
|
||||
|
||||
6. **`SetResidualValueState` is a fourth typed companion, and neither scout nor the brief names
|
||||
it.** `MGPipeApplySetResidualValueState` takes `const ResidualValueBlock&` — not a payload,
|
||||
not a `const void*` — and `MGPResidualValueState` is **never instantiated on the live path**.
|
||||
The encoder has to invent both the record fill and the blob fill. Budget it as w1's hardest
|
||||
row, not as one of the easy `kHasBlob` eight.
|
||||
|
||||
7. **Three of the brief's 19 have no applier entry point at all** — `SetShaderBuffers` (38),
|
||||
`SetStreamOutputTargets` (39), `DrawVbo` (59). `scout-premortem:§3` cites
|
||||
`PipeApply.h:756, 941, 1028-1030` for a six-call row; those five citations cover five *other*
|
||||
calls. For these three, "what crosses today" is **nothing**, and P5 writes the first producer
|
||||
*and* the first consumer.
|
||||
|
||||
8. **`CreateRenderState` and `SetDynamicState` declare a `Blob.Size` that nothing ever reads.**
|
||||
`scout-wire-codec:§4.2`'s "`Size = 0`" column is stale for four rows (add
|
||||
`CreateVertexElements` and `ResourceSubData`'s buffer half). The applier's only four
|
||||
`Blob.Size` reads are `PipeApply.cpp:702`, `:1998`, `:2353`, `:2784`. A fifth
|
||||
flags-vs-payload-vs-signature disagreement for the reviewer's list.
|
||||
|
||||
9. **The emit table is 71 function pointers, not 69.** R-4 says 69 slots; that is
|
||||
`GLFunctionsTable`'s count. The table the client actually installs is
|
||||
`GlobalBackendFunctionsTable` = 69 + `Present` + `SetSwapInterval`. R-4's rule (no null slot,
|
||||
no pass-through) applies to all 71, and `Present` is on the reduced path. The Bool member is
|
||||
not a verb and is answered from `kCapCpuXfbPrimitiveAccounting`.
|
||||
|
||||
10. **`prefersCpuXfbPrimitiveAccounting` is a member of `GLFunctionsTable`
|
||||
(`BackendObject.h:274`), not of `DynamicBackendParameters`.** So it does **not** ride inside
|
||||
`MGPCaps::Dynamic`, and R-8's "same redundancy as (6)" is the wrong frame — it has three
|
||||
spellings and no carrier in `MGPCaps` except the cap bit. Its one non-test client reader is
|
||||
`GL_Query.cpp:221`.
|
||||
|
||||
11. **The persistent-map census is 21 sites, and `MEASUREMENTS.md:111`'s 20 is wrong.** Ruling
|
||||
and the missing site named above. **`ARCHITECTURE.md`'s cited "§5.7" attribution table does
|
||||
not exist in the tree.**
|
||||
|
||||
12. **`ScopedDefaultUnpackState` has six value shadows, not five** (`Managers.cpp:5491-5496`);
|
||||
both the brief and the scout say five.
|
||||
|
||||
13. **`CanTouchGLNow()` guards 16 call sites, not 19.** 19 is the raw grep: 1 definition
|
||||
(`Managers.cpp:928`) + 2 comment mentions (`:1967`, `:1979`) + 16 calls. The brief's "19
|
||||
sites" over-counts. `IsBackendContextCurrentOnThisThread`'s 16 is right.
|
||||
|
||||
14. **`MGPipeApply*` is 37 entry points and 41 call sites**, not "~45 entry points" — and
|
||||
`DirectGLES.cpp:12446` is `Present()`'s fence poll, **not** `ClampSamplesToBackendSupport`
|
||||
(which is `BackendObject_DirectGLES.cpp:807-828`). Table 3's `pActiveBackendObject` row is
|
||||
still correct but the diff is six functions, not one line.
|
||||
|
||||
15. **The 18 `build-split` unit aborts are not a poison problem.** They are
|
||||
`Fatal{ProtocolCorruption}` trip wires the tests *expect*; seven test TUs test
|
||||
`MOBILEGL_PIPE_POISON` without including the only header that defines it, so the macro reads
|
||||
as 0 and they compile the "logs and carries on" arm while `PipeApply.cpp` compiles the
|
||||
aborting one. Invisible in a push build (where it really is 0) and in a verify build (where
|
||||
`-DMOBILEGL_PIPE_VERIFY=1` is on the command line); `MOBILEGL_BUILD_DISAGGREGATED` is the one
|
||||
arming condition behind the header. Fixed in c0's own commit, test-local, no p1 surface.
|
||||
|
||||
16. **`MGHostSpan`, not `MGPHostSpan`.** The header is `MGPipeHostSpan.h`; the struct is
|
||||
`MGHostSpan` (`:28`). `MGPHostSpan` does not exist.
|
||||
|
||||
17. **`ARCHITECTURE.md`'s own corrections, carried here so they are not lost**: `:83` says 61
|
||||
`PipeInputs` fields, it is 63; `:492` cites `PipeStats.h:126` for
|
||||
`MapPersistentRoundtrips`, it is `:141`; ROADMAP's P4a row says six
|
||||
`MGPipeUnmigratedEmulation` sites, it is five calls plus one comment; `ARCHITECTURE.md:19`
|
||||
says eight EGL lifecycle virtuals, there are nine (`ResizeEGLWindowSurface` is the
|
||||
uncounted one).
|
||||
|
||||
---
|
||||
|
||||
## §7 R-15 — getter-shaped slots are answered locally, and the emit table's three classes
|
||||
|
||||
**R-15 (integrator ruling, made after the verb census).** A `GLFunctionsTable` slot whose answer
|
||||
is a **static property of the server's device** is answered on the client **from the caps
|
||||
mirror**. It is never emitted and never `Fatal`. The gate already exists and already runs on
|
||||
every lane: `AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer`
|
||||
(`MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp:580-623`) pins that the caps copy and
|
||||
`glGetIntegeri_v` give one number.
|
||||
|
||||
This settles the census's sharpest finding: `GetIntegeri_v` is reached by the **first
|
||||
`glCompileShader` of every context** (`CompileEnv.cpp:134-138` ← `MG_State/GLState/Core.cpp:39`), not by any verb,
|
||||
so an all-`Fatal` table would abort every scenario before it drew anything — and an emitter for
|
||||
it would be a round trip for six constants the snapshot already carries.
|
||||
|
||||
### The three classes of the 71 slots. c1 does not re-derive this.
|
||||
|
||||
**Class A — answered locally from the caps mirror (2 slots). No record, ever.**
|
||||
|
||||
| slot | answered from |
|
||||
|---|---|
|
||||
| `GetIntegeri_v` (`BackendObject.h:205`) | `MGPCaps::Dynamic.MaxComputeWorkGroupCount` / `MaxComputeWorkGroupSize` (`BackendObject.h:392-393`) — the only indexed pnames the device owns. Every other indexed pname is frontend state and is answered before any table is consulted. |
|
||||
| `IsTimerQuerySupported` (`:245`) | `kCapTimerQuery` (`MGPipeTypes.h:114`). A capability predicate, not a call: today a null slot means `COUNTER_BITS = 0` (`GL_Query.cpp:792`). |
|
||||
|
||||
`GLFunctionsTable::PrefersCpuXfbPrimitiveAccounting` (`:274`) is in the same class by the same
|
||||
argument — `kCapCpuXfbPrimitiveAccounting` — and is not a slot.
|
||||
|
||||
**Class B — emitted in P5 (5 slots).** The verb census's answer, and nothing else:
|
||||
`Clear`, `DrawArrays`, `ReadPixels`, `BlitFramebuffer`, `Present`.
|
||||
`Present` is in this class despite having **zero `MG_Impl` call sites** — it is reached through
|
||||
`EGLImpl.cpp:178` → `BackendObject.cpp:396`, so c1 cannot find it by mirroring GLImpl.
|
||||
|
||||
**Class C — `Fatal{UnmigratedVerb, "<slot>"}` (64 slots).** Everything else, including
|
||||
`SetSwapInterval`, `GetGpuTimestampNs` (a live GPU timestamp, not a static property, so **not**
|
||||
class A), and the whole sync / query / transform-feedback / compute / copy / mipmap surface.
|
||||
|
||||
### The cross-cutting rule R-4 would otherwise break
|
||||
|
||||
**Forty-one of the 69 slots are null-checked at their call site, and several of those null checks
|
||||
are CAPABILITY PROBES rather than safety checks.** R-4 forbids a null slot — so in the emit table
|
||||
every one of those probes answers "supported" and the fallback behind it silently disappears.
|
||||
That is not a theoretical risk: it is how a split lane produces a plausible picture for the wrong
|
||||
reason. Three named cases; the rule generalises to all 41.
|
||||
|
||||
| probe site | what it decides today | reads instead |
|
||||
|---|---|---|
|
||||
| `GL_Query.cpp:481`, `:785` — `BeginOcclusionQuery != nullptr` | whether the target is rejected outright | `kCapOcclusionQuery` |
|
||||
| `GL_Query.cpp:534` — the `BeginXfbPrimitivesQuery` ternary | GPU query vs CPU primitive accounting | `kCapXfbPrimitivesQuery` |
|
||||
| the `SubDataResident` op-table slot | whether the resident-upload path exists at all | `kCapResidentSubData` |
|
||||
|
||||
**A null check on a slot may never survive into the client under split.** It becomes a caps-mirror
|
||||
read — class A's mechanism — whatever class the slot itself is in. That is exactly
|
||||
`ARCHITECTURE.md:114`'s "`CallMask` replaces 'is this table slot null' as the implicit capability
|
||||
probe", now with a concrete list of what has to move.
|
||||
|
||||
---
|
||||
|
||||
## §7b R-17 — `MGPWireRecHeader::Flags` is ring framing, never call flags
|
||||
|
||||
**The wire record header and the ring record header are the same eight bytes**, and
|
||||
`MGPWireRecHeader::Flags` **is** `RingRecordHeader::flags`. The two enums that name those bits
|
||||
overlap and disagree:
|
||||
|
||||
| bit | `MGPipeCallFlags` | `RingRecordFlags` | |
|
||||
|---|---|---|---|
|
||||
| 0 | `kNeedsAck` | `kRecNeedsAck` | agree |
|
||||
| 1 | `kHasBlob` | `kRecHasBlob` | agree |
|
||||
| 2 | `kVarTail` | `kRecPad` | **collide** |
|
||||
| 3 | `kHostSpan` | `kRecBorrowSlot` | **collide** |
|
||||
| 4 | `kReplySlot` | `kRecVarTail` | **collide** |
|
||||
| 5 | `kOptional` | — | no counterpart |
|
||||
|
||||
**Stamping `MGPipeCallFlagsFor(op)` into that field is a silent, data-dependent corruption**, and
|
||||
the first two bits agreeing is exactly what makes it survive a debugger.
|
||||
|
||||
**One of the three is already defended, and only one.** `RingProducer::Reserve` masks `kRecPad`
|
||||
out of whatever the caller passes (`Ring.cpp`: `flags & ~kRecPad`), so a stamped `kVarTail` does
|
||||
**not** delete the record for anyone who goes through `Reserve`. That defence is exactly one bit
|
||||
wide:
|
||||
|
||||
- `kVarTail` → `kRecPad`: **masked by `Reserve`.** But the mask is not in the path of a producer
|
||||
that writes the header *itself* — which is precisely what a codec with its own header struct
|
||||
does, since these are the same eight bytes. Then `Pop` skips the record as a wrap filler and
|
||||
it vanishes with no error raised anywhere.
|
||||
- `kHostSpan` → `kRecBorrowSlot`: **undefended.** The consumer believes the record borrowed a
|
||||
slot into the GPU timeline, so it retires on `completedFrameSerial` instead of on apply.
|
||||
- `kReplySlot` → `kRecVarTail`: **undefended.** The record claims a variable tail it does not
|
||||
have — and after R-16 this now fires on **fourteen** rows rather than ten.
|
||||
|
||||
So the failure is not one dramatic disappearance; it is a lifetime lie and a phantom tail on
|
||||
every affected record, plus a disappearance only on the path that skips `Reserve`. The test
|
||||
below pins all three shapes, including the masking, so the defence cannot quietly go away either.
|
||||
|
||||
**Ruling: the two spaces are disjoint by translation, not shared.** The encoder maps one to the
|
||||
other explicitly and nothing else writes the field. The call's flags are **not on the wire at
|
||||
all** and do not need to be — the opcode is, and `MGPipeCallFlagsFor(op)` recovers them exactly
|
||||
on either side.
|
||||
|
||||
Enforcement, split by what each file can see:
|
||||
|
||||
- the generator pins each `MGPipeCallFlags` bit **read out of `MGPipe.h`**, plus
|
||||
`kMGPipeCallFlagsAllBits == 0x3F`, so a renumbering or a seventh flag is a build break;
|
||||
- `MG_Test/Wire/RingTest.cpp` holds the **cross-enum table** — the one place in the tree that
|
||||
sees both, since MG_Pipe is below MG_Remote and may not include `Ring.h`. It asserts all six
|
||||
correspondences and both totals, and then *demonstrates* the defect: a record framed with
|
||||
`draw_vbo`'s call flags is popped as a wrap filler and vanishes, while the same record framed
|
||||
with `kRecVarTail` round-trips.
|
||||
|
||||
The generated comment on the field said "MGPipeCallFlags of the call, for asserts and tracing",
|
||||
which **invited** the defect. It now says what the field is for.
|
||||
|
||||
---
|
||||
|
||||
## §8 Ownership amendments
|
||||
|
||||
- **`MobileGL/MG_Pipe/MGPipeTypes.h` is c0's file** (integrator ruling A; the BRIEF §5 ownership
|
||||
table is amended). It was unowned, which is how the respecify-scope gap in table 1 row 19b had
|
||||
no one to close it. A package that needs a payload struct shape changed goes through the
|
||||
integrator, as with the three `.def` files.
|
||||
- Consequently the row-19b carrier is **landed, not merely specified** — see §2 table 1 row 19b
|
||||
and `MGPipeTypes.h`'s `HasRespecifiedLevel` / `RespecifiedUploadTarget` / `RespecifiedLevel`
|
||||
and the five `MGPipeRespecify*` helpers beside them.
|
||||
|
||||
@@ -0,0 +1,485 @@
|
||||
# CONTRACT-P5B — the wire contract for the 25 measured class-C slots
|
||||
|
||||
Authority: this file, beside `CONTRACT-P5.md`, which stays authoritative for everything it
|
||||
covers (table 0's encodings, the byte carriers, field ownership, role/thread ownership, the
|
||||
knobs, R-1…R-17). This file ADDS rows to those tables for the verb-migration phase P5b
|
||||
(`~/w7/notes/p5/INTEGRATOR-DECISIONS.md` ID-66, ID-68; the census
|
||||
`~/w7/notes/p6/census-classC.md`) and makes the rulings the census left open. Where it
|
||||
disagrees with CONTRACT-P5.md, this file is newer and wins, and §6 lists every such place.
|
||||
|
||||
**How to change it.** Package c0b's file, edited by the integrator first. A migration package
|
||||
that needs a row changed goes through the integrator; the packages compile against the rows
|
||||
below from day one, which is the whole reason the file exists.
|
||||
|
||||
Base: `feat/disaggregated @ c9878d69` (P5's c1g landed). Every `file:line` below was read at
|
||||
that commit.
|
||||
|
||||
---
|
||||
|
||||
## §0 What P5b is, and the four rules above every row
|
||||
|
||||
P5 landed the first IPC frame on the reduced path: six verbs cross the wire to a real apply
|
||||
thread under `MOBILEGL_TRANSPORT=inproc`, and 64 of the 71 client slots are
|
||||
`Fatal{UnmigratedVerb, "<slot>"}`. The census measured which of the 64 real workloads hit
|
||||
first: **every Minecraft trace first-stops at `DrawElements`**
|
||||
(`improved-transparency-minecraft-26.3` at `DrawElementsInstancedBaseVertex`), and the
|
||||
inproc integration lane's 505 verb aborts are led by `BindImageTexture` 138,
|
||||
`BeginTransformFeedback` 95, `DrawElements` 56, `DispatchCompute` 49, `PatchParameteri` 43,
|
||||
`CopyImageSubData` 30, `MemoryBarrier` 18, `MultiDrawElementsBaseVertex` 18, `ClearBufferiv`
|
||||
12, `ClearBufferuiv` 8 and a measured tail of 15. **P5b migrates those 25, in four packages,
|
||||
under inproc, with the lane's abort count and the traces' first-blocker list as the only
|
||||
gates.**
|
||||
|
||||
The four rules every row below is held to. A, B and C are CONTRACT-P5 §0's, restated only
|
||||
where P5b sharpens them; D is new.
|
||||
|
||||
**Rule A — a content record declares its bytes.** Unchanged. P5b adds ONE content-carrying
|
||||
row (`set_storage_block_binding`'s block name) and it is `kHasBlob`, staged whole in
|
||||
`SEG_STAGE`, `Size = strlen + 1`, never 0.
|
||||
|
||||
**Rule B — no host pointer crosses.** Unchanged, and P5b is the phase that ARMS the one
|
||||
conditional `MGHostSpan` on the hot path: `draw_vbo`'s user-index span
|
||||
(CONTRACT-P5 table 1 row 16). d1 fills it with `Ptr = nullptr, Seg = SEG_STAGE`, the encoder
|
||||
and decoder run all four honesty arms on it, and the sink resolves it through
|
||||
`MG_Pipe::MGPipeHostBytes`. `kCapNeedsHostIndexBytes` and `kCapNeedsHostUboBytes` STAY 0 in
|
||||
P5b — they are about the SERVER needing the bytes of VBO-backed index buffers (P8's index host
|
||||
mirror, ARCHITECTURE §10.3), not about a client array the client alone holds.
|
||||
|
||||
**Rule C — an applier entry point may not hold a pointer past its return.** Unchanged, and
|
||||
it now binds the sinks: every `WireVerbSink::On*` argument that points into a record or a
|
||||
staged run (`ranges`, `userIndices`, `indirect`, the block `name`) is valid for the call only.
|
||||
|
||||
**Rule D — a P5b verb crosses AS THE CALL, not as a reading of it.** A record for one of the
|
||||
25 carries the GL arguments the frontend handed the backend slot - the GL enums verbatim,
|
||||
the GL names the backend keys on, the offsets as the app spelled them - plus the handle the
|
||||
P7/P8 form will dispatch on instead. The server's `ServerVerbSink` reproduces the backend
|
||||
call the monolith makes, and the backend keeps reading the frontend state it reads today
|
||||
through the BARRIER-PULLED fields of its verb class (CONTRACT-P5 table 2, `rsp`). That is
|
||||
what makes migrating a verb a two-file change per slot rather than a backend rewrite, it is
|
||||
why the monolith adapter is byte-identical (nothing on the monolith path changes at all),
|
||||
and it is also the honest statement of the debt: P5b makes the verbs CROSS; retiring the
|
||||
pulls they read is P3b/P4b/P7/P8's, exactly as CONTRACT-P5 table 2 already says, one row per
|
||||
field. Under `MOBILEGL_IPC_STRICT_ERRORS=1` every such pull is still `Fatal`, so the debt
|
||||
stays measurable.
|
||||
|
||||
**The census's correction, made structural.** None of the five P5 class-B verbs has an
|
||||
`MGPipeApply*` entry point; they reach `WireVerbSink`. None of the 25 P5b slots has one
|
||||
either and NONE GAINS ONE (§6.1). The seam a migration package writes is one
|
||||
`ServerVerbSink::On<Row>` body plus one emit-table row on the client - and both already exist
|
||||
as named stubs on this head.
|
||||
|
||||
---
|
||||
|
||||
## §1 Table 0 additions — encodings the P5b rows introduce
|
||||
|
||||
| field | the ruling | zero means | who reads it |
|
||||
|---|---|---|---|
|
||||
| **`MGPClear::Kind` / `::ValueClass`** | `kMGPipeClearKind{Whole=0, Color=1, Depth=2, Stencil=3, DepthStencil=4}`, `kMGPipeClearValueClass{Float=0, Int=1, Uint=2}`, in `MGPipeTypes.h` beside the struct. **One spelling.** Through P5 the numbers lived in `Client/EmitTables.h` and `Server/PipeApplier.h` as two hand-minted copies, each flagged for the integrator; both now alias the contract's. The GL-entry → Kind map is written beside the constants. | `Whole` with `BufferMask = 0`: a clear of nothing, legal | f1's emitters, `ServerVerbSink::OnClear` (live for all five kinds since v1) |
|
||||
| **`MGPDrawFlagBit::kDrawIsIndirect` (1<<5)** | The draw's ranges come from a `GL_DRAW_INDIRECT_BUFFER`; `draw_vbo`'s SECOND TAIL is one `MGPDrawIndirect` instead of the user-index span. **Exclusive with `kDrawHasUserIndices`** and **`NumDraws` must be 0** - both `Fatal{ProtocolCorruption}` at the layout, on both sides. | clear = the range tail describes the draw | d1's emitters, the layout, `OnDrawVbo` |
|
||||
| **`draw_vbo`'s second tail** | `WireRecordLayout::SecondTailIsHostSpans` says what it is: spans (`kDrawHasUserIndices`, and `set_shader_buffers` always) or the indirect block (`kDrawIsIndirect`). The encoder's host-span honesty pass reads the flag, not "a second tail exists" - a 40-byte indirect block read as spans is one span and a quarter of garbage. | — | codec |
|
||||
| **GL enums on the wire** | `MGPMipPlan::Target`, `MGPCopyRegion::SrcTarget/DstTarget`, `MGPCopyFromFramebuffer::Target/InternalFormat`, `MGPImageBind::Access/Format`, `MGPPatchParameter::Pname`, `MGPMemoryBarrier::Bits`: **the GL token verbatim**, in a field wide enough for it (every GL texture target and access token fits a `Uint16`; formats and bitfields take a `Uint32`). Rule D: the sink passes the token to the backend slot that takes it. NOT `MGPipeResourceTarget`: the tree has no resource-target → GL-enum inverse to spend on a field the backend only ever forwards. | 0 = no target / no bits, refused where the backend would refuse it | sinks |
|
||||
| **`MGPImageBind::Access`** | the GL access token (`GL_READ_ONLY` 0x88B8 …), NOT `MGPImageView::Access`'s three-value encoding (CONTRACT-P5 table 0 row `MGPImageView::Access`). Two different records, two different jobs: the set record describes a resolved unit for the draw prep, this record reproduces a call. | — | `OnBindShaderImage` |
|
||||
| **GL names on the wire (`GlName` fields)** | `MGPCopyRegion::SrcGlName/DstGlName`, `MGPImageBind::GlName`, `MGPStreamOutputBind::GlName`, `MGPStorageBlockBinding::GlName`. **A GL name is never an identity** (ARCHITECTURE 4.2.1); it is either the key the backend has ALWAYS used (`XfbImpl::g_xfbObjects[name]`, `DirectGLES.cpp:1401`) or the key of a BARRIER-PULLED sticky forward the P5b sink resolves through (`MGB_CTX->GetTextureObject(name)`, `GetProgramObject(name)` - CONTRACT-P5 table 2's forward rows, retired by P7/P9). Every such field sits beside the handle that replaces it. | 0 = GL's name 0 (the default object / no texture) | sinks, through the forwards |
|
||||
| **the block name blob** | `MGPStorageBlockBinding::Name`: `SEG_STAGE`, `Size = strlen + 1`, the NUL travels. Decoder bound: 4096 bytes; a run whose last byte is not NUL is `Fatal{ProtocolCorruption, "SetStorageBlockBinding.Name"}`. | `Size == 0` is Fatal (rule A) | decoder |
|
||||
|
||||
---
|
||||
|
||||
## §2 Table 1 additions — the 25 slots, by package
|
||||
|
||||
Columns per slot: **wire record** (row it rides, every new field and its carrier) · **class**
|
||||
(all move C → B when the package flips them) · **sink** (`WireVerbSink` method; `ServerVerbSink`
|
||||
override, a named-Fatal stub on this head) · **reply slot** · **thread rule** · **refusals** ·
|
||||
**monolith**.
|
||||
|
||||
Every row: **no reply slot** (nothing the client acts on comes back - ID-31/R-5 apply to
|
||||
the four acceptance rows only), **thread rule = CONTRACT-P5 table 3 unchanged** (the verb
|
||||
barrier holds; the client's `EmitAndWait` blocks until `appliedSeq >= seq`; the server stamps
|
||||
the verb boundary named in `FieldOwnership.def`'s `MGP_VERB_OP_LIST` before the sink runs;
|
||||
`gPipeInputs` has one writer at a time), **monolith = untouched** (the client emit table
|
||||
exists only under split; the backend slot is called directly as today; G2/G14 name-for-name).
|
||||
Those three columns are therefore stated once here and the rows carry only what differs.
|
||||
|
||||
Refusal vocabulary, shared by all four packages: a record the codec cannot prove is
|
||||
`Fatal{ProtocolCorruption, "<row>.<field>"}`; a shape the sink does not implement yet is
|
||||
`Fatal{UnmigratedVerb, "<GL slot>[+<QUALIFIER>]"}` - the SAME family the client's class-C
|
||||
table raises and the census greps, with the qualifier naming the arm
|
||||
(`DrawElements+CLIENT_INDICES`, `CopyImageSubData+RENDERBUFFER`,
|
||||
`ClearNamedFramebufferfv+UNBOUND`); a backend that leaves the slot null DECLINES
|
||||
(`return false`), which is the monolith's null-slot answer in the same words.
|
||||
|
||||
### d1 — indexed / instanced / multi-draw / indirect (10 measured + 9 companions)
|
||||
|
||||
**One row: `draw_vbo` (59), `MGPDrawInfo` + `MGPDrawRange[NumDraws]` + a conditional second
|
||||
tail.** ROADMAP P8's "`draw_vbo` 收编 multi-draw 族" is exactly this: the twenty GL draw entry
|
||||
points collapse onto one record, and the `MGPDrawRange` array already has the shape the
|
||||
`glMultiDraw*` family has. The P5 sink (`Server/PipeApplier.cpp` `OnDrawVbo`) implements
|
||||
`NumDraws == 1`, non-instanced, VBO-backed; d1 fills in the cross product.
|
||||
|
||||
| slot (census entries) | record fields | sink dispatch (`GLFunctionsTable` slot the sink calls) |
|
||||
|---|---|---|
|
||||
| `DrawElements` (56 + every Minecraft trace) | `IndexSize` = 1/2/4 from `type`; `IndexResource` = the VAO's bound element buffer handle (the same handle `set_index_buffer` (32) carried at validate); one range `{Start = indices / IndexSize, Count = count, IndexBias = 0}`; `Flags = 0` | `DrawElementsBaseVertex(mode, count, type, (void*)(Start*IndexSize), 0)` - the P5 arm, unchanged |
|
||||
| `DrawElementsBaseVertex` (4) | as above, `IndexBias = basevertex` | `DrawElementsBaseVertex(…, basevertex)` |
|
||||
| `DrawElementsInstancedBaseVertex` (trace-only, `improved-transparency-minecraft-26.3`) | `InstanceCount = instancecount`, `IndexBias = basevertex` | `DrawElementsInstancedBaseVertex(mode, count, type, offset, instancecount, basevertex)` |
|
||||
| `DrawArraysInstanced` (2) | `IndexSize = 0`, range `{first, count, 0}`, `InstanceCount` | `DrawArraysInstanced` |
|
||||
| `DrawArraysInstancedBaseInstance` (4) | + `StartInstance = baseinstance`. The vertex-FETCH base instance already crossed in `set_vertex_buffers::BaseInstance` at validate (D-H1; `MGP_SET_BASE_INSTANCE` runs before `MGP_FILL`, `GL_Drawing.cpp:663-688`); `StartInstance` is `gl_BaseInstance`'s value and feeds the GL call | `DrawArraysInstancedBaseInstance` |
|
||||
| `MultiDrawArrays` (2) | `IndexSize = 0`, `NumDraws = drawcount`, range i = `{first[i], count[i], 0}` | `MultiDrawArrays(mode, first[], count[], drawcount)` - the sink rebuilds the two arrays from the ranges (bounded locals; rule C) |
|
||||
| `MultiDrawElements` (2) | `NumDraws = drawcount`, range i = `{indices[i] / IndexSize, count[i], 0}` | `MultiDrawElementsBaseVertex(mode, count[], type, offsets[], drawcount, zeros[])` |
|
||||
| `MultiDrawElementsBaseVertex` (18) | range i `IndexBias = basevertex[i]` | `MultiDrawElementsBaseVertex(…, basevertex[])` |
|
||||
| `MultiDrawElementsIndirect` (2) | `Flags = kDrawIsIndirect`, `NumDraws = 0`, second tail `MGPDrawIndirect{Buffer = the bound GL_DRAW_INDIRECT_BUFFER handle, ParameterBuffer = null, Offset = (Uint64)indirect, ParameterOffset = 0, Stride = stride, DrawCount = drawcount}` | `MultiDrawElementsIndirect(mode, type, (void*)Offset, DrawCount, Stride)` |
|
||||
| `MultiDrawArraysIndirectCount` (2) | as above with `IndexSize = 0`, `ParameterBuffer = the bound GL_PARAMETER_BUFFER handle`, `ParameterOffset = (Uint64)drawcount` (the GLintptr the call spells `drawcount`), `DrawCount = maxdrawcount` | `MultiDrawArraysIndirectCount(mode, (void*)Offset, (GLintptr)ParameterOffset, DrawCount, Stride)` - the backend reads the count where it does today (`DirectGLES.cpp:6510`) |
|
||||
| companions, unmeasured, same row, d1 owns the slots: `DrawElementsInstanced`, `DrawElementsInstancedBaseInstance`, `DrawElementsInstancedBaseVertexBaseInstance`, `DrawRangeElements`, `DrawRangeElementsBaseVertex` (`Flags |= kDrawHasIndexRange`, `MinIndex/MaxIndex = start/end`), `MultiDrawArraysIndirect`, `MultiDrawElementsIndirectCount`, `DrawArraysIndirect`, `DrawElementsIndirect` (`kDrawIsIndirect`, `DrawCount = 1`) | | |
|
||||
|
||||
**Sink:** `WireVerbSink::OnDrawVbo(const MGPDrawInfo&, const MGPDrawRange* ranges, const
|
||||
MGHostSpan* userIndices, const MGPDrawIndirect* indirect)` - the P5 signature plus the
|
||||
indirect block. On this head the sink declines by name: `MultiDrawArrays` /
|
||||
`MultiDrawElements` for `NumDraws != 1`, `DrawArraysInstanced` / `DrawElementsInstanced` for
|
||||
the instanced arms, `DrawElements+CLIENT_INDICES` for a span,
|
||||
`MultiDraw{Arrays,Elements}Indirect` for the block.
|
||||
|
||||
**Client-side index arrays (no element buffer bound) — the P8 resolve-on-client rule,
|
||||
applied.** The bytes exist on the client only, so the client resolves them: d1's emitter
|
||||
stages `count * IndexSize` bytes at `indices` into `SEG_STAGE` and names the run in the
|
||||
existing conditional `MGHostSpan` tail (`Flags |= kDrawHasUserIndices`, `Ptr = nullptr, Seg =
|
||||
SEG_STAGE`). The codec already runs all four honesty arms on it
|
||||
(`PipeWireCodec.cpp` `CheckHostSpanIsHonest(span, segments)`); the sink resolves it with
|
||||
`MGPipeHostBytes` and passes the pointer as `indices`. This is the shape CONTRACT-P5 table 1
|
||||
row 16 froze and table 0 said "P5 must produce none of"; P5b produces it for exactly the
|
||||
case where nothing else can. **`kCapNeedsHostIndexBytes` stays 0**: it asks for the bytes of
|
||||
VBO-backed index buffers (restart rewriting, multi-draw flattening) and is answered by P8's
|
||||
`Server/IndexHostMirror` (ARCHITECTURE §10.3), not by a per-draw span. **A multi-draw with
|
||||
client-side indices** (`indices[i]` are `drawcount` separate client pointers, one span can
|
||||
name one run) is refused by name, `Fatal{UnmigratedVerb, "MultiDrawElements+CLIENT_INDICES"}`,
|
||||
until P8's `HostResolve.cpp` flattens it; no measured workload has one. **P8 amendment (ID-P8-6):**
|
||||
the client flattens client indices into an owned EBO (`EmitTables.cpp:688-709`), so only a foreign
|
||||
peer can reach the server latch, which is now `ProtocolCorruption`.
|
||||
|
||||
**Client-side VERTEX arrays** (attribute pointers into client memory, no VBO) are NOT d1's
|
||||
and are not a wire question: Espryt's `SyncClientSideAttributesForDrawArrays`
|
||||
(`DirectGLES.cpp:6284-6291`) and its `DrawElements` twin upload them on the SERVER by reading
|
||||
the frontend VAO through the BARRIER-PULLED `GetBoundVertexArray` (CONTRACT-P5 table 2, "P8").
|
||||
Under inproc that read dereferences the client's pointers in the same address space and is
|
||||
correct by the barrier; under spawn (P6) it is exactly the pull P8's `HostResolve.cpp`
|
||||
(client array ranges, max-index scan) retires. d1 changes nothing there.
|
||||
|
||||
**`MinIndex / MaxIndex`** stay `~0` (unknown) for every P5b draw except the two
|
||||
`DrawRangeElements*` companions; **`XfbCpuCapturedVertices`** rides only when the client's
|
||||
XFB accounting has a value (`kDrawHasXfbCount`), which t2's rows make possible;
|
||||
**`RestartIndex` / `kDrawPrimitiveRestart`** carry the frontend's primitive-restart state
|
||||
verbatim (the backend's `ScopedRestartIndexSubstitution` reads the index bytes it needs on
|
||||
its side - from the bound buffer, or from the resolved span).
|
||||
|
||||
**Stamp:** `DrawVbo → DrawArrays` (all twenty share `kDraw`'s mask; the Fatal NAME a pull
|
||||
prints is `DrawArrays` for every one of them - known, accepted, and the reason d1 reports
|
||||
next blockers by the wire's `UnmigratedVerb` name, which is exact).
|
||||
|
||||
**d1's gate:** the lane's `DrawElements` / `MultiDrawElementsBaseVertex` / `DrawArrays*` /
|
||||
`DrawElementsBaseVertex` / `MultiDraw*` first-blocker entries → 0, each affected scenario at
|
||||
its NEXT first blocker by name; **and every Minecraft trace reaches its next first blocker
|
||||
under inproc, reported by name** (the census could not see past `DrawElements`).
|
||||
|
||||
### i1 — image / compute / barrier / copy-image / storage block (5 measured + 2 companions)
|
||||
|
||||
| slot (entries) | row (opcode) | record | sink | on this head |
|
||||
|---|---|---|---|---|
|
||||
| `BindImageTexture` (138) | **`bind_shader_image` (72), NEW**, `MGPImageBind` 40 B | `Res` = the texture's handle (null for 0), `Unit`, `GlName`, `Level`, `Layer`, `Layered`, `Access` = GL token, `Format` = GL enum. Emitted at the call (`GL_Texture.cpp:6736`), AFTER the frontend has written the unit's `ImageTextureBinding` and `MGP_FILL(BindImageTexture)` has run - the record's verb boundary is what makes the server's read of that binding legal. `set_shader_images` (37) still travels at the next validate, untouched: it is the draw-prep set, this is the call. | `OnBindShaderImage(const MGPImageBind&)` → `GL.BindImageTexture(Unit, GlName, Level, Layered, Layer, Access, Format)`. Espryt ignores everything but `Unit` and syncs the unit's binding from the barrier-pulled `GetImageTextureBinding` (`DirectGLES.cpp:9154`, `:2471`); Magma's slot is a no-op (`DirectVulkan.cpp:665`), so the same call is right for both. | `Fatal{UnmigratedVerb, "BindImageTexture"}` stub |
|
||||
| `DispatchCompute` (49) | `launch_grid` (60), `MGPGridInfo` | `GridX/Y/Z` = the three counts; `Block* = 0` in P5b (the local size is a link artifact the backend reads from its own program; P7 Magma may fill it from the reflection archive); `IsIndirect = 0`. Companion `DispatchComputeIndirect`: `IsIndirect = 1`, `IndirectBuffer` = the bound `GL_DISPATCH_INDIRECT_BUFFER` handle, `IndirectOffset = indirect`. The emitter keeps b1's pre-verb hook ORDER the class-C stub already has: `PushPersistentMapsBeforeVerb(); MarkGpuWritesForDispatch();` then the record (`EmitTables.cpp` `DispatchCompute_Unmigrated`). | `OnLaunchGrid(const MGPGridInfo&)` → `GL.DispatchCompute(GridX, GridY, GridZ)` / `GL.DispatchComputeIndirect(IndirectOffset)` | stub, `"DispatchCompute"` / `"DispatchComputeIndirect"` by `IsIndirect` |
|
||||
| `MemoryBarrier` (18) | `memory_barrier` (61), `MGPMemoryBarrier` | `Bits` = the GLbitfield verbatim (the frontend already validated it and already folds `glTextureBarrier` onto it, `GL_Drawing.cpp:920-937`); `ByRegion = 0`. Companion `MemoryBarrierByRegion`: `ByRegion = 1`. | `OnMemoryBarrier(const MGPMemoryBarrier&)` → `GL.MemoryBarrier(Bits)` / `GL.MemoryBarrierByRegion(Bits)`; Espryt's atomic-counter lowering stays inside the backend (`DirectGLES.cpp:8836`) | stub |
|
||||
| `CopyImageSubData` (30) | `resource_copy_region` (53), `MGPCopyRegion` **64 → 72 B** | `Src/Dst` handles (kind Texture, or Renderbuffer when the target is `GL_RENDERBUFFER`), `SrcTarget/DstTarget` = the GL targets verbatim, `SrcBox = {srcX, srcY, srcZ, w, h, d}`, `DstX/Y/Z`, `SrcLevel/DstLevel`, **`SrcGlName/DstGlName`** (new). | `OnResourceCopyRegion(const MGPCopyRegion&)` → rebuilds two `CopyImageEndpoint`s (`BackendObject.h:33`) from the GL names through the barrier-pulled `MGB_CTX->GetTextureObject(name)` (`rsp`) and calls `GL.CopyImageSubData(src, SrcTarget, SrcLevel, SrcBox.X/Y/Z, dst, DstTarget, DstLevel, DstX/Y/Z, SrcBox.W/H/D)`. **A renderbuffer endpoint is refused by name** (`"CopyImageSubData+RENDERBUFFER"`): no sticky forward hands out a renderbuffer object, and P7 is where the backend takes handles. | stub |
|
||||
| `ShaderStorageBlockBinding` (4) | **`set_storage_block_binding` (75), NEW**, `MGPStorageBlockBinding` 40 B, `kHasBlob` | `ShaderCso` = the program's CSO handle, `GlName` = the GL program name, `Binding`, `Name` = the block name staged in `SEG_STAGE`, `Size = strlen + 1`. Emitted at `GL_Program.cpp:3394` after the frontend recorded the binding on the program (`SetShaderStorageBlockBinding`, `:3392` - which is what reseeds a rebuilt backend program and what `GL_BUFFER_BINDING` reports; the record is the "push it onto the already-built driver program" optimisation Espryt performs, `DirectGLES.cpp:9182-9218`). | `OnSetStorageBlockBinding(const MGPStorageBlockBinding&, const char* name)` → `GL.ShaderStorageBlockBinding(GlName, name, Binding)`; both backends resolve the program through the barrier-pulled `GetProgramObject(GlName)` / `TryGetDirectVulkanProgram` (`rsp`, P9). | stub |
|
||||
|
||||
**The `copy-image-shadow-mirror` emulation** (`DirectGLES.cpp:8997`, `MGPipeUnmigratedEmulation`
|
||||
→ `Fatal{UnmigratedEmulation}` under any non-monolith transport, `PipeApply.cpp:2947`).
|
||||
Migrating `CopyImageSubData` moves the first blocker from the client's
|
||||
`Fatal{UnmigratedVerb, "CopyImageSubData"}` to the server's
|
||||
`Fatal{UnmigratedEmulation, "copy-image-shadow-mirror"}` on every Espryt copy between two
|
||||
textures with CPU shadows. **Ruling (i1): under split the server SKIPS the mirror** - the site
|
||||
becomes `if (monolith) mirror else skip`, behind `#if MOBILEGL_BUILD_DISAGGREGATED` so the
|
||||
pull build's code does not move (G1) - and the client-side mirror ROADMAP P8 names ("CopyImage
|
||||
镜像搬到 client") is P8's. What the skip loses is bounded by two Fatals: a later `glGetTexImage`
|
||||
of the destination served from the client shadow (class C, wave 3, P9) and a texture re-mint
|
||||
that re-uploads it (`texture-remint-pull`, also Fatal). i1 reports which scenarios reach either.
|
||||
|
||||
**Stamps:** `BindShaderImage → BindImageTexture` (kTextureOp), `LaunchGrid → DispatchCompute`
|
||||
(kDispatch; `GetProgramForDispatch` is FATAL in CONTRACT-P5 table 2 because "there is no
|
||||
compute on the reduced path" - **i1 moves it to BARRIER_PULLED, "P7 (Magma), P8 (Espryt)"**, the
|
||||
same shape as `GetProgramForDraw`, in `FieldOwnership.def`; that is a one-row edit in a c0b file
|
||||
and i1 may make it), `MemoryBarrier → MemoryBarrier`, `ResourceCopyRegion → CopyImageSubData`
|
||||
(new row, §6.3), `SetStorageBlockBinding → ShaderStorageBlockBinding`.
|
||||
|
||||
**i1's gate:** the lane's five first-blocker counts → 0; each affected scenario at its next
|
||||
first blocker by name (the census predicts `DispatchCompute` → `MemoryBarrier` → readback for
|
||||
the image-store cases, and the emulation Fatal for some copies).
|
||||
|
||||
### t2 — transform-feedback spans, the XFB object bind, the patch parameter (3 measured + 4 companions)
|
||||
|
||||
| slot (entries) | row (opcode) | record | sink | on this head |
|
||||
|---|---|---|---|---|
|
||||
| `BeginTransformFeedback` (95) | `begin_stream_output` (62), `MGPStreamOutputBegin` | `PrimitiveMode` verbatim. Emitted at `GL_Drawing.cpp:1274` after the frontend's `BeginTransformFeedback(primitiveMode, program)` recorded the span - the capture program and the capture-buffer bindings the backend reads at the next draw (`StartPendingTransformFeedback`, `DirectGLES.cpp:1224-1250`) are `kXfbSpan`/`kDraw` BARRIER-PULLED fields (`GetTransformFeedbackProgram`, `GetBufferBindingPoint`…). **`set_stream_output_targets` (39) is NOT required for t2**: it has no applier and no producer, and the pulls cover the bindings while the barrier holds. Producing it is P9's (XFB scatter) and would need the consumer nobody can test yet (CONTRACT-P5 §7 `WireVerbSink` header). | `OnBeginStreamOutput(const MGPStreamOutputBegin&)` → `GL.BeginTransformFeedback(PrimitiveMode)` | stub |
|
||||
| `PatchParameteri` (43) | **`patch_parameter` (73), NEW**, `MGPPatchParameter` 8 B | `Pname = GL_PATCH_VERTICES`, `Value`. Espryt pushes `glPatchParameteri` at the call (`DirectGLES.cpp:8760`) and NOT from its draw sync, so `set_patch_state` (43) - which carries the same number into the applier's working block at validate - cannot stand in; both travel, as both pushes happen today. Emitted at `GL_Drawing.cpp:844`. Magma registers no slot: the sink's null-slot DECLINE is the monolith's null-slot skip in the same words. | `OnPatchParameter(const MGPPatchParameter&)` → `GL.PatchParameteri(Pname, Value)` | stub |
|
||||
| `BindTransformFeedback` (2) | **`bind_stream_output` (74), NEW**, `MGPStreamOutputBind` 16 B | `GlName` (Espryt's own key, `XfbImpl::g_xfbObjects[name]`, `DirectGLES.cpp:1401`; 0 = the default object), `LifetimeId` (the D21 counter-slot rekey's key; the frontend's `GetBoundTransformFeedbackLifetimeId`). Emitted at `GL_Drawing.cpp:1673` after `BindTransformFeedbackObject(id)`. | `OnBindStreamOutput(const MGPStreamOutputBind&)` → `GL.BindTransformFeedback(GlName)` | stub |
|
||||
| companions, same rows, t2 owns the slots: `EndTransformFeedback` → `end_stream_output` (63) `MGPXfbAccounting{CapturedVertices, PrimitivesWritten, PrimitiveMode}` = the frontend's own accounting at the end (`GL_Drawing.cpp:1371`; the client's fence wait there is dropped per ARCHITECTURE 8.5 / CONTRACT-P5 table 2's "two new producers" - the capture targets are marked GPU-written instead); `PauseTransformFeedback` / `ResumeTransformFeedback` → (64)/(65) `MGPStreamOutputControl{Reserved = 0}`; `DeleteTransformFeedback` has no row in P5b (unmeasured; a `bind_stream_output` of name 0 is what the backend does on delete of the bound object, `DirectGLES.cpp:1422`, and the driver object leak until P9 is recorded, not solved). | | `OnEndStreamOutput` / `OnPauseStreamOutput` / `OnResumeStreamOutput` → the three GL slots | stubs |
|
||||
|
||||
**`FixupGsStripCaptureOrder` (`GL_Drawing.cpp:1290`)** probes `EndTransformFeedback != nullptr`
|
||||
to decide whether the backend owns the capture. `SlotCaps.h` deliberately did not convert this
|
||||
probe ("absent would be wrong"); with t2's emitter installed the slot is non-null on the
|
||||
client, which for a server running Espryt is the right answer and for a server running Magma
|
||||
(slot null on the server, the sink DECLINES) is WRONG in the P5 direction: the client would
|
||||
skip a reorder Magma needs. **Ruling:** t2 answers the probe from the caps mirror - the
|
||||
existing `kCapCpuXfbPrimitiveAccounting`-style shape, a new cap bit
|
||||
`kCapBackendOwnsXfbCapture` (1<<9) in `MGPCapBit`, set by the server from its own table's
|
||||
`EndTransformFeedback != nullptr` - through `MGL_BACKEND_SLOT_CAP`. That is the one new cap
|
||||
bit of P5b and it is c0b's file; t2 adds it (§6.5).
|
||||
|
||||
**Stamps:** `BeginStreamOutput → BeginTransformFeedback`, `EndStreamOutput →
|
||||
EndTransformFeedback`, `Pause/Resume` likewise (all pre-existing rows), `PatchParameter →
|
||||
PatchParameteri` (kQuery: reads nothing), `BindStreamOutput → BindTransformFeedback` (kXfbSpan).
|
||||
|
||||
**t2's gate:** the three first-blocker counts → 0; each scenario at its next blocker by name
|
||||
(the census predicts `EndTransformFeedback` or an XFB query end - the query family is wave 3
|
||||
and stays `Fatal{UnmigratedVerb}`).
|
||||
|
||||
### f1 — the clear family, the framebuffer-sourced copies, mips (7 measured + 4 companions)
|
||||
|
||||
| slot (entries) | row (opcode) | record | sink | on this head |
|
||||
|---|---|---|---|---|
|
||||
| `ClearBufferiv` (12), `ClearBufferuiv` (8), `ClearBufferfv` (6), `ClearBufferfi` (1) | `clear` (57), `MGPClear` | `Fbo = kMGPipeNullHandle` (the bound draw framebuffer, as `EmitClear` does), `Kind` / `ValueClass` per the table in `MGPipeTypes.h` beside the constants, `DrawBufferIndex = drawbuffer`, `ColorValue[4]` = the four values' bits (`memcpy`; a float clear crosses as its bit pattern, exactly as MOBILEGL_PIPE_VERIFY compares floats), `DepthValue`, `StencilValue`. Emitted at `GL_Framebuffer.cpp:2829-2850` with `BeforeReadOnlyVerb()` first, as `EmitClear`. | `OnClear` - **already live for all five kinds** (`PipeApplier.cpp` `OnClear`): `GL.ClearBuffer{fv,iv,uiv,fi}` by `Kind`/`ValueClass`. The only work is the client. | live |
|
||||
| `ClearNamedFramebufferfv` (2) + companions `fi/iv/uiv` | `clear` (57) | as above with **`Fbo` = the named framebuffer's handle**, preceded by its `MGPipeFramebufferTarget::Named` record (ID-19; the frontend already emits it: `PipePublishFramebufferByName`, `GL_Framebuffer.cpp:690`). | `OnClear`: `Fbo` null or equal to the applier's `BoundFramebuffer[Draw]` → the bound-form clear above. **Otherwise refused by name, `Fatal{UnmigratedVerb, "ClearNamedFramebufferfv+UNBOUND"}`**: the backend slot takes a frontend `SharedPtr<FramebufferObject>` (`DirectGLES.cpp:9308`), no forward hands one out, and the bound-form entries re-sync the CLIENT's binding through the barrier-pulled `GetFramebufferBindingSlot` (`SyncCurrentFBO`), so a server-side rebind-clear-restore through the applier would be undone by the pull. The handle-keyed `SyncAndBindFramebufferObject` sibling Espryt needs (`DirectGLES.cpp:4409`) is P7's / P3b-P4b's, or f1's stretch behind `#if MOBILEGL_BUILD_DISAGGREGATED` if the one measured scenario needs it. | live for the bound case; the unbound refusal is f1's to add |
|
||||
| `CopyTexImage2D` (3) + companion `CopyTexSubImage2D` | **`copy_framebuffer_to_texture` (76), NEW**, `MGPCopyFromFramebuffer` 48 B | `Dst` = the bound texture's handle at the active unit (P8 form), `Target` = the GL target verbatim, `Level`, `InternalFormat` (image form; 0 for sub-image), `X, Y, Width, Height` = the read-framebuffer rectangle, `XOffset, YOffset` (sub-image form), `SubImage = 0/1`. The image form's level redefinition has already crossed as the level's `resource_respecify` from the frontend's own `TexImage2D_State` (`GL_Texture.cpp:4541`) before the backend slot is reached (`:4550`); the record carries only the copy. The read framebuffer is the bound one (`set_framebuffer_state`). | `OnCopyFramebufferToTexture(const MGPCopyFromFramebuffer&)` → `GL.CopyTexImage2D(Target, Level, InternalFormat, X, Y, Width, Height, 0)` or `GL.CopyTexSubImage2D(Target, Level, XOffset, YOffset, X, Y, Width, Height)`; the backend resolves the bound texture through the barrier-pulled unit state (`DirectGLES.cpp:8528-8531`, kBlitOrCopy's mask). | stub, `"CopyTexImage2D"` / `"CopyTexSubImage2D"` |
|
||||
| `GenerateMipmap` (2) | `generate_mipmap` (54), `MGPMipPlan` | `Res` = the bound texture's handle, `Target` = the GL target verbatim, `BaseLevel/LevelCount` from the texture's base level and level count (informational in P5b). Emitted at `GL_Texture.cpp:1681`. | `OnGenerateMipmap(const MGPMipPlan&)` → `GL.GenerateMipmap(Target)`; the backend resolves the bound texture through the barrier-pulled unit state (`DirectGLES.cpp:8769-8772`). | stub |
|
||||
|
||||
**The two mipmap emulations** (`generate-mipmap-storage` `DirectGLES.cpp:8051`,
|
||||
`generate-mipmap-cpu-fallback` `:8702`, both `Fatal{UnmigratedEmulation}` under split) are
|
||||
reached only for `R11FG11FB10F`, depth-only, `RGB16F` and `RGB32F` textures
|
||||
(`DirectGLES.cpp:8774-8778`). **Ruling (f1):** migrating `GenerateMipmap` moves those cases'
|
||||
first blocker to the emulation Fatal, reported by name; every other format renders. The
|
||||
storage grow reads the frontend level shadows to DECIDE the levels and could be re-derived from
|
||||
the applier's descriptor (`Levels`) - f1 may do that behind `#if MOBILEGL_BUILD_DISAGGREGATED`;
|
||||
the CPU three-channel fallback needs `OnTextureWriteback` (ROADMAP P8/P9) and stays Fatal.
|
||||
|
||||
**Stamps:** `Clear → Clear` (pre-existing), `CopyFramebufferToTexture → CopyTexImage2D` (the
|
||||
sub-image companion shares the kBlitOrCopy mask), `GenerateMipmap → GenerateMipmap`.
|
||||
|
||||
**f1's gate:** the seven first-blocker counts → 0; each scenario at its next blocker by name.
|
||||
|
||||
---
|
||||
|
||||
## §3 Table 2 — field ownership under P5b
|
||||
|
||||
**P5b adds no PipeInputs field and retires none.** Every one of the 25 verbs reads, on the
|
||||
server, exactly the fields its verb class's may-read mask names (`FillPoints.def`) and finds
|
||||
them in the class CONTRACT-P5 table 2 already assigned: RECORD_SUPPLIED where a record
|
||||
carries them, BARRIER_PULLED (counted in `rsp`) for the frontend-object and shutter fields,
|
||||
FATAL for the three off-path ones. Two of the three FATAL rows are reached by P5b verbs and
|
||||
move:
|
||||
|
||||
| field | CONTRACT-P5 class | P5b | why |
|
||||
|---|---|---|---|
|
||||
| `GetProgramForDispatch` | FATAL ("no compute on the reduced path") | **BARRIER_PULLED, "P7 (Magma), P8 (Espryt)"** | i1 puts compute on the path; the field is `GetProgramForDraw`'s twin (`DirectGLES.cpp:5779`, `VulkanRenderer.cpp:7327`) and takes its class and its retiring phases. i1 edits the row. |
|
||||
| `GetTransformFeedbackPausedPrimitiveCounter` | FATAL ("reachable only from kQuery") | unchanged | its one reader is Magma's XFB query end (`kQuery`), and the query family is wave 3. |
|
||||
| `GetBoundTransformFeedbackName` | FATAL (dead) | unchanged | |
|
||||
|
||||
**`rsp` grows, and that is the phase's measurement.** Every P5b verb that draws, dispatches
|
||||
or copies pulls the same 20 non-sticky rows a P5 draw pulled plus the sticky forwards its arm
|
||||
uses (`GetTextureObject` for `CopyImageSubData`, `GetProgramObject` for
|
||||
`ShaderStorageBlockBinding`). The end-of-P5b `rsp` per Minecraft frame is the number that
|
||||
sizes P3b/P4b/P7/P8, and each package's report carries it for its scenarios.
|
||||
|
||||
**The server stamp:** every P5b row that is a verb boundary has a `MGP_VERB_OP_LIST` row in
|
||||
`FieldOwnership.def` on this head (18 rows; `FieldOwnershipTest` pins them). A P5b record that
|
||||
arrived with NO stamp row would apply under the previous verb's serial and read a stale field
|
||||
as fresh - the one silent failure the table has - which is why the six rows landed in c0b
|
||||
rather than being left to the packages.
|
||||
|
||||
---
|
||||
|
||||
## §4 Table 3 — role and thread ownership under P5b
|
||||
|
||||
Unchanged, and the one thing to restate: **the verb barrier holds for every P5b verb.**
|
||||
`ClientSession::EmitAndWait` blocks the GL thread until `appliedSeq >= seq` for every one of
|
||||
the 25 records as it does for the five P5 verbs; the server's `PipeApplier::ApplyOne` stamps
|
||||
the verb boundary, runs the sink, clears the stamp, and only then does the session publish
|
||||
`appliedSeq` (R-1, R-9). The pre-verb hooks keep their P5 order on the client:
|
||||
`PushPersistentMapsBeforeVerb()` then `MarkGpuWritesFor{Draw,Dispatch}()` then the record
|
||||
(`EmitTables.cpp` `BeforeDrawVerb` / `DispatchCompute_Unmigrated`; ID-18). A P5b emitter for a
|
||||
verb that reads buffers but starts no shader (the clears, the copies, the mip generation, the
|
||||
barrier, the XFB and patch controls) calls `BeforeReadOnlyVerb()`.
|
||||
|
||||
**What the barrier buys P5b and what it costs.** It is why Rule D works at all - the server's
|
||||
backend reads the client's `gPipeInputs` fill of the moment - and it is why P5b's inproc is
|
||||
lockstep and slow (P5 said so; "P5 预期变慢" is P5b's expectation too, recorded, not gated).
|
||||
Retiring it per family is P9's, after the pulls it protects are gone.
|
||||
|
||||
---
|
||||
|
||||
## §5 Knobs
|
||||
|
||||
**No new knob.** `MOBILEGL_TRANSPORT=inproc` with the P5 defaults is P5b's whole
|
||||
configuration. One cap bit is added (§6.5, `kCapBackendOwnsXfbCapture`), which is not a knob.
|
||||
`MOBILEGL_IPC_STRICT_ERRORS=1` remains the way to see the pulls a P5b verb costs, and
|
||||
`MOBILEGL_IPC_AUDIT=1` remains rule C's control - now with a second consumer worth auditing,
|
||||
the user-index span.
|
||||
|
||||
---
|
||||
|
||||
## §6 Rulings this file makes, and what would overturn each
|
||||
|
||||
1. **No P5b slot gains an `MGPipeApply*` entry point; all 25 reach `WireVerbSink`.** The
|
||||
applier's 37 entry points are the object and state families and write `gPipeInputs` /
|
||||
the applier's records; a verb is a backend CALL, and the census's correction - the five P5
|
||||
class-B verbs already go to the sink - is the pattern, not the exception. The generated
|
||||
route tables (`PipeRoute.h`, R-17, 33 + 4 = 37) are therefore untouched and
|
||||
`PipeCatalogueTest`'s partition still reads 33 installed + (76 − 33) null + 4 escapes.
|
||||
*Overturned by:* a verb that must be replayable from the monolith side through a table
|
||||
(the MGPipe recorder, ROADMAP P13) - at which point every sink method becomes a table row
|
||||
and the monolith adapter is the backend call; none of P5b needs it.
|
||||
|
||||
2. **Five rows appended, none inserted, opcodes 72..76.** `bind_shader_image`,
|
||||
`patch_parameter`, `bind_stream_output`, `set_storage_block_binding`,
|
||||
`copy_framebuffer_to_texture`. Each exists because the backend does work AT THE CALL that
|
||||
no validate-time set record reproduces (§0 Rule D; the evidence per row is in
|
||||
`MGPipeTypes.h` beside the struct). *Overturned by:* the backend moving that work into its
|
||||
draw prep (e.g. Espryt applying `glPatchParameteri` from `SyncRenderState`), which is a G5
|
||||
byte-identical region and therefore not P5b's to touch.
|
||||
|
||||
3. **`resource_copy_region` is `glCopyImageSubData` and nothing else.** CONTRACT-P5 / the
|
||||
`FieldOwnership.def` header left "which of three verbs" to the emitting phase; P5b gives
|
||||
the two framebuffer-sourced copies their own row (76) because they have a different
|
||||
source (the read framebuffer, no handle) and a different shape (a level redefinition on
|
||||
the image form). *Overturned by:* nothing cheaper than a second stamp rule.
|
||||
|
||||
4. **`kCapNeedsHostIndexBytes` stays 0; the user-index span is armed anyway.** The cap is the
|
||||
server's request for VBO-backed index bytes (P8's mirror); the span is the client's only
|
||||
way to hand over bytes only it has. The two were conflated in CONTRACT-P5 table 0's cap-bit
|
||||
row ("the two bits are the only things that ask for [a span]") because P5 produced neither.
|
||||
*Overturned by:* a measured draw whose client-side index array exceeds what one staged
|
||||
run can hold at the default `SEG_STAGE` (32 MiB; the R-10 `maxrec=` counter is what would
|
||||
say so), at which point d1 chunks or refuses by size.
|
||||
|
||||
5. **One new cap bit, `kCapBackendOwnsXfbCapture` (1<<9).** The `EndTransformFeedback !=
|
||||
nullptr` probe at `GL_Drawing.cpp:1290` decides whether the CLIENT reorders captured
|
||||
records; under split the answer is the SERVER's table, so it is a cap, set by the server
|
||||
from `GL.EndTransformFeedback != nullptr` and read through `MGL_BACKEND_SLOT_CAP`. It is the
|
||||
first of the "XFB span family" cap bits `SlotCaps.h` said would come; t2 adds it to
|
||||
`MGPCapBit` (c0b's file, granted for this bit) in the same commit that flips the probe.
|
||||
*Overturned by:* Magma registering `EndTransformFeedback`, which removes the difference.
|
||||
|
||||
6. **`MGPCopyRegion` widens 64 → 72 for two GL names.** A record with no producer and no
|
||||
consumer has no wire history to preserve; the names are the barrier-pulled lookup keys
|
||||
the P5b sink needs and the handles beside them are the identities P7 will use.
|
||||
*Overturned by:* P7 landing handle-keyed endpoints, when the two fields become padding
|
||||
again (and stay in the layout - a shrink would move every field behind them).
|
||||
|
||||
7. **Renderbuffer endpoints, unbound DSA clears and multi-draws with client-side indices are
|
||||
refused BY NAME, ID-57's shape.** Each is a form no barrier-pulled forward can serve, none
|
||||
was measured, and a named Fatal at the client or sink is the outcome R-4 asks for; a
|
||||
silently wrong picture is the one it forbids. *Overturned by:* a measured workload hitting
|
||||
one, which then gets its P7/P8 mechanism moved forward.
|
||||
|
||||
8. **The `copy-image-shadow-mirror` site is skipped under split; the two mipmap emulation
|
||||
sites stay Fatal.** The mirror's loss is bounded by two other Fatals (§2 i1); the mipmap
|
||||
storage grow may be re-derived from the descriptor, the CPU fallback cannot be without
|
||||
`OnTextureWriteback`. *Overturned by:* a measured `GetTexImage` of a copy destination
|
||||
before P9's readback carrier lands - which would show as the wave-3 `GetTexImage` Fatal
|
||||
anyway.
|
||||
|
||||
9. **`GetProgramForDispatch` moves FATAL → BARRIER_PULLED.** §3. *Overturned by:* nothing;
|
||||
the FATAL class was a statement about the reduced path, and P5b widens the path.
|
||||
|
||||
10. **The emit table's class-C list is partitioned per package** (`EmitTables.cpp`:
|
||||
`MGR_UNMIGRATED_{D1,I1,T2,F1,TAIL}_SLOTS`, `kEmittedSlots{D1,I1,T2,F1}`, five ownership
|
||||
`static_assert`s: 19 / 7 / 7 / 11 / 20 = 64). Four packages editing one X-macro list and
|
||||
one count would conflict on every landing; four lists and four counts do not, and a
|
||||
package that touches another's list breaks the other's assertion. *Overturned by:*
|
||||
nothing - it is arithmetic, and the totals `UnmigratedSlotCount() == 64` /
|
||||
`ImplementedVerbCount() == 5` the P5 tests pin are unchanged on this head.
|
||||
|
||||
11. **The server stubs are `Fatal{UnmigratedVerb, "<GL slot>"}`, the same family and name
|
||||
as the client's**, suffixed "(server sink)" in the message text only. A client flipped
|
||||
ahead of its server half therefore aborts under the census's own grep with the right
|
||||
slot name, and the lane's first-blocker table stays meaningful across a partial landing.
|
||||
*Overturned by:* nothing.
|
||||
|
||||
12. **Client-side vertex arrays are not a P5b question.** They are served on the server
|
||||
through the barrier-pulled `GetBoundVertexArray` (CONTRACT-P5 table 2, "P8"), correct
|
||||
under inproc by the barrier and the shared address space, and retired by P8's
|
||||
`HostResolve.cpp`. *Overturned by:* P6's spawn transport - which is after P5b by ID-68.
|
||||
|
||||
---
|
||||
|
||||
13. **Measured named blits use the existing `blit` row with a scoped client binding override.**
|
||||
P5b's next trace census reaches `BlitNamedFramebuffer` in iris-BSL and
|
||||
improved-transparency on both backends. The client saves its read/draw binding objects,
|
||||
binds the named arguments in the client shadow only, validates `BlitNamedFramebuffer`,
|
||||
and emits the original rectangles/mask/filter plus both original `MGPBlit` handles.
|
||||
The barrier holds those bindings until the server's bound `BlitFramebuffer` finishes;
|
||||
then RAII restores both client bindings. Their version changes make the next ordinary
|
||||
verb republish the restored state. The existing classified BARRIER-PULLED accessors
|
||||
remain the sole frontend read path; no pointer is added to the record or to a side channel,
|
||||
and no driver call occurs on the client. Name 0 carries `kMGPipeDefaultFramebuffer`.
|
||||
This adds no opcode and supersedes only named-blit's wave-3 deferral. *Overturned by:*
|
||||
removing the lockstep barrier or adding process separation; then both framebuffer
|
||||
handles need server-only resolution before dispatch. Pixel/binding gates cover unbound
|
||||
endpoints, default endpoints and ordinary clear/blit immediately after restoration.
|
||||
|
||||
## §7 The 71 slots after P5b's contract commit, and after the four packages
|
||||
|
||||
On this head the partition is CONTRACT-P5 §7's, unchanged: **A = 2, B = 5, C = 64**
|
||||
(`RemoteClientTest` pins it). When the four packages land it is **A = 2, B = 5 + 25 (+ the
|
||||
companions each package flips, up to 44), C = the wave-3 tail (20 at least)**, and the
|
||||
`static_assert`s in `EmitTables.cpp` say which package moved which slot.
|
||||
|
||||
The wave-3 tail no P5b package owns, by name, so nobody discovers it by grep: `BlitNamedFramebuffer`,
|
||||
`GetTexImage`, `GetTextureImage`, `WaitSync`, `DeleteSync`, `FenceSync`, `ClientWaitSync`,
|
||||
`GetSyncStatus`, `BeginTimeElapsedQuery`, `EndTimeElapsedQuery`, `QueryCounterTimestamp`,
|
||||
`IsQueryResultAvailable`, `GetQueryResult64`, `DeleteBackendQuery`, `BeginOcclusionQuery`,
|
||||
`EndOcclusionQuery`, `BeginXfbPrimitivesQuery`, `EndXfbPrimitivesQuery`, `GetGpuTimestampNs`,
|
||||
`SetSwapInterval` - P9 (readbacks), P10 (queries, syncs, swap interval). (2026-09-29: all are
|
||||
emitted now; the last one, `SetSwapInterval`, went in P10 - `CONTRACT-P10.md` §2.)
|
||||
|
||||
---
|
||||
|
||||
## §8 Ownership
|
||||
|
||||
- `MG_Pipe/MGPipeTypes.h`, the three `.def` files, `FieldOwnership.def`, `gen_pipe*.py`,
|
||||
`CONTRACT-P5.md`, `CONTRACT-P5B.md`: c0b's (inherited from c0). A package that needs a payload
|
||||
shape changed goes through the integrator. Two grants are pre-made in this file: t2 may add
|
||||
`kCapBackendOwnsXfbCapture` (§6.5); i1 may edit `GetProgramForDispatch`'s row (§6.9).
|
||||
- `MG_Remote/Wire/PipeWireCodec.{h,cpp}`: w1's; c0b landed the sink methods and the arms; a
|
||||
package edits ONLY the layout arm of a row it owns, and only with the integrator.
|
||||
- `MG_Remote/Server/PipeApplier.{h,cpp}`: v1's; each package replaces ITS stub bodies and
|
||||
nothing else (the per-package blocks are marked).
|
||||
- `MG_Remote/Client/EmitTables.{h,cpp}`: c1's; each package edits ITS `MGR_UNMIGRATED_*_SLOTS`
|
||||
list, ITS `kEmittedSlots*`, and adds ITS emitters in the class-B block.
|
||||
- `MG_Test/Wire/PipeWireCodecTest.cpp`: each package appends cases for its rows below c0b's.
|
||||
- The backends (`MG_Backend/DirectGLES`, `DirectVulkan`): untouched by c0b; a package that
|
||||
must touch one does so behind `#if MOBILEGL_BUILD_DISAGGREGATED` and names the region in
|
||||
its report (G1 admits no pull-build symbol motion; G5's untouched regions are pinned).
|
||||
|
||||
|
||||
## §9 Wave 3: fence sync (integrator-approved, 2026-09-16)
|
||||
|
||||
The dynamic Minecraft census exposes `FenceSync`. All five sync slots migrate together on
|
||||
existing opcodes: FenceCreate/FenceStatus/FenceWait/FenceDestroy/FenceWaitServer. No opcode
|
||||
moves and no creation reply is introduced. The client allocates a Fence-kind `{slot, gen}`;
|
||||
a local opaque proxy satisfies the frontend's `BackendSyncHandle` API. Only the handle crosses
|
||||
SEG_CMD. The server owns a generation-checked table of native backend sync handles, creates,
|
||||
waits, queries and deletes them exclusively on the apply thread, and releases remaining native
|
||||
objects before the backend is detached. Client orphan deletion after session shutdown only
|
||||
releases its proxy. Duplicate creation and missing, destroyed or stale wire handles are protocol
|
||||
corruption, since GL argument errors were already handled by the frontend.
|
||||
|
||||
`MGPFenceWait` grows 16 → 24 bytes: append `Uint32 Flags; Uint32 Pad0;` after TimeoutNs.
|
||||
Flags preserves `GL_SYNC_FLUSH_COMMANDS_BIT` for ClientWaitSync; server WaitSync accepts only
|
||||
zero flags and GL_TIMEOUT_IGNORED. Existing ABI fingerprinting rejects mixed layouts.
|
||||
FenceStatus and FenceWait retain their existing reply slots: an OK reply is exactly one Uint32,
|
||||
respectively 0/1 or the backend's GL wait enum. Missing backend declines and never manufactures
|
||||
an OK result. A present backend with no FenceSync slot or a FenceSync call returning null uses
|
||||
exactly GL_Sync.cpp's existing always-signaled fallback; a real native fence's failed/timeout
|
||||
wait answer is returned unchanged. A missing native wait/status slot uses the same frontend
|
||||
fallback. This is compatibility with existing monolith behavior, not an unconditional success.
|
||||
|
||||
All five opcodes stamp their matching MGPipeVerb (`FenceSync`, `GetSyncStatus`,
|
||||
`ClientWaitSync`, `DeleteSync`, `WaitSync`). Every record keeps the verb barrier. The sole `GL_Sync.cpp` FenceSync guard changes from the class-C LOCAL macro to its original
|
||||
table pointer expression: in a pull build these are identical. Under split it reaches the
|
||||
class-B emitter, and the server preserves the optional-native fallback. No backend changes. The wave-3 tail's five slots move C → B; d1/i1/t2/f1 ownership
|
||||
counts remain unchanged.
|
||||
@@ -0,0 +1,543 @@
|
||||
# CONTRACT-P5C — the wire contract for zeroing the inproc split's direct cross-role memory access
|
||||
|
||||
Authority: this file, beside `CONTRACT-P5.md` (table 0's encodings, the byte carriers, field
|
||||
ownership, role/thread ownership, the knobs, R-1…R-17) and `CONTRACT-P5B.md` (the 25 migrated
|
||||
class-C slots). Where it disagrees with either, this file is newer and wins, and §7 lists
|
||||
every such place.
|
||||
|
||||
**How to change it.** Package c0c's file, edited by the integrator first. A P5c package (tx /
|
||||
ev / hd / ct / rv / gt) that needs a row changed goes through the integrator; the packages
|
||||
compile against the rows below from day one, which is the whole reason the file exists.
|
||||
|
||||
Base: `feat/disaggregated @ 11ac3de6` (the P5c plan commit; code-identical to the audit head
|
||||
`a79a0af6`). Every `file:line` below was read at that commit. The audit this contract
|
||||
discharges is `~/w7/notes/p5c/p5c-audit-v1.md` (59 sites); ROADMAP.md "P5c 计划" tabulates
|
||||
them as T1–T5, B1–B4, R1–R5, C1, G1–G6, A1.
|
||||
|
||||
---
|
||||
|
||||
## §0 What P5c is, and the one rule above every row
|
||||
|
||||
P5/P5b put verb records, `SEG_STAGE` blobs, replies and caps snapshots on the wire. The audit
|
||||
proved the two roles still read and write each other's memory directly at 59 sites, correct
|
||||
only because the verb barrier and the shared address space happen to hold. P6 replaces how
|
||||
the rings are delivered; it cannot replace a read of memory that does not exist in the other
|
||||
process. **P5c makes `inproc` honest: between the two roles, nothing crosses except
|
||||
`SEG_CMD` / `SEG_STAGE` / `SEG_REPLY` / `SEG_EVENT` / the caps snapshot / control frames —
|
||||
so that P6 is a transport swap and nothing else.**
|
||||
|
||||
Rules A (a content record declares its bytes), B (no host pointer crosses) and C (an applier
|
||||
entry point may not hold a pointer past its return) are CONTRACT-P5 §0's and bind every row
|
||||
here unchanged. Rule D (a verb crosses AS THE CALL) is CONTRACT-P5B §0's and is NOT relaxed:
|
||||
P5c changes WHERE the sink resolves the call's objects from (the record's handles, §3), not
|
||||
what the record carries. P5c adds one rule:
|
||||
|
||||
**Rule E — a role may not name the other role's memory.** With an active transport, the
|
||||
apply thread resolves every object from a handle the record carried (`§3`), every byte from
|
||||
`SEG_STAGE` adopted into server-owned storage (`§2`), and every reverse notification is a
|
||||
`SEG_EVENT` record (`§4`); the GL thread reaches server state only through replies, events
|
||||
and the caps snapshot. A violation is `Fatal{RoleViolation, "<surface>"}` and the two guard
|
||||
layers that raise it are §6's. "The barrier held" and "same address space" are no longer
|
||||
accepted as mechanisms anywhere in this file.
|
||||
|
||||
Refusal vocabulary is CONTRACT-P5B's, unchanged: `Fatal{ProtocolCorruption, "<row>.<field>"}`
|
||||
for a record the codec cannot prove, `Fatal{UnmigratedVerb, "<GL slot>[+<QUALIFIER>]"}` for a
|
||||
shape no sink implements, a null backend slot DECLINES (`return false`). P5c adds one family:
|
||||
`Fatal{RoleViolation, "<surface>"}` (§6) and `Fatal{EventRingOverflow}` (§4.4).
|
||||
|
||||
---
|
||||
|
||||
## §1 Table 0 additions — encodings the P5c rows introduce
|
||||
|
||||
| field | the ruling | zero means | who reads it |
|
||||
|---|---|---|---|
|
||||
| **reverse-channel `MGPBlobRef`** | An `OnBufferWriteback` blobref on the wire names **SEG_EVENT**: `Seg = Wire::kSegEvent` (4, `PipeWireCodec.h:62`), `Offset` = the byte offset of the inline payload inside the SEG_EVENT mapping (`EventRingConsumer::OffsetInSegment`, `EventRing.h:182-185`), `Size` = the head's `Size`, cross-checked against the record's tail. **Never a host address** — rule B binds the reverse direction exactly as it binds `MGHostSpan` (R-2). Under monolith the blobref keeps its P3a shape (`Seg = kMGHostSpanSegNone`, `Offset` IS the mapped address); the two shapes are distinguished by `Seg`, not by the transport knob. | `Size == 0` with a live resource is legal (a zero-length writeback) | the client consumer (`ResourceTracker.h`, §4.3) |
|
||||
| **`MGPObjectDeath` reuses `MGPHandleOnly`** | `{Handle, Kind, Pad0}` (16 B, `MGPipeTypes.h:93-98`). `Kind` is the `MGPipeKind` of the dead frontend object, widened to `Uint32` exactly as the existing rows widen it. One payload for all seven kinds the death switch handles (`Managers.cpp:224-257`): Texture, Framebuffer, Renderbuffer, SamplerCso, ShaderCso, SamplerViewCso, VertexElementsCso. | `Handle` null = "the object never crossed" — the client emits NOTHING in that case (§5.2), so a null handle arriving is `Fatal{ProtocolCorruption, "ObjectDeath.Handle"}` | `ServerVerbSink::OnObjectDeath` |
|
||||
| **`MGPApplierReset::ContextSerial`** | `Uint64`. The client-side context's make-current serial at the edge that primed it. P5c has exactly one context per session, so the value is ASSERTED equal to the session's, not dispatched on; P6's multi-context shape reads it for real. | 0 = the first make-current | `ServerVerbSink::OnApplierReset` |
|
||||
| **`MGPContextValues`** | The rv field table (§5.3), one fixed-width POD, `MGP_ASSERT_POD`-pinned. Whole-record hash-suppressed like every other `set_*` call: it crosses only when one of its source values moved since the last emission. There is no dirty mask in the payload — a suppressed record means "nothing moved", never "field invalid". | — | `MGPipeApplySetContextValues`, the server accessors it feeds |
|
||||
| **texture per-level extent, server-side** | `max(1, base_extent >> level)` computed from the descriptor's `Width/Height/Depth` and `Levels` (`MGPResourceDesc`, `MGPipeTypes.h:295-360`). The client does NOT emit per-level extents; the derivation is the mip chain's definition and two honest ends compute the same number. | — | tx's `SyncMipmapsToBackend` (§2.2) |
|
||||
| **`EventKind` = 4: `kEventGlError`** | `EventRing.h`'s enum gains `kEventGlError = 4` beside the existing three (:46-51). Head `EventGlErrorHead { Uint32 Code; Uint32 MessageBytes; }` (8 B) followed by the NUL-terminated message inline, `MessageBytes = strlen + 1`, **capped at 1024** (a longer message is truncated at the producer; the cap is a static_assert, not a check). `Code` is the frontend `ErrorCode`, widened. | `MessageBytes == 0` is `Fatal{ProtocolCorruption, "kEventGlError"}` (the NUL travels, rule A's twin) | `ClientSession::DrainEventRing` |
|
||||
| **`MGPSurfaceInfo` zero-extent semantics** | `Width == 0 && Height == 0` is the FORMAT-ONLY publication (the DirectGLES shape: the placeholder's extent is deliberately left alone, only the depth/stencil `InternalFormat` moves); a non-zero extent is the SWAPCHAIN shape (storage at the extent, then format). `Samples`/`Layers` are informational only (1/1 from Vulkan, 0/0 from GLES; the placeholder attachment model has no MSAA) and the consumer ignores them. | see left | the surface-changed consumer |
|
||||
|
||||
---
|
||||
|
||||
## §2 tx — the server's texture staged shadow
|
||||
|
||||
The texture half of `resource_subdata` stages the level bytes into `SEG_STAGE` today and
|
||||
`ApplyTextureUpload` drops the pointer (`PipeApply.cpp:989-1036`: the gate, the accumulation
|
||||
and the serial — the `bytes` parameter is used only by the non-null gate at :1006-1008).
|
||||
Espryt then re-reads the client's `MipmapStorage` at sync time (Managers.cpp:6940, :7129,
|
||||
:7198, :7315), reads per-level shape from the client object (:7314, :7332,
|
||||
`DirectGLES.cpp:8502-8507`, :8528-8529), and falls back to the client for dirty regions with
|
||||
no pending upload (Managers.cpp:7360, :7371). Magma WRITES the client's level storage
|
||||
(`VulkanRenderer.cpp:1562-1609`: `AllocateStorage` + `MarkStorageDirty(..., false)`).
|
||||
**tx ends all of it: the staged bytes are adopted, and the server's sync reads nothing but
|
||||
its own shadow and the descriptor.**
|
||||
|
||||
### 2.1 `StagedTextureStore` — ownership and coverage
|
||||
|
||||
`MobileGL/MG_Remote/Server/StagedTextureStore.h`, the texture twin of `StagedShadow.h`
|
||||
(R-11), and held to its four rulings:
|
||||
|
||||
1. **Keyed by the wire handle the record carried** (`KeyForHandle` = `{slot, gen}` under a
|
||||
tag bit) — AMENDED from "the twin's address" at tx's landing, for two reasons that
|
||||
together left no honest alternative: constructing a texture twin calls `glGenTextures`
|
||||
(Managers.cpp:5496), so a twin-address key either forces twin minting at adopt time (a
|
||||
driver name burnt for a texture that may never draw, and a GL context required in the
|
||||
R-16 unit case — buffer twins mint no GL and can be created lazily) or strands levels
|
||||
adopted before the first sync; and the handle is rule E's cross-role identity anyway,
|
||||
`{slot, gen}`-exact across a slot recycle. Magma's T5 path, which has no wire handle in
|
||||
its identity tables, keys by twin address under a SECOND tag bit, so the two key spaces
|
||||
can never collide. Every event that ends a key's life — respecify, destroy
|
||||
(`resource_destroy`'s texture arm, delivered precisely because the store is
|
||||
handle-keyed), context death — has a call site to drop it from.
|
||||
2. **Coverage is the WHOLE STAGED RUN per (uploadTarget, level)** — AMENDED from "the
|
||||
region set" at tx's landing: a texture `resource_subdata` always stages the entire
|
||||
level shadow (`Blob.Size` IS the declared byte count), so the run is the honest unit;
|
||||
the region set stays what it always was — the dirty SHAPE the pending set carries.
|
||||
Region-precise coverage would `Fatal` on the legal
|
||||
texStorage→small-box `glTexSubImage`→conversion-fallback flow, whose sync reads the
|
||||
whole level. A sync that reaches a level no record covered is
|
||||
`Fatal{StageSnapshotTooNarrow, "<site>"}` — the same words as the buffer half, and for the
|
||||
same reason (`StagedShadow.h:96-116`: moving zeroes into the store is silent data loss,
|
||||
not a missing optimisation).
|
||||
3. **`copies` is a constructor parameter**, false reproducing the monolith expression
|
||||
character for character (the client shadow answers, nothing is allocated). G1's pull-build
|
||||
byte-identity is preserved the same way R-11 preserved it.
|
||||
4. **A header with diagnostics**, not a block inside Managers.cpp, so a unit case builds one
|
||||
store of each kind and asserts the difference (R-16 — `ServerLoopTest.cpp:616-760` is the
|
||||
shape the tests copy).
|
||||
|
||||
`PendingUpload` (`PipeApply.h:289-295`) gains NO byte pointer: the record's bytes move to the
|
||||
store at apply time, and the pending set keeps its job of naming which (target, level, box)
|
||||
is dirty-by-record. The store, not the pending set, is the byte owner; a pending upload with
|
||||
no store coverage is the `Fatal{StageSnapshotTooNarrow}` case, not a silent re-read.
|
||||
|
||||
### 2.2 What the sync path reads after tx
|
||||
|
||||
| the read today | after tx |
|
||||
|---|---|
|
||||
| level texels from client `MipmapStorage` (`MapMipmapData`) | the store's adopted bytes |
|
||||
| per-level extent from the client object | §1's derivation from the descriptor |
|
||||
| texture target / levels count from the client object | the descriptor (`Target`, `Levels`; the `MGB_STORAGE_LEVELS` handle arm already reads `pushedStorage->Desc.Levels`, Managers.cpp:6651-6652) |
|
||||
| dirty region, level HAS a pending upload | the record's regions (unchanged — already record-supplied, Managers.cpp:7358-7369) |
|
||||
| dirty region, level has NO pending upload (GPU-generated mips, re-derivation) | **the server's own dirty mark on the shadow** (§2.3) — the client is never asked |
|
||||
| level EXISTS at all (`Levels = N` says nothing about a sparse chain) | the store's DEFINED-NESS, fed by the respecify hook: a named level redefines that one, an immutable whole-resource respecify defines the whole chain (all six cube faces included, GL 4.6 core 8.19), a mutable whole-resource respecify defines nothing. Without this the §1 derivation would define `{0,0,0}`-extent holes into existence and change FBO/sampling completeness — added at tx's landing, the contract's own omission |
|
||||
|
||||
### 2.3 GPU-generated mips (T5) — who owns "dirty" now
|
||||
|
||||
A GPU-side generation (Magma's `GenerateMipmap`, Espryt's re-derivation arms) dirties the
|
||||
SERVER's shadow, not the client's. The two backends land this in their own shapes, both
|
||||
final: **Magma**'s `EnsureGenerateMipmapStorageAllocated` is replaced by a store operation
|
||||
that marks the level Defined at the §1 extent and GpuDirty-in-shadow (the store answers
|
||||
"exists" and "needs upload" for it; the client object is never touched). **Espryt**
|
||||
generates the levels ON THE DRIVER, so no server shadow allocation is needed at all — the
|
||||
levels are already where a sync would put them, and the shadow's staleness is the correct
|
||||
state ruled below. Either way the client's `TextureObjectMipmap` is not touched from the
|
||||
apply thread — `AllocateStorage` and `MarkStorageDirty` on it are §6 layer-1 surfaces.
|
||||
|
||||
The client shadow for such levels is stale and that is CORRECT: the two readers that could
|
||||
observe the staleness are both named refusals under split — `glGetTexImage` served from the
|
||||
client shadow (class C, P9) and `texture-remint-pull` (`Fatal{UnmigratedEmulation}`). This is
|
||||
i1's copy-image-shadow-mirror ruling (CONTRACT-P5B §2) applied to the mip chain, and tx
|
||||
reports which scenarios reach either Fatal. **AMENDED (P9 W2):** the second reader is gone rather
|
||||
than refused — under a transport a re-mint reads the GPU level itself
|
||||
(`RequireImageBindableStorageByHandle`) and never the client shadow, so a GPU-generated level
|
||||
survives it; the marker and `OnTexturePullRequest` were deleted
|
||||
(`docs/Disaggregated/notes/p9/W2-REMINT.md`).
|
||||
|
||||
### 2.4 The audit gate (E-P5c #2)
|
||||
|
||||
`MOBILEGL_IPC_AUDIT=1` fills retired staging with `0xDD` (R-11). After tx the texture bytes
|
||||
are adopted before retire, so the `0xDD` fill covers them: **reverting the adoption to
|
||||
pointer-dropping MUST turn one named texture scenario red on the four A/B traces.** That
|
||||
revert is tx's red-once (R-16), and it is the gate that makes "the server consumes the staged
|
||||
bytes" a checked fact rather than a design intention.
|
||||
|
||||
---
|
||||
|
||||
## §3 hd — sinks and twins resolve by the handle the record carried
|
||||
|
||||
The handle-keyed `GetOrCreate(MGPipeHandle)` (`SlotTables.h:278-326`) exists since P3a and
|
||||
has deliberately no caller at the contract commit (:275-277). **P5c is its first caller.**
|
||||
The rules:
|
||||
|
||||
1. **With an active transport, `MGPipeSlots()` is a client-only surface.** `Acquire`,
|
||||
`FindByLifetimeId`, `Free` called from the apply thread are `Fatal{RoleViolation,
|
||||
"MGPipeSlots"}` (§6 layer 1). The minting `GetOrCreate(const StatePtr&)`
|
||||
(`SlotTables.h:209-252`) and the death switch's `DestroyByLifetimeId` arms are
|
||||
monolith-only; split paths call the handle forms (`GetOrCreate(handle)`,
|
||||
`ReleaseByHandle`, :350-361).
|
||||
**THE NAMED EXEMPTION, ruled at the tx/ev/hd merge:** the sites whose handle-carrying
|
||||
records the client does not EMIT yet. `set_shader_buffers` and
|
||||
`set_stream_output_targets` sit in the catalogue but are P4b's to emit
|
||||
(`SetHashSuppressor.h` says so), so the buffer binding-point ensures they would feed
|
||||
(`EnsureBufferResource`→`HandleOfBuffer`, Managers.cpp:3199, and its SSBO/UBO/atomic/
|
||||
XFB/PBO callers) and the GPU-written announcement (`MarkBufferGpuWritten`'s handle
|
||||
answer, `MGPipeAnnounceBufferGpuWritten` for Magma) have no record handle to resolve
|
||||
from in P5c. Those probes run inside `MGPipeReverseAnnouncementScope`
|
||||
(`SlotAllocator.h`) — one read-only lifetime-id read, scoped, named and greppable,
|
||||
retired by P4b's emission (Espryt) and P7's server-side binding table (Magma). Every
|
||||
other apply-thread allocator access (`Acquire`, `Free`, and `FindByLifetimeId` outside
|
||||
the scope) stays Fatal with no exemption. The audit's B2 row is correspondingly
|
||||
re-scoped: what hd retires is every site a record DOES name; the rest is this scope.
|
||||
**THE SECOND NAMED EXEMPTION, ruled the same day:** the G6 frontend-keyed twin registry
|
||||
— every `HandleOf`/`Find`/`GetOrCreate(const StatePtr&)` probe the texture / sampler /
|
||||
sampler-view / VAO / program resolvers make, and the mailbox-delivered death switch
|
||||
until ct's record replaces the hop (ct keeps a documented NoSession server-only-fixture
|
||||
arm, and that arm is inside the scope too). These run inside
|
||||
`MGPipeFrontendKeyedRegistryScope` (`SlotAllocator.h`), the executable form of §5.4's
|
||||
pinned object-surface list: wrapping a site names the debt in code, an unwrapped probe
|
||||
still aborts, and the guard's death tests pin exactly that. Retires with the G6 row's
|
||||
own phases (P3b/P4b rekeys; the death switch's scope dies when the mailbox hop does).
|
||||
2. **The records already carry the handles.** `MGPBlit::ReadFbo/DrawFbo`, `MGPMipPlan::Res`,
|
||||
`MGPCopyFromFramebuffer::Dst`, `MGPDrawIndirect::Buffer` / `ParameterBuffer`,
|
||||
`MGPGridInfo::IndirectBuffer` — hd's edit is to USE them, not to add fields. A site that
|
||||
needs an object no record names is a missing handle and stops at the integrator, not a
|
||||
quietly-added lifetime-id probe.
|
||||
3. **Named blit.** `ServerVerbSink::OnBlit` resolves `ReadFbo`/`DrawFbo` through the FBO
|
||||
twin table by handle — AMENDED in shape at hd's landing: the named backend ENTRY's
|
||||
signature takes frontend `SharedPtr<FramebufferObject>` and the shared function table
|
||||
cannot grow a handle parameter without moving G1, so the record's handles reach the
|
||||
backend through a split-only verb-handle workspace in `MGPipeApplierState` (the sink
|
||||
writes it before dispatch, the backend reads it within the same verb), and
|
||||
`BlitFramebuffer` takes its named arm when the workspace names a pair: twins resolved
|
||||
by `GetOrCreateByHandle`, frontend objects reached through the twin table's state note
|
||||
(`NoteStateForHandle`/`StateForHandle` — a server-side note seeded by the record-driven
|
||||
sync, object-class, P3b/P4b retires it with the twin tables). A backend that does not
|
||||
consume the workspace declines LOUDLY (Magma until P7) — a blit that silently landed on
|
||||
the bound FBO would be a wrong picture, not a missing feature. The client's
|
||||
`ScopedBlitBindings` (`EmitTables.cpp:741-762`) is deleted from the split path — it exists
|
||||
only to stage values for the server's binding-slot read, and the read is gone. The
|
||||
record's `MGPBlit` payload is unchanged.
|
||||
4. **CopyTex** (`DirectGLES.cpp:8560-8563`, :8655-8658) resolves the destination from
|
||||
`MGPCopyFromFramebuffer::Dst`; the `GetTextureUnitObject` unit-slot read is a layer-1
|
||||
surface at these two sites.
|
||||
5. **Indirect buffers.** Draw-indirect / parameter / dispatch-indirect buffer objects come
|
||||
from the record's handle fields (CONTRACT-P5B §2 d1/i1 already put them there); the
|
||||
`GetBufferBindingSlot(DrawIndirect)` read at `DirectGLES.cpp:6702-6703` and its dispatch
|
||||
twin are layer-1 surfaces at those sites.
|
||||
6. **`HasDefinedContent` is the descriptor's bit OR the staged coverage set** — AMENDED at
|
||||
hd's landing, because this contract's premise was wrong: the bit does NOT move only at a
|
||||
storage-defining call. The streaming idiom (`glBufferData(size, NULL)` then
|
||||
`glBufferSubData`) defines content through subdata with NO new descriptor
|
||||
(Managers.cpp:3130-3146 says so, `ServerLoopTest`'s streaming cases pin it), and a pure
|
||||
descriptor read would orphan live bytes at the next respecify. The answer is
|
||||
`Desc.HasDefinedContent || the staged coverage set is non-empty` — the coverage set IS
|
||||
"content records delivered since the last orphan", pointwise equivalent to the
|
||||
frontend's flag and purely server-side. `bufferObject->HasDefinedContent()`
|
||||
is a layer-1 surface.
|
||||
7. **caps.** Magma's four reads of the client mirror (`DirectVulkan.cpp:713-715`,
|
||||
`VulkanRenderer.cpp:667-676`, `VertexInputStateFactory.cpp:249-250`, :502-503) read
|
||||
`ServerLoopInstance().Backend()->GetDynamicParameters()` — the server's own backend, the
|
||||
same source Espryt's `ActiveBackendFormatCaps` already prefers (`Utils.cpp:45-48`).
|
||||
`Utils.cpp:50`'s fallback arm to `pActiveBackendObject` is **refused** with an active
|
||||
transport (`Fatal{RoleViolation, "caps-mirror"}`): the server's backend exists whenever
|
||||
the session does, and a silent read of the mirror would hide a bring-up ordering defect.
|
||||
8. **The legacy buffer arm is a named refusal under transport** (B3). `MappedData()` /
|
||||
`IsMapped()` / `GetChangeSerial()` / `SyncPersistentMappedRange()` on the frontend
|
||||
`BufferObject` (Managers.cpp:1012-1022, :1119-1121, :2944-2949, :3281-3327) take
|
||||
`Fatal{RoleViolation, "buffer-legacy-arm"}` from the apply thread — the same shape as
|
||||
`Fatal{PipeLegacyMemosDisabled}`, closing the arm that a cleared subsystem bit 7 currently
|
||||
leaves reachable with one `MGLOG_D`.
|
||||
|
||||
---
|
||||
|
||||
## §4 ev — SEG_EVENT is the only reverse channel
|
||||
|
||||
The segment, the ring, the three event heads and the full consumer exist
|
||||
(`EventRing.h`, `ClientSession.cpp:176-267`); production has **zero producers**
|
||||
(`ServerSession::PublishEvents`, :484-492, has no caller). ev wires the three producers and
|
||||
retires every direct call into the client. The ten `MGPipeCallbacks` members stay ten; P5c
|
||||
arms three and a half (writeback, gpu-written, surface-changed, and gl-error as the fourth
|
||||
kind) — the remaining six keep CONTRACT-P5's P9 phase.
|
||||
|
||||
### 4.1 Producer ownership (Table 3 amendment)
|
||||
|
||||
With an active transport, the three reverse entries of `gMGPipeCallbacks` are installed by
|
||||
the SERVER session (producer callbacks that Reserve + Publish into SEG_EVENT), never by the
|
||||
client; `MGPipeInstallClientResourceCallbacks` (`ResourceTracker.h:606-613`) is
|
||||
monolith-only. A second installation over a live entry is `Fatal{RoleViolation,
|
||||
"callback-double-install"}` — the "never over an entry a backend already claimed" comment
|
||||
becomes a check. The apply thread calling a client-installed callback (the R1/R2 shape) is a
|
||||
layer-1 violation; the GL thread calling a server-installed producer is layer 2.
|
||||
|
||||
`DrainEventRing` calls the client consumers BY NAME (`MGPipeClientOnBufferWriteback` /
|
||||
`MGPipeClientOnGpuWritten`, `ResourceTracker.h:553-601`), not through `gMGPipeCallbacks` —
|
||||
the global table is a producer-side surface under split. **The drain points are TWO classes**
|
||||
(AMENDED at the regression triage): inside `EmitAndWait` after the barrier
|
||||
(`ClientSession.cpp:836`), AND at the return of the blocking EGL lifecycle RPCs
|
||||
(`BackendObject_Remote`'s surface creation / resize / make-current). Both satisfy the same
|
||||
safety premise — the apply thread is known idle because the work it was doing (the verb, the
|
||||
RPC) has completed and this thread has published nothing since. The second class exists
|
||||
because a surface-changed event posted during bring-up must be applied BEFORE the first
|
||||
frontend query of the default framebuffer's attachments: waiting for the first verb's drain
|
||||
answered the placeholder format to `glGetFramebufferAttachmentParameteriv`, and every buffer
|
||||
allocated from that answer was blit-incompatible with the real surface.
|
||||
|
||||
### 4.2 The three (+1) producers
|
||||
|
||||
| event | producer site (today's direct call) | payload |
|
||||
|---|---|---|
|
||||
| `kEventBufferWriteback` | `Ops_H_Readback`'s writeback (Managers.cpp:2339-2343): the server copies the mapped bytes INTO the event record's inline tail instead of casting the pointer into `MGPBlobRef::Offset` | `EventBufferWritebackHead` + inline bytes |
|
||||
| `kEventGpuWritten` | `MarkBufferGpuWritten` (Managers.cpp:2722-2753) and Magma's three bypasses (`UniformManager.cpp:1075`, :1231, `VulkanRenderer.cpp:11618`) — Magma's direct `bufferObject->MarkGpuWritten()` calls are ROUTED THROUGH the callback, the bypass is deleted | `EventGpuWrittenHead` + `EventRange[RangeCount]` |
|
||||
| `kEventSurfaceChanged` | `PublishDefaultFramebufferDepthStencilFormat` (`DirectGLES.cpp:11519-11610`) and the swapchain twin (`SwapchainObject.cpp:276-333`): the backend fills `MGPSurfaceInfo` and posts; it no longer calls `AllocateStorage`/`SetInternalFormat` on `pDefaultFramebufferInfo` | `EventSurfaceChangedHead` |
|
||||
| `kEventGlError` | `PipeInputs::RecordError` (`PipeFill.cpp:1764-1772`) posts with `ErrorCode` + message (§1). **Ordering is P9's** (CONTRACT-P5 table 2 row `RecordError`): the event is observed at the next drain point, which preserves per-thread program order of error-then-read but not cross-verb interleaving; that is the accepted P5c shape and the contract says so rather than discovering it in P6. | `EventGlErrorHead` + inline message |
|
||||
|
||||
`PipeInputs::InvalidateCompileEnv` (`PipeFill.cpp:1753-1756`) is DELETED with an active
|
||||
transport: R-12's caps re-publication (`CapsMirror.cpp:78-80`) already invalidates the
|
||||
compile environment on the client, and the forward is a write into the frontend with no wire
|
||||
shape. Its FieldOwnership.def row moves to FATAL under split (§5.4).
|
||||
|
||||
### 4.3 The consumer's segment arm (and the guard that has to die first)
|
||||
|
||||
`MGPipeClientOnBufferWriteback`'s monolith guard (`ResourceTracker.h:560-564`) currently
|
||||
REJECTS any `Seg != kMGHostSpanSegNone` with `MGLOG_E_ONCE` — wiring a producer without
|
||||
removing it would drop every writeback event on arrival. The replacement arm:
|
||||
|
||||
```
|
||||
Seg == kSegEvent -> resolve via the session's SegmentTable entry for kSegEvent
|
||||
(installed at session start, ServerSession.cpp:333-335 and its
|
||||
client twin); bounds-checked against the announced segment size
|
||||
Seg == kMGHostSpanSegNone -> monolith only; with an active transport it is
|
||||
Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"} (rule B,
|
||||
reverse direction)
|
||||
anything else -> Fatal{ProtocolCorruption, "OnBufferWriteback.Seg"}
|
||||
```
|
||||
|
||||
The surface-changed consumer (stub at `ClientSession.cpp:240-256` today) APPLIES the
|
||||
`MGPSurfaceInfo` to the client-owned `pDefaultFramebufferInfo` — the allocate/format writes
|
||||
happen on the GL thread against client memory, which is where R3's ownership always was.
|
||||
|
||||
### 4.4 Overflow (P5c's ruling, P9 owns the policy)
|
||||
|
||||
All four events are **lossless** in P5c. `Reserve` returning nullptr is
|
||||
`Fatal{EventRingOverflow}`, not a drop: the ring is 256 KiB, events are drained at every verb
|
||||
barrier, and a full ring under lockstep means a producer burst no measured workload has.
|
||||
`CountDrop` and the `eventRingFull` latch stay built and stay unused-by-policy, exactly as
|
||||
`EventRing.h:14-16` rules — P9 decides between waiting and dropping; P5c makes overflow a
|
||||
defect instead. The exit gate `eventDropped == 0` at drain points (E-P5c #3) is what checks
|
||||
this row.
|
||||
|
||||
**AMENDED at the regression triage — the ring is sized for events, not buffers.** A
|
||||
whole-buffer writeback larger than the ring can NEVER fit (one record is capped at
|
||||
capacity/2), so the CLIENT slices the request (`BufferWritebackSliceBytes`: a quarter of the
|
||||
ring, floored at 4 KiB — a quarter, not the half, because the record carries its header
|
||||
beside the payload and the ring may still hold small events from the same verb). Each slice
|
||||
round-trips its own barrier + drain, so the ring holds at most one slice's bytes, and the
|
||||
in-order channel makes the last slice's landing imply every earlier one. This is a
|
||||
client-side ruling about request SHAPE, not a drop policy; the Fatal stays for a producer
|
||||
burst of real events.
|
||||
|
||||
### 4.5 Round-trip red-once
|
||||
|
||||
Each of the three P5 events gets a unit round-trip beside `SessionTest.cpp:747-823` (the
|
||||
fixture already drives one producer and one consumer over real segments), and ONE integration
|
||||
scenario where reverting a producer to the raw-pointer shape turns the run red (E-P5c #3).
|
||||
`kEventGlError` gets the unit round-trip only; its ordered-error sibling is P9's.
|
||||
|
||||
---
|
||||
|
||||
## §5 ct + rv — the two control records and the residual-value record
|
||||
|
||||
### 5.1 `applier_reset` — opcode 77, appended
|
||||
|
||||
`X(ApplierReset, MGPApplierReset, kScreen, kNone)`, appended after
|
||||
`CopyFramebufferToTexture` (PipeCalls.def:233; opcode = position = 77).
|
||||
`struct MGPApplierReset { Uint64 ContextSerial; };`, `MGP_ASSERT_POD(MGPApplierReset, 8)`.
|
||||
|
||||
- **Producer:** the GL thread, at the `tracker.FreshlyPrimed()` edge
|
||||
(`PipeFill.cpp:2654-2667`), with an active transport ONLY. The emit happens BEFORE the
|
||||
block's client-side resets (CsoCache, hash suppressor, vertex-input emitter — all client
|
||||
surfaces, they stay); the record's barrier is what orders the server's reset against every
|
||||
verb that follows.
|
||||
- **Sink:** `ServerVerbSink::OnApplierReset(const MGPApplierReset&)` →
|
||||
`MGPipeApplierReset()` (`PipeApply.cpp:1202-1300`) after asserting `ContextSerial` against
|
||||
the session's. `g_applier` stays server-private (`PipeApply.cpp:409`).
|
||||
- **The GL thread calling `MGPipeApplierReset()` directly with an active transport is
|
||||
`Fatal{RoleViolation, "g_applier"}`** (G2, layer 2). Monolith keeps the direct call —
|
||||
G1's byte-identity is untouched because the emit table exists only under split.
|
||||
|
||||
### 5.2 `object_death` — opcode 78, appended
|
||||
|
||||
`X(ObjectDeath, MGPHandleOnly, kCtxObject, kNone)` — the framebuffer family's FIRST wire
|
||||
delete opcode (G3). Emitted by the GL thread from
|
||||
`OnFrontendStateObjectDestroyed` (Managers.cpp:206-256) with an active transport:
|
||||
|
||||
1. Look the dying object's handle up in the CLIENT's own allocator
|
||||
(`MGPipeSlots().FindByLifetimeId` — legal: client surface, client thread).
|
||||
2. **No handle → emit nothing.** The server never saw the object, so there is no twin to
|
||||
kill; this replaces the mailbox's unconditional delivery.
|
||||
3. Handle → `EmitAndWait(ObjectDeath, {handle, kind})`. The wait keeps the death's
|
||||
ordering against in-flight verbs that name the handle, which is the only property the
|
||||
blocking `RunOnApplyThread` provided and the only one P5c keeps.
|
||||
|
||||
Sink: `ServerVerbSink::OnObjectDeath(const MGPHandleOnly&)` → the per-kind release
|
||||
(`ReleaseByHandle`, `SlotTables.h:350-361`; the SamplerViewCso arm's idempotent
|
||||
secondary path, Managers.cpp:239-256, is keyed by the view's handle the same way). The
|
||||
mailbox hop (Managers.cpp:208-222) is deleted under split; it stays for monolith, where it
|
||||
never fired anyway (the guard at :208 is transport-gated). **AMENDED at ct's landing:** a
|
||||
documented NoSession fallback arm delivers a death to a server-only fixture's loop (a
|
||||
transport configured with no client session is not a real split); with the §3.1
|
||||
exemption, that arm's `DestroyByLifetimeId` probes ride inside the registry scope.
|
||||
|
||||
### 5.3 `set_context_values` — opcode 79, the rv field table
|
||||
|
||||
`X(SetContextValues, MGPContextValues, kCtxState, kNone)`. One POD, emitted at validate when
|
||||
any covered value moved (the tracker's value-class dirty accounting already watches all of
|
||||
them — "值类零新增记账", ARCHITECTURE §5.2), hash-suppressed as a whole. The fields and what
|
||||
each retires:
|
||||
|
||||
| payload field | PipeInputs accessor it feeds | retires (FieldOwnership.def row) |
|
||||
|---|---|---|
|
||||
| `Uint32 ActiveTextureUnit` | `GetActiveTextureUnit` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED |
|
||||
| `Uint32 MaxTouchedTextureUnit` | `GetMaxTouchedTextureUnit` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED (the hash-suppressed set's high-water mark rides THIS record's unsuppressed value) |
|
||||
| `Uint32 TouchedBufferBindingPointCount[15]` (indexed by BufferTarget, the Coverage.def 15) | `GetTouchedBufferBindingPointCount(target)` | BARRIER_PULLED "P3b/P4b" → RECORD_SUPPLIED |
|
||||
| `Uint8 IsTransformFeedbackActive`, `Uint8 IsTransformFeedbackPaused` | the two accessors | BARRIER_PULLED "P3b/P4b (Espryt), P7 (Magma)" → RECORD_SUPPLIED |
|
||||
| `Uint64 TransformFeedbackGeneration`, `Uint64 BoundTransformFeedbackLifetimeId`, `Uint64 TransformFeedbackCapturedVertices` | the three accessors | BARRIER_PULLED → RECORD_SUPPLIED |
|
||||
|
||||
- **`GetCurrentVertexAttribute` does NOT ride this record.** Its problem is the applier's
|
||||
cross-view conversion (FieldOwnership.def:77-82), so `MGPVertexAttribDefaults` grows to
|
||||
carry the frontend's CONVERTED value (all three views as the frontend computed them), the
|
||||
same amendment shape as P5b's `MGPCopyRegion` 64 → 72 B (CONTRACT-P5B §2 i1). The row
|
||||
becomes RECORD_SUPPLIED; `set_vertex_attrib_defaults` keeps its opcode.
|
||||
- **The three shutters** (`GetSamplingResolutionGeneration`, `GetTextureBindGeneration`,
|
||||
`GetTextureContextId`) carry NO wire field: they move to APPLIER_DERIVED, answered from the
|
||||
applier's own Serial — which is what their rows have said since P5 ("a shutter, not a
|
||||
value", FieldOwnership.def:94-102). rv's edit makes the accessor DO it.
|
||||
- **`GetTransformFeedbackProgram` stays BARRIER_PULLED** (object-class:
|
||||
`SharedPtr<ProgramObject>`; the twin tables are P3b/P4b/P7's). rv's exit line "值类行为 0"
|
||||
(E-P5c #4) is checked against the value-class list pinned in `FieldOwnershipTest`, and the
|
||||
object-class rows are re-annotated with their retiring phase in the same edit so the test
|
||||
can pin that they are ALL the remaining rows.
|
||||
|
||||
### 5.4 FieldOwnership.def amendments (one list, made by c0c)
|
||||
|
||||
The generator's four classes are unchanged. c0c edits rows; rv/tx/hd land the code:
|
||||
|
||||
- The §5.3 value rows → RECORD_SUPPLIED (derived, via Coverage.def's emitted list gaining
|
||||
`SetContextValues` rows; the derivation's contradiction rule is what proves the edit
|
||||
landed).
|
||||
- The three shutters → APPLIER_DERIVED ("the applier's Serial").
|
||||
- `GetCurrentVertexAttribute` → RECORD_SUPPLIED (the amended payload).
|
||||
- `InvalidateCompileEnv` (field + forward rows) → FATAL with mechanism "DELETED under split
|
||||
by P5c ev; R-12's caps re-publication replaced it" — the class is stated unconditionally
|
||||
because the table only ever describes the split server's reads (under monolith there is no
|
||||
server to read), so "under split" would be noise in a column that means it everywhere;
|
||||
- **The texture family is NOT a set of new field rows** — a ruling, because the texel /
|
||||
per-level extent / dirty reads are reads of frontend OBJECT internals reached through
|
||||
`GetTextureUnitObject` / the `GetTextureObject` sticky forward, not `MGPipeInputField`
|
||||
enum values, and a row naming a non-field stops the generator by design. What pins them
|
||||
instead: (a) `GetTextureUnitObject`'s existing row is re-annotated — the tx-retired reads
|
||||
leave its site list, what remains is the unit-object POINTER reads (P3b/P4b/P7); (b) the
|
||||
object-surface list (texel bytes, dirty region, `HasDefinedContent`,
|
||||
`MappedData`/`IsMapped`/`GetChangeSerial`) is pinned verbatim in `FieldOwnershipTest` as
|
||||
the layer-1 surface set, and the §6 guard is what enforces it. **AMENDED at gt's
|
||||
landing:** "per-level extent" leaves the guard list — the pinned object-class rows'
|
||||
per-frame binding walk legally depends on `GetMipmapTexelSize`, so extent is retired AT
|
||||
THE SITES (tx's sync reads the store) rather than guarded on the accessor. This is the
|
||||
honest form of ROADMAP's "把纹理家族补进 FieldOwnership.def": the .def's mechanism covers
|
||||
fields, and the family's object reads get a pinned list plus a guard that can go red.
|
||||
|
||||
---
|
||||
|
||||
## §6 gt — the role guards (semantics, two layers)
|
||||
|
||||
With an active transport, in a split build only (`MOBILEGL_BUILD_DISAGGREGATED`; the pull
|
||||
build's bytes do not move, G1):
|
||||
|
||||
**Layer 1 — apply thread may not touch frontend surfaces.** `Fatal{RoleViolation,
|
||||
"<surface>"}` raised inside: `MGPipeSlots()` (all three entry points); the frontend
|
||||
`BufferObject` legacy accessors (§3.8); the frontend `TextureObjectMipmap` /
|
||||
`MipmapStorage` mutators and maps (`AllocateStorage`, `MarkStorageDirty`, `MapMipmapData`
|
||||
and the storage-dirty queries); `pDefaultFramebufferInfo`'s attachment writes; the
|
||||
client-caps-mirror fallback (`Utils.cpp:50`); any BARRIER-PULLED accessor whose row is no
|
||||
longer BARRIER_PULLED after §5 (the stamp mechanism's `MGPipeInputUnfreshRead` already
|
||||
aborts these — gt's edit is that it aborts as RoleViolation with the surface named, and the
|
||||
VALUE-class set is empty at exit). Implementation surface: `ServerLoop::OnApplyThread()`
|
||||
gated checks at the named entries; compiled out entirely in pull builds.
|
||||
|
||||
**Layer 2 — GL thread may not touch server surfaces.** `Fatal{RoleViolation, "<surface>"}`
|
||||
raised on: direct `MGPipeApplierReset()` (§5.1); `ServerLoop::Backend()` /
|
||||
`MGPipeGetResourceOps()` reads AFTER session start (the bring-up window before
|
||||
`ClientSessionInstance().Start()`, `MG_Backend/Init.cpp:100-247`, is the documented exception — it is
|
||||
single-threaded and pre-dates the roles); a write into `gPipeInputs` outside the residual
|
||||
fill, and any client-side read of a server-stamped field outside a barrier wait.
|
||||
|
||||
**A1 is ruled: `InBarrierWait()` is WIRED, not deleted.** CONTRACT-P5 table 3's sentence —
|
||||
"the client checks it when it touches `gPipeInputs` outside a barrier (`InBarrierWait` /
|
||||
`ApplyThreadIsInsideApplier`)" — is currently half-true: `ApplyThreadIsInsideApplier` fires
|
||||
at emit (`ClientSession.cpp:748`), `InBarrierWait` (`:880`) has zero readers. gt makes the
|
||||
sentence true: the layer-2 `gPipeInputs` check consults `InBarrierWait()` exactly as the
|
||||
contract describes. Deleting the sentence instead was considered and rejected: the check is
|
||||
the only client-side half of the gPipeInputs single-writer rule, and P6 needs it armed, not
|
||||
forgotten.
|
||||
|
||||
**Red-once (R-16), per layer, mandatory:** each layer is switched off in one named
|
||||
integration scenario and the run MUST go red with the layer's Fatal — this is E-P5c #1's
|
||||
"关掉任一层守卫必须能在一条具名用例上变红", and it is what makes the guard a gate rather
|
||||
than a comment. **CI lane, AMENDED at gt's landing:** a scenario-green
|
||||
`MOBILEGL_IPC_STRICT_ERRORS=1` lane is impossible while the object-class rows live (the
|
||||
first Clear pulls `GetFramebufferBindingSlot@Clear` and aborts by design), so the lane is
|
||||
two-sided: the unit lane under strict is the hard green gate (the RemoteGuards and
|
||||
FieldOwnershipTest strict arms live there), and the integration-split scenario lane is
|
||||
EXPECTED-RED with the `BARRIER-PULLED, MOBILEGL_IPC_STRICT_ERRORS=1` marker asserted in
|
||||
each entry's private log — the shape `split_negative_controls.sh` already drives. It
|
||||
becomes a plain green lane once P7 retires the object-class rows.
|
||||
|
||||
**`rsp` at exit.** `ResidualPulls` enters the per-frame stats line and is MEASURED on the
|
||||
four A/B traces; the remaining reads are exactly the object-class rows pinned by
|
||||
`FieldOwnershipTest` (§5.3), value rows = 0. **AMENDED at gt's landing:** the sticky-forward
|
||||
blind spot this paragraph assigned to gt was already closed (`MGPipeStickyForwardPull` has
|
||||
counted into `ResidualPulls` since `d709ef3e`; `FieldOwnershipTest` pins it). What the
|
||||
measurement actually found on the locally available traces: bsl in-world 948.5 rsp/frame
|
||||
(38.7/draw, 123 frames), iris-complementary 1591.9, iris-iterationrp 286.2 up to its
|
||||
pre-existing `Fatal{UnmigratedEmulation, "texture-remint-pull"}` — all object-class, zero
|
||||
value-class. The residual known blind spot is narrower than planned: a record applied
|
||||
OUTSIDE a verb boundary withdraws the stamp (`PipeApplier.cpp` notes it), and a sticky pull
|
||||
inside one escapes the count; no measured workload produces one.
|
||||
|
||||
---
|
||||
|
||||
## §7 Amendments to CONTRACT-P5 / P5B (the complete list)
|
||||
|
||||
1. **Table 0** gains §1's six rows (reverse-channel blobref, ObjectDeath's payload reuse,
|
||||
ApplierReset::ContextSerial, MGPContextValues, server-side level extent, kEventGlError).
|
||||
2. **Table 1** gains three rows: `applier_reset` (77), `object_death` (78),
|
||||
`set_context_values` (79) — all appended, opcode = position, never inserted.
|
||||
3. **Table 2** (FieldOwnership.def): the §5.4 amendments; BARRIER_PULLED's value-class
|
||||
membership goes to zero and `FieldOwnershipTest` pins the remainder.
|
||||
4. **Table 3**: the three reverse `MGPipeCallbacks` entries move from "client installs" to
|
||||
§4.1's producer ruling; `g_applier` gains "reset only via `applier_reset` under
|
||||
transport"; the `InBarrierWait` sentence becomes true (§6); the bring-up window is named
|
||||
as the one legal cross-role read.
|
||||
5. **CONTRACT-P5B §2 f1's named blit**: "降为现有 bound backend 调用" is superseded by
|
||||
§3.3 — the sink resolves `ReadFbo`/`DrawFbo` from the record and calls the named entry;
|
||||
`ScopedBlitBindings` is deleted from the split path. P5b's "P5b 留下的 inproc 依赖"
|
||||
list loses that row.
|
||||
6. **`MGPVertexAttribDefaults` grows** (§5.3) — the second payload-size amendment after
|
||||
P5b's `MGPCopyRegion`; the ABI fingerprint (`SessionRings.h:498-524`) is what catches a
|
||||
desync, and `PipeCatalogueTest` pins the new size.
|
||||
7. **`EventKind` gains `kEventGlError = 4`** — the first event-kind addition; the kind
|
||||
space is append-only for the same reason the opcode space is.
|
||||
|
||||
## §8 Exit gates this contract serves (E-P5c, restated for the packages)
|
||||
|
||||
1. Guards armed: `integration-split` (107 at plan time; 111 as landed — ct added four
|
||||
`CtWireScenario` entries) green, broad inproc census with zero regressions
|
||||
vs `348d22a4`, 79 traces with no new first blocker; each guard layer red-once (§6).
|
||||
2. `MOBILEGL_IPC_AUDIT=1`'s `0xDD` covers texture staged bytes on the four A/B traces;
|
||||
reverting adoption goes red (§2.4).
|
||||
3. Three events round-trip; `eventDropped == 0` at drain points; producer→raw-pointer
|
||||
revert goes red (§4.5).
|
||||
4. `rsp` measured per frame on the four A/B traces; remaining pulls are exactly the pinned
|
||||
object-class rows (§5.3, §6).
|
||||
5. Redmi four-arm retest recorded (barrier tax re-measured after writeback / gpu-written
|
||||
move to events) — recorded, not gated.
|
||||
6. G1 0/0/0/0, G2, G14, G5 as usual.
|
||||
|
||||
Explicitly NOT P5c (ROADMAP "留给后续阶段的"): object-class BARRIER_PULLED rows and the
|
||||
frontend-keyed twin registry (P3b/P4b, P7); XFB scatter / `OnXfbScatterReady` /
|
||||
`OnTexturePullRequest` / ordered `OnGlError` (P9); the EGL forwarders' control plane (P6);
|
||||
copy-image shadow mirror and CopyTex texel writeback (P8/P9); `s_synced` /
|
||||
`g_syncedRenderStateParameters` context-generation resets (P6).
|
||||
@@ -0,0 +1,804 @@
|
||||
# CONTRACT-P5E — the client runs ahead of apply on the Espryt draw path
|
||||
|
||||
> Historical P5e backend scope: the later [Magma run-ahead contract](CONTRACT-MAGMA-RUNAHEAD.md)
|
||||
> supersedes this document's "Magma never publishes bit 10" restriction after P5f and the
|
||||
> server-buffer migration. The wire wait classes, events and present-credit protocol remain.
|
||||
|
||||
Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17),
|
||||
`CONTRACT-P5B.md` (the class-C slots) and `CONTRACT-P5C.md` (rule E, the two named exemptions,
|
||||
SEG_EVENT, the guards). Where it disagrees with any of them this file is newer and wins; §8 lists
|
||||
every such place. Base: `feat/disaggregated @ 2fde7034` (code head `1f8de61b`). Every `file:line`
|
||||
was read at that commit and re-resolved at c0e's head; paths are under `MobileGL/` unless they
|
||||
start with `docs/`.
|
||||
|
||||
**How to change it.** Package c0e's file, edited by the integrator first. A P5e package (id / vi /
|
||||
sb / pg / tx2 / fb / ra) that needs a row changed goes through the integrator; the packages compile
|
||||
against the rows below from day one.
|
||||
|
||||
**What c0e actually landed, where it differs from the plan this file was written as.** Four places,
|
||||
each argued where it sits: `MGPProgramDesc` GREW 192 → 200 bytes because it has no spare byte (§1);
|
||||
the applier's per-class binding-point windows are spelled `ShaderBufferStart/Count/WritableMask`
|
||||
rather than bare `Start/Count/WritableMask` (§5.6); the suppressor's three slots are named
|
||||
`SetShaderBuffersUniform/ShaderStorage/AtomicCounter` (§1); and the wire codec's record layout now
|
||||
carries THREE tails rather than two, because `set_program_bindings` has three (§1). Everything else
|
||||
below is the plan as written, and the nineteen rulings of §9 are as the integrator decided them
|
||||
(ID-80…98).
|
||||
|
||||
---
|
||||
|
||||
## §0 What P5e is, and the rule above every row
|
||||
|
||||
P5d made `inproc` CPU-bound on the client thread with the frame still `client work + (per draw)
|
||||
wait for apply + handoff` (R-1, `MG_Remote/Client/ClientSession.cpp:932-959`). The wait exists
|
||||
because the apply of a draw still dereferences client-owned memory: the object-class BARRIER_PULLED
|
||||
rows of `MG_Pipe/FieldOwnership.def:72-106` and the frontend-keyed twin registries
|
||||
(`MG_Backend/DirectGLES/SlotTables.h:411-449`) inside the two P5C §3.1 exemption scopes. **P5e
|
||||
retires the lockstep for every op on the Espryt draw path: the client publishes and moves on; the
|
||||
server resolves every object from a handle a record carried, and the only client memory it may read
|
||||
is the residual fill of a record the client is provably blocked behind.**
|
||||
|
||||
Rules A–E bind unchanged. P5e adds one definition and one rule:
|
||||
|
||||
**Definition — a BARRIERED record.** A record is barriered iff `MGPipeBarriered(op, payload,
|
||||
applierState)` is true, a pure function of wire-visible data computed identically by the emit
|
||||
table and by the sink (§2.1). The client blocks in `WaitForApplied(seq)` after publishing a
|
||||
barriered record; it never blocks after an unbarriered one.
|
||||
|
||||
**Rule F — an unbarriered record's apply names no client memory.** With a live wire on a
|
||||
run-ahead server (§6), the apply of an unbarriered record reads NOTHING from client-owned memory:
|
||||
no frontend object dereference, no `gPipeInputs` BARRIER_PULLED row, no `MGPipeSlots()` probe or
|
||||
mint, no `SharedPtr` to a frontend object taken or held. Every object is resolved from a handle the
|
||||
record or the applier state carries, through a handle-keyed twin table (§4). A violation is
|
||||
`Fatal{UnmigratedPipeInput, "<field>@<verb>"}` (a pulled row, §3.3), `Fatal{RoleViolation,
|
||||
"MGPipeSlots"}` (an allocator probe, §4.4) or `Fatal{RoleViolation, "<surface>"}` (P5C §6 layer 1),
|
||||
**regardless of `MOBILEGL_IPC_STRICT_ERRORS`** — the value would be torn or stale by construction,
|
||||
so there is no "count it" arm. A barriered record's apply keeps P5C's rules exactly: its pulls count
|
||||
into `rsp`, its probes are legal inside a scope, and the client's wait is what makes them legal.
|
||||
|
||||
Refusal vocabulary is P5C's, plus: `Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` (§5.1),
|
||||
`Fatal{UnmigratedVerb, "set_global_constants"}` (§5.5), `Fatal{UnmigratedEmulation,
|
||||
"image-bindable-redirty"}` (§5.2), `Fatal{ProtocolCorruption, "SetSamplerViews.Count"}` /
|
||||
`"BindSamplerStates.Count"` / `"SetShaderBuffers.Class"` / `"SetShaderBuffers.Count"` /
|
||||
`"SetProgramBindings.<field>"` / `"Present.FrameSerial"` / `"ImageView.Access"` (§1, §5),
|
||||
`Fatal{PresentCreditTimeout}` (§2.4), `Fatal{RoleViolation, "gPipeInputs"}` (§3.5).
|
||||
`Fatal{EventRingOverflow}` is RETIRED on a run-ahead server (§2.6).
|
||||
|
||||
---
|
||||
|
||||
## §1 Table 0 additions — encodings the P5e rows introduce
|
||||
|
||||
| field | the ruling | zero means | who reads it |
|
||||
|---|---|---|---|
|
||||
| **`MGPCapBit::kCapRunAheadApply = 1ull << 10`** (`MGPipeTypes.h`, bits 0..9 used) | Set in the DirectGLES arm of the server's `CallMask` (`MG_Backend/Init.cpp`), NEVER in the DirectVulkan arm. THE ARM IS A PURE FUNCTION, `MGPipeRunAheadCapBitsFor(backendType, ready)` in `MGPipeTypes.h`, so "Magma never" is a unit case (`MG_Test/Pipe/MagmaPipeIdentityTest.cpp`) rather than a line only a live split session reaches; `ready` is `MG_Backend/Init.cpp`'s `kMGPipeP5eRunAheadReady`. It says "this server applies unbarriered records without reading client memory". The client latches `RunAheadArmed()` = `m_barrierArmed && Ipc.RunAhead && Caps().HasCap(kCapRunAheadApply)` at the first caps adoption after `Start`; a later snapshot may only turn it OFF. | absent = lockstep (Magma; and Espryt until the integration commit flips `kMGPipeP5eRunAheadReady`, §6.2) | `ClientSession::RunAheadArmed`, `MGPipeBarriered` on both sides |
|
||||
| **`MGPipeWaitClass`** — a FIFTH column of `PipeCalls.def`'s X-macro (`MGPipe.h`'s enum, spelled as a bare token like the class column), generated beside `kMGPipeCallFlags` into `generated/PipeWire.inc` as `MGPipeWaitClassFor(op)` | `kWaitReply` (every `kReplySlot` row), `kWaitPresent` (`Present`), `kWaitApplied` (the static barriered rows of §2.2), `kWaitNone` (everything else). Opcode order and the flag column are untouched: the column is a value, not a new op. `gen_pipe.py` refuses a token that is not an `MGPipeWaitClass` enumerator AND a row where the flag column and this one disagree in either direction — a `kReplySlot` row that did not wait is a waiter that hangs, a `kWaitReply` row with no slot blocks on an answer nobody posts. Three negative controls in `--self-test` (twelve now, up from nine). | — | `EmitAndWaitTails`, `ServerVerbSink::ApplyOne`, `MGPipeBarriered` |
|
||||
| **`MGPDrawInfo::Flags` gains `kDrawClientArrays`** = `1u << 6`, the next free bit beside `kDrawIsIndirect` (`MGPipeTypes.h`'s `MGPDrawFlagBit`; `MGPDrawInfo` stays 56 bytes, the flag byte had two bits left) | Set by the client emit arm when any ENABLED attribute of the bound VAO has no buffer. §2.1 escalation (ii). `PipeCatalogueTest` pins the bit's value and its disjointness from the five before it. | no client array in this draw | `MGPipeBarriered`; `EmitDrawRecord`'s refusal (§5.1) |
|
||||
| **`MGPPresent::FrameSerial`** | Minted by the CLIENT, `++m_presentsSent`, 1-based — no longer 0 (`EmitTables.cpp:1040`). The sink calls `ServerSession::ReturnPresentCredit(FrameSerial)` after `table->Present()` returns (today `ReturnPresentCredit` has no production caller, `ServerSession.cpp:677-682`), one credit per swap. | 0 arriving on a run-ahead server is `Fatal{ProtocolCorruption, "Present.FrameSerial"}` | `OnPresent`, `WaitForPresentAck` |
|
||||
| **`MGPShaderBuffers` + `MGPBufferRange[]`** (`MGPipeTypes.h`, wire op 38, catalogued since P4a, no route/sink/emitter today) | One record per **Class** ∈ {Uniform=0, ShaderStorage=1, AtomicCounter=2} — the three values are `kMGPipeShaderBufferClassUniform/ShaderStorage/AtomicCounter` in `MGPipeTypes.h`, which is the ONLY spelling of them: the payload's comment had named them since P4a and nothing numbered them, so the emitter and the applier were one literal each away from disagreeing. `Start = 0` always; `Count = TouchedBufferBindingPointCount[target]` (the rv value already carried) clamped to `kMGPipeMaxBufferBindingPoints = 84` (pinned against `BufferState.h:29` in `PipeFill.cpp`); per entry `Res` = the buffer handle or null, `Offset/Size` from the point's range, **`Size = kMGPipeWholeBuffer` (`MGPipeTypes.h:406`) for a base binding** so the server re-resolves the extent against ITS descriptor at use (a `glBufferData` between emit and apply then binds the new extent, as GL does); `WritableMask` bit per entry for classes 1/2. `HostSpanCount = 0` on Espryt for the whole of P5e (`kCapNeedsHostUboBytes` is 0, `CONTRACT-P5.md:64`); the codec's honesty pass (`PipeWireCodec.cpp:1783-1799`) is the guard. `Class >= 3` → `Fatal{ProtocolCorruption, "SetShaderBuffers.Class"}`; `Start + Count > 84` → `"SetShaderBuffers.Count"`. XFB targets do NOT ride this record (`set_stream_output_targets` keeps its row, still unemitted: XFB stays lockstep, §5.7). | `Res` null = nothing bound at that point | `MGPipeApplySetShaderBuffers`, `SyncBufferBindingPointsByRecord`, the UBO loop, `SyncAtomicCounterBuffers` |
|
||||
| **`set_program_bindings` — opcode 80, appended.** `X(SetProgramBindings, MGPProgramBindings, kCtxState, kVarTail\|kHostSpan, kWaitNone)` | Head `MGPProgramBindings { MGPipeHandle Cso; Uint64 Signature; Uint32 BlockBindingCount, SamplerUnitCount, StorageOverrideCount, Pad0; }` (32 B, `MGP_ASSERT_POD`); three tails in order: `Int32 BlockBindings[BlockBindingCount]` (dense, GL uniform-block index order), `MGPProgramSamplerUnit {Uint32 Location; Int32 Unit;}[SamplerUnitCount]` (8 B, sparse, ascending Location), `MGPProgramStorageOverride {MGHostSpan Name; Int32 Binding; Uint32 Pad0;}[StorageOverrideCount]` (40 B; the storage-block override map is name-keyed BY DESIGN, `ProgramObject.h:1030-1034`; the name travels as a host span staged whole). `Signature` = the client's commutative hash of the override map, the same function as `Managers.cpp:10832-10841`, so the server's rebuild clause is one `Uint64` compare. Bounds: `kMGPipeMaxProgramBlockBindings` 64, `kMGPipeMaxProgramSamplerUnits` 256, `kMGPipeMaxProgramStorageOverrides` 64, else `Fatal{ProtocolCorruption, "SetProgramBindings.<field>"}` — a larger sampler array is a counted refusal, not a truncation. **THE RECORD ALWAYS DECLARES THREE TAILS**, empty ones included: the tails are positional and the third one's offset is derived from the first two, so a record that declared "one tail" when only the block bindings were present would put the same bytes at a different offset than one that declared three, and the two halves of the wire would disagree about where the overrides start. The wire codec's `WireRecordLayout` grew from two tails to three for it (`MG_Remote/Wire/PipeWireCodec.h`), and the override names are walked through `CheckHostSpanIsHonest` in the decode arm rather than by the encoder's blanket second-tail pass — they are MEMBERS of a 40-byte element, and reading that tail as bare spans would see one span and a quarter of garbage. Emitted BEFORE `create_shader_state` at the same validate point (a rebuild inside the verb must already see the bindings). Dirty rule: bit `NewShaderBindings` (`Tracker.h:364-371, 419-448`) already mixes the three counters; the emitter latches on `(Cso, backendStateVersion, blockBindingVersion)`. A re-issued create clears the record's tails (`GlobalConstants`' rule, `PipeApply.cpp:2903-2909`). | all three counts 0 = "defaults" | `MGPipeApplySetProgramBindings`, the program twin's sync and memo keys |
|
||||
| **`MGPProgramDesc::LinkStatus`** (a `Uint8`; **the descriptor grows 192 → 200**, `PipeCatalogueTest` pins the new size) | **THE DRAFT SAID "in the descriptor's existing pad, size unchanged" AND THAT WAS WRONG.** `MGPProgramDesc`'s four trailing `Uint8`s end at offset 24 and `MGPBlobRef` is 8-aligned, so the fixed head is exactly full and a fifth byte costs the alignment slack behind it: 20 bytes of handle and counters + 5 bytes of flags, padded to 32, plus 168 of blob refs. It is paid once, on a record that fires at most once per link, and it is confined to the push build — this payload is only ever instantiated under `MOBILEGL_PIPE_PUSH` (`PipeApply.cpp`, `PipeRoute.cpp` and the emitters are all push-only sources), which is why G1 does not see it. 1 = the link the archive describes succeeded. `create_shader_state` is re-issued on the same handle at every link that changes `GetLinkVersion` (today's rule) and the record's `Serial` moves with it; a failed link re-issues with `LinkStatus = 0` and empty blobs IFF the frontend reports the program unlinked afterwards (§9 ruling 9 pins which). A draw on a `LinkStatus == 0` program DECLINES on the server, as the frontend raises `INVALID_OPERATION`. | not linked | the program twin (`SyncCurrentProgram`) |
|
||||
| **`MGPProgramDesc::Spirv[6]` / `Reflection` blobs are NON-EMPTY** on a live wire | The client encodes the link artefacts once per link with `ProgramArtifactsCodec` into `SEG_STAGE`; the applier decodes into RECORD-OWNED `LinkArtifacts`/`SpirvArtifacts` and the companion pointers (`ProgramEmit.h:230-260`) are deleted under a transport. `Size 0` with no companion is the existing trip wire `Fatal{ProtocolCorruption}` (`PipeApply.cpp:2858-2871`). A `SharedPtr` pin of the client's artefacts was considered and REFUSED: rule B, and `Link()` replaces them in place (`ProgramObject.cpp:348-356`). | — | the program twin's rebuild |
|
||||
| **`MGPImageView::Access`** | 0 = `GL_READ_ONLY`, 1 = `GL_WRITE_ONLY`, 2 = `GL_READ_WRITE` — the encoding `MGPipeEncodeImageAccess` already writes (`MG_Impl/Pipe/ImageEmit.h`). The NUMBERS move to `MG_Pipe/MGPipeValueTypes.h` as `enum class MGPipeImageAccess : Uint8` with `MGPipeDecodeImageAccess` / `MGPipeImageAccessIsValid` / `MGPipeImageAccessReads` / `MGPipeImageAccessWrites` beside them, and the client encode NAMES those enumerators instead of literal 0/1/2 — one table, both roles, moved rather than duplicated. The encode also stops being a private static so the unit case can reach both halves. The server comment at `DirectGLES.cpp:2662-2669` is deleted as stale (fb). Any other value is `Fatal{ProtocolCorruption, "ImageView.Access"}` AT THE READER: the value header may not log, so it answers the question and the caller owns the refusal. | read-only | `SyncImageTextureBinding` |
|
||||
| **`kMGPipeSubsystemBufferBindings = 1ull << 13`** (`MG_Pipe/MGPipe.h`, bits 14..62 now reserved) | Owns dirty bits 15/16/17 (`NewConstBuffers/NewShaderBuffers/NewSoTargets`, `Tracker.h:97-99`) and the `SetShaderBuffers` emitter. Caps-required under a transport like bits 7–12; the default push mask becomes `kMGPipeSubsystemsMigratedAtP5e` = `0x3fff`, and `0x1fff` survives as P5e's "everything P4a had and nothing of mine" control. Bit 13 requires bit 7 (a range names a Buffer handle). `NewSoTargets` rides the bit even though `set_stream_output_targets` stays unemitted for the whole of P5e: the bit answers "which A/B switch owns this family's legacy arm", and an operator clearing it has to get the whole binding-point family's frontend walk back rather than two thirds of it. `TrackerTest.cpp` is rewritten, not deleted, and now also pins that the phase constant is the WHOLE dirty-bit set — after P5e every bit names a subsystem. | bit clear = the frontend walk (monolith / A/B only) | `MGPipeSubsystemForDirty`, `SubsystemForEmitter` |
|
||||
| **`SetHashSuppressor` slots** (`MG_Impl/Pipe/SetHashSuppressor.h`) | `SetShaderBuffers` becomes THREE slots (`SetShaderBuffersUniform` / `SetShaderBuffersShaderStorage` / `SetShaderBuffersAtomicCounter`) — one per Class, so an emission of one class never cancels another's; `SetProgramBindings` gets one. `SetStreamOutputTargets` stays "P4b" (unemitted). | — | the emitters |
|
||||
| **`IpcTable::RunAhead`** = `MOBILEGL_IPC_RUN_AHEAD` (default 1, range 0..1); **`IpcTable::PresentCredit`** = `MOBILEGL_IPC_PRESENT_CREDIT` (default **1**, range 1..8) | Parsed unconditionally like every IPC knob (`ConfigLoader.cpp:356-403`), and both are in the IPC log line, because a knob that silently means nothing on one arm of an A/B is how an A/B stops being one. `PipeVerify` forces `RunAhead = 0` beside `BatchWaits = 0`. `RunAhead=1` on a server without the caps bit logs once "run-ahead requested, server does not publish kCapRunAheadApply" and runs lockstep. The stats line gains `credit-waits`. | `RunAhead=0` is the A/B control: identical server code, only the client's wait differs | `RunAheadArmed`, `EmitPresent` |
|
||||
| **`BufferState` bind-point generations** (client-only, `#if MOBILEGL_PIPE_PUSH`) | One `Uint64` per `BufferBindPointTargets` entry beside `NoteBufferChanged` (`BufferState.h:69-75`), bumped by `glBindBufferBase/Range` (`GL_Buffer.cpp:1501-1520, 1624-1650`) and `SetNamedTransformFeedbackBinding`. Not a wire field; listed because it is the emit rule's input and the pull build's object must not resize (the licence `TextureState::NoteImageUnitTouched` took at P5d r3). | — | bits 15/16/17's shutters |
|
||||
|
||||
---
|
||||
|
||||
## §2 ra — the wait rule
|
||||
|
||||
### 2.1 The barriered predicate (one function, two callers)
|
||||
|
||||
```
|
||||
MGPipeBarriered(op, payload, st) =
|
||||
MGPipeWaitClassFor(op) != kWaitNone // static column, §2.2
|
||||
|| (MGPipeCallClassFor(op) == kCtxVerb && st.IsTransformFeedbackActive) // (i) XFB stays lockstep
|
||||
|| (op == DrawVbo && (payload.Flags & kDrawClientArrays)) // (ii) client arrays
|
||||
```
|
||||
`st.IsTransformFeedbackActive` is `MGPContextValues`' value (`MGPipeTypes.h:1698`): on the client
|
||||
it is `ctx.IsTransformFeedbackActive()` at emit, on the server the applied value — `set_context_values`
|
||||
precedes the verb on the ring, so the two agree. **c0e LANDED THE FUNCTION, all three clauses, in
|
||||
`MG_Pipe/PipeApply.cpp`**, and gave `MGPipeApplierState` its own `IsTransformFeedbackActive` mirror
|
||||
written by `MGPipeApplySetContextValues`: the predicate has to be computable on the server from
|
||||
APPLIER state, and `gPipeInputs` is the block a run-ahead client stops filling, so reading the
|
||||
answer out of there would be reading the very memory rule F exists to stop it reading. `payload`
|
||||
may be null, which makes clause 3 fall through — a caller holding an opcode but not yet the bytes
|
||||
must not be told a draw is unbarriered on that account. Escalation (ii) is a refusal under run-ahead
|
||||
(§5.1), so on a run-ahead server it never reaches the sink; it is listed so the predicate is total.
|
||||
**No other runtime escalation exists**; adding one is an integrator ruling and a row here.
|
||||
|
||||
**There was an escalation (iii) and it was WITHDRAWN — ID-133 added it, ID-136 took it back out,
|
||||
and the reason is the most reusable thing P5e learned about escalations.** ID-133 barriered a
|
||||
plain multi-draw (`NumDraws > 1 && !kDrawIsIndirect`) so that `MultiDrawImpl::RunIndirect`'s read
|
||||
of the client's `GL_DRAW_INDIRECT_BUFFER` binding became a legal barriered pull instead of
|
||||
`Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"}` on 18 lane entries. It worked, and
|
||||
it was the wrong instrument:
|
||||
|
||||
- There is exactly **one draw opcode** — all twenty draw entry points collapse onto `draw_vbo` —
|
||||
and the multi-draw tier is resolved on the SERVER, per batch (`MultiDraw.cpp`'s
|
||||
`ResolveTierForBatch`), from driver caps the client does not hold. So "escalate the indirect
|
||||
multi-draw op" has no op to name and no predicate both roles can compute, and the narrowest
|
||||
available key charged **every plain `glMultiDraw*` on every tier**, including the `auto` → `ext`
|
||||
default the phone ships — a per-batch rendezvous on the shipping arm, added to satisfy a lane.
|
||||
- And the read was never a data dependency: `BoundDrawIndirectBufferId` SAVES AND RESTORES a GL
|
||||
binding name around the tier's own scratch command buffer. Giving it the handle arm its
|
||||
neighbour `ResolveBoundIndexBuffer` already had retires the pull outright.
|
||||
|
||||
**The rule this leaves for the next candidate escalation: ask WHICH ARM PAYS FOR IT, not which
|
||||
lane it turns green.** A wait added to make a lane green is a real cost on a real path; if the
|
||||
predicate cannot name the arm that needs it, the escalation is charging arms that do not.
|
||||
|
||||
The two halves — retiring the pull and withdrawing the clause — landed in one commit (ID-136
|
||||
overrode ID-113's package boundary for it): the pull retired without the clause withdrawn is a
|
||||
cost with no reason, and the clause withdrawn without the pull retired puts the 18 entries back
|
||||
on the unbarriered arm.
|
||||
|
||||
### 2.2 The static column, row by row
|
||||
|
||||
| class | rows | why |
|
||||
|---|---|---|
|
||||
| `kWaitReply` | every `kReplySlot` row (`PipeCalls.def:115-202, 206, 221`): GetCaps, ResourceCreate/Respecify, MapPersistent, FenceStatus/Wait, QueryAvailable/Result/Timestamp, SetTextureParams, ResourceSubData (texture half, §2.5), ResourceReadback, GetTextureImage, ReadPixels | the answer is not derivable (R-5) |
|
||||
| `kWaitPresent` | `Present` | the credit, §2.4 |
|
||||
| `kWaitApplied` | `ApplierReset`; `GenerateMipmap` (`kCtxObject`, waits today for the reason at `ClientSession.cpp:900-914` — until tx2 lands its `VerbMipRes` arm, then `kWaitNone`); `SetStorageBlockBinding` (`kCtxVerb`, resolves through `GetProgramObject(GlName)`, `PipeApplier.cpp:805-808`; trailing: pg resolves through `MGPStorageBlockBinding::ShaderCso` and flips it); `BeginStreamOutput`, `EndStreamOutput`, `PauseStreamOutput`, `ResumeStreamOutput`, `BindStreamOutput` (§5.7); `CopyFramebufferToTexture` (the CopyTex endpoint still syncs the frontend texture, `DirectGLES.cpp:9714-9735`; trailing, tx2) | each still pulls a row or probes; the wait keeps P5C's semantics for it |
|
||||
| `kWaitNone` | `DrawVbo`, `LaunchGrid`, `Clear`, `Blit`, `MemoryBarrier`, `Flush`, `SetSwapInterval`, `BindShaderImage`, `PatchParameter`; every `kCtxState` / `kCtxCso` / `kCtxObject` row without a reply (incl. `ObjectDeath`, `ResourceSubData` buffer half); `ResourceDestroy`, `UnmapPersistent`, `FenceCreate/Destroy`, `FenceWaitServer`; `QueryCreate/Begin/End/Destroy/Counter` | rule F holds for their sinks after §4/§5 |
|
||||
|
||||
The column is inert until the integration commit publishes the caps bit (§6.2), after vi/sb/pg/tx2/fb.
|
||||
|
||||
### 2.3 `EmitAndWaitTails` after `PublishAndNotify` (`ClientSession.cpp:877`)
|
||||
|
||||
```
|
||||
if (ownsReplySlot) -> WaitForAppliedBudget + DrainEventRing + ReadReply (unchanged)
|
||||
else if (RunAheadArmed())
|
||||
switch (class) kWaitNone: return seq;
|
||||
kWaitApplied: WaitForAppliedBudget + DrainEventRing + ReleaseFillPins(); return seq;
|
||||
kWaitPresent: return seq; // the credit wait ran BEFORE encode, §2.4
|
||||
else -> today's :915-959 shape, byte for byte (lockstep, Magma, verify)
|
||||
```
|
||||
`ReleaseFillPins()` clears the four O-class `SharedPtr` rows of `gPipeInputs` (`PipeInputs.h:801-809`)
|
||||
on the GL thread once the barriered apply returned — under run-ahead the next fill may be a frame
|
||||
away and a pinned VAO/program must not make the apply thread its last owner. `VerbBarrier==0`
|
||||
keeps its lockstep meaning (nothing but reply rows waits); under run-ahead it removes the
|
||||
`kWaitApplied` waits too and is the run-ahead NEGATIVE CONTROL: expected red is
|
||||
`Fatal{UnmigratedPipeInput, ...}` at the first barriered row's stale pull.
|
||||
|
||||
### 2.4 Present pacing
|
||||
|
||||
`EmitPresent` (`EmitTables.cpp:977-1053`): `BeforeReadOnlyVerb(); if (m_presentsSent >= PresentCredit)
|
||||
WaitForPresentAck(m_presentsSent + 1 - PresentCredit, kBarrierTimeoutMs)` — `ShutDown` returns, `TimedOut` is
|
||||
`Fatal{PresentCreditTimeout}`; then `DrainEventRing`, encode, publish, `PumpControlPlane`. Credit 1
|
||||
= the client publishes frame N+1 while the server applies and swaps frame N: one frame of overlap,
|
||||
at most one frame of added latency. Budget at ~850 draws + ~0.36 MB pmap/frame: SEG_CMD ~70 KB +
|
||||
set/CSO records per frame against 8 MiB, SEG_STAGE ~1 MB/frame against 32 MiB, SEG_REPLY unchanged.
|
||||
The credit, never the bytes, is the steady-state backpressure; `WaitForCmdSpace` and the stage bell
|
||||
(`PipeWireCodec.cpp:836`) are the safety net and both drain events on exit (§2.6).
|
||||
|
||||
### 2.5 Drains and forced waits
|
||||
|
||||
Drain points: every wait exit in `ClientSession` (reply, applied, present-ack, cmd-space, stage
|
||||
bell), the four EGL RPC returns (`BackendObject_Remote.cpp:200-245`), `Stop`. Forced waits:
|
||||
`MapBuffer(READ)` / `GetBufferSubData` / a `CopyBufferSubData` source → `SyncGpuWrites` (reply
|
||||
row, unchanged); `glFinish` → `WaitForApplied(LastPublishedSeq)` + drain (a new client `Finish`
|
||||
slot; `Flush`/`Finish` stay no-ops on the wire, ARCHITECTURE:415); every `Server*` EGL forwarder
|
||||
waits `WaitForApplied(LastPublishedSeq)` BEFORE the RPC (the control channel is pumped between
|
||||
drain batches, `ServerLoop.cpp:400-404`, so a make-current would otherwise land between two run-ahead
|
||||
records); `ServerSwapEGLBuffers` is not on that list — present is the swap. **`glGetError`
|
||||
relaxation:** a run-ahead verb's `kEventGlError` is observed at the next drain point, i.e. at most
|
||||
one present credit later than the call after it; P5C §4.2 already assigns ordering to P9 and P5e
|
||||
names the window. **`resource_subdata`:** the BUFFER half passes `wantReply = false` through
|
||||
`MGPipeRouteResourceSubData` (the Bool is discarded today, `PipeFill.cpp:839`; the server still
|
||||
posts, the slot is never read — legal, `ReplySlot.h:16, 105`), which is what makes the persistent-map
|
||||
push (`PersistentMapTracker.cpp:735, 850`) fire-and-forget.
|
||||
**THE TEXTURE HALF'S WAIT IS PAID OFF (P12 — the "trailing" this paragraph used to carry), and the
|
||||
argument is that its answer is DERIVABLE on the wire arm, not that nobody reads it.**
|
||||
`DrainTextureSubData` still clears the level's dirty flag on an accepted answer (D-D5); what it no
|
||||
longer buys is a round trip for it, because (a) the emission is already gated on
|
||||
`CapsMirrorInstance().ServerConsumes(kMGPipeSubsystemTextureResources)` (`PipeFill.cpp:1719-1740`) —
|
||||
the same published fact the applier's own acceptance belt reads (`PipeApply.cpp:1408-1419`), so the
|
||||
one refusal this acceptance exists to carry ("no consumer") cannot be the answer to a record this
|
||||
client emitted; (b) what remains is a loud bug class (`MGP_TRIP_WIRE_REPORT` for a malformed texture
|
||||
record, `ResolveResourceIn`'s counted miss for a handle the applier does not hold); and (c) a record
|
||||
the CLIENT cancelled still answers "not accepted", so the level stays dirty and is retried. **The
|
||||
wait moves to `SEG_STAGE`**: a run that does not fit reclaims retired bytes and parks on the stage
|
||||
doorbell, which is proportional to the window rather than to the call count. `MGPipeSubDataWantsItsReply`
|
||||
(`PipeRoute.h`) stays the ONE place that decides, now with the arm test inside
|
||||
`MOBILEGL_BUILD_DISAGGREGATED` and the original expression as its fall-through; monolith and every
|
||||
non-wire arm keep the old answer, which is what G1 measures. Gate:
|
||||
`RemoteClientControls.TextureUploadsStopOwningAReplyOnTheWireArm` reads back the same `wait_replies`
|
||||
the P65LinkMetrics line prints (`LinkMetricsReplyWaits`), asserting a pure upload moves it by ZERO
|
||||
while a `resource_create` row still moves it; red-once with the arm test removed is exit 142.
|
||||
**`ResourceCreate`, `ResourceRespecify` and `SetTextureParams` are `kWaitReply` ROWS, and what the
|
||||
caller does with that is now per-object rather than per-row - THIS PARAGRAPH IS THE ONE P12
|
||||
SUPERSEDED, and it is rewritten rather than deleted because its reasoning is what the replacement
|
||||
had to answer.** Each one's answer carries a fact about the SERVER's record table that the client's
|
||||
own tracker cannot know by itself (`RespecifyOnce`, `TextureEmit.h`, "the applier's REFUSAL is the
|
||||
only signal that says 'I hold nothing for this handle'" - the scope
|
||||
`MGPipeApplierReleaseObjectRecords` leaves behind for a served context whose frontend objects live
|
||||
on). What P12 added is the observation that the client holds a REASONABLE COPY of that fact:
|
||||
`MGPipeHandleIsPublished`, set only when a create was ACCEPTED, is exactly "the applier holds this
|
||||
handle", and it is what `MGPipeResourceRespecifyWantsItsReply` (`PipeRoute.h`) and
|
||||
`Wire_SetTextureParams` read. So:
|
||||
|
||||
- **`SetTextureParams` and the texture half of `ResourceRespecify` wait only until the object is
|
||||
CONFIRMED.** For a published object the record is fire-and-forget and the caller reads a
|
||||
provisional accept; the object's one real confirmation is its create, paid at its FIRST USE
|
||||
(`EmitTextureParams` -> `HealUnpublishedRecord`, `EmitResourceRespecify`'s unpublished arm). When
|
||||
the latch is clear both rows still wait, and the refusal-driven `RespecifyOnce` heal is unchanged
|
||||
- which is what keeps the paragraph above true rather than merely overruled.
|
||||
- **The buffer half of `ResourceRespecify` never waits**: both call sites discard its Bool
|
||||
(`PipeFill.cpp:1105/:1142`).
|
||||
- **`ResourceCreate` is the one row whose answer may not be dropped**, because `PublishCreate`
|
||||
latches on it and a false latch makes every row above fire-and-forget against a record the server
|
||||
does not hold. It was given a WINDOW instead of a rule - `MOBILEGL_IPC_CREATE_WINDOW`, answers
|
||||
deferred and collected in order, a late DECLINE un-latching the object - and **the window is OFF
|
||||
BY DEFAULT because the deferral was measured and cannot be collected**: the reply pool is
|
||||
`kDefaultReplySlotCount` = 8 slots deep and one load frame posts 55,903 replies (the server
|
||||
answers every reply-owning record whether or not the client wants the answer), so an answer is
|
||||
overwritten after 8 replies - 0.65 of a create. On the device all 4,536 creates took the blocking
|
||||
path at window 4 and at window 1 alike (28.64 s both ways, against the handoff's 31.3 s
|
||||
pre-window baseline on the same device). `WireTables.cpp`'s section comment carries the counters
|
||||
and the arithmetic. The client-visible consequence is worth stating here: with the window ON, the
|
||||
un-latch that the paragraph above relies on lives in the same unreadable drain, so a refused
|
||||
create would stay latched - a protocol change (a deeper pool, or a "do not answer me" bit) is
|
||||
what a retry would need.
|
||||
|
||||
Measured split for the two shapes a stitcher produces (P12,
|
||||
`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`): an atlas stitch - 64 sub-uploads into
|
||||
one level - goes from 64 reply waits to **0**, while a texture per sprite - 64 textures, 256
|
||||
`glTexParameteri` - goes from 384 to 320 at the contract commit; the P12 first-use change takes the
|
||||
rest of the params/respecify rows out of that frame's wait list entirely (one frame: 43,232 waits ->
|
||||
4,543, 221.5 s -> 31.3 s, `MOBILEGL-CS-HANDOFF-2.md` §1).
|
||||
per call, analyzed separately rather than swept in here.
|
||||
|
||||
### 2.6 Event-ring flow control (ARCHITECTURE §11.7 made real)
|
||||
|
||||
On a run-ahead server `Reserve == nullptr` is NOT `Fatal{EventRingOverflow}`: the producer latches
|
||||
`eventRingFull` (exists, `EventRing.h:135-143`), publishes, rings, and `ApplyThreadMain` parks at
|
||||
the record boundary with `eventRingFull == 0` added to `ready` (`ServerLoop.cpp:391-398`). The
|
||||
client re-checks `m_events.RingIsFull()` at every park exit and drains before re-parking. Deadlock
|
||||
argument: the server blocks only on the event ring; the client blocks only on watermarks the
|
||||
server advances; every client wait drains; so at most one side is parked at any instant. The
|
||||
lockstep arm keeps the Fatal (P5C §4.4).
|
||||
|
||||
### 2.7 Object lifetime under run-ahead
|
||||
|
||||
The client frees a slot only after its death record went out (`PipeFill.cpp:1623-1626`), the ring
|
||||
is in order, every record naming `{s,g}` precedes `destroy{s,g}` which precedes `create{s,g+1}`,
|
||||
and every server twin table Gen-checks (§4.3). **No wait is added at any delete.** Persistent maps:
|
||||
`Forget()` (`PersistentMapTracker.cpp:599-606`) drops tracker state only; pushed bytes were copied
|
||||
into SEG_STAGE at `StageOptional` and reclaim waits on `retiredSeq`, so a map released with blocks
|
||||
in flight is safe. The P5d deferred-destroy queue (`PipeMutation.h:216-236`) STAYS LIVE: a
|
||||
barriered record's apply may still pin a frontend object (its fill's `SharedPtr` rows, XFB
|
||||
`targets`), so the apply thread can still be a last owner. Under `RunAheadArmed` an enqueue from an
|
||||
UNBARRIERED apply is a finding: `MGLOG_E_ONCE` with the kind, Fatal under strict.
|
||||
|
||||
---
|
||||
|
||||
## §3 `gPipeInputs` — server-role memory for unbarriered records
|
||||
|
||||
1. **The client fills only for barriered records.** In `MGPipeValidateForVerb`
|
||||
(`PipeFill.cpp:2962-3306`), when `RunAheadArmed()` and `!MGPipeBarriered(...)`: step 2 (tracker
|
||||
walk) and step 3 (emitters) run unchanged; `:2989` (`++CurrentVerbSerial`), `:2999-3001`
|
||||
(`SetVerb/SetIdentity`), step 4 (`:3252-3270`, `CopyField` + `FilledGen`) and `MGPipeLeaveVerb`'s
|
||||
writes are SKIPPED; `MGPipeNoteFrontendMutation` returns at `:554`. The residual-value trip
|
||||
wire (`:3284-3294`) still emits — it compares on the server against the applier mirror.
|
||||
`RunAhead=0` on a caps-publishing server uses the SAME fill decision (the A/B changes one
|
||||
variable: the wait). `PipeVerify` forces lockstep, a fill for every verb, and every record
|
||||
barriered (both roles read the in-process flag; the comparator `:3304` is inproc-only).
|
||||
2. **The server's stamp is server-private.** `MGPipeServerStampVerbBoundary` writes
|
||||
`m_filled/m_currentVerb/m_serverStampedVerb` into applier-owned storage, never into the shared
|
||||
block; `MGPipeServerClearVerbBoundary()` from the GL thread (`:2997`) is DELETED under a
|
||||
transport. For a barriered record the server reads the client's fill (the client is parked) and
|
||||
its own stamp; for an unbarriered one it does not touch the block. `SetIdentity` (`:3001`) moves
|
||||
to `ApplyOne`, once per session. This removes the E note's hazard (b.4).
|
||||
|
||||
**LANDED by P5f f1 — as the dual block, not as moved fields** (`MOBILEGL_IPC_ROLE_SPLIT_STATE=1`,
|
||||
`PipeInputs.h`'s `gPipeInputsClientBlock`). The client could not be stopped from writing the
|
||||
server's stamp storage while both roles shared one object, so the split landed one level down:
|
||||
the fill side writes the CLIENT block (`MGPipeClientInputs()`, `PipeFill.cpp`), the stamp and
|
||||
every backend read keep the SERVER block (`gPipeInputs`), and "applier-owned storage" is the
|
||||
server block itself. The GL-thread `MGPipeServerClearVerbBoundary()` calls are re-pointed, not
|
||||
deleted: they now clear the client block's flag (`MGPipeClientClearVerbBoundary()`), which is
|
||||
never raised, so withdrawing the server's stamp is the applier's job alone
|
||||
(`PipeApplier::LeaveApplier`). `SetIdentity` landed as `MGPipeServerBlockNoteIdentity()`, called
|
||||
from `PipeApplier::Attach` and refreshed by each server stamp from the applier's own
|
||||
served-context serial. And the ra2 caveat below is discharged by the knob's other half: under
|
||||
the dual block a BARRIER_PULLED read has no value to be stale WITH, so `CountBarrierPull` is an
|
||||
unconditional named `Fatal{UnmigratedPipeInput}` there — never a silent stale read. The
|
||||
knob-off arm keeps the shared block and the old semantics, byte for byte; it is the A/B arm and
|
||||
the negative control, not a second contract.
|
||||
3. **The detector.** `MGPipeInputUnfreshRead` (`PipeInputs.cpp:191-224`) under an unbarriered
|
||||
record takes `StrictBarrierPullFatal` (`:60-66`) regardless of `StrictErrors`; the seven sticky
|
||||
forwards (`MGPipeStickyForwardPull`, `:251-257`) the same; `CountBarrierPull` stays for barriered
|
||||
records. This is what turns `integration-split-strict` into a hard green lane (§7).
|
||||
4. `VertexAttribDefaultsOf` (`PipeFill.cpp:84-86`) diffs against the emitter's own last header
|
||||
(`g_attribDefaultLastHeader`, `:2949`), never against the block.
|
||||
5. `RefusePipeInputsTouchWhileApplierOwnsIt` (`ClientSession.cpp:1015-1056`) is re-derived: the
|
||||
`BatchWaits` early return at `:1037` goes; under `RunAheadArmed` any GL-thread write to the block
|
||||
outside a barriered fill is `Fatal{RoleViolation, "gPipeInputs"}` by name.
|
||||
`Fatal{BarrierViolation}` (`:840`) stays for `BatchWaits==0 && !RunAhead`.
|
||||
|
||||
**AMENDED by ra2 (ID-132, and it is the sentence this section got wrong).** "Outside a
|
||||
barriered fill" was the whole test, and a barriered fill was exempted on the strength of what
|
||||
the caller SAID about itself: *"this touch is the residual fill of a record this thread is
|
||||
about to park behind"*. That is a claim about the **future**, and the write is in the
|
||||
**present** — the order at the validate point is fill, then emit, then park, so at the instant
|
||||
of the write the apply thread is still draining the unbarriered records the client ran ahead
|
||||
of. **A guard whose exemption cannot be false on the class it exists for is not a guard.** It
|
||||
never fired on any of the 70 red lane entries; what fired instead was the apply thread, on
|
||||
`Fatal{UnmigratedPipeInput, "<field>@<the CLIENT's verb>"}`, after the GL thread had bumped
|
||||
`CurrentVerbSerial`, withdrawn `m_serverStampedVerb` and renamed `m_currentVerb` underneath a
|
||||
record it was inside.
|
||||
|
||||
**The rule as landed:** under `RunAheadArmed` a GL-thread write to the block is legal only
|
||||
while `ApplyThreadIsInsideApplier()` is false, barriered fill or not; and the three fill sites
|
||||
(`MGPipeValidateForVerb` phase 1, its step-4 residual walk, `MGPipeNoteFrontendMutation`)
|
||||
establish that by taking §2.5's forced wait first (`QuiesceApplierBeforeFill`,
|
||||
`PipeFill.cpp`). **Two waits per filling verb, not one**, because the block is written in two
|
||||
phases that straddle publication — the serial bump / stamp withdrawal / verb rename before the
|
||||
emitters, the 63-field walk after them — and the records those emitters published are records
|
||||
whose apply reads the block. The red-once is deleting a wait; the guard then aborts by name on
|
||||
the first filling verb behind a run-ahead backlog. Pinned by
|
||||
`RemoteGuards.ClientBarrieredFillWhileTheApplierIsInsideUnderRunAheadIsFatalByName` with its
|
||||
green control beside it; both run in the `unit` lane, because the strict lane runs under
|
||||
lockstep and cannot see this class by construction.
|
||||
|
||||
The same correction applies to **§3.2 above, which is UNLANDED and was never marked as such**:
|
||||
"the server's stamp is server-private … never into the shared block" describes storage the
|
||||
applier owns, and `MGPipeServerStampVerbBoundary` still writes `m_filled` / `m_currentVerb` /
|
||||
`m_serverStampedVerb` into `gPipeInputs` itself. That gap is exactly what made the race
|
||||
possible, and splitting the stamp is the P11 item — but note that per-role stamps **without**
|
||||
versioning the BARRIER_PULLED values would be worse than today, because it turns a loud
|
||||
`Fatal{UnmigratedPipeInput}` into a silent stale read. Both, or the values retired first.
|
||||
*(P5f f1: LANDED — see the LANDED note on §3.2 above. The dual block makes the server block
|
||||
itself applier-owned, and the unconditional dual-block Fatal in `CountBarrierPull` is the
|
||||
"both": no BARRIER_PULLED value survives to be read stale.)*
|
||||
|
||||
---
|
||||
|
||||
## §4 id — handle-keyed twins
|
||||
|
||||
### 4.1 The rule
|
||||
With a live wire, every twin on the apply thread is resolved BY THE HANDLE the applier state or
|
||||
the record carries, never by a frontend object: `GetOrCreate(MGPipeHandle)` (`SlotTables.h:294-342`),
|
||||
`FindByHandle` (`:420-426`), `ReleaseByHandle` (`:394-408`). `HandleOf(stateObj)`,
|
||||
`Find(StateObject*)`, `GetOrCreate(const StatePtr&)`, `NoteStateForHandle/StateForHandle` and
|
||||
`Entry::stateRef` survive as the MONOLITH-GLUE half (§5.8) and are unreachable from an
|
||||
unbarriered apply. The twin sync bodies take `(MGPipeHandle, const <Record>&)` and store
|
||||
`m_handle` (the `m_pushedSyncHandle` pattern, `Managers.h:1857`); the object parameter is deleted
|
||||
from the transport overload, not defaulted to null. The E note's "content-addressed VAO CSO" is
|
||||
corrected: every kind mints its slot off the object's own lifetime id (`VertexInputEmit.h:170-172`,
|
||||
`TextureEmit.h:422`, `FramebufferEmit.h:257`, `ProgramEmit.h:411-422`), so the rekey is a
|
||||
replacement of the lookup ARGUMENT, not of the table.
|
||||
|
||||
### 4.2 Per kind
|
||||
| kind / table | key on the server | the record that names the handle before any verb reads it | twin created | twin dies |
|
||||
|---|---|---|---|---|
|
||||
| Buffer `g_backendBufferResources` (already handle-keyed) | `Res` from `VertexBuffers[]`, `IndexBuffer`, `BoundShaderBuffers[c][i]`, `Desc.BufferForTexBuffer`, `VerbIndirect*` | `resource_create/respecify` | `EnsureBufferResourceForHandle(nullptr, h)` | `resource_destroy` |
|
||||
| VAO `g_backendVertexArrayObjects` | `st.BoundVertexElements` (`PipeApply.h:594`; one CSO per VAO, re-created on the same handle when the config moves) | `create/bind_vertex_elements` | first `ResolveVaoTwin(handle)` at a draw | `object_death{VertexElementsCso}` / `delete_vertex_elements` |
|
||||
| Texture `g_backendTextureObjects` | `BoundSamplerViews[u].Texture`, `BoundShaderImages[u].Res`, `FramebufferRecords[h].State.{Color[i],Depth,Stencil}.Res`, `VerbMipRes`, `VerbCopyTexDst`, `MGPCopyImage` src/dst | `resource_create/respecify` (Texture) | first `SyncTextureToBackendByHandle(h)` | `object_death{Texture}` / `resource_destroy` |
|
||||
| Renderbuffer `g_backendRenderbufferObjects` | `MGPSurface.Res` with `Kind == Renderbuffer` | `resource_create/respecify` (Renderbuffer) | first attach sync | `object_death{Renderbuffer}` |
|
||||
| Framebuffer `g_backendFramebufferObjects` | `st.BoundFramebuffer[t]`, `record.Fbo`, `VerbBlitReadFbo/DrawFbo`; `kMGPipeDefaultFramebuffer` is never a twin | `set_framebuffer_state` | `GetOrCreateByHandle(record.Fbo)` (already, `DirectGLES.cpp:3197`) | `object_death{Framebuffer}` |
|
||||
| Program `g_backendProgramObjects` | `st.DrawProgram` / `DispatchProgram` / `BoundShaderCso`; composites at the band | `create_shader_state` (+ `set_program_bindings`) | first `SyncCurrentProgram(handle)` | `object_death{ShaderCso}` / `delete_shader_state` |
|
||||
| Sampler `g_backendSamplerObjects` | the CSO family ONLY: `st.BoundSamplerStates[u]` or `TextureResources[h].Params.BuiltinSampler` | `create_sampler_state` | `ResolveSamplerCsoTwin` (already, `Managers.cpp:13457-13486`) | `object_death{SamplerCso}` |
|
||||
| SamplerView `g_backendSamplerViews` | `BoundSamplerViews[u].View` (no draw-path resolver today; unchanged) | `create_sampler_view` | — | `object_death{SamplerViewCso}` |
|
||||
|
||||
The identity family of samplers (`ResolveUnitSamplerBackend` `DirectGLES.cpp:4985-5026`, the mint
|
||||
at `:5623-5630`, `Managers.cpp:13264-13280`) is deleted under a transport; a unit whose
|
||||
`BoundSamplerStates[u]` is null while the frontend held a sampler is a missing record and §5.3's
|
||||
window rule makes it unrepresentable. The creation moment is LAZY everywhere (no twin at
|
||||
`create_*`); the applier's Gen check refuses a bind or verb naming an unknown or stale handle
|
||||
(`PipeApply.cpp:2266, 2549, 2625, 2683, 2896`), so a twin is never minted for an object the ring has
|
||||
not introduced. `applier_reset` touches neither twins nor object records (`PipeApply.h:800-832`).
|
||||
|
||||
### 4.3 ABA
|
||||
`Gen` moves only on reuse (`SlotAllocator.cpp:176-189`, and `:240-258` for the composite band —
|
||||
"the one place Gen may move", in the file's own words). At the server every record naming `{s,g}`
|
||||
is applied before `object_death{s,g}`, which is applied before `create{s,g+1}`. `GetOrCreate({s,g+1})`
|
||||
on a live `{s,g}` entry resets it (`SlotTables.h:338`); `FindByHandle({s,g})` after the recycle
|
||||
answers null (`:424`); a backward `{s,g-1}` is refused (`:331`). Even with a death skipped (an
|
||||
object that never crossed emits none, `WireTables.cpp:595-596`) the forward-Gen reset answers.
|
||||
Twin memos key on `{slot,gen}` plus server serials, never on a pointer. `BackendSlotTable` gains
|
||||
the composite band (`m_band`, indexed `slot - base`, as `SlotAllocator.h:144` / `PipeApply.h:511`)
|
||||
so a pipeline program (slot >= 983040) does not grow the ordinary table to `kMaxHandleSlot` entries
|
||||
— a P5e prerequisite, not a follow-up, because the rekey makes it the ordinary path.
|
||||
|
||||
### 4.4 The scopes and the guard
|
||||
`MGPipeRefuseAllocatorFromApplyThread` (`SlotAllocator.cpp:22-38`) exempts a probe iff a scope is
|
||||
active AND `MGPipeApplierCurrentRecordIsBarriered()` (server-private, set by `ApplyOne` from
|
||||
§2.1; `true` for every record on a non-run-ahead server, §6). **c0e landed the storage and the
|
||||
writer** (`MGPipeApplierSetCurrentRecordBarriered`, `MG_Pipe/PipeApply.cpp`) with `true` as the
|
||||
default — the only seam in this phase whose body is implemented rather than aborting, because
|
||||
there the safe answer and the landed lockstep answer are the same answer, and an abort would take
|
||||
down the monolith and the lockstep split alike. id owns the `ApplyOne` call site. **`MGPipeFrontendKeyedRegistryScope`
|
||||
is deleted at its draw-path sites** — `DirectGLES.cpp:1609, 1734, 2588, 3303, 4236, 4329, 4585, 4926,
|
||||
4993, 5328, 5689, 6729, 6775, 9695` and `Managers.cpp:256, 2938 (draw-path callers), 4544, 5882,
|
||||
7087, 8481, 8717, 9256, 9310, 9709, 12949, 13273, 13570` — with the frontend probes they wrap; the
|
||||
class survives only around the barriered-row sites (`DirectGLES.cpp:6166, 8837, 9161, 9216, 10029,
|
||||
11884`, the verify arms `Managers.cpp:10382, 10403`) with their P8/P9 phases re-annotated.
|
||||
`MGPipeReverseAnnouncementScope` is renamed `MagmaP7AllocatorDebtScope`, compiled for the Magma
|
||||
sites only (`VulkanRenderer.cpp:4554, 4634, 8986`, `ResourceTracker.h:723`), exempting only when
|
||||
the server backend is DirectVulkan. The renderbuffer cross-check `Managers.cpp:9709-9720` (not
|
||||
transport-gated today, unlike `:9628-9632`) is deleted by the rekey, not gated. The NoSession
|
||||
server-only fixture hop (`Managers.cpp:224-246`) is deleted; such a fixture's twins die at Stop.
|
||||
|
||||
---
|
||||
|
||||
## §5 Per family — the clean gate, the records, the windows
|
||||
|
||||
### 5.1 VAO / vertex input / index (vi)
|
||||
Twin from `st.BoundVertexElements`; memo key `{BoundVertexElements, rec->ContentSerial,
|
||||
VertexBuffersSerial, IndexBufferSerial}` (all server-owned); the buffer walk is over
|
||||
`st.VertexBuffers[Start..+Count).Res` (dedupe on `{slot,gen}`) with
|
||||
`EnsureBufferResourceForHandle(nullptr, Res)` and `IsBufferDrawCleanByHandle(h, res, nullptr)`;
|
||||
`ResolvedDrawBuffers::Entry::frontend` leaves the split arm; the IBO arm reads `st.IndexBuffer.Res`
|
||||
+ `IndexBufferSerial` (also for the restart substitution, `DirectGLES.cpp:6447-6461`);
|
||||
`SyncCurrentVertexAttributeValues` keys on `{elementsHandle, ContentSerial, activeMask}` and reads
|
||||
`rec->Attributes[i].Enabled`; the dead `GetConfigVersion` read at `:4775` is deleted. **Client-memory
|
||||
vertex arrays are NOT refused under split today** (only `CLIENT_INDICES/CLIENT_COMMANDS/
|
||||
UNBOUND_PARAMETER`, `EmitTables.cpp:651-685`; the server dereferences `attrib.Offset` as a raw
|
||||
pointer, `Managers.cpp:5382`): under `RunAheadArmed` a draw with `kDrawClientArrays` is
|
||||
`Fatal{UnmigratedVerb, "DrawArrays+CLIENT_ARRAYS"}` raised on the GL thread in `EmitDrawRecord`'s
|
||||
array arm; the lockstep arm is unchanged; the staged form (a per-attribute `{BindingIndex,
|
||||
MGHostSpan}` tail like `kDrawHasUserIndices`) is P8's. Two pins by test: `CurrentBufferMutationEpoch()`
|
||||
(`Managers.cpp:794`) is apply-thread-only under a transport; the VAO twin key is 1:1 only while the
|
||||
vertex-elements CSO stays identity-addressed. **P8 amendment (ID-P8-6):** client arrays and client
|
||||
indices became owned buffers (`beba0256`); Magma's `client-vertex-array` site is unreachable from our
|
||||
client and is now `ProtocolCorruption`.
|
||||
|
||||
### 5.2 Textures (tx2)
|
||||
Every draw-path entry is `SyncTextureToBackendByHandle(h, imageBindable)`: record first, twin by
|
||||
`GetOrCreateByHandle`, three syncs `(h, *rec)`; the by-value twin copy + second `Find`
|
||||
(`DirectGLES.cpp:1745-1755, 1781-1805`) go with the map arm. Clean = `m_isInitialized &&
|
||||
m_syncedResourceSerial == storage->Serial && storage->PendingUploads.empty() && m_syncedParamsSerial ==
|
||||
rec->ParamsSerial && !rec->Params.ForceResync && !m_forceTextureParamsResync &&
|
||||
m_syncedBuiltinSampler == rec->Params.BuiltinSampler && m_syncedBuiltinSamplerSerial ==
|
||||
SamplerCso(rec->Params.BuiltinSampler)->Serial && !rec->Params.SamplerResync && !m_forceSamplerResync
|
||||
&& rec->Desc.StorageKind == Mipmap` — no new state.
|
||||
|
||||
**`storage` is the STORAGE RECORD, which is `rec` itself for a texture that owns its texels and
|
||||
the record `rec->Desc.ViewOf` names for a `glTextureView`** (P3b/P4b wave 2-D package D3;
|
||||
`Managers.cpp PipeTextureStorageRecordForRecord`). This clause was `rec->Serial` /
|
||||
`rec->PendingUploads` through P5e and **that was wrong**, which is why it is corrected here rather
|
||||
than restated: a view owns no texels and the client keeps ONE emission cursor per storage — an
|
||||
upload through a view's own name is remapped onto its owner before it is emitted
|
||||
(`MG_Impl/Pipe/TextureEmit.h`) — so every `resource_subdata` for either name is keyed on the OWNER
|
||||
and `ApplyTextureUpload` moves the OWNER's `Serial` and `PendingUploads` alone. A view's own
|
||||
`Serial` is moved by nothing an upload does, so a gate that read it answered CLEAN for every owner
|
||||
write after the view's first sample and the view went on sampling the texels it was minted with.
|
||||
The monolith gate has this for free: `GetContentVersion()` through a view is forwarded to the owner
|
||||
(`TextureObjectView.cpp:100-102`). The three PARAMETER clauses stay `rec`'s — a view has its own
|
||||
texture parameters and its own built-in sampler, which is the whole reason the second name exists —
|
||||
as does `Desc.StorageKind`. The resolution is transitive and BOUNDED (one hop always reaches
|
||||
storage, because `glTextureView` composes a view-of-a-view onto the root at creation, but this side
|
||||
may not depend on a client invariant to terminate), and a storage record that cannot be resolved is
|
||||
**not clean**: the direction that re-syncs, not a refusal, and it raises nothing.
|
||||
`SyncTextureViewToBackendByRecord` stamps that same storage serial — or 0, which reads as never
|
||||
clean — at both its arms, so the stamp and the gate name one quantity. Deletion ordering cannot
|
||||
strand the walk, and the guarantee is the check, not the emit order: a view holds a strong
|
||||
reference to its storage owner and the client emits `resource_destroy` from the DESTRUCTOR rather
|
||||
than from `glDeleteTextures` (`TextureObject.cpp:63`), so the storage cannot go away under a live
|
||||
view whatever order the application deletes the two names in — but the two destroys leave one
|
||||
destructor chain back to back, owner FIRST when the view held the owner's last reference, so for
|
||||
one apply the view record is live with `ViewOf` naming a freed slot. No draw can land in that
|
||||
window, and the walk tests `Live`/`Gen`, so it answers null (not clean) rather than a recycled
|
||||
stranger's record.
|
||||
|
||||
The four in-body live reads (`GetTarget` at
|
||||
`Managers.cpp:8876-8877, 8620-8621`; `IsTextureView` `:7060`; the TexBuffer backing `:8223-8231`)
|
||||
read `Desc.Target / Desc.ViewOf / Desc.BufferForTexBuffer`; `GetExternalIndex` in logs becomes
|
||||
`Desc.GlNameForDiag`. The unit work list is `{Res, backend}` keyed `(ContextSerial,
|
||||
SamplerViewsSerial, SamplerViewStart, SamplerViewCount, g_backendContextGeneration)`;
|
||||
`PairingsIntact` is deleted; `ResolveAndBindUnitTextures` is one pass over the window with the
|
||||
target from `SamplerViewCsos[View].View.Target`, then an unbind of every target the window did not
|
||||
claim driven by `g_boundTexturesCache`; the alias arbiter dies (the client arbitrated,
|
||||
`SamplerEmit.h:751-757`). **Named delta (P5e-5):** the unit list syncs the program-resolved texture
|
||||
per unit, not every slot of every touched unit; coverage is unchanged (views ∪ image units ∪
|
||||
attachment lists ∪ the waited texture ops). `RequireImageBindableStorage`'s re-dirty arm
|
||||
(`Managers.cpp:5930-5962`, already aborting three frames deep) becomes a NAMED refusal at its
|
||||
entry under a transport, `Fatal{UnmigratedEmulation, "image-bindable-redirty"}`; `ImageBindableHint`
|
||||
is the prevention, P9 owns the pull. **AMENDED (P9 W2):** there is no pull. `c1bf7e15` (P5f) made the
|
||||
by-handle arm a server-owned promotion (`RequireImageBindableStorageByHandle`: GPU level read back,
|
||||
staged store for pending boxes, immutable core formats kept) and the frontend re-dirty arm is
|
||||
monolith-only; P9 W2 closed its two holes (a level the driver does not hold is not read; a refused
|
||||
readback falls back to a covered staged level) and deleted `OnTexturePullRequest` and the
|
||||
`texture-remint-pull` marker (`docs/Disaggregated/notes/p9/W2-REMINT.md`). `GenerateMipmap(target)` under a transport resolves the
|
||||
texture from `VerbMipRes` (as `EnsureGenerateMipmapStorageAllocated` already does, `DirectGLES.cpp:8736`),
|
||||
never the active unit (`:9563-9566`), and its row flips to `kWaitNone`. `m_force*Resync` are
|
||||
server-set and never cleared by the wire; the wire bits are never cleared by the server (D-E2, now
|
||||
load-bearing for a gate). The pinned guard list (`MipmapStorage.cpp:39-51`, `RemoteClientTest.cpp:1938-1978`)
|
||||
gains the six shape accessors it exempts today plus `GetSamplerObject/GetTextureParamsVersion/GetContentVersion`.
|
||||
|
||||
### 5.3 Samplers and the two unit windows (tx2; the rule binds the emitters)
|
||||
The effective sampler of unit u is `st.BoundSamplerStates[u]` if non-null else
|
||||
`TextureResources[view.Texture].Params.BuiltinSampler`; its values are the CSO record's
|
||||
(`MGPipeValueTypes.h:462-486`); the raw-depth-fetch substitution stays on the server
|
||||
(ARCHITECTURE §5.5); `texture2D->GetFormat()` becomes `SamplerViewCsos[View].View.InternalFormat`.
|
||||
**Window rule (A8 closed):** `set_sampler_views` and `bind_sampler_states` MUST carry `Start == 0`
|
||||
and `Count >= MaxTouchedTextureUnit + 1` (0 when none touched), as `SamplerEmit.h:733-737` emits
|
||||
today; at every draw/dispatch apply the sink checks the two counts against the applied
|
||||
`MGPContextValues::MaxTouchedTextureUnit` and a narrower window is `Fatal{ProtocolCorruption,
|
||||
"SetSamplerViews.Count"}` / `"BindSamplerStates.Count"` — a server-side re-derivation is refused.
|
||||
The `BindCurrentTextures` memo key is `{DrawProgram.{Slot,Gen}, ShaderCso.Serial, BindingsSerial,
|
||||
SamplerViewsSerial}` (ruling 6). The decline arms `DirectGLES.cpp:5152-5169` and `:2073-2098` become
|
||||
named refusals under a live wire; `CaptureUnitBindings/UnitBindingsUnchanged/g_observedUnitBindings`
|
||||
(`:1867-1957`) are monolith-only text.
|
||||
|
||||
### 5.4 Framebuffers, attachments, images (fb)
|
||||
`SyncCurrentFBO` is `SyncCurrentFBOByRecord` only; a decline under a transport is
|
||||
`Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<verb>"}`; `FramebufferRecordMatchesBinding`
|
||||
(`:3017-3033`) is monolith-only. `BackendFramebufferObject::SyncToBackend(record, target)`: the
|
||||
attachment walk is over `Color[0..7] + Depth + Stencil` (`Color[0]` alone when `IsDefault`) — the
|
||||
record's 11 surfaces ARE the point set (the emitter refuses a point at or above the wire width,
|
||||
`FramebufferEmit.h:489-501`; FRONT/BACK tokens exist only on the default framebuffer); the empty
|
||||
point is `Kind == None || Res null` for the detach (`Managers.cpp:10310`) and the N-6 refusal
|
||||
(`:9601-9606`) alike; `m_syncedFrontendAttachmentVersions` is replaced by the record hash;
|
||||
`g_attachmentBackendIdGeneration` stays. `SyncAttachmentSurface(target, surface, attachment)` takes
|
||||
no object; storage through `SyncMipmapsToBackendByHandle` / RBO `SyncToBackendByHandle(surface.Res)`.
|
||||
The two attachment texture lists key `(record.Fbo, ContentHash, ContextSerial,
|
||||
g_backendContextGeneration)` with entries `{Res, backend}` and `PairingsIntact` = `entry.Res ==
|
||||
surface.Res`. `ForceBindCurrentFBO` is handle-only and stamps nothing frontend (the stamps at
|
||||
`:4734-4738` and the legacy memo are skipped together under the transport test). The named blit
|
||||
takes two `const MGPFramebufferState&` (draw buffers, `ReadSurface`, `Kind`, layer/level/layered/
|
||||
format from the surface, `Samples` from the record, `kMGPipeDefaultFramebuffer` for the default)
|
||||
and retires `StateForHandle`; the bound arm reads `FramebufferRecordFor(st.BoundFramebuffer[t])`.
|
||||
`ScopedDetachedTextureFramebufferAttachments` walks a reverse index texture → FBO slots maintained
|
||||
at `set_framebuffer_state` apply. `MGPFramebufferState::Complete` is never a "may I draw" gate.
|
||||
`SyncReadFramebufferTextureAttachments` is not gated on the framebuffer bit (`:2227-2230`), so its
|
||||
retirement moves the object A/B baseline in BOTH arms (re-baselined, not a regression).
|
||||
**Default-framebuffer resize:** the surface-changed consumer's `AllocateStorage` (`ClientSession.cpp:210-221`)
|
||||
reaches `MGP_NOTE_AGGREGATE(FramebufferAttachment)` (`TextureObject.cpp:84-102`), so bit
|
||||
`NewFramebuffer` (`Tracker.h:594`) re-emits the record — verified at this head, pinned by a test in fb.
|
||||
Images: `SyncImageTextureBinding(const MGPImageView&)`: twin by `FindByHandle(Res)`, `Access`
|
||||
decoded, target and storage format off the TWIN (no new wire field; `MGPImageView` is 24 B, no
|
||||
pad); `ResolveShaderImageRecord`'s identity test and I5 seam log go. The sweep gate is
|
||||
`(ShaderImagesSerial, TextureShutterSerial, ContextSerial, g_backendContextGeneration)` — never a
|
||||
backend re-mint counter, so the "re-minted inside the sweep" property (`:2890-2901`) holds; the walk
|
||||
stays the UNION of the record window and `g_imageUnitHighWaterMark` (`:2811-2821`), never narrowed
|
||||
to the window. `MarkWritableImageBufferTexturesGpuWritten` is DELETED under a transport (the client
|
||||
owns the GPU-write set, `GpuWritePending.h:9-55`, `:83-115`); `WritableMask` survives as the record
|
||||
of which points P9's narrowing will name.
|
||||
|
||||
### 5.5 Programs (pg)
|
||||
Twin from the ShaderCso handle; `SyncToBackend(h, const MGPipeShaderCsoRecord&)` answers every
|
||||
reflection question from the record-owned archive and the bindings tails (`GetLinkedShaderSnapshot`
|
||||
`:12049` is a debug log, deleted on this arm). The nine-clause clean condition (`DirectGLES.cpp:4437-4507`)
|
||||
keeps its clause count and moves its inputs: `record.Serial` vs `m_syncedShaderCsoSerial` (exists,
|
||||
stamped at `Managers.cpp:12944-12966`, unread today — P5e is its first reader), `BindingsSerial`,
|
||||
`record.Signature`, `Desc.SpirvStatus`, `Desc.LinkStatus`; `ImageUnitFormatsStillMatch` and the
|
||||
patch clauses are fb's / value rows. The stash `g_currentDrawFrontendProgram` (`:4097-4098`)
|
||||
becomes `{MGPipeHandle, twin*}` compared against `st.DrawProgram`; `PrepareForCompute` reads
|
||||
`st.DispatchProgram`. The sampler-pass memo (`:5522`) keys on `BindingsSerial`;
|
||||
`sampledTargetForUnit` reads the sampler-unit tail and the archive's uniform types. **`MapUBO()`
|
||||
has no run-ahead answer:** when `ResolveGlobalConstantsRecord` declines under a transport it is
|
||||
`Fatal{UnmigratedVerb, "set_global_constants"}`, not a torn upload. `GetProgramObject/
|
||||
ValidateProgramName` keep their P9 rows: `GetBackendProgramId(name)` (`:6151`) is a monolith entry;
|
||||
`ShaderStorageBlockBinding` (`:10020`) is reached from the barriered `set_storage_block_binding`
|
||||
(§2.2) until pg's trailing item resolves it by `MGPStorageBlockBinding::ShaderCso`. Codec
|
||||
completeness: `ProgramArtifactsCodec.cpp:242`'s skipped member is named and either encoded or
|
||||
proven unread by the twin before pg claims "every reflection question is answered".
|
||||
|
||||
### 5.6 Buffer binding points (sb)
|
||||
Applier: `BoundShaderBuffers[3][84]`, `ShaderBufferStart[3]` / `ShaderBufferCount[3]` /
|
||||
`ShaderBufferWritableMask[3]` (the draft said bare `Start/Count/WritableMask`; they sit in
|
||||
`MGPipeApplierState` beside `SamplerViewStart`/`SamplerViewCount` and take the same shape), ONE
|
||||
`ShaderBuffersSerial` advanced on every applied record and ADVANCED (never zeroed) by
|
||||
`MGPipeApplierReset` (`VertexBuffersSerial`'s rule, `PipeApply.h`); the emitter's latch resets with
|
||||
it (`PipeFill.cpp:3129-3143`'s list). c0e declares all of it, with
|
||||
`MGPipeApplySetShaderBuffers` declared and aborting by name until sb writes the body.
|
||||
**`WritableMask` IS A `Uint32` AND THE WINDOW IS 84** (`MGPShaderBuffers::WritableMask`, unchanged
|
||||
since P4a): the mask can only describe the first 32 points of a class, so sb either narrows the
|
||||
window it sets bits for, widens the field, or states the bound — an integrator ruling sb must ask
|
||||
for rather than pick, because the field is on the wire. `SyncBufferBindingPointsByRecord(Class, glTarget)`: per entry
|
||||
`EnsureBufferResourceForHandle(nullptr, Res)`; whole-vs-range is `Offset == 0 && Size ==
|
||||
kMGPipeWholeBuffer`; clean = `resource->syncedChangeSerial` vs the resource record's `Serial`
|
||||
(`Managers.cpp:3151`) plus `ShaderBuffersSerial`; no twin key is introduced (a point names an object,
|
||||
it is not one). The UBO loop (`DirectGLES.cpp:5447-5497`) reads `BoundShaderBuffers[Uniform][binding]`
|
||||
where `binding` is the program's block binding (pg's tail); `binding >= Count` = nothing bound;
|
||||
`GetBufferResource(obj)` `:5472` becomes `FindBufferResourceForHandle`. `SyncAtomicCounterBuffers`
|
||||
reads `Count[AtomicCounter]`; `GetBufferBindingPointCount`'s sticky forward becomes FATAL under
|
||||
split (`InvalidateCompileEnv`'s shape, `FieldOwnership.def:163-165`). The three backend GPU-write
|
||||
marks (`DirectGLES.cpp:606, 658, 2775`) are deleted under a transport in ONE edit with fb's.
|
||||
**Client shutter (the P4b hole, verified: bits 15/16/17 read the content aggregate, `Tracker.h:525,
|
||||
636-639`, and `glBindBufferBase` moves only `BindingSlot::m_version`):** bit 15 =
|
||||
`Mix(bindPointGen[Uniform], programIdentity)`, bit 16 = `Mix(gen[ShaderStorage], gen[AtomicCounter])`,
|
||||
bit 17 = `Mix(gen[TransformFeedback], TransformFeedbackGeneration)`; the content aggregate leaves all
|
||||
three. `Touch` joins `gen_pipe_dirty_surface.py`'s `MUTATOR_PREFIXES` (`:87-90`) and `DirtySurface.def`
|
||||
gains the rows the generator then demands. `Coverage.def`'s EMITTED list gains `GetBufferBindingPoint
|
||||
→ SetShaderBuffers` as SHAPE-ONLY — **landed by c0e, with BOTH of its `PipeFill.cpp` switch arms
|
||||
(`SubsystemForEmitter` and `EmittedCallSuppliesTheWholeField`) and the pairing `static_assert`s**,
|
||||
because that file belongs to the contract package for the whole phase and an emitter enumerator
|
||||
whose dispatch arm lived in the family's own worktree is exactly the merge trap the file's own
|
||||
comment was written about. It is inert until the family's wired constant leaves 0.
|
||||
`FieldOwnership.def:80-81` flips to RECORD_SUPPLIED when the four Espryt consumers are gone. `GetBufferBindingSlot`'s 15-pointer copy (`PipeFill.cpp:116-124`) needs no
|
||||
special case: an unbarriered record has no fill at all.
|
||||
|
||||
### 5.7 XFB — stays lockstep
|
||||
`StartPendingTransformFeedback` (`:1381-1510`), `SyncTransformFeedbackBindingPoints` (`:562-592`),
|
||||
`ReadbackCapturedRanges` (`:1118-1160`) and the `xfb.targets` pins are not migrated
|
||||
(`GetTransformFeedbackProgram` is an object row no package retires; the targets hold `SharedPtr`s
|
||||
across two verbs, which rule C forbids an applier entry to do without a barrier). Escalation (i)
|
||||
makes every verb inside an active span barriered; the cost on the Espryt steady path is one bool
|
||||
test per draw (`:1386`).
|
||||
|
||||
### 5.8 Monolith / G1 / the push build under `Transport=monolith` (ruling 1)
|
||||
Every server-side handle arm is selected by `MG_Config::Transport != Monolith` (the idiom at
|
||||
`DirectGLES.cpp:3226, 4725, 6534`, `Managers.cpp:9629`) AND the family bit; it runs under ANY
|
||||
active transport, lockstep or run-ahead, so `RUN_AHEAD=0` is a pure wait-rule A/B on identical
|
||||
server code. The push build under `Transport=monolith` keeps its frontend arms token for token
|
||||
(behaviour and bytes), which the verify comparator needs; the pull build compiles none of it (every
|
||||
new symbol sits under `MOBILEGL_PIPE_PUSH` / `MOBILEGL_BUILD_DISAGGREGATED`; the `TwinRegistry`
|
||||
alias keeps its mangled name, `Managers.h:626-640`; `BackendTextureObject`'s pull-build layout does
|
||||
not move — the admitted-resize set stays empty). Two overloads, not an `#if` inside one body — the
|
||||
frontend one is visibly the monolith-glue half, as `GetOrCreate(const StatePtr&)` is
|
||||
(`SlotTables.h:236-244`). G1 0/0/0/0, G2/G14 name-set rules kept; the new public names are listed in
|
||||
BRIEF-P5E per package. `MOBILEGL_PIPE_VERIFY` forces lockstep (`ConfigLoader.cpp:377`, confirmed).
|
||||
|
||||
---
|
||||
|
||||
## §6 Magma keeps the lockstep
|
||||
|
||||
1. The DirectVulkan arm of `CallMask` never sets `kCapRunAheadApply`; `RunAheadArmed()` is false;
|
||||
the client runs today's `:915-959` path; `MGPipeApplierCurrentRecordIsBarriered()` answers `true`
|
||||
for every record on a server that does not publish the bit, so Magma's probes inside
|
||||
`MagmaP7AllocatorDebtScope` and its BARRIER_PULLED reads keep P5C's semantics and `rsp`
|
||||
accounting. P7 retires them.
|
||||
2. On Espryt the bit is published by `Init.cpp`'s DirectGLES arm gated on one constant
|
||||
(`kMGPipeP5eRunAheadReady`, false until the integration commit), so every package lands with the
|
||||
column inert and nothing changes behaviour until the integrator flips it.
|
||||
3. Negative controls: `MOBILEGL_IPC_RUN_AHEAD=1` on Magma logs once and runs lockstep;
|
||||
`MagmaPipeIdentityTest`/`ServerLoopTest` pin that a DirectVulkan `CallMask` has no bit 10.
|
||||
|
||||
---
|
||||
|
||||
## §7 The strict gate
|
||||
|
||||
`integration-split-strict` (`.github/workflows/test.yml`) becomes a HARD GREEN lane for the Espryt
|
||||
run-ahead server: with `MOBILEGL_IPC_STRICT_ERRORS=1` and the caps bit published, every scenario
|
||||
must complete. Because §3.3 aborts on an unbarriered pull regardless of the knob, strict adds only
|
||||
the barriered rows' pulls, and a revert of any handle arm in vi/sb/pg/tx2/fb goes red HERE, by
|
||||
field and verb, not in a picture. The `PipeSlotPeek` harness asserts zero scope entries per frame
|
||||
on the draw path.
|
||||
|
||||
**The allowlist is DERIVED, not written** (ID-116, refined by ID-125 and ID-128). A
|
||||
`<field>@<verb>` pull is ADMITTED iff the field's ownership row is `BARRIER_PULLED`, the verb has a
|
||||
stamp row, the field is inside `kMGPipeClassFieldMask[class of verb]`, and any one of:
|
||||
|
||||
1. the verb's wire op is statically barriered (`MGPipeWaitClassFor(op) != kWaitNone`);
|
||||
2. the field's retiring phase does not name P5e — a debt some later phase owes;
|
||||
3. the record was barriered BY ESCALATION (`MGPipeBarriered(op, payload, st)` true while the op is
|
||||
`kWaitNone`): an open transform-feedback span or a draw carrying client vertex arrays, both of
|
||||
which this contract puts outside P5e (§5.7, ID-82).
|
||||
|
||||
Disjuncts 1 and 2 are static and generated by `scripts/gen_pipe_field_ownership.py` into
|
||||
`MGPipeBarrierPullAdmitted(field, verb)`; `--print-admitted` gives CI the same table. Disjunct 3 is
|
||||
a fact about the record and is stamped beside the barriered stamp in `PipeApplier::ApplyOne`. An
|
||||
admitted pull is **loud, not fatal** (ID-117): one deduped `Admitted{UnmigratedPipeInput,
|
||||
"<F>@<V>"} [BARRIER-PULLED, ADMITTED|ADMITTED-ESCALATED, retires in <phase>]` per process, keeping
|
||||
the `Fatal{UnmigratedPipeInput` grammar so every filter written since P5c still means "red".
|
||||
|
||||
**The lane is a two-sided ratchet** (ID-119). Its log set comes from
|
||||
`ctest --show-only=json-v1`, not from a directory — the directory form missed 26 of the entries,
|
||||
exactly the readback population the allowlist is about. An unadmitted marker fails; so does an
|
||||
admitted pair the expected set (`MG_IntegrationTest/Harness/strict-expected-markers.txt`) does not
|
||||
carry, AND an expected pair that no longer appears, so the lane cannot rot green.
|
||||
|
||||
**The `rsp` pin, restated** (ID-119). The former pin — "`rsp` is 0 on unbarriered records" — was
|
||||
VACUOUS: `CountBarrierPull`'s unbarriered arm is `[[noreturn]]` and runs before `++g_residualPulls`,
|
||||
so it was true of every possible implementation. What is checkable: `rsp` counts barriered pulls
|
||||
only, and under the strict knob every one it counts is an ADMITTED pull, because the rest abort.
|
||||
Its shape is asserted against `draws` rather than against 0 — `rsp == 0 || rsp >= draws` — since
|
||||
the draw path's retirement is observable as `rsp` ceasing to scale with `draws` (measured on the
|
||||
device: `rsp` ~= the draw count under inproc, 0 under monolith).
|
||||
|
||||
**The lane has a positive control** (ID-115). `DirectGLES.Split.StrictArming.` runs one drawing
|
||||
inproc entry with `MOBILEGL_PIPE_STATS=1`, `MOBILEGL_PIPE_STATS_PERIOD=1` and a private log path,
|
||||
and asserts `vbs > 0` — the server stamped a verb boundary. Everything strict checks is downstream
|
||||
of that stamp and the monolith arm never stamps, so without it "the lane is green" and "strict was
|
||||
never armed" are the same observation.
|
||||
|
||||
**Magma keeps the expected-red step, in a lane of its own.** The two `DirectVulkan.Split.NamedBlit`
|
||||
entries carry `integration-magma-split` (so spelled: `ctest -L` is a regex and
|
||||
`-L integration-split` matches `integration-split-magma`) and are asserted RED on
|
||||
`Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@Clear"}`. Admitting that pair instead would
|
||||
forgive the same read on Espryt, where it is this phase's debt — one retiring-phase string serves
|
||||
both backends.
|
||||
|
||||
---
|
||||
|
||||
## §8 Amendments to CONTRACT-P5C (the complete list)
|
||||
|
||||
1. **§3.1's two named exemptions** are superseded by §4.4: the registry scope is deleted at every
|
||||
draw-path site and exempts only a barriered record elsewhere; the announcement scope is Magma's.
|
||||
2. **§4.4 overflow**: lossless stays, but a run-ahead server parks on `Reserve == nullptr` (§2.6).
|
||||
3. **§5.2 `object_death`**: the ring keeps the ordering; the row is `kWaitNone`.
|
||||
4. **§5.1 `applier_reset`** is `kWaitApplied`; its `ContextSerial` assertion stands.
|
||||
5. **§6 CI lane**: the two-sided lane becomes one-sided green for Espryt (§7).
|
||||
6. **§6 layer 2** gains `Fatal{RoleViolation, "gPipeInputs"}` (§3.5); `InBarrierWait()` is
|
||||
re-derived as `MGPipeBarriered`. 7. **§3.3's state note** is retired by fb (§5.4).
|
||||
8. **`FieldOwnership.def`**: `GetBoundVertexArray`, `GetTextureUnitObject`, `GetImageTextureBinding`,
|
||||
`GetFramebufferBindingSlot`, `GetProgramForDraw/Dispatch`, `GetBufferBindingPoint` gain retiring
|
||||
phase "P5e (Espryt unbarriered), P7 (Magma)"; ~~`GetBufferBindingPointCount`'s forward → FATAL under
|
||||
split~~ (**UNLANDED — see below**); `GetTransformFeedbackProgram`, `GetProgramObject`,
|
||||
`GetTextureObject`, `ValidateProgramName`, `HasOpenTransformFeedbackSpan`, `RecordError` keep
|
||||
their rows (barriered-only readers).
|
||||
|
||||
> **UNLANDED, and deliberately so (P5e gl, ruling ID-120). Lands with P7/P13.** The clause
|
||||
> "`GetBufferBindingPointCount`'s forward → FATAL under split" is NOT in the code and must not
|
||||
> be put there this phase. `FieldOwnership.def:147` (the field row) and `:179` (the forward row)
|
||||
> both say `BARRIER_PULLED`, and the generator refuses a field/forward pair that disagrees —
|
||||
> pinned by `FieldOwnershipTest.TheSevenStickyForwardsAgreeWithTheirFieldRows`. So landing the
|
||||
> clause as written would either trip the generator or drag the FIELD row to FATAL with it, and
|
||||
> that second reading changes **23 pairs** of the derived admitted set (§7, measured with
|
||||
> `--print-admitted | grep -c '^GetBufferBindingPointCount@'`; ID-120 said 10, which was
|
||||
> counted before ID-125's retiring-phase disjunct widened the set). The row's retiring phase is
|
||||
> "P7/P13", which is exactly what admits it on every stamped verb today; a FATAL row is
|
||||
> admitted nowhere, so those reads would start aborting on a migration this phase never
|
||||
> promised.
|
||||
>
|
||||
> The divergence is STATED HERE rather than discovered from a red gate, for ID-105's reason:
|
||||
> when a contract clause is arithmetically impossible against the code, the contract is what
|
||||
> changes. The phase that retires the buffer-binding-point family (P7 for the Magma half, P13
|
||||
> for the transfer half) lands the field row and its forward TOGETHER, in one commit, and
|
||||
> deletes this note.
|
||||
9. **Table 1** gains `set_program_bindings` (80), appended; `set_shader_buffers` (38) gains its
|
||||
route, sink and emitter (`PipeCatalogueTest.cpp:170-171, 242-243` invert). **Table 0** gains §1's
|
||||
thirteen rows. **Table 3** gains `IpcTable::RunAhead/PresentCredit`.
|
||||
10. **P5C §6's `rsp` paragraph**: the measured object-class pulls (948.5/frame on bsl) go to 0 on
|
||||
unbarriered records; what remains is the barriered allowlist of §7.
|
||||
|
||||
## §9 The rulings, as made (ID-80…98; BRIEF-P5E §6 has the conflicts they resolve)
|
||||
|
||||
All nineteen are ACCEPTED AS WRITTEN. They are restated here rather than referenced, because a
|
||||
package reads this file and not the brief.
|
||||
|
||||
1. Monolith arm selection: transport check + family bit, frontend arms kept for monolith (§5.8) —
|
||||
NOT S7's "handle arm under monolith too" (it would make the monolith push build depend on the
|
||||
new emissions and lose the verify comparator's frontend arm).
|
||||
2. Client vertex arrays: refuse under run-ahead (§5.1), stage in P8; wait-escalation rejected (it
|
||||
would need `Find(vao)` on the apply thread, which §4.4 forbids for a draw).
|
||||
3. XFB / atomic dispatch waits: the wire-derived predicate (§2.1); atomic-counter dispatch needs no
|
||||
escalation because sb lands 4.5.
|
||||
4. Present credit default 1, range 1..8; credit 2 is a measurement arm only.
|
||||
5. `resource_subdata`: buffer half `kWaitNone`, texture half keeps the reply (trailing D-D5).
|
||||
6. `BindCurrentTextures` memo key: both S2's and S4's rows (§5.3).
|
||||
7. Image sweep gate: applier serials, never a backend counter (§5.4); keep the union walk.
|
||||
8. Program archive: encode per link into SEG_STAGE (§1), measured on MC + a shader pack before pg
|
||||
is sized; the `SharedPtr` pin is refused; lazy encode at first bind is the fallback.
|
||||
9. Failed relink: re-issue with `LinkStatus = 0` iff the frontend reports the program unlinked
|
||||
afterwards; pg verifies which at `ProgramObject.cpp:348-356, 518-522`. Never an `object_death`.
|
||||
10. `set_shader_buffers` window: the touched high-water count per target; capacity 84 on the wire,
|
||||
the backend clamps (`DirectGLES.cpp:500-504`). 11. Suppressor: three class slots.
|
||||
12. Magma scope: one renamed scope, backend-kind-keyed exemption (§4.4).
|
||||
13. Deferred-destroy queue: stays live (§2.7). 14. `ResolvedDrawBuffers`: measured after vi.
|
||||
15. `Blit`'s `kWaitNone` is fb's landing, not a separate package; `Clear`'s is tx2+fb.
|
||||
|
||||
---
|
||||
|
||||
## §10 What c0e landed, by name — the surface every other package compiles against
|
||||
|
||||
Inert by construction: the caps bit is not published (`kMGPipeP5eRunAheadReady` is `false`), the
|
||||
new opcode has no route, the new subsystem bit is not in `kMGPipeWiredSubsystems`, and every new
|
||||
applier entry point and by-handle backend signature has a body that aborts by name. Unit,
|
||||
`integration-split` and the generator gates are green with all of it in the tree.
|
||||
|
||||
| where | the names |
|
||||
|---|---|
|
||||
| `MG_Pipe/MGPipeTypes.h` | `kCapRunAheadApply`, `MGPipeRunAheadCapBitsFor(BackendType, Bool)`, `kMGPipeMaxBufferBindingPoints`, `kMGPipeShaderBufferClass{Uniform,ShaderStorage,AtomicCounter,Count}`, `MGPProgramBindings`, `MGPProgramSamplerUnit`, `MGPProgramStorageOverride`, `kMGPipeMaxProgram{BlockBindings,SamplerUnits,StorageOverrides}`, `MGPProgramDesc::LinkStatus`, `kDrawClientArrays` |
|
||||
| `MG_Pipe/MGPipeValueTypes.h` | `MGPipeImageAccess`, `kMGPipeImageAccess{ReadOnly,WriteOnly,ReadWrite}`, `MGPipeImageAccessIsValid`, `MGPipeDecodeImageAccess`, `MGPipeImageAccessReads/Writes` |
|
||||
| `MG_Pipe/MGPipe.h` | `MGPipeWaitClass` (`kWaitNone/kWaitApplied/kWaitReply/kWaitPresent/kWaitClassCount`), `kMGPipeSubsystemBufferBindings`, `kMGPipeSubsystemsMigratedAtP5e` |
|
||||
| `MG_Pipe/generated/PipeWire.inc` | `MGPWireOp::SetProgramBindings` (80), `kMGPipeWaitClasses[]`, `MGPipeWaitClassFor(op)` |
|
||||
| `MG_Pipe/PipeApply.h` | `MGPipeBarriered(op, payload, st)` (implemented), `MGPipeApplierCurrentRecordIsBarriered()` / `MGPipeApplierSetCurrentRecordBarriered()` (implemented, default `true`), `MGPipeApplySetShaderBuffers` and `MGPipeApplySetProgramBindings` (declared, aborting), `MGPipeApplierState::{BoundShaderBuffers, ShaderBufferStart, ShaderBufferCount, ShaderBufferWritableMask, ShaderBuffersSerial, IsTransformFeedbackActive}`, `MGPipeShaderCsoRecord::{BlockBindings, SamplerUnits, StorageOverrides, Signature, BindingsSerial}`, `MGPipeProgramStorageOverride` |
|
||||
| `MG_Impl/Pipe/SetHashSuppressor.h` | `MGPipeSuppressorSlot::{SetShaderBuffersUniform, SetShaderBuffersShaderStorage, SetShaderBuffersAtomicCounter, SetProgramBindings}` |
|
||||
| `MG_Impl/Pipe/Tracker.h` | `kMGPipeDirtyEmittedAtP5e`; bits 15/16/17 mapped onto bit 13 |
|
||||
| `MG_Impl/Pipe/ImageEmit.h` | `MGPipeEncodeImageAccess` promoted to a free function over the shared table |
|
||||
| `MG_Backend/DirectGLES/Managers.h` | `VertexArrayImpl::ResolveVaoTwin(handle)`, `TextureImpl::SyncTextureToBackendByHandle`, `TextureImpl::ResolveTextureTwin`, `BackendTextureObject::SyncMipmapsToBackendByHandle`, `BackendFramebufferObject::SyncToBackendByHandle`, `BackendRenderbufferObject::SyncToBackendByHandle`, `BackendProgramObjectImpl::SyncToBackendByHandle`, `PrgramImpl::ResolveProgramTwin` — all declared, all aborting by name, one block at the end of `Managers.cpp` |
|
||||
| `Config.h` / `ConfigLoader.cpp` | `IpcTable::RunAhead`, `IpcTable::PresentCredit`; the push default is `kMGPipeSubsystemsMigratedAtP5e` |
|
||||
| `MG_Backend/Init.cpp` | `kMGPipeP5eRunAheadReady` (`false`) and the caps arm through `MGPipeRunAheadCapBitsFor` |
|
||||
|
||||
**The three red-onces c0e ran, and what they name.** (1) flipping one row's `WaitClass` without
|
||||
regenerating → `gen_pipe: OUT OF DATE: MobileGL/MG_Pipe/generated/PipeWire.inc`, rc 1; with the
|
||||
regeneration, `PipeCatalogue.EveryRowCarriesTheWaitClassTheContractGivesIt` fails naming
|
||||
`GenerateMipmap`. (2) `sizeof(MGPProgramBindings)` 32 → 40 →
|
||||
`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` fails "Which is: 40 / 32". (3) making
|
||||
the caps arm ignore the backend type →
|
||||
`MagmaPipeIdentityTest.AMagmaServerNeverPublishesTheRunAheadCapBit` fails "Which is: 1024 / 0".
|
||||
@@ -0,0 +1,757 @@
|
||||
# CONTRACT-P6 — the backend runs in a second process
|
||||
|
||||
Authority: this file, beside `CONTRACT-P5.md` (table 0, byte carriers, field ownership, R-1…R-17),
|
||||
`CONTRACT-P5B.md` (the class-C slots), `CONTRACT-P5C.md` (rule E, the two named exemptions, SEG_EVENT,
|
||||
the guards), `CONTRACT-P5E.md` (rule F, the barriered predicate, the wait rule) and
|
||||
`CONTRACT-MAGMA-RUNAHEAD.md`. Where it disagrees with any of them this file is newer and wins; §10
|
||||
lists every such place. Base: `feat/disaggregated @ c25a7760`. Every `file:line` was read at that
|
||||
commit; paths are under `MobileGL/` unless they start with `docs/`.
|
||||
|
||||
**How to change it.** Package `c6`'s file, edited by the integrator first. A P6 package
|
||||
(`lk` / `hs` / `so` / `sm` / `cp` / `dl` / `st` / `t6`) that needs a row changed goes through the
|
||||
integrator; the packages compile against the rows below from day one.
|
||||
|
||||
**What this file is written from.** [`a6-audit-v1.md`](../notes/p6/a6-audit-v1.md)
|
||||
(198 rows, seven items, each adversarially re-resolved) and
|
||||
[`a6-link-experiment.md`](../notes/p6/a6-link-experiment.md) (the 184 symbols).
|
||||
It supersedes [`P6-CONTRACT-DRAFT.md`](../notes/p6/P6-CONTRACT-DRAFT.md), which was
|
||||
written before its own audit and is **wrong in four places** (§10.1). The re-scoping against the
|
||||
eventual client-in-a-VM end state is
|
||||
[`P6-ENDSTATE-REVIEW.md`](../notes/p6/P6-ENDSTATE-REVIEW.md); this file takes from it
|
||||
only what P6 must not preclude, and §11 says what P6 explicitly does not take on.
|
||||
|
||||
---
|
||||
|
||||
## §0 What c6 is, and the rule above every row
|
||||
|
||||
`c6` lands **this file and two headers**. It changes no behaviour, adds no caller, and moves no
|
||||
byte on any wire. Every rule below that describes an action names the package that implements it.
|
||||
A rule with no package named is a statement about the tree as it already is.
|
||||
|
||||
**Rule G — a role may not name a process-local handle.** Rule E forbade naming the other role's
|
||||
*memory*; rule G extends it to *identity*. A value whose meaning comes from the owning process's
|
||||
kernel or loader state — `EGLDisplay`, `EGLSurface`, `EGLContext`, `ANativeWindow*`, any `void*`
|
||||
window handle, an fd number, a pid, a mapped address — either crosses as a **token** defined in §2,
|
||||
or does not cross. Descriptors cross only by a mechanism the transport **declares it supports**, and
|
||||
where the transport declares none they do not cross at all and the bytes are transferred instead;
|
||||
a transport may not invent a third answer.
|
||||
|
||||
> The draft hard-coded `SCM_RIGHTS` inside rule G. That is one transport's implementation of one
|
||||
> clause, not the rule, and writing it into the rule is what would make the rule wrong the first
|
||||
> time a transport has no descriptor passing at all.
|
||||
|
||||
**Rule H — a value whose meaning comes from the peer's window system never crosses. Permanently.**
|
||||
Not "until P12". P12 gives the *server* its own display; it does not give the client's window a way
|
||||
across. §6 is rule H's whole enforcement.
|
||||
|
||||
**Rule G's teeth** are §9's arm-proof gate: a spawn lane that silently ran monolith satisfies every
|
||||
other gate in this file.
|
||||
|
||||
---
|
||||
|
||||
## §1 The one fact that makes c6 possible: `Spawn` is unreachable today
|
||||
|
||||
`ConfigLoader.cpp:339-346` is the only writer of `MG_Config::Transport` for the string `spawn`, and
|
||||
it writes **`TransportMode::Monolith`** at `:344`, with a log line saying P5 does not implement it.
|
||||
|
||||
Two consequences, and the second is why this file can exist without changing behaviour:
|
||||
|
||||
1. **`ConfigLoader`'s named refusal cannot falsify itself.** It lands on the same value a server's
|
||||
resting state would have, so "the child was forced to monolith" and "the parent fell back" are
|
||||
one state. `sm` fixes this; until then, no lane can prove it ran spawn (§9.5).
|
||||
2. **Every rule in this file conditioned on `Transport == Spawn` is vacuous in c6's own tree.** It
|
||||
constrains a state no process can enter. That is how D1 (§3.1) is stated normatively while
|
||||
changing nothing, and `c6`'s gate pins the vacuity so the `ConfigLoader` change cannot land early.
|
||||
|
||||
---
|
||||
|
||||
## §2 Table 0 additions
|
||||
|
||||
| row | encoding | invalid | consumer | package |
|
||||
|---|---|---|---|---|
|
||||
| `DisplayToken` / `SurfaceToken` / `ContextToken` | `Uint64`, minted by the client, dense from 1, never reused in a session. Server keeps token → native handle; it never sees the client's EGL values. `protocol.fbs`'s `display` / `surface` / `context` are already `ulong` and are these. **The client already mints them** (`MG_State/EGLState/Core.h`'s `EncodeHandle`), contrary to `Server/SurfaceControlFrame.h:26-29`, which says inproc bit-casts the handle — that comment is corrected by `cp`. | `0` on any op but `ReleaseCurrent` → `Fatal{ProtocolCorruption}` | the server's surface map | `cp` |
|
||||
| `SurfaceOpKind::InitCapabilities = 11` | **Appended.** The draft claimed `ServerInitCapabilities` needs no wire form because "`CapsSnapshot` answers it". The *answer* is a `CapsSnapshot`; the **request** has a live client caller (`Client/BackendObject_Remote.cpp:126`) and the codec **refuses to encode it** (`Protocol/SurfaceOpCodec.cpp:119-122`, `InprocOnlyOpOnTheWire`). A spawn client cannot ask. | unknown tag → `Fatal{ProtocolCorruption}`, never ignored | the server's control pump | `cp` |
|
||||
| `SurfaceReply.result: int` | **Appended, id 5.** Carries the `MobileGLResult` that `ServerApplyWireSurfaceOp` already computes and drops. Without it a client cannot tell a server that died mid-op from a legitimate refusal, and therefore cannot choose between arming the device-lost latch and returning `EGL_FALSE`. | — | the client's control demultiplexer | `hs` |
|
||||
| `SurfaceReply.defaultFb` | **`(deprecated)`.** The encoder passes a literal `0` offset (`SurfaceOpCodec.cpp:184`) and the decoder never reads it; `kEventSurfaceChanged` is the landed carrier (P5f `fc`). The slot is **burned, never deleted** — a deleted field frees its vtable slot for the next append. | — | nobody, by construction | `hs` |
|
||||
> **asio was considered for the byte stream and declined (2026-09-21).** It is already vendored
|
||||
> and on the include path, so the question is a fair one. But spawn needs five platform
|
||||
> mechanisms and asio covers one: its own `socketpair` returns `operation_not_supported` on
|
||||
> Windows (`3rdparty/asio/include/asio/detail/impl/socket_ops.ipp:683`), it has no descriptor
|
||||
> passing at all, segment delivery is POSIX-only by design, and there is no `fork`. So adopting
|
||||
> it would not make Windows work; it would only put asio headers inside `Transport/`, which
|
||||
> `ITransport.h` keeps dependency-light on purpose. **Revisit at P6.5**, where
|
||||
> `asio::generic::stream_protocol` carries a raw (family, type, protocol) triple and therefore
|
||||
> gives AF_VSOCK from the same implementation (2026-09-22: the end state's link is TCP, not
|
||||
> AF_VSOCK - §10.3 - and the argument is the same for `asio::ip::tcp`) - and where `ITransport` lets an `AsioTransport`
|
||||
> land beside `SocketTransport` without touching a caller.
|
||||
|
||||
| `LinkTerms{dataPlane, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` | Appended to `Hello`/`Welcome`. `dataPlane` has exactly **one** legal value in P6, `SharedSegments`; `wireForm` exactly one, `StructImage`. Anything else is refused **by name**. The four sizes are **stated by the SERVER**. | any other value → `Refuse{ProtocolMismatch}` | the handshake | `hs` → **not landed** (verified 2026-09-22: `protocol.fbs` has no `LinkTerms`); re-homed to P6.5 `nd`, §10.3 |
|
||||
| `Refuse{code, detail, peer values}` | Appended `CtrlMsg` union tag. A handshake disagreement is an **answer**, not an abort (§5.1). | — | both sides | `hs` → **not landed** (no `Refuse` in `union CtrlMsg`); re-homed to P6.5 `wf`, §10.3 |
|
||||
| `wireFingerprint` / `buildFingerprint` | The single `abiFingerprint` splits. §4. | mismatch → `Refuse`, never `Fatal` | the handshake | `hs` → **partially landed** (explicit build stamp, `abiMajor`/`abiMinor` checked, real pids; the split itself and the `PipeFields.def` layout digest are not); re-homed to P6.5 `wf`, §10.3 |
|
||||
| `FatalCode` | **Unchanged**, 7 values (`protocol.fbs:248-256`). `table Fatal` appends `family: string`. §5.2. | — | — | `dl` |
|
||||
|
||||
All schema edits are **append-only**. A FlatBuffers table field's id is its vtable slot and an enum
|
||||
value **is** the wire value; retired fields become `(deprecated)`, never disappear. `CapsSnapshot`'s
|
||||
four deprecated fields are the worked precedent and their reasoning is in `protocol.fbs` itself.
|
||||
|
||||
> **`hs` implementation note, from c6's review.** `flatc` drops deprecated fields from the generated
|
||||
> `Create*` helper. Deprecating `defaultFb` while appending `result` therefore changes
|
||||
> `CreateSurfaceReply`'s arity, and the existing 6-argument call at `SurfaceOpCodec.cpp:183-184`
|
||||
> would **silently bind its `/*defaultFb=*/0` to `result`**. `hs` converts that call site to
|
||||
> `SurfaceReplyBuilder` with explicit `add_*` in the same commit, so a positional mistake is
|
||||
> impossible rather than merely unlikely.
|
||||
|
||||
---
|
||||
|
||||
## §3 Role, Dial, and the six predicates
|
||||
|
||||
### 3.1 D1 — what `Transport` reads as in the server process
|
||||
|
||||
**`TransportMode::Spawn`. The child does not force `Monolith`.**
|
||||
|
||||
`ARCHITECTURE.md:488`'s literal instruction ("`mobilegl_server_main` hard-sets `MG_Config::Transport`
|
||||
to `Monolith` before reaching `Init()`") is **amended by this file**. a6 measured why: 226 live lines
|
||||
test `Transport != Monolith` to select the **server** arm, 148 of them in `MG_Backend`. Forcing
|
||||
`Monolith` flips all of them to frontend glue in the one process with no frontend. The sharpest
|
||||
instance is `PipeInputs::IsLive()` (`MG_Impl/Pipe/PipeFill.cpp:2135`): forced monolith, it reads
|
||||
`MG_State::pGLContext`, which is null there, so **58 `MGB_CTX_LIVE` guards all answer "not live"**.
|
||||
|
||||
Anti-recursion moves to a **separate, non-inherited axis**: `MG_Config::Dial == DialMode::No` in the
|
||||
child. The envp scrub stays as the second, independent catch, and S3 (§9) still falsifies it —
|
||||
and the diagnostic must **name which of the two fired**, or the double safety is untestable as two
|
||||
things. Package: `sm`.
|
||||
|
||||
### 3.2 D1c — there are six predicates, not three
|
||||
|
||||
a6 proposed three. Three do not cover the tree.
|
||||
|
||||
| predicate | asks | pull-build shape |
|
||||
|---|---|---|
|
||||
| `MGPipeSplitActive()` | do records cross a role boundary at all | `#else` returns `false` |
|
||||
| `MGPipeServerArm()` | am I executing as the server **right now** | `#else` returns `false` |
|
||||
| `MGPipeShouldDial()` | should this process connect out | `#else` returns `false` |
|
||||
| `MGPipeSessionLive()` | is a peer session live | role-defined; `#else` `false` |
|
||||
| `MGPipeBlocksAreDistinct()` | do the two roles share one storage object | `#else` `false` |
|
||||
| `MGPipeBackendIsLocal()` | is the backend in **this** process | `#else` `true` |
|
||||
|
||||
`ContextEpoch::Server` and `ActiveXfbState`'s role index are **`MGPipeSplitActive()`** sites, not
|
||||
`MGPipeServerArm()` sites: they are partition keys, and the `ServerArm` disjunct would make a
|
||||
partition key thread-dependent.
|
||||
|
||||
**D14 — all six must fold away in the pull build**, and the required shape is `SlotCaps.h`'s: the
|
||||
non-disaggregated build gets a **different definition** under `#else`, not a runtime-false predicate.
|
||||
`MGPipeSplitActive()` must additionally remain exactly one load of a process global and must never
|
||||
be latched into a `static Bool`. G1 is 0/0/0/0 and `.text` unchanged; anything else is a c6 defect.
|
||||
Package: `sm`.
|
||||
|
||||
### 3.3 D13 — rename before introducing
|
||||
|
||||
`MGPipeRoleSplitActive()` (`MG_Backend/MGPipe/PipeInputs.cpp:292`) means
|
||||
`Ipc.RoleSplitState && Transport != Monolith` — the **rehearsal knob**, default off. It is renamed
|
||||
to **`MGPipeRoleSplitRehearsalActive()`**, which is what it has always meant, and
|
||||
`MGPipeBlocksAreDistinct()` is introduced beside it as the wider predicate. a6's shape — rename, then
|
||||
widen the same function — is **overturned**: a widened function under a narrow name is a name that lies.
|
||||
|
||||
Blast radius, measured at this head: **12 sites**, of which `DualBlockScenario.cpp`'s assertion
|
||||
*message* embeds the old name and must be updated with it. Package: `sm`.
|
||||
|
||||
### 3.4 D10 — the four guards that cannot arm in a server-only process
|
||||
|
||||
| guard | today | becomes | package |
|
||||
|---|---|---|---|
|
||||
| `MGPipeApplierReset` layer 2 (`MG_Pipe/PipeApply.cpp:1353-1355`) | needs `ClientSession::Active() != nullptr`, permanently null in a server process | middle conjunct → `MGPipeSessionLive()`; third → `!MGPipeServerArm()` | `sm` |
|
||||
| `MGPipeServerBlockNoteIdentity` (`PipeInputs.cpp:314`) | early-returns unless the rehearsal knob is on | gate → `MGPipeBlocksAreDistinct()` | `sm` |
|
||||
| `CountBarrierPull`'s unconditional Fatal arm (`PipeInputs.cpp:174`) | knob-gated | rides `MGPipeBlocksAreDistinct()` | `sm` |
|
||||
| the two `RefuseFromApplyThread` helpers (`MG_Impl/Pipe/SlotAllocator.cpp:29-57`) | `ServerLoop::OnApplyThread()` | OR'd with a process-role fact set once by `ServerMain` | `sm` |
|
||||
|
||||
**The one that crashes rather than merely failing to arm.** With `MGPipeServerBlockNoteIdentity`
|
||||
early-returning, a spawn server's `gPipeInputs.ContextIdentity()` stays `nullptr` for the life of the
|
||||
process, and `DirectGLES.cpp:199-213`'s fb-slot memo does a **raw pointer compare with no generation**
|
||||
against it — so `nullptr == nullptr` reads as a **cache hit** and the first call uses an
|
||||
uninitialised slot. `PipeInputs.h:878-884`'s own comment says a named Fatal was intended there.
|
||||
`sm` lands the gate change; `st` lands the named Fatal.
|
||||
|
||||
---
|
||||
|
||||
## §4 The handshake (`hs`)
|
||||
|
||||
### 4.1 One equality test answers three different questions today
|
||||
|
||||
`AbiFingerprintInputs` (`Transport/SessionRings.h:544-566`) mixes wire properties, struct-layout
|
||||
properties and pure build identity into one `Uint64`. It splits:
|
||||
|
||||
**`wireFingerprint` — always compared.** Inputs, in order:
|
||||
an ordered **catalogue digest** over `PipeCalls.def`'s `(opcode, payload struct name, call-flag row,
|
||||
WaitClass)`; a **payload layout digest** derived from `PipeFields.def` (per member: name, `offsetof`,
|
||||
`sizeof`); the record-header layout; both blob-codec version stamps; the format-capability table's
|
||||
two extents; `kOpCount`; `MOBILEGL_ABI_VERSION`; endianness; pointer width; **and**
|
||||
`sizeof(MGPCaps)` and `sizeof(DynamicBackendParameters)`.
|
||||
|
||||
Those last two **stay compared** because they are wire facts, not build facts: `MGPWireRec_GetCaps`
|
||||
is composed from `sizeof(MGPCaps)` (`MG_Pipe/generated/PipeWire.inc`). `sizeof(GLFunctionsTable)` is
|
||||
**deleted** from the mix: it never crosses the wire, so adding one backend slot today invalidates
|
||||
every peer for no wire reason.
|
||||
|
||||
> **Why the layout digest is not optional.** `SessionRings.h:561-565`'s own comment says the git
|
||||
> stamp is in the mix because *two builds of the same sizes can still disagree about a FIELD ORDER,
|
||||
> which no sizeof can see*. A catalogue digest over `(opcode, struct name, flags, WaitClass)` sees
|
||||
> neither a member reorder inside a payload struct nor a width change. So the widely-proposed "drop
|
||||
> the git stamp, add a catalogue hash" is **wrong as usually stated** — it removes a guard and does
|
||||
> not replace it. The layout digest is the replacement, and `gen_pipe.py` already CI-enforces that
|
||||
> `PipeFields.def` names every member, so it is a generator addition and not a toolchain.
|
||||
|
||||
**`buildFingerprint` — compared only while `Dial == Fork`.** Under fork the child *is* the same
|
||||
binary, so comparing it catches a stale `MOBILEGL_IPC_SERVER_PATH` pointing at an old
|
||||
`libMobileGLServer.so` — a real P6 failure mode. **P6's policy is therefore strictly stronger than
|
||||
today's, not looser**; the end state is a policy flip, not a redesign.
|
||||
|
||||
### 4.2 The missing build stamp
|
||||
|
||||
a6 found `CMakeLists.txt:188-195` takes the git hash with **no `RESULT_VARIABLE` and no fallback**, so
|
||||
a failed `git rev-parse` silently yields `""`. Verified live: building this worktree from WSL
|
||||
produces `GIT_COMMIT_HASH_SHORT ""`, because the worktree's `.git` file holds a Windows path WSL's
|
||||
git cannot resolve. Two builds from different commits then get **identical** fingerprints and the
|
||||
stamp silently stops doing its documented job.
|
||||
|
||||
**The fix may not touch `GIT_COMMIT_HASH_SHORT`.** That macro is baked into
|
||||
`glGetString(GL_VERSION)` (`MG_Impl/GLImpl/Getter/GL_Getter.cpp:614`), so redefining it changes an
|
||||
**application-visible GL string** and the pull build's `.rodata` — a behaviour leak and a G1 break
|
||||
at once. Instead `MGGitHash.h.in` gains a **second** pair, `MOBILEGL_BUILD_STAMP_VALUE` and
|
||||
`MOBILEGL_BUILD_STAMP_PRESENT`, consumed only by the fingerprint, and the CMake step gains a
|
||||
`RESULT_VARIABLE`. A build with no stamp sets `PRESENT` to 0, and a `Dial == Fork` handshake whose
|
||||
peer reports `PRESENT == 0` is a **named refusal**: under fork the two peers are supposed to be the
|
||||
same binary, and without a stamp that premise cannot be checked at all. Package: `hs`.
|
||||
|
||||
### 4.3 Two verified asymmetries `hs` also fixes
|
||||
|
||||
- The client checks only `abiFingerprint` and **never `abiMajor`/`abiMinor`** (`ClientSession.cpp:566-568`).
|
||||
- `Hello::pid` is hard-coded `0` and `Welcome::serverPid` echoes the client's, so it is always 0
|
||||
(`ClientSession.cpp:519-521`, `ServerSession.cpp:532-534`). §9.5's arm-proof gate wants the child
|
||||
pid; `serverPid` is its natural carrier and must start carrying it.
|
||||
|
||||
### 4.4 Segment geometry (D-size)
|
||||
|
||||
`MOBILEGL_IPC_RING_MB` / `STAGE_MB` are read from the **client** process's env with ceilings of
|
||||
1024 / 4096 MiB (`ConfigLoader.cpp:363-364`), while the **server** creates the segments. Under spawn
|
||||
that is a guest environment variable sizing an allocation inside the renderer process. The four sizes
|
||||
become **server-stated** `LinkTerms` fields; the client's values become a *request* the server may
|
||||
clamp, and the clamped values are reported in `Welcome`.
|
||||
|
||||
`ShmSegment::Adopt`'s `fstat` "do not trust the peer's declared size" check
|
||||
(`Transport/ShmSegmentPosix.cpp:127-133`) is today the **only** bound, and it has no analogue on a
|
||||
link with no fd. The compile-time ceiling that refuses an oversized announcement by name is
|
||||
**P6.5's** to define; P6 does not pretend to have answered it.
|
||||
|
||||
---
|
||||
|
||||
## §5 Death, refusal, and what P5e changed
|
||||
|
||||
### 5.1 The handshake never aborts
|
||||
|
||||
A version or terms disagreement is a `Refuse{code, detail, peer values}` control message plus
|
||||
`MOBILEGL_ERR_PROTOCOL_MISMATCH` on **both** sides: the server sends `Refuse` and exits 0, the client
|
||||
reaps the child and refuses **by name**, never falling back to monolith. Both `FatalAbiMismatch` call
|
||||
sites go (`ServerSession.cpp:471-483`, `ClientSession.cpp:565-568`). The worked shape is eight lines
|
||||
away in the same function: `ServerSession.cpp:456-466` already returns
|
||||
`MOBILEGL_ERR_PROTOCOL_MISMATCH` for a malformed frame, with the correct `msg_as_Hello() != nullptr`
|
||||
guard. Package: `hs`.
|
||||
|
||||
### 5.2 `Fatal{}` keeps aborting in P6, and gains one funnel
|
||||
|
||||
a6 censused **92 abort sites** under `MG_Remote/` carrying **30 distinct family words**, against a
|
||||
**7-value** wire `FatalCode`. So `Session::Fail(FatalCode, detail)` as usually proposed **cannot carry
|
||||
today's vocabulary in its first argument**.
|
||||
|
||||
**D2 ruling.** The first argument is an internal `MGFatalFamily`, generated from a new
|
||||
`MG_Remote/FatalFamilies.def` — one row per family word, each row carrying its `FatalCode`
|
||||
projection. The **wire** keeps `FatalCode` unchanged; `table Fatal` appends `family: string`. The
|
||||
projection is a **total table**, not a switch with a `default:` arm — this build has **no `-Werror`
|
||||
anywhere** (verified: `CMakeLists.txt`, every `*.cmake`, both workflows), so a gate premised on
|
||||
`-Wswitch` failing the build is fiction.
|
||||
|
||||
`Session::Fail` stays `[[noreturn]]` in P6 and still ends in `abort()`. Log lines, CI greps and every
|
||||
red-once are byte-identical. What the funnel buys, and what makes it worth `dl`'s two days:
|
||||
|
||||
- **one** place that publishes a `SessionFault{family, detail, seq, op}` frame to the peer **before**
|
||||
dying, so the guest's log names which record killed the session instead of reading a bare EOF;
|
||||
- one telemetry point;
|
||||
- a CI census gate refusing any **new** bare abort outside the funnel.
|
||||
|
||||
> **LANDED, in three steps.** (1) `FatalFamilies.def` + `FatalFamily.h` generate `MGFatalFamily`,
|
||||
> its total projection `FatalCodeForFamily` and `FatalFamilyName`; 32 rows, tested. (2) `SessionFail`
|
||||
> (`FatalFunnel.{h,cpp}`) is the funnel: the ~90 scattered `MGLOG_F("...Fatal{...}"); abort();` pairs
|
||||
> under `MG_Remote/` become one call each, the message string passed **verbatim** so every line and
|
||||
> grep is byte-identical, the family enum riding alongside for the coarse code. `SessionFaultCount()`
|
||||
> is the telemetry point. The census now also requires every `SessionFail`/`WireLogFatal` call's
|
||||
> string to carry a `Fatal{` word, and its baseline drops from 93 bare aborts to 4 (both funnels).
|
||||
> (3) `table Fatal` gains `family: string`; `SessionFail` publishes the frame on whichever session
|
||||
> control plane is active before aborting, and the client's control-reply loop recognizes it, names
|
||||
> the family and latches device-lost. **seq/op are NOT threaded** through the 90 sites — the message
|
||||
> already names the op — so the frame carries `code`, `family` and `message`, not a field always 0.
|
||||
> A `kill -9` bypasses `abort()` and is caught by `dl`'s hangup witness instead; the self-detected
|
||||
> fatal is the one that publishes.
|
||||
|
||||
**The policy flip is not P6's.** Making `Fail` return needs a real return path invented at every site
|
||||
whose callers rely on `[[noreturn]]`, and a6 counted how many do. That is `Ph`'s (§11).
|
||||
|
||||
Two sites carry **no `Fatal{` marker at all** (`Client/PersistentMapTracker.cpp:1052-1058`,
|
||||
`Transport/WireLog.cpp:41-50`), so "the log stays verbatim" is not true of them and the family grep
|
||||
cannot see them. `dl` gives each a family tag.
|
||||
|
||||
### 5.3 The device-lost latch does not exist, and P6 writes it
|
||||
|
||||
`P6-CONTRACT-DRAFT §5.1` says the latch "is implemented unchanged". It is not implemented at all:
|
||||
`FatalCode::DeviceLost`/`ServerCrashed` have zero producers and zero consumers,
|
||||
`glGetGraphicsResetStatus` returns `GL_NO_ERROR` unconditionally
|
||||
(`MG_Impl/GLImpl/Getter/GL_Getter.cpp:2855-2861`), and `MOBILEGL_IPC_RESPAWN` / `IDLE_EXIT_S` are
|
||||
parsed **nowhere** (only a comment at `Config.h:466`). Today a dead server leaves the client
|
||||
silently `DECLINE`-ing every verb forever with `glGetError` clean.
|
||||
|
||||
**It is a package (`dl`), not carriage.** The latch is session-scoped and set from
|
||||
`Doorbell::PeerHungUp()` — D5c amends "`Dead()`" here, and §5.4's block says why — **never from a
|
||||
timeout**. `MOBILEGL_IPC_RESPAWN=1` is a named refusal until some stage implements the re-push.
|
||||
|
||||
**LANDED.** `ClientSession::DeviceLost()` / `LatchDeviceLost()`, armed at the verb barrier, at the
|
||||
forced run-ahead wait and at the control-reply wait, and read by `GetGraphicsResetStatus`, which
|
||||
now answers **`GL_UNKNOWN_CONTEXT_RESET`**. Not `GUILTY` or `INNOCENT`: those assign blame, and the
|
||||
server died for a reason that never crossed the wire — it could as easily have been a driver fault
|
||||
as this client's draw. The consult and its include sit wholly inside `MOBILEGL_BUILD_DISAGGREGATED`;
|
||||
G1 re-measured after it, **0 symbol diff and `.text` unchanged at `0xa52203`**. That measurement
|
||||
also caught an unrelated 16-byte drift the guards did not: P6's O_APPEND log-sink fix was compiled
|
||||
into the pull build too, and is now guarded for the same reason.
|
||||
|
||||
### 5.4 Slow, dead, and frozen are three states
|
||||
|
||||
- A published, never-applied record is **discarded at the latch, not waited on**.
|
||||
- **Slow and dead are distinguished without a timeout threshold.** A server one frame behind is
|
||||
P5e's intended steady state; a server that is gone is `Fatal{ServerCrashed}`. `SocketDoorbell`
|
||||
already latches `m_dead` on EOF — take the fact from there.
|
||||
- **A frozen peer produces no hangup**, and a6 found the tree has no defence: the control-reply wait
|
||||
`m_controlDone.wait(lock, pred)` (`Server/ServerLoop.cpp:724`) has **no deadline**, while the ring
|
||||
side has `kBarrierTimeoutMs`. A SIGSTOP'd or freezer'd server wedges the GL thread inside
|
||||
`eglMakeCurrent` with no diagnostic.
|
||||
|
||||
**D5 ruling — two waits, two answers.** (a) The server's *in-process* mailbox wait gains a bound and
|
||||
its expiry is `Fatal{ControlReplyTimeout, "<op>@<seq>"}`. (b) The client's *cross-process* reply wait
|
||||
is bounded by `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` (default 5000) and its expiry is **not** fatal: the
|
||||
client consults the doorbell's death latch — dead latches device-lost, alive-but-silent is a named
|
||||
diagnostic. Packages: `cp` (a), `dl` (b). (P7, p7/spawnhang: "silent" is now literal — a server whose
|
||||
apply thread is running the op says so every 250 ms with `SurfaceProgress`, and each report restarts
|
||||
the client's bound, up to 120 s; a server that never took the op, or is frozen, sends none.)
|
||||
|
||||
> **D5c — the client's self-bell CANNOT witness the server's death, and `sm` is why.** Measured, not
|
||||
> predicted: `minecraft-1.21.4-fabric-iris-iterationrp-in-world` kills the *server* process outright
|
||||
> on this machine (llvmpipe: `LLVM ERROR: Cannot select: intrinsic %llvm.x86.vcvtps2ph.256`, SIGABRT
|
||||
> at call 159429), and the client did **not** take the `SessionWait::ShutDown` path that already
|
||||
> exists and is already correct. It sat out the full 120 s and reported
|
||||
> `Fatal{BarrierTimeout, "ResourceRespecify"}` — the wrong diagnosis, at 120 s a case, for a server
|
||||
> that had been gone since the first second. Under monolith the same trace fails in 3 s.
|
||||
>
|
||||
> The cause is structural and is the price of the two-fd bell: the client parks on `clientBell[0]`
|
||||
> (slot 5) and rings *itself* through `clientBell[1]` (slot 6), so it holds **both ends** of that
|
||||
> socketpair. EOF arrives only when every writer closes, and the client is one of them — so that
|
||||
> descriptor can never hang up, no matter what happens to the peer. A bell that can wake itself is
|
||||
> a bell that cannot hear a death.
|
||||
>
|
||||
> **`dl` therefore needs a witness descriptor whose far end only the SERVER holds**, polled beside
|
||||
> the park fd and never read from, whose `POLLHUP`/EOF latches `Dead()`. The control socket is
|
||||
> already exactly that and needs no new descriptor. The fix does not belong to `sm`: holding both
|
||||
> ends is required for self-notification and is not the defect — assuming one descriptor could
|
||||
> answer both questions is.
|
||||
>
|
||||
> **LANDED.** `SocketDoorbell::SetDeathWitness(fd)`; the client hands it
|
||||
> `SocketTransport::StreamFd()`. The witness enters the poll set with `events = POLLRDHUP` **and
|
||||
> nothing else**, which is what lets it watch a socket it must never read: `POLLHUP`/`POLLERR`/
|
||||
> `POLLNVAL` arrive in `revents` unrequested, so the hangup is seen while `POLLIN` never is — a
|
||||
> control reply queued on that descriptor can neither wake the bell nor be consumed from under
|
||||
> `SocketTransport`'s reassembler. Measured: `kill -9` on the server is noticed in **0.01 s**
|
||||
> against a 120 000 ms barrier, and with the witness removed the same test takes the full 2 s
|
||||
> retry budget and fails naming this paragraph.
|
||||
>
|
||||
> **`Dead()` AND `PeerHungUp()` ARE TWO FACTS, and only the second may arm the latch.** `Park`
|
||||
> sets `m_dead` for `POLLERR`, `POLLNVAL` and unrecognised `revents` as well — each a fault in
|
||||
> *this* process's descriptor — and `CondVarDoorbell::Kill()` sets it on every orderly `Stop()`.
|
||||
> A latch taken from `Dead()` would report a lost GPU for a local fd bug and a context reset for
|
||||
> every clean exit. `ADeadBellIsNotAlwaysAHungUpPeer` is the red-once for the distinction;
|
||||
> `AnOrderlyStopIsNotADeviceLoss` is **not** — it stays green under the collapse, because under
|
||||
> spawn the self bell is a `SocketDoorbell` and `Stop()` never kills one. That was found by
|
||||
> running the falsification rather than by reading the code.
|
||||
|
||||
---
|
||||
|
||||
## §6 The control plane (`cp`)
|
||||
|
||||
### 6.1 What `fc` landed, and what is left
|
||||
|
||||
P5f package `fc` framed the twelve `Server*` forwarders as value-only `SurfaceControlFrame`s and
|
||||
landed the wire codec. The draft concluded `cp` is "transport only". **Three things are not
|
||||
transport**, and a6 found all three:
|
||||
|
||||
1. The `InitCapabilities` **request** has no wire form (§2).
|
||||
2. `SurfaceReply` cannot carry a `MobileGLResult` (§2).
|
||||
3. **Neither side of the control plane is wired to a socket.** The apply thread's `ready` predicate
|
||||
has exactly three terms — stop, `ControlIsPending()`, `cmdHead != LocalTail()` — and a socket is
|
||||
not among them (`ServerLoop.cpp:398-402`), so a `SurfaceOp` arriving on the wire is read by
|
||||
**nobody**. The client's pump consumes only `CapsSnapshot` and drops everything else with a WARN
|
||||
(`ClientSession.cpp:1425-1432`).
|
||||
|
||||
### 6.2 D4 — who reads the control socket
|
||||
|
||||
A dedicated **`mgl-srv-io` thread**: `ReceiveFrame` → `DecodeWireSurfaceOp` → post into the
|
||||
**existing** one-slot mailbox as an ordinary non-apply-thread poster. `ServerLoop.cpp:673-727` is not
|
||||
changed. The io thread is additionally the server's **only** sender on the control transport:
|
||||
`PublishCapsSnapshot` enqueues and the io thread drains one outbound FIFO — which is what fixes the
|
||||
ordering hazard that `CapsSnapshot` is pushed from **inside the dispatch, on the apply thread,
|
||||
mid-op** (`ServerLoop.cpp:959-969`).
|
||||
|
||||
Rejected: adding a fourth term to the apply thread's `ready` predicate. That puts frame decode on the
|
||||
thread holding the native context and makes every control arrival a wake of the applier.
|
||||
|
||||
### 6.3 D7 — `seq`
|
||||
|
||||
The **client** mints, always, dense from 1, never reused in a session. `seq == 0` arriving on the
|
||||
wire is `Fatal{ProtocolCorruption, "SurfaceOp.seq"}` at the **decoder**, not at
|
||||
`RunSurfaceControlFrame`, whose local mint (`ServerLoop.cpp:659-662`) stays and becomes reachable only
|
||||
for a local, non-wire post. Correlation lives in the client's control demultiplexer;
|
||||
`DecodeWireSurfaceReply` keeps its void signature and stops being the place correlation was expected.
|
||||
|
||||
### 6.4 `ForgetCurrentTuple`
|
||||
|
||||
The N-3 rule holds identically on the frame path: the forgetting calls are inside the dispatch
|
||||
(`ApplySurfaceControlFrame`) and `ServerLoopTest`'s C7/N-3 controls drive them through framed posts
|
||||
unchanged. No change.
|
||||
|
||||
---
|
||||
|
||||
## §7 Windows — rule H's enforcement (`cp`)
|
||||
|
||||
### 7.1 The blacklist is open, and the end state's first guest walks through it
|
||||
|
||||
`WindowBackendForWireWindowKind` (`Protocol/SurfaceOpCodec.cpp:104-113`) **accepts**
|
||||
`WindowKind::X11 → WindowBackend::X11` and `Win32Hwnd → Win32`. `DecodeWireSurfaceOp` (`:141-179`)
|
||||
refuses only `AndroidNativeWindow` and `MetalLayer`. An accepted kind's `nativeToken` is copied
|
||||
through and `UnpackWindowHandle` (`Server/ServerLoop.cpp:875-882`) casts it to `void*`
|
||||
**unconditionally**, whence it reaches `eglCreateWindowSurface` in the server process.
|
||||
`DetectWindowBackend()` is a compile-time `#if` chain (`MG_Impl/EGLImpl/EGLImpl.cpp:59-70`), so a
|
||||
guest cannot avoid announcing its own window system.
|
||||
|
||||
### 7.2 D8 — whitelist, and in P6 the accept set for a window op is empty
|
||||
|
||||
- `X11` → `Fatal{UnmigratedSurface, "X11@P12"}`; `Win32Hwnd` → `"Win32Hwnd@P12"`.
|
||||
- `None` on a **window** op → `Fatal{ProtocolCorruption, "SurfaceOp.windowKind"}`. Today it is
|
||||
accepted, maps to `WindowBackend::Unknown`, and the raw token is copied through anyway.
|
||||
- Separately and independently: `nativeToken != 0` on **any** op →
|
||||
`Fatal{ProtocolCorruption, "SurfaceOp.nativeToken"}`.
|
||||
- `Pbuffer` / `Surfaceless` are surface **shapes**, not window backends, and remain the only things
|
||||
P6 lands.
|
||||
|
||||
**Do not append `WindowKind::ServerOwned` in P6.** The schema is append-only, so an enumerator
|
||||
appended now and re-meant later is a wire value burned; P6 has no consumer for it. P12 appends it.
|
||||
|
||||
### 7.3 The client emits unconditionally, and P6 must decide what that costs
|
||||
|
||||
`Client/BackendObject_Remote.cpp:214-216` posts `SetWindowHandle` with no split-arm check, so on a
|
||||
real Android app the **first `eglCreateWindowSurface` aborts the server process** and the client sees
|
||||
only a dead peer. `cp` mirrors the refusal on the **client** side, so the diagnostic names the cause
|
||||
in the process that caused it. This is P6's, not P12's, because P6's device lanes run on a phone.
|
||||
|
||||
### 7.4 `m_windowHandle` has four stores, not one
|
||||
|
||||
a6's precise shape, correcting the corpus (and `P6-ENDSTATE-REVIEW §2.5`): four stores
|
||||
(`MG_Backend/BackendObject.cpp:475`, `:365`, `:206`, `:207`), five reads, and `:283` is a **call**,
|
||||
not a store. `.Width`/`.Height` have **no readers**. The smallest latch point for a server-owned
|
||||
window is `RegisterEGLWindowSurface` (`:232-238`), and the non-obvious fourth edit is the
|
||||
`sameHandle` dedup key (`BackendObject_DirectGLES.cpp:962-967`), which under server ownership never
|
||||
matches and would rebuild the native context on every `eglCreateWindowSurface`. Recorded here for
|
||||
P12; P6 changes none of it.
|
||||
|
||||
---
|
||||
|
||||
## §8 The data plane (`lk` implements, `c6` declares)
|
||||
|
||||
### 8.1 Why a seam at all
|
||||
|
||||
`ITransport.h:16-20` says in its own header that the hot path bypasses it: it is the **control**
|
||||
plane, and that scope is already correct for a socket. Below it there is **no interface of any kind** —
|
||||
`Ring.h`, `ReplySlot.h` and `EventRing.h` take raw `void* base + capacity`. So "swap the transport"
|
||||
has nothing to swap.
|
||||
|
||||
`c6` declares `Transport/ILink.h` and `Transport/StreamLink.h`. `lk` implements `ShmLink` by moving
|
||||
today's `SessionSegments` / `Ring` / `ReplySlotPool` / `EventRing` behind the interface, line for
|
||||
line, with **zero behaviour change**.
|
||||
|
||||
### 8.2 The seam is a setup-time interface, not a hot-path one
|
||||
|
||||
`ILink` hands out POD handles **once at attach** and then stays out of the way. No `ILink` virtual is
|
||||
called per record, per publish, or inside any spin predicate. Three grounds, and the second is
|
||||
decisive:
|
||||
|
||||
1. Two virtuals per record at the 4000 records/frame that `SEG_CMD` is sized against is ~8000
|
||||
indirect calls/frame — about 0.5% of the measured 15.72 ms client frame. Affordable, but the only
|
||||
lane that could measure it has ±4% run-to-run dispersion, **eight times the effect**, so the gate
|
||||
`P6-ENDSTATE-REVIEW` demands could only ever print "within noise" — a green that proves nothing.
|
||||
2. **The spin predicate forbids it outright.** `Doorbell::Wait` is a non-virtual template whose spin
|
||||
loop calls `ready()` up to `SpinItersPerUs() * spinUs` times; at 916 waits/frame with 99.8%
|
||||
resolved inside the spin, the predicate runs 10⁵–10⁶ times per frame and reads the watermarks
|
||||
straight off the page. That is the 31.58%-of-the-GL-thread path. A virtual there is not a rounding
|
||||
question.
|
||||
3. The encode path already depends on four inline accessors around its single `Reserve`, and
|
||||
virtualising `Reserve` alone would force either four more virtuals or moving R-10's wrap counters
|
||||
behind the seam — and those are deliberately encoder-owned.
|
||||
|
||||
**The gate is therefore an `objdump` call-count of the encode path, not a wall clock**: after `lk` the
|
||||
path must have no more out-of-line calls than today. That is falsifiable; "within noise" is not.
|
||||
|
||||
### 8.3 `RingControl`'s regroup is `lk`'s, and it is a wire break the day a second build exists
|
||||
|
||||
The watermark cache line is `{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial,
|
||||
presentAckSerial}`; `eventRingFull`/`eventDropped` sit in a **separate** `alignas(64)` group with
|
||||
`serverEpoch`/`ringGeneration`/the two park flags. And `submittedSeq` is **producer-written**
|
||||
(`Ring.h`'s "THE FIVE WATERMARKS" block) while the other four are consumer-written — so the group has **mixed writers**, and any
|
||||
"single-writer progress block aliased over the watermark line" is unsound.
|
||||
|
||||
`lk` therefore **regroups**: `submittedSeq` moves to the producer's own line, beside `cmdHead`,
|
||||
which is the other thing the producer writes; what is left — the four consumer-written watermarks —
|
||||
becomes a named `LinkProgress` member with per-field `offsetof` static_asserts.
|
||||
|
||||
> **Correction, found while `lk` was measuring its own blast radius.** An earlier revision of this
|
||||
> section, and of `ILink.h`, also promoted `eventRingFull`/`eventDropped` into the watermark group
|
||||
> and called the result single-writer. **Both were wrong.** `eventRingFull` has two writers: the
|
||||
> server latches it with `store(1)` when `SEG_EVENT` fills (`Transport/EventRing.h:140`) and the
|
||||
> **client clears it with an `exchange(0)` on every drain** (`:200`). Promoting it would put a
|
||||
> per-drain RMW by the client on the same cache line as `appliedSeq` — which the server
|
||||
> release-stores every 64 records and the client's spin predicate reads 10⁵–10⁶ times per frame.
|
||||
> That is new false sharing on the hottest line in the system, and it would land in exactly the
|
||||
> numbers this package's gate has to hold constant. The two flags stay in the doorbell/generation
|
||||
> group, where mixed writers already live beside `consumerParked`/`producerParked`, and cross the
|
||||
> seam through a separate `EventFlags()` accessor. *Accessor*, not line: they share
|
||||
> `serverEpoch`'s line, which is where mixed writers already live.
|
||||
|
||||
It is free **today** — both peers are the same binary, enforced by the fingerprint — and a wire break
|
||||
the day a second build exists. It is **`lk`'s and not `c6`'s** because the rename blast radius is
|
||||
**109 sites in production alone**, not the 39 an earlier draft assumed, and because five of them sit
|
||||
behind `#if defined(MGITEST_SPLIT_RUNTIME_PEEK) && !defined(__ANDROID__)` — an Android configure
|
||||
compiles them out, so a missed rename is green on device and red only on a desktop split-peek build.
|
||||
`lk` publishes the per-file itemisation before it starts.
|
||||
|
||||
### 8.4 What `StreamLink` is for
|
||||
|
||||
Every method is a named refusal, `Fatal{UnmigratedDataPlane, "StreamLink@P6.5"}`. It exists so the
|
||||
seam's adequacy is a **compile**, not an argument: if `ILink` cannot name what a byte stream needs,
|
||||
`StreamLink` cannot be declared against it. Writing it is what revealed that the seam needs
|
||||
`FlushProgress()`, `ResolveSpan()` and a reply pair — three methods the brief's own list
|
||||
(progress transmission, send window, flush) did not name.
|
||||
|
||||
`c6`'s red-once: delete one `ILink` method `StreamLink` declares and the build goes red.
|
||||
|
||||
### 8.5 The purity gate (`lk`)
|
||||
|
||||
After `lk`, the identifiers `RingControl`, `RingProducer`, `RingConsumer`, `ReplySlotPool` and
|
||||
`EventRingConsumer`, and any `m_shm.` expression, appear **only** inside `Transport/ShmLink.*` and
|
||||
the transport's own tests. Same shape as the existing A/B/C purity gates. The baseline is recomputed
|
||||
mechanically at `lk`'s head and published per file — the earlier draft's ratchet number was wrong by
|
||||
a factor of three, and a ratchet installed at the wrong number is red on day one for reasons that
|
||||
have nothing to do with a leak.
|
||||
|
||||
### 8.6 `SEG_STAGE`'s cursors are deliberately dead
|
||||
|
||||
`Ring.h`'s "DEAD IN P5, DELIBERATELY" states it and a test pins it: `SEG_STAGE` is **not a ring**; staging is an
|
||||
encoder-local linear allocator reclaiming on `retiredSeq`, and all three stage cursors stay zero for
|
||||
the whole of P5. A stream link's send window either revives them **completely** or does not use them;
|
||||
the header names the middle state as *a guaranteed hang rather than a slow path*. P6.5's, recorded
|
||||
here so it is not rediscovered.
|
||||
|
||||
---
|
||||
|
||||
## §9 The gate
|
||||
|
||||
Beyond the five-part gate (`docs/Disaggregated/ARCHITECTURE.md` §13.2):
|
||||
|
||||
1. **G1** — pull build 0/0/0/0, `.text` unchanged. Every predicate in §3.2 folds away.
|
||||
2. **G2/G14** — `integration-spawn`'s case-name set identical to `integration-split`'s.
|
||||
3. `integration-spawn` green at the same count as `integration-split`.
|
||||
4. **Process tree, mechanical** — exactly one extra child while running, zero after; `HeadlessGL`'s
|
||||
fork pre-check leaves no orphan.
|
||||
5. **Arm proof (ID-124)** — every spawn entry records the child pid and `transport=spawn` in its own
|
||||
private log. A spawn lane that fell back to monolith passes items 1–4. §1 is why this gate is not
|
||||
optional: today the fallback and the intended state are **the same value**.
|
||||
6. **Red-once per package (R-16)**, one named pair each.
|
||||
7. **Device** — Redmi `2f7cbe2e`, reboot-clean, paired A/B in one thermal window.
|
||||
8. **Performance recorded, not gated**, but three numbers are mandatory: the socket doorbell's cost
|
||||
against `inproc`'s condvar; `SEG_STAGE` bytes/frame; and records/frame **post-chunking**. The last
|
||||
two are takeable on the existing inproc lane today and `MEASUREMENTS.md:342` says in as many words
|
||||
that no post-chunking distribution exists.
|
||||
|
||||
> Item 8's wording must be able to **accept a negative regression without looking suspicious**: today
|
||||
> the client spins ~1600 times/frame on a shared cache line with 99.8% of waits resolved there, and a
|
||||
> socket link has no such line. Converting that into ~4 blocking reads/frame may be **cheaper**.
|
||||
|
||||
Negative controls, each run red once:
|
||||
|
||||
| # | knob | what it must falsify | package |
|
||||
|---|---|---|---|
|
||||
| S1 | `MOBILEGL_IPC_SERVER_PATH=/nonexistent` | named refusal, not a silent monolith fallback | `sm` |
|
||||
| S2 | `kill -9` the server mid-frame | the latch; the client never hangs and never keeps submitting. Red-once the **wrong implementation** too: set the latch from a timeout and the paired control "a server one frame behind is the normal state" must go red | `dl` |
|
||||
| S3 | child envp not scrubbed | `Dial = No` catches it **and the diagnostic names which of the two catches fired** | `sm` |
|
||||
| S4 | inherited P5f epoch invalidation disabled | a replaced context inherits stale synced state; a still-live XFB lifetime retains its paused span | `st` |
|
||||
| S5 | `WindowKind::X11` with a plausible XID reaches the server | named Fatal, **and the gate falsifies that the token reached `UnpackWindowHandle`**. Today's S5 tests only `AndroidNativeWindow` and stays green with §7.1's hole open | `cp` |
|
||||
| S6 | a peer offers `LinkTerms.dataPlane = StreamOnly` | refused by name, **and the server survives to serve the next connection** | `hs` |
|
||||
| S7 | a bare `abort()` added outside `Session::Fail` | the census gate goes red | `dl` |
|
||||
| S8 | — | `integration-spawn` asserts `SessionFaultCount() == 0` over a whole run: the **good-path** assertion E1/ID-122 wanted and could not express, and which S5/S6 can falsify | `t6` |
|
||||
|
||||
---
|
||||
|
||||
## §10 Amendments
|
||||
|
||||
### 10.1 To `P6-CONTRACT-DRAFT.md`, which this file supersedes
|
||||
|
||||
1. §1's "ABI fingerprint | Unchanged … same-machine, same-binary" → §4.
|
||||
2. §5.1's "The latch … is implemented unchanged" → **false**; §5.3.
|
||||
3. §4's "P6's remainder is TRANSPORT ONLY" → **false in three ways**; §6.1.
|
||||
4. §7's "a transport swap and nothing else" → **no longer true**. This file adds `lk`, `hs` and `dl`
|
||||
to the package list and says so here rather than smuggling it.
|
||||
|
||||
### 10.2 To the earlier contracts
|
||||
|
||||
1. `CONTRACT-P5C.md:537-539` — EGL value framing was discharged by P5f `fc`, the unpack/render-state
|
||||
epoch work by P5f `fs`. P6 carries the existing frames over the socket and verifies the inherited
|
||||
lifecycle rules in the second process.
|
||||
2. `CONTRACT-P5C.md:56` — P6's multi-context shape. **Declined**: P6 stays single-context. §11.
|
||||
3. `Transport/ReplySlot.h`'s "P6 debt" for the 2 MiB reply cap and chunked readback. **Declined**:
|
||||
P9's. Note `P6-SPAWN-PLAN §8.5` already reassigned it and the header was never updated — two
|
||||
documents in the tree disagreed; this file settles it.
|
||||
4. `Ring.h`'s "BATCHING MAY ONLY MAKE A WATERMARK LATE" — `appliedSeq` is excluded from lazy publication **by name**. P6 does not change
|
||||
the behaviour; it records that any future stream link **amends** this rule rather than inferring
|
||||
around it. Several designs have argued the late-never-early invariant licenses lazy `appliedSeq`;
|
||||
it explicitly carves `appliedSeq` out.
|
||||
5. `ARCHITECTURE.md:488` — the child-forces-monolith rule. **Amended**; §3.1.
|
||||
6. `ARCHITECTURE.md:9` and `MG_Backend/MGPipe/PipeInputs.h:281`, `:744`, `:957`, and
|
||||
`P5F-WIRE-COMPLETENESS §1.4` — "the server does not link `MG_Impl`". **False at this head.** §12.
|
||||
|
||||
### 10.3 Amendments of 2026-09-22 — the end state re-ruled
|
||||
|
||||
1. **The end state is TCP across machines, OSes and architectures**, not an AVF pVM over
|
||||
`AF_VSOCK`. `docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md` carries the retraction at its head; its
|
||||
§3 gains form **D**. Same-machine `spawn` (AF_UNIX + shared segments) stays as the local form.
|
||||
2. **The transport stack has two independent axes.** The control plane (`ITransport`: `fork`
|
||||
inherited fds / `unix:<path>` / `tcp://host:port`) and the data plane (`ILink`: `ShmLink` /
|
||||
`StreamLink`) are chosen separately, negotiated in the handshake, and may be mixed — on one
|
||||
machine, control over TCP with data over shared segments is a legal pair. Callers above
|
||||
`Transport/` see `ITransport` + `ILink` and nothing else; branching on the link kind above that
|
||||
line is a purity-gate failure (the `lk` grep gate, widened). Design: `ARCHITECTURE.md` §11.9.
|
||||
3. **§8.2's "`ITransport` is not extended" is amended in one respect**: descriptor passing leaves
|
||||
the control plane and becomes the shared-segment data plane's *delivery*. `ShmLink` brings its
|
||||
own AF_UNIX aux rendezvous, named in `Welcome`, and `SCM_RIGHTS` travels on that and nowhere
|
||||
else. Rule G is unchanged: a transport that declares no descriptor passing (TCP) passes none,
|
||||
and the data plane that needs it must bring its own channel or not be selected. `auto`
|
||||
selection is decided by a **successful delivery**, never by inspecting addresses, and a
|
||||
fall-back to `Stream` is named in the log and the stats line.
|
||||
4. **§2's three `hs` rows** (`LinkTerms`, `Refuse`, the fingerprint split) did not land in `hs`;
|
||||
`protocol.fbs` carries neither `LinkTerms` nor a `Refuse` member of `CtrlMsg`. They are P6.5's
|
||||
(`nd`, `wf`), and S6 in §9 moves with them. `CURRENT_STAGE_PROGRESS.md`'s earlier claim that
|
||||
S6 had been run red is withdrawn.
|
||||
5. **Fixed-width wire form is no longer negotiable-later.** Two different binaries from two
|
||||
compilers are the end state, so the `PipeFields.def`-derived layout digest, the fixed-width
|
||||
rewrite of `DynamicBackendParameters` / `MGPCaps` / the `RenderStateParameters` blob, and the
|
||||
`buildFingerprint`-only-under-`Dial == Fork` policy are P6.5 `wf`, taken off P7.
|
||||
6. **`Ph` gains pairing/authentication**: a `Hello` without a pairing token is refused on any
|
||||
non-loopback listener. `Ph` precedes P12's non-loopback listen.
|
||||
7. **"The 511 class-C entries" in §11** is the P5-joint census
|
||||
(`docs/Disaggregated/notes/p6/census-classC.md`). At this head the `MGR_UNMIGRATED_*` lists are
|
||||
empty; `SetSwapInterval` (P10) and `DeleteTransformFeedback` (P9) are the two that remain.
|
||||
(2026-09-29: both are gone. P10 made the `SetSwapInterval` slot the server forwarder, so class C
|
||||
is empty - `CONTRACT-P10.md` §2.)
|
||||
|
||||
---
|
||||
|
||||
## §11 Not P6
|
||||
|
||||
Real window arrival and the `android:process=":mgl"` Service (P12); multi-context (P12); chunked
|
||||
readback and the reply-slot pool (P9); `DynamicBackendParameters`' fixed-width rewrite (landed by P6.5 wf);
|
||||
`MOBILEGL_IPC_POLL_ESCALATE` (P10); remaining monolith-only frontend-object / twin-registry glue
|
||||
(P3b/P4b) and the named P7 functionality debts; Windows `pipe:` / `unix:`; the 511 class-C entries.
|
||||
|
||||
**The two-axis transport stack — control × data, TCP-capable, cross-build — is P6.5** and is the
|
||||
IPC track's next stage (§10.3). P9 builds its reply semantics on P6.5's message replies rather than
|
||||
on a slot pool, and P11's T0/T1 are refused by name on a stream data plane (landed by P11 A1 at
|
||||
the handshake: `Transport/AdoptTier.cpp`, `SettleAdoptTierAtHandshake`; CONTRACT-P11 §1).
|
||||
**Untrusted-guest hardening is `Ph`**, and it must land before any shipping server app accepts a
|
||||
connection from a guest it did not produce — including the `Fatal`-policy flip (§5.2), the
|
||||
handle-slot budget, and the caps in §12.
|
||||
|
||||
E1's re-specification (ID-122) is **not** discharged by P6 and does not become P6's because P6 starts.
|
||||
|
||||
---
|
||||
|
||||
## §12 Recorded debt
|
||||
|
||||
### 12.1 The server image links `MG_Impl` in P6 (D12)
|
||||
|
||||
**P6's server image is the whole `libMobileGL.so`** — which is what `ARCHITECTURE.md:496`'s "one
|
||||
shared library, two roles" always meant. The assertions that the server does not link `MG_Impl` are
|
||||
false and become debt with named owners.
|
||||
|
||||
Measured: **184 symbols**, 177 of them real function calls, that the server set needs and only
|
||||
`MG_Impl` / `MG_State` / `MG_Remote/Client` define. Owners: **P7 101**, **P3b/P4b 14**, both backends
|
||||
**60**, **P6's own 6**. There is additionally **no module target to link** — `SOURCE_FILES` is one
|
||||
flat list feeding both library targets — so establishing module boundaries is itself named work,
|
||||
owed to P13. Method and full symbol list:
|
||||
[`a6-link-experiment.md`](../notes/p6/a6-link-experiment.md).
|
||||
|
||||
**P6's own six** are not P7's and are `sm`/`lk`'s to clear: `Server/PipeApplier.cpp` calls
|
||||
`Client::ClientSession::NoteApplyThreadEnteredApplier()` / `NoteApplyThreadLeftApplier()`;
|
||||
`Wire/PipeWireCodec.cpp` needs `MG_State::GLState::DecodeProgramArchive` and
|
||||
`Client::AdoptTierIsEmulate()`; `MG_Pipe/PipeApply.cpp` needs `Client::ClientSession::Active()` and
|
||||
`AdoptTierIsEmulate()`.
|
||||
|
||||
**Gate (landed, P7 wave 0 — ID-P7-7)**: `scripts/link_ratchet.py` recomputes this set from an
|
||||
explicit per-object partition on every `build-linux-split` run and fails on any symbol absent from
|
||||
`scripts/data/link_ratchet_baseline.txt` — which stands at **186** on `e8b2c4bd`, the 184 plus
|
||||
P6.5's `Client::ClientSession::StartSpawned()` and `MG_State::GLState::ProgramArtifactsSchemaFingerprint()`,
|
||||
with the three backend buckets reproducing a6's 101 / 14 / 60 exactly — while a symbol that
|
||||
disappears is reported rather than failed, so progress never turns the build red
|
||||
([`link-ratchet.md`](../notes/p7/link-ratchet.md)).
|
||||
|
||||
### 12.2 D11 — five unbounded peer-driven allocations, capped, with no `try`/`catch`
|
||||
|
||||
Every peer-declared count is bounded **before** it reaches an allocator and the bound is a named
|
||||
refusal. `MG_Remote` gets no `try`/`catch`: a cap that is checked is better than an exception that is
|
||||
caught, and the funnel (§5.2) is where the diagnostic lives.
|
||||
|
||||
| site | today | package |
|
||||
|---|---|---|
|
||||
| `MG_Pipe/PipeApply.cpp:1563-1569` | resizes to a peer-supplied `Cso.Slot + 1` with **no slot limit**; every sibling create-family goes through `RecordAt`'s bounded path. `Slot = 0xFFFFFFFE` asks for ~1.7 TB | `dl` |
|
||||
| `Server/PipeApplier.cpp:416-441` | readback scratch resized from `Box.W * Box.H * bpp` **before** any size gate. 65535² RGBA8 asks for ~17 GB | `dl` |
|
||||
| `MG_State/.../ProgramArtifactsCodec.cpp:166-175`, `:216-223` | count bounded by remaining **bytes**, then multiplied by `sizeof(element)` | `dl` |
|
||||
| `Server/StagedTextureStore.h` | resize not bounded against the level's declared extent | `Ph` |
|
||||
| `SEG_EVENT` overflow | a guest that merely stops draining kills the host renderer in 60 s | `Ph` |
|
||||
|
||||
### 12.3 One guest per server process (D9)
|
||||
|
||||
For the whole of P6, one server process serves exactly one client session, and it is a **structural
|
||||
fact, not a preference**: `g_Display`/`g_Context`/`g_Surface`/`g_Config` are per-process file-statics
|
||||
and `InitDisplayAndContext` opens with an unconditional `DestroyEGLContext()` whose last act is
|
||||
`eglTerminate` — a **process-wide** EGL teardown. `ServerSession::Accept`'s same-object arm
|
||||
(`ServerSession.cpp:425`) returns `MOBILEGL_ERR_INVALID_ARGUMENT` **with no log line at all**; `sm`
|
||||
gives it one. Under `Dial == Fork` a second guest is unreachable by construction, so the invariant
|
||||
becomes falsifiable only under `Dial == Connect`, which P6 does not land.
|
||||
|
||||
### 12.4 Settled by ruling rather than by experiment
|
||||
|
||||
- **Who runs the applier in the server process**: `ServerMain` **must** drive it through
|
||||
`ServerLoop::Start()`, so `g_applyThreadKey` is published and the dozen `OnApplyThread()` guards
|
||||
keep meaning what they mean. Owner `sm`; red-once drives the applier without `Start()`.
|
||||
- **The server's exit primitive**: `_exit`, after an explicit log flush. It decides whether
|
||||
`g_processTeardown` is set and therefore whether seven twin destructors take their early-out.
|
||||
Owner `sm`.
|
||||
- **The handshake asserts the resolved subsystem mask**; a mismatch is `Refuse`, not `Fatal`. The
|
||||
envp scrub list does **not** include `MOBILEGL_PIPE_*`, so the two processes can resolve different
|
||||
masks from their own environments. Owner `hs`.
|
||||
- **P6 maps no segment read-only.** `ShmSegment.h:62-63`'s stated design — the peer gets
|
||||
`SEG_CMD`/`SEG_STAGE` read-only on the server side — **cannot be implemented as written**, and the
|
||||
comment is corrected rather than left to mislead. Owner `lk`.
|
||||
- **`DualBlockScenario`'s `serverIdentity != nullptr`** is an in-process fact, and G2/G14 requires
|
||||
the spawn lane to keep the case name. Its assertions are **re-homed**, not weakened: `DualBlockPeek`
|
||||
becomes a control-plane query answered by the server process. The declared fallback — assert
|
||||
`MGPipeBlocksAreDistinct()` on the client side and carry the identity half by §3.4's server-side
|
||||
Fatal — must be **declared here** if taken, not discovered in review. Owner `t6`.
|
||||
@@ -0,0 +1,60 @@
|
||||
# P6.5 第一波:TCP 控制与 stream 数据
|
||||
|
||||
2026-09-22。依据 `docs/Disaggregated/ROADMAP.md` 的「立即的下一项」。本文件记录实现所需的协议细节;验收结果单独记入阶段报告,不能用本文件代替运行证据。
|
||||
|
||||
## 范围与配置
|
||||
|
||||
本波实现 POSIX client/server 的全 TCP 连接;主验收形态是 WSL x86_64 client 与 Redmi aarch64 server。保留 inproc 与 fork/UNIX 的共享段路径。Windows client、TCP 控制加共享段、压缩、内容去重、异步 reply、真实窗口均不在本波。
|
||||
|
||||
`MOBILEGL_TRANSPORT=spawn` 表示两进程拓扑。`MOBILEGL_IPC_CONTROL` 选择 `fork`(默认)、`unix:<path>` 或 `tcp://host:port`,`MOBILEGL_IPC_DATA=auto|shm|stream` 选择数据面。本波 TCP 只接受 stream,fork/UNIX 只接受 shm;不支持的显式组合具名拒绝。Connect 不启动本地子进程,实际 server pid 来自 Welcome。
|
||||
|
||||
TCP 两条连接依次为控制和数据。每条均设置 NODELAY、KEEPALIVE,Linux/Android 另设置 KEEPIDLE=2、KEEPINTVL=1、KEEPCNT=3、USER_TIMEOUT=5000。网络错误和 EOF 才触发 peer-hung-up;apply 超时不是 device loss。无 `MOBILEGL_IPC_TOKEN` 的 listener 仅能绑定 loopback;错令牌返回 Refuse,不打印令牌值。(P7 包 f2-auth,PH-7 (5))`--serve` 的 TCP supervisor 在 fork 之前自己读每条连接的首帧、先验令牌:未认证对端只得到 `Refuse{Authentication}`,不带 wire 指纹与 build stamp,也不花一次 fork;首帧须在 `MOBILEGL_IPC_PREAUTH_MS`(默认 2000)内到齐;同时待认证的连接至多 `MOBILEGL_IPC_PREAUTH_MAX`(默认 8,上限 256),满时新连接顶掉占位最多的地址最老的一条(`Refuse{Busy}`),自己地址已占最多时才被拒;同一地址失败(错令牌、畸形首帧、超时、静默占位超过 250 ms)达 `MOBILEGL_IPC_AUTH_BACKOFF_AFTER`(默认 5,0 关)次后在 accept 处拒绝,窗口自 `MOBILEGL_IPC_AUTH_BACKOFF_MS`(默认 1000)起逐次翻倍、上限 60 s,一次认证成功清零。这四个旋钮只读环境变量(supervisor 不跑 ConfigLoader)。细节 `MG_Remote/Server/PreAuthGate.h`,记录债见 CONTRACT-P7 §12。
|
||||
|
||||
## 握手与布局
|
||||
|
||||
Hello/Welcome 追加 `wireFingerprint`、`LinkTerms`,Hello 带 dial 与 token,Welcome 带实际 backend。原 union 标签不重排;Refuse 与 LogFlush 追加。Refuse 是正常的拒绝结果,不能改成 ABI mismatch abort。
|
||||
|
||||
wire 指纹覆盖 PipeFields 逐成员名称/offset/size、按序 opcode/payload/flags/WaitClass、记录头、句柄、render-state 字段表、blob codec 版本、协议版本、字节序、指针宽度与 caps/dynamic-parameters 布局。build stamp 单独比较:Fork 必须相同,Connect 不同只警告,`MOBILEGL_IPC_REQUIRE_SAME_BUILD=1` 则拒绝。窗口尺寸由 server 陈述,不进入布局指纹。DynamicBackendParameters 使用定宽整数;没有借机改 GLFunctionsTable ABI。
|
||||
|
||||
启动必须收到并采用真实的首个 CapsSnapshot,之后才能锁存 run-ahead 并宣布 session started。
|
||||
后台控制 reader 尚未把 caps 放入 inbox,不能解释成 server 不支持能力;也不能先用 placeholder
|
||||
永久锁成 lockstep,再期待后来的快照自动提升。后续快照仍只允许 demotion,缺少首 caps 是有界启动失败。
|
||||
|
||||
program archive 在 split 构建使用 v2,头中校验真实序列化 schema 摘要,且版本和摘要进入握手指纹。v1 的宿主对象大小 echo 在 Linux 是 1056,在 Android 是 0,不能用于跨端兼容判定;libstdc++ 与 libc++ 的字符串/容器布局不相同,即使两端都是 LP64。payload 按字段序列化,schema 递归覆盖字段名、次序和类型,不包含容器宿主 sizeof/offsetof。宿主大小编译绊线保留为本地维护检查;非 split 路径保留 v1,G1 不因此让步。
|
||||
|
||||
## 数据面与生命周期
|
||||
|
||||
ILink 拥有数据面存储与端点。ShmLink 保留映射、reply slot pool 和现有 ring 实现;StreamLink 拥有私有镜像。session 只缓存端点/进度指针;reserve/pop 仍使用原有非虚 ring 操作。测试兼容的原始 ring 构造入口和启动时的 descriptor 交付不作为生产链路选择入口。
|
||||
|
||||
数据帧含 MGLD magic、长度、kind、两个 64-bit 字段及 payload。Stage 在引用它的 Cmd 之前发送;Cmd/Event 按逻辑 cursor 严格顺序接收并检查容量。接收线程先写数据,再 release 发布 head/水位。每侧数据连接有一个独立 reader;发送在调用线程串行进行,不引入无界发送队列。
|
||||
|
||||
进度保留单调、`>=`、只能晚不能早的规则。Progress 必须包含 **cmdRetiredTail**:FreeBytes 依赖退役尾,只有 applied tail 不能回收 borrowed record,也不能保证环回绕。event credit 以 ClientProgress 返回。越窗、非法方向、倒退水位是 ProtocolCorruption。
|
||||
|
||||
server 在 park 前、Reply 之后、present/completion 变化时冲刷进度,并按 64 条记录/1 ms 合并普通进度。client 每个等待点先 Flush。Stream reply 使用 seq 标识的消息邮箱;已经观察到 applied 却没有相应 reply 必须拒绝,不能永久等待。ShmLink 保留旧 slot 语义。
|
||||
|
||||
Present 与 `glFlush` 也是 client 的发送边界:将已经发布的批次交给对端,保持 run-ahead,不新增 apply 等待。单靠下一次 credit wait 冲刷,会让最后一帧在 client 随后空闲时永久留在本地,credit=3 时还会把前三帧积在发送端。
|
||||
|
||||
已经丢失连接或在 stage/cmd 空间等待中观察到真实挂断时,编码器取消本次及后续发射:不复制新 blob、不增加 wire seq,返回 DECLINED,并保留本地 reply ticket 的合法性。只因超时不能闩 device loss;stage 空间等待使用与 barrier 一致的 120 秒上限,让传输层有时间报告半开连接的失败。
|
||||
|
||||
两条 TCP 连接之间没有到达顺序保证。因此 surface RPC 前先等待此前提交的命令;SurfaceReply 另携带 **eventHead**。server 冲刷事件后发回复,client 等数据 reader 收到该 head 后才完成 RPC。仅按两个 send 的先后推断 SurfaceChanged 已到达是错误的。
|
||||
|
||||
## 控制、日志和进程
|
||||
|
||||
client 的专用控制 reader 直接落盘 LogLine,其他控制消息进入有界 inbox,避免 GL 线程等数据时 server 因日志 socket 写满而停住。server 同时写本地日志并前送;client 写自己的 `<base>.server.log`。`MGPipeSyncPeerLog()` 先同步命令,再以 LogFlush/ack 对齐日志观察点。正常的无远端情形是 no-op,损坏的 ack 不能静默成功。
|
||||
|
||||
supervisor 接受连接后 fork,每会话独立进程,父进程不创建 backend/EGL;同时只允许一会话,第二个返回 Busy。父进程关闭自己的 socket 副本时 **不调用 shutdown**,否则会关闭 child 仍使用的连接。后端由 Hello 请求,显式 server 环境钉住的后端不匹配则拒绝。server 子进程保留 P6 的 PipeStats Init/Shutdown。
|
||||
|
||||
正常 client 结束请求时只先关闭控制 socket 的写半边,等 server 完成 backend 销毁、最终日志和进程退出后的 EOF,再完成本地关闭。否则连续的独立 client 会抢到前一会话仍在清理的时间窗。Busy 拒绝在读取 Hello 后发送,避免带未读请求直接 close 引发 TCP RST,丢掉已经排队的 Refuse。
|
||||
|
||||
trace flavour 的前台 MobileGLServerService 从 nativeLibraryDir 启动可执行 server,设置服务端角色与反递归标志,持 wake lock。`am force-stop` 终止该包的服务进程。
|
||||
|
||||
## 带宽和测量口径
|
||||
|
||||
第一波不启用压缩或内容去重。P6 gate 8 的 rd12 stage 中位/均值是 816,248 / 1,534,555 B/帧,60 fps 对应约 49 / 92 MB/s;OpenRA 短样本均值明显受加载尖峰影响。该数据已经说明 Wi-Fi 可能成为瓶颈,但没有压缩率、额外 CPU 或纹理重复率证据,不能据此选定压缩算法,也不能宣称已满足 60 fps。
|
||||
|
||||
`MOBILEGL_PIPE_STATS=1` 下,P65LinkMetrics 记录逐帧 reply 次数、RTT 固定桶、stage bytes、wall time 与 client thread CPU。p50/p99 为桶上界;回复的测量包含必要的发送、排队与 apply,不能冒充空载网络 ping。Stage 字节/帧除以 wall time 是负载流量;链路吞吐另用真实 TCP 64 MiB 突发测量。与 PipeStats 的同源字节计数是独立观测,不能相加。
|
||||
|
||||
## 验收中的两处澄清
|
||||
|
||||
- 车道 parity 比较 DirectGLES 的可比用例集合,而不是不同标签下的总条数;当前基线是 102 条。TCP 每条还需真实 `control=tcp data=stream server=... pid=N` 证明与本地零子进程断言。fixture 不算用例通过数,禁止用 EGL 启动失败的 SKIP 代替通过。
|
||||
- lf 文档列出的四个 arming 场景中,UnlocatedIoBlock、PrimitivesGeneratedNoXfb、PointSizeDemotion 的 marker 来自 server;关闭日志前送应让对应断言变红。PipeVerifyArming 的 marker 来自 **client PipeFill**,关闭 server 前送本来就不应使它变红;它应继续通过,作为角色分离对照。不得伪造一个 server marker 来满足错误的负控预期。
|
||||
@@ -0,0 +1,199 @@
|
||||
# CONTRACT-P7 — Magma(DirectVulkan)全量走完拆分路径
|
||||
|
||||
权威:本文,与 `CONTRACT-P5.md`(表 0、字节载体、字段归属、R-1…R-17)、`CONTRACT-P5C.md`(规则 E、两个具名豁免、`SEG_EVENT`、守卫)、`CONTRACT-P5E.md`(规则 F、等待规则)、`CONTRACT-MAGMA-RUNAHEAD.md`、`CONTRACT-P6.md`(规则 G/H、死亡漏斗、§12 记录债)、`CONTRACT-P65.md`(两轴传输、布局指纹、`LinkTerms`)并列;与它们不一致处以本文为准,§10 逐条列出。基线:`feat/disaggregated @ e8b2c4bd`。`file:line` 均在该提交上读取;路径在 `MobileGL/` 下,除非以 `docs/` 开头。
|
||||
|
||||
**怎么改。** 集成者的文件。wave 2 的实现包(A / B / C / D / E / F,§9)需要改行时经集成者;包从第一天起就对着下面的行编译。
|
||||
|
||||
**写自什么。** [`notes/p7/PLAN-PH-P34B-P7.md`](../notes/p7/PLAN-PH-P34B-P7.md)(三行逐项审计,66 个代理,每个 done / unnecessary 判定两名反驳者)与 [`notes/p7/INTEGRATOR-DECISIONS-P7.md`](../notes/p7/INTEGRATOR-DECISIONS-P7.md)(ID-P7-1..11)。本文只写**规范**——树必须变成什么样、门怎么判;为什么这样定在裁定日志里。
|
||||
|
||||
---
|
||||
|
||||
## §0 P7 是什么,以及一条高于每一行的规则
|
||||
|
||||
P7 收官 = 五道出口门同时成立(§2–§7),而不是路线图 P7 行的交付列逐字做完。路线图行里两项**本阶段拒做**(ID-P7-5:`SetupDrawSnapshot` 探测字段塌 dirty mask、`VertexInputStateFactory` 内容寻址 CSO——它们是 monolith 路径上的性能项,`VulkanRenderer::SetupDraw` 在任何非 monolith 传输下分支到 `SetupWireDraw`,split 下不跑,且改它们动 pull `.text`),一项只做窄半(内部 shader 烘焙的 monolith 臂退役与直通 TCS 烘焙归 P13 / OQ-7 宽半)。
|
||||
|
||||
**规则 I — Magma 的 wire 臂上没有 `std::abort()`。** 树上每一处 `@P7` 具名拒绝今天都是 `MGLOG_F` + `std::abort()`,绕过 `Session::Fail`、不给对端 `SessionFault` 帧、普查门看不见。P7 之后,`MG_Backend/DirectVulkan` 下任何 wire 臂的拒绝只有两种形状:(a) **decline**——`MGLOG_E_ONCE` + 返回失败 + 与 monolith 臂相同的可观测(屏蔽属性、跳过 blit……);(b) **具名 Fatal**——经 `MG_Pipe` 的 `MGPipeSessionFailHook` 进 `MG_Remote::SessionFail`,带 `FatalFamilies.def` 里的家族词。`MG_Backend` 不 include `MG_Remote`(include 闭包门),hook 由 server 角色初始化安装。规则 I 的牙齿是 §3 的普查门:扩到 server 镜像后,`MG_Backend` 下裸 abort 的站点数只能下降。
|
||||
|
||||
**规则 J — 每个修复先在两进程臂上红一次。** wave 0 之前树上没有一条车道能让 Magma 两进程用例变红(四条 split 臂硬编码 `MOBILEGL_BACKEND_TYPE=DirectGLES`,`MG_IntegrationTest/CMakeLists.txt:2002/:2025/:2031`),所以 ID-P7-10:车道落地前不派 `@P7` 退役的活;落地后每个退役都带一条在 `integration-magma-spawn`(或 `-tcp`)上先红后绿的用例。
|
||||
|
||||
---
|
||||
|
||||
## §1 与前序契约的关系:什么已经不是 P7 的
|
||||
|
||||
| 项 | 谁做了 | 证据 |
|
||||
|---|---|---|
|
||||
| `DynamicBackendParameters` / `MGPCaps` / `RenderStateParameters` 定宽 + 布局指纹 | P6.5 wf(`fe28bdb5`) | `CONTRACT-P65.md` 「握手与布局」;`CONTRACT-P6.md:666` 仍记在 P7 名下,§10 更正 |
|
||||
| server buffer consumers、Android 旋转 blit | `38919d45` | `notes/p5f/magma-inproc-fix.md` |
|
||||
| Magma run-ahead、GPU 延迟退休、VAO 身份碰撞 | `194382c9` | `notes/p5f/magma-runahead.md`;`MG_Backend/Init.cpp:101` `kMGPipeMagmaRunAheadReady = true` |
|
||||
| packed-float mip 原生 blit、resolve、占位图像、copy-image 端点、XFB 捕获 span、纹理回读 | `ed507b1a..d56c0e83`(随 `e1bf3677` 合入) | `WirePlaceholderImages.inc`、`WireFramebuffer.inc:634-636` |
|
||||
| 颜色 blit 烘焙(仅 split 臂) | P5f fv | `Renderer/WireColorBlit.{vert,frag,inc}`、`WireColorBlitSpirv.h` |
|
||||
| D-B8 具名 UBO 的载体 | 直绑常驻 `VkBuffer` range 的备选已在树上 | `UniformManager.cpp:2375` 直绑分支;欠一次 `stage-ubo-named` 字节/帧测量后关 OQ-6 |
|
||||
|
||||
---
|
||||
|
||||
## §2 出口门 1:集成 + trace 在 Magma 的 push 与 split 下全绿
|
||||
|
||||
**2.1 车道形状(wave 0 包 L)。** `mgl_itest_register_split_arms` 获得后端维度;DirectVulkan × {Split(inproc), Spawn, Tcp} 三臂的环境为 `MGL_ITEST_VULKAN_{SPLIT,SPAWN,TCP}_ENVIRONMENT`(`MOBILEGL_BACKEND_TYPE=DirectVulkan` + monolith `DirectVulkan.` 臂已用的 ICD 钉)。两档标签:
|
||||
|
||||
- **gating**:`integration-magma-split`(inproc,含义不变)、`integration-magma-spawn`、`integration-magma-tcp`——今天 `integration-magma-split` 的 73 条(及 magma-buffers / magma-runahead / magma-caches 子标签)重铺三臂;三臂**同数绿**才算门 1 的集成半边成立。
|
||||
- **informational**:`integration-magma-{spawn,tcp}-all`——所有经共享宏注册的场景 × DirectVulkan × 两进程。首轮红名单是 wave 2 的工作清单;不进 CI 门,直到某条目连续绿后**按名**升入 gating(`test.yml` 的 require_green 名单)。
|
||||
|
||||
名集合纪律不变:`scripts/ci/spawn_lane_parity.py` 规范化后 split / spawn / tcp 三集合相等;G14 只增不删;每条目私有角色日志(`SplitLogPaths.cmake.in` 由宏累积)。
|
||||
|
||||
**2.2 臂证明。** Magma 两进程条目必须在 client 私有日志里同时有 `transport=spawn`(或 `control=tcp data=stream`)与 `backend=DirectVulkan`(或等价的 ConfigLoader 行)——一条 Magma 车道静默跑成 GLES 会满足其它所有门。`junit_tally.py` 的 `--require-spawn` / `--require-tcp-ran` 各加后端断言。
|
||||
|
||||
**2.3 死亡通知前提。** `MG_Remote/Client/WireTables.cpp:740` 只在 DirectGLES 下安装对象死亡通知,Magma 两进程 client 的 `ObjectDeaths` 恒为 0。改成后端无关(wave 0 L);Magma 自己的 `StateObjectDeathOps` 表(wave 2-C)落地前,`CtWireScenario` 的死亡用例在 Magma 臂上按名 skip 并列入 §12。
|
||||
|
||||
**2.4 trace 半边。** `retrace-split` 已是 39 case × 2 后端 × {inproc, spawn}(`test.yml:2449/:2470`);欠 `iterationrp × DirectVulkan` 的三个旋钮在 retrace-split 作业里与 `:2183` 同样导出(wave 0 L)。门 1 的 trace 半边 = 78 条 DirectVulkan split 行全部达阈值。
|
||||
|
||||
**2.5 门 1 的判据。** gating 三臂同数绿(wave 0 落地时:magma-split 73 = 71 PASS + 2 SKIP,magma-spawn 52 = 50 + 2,magma-tcp 54 = 52 + 2 fixture——`MagmaRunAheadScenario`(14)与 `MagmaWireCacheScenario`(7)只注册 inproc 臂,`spawn_lane_parity.py` 的 `MAGMA_INPROC_ONLY` 具名例外;跨进程的 hold / cache peek 是 §12 的债)+ parity 通过 + retrace-split 78 条 DV 行达阈值 + informational 车道(`integration-magma-full-{split,spawn,tcp}`,整个集成二进制:510 / 510 / 512 条,首轮 453 PASS / 57 SKIP / **0 红**,tcp 臂 1 红 = `IterationRPProgram203Scenario` 的 exposure texel,StreamLink × Magma 交叉项,见 `notes/p7/magma-two-process-first-run.md`)的红名单为空或每条按名归入 P8 / P9 / P13 并在 `notes/p7/` 有票。**主机 lavapipe 打不到任何 `@P7` 拒绝**(驱动能力差异),门 1 的红名单实际来自真机(§7.1 的设备窗口)。
|
||||
|
||||
---
|
||||
|
||||
## §3 `@P7` 具名拒绝:普查、退役与 decline
|
||||
|
||||
**3.1 普查数(钉住)。** 基线上 `grep -rn "@P7" MobileGL/ --include=*.cpp --include=*.inc --include=*.h` = **15 处 / 4 文件**(wave 0 包 C 以 `fatal_census.py --json` 钉住;两次审计的 14 串 / 13 语句是把一个三元里的两个串算成一条),全部在 `Renderer/UniformManager.cpp`、`Renderer/WireFramebuffer.inc`、`Renderer/WireDraw.inc`、`Renderer/VulkanRenderer.cpp`。另有两项 P7 归属但不带 `@P7` 字样:`Fatal{RoleViolation, "buffer-legacy-arm"}`(`WireDraw.inc:14/:204`)与「Magma 无 `StateObjectDeathOps`」(两条集成用例 skip)。
|
||||
|
||||
**3.2 退役表。** 「退役」= 实现该形状,与 monolith 臂同可观测;「decline」= 规则 I (a)。
|
||||
|
||||
| 名 | 站点 | 判定 | 形状 |
|
||||
|---|---|---|---|
|
||||
| `uniform-buffer-byte-tail` | `UniformManager.cpp:2391` | **退役**(真机 MC 报告 §5 的活边界) | 拷贝窗口外扩到 4 字节边界进 transient slice,再 `vkCmdCopyBuffer` 对齐内部到正确子字目的;`CopyWireBufferRangeToSlice` 扩 |
|
||||
| `copy-image-in-place` | `VulkanRenderer.cpp` | **已退役(wave 2-B,`4f857aa5`)** | `srcImage == dstImage` 时单次转 `GENERAL`、两侧布局 GENERAL、一次写回;同 mip+layer 且矩形重叠 → decline(GL 未定义) |
|
||||
| `mipmap-shader-format-or-shape`(真机 iterationt 的死因) | `WireFramebuffer.inc` | **已退役(P8-F,ID-P8-6)**:wire 臂改具名拒绝 `MipmapShaderFormatOrShape`,与 monolith 一致;此前半退役(wave 2-B,`c4be3800`) | Fatal 行带上格式 / 尺寸 / aspect / 缺的 feature 位;image 不能作附件时经 scratch 走颜色 shader mip;仍无 shader 臂的形状**具名**拒绝 |
|
||||
| **wire 回读竞态(曾被当作 OpenRA 真机 0.976494 的原因;真因是下一行)** | `WireFramebuffer.inc` / `VulkanRenderer.cpp` | **已修(wave 2-B,`76b74693`)** |
|
||||
| **wire 臂 completed-frame-serial floor 不健全(OpenRA 真机 0.976494 / 0.988998 的真因,ID-P7-33)** | `VulkanRenderer.cpp` `OnSubmitsCompletedUpTo`、`VkBufferManager.cpp` `WriteWireBuffer` | **已修(wave 2-B3,`511c1f16` / `c9164f25`)** | 一个 serial 两次提交(S1 pooled-fence 中帧 flush、S2 Present),逐记录退休把 floor 提前 → 帧 N 首次 `glBufferSubData` 走无序 host memcpy 被 S2(N-1) 的排队拷贝盖掉;floor 只升到可证 serial + 有读者在飞的 buffer 一律有序;计数 26→0,red-once `StreamedBufferSubDataBeforeEachDrawIsOrdered` Split+Spawn;32 处静默出口具名(`WireDeclines.def`)。留:三处单 fence 退休站点的聚合等待(B4) | `ReadWirePixels` 在 pooled fence 下 flush 帧内 draw 但不等待,拷贝是后一次提交,二者只靠一条 barrier 相连,而 `UNDEFINED` / `PRESENT_SRC_KHR` 的源 scope 映射成 `TOP_OF_PIPE`+0 = 空 scope——刚 present 过的默认 framebuffer正是这个布局;修后回读等它读的那一帧(red-once:`fences=2` vs `1`,`completed=71` 不变)。真机验收 = 三次 `pm clear` 冷跑 1.0 |
|
||||
| `vertex-layout` | `WireDraw.inc:245`、`VertexInputStateFactory.cpp:439-500` | **已拆分(wave 2-C,`f1b04edf`)** | `buffer-window` 一种保留为协议错(经 hook 的具名 Fatal,家族词暂留 `UnmigratedVerb`——改成更诚实的 `ProtocolCorruption` 要在 `MG_Pipe::MGPipeFatalFamily` 加一行 + 适配臂,§3.3 未花这笔,记 §12);其余**七**种形状/格式原因(含审计之后新增的 `offset-overflow`)改 monolith 的「屏蔽该属性继续画」,`BuildWireVertexInput` 出 `unsupportedAttribMask`;`vertex-format-conversion` 同包改 decline。`WireDraw.inc` 从此无 `@P7`(普查 15 → 13 处 / 3 文件) |
|
||||
| `default-color-blit-shape` | `WireFramebuffer.inc` | **已退役(wave 2-B,`583e12de`)** | 恒等变换回落 `vkCmdBlitImage` 臂;非恒等旋转先 resolve/copy 进 scratch 2D RGBA 再 shader blit;四旋转之外 decline。顺手修出拒绝背后的活 bug:普通 blit 臂对 3D 源按 layer 寻址(z 0..1 读到 slice 0)。旋转半边只在真机可证 |
|
||||
| `multisample-blit-shape` / `-aspect` | `WireFramebuffer.inc` | **已退役(wave 2-B2,`452d33b5` / `51fa513d`)**:shape 走 scratch resolve + blit、MS 目的地具名 decline;aspect 走 `VK_KHR_depth_stencil_resolve` 或第三套烘焙 shader resolve(`WireMultisampleResolve.*`),无臂的采样数形状具名 decline;树上 `@P7` 拒绝站点 **0**(剩 19 行全是注释) | shape:resolve 进 scratch 单采样再 `vkCmdBlitImage` 缩放/翻转;aspect:MS 深/模板 → 单采样用 `VK_KHR_depth_stencil_resolve`(`DynamicBackendParameters` 里查扩展)或 `texelFetch(sampler2DMS)` 写 `gl_FragDepth` 的 shader resolve;单采样 → MS 尺寸不同 decline |
|
||||
| `depth-stencil-mipmap` | `WireFramebuffer.inc` | **已退役(wave 2-B,`227d8220`)** | DEPTH-only 走 `WireDepthMipmap.{vert,frag}` 烘焙程序((A));`MOBILEGL_BAKED_INTERNAL_SHADERS` 表驱动新鲜度门((D))覆盖树内全部烘焙头——首跑抓出 iterationRP witness 头过期;D\|S 合并 / 仅 stencil → decline |
|
||||
| `uniform-buffer-byte-tail` → **已退役(wave 2-A,`450d3075`)** | — | — | 见上行;真正可达的形状是 `size` 非 4 倍数(offset 非对齐在前端就 `GL_INVALID_VALUE`) |
|
||||
| `texel-buffer-native-format` | `UniformManager.cpp` | **已 decline(wave 2-A)** | 零占位 view,fetch 读零;只在 Adreno 上有可观测(`GL_RGB32F` buffer texture) |
|
||||
| `unaligned-{texel,storage,atomic}-buffer-range` | `UniformManager.cpp` | **已具名 decline(wave 2-A)** | 跳过该绑定;可写 range 的 copyback 不做(分不出 readonly、录制点在 B/C 的 draw 尾,配方 `magma-a.md` §3.1);真正活的缺口是 `glBindBufferRange(GL_ATOMIC_COUNTER_BUFFER, …, 4, 4)`,Adreno 64 字节对齐 |
|
||||
| `storage-buffer-native-range` / `uniform-block-native-range` | `UniformManager.cpp` | **已钳制(wave 2-A)** | 钳到 `maxStorageBufferRange` / `maxUniformBufferRange` |
|
||||
| `uniform-buffer-dynamic-offset` | `UniformManager.cpp` | **已 decline(wave 2-A)** | >4 GiB transient ring 跳过绑定 |
|
||||
| `storage-buffer-offset-overflow` / `texel-buffer-offset-overflow` | `UniformManager.cpp` | 具名 Fatal(经 hook) | 算术不变量,不带 `@P7` |
|
||||
| `vertex-format-conversion` | `WireDraw.inc:272` | decline | 可选:packed converter |
|
||||
| `buffer-legacy-arm` | `WireDraw.inc:14/:204` | 具名 Fatal 经 hook(`RoleViolation`) | 已是拒绝,只换漏斗 |
|
||||
|
||||
**3.3 普查门(wave 0 包 C)。** `scripts/ci/fatal_census.py` 的扫描根扩到 server 镜像(`MG_Remote`、`MG_Pipe`、`MG_Backend`、`MG_Impl/Pipe`、`MG_State`,MG_Test / MG_Benchmark / MG_IntegrationTest 除外);`abort_sites` 成为**向下棘轮**;每个 `Fatal{Word` 在 `FatalFamilies.def` 有行;`Refuse{Word}` 对 `protocol.fbs` `RefuseCode` 校验。baseline 数(wave 0 落地,`8bb6309a`):**79 处 abort / 20 文件**(扩容前 4 / `MG_Remote` 一处),**43 个家族词**(补了 11 行 `.def`,`InitialBytesNotCarried` 并入 `UncarriedInitialBytes`),3 个 `Refuse{}` 词,0 无标记;`ResourceUnavailable` 投影到 `ServerCrashed` 而非 `SegmentMismatch`(段几何没错的时候不说段错了);Magma 三个 wire fatal 漏斗(`MagmaWireFatal` / `WireDescriptorFatal` / `WireBufferLegacyFatal`)经 `MG_Pipe` 的 `MGPipeSessionFailHook` 进 `SessionFail`,家族词保持 `UnmigratedVerb`(改名会作废自 P5b 起的拒绝普查计数);`Refuse{AuthenticationRequired}` 改为词表里的 `Authentication`。
|
||||
|
||||
---
|
||||
|
||||
## §4 出口门 4:184 符号棘轮下降 P7 的 101 个
|
||||
|
||||
**4.1 定义(wave 0 包 R,`scripts/link_ratchet.py`)。** 在 disaggregated 构建的对象树上:按 `notes/p6/a6-link-experiment-data.md` 的路径规则把 `MobileGL.dir` 下对象分 SERVER / FRONTEND / SHARED;集合 = (SERVER 未定义) − (SERVER 已定义) − (SHARED 已定义) ∩ (FRONTEND 已定义)。baseline 存**符号列表**(`scripts/data/link_ratchet_baseline.txt`),`--assert-monotone` 只对新增符号红,消失只提示重基线(ID-P7-7)。基线数(`scripts/data/link_ratchet_baseline.txt`,`8bb6309a`):**186** = `p7-magma` **101** / `p3b-p4b-espryt` **14** / `both-backends` **60** / `p6-core` 11——三个后端桶与 a6 的手数逐符号相同,多出的 2 个是 P6.5 的 `ClientSession::StartSpawned()` 与 `ProgramArtifactsSchemaFingerprint()`。Espryt 桶里 5 个是 `MG_Impl/Pipe/SlotAllocator.cpp.o` 定义的 `MG_Pipe::` 符号,按文件位置落在前端桶、只有 P13 的模块搬迁能清(标 `# P13`),所以 P3b/P4b 的可清数是 9 不是 14。`MG_Remote/FatalFunnel.cpp.o` 归 SHARED(两个角色都经它);`MG_Backend/Init.cpp.o` 按 a6 规则归 SERVER,虽然它引用 5 个 client-session 符号。CI 步骤在 `build-linux-split` 作业;baseline 由参考工具链生成,CI 的 `clang++-20` 内联决策可能让首轮红——三元规则:已列类的兄弟方法、无新引用对象 = 内联,重基线;新引用对象永远不是内联。**重基线(wave 2-B2,`3c2867d3`):186 → 173** = `p7-magma` **88** / `p3b-p4b-espryt` 18 / `both-backends` 56 / `p6-core` 11;(B′) 清掉 13 个(`ProgramObject::*`、`SamplerObject::Set*`、`ShaderObject::*` 的编译/链接路径)。剩下的 88 不是「隐藏程序」而是 **monolith draw 路径**(`UniformManager.cpp.o` 引用 `TextureObjectBase`/`TextureObjectWithOneMipmap` 等 62 个、`VkSamplerManager.cpp.o` 引用 `SamplerObject` 14 个 getter、12 个单例),`SetupDraw` 在运行时才分支到 `SetupWireDraw`(ID-P7-5 保留该运行时)——`#if` 掉这些 monolith 臂与 (B′) 是同一种操作、属 wave 3,故不标 `# P13`;§4.2 的「降到 0」对 (B′) 单独不成立。**收口(ratchet88,`p7/ratchet88`):173 → 171** = `p7-magma` **86** / `p3b-p4b-espryt` 18 / `both-backends` 56 / `p6-core` 11;上一句的「不标 `# P13`」作废。先定的事实:disaggregated 构建的缺省传输就是 monolith(`ConfigLoader.cpp:322-330`;`SetupDraw` 只在非 monolith 下转 `SetupWireDraw`,`VulkanRenderer.cpp:7064-7073`),而它被三道门消费——CI `integration (monolith-control)` 在 disaggregated 主工件上跑 `integration-gpu`(`test.yml:303-309/:390`)、`build-linux-split` 的 monolith 遍是硬门(`test.yml:1531`)、设备 monolith-control 回放用同一 disaggregated APK 且是门 3 的同会话参照臂(`apk.yml:356-367`、§7.2)——所以 `#if` 掉 monolith 臂不是 (B′) 那种等价替换,而是删掉参照臂。逐重定位归因(`~/w7/logs/ratchet88/ATTRIBUTION.md`):清掉的 2 个是 `MGPipeTextureLegacyArmScope` ctor/dtor——P5c 给 `VkTextureManager::SyncTexture` 的具名豁免,monolith 下空操作(`OnApplyThread()` 恒假),非 monolith 下临时探针(豁免一武装即 abort)在 magma split/spawn/tcp 与三条 full 车道零触发,连同类、计数器和守卫里的豁免行退役(守卫本身留)。余 86 全在活的 monolith 臂上,按名记 §12、baseline 标 `# P13`。B2 §4.3 的归因更正:对应用纹理的读经 `ITextureObject` 虚派发、不产生链接符号;`UniformManager.cpp.o` 的 62 个**全部**来自后端**构造**前端纹理当占位(`GetFallbackTexture` / `GetFallbackMultisampleTexture` / `MakePlaceholderTextureObject`,6 个无外联 key function 的纹理类的 vtable 在本对象就地发射,引出 52 个虚函数)。
|
||||
|
||||
**4.2 P7 的目标。** `--bucket` 归到 DirectVulkan 引用对象的那一桶(a6 的 101)降到 **0**。唯一还在构造前端 `SamplerObject` / `ShaderObject` / `ProgramObject` 的 DirectVulkan 站点是 `VulkanRenderer.cpp:4526-4583 / 4624-4684`(monolith 臂的隐藏 blit / 深度 mip 程序),其 early-return 是运行时而非 `#if`,所以符号仍被引用。降到 0 的路径:在 **disaggregated 构建里** monolith 臂也改用烘焙模块(§5.2 的 (B')),pull 构建的代码逐语句原样——G1 不动。若 wave 3 实测某桶符号无法在不动 pull `.text` 的前提下消失,按名记入 §12 并在 baseline 里 `# P13` 标注,不算门 4 失败。**2026-09-23(ID-P7-55):** P13 判据扩为「不动 pull `.text` 去不掉,**或**去掉会改变 disaggregated 构建 monolith 参照臂的可观测行为」;`p7-magma` 桶余 86 全按后者标 `# P13`(逐组见 §12),门 4 以此读法关闭。
|
||||
|
||||
**4.3 与 P3b/P4b 的 14 个。** 不是 P7 的目标;棘轮落地后 P3b/P4b 的 Tier 1 才可调度。
|
||||
|
||||
---
|
||||
|
||||
## §5 内部 shader、反射归档与 resident 上传(wave 2-B / 2-C 的规范)
|
||||
|
||||
**5.1 烘焙 (A)(D)。** `WireDepthMipmap.{vert,frag}` + 生成的 `WireDepthMipmapSpirv.h` + `WireDepthMipmap.inc`(深度附件 render pass + pipeline),`GenerateWireMipmap`(`WireFramebuffer.inc:752-769`)深度分支接入;新鲜度门 `MOBILEGL_BAKED_INTERNAL_SHADERS`:MG_Test 用树内 glslang 重编每个签进树的 SPIR-V 头并逐字节比对,**表驱动**,覆盖 `MG_Util/SelfTest/DriverPostIterationRPWitnessSpv.h` 与 `PrimitivesGeneratedNoXfbProbeSpv.h`。
|
||||
**5.2 (B') disaggregated 构建的 monolith 臂改用烘焙模块**(§4.2 需要):`InitializeBlitResources` / `InitializeDepthMipmapResources` / `BlitWithShader` / `GenerateDepthMipmapWithShader` 在 `#if MOBILEGL_BUILD_DISAGGREGATED` 下走烘焙管线;`#else` 分支保留今天的代码逐语句原样。两处 `MagmaP7AllocatorDebtScope` 随之删除(先用临时断言证明它们在非 monolith 传输下从不武装)。
|
||||
**5.3 OQ-8。** `LinkArtifacts` 加有序 `storageBlocks{name, binding, dataSize}`(顺序 = stage 序 → 规范化描述符名 → binding,跨 stage 去重,与 `MagmaProgramSource.h:194-195` / `DirectVulkan.cpp:220-260` 今天的资源查询序相同),进 `Visit()`,archive codec 版本 +1(`wireFingerprint` 随之变);`MagmaProgramSource::EnsureStorageBlocks` 与 `DirectVulkan.cpp` 的 `GetProgramResourceCache` 都改读它;`g_programResourceCaches` 及其 rehash 悬挂风险随之删。单测:多 stage + 数组 SSBO + atomic-counter block 的索引序与 SPIRV-Reflect 逐项相同。
|
||||
**5.4 OQ-10。** `MG_Backend/Init.cpp` `InitServerRoleCommon` 按 **server 自己的 wire 资源表** 判 `SubDataResident != nullptr` 发布 `kCapResidentSubData`(不按后端枚举猜——ID-39 的教训);`RemoteClientTest.cpp:594` 改期望;integration-magma-buffers 与 integration-split 各一条白盒用例证明发出的是 `BufferSubDataResident(49)`;26.3 in-world 回归。
|
||||
**5.5 Magma `StateObjectDeathOps`。** 镜像 `DirectGLES/Managers.cpp:335-474` 的表,从 DirectVulkan 后端注册处安装;删六处 Magma 特判;`PipeSlotPeek` 断言死亡后槽位回落;red-once = 卸表看槽位泄漏断言红。
|
||||
|
||||
---
|
||||
|
||||
## §6 出口门 2:verify 零分歧
|
||||
|
||||
verify **在 wave 3 之前**只在 monolith 臂注册(`MG_IntegrationTest/CMakeLists.txt` 的 `if (MOBILEGL_PIPE_VERIFY)` 块);wave 3 V1 起同一文件的 `if (MOBILEGL_BUILD_DISAGGREGATED AND MOBILEGL_PIPE_VERIFY)` 块把整套用例在 split(inproc) 臂上也注册了一遍,所以这句只是门 2 的**起点**记录,不是现状。门 2 = **verify × {monolith, split(inproc)} × Magma 零分歧**,且两条既有负控(`VERIFY_CORRUPT`、`POISON_OMIT`)在 split 臂上各红一次。前置债:verify+split 撞旧 `PipeRespecifyScope` 断言(`notes/p65/validation-status.md:118`)——放宽为「按 level 的 producer 各自覆盖其宣告范围」,正反单测各一。trace 侧:8 个 verify case × DirectVulkan × inproc 一轮,分歧计数 0。**状态(wave 3 V1,`p7/verify-split`,`notes/p7/verify-split.md`)**:前置债已清(`70f86064`),`integration-verify-split` 双后端 1082/1082(集成树 verify 构建,M2 之后) 绿、两条负控在 split 臂上各红一次(POISON_OMIT 用 split 臂自己的 `ReadPixels:GetPixelStoreParameters` 配对)、trace 侧 8 case 分歧 0,门 2 在主机上成立;唯一的真实分歧是 server `read_pixels` 的 ID-49 中性 pack 窗口,按预言而非规则处理,spawn 不在门内也无法测(比对器是同进程仪器)。
|
||||
|
||||
---
|
||||
|
||||
## §7 出口门 3 与 5:真机与 CTS
|
||||
|
||||
**7.1 门 3 分母(ID-P7-4,设备窗口 #1 E0a 补正)。** `trace_cases.json` 的 CI split 子集(39)× DirectVulkan × `--use-pbuffer`,排除**在 monolith 臂上就红的 case**——它们按定义不是分离缺陷:`create-indirect`(OQ-17:`dev@81b17c0b` 起在 Adreno 830 上就坏)、`minecraft-1.21.11-main-menu`(trace 自身 UBO offset 不满足本机 32 字节对齐,`notes/p65/mainmenu-trace-difference.md`)、`minecraft-1.21.4-fabric-iris-photon-v1.3b-in-world`(Adreno 830 的 `vkCreateGraphicsPipelines` 对 photon 程序返回 `VK_ERROR_UNKNOWN`,`Renderer/PipelineFactory.cpp:660`,monolith SSIM 0.0089;桌面 lavapipe 绿;按 OQ-17 先例归 `dev` 侧独立条目);rd12 in-world 不在 `--matrix` 里。**分母 = 36**。`iterationrp` 带 `apk.yml:460-462` 的三个 Magma 旋钮。排除表随每次窗口更新于 `notes/p7/device-window-<n>/E0-attribution/exclusions.md`。
|
||||
**7.2 门 3 判据(逐 case 对 monolith 臂,不是对 golden 的平 1.0)。** E0a 实测 monolith × DirectVulkan 对 golden 的 SSIM 在 0.996–1.000(iris 光影 case 最低 0.99617),只有 OpenRA 是 1.000000——golden 来自桌面,阈值吸收驱动差。所以门 3 = 同一 reboot-clean 会话内,对分母中每个 case:inproc **与** spawn(或 tcp)三遍**逐位相同**(零翻转),且每遍 `ssim vs golden` ≥ 该 case 阈值 **且与 monolith 臂同会话读数之差 ≤ 0.0005**(OpenRA 两臂都必须 1.000000、mismatch 0);`RUN_AHEAD=0` 对照臂一遍记录。证据目录 `docs/Disaggregated/notes/p7/device-window-<n>/`。定位实验顺序 E0–E6 见计划 §3 wave 1。**状态(W4,p7w4 = `ae3f43ab`,ID-P7-25)**:inproc 一遍 34/36 与 monolith 差 ≤ 0.0005;OpenRA 是与温度无关的约 50% 竞态(两张图,lockstep 第三张),验收改为 run-ahead 与 lockstep 各 6 遍全等 golden;bsl-esc-menu inproc OOM 待 spawn 臂。**状态(W5,p7w5 = `713bea9a`,ID-P7-34)**:OpenRA **27/27 golden**(inproc/spawn × run-ahead/lockstep × 冷/热 + FIF=8 + dump-armed)——项关闭;余 bsl-esc-menu(M2)与终局形式(p7w6:分母 36 三遍逐位 + spawn 臂同会话)。**状态(W6,p7w6 = `0e16ca27`,ID-P7-49)**:bsl-esc-menu inproc/spawn 各三遍 6/6,实际图像六张逐位相同且与 E0a monolith SHA 相同,M2 的设备正确性项关闭;36 例的终局三遍及同会话 monolith 对照仍待跑。W6 APK 在 M3 之前构建,不能代替 M3 后的设备回归。
|
||||
**7.2 修订(ID-P7-62,g3det,2026-09-23;集成者终裁):monolith 自身不可逐位复现的 case。** 「三遍逐位相同」预设同会话参照可逐位复现。门 3 的 monolith 臂自此每例 **×5**(split 臂仍 ×3);同会话 monolith ≥ 3 遍而图不全逐位相同的 case,split 臂的逐位条款换成分布判据:(1) split 每遍对本会话**每个** monolith 读数 `|ssim − m| ≤ 0.0005`;(2) **最近 monolith**:split 每遍到其最近 monolith 遍(差异像素最少,相同取逐通道差小者,逐通道差也量到这一遍)须 **`px ≤ 1.25·D_mm + 32` 且 `Δ ≤ 1.5·Δ_mm + 8`**,`D_mm` / `Δ_mm` = 各 monolith 遍到另一 monolith 遍的最近差异像素数 / 逐通道差的最大值(留一法,按遍)。monolith 逐位相同或不足 3 遍的 case 仍按逐位判;按分布判的 case 按名列出,其跨臂不同由 (2) 判、不需裁定行。p7w7-6afa077f(boot `8e396037…`)列名 `iris-sundial-lite`、`iris-bliss`、`iris-derivative-main-d24.4.14`(derivative 已定位:光影包的计算降噪在同一 `glDispatchCompute` 内 `imageLoad` halo 后 `imageStore` 回同一图像,workgroup 间竞争;`RUN_AHEAD=0`、lockstep、monolith `FRAMESINFLIGHT=1` 都不能逐位复现,主机 sync validation 两臂 0 hazard;不是分离缺陷)。同会话图模拟(monolith 池任取 K 遍作参照,配门 3 自己的 split 6 遍)的 (1)+(2) 通过率:×3 sundial / bliss / derivative = 100% / 97.5% / 95%,×5 = 100% / 99.9% / 100%,纯噪声对照(另取 monolith 遍当 split)95–100%;首版「全部图对 ≤ 1.25 倍」bliss ×3 只 16.2%,过渡版 `Δ ≤ Δ_mm + 1` derivative 0%、纯噪声 70%。窗口 2 按终裁只读重判(加 g3det 同 boot 的 monolith 遍)36/36,只余 create-instancing 的跨臂裁定(`PASS-NEEDS-ADJUDICATION`,非判据)。证据 `~/w7/logs/g3det/`、`~/w7/logs/g3-tools-rev2/`,判读见 `docs/Disaggregated/notes/p7/device-window-2/tools/RUNBOOK.md` §3 / §6。
|
||||
|
||||
**窗口 1b p7w6 DirectGLES × TCP(ID-P7-52,独立调查而非门 3)**:38 例含 9 个缓存跳过,29 次尝试为 16 pass、8 case-level idle timeout、2 visual fail、3 missing-result timeout。missing-result 与 timeout 不作视觉红;Photon v1.3b / D24.4.14 的两张真实错图及逐例 phone reap 状态见 notes/p65/window-1b-p7w6.md。B4 host gate 与未知子集尝试重叠,timeout 墙钟可能受 CPU 竞争影响;M3 后设备门 3 和 CTS 仍待最终 APK。
|
||||
**状态(终局窗口 3,`p7w8-78e71f2b`,ID-P7-63)**:门 3 **PASS 36/36**(33 例四臂逐位同;三例 Iris 按 ID-P7-62 分布判据通过;OpenRA 四臂 1.000000)。
|
||||
**7.3 门 5。** 块与 caselist(`tools/cts/caselists/`,取自 VK-GL-CTS `0b04c470e`,即设备上 glcts 的源):`KHR-GL46.texture_*`(1055)、`KHR-GL46.shader_image*`(125)、`KHR-GL46.packed_pixels`(4732)、`KHR-GL46.direct_state_access.*`(371)、**UBO 在 KHR 包里没有专属组**——取 GTF 模块 `GTF-GL46.gtf31.GL3Tests.uniform_buffer_object*`(90,设备 glcts 若未含 GTF 则该块记 unrun)、另加 `KHR-GL46.shader_storage_buffer_object`(124,簇 A 的 unaligned / native-range 拒绝实际打的是 SSBO range)。`KHR-GL46` 在 Android 跑器上未证实可开 4.6 core context:窗口先探针;不行则 `$BASE` 与 AFTER 都用 `gl33` 变体(texture 868 / packed_pixels 4732 / ubo 90;GL 3.3 无 shader image、无 DSA),**同一 API 版本比 0.5 pp**。先 monolith × DirectVulkan 基线入库 `tools/cts/baselines/`,迁移落地后 inproc × DirectVulkan 比对:每块 pass 率差 ≤ 0.5 pp(Pass/(Pass+Fail),NS 不入分母),**新增 crash = 0 单独硬红**。完整 caselist(~56k)在合 `dev` 前跑,不在关键路径。
|
||||
|
||||
---
|
||||
|
||||
## §8 子系统分母与 40% 检查点
|
||||
|
||||
分母 9 项:(1) Magma 两进程车道 gating 三臂同数绿;(2) §3.2 的六个「退役」;(3) `StateObjectDeathOps`;(4) 烘焙 (A)(D);(5) OQ-8;(6) OQ-10;(7) verify×split(**wave 3 V1 已完成**,`notes/p7/verify-split.md`;中点检查点那次计数时它还未成,两句说的是不同时刻);(8) 真机 ssim 1.0;(9) CTS 五块 ≤0.5 pp。**中点** = wave 2 的 A / B / C 三簇全部合入集成树之时;届时完成 < 4/9 → 停下重定基线(路线图唯一幸存的再基线绊线,此前没有分母、不可执行)。**检查点已过(2026-09-22,B2 落地时):6/9 成**——(1) 三臂 93/72/74 绿、(2) 六退役全成(ID-P7-31)、(3)(5)(6) 随 C、(4) 随 B;未成:(7) verify×split(wave 3)、(8) 真机(OpenRA 归 B3、bsl-esc-menu 归 M1)、(9) CTS AFTER(wave 4)。不触发重定基线。
|
||||
|
||||
---
|
||||
|
||||
## §9 wave 2 的文件分区与合并规则
|
||||
|
||||
| 簇 | 文件 | 内容 |
|
||||
|---|---|---|
|
||||
| A | `Renderer/UniformManager.cpp` | §3.2 的 UBO / SSBO / texel 对齐与 native-range 行 |
|
||||
| B | `Renderer/WireFramebuffer.inc`、`Renderer/VulkanRenderer.cpp`、新 `WireDepthMipmap.*` | copy-image、default-color-blit、multisample、深度 mip 烘焙 (A)(D)、(B')、AllocatorDebtScope |
|
||||
| C | `Renderer/VertexInputStateFactory.cpp`、`Renderer/WireDraw.inc`、`Renderer/VkBufferManager.cpp`、`MG_Backend/Init.cpp`、`Renderer/MagmaProgramSource.h`、`MG_State/.../ProgramArtifacts.h` | vertex-layout 拆分、`StateObjectDeathOps`、OQ-10、OQ-8 |
|
||||
| D | `MG_Backend/DirectGLES/*`、`MG_State/*`、`MG_Impl/Pipe/*`、`MG_IntegrationTest/*` | P3b/P4b 的 Espryt 项(与 A/B/C 零重叠) |
|
||||
| E | `docs/`、契约 | 已落地项更正、OQ 关闭、`MEASUREMENTS.md` §7.2 同名重跑 |
|
||||
| F | `MG_Remote/Transport/*`、`MG_Remote/Server/ServerMain.cpp`、`Handshake.h`、`Protocol/protocol.fbs` | Ph 小件(令牌、D11 五处、PH-8 单测) |
|
||||
|
||||
合并规则:集成者串行合并;每次合并后 gating 三臂 + `link_ratchet.py --assert-monotone` + G1 + parity;**棘轮上升的提交拒收**;每包一条 red-once 记在 `notes/p7/`;性能只记录不设门(ID-P7-9)。实现方 `opus`、审查方 `fable`(ID-P7-8)。
|
||||
|
||||
---
|
||||
|
||||
## §10 对前序契约的更正
|
||||
|
||||
1. `CONTRACT-P6.md:666`(§10.3)把 `DynamicBackendParameters` 定宽记在 P7 名下——已由 P6.5 wf 落地,P7 不再持有。
|
||||
2. `CONTRACT-P6.md` §12.1「棘轮在 CI 里重算 184 并断言只降」——由 wave 0 包 R 落地为 `scripts/link_ratchet.py`;「移交 P6.5」从未被 P6.5 接收,归 P7。
|
||||
3. `CONTRACT-P6.md` §12.2 D11 把五行中的三行分给 P6 `dl`——树上五处全部仍在原状态(`PipeApply.cpp:1575`、`PipeApplier.cpp:444`、`ProgramArtifactsCodec.cpp:309`、`StagedTextureStore.h:440`、`SlotTables.h:347/:371`);归 Ph(计划 §1.1 D11),wave 2-F 做。**wave 2-F 实况(`notes/p7/ph-f.md`)**:落地的是 PH-7 (1)–(4)(常量时间令牌合一、≥16 字节、无令牌只 loopback、`Welcome.dataNonce` 绑定数据连接——`protocol.fbs` 变、`wireFingerprint` 随 `MOBILEGL_PROTOCOL_CONTROL_REVISION` 变)与 PH-8 单测;**在本条写作时点,D11 五处与 PH-2 仍未落地(状态后来由 ID-P7-54 更新)**:每处都要一条字节来自对端的两进程负控,而树上没有能越过 client 自身守卫伪造记录的对端驱动(= fuzz 臂 2 的骨架),按计划停止规则未开工。
|
||||
4. `CONTRACT-P6.md` §5.2「`Session::Fail` 在 P6 保持 `[[noreturn]]`」——Ph 的翻转按 ID-P7-1 重定界,不做 98 站点的字面翻转;`Server/StagedShadow.h:115`、`Server/StagedTextureStore.h:344` 两处旁路由 wave 0 包 C 收进漏斗。
|
||||
5. `ARCHITECTURE.md` §8.5「XFB scatter 在 client」——树上取的是 server staged shadow + `OnBufferWriteback`(P5c/P5f);`OnXfbScatterReady` 是死声明,删除。
|
||||
6. **PH-4 mutable texture-level bounds (2026-09-23, F2 commits `2c6a6e25` / `51480f8c`).** A named `ResourceRespecify` carries exact per-level W/H/D alongside `(uploadTarget, level)`; on split the dimension words reuse `MGPResourceDesc.BufOffset/BufSize` only for non-buffer named image levels, and `WireFingerprint` mixes the extent-carrier revision. The applier stores the exact accepted extent, clears the transient range carrier from its descriptor copy, and passes the extent/target/fallback internal format into `StagedTextureStore.NoteLevelDefined`. The store derives its byte bound under server device limits before uploads; every `ResourceSubData` slab must match that saved extent and stay within the derived byte bound. TexBuffer and multisample targets have no legal subimage producer and staged byte records for them are named `ProtocolCorruption`. **PH-5** charges `sizeof(value_type)` for a vector count against `ReadCursor::Remaining()` before resize, using division to avoid count multiplication overflow. **PH-2** sends out-of-range and backwards-generation handle refusals through `MGPipeSessionFail` at reachable DirectGLES adoption guards; the slot table keeps the same named refusal as a backstop. Luna integration review = land; 49 targeted cases pass, including raw-peer extent, MS, slot and stale-generation controls; strict pull G1 against `adf2cbe0` is byte-identical (`.text=0xa52203`, no symbol changes). |
|
||||
7. **PH-1 (3)(4) 每会话闩(F2 latch 包,2026-09-23,ID-P7-1)——对 `CONTRACT-P5.md` R-2 与 `CONTRACT-P6.md` §5.2 的更正。** 在 **spawn / TCP 会话子进程**里(`ServerMain::RunSession` 调 `ArmSessionLatch()`,全树唯一的武装点),输入字节来自对端、且已在 `Bool` / `MobileGLResult` 函数里的站点(R-2 的诚实臂与解码边界、`PipeApplier.cpp` 七处、`ServerLoop.cpp` 四处、`SurfaceOpCodec.cpp` 三处,逐站表见 `scripts/ci/ph_latch_sites.py --table`)不再 abort:`SessionLatch` 记同一行 `Fatal{Family, "word"}`、发同一个 SessionFault 帧(只发第一个),记下**第一个**故障后返回 false;`DrainRing` 在每次 pop 之前查一次闩(codex closeout finding 6 把控制线程闩的窗口缩窄到「查闩到 pop」,没有关闭:落在其间的闩再放过已放行的那一条,会话在下一次检查结束),apply 线程离开循环,控制泵与 `ServerApplyWireSurfaceOp` 对闩后的控制帧回 `PROTOCOL_MISMATCH` 不派发,`RunSession` 的控制等待按 100 ms 切片、见闩即关会话,以 **`kSessionLatchedExitCode = 75`** 退出;supervisor 的 reap 行记 `exit=75 (latched fault)` 并计入 `sessionsFaulted`,照常服务下一连接。所以 R-2 的「Fatal{ProtocolCorruption}」在会话子进程里读作「闩 + exit 75」。**inproc 保持 Fatal**:client 与 server 同进程,闩住而存活的 server 会让同进程 client 的 GL 线程停在屏障上、没有 EOF 可醒,也没有 supervisor 去服务「下一连接」——进程就是会话;client 从不武装。未武装时 `SessionLatch` 逐字节等于 `SessionFail`,死亡测试全部保持。负控:`MG_Test/Wire/PeerLatchTest.cpp`(每站一行 raw-peer,外加批内闩与静默对端两例)与 `ServerLoopTest.cpp` 的 `ServerLoopLatchTest.*`;`ph_latch_sites.py` 的 MECHANICS 表把每一道「拒绝其余工作 / 关会话」的检查对到它的负控。
|
||||
|
||||
---
|
||||
|
||||
## §11 不是 P7 的
|
||||
|
||||
- `SetupDrawSnapshot` dirty mask、`VertexInputStateFactory` 内容寻址 CSO(ID-P7-5)→ 优化阶段。
|
||||
- 内部 shader 烘焙的 pull 构建 monolith 臂退役(动 pull `.text`)、直通 TCS 烘焙(64 模块)→ P13 / OQ-7 宽半。
|
||||
- `MG_Util` 头/对象库拆分 → P13。
|
||||
- Ph 的 fuzz 臂 2(对端字节可达站点逐站点闩转换)、PH-6、PH-7 (4)(5) → P7 之后(计划 wave 5)。
|
||||
- P3b/P4b 的 CTS AFTER → 该阶段。Iris 普查的 P9 例外(`texture-remint-pull`):wave 3 包 E1 在 `3c80cd62` 的主机普查里**为空**——77 行 × {monolith, inproc, spawn} 231 次全活,该站点一次未到达(`docs/Disaggregated/notes/p7/iris-census-3c80cd62.md` §4);设备侧读数留 wave 4。
|
||||
|
||||
---
|
||||
|
||||
## §12 记录债
|
||||
|
||||
- **F2 latch(PH-1 (3)(4)、fuzz 臂 2,§10 第 7 条)留下的记录债**:(1) **第二批对端可达站点仍是 Fatal**——`PipeApplier.cpp` 的 `WireProtocolFatal`:`OnFenceCreate` / `Destroy` / `Status` / `Wait` / `WaitServer` 的 `Fence.*` 七处、`OnClear` 的 `Clear.Fbo` / `MGPClear::ValueClass` / `MGPClear::Kind`、`OnGetTextureImage` 的 `.extent` / `.range`(`.reply` 是 server 内部不变量,不算)、`OnDrawVbo` 的 `MGPDrawInfo::IndexSize` 与 `DrawVbo.userIndices`、`OnDeleteStreamOutput`;`QueryServer.inc` 的 `Query*` 十一处(`OnQueryCreate` / `Begin` / `End` / `Counter` / `Available` / `Result` / `Destroy` / `Timestamp`)。它们**都在读对端字节的 `Bool On*` 动词里**,满足 PH-1 (3) 的判据,只是不在计划点名的「`PipeApplier.cpp` 七处」里;只有 `FindFence`(`PipeApplier.cpp:115`)与 `FindQuery`(`QueryServer.inc:2`)返回引用、无诚实拒绝。今天它们 abort(signal 6),supervisor 照常服务下一连接;机械转换 + 每站一行 raw-peer,归 Ph。(2) **PH-2 仍是 Fatal**:`SlotTables.h`、`Managers.cpp`、`Managers.h` 的 `BackendSlotTable.{HandleSlot,Generation}` 八处走 `MGPipeSessionFail`;它们全在 `#if MOBILEGL_BUILD_DISAGGREGATED` 里,改闩**不动** pull `.text`。没改的原因:`MG_Backend` 到漏斗只有单向的 `MGPipeSessionFail` 缝,改闩要一条新的 `MGPipeSessionLatch` 缝,外加证明每个调用者拿到 null twin 后不会在同一记录里撞上另一处 abort——pull 臂的 `return m_nullTwin` / `return nullptr` 是从没在 split 下审过的 release 兜底。八处标 `@Ph-declined (ID-P7-1)`,`PeerLatchTest` 的两行 D11 slot 负控断言 `signal=6` + 具名首行 + 下一会话照常渲染。(3) 生成门 `MGPipeWireProtocolFatal`(`MG_Pipe/generated/PipeWire.inc`)与 `MG_Pipe` 里的 D11 钩(`PipeApply.cpp` 的 CSO slot、respecify 载体)未武装时照旧 abort;武装的会话子进程由 `AdmitsTheGeneratedGate` 先问生成门的两个问题(opcode、最短长度),`PipeApplier::ApplyOne` 在打 verb 戳、读 `MGPipeBarriered` 之前就先问。(4) 返回引用的 9 处 `SessionFail`(`ServerSession.cpp` 七处、`Client/WireTables.cpp:118`、`Client/EmitTables.cpp:212`)标 `@Ph-declined (ID-P7-1)`,保持 Fatal。(5) `ProgramArtifactsCodecTest.AVectorCountCannotReservePastTheRemainingArchiveBytes` 把 PH-5 的界退回旧的按字节界照样绿(计数只比 PH-5 的界多 1,元素读取先把字节读完);有判别力的是 `ProgramArtifactsCodecTest.ACountOnlyTheByteBoundAdmitsIsRefusedBeforeItsResize` 与 `PeerLatchTest` 的 `D11ArchiveVectorCount`(在会话子进程的 `RLIMIT_AS` 之下,旧界的 resize 以 `std::bad_alloc` 死)。
|
||||
- ~~Magma 两进程 client 在 `StateObjectDeathOps` 落地前 `ObjectDeaths=0`:`CtWireScenario` 死亡用例在 Magma 臂按名 skip(§2.3)。~~ **已退(wave 2-C,`dce9953d`)**:两处 skip 守卫删除,死亡用例在三臂全绿;泄漏的是 **server 的 twin**(framebuffer 无 wire delete opcode,用例现在两边计数都读)。
|
||||
- §5.4 的「白盒断言发出的是 `BufferSubDataResident(49)`」在本树**不可达**:resident 发射需要 GPU 常驻存储 → 需要 `AdoptPersistentMap` → split 下 T2 对每次采纳都 decline(`PipeApply.cpp:2331`),T0/T1 归 P11。本阶段证明到「server 按自己的表发布该位、client 镜像在双后端 × 三传输收到」为止;`rsd=` PipeStats 计数器在 P11 落地采纳档后会显示这些记录。
|
||||
- §5.3 的 `g_programResourceCaches` 删除只在 disaggregated 构建:monolith 消费者已改读归档(red-once 在 monolith 臂也红),但归档成员必须 `#if MOBILEGL_BUILD_DISAGGREGATED` 才能守 G1,pull 构建保留旧缓存——钉子挪动那天一行改完,**P13**。
|
||||
- 门 3 的「不确定」在 wave 1 E0 之前无法区分「同 case 多次结果不同」与「不同 case 各自低于阈值」;两个历史点值(0.988998 / 0.976494)无 per-case 归属。**已归属(W4,ID-P7-25)**:两个点值都是 OpenRA 同一帧里被静默丢掉的一段 draw(9836 / 14658 像素),run-ahead 与 spawn 臂都掷硬币;修复归 B3。
|
||||
- ~~`TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary` 在三条 split 臂按名排除~~ **已退(wave 2-D3,`1e1f4bad`)**:split 的 clean gate 改读 storage record(`IsDrawSyncCleanByRecord` 走 `Desc.ViewOf` 到 owner,与 Magma 的 `ResolveWireTextureStorage` 同形),排除删除,三臂 3 红 → 14/14;P5E §5.2 改写。留下的:该场景的 Magma 臂只在 informational 层;`KHR-GL46.texture_view.coherency` 无 split 读数(门 5)。
|
||||
- **OpenRA 调查裁判点名的 Magma 债(`notes/p7/device-window-1/W4-verify/README.md` §3b、`~/w7/notes/evidence/w4/judge.json`),B3 不动、记在这里**:`TryDrainFrameTransients` 的 wire-only 块 `CollectWireObjects(m_completedSubmitCounter, /*all=*/true)`(`VulkanRenderer.cpp:13526`)无视 `RetireWireObjects` 给未来对象打的 `submitIndex = m_submitCounter + 1` 标签(`WireDraw.inc:113-118, 175-194`);`HasPendingRecordedWork`(`:13435-13441`)看不见 pre-pass 命令流;`FlushWirePendingCommandsForTextureUpdate = !HasPendingRecordedWork() || FlushPendingCommands()`(`VulkanRenderer.h:148-152`)把「此刻没在录」当「没在飞」;`StagedTextureStore::AdoptRun` 在 extent 变化时不重置 `Bytes`/`Covered`(`StagedTextureStore.h:234-243` vs `:270-284`,一条单测即红);`FrameContext::WaitAndAcquireNextImage`(`FrameContext.cpp:303-312`)只凭槽 fence 释放中帧 flush 过的命令缓冲。本 trace 都不可达,但都是真债。
|
||||
- **bsl-esc-menu-854 在 wire 臂上是内存缺陷不是排除项**(W4 §2/§3c):monolith 807 MiB 峰值通过;spawn 下 server 进程涨到 ~1.2 GiB 在 scudo 二级分配器 `internal map failure (Out of memory)` 死(系统尚余 8 GB → 是映射数或每进程上限,不是系统 OOM),client 同时 543 MiB;inproc 1.6 GiB 也死。**M1 已归因(主机 lavapipe,三臂同图)**:不是 store 的字节复制,是 split server **无界累积死 `VkBuffer`**——`RespecifyWireBuffer`(`VkBufferManager.cpp:189-224`)对每个 `glBufferData` 无条件 `DeferRelease` 进按帧槽分桶的 `m_deferredBufferReleases`,而桶只在 `BeginFrame`/`CollectAllDeferredReleases` 扫(`TryDrainFrameTransients` 与 present 路径),**pbuffer 回放整跑 server 只收到 1 条 present 记录**、drain 约每分钟成功两次 → 25 935 个活 VkBuffer(25 923 死)对 monolith 7 个,lavapipe 上每个一条 `memfd` 映射(27.4k → 41k),+12.2 MiB / 7.2 s,与真机 server 527 → 1162 MB 的爬升同量;轻例 `iris-bsl-in-world` 同形 1000×小(93 活 / 74 死)——随帧界之间的调用数增长,不是结构性。`StagedTextureStore` 是每层第二份拷贝(43 MiB)但平台;`PendingUploads` 0 字节(与 B3 无关);monolith 式条件 orphan 无效(6701 vs 6702)。附带:`CollectGarbage()`/`pruneWire`(`VulkanRenderer.cpp:6989`)在 `SetupDraw` 转 `SetupWireDraw` 之后、wire 臂从不执行。修复包 **M2**(`VkBufferManager.{h,cpp}`:按 serial 的 fence-gated 回收 + `MOBILEGL_IPC_WIRE_DEFERRED_MB` 水位线;red-once = 单帧内 N 次 respecify 后活 VkBuffer ≤ k×wire buffers)。**未答**:真机上是 `vm.max_map_count` 还是字节(采样器没记 maps 行数;Adreno 的 VMA 256 MiB 块推测是字节)。**「只有一条 present」已答(P-present 调查)**:不是 pbuffer、不是 client 决定——该 fixture 1 303 535 次调用里只有 **2 次 `eglSwapBuffers`**(call 1 301 789 与目标 call 1 303 534),harness 在目标 swap 上先快照后 `exit`(apitrace `retrace_main.cpp` 的 `frameComplete` → `mobilegl_apitrace_exit` 抛出(`tools/trace_replay/CMakeLists.txt:186-189` 的宏替换),在 apitrace `glretrace_egl.cpp` 的 `frame_complete` 里、`drawable->swapBuffers()` 之前解栈),所以到达 MobileGL 的 swap = 1、present 记录 = 1、`OnPresent` = 1;规则 `presents = (swaps with call_no ≤ target) − 1`(OpenRA 30 → 29,B3 测得 33 次 `BeginFrame` = 29 present + 起会 + drain)。client 的 `SwapBuffers → BackendObject::SwapEGLBuffers → Present()` 链无任何 surface-kind 分支,credit 只停不丢。结论:**server 侧任何挂在 present 上的回收都不能假设一帧有界**(一帧可以是 1.3 M 调用的 Iris 换包重编译),M2 的按 serial 回收正是这个形状;`TryDrainFrameTransients` 的两道闸 + 每 8 次的节流不是替代品。附带债:`BackendObject.cpp:385-388` 单活动面守卫对多 surface 的 client 会静默丢 present(本 trace 不触发)——该给 `MGLOG_E_ONCE`/计数。
|
||||
- ~~`MEASUREMENTS.md` §7.2 的同名比对自 P5b 起未重跑;「22 → texture readback」「3 → query」两行债务的关闭以 wave 3 的重跑为准。~~ **已退(wave 3 包 E1,`3c80cd62`)**:同名重跑 27 条 inproc 1 passed / 26 failed → 24 passed / 3 aborted / 0 failed(spawn 同)。22 条 readback 中 19 绿;3 条是 `MOBILEGL_PIPE_PUSH=0` 对照臂的具名停止(`PipeLegacyMemosDisabled` / `Magma:clear-framebuffer-record`),不是错答;3 条 query 全绿(`MEASUREMENTS.md` §7.2 末)。
|
||||
- **spawn 上的 verify 需要一个跨进程的比对形状**(wave 3 V1,`notes/p7/verify-split.md` §3.3 / §6):比对器是同地址空间的仪器,两臂是 applier 写的 `gPipeInputs` 与前端 `GLContext`。`MobileGLServer` 链的是整个库,所以 `MGPipeVerifyReadHook` 在 server 进程里**在**、也被调用;缺的是**预言**——server 的 main 只走到 `MG_Backend::InitServerRoleForSpawn()`,不调 `MG_State::Init()`,`MG_State::pGLContext` 为空,hook 在 `ctx == nullptr` 处返回。补它 = server 把读到的字段值随回复带回由 client 比(或 server 侧立等价镜像),不是注册也不是链接问题。门 2 的文本是 `{monolith, split(inproc)}`,不含 spawn,所以这是记录债而不是门内项。
|
||||
- Android 模拟器 retrace 车道每轮 2–6 例 flaky(含 monolith 对照):门 1 的任何模拟器证据都先过「连续 N 轮绿」再算数。
|
||||
- **X1 之后的两条残余**(`notes/p7/x1-persistent-map-segv.md`):(a) 首次 push 后页面在映射存活期间重新武装,一个**屏蔽了 SIGSEGV 的非 GL 线程写者**仍会被杀——彻底关闭要把武装改成逐 buffer 可选,是设计改动;(b) `RefaultOfANonWriteAccess` 仍可能拒掉同 `(tid, pc, address)` 的一次合法重复写——延后武装后不再常见,未观察到,记录。
|
||||
- **P5d 的 Adreno 数字存疑**:X1 证明在 arm64 上注册总是加保护、handler 总是不认领(bionic 指针标记 0xb4 与内核的 `si_addr` 比较),即 mprotect 臂在 Android 上**从未**回答过一次 fault;P5d 报告里把收益记在 mprotect 臂上的设备测量需要重取(`P5D-INPROC-PERFORMANCE.md`)。归优化阶段,不阻塞 P7。
|
||||
- **B2 返工留下的记录债(`notes/p7/magma-b2.md` §6,ID-P7-35/37)**:(1) 多重采样颜色 resolve 的第二段 `vkCmdBlitImage` 不裁剪目的矩形(越界 = `VUID-vkCmdBlitImage-dstOffset-00248/00249`),与 monolith 臂共有,两臂一起裁是独立的活;(2) `MagmaWireFatal("multisample-resolve-region")` 对源矩形越出读缓冲的 blit 仍带走会话——GL 说那些像素未定义而非错误,应 decline 或钳制;(3) 1 → N 等矩形 decline 拒绝的是合法形状(18.3.1 样本复制),Vulkan 无对应命令、monolith 的可观测本身是 VU 违规;(4) tcp 车道带不了 server 端测试旋钮——六条 knob 条目只在 split + spawn,`spawn_lane_parity.py` 的 `MAGMA_SERVER_ENV_KNOB_NO_TCP` 具名;收回要 `tcp_server_fixture.py` 接受每用例 server 环境;(5) X 镜像与跨格式 Y 镜像的深 / 模板 resolve 以规则 I(a) 具名 decline(合法 blit);跨格式那条在 renderbuffer 与 texture 的 D24S8 映射到不同 `VkFormat` 的设备上会让 `MsFlip.` 变红;(6) **monolith `DirectVulkan` 臂的翻转 / 缩放多重采样深度 blit**:翻转被拒(目的地保持清屏值、无错误)、缩放被执行(GL 说 `INVALID_OPERATION`)——wire 与 monolith 的分歧,wire 臂是更正确的一方,monolith 修正归 P13 / G1;(7) **Espryt(DirectGLES)的翻转 / 缩放多重采样深度 resolve**:翻转什么也不写且不报错、缩放不报错——P3b / P4b 债;用例在这两类条目上以具名理由 `Skipped`。
|
||||
- **B3 / B4 记录债(ID-P7-36/37/51)**:B3 指出的 WaitForFrameSerial 首 fence 假成功、pre-pass pending、FlushWirePendingCommandsForTextureUpdate 单 fence 假设、Present 槽回收前缺完整提交等待和 AdoptRun extent 覆盖,已由 B4 在 pipe b19297d5 修复并通过完整门;集成者 review land 见 notes/handoff/review-b4.md。
|
||||
- **V1 留下的记录债(`notes/p7/verify-split.md` §6,ID-P7-37)**:verify 在 spawn 上需要一个跨进程的比对形状(server 把读到的字段值随回复带回、client 比),门 2 的文本是 `{monolith, split(inproc)}`;server `ReadPixels` 窗口里 pack 半边的字段级盲区很窄(只有恰等于中性 pack 的错推送不可见);DirectGLES × split 上四条 Magma decline 契约用例按名从 verify-split 车道减掉——Espryt 是否 decline 这些形状归 P3b / P4b。
|
||||
- **F 片 1–3 之后(`notes/p7/ph-f.md` §4,ID-P7-37)**:`wireFingerprint` 随 `Welcome.dataNonce` / `DataBind` 变更(`0x7d4470f241d23539` → `0x03c4c293a54b3e08`),新 server 对旧 client 回 `Refuse{WireFingerprint}`,旧 server 对新 client **没有 Refuse 帧**(旧 `AcceptPair` 等第二条连接 2000 ms 后关闭,client 只见 `no Welcome … (rc=6)`)——设备上的 server 必须随下一个 APK 重部署;D11 五处 + PH-2、PH-6、PH-1 (3)(4)、PH-7 (5)(fork 前认证;**已由包 f2-auth 做,见本节末条**)未做,前置是一个能向 spawn / TCP server 投递畸形记录的对端字节驱动(fuzz 臂 2 的第一块),包 **F2**。
|
||||
- **F 修复轮之后的两条小债(集成者审查,ID-P7-44)**:`ServerMain.cpp` `RunSession` 在 `Accept()` 返回到 `close(sourceFd)` 之间,supervisor 已转发的一条 DataBind 随 child 队列关闭而得不到具名拒绝(微秒级窗口;**已关,包 f2-auth**:child 先 `shutdown(SHUT_RD)` 再收尽已排队的描述符逐条具名拒绝,Accept 之前的每条退出路径经 `ExitBeforeAccept` 同样处理,只剩 child 崩溃仍会丢);`protocol_revision_pin.py` 对 `protocol.fbs` 的注释 / 空白改动同样要求 bump(严格、有意)。
|
||||
- **M2 r2 的代价(集成者审查,ID-P7-46)**:wire store 的 destroy epoch 进了 `UniformManager` 的两个 descriptor memo,所以每次销毁事件之后同 program 的第一笔 draw 多一次 descriptor set 分配 + 写(bsl 整跑 ~2.6 万次销毁 / 130 万次调用),并把 §6.1 的长帧内 descriptor set 增长每事件多推一个 set——这是 M3 的分母,不是新债;`ServerSpawn.cpp` 放行名单(`WIRE_DEFERRED_MB` / `SPIN_US` / `SERVER_AFFINITY` / `AUDIT`)之外的 `MOBILEGL_IPC_*` 仍被清洗,都是协议形状(双方必须一致)或 client 读的值,不记债。
|
||||
- **M3 长帧 descriptor 游标(ID-P7-50)**:M2 r2 上述额外 descriptor 写入的增长已由 M3 的 2048 Acquire 边界回卷约束;证明在 graphics queue idle 后才重写 set,避免依赖 B4 尚未修的单 fence 退休。主机 bsl 确有一次回卷(缓存总数 3555 是多个 layout 的历史集合,2048 是单 epoch Acquire 上限,不是进程全局缓存上限)。queue idle stall 是性能记录项;M3 后 APK 的真机回归仍待取。
|
||||
- **Ph 簇 F 的两处预认证暴露**(**(a)(b) 已由包 f2-auth 关,见本节末条;本地对端债与指纹不对称仍在**)(wave 2-F 之前就在树上,F 修复轮审查核实,归 PH-7 (5)「有界的预认证工作 + 失败退避」,P7 之后):(a) TCP supervisor 对每条连接**先 fork 再读 Hello**(`ServerMain.cpp` `RunSession`),未认证对端每连接可让 server 付出一次 fork + 最长 10 s 的 Hello 等待;(b) `ValidatePeerHandshake` 在 `AuthenticatePeerToken` **之前**跑,`Refuse{ProtocolVersion|WireFingerprint|BuildFingerprint}` 把本端 wire 指纹与 build stamp 送给了未认证的对端。同一轮记下的本地对端债:unix 端点仍按到达顺序配对(`AcceptPair` 250 ms / 2000 ms 窗口),抽象命名空间套接字没有 mode bits,「只有本 app 能连」靠 SELinux 域隔离而非传输层。**指纹不对称**(§10 第 3 条的 `wireFingerprint` 变化):新 server 对旧 client 回 `Refuse{WireFingerprint}`;旧 server 对新 client **没有任何 Refuse 帧**——旧 `AcceptPair` 等第二条连接 2000 ms 后关掉两条,client 只看到 `no Welcome ... (rc=6)`,即设备上的 server 未随本次构建重装。细节 `notes/p7/ph-f.md` §2.2 / §6.4。
|
||||
- **门 4 的 `p7-magma` 余 86 = `# P13`(ratchet88,§4.1)**:全在 disaggregated 构建的缺省传输 monolith 臂上(CI monolith-control 与门 3 参照臂在用),不动那条臂就消不掉;P13 删 `MGB_CTX`、建 server 镜像边界时随 monolith 臂离开 server 对象。四组:
|
||||
- 占位纹理 62(`UniformManager.cpp.o`):`TextureObject{1D,2D,2DCube,3D}` ctor、`TextureObjectBase` ctor/dtor、`TextureObjectWithOneMipmap` vtable/typeinfo、`TextureObjectBase::*` 32 + `TextureObjectWithOneMipmap::*` 20 个虚函数、`MGPipeNoteAggregate` / `TextureObjectBase::PipePublishParams`;唯一不等 P13 的路是让 monolith 臂改用 wire 原生占位(`WirePlaceholderImages.inc`),但那会改参照臂的可观测(未绑 1D/3D/立方/数组采样器由丢 draw 变成画、shadow 占位 RGBA8 → D16、MS 占位由未定义变成 (0,0,0,1)),须单独裁定 + 设备窗口。
|
||||
- 采样器 14:`SamplerObject::Get{BorderColor,BorderColorI,BorderColorUI,CompareMode,MagFilter,MaxAnisotropy,MaxLod,MinFilter,MinLod,MipmapMode,SamplerCompareFunc,WrapR,WrapS,WrapT}`(`VkSamplerManager` 的 `<SamplerObject>` 实例化;5 个另由 `UniformManager::ProgramSamplesOnlySingleLevelTextures`)。
|
||||
- 对象读 6:`FramebufferAttachmentObject::{GetSize,IsComplete,IsValid}`、`RenderbufferObject::IsAllocated`、`VertexArrayObject::GetIndexBufferBindingSlot`、`BufferObject::EnsureGpuResidentStorage`(monolith render pass / clear / blit / 回读 / 索引 / SSBO 与 XFB)。
|
||||
- `MGB_CTX` 访问 4:`PipeInputs::{GetTextureObject,HasOpenTransformFeedbackSpan,InvalidateCompileEnv}`、`GLImpl::CopyTextureImageToClientOrPBO_State`。
|
||||
- **PH-6 / fuzz 臂 3 的记录债(包 f2-event,ID-P7-2;细节 `~/w7/logs/f2-event/NOTE.md` §5)**:(1) **被丢事件所属的那条记录照常完成**:它仍贴 `kStatusOk` 回复并推进 `appliedSeq`(`ServerLoop.cpp` `DrainRing` 在该记录之后才停),而 `Client/` 里没有任何代码读 `eventDropped`(`EventRingConsumer::DroppedEvents()` 只有测试在读)——一个先发不等的 readback、事后才等其回复的 client,可能在看到 EOF 之前读到缺那次 writeback 的影子。只有不等待的 client 走得到(等待中的 client 在等待里就排空了)。修法候选是让触发 forfeit 的记录回复失败,或 client 在 `eventDropped != 0` 时闩 device-lost;两者都动 client 的等待 / 回复契约,未做。(2) **inproc 不被告知**:forfeit 停掉 apply 线程,同进程的 client 收不到任何信号。这不是「只有测试能到达」(初版 NOTE 的说法不对):(3) 的场景在 inproc 上同样可达,之后 client 的下一次等待在 120 s 后以 `Fatal{BarrierTimeout}` 死——诊断名字是错的,真因是 forfeit(审查指出,本包未测)。(3) **默认 2000 ms 未在设备上测过**:合法 client 停调 GL 超过旋钮(如 Android 退后台)且队里有一环以上未等待的事件(XFB writeback)会丢会话;此前是 30 s 后 Fatal。(4) **lockstep 臂不变**:无 `kCapRunAheadApply` 的 server(Magma)满环仍是 P5C 的 `Fatal{EventRingOverflow}`,不排空的对端让会话 SIGABRT(计入 `sessionsFaulted`,下一连接照常服务);fuzz 臂 3 没有 Magma 臂。(5) **对端不被告知原因**:没有 forfeit 的 wire 词,client 只见 EOF → device-lost;加 `Refuse` / `Fatal` 码会改 `wireFingerprint`。(6) **shm 的 `eventRingFull` 在对端可写的页上**:持续写 0 的对端让保留循环不 park 地自旋,直到旋钮耗尽后以 `TooSlow` forfeit 或会话被 Stop(有界,只记)。(7) **stream 面的阻塞 `sendmsg`**(`StreamLink.cpp:158`,apply 线程经 `StreamLink::Flush` 同步 `SendEvents`)**不是无界的**:数据套接字带 `TCP_USER_TIMEOUT=5000`(`SocketTransport.cpp:99-112`)。实测(临时探针,未入库):对端 SIGSTOP、旋钮 8000,stream 面 5327 ms 时链路被内核判死 → `ReverseChannelForfeit{PeerGone}`、exit 0;shm 面等满 8006 ms → `NotDraining`、exit 0。即 stream 面上一个冻住(活着但不 ACK)的对端约 5 s 被叫作 PeerGone,与旋钮无关;apply 线程恰好卡在 send 里时控制流又结束,`Stop()` 的 5000 ms join 与这 5 s 赛跑——未构造出,记录。(8) forfeit 后队列里剩下的记录不再应用(有意);`RunSession` 每会话每 100 ms 轮询一次控制套接字(包内原为 250 ms;与 PH-1 (3) 闩的控制切片在 p7/f2-all 合并为一个 100 ms 切片,每轮先问闩、再问 apply 线程),且每一轮都查 apply 线程(记录,不是门)。(9) 排在 PH-6 等待后面的 surface op:server 的控制线程阻塞在 apply 线程的邮箱里(P5e 的等待谓词不含控制请求,本包未改),对端活着但只是不排空时,这个 op 要等旋钮耗尽才被回答;对端死亡则由数据 bell 报告(shm 面靠 server bell 挂在控制套接字上的死亡见证,镜像 CONTRACT-P6 D5c)。(10) **server bell 的死亡见证改变了 shm 会话的正常收尾顺序**:client 的 `ShutdownSend`(`ClientSession::Stop` 第 2 步)现在可能先让 apply 线程从 park 里以 dead bell 退出、再由控制循环读到 EOF;两条路走同一个退出块,每次 shm 会话正常收尾 server 日志多一行 INFO `the peer hung up (witness fd …, revents=0x2000)`(其措辞是为 client 侧写的)。
|
||||
- **PH-7 (5) + fuzz 臂 1 + ID-P7-44(包 f2-auth,分支 `p7/f2-auth`,含 fix round)关掉的与留下的**。关掉:TCP `--serve` supervisor 在 fork 前读首帧、先验令牌(未认证对端只得 `Refuse{Authentication}`,无指纹 / stamp,无 fork);首帧期限 `MOBILEGL_IPC_PREAUTH_MS`(默认 2000);待认证连接上限 `MOBILEGL_IPC_PREAUTH_MAX`(默认 8,钳到 256),满时按地址公平让位(新连接顶掉占位最多地址的最老一条,`Refuse{Busy}` "displaced ...");失败退避 `MOBILEGL_IPC_AUTH_BACKOFF_AFTER`(默认 5,0 关)/ `MOBILEGL_IPC_AUTH_BACKOFF_MS`(默认 1000,逐次翻倍,上限 60 s,认证成功清零,10 分钟遗忘,最多跟踪 256 个地址),失败 = 错令牌、畸形首帧、超时、静默占位超过 250 ms 后关闭或被顶掉;accept 遇 EMFILE/ENFILE/ENOBUFS/ENOMEM 暂停 100 ms 而非退出 73;`RunSession` 自读首帧(单会话 TCP、unix 端点)每条出路具名,令牌先于 `ValidatePeerHandshake`;ID-P7-44 见上。无 schema 变化,`MOBILEGL_PROTOCOL_CONTROL_REVISION` 与 pin 不动;**对端可见的行为变化要设备 server 随下一个 APK 重部署**:未认证的错指纹 Hello 得 Authentication 而非 WireFingerprint;静默连接 2 s 后得 Authentication(原来是 fork + 10 s 后 exit 67 且计入 faulted);会话忙时静默的第二条连接得期限拒绝而非 Busy;满队时的让位拒绝。**留下的记录债**:(1) unix 端点仍对每个 `AcceptPair` 先 fork 再读 Hello(本地对端债,不在远端威胁模型内;只补了具名拒绝);(2) 退避按对端地址计,NAT / `adb forward` 回环后的对端共用一个键,同址合法 client 会被连坐;轮换地址(如 IPv6)只受上限、期限与按地址让位约束;(3) nonce 不符的 DataBind(child 内比较)与找不到活会话的 DataBind 不计入退避(后者可能是合法的陈旧连接),两者都不花 fork;(4) 会话建立后的畸形控制帧只以日志行结束会话(child exit 0),握手之后没有给对端的帧;(5) 四个旋钮只读环境变量,不在任何配置表或 Android service extra 里;(6) ID-P7-44 的控制依赖 Linux af_unix dgram 语义(接收端 `SHUT_RD` 后 `sendmsg` 得 EPIPE),child 崩溃时已排队的 DataBind 仍被内核丢弃;(7) 测试专用杠杆 `MOBILEGL_TEST_DELAY_SESSION_HANDSHAKE_MS`(与 `MOBILEGL_TEST_DELAY_FIRST_CAPS_MS` 同类,未设不生效)。
|
||||
- **门 5 修复(ID-P7-58)的记录债**:(1) 探针判坏的设备上 shader resolve 臂 decline(无 `VK_EXT_shader_stencil_export` 的模板面、格式不带 `SAMPLED_IMAGE | DEPTH_STENCIL_ATTACHMENT`、pipeline 失败)时回落到已知不写目标的 render-pass 臂,读回 0、无 GL 错误(POST 上是 UNFIXABLE 行,只有 `MGLOG_W_ONCE`);(2) 探针只测 4×、4×4、整幅 render area,别的采样数 / 尺寸 / 子矩形上坏的驱动看不到;(3) 非 1:1 单采样深度 blit(缩放 / 翻转 / default FB)在无 `BLIT_DST` 的深度格式上仍调 `vkCmdBlitImage`(monolith 同样);(4) 跨格式 wire 深度 blit 不走 monolith 的 `BlitDepthAcrossFormats`;(5) tcp 带不了 `MsResolveBug.` / `MsFlipBug.` / `MsResolveElide.`(server 环境旋钮,`MAGMA_SERVER_ENV_KNOB_NO_TCP` 具名);(6) 图像单元:无效 / 空的存储单元在有 VK_EXT_robustness2 `nullDescriptor` 处绑 null 描述符(即 GL 8.26 的确切语义),否则绑该单元私有的占位(残余:同一 draw 内同一单元先存后读读到所存值;私有占位满 `kWirePrivateStoragePlaceholderCap` = 384 个后新的 (形状, 单元) 回落到按形状共享);在存储里但落在 [BASE, MAX] 之外或 mip 不完整的 level 仍按原样绑定(与 monolith 一致,门 5 以它为参照);(7) 切带回读在 Magma 上每带一次完整 `ReadWirePixels`(flush + 转换 + 等待),2400×1080 截图 5 带——只记性能。
|
||||
- **`ArmPrimGenReroute` 仍是每进程首设备裁决**:`VulkanRenderer.cpp` 的 `static const PrimitivesGeneratedNoXfbMeasurement s_measurement` 让同进程后来的设备沿用第一个设备的测量;这段编进 pull 构建,改它动 G1,只记(深度 resolve 的同形问题已按设备身份记忆,codex closeout finding 7)。
|
||||
- **回调里闩住的记录仍推进 `appliedSeq`**:`SessionRings.h` 的 `SessionConsumer::ApplyOne` 在回调返回后照常 `++m_appliedSeq` 并发布,等这条记录的 client 在 SessionFault / 会话关闭前短暂看到它已应用;只影响本会话,与上文 PH-6 债 (1) 同形。
|
||||
- **PH-5 的最小编码界只管 vector**:`ProgramArtifactsCodec.cpp` 对 vector 计数按元素最小编码字节数(`MinEncodedBytes`)对剩余字节设界,伪造计数的瞬时内存约为归档的 4×(最坏 `String` 32 / 8);map / set 解码仍按每项 1 字节计、逐项增长,4× 不是整个解码的上界。
|
||||
- **窗口 2 reducer 的已知偏差(记录,不判)**:`$BASE` 为 Fail、AFTER 变 NotSupported 的用例离开分母、抬高 rate,可抵消真正的 Pass→Fail(dsa 2 例 Pass→Fail 同时 2 例 `$BASE` Fail→NS 读 −0.003 pp 通过,没有后者是 −0.541 pp 失败);只有 `WARN <block>: NotSupported …` 行可见。
|
||||
- **Adreno 上的 robustness2 启用待下一设备窗口确认**:wire 臂现对每个非 monolith 设备启用 VK_EXT_robustness2 `nullDescriptor`,设备窗口 2 跑的 `6afa077f` 不含它;下一窗口核对 Redmi server 日志的 `Magma wire: an invalid or unbound image unit binds …` 行是哪一臂、`KHR-GL46.shader_image_load_store.incomplete_textures` 仍 Pass、门 3 不受影响。
|
||||
@@ -0,0 +1,53 @@
|
||||
# CONTRACT-P8:server 侧仿真缺口与 split 覆盖
|
||||
|
||||
> P8(2026-09-29 起,重定界见 `docs/Disaggregated/notes/p8/PLAN-P8.md`)落地后的规则。每条写明:规则、代码落点、守住它的用例。包说明见 `docs/Disaggregated/notes/p8/{A,B,C,D,E,F,SE,SV,MF,S}.md`,裁定见 `INTEGRATOR-DECISIONS-P8.md`。
|
||||
|
||||
## 1. Espryt 生成 mip(B、SE)
|
||||
|
||||
- server 按记录的生成窗口 [BASE+1, min(MAX+1, 存储层数, 链长)) 生成,与 Magma 同口径:`GenerateMipmapWindowByRecord`(`DirectGLES.cpp:11759`);两条 blit 链都走这个窗口。
|
||||
- RGB16F / RGB32F:先试驱动原生生成;驱动拒绝时 server 从**自己的**基级做 2×2 box,永不读 client 影子。
|
||||
- 基级读不回来时,暂存 store 只在「整级、extent 相符、未被驱动写过」时可代替;否则具名拒绝 `generate-mipmap-store-declined`(`DirectGLES.cpp:13134`),生成的层保持原样。driver-written 标在本次生成自己打标之前读取。
|
||||
- 用例:`GenerateMipmapServerScenario`(两后端 × monolith / Split / Spawn / Tcp;Espryt 强制 CPU 臂 `.CpuMip.`、`.CpuMipRefused.` 只上 Split / Spawn)。
|
||||
|
||||
## 2. Espryt server 暂存副本与 GPU 写(C)
|
||||
|
||||
- shader 写过的 buffer(SSBO / 原子计数、可写 buffer image、XFB)在 server 孪生体上打 `serverGpuWritten` 标。
|
||||
- server 的 CPU 读者(restart 重写、multi-draw rebase、`*IndirectCount`、CPU 取用的 indirect 字段、XFB scatter、fp64 收窄)一律经 `SplitHostBytesForCpuRead`(`Managers.cpp:3613`):有标或有排队常驻写时先刷新再读;T0 store 先落写再 `glFinish`。
|
||||
- 用例:`GpuWrittenDrawInputScenario`(Espryt Split / Spawn / Tcp 门控)。
|
||||
|
||||
## 3. Magma wire 臂的 indirect(D、SV)
|
||||
|
||||
- `glDraw*Indirect` / `glMultiDraw*Indirect[Count]` 从 wire store 直接录成 `vkCmdDraw*Indirect[Count]`(`DrawWireIndirectNative`,`WireDraw.inc:627`);CPU 展开只留给设备发不出的 COUNT 形态(与 monolith 同条件)。
|
||||
- `glDispatchComputeIndirect` 同样原生(`WireDraw.inc:844`),无 CPU 路径。
|
||||
- 屏障规则:store 被 shader 写过(`MarkWireBufferGpuWritten`)后,首个 indirect 读之前录一次 MEMORY_WRITE → INDIRECT_COMMAND_READ。
|
||||
- 用例:`WireIndirectDrawScenario`、`WireIndirectDispatchScenario`(计数经 `WireIndirectPeek`:原生可发时 CPU 展开 = 0)。
|
||||
|
||||
## 4. 驱动拒读的纹理回读(E)
|
||||
|
||||
- split 的 `glGetTexImage` 只读 GPU。驱动拒绝时由 server 暂存 store 作答,条件同 §1(整级、extent 相符、未被驱动写过),否则具名拒绝 `texture-readback-store-declined`(`DirectGLES/WireTextureReadback.inc:411`)。
|
||||
- store 跟随同格式、单上传目标、源可信的 `glCopyImageSubData`(`FollowCopyImageInStagedStore`,`Managers.cpp:11032`);跟不了的形状打粘性 driver-written 标。
|
||||
|
||||
## 5. 分类与拒绝(F、SV)
|
||||
|
||||
- 我们的 client 发不出的形状一律 `ProtocolCorruption`:multi-draw `CLIENT_INDICES` / `INSTANCED`(`MultiDrawShapeLatch`,`PipeApplier.cpp:779`)、Magma `client-vertex-array`。
|
||||
- Magma 生成 mip 的形状 / 格式缺口是具名拒绝:`MipmapShaderFormatOrShape`、`MipmapDepthStencilAspect`(`WireFramebuffer.inc:1401`、`:1465`),判定在长链之前;3D 原生 blit 用 1 层、深度走 z 偏移。
|
||||
|
||||
## 6. 默认帧缓冲的 surface 发布(SE)
|
||||
|
||||
- push 构建里,只要 `OnSurfaceChanged` 回调在,Espryt 就发布默认帧缓冲的格式与尺寸,不再以本进程的前端占位为条件(`DirectGLES.cpp:16239`);spawn / tcp 的 client 由此拿到 server surface 的真实深度模板格式。pull 构建不变。
|
||||
|
||||
## 7. Espryt flush 阶梯(MF,dev `f973008c`)
|
||||
|
||||
- 部分范围的失效映射不得越过同一 store 上 GPU 还没跑的环形拷贝:两条阶梯(push `FlushPendingRangesFrom`、pull `FlushPendingRangesNow`)都以 `ringCopyRetireSerial` 守住(`Managers.cpp:1496`、`:1785`);整 buffer 映射除外。
|
||||
- G5 两个脚本把两条阶梯的评审后正文钉在 ID-P8-14。
|
||||
|
||||
## 8. 覆盖与门(A、SE)
|
||||
|
||||
- 归一化前缀后,每个 monolith 用例要么登上 split 门控臂,要么在 `scripts/data/split_coverage_exemptions.txt` 里有原因类(mechanism / single-backend / no-records / pending-fix):`scripts/ci/split_coverage.py`。
|
||||
- `MGPipeUnmigratedEmulation` 的名单与调用点一一对应:`scripts/ci/unmigrated_emulation_sites.py`。
|
||||
- server 读的旋钮车道:要么独立 tcp supervisor,要么只上 split + spawn,要么证明对其他用例惰性。
|
||||
|
||||
## 不变量
|
||||
|
||||
- G1:pull 构建符号不变(整阶段 0 / 0);pull 的 `.text` 只随 dev 修复变化。
|
||||
- 控制协议修订号不变(5);线上格式不变。
|
||||
@@ -0,0 +1,71 @@
|
||||
# P9:反向通道
|
||||
|
||||
2026-09-29。依据 `docs/Disaggregated/notes/p9/`(交接 [`HANDOFF-P9.md`](../notes/p9/HANDOFF-P9.md)、裁定 `INTEGRATOR-DECISIONS-P9.md`)。本文只记实现所需的协议与行为;验证结果以 notes 里的运行证据为准。
|
||||
|
||||
## 范围
|
||||
|
||||
server → client 的结果回传在 split 下不阻塞、不丢、不乱序。本阶段落地:PACK-PBO 回读不等回复(§1);server 日志按级别转发、不阻塞(§2);`OnGlError` 的观察时点写成契约(§3);两条故障注入(§4);纹理重铸不需要拉取(§5)。事件量的批处理 / 收窄没有实测需求,不做(ID-P9-7)。路线图原文里已由前序阶段做完的(异步 reply、2 MiB 回复槽与分带回读、XFB 删除)不重做,见交接 §2。
|
||||
|
||||
## 1. 读进 pack buffer 的回读(W1)
|
||||
|
||||
### 线上格式
|
||||
|
||||
- 追加两个 op(只追加,序号 82、83):`ReadPixelsToBuffer`(kCtxVerb,verb = ReadPixels)、`GetTextureImageToBuffer`(kCtxObject,verb = GetTextureImage)。标志 `kNone`(无回复槽、无 blob、无尾),等待类 `kWaitNone`。
|
||||
- 载荷 `MGPReadbackToBuffer`(104 字节):`Src` 是回复形式的 `MGPReadbackInfo` 原样(`Src.DstOffset = 0`、`Src.DstSize` = 紧排 w·h·d·bpp,只校验不定位);`Dst` 是 pack buffer 的资源句柄;`DstBase` = 应用偏移 + `GL_PACK_SKIP_{PIXELS,ROWS,IMAGES}`;`RowStride`(行长与对齐已算入);`ImageStride`(只在 `Src.Box.D > 1` 时读);`SwapGroup` = `GL_PACK_SWAP_BYTES` 要反转的字节组宽(2/4/8,0 = 不换)。布局算术只在 client(`ReadbackLayout`)。
|
||||
|
||||
### client
|
||||
|
||||
- 目的是 pack buffer 且 `PackBufferReadbackTarget` 给出句柄时走新形式:先 `MarkReadPixelsPackBuffer()`(`GpuWritePending` 第 4 行,把 buffer 标成 GPU 写过),再发记录,**不等**。`glReadPixels` 仍按回复槽上限分带(为了 server 的 scratch 有界,不是为了线路);`glGetTexImage` 整级一条。
|
||||
- 不走新形式(回到回复形式:等回复、client 自己把行 `UploadSubData` 进 buffer)的五种情况:`MOBILEGL_IPC_PBO_READBACK_SYNC=1`;buffer 没有回读路径(`BufferWritebackIsReachable` 为假);buffer **已映射**(只有持久映射能跨读存活,它的读者直接读指针,永远不会触发同步);范围超出 `MGPSubData` 能表达的 buffer 偏移(2³¹−1);buffer 还没上过线(无句柄)。
|
||||
- 此后对该 buffer 的任何 CPU 读(`glGetBufferSubData`、`glMapBufferRange`、以它为 UNPACK 源的纹理上传)经 `SyncGpuWrites` → `resource_readback` → `OnBufferWriteback` 取回字节。**UNPACK 源读取在 split 下先 `SyncGpuWrites`**(`GL_Texture.cpp` 的 `MGL_SYNC_PIXEL_UNPACK_SOURCE`,pull 构建展开为空)。
|
||||
- monolith 不变:后端在自己的 `ReadPixels` 里映射驱动 PBO 回影子。
|
||||
|
||||
### server
|
||||
|
||||
- 读取与回复形式逐字节相同(`ReadBoundFramebufferTight`:中性 PACK 读进 scratch;`ReadTextureImageTight`:两后端各自的 `ReadTextureImageWire`);`ReadPixelsToBuffer` 的紧排大小仍受本端 `maxReplyBytes` 约束(PH-3)。
|
||||
- 落地走 applier 的 buffer 写入口:按 `SwapGroup` 反转后,每段连续区间合成一条 buffer 半边的 `MGPSubData`(`HasLiveHostWrites = 0`)调 `MGPipeApplyResourceSubData`——布局紧排时整读一条、行紧排时每层一条、否则每行一条;行间 padding 不写。于是两后端都与"client 写了同样字节的 `glBufferSubData`"同形:Espryt 进 R-11 暂存影子并排队范围,Magma `WriteWireBuffer` 与在飞读取排序;serial、epoch 与后续记录的顺序都照旧。**不用驱动的 PBO 读**:它只写后端存储,下一次从暂存影子排空的范围会把旧字节盖回去。
|
||||
- **读之前**先查完全部形状:盒与格式、紧排大小是否溢出、PH-3 上限、`Src.DstOffset/DstSize`、布局(`RowStride` 不小于紧排行、`ImageStride` 不小于行、`SwapGroup` ∈ {0,2,4,8}、最后一行的起点 ≤ 2³¹−1、紧排宽度 ≤ 2³²−1)。任何一条不过 → 每个 sink 唯一的闩锁点 `Fatal{ProtocolCorruption, "ReadPixelsToBuffer.shape"}` / `"GetTextureImageToBuffer.shape"`(原因写在其后)。查过之后落地不会再遇到要拒绝的范围;范围是否落在 buffer 内由 applier 自己的写入口判。
|
||||
- 读失败是响亮的:无 `GL.ReadPixels` → `Fatal{ReadbackDeclined, "ReadPixelsToBuffer"}`;纹理级读不出 → `Fatal{ReplyError, "GetTextureImageToBuffer"}`。回复形式的这些情况在 client 端以同名 Fatal 结束,新形式没有回复可载,就在 server 端闩。三组闩锁点各有 `PeerLatchTest` 行或不可达论证(`ph_latch_sites.py`)。
|
||||
- 准入推导:一个 verb 只有在**盖它戳的每个 op**都等待时才算静态屏障(`gen_pipe_field_ownership.py`);`ReadPixelsToBuffer` 盖 ReadPixels 戳而不等,所以 ReadPixels 不再静态屏障(现有生产表无 BARRIER_PULLED 字段,只影响合成夹具)。
|
||||
- 计数:`ServerVerbSink::BufferReadbacks()` / `BufferReadbackWrites()`。
|
||||
|
||||
## 2. server 日志转发(W3)
|
||||
|
||||
- **级别端到端**:`LogForwarder` 为 `void(*)(void* user, int level, const char* line)`(`SetLogForwarder` 与 `SetSessionLogForwarder` 两种),`level` 是 `MOBILEGL_LOG_LEVEL_*`,由 `Log()` 已收到的 Android 优先级映射(`Log()` 签名不变,G1)。线上用 `LogLine.level`(`protocol.fbs` 本来就有,**不改 schema、不动控制修订号**);未知线上级别按 ERROR 收,不降级。client 写 `<base>.server.log` 时保留级别:server 格式化过的行原样,裸行加 `[peer/LEVEL]: `。
|
||||
- **不阻塞任何打日志的线程**:server 装的转发器是 `Transport/LogForward` 的队列 + 独立发送线程(`mgl-log-fwd`);打日志的线程只加锁、拷贝,不碰 socket。
|
||||
|
||||
| 级别 | 行为 |
|
||||
|---|---|
|
||||
| ≤ WARN | 有损:队列里这类行满 1 MiB 后丢弃并计数;每段缺口在流内补一行 `LogForward{Dropped} - N line(s) …` |
|
||||
| ERROR | 无损:限速 256 行/s、突发 1024,超速的按序等待;超过 8 MiB 上限时多出的合成一行 ERROR `LogForward{Coalesced} - N …`,不静默消失 |
|
||||
| FATAL | 最多等 2 s 把本行写出;若上一次 FATAL 等待已超时且其后无任何写出,后续 FATAL 不再等(树里 `MGLOG_F` 也用于可恢复的 EGL 加载消息) |
|
||||
|
||||
- `LogFlush` 的 ack 经同一队列排在此前所有已转发行之后(`MGPipeSyncPeerLog` 的承诺不变)。会话每个出口:先清转发器,再最多 2 s 排空,发送线程仍卡在写上就切断控制 socket 让它返回。会话结束时 server 本地记一行计数(`forwarded / dropped(<=WARN) / coalesced(ERROR) / paced(ERROR) / notices / abandoned / fatal-flush-timeouts / transport-failed / max-offer-us`;有损失为 WARN,否则 DEBUG)。
|
||||
- `MGPipeCallbacks::OnLog` 删除(零生产者、零消费者、无 `EventKind`)。
|
||||
|
||||
## 3. `OnGlError` 的观察时点(W5)
|
||||
|
||||
保持 P5e 的放宽(`CONTRACT-P5E.md` §"glGetError relaxation"、`CONTRACT-P5C.md` §4.2):run-ahead 下一个动词产生的 GL 错误在**下一个 drain 点**被 client 观察到,最迟晚一个 present credit;`glGetError` 不触发强制追平。收紧(`glGetError` 前 `WaitForApplyToCatchUp`)会新增同步点,需用户裁定。
|
||||
|
||||
## 4. 故障注入
|
||||
|
||||
- **F1**:pack buffer 读已发出、server 在落地前死亡,随后 client 读该 buffer。device lost 使 `resource_readback` 被 DECLINED,影子仍是读之前的字节;`AwaitBufferWriteback` 在 `ClientSession::DeviceLost()` 时以 `Fatal{ReadbackDeclined, "buffer-writeback"}` 结束,**不把旧字节当像素返回**(与回复形式的 `ReadbackDeclined` 同名)。门:`RemoteClientControls.APackReadOwedWhenTheDeviceIsLostNeverReadsBackAsTheOldBytes`。
|
||||
|
||||
- **F2**:client 停读控制 socket(`ClientSession::PauseControlReaderForTest`),server 以测试旋钮 `MOBILEGL_TEST_APPLY_LOG_FLOOD=<n>` 让 apply 线程每条记录打 n 行 WARN + 1 行 ERROR。apply 线程不被日志卡住(约 16 MiB / 400 条记录 0.4 s 过完,单行交接最长 53–148 µs);≤ WARN 的丢弃全部有流内说明;≥ ERROR 全部按序到达。控制与 SEG_EVENT 都不排空时,会话经既有 `ReverseChannelForfeit{NotDraining}` 以 exit 0 结束,同一 supervisor 接下一个连接。门:`LogForwardTest`(15 例)、`EventForfeitPeer.StreamLogFlood*`(2 例)。
|
||||
|
||||
## 5. 纹理重铸(W2)
|
||||
|
||||
- **没有纹理拉取**。重铸(为 image 绑定把纹理升级成可绑定存储)是 server 本地路径,反向通道不含任何"向 client 要纹素"的请求:
|
||||
- Espryt(`RequireImageBindableStorageByHandle`):只读回驱动上**确实存在且尺寸相符**的层(`NativeTextureLevelHasExtent`),pending 盒用 `StagedTextureStore` 合并;驱动没有的层——store 覆盖就重放 store,否则跳过(GL 里其内容本就未定义);驱动拒绝读回而 store 覆盖该层时退回 store(记一次 `remint-readback-fallback`),两者都没有才 `Fatal`;已 immutable 且无需加宽的核心格式原样保留。
|
||||
- Magma:重建带 `STORAGE` 用途的 VkImage,`PreserveTextureContentsOnRecreate` GPU→GPU 拷贝。
|
||||
- `TextureRemintPulls`(`trp=`)在 server 路径计数(重铸了已持有的存储时;保留路径不计)。
|
||||
- `MGPipeCallbacks` 删去 `OnTexturePullRequest`;op 50 `ResourceSubDataComplete` 留作退役行(解码仍拒收),`protocol.fbs` 不变。两处 `Fatal{UnmigratedEmulation, "texture-remint-pull"}` 删除(`EveryUnmigratedEmulationIsNamedOnce` 5 → 4)。
|
||||
- 测试旋钮 `MGITEST_ESPRYT_FORCE_REMINT_READBACK_FAILURE`(server 读)强制读回失败,驱动 fallback 用例。
|
||||
- 门:`TextureRemintPullScenario`(14 例)× 两后端 × monolith/Split/Spawn/Tcp,外加 Espryt Split/Spawn 的 4 条强制失败条目;monolith Espryt 上 7 个 GPU 写过的用例按名跳过,指向 dev 缺陷(ID-P9-8)。
|
||||
|
||||
## 不变量
|
||||
|
||||
- G1:pull 构建符号增 0 减 0、`.text` 不变(新代码全在 `MOBILEGL_BUILD_DISAGGREGATED` 下;`GL_Texture.cpp` 的宏在 pull 构建里为空)。
|
||||
- 目录只追加:`MGP_CALL_LIST_DOCUMENTED_COUNT` 81 → 83,`kMGPipeVerifiedPayloadCount` 82 → 83,stamp 表 32 → 34 行。
|
||||
- 回调 9 → 7(`OnLog`、`OnTexturePullRequest` 删除;剩 `OnGlError`、`OnGpuWritten`、`OnBufferWriteback`、`OnTextureWriteback`、`OnMipLevelsGenerated`、`OnSurfaceChanged`、`OnCapsInvalidated`,其中后三个仍无生产者,不在本阶段)。
|
||||
- 控制协议修订号不变(3);`protocol.fbs` 未改。
|
||||
@@ -1,6 +1,6 @@
|
||||
# 阶段笔记(notes)
|
||||
|
||||
这一层放**各阶段的全部细节**:每个阶段一个目录,目录里的 `README.md` 是该阶段的**汇总**(摘要、阶段表原行、逐门实测、落地形状、当时的进度页),其余文件是那个阶段的原始工作笔记——包计划(`BRIEF-*.md` / `TASK-*.md`)、契约草稿、集成者逐条裁定(`INTEGRATOR-DECISIONS-*.md`)、只读审计与普查、包报告、设备记录与证据。顶层的索引([`../ROADMAP.md`](../ROADMAP.md)、[`../MEASUREMENTS.md`](../MEASUREMENTS.md)、[`../CURRENT_STAGE_PROGRESS.md`](../CURRENT_STAGE_PROGRESS.md))只留一句话并链到这里;不属于某个阶段的设计正文在 [`../design/`](../design/),上手与参考在 [`../guide/`](../guide/)。
|
||||
这一层放**各阶段的全部细节**:每个阶段一个目录,目录里的 `README.md` 是该阶段的**汇总**(摘要、阶段表原行、逐门实测、落地形状、当时的进度页),其余文件是那个阶段的原始工作笔记——包计划(`BRIEF-*.md` / `TASK-*.md`)、契约草稿、集成者逐条裁定(`INTEGRATOR-DECISIONS-*.md`)、只读审计与普查、包报告、设备记录与证据。顶层的索引([`../ROADMAP.md`](../../Disaggregated/ROADMAP.md)、[`../MEASUREMENTS.md`](../../Disaggregated/MEASUREMENTS.md)、[`../CURRENT_STAGE_PROGRESS.md`](../../Disaggregated/CURRENT_STAGE_PROGRESS.md))只留一句话并链到这里;不属于某个阶段的设计正文在 [`../design/`](../../Disaggregated/design/),上手与参考在 [`../guide/`](../../Disaggregated/guide/)。
|
||||
|
||||
## 按阶段
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
| **P12**(已收官)server 自有屏幕窗口 | [`p12/`](p12/README.md) | `PLAN-P12.md`、`INTEGRATOR-DECISIONS-P12.md`、`device/` |
|
||||
| P8 / P9 / P10 / P11 / P13(待排) | [`p8/`](p8/README.md)、[`p9/`](p9/README.md)、[`p10/`](p10/README.md)、[`p11/`](p11/README.md)、[`p13/`](p13/README.md) | 目前只有路线图上的完整范围与出口门 |
|
||||
|
||||
跨阶段:[`DEBTS.md`](DEBTS.md)(仍开放的债务与去向)、[`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)(开放问题全文,编号稳定);[`handoff/`](handoff/)(P7 的集成者审查稿,路径被 P7 交接文档引用,保持原位);[`recovered/`](recovered/)(从失败的 workflow 运行里捞回的 P0–P2 期材料,`wf1` 设计定稿前的计划草案、`wf2` P0、`wf3` P0.5、`wf4` P1、`wf5` P2;与阶段目录逐字节重复的副本已于 2026-09-24 删除,只留独有的);[`perf-five-arm-20260928/`](perf-five-arm-20260928/README.md)(非阶段的五臂传输性能对比与瓶颈归因,2026-09-28);[`anland/`](anland/README.md)(非阶段的调研:MobileGL 给 Anland——root Android 上的 Wayland 宿主——当 GL 提供者,两边各要做什么,2026-09-30)。
|
||||
跨阶段:[`DEBTS.md`](../../Disaggregated/DEBTS.md)(仍开放的债务与去向)、[`OPEN-QUESTIONS.md`](../../Disaggregated/OPEN-QUESTIONS.md)(开放问题全文,编号稳定);[`handoff/`](handoff/)(P7 的集成者审查稿,路径被 P7 交接文档引用,保持原位);[`recovered/`](recovered/)(从失败的 workflow 运行里捞回的 P0–P2 期材料,`wf1` 设计定稿前的计划草案、`wf2` P0、`wf3` P0.5、`wf4` P1、`wf5` P2;与阶段目录逐字节重复的副本已于 2026-09-24 删除,只留独有的);[`perf-five-arm-20260928/`](perf-five-arm-20260928/README.md)(非阶段的五臂传输性能对比与瓶颈归因,2026-09-28);[`anland/`](anland/README.md)(非阶段的调研:MobileGL 给 Anland——root Android 上的 Wayland 宿主——当 GL 提供者,两边各要做什么,2026-09-30)。
|
||||
|
||||
## 旧引用怎么找
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
| `docs/Disaggregated/P5E-RUNAHEAD.md` | [`p5e/P5E-RUNAHEAD.md`](p5e/P5E-RUNAHEAD.md) |
|
||||
| `docs/Disaggregated/P5F-WIRE-COMPLETENESS.md` | [`p5f/P5F-WIRE-COMPLETENESS.md`](p5f/P5F-WIRE-COMPLETENESS.md) |
|
||||
| `docs/Disaggregated/P6-SPAWN-PLAN.md`、`P6-CONTRACT-DRAFT.md`、`P6-ENDSTATE-REVIEW.md` | [`p6/`](p6/README.md) 同名文件 |
|
||||
| `docs/Disaggregated/CURRENT_STAGE_STATUS.md` | 并入 [`../CURRENT_STAGE_PROGRESS.md`](../CURRENT_STAGE_PROGRESS.md);其中 P7 的出口门 / 已落地 / 真机三节在 [`p7/README.md`](p7/README.md),P12 部分在 [`p12/README.md`](p12/README.md) |
|
||||
| `docs/Disaggregated/CURRENT_STAGE_STATUS.md` | 并入 [`../CURRENT_STAGE_PROGRESS.md`](../../Disaggregated/CURRENT_STAGE_PROGRESS.md);其中 P7 的出口门 / 已落地 / 真机三节在 [`p7/README.md`](p7/README.md),P12 部分在 [`p12/README.md`](p12/README.md) |
|
||||
| 旧 `CURRENT_STAGE_PROGRESS.md` 的 P6.5 第一波与 P6 收官快照 | [`p65/README.md`](p65/README.md)、[`p6/README.md`](p6/README.md) |
|
||||
| 当前阶段页的明细(带裁定编号的表、余项清单、证据位置) | [`p12/README.md`](p12/README.md) 的"当前状态详情"节 |
|
||||
| `docs/Disaggregated/devices/pin-verification-2026-09-07.md` | [`../guide/pin-verification-2026-09-07.md`](../guide/pin-verification-2026-09-07.md) |
|
||||
| `ARCHITECTURE.md` 的设计正文(§1–§17、附 A / B) | [`../design/`](../design/) 下十个文件,编号不变;对照表在 [`../ARCHITECTURE.md`](../ARCHITECTURE.md) |
|
||||
| 旧 `README.md` 的构建运行、代码地图、术语;旧 `ROADMAP.md` 的通用纪律 | [`../guide/`](../guide/) |
|
||||
| `docs/Disaggregated/devices/pin-verification-2026-09-07.md` | [`../guide/pin-verification-2026-09-07.md`](../../Disaggregated/guide/pin-verification-2026-09-07.md) |
|
||||
| `ARCHITECTURE.md` 的设计正文(§1–§17、附 A / B) | [`../design/`](../../Disaggregated/design/) 下十个文件,编号不变;对照表在 [`../ARCHITECTURE.md`](../../Disaggregated/ARCHITECTURE.md) |
|
||||
| 旧 `README.md` 的构建运行、代码地图、术语;旧 `ROADMAP.md` 的通用纪律 | [`../guide/`](../../Disaggregated/guide/) |
|
||||
|
||||
**按编号引用的旧章节**(小节编号在新位置原样保留):
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
| `MEASUREMENTS.md` §1 / §2 / §3 / §4 / §5 | [`p0`](p0/README.md) / [`p1`](p1/README.md) / [`p2`](p2/README.md) / [`p3a`](p3a/README.md) / [`p4a`](p4a/README.md) 的"实测"节 |
|
||||
| `MEASUREMENTS.md` §6 / §7 / §8 / §9 | [`p5`](p5/README.md) / [`p5b`](p5b/README.md) / [`p5c`](p5c/README.md) / [`p5d`](p5d/README.md) |
|
||||
| `MEASUREMENTS.md` §10 / §11 / §12 / §13 | [`p5e`](p5e/README.md)(§10、§11)/ [`p6`](p6/README.md) / [`p34b`](p34b/README.md) |
|
||||
| `ARCHITECTURE.md` §17.1–§17.4 / §17.5 / §17.6 / §17.7 | [`p5`](p5/README.md) / [`p5b`](p5b/README.md) / [`p5c`](p5c/README.md) / [`p5e`](p5e/README.md) 的"落地形状"节([`../design/10-phase-shapes.md`](../design/10-phase-shapes.md) 留一张索引表) |
|
||||
| `ROADMAP.md` 阶段表某行、"P5c 计划与审计"、"立即的下一项:P6.5 第一波"、债务表、开放问题 | 对应阶段 `README.md` 的"阶段表行"节;[`p5c`](p5c/README.md);[`p65`](p65/README.md);[`DEBTS.md`](DEBTS.md)(历史全表在 [`p5b`](p5b/README.md) 末节);[`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md) |
|
||||
| `ARCHITECTURE.md` §17.1–§17.4 / §17.5 / §17.6 / §17.7 | [`p5`](p5/README.md) / [`p5b`](p5b/README.md) / [`p5c`](p5c/README.md) / [`p5e`](p5e/README.md) 的"落地形状"节([`../design/10-phase-shapes.md`](../../Disaggregated/design/10-phase-shapes.md) 留一张索引表) |
|
||||
| `ROADMAP.md` 阶段表某行、"P5c 计划与审计"、"立即的下一项:P6.5 第一波"、债务表、开放问题 | 对应阶段 `README.md` 的"阶段表行"节;[`p5c`](p5c/README.md);[`p65`](p65/README.md);[`DEBTS.md`](../../Disaggregated/DEBTS.md)(历史全表在 [`p5b`](p5b/README.md) 末节);[`OPEN-QUESTIONS.md`](../../Disaggregated/OPEN-QUESTIONS.md) |
|
||||
| `ARCHITECTURE.md:NNN`、`MEASUREMENTS.md:NNN` 这类**行号**引用 | 指向写下它时的那个版本,今天已不对应;按上下文所说的节去找,或 `git show <当时的头>:docs/Disaggregated/<文件>` |
|
||||
| `~/w7/notes/<phase>/<file>.md` | `docs/Disaggregated/notes/<phase>/<file>.md`(这些笔记原先只存在于一台机器的 `~/w7/notes/`) |
|
||||
|
||||
@@ -163,4 +163,4 @@ A 的原生 AHB 入口与 B 的扩展都要 Anland 作者合入,先给他们
|
||||
## 旁支
|
||||
|
||||
- Termux 原生(bionic)程序可以进程内直接加载 MobileGL(monolith,厂商驱动),不需要 split;帧交接同样是 A(A1、A2、A4)或 B(Surface 交给该进程)二选一。但 Termux 线目前基于 Anland 5.x(lfdevs 维护)。
|
||||
- 这项工作不在 [`../../ROADMAP.md`](../../ROADMAP.md) 里,要做是一个新阶段。
|
||||
- 这项工作不在 [`../../ROADMAP.md`](../../../Disaggregated/ROADMAP.md) 里,要做是一个新阶段。
|
||||
@@ -1,6 +1,6 @@
|
||||
# P0 — 卫生、度量、门、骨架(2026-09-05)
|
||||
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../ROADMAP.md)、[`MEASUREMENTS.md`](../../MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../../Disaggregated/ROADMAP.md)、[`MEASUREMENTS.md`](../../../Disaggregated/MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../../Disaggregated/ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
|
||||
## 摘要
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# P0.5 — 值头与制品头抽取(`5d99ee43`)
|
||||
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../ROADMAP.md)、[`MEASUREMENTS.md`](../../MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../../Disaggregated/ROADMAP.md)、[`MEASUREMENTS.md`](../../../Disaggregated/MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../../Disaggregated/ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
|
||||
## 摘要
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# P1 — `PipeInputs` 替换与 verify harness
|
||||
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../ROADMAP.md)、[`MEASUREMENTS.md`](../../MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(2026-09-24 从上层索引移入,原文照录)都在这里;上层只留摘要——[`ROADMAP.md`](../../../Disaggregated/ROADMAP.md)、[`MEASUREMENTS.md`](../../../Disaggregated/MEASUREMENTS.md)、[`ARCHITECTURE.md`](../../../Disaggregated/ARCHITECTURE.md)。文中 `file:line` 与"今天""当前"按各段写成时的头理解;文中的 `MEASUREMENTS.md §N` / `ARCHITECTURE.md §17.x` 编号保留,对照表见 [`../README.md`](../README.md)。
|
||||
|
||||
## 摘要
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# P10 A:fence 轮询本地作答——配对测量与 Espryt 发现
|
||||
|
||||
2026-09-29,`p10/main@8d0a1b6c`,主机 WSL Arch(Espryt = mesa llvmpipe ES 3.2;Magma = lavapipe)。协议与门见 [`CONTRACT-P10.md`](../../../../MobileGL/MG_Remote/CONTRACT-P10.md) §1。只记录、不设门。
|
||||
2026-09-29,`p10/main@8d0a1b6c`,主机 WSL Arch(Espryt = mesa llvmpipe ES 3.2;Magma = lavapipe)。协议与门见 [`CONTRACT-P10.md`](../../contracts/CONTRACT-P10.md) §1。只记录、不设门。
|
||||
|
||||
## 负载
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
| tcp · openra | 399.0 | 384.9 | 391.8 | 最多 8% | 0.16–0.19 | 2.1 | 1.8 |
|
||||
| shm · rd12 | 120.2 | 120.1 | — | ≤ 0.4% | 0 | 0.1 | 8.2 |
|
||||
|
||||
- **结论:loopback 上 credit 没有噪声以外的影响。** rd12·tcp 的 19.4 ms 帧由 client CPU(~9.9 ms)和生产者在传输里的停车(~11.2 ms,发送 + 环 / stage / ack 等待)组成,credit 等待只有 ~0.2 ms。五臂报告(09-28)建议 #2"credit 2"在 loopback 上没有收益;要追的是 stream 传输的停车(性能债,[`../DEBTS.md`](../DEBTS.md))。
|
||||
- **结论:loopback 上 credit 没有噪声以外的影响。** rd12·tcp 的 19.4 ms 帧由 client CPU(~9.9 ms)和生产者在传输里的停车(~11.2 ms,发送 + 环 / stage / ack 等待)组成,credit 等待只有 ~0.2 ms。五臂报告(09-28)建议 #2"credit 2"在 loopback 上没有收益;要追的是 stream 传输的停车(性能债,[`../DEBTS.md`](../../../Disaggregated/DEBTS.md))。
|
||||
- 跨机(Wi-Fi)credit 1 → 2 的 +26% 仍以 P12 的数字为准([`../p12/CROSSHOST-ACCEPTANCE.md`](../p12/CROSSHOST-ACCEPTANCE.md)),那里瓶颈是往返延迟。
|
||||
- 五臂报告里 tcp-loopback rd12 的 21–23 fps(74% 在 credit 等待)是旧构建、未定频的结果,与本次不可直接比较。
|
||||
- Magma 没测:`profile_matrix.sh` / `run_matrix.sh` 在两处写死了 DirectGLES。
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
> 门(集成头):见 ID-P10-12。
|
||||
>
|
||||
> 下面是该阶段在路线图上的原始范围与出口门(2026-09-24 从 [`ROADMAP.md`](../../ROADMAP.md) 阶段表移入,原文照录)。
|
||||
> 下面是该阶段在路线图上的原始范围与出口门(2026-09-24 从 [`ROADMAP.md`](../../../Disaggregated/ROADMAP.md) 阶段表移入,原文照录)。
|
||||
|
||||
## 摘要
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user