[Chore, Docs] (...): drop the archived notes and move the skill under tools

- the archive freeze rule goes with the directory
- code comments lose citations to the removed notes
This commit is contained in:
BZLZHH
2026-10-31 19:29:06 +08:00
parent b5d64175a1
commit 6e36bf2c1b
1064 changed files with 1022 additions and 200727 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
# 设计与架构(索引)
> 这一页只有设计要点和章节去向;每一部分的完整设计在 [`design/`](design/)。章节编号 §1–§17、附 A / B 沿用至今,代码注释里写的 `ARCHITECTURE.md §N` 按下表找到对应文件。各阶段的 wire 契约原文已冻结在 `docs/archive/contracts/CONTRACT-*.md`,与设计文字冲突时以更新的契约为准。
> 这一页只有设计要点和章节去向;每一部分的完整设计在 [`design/`](design/)。章节编号 §1–§17、附 A / B 沿用至今,代码注释里写的 `ARCHITECTURE.md §N` 按下表找到对应文件。各阶段的 wire 契约原文见 git 历史,与设计文字冲突时以更新的契约为准。
## 一句话
@@ -32,5 +32,5 @@
| 12–13 | persistent map 与大缓冲采纳;回读、往返清单、五部分验证门 | [`design/07-memory-readback-verification.md`](design/07-memory-readback-verification.md) |
| 14–15 | 帧节奏与线程;进程、握手、EGL、Android(含上屏 server)、崩溃处理 | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) |
| 16、附 A、附 B | 源码目录与构建选项;全部开关;边界计数器 | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) |
| 17 | 各阶段落地时的形状与仍生效的规则(链到 `docs/archive/notes/`) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
| 17 | 各阶段落地时的形状与仍生效的规则(原文见 git 历史) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
| — | 状态归属:多 session / 多 context / share group(anland 统一 server 前置) | [`design/11-state-ownership.md`](design/11-state-ownership.md) |
+2 -2
View File
@@ -1,6 +1,6 @@
# 当前阶段:P13 已收官(2026-10-07);下一阶段 P14
> **更新 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)。
> **更新 2026-10-07**。这一页只有摘要;计划与裁定见 git 历史。上一阶段 P8 于 09-30 收官。
## 目标
@@ -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`](DEBTS.md) 中「需裁定」各项;FCL 无声死亡留给用户(`docs/archive/notes/p13/fcl-soak/README.md`)。
**待用户决策**:[`DEBTS.md`](DEBTS.md) 中「需裁定」各项;FCL 无声死亡留给用户(细节见 git 历史)。
+9 -9
View File
@@ -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` 构建。
>
> 路线图只列"有债务、看这里"。每条写明去向;已关闭的历史债与当时的完整债务表见 [`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 不再丢设备)。去向「需裁定」= 等用户定;「可进行」= 随时可开的小修,不依赖别的阶段。
> 路线图只列"有债务、看这里"。每条写明去向;已关闭的历史债与当时的完整债务表见 git 历史。开放问题另见 [`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 等待;数字见 `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 原型量 |
| 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 等待;数字见 git 历史 |
| ~~P13 转出(W9):缓存容量重调~~ **P14 关闭(读数)**:`MOBILEGL_PIPE_STATS` 在 rd12 / openra 上 render-state CSO 稳态每帧新铸 0 个,Magma 管线 memo 命中 99.0% / 92.7%,没有缓存抖动,P8 容量保留 | 已关闭(W9 节,见 git 历史) |
| 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 缓存(设计草稿(见 git 历史),估计 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;证据与机理见 [`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 |
| 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;证据与机理见 git 历史 | 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(`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 与线程落位) |
| P10 转出(性能):设备上 tcp loopback 的 rd12 每帧 19.4 ms 中 ~11.2 ms 是生产者在 stream 传输里的停车(发送 + 环 / stage / ack 等待),credit 等待只有 ~0.2 ms(见 git 历史 §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×(见 git 历史)。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 不会传播;`docs/archive/notes/p11/gate.sh` 是事实上的门 | 需要时入库 |
| 树外脚本:普查跑器、`wsl_p5_gate.sh` 等在 `~/w7/notes/`,git merge 不会传播;事实上的门脚本见 git 历史 | 需要时入库 |
已关闭的历史债(P5 的 27 个 wrong-answer、`rsp` 残余读、P5b 的 inproc 依赖、ABI 指纹、默认 staging 装不下 128 MiB 上传、184 符号棘轮 CI 门)及当时的完整债务表见 [`docs/archive/notes/p5b/README.md`](../archive/notes/p5b/README.md) 末节。
已关闭的历史债(P5 的 27 个 wrong-answer、`rsp` 残余读、P5b 的 inproc 依赖、ABI 指纹、默认 staging 装不下 128 MiB 上传、184 符号棘轮 CI 门)及当时的完整债务表见 git 历史。
+21 -21
View File
@@ -1,6 +1,6 @@
# 实测(索引)
> 这一页只有结论和去向;完整的表、命令、协议与读法在各阶段 `docs/archive/notes/<阶段>/README.md` 的"实测"节。代码和旧笔记里的 `MEASUREMENTS.md §N` 按下表"§"列找到对应阶段(小节编号在那里原样保留)。
> 这一页只有结论和去向;完整的表、命令、协议与读法在各阶段的"实测"节(原文见 git 历史)。代码和旧笔记里的 `MEASUREMENTS.md §N` 按下表"§"列找到对应阶段(小节编号在那里原样保留)。
**口径**:性能只记录、不设门;看**逐线程 CPU 时间**,不看整机帧率。性能对比只用一台手机(Redmi `2f7cbe2e`,Adreno 830),定频、同热窗口、两臂交替跑([`guide/pin-verification-2026-09-07.md`](guide/pin-verification-2026-09-07.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` 自证。
见 [`docs/archive/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md`](../archive/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md) §2.6–2.10。
见 git 历史 §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)。全文 [`docs/archive/notes/perf-five-arm-20260928`](../archive/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)。全文见 git 历史。
## 按阶段
| § | 阶段 | 一句结论 | 全文 |
|---|---|---|---|
| 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 | P0 | 能从应用进程拉起第二个进程;跨进程共享 GPU 内存只有一条路在所有设备上可用 | git 历史 |
| 2 | P1 | 逐 draw 核对推送状态,79 条 trace 零分歧 | git 历史 |
| 3 | P2 | 桌面基准:新 tracker 比它替掉的旧填充更便宜 | git 历史 |
| 4 | P3a | 两机真机 Release:接口代价 +6–12%,VAO 切换密集的场景 +27–30% | git 历史 |
| 5 | P4a | 换到 Redmi;P4a 自己只加 3–6 个百分点 | git 历史 |
| 6 | P5 | 两线程第一帧画对;真机两线程臂还跑不完 | git 历史 |
| 7 | P5b | 真机两线程跑通四条游戏 trace;两线程额外代价 +6–18% | git 历史 |
| 8 | P5c | 两线程之间零共享内存后,全部测试仍绿 | git 历史 |
| 9 | P5d | 游戏内 7–13 → 103–106 fps,client CPU 15.0 → 9.2 ms/帧 | git 历史 |
| 10–11 | P5e | 发完就走比每条都等快 69%;重负载下与单线程持平 | git 历史 |
| 12 | P6 | 两进程与两线程持平;每帧数据量与记录数首次实测 | git 历史 |
| 13 | P3b/P4b | 纹理上传形状在四种拓扑下完全一致 | git 历史 |
| 14 | P6.5 | 跨机 TCP 102/102;断线约 5 s 检测到 | git 历史 |
| 15 | P7 | 真机画面 36/36;server 内存无界增长收住(主机 825 → 546 MiB) | git 历史 |
| 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) | git 历史 |
| 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 | git 历史 |
| 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 路径可能多发几个小框 | git 历史 |
| 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 格 / 臂无一变差 | git 历史 |
+1 -1
View File
@@ -5,7 +5,7 @@
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` 已删([`docs/archive/notes/p9/W2-REMINT.md`](../archive/notes/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` 已删(见 git 历史)。
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。
+2 -3
View File
@@ -21,10 +21,9 @@
|---|---|
| 现在做到哪、下一步 | [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md) |
| 设计要点与各部分设计 | [`ARCHITECTURE.md`](ARCHITECTURE.md) → [`design/`](design/) |
| 分几个阶段、每个阶段做了什么 | [`ROADMAP.md`](ROADMAP.md) → [`docs/archive/notes/<阶段>/`](../archive/notes/README.md) |
| 分几个阶段、每个阶段做了什么 | [`ROADMAP.md`](ROADMAP.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) |
| 各阶段的原始计划、裁定、报告、证据(已冻结) | [`docs/archive/notes/`](../archive/notes/README.md) |
整理记录:2026-09-24 两次整理——顶层只留索引,设计正文拆到 `design/`,上手与参考放 `guide/`,阶段细节放 `notes/<阶段>/`(2026-10 起冻结在 `docs/archive/notes/`);整理前的版本见提交 `1fb18d9`。更早的设计竞赛与评审稿在 git 历史(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`)。
整理记录:2026-09-24 两次整理——顶层只留索引,设计正文拆到 `design/`,上手与参考放 `guide/`,阶段细节放 `notes/<阶段>/`(这些笔记已从树中删除,见 git 历史);整理前的版本见提交 `1fb18d9`。更早的设计竞赛与评审稿在 git 历史(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`)。
+24 -24
View File
@@ -1,6 +1,6 @@
# 路线图(索引)
> **2026-10-07**:P0 到 P7、P12、P9、P10、P11、P8、**P13** 已完成——拆分路线与单进程路线都走完。P13(退役 pull 路径):单进程也只走记录臂,pull / push 两套构建、`MOBILEGL_PIPE_PUSH` 与 `MOBILEGL_PIPE_LEGACY_MEMOS` 编译期开关、残余值块和后端的前端对象臂全部删除;库拆成按 link ratchet 分区的七个模块,split 构建里 server 一半单独真链接;G1 退役,由零 `MG_Remote` 符号、skip 普查与 recorder 金标接班;性能工作排在其后,基线是 `dev` 构建。下一项是 **P14**(状态归属)。每个阶段的计划、验收结果、实测与报告在 `docs/archive/notes/<阶段>/`(点阶段名进入)。
> **2026-10-07**:P0 到 P7、P12、P9、P10、P11、P8、**P13** 已完成——拆分路线与单进程路线都走完。P13(退役 pull 路径):单进程也只走记录臂,pull / push 两套构建、`MOBILEGL_PIPE_PUSH` 与 `MOBILEGL_PIPE_LEGACY_MEMOS` 编译期开关、残余值块和后端的前端对象臂全部删除;库拆成按 link ratchet 分区的七个模块,split 构建里 server 一半单独真链接;G1 退役,由零 `MG_Remote` 符号、skip 普查与 recorder 金标接班;性能工作排在其后,基线是 `dev` 构建。下一项是 **P14**(状态归属)。每个阶段的计划、验收结果、实测与报告见 git 历史。
## 目标
@@ -15,29 +15,29 @@ client(跑应用的一方)与 server(跑驱动的一方)可以在**不
| 阶段 | 做什么 | 状态 |
|---|---|---|
| [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) |
| P0 | 打地基:计数器、调用目录与生成器、CI 门;验证 Android 能拉起第二个进程 | ✅ 09-05 |
| P0.5 | 抽出前后端共用的数据类型头,切断后端对前端头文件的依赖 | ✅ |
| P1 | 后端改读一个"输入块",并能逐 draw 核对它与前端真实状态一致 | ✅ |
| P2 | 前端开始主动推送渲染状态;接口代价实测 +6–12%,决定继续 | ✅ 09-08 |
| P3a | GLES 后端的 buffer、顶点数组改用句柄 | ✅ 09-08 |
| P4a | GLES 后端的帧缓冲、纹理、sampler、program 改用句柄 | ✅ 09-08 |
| P5 | 第一次拆成两线程:命令队列 + 后端线程(每条命令都等) | ✅ 09-16 |
| P5b | 把剩余命令迁到队列上;真机首次以独立后端线程跑通目标游戏 | ✅ 09-16 |
| P5c | 两线程之间不再偷偷共享内存,只经队列交换 | ✅ 09-17 |
| P5d | 两线程版本的性能:游戏内 7–13 fps → 103–106 fps | ✅ 09-18 |
| P5e | client 发完就走,不再每条命令等后端;重负载下与单线程持平 | ✅ 09-19 |
| P5f | 所有状态都经队列传递,为拆进程扫清最后障碍;Vulkan 后端也能发完就走 | ✅ 09-20 |
| P6 | 拆成两个进程;真机上两进程与两线程成本持平 | ✅ 09-22 |
| P6.5 | 传输分控制面 / 数据面;电脑当 client、手机当 server 经 TCP 跑通 | 第一波 ✅,余项交 CI |
| Ph | server 能安全地接受外来连接:令牌、限额、坏数据不致崩溃 | 必需项 ✅(随 P7) |
| P3b / P4b | GLES 后端的深化与收尾 | 大部分 ✅,余项并行 |
| P7 | Vulkan 后端完整迁移;真机画面检查 36/36 通过 | ✅ 09-23 |
| **P12** | **server 自己开窗口上屏,client 不需要窗口** | ✅ 09-29:出口门 B(跨机 TCP)✅ 09-28、A(FCL + 杀 server)✅ 09-29;契约见 git 历史;G1 成立;余项进 [`DEBTS.md`](DEBTS.md) |
| **P8** | ~~把剩余的仿真路径挪到正确的一侧,补齐协议~~ → **补齐 server 侧仿真缺口与 split 覆盖** | ✅ 09-30:重定界(原 10 项里 6 项前序已完成);Espryt 生成 mip / 暂存副本与 GPU 写 / 驱动拒读、Magma wire 原生 indirect 与 dispatch;246 例登上 split 三臂(`split_coverage.py`);Create 在 Adreno 上的丢拷贝修到 dev 与 feat;契约见 git 历史;余项进 [`DEBTS.md`](DEBTS.md) |
| **P9** | **反向通道:回读、日志、重铸不再同步等待 client** | ✅ 09-29:PACK-PBO 回读 fire-and-forget(两后端)、日志分级转发、重铸无拉取(`OnLog` / `OnTexturePullRequest` 删,回调 9 → 7),F1 / F2;契约见 git 历史;G1 成立;事件量批处理无实测需求不做;余项进 [`DEBTS.md`](DEBTS.md) |
| **P10** | **同步对象、查询与帧节奏** | ✅ 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 在噪声内;契约见 git 历史;G1 成立;余项进 [`DEBTS.md`](DEBTS.md) |
| **P11** | **同机大缓冲零拷贝共享** | ✅ 09-29:T0 零拷贝导入(两后端,默认开,自测不过或 Stream 上静默退回 T2);同机外部 client 经 helper + 令牌 broker 走共享内存(B1);连接按 `PairBind` nonce 配对(修订 5);采纳档位在握手时定;契约见 git 历史;余项进 [`DEBTS.md`](DEBTS.md) |
| **P13** | **删掉旧的"后端直接读前端"路径** | ✅ 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),实现待做 |
## 另见
@@ -41,7 +41,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 起发布,后者契约 `docs/archive/contracts/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 起发布,后者契约见 git 历史)。**不存在"归属开关"(D-B7)**:multi-draw 分档与 restart 重写永远由 server 拥有,client 只在 caps 要求时提供索引字节。
## 4. 记录与 payload 约定(`MG_Pipe/MGPipeTypes.h`)
@@ -55,8 +55,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:[`docs/archive/notes/p9/W2-REMINT.md`](../../archive/notes/p9/W2-REMINT.md)。
- 普查、覆盖矩阵、门与 red-once:见 git 历史。
### 8.5 XFB scatter 留在 server
补丁循环需要"捕获前的字节",而拆分后权威影子归 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)。
补丁循环需要"捕获前的字节",而拆分后权威影子归 server(`StagedShadow`),所以 scatter 在 server 原地跑完;回程只有一条 `OnBufferWriteback`,载补好的整段,按 `SEG_EVENT` 容量的四分之一切片(`MGPipeBufferWritebackSliceBytes`),最后一片落地即蕴含之前每一片。孤儿目标(`HasDefinedContent == 0`)零填充后照常散射。`OnXfbScatterReady` 已删除(回调 10 → 9)。落地论证见 git 历史(espryt D1)。
@@ -69,4 +69,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 要按真实消费者重新裁定,不照抄本节([`docs/archive/notes/p8/README.md`](../../archive/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 要按真实消费者重新裁定,不照抄本节(见 git 历史)。
+1 -1
View File
@@ -92,4 +92,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)是第二波。细节 [`docs/archive/notes/p65/README.md`](../../archive/notes/p65/README.md),契约 `docs/archive/contracts/CONTRACT-P65.md`。
第一波(全 TCP 控制 + stream 数据)已落地;同机混搭(nd `auto` / sd)是第二波。细节与契约见 git 历史。
@@ -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`](../../archive/contracts/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` §1) |
T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `hasLiveHostWrites`);在每个 validate 点按 64 KiB 块推送可达的已映射 buffer,脏页追踪 + 哈希抑制只发变化的块(P5d);门 `PersistentCoherentMapScenario`。`mpr` 数的是每一次 `MapPersistent` 发射(铸成或拒绝都算),所以在 monolith 下也可断言。
@@ -42,7 +42,7 @@ TCP `--serve` 的 supervisor(`ServerMain.cpp` 的 `TcpSupervisor`,判定部
- 交付链(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 一退后台游戏就暂停(`docs/archive/contracts/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 一退后台游戏就暂停(见 git 历史)。一台设备同一时刻一个 server;一个会话一种表面模式(`SurfaceModeMismatch`);无显示的 server 对 ServerOwned 具名拒绝(`NoServerDisplay`)。
- 失窗:`surfaceDestroyed` 有界(3 s)阻塞,直到 apply 线程(批内每条记录前也检查)放掉后端表面并以 `Fatal{ServerWindowLost}` 闩住会话;server 继续监听。进程内会话结束时手动做进程退出本来白给的清理(Espryt twin 与单元影子、swap interval、Magma renderer)。
- `HeadlessGL` 的 fork 预检会产生孤儿 server:server 的 EOF 检测即时且无条件退出。
@@ -82,4 +82,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`、server 应用的控制记录 `ar`(applier_reset)/ `od`(object_death)、`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` 头部,那份清单是契约。
关闭时每站点一次全局 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`、server 应用的控制记录 `ar`(applier_reset)/ `od`(object_death)、`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 仍有效(见 git 历史 §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
+4 -4
View File
@@ -8,10 +8,10 @@
| 原节 | 主题 | 仍生效的规则 | 全文 |
|---|---|---|---|
| 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.1 | verb barrier 与诚实的同地址空间传输(P5) | 同地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`;reply 以记录序号为 slot id(R-3);等待看门狗 120 s 只判死锁,不是性能门 | git 历史 |
| 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(a) 的对照是常驻用例(`RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`、`PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`),不重跑 lane | 同上 |
| 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 对照 | [`docs/archive/notes/p5e/README.md`](../../archive/notes/p5e/README.md) |
| 17.5 | class-C verb 迁移(P5b) | 迁移顺序由动态普查决定;一次迁移 = 一个发射器 + 一个 sink 体;记录逐字带 GL 调用参数,未测量的形式按名拒绝(规则 D) | git 历史 |
| 17.6 | 共享地址空间访问归零(P5c) | server 端纹理 staged shadow;`SEG_EVENT` 是唯一反向通道;sink / twin 按记录句柄解析;`applier_reset` / `object_death` 控制记录;角色守卫 `Fatal{RoleViolation}` | git 历史 |
| 17.7 | run-ahead(P5e) | 规则 F:unbarriered 记录的 apply 不读任何 client 内存(违反即具名 Fatal,与 strict 开关无关);`gPipeInputs` 是 server 角色内存;`RunAheadArmed()` 在首次 caps 采纳时锁存,只能关不能开;`RUN_AHEAD=0` 是 A/B 对照 | git 历史 |
@@ -1,6 +1,6 @@
# 状态归属:session / context / share group / thread current(P14 设计)
> 为 anland 内置统一 server 而做。动因与否定项见 [`docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md`](../../archive/notes/anland/handoff-legacy-mobilegl-unified.md):不许用「每应用 fork 一个渲染进程」掩盖全局状态归属。本文行号对 `2f64ede8`(+ `4156ca6e` 文档提交),代码移动后以符号名检索。
> 为 anland 内置统一 server 而做。动因与否定项见 git 历史:不许用「每应用 fork 一个渲染进程」掩盖全局状态归属。本文行号对 `2f64ede8`(+ `4156ca6e` 文档提交),代码移动后以符号名检索。
## 结论
+1 -1
View File
@@ -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` 备用路线)见 [`docs/archive/notes/p65/README.md`](../../archive/notes/p65/README.md) 的"两边怎么配"。
- 电脑当 client、手机当 server 的跨机配置(起 server、设令牌、`adb forward` 备用路线)见 git 历史中的"两边怎么配"。
- 性能对比只用 Redmi `2f7cbe2e`,按 [`pin-verification-2026-09-07.md`](pin-verification-2026-09-07.md) 定频,reboot-clean、同热窗口、配对 A/B。P13 之后的性能工作以 `dev` 分支构建为「之前」基线(用户 2026-10-07)。
@@ -1,28 +0,0 @@
# 设备定频档案与核验
本分支性能 A/B 所用设备的定频方式与核验结果。规则:每次运行前后各跑一次 `pin_device.sh <serial> check`,DRIFT 的运行作废重跑;stock 值硬编码、不从已钉设备采样(否则一次 restore 会把钉频变成永久);温度门 40 °C;reboot-clean、同热窗口、两臂背靠背。
## 当前战役设备(2026-09-11 起,P4a 之后唯一用于 A/B 的设备)
| 设备 | SoC / GPU | 大核 | 小核 | GPU | 备注 |
|---|---|---|---|---|---|
| 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,见 [`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 使用)
结论:`bench.sh` 的 `/proc/ppm` + `/proc/gpufreq` 钉法在两台上都不存在(MT6993 已改 `/proc/gpufreqv2/`),所以用 `bench.sh --no-pin` + `tools/device_bench/pin_device.sh <serial> pin|unpin|check`(纯 adb + su;exit 0 PINNED / 1 DRIFT / 2 UNPINNED,UNPINNED 非零使 `check && measure` 不可能量到未钉设备)。`devices/*.env` 的 `PROFILE_VERIFIED=1` 只认证文件里的节点与钉值,不认证 `bench.sh` 能驱动它们;给 `bench.sh` 加 `PIN_STYLE` 仍未做。
| | 小米 24129PN74C(SM8750 / Adreno 830v2) | Oppo PLG110(MT6993 / Mali,gpufreqv2) |
|---|---|---|
| 大核 | policy6 → 1958400(精确 OPP;stock 1017600–2841600) | policy4 → 2000000(最近 OPP,+2.1%;stock 300000–3500000,厂商守护随后改回 3200000) |
| 小核 | policy0 → 1555200(精确;stock 556800–2745600) | policy0 → 1600000(+2.9%;stock 300000–2100000) |
| 额外 | — | policy7(单核 4.21 GHz)同钉 2000000,否则一个热线程就打破钉频 |
| 方法 | `scaling_min_freq = scaling_max_freq = 目标`;governor(walt / sugov_ext)不动;写序 min→`cpuinfo_min_freq`、max→目标、min→目标(min 高于当前 max 会被 cpufreq 钳住) | 同左 |
| GPU | `kgsl-3d0/{min,max}_pwrlevel = 0` → 1100 MHz(devfreq 路径封顶 1050);`gpuclk` 单位是 Hz | `/proc/gpufreqv2/fix_target_opp_index = 0` → 1716000 kHz,恢复写 −1;不用 ged `custom_*`(取 OPP 下标、且被 urcc-service 争写) |
| 温度门 | `cpuss-0-0`(zone13),idle 34.7 °C | `soc_max`(zone14),idle 36–37 °C,门只勉强可达 |
| 30 s 八路负载核验 | 7 个样本零漂移,47 → 52 °C | 7 个样本零漂移,53 → 62 °C |
| 陷阱 | `gpubusy` 打的是周期数,busy 百分比读 `gpu_busy_percentage` | `scaling_governor/min/max` 是 0660 system,必须经 su 读,否则空值看起来像在工作 |
两台核验后均已解钉并复核 UNPINNED,写过的节点全部恢复。未核验:Oppo GPU 钉在图形负载下的实际时钟(核验窗口 GPU 处于 power-collapse)、整场 bench 窗口内的钉频持久性、重启后状态、厂商热管理介入。
+2 -2
View File
@@ -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](../../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).
The fps numbers come from the 3-rep A/B in SCOREBOARD.md (section pwab).
The mechanism is analysed in depth in PRESENT-WAIT.md.
## Summary
-655
View File
@@ -1,655 +0,0 @@
# MobileVA: a vendor-agnostic VA-API driver on Android MediaCodec (plan, 2026-10-04)
> Status: **plan only. No code, no build and no device work has been done for it.** Facts in this
> note are tagged **[verified]** (observed on the device, or read from code in this repo, with the
> 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: [`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).
## 1. Goal and why
Hardware video decode in the anland KDE Plasma container comes from `/usr/lib/dri/msm_drm_drv_video.so`
today. That driver:
- is an unpackaged third-party binary ("DroidSpaces V4L2 VA-API driver 0.4.7", copied in on
2026-09-21). It comes from no pacman package and from nothing in the anland repo. **[verified]**
- is **Qualcomm-only**. It drives the kernel's V4L2 stateful decoder at a hardcoded `/dev/video32` with
Qualcomm-private modes and UBWC formats (STREAM_OUTPUT_MODE=SECONDARY, Q10C/TP10/Q12A). **[verified]**
- **CPU-copies** every decoded frame into NV12/P010 dma-bufs it allocated itself (`nv12_copy`).
**[verified]**
- loads only because the render node's DRM driver name is `msm_drm`. No `LIBVA_DRIVER_NAME` is
set. **[verified]**
Because neither the phone's EGL (no `EGL_EXT_image_dma_buf_import`) nor its Vulkan (no
`VK_EXT_external_memory_dma_buf`) imports a raw dma-buf, MobileGL then copies those foreign buffers a
**second** time on the CPU, into server YUV AHardwareBuffers (`SharedImages::ImportForeignYuv` /
`RefreshForeign`, 7218bacb). **[verified]** That costs about 31% of one server core at 720p30/60.
**[verified, 2026-10-04]**
So each frame takes two CPU copies, and the path works only on Qualcomm.
**Goal.** A libva driver of our own in the container. It forwards the compressed bitstream to the
MobileGL server process on Android, which decodes it with NDK `AMediaCodec` into AHardwareBuffers
that MobileGL owns. Decoded frames then reach GL with **zero CPU copies on any Android SoC**.
## 2. User decisions (binding)
| Topic | Decision |
|---|---|
| Location | A new top-level directory **`MobileVA/`** beside `MobileGL/`, with its own CMake targets. It is **never linked into the GL library**. It may reuse MobileGL's transport and shared-image registry. |
| Where decode runs | Inside the existing `:mobilegl` server process of the anland APK. |
| Codec priority | H.264, then VP9, then AV1, then HEVC (including 10-bit / P010; VP9 profile 2 and AV1 10-bit where the device supports them). **Only H.264 (milestone 1) is in scope for the first implementation.** |
| Old driver | Stays installed as an A/B fallback. Ours is the default. One documented knob switches back, and Chrome's GPU process must honour it too. |
| Clients | Chrome, mpv and GStreamer now; ffmpeg is the test harness. Firefox comes later. Nothing may block it or any other client: no app-name checks, no assumptions about Chrome's call order, and the VA semantics implemented honestly. |
| Vendor-agnostic | Only public Android NDK/SDK and Linux facilities: AMediaCodec, AImageReader, AHardwareBuffer, sync_file, dma-buf. Capabilities come from what the device's MediaCodec reports, never hardcoded limits. No vendor gralloc handles, no qcom/kgsl, no vendor format constants or vendor MediaFormat keys. |
| Zero copy | No CPU copy on the main path. An exported dma-buf's declared layout (fourcc, offsets, pitches, modifier) must be truthful, so an opaque or compressed layout is never exported as linear NV12. CPU readback (vaDeriveImage / vaGetImage / vaMapBuffer) may copy; that copy is inherent. |
| MobileGL | Core profile only, so no fixed-function test apps. |
## 3. What we already know
### 3.1 How clients drive VA
| # | Fact | Status |
|---|---|---|
| C1 | Chrome 153's VaapiVideoDecoder uses **ALLOCATE mode**. It calls `vaCreateSurfaces(n=1)` once per frame-pool frame (about 22 frames), then `vaExportSurfaceHandle` on each surface once: one fd, `SEPARATE_LAYERS`, R8 + GR88 (e.g. 1280x720, stride 1280, slice 736, uv_offset 942080). It imports each export as a **single NV12 EGLImage** bound to `GL_TEXTURE_EXTERNAL_OES` and caches the import. It never allocates decode buffers through GBM. | **[verified]** (memory `chrome-vaapi-yuv-foreign-copy`, DMD_VA_LOG traces) |
| C2 | Chrome accepts a VA driver whose vendor string is neither Intel nor AMD. The DroidSpaces driver gives `kVideoDecoderName = VaapiVideoDecoder` and `kIsPlatformVideoDecoder = true`. | **[verified]** (skill `probes/cdpmedia.py`) |
| C3 | Chrome's GPU sandbox is off by default in `mobilegl-startup.sh chrome` (`--disable-gpu-sandbox`). With the sandbox on, the GPU process may not call `socket()`/`connect()` after start-up. | **[verified]** (startup script comment, anland 41ec066) |
| C4 | The old driver exports P010 only as `COMPOSED_LAYERS` ("SEPARATE_LAYERS not implemented"). | **[verified]** |
| C5 | MobileGL imports **no single-channel dma-bufs** (R8, GR88). So KWin's own NV12 path, which splits a buffer into R8 + GR88 imports, is not served, and KWin advertises no YUV format over linux-dmabuf. | **[verified]** (skill SKILL.md) |
| C6 | mpv's `--hwdec=vaapi` GL interop imports **each layer separately** as R8 / GR88 EGLImages. With C5, it would fail on MobileGL today. | **[unverified]**; check with LIBVA_TRACE and mpv `-v` |
| C7 | ffmpeg, mpv (it uses ffmpeg's hwaccel), GStreamer `vah264dec` and Chrome pass H.264 slice data as the **whole slice NAL unit without its start code**: it starts at the NAL header byte and keeps the emulation-prevention bytes. | **[unverified]** (memory of the client sources) |
| C8 | Whether Chrome calls `vaSyncSurface` before it samples a surface, or relies on implicit dma-buf sync. ffmpeg syncs inside `vaDeriveImage`/`vaGetImage` downloads. | **[unverified]**; record with `LIBVA_TRACE` |
| C9 | Chrome reads `LIBVA_DRIVER_NAME` through libva. Chrome itself picks a driver name only for some Intel cases. | **[unverified]** |
| C10 | GStreamer's `va` plugin registers elements only for drivers on its allowlist unless `GST_VA_ALL_DRIVERS=1` is set. | **[unverified]** |
| C11 | `LIBVA_TRACE=/path` in libva logs every call with its parameters, for **either** driver. That makes it the A/B observation tool. | **[unverified]** for this libva build |
### 3.2 Platform and repo facts
| # | Fact | Status |
|---|---|---|
| P1 | Device: Lenovo TB321FU (Y700), Adreno 750, Android 15, kernel 6.1. Container: Arch Linux ARM, glibc. libva headers in the cross sysroot are **VA-API 1.24** (`~/sysroots/arch-kde-mgl/usr/include/va`). | **[verified]** |
| P2 | Server YUV shared images are AHBs: `Y8Cb8Cr8_420` / `YCbCr_P010`, usage `GPU_SAMPLED_IMAGE \| CPU_WRITE_OFTEN \| CPU_READ_RARELY`. Plane offsets come from `AHardwareBuffer_lockPlanes`. They are exported with modifier `DRM_FORMAT_MOD_INVALID`. A returning fd is identified by `(st_dev, st_ino)`, so a re-import of our own buffer is **zero-copy** (no foreign path). | **[verified]** (`MG_Remote/Server/SharedImageRegistry.cpp`, yuvprobe GBM NV12 case) |
| P3 | Espryt converts YUV to RGBA with `GL_EXT_YUV_target` (Adreno 750 has it). Magma converts with `VkSamplerYcbcrConversion` on the AHB external format. Both convert once per reader frame epoch. | **[verified]** (`DirectGLES.cpp ConvertYuvForSampling`, `WireYuvImage.cpp`) |
| P4 | The foreign-copy fallback is labelled. Its first use logs "a foreign ... dma-buf is sampled through image N, refreshed by a CPU copy", and `Image::ForeignSource::Copies` counts each copy. That is the proof hook for "our buffers never take the fallback". | **[verified]** (code) |
| P5 | The YUV conversion paths call `RefreshForeign` but, from a read of the code, do **not** wait on the image's write fence (`DupWriteFence`) the way the RGBA shared-image path does. | **[unverified]**: code read only; confirm before relying on write fences for YUV |
| P6 | The client shared-image C ABI (`MG_Impl/SharedImageApi.h`) makes its round trips on the **process's GL client session**, the same ring its GL calls ride, so "like any GL call they must not race the GL calls of another thread". A VA driver therefore **cannot** use it from a decode thread. It needs its own connection. | **[verified]** (header comment) |
| P7 | The server listener pairs a client's two connections by a `PairBind` first frame (`MG_Remote/Server/PairAcceptor.h`) and refuses any other first frame by name. | **[verified]** (code) |
| P8 | A hung shader stalls every GPU user on the device, and the server process is shared by every desktop client. Process-global state in the server is a cross-session hazard (memory `multi-session-server-cross-session-hazards`). | **[verified]** |
| P9 | NDK `AMediaCodecStore` / `AMediaCodecInfo` (the codec list) need API 36. On Android 15 (API 35) the codec list comes from Java `MediaCodecList` through JNI. | **[unverified]**: from NDK headers; check `__ANDROID_API__` guards |
| P10 | `JNI_GetCreatedJavaVMs` is callable from NDK code on API 31+. That lets a lazily loaded .so reach the app's JavaVM. | **[unverified]** |
| P11 | Codec2 decoders give each output buffer the `presentationTimeUs` of the input that produced it. | **[unverified]**; spike S2 |
| P12 | `AImage_deleteAsync(image, releaseFence)` (API 26) returns a codec output buffer once a GPU read finishes, with no CPU wait. | **[unverified]** |
## 4. Architecture
```
container (glibc) Android :mobilegl process (bionic)
─────────────────────────────────────────── ──────────────────────────────────────────────────────
client (Chrome GPU proc / mpv / gst / ffmpeg)
└─ libva ─ /usr/lib/dri/mobileva_drv_video.so libMobileGL.so (server) libMobileVAService.so
• VA objects (config/context/surface/buffer) listener @anland-mobilegl ──▶ ServiceBind "mobileva"
• H.264 header rebuild → Annex-B access unit SharedImageRegistry ◀─────── C ABI: allocate decode-
• own socket connection + reader thread (AHB images, fences) target YUV images, fences
│ • per-connection session
└──── unix:@anland-mobilegl (own connection, ───────────────────────────────▶ • AMediaCodec (async) →
MobileVA protocol, fds via SCM_RIGHTS) AImageReader (PRIVATE)
• copy thread: own EGL ctx,
GL consumer (Chrome/mpv via MobileGL EGL) ── imports exported fd ──▶ registry GL_EXT_YUV_target Y2Y copy
Identify(inode) → same AHB, zero-copy codec AHB → surface AHB
```
### 4.1 Directory and targets
```
MobileVA/
CMakeLists.txt option MOBILEVA_BUILD_DRIVER (Linux), MOBILEVA_BUILD_SERVICE (Android), MOBILEVA_BUILD_TESTS (host)
Common/ protocol (POD, versioned, length-prefixed; SCM_RIGHTS helpers), BitReader/BitWriter,
exp-Golomb, emulation-prevention insert/strip, capability types
Codec/H264/ slice-header pre-parse, SPS/PPS synthesis from VA params, access-unit assembly
(pure C++, no libva/Android deps beyond va.h structs; host-testable)
Driver/ mobileva_drv_video.so: __vaDriverInit_1_<minor>, the VA vtable, objects, connection
Service/ libMobileVAService.so: session, MediaCodec decoder, AImageReader, GPU copy, caps (JNI)
Test/ gtest host tests
```
- The top-level `CMakeLists.txt` adds `MobileVA/` behind an option. No MobileGL target links any
MobileVA target.
- The driver has no link dependency on libva (drivers implement `VADriverVTable`) and none on
libMobileGL. It reads the endpoint from `/etc/mobilegl/client.conf` (`MOBILEGL_IPC_CONTROL`) the way
the GL client does, overridable by `MOBILEVA_ENDPOINT`.
- The service links `mediandk`, `nativewindow`, `EGL`, `GLESv3`, `android`, `log`. It reaches the
registry through a narrow C ABI that libMobileGL.so (server build) exports (§4.3).
### 4.2 Where the H.264 rebuild runs: in the client
The driver rebuilds the headers and assembles a complete Annex-B access unit **in the client
process**. The server receives a codec-generic request: `Decode{context, target surface, seq, config
bytes if changed, AU bytes, flags}`.
**Why:**
- A parser bug then fails one client's decode, not the shared server process that every desktop client
depends on (P8).
- The rebuild is pure and host-testable.
- The wire protocol stays codec-agnostic for VP9/AV1/HEVC.
All parsing uses bounds-checked readers and returns a VA error; it never asserts.
### 4.3 Transport, sessions and the server hooks
- **Connection.** One connection per `VADriverContext` (one per `vaInitialize`), opened at
`vaInitialize`. Chrome opens its VA display before any sandbox (C3). The first frame is a new
`CtrlMsg` **`ServiceBind{name:"mobileva", version}`**, appended to `protocol.fbs`'s union. The
existing `PairAcceptor` hands such a connection to a registered service by name, and refuses
unknown names by name.
- This is the only MobileGL transport change. Everything after the first frame is MobileVA's own
protocol, defined in `MobileVA/Common`.
- Fallback if the acceptor change turns out invasive: the service listens on its own abstract socket
`@anland-mobilegl-va` in the same process. It still uses the same registry.
- **Protocol (draft).** Length-prefixed frames `{magic, version, type, length, requestId}`; fds ride
`SCM_RIGHTS` on the same socket.
| Request | Answer |
|---|---|
| `Hello{version, pid}` | `Welcome{version, caps[]}` |
| `CreateContext{codec, profile, w, h}` | `{ctx}` or error |
| `CreateSurfaces{w, h, fourcc, n}` | per surface `{id, fd, offsets, pitches, modifier, allocation size}` |
| `ImportSurface{fd, layout}` | `{id}`, only for buffers the registry identifies as its own; anything else gets `VA_STATUS_ERROR_UNSUPPORTED_MEMORY_TYPE` |
| `DestroySurfaces` / `DestroyContext` | |
| `Decode{...}` | no answer on success; errors come back as events |
| `Sync{surface}` | answered when the surface's latest decode is complete, or on timeout or error |
| `QueryStatus{surface}` | answered at once |
| `Readback{surface}` | the CPU fallback for `vaGetImage` (§6.4) |
| server events | `SurfaceDone{id, gen, status}`, `ContextError{ctx, reason}` |
A reader thread in the driver dispatches answers by request id, so a blocking `vaSyncSurface` on one
thread never stalls `vaEndPicture` on another. No assumption is made about which threads a client
uses.
- **Handles are per connection.** Surface, context and buffer ids live in maps owned by the
connection, never in globals (P8). Each connection owns a `SharedImages::SessionHolder` for the
images it allocated. A GL session that imports an exported fd takes its own `ImageRef`, so images
outlive the VA connection while they are still sampled.
- **Server C ABI** (new, in `MG_Remote/Server`, exported from the server libMobileGL.so):
- `mobilegl_server_register_service(name, version, accept_fn, user)`
- `mobilegl_server_holder_create/destroy`
- `mobilegl_server_image_allocate(holder, w, h, fourcc, flags, out{id, AHardwareBuffer*, fd, layout})`
with a flag `DECODE_TARGET` that adds `GPU_COLOR_OUTPUT` to the YUV usage
- `mobilegl_server_image_release(holder, id)`
- `mobilegl_server_image_begin_write(id)` / `publish_write(id, sync_file)` / `dup_read_fences(id, fds, cap)`
- **Loading.** The server `dlopen`s `libMobileVAService.so` from its own directory (`dladdr`) on the
first `ServiceBind{"mobileva"}`. If the library is missing, it refuses cleanly. The service reaches
the JavaVM with `JNI_GetCreatedJavaVMs` (P10) for the codec list. No Java change is needed in the
APK; anland only packs the extra `.so` into jniLibs.
- **Failure isolation.**
- The driver sends with `MSG_NOSIGNAL` and treats `EPIPE`/EOF as "server gone": every pending and
future call returns `VA_STATUS_ERROR_OPERATION_FAILED` (Sync: `..._DECODING_ERROR`). It never
aborts.
- Objects created after a loss reconnect lazily, so a long-lived VADisplay (Chrome keeps one per
GPU process) can recover.
- Server side, a malformed request ends that connection only.
- **The service never touches backend state.**
- It has its own EGL context on its own thread, never in the backend's share group.
- It **never calls `eglTerminate`** on the default display, which Espryt shares in-process.
- Its only interface with Espryt/Magma is the registry's sync_file fences.
## 5. Capabilities
At service load, enumerate `MediaCodecList(REGULAR_CODECS)` over JNI. For each **decoder** that is
`isHardwareAccelerated()` (API 29) and not an alias, and for each supported type (`video/avc`, later
`video/x-vnd.on2.vp9`, `video/av01`, `video/hevc`), record:
- the codec name
- `profileLevels`
- `VideoCapabilities` width/height ranges and alignment
- `getMaxSupportedInstances()`
- the `FEATURE_LowLatency` (API 30) and `FEATURE_AdaptivePlayback` features
Log the table once.
Translation to VA, with host tests:
| MediaCodec AVC profile | VA profile |
|---|---|
| `AVCProfileConstrainedBaseline` or `AVCProfileBaseline` | `VAProfileH264ConstrainedBaseline` |
| `AVCProfileMain` | `VAProfileH264Main` |
| `AVCProfileHigh` | `VAProfileH264High` |
| `AVCProfileHigh10` | `VAProfileH264High10`, only once P010 output works |
- Entrypoint: `VAEntrypointVLD` only.
- `VAConfigAttribRTFormat`: `YUV420` (`YUV420_10` with a 10-bit profile).
- `VAConfigAttribMaxPictureWidth/Height` and `vaQuerySurfaceAttributes` `MaxWidth/MaxHeight`: the
codec's upper bounds.
- Surface pixel formats: NV12 (P010 later).
- Memory types: `VA` and `DRM_PRIME_2`.
- If no hardware decoder exists for a type, its profiles are simply not listed.
- If `vaCreateContext` exceeds `getMaxSupportedInstances`, or `AMediaCodec_configure` fails, it returns
`VA_STATUS_ERROR_ALLOCATION_FAILED`, so the client falls back to software for that stream.
## 6. The four hard problems
### 6.1 Parsed parameters vs elementary stream (H.264)
VA gives `VAPictureParameterBufferH264`, `VAIQMatrixBufferH264`, and per slice
`VASliceParameterBufferH264` plus the slice data. That slice data is the whole slice NAL (C7, to
confirm). MediaCodec wants Annex-B access units with SPS/PPS. The client never hands VA the original
SPS/PPS, so we **synthesize** them.
1. **Slice NALs go in verbatim** with a `00 00 00 01` start code; a start code already present is not
duplicated. Slices with `VA_SLICE_DATA_FLAG_BEGIN/MIDDLE/END` are concatenated. One `vaEndPicture`
produces one access unit.
2. **Pre-parse each slice header** with a bounds-checked RBSP reader over a de-escaped copy of the
first ~64 bytes. The parse is driven by fields we know from the VA picture parameters:
- `first_mb_in_slice`, `slice_type`, `pic_parameter_set_id`, `frame_num` (u(v),
log2_max_frame_num), `field_pic_flag`/`bottom_field_flag`, `idr_pic_id`,
`pic_order_cnt_lsb`/`delta_pic_order_cnt_bottom`, `delta_pic_order_cnt[0..1]`,
`redundant_pic_cnt`, `direct_spatial_mv_pred_flag`, `num_ref_idx_active_override_flag`.
- We need **`pic_parameter_set_id`** to give our PPS the same id the slices name.
- We need **`num_ref_idx_active_override_flag`**: VA gives only the *effective*
`num_ref_idx_l0/l1_active_minus1` per slice, and the PPS defaults
(`num_ref_idx_l{0,1}_default_active_minus1`) are **not** in VA. PPS default = the effective value
of a slice with override = 0; any value if every slice overrides.
3. **SPS** (id 0):
- `profile_idc` and constraint flags come from the config's VAProfile: CB = 66 with
constraint_set0+1, Main = 77, High = 100, High10 = 110.
- For profile ≥ 100: `chroma_format_idc` and `bit_depth_*_minus8` from VA,
`qpprime_y_zero_transform_bypass_flag` = 0, `seq_scaling_matrix_present_flag` = 0 (scaling lists
go in the PPS, see 4).
- From VA: `log2_max_frame_num_minus4`, `pic_order_cnt_type`, `log2_max_pic_order_cnt_lsb_minus4`,
`delta_pic_order_always_zero_flag`, `max_num_ref_frames = num_ref_frames`,
`gaps_in_frame_num_value_allowed_flag`, `pic_width_in_mbs_minus1`, `frame_mbs_only_flag`,
`mb_adaptive_frame_field_flag`, `direct_8x8_inference_flag`.
- **Gotcha:** VA's `picture_height_in_mbs_minus1` is the *frame* height in MBs, so
`pic_height_in_map_units_minus1 = ((h+1) >> !frame_mbs_only) - 1`.
- `frame_cropping_flag` = 0. VA surfaces are coded-size; the client crops.
- `level_idc` is **not in VA**. Pick the smallest level in Table A-1 whose MaxFS ≥ frame MBs,
MaxDpbMbs ≥ MBs × max(num_ref_frames, 1) and MaxMBPS ≥ MBs × 60, capped at the codec's maximum
level. This is a heuristic (open question Q3).
- **VUI** carries only `bitstream_restriction_flag` = 1 with `max_num_reorder_frames` = 0,
`max_dec_frame_buffering` = max(num_ref_frames, 1), motion-vector limits at their maximum and no
byte or bit limits. This is deliberate; see §6.2. Colour description is irrelevant to the decoded
samples, so it is not emitted.
- **POC type 1** needs `offset_for_non_ref_pic`, `offset_for_top_to_bottom_field` and the
`offset_for_ref_frame[]` cycle, and **none of them are in VA**. VA passes resolved POCs instead.
POC drives temporal direct and implicit weights, so it changes pixels. M1 **refuses** POC type 1
at `vaEndPicture` (`VA_STATUS_ERROR_DECODING_ERROR`, logged once); see TODO H264-2.
4. **PPS** (id from the slice):
- `seq_parameter_set_id` = 0.
- From VA: `entropy_coding_mode_flag`, `bottom_field_pic_order_in_frame_present_flag` (VA
`pic_order_present_flag`), `weighted_pred_flag`, `weighted_bipred_idc`, `pic_init_qp_minus26`,
`pic_init_qs_minus26`, `chroma_qp_index_offset`, `deblocking_filter_control_present_flag`,
`constrained_intra_pred_flag`, `redundant_pic_cnt_present_flag`.
- `num_slice_groups_minus1` = 0. FMO is refused.
- Then, when any of them is not the default: `transform_8x8_mode_flag`, the scaling lists and
`second_chroma_qp_index_offset`.
- **Scaling lists:** when the IQ matrix is not all-flat-16, set `pic_scaling_matrix_present_flag`
= 1 and write all 6 (+2 with 8x8) lists **explicitly**. VA gives effective lists, so the
fall-back rules never matter.
- **Gotcha (Q4):** whether `VAIQMatrixBufferH264` lists are in zigzag or raster order differs in our
memory of the client sources. Verify with an x264 `--cqm jvt` stream and the A/B driver before
trusting either.
5. **Emit** SPS/PPS before the access unit only when their bytes differ from the last sent with the
same id, and again after every (re)configure or flush.
6. **Configure** MediaCodec lazily at the first `vaEndPicture`, when SPS/PPS exist: `csd-0` = SPS,
`csd-1` = PPS, plus width/height. Re-configure, after draining the old codec, when the coded size,
profile or bit depth changes (§6.2).
7. **M1 refuses** (logged once, `VA_STATUS_ERROR_UNSUPPORTED_PROFILE` or `_DECODING_ERROR`):
- field pictures (`field_pic_flag` = 1; MBAFF frames are fine)
- FMO/ASO
- chroma formats other than 4:2:0
- POC type 1
### 6.2 Output order and surface mapping
- **Tagging.** Every queued input carries `presentationTimeUs = seq`, a per-context monotonic decode
counter. The context keeps `seq → {surface id, surface generation}`. An output buffer's
`presentationTimeUs` names its surface (P11, to confirm). MediaCodec's own output **order is
irrelevant**: we never present in its order. Clients that reorder (Chrome, ffmpeg) display in their
own order.
- **Release.** Release every output with `render=true` into the AImageReader. Match the acquired
`AImage` to the released buffer FIFO, asserting `AImage_getTimestamp == pts × 1000`.
- **Latency is the deadlock risk.** ffmpeg (framemd5) downloads a frame, which calls `vaSyncSurface`
on its only thread, after its own reorder delay. If MediaCodec holds that frame waiting for more
input, nothing more comes: deadlock. Mitigations, in order:
1. The VUI rewrite (`max_num_reorder_frames = 0`). A conforming decoder then bumps every picture as
soon as it is decoded, so decode-order output gives the lowest latency. This does not change
samples: POC values in slice headers are untouched.
2. `KEY_LOW_LATENCY = 1` where `FEATURE_LowLatency` is listed, plus `KEY_PRIORITY = 0` and a high
`KEY_OPERATING_RATE`.
3. **Spike S2 must measure input→output lag** on the device (H.264 with B-pyramids). If any
hardware decoder still holds frames, document it and bound `vaSyncSurface` by a timeout
(`VA_STATUS_ERROR_TIMEDOUT`, default 2 s; `vaSyncSurface2` gets the client's own timeout). Never
wait forever.
- **Surface state.** Each surface has `pendingGen`, which `Decode` increments, and `doneGen`, set when
its copy fence is published. `vaSyncSurface` waits for `doneGen ≥ pendingGen` *as of the call*. It
answers only after the copy's fence has **signalled**, because after `vaSyncSurface` a client may
`mmap` the exported dma-buf. `vaQuerySurfaceStatus` returns `VASurfaceRendering` or
`VASurfaceReady` without blocking.
- **Readers that never sync** (C8). At `Decode` the service calls `begin_write(image)`, which opens a
pending write generation with no fence yet. At copy completion it calls `publish_write(image,
fence)`.
- **TODO (P5):** both backends' YUV sampling must honour the write fence and, for a pending write
without a fence, CPU-wait on the registry with a bound (e.g. 100 ms, logged once). That makes the
GL path correct whether or not a client syncs.
- It does not deadlock: the decode input comes from the client's VA connection (another server
thread), not from the GL session that waits.
- **Lost outputs.** If an output with `seq = N` arrives while older entries are still pending, the
decoder dropped them (decode-order output is expected). Complete them with
`VA_STATUS_ERROR_DECODING_ERROR` (`vaQuerySurfaceError`), log, and never leave a waiter hanging.
- **Seek / flush.** VA has no flush; clients restart at an IDR. Feed it as is; MediaCodec's references
reset at the IDR. Late outputs of pre-seek frames still land in their own surface generation. A
surface re-targeted since then has a newer generation, so the stale output is **dropped, not
copied**. The generation check matters because "last write wins" is not guaranteed.
- **Resolution change.**
- A new coded size inside the same context drains the old codec: queue EOS, wait for all outputs,
copy them. Then configure a new one. The change happens at an IDR, so no references are lost.
- Chrome and ffmpeg usually recreate the VA context instead, which simply creates a new codec.
- **Destroy mid-stream.** `vaDestroyContext` stops and deletes the codec, drops pending outputs, and
completes all waiters with an error. `vaDestroySurfaces` on a surface with a pending decode marks the
entry dead; its output is discarded.
- **Hidden frames** do not exist in H.264. VP9/AV1 notes are in §9.
### 6.3 Buffer ownership: zero-copy and truthful export
**Constraint.** VA surfaces are created up front and exported once, and clients cache the import (C1).
MediaCodec decodes into buffers that the AImageReader BufferQueue allocates and picks per frame. No
public API lets us hand MediaCodec our own buffers.
| Option | Verdict |
|---|---|
| **A. Aliasing**: re-point a surface's shared image at the codec's output AHB | **Rejected.** It is correct only for importers that go through MobileGL by inode. A client that `mmap`s the exported fd (GStreamer downstream of DMABuf caps, ffmpeg `hwmap`, a future direct KWin/Firefox import) would read the surface's own, stale memory. The codec's buffer is typically opaque or compressed (UBWC-like), so it can never be exported as linear NV12 either. |
| **B. Server GPU copy** into the surface's own linear NV12 AHB | **Chosen.** Correct for every consumer, keeps the export truthful, and costs one small GPU pass (1080p NV12 ≈ 3 MB read + 3 MB written; 4K60 ≈ 1.5 GB/s, small against the GPU's bandwidth). Measure it in M1. |
**Copy path (B):**
- The AImageReader uses `AIMAGE_FORMAT_PRIVATE` with usage `GPU_SAMPLED_IMAGE`, so the codec keeps its
preferred (possibly compressed) layout, and `maxImages` ≈ 6.
- The copy thread owns an EGL context (GLES 3) and imports the codec AHB (`AImage_getHardwareBuffer`
→ `eglGetNativeClientBufferANDROID` → `EGL_NATIVE_BUFFER_ANDROID` → `GL_TEXTURE_EXTERNAL_OES`). It
imports the surface AHB the same way, as an FBO attachment.
- It draws with **`GL_EXT_YUV_target`**: a `__samplerExternal2DY2YEXT` source written to a
`layout(yuv) out` target, which is a raw YUV→YUV copy with no colour conversion. NEAREST sampling
gives every 2x2 quad the same chroma sample, so the target's chroma subsampling writes it back
exactly. 8-bit and 10-bit values survive the float round trip. **Bit-exact by construction**, to be
proven by spike S3.
- Copy region = min(surface, codec crop rect); the rest of the surface is untouched.
- **Write after read.** Before overwriting a surface, take `dup_read_fences` and make the GL context
wait on them (`EGL_ANDROID_native_fence_sync` import + `eglWaitSyncKHR`).
- **Write publish.** Create an EGL native fence, `publish_write(image, fence)`, and
`AImage_deleteAsync(image, dup(fence))` so the codec buffer returns as soon as the GPU has read it.
No CPU wait on the main path. The only CPU wait is the one `vaSyncSurface` does for its answer.
- **Decode-target images** are the registry's YUV images plus `GPU_COLOR_OUTPUT` (new allocation flag).
Check `AHardwareBuffer_isSupported` with exactly that usage. CPU usage stays on, which keeps
allocators off compressed layouts and makes `lockPlanes` meaningful.
- **Truthful export.** Once per format at service start, a **layout probe** checks that the CPU view
equals the raw dma-buf view: write a pattern through `lockPlanes` and read it back through `mmap` of
the exported fd at the claimed offsets (spike S3).
- Pass: surfaces are exported as `DRM_FORMAT_NV12`, two layers or one composed object, with the
probed offsets and pitches and modifier **`DRM_FORMAT_MOD_LINEAR`**. MobileGL's importer already
accepts LINEAR or INVALID.
- Fail: export with `DRM_FORMAT_MOD_INVALID`, and route `vaDeriveImage` to "unsupported" so clients
use `vaGetImage`, which the server serves by `Readback`.
- **Export flags.** `vaExportSurfaceHandle` honours `SEPARATE_LAYERS` (R8 + GR88 / R16 + GR1616) and
`COMPOSED_LAYERS` (NV12 / P010) for every client. It returns a `dup` of the surface's fd each call.
- **Fallbacks when there is no `GL_EXT_YUV_target`** (non-Adreno devices; availability unverified):
1. A Vulkan pass: read with `VkSamplerYcbcrConversion` in `RGB_IDENTITY` / full range / NEAREST, and
write through per-plane views of the surface AHB, if it maps to `G8_B8R8_2PLANE_420_UNORM` with
the needed usage.
2. A **labelled, counted CPU copy** from a `YUV_420_888` + `CPU_READ_OFTEN` AImageReader. Never
silent; logged once with the reason. Chosen at service start and reported in the caps log.
- **Proof of "no CPU copy on the GL path."**
- The server log must have no "foreign ... refreshed by a CPU copy" line (P4) during playback.
- MobileVA's stats line (`MOBILEVA_STATS=1`, every 5 s per context) shows `decoded`, `gpu_copies`,
`cpu_copies = 0` and `readbacks = 0`.
### 6.4 CPU access (vaDeriveImage / vaGetImage / vaMapBuffer)
- `vaDeriveImage` (only with a passing layout probe) returns an NV12 `VAImage` whose buffer maps the
surface's dma-buf in the client (`mmap`, `DMA_BUF_IOCTL_SYNC` START/END around map/unmap). It
implies a `vaSyncSurface`.
- `vaGetImage` copies from that mapping, or from `Readback` when no mapping is allowed.
- Parameter and slice buffers are client memory only. `vaMapBuffer` on them is plain memory.
- `vaPutImage` / `vaPutSurface` / VPP / encode: `VA_STATUS_ERROR_UNIMPLEMENTED`.
## 7. A/B knob, install, environment
- **Install.**
- `xdeploy.sh` installs `/usr/lib/dri/mobileva_drv_video.so` by rename-into-place, beside the old
`msm_drm_drv_video.so`, which stays.
- The APK packs `libMobileVAService.so` next to `libMobileGL.so`.
- The driver exports the init symbol for the sysroot's libva (`__vaDriverInit_1_24`, built from
`VA_DRIVER_INIT_FUNC`). libva also tries lower minors.
- **Default and knob.**
- `/etc/environment.d/10-mobilegl.conf` and `/etc/profile.d/mobilegl.sh` set
`LIBVA_DRIVER_NAME=mobileva` and `GST_VA_ALL_DRIVERS=1` (C10).
- The one documented knob is `MOBILEGL_VA_DRIVER`, which `mobilegl-startup.sh` and a helper
`mobilegl-va-driver {mobileva|msm_drm}` (it rewrites both files) turn into `LIBVA_DRIVER_NAME`.
`msm_drm` is the old driver. A per-process `LIBVA_DRIVER_NAME=msm_drm mpv ...` works too.
- `mobilegl-startup.sh chrome` exports the chosen name explicitly, so the GPU process (which
inherits the browser's environment) uses it (C9).
- `MOBILEGL_CHROME_VIDEO_DECODE=software` keeps its meaning.
- **Proof of which driver a process uses:** `grep _drv_video /proc/<pid>/maps`; the driver's
`vaQueryVendorString` ("MobileVA (MediaCodec <codec name>)"); `MOBILEVA_TRACE=1` logs every VA call.
- **Render node.** The driver never `ioctl`s the DRM fd libva hands it, and accepts DRM, Wayland and
X11 display types (Chrome uses DRM; mpv may use Wayland).
## 8. Verification plan (per codec, on the device, both backends)
**Bit-exactness.**
- Generate streams in the container with ffmpeg `libx264`:
- Baseline / Main / High
- CAVLC (`-coder 0`), weighted prediction (`-weightp 2`), B-pyramid with `-bf 3 -refs 4`
- multiple slices (`-slices 4`), custom CQM (`-x264opts cqm=jvt`)
- odd sizes (1278x718), long GOP (`-g 600`)
- a mid-stream resolution change: two encodes concatenated into `.ts` with `-c copy`
- For each: `ffmpeg -hwaccel vaapi -hwaccel_device /dev/dri/renderD128 -i X -pix_fmt nv12 -f framemd5 hw`
against `ffmpeg -i X -pix_fmt nv12 -f framemd5 sw`.
- Expect identical hashes. Report every mismatch. Repeat with `LIBVA_DRIVER_NAME=msm_drm` (A/B).
**vainfo.** `vainfo --display drm --device /dev/dri/renderD128` profiles and entrypoints equal the
service's logged MediaCodec caps table, nothing more.
**mpv.**
- `--hwdec=vaapi-copy` correct: render to a raw file (`--o`) and framemd5 it against software.
- `--hwdec=vaapi` (GL interop on MobileGL) plays with correct colours. Prerequisite: TODO R3,
per-plane R8/GR88 import (C5/C6).
**GStreamer.** `filesrc ! qtdemux ! h264parse ! vah264dec ! videoconvert ! checksumsink` equals the
same pipeline with `avdec_h264`.
**Chrome.**
- `cdpmedia.py` shows `VaapiVideoDecoder` / `kIsPlatformVideoDecoder = true`, and
`/proc/<gpu>/maps` shows `mobileva_drv_video.so`. YouTube is forced to AVC by a `CDP_EXPR` override
of `MediaSource.isTypeSupported`; also local files.
- Colours exact: a pattern clip with known BT.709 limited-range patches, pixel-checked in screenshots.
- No flicker: two screenshots plus a screen recording.
- No GPU-process restarts: no `exit_code` in Chrome's log.
- Seeking works: repeated `video.currentTime = t`.
- `getVideoPlaybackQuality()` dropped/total frames at 720p60, 1080p60 and 4K60 (local H.264 files;
YouTube AVC only goes to 1080p).
**Cost.**
- Server-process CPU (`/proc/<pid>/stat` over 30 s, skill `device/measure.sh`) and the Chrome GPU
process's CPU, for ours vs `msm_drm` on the same content.
- No foreign-copy log line and `cpu_copies = 0` (§6.3).
- GPU copy time per frame (`GL_EXT_disjoint_timer_query` if present, else fence-to-fence).
**Desktop.** Still fine on both backends: `desktop-verify.sh`, scrolling, resize, lock/unlock
(`kscreenlocker_greet --testing`), panels.
**Host tests (WSL archlinux, clang, `ctest -E DirectVulkan`, never two suites at once).**
- Bit writer/reader, exp-Golomb, emulation prevention.
- Slice-header pre-parse on real x264 slices.
- SPS/PPS synthesis: golden bytes for fixed VA inputs. Also a round trip: parse a real stream's
SPS/PPS into VA structs the way a client would, rebuild, re-parse, and compare every semantic field
except VUI.
- Surface/order mapping state machine: reorder, drops, generation-stale outputs, timeouts, destroy
mid-stream.
- Protocol encode/decode round trips and malformed-frame rejection.
- Capability translation.
Device rules: battery ≥ 40%, USB serial HA27Q3LQ, leave the device on the merged build, on Magma, with
Plasma up.
## 9. Milestone TODO list
### M1: H.264, small and simple (the only milestone in scope now)
**Spikes first.** Throwaway probes; keep them as skill probes only if they stay useful.
- **S1.** `LIBVA_TRACE` with the **old** driver for ffmpeg, mpv (both hwdec modes), GStreamer and
Chrome: call order, threads, whether and when `vaSyncSurface` comes, slice-data framing, export
flags. Settles C6–C11.
- **S2.** NDK MediaCodec latency probe (adb-shell ELF). Feed an x264 B-pyramid stream as is vs with
the VUI rewrite, ± `KEY_LOW_LATENCY`, through an AImageReader. Log input→output lag in frames and
check pts propagation (P11). **Go/no-go for the design in §6.2.**
- **S3.** Y2Y copy probe: codec output (PRIVATE) → `GL_EXT_YUV_target` copy into an NV12 AHB with
`GPU_COLOR_OUTPUT | CPU_*`. Compare bytes with a ByteBuffer-mode decode of the same frame. Also the
layout probe (lockPlanes vs `mmap`). Settles §6.3 and the truthful-export check.
**Then:**
1. `MobileVA/` skeleton + CMake options, host test target; nothing links into MobileGL.
2. `Common/`: protocol, framing, SCM_RIGHTS, bit I/O, emulation prevention + tests.
3. `Codec/H264/`: pre-parse, SPS/PPS synthesis, AU assembly, change detection + golden tests.
4. MobileGL hooks:
- `ServiceBind` in `PairAcceptor` and a service registry
- the server C ABI (§4.3)
- the `DECODE_TARGET` allocation flag
- **YUV sampling waits on write fences and on pending writes** (P5), in both backends
- tests in `MG_Test` (SharedImageTest, pairing tests)
5. `Service/`: lazy load, JNI caps, session per connection, codec + reader per context, seq→surface
map, copy thread + fences, Sync/Query/Readback, stats, failure isolation.
6. `Driver/`: the vtable, objects, connection + reader thread + lazy reconnect, export/derive/get
image, `MOBILEVA_TRACE`.
7. Packaging: anland jniLibs, `xbuild.sh`/`xdeploy.sh` install, environment files, knob + helper,
`mobilegl-startup.sh chrome`.
8. Verification (§8) on Espryt and Magma, with numbers.
9. Land:
- README (knobs, A/B switch)
- skill SKILL.md / reproduce.md (build + install the driver and service on a new machine)
- memory files for non-obvious findings
- remove worktrees and build dirs
### M2: VP9 (TODO, not started)
- VA VP9 slice data is the **whole frame** (uncompressed header + compressed header + tiles), so there
is no header rebuild. Feed each frame as one input; clients split superframes (unverified per
client).
- **Profiles:** 0 (8-bit 4:2:0), 2 (10-bit → P010). Profiles 1/3 (4:4:4) only if MediaCodec lists
them.
- **Hidden frames** (`show_frame = 0`, typically the alt-ref).
- The target surface must hold the hidden frame, because the client later displays it by
`show_existing_frame` **without** a VA call (Chrome's VP9 decoder does this; unverified for
others). MediaCodec never outputs a hidden frame.
- Plan: after the hidden frame, queue a **synthesized 1-byte `show_existing_frame` header**
(frame_marker, profile bits, `show_existing_frame = 1`, `frame_to_show_map_idx` = a slot from the
frame's `refresh_frame_flags`). Map its output to the hidden frame's surface.
- **Gotcha to verify first:** in some decoders, showing an existing frame may update
`last_show_frame` / previous-frame MV state, which feeds `use_prev_frame_mvs` of the next frame.
Injecting between a hidden ARF and the next frame could then change pixels. Test with libvpx ARF
streams and framemd5. If it breaks, inject only after the next shown frame, or keep the hidden
frame's surface pending until a later output can be routed to it (open question Q6).
- A client-submitted `show_existing_frame` (some clients do submit it) maps to "copy the slot's last
known surface" with no codec input.
- Resolution changes on non-key frames (reference scaling) are legal in VP9; adaptive playback needs
`KEY_MAX_WIDTH/HEIGHT` = context size.
### M3: AV1 (TODO, the hardest)
- VA AV1 slice data carries **tile data only** (tile-group OBU payloads, with per-tile
offsets/sizes in `VASliceParameterBufferAV1`). The **sequence header OBU, frame header OBU and tile
group framing must all be synthesized** from `VADecPictureParameterBufferAV1`.
- **Sequence header:**
- From VA: profile, `still_picture`, `use_128x128_superblock`, the tool-enable flags,
`enable_order_hint` + `order_hint_bits`, bit depth, `mono_chrome`, colour range, subsampling,
chroma position, `film_grain_params_present`.
- Chosen: one operating point (idc 0), `seq_level_idx` from size (open question Q3),
`frame_id_numbers_present_flag = 0`, `reduced_still_picture_header = 0`, no timing/decoder-model
info.
- **Frame header:** the full `uncompressed_header()` from VA:
- frame type, show/showable flags, `error_resilient_mode`, `disable_cdf_update`, screen content /
integer MV, `frame_size_override_flag = 1` with explicit sizes, render size, superres,
`allow_intrabc`, explicit `ref_frame_idx[]` (`frame_refs_short_signaling = 0`),
`allow_high_precision_mv`, interpolation filter, `is_motion_mode_switchable`, `use_ref_frame_mvs`,
`refresh_frame_flags`, `primary_ref_frame`, `disable_frame_end_update_cdf`
- `tile_info()` from VA tile counts/sizes + `context_update_tile_id`; our own `TileSizeBytes = 4`
- quantization (deltas, qmatrix), segmentation (write every feature explicitly), delta q/lf
- loop filter **with every ref/mode delta written explicitly**, CDEF, loop restoration, `tx_mode`,
reference select, skip mode, warped motion, reduced tx set
- global motion params, film grain (`update_grain = 1`, full params)
- **Gotchas:**
- **Global motion params are coded relative to the primary reference frame's params**
(`PrevGmParams`), so the rebuilder must track every frame's GM params per slot exactly as the
decoder does.
- `ref_order_hint[]` is written only when error-resilient and order hints are enabled.
- Multiple tile groups per frame are merged into one tile group OBU with re-written
`tile_size_minus_1` fields.
- **Hidden frames:** as for VP9, synthesize a frame-header OBU with `show_existing_frame = 1` (temporal
delimiter first). **Never for a hidden KEY frame:** showing an existing key frame runs the reference
refresh process and changes decoder state. That case is open question Q7.
- **Film grain:** MediaCodec outputs grain-applied frames. Copy them into
`current_display_picture`'s surface. The grain-free reference surface is only a handle for us,
because MediaCodec keeps its own references. framemd5 against libdav1d is expected to match (grain
synthesis is normative).
- **10-bit:** P010 surfaces; S3's copy proof must be repeated for 10-bit.
### M4: HEVC + 10-bit (TODO)
- VA HEVC slice data is the whole slice NAL. **VPS/SPS/PPS** are synthesized from
`VAPictureParameterBufferHEVC` (+ `...HEVCRext` / SCC).
- **Missing from VA:** `profile_tier_level` level (heuristic), VPS fields (derive from SPS values) and,
above all, **the SPS's short-term RPS candidate sets**. VA gives only `num_short_term_ref_pic_sets`
and the slice-level `st_rps_bits`, and the long-term SPS candidates are missing too.
- **Approach:** synthesize an SPS with `num_short_term_ref_pic_sets = 0` and
`long_term_ref_pics_present` as needed. **Rewrite every slice segment header** with an explicit
`st_ref_pic_set` (and explicit long-term entries) built from the picture's RPS in VA
`ReferenceFrames`:
- `ST_CURR_BEFORE`/`AFTER` give used entries.
- DPB pictures with neither flag give "foll" entries with `used_by_curr = 0`.
- `LT_CURR` gives long-term entries via `poc_lsb_lt` and an explicit MSB.
- This is feasible because HEVC slice data starts **byte-aligned** after `byte_alignment()`
(`slice_data_byte_offset` in VA). Re-serialize the header, re-escape it, and append the original
slice data bytes.
- Entry points (tiles/WPP) are copied as parsed.
- **Scaling lists:** write `VAIQMatrixBufferHEVC` explicitly, including the 16x16/32x32 DC values.
- **10-bit:** Main10 → P010; verify 10-bit Y2Y precision in S3.
- `sps_max_num_reorder_pics = 0` / `sps_max_latency_increase_plus1 = 0` for low latency, as in H.264.
### Later TODOs (from M1 deferrals)
| Id | TODO |
|---|---|
| H264-1 | Field pictures (PAFF): both fields arrive as two VA pictures with the same target. Assemble both field NALs into one access unit, or feed them separately and map the single output to the shared target. Verify the codec's behaviour. |
| H264-2 | POC type 1: rewrite slice headers to POC type 0 (CABAC slice data is byte-aligned after `cabac_alignment_one_bit`; CAVLC needs a bit shift), or recover the offsets from the resolved POCs the client sends. |
| H264-3 | High10 / 4:2:2 only if a device lists them. |
| FF | **Firefox:** it decodes with ffmpeg's VA hwaccel in its RDD process and exports with `vaExportSurfaceHandle`, probably `SEPARATE_LAYERS` imported per plane (needs R3). Checks: whether the RDD sandbox allows our `connect()` (connect at `vaInitialize`; `MOZ_DISABLE_RDD_SANDBOX=1` only for diagnosis), how its `vaapitest` probe process reacts (vaInitialize + profile query must be fast and side-effect free), and which prefs are needed for a non-allowlisted GL vendor. No app-specific code. |
| R1 | **Copy elision** for MobileGL-internal readers: let GL readers sample the codec AHB directly and write the surface copy only when someone can see the raw memory. Blocked: a third party may `mmap` the exported fd with no VA call, so we cannot know. Revisit only with a safe signal. |
| R2 | **Explicit fences end-to-end:** attach the copy's sync_file to the exported dma-buf with `DMA_BUF_IOCTL_IMPORT_SYNC_FILE` (kernel ≥ 6.0; the device has 6.1), so implicit-sync consumers wait on the GPU and `vaSyncSurface` can answer before the fence signals. |
| R3 | **Per-plane imports in MobileGL:** R8/GR88 (R16/GR1616) EGLImages that alias a plane of a server YUV image. Needed by mpv's GL interop, Firefox and KWin's YUV linux-dmabuf path. Espryt: GPU plane extraction via Y2Y sampling; Magma: per-plane views. |
| R4 | One GPU pass instead of two for MobileGL readers (Y2Y copy + RGBA conversion), once R1 is solved. |
| R5 | Shared-memory ring for large access units instead of socket writes, if profiling shows socket cost at 4K. |
## 10. Open questions
- **Q1.** Do Adreno's (and other vendors') hardware AVC decoders honour `max_num_reorder_frames = 0`
/ `KEY_LOW_LATENCY` with immediate output? (S2.) If not, what bounded strategy do we take for
single-threaded sync-after-decode clients?
- **Q2.** Is `GL_EXT_YUV_target` rendering into a `Y8Cb8Cr8_420 | GPU_COLOR_OUTPUT | CPU_*` AHB
allowed and bit-exact? (S3.) How common is the extension outside Adreno?
- **Q3.** Level selection for synthesized SPS / sequence headers when VA gives none: smallest
sufficient level, or the codec's maximum? Some decoders size their DPB by level.
- **Q4.** `VAIQMatrixBufferH264` list order convention. Verify against the old driver with a CQM
stream.
- **Q5.** Does Chrome sync surfaces at all (C8)? If not, the bounded server-side wait in P5 is
load-bearing for correctness, not just a safety net.
- **Q6.** VP9 hidden-frame injection vs `last_show_frame` / previous-MV state. Where may a synthesized
`show_existing_frame` be inserted safely?
- **Q7.** AV1 hidden key frames: how do we obtain their pixels without a state-changing
show-existing?
- **Q8.** `ServiceBind` on the shared listener vs a separate socket: confirm the acceptor change stays
small and keeps the PairAcceptor tests' guarantees.
- **Q9.** Concurrency limits: how many simultaneous hardware decoders the device allows
(`getMaxSupportedInstances`), and how Chrome reacts to `ALLOCATION_FAILED` on the N+1th video.
- **Q10.** Server restart: Chrome's GPU process restarts with the GL session anyway. Do mpv and
GStreamer recover from `OPERATION_FAILED` mid-stream, or must the driver reconnect transparently and
re-create contexts? (It cannot rebuild MediaCodec references; it would have to wait for the next
IDR.)
-201
View File
@@ -1,201 +0,0 @@
# glmark2 performance work: handoff
Status on 2026-10-04 after the first optimization pass on top of the
[baseline](glmark2-baseline.md). Everything here was measured on the anland Plasma desktop of the
Lenovo TB321FU (Adreno 750) at **pinned clocks** (CPU 1.80 / 2.25 / 2.25 / 2.25 GHz, GPU 680 MHz;
see the baseline). Single runs or two at most per point, so the numbers are **approximate**
(light scenes vary about +-15% run to run). The tablet was in portrait for the later runs, so the
"after" fullscreen numbers are 1600x2560 and the "before" ones 2560x1600 (same pixel count).
## Headline
glmark2 score, `-b :duration=5`, default scene list:
| | stock kgsl stack | Espryt before | Espryt now | Magma before | Magma now |
|---|---|---|---|---|---|
| es2, 800x600 window | ~1076-1180 | ~1319 | **~1887** | ~778 | **~1213** |
| es2, fullscreen | ~1177 | ~572 | ~561 | ~354 | ~404 |
| desktop GL, 800x600 window | ~1161 | ~1283 | **~1809** | ~743 | **~1186** |
The "now" es2 window scores include the two `buffer update-method=map` scenes, which MobileGL now
runs (they were Unsupported before), so they are a fair comparison with stock's.
Per scene (es2 window unless marked F = fullscreen; FPS):
| scene | stock | Espryt before | Espryt now | Magma before | Magma now |
|---|---|---|---|---|---|
| texture | 1269 | 1672 | ~2210 | 915 | ~1580 |
| build use-vbo=false | 1095 | 467 | ~1215 | 472 | ~1040-1190 |
| build use-vbo=true | 1287 | 1675 | ~2450 | 1062 | ~1730 |
| desktop blur | 976 | 275 | **~1000** | 157 | ~680 |
| desktop shadow | 1044 | 234 | **~1000** | 125 | ~830 |
| ideas | 688 | 138 | ~550 | 66 | ~354 |
| terrain | 443 | 149 | ~220 | 115 | ~157 |
| shadow | 975 | 1139 | ~1840 | 644 | ~1170 |
| refract | 838 | 981 | ~1280 | 331 | ~520 |
| jellyfish | 1185 | 1438 | ~2170 | 841 | ~1155 |
| buffer map (2 scenes) | 330 / 408 | Unsupported | ~465 | Unsupported | ~431 |
| texture F | ~1430 | ~726 | ~597 | ~445 | ~398 |
| terrain F | - | - | ~142 | - | ~112 |
**Remaining gap:** Espryt is at or above stock in every windowed scene except **ideas (0.8x)** and
**terrain (0.5x)**. Magma is behind on most draw-heavy scenes (ideas 0.5x, terrain 0.35x, desktop
0.7-0.8x, refract 0.6x). **Fullscreen** is the biggest gap on both backends (0.4-0.5x of stock),
and nothing landed here addresses it.
## Done (all on origin/feat/disaggregated)
| commit | change | why / evidence | effect (approx) |
|---|---|---|---|
| `4722fa38` | the baseline report | - | - |
| `66b47a10` | **glFinish waits for the GPU** on the remote path (a server fence created, waited with `GL_SYNC_FLUSH_COMMANDS_BIT`, destroyed); **Magma submits** once 1024 retired per-clear objects pile up with nothing submitted; **ES contexts advertise** `GL_OES_rgb8_rgba8`, `GL_OES_depth24`, `GL_OES_mapbuffer` (with `glMapBufferOES` / `glUnmapBufferOES` / `glGetBufferPointervOES` exported as the core calls) | glFinish only waited for the server's apply, so off-screen rendering ran unbounded (glmark2 `--off-screen` "6846" / "12117" fps) and drove Magma's server to `VK_ERROR_OUT_OF_HOST_MEMORY` in `ClearWireFramebuffer`; es2 `--off-screen` could not start (glmark2 picked `GL_RGBA4` and no depth) | off-screen now real numbers (Espryt texture ~866, Magma ~800) and no crash; es2 `--off-screen` runs; the two buffer-map scenes run |
| `2de0d6dd` | **client-array buffers are reused** from a per-share-group pool (respecify, a non-blocking record, instead of a blocking ResourceCreate + destroy per array per draw); contiguous element ranges copied in one memcpy; the fetch plan skips sorting an already ascending range | desktop blur/shadow paid 28/52 blocking creates per frame, build use-vbo=false 2; `std::sort` was 27% of the client in build use-vbo=false | build use-vbo=false Espryt 467 -> ~1100, Magma 472 -> ~1040; blocking replies per frame in desktop 29/53 -> 1 (fps of desktop barely moved at this step: the next item was the limit) |
| `d627fca3` | **targeted wake-ups:** the client publishes the applied sequence it waits for (`RingControl::producerWakeSeq`, its own cache line); the server rings a parked client only once that sequence is applied (or the event ring filled); the parked flag is stored with release so the notifier sees the target | while the client waited for its present reply the server rang it after every record it applied: 18% of Espryt's apply thread was `SocketDoorbell::Notify`, and every ring was a futile wake-up and re-park on the client | Espryt desktop ~250 -> ~800, ideas 138 -> ~380, texture 1672 -> ~2190; Magma desktop ~150 -> ~550-650, ideas 45 -> ~137, refract 372 -> ~540 |
| `963f4d81` | Magma **reuses completed command buffers** (pool resets them at the next begin) instead of `vkFreeCommandBuffers` + `vkAllocateCommandBuffers` per submission | `FreeRetiredCommandBuffersCompletedUpTo` was 15% of Magma's apply thread in ideas | small: Magma texture / desktop +~10% |
| `915882b2` | the server **times records only when** `MOBILEGL_SERVER_FRAME_STATS` is on | two `steady_clock::now()` per record were ~5% of Espryt's apply thread | with the next row |
| `e5ee31b0` | the anland server build uses **`android-30`** (the APK's minSdk) in `build-android.sh` / `reproduce.md`: `thread_local` is native ELF TLS instead of emulated TLS | `__emutls_get_address` + `pthread_getspecific` were 7-12% of Espryt's apply thread. **An existing build dir keeps its API level: reconfigure (delete it) to get android-30** | Espryt (with the row above) desktop ~800 -> ~1000, ideas ~380 -> ~550, terrain 168 -> ~220; Magma unchanged |
| `bb31f374` | Magma **keeps the wire draw's render pass open** across draws to the same attachments. Every command that may not sit inside a pass (barriers, copies, clears, blits, dispatches, other passes' begins) in the resource managers and wire helpers first calls `EndActiveRenderPassOn(cb)` (`RenderPassGuard.h`); the per-draw "make image writes visible" barrier for sampled images is only recorded when an image may have been written since the last one (`WireImageWriteEpoch`, bumped by every pass end, out-of-pass work and storage-image draw); same-layout attachment barriers are skipped when the pass continues | `SetupWireDraw` ended and began a VkRenderPass for every draw (ideas: 305 passes per frame; End/Begin ~21% of the apply thread, plus `vkEndCommandBuffer` cost), and on a tiler each pass loads and stores its attachments | Magma ideas ~146 -> ~354, desktop shadow ~640 -> ~830; visually checked on ideas, terrain, shadow, refract, jellyfish, desktop, bump, conditionals, effect2d, Plasma, Chrome and the lock screen |
Each landed with the host unit tests green (WSL clang, `ctest -L unit -E DirectVulkan`, 2851
tests) and a device check on both backends (frames shown, Plasma, Chrome page + scroll input,
`kscreenlocker_greet --testing`).
## Findings
- **The common shape before the fixes: nothing was saturated.** GPU 7-40% busy, client and server
threads each ~30-50% busy, both waiting for each other with a wake-up per handoff. Removing
futile wake-ups (`d627fca3`) was worth more than any per-draw CPU saving.
- **Espryt after the fixes:** the remaining windowed gaps are ideas and terrain. Its present reply
is still a synchronous round trip per frame, and the server's per-draw state sync
(`DG::PrepareForDraw`, 28% of the apply thread in ideas before) is now the larger share.
- **Magma after the fixes:** CPU-bound on the server. Remaining per-draw costs in
`SetupWireDraw` (descriptor sets, `GetOrCreateProgram` / pipeline hash, `BuildWireVertexInput`,
vectors allocated per draw, scudo lock contention ~20% before), and per frame
`BlitDefaultFramebufferToSharedImage` (~30% of the apply thread: frame flush + a second submit
for the copy + `vkEndCommandBuffer`) plus `Present()` of the client's offscreen default surface
(a swapchain acquire + submit that nobody looks at) and GPU progress markers per submit.
- **Fullscreen:** every frame is rendered into the server's default framebuffer and then copied
into the window's shared image (Espryt `glBlitFramebuffer`, Magma `vkCmdCopyImage`); at 4 MP the
GPU is ~98% busy on texture at ~600 fps while stock does ~1430 and is even faster fullscreen than
windowed (it presumably skips composition).
**Dead end: an adaptive client spin.** Growing the doorbell spin up to 1 ms towards the measured
reply latency (and shrinking it on long waits) gave +30-80% on light scenes (texture, jellyfish,
shadow) on Magma but cost refract -35% (370 -> 236) and fullscreen terrain -35% (90 -> 58),
reproducibly; the present reply simply got slower while the client spun. Polling the shared line
less often (backoff up to 64 iterations) did not change that, so it is not cache-line ping-pong. A
static `MOBILEGL_IPC_SPIN_US=1000` on the client showed the same mixed picture on Espryt. Not
landed; the targeted wake-up removed most of what the spin bought without the regressions.
**Traps:**
- **Pin the clocks.** Unpinned, the stock stack's light scenes ran at GPU 310 MHz / big cores 672
MHz and scored 425 instead of ~1100.
- **glFinish and `--off-screen`.** Before `66b47a10` off-screen MobileGL numbers were meaningless.
- **The stock desktop turns black mid-run** (a pure-black 21707-byte screenshot) while
SurfaceFlinger keeps latching ~125 buffers/s, and its score then jumps to ~1420-1500. Probe the
screen every ~15 s during stock runs and discard runs with a black probe; restart the stock
container + app (`stfix.sh`) to get the picture back. A powerdevil inhibit did not prevent it.
- **The MobileGL app's extra-keys bar** shrinks its desktop to ~2560x1410; set
`extra_keys_mode=with_keyboard` for fullscreen comparisons (it was restored afterwards).
- **The MobileGL session restores a System Settings window** at login; close it before measuring.
- **Rotation:** the stock app follows auto-rotate; check glmark2's "Surface Size" line.
- **Symbolizing:** the APK's server lib is stripped; a RelWithDebInfo build of the same tree was at
+0x80 for every function (check with `llvm-nm -D` on both). Container clients: simpleperf prints
offsets from the start of the executable mapping (add the r-x segment's page-aligned vaddr,
0x4fb000 for the lib of 2026-10-04). Off-CPU (`sched_switch`) samples of container processes
get their user frames attributed to Android's libs, so use the client's own `P65LinkMetrics` for
its waits.
- `pkill -f <script>` inside `su -c "..."` kills the command itself; use pids.
## Yet to do (ranked)
1. **Fullscreen: render straight into the window's shared images** (both backends). The default
framebuffer becomes the current back shared image (an AHB-backed EGLImage FBO on Espryt, the
imported image on Magma); the bottom-up row order goes to linux-dmabuf's `Y_INVERT` flag instead
of the flipping blit. Gain: fullscreen ~1.5-2x. Risk: default-framebuffer semantics (reads,
blits, buffer age, resize, device loss), KWin's handling of `Y_INVERT`. Verify: fullscreen
glmark2, Chrome, desktop-verify, the retrace CI.
2. **Asynchronous shared-image present.** Today `PresentToSharedImage` is `EmitAndWait`, so the
client cannot record frame N+1 while the server finishes N. Commit the `wl_buffer` from the reply
on the event ring (or order the compositor's acquire after the producer's present server-side),
and allow present credit 2. Gain: light scenes 1.3-1.8x. Risk: frame ordering with the
compositor, buffer reuse with 3 images, an app that draws one frame and idles. Verify: Chrome
flicker barcode test, KWin, desktop-verify.
3. **Magma per-frame submissions:** append the shared-image copy to the frame's command buffer (one
submit per frame), skip `Present()` for a surface whose frames only go to shared images, coalesce
GPU progress markers. Gain: Magma light scenes 1.2-1.4x. Risk: the copy's fence/semaphore
export (`SI::PublishWrite`), hang-watch coverage. Verify: Magma unit tests, shared-image tests.
4. **Magma per-draw CPU:** cache descriptor sets / pipeline lookups across draws when unchanged
(keyed on monotonic lifetime ids), stop rebuilding the vertex input state per draw, replace the
per-draw vectors with per-frame arenas. Gain: ideas/terrain 1.3-2x. Verify: retrace CI with
Magma.
5. **Terrain** (both backends at 0.35-0.5x of stock): not profiled after the fixes; it has 43
respecifies, several FBO passes and a GenerateMipmap per frame. Profile first.
6. **Espryt per-draw state sync** (`DG::PrepareForDraw`): the Minecraft roadmap's version-gated
early-outs. Gain 10-20% on draw-heavy scenes. Risk: stale caches; retrace CI.
7. **Blocking fence create/destroy:** `EmitFenceSync` / `EmitDeleteSync` are `EmitAndWait`; an app
that makes a fence per frame (Minecraft, Chrome) pays two round trips. Make them fire-and-forget
(the client already mints the handle).
8. **Client-to-server doorbell:** the server parks between records and the client pays a socket
write per record while it streams. Try a server-side spin sized to the inter-record gap of an
actively streaming client (watch the spin regression above), or ring only at batch boundaries.
9. `CreateVertexElements` is re-sent per draw (ideas: 76 per frame); deduplicate by content on the
client.
## Device state at handoff (2026-10-04 ~18:50 device time)
- **Up:** the MobileGL anland (`com.anland.consumer.mobilegl`) on **Magma** (DirectVulkan), Plasma
running (KWin + plasmashell, no sticky software renderer). The original anland
(`com.anland.consumer`) is force-stopped and its `arch-kde` container is stopped.
- **Deployed build = `bb31f374`** (code of every commit above; the later commits on the branch are
docs and CI): the server is a RelWithDebInfo NDK build for android-29 swapped into the installed
APK (`/data/app/.../com.anland.consumer.mobilegl-*/lib/arm64/libMobileGL.so`), the client the
Release cross build in the container's `/opt/mobilegl/lib/libMobileGL.so`. The APK's previous
server lib (`18a5dba0`) is kept at `/data/local/tmp/anl/srv-apk.so`.
- **Clocks: back at the original governors and limits** (CPU walt 902400-2265600 / 614400-3148800 /
499200-2956800 / 672000-3302400, GPU msm-adreno-tz 231-903 MHz). They were restored just before
the request to leave them pinned arrived, and were left as they are. The saved originals stay in
`/data/local/tmp/anl/pb-clocks.orig`. To pin again for measuring and to restore afterwards:
```sh
adb shell 'su -c "sh /data/local/tmp/anl/pbclk.sh pin"' # CPU 1.80/2.25/2.25/2.25 GHz, GPU 680 MHz
adb shell 'su -c "sh /data/local/tmp/anl/pbclk.sh restore"' # back to pb-clocks.orig
# the restore did not take on two limits once; write them again if `pbclk.sh show` disagrees:
adb shell 'su -c "echo 614400 > /sys/devices/system/cpu/cpufreq/policy2/scaling_min_freq; echo 231000000 > /sys/class/kgsl/kgsl-3d0/devfreq/min_freq"'
```
- The MobileGL app's `extra_keys_mode` is back at its original (unset = the bar is shown).
- The phone-side helper scripts and run outputs are in `/data/local/tmp/anl/` (`pb/`, `prof/`,
`pb-unpinned/`), the container's in `/root/pb` of both containers.
## How to reproduce
Helper scripts: `tools/device_bench/anland_glmark2/` (copied verbatim from the session; the phone
side expects them in `/data/local/tmp/anl/`, push with `adb push <file> /data/local/tmp/anl/`).
| script | where | does |
|---|---|---|
| `pbclk.sh save\|pin\|restore\|show` | phone, root | saves the governors/limits to `/data/local/tmp/anl/pb-clocks.orig`, pins CPU 1.80/2.25/2.25/2.25 GHz + GPU 680 MHz, restores |
| `pbbatch.sh <container> <stock\|mgl> <label> <cfg...>` | phone, root | per config (`es2w es2f glw glf es2o glo`): cool-down (`pbcool.sh`), samples (`pbsamp.sh`), screenshots + SurfaceFlinger frames (`pbshown.sh`), 15 s screen probes (`pbprobe.sh`), runs glmark2 through `pbrun.sh` in the container (`/root/pbrun.sh`), black-screen restart for stock (`stfix.sh`) |
| `ab.sh <container> <tag> <cmd...>` / `pbset.sh <tag> [env]` | phone, root | one run / the affected-scene set |
| `lm.sh <tag>` | phone, root | per-frame `P65LinkMetrics` averages of a run made with `MOBILEGL_PIPE_STATS=1` |
| `pbprof.sh`, `profrun.sh`, `gprof.sh` | phone, root | `simpleperf` (flat, per-thread `top -H`, GPU busy) and `--trace-offcpu -g` call graphs of the server apply thread + client |
| `visset.sh <tag>` | phone, root | mid-run screenshots of ten scenes |
| `dep.sh`, `smoke.sh`, `wtest.sh` | host | deploy a server + client build and restart on a backend; Chrome + lock screen smoke check; WSL unit tests |
| `pbparse.py`, `mktables.py` | host | glmark2 outputs -> per-scene tables |
| `symagg.py`, `incl.py`, `offcpu.py`, `callers.py`, `timeline.py` | host | symbolize and aggregate simpleperf output (flat by function, inclusive call graph, off-CPU waits, callers of a function, on/off-CPU timeline) |
```sh
# clocks (phone, root)
sh /data/local/tmp/anl/pbclk.sh save; sh /data/local/tmp/anl/pbclk.sh pin
sh /data/local/tmp/anl/pbclk.sh restore # when done (also on abort)
# backends / stacks
sh /data/local/tmp/anl/switch.sh DirectGLES|DirectVulkan # MobileGL anland (skill script)
am force-stop com.anland.consumer.mobilegl; droidspaces -C /data/local/Droidspaces/Containers/arch-kde/container.config start
monkey -p com.anland.consumer -c android.intent.category.LAUNCHER 1 # stock
# one glmark2 run in the MobileGL container (root shell in the container)
MGLOG=/tmp/pb-x.log mgrun offscreen bash -c 'MOBILEGL_PIPE_STATS=1 glmark2-es2-wayland -b ideas:duration=10'
# profile the busiest server apply thread + the client
simpleperf record --trace-offcpu -e cpu-clock -f 2000 -g -t <apply tid>,<client pid> --duration 6 -o x.data
simpleperf report-sample -i x.data --show-callchain > x.samples
python incl.py x.samples <tid> <unstripped libMobileGL.so> <offset> com.anland.consumer.mobilegl
```
+1 -2
View File
@@ -2,8 +2,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
[`docs/archive/notes/p12/CREATE-WINDOW-MEASURED.md`](../archive/notes/p12/CREATE-WINDOW-MEASURED.md) (in Chinese).
with the logs and the attempts that failed, is in the git history (in Chinese).
## What the knob does
-12
View File
@@ -1,12 +0,0 @@
# 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.
@@ -1,63 +0,0 @@
# 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.
-54
View File
@@ -1,54 +0,0 @@
# 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)。
-354
View File
@@ -1,354 +0,0 @@
# 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 构建自己的参数预处理,新旧逐字相同。
-64
View File
@@ -1,64 +0,0 @@
# 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 窗口没有重测。
-642
View File
@@ -1,642 +0,0 @@
# 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.
-485
View File
@@ -1,485 +0,0 @@
# 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.
-543
View File
@@ -1,543 +0,0 @@
# 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).
-804
View File
@@ -1,804 +0,0 @@
# 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".
-757
View File
@@ -1,757 +0,0 @@
# 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`.
-60
View File
@@ -1,60 +0,0 @@
# 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 来满足错误的负控预期。
-199
View File
@@ -1,199 +0,0 @@
# 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 不受影响。
-53
View File
@@ -1,53 +0,0 @@
# 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);线上格式不变。
-71
View File
@@ -1,71 +0,0 @@
# 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` 未改。
-74
View File
@@ -1,74 +0,0 @@
# 阶段笔记(notes)
这一层放**各阶段的全部细节**:每个阶段一个目录,目录里的 `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/)。
## 按阶段
| 阶段 | 汇总 | 重要文件 |
|---|---|---|
| P0 卫生、度量、门、骨架 | [`p0/`](p0/README.md) | `BRIEF-P0.md`、`p0-device-findings.md` |
| P0.5 值头与制品头 | [`p05/`](p05/README.md) | `BRIEF-P05.md`、`p05-results/` |
| P1 `PipeInputs` + verify | [`p1/`](p1/README.md) | `BRIEF-P1.md`、`P1-LANE-FINDINGS.md` |
| P2 渲染状态 CSO | [`p2/`](p2/README.md) | `BRIEF-P2.md`、`INTEGRATOR-DECISIONS.md`、`ab/`、`bench/` |
| P3a 句柄 wave 1 | [`p3a/`](p3a/README.md) | `BRIEF-P3A.md`、`INTEGRATOR-DECISIONS.md` |
| P4a 句柄 wave 2 | [`p4a/`](p4a/README.md) | `BRIEF-P4A.md`、`INTEGRATOR-DECISIONS.md`、`ab-redmi/` |
| P5 传输 + inproc | [`p5/`](p5/README.md) | `BRIEF-P5.md`、`PACKAGE-PREAMBLE.md`、`p5-results/` |
| P5b verb 迁移 | [`p5b/`](p5b/README.md) | `BRIEF-P5B.md`、`p5b-results/p5b-close-codex-review.md` |
| P5c 共享内存读点归零 | [`p5c/`](p5c/README.md) | `p5c-audit-v1.md` |
| P5d inproc 性能 | [`p5d/`](p5d/README.md) | `P5D-INPROC-PERFORMANCE.md`(报告)、`RESULTS-P5D-R3.md` |
| P5e run-ahead | [`p5e/`](p5e/README.md) | `P5E-RUNAHEAD.md`(报告)、`INTEGRATOR-DECISIONS-P5E.md`(ID-80..136)、`TASK-*.md` |
| P5f 一切状态上 wire | [`p5f/`](p5f/README.md) | `P5F-WIRE-COMPLETENESS.md`(计划)、`close-report.md`、`magma-runahead.md`、`HANDOFF-CI-INPROC-20260920.md` |
| P6 spawn transport | [`p6/`](p6/README.md) | `P6-SPAWN-PLAN.md`、`P6-CONTRACT-DRAFT.md`、`P6-ENDSTATE-REVIEW.md`、`a6-audit-v1.md`、`gate7-device-ab.md` |
| P6.5 两轴传输 + 跨机 | [`p65/`](p65/README.md) | `evidence-index.md`、`validation-status.md`、`code-review-findings.md` |
| P3b / P4b Espryt 深化 | [`p34b/`](p34b/README.md) | `espryt-d1.md` … `espryt-d3.md` |
| P7 Magma 全量迁移(含 Ph) | [`p7/`](p7/README.md) | `PLAN-PH-P34B-P7.md`、`INTEGRATOR-DECISIONS-P7.md`(ID-P7-1..63)、`HANDOFF-2026-09-23.md`、`device-window-*/`;审查稿在 [`handoff/`](handoff/) |
| **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`](../../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)。
## 旧引用怎么找
**2026-09-24 整理时移动的文件**(旧笔记、代码注释与契约里仍按旧名引用它们,文件名没变):
| 旧位置 | 新位置 |
|---|---|
| `docs/Disaggregated/P5D-INPROC-PERFORMANCE.md` | [`p5d/P5D-INPROC-PERFORMANCE.md`](p5d/P5D-INPROC-PERFORMANCE.md) |
| `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`](../../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`](../../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/) |
**按编号引用的旧章节**(小节编号在新位置原样保留):
| 旧引用 | 现在在 |
|---|---|
| `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`](../../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/`) |
## 收录规则
| | |
|---|---|
| **收录** | 全部 `.md` 笔记,以及阶段自己决定入库的小脚本、判读工装与证据摘要(如 `p7/device-window-*/`、`p12/device/`、`p12/gate/`) |
| **不收录** | 大体积、随每次运行重新产生、只在产生它的机器上有意义的证据数据:完整运行日志、APK 与 `.idsig`、`nm` 转储、设备 logcat。上层文档引用它们时给的是路径,不是内容 |
新阶段开工时建 `notes/<阶段>/`,把计划、裁定与报告都放进去;阶段收官时补齐该目录的 `README.md` 汇总,顶层索引只改一行。
## 不受 citation lint 管
CI 的文档引用 lint 只检查顶层、`design/` 与 `guide/`(`.github/workflows/test.yml`),**本目录不在其中,这是有意的**:笔记里的 `file:line` 写在各自当时的代码头上,拿今天的 HEAD 解析必然大面积失败——那是它们作为历史记录的正确状态。要核对某份笔记的引用,用它自己声明的那个 revision:
```bash
python3 scripts/check_doc_citations.py --rev <当时的头> docs/Disaggregated/notes/<phase>/<file>.md
```
-166
View File
@@ -1,166 +0,0 @@
# MobileGL 作为 Anland 的 GL 提供者:调研(2026-09-30)
> 非阶段笔记。只读源码调研,**没做原型、没上设备**。Anland 审计于 `SuperTurtleDev/anland@cc169180`(6.x main),MobileGL 审计于 `p8/main@648800a2`(P8 收官头)。结论里凡是"要做 / 要改"的都是推断,事实部分在两份报告里逐行有出处(方案 B 与 Mesa 对照两处的 Anland 事实直接链到源码)。
| 文件 | 内容 |
|---|---|
| [`report-anland.md`](report-anland.md) | Anland 侧:缓冲入口契约、伪造 AHB、同步、帧节奏、进程与 SELinux、容器 GPU 现状、Xwayland glamor、衍生项目、改动插入点(382 处引用) |
| [`report-mobilegl.md`](report-mobilegl.md) | MobileGL 侧:Linux client 构建与导出、EGL / GLX 能力、split 的 present 路径、AHB / fd / fence 现状、会话与多窗口限制、GL 覆盖面、文档漂移 |
| [`kgsl-as-server.md`](kgsl-as-server.md) | 把内核 kgsl 看成 server:A / B 实为"缓冲归谁"之分,6.x + Mesa 是 A、5.x 是 B;拆分层级与 virtio-gpu native context / virgl 的对应 |
| [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md) | 计划:server 分配 AHB 当 dma-buf,linux-dmabuf + GBM 后端 + 薄节点,窗口零拷贝;厂商中立规则与分阶段 |
| [`../../../MobileVA/plan.md`](../../../MobileVA/plan.md) | 计划(未实现):MobileVA,基于 Android MediaCodec 的厂商中立 VA-API 驱动,解码进 server 的 YUV 共享图像、全程无 CPU 拷贝;旧的高通专用驱动留作 A/B 回退;H.264 先行,VP9 / AV1 / HEVC 的 TODO 与待定问题 |
| [`runbook-plasma.md`](runbook-plasma.md) | 操作手册:在 Windows 主机 + 平板上从零拉起 / 重启后拉起 MobileGL + anland + KDE Plasma,server / client / KWin 的构建部署循环、切后端、验证、日志与常见坑(配套 skill `.claude/skills/anland-mobilegl-plasma/`) |
| [`brief-anland.md`](brief-anland.md)、[`brief-mobilegl.md`](brief-mobilegl.md) | 派给 Codex 的原始提示词。**两份都把后端名写反了**:Espryt = DirectGLES(GLES),Magma = DirectVulkan(Vulkan);报告已纠正 |
## 结论
可行,但两边都要新东西:
- MobileGL 没有"把帧交回 client"这条路:`MGPPresent` 只有 `FrameSerial`,P12 画在 server 自己的 SurfaceView 上;没有 Wayland WSI、没有可导出的颜色图像、没有 sync_file。
- Anland 的 GPU 缓冲入口只有 `linux-dmabuf` v3 的单平面线性 AR24/XR24,靠高通 snapalloc 布局伪造 AHB(非高通直接失败,没有 CPU 退路);没有原生 AHB 入口。
- 两条路线:**A** 走标准 Wayland 缓冲(MobileGL 像 Mesa 一样交 `wl_buffer`);**B** 让 Anland 把窗口的 Surface 直接交给 MobileGL server(FCL 形态)。一个应用一个 GL 顶层窗口(游戏)选 B:接口面小、零拷贝、厂商中立,MobileGL 侧主要是推广 P12 已有机制;A 的 `wl_shm` 回读作兜底,A 的原生 AHB 入口按需再做。
## Anland 是什么
root Android(KernelSU / SukiSU 模块)上的 Wayland 宿主:守护进程 `waylandbridge`(root,`awl_daemon` 域,bionic,链 Android EGL / GLESv3)实现 Wayland 协议。每个 `xdg_toplevel` 按需挂到一个独立的 Android 窗口(一个 Activity,来自宿主 APK 或接入 libawl 的应用;可随时分离,Wayland 窗口不死);多个 toplevel 可同时各挂一个;popup 与子 surface 合成进父窗口,不单独开窗。Droidspaces 容器经 `/run/anland/wayland-0`(绑定自 `/data/local/tmp/awl`)连入。容器内 GPU 加速 = Mesa freedreno / turnip on kgsl,只有 Adreno;Mali 等走 llvmpipe。
## 两条路线
```
A:标准 Wayland 缓冲
容器应用 ─ MobileGL client(glibc libEGL/libGL)── wl_buffer(wl_shm / AHB 令牌)──→ waylandbridge ─ SC / GLES 合成 ─ SurfaceFlinger
│ 同机 unix socket + 共享内存(split 现成) ↑
Android 侧 MobileGL render server(root 守护进程,厂商驱动)── AHB 完整句柄(原生入口)──┘
B:窗口 Surface 直通
容器应用 ─ MobileGL client ─ 令牌(Anland 扩展)─→ waylandbridge
│ 同机 unix socket + 共享内存 │ binder:窗口 Surface + 挂上 / 分离 / 尺寸
Android 侧 MobileGL render server ←────────────────────────┘
└ 厂商 EGL / Vulkan swapchain 直接画进该 Surface ─ SurfaceFlinger(Anland 的 popup / 子 surface / 光标作为子层叠在上面)
```
- 两条路线里 Wayland 连接、`xdg_toplevel`、输入与输入法都归 Linux 应用 / Anland;MobileGL 只管帧。
- P12 的 server 自有窗口不适用:Anland 下 Android 窗口归 Anland(Activity 的 Surface 交给 waylandbridge 绘制,输入也从这里转成 Wayland 事件),应用的帧必须作为 `wl_buffer` 提交到自己的 `wl_surface`;P12 画进 MobileGL 自己的 Activity,会绕开 Anland 的窗口与输入。B 不是 P12:窗口仍是 Anland 的,只是生产者换成 MobileGL。
- 容器内直接让 Magma 跑在 turnip 上:只有 Adreno,和现有 Mesa 同一驱动,价值小;MobileGL 的价值是把厂商驱动(含 Mali)带进来,只能靠 split 跨到 bionic。
## 对照:容器里现在的 Mesa(freedreno / turnip)
Mesa 属于 A,准确地说是 A 里"Anland 不改、走 dma-buf"那一档,只是渲染在应用进程内、没有 server。
| | Mesa freedreno / turnip | 方案 A | 方案 B |
|---|---|---|---|
| 在哪渲染 | 应用进程内,容器里直接调 kgsl([anland-session.sh:108–112](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/anland-session.sh#L108-L112):`GALLIUM_DRIVER=kgsl`、`FD_FORCE_KGSL=1`) | Android 侧 render server | Android 侧 render server |
| 缓冲谁分配 | 应用进程里的 Mesa 经 kgsl 分配,没有 gralloc 元数据 | server 用 gralloc 分配 AHB | Anland 窗口 Surface 自己的缓冲队列 |
| 怎么交给 Anland | 标准 Wayland:`zwp_linux_dmabuf_v1` 单平面线性 AR24/XR24 | 标准 Wayland:`wl_buffer` | 不经过 Wayland 缓冲,直接画进 Surface |
| Anland 怎么处理 | 伪造 AHB(高通 snapalloc donor)→ SC / GLES 合成 | 原生 AHB 入口,不伪造 | 不经手,只在上面叠子层 |
| 同步 | 隐式:提交时 `DMA_BUF_IOCTL_EXPORT_SYNC_FILE` 从 dma-buf 抓写 fence,失败就轮询 dma-buf([awl_surface.c:199–209](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L199-L209));Mesa EGL 不走 explicit sync v1 是推断 | explicit sync(sync_file) | 缓冲队列内部 |
Anland 6.x 的 dma-buf 入口就是照 Mesa / kgsl 的产物做的:
- 伪造 AHB 的起因:注释写明容器 kgsl dma-buf 缺 gralloc 元数据,直接导入被拒([awl_ahb.cpp:10–14](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L10-L14));这也是只有 Adreno 能加速的原因之一。
- Xwayland 补丁迁就 kgsl:把 DRI3 报的错误 modifier `1274` 改成 LINEAR,强制提交 `DRM_FORMAT_MOD_INVALID`,无 v4 feedback 也启用 glamor([anland-kgsl-glamor.patch](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch))。
- SC 直出 / 拷贝切换按 turnip WSI 调:vkmark 123 fps 对 ~1300 fps([awl_sc.cpp:60–75](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L60-L75))。
对 MobileGL 的含义:Mesa 相当于"容器里的 monolith MobileGL",因为 kgsl 能在 glibc 进程里直接用;厂商驱动(bionic)不能,所以 MobileGL 必须 split,渲染挪到 Android 侧,缓冲来源从 kgsl dma-buf 变成 gralloc AHB。"Magma 跑在容器 turnip 上"就是 Mesa 这条路的翻版。
B 在 6.x 没有对应物(今天只有 waylandbridge 自己往窗口 Surface 里画)。5.x 的私有 Anland Display Protocol 介于两者之间:
- 像 B:Android 端从窗口自己的缓冲队列 `dequeueBuffer`,只导出 `handle->data[0]` 给容器里的合成器(如 KWin + Mesa),合成器画完回 sync_file,Android 端 `queueBuffer`;缓冲属于窗口,零拷贝、不伪造([protocol.h:7–49](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/common/protocol.h#L7-L49)、[native_consumer.c:144–172](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L144-L172)、[:955–1003](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L955-L1003))。
- 像 A:缓冲仍以 dma-buf 跨进程,依赖容器 Mesa / kgsl 能把 gralloc 的 `data[0]` 当线性缓冲导入,同样只有 Adreno。
6.x 为了让原生合成器不改就能接入,换回了标准 Wayland 缓冲(A 这条路)。
换个角度——把内核 kgsl 当 server 读这张表——见 [`kgsl-as-server.md`](kgsl-as-server.md)。
## 两边现状
| | MobileGL | Anland |
|---|---|---|
| 帧入口 / 出口 | `MGPPresent{FrameSerial}`;P12 画在 server 窗口 | `wl_shm` v2(ARGB / XRGB);`linux-dmabuf` v3 单平面线性 AR24/XR24 |
| 窗口 / Surface | P12 `ServerDisplay` 租约(acquire / release / 几何),每进程一个自有窗口,失窗即 `Fatal{ServerWindowLost}` | 每个挂上的窗口一个 Surface,经 binder `T_SURFACE` 用 `ANativeWindow_readFromParcel` 收进守护进程([waylandbridge.cpp:1587–1626](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1587-L1626));EGL 模式守护进程是它的生产者([awl_renderer.cpp:101](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L101)),SC 模式只从它挂子层([awl_sc.cpp:676](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L676)),缓冲队列空闲 |
| 窗口系统 | 无 Wayland;EGL 扩展只有 `EGL_KHR_create_context`、`EGL_MESA_platform_surfaceless` | dma-buf → 高通 snapalloc donor 伪造 AHB(写死 HAL BGRA=5,要求 2 fd 句柄) |
| 同步 | 无 sync_file / SYNC_FD;`eglCreateSync` 创建即 signaled,`eglCreateImage` 只登记 | explicit sync v2(sync_file),acquire / release 都有;无 drm-syncobj |
| 原生缓冲 | AHB 只用于 P11 T0 的 BLOB 缓冲存储 | 无原生 AHB 入口;但 SC 池自己分配并上屏 RGBA AHB(`GPU_COLOR_OUTPUT\|GPU_SAMPLED_IMAGE\|COMPOSER_OVERLAY`),消费端能力已有 |
| 并发 | 每 supervisor 一个会话(`Refuse{Busy}`);`pGLContext`、Espryt `g_Display/g_Context/g_Surface`、Magma `pVulkanRenderer` 全局;多 context 未做(DEBTS) | 客户端不限;每窗 ≤16 层;队列 8;SC 模式为默认(`sc_enabled=1`) |
| 部署 | Linux client 只验过 x86_64;无 aarch64 glibc;只有 `libEGL.so(.1)` 别名,无 glvnd | 模块启动,`awl_daemon` 域 permissive;容器进程在 `droidspacesd` 域 |
## MobileGL 要做的
### 两条路线共用
| # | 项 | 内容 | 量级 |
|---|---|---|---|
| C1 | aarch64 glibc 构建与加载 | 交叉工具链 + CI + 与 Android server 的 wireFingerprint 验收;建议做 glvnd vendor(`__egl_Main` + egl_vendor.d JSON),容器 Mesa 就是 glvnd | M |
| C2 | 多 client / 多窗口 / 多 context | 先 worker-per-client(离屏 fork-per-session 有底子,去掉 Busy、做路由),再 per-surface 状态,再真 share group;单窗口游戏可暂缓,GTK / Qt / 浏览器离不开 | L |
| C3 | 部署 | `libMobileGLServer.so --serve unix:/data/local/tmp/awl/mobilegl.sock` 作 root 守护进程由模块拉起、独立 SELinux 域,不走 APK;GLES 有 waylandbridge 先例,Vulkan 需实测 | S |
| C4 | 桌面程序会撞的空壳 | EGL sync / image 空壳;无 `EGL_KHR_surfaceless_context`;GLES context 名不副实(`GL_VERSION` 报桌面 4.6);无固定管线;device lost 时 swap 不返回 `EGL_CONTEXT_LOST`(与 design/08 不符) | 按需 |
| C5 | X11 | GLX 外壳 + MIT-SHM 回读上屏(通用、慢);DRI3 需要 Xwayland glamor(GBM + dma-buf 导入导出)→ 建议 Xwayland 继续用 Mesa / 软件 | 后置 |
### 仅方案 A
| # | 项 | 内容 | 量级 |
|---|---|---|---|
| A1 | 帧回交 | server 每个窗口 surface 约 4 个 AHB 槽(RGBA8888);Espryt 用 EGLImage 当默认帧缓冲,Magma 用外部内存 VkImage 代替 swapchain;Present 带 surface id / 槽号 | L |
| A2 | 真 fence | Espryt `EGL_ANDROID_native_fence_sync`、Magma `VK_KHR_external_semaphore_fd`(SYNC_FD)导出完成 fence;导入 release fence 后才复用槽 | M |
| A3 | 协议 | 新窗口类型 + 缓冲池注册 / 帧就绪(带 fence fd)/ 缓冲释放;现有 fd 传递单条 1 fd、sideband 256 B、client 只在握手收 → 要常驻接收;TCP 不传 fd,只能走 unix / `fd:` 端点 | M |
| A4 | client 端 Wayland | `EGL_PLATFORM_WAYLAND_KHR` / `wl_egl_window`;attach / damage / commit、frame callback 定节奏、configure / resize、viewporter 与分数缩放(缓冲像素 ≠ 逻辑尺寸);Anland 隐藏窗口会扣住 callback,不能因此 `PresentCreditTimeout` 丢会话。`wl_shm` 兜底也要这一项 | M |
### 仅方案 B
| # | 项 | 内容 | 量级 |
|---|---|---|---|
| B1 | Surface 接入 | server 经 binder 从 Anland 接窗口 Surface(`ANativeWindow_readFromParcel`,API 34+,与 waylandbridge 同一要求),交给现有 swapchain 路径(Espryt `eglCreateWindowSurface`、Magma `vkCreateAndroidSurfaceKHR`) | S |
| B2 | 推广 `ServerDisplay` | P12 的租约从"进程唯一的自有窗口"推广为"每个 surface 由 Anland 按令牌提供的窗口";尺寸由 Anland 事件驱动 | M |
| B3 | 失窗非致命 | Anland 分离窗口 → Surface 销毁 → 释放后端 surface,client 的 `eglSwapBuffers` 阻塞或丢帧直到重新挂上;今天 P12 是 `Fatal{ServerWindowLost}` 闩会话 | M |
| B4 | client 端 | `wl_egl_window` 只用来拿 `wl_surface` 与尺寸:从 Anland 扩展取令牌交给 server、转发 resize,不提交 `wl_buffer`;节奏靠 server 端 swapchain 的 FIFO | S |
## Anland 要改的
### 方案 A
| 档 | 改动 | 得到 |
|---|---|---|
| 不改 | server 回读进 `wl_shm`(写进 client 的 shm pool) | 所有 GPU 可用;每帧回读 + 上传(1080p ≈ 8 MB);正确性基线,也是 B 覆盖不到时的兜底 |
| 不改(dma-buf) | server 专门分配线性 BGRA,client 解析 AHB 线格式取 fd[0] 走 `linux-dmabuf` | 只有高通,脆弱,不推荐 |
| 原生 AHB 入口 | ① 给 render server 的本机注册通道(`AHardwareBuffer_recvHandleFromUnixSocket` 或新 binder 事务),返回绑定 client uid 的令牌 ② 新 Wayland 全局把令牌包成 `wl_buffer` ③ `awl_buffer` / `awl_bq_buffer` 改带类型标签(现把 `dmabuf_fd<0` 当空) ④ `awl_ahb_cache_get` 原生缓冲跳过伪造 ⑤ `awl_ahb_hwc_scanout_ok` 不作用于原生缓冲 ⑥ 原生缓冲只走显式 release ⑦ GL / SC 路径正确处理 RGBA | 不依赖高通、Mali 可用;保留 UBWC 与 HWC 直出;glibc client 不碰 gralloc |
### 方案 B
| 改动 | 说明 |
|---|---|
| Wayland 扩展 | 把某个 `wl_surface` 标成外部渲染 → 返回令牌;允许不提交 `wl_buffer` 也能映射;frame callback 仍按 vsync 发 |
| Surface 交接 | 把该窗口的 Surface 与挂上 / 分离 / 尺寸事件交给 MobileGL server(守护进程 `ANativeWindow_writeToParcel` 转发,或 server 调 `anland.host` 新事务在回包里取);分离前先通知收回 |
| SC 结构 | 该窗口强制 SC 模式;不给根 surface 建子层;popup / 子 surface / 光标照常作为子层叠在上面 |
| 鉴权 | 只把 Surface 交给与该 `wl_surface` 同一 client uid 的会话 |
局限:只覆盖 toplevel 的根 surface(子 surface 上的 GL 如视频播放器走 A);GL 画面与其他图层不是原子提交;父层之下的子 surface 看不见;EGL 模式不可用;Anland 基于缓冲的机制(扣缓冲停车、SC 直出 / 拷贝切换)不作用于这个窗口。
### 共用
| 档 | 改动 | 得到 |
|---|---|---|
| 顺手修 | `DRM_FORMAT_MOD_INVALID` 写成 `0x00ffffff00000000`(UAPI 为 `0x00ffffffffffffff`);offset 被忽略、modifier 不校验 | 修 bug;只往格式表加 AB24 不够(伪造写死 BGRA) |
| 部署 | `service.sh` 拉起 MobileGL server;sepolicy 新域:`droidspacesd` connectto、双向 fd use、SurfaceFlinger / HWC / system_server 对新域 fd 的 use;B 另需 binder 互调规则 | 必需 |
| 可选 | `linux-dmabuf` v4 feedback(main_device)、`wp_presentation` | Xwayland / 帧时统计 |
A 的原生 AHB 入口与 B 的扩展都要 Anland 作者合入,先给他们接口草案;B 贴合 Anland 现有的 SC 结构,但是 Anland 专用,不是标准协议。
## 分期
1. **M0**(Anland 不改):C1 + A4 + `wl_shm` 回读,单应用单窗口闭环;验节奏、resize、隐藏窗口。之后作兜底。
2. **M1**(方案 B):Anland 扩展 + B1–B4;一个 GL 顶层窗口的应用零拷贝。
3. **M2**:C2 多 client / 多窗口 / 多 context,GLES context。
4. **M3**(按需):A1–A3 + Anland 原生 AHB 入口,覆盖子 surface 上的 GL 与 EGL 模式。
5. **M4**:C5 X11 走 MIT-SHM;glamor / GBM 视需求。
## 只能上设备回答的
- root 守护进程能否加载厂商 Vulkan(GLES 有 waylandbridge 先例)。
- enforcing 下完整的 SELinux 规则。
- B:外部进程连上 SurfaceView 父 Surface 当生产者,与 Anland 的子 SurfaceControl 并存是否正常;resize 时缓冲尺寸与层几何短暂不一致的表现。
- A:各家 gralloc 的 RGBA / UBWC 缓冲能否被 SF 直接上屏;Mali 上回读路径的性能。
## 旁支
- Termux 原生(bionic)程序可以进程内直接加载 MobileGL(monolith,厂商驱动),不需要 split;帧交接同样是 A(A1、A2、A4)或 B(Surface 交给该进程)二选一。但 Termux 线目前基于 Anland 5.x(lfdevs 维护)。
- 这项工作不在 [`../../ROADMAP.md`](../../../Disaggregated/ROADMAP.md) 里,要做是一个新阶段。
-24
View File
@@ -1,24 +0,0 @@
# Brief A — Anland deep dive (READ-ONLY research)
Context: we are evaluating whether MobileGL (a desktop-OpenGL implementation for Android that translates GL onto the device's GLES driver ("Magma" backend) or Vulkan driver ("Espryt" backend), and which can now run split into a *client* process (the app's libGL/libEGL, can be glibc Linux) and a *render server* process (on Android, uses the vendor bionic GPU driver), connected by unix socket + shared memory or TCP) could be the GL provider for graphical Linux apps shown through **Anland**. We need precise facts about what Anland accepts and how it works, so we can say what MobileGL must implement and what Anland must change.
Repos are already cloned (shallow, read-only) under:
`<scratchpad>/anland/`
- `SuperTurtleDev_anland` (main = 6.x; the core: `services/waylandbridge/`, `app/`, `app/libawl/`, `module/`, `anland-session/`, `patches/`)
- `SuperTurtleDev_anland-shell`, `lfdevs_anland-termux`, `E7G_wlroots-anland`, `asveroid_anland-plasma-termux`, `Yizhou147_anland-drm`
You may also `git clone --depth 1 -b legacy https://github.com/SuperTurtleDev/anland.git` into that same `anland/` folder if you need to compare with 5.x, and use the web / GitHub (`gh`) for issues/discussions about GPU acceleration. Do NOT modify any other location. Use the shell available to you (Git Bash on Windows is fine; WSL `wsl.exe -d Arch -- bash -lc "..."` also works).
Answer each question with file:line citations. Be concrete; quote struct/enum values and constants where relevant.
1. **Buffer ingress contract of waylandbridge.** Which wl_buffer kinds are accepted (wl_shm formats; zwp_linux_dmabuf_v1 version, formats, modifiers, number of planes, feedback/default-feedback support, `main_device`/dev_t advertised?). What exactly happens to a dma-buf from `create`/`create_immed` to display: read `awl_dmabuf.c`, `awl_bufferqueue.*`, `awl_ahb.cpp` (the "forging" of an AHardwareBuffer around a container dma-buf: donor blob, calibration, `AHardwareBuffer_recvHandleFromUnixSocket`), `awl_gl.cpp`, `awl_sc.cpp`, `awl_renderer.cpp`. What are the preconditions for a dma-buf to be accepted (linear only? stride rules? size? format BGRA only? what if the buffer came from gralloc already?) and failure modes (refuse vs GPU copy). Is there any path that accepts a *native* AHardwareBuffer / gralloc handle from a client directly (not a raw dma-buf)? How is UBWC/compression handled or excluded?
2. **Sync.** `awl_esync.c`: which explicit-sync protocol (zwp_linux_explicit_synchronization_v1 version, linux-drm-syncobj?), acquire fence handling (waits CPU-side? hands to SF as acquire fence?), release fences, `wl_buffer.release` timing in SC vs EGL modes. Implicit sync assumptions for clients that don't use explicit sync.
3. **Presentation / frame pacing.** frame callbacks, presentation-time, how many buffers a client should cycle, what "parking" of hidden windows does to clients (frame callbacks stop?), viewporter/fractional-scale, subsurfaces.
4. **Connection & process model.** How a container app reaches waylandbridge (socket path, bind mount, binder fd passing, `anland-session`/`setup.sh`), what uid / SELinux domain the daemon runs in (`module/sepolicy.rule`, `plat_service_contexts.anland`, `service.sh`), what the container's processes run as, and whether a *second* Android-side daemon (e.g., a GL render server executable shipped by us) could be launched the same way and reached from the container (socket location, SELinux rules that would be needed for fd passing between container processes and that daemon, or an untrusted_app APK process). How does libawl let a third-party Android app host Wayland windows (binder API, `Awl.java`, `AwlClient.java`, `include/awl.h`) — could an Android-side GL server use it?
5. **GPU acceleration today.** How container apps get GPU rendering now (Mesa turnip/freedreno on kgsl? zink? llvmpipe on Mali?): check `patches/xwayland/anland-kgsl-glamor.patch` + README, `anland-session/*`, `lfdevs_anland-termux` (mesa version/build flags, what drivers, termux-native vs proot vs chroot), `asveroid_anland-plasma-termux`, anland-shell. Any mention of Mali/PowerVR/Xclipse support or lack thereof, GL version reached, known perf numbers. What does Xwayland glamor need (GBM, EGL_EXT_image_dma_buf_import, EGL_MESA_image_dma_buf_export, DRI3) and what did the patch change?
6. **The waylandbridge renderer itself.** Does waylandbridge use Android's own EGL/GLES (bionic, vendor driver) — how is it statically linked yet using the vendor driver? What does it tell us about a root native executable on Android loading the vendor GLES/Vulkan driver (linker namespaces)?
7. **Derivatives.** `E7G_wlroots-anland` ("native Anland dma-buf backend"), `Yizhou147_anland-drm`, legacy 5.x "Anland Display Protocol": one paragraph each on what they reveal about alternative frame paths (e.g., a private protocol that could carry AHB handles).
8. **Where Anland would need to change** to (a) accept buffers rendered by an Android-side GL server that allocates them with AHardwareBuffer_allocate (gralloc, possibly UBWC, RGBA8888 i.e. DRM ABGR8888), and (b) avoid the forge round-trip. List the concrete insertion points (functions/files) and the smallest viable change vs the clean change.
Output: write the full report (Markdown, Chinese or English, tables welcome, every claim cited file:line) to
`<scratchpad>/report-anland.md`
**BEFORE** your final message; then make your final message a 10-line summary. Mark anything you inferred (not read) as "inferred".
@@ -1,21 +0,0 @@
# Brief B — MobileGL inventory for a "GL provider for Linux-on-Android (Anland)" scenario (READ-ONLY)
Context: Anland (github.com/SuperTurtleDev/anland, 6.x) is a Wayland host on rooted Android: Linux apps run in a glibc container (Droidspaces chroot; also Termux native (bionic) and PRoot) and connect to a root Android daemon `waylandbridge` over a wayland socket. waylandbridge accepts `wl_shm` and `zwp_linux_dmabuf_v1` buffers (today: single-plane ARGB8888/XRGB8888, LINEAR/INVALID modifier only), forges an AHardwareBuffer around the dma-buf and hands it to SurfaceFlinger (SurfaceControl mode) or composites it with GLES (EGL mode); it also implements zwp_linux_explicit_synchronization_v1. Container apps get GPU today only through Mesa (turnip/freedreno on kgsl, zink) — i.e. Adreno only.
The question: what must **MobileGL** implement so that Linux apps in that container (and Xwayland/X11 apps) render through MobileGL — most plausibly by using the disaggregated split: a glibc aarch64 *client* in the container, the Android *render server* using the vendor driver — and hand frames to Anland.
Code: the up-to-date integration tree is in Arch WSL at `~/w7/p9-main` (branch feat/disaggregated, P8 closed). Operate it ONLY through `wsl.exe -d Arch -- bash -lc "<cmd>"` and read-only commands (cat/grep/git log/git grep/find). Do NOT build, do NOT edit, do NOT run git commands that change the tree (checkout/restore/stash/reset/commit), do NOT touch other trees or devices. Docs to start from: `docs/Disaggregated/ARCHITECTURE.md`, `design/*.md` (esp. 06-transport, 08-runtime-and-platform, 09-build-switches-counters), `ROADMAP.md`, `notes/p12/*`, `notes/DEBTS.md`, `notes/OPEN-QUESTIONS.md`, `MobileGL/MG_Remote/CONTRACT-*.md`, then the code (`MobileGL/MG_Remote/`, `MobileGL/MG_Impl/EGLImpl*`, `MobileGL/MG_Impl/GLXImpl/`, backends' surface/swapchain/present code, CMake options for Linux/client/server builds, `android-plugin/` server APK / `MobileGLDisplayActivity` / `MobileGLServerService`).
Answer each with file:line citations (paths relative to the tree root):
1. **Linux client build.** Can the client side be built for Linux glibc today (x86_64 WSL/CI yes? aarch64?) Which CMake options/presets; which libraries are produced (names/sonames: libGL? libEGL? libMobileGL.so?); which API entry points are exported (desktop GL core/compat versions, GLES API via eglBindAPI(EGL_OPENGL_ES_API)? GLX? EGL?). Is there libglvnd vendor integration (`__egl_Main`, `__glx_Main`, egl_vendor.d JSON) or does it replace libGL/libEGL directly?
2. **EGL frontend capabilities.** Supported EGL platforms/native window types on Linux (surfaceless only? `EGL_PLATFORM` handling, x11/wayland/gbm?), `eglGetPlatformDisplay`, config attributes, window/pbuffer/pixmap surfaces, context sharing, multiple contexts/threads per client, which EGL extensions are advertised (esp. EGL_EXT_image_dma_buf_import(_modifiers), EGL_MESA_image_dma_buf_export, EGL_KHR_fence_sync / EGL_ANDROID_native_fence_sync, EGL_KHR_swap_buffers_with_damage, EGL_EXT_buffer_age). What does GLXImpl do (does it talk to an X server? DRI? or is it a stub for desktop builds)?
3. **Present path in split mode today.** Trace `eglSwapBuffers` from client to server for (a) client-owned window (`SetWindowHandle`, Rule H), (b) `MOBILEGL_IPC_SURFACE=server` ServerOwned (P12), (c) pbuffer/surfaceless. Where does the server get its presentation target in each backend (Espryt: VkSurfaceKHR/VkSwapchainKHR from ANativeWindow? Magma: EGL window surface?). Is there ANY existing code that renders the default framebuffer into an exportable buffer (AHardwareBuffer, dma-buf, VK_ANDROID_external_memory_android_hardware_buffer, VK_KHR_external_memory_fd, EGLImage from AHB, `eglGetNativeClientBufferANDROID`) and sends it back to the client, or that exports a fence fd (VK_KHR_external_semaphore_fd SYNC_FD / EGL_ANDROID_native_fence_sync)? Grep for AHardwareBuffer, dma_buf, DMA_BUF, SCM_RIGHTS, sync_file, SYNC_FD, external_memory across the tree and report every hit's purpose (e.g. P11 adoption / T0 POST).
4. **Fd passing & transports.** Which transports can carry file descriptors (control plane is said not to; the ShmLink aux AF_UNIX rendezvous does SCM_RIGHTS) — where is that code, and how hard is it to add "server → client: here is a buffer fd + a fence fd per present"? Who creates the shm segments (client-owned SEG_CMD/SEG_STAGE via memfd on Linux) — relevant because an Android untrusted_app server may be SELinux-denied `fd use` on fds created by a root container process.
5. **Session/multiplicity limits.** One server per device, one session at a time (`Refuse{Busy}`), one surface mode per session, server-owned single window, "fork-per-session" supervisor for offscreen — cite where these limits live and how deep they are (supervisor only, or process-global state in backends?). What would it take to serve N concurrent clients each with M windows (a Linux desktop runs many GL apps + possibly Xwayland glamor)?
6. **Window-system-facing events already in the protocol.** Resize/geometry, `SurfaceOp/SurfaceReply`, `WindowKind`, device-lost behavior, present credit (`MOBILEGL_IPC_PRESENT_CREDIT`) and how a compositor's frame callbacks / buffer release would plug in.
7. **GL coverage relevant to generic Linux desktop apps** (not only Minecraft): max GL version per backend, compatibility profile / fixed-function support, GLES contexts, GLX_ARB_create_context, known gaps listed in docs. One short table.
8. **Anything in docs/notes/OPEN-QUESTIONS/DEBTS that already anticipates Linux-on-Android, Wayland, dma-buf, AHB export, multi-client, or Termux** — quote it.
Output: write the full report (Markdown; Chinese OK; tables welcome; every claim cited) to the Windows path
`<scratchpad>/report-mobilegl.md`
using your own file-writing tool (not a heredoc inside bash -lc), **BEFORE** your final message; then make your final message a 10-line summary. Mark anything you inferred (not read) as "inferred".
@@ -1,174 +0,0 @@
# 为了在天玑 1100 + Mali(内核 4.14)上跑起来,改了什么、为什么
> 本文只记**改动与动机**。参考机(Lenovo Y700 / Adreno 750 / 内核 6.1 / KernelSU / 有 systemd 的
> Arch 容器)的完整操作手册见 [`runbook-plasma.md`](runbook-plasma.md);本文的改动都在分支
> `feat/disaggregated-fix-d1100-mali` 上,两个在 MobileGL 里(本仓库),两个在 anland 的
> consumer app 里。
>
> 判据:改之前 `screencap` 是 37 KB 的纯黑;改之后是 1.4 MB 的完整 Plasma 桌面
> (壁纸 + 面板 + 时钟),client 日志的验收项从 `presented through wl_shm` 变成
> `presented through linux-dmabuf shared images`。
## 这台机器与参考机的差异(每条改动都从这里来)
| 项 | 参考机 | 本机 |
|---|---|---|
| GPU / 驱动 | Adreno 750 | **Mali**(`/dev/mali0`) |
| 内核 | 6.1 | **4.14.186**(cgroup v1) |
| 容器 | Arch + **systemd** | Fedora 44,**无 systemd**(`custom_init=ds-init.sh`) |
| 容器里的编译器 | 有 | **没有**,且无 DNS → 全部主机交叉编译 |
| `/dev/dri` | 有 `renderD*` | 只有 `card0` |
---
# MobileGL 仓库(本分支)
## 改动 1:dma-buf 身份认不出来时,用导入自己的 extent + format 认领
**文件**:`MobileGL/MG_Remote/Server/SharedImageRegistry.{h,cpp}`、`MobileGL/MG_Remote/Server/PipeApplier.cpp`
**改了什么**:`Identify(int fd, …)` 增加 `width/height/fourcc` 三个参数(导入记录本来就有它们)。
内核身份 `(st_dev, st_ino)` 仍然优先;只有当注册表已经发现身份**不唯一**时,才退化为:在该身份
下已导出的活镜像里,挑 **extent 与 fourcc 都相同、且尚未被认领** 的那个,按 `Id` 从小到大
(= 分配顺序 = 生产者交给合成器的顺序),认领一次。
**为什么必须改**:零拷贝的整条链是"server 分配 AHardwareBuffer → 导出 dma-buf fd → client 把它做成
`wl_buffer` → 合成器 `eglCreateImageKHR` 导回 → server 认出是自己那块"。认的依据只有
`(st_dev, st_ino)`。本机内核给**所有** dma-buf descriptor 发同一个身份(实测三个不同的 buffer):
```
bo0: fd=15 dev=12 ino=10658 size=0 fdinfo[pos: 0 flags: 02000000 mnt_id: 12 ]
bo1: fd=17 dev=12 ino=10658 size=0 …
bo2: fd=19 dev=12 ino=10658 size=0 …
```
`fdinfo` 里没有 name、没有别的唯一量——用户态拿不到任何能区分 buffer 的东西。注册表检测到
"两块活镜像同一个身份"后**按设计 fail-closed**:
```
[ERROR] two live shared images have one dma-buf identity (dev 12 ino 10658); this kernel does not
keep dma-buf inodes unique …
[WARN] a dma-buf import was refused: dma-buf identities are not unique on this kernel
```
于是**每一个** linux-dmabuf 窗口都被拒:client 全退回 `wl_shm`(`8 presented through wl_shm`),
合成器也没法采样它上不了屏的东西。fail-closed 本身是对的(猜错会把别人的像素当自己的),
缺的只是**内核给不出身份时还有别的可用依据**——导入声明的尺寸和格式,加上 server 自己的分配顺序。
**验证**:自包含探针 `eglprobe4.c`(主机 clang+lld 编,容器里以 Gold 跑)——`gbm_bo_create`
取一块 server shared image 的 fd,再走合成器的导入路径:修前
`eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) -> (nil)`,修后 `-> 0x1`;会话里
refusal 归零、client 日志变成 `presented through linux-dmabuf shared images`。
## 改动 2:damage 变体被驱动拒绝时,回落普通 `eglSwapBuffers`
**文件**:`MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp` 的 `Present()`
**改了什么**:`eglSwapBuffersWithDamageEXT` 返回 EGL_FALSE 时,用 `eglSwapBuffers` 再试一次,
并 `MGLOG_W_ONCE` 说明一次(没有该扩展的驱动照旧走普通入口)。
**为什么必须改**:server 的 `Present()` 原本优先用 WithDamage 变体(注释说"驱动提供哪个拼写就用
哪个")。本机的 Mali/4.14 vendor EGL **暴露了这个入口,却对窗口面返回 EGL_FALSE 且不设 EGL 错误**:
```
server window: 1080x2194 fmt=0x1 (pointer=0xb4000070d902ad00) ← ANativeWindow 是活的、尺寸对
swap entry points: withDamage=0x71a0887040 plain=0x71a087c62c
withDamage → swap=0 err=0x3000 ← 同一块 surface、同一线程
plain → swap=1 err=0x3000
```
排除了绑定问题(`session.Draw == eglGetCurrentSurface(EGL_DRAW)`、context 也 current),也不是窗口
问题(`ANativeWindow_getWidth/Height/Format` 都正常)——就是驱动拒。信任那个指针的后果是
**一帧都不入队**:SurfaceView 的 BLAST 层 `frame events` 全 0、`geomLayerBounds` 0×0,屏幕上只有
`Background for SurfaceView` 那块纯黑占位;合成器又能采样但因为上不了屏,client 也跟着退回
`wl_shm`。这条是"屏幕全黑"的最后一环。
**验证**:修后 `frame events` 出现真实时间戳,`screencap` 从 37 KB 变 1.4 MB 的完整桌面。
---
# anland 仓库(consumer app)
这两条不在本仓库里,但缺任何一条桌面都上不了屏。
## 改动 3:`:mobilegl` worker 设置 `MOBILEGL_IPC_SURFACE=server`
**文件**:`consumers/anland_v5/android_consumer/app/src/main/java/com/anland/consumer/MobileGLWorker.java`
**改了什么**:在 `Os.setenv("MOBILEGL_IPC_ROLE", "server", …)` 旁边加
`Os.setenv("MOBILEGL_IPC_SURFACE", "server", true)`。
**为什么必须改**:`Config.h` 里 `IpcTable::Surface` 的默认值是 `IpcSurface::Offscreen`。
client 侧(容器里的 `mobilegl-startup.sh`/`start-mobilegl-session.sh`)按 `server` 去要
server 的窗口,而 **server 自己的进程没有任何 `MOBILEGL_IPC_SURFACE`**,于是它把 app 递过来的
窗口 attach 了、却**从不创建 server-owned 窗口面、也从不往上面 swap**。日志上表现为只有一句
`a ServerOwned window surface is served on its window`,没有真正的
`surface=window … owner=server`。补上之后这一行立刻出现。
## 改动 4:几何回调不再把 Surface 塌成 0×0
**文件**:`…/MainActivity.java`(`MobileGLConnection` 的几何回调)
**改了什么**:回调收到 `(0,0)`("不指定尺寸")时,只有在视图**确实已有尺寸**时才
`holder.setSizeFromLayout()`;否则什么都不做,保留现有 surface。
**为什么必须改**:这个回调可能在视图第一次布局之前被调用,此时 `setSizeFromLayout()` 会把
`SurfaceHolder` 定成 0×0;0×0 的 surface 上任何生产者都无法 queue,之后每一帧
`eglSwapBuffers` 都会失败——正是"BLAST 层 `geomLayerBounds` 0×0 且永远没有 buffer"的成因。
---
# 环境侧(不是代码,但缺了就起不来)
1. **`libatomic.so.1`**:主机交叉编出来的 KWin 需要它,而这个 Fedora 运行时镜像没有
(`kwin_wayland: error while loading shared libraries: libatomic.so.1`)。从 sysroot 把
`libatomic.so.1.2.0` + 软链补进 rootfs(容器停着时直接写
`/data/local/Droidspaces/fedora-kde/usr/lib64/`)。`ldd` 其余 Qt6/KF6 依赖都齐。
2. **无 systemd 的会话**:容器里 systemd 259 因"内核 4.14 + cgroup v1"直接拒绝启动,会话由
`ds-init.sh` 自己拉起。等价物是把 `mobilegl-startup.sh plasma` 的那套 env 写进
`/opt/mobilegl/bin/start-mobilegl-session.sh`(PATH 前置 `/opt/mobilegl/kwin/bin` 让
`kwin_wayland_wrapper` 起我们的 KWin),再让 `ds-init.sh` 的最后一句 exec 它。**同时必须去掉
`LIBGL_ALWAYS_SOFTWARE=1`/`GALLIUM_DRIVER=llvmpipe`**:MobileGL 对"要求软件渲染的进程"主动
让位,留着它整个会话就还是 llvmpipe。
3. **GBM 后端的路径**:Fedora 的 loader 搜 `/usr/lib64/gbm`(上游脚本写的是 Arch 的
`/usr/lib/gbm`);`libGLX_mobilegl.so.0` 同理放 `/usr/lib64`。
4. **`/dev/dri` 只有 `card0`**:`MOBILEGL_GBM_NODE` 退到 `/dev/null` —— 那个节点只当身份、
后端从不对它 ioctl,这是 `mobilegl-startup.sh` 自己的回退路径,不需要改设备节点权限。
5. **KWin 的构建**:pristine `kwin-6.7.5` + `Arch_v5/kwin.patch` + `anland_backend_Arch_v5` +
顶层 `libdisplay_producer/`(后者的 `protocol.h`/`socket_utils.c` 是指向 `common/` 的符号链接,
**拷贝时要 -L 解引用**;也**不要**把 `common/` 整个拷进 backend 目录,会让 `kde_clang_format`
生成重名 target)。6.7.5 上只有 `src/opengl/eglcontext.cpp` 的 Hunk #2 打不上,按补丁原意手工把
no-arg `makeCurrent()` 里的 `EGL_NO_SURFACE, EGL_NO_SURFACE` 换成
`m_display->defaultSurface()`。
---
# 已知但未处理
- daemon 的 producer/consumer 每 ~5 s 抖一次(`producer connected/disconnected`、
`consumer re-deposited 5 fds`,kwin 侧 `consumer disconnected, entering fallback`)。MobileGL
模式下合成不受影响(窗口与默认 framebuffer 属于 KWin),桌面照常上屏,成因未查。
- 桌面静止时两次 `screencap` 的 md5 相同(时钟只到分钟、无动画);判断"活着"要看 server 的
`P65ServerFrame` 计数或 SurfaceView 的 frame events。
- **"桌面全黑"里有一半是锁屏**:会话跑一会儿后 kscreenlocker 会把屏幕锁上,看到的黑屏其实是
greeter(它自己也打 `Renderer Name: …`,所以日志里会误以为是桌面的渲染器)。判断方法:
`pgrep -a -f kscreenlocker_greet`。修法:写 `~/.config/kscreenlockerrc`
(`[Daemon] Autolock=false`/`LockOnStart=false`)后重启会话;**杀 greeter 没用**,kwin 会用
`--immediateLock` 再起一个,D-Bus 的 `SetActive(false)` 也解不开。这一条是"看不到完整桌面"
的直接原因,与渲染无关。
- **Magma(DirectVulkan)已复验:桌面能上屏**(`Renderer Name: Magma`、`8 presented through
linux-dmabuf`、SurfaceView frame events 是真实时间戳、`screencap` 1.05 MB 的桌面 + 面板 + 光标)。
上面三条代码改动都与后端无关,两个后端都撞到同一个坑,也都因此被修好。
- **Magma 少画字(未修,已缩小到一条线索)**:同一场景下所有 `Text` 都不画(键盘按键是空白白块、
没有 q/w/e…/"American English",面板时钟也没有),而矩形/图标/壁纸都正常。已用探针**逐条排除**:
`eglprobe6/8`(ES2 的 `GL_ALPHA` 与 ES3 的 `GL_R8`/`GL_RED` 采样 `.a`/`.r` 均正确)、
`eglprobe7`(`UNPACK_ALIGNMENT=1` 的窄行上传正确)、`eglprobe9`(图集模式:大纹理 +
`glTexSubImage2D` 偏移 + `GL_UNPACK_ROW_LENGTH`,值全对、未触碰区域仍为 0)、
`eglprobe10`(`fwidth` 与距离场 `smoothstep` 都正常)。
剩下的一手证据在容器日志里:**Magma 下 Qt 的 RHI 上下文建立失败**——
`QRhiGles2: Failed to make context current. Expect bad things to happen.` 与
`QWaylandGLContext::makeCurrent: eglError: 0x3002`(EGL_NOT_INITIALIZED),Espryt 下没有这两条。
下一步应从 `DirectVulkan` 的 `MakeEGLCurrent`/`OnClientContextBound`(`m_contextBindings`
按上下文找 surface 的那条路,注释里明确写着"Qt 每个窗口一个线程与上下文")查起。
顺带记一个可查项:Magma 的 `GL_EXTENSIONS` **不含 `OES_standard_derivatives`**,而 `fwidth`
实际可用。
@@ -1,186 +0,0 @@
# Handoff: anland 5.x + MobileGL unified server
Updated 2026-10-04. This replaces the 2026-10-02 (post-P14) handoff; git history keeps the old text.
## What this is
MobileGL runs disaggregated on a rooted Android tablet:
- **Client:** the glibc library in the Droidspaces container `arch-kde-mgl`, where it is the system-wide glvnd EGL/GLX vendor and the GBM backend.
- **Transport:** the client streams pipe records over abstract socket `@anland-mobilegl`.
- **Server:** runs in-process in the anland APK's `:mobilegl` foreground service.
- **Backends:** Espryt (DirectGLES) or Magma (DirectVulkan), chosen in the app settings.
KDE Plasma runs on top with zero-copy windows (server-allocated AHB dma-bufs over linux-dmabuf). Everything is vendor-agnostic: AHB, sync_file, EGL_ANDROID_*, VK_ANDROID_*; no kgsl or qcom specifics.
## Remotes and heads
- MobileGL: <https://github.com/MobileGL-Dev/MobileGL> `feat/disaggregated`, head `2d424146`. Integration worktree: `.claude/worktrees/anland-unified`.
- anland: <https://github.com/MobileGL-Dev/anland> `legacy-mobilegl-unified`, head `41ec066`. Integration worktree: `anland/.claude/worktrees/mgl-unified`.
- Version: 26.10 (`MobileGL/Config.h`, `android-plugin/app/build.gradle.kts`).
## Where to start reading
| Topic | Doc |
|---|---|
| Reproduce the whole setup on a new machine or phone | `.claude/skills/anland-mobilegl-plasma/reproduce.md` (and `SKILL.md`) |
| Day-to-day bring-up and checks | `runbook-plasma.md` |
| Zero-copy windows | `plan-ahb-dmabuf.md` |
| X11 on the GPU (glamor, DRI3/Present GLX) | `plan-x11-gpu.md` |
| glmark2 performance: baseline and the optimization handoff | `../../../Performance/glmark2-baseline.md`, `../../../Performance/glmark2-handoff.md` |
| VA-API driver on MediaCodec (plan only, nothing implemented) | `../../../MobileVA/plan.md` |
## State of the desktop (verified on the device, 2026-10-04)
- **App-driven bring-up:** opening the anland app starts daemon → server → container → Plasma. A foreground service keeps it alive, the server idles while the app is hidden (KWin DPMS off), and the notification has a "Stop desktop" action.
- **Both backends** run the desktop: KWin, plasmashell, Kickoff, notifications, the lock screen, resize on extra-keys-bar toggle, and suspend/resume on window loss.
- **Buffer age and damage** work end to end on both backends.
- **glvnd vendor:** MobileGL is the system-wide vendor (`10_mobilegl.json`, `/etc/mobilegl/client.conf`). It steps aside for `LIBGL_ALWAYS_SOFTWARE`, foreign GBM devices, and when no server answers (falls through in about 0.1 s).
- **X11:** Xwayland glamor on MobileGL. GLX presents through DRI3 + Present shared images, with a MIT-SHM readback fallback.
- **Chrome:**
- runs its GPU in a separate process with real EGL native fences, with no flicker;
- recovers from device loss (as do KWin and plasmashell);
- Shadertoy-style GPU faults are contained per session;
- a hanging client is cut off on Magma (watchdog `MOBILEGL_GPU_HANG_BUDGET_MS`) and on Espryt (GUILTY reset).
- **Chrome video:** hardware decode works through the container's VA-API driver. NV12/P010 dma-bufs are sampled via `GL_OES_EGL_image_external` on both backends. Foreign YUV buffers are CPU-copied into server YUV images, and 4K60 is fine. `MOBILEGL_CHROME_VIDEO_DECODE=software` opts out.
- **Device state at handoff:**
- MobileGL anland on Magma, Plasma up, server/client build `bb31f374`; later commits are docs, CI and a DirectGLES multi-session fix that is not yet deployed;
- clocks NOT pinned (restored); the original anland APK is stopped;
- pin and restore helpers: `su -c "sh /data/local/tmp/anl/pbclk.sh pin|restore"`.
## Work streams and where each one stopped
### 1. glmark2 performance (stopped at a handoff)
Details: `docs/Performance/glmark2-handoff.md`.
- **Now:** MobileGL beats the stock kgsl stack in windowed glmark2 on both backends (es2 800x600: Espryt ~1887, Magma ~1213, stock ~1100). Fullscreen is still about half of stock (Espryt ~561, Magma ~404, stock ~1177).
- **Next, ranked:**
1. render straight into the window's buffers (removes the fullscreen copy);
2. non-blocking present;
3. merge Magma's per-frame submissions;
4. Magma descriptor and pipeline caching;
5. profile terrain;
6. Espryt per-draw state sync;
7. non-blocking fences.
- Bench scripts: `tools/device_bench/anland_glmark2/`.
### 2. GitHub Actions CI (stopped mid-way, no handoff from its agent)
- **Background:** CI had been red since about 2026-09-26 (Test workflow) and since 2026-10-02 (APK workflow retraces). Most runs show `cancelled` only because each newer push cancels them.
- **Landed:**
| Commit | What |
|---|---|
| `eb71b669` | The pull and verify builds compile again |
| `c1e957cb` | Retired the P3a/P4a untouched-region gates; those phases closed and later work legitimately changed the regions |
| `78c5f8da` | Census records `Refuse{Recover}` as a local refusal |
| `32d27b14` | Real bug: a session's backend registration no longer trips the strict apply-thread pin while another session is applying (DirectGLES) |
| `2d424146` | Failed ctest entries are annotated with their failure lines, so failures can be read from the check-run annotations without a signed-in log reader |
- **Run on `2d424146`:** in progress at handoff. Gates, include-closure and flatc are green; the builds and integration jobs were still running.
- **Still open:**
- the APK workflow's **retrace failures** (Retrace and validate), on DirectGLES for minecraft-1.21.11-main-menu, 1.21.4 startup/in-world/main-menu, 1.17-main-menu-854 and OpenRA, and on DirectVulkan for 1.17-main-menu-854;
- they first appeared around `5accb098` / `8da082f8` (2026-10-02); last green APK run `2f64ede8`;
- not yet bisected. Candidates:
- the clear-colour default (0,0,0,0), which is GL-correct, so judge any golden against the spec;
- anonymous-struct sampler handling in glslang;
- sampler CSO publishing;
- the scissor-shadow fix;
- the inactive-fragment-output location fix;
- the external-texture and YUV changes;
- the perf commits.
- **User guidance on judging checks:**
- phase-era process gates may be overly strict, and superseded ones may be removed with a reason;
- retraces and OpenGL-level checks must pass;
- a feature a backend does not implement (e.g. iterationRP) may be excluded narrowly per backend.
- **Reading CI without `gh`:** `curl https://api.github.com/repos/MobileGL-Dev/MobileGL/actions/runs?branch=feat/disaggregated`, then `/actions/runs/<id>/jobs` and `/check-runs/<job>/annotations`. Raw logs need sign-in.
### 3. Chrome hardware video (landed) → vendor-agnostic VA-API (planned only)
- **Container driver:** `/usr/lib/dri/msm_drm_drv_video.so` is an unpackaged third-party, Qualcomm-only driver. It drives V4L2 at `/dev/video32` with Qualcomm modes and copies frames on the CPU itself. libva picks it because the render node's DRM driver name is `msm_drm`.
- **Plan in `docs/MobileVA/plan.md`:** a new `MobileVA/` directory; MediaCodec decode in the `:mobilegl` process; H.264 first, then VP9, AV1, HEVC/10-bit; the old driver kept as A/B fallback.
- **Prerequisites the plan found:**
- per-plane R8/GR88 dma-buf import, which mpv's GL path and KWin's NV12 path need;
- YUV sampling paths that wait on the shared image's write fence (they appear not to).
### 4. MobileGL work landed since the previous handoff (newest first, grouped)
- **Perf:**
- `bb31f374` Magma keeps render passes open across draws;
- `e5ee31b0` android-30 for native TLS;
- `915882b2` record timing only with stats on;
- `963f4d81` Magma command buffer reuse;
- `d627fca3` the server wakes a parked client only when its sequence is applied;
- `2de0d6dd` client-array buffer reuse;
- `66b47a10` glFinish waits for the GPU, plus ES OES strings.
- **YUV:** `7218bacb` NV12/P010 shared images, `GL_OES_EGL_image_external`, Espryt `GL_EXT_YUV_target`, Magma Ycbcr conversion.
- **Device loss and hangs:**
- `287e1df8` cut off the session that hangs the GPU;
- `7823bcd9` client fresh-session recovery;
- `90e63c9e` robustness extensions;
- `58c3d9bd` per-session device-loss latch;
- `36190b7c`, `cf763b40`, `1dd888cb`, `f22ae625`, `f52a5467` loss and teardown edge cases.
- **Multi-thread and multi-context fixes:** `a9f22ebb`, `25a76c1b`, `393002a4`, `a84104a4`, `2acc6c41`, `8024b645`.
- **Chrome:**
- `4f6f4125` real EGL native fences;
- `30977c18` opt-in ES identity for ES contexts (the GPU process on ANGLE);
- `5accb098` glslang anonymous-struct samplers (Shadertoy page fault).
- **X11:**
- `198d6fea` GLX via DRI3/Present;
- `08f23543` glFlush implicit sync for glamor;
- `5de6ecd4`, `7fd965ce` shared-image upload and publish;
- `cd58f048` inactive fragment output location;
- `316ffe02` never dial its own X display.
- **glvnd:** `69e96f5b`, `4095624f`, `4f679d66`, `3d69c375`, `552dd3d3`.
- **Present:**
- `74919a3b` / `7090e31d` buffer age and damage;
- Espryt fixes `815217cc` (scissor shadow is per-session), `c822a9fe`, `228bd24c`;
- `1ff7ca53` / `caa00995` frame-callback pacing;
- `5a7b23f6` / `258b9460` / `16b07cf4` suspend, resume and resize.
- **Shared images:** `38842985`, `3d17e8c1`, `350fc699`, `67226176`, `b5cab441`, `7520e53b`.
- **Misc:**
- `8da082f8` clear colour starts at (0,0,0,0);
- `51c37bcb` sampler param conversion;
- `15f9b27a` sampler CSO per share group;
- `239fc228` gl_FragCoord access chains;
- `bfa7987c` Magma declines invalid SPIR-V;
- `6125f941` surfaceless/no-config EGL;
- `4633ca22` frame-stats log gated (`MOBILEGL_SERVER_FRAME_STATS`);
- `2a1889b8` gtest discovery at ctest start.
### 5. anland work landed (newest first)
- `41ec066` / `f60b261`: Chrome video decode modes.
- `e48874c`: KWin recovers from its own session loss.
- `b9f8162` / `cf07bc7`: Chrome GPU process by default.
- `3d8c4dd` / `5d30340`: Xwayland glamor.
- `89894f1` / `a55e975`: DPMS off while hidden.
- `74bd2d3`: the app starts the desktop, foreground service.
- `0fd5d01` / `144f55c`: APK build guards.
- `90c44d7`: system-wide vendor.
- `23fc73f`: buffer age and damage in KWin.
- `9773d1e` / `2dfda4a`: resize follow.
- `f9e41df`: GBM device for linux-dmabuf.
- `5f15240`: backend choice.
## Known open issues (rough priority)
1. **CI:** retrace failures (see stream 2) and whatever the `2d424146` run still shows red.
2. **Fullscreen performance** at about half of stock (see stream 1).
3. **Espryt's ~10 s Preemption-Fault hang path:** the driver doesn't report the reset to the server's contexts, so a hanging client stalls the desktop for 10 s per hang there.
4. **YUV gaps:**
- YUV sampling doesn't wait on the write fence (suspected);
- no single-channel (R8/GR88) dma-buf import, so KWin advertises no YUV formats and mpv's GL interop won't work;
- the CPU copy for foreign YUV buffers is a fallback, not zero-copy.
5. **Chrome** doesn't use buffer age (full-frame repaints). The Magma text-caret frame costs about 24 ms.
6. **A rare Magma X11 rectangle glitch.** The readback GLX fallback is slower with glamor.
7. **Two GoldControllerTest failures** on Windows hosts. MSVC builds of this branch are broken; build tests in WSL with clang.
8. **Leftovers:**
- container probe files under `~swung0x48/yuvtest` and the local `v.html` tabs in the Chrome profile;
- an empty directory `.claude/worktrees/quirky-poincare-df3f3e` (locked by Windows);
- WSL `~/mgl-espryt*` / `~/mgl-magma*` directories of unknown owner (kept).
## Rules this work follows
- **Commits:** `--no-verify`, one short sentence, no Co-Authored-By trailer. No external project names in code or comments.
- **Tests:** never run the DirectVulkan integration tests (`ctest -E DirectVulkan`); never run two ctest suites at once.
- **Worktrees:** one worktree and build directory per task; remove the worktree, branch and build directories right after merging. Keep `anland-unified`, `mgl-unified`, WSL `~/mgl-xbuild-a64`, `~/mgl-anl-bld` and `~/sysroots`. Don't touch `wgl-host` or `~/repos/mobilegl-cts-gl33`.
- **Core profile only:** don't use fixed-function test apps such as glxgears.
- **Device:**
- stop device work while the user is using the phone;
- on USB power, watch the battery (stop below 40%);
- wireless adb needs re-enabling after a reboot.
@@ -1,47 +0,0 @@
# 把 kgsl 看成 server:A / B 两条路线的另一种读法(2026-09-30)
> 接 [`README.md`](README.md) 的"对照:容器里现在的 Mesa"。只读推演,没做实验;Anland 事实链到审计版本(6.x `cc169180`、5.x `9ab13eb1`)。
## 结论
A / B 的区别不在"渲染器放在哪",而在**缓冲归谁**:
- **A**:server 分配缓冲、画好,把句柄交给合成器。
- **B**:窗口把自己的缓冲交给 server,server 直接画进去。
把内核 kgsl 当 server,Anland 两代现有路径正好各占一条:6.x + Mesa 是 A,5.x 私有协议是 B。
## 对照
| | 6.x + Mesa | 5.x 私有协议 | MobileGL A | MobileGL B |
|---|---|---|---|---|
| client | 应用进程里的完整用户态驱动(freedreno / turnip) | 合成器进程(如 KWin)里的 Mesa | 应用进程里的 GL 前端 | 同 A |
| server | 内核 kgsl | 内核 kgsl | Android 用户态进程(厂商驱动,往下仍是 kgsl / kbase) | 同 A |
| 命令通道 | ioctl + 共享 GPU 内存 | 同左 | MGPipe(共享内存环 + 门铃) | 同 A |
| 缓冲归谁 | kgsl 分配 → dma-buf 给 client → `wl_buffer` → Anland 导入(伪造 AHB) | 窗口缓冲队列分配 → `data[0]` 以 dma-buf 交给 kgsl 渲染 → 回 sync_file → Android 端 `queueBuffer` | server 用 gralloc 分配 AHB → Anland | 窗口 Surface 交给 server |
| 形状 | A | B | A | B |
5.x 出处:[protocol.h:7–49](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/common/protocol.h#L7-L49)、[native_consumer.c:144–172](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L144-L172)、[:955–1003](https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L955-L1003);6.x 伪造 AHB 的起因:[awl_ahb.cpp:10–14](https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L10-L14)。
## 读出来的三点
1. **B 不是新东西。** Anland 5.x 就是 B,只是 server 在内核。6.x 放弃它是为了让原生合成器不改就能接入,不是因为做不通。
2. **server 在内核时两条路都只需要传 fd;挪到用户态就多一层。**
- 内核对象(dma-buf fd、sync_file)任何进程都认。
- server 是用户态进程时:A 要传完整 AHB(gralloc 元数据 + dma-buf),B 要传缓冲队列的生产端(Surface)。
- 与现状正好相反:kgsl 句柄通用但缺 gralloc 元数据,所以 Anland 只能伪造;MobileGL server 的 AHB 本来就带元数据,Anland 只要肯收完整句柄。
- B 也可以照 5.x 逐缓冲交接(Anland `dequeueBuffer` → 整个 AHB 交 server → 回 sync_file → Anland `queueBuffer`);对 MobileGL 交 Surface 更省,直接复用 swapchain。
3. **拆分层级不同 = virtio-gpu 的 native context 对 virgl / venus。**
| | Mesa / kgsl(拆在硬件命令层) | MobileGL(拆在 GL 状态层) |
|---|---|---|
| client 要懂什么 | 这块 GPU 的硬件命令 | 只懂 GL |
| 能覆盖的 GPU | 有开源用户态驱动的(现在只有 Adreno) | 有厂商驱动的(含 Mali) |
| 额外开销 | 几乎没有 | 状态序列化 + IPC |
## 对 MobileGL 的启示
- **server 必须天然多 client**:kgsl 同时服务任意多进程、每进程独立 GPU 上下文——对应 README 的 C2(今天每 supervisor 一个会话)。
- **fence 导出(A2)没有原理障碍**:厂商驱动导出 SYNC_FD,最终也是向 kgsl / kbase 要一个 sync_file,fence 仍是内核对象。
@@ -1,95 +0,0 @@
# 计划:AHB 当 dma-buf,server 当分配器(窗口零拷贝)(2026-10-02)
> 接 [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md)。调研 + 设备探针,未动代码。硬约束:**厂商中立**——只依赖 Android 公开设施(AHardwareBuffer、EGL_ANDROID_*、VK_ANDROID_external_memory_android_hardware_buffer、sync_file)和通用 Linux 设施(dma-buf、SCM_RIGHTS、libgbm 后端 ABI、linux-dmabuf 协议),不碰 kgsl / gralloc 私有布局 / 厂商修饰符。
## 现状:每个窗口每帧 GPU→CPU→GPU
KWin 自己(`MOBILEGL_IPC_SURFACE=server`)直接画在 Android Surface 上,没问题。其余所有 Wayland 客户端:
1. **生产端**(`EGLImpl.cpp:210` → `WaylandWindow.cpp:398`):server 渲染进 pbuffer;`eglSwapBuffers` 时客户端 `ReadPixels` 整帧,按 ~2 MiB 回复槽切带,**每带一次阻塞往返**;server `glReadPixels`/`vkCmdCopyImageToBuffer`+等待+重排 → 回复槽 memcpy → 客户端 memcpy → BGRA 换序+翻转写进 `wl_shm`。CPU 整帧遍历:Espryt 4 次,Magma ~6 次。
2. **消费端**(KWin 也是 MobileGL 客户端):`glTexSubImage2D` 上传 wl_shm → 解包 malloc+拷贝 → 层影子拷贝 → **整层**(不是脏区)进 SEG_STAGE → server `AdoptRun` 再拷贝 → 驱动上传。又 4 次。
3. linux-dmabuf 根本没开:KWin 6.7 的 `EglBackend::initWayland` 要求 `drmDevice()`,anland 后端在 MobileGL 模式下给空设备,`kwin.patch` 让它直接退回 wl_shm(还连带了 screencast 的空指针崩溃补丁)。
2560×1412 的窗口一帧 14.5 MB,上面合计 ~8–10 次整帧 CPU 遍历 + 若干阻塞往返。
## 目标形状
```
client(GL) ──渲染──▶ [AHB] ◀──采样── KWin(GL) 全在 server 进程里:同一块 AHB,两个会话各自导入
│ ▲ dma-buf fd CPU 拷贝 0 次;fd 每块缓冲只传一次
└─ wl_buffer(linux-dmabuf, fd) ─▶ KWin ─ eglCreateImage(DMA_BUF, fd) ─▶ server 按身份认回 AHB
```
关键点:**生产者和消费者都是同一个 server 的会话**。所以"导入 dma-buf"不需要驱动支持 `EGL_EXT_image_dma_buf_import`(Adreno 的 Android EGL 就没有)——server 只需认出"这是我发出去的那块 AHB",再走 Android 标准导入:Espryt 用 `eglGetNativeClientBufferANDROID` + `EGL_NATIVE_BUFFER_ANDROID`,Magma 用 `VK_ANDROID_external_memory_android_hardware_buffer`。两者都是 Android 规范要求的,厂商中立。
## 设备探针(HA27Q3LQ,内核 6.1,Android 15)
- AHB(RGBA8,GPU 用途,有/无 CPU 用途)的 native_handle = 2 个 fd,都是 `/dmabuf:` 文件(图像 + 32 KB 元数据)。
- 从 Android 侧(`shell` 域)经 SCM_RIGHTS 发给容器进程(`droidspacesd` 域):收到、`mmap`、像素逐点正确;两边 `st_ino` 相同 → **按 inode 认身份可行**。
- 容器里有 `/dev/dri/renderD128`(显示驱动 msm_drm,组 `droidspaces-gpu`),Mesa 26.3 的 libgbm 会按 `GBM_BACKEND` 或驱动名加载 `/usr/lib/gbm/<name>_gbm.so`(ABI v1,`gbm_backend_abi.h` 已装)。**但这个节点是厂商的,别的机器未必有**——见下面"薄节点"。
- KWin 6.7:`RenderDevice::open(path)` = `DrmDevice`(open+fstat+`gbm_create_device`) → `eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm)`;`initWayland` 的 tranche 设备号取 `renderDevNode()` 否则 `drmDevice->deviceId()`;dma-buf 导入走 `eglCreateImage(EGL_LINUX_DMA_BUF_EXT)`,格式表走 `eglQueryDmaBufFormats/ModifiersEXT`。
## 厂商中立的几条规则
1. **只分配 AHB**,只用公开 NDK 拿 fd:`AHardwareBuffer_sendHandleToUnixSocket` 发到 server 自己的 socketpair,`recvmsg` 取 SCM_RIGHTS 里的 fd(不依赖 LL-NDK 的 `getNativeHandle`,也不解析 handle 里的 int)。
2. **不假设哪个 fd 是图像**:发 fd[0] 作 plane 0,身份登记 handle 里的所有 fd。
3. **不假设布局**:默认通告 `DRM_FORMAT_MOD_INVALID`("驱动定义布局",正好就是 AHB 的语义)。`LINEAR` 只在启动自检通过时才通告:分配带 CPU 用途的 AHB,`lock` 写图案,`mmap` fd[0] 按 `describe().stride` 读回比对。自检失败就不通告,CPU 映射走 server(`AHardwareBuffer_lock` + 拷贝)。
4. **身份**:首选 `st_ino`(启动时分配两块确认 inode 互异——老内核所有 dma-buf 共用一个 anon inode);不唯一时退到 `DMA_BUF_SET_NAME` 打 `mgl:<id>` 标签、读 fdinfo 的 `name:`。非本 server 分配的 dma-buf → `EGL_BAD_MATCH`(KWin 会回退 shm)。
5. **同步只用 sync_file**:`EGL_ANDROID_native_fence_sync`、`VK_KHR_external_semaphore_fd`(SYNC_FD)、必要时 `DMA_BUF_IOCTL_EXPORT/IMPORT_SYNC_FILE`(通用 Linux ≥5.20/6.0)。不用 DRM syncobj(要真 DRM 驱动)。
6. **薄节点**:GBM 后端用 `GBM_BACKEND=mobilegl` 选中(`/usr/lib/gbm/mobilegl_gbm.so`),不装成 `msm_drm_gbm.so`。它对设备 fd 不发任何 ioctl,只当"打开了某个设备"的令牌;真正的通道是到 server 的连接。节点路径由 anland 配置(`MOBILEGL_GBM_NODE`):有可访问的 DRM 节点就借来当身份(只 open/fstat),没有就用占位节点。MobileGL 现有的假 EGL device 里写死的 `/dev/dri/renderD128` 默认值同样改成跟这个配置走。
## 归属:这事主要是 MobileGL 的
| 部分 | 在哪 | 内容 |
|---|---|---|
| 分配器 + 身份登记 | **MobileGL server**(Android 侧) | AHB 分配、fd 导出、inode 登记、会话生命周期/引用计数、隐式同步(server 看得见两端,就当"内核") |
| 后端导入 | **MobileGL Espryt / Magma** | AHB → EGLImage → 纹理/渲染缓冲;AHB → VkImage(每会话各自 VkDevice,各导一次) |
| 生产端 WSI | **MobileGL 客户端 EGL**(glibc) | `wl_egl_window` 改为 AHB 交换链 + `zwp_linux_dmabuf_v1`,没有该 global 时退回现在的 wl_shm |
| 消费端 EGL | **MobileGL 客户端 EGL** | `EGL_EXT_image_dma_buf_import(_modifiers)`、`eglQueryDmaBuf*`、真 `eglCreateImage`(现在是空壳)、`glEGLImageTargetTexture2DOES`/`RenderbufferStorageOES` |
| GBM 后端 | **MobileGL 新组件** `mobilegl_gbm.so`(glibc) | gbm ABI v1:`bo_create*` → server 分配,`bo_import`,`bo_map`,格式查询;外加 EGL 的真 GBM 平台(现在 GBM 窗口被拒) |
| 合成器 | anland | 后端打开薄节点建 `RenderDevice`;撤掉 `initWayland` 跳过 dma-buf 与 screencast 两处补丁;启动脚本设 `GBM_BACKEND`/`MOBILEGL_GBM_NODE` |
ANativeWindow 只剩 KWin 输出那一块,不变。
## 分阶段
**P0 去风险(小)**
- server 真实所在域 `untrusted_app` → 容器 `droidspacesd` 传 dma-buf fd 过 SELinux(ashmem 段已经能过,dma-buf 类别要实测;看 `avc: denied`)。
- 确认 libgbm 在 `GBM_BACKEND` 指定时不对 fd 发 ioctl(读 Mesa 26.3 `backend.c` 或拿 `/dev/null` 实测)。
- 确认 KWin 拿非 DRM 的设备号(占位节点)时 tranche/feedback 不出事。
- Magma 跨 VkDevice 同一 AHB 两次导入可用;Espryt 同一 AHB 两个 context 的 EGLImage。
**P1 server 分配器 + 登记表**
- `SharedImageRegistry`:`Allocate(w,h,format,usage) → {id, fds, stride, modifier}`;按 inode(或 name 标签)查找;归属会话 + 引用计数;会话死掉释放(键用单调 id,不用客户端句柄——P14 的跨会话句柄冲突教训)。
- 线协议新动词:分配 / 释放 / 按 fd 导入(fd 走现成的 `ITransport::ShareFd`)。
- 单元测试:注册、身份、跨会话释放只放自己的。
**P2 生产端:wl_egl_window 走 AHB 交换链**
- 先做最稳的一步:server 照旧渲染进 pbuffer/目标,`eglSwapBuffers` 时**GPU blit**(带 Y 翻转)到当前空闲 AHB——CPU 零拷贝,只多一次 GPU 拷贝。之后再让默认帧缓冲直接落在 AHB 上(Magma 已有默认 FB 变换机制;Espryt 用 FBO 冒充默认 FB)。
- 客户端:绑定 `zwp_linux_dmabuf_v1`,每块 AHB 建一次 `wl_buffer`(`create_immed`,`ABGR8888`/`XBGR8888` = GL RGBA8 内存序,不再换序);按 `wl_buffer.release` 轮转 3 块;resize 重建。没 global → 现路径。
- 同步第一版:swap 返回前 server CPU 等自己的渲染完成(现在 readback 本来就全等,不退步);server 记录每块的"最后消费者 fence",生产者重用前等它。
**P3 消费端:KWin 导入**
- 客户端 EGL 通告上述扩展;`eglCreateImage(EGL_LINUX_DMA_BUF_EXT)` 把 fd 发给 server → 认身份 → 后端导入;`glEGLImageTargetTexture2DOES` 绑到 KWin 的纹理。
- anland:后端建真 `DrmDevice`(薄节点 + `mobilegl_gbm.so`),撤补丁,linux-dmabuf 起来。这一步和 P2 合起来就是第一个可见成果:plasmashell/Qt/GTK/glmark2-wayland 零拷贝。
**P4 GBM 后端**
- `mobilegl_gbm.so` + EGL 的 GBM 平台(`eglGetPlatformDisplay(GBM)`、`eglCreateImage` 从 gbm bo)。KWin 的 `GbmGraphicsBufferAllocator` 随之可用 → screencast 走 dma-buf,screencast 补丁可撤。
- 格式:ARGB8888 等 AHB 没有的通道序,按 RGBA8 分配,server 记"声明格式",导入时用纹理 swizzle 对齐语义(两端都是 MobileGL,server 都看得见)。
**P5 同步与性能**
- 去掉 swap 时的 CPU 等待:sync_file 在 server 内做 GPU 等(Espryt `EGL_ANDROID_native_fence_sync` + `eglWaitSyncKHR`;Magma sync-fd 信号量跨会话设备)。
- 可选 `DMA_BUF_IOCTL_IMPORT_SYNC_FILE` 给外部 CPU 映射者隐式同步。
- `EGL_EXT_buffer_age` + 真实损伤区,Qt 局部重绘。
**P6 其它消费者(按需)**
- Xwayland glamor 跑在 MobileGL 的 GBM 平台上(现在强制 llvmpipe + `-shm`)。
- Chrome 原生 GBM 路径(它会 `drmGetDeviceFromDevId` 枚举节点——占位节点下可能不行,是风险项;现在的 wl_egl_window 路径在 P2 后已经零拷贝)。
## 风险
- SELinux:dma-buf fd 从 `untrusted_app` 进 `droidspacesd`(P0 第一项)。
- 方向:GL 默认帧缓冲自下而上、`wl_buffer` 自上而下。P2 的 blit 顺手翻转;直接渲染时可选用 `wl_surface.set_buffer_transform(FLIPPED_180)`(竖直翻转,KWin 着色器里免费),需实测 KWin 处理正确。
- 每会话一个 VkDevice(Magma):同一 AHB 在两个设备上,同步只能靠 sync_file 或 CPU 等。
- 生命周期:客户端崩溃时 KWin 仍持有它的 `wl_buffer`/EGLImage——AHB 引用要由导入者也持有,不能随生产会话一起释放。
- 资源:每窗口 3 块 AHB(2560×1412 约 44 MB),比现在的 pbuffer + 3 块 shm 少不了多少,但不再需要回复槽带宽。
-174
View File
@@ -1,174 +0,0 @@
# Plan: X11 apps on the GPU, zero-copy (Xwayland glamor + GLX DRI3/Present) (2026-10-03)
> Follows [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md) (the Wayland half: server-allocated AHB
> "shared images" exported as dma-bufs). Same hard rules: vendor-neutral (AHB, dma-buf, sync_file,
> the libgbm backend ABI, linux-dmabuf, DRI3/Present), no layout assumptions (modifier INVALID).
## Today
```
GLX app --glXSwapBuffers--> ReadPixels (server->client CPU) --xcb_put_image--> Xwayland (-shm, llvmpipe)
Xwayland --wl_shm--> KWin --glTexSubImage2D upload--> server
```
Two full-frame CPU trips per frame for every X11 GL app, and every ordinary X11 app is drawn by
pixman/llvmpipe and uploaded again by KWin.
## Target
```
GLX app --render--> pbuffer --GPU copy (PresentToSharedImage)--> [AHB image]
--DRI3 PixmapFromBuffer(s)(dma-buf, once per buffer)--> Xwayland pixmap (glamor: gbm_bo_import -> EGLImage)
--Present PresentPixmap--> Xwayland flips it (attaches the pixmap's dma-buf wl_buffer) or GPU-copies it
Xwayland (glamor on MobileGL EGL/GBM) --linux-dmabuf wl_buffer (window pixmaps are shared images)--> KWin
```
Every hop is a session of the same MobileGL server sampling or writing the same AHardwareBuffer.
CPU copies: zero.
## Stage 1 - Xwayland on MobileGL (glamor)
What Xwayland 24.1.13 (`hw/xwayland/xwayland-glamor-gbm.c`, `glamor/glamor.c`) needs, and where it is:
| Need | Status |
|---|---|
| render node from linux-dmabuf v4 feedback `main_device` (drmGetDeviceFromDevId) | KWin's thin node: must be a real DRM render node (`MOBILEGL_GBM_NODE`=/dev/dri/renderD128). `/dev/null` makes glamor fail -> shm fallback |
| `gbm_create_device(fd)` with `GBM_BACKEND=mobilegl` | `mobilegl_gbm.so` (exists) |
| EGL client ext `EGL_KHR/MESA_platform_gbm`, `eglGetPlatformDisplay(GBM, gbm)` | exists; the foreign-device decline keys on the backend name `mobilegl` |
| `eglCreateContext(EGL_NO_CONFIG_KHR, core 3.1)` + surfaceless `eglMakeCurrent` | cherry-picked from the surfaceless branch (6125f941), to be dropped on rebase |
| GL >= 2.1 desktop, `GL_ARB_vertex_array_object`, `GL_OES_EGL_image`; renderer not llvmpipe | exists on a split client |
| `EGL_EXT_image_dma_buf_import(_modifiers)` -> dmabuf_capable | exists (shared images) |
| `gbm_bo_create_with_modifiers2(XRGB8888/ARGB8888, {INVALID, LINEAR})`, `gbm_bo_get_fd_for_plane`, `_handle_for_plane`, `_stride_for_plane`, `_offset`, `_modifier`, `gbm_bo_import(FD / FD_MODIFIER)` | exists; now covered by a host test through the real libgbm loader |
| `eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT)` of its own bos, `glEGLImageTargetTexture2DOES` | exists (server identifies its image by inode) |
| KWin's tranche lists XRGB8888/ARGB8888 with modifier INVALID | KWin adds INVALID+LINEAR when the EGL reports no modifiers (ours) |
| **implicit sync**: glamor renders into window pixmaps and commits after `glFlush`, assuming the kernel orders KWin's read after it | **new**: see below |
| **Xwayland must never dial its own display** | **new**: see below |
### Implicit sync by flush (new shared_image op `Flush`)
The server already orders an image between sessions: a writer publishes a sync_file per write
(`PublishWrite`), a reader waits on it at its first use and publishes its frame's read fence at
its frame boundary (swap). glamor has neither a swap nor an explicit write: it renders into the
image through an FBO and calls `glFlush` before committing.
- Client: `glFlush`/`glFinish` in a process that has bound a shared image to a texture
(`glEGLImageTargetTexture2DOES`) and does not own the server window (not KWin) send the new
in-stream `shared_image Flush` op and wait for its reply. Policy is a pure function
(`SharedImageFlushPolicy`), overridable with `MOBILEGL_SHARED_IMAGE_FLUSH_SYNC=0/1`.
- Server: `BackendObject::PublishSharedImageAccesses()` - every image the session used since its
last boundary gets the flush's fence as a write (generation moves) and as a read.
Espryt: `ExportFence` (EGL_ANDROID_native_fence_sync), Magma: release held images, submit with
the exportable semaphore, export it. A session that has flushed this way also waits (GPU) for
the pending read fences of an image before its first use in a frame (write-after-read).
- The reply comes after the fence is published, so KWin (which samples after Xwayland's commit,
which follows the reply) always finds the new generation.
### X-connection safety (`MG_Util/X11/DisplayGuard`)
One rule, one function: `MayDialX11Display(name)` is false when this process **serves** that
display: it holds a listening unix socket named `/tmp/.X11-unix/X<n>` (path or abstract) per
`/proc/net/unix` + `/proc/self/fd`, or its executable is an X server (Xwayland, Xorg, Xvfb, ...).
`MOBILEGL_X11_DIAL=0` turns all dialing off. Every place MobileGL opens an X connection asks it
first: EGLState's default-visual query (eglGetDisplay of a non-X native display has platform NONE,
so the old "only on X displays" test still dialed for `eglGetDisplay(gbm_device)`), DirectGLES's
visual query and window-surface display, Magma's Xlib surface. GLX never opens a connection (it
uses the application's). KWin's launcher also drops `DISPLAY` from Xwayland's environment.
### KWin (anland `kwin.patch`, `mobilegl-startup.sh`)
`ANLAND_MOBILEGL=1` and `ANLAND_XWAYLAND_GLAMOR != 0`: Xwayland starts **without** `-shm`, with
`MOBILEGL_IPC_SURFACE=offscreen`, its own `MOBILEGL_LOG_FILE_PATH=/tmp/mgl-xwayland`, `GBM_BACKEND`
kept, no `LIBGL_ALWAYS_SOFTWARE`/`GALLIUM_DRIVER`, no `DISPLAY`. Otherwise the old `-shm` +
software environment. If glamor cannot initialise, Xwayland itself falls back to shm rendering.
## Stage 2 - GLX presentation through DRI3 + Present
Per GLX window drawable (`GLXImpl/X11Present.{h,cpp}`, logic behind two small interfaces so the
host tests drive it with fakes):
- **Path selection** (`SelectPresentPath`): DRI3 >= 1.0 and Present >= 1.0 on the connection,
shared images available, depth 24/32, `MOBILEGL_GLX_PRESENT` not `readback` -> `Dri3Present`;
else MIT-SHM >= 1.2 with fd passing -> `ShmPutImage`; else `PutImage` (today's bands).
- **Buffers**: up to 3 (4 when Xwayland holds all three past a 1 s wait) shared images of the
window's format (depth 24 -> XRGB8888, 32 -> ARGB8888: exactly what glamor imports them as), each
imported once as a pixmap (`PixmapFromBuffers` with modifier INVALID on DRI3 >= 1.2, else
`PixmapFromBuffer`, checked). A buffer is busy from its PresentPixmap until its `IdleNotify`.
- **Present**: `PresentToSharedImage` (GPU copy, rows top first, returns once submitted with the
write fence published), then `PresentPixmap` with an XFixes update region from the swap's
damage (flipped to X's top-left origin; none = whole), serial per frame. Events arrive on a
special-event queue (`xcb_register_for_special_xge`), never in the application's queue.
- **Pacing**: `FrameThrottle` (the Wayland rules) on `CompleteNotify`: interval >= 1 waits for the
previous frame's completion, `target_msc = last_msc + interval`; interval 0 uses
`PresentOptionAsync`, unpaced while completions come, ~1 fps once one is 200 ms overdue.
- **Errors**: a refused `PixmapFromBuffer`, a `PresentPixmap` error (checked lazily, no round
trip), or the window's destruction (`ConfigureNotify` with `PresentWindowDestroyed`) latch the
drawable onto the readback path for good, logged once.
- **Resize**: `ConfigureNotify` (and the existing throttled `XGetGeometry`) resize the pbuffer;
buffers of the old size are dropped as they come back idle.
- **Fallback** (no DRI3/Present: Xwayland `-shm`, a remote X server): readback into a MIT-SHM
segment (memfd, `xcb_shm_attach_fd`) + `xcb_shm_put_image`; without MIT-SHM, today's bands.
- `GLX_EXT_buffer_age` (`GLX_BACK_BUFFER_AGE_EXT`) answers the EGL buffer age of the drawable.
## Tests (host, WSL)
`X11DisplayGuardTest` (display names, /proc parsing, the Xwayland case), `X11PresentTest` (path
selection, rotation, idle/complete/configure handling, pacing, resize, error latching, fake xcb
and fake images), `SharedImageSync` additions (accesses publish as write + read),
`SharedImageFlushPolicyTest`, `GbmGlamorTest` (the real libgbm loader with `GBM_BACKEND=mobilegl`
over a fake shared-image library: glamor's create/export/import calls).
## Device verification (phase B)
`scripts/x11/` in the skill: deploy, check Xwayland's glamor log lines and its dma-buf wl_buffers in
KWin (`WAYLAND_DEBUG`-free: `/proc/<kwin>/fdinfo` dma-buf count and the server log's import lines),
glxgears/glxinfo/an X11 GL app before/after (fps, `top` CPU of app/Xwayland/KWin/server), and
ordinary X apps (xterm, xeyes) for glamor rendering.
## Risks
- `drmGetDeviceFromDevId` on the borrowed render node inside the container (sysfs visibility).
- Adreno: cross-context ordering relies on the Flush op's fence; a missed flush shows as tearing,
not corruption of other windows.
- The extra round trip per Xwayland `glFlush` (glamor flushes once per main-loop wakeup).
- DRI3 clients that are not MobileGL (Mesa Vulkan WSI) hand Xwayland dma-bufs the server never
allocated: `PixmapFromBuffers` fails with BadAlloc for them, as on any driver mismatch.
## Device results (2026-10-03, TB321FU / Adreno 750, x11-gpu 95aa618d server + client)
glxgears 1280x720, % of one core, `x11-verify.sh measure`; "before" = the old stack (`-shm`
llvmpipe Xwayland, GLX readback + PutImage).
> glxgears is fixed-function GL (glBegin/glEnd display lists, glLight), which MobileGL does not
> implement (Core Profile only), so its window stays black. These numbers measure presentation
> throughput of empty frames through each path, not rendering. Check X11 rendering with a
> core-profile or shader-based GLX client, and verify it from pixels (mgl-xgrab / XGetImage).
| run | fps | phone | glxgears | Xwayland | kwin | server | GPU |
|---|---|---|---|---|---|---|---|
| before, unpaced | 160 | 229% | 28% | 42% | 71% | 37% | 15% |
| Espryt DRI3+Present, interval 1 | 98-101 | 210% | 12% | 5% | 28% | 32% | 12% |
| Espryt DRI3+Present, vblank_mode=0 | 1440-1780 | 296% | 29% | 29% | 36% | 62% | 72% |
| Espryt readback + MIT-SHM on glamor | 46 | 168-188% | 5% | 78% | 11% | 35% | 5% |
| Magma DRI3+Present, interval 1 | 105-112 | 210% | 12% | 5% | 24% | 43% | 16% |
| Magma DRI3+Present, vblank_mode=0 | 940-970 | 282% | 24% | 18% | 33% | 60% | 77% |
| Magma readback + MIT-SHM on glamor | 27 | 161% | 2% | 50% | 7% | 70% | 5% |
- Glamor engages on both backends: Xwayland runs without `-shm`, offers DRI3 1.2 + Present 1.2,
its windows are linux-dmabuf shared images; GLX windows log `presented through DRI3+Present`.
- Found on the device and fixed: uploads into a texture bound to a shared image were dropped
(Espryt) - an X server's PutImage into a window pixmap showed black windows. They are now
drained at the producer's flush and written into the image (RGB-converted for X formats, which
the driver holds as RGB8).
- The forced readback path is slower on a glamor Xwayland than on the old llvmpipe one (glamor
uploads every frame through the server); it is only the fallback now.
- Fixed (cd58f048): after a hide/show or lock/unlock round, X11 Qt windows kept stale or black
regions in their window pixmap. Root cause: every glamor copy and fill drew nothing. glamor's
fragment shaders name gl_FragColor only in the inactive arm of a version guard and write their
own `frag_color`; the legacy lowering still declared `mg_FragColor` ahead of it, and the IO
resolver gave that dead output location 0 and `frag_color` location 1, which no draw buffer
reads. Uploads (PutImage) still landed, so windows looked right until Xwayland copied between
window buffers (it swaps the window pixmap on every commit and reallocates buffers after a
hide), and then showed old content or zeros. Inactive fragment outputs now take the top of the
draw-buffer range. Host test: ProgramTest.InactiveLoweredFragColorDoesNotDisplaceTheActiveOutput.
On the device, an X client checking its own XGetImage against a CPU model (PutImage, CopyArea
and FillRectangle; MIT-SHM and plain; depth 24 and 32) sees 0 bad pixels through hide/show/lock
on both backends. One run in about 20 on Magma (no SHM, depth 24) left a 2751-pixel rect
mismatched; it did not come back in the repeats.
-270
View File
@@ -1,270 +0,0 @@
# Anland × MobileGL: buffer, synchronization, process and integration study
Research date: 2026-09-30. This is a source audit, not a device validation. All audited checkouts were clean; exact revisions are in the table below. Repository citations link to those revisions. Citations of third-party material that was snapshotted during the audit (Xwayland, drm_fourcc.h, Mesa/Termux build files, libwayland, the AHB header, Anland issues/PRs) link to the upstream source at the snapshotted revision; `refs/heads/main` links (the Android header) may have drifted since 2026-09-30. Statements marked **inferred** are architectural conclusions or proposed changes, not implemented behavior.
| Tree | Audited revision | Scope |
|---|---|---|
| `SuperTurtleDev_anland` | `cc1691805fe6aca6851b6fcccb6a1e33019026c1` | Current 6.x main |
| `SuperTurtleDev_anland-shell` | `920fcfc8159690f5232e2d9c7c3294d4c1481145` | Droidspaces launcher |
| `lfdevs_anland-termux` | `a70879f059a2e8439fdb132195c3dd10d4810106` | Legacy-derived Termux integration |
| `E7G_wlroots-anland` | `351399132de0b208ddb36d0ea2fa74cbe668fb72` | Private-protocol wlroots backend |
| `asveroid_anland-plasma-termux` | `099710d413b47d2f3f8c5091a95ee2732d3be0e6` | Termux Plasma installer |
| `Yizhou147_anland-drm` | `850f7ca0e0f25d34dd04baa7e12e8751bcff4951` | Experimental DRM consumer |
| `SuperTurtleDev_anland_legacy` | `9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46` | 5.x comparison |
Main explicitly identifies itself as the 6.x rewrite and explains that 5.x used a private display protocol requiring compositor-specific backends. Do not apply Termux/5.x results automatically to waylandbridge 6.x. [anland/README.md:9–25](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/README.md#L9-L25>) [Termux/README.md:7–13](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/README.md#L7-L13>)
## Decision in brief
**Inferred:** MobileGL can supply rendering to Anland, but successful GL command execution is only half the integration. Current 6.x needs either a `wl_shm` buffer or its narrow raw-dma-buf representation. It does **not** currently expose a native AHardwareBuffer producer import API. The best path for an Android render server is an added native-AHB buffer ingress that preserves the complete gralloc handle, with Wayland buffer lifetime and sync-file fences carried alongside it. The existing Android rendering side already imports AHBs as EGLImages and submits AHBs to SurfaceControl. [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [waylandbridge/awl_internal.h:65–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L65-L81>) [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) [waylandbridge/awl_sc.cpp:727–743](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L743>)
The highest-impact findings are:
- dma-buf is **v3, one plane, AR24/XR24**, with no device feedback. Native RGBA8888/DRM ABGR8888 is outside that format list. [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_dmabuf.c:105–127](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L105-L127>) [waylandbridge/awl_dmabuf.c:212–237](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L212-L237>) [drm-uapi/drm_fourcc.h:193–199](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L193-L199)
- The actual local `DRM_FORMAT_MOD_INVALID` constant is **wrong relative to DRM UAPI**: `0x00ffffff00000000ULL`, versus UAPI `0x00ffffffffffffff`. Linear `0` is unambiguous. [waylandbridge/awl_internal.h:35–40](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L35-L40>) [drm-uapi/drm_fourcc.h:467–492](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L467-L492) [drm-uapi/drm_fourcc.h:527–544](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L527-L544)
- Both dma-buf rendering modes depend on a **Qualcomm snapalloc-style donor forge**. A GPU-copy fallback for pitch or pacing still needs the forge to succeed; it is not a generic fallback for unknown gralloc layouts. [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [waylandbridge/awl_ahb.cpp:325–340](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L325-L340>) [waylandbridge/awl_sc.cpp:903–935](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L903-L935>)
- The “statically-linked ELF” description does not mean a fully static graphics stack: the target links Android `EGL`, `GLESv3`, `nativewindow`, `android`, and `binder_ndk`. [anland/README.md:73–78](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/README.md#L73-L78>) [waylandbridge/CMakeLists.txt:303–308](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L303-L308>) [waylandbridge/CMakeLists.txt:364–365](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L364-L365>)
## 1. Buffer ingress contract and the complete dma-buf path
### 1.1 What the Wayland endpoint advertises and accepts
| Item | Exact current behavior |
|---|---|
| Core compositor | `wl_compositor` **v4**. [waylandbridge/awl_surface.c:1110–1120](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L1110-L1120>) |
| Shared memory | The pinned libwayland's `wl_display_init_shm()` creates `wl_shm` **v2** and supplies ARGB8888 and XRGB8888. Anland adds no extra formats in server initialization. In `wl_shm` these are **0 and 1**, not DRM fourcc values. [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [libwayland/wayland-shm.c:451–475](https://github.com/SuperTurtleDev/libwayland/blob/37032ecd5bf7657629b9d9725c084025497840d9/src/wayland-shm.c#L451-L475) [waylandbridge/awl_surface.c:1155–1160](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L1155-L1160>) |
| dma-buf global | `zwp_linux_dmabuf_v1` **v3**, with client binds capped at 3. Only `destroy` and `create_params` are implemented on the factory. [waylandbridge/awl_dmabuf.c:212–237](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L212-L237>) |
| dma-buf formats | `AWL_FORMAT_ARGB8888 = fourcc('A','R','2','4')` and `AWL_FORMAT_XRGB8888 = fourcc('X','R','2','4')`; little-endian memory is `[B,G,R,A]` or `[B,G,R,X]`. No AB24/XB24, RGB565, YUV or multi-plane video format. [waylandbridge/awl_internal.h:29–33](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L29-L33>) [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_dmabuf.c:54–57](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L54-L57>) |
| Advertised modifiers | Each format has entries named `DRM_FORMAT_MOD_INVALID` and `DRM_FORMAT_MOD_LINEAR`. The former is locally defined as **`0x00ffffff00000000ULL`**, the latter as **`0ULL`**. Bind sends `format` for every entry, so the two formats are each announced twice; modifier events are conditional on protocol version. [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_dmabuf.c:217–229](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L217-L229>) [waylandbridge/awl_internal.h:35–40](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L35-L40>) |
| Planes and offset | Only `plane_idx == 0`; other planes close the supplied fd and post `ZWP_LINUX_BUFFER_PARAMS_V1_ERROR_PLANE_IDX`. Repeated plane-0 `add` overwrites the previous fd. **Offset is ignored**, not validated or retained. [waylandbridge/awl_dmabuf.c:105–127](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L105-L127>) |
| Feedback / device | No `get_default_feedback`, `get_surface_feedback`, feedback object, format-table/tranche events, or `main_device`/`dev_t` advertisement in this v3 implementation. This is absence of a device advertisement, not a zero-valued or fabricated `dev_t`. The Xwayland patch explicitly compensates for it. [waylandbridge/awl_dmabuf.c:212–237](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L212-L237>) [anland/patches/xwayland/README.anland.md:29–39](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/README.anland.md#L29-L39>) |
**Modifier mismatch:** DRM UAPI defines INVALID as `fourcc_mod_code(NONE, DRM_FORMAT_RESERVED)`, with `NONE=0` and `DRM_FORMAT_RESERVED=((1ULL<<56)-1)`. Thus the advertised nonzero value in Anland is not standard INVALID. **Inferred:** a client that compares exact modifier values can reject/misinterpret it. However, Anland's creation path itself does not reject an unadvertised modifier: it only passes the value into logging, then discards it from the persistent buffer wrapper. Therefore “only linear supported” is the intended usable layout, **not a correctly enforced request validator**. [drm-uapi/drm_fourcc.h:467–492](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L467-L492) [drm-uapi/drm_fourcc.h:527–544](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L527-L544) [waylandbridge/awl_internal.h:35–40](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L35-L40>) [waylandbridge/awl_dmabuf.c:49–85](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L49-L85>)
### 1.2 From `create` / `create_immed` to a displayed frame
1. **Protocol creation:** `params_add` records one fd, byte stride and modifier; `create`/`create_immed` require an fd and positive width/height. `dmabuf_buffer_create` rejects formats outside AR24/XR24, takes the fd, records its inode with `fstat`, and constructs a `wl_buffer`. There is no EGL/gralloc import at this stage. `create` sends `created` or `failed`; `create_immed` creates the requested object or posts `INVALID_WL_BUFFER`. Both ignore the `flags` argument. [waylandbridge/awl_dmabuf.c:49–85](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L49-L85>) [waylandbridge/awl_dmabuf.c:105–175](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L105-L175>)
2. **Commit:** `awl_surface_apply_buffer` duplicates the dma-buf fd, takes the explicit acquire fence or snapshots implicit write completion with `awl_dmabuf_export_sync_file`, copies geometry/format into `struct awl_bq_buffer`, and references the `awl_buffer` through an `awl_bufref` release cookie. A full queue returns/drops the new frame immediately. Destroying the protocol buffer does not invalidate already queued frames, which hold their own fd and wrapper references. [waylandbridge/awl_surface.c:184–218](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L184-L218>) [waylandbridge/awl_dmabuf.c:33–46](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L33-L46>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>)
3. **Queue:** mailbox-like draining drops superseded heads only while the next element is ready, assuming FIFO completion. The retained head and consumer references determine lifetime; the last reference runs the release callback and closes its owned fds. Capacity is **8** elements, including the head. [waylandbridge/awl_bufferqueue.c:43–48](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L43-L48>) [waylandbridge/awl_bufferqueue.c:201–210](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L201-L210>) [waylandbridge/awl_bufferqueue.c:379–388](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L379-L388>)
4. **AHB resolution:** each layer has an eight-slot cache. Its key is **`(ino,width,height,stride)`**, not modifier or format. A miss calls `awl_ahb_wrap`; a hit reuses the registered AHB and, for GL, its EGLImage/texture payload. A failed forge leaves the cache unchanged for a later retry. [waylandbridge/awl_ahb.hpp:53–69](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.hpp#L53-L69>) [waylandbridge/awl_ahb.cpp:534–569](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L534-L569>)
5. **EGL renderer:** ready dma-buf → forged AHB → `eglGetNativeClientBufferANDROID` → `eglCreateImageKHR(..., EGL_NATIVE_BUFFER_ANDROID, ...)` → `glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, ...)` → per-window GPU composition → `eglSwapBuffers`. A missing texture skips that layer; no direct `EGL_LINUX_DMA_BUF_EXT` fallback is attempted here. [waylandbridge/awl_renderer.cpp:292–323](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L292-L323>) [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) [waylandbridge/awl_renderer.cpp:382–419](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L382-L419>)
6. **SurfaceControl renderer:** `enum sc_mode { SC_MODE_NONE=0, SC_MODE_SCANOUT, SC_MODE_EGL, SC_MODE_EGL_SHM }`. `SCANOUT` submits the forged client AHB through `ASurfaceTransaction_setBuffer[WithRelease]`. `EGL` samples that forged AHB into a platform-allocated AHB for this layer. `EGL_SHM` uploads shared-memory pixels into a texture and blits into such a target. Every Wayland layer becomes a SurfaceControl sibling. **Inferred:** the name `SCANOUT` describes Anland's zero-copy submission; actual HWC-plane assignment remains SurfaceFlinger's decision. [waylandbridge/awl_sc.cpp:1–30](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L1-L30>) [waylandbridge/awl_sc.cpp:358–364](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L358-L364>) [waylandbridge/awl_sc.cpp:727–743](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L743>) [waylandbridge/awl_sc.cpp:903–975](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L903-L975>)
### 1.3 What the forge actually does
The source describes a specific Qualcomm **snapalloc** layout: native handle **fd[0] = pixel dma-buf, fd[1] = metadata blob**, observed as about **28 KiB**. It explains that a container KGSL dma-buf lacks that descriptor and a bare `GB01+fd` import was rejected. `donor_blob_fd` explicitly refuses a donor unless `numFds == 2`. This is not a vendor-neutral construction of an AHB from any dma-buf. [waylandbridge/awl_ahb.cpp:5–28](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L5-L28>) [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>)
Calibration allocates BGRA donors of **300×300** and **1134×567**, both with `AWL_AHB_DONOR_USAGE = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE`. It reads allocator-reported stride and pixel-allocation size, mmaps their metadata blobs, and locates geometry/size fields by comparing the two layouts. Blob sizes must agree and be positive; native-handle integer counts must agree and be at most **64**. It requires recognized stride-in-pixels, stride-in-bytes, height, allocation/exact size, extent and native-handle integer fields; failure reports **“vendor layout changed, refusing to forge.”** [waylandbridge/awl_ahb.hpp:19–23](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.hpp#L19-L23>) [waylandbridge/awl_ahb.cpp:172–220](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L172-L220>) [waylandbridge/awl_ahb.cpp:223–274](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L223-L274>)
`awl_ahb_wrap` serializes calibration/import with a mutex and hardcodes **`AWL_HAL_BGRA_8888 = 5`**. It reuses an existing matching-geometry AHB as donor, without modifying its live blob, or allocates a **4×4** donor. It patches the cold donor's blob and integer payload to client geometry, **`stride/4`** and **`stride*height`**. Both calibrated allocation-size and exact-size metadata fields are set to that product; this path does not query or validate the actual client dma-buf extent. [waylandbridge/awl_geom.h:42–51](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_geom.h#L42-L51>) [waylandbridge/awl_ahb.cpp:300–320](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L300-L320>) [waylandbridge/awl_ahb.cpp:325–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L325-L395>)
It then fabricates the Android `GraphicBuffer::flatten` representation: **13 int32 header words**, magic **`0x47423031` (`GB01`)**, width/height/stride/HAL format, layer count **1**, usage words, synthetic buffer id with **bit 63 set**, generation **0**, **two fds**, and the donor's integer count. A local `AF_UNIX/SOCK_STREAM` socketpair carries the flattened bytes plus `SCM_RIGHTS` containing **`dup(client_pixel_fd)` and `dup(donor_blob_fd)`**. `AHardwareBuffer_recvHandleFromUnixSocket` reconstructs/registers the AHB through Android/gralloc. That socketpair is entirely inside the daemon; it is **not** a client-facing native-AHB transport. [waylandbridge/awl_ahb.cpp:398–480](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L398-L480>)
### 1.4 Usable layout preconditions versus checks that are actually implemented
| Condition | What is checked / practical implication |
|---|---|
| Format | Protocol admits only AR24/XR24; forge always describes HAL BGRA8888. RGBA8888 is DRM **AB24**, so simply relabeling it AR24 swaps red/blue. [waylandbridge/awl_dmabuf.c:54–57](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L54-L57>) [waylandbridge/awl_ahb.cpp:334–336](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L336>) [drm-uapi/drm_fourcc.h:193–199](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L193-L199) |
| Linear, zero-offset data | Offset is ignored and modifier is not kept in `awl_buffer` or the queue. **Inferred:** correct pixels require the simple packed layout described by the forge; arbitrary tiling/compression cannot be represented by supplying an fd alone. [waylandbridge/awl_dmabuf.c:105–127](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L105-L127>) [waylandbridge/awl_internal.h:71–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L71-L81>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>) |
| Dimensions / byte extent | Only `width>0,height>0` are validated at request time. There is no request-time `stride>=width*4`, overflow, actual-fd-size or maximum-texture-size check in this constructor. **Inferred:** the client must provide sufficiently large storage with sane geometry; protocol creation success does not establish import or sampling safety. [waylandbridge/awl_dmabuf.c:49–85](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L49-L85>) [waylandbridge/awl_dmabuf.c:130–175](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L130-L175>) [waylandbridge/awl_ahb.cpp:377–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L377-L395>) |
| Byte stride | Forge converts with integer `stride/4`. Scanout rejects a stride not divisible by 4; that is not a universal ingress rejection. **Inferred:** valid packed-32-bit buffers should use a stride divisible by 4 and at least width×4. [waylandbridge/awl_ahb.cpp:87–91](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L87-L91>) [waylandbridge/awl_ahb.cpp:377–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L377-L395>) |
| HWC-compatible stride | Let `p = stride/4`. `awl_ahb_hwc_scanout_ok` probes allocator stride for **width p**, height **64**, BGRA and donor usage. It accepts if the returned pitch equals p, or if the probe returns **0** (no verdict, keep HWC). It does not merely compare allocator stride at the buffer's logical width. [waylandbridge/awl_ahb.cpp:60–91](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L60-L91>) |
| Non-fixed-point pitch | Forces a GPU blit into a platform allocation. The source documents pitch **1024 px → gralloc 1280 px** causing DRM `ADDFB2` failure/blank HWC output while GPU composition/screenshots looked correct. The forge must still succeed for that source texture. [waylandbridge/awl_sc.cpp:22–30](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L22-L30>) [waylandbridge/awl_sc.cpp:903–935](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L903-L935>) |
| Unsupported donor layout | Wrong fd count, unrecognized metadata, mmap/allocation/import failure → `NULL`, log, blank/skipped or old retained layer. There is no generic CPU upload fallback in the audited main. [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [waylandbridge/awl_ahb.cpp:264–274](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L264-L274>) [waylandbridge/awl_ahb.cpp:337–340](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L337-L340>) [waylandbridge/awl_renderer.cpp:295–323](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L295-L323>) [waylandbridge/awl_sc.cpp:729–735](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L729-L735>) |
**Already-gralloc-allocated input:** extracting only its pixel fd and submitting it through linux-dmabuf still enters this forge; the original gralloc metadata and complete native handle never reach the renderer. There is no detection of “this came from gralloc, use its original handle.” **Inferred:** a genuinely linear BGRA allocation with compatible pitch might happen to work, but native allocation provenance alone is no guarantee. A compressed native allocation is especially unsuitable for this reconstruction. [waylandbridge/awl_internal.h:71–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L71-L81>) [waylandbridge/awl_ahb.cpp:422–464](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L422-L464>) [waylandbridge/awl_ahb.cpp:562–569](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L562-L569>)
**UBWC/compression:** neither supported modifier list names a Qualcomm compressed modifier; no modifier or compression metadata survives in the queue. The donor path assumes the sampled BGRA layout, but contains no general UBWC detection, decompressor, or universal vendor “disable compression” operation. **Inferred:** compression is excluded from the usable raw-dma-buf contract, although the request handler fails to enforce that exclusion. `DRM_FORMAT_MOD_INVALID` must not be treated as permission to send an opaque compressed AHB pixel fd. A later GPU copy does not solve this: first it must import/sample the source correctly. [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_dmabuf.c:49–85](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L49-L85>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>) [waylandbridge/awl_ahb.cpp:334–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L395>) [waylandbridge/awl_sc.cpp:922–930](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L922-L930>)
**Native-handle ingress today:** none in the audited buffer factory/global list or Binder transaction table. Binder `T_CONNECT=18` transfers a Wayland **connection socket**; `T_SURFACE=1` transfers an Android **destination Surface**. Neither submits a producer AHardwareBuffer. The sole `recvHandleFromUnixSocket` in the frame-import path consumes the daemon's fabricated local wire message. [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [waylandbridge/waylandbridge.cpp:140–197](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L140-L197>) [libawl/AwlClient.java:80–95](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L80-L95>) [libawl/AwlClient.java:335–381](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L335-L381>) [waylandbridge/awl_ahb.cpp:444–464](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L444-L464>)
**External evidence, not merged behavior:** draft PR [#92](https://github.com/SuperTurtleDev/anland/pull/92), open and unmerged when retrieved, reports black Nautilus/Kitty/Chromium content on Snapdragon 870/Adreno 650 with `sc_enabled=0`; its sample is AR24 **2024×1344, stride 8192, modifier 0**, with nonzero CPU-mappable pixels but forge failure. The proposal adds synchronized mmap/CPU upload. It is not present in the audited main and is explicitly limited to linear packed data. [anland PR #92](https://github.com/SuperTurtleDev/anland/pull/92) [anland PR #92](https://github.com/SuperTurtleDev/anland/pull/92) [anland PR #92](https://github.com/SuperTurtleDev/anland/pull/92)
Android service access can also fail more severely than a returned import error: closed issue [#84](https://github.com/SuperTurtleDev/anland/issues/84) reports a 6.1.2 process abort inside `AHardwareBuffer_allocate`, with **“Failed to load matching allocator for mapper version 4”** and allocator/mapper service-discovery denials. Current policy contains the corresponding mapper/allocator `find` grants. This is historical device evidence, not a claim that the same defect remains unfixed in the audited revision. [anland issue #84](https://github.com/SuperTurtleDev/anland/issues/84) [anland/module/sepolicy.rule:159–161](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L159-L161>)
## 2. Synchronization and buffer release
### 2.1 Protocol and producer completion
The implemented protocol is **`zwp_linux_explicit_synchronization_v1` v2**, using per-commit Linux **sync_file fds**. `set_acquire_fence` validates with `SYNC_IOC_FILE_INFO`, rejects invalid fds and duplicate pending fences, and stores `pend_acquire_fd`. `get_release` creates a `zwp_linux_buffer_release_v1` object. Commit validation rejects acquire/release without an attached buffer; an acquire fence on `wl_shm` is `UNSUPPORTED_BUFFER`, although requesting a release object alone for shm is permitted. Sync-subsurface latching moves fence/release state with the buffer. [waylandbridge/awl_esync.c:1–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L1-L26>) [waylandbridge/awl_esync.c:121–170](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L121-L170>) [waylandbridge/awl_esync.c:179–200](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L179-L200>) [waylandbridge/awl_esync.c:265–277](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L265-L277>) [waylandbridge/awl_bufferqueue.c:175–179](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L175-L179>)
No **linux-drm-syncobj** global is registered or implemented by waylandbridge. The XML comment saying the older explicit-sync protocol is superseded is not an implementation of its successor. In particular, the pinned Xwayland source has its own syncobj-related code; that does not mean the Anland compositor advertises it. [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [waylandbridge/CMakeLists.txt:309–349](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L309-L349>) [waylandbridge/protocols/linux-explicit-synchronization-unstable-v1.xml:39](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/protocols/linux-explicit-synchronization-unstable-v1.xml#L39>) [xserver/hw/xwayland/xwayland-glamor-gbm.c:1788–1797](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1788-1797)
| Consumer path | Acquire fence handling |
|---|---|
| Whole-window EGL renderer | Calls `awl_bufferqueue_gethead(q,100)`. This is a CPU `poll`; **100 ms is only a warning threshold**. On timeout it waits again with **`-1`**, i.e. indefinitely, before sampling. A real wait error logs and can return the head anyway. Header descriptions calling the wait “bounded” are incomplete compared with this body. [waylandbridge/awl_renderer.cpp:276–279](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L276-L279>) [waylandbridge/awl_bufferqueue.c:121–137](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L121-L137>) [waylandbridge/awl_bufferqueue.c:395–417](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L395-L417>) |
| SC scanout / SC source selection | `tryhead` never waits; an unsignaled frame retains the previous buffer/geometry. The render-thread looper watches completion and triggers another pass. When scanout submits the buffer, its original acquire fence is already signaled, so `setBuffer` receives **`-1`**, not the client fence. [waylandbridge/awl_bufferqueue.c:420–425](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L420-L425>) [waylandbridge/awl_sc.cpp:472–501](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L472-L501>) [waylandbridge/awl_sc.cpp:727–740](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L740>) [waylandbridge/awl_sc.cpp:847–860](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L847-L860>) |
| SC GPU-copy destination | The daemon exports the blit's native fence and passes that fence to SF as the destination AHB's acquire fence. Reusing a target waits on SF's prior release fence via `eglWaitSyncKHR` where available; fallback is CPU wait. These are the daemon-owned target's fences, distinct from the client-source fence. [waylandbridge/awl_sc.cpp:752–796](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L752-L796>) [waylandbridge/awl_gl.cpp:399–411](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L399-L411>) |
Poll treats error/hangup/invalid-fd events as completion to avoid permanent stalls; `SYNC_IOC_FILE_INFO` validates the explicit fence type but does not make these error paths successful rendering. **Inferred:** MobileGL should send valid, signaled-on-completion sync_files and propagate rendering failure separately rather than rely on error-event behavior. [waylandbridge/awl_bufferqueue.c:111–137](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L111-L137>) [waylandbridge/awl_bufferqueue.c:175–192](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L175-L192>)
### 2.2 Release fences and `wl_buffer.release`
The common rule is **last queue/consumer reference → `bq_release_cb`**. If the commit has an explicit release object, it gets `fenced_release(release_fd)` or `immediate_release` when no fence exists. Otherwise the daemon tries to import its read-completion fence into the dma-buf reservation with `DMA_BUF_IOCTL_IMPORT_SYNC_FILE`, `DMA_BUF_SYNC_READ`. A live `wl_buffer` then gets **`wl_buffer.release` in both cases**, even with explicit sync. `release` therefore need not mean the fence has already signaled. [waylandbridge/awl_bufferqueue.c:201–210](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L201-L210>) [waylandbridge/awl_surface.c:84–108](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L84-L108>) [waylandbridge/awl_esync.c:66–78](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L66-L78>)
| Mode | When the consumer reference is returned |
|---|---|
| EGL whole-window | After draw submission, with a native GPU-read fence; `glFinish()` if native-fence creation fails. `put` happens **before** the vsync-blocking `eglSwapBuffers`. The ring may still retain its current head, so this is not an unconditional immediate protocol release of the newest buffer. [waylandbridge/awl_renderer.cpp:382–405](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L382-L405>) [waylandbridge/awl_bufferqueue.c:379–388](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L379-L388>) |
| SC `SCANOUT` | Held while SF uses it. API **36+**, if the symbol is available: `setBufferWithRelease` → per-buffer callback → `put(element,release_fence)`. Older **29–35** strategy: transaction `OnComplete` reports the **previous** buffer's release fence after replacement. [waylandbridge/awl_sc.cpp:86–97](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L86-L97>) [waylandbridge/awl_sc.cpp:329–353](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L329-L353>) |
| SC `EGL` | Client source is put with the blit's fence after copying; SF keeps a reference to Anland's separate pool slot instead. Again the ring head may defer actual protocol release. [waylandbridge/awl_sc.cpp:94–97](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L94-L97>) [waylandbridge/awl_sc.cpp:922–935](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L922-L935>) |
| shm, either renderer | Logic stores the latest shm source; backend uploads the damaged pixels. `awl_surface_shm_end(consumed=1)` sends source `wl_buffer.release` and immediate explicit release after the CPU source has been read. Failed uploads leave the source pending. [anland/include/awl.h:325–342](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L325-L342>) [waylandbridge/awl_surface.c:1201–1209](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L1201-L1209>) |
Without explicit sync, commit snapshots existing writer fences using `DMA_BUF_IOCTL_EXPORT_SYNC_FILE` with `DMA_BUF_SYNC_READ`; `ENOTTY` or `EINVAL` disables future export attempts and readiness falls back to polling the dma-buf itself for `POLLIN`. This assumes the producer driver records completion in the dma-buf reservation. The release-side import return value is ignored by `bq_release_cb`, so there is no implemented CPU-wait fallback if that ioctl is unsupported. **Inferred:** a split MobileGL producer should prefer explicit acquire **and** release synchronization; CPU-complete submission is a slower safe producer fallback, but reuse must still honor the consumer's release fence. [waylandbridge/awl_surface.c:199–209](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L199-L209>) [waylandbridge/awl_bufferqueue.c:140–159](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L140-L159>) [waylandbridge/awl_bufferqueue.c:184–192](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L184-L192>) [waylandbridge/awl_surface.c:90–102](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L90-L102>)
## 3. Presentation, pacing, buffering, scaling and subsurfaces
`wl_surface.frame` creates callbacks on the surface's list. `awl_surface_presented()` sends each outstanding callback `done(awl_now_ms())`; `awl_now_ms` derives from **CLOCK_MONOTONIC** and wraps into a 32-bit millisecond value. It does not deliver a hardware presentation timestamp. No `wp_presentation` global or presentation-time implementation appears in server setup/build registration. **Inferred:** frame callbacks are pacing opportunities here, not evidence that a particular frame physically scanned out. [waylandbridge/awl_surface.c:825–859](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L825-L859>) [waylandbridge/awl_surface.c:1221–1238](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L1221-L1238>) [waylandbridge/awl_internal.h:43–46](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L43-L46>) [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [waylandbridge/CMakeLists.txt:309–349](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L309-L349>)
EGL uses **one render thread per window**, `eglSwapInterval(...,1)`, and sends callbacks for the layers drawn after `eglSwapBuffers` returns. SC has a shared Choreographer render loop: callbacks go out on the **vsync tick** for visible layers that have a buffer; commits can trigger one off-tick latch opportunity per window/interval, and fence completion can retry it. Off-tick passes do not send frame callbacks. The comments document that sending callbacks from SF `OnComplete` introduced an extra vsync and reduced a paced client to **29 fps**. [waylandbridge/awl_renderer.cpp:417–439](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L417-L439>) [waylandbridge/awl_sc.cpp:46–75](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L46-L75>) [waylandbridge/awl_sc.cpp:431–440](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L431-L440>) [waylandbridge/awl_sc.cpp:982–984](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L982-L984>) [waylandbridge/awl_sc.cpp:1105–1145](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L1105-L1145>)
**How many buffers:** there is no protocol requirement to use exactly two or three. The queue holds **8** and the AHB cache holds **8**. SC's own copy-target pool is **4** because three slots skipped alternate frames in the documented SF pipeline. SC comments describe up to three client buffers held by SF plus the newest queue head as a fourth, and Mesa mailbox commonly having four. **Inferred recommendation:** start a dma-buf/AHB swapchain with **four reusable buffers**, pace normal GUI rendering by callbacks, and handle release/fence backpressure; allow more buffers if device testing requires them. A one-buffer renderer cannot assume the current head will be released without replacement. Do not interpret an eight-slot queue as eight-buffer minimum. [waylandbridge/awl_bufferqueue.c:43–48](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L43-L48>) [waylandbridge/awl_ahb.hpp:53–63](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.hpp#L53-L63>) [waylandbridge/awl_sc.cpp:224–235](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L224-L235>) [waylandbridge/awl_sc.cpp:389–400](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L389-L400>) [waylandbridge/awl_bufferqueue.c:379–388](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L379-L388>)
A client repeatedly running ahead of vsync switches from SC scanout to GPU-copy mode after **15** consecutive ticks with dropped/superseded frames, and returns after **120** quiet ticks. The comments call these roughly ¼ s / 2 s, but the code thresholds are ticks, so their duration depends on refresh rate. The stated experiment is **vkmark 123 fps scanned out versus ~1300 fps copying**; those are source-comment measurements of producer throughput, not 1300 screen refreshes or a portable benchmark result. [waylandbridge/awl_sc.cpp:389–400](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L389-L400>) [waylandbridge/awl_sc.cpp:871–884](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L871-L884>)
**Parking:** detaching/pausing an Android host window marks it unattached and tears down its renderer/SC tree. New dirty notifications become no-ops and do not send `frame_done`. Commit-time queue draining is disabled for detached roots, intentionally withholding buffers so ordinary clients park on swap/release/callback waits. Cursor queues are an explicit exception and still drain. **Inferred:** this is cooperative backpressure, not SIGSTOP or a guarantee that an app ignoring callbacks and continually allocating new buffers uses zero CPU; queue-full frames are returned immediately. [waylandbridge/waylandbridge.cpp:328–333](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L328-L333>) [waylandbridge/waylandbridge.cpp:1118–1122](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1118-L1122>) [waylandbridge/awl_surface.c:231–249](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L231-L249>) [waylandbridge/awl_surface.c:211–215](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L211-L215>)
**Scaling:** actual globals are **`wp_viewporter` v1** and **`wp_fractional_scale_manager_v1` v1** (some comments inaccurately use a `zwp_` prefix). Source/destination state is double buffered; output scale stays 1; preferred scale is quantized in **1/120** units as `(zoom_pct*120+50)/100`, sent on object creation and on zoom changes. Viewport destination determines logical size when set. MobileGL WSI must distinguish surface logical size from pixel-buffer dimensions. The last sentence is **inferred** from these transformations. [waylandbridge/awl_viewport.c:1–17](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_viewport.c#L1-L17>) [waylandbridge/awl_viewport.c:53–63](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_viewport.c#L53-L63>) [waylandbridge/awl_viewport.c:352–369](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_viewport.c#L352-L369>) [waylandbridge/awl_viewport.c:416–456](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_viewport.c#L416-L456>)
**Subsurfaces:** `wl_subcompositor` v1 supports synchronized and desynchronized children, parent-applied position/stacking, ancestor-recursive effective sync, and cascade application of latched state. Each child has its own buffer queue; SC maps layers to siblings, EGL composites the traversal. The public collection limit is **`AWL_MAX_LAYERS=16`**; the renderer separately includes a cursor slot. **Inferred:** whole-desktop nested compositors and clients with many layers should be tested against this finite layer collection, and a MobileGL integration must preserve per-surface rather than only per-window buffer/fence state. [waylandbridge/awl_subsurface.c:10–35](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_subsurface.c#L10-L35>) [waylandbridge/awl_subsurface.c:396–417](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_subsurface.c#L396-L417>) [waylandbridge/awl_subsurface.c:494–506](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_subsurface.c#L494-L506>) [anland/include/awl.h:369–388](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L369-L388>) [waylandbridge/awl_renderer.cpp:284–288](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L284-L288>)
## 4. Connection and process model, including another Android daemon
### 4.1 Container connection and identities
The actual default is **`/data/local/tmp/awl/wayland-0`**. Config keys `runtime_dir` and `socket_listen` choose another directory or disable the pathname listener. `socket_listen` defaults true; the daemon makes the runtime directory mode **0777**, binds an `AF_UNIX/SOCK_STREAM` listener with backlog **8**, and chmods the socket **0666**. This contradicts the README table's blanket “No socket file” statement; that statement describes Binder-only operation, not the default code path. [waylandbridge/waylandbridge.cpp:1175–1199](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1175-L1199>) [waylandbridge/waylandbridge.cpp:2085–2105](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L2085-L2105>) [waylandbridge/waylandbridge.cpp:2126–2143](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L2126-L2143>) [anland/README.md:31–38](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/README.md#L31-L38>)
Droidspaces is expected to bind **`/data/local/tmp/awl:/run/anland`** into the container. This checkout documents/assumes that preconfiguration; `anland-session/setup.sh` does not create the Android/container bind mount. Setup runs as the non-root desktop user and installs the user service and patched Xwayland. The session sets `XDG_RUNTIME_DIR=/run/user/$(id -u)` if absent, links **`$XDG_RUNTIME_DIR/wayland-anland` → `/run/anland/wayland-0`**, and exports **`WAYLAND_DISPLAY=wayland-anland`**. It launches rootless Xwayland, obtains the display number with `-displayfd`, runs miniwm, and publishes environment through `~/.anlandx-env` and `systemctl --user set-environment`. [shell/src/main/java/com/anland/shell/ds/DsCli.java:31–44](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L31-L44>) [anland/anland-session/setup.sh:1–13](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/setup.sh#L1-L13>) [anland/anland-session/setup.sh:61–64](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/setup.sh#L61-L64>) [anland/anland-session/anland-session.sh:26–45](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/anland-session.sh#L26-L45>) [anland/anland-session/anland-session.sh:108–120](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/anland-session.sh#L108-L120>) [anland/anland-session/anland-session.sh:146–184](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/anland-session.sh#L146-L184>)
`service.sh` is the module's late-start service: it labels `waylandbridge` **`u:object_r:awl_daemon_exec:s0`** and starts it directly with `nohup`. Policy transitions **`ksu + awl_daemon_exec → awl_daemon`**, and explicitly marks **`awl_daemon` permissive**. There is no UID drop in this launch/main path. **Inferred:** in the intended root-manager launch, the daemon remains **UID 0**, SELinux domain **`u:r:awl_daemon:s0`**; the executable label is a different SELinux object type. Do not generalize this to launches through an unconfigured root manager/domain. [anland/module/service.sh:1–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L1-L26>) [anland/module/sepolicy.rule:14–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L14-L26>) [waylandbridge/waylandbridge.cpp:2110–2124](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L2110-L2124>)
Container apps run as the selected **Linux desktop account**, not a universal fixed UID: anland-shell defaults to the first non-root account, probes passwd for the UID, then uses `su - <user>` and `systemd-run --user`; explicit root launches use a different direct path. Anland's policy comments identify container Chrome as **`droidspacesd`**, and rules grant that domain socket/fd access. **Inferred:** this is the expected Android SELinux domain for the documented Droidspaces deployment; exact host UID mapping and domain for another chroot/PRoot/LXC setup must be measured because Droidspaces implementation/user-namespace configuration is not included here. [shell/src/main/java/com/anland/shell/ds/DsCli.java:318–339](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L318-L339>) [shell/src/main/java/com/anland/shell/ds/DsCli.java:374–392](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L374-L392>) [shell/src/main/java/com/anland/shell/ds/DsCli.java:435–458](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L435-L458>) [anland/module/sepolicy.rule:76–87](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L76-L87>) [anland/module/sepolicy.rule:142–154](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L142-L154>)
The Binder name is **`anland.host`**, interface **`anland.IHost`**, service label **`u:object_r:anland_host_service:s0`**. `customize.sh` copies the device's live platform service contexts and appends the supplied mapping. Third-party app domains `untrusted_app_29` and `untrusted_app` are allowed to find the service. Socket pathname access is deliberately narrower: `awl_runtime_sock` is created by a type transition and container `droidspacesd` gets socket-file access and `connectto`; APKs are intended to use Binder connection injection. World-writable DAC permissions alone are insufficient. [anland/module/plat_service_contexts.anland:1](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/plat_service_contexts.anland#L1>) [anland/module/customize.sh:13–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/customize.sh#L13-L26>) [anland/module/sepolicy.rule:4–7](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L4-L7>) [anland/module/sepolicy.rule:76–92](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L76-L92>) [waylandbridge/waylandbridge.cpp:2153–2177](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L2153-L2177>)
### 4.2 Binder fd injection and libawl
`Awl.getWaylandFd()` delegates to `AwlClient.connect()`, which creates **the socketpair in the app**, sends one end through Binder **`T_CONNECT=18`**, closes its local duplicate of the sent end, and keeps the other for `wl_display_connect_to_fd`. Creating the pair in the app preserves its `SO_PEERCRED` UID/PID for ownership checks. The exact Java parcel body is **`writeInt(1); writeInt(0); writeFileDescriptor(theirs)`**, after the interface token. Use that implementation as the authority: nearby daemon comments/log text contain inconsistent abbreviated descriptions of the ParcelFileDescriptor wire. [libawl/Awl.java:69–78](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L69-L78>) [libawl/AwlClient.java:335–381](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L335-L381>) [anland/include/awl.h:140–150](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L140-L150>) [waylandbridge/waylandbridge.cpp:1820–1847](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1820-L1847>)
The Binder handler passes the fd to `awl_server_add_client`; a pipe marshals the integer fd to the Wayland event thread, which calls `wl_client_create`. Binder is used to establish the connection; subsequent Wayland messages and buffer/fence fd passing use that socket. [waylandbridge/awl_server.c:308–347](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L308-L347>) [waylandbridge/waylandbridge.cpp:1837–1847](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1837-L1847>)
`Awl.java` exposes connection, window listing/events, `attachWindow`, and native client spawning. `spawnClient` inherits the connection as **`WAYLAND_SOCKET=<fd>`** through fork/exec. `attachWindow` hosts a Wayland window in an Activity; `AwlClient.surface` sends window id, dimensions, Android `Surface`, death token and host identity through **`T_SURFACE=1`**. The daemon compares the Binder caller UID with the Wayland client's UID for non-allowlisted hosts; root/self/host APK have privileged scope. [libawl/Awl.java:25–41](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L25-L41>) [libawl/Awl.java:188–227](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L188-L227>) [libawl/Awl.java:279–294](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L279-L294>) [libawl/AwlClient.java:28–40](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L28-L40>) [libawl/AwlClient.java:80–95](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L80-L95>) [waylandbridge/waylandbridge.cpp:1528–1567](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1528-L1567>)
`include/awl.h` is the daemon's **logic-layer C API**, despite its header comment saying v2 while **`AWL_ABI_VERSION=3`**. It provides `awl_server_start/add_client`, window callbacks, per-surface queue access and shm access. It is not an Android client AHB-submission library; the Android AAR and this server-internal API serve different roles. [anland/include/awl.h:1–25](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L1-L25>) [anland/include/awl.h:132–150](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L132-L150>) [anland/include/awl.h:309–342](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L309-L342>)
**Could an Android GL server use libawl? — inferred:** yes, as a Wayland client/host under its own identity, or by implementing the same Binder connection protocol natively. It still needs Wayland WSI and one of Anland's accepted buffer types. An unprivileged server APK cannot use `attachWindow` to claim arbitrary existing container-UID windows. For split rendering while the Linux application owns the Wayland connection, keep that ownership and associate imported buffers with that client's resources through a new controlled native-buffer import/broker interface. `libawl` alone does not create this association. [libawl/Awl.java:69–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L69-L81>) [libawl/Awl.java:279–294](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/Awl.java#L279-L294>) [waylandbridge/waylandbridge.cpp:1528–1567](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1528-L1567>) [waylandbridge/awl_internal.h:71–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L71-L81>)
### 4.3 Launching a second Android-side daemon and required access
**Inferred: feasible using the same deployment pattern.** A bionic MobileGL render-server ELF can be launched by a root-module service and expose, for example, **`/data/local/tmp/awl/mobilegl.sock`**, seen as **`/run/anland/mobilegl.sock`** in the existing bind mount. Anland already demonstrates a second Android native service: PulseAudio is launched from the same module, in `awl_daemon` but under the host APK UID because Android audio policy requires a package-associated UID. That audio-specific UID rule is not evidence that GLES requires an APK UID. [anland/module/service.sh:29–42](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L29-L42>) [anland/module/service.sh:119–138](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L119-L138>) [anland/module/service.sh:153–170](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L153-L170>) [shell/src/main/java/com/anland/shell/ds/DsCli.java:31–35](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L31-L35>)
The following are **inferred policy requirements**, mapped to working Anland rules rather than claimed as a tested complete policy for every vendor:
| Direction / resource | Existing evidence and implication for MobileGL |
|---|---|
| Container connects to render server | Need runtime-directory traversal, socket-file read/write/open permissions, and `droidspacesd → mobilegl_daemon:unix_stream_socket connectto`; mirror the existing runtime socket labeling/allow rules. Reusing `awl_daemon` and its runtime socket type is the smallest deployment experiment; a separate domain/type is the maintainable change. [anland/module/sepolicy.rule:76–102](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L76-L102>) |
| Cross-domain fd passing | Grant **both relevant directions of `fd use`**, e.g. `mobilegl_daemon ↔ droidspacesd` and `mobilegl_daemon ↔ awl_daemon`, plus communication and underlying file/device access. Anland has explicit `droidspacesd ↔ awl_daemon` grants. Shared memory/dma-buf/fence access is more than pathname connectivity. [anland/module/sepolicy.rule:142–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L142-L146>) [anland/module/sepolicy.rule:111–120](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L111-L120>) |
| APK-based GL server | Anland's grants for `untrusted_app[_29] ↔ awl_daemon` do not automatically authorize `untrusted_app ↔ droidspacesd`. Either add appropriate direct socket/fd permissions for the new relation, or use a trusted Android broker to pass a socketpair through Binder. Binder service labeling/find, `call transfer`, `fd use`, and socket read/write rights are separate requirements. [anland/module/sepolicy.rule:4–7](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L4-L7>) [anland/module/sepolicy.rule:39–51](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L39-L51>) [anland/module/sepolicy.rule:76–87](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L76-L87>) |
| Native server's Android graphics access | Mirror GPU node, dma-buf heap, vendor library, allocator/mapper service access; root DAC does not replace SELinux/service-manager policy. The audited domain is permissive, so its allow list is not proof of completeness in enforcing mode. [anland/module/sepolicy.rule:18](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L18>) [anland/module/sepolicy.rule:111–125](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L111-L125>) [anland/module/sepolicy.rule:159–161](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L159-L161>) |
| Imported native buffers reach SF/HWC | Consumers may need `fd use` for the domain that originally opened/allocated the fd, not merely the relay daemon. Existing rules explicitly grant `surfaceflinger`, `system_server`, and `hal_graphics_composer_default` access to `awl_daemon` fds. For a new allocation domain, audit/add that new origin's grants too. [anland/module/sepolicy.rule:126–141](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L126-L141>) |
**Inferred transport limit:** local Unix fd passing is a natural fit. TCP cannot transmit a kernel fd/AHB handle as a usable remote kernel object; TCP rendering needs a local allocation broker and pixel/data transfer, or another explicitly implemented transport, before the final local Wayland buffer submission. Anland's relevant existing paths all use local fd passing or local shm, not TCP buffer ingress. [waylandbridge/waylandbridge.cpp:2085–2105](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L2085-L2105>) [libawl/AwlClient.java:342–365](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/app/libawl/src/main/java/com/anlandnext/awl/AwlClient.java#L342-L365>) [waylandbridge/awl_ahb.cpp:444–464](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L444-L464>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>)
## 5. GPU acceleration supplied today and Xwayland requirements
### 5.1 What each project actually configures
| Project / environment | Observed rendering stack |
|---|---|
| Main 6.x container session | Defaults **`MESA_LOADER_DRIVER_OVERRIDE=kgsl`**, **`GALLIUM_DRIVER=kgsl`**, **`FD_FORCE_KGSL=1`**. Dependency setup installs an external lfdevs Mesa tarball containing EGL/GLX/GBM/Gallium/`kgsl_dri.so` and Turnip Vulkan. These are container-side Mesa drivers; waylandbridge's Android EGL is separate. [anland/anland-session/anland-session.sh:108–112](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/anland-session.sh#L108-L112>) [anland/anland-session/deps_noble.sh:104–113](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/deps_noble.sh#L104-L113>) [anland/anland-session/deps_noble.sh:130–141](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/anland-session/deps_noble.sh#L130-L141>) |
| anland-shell | Root/Droidspaces app launcher, not a renderer. It supplies the same KGSL defaults and starts the selected user's app in the container session. [shell/src/main/java/com/anland/shell/ds/DsCli.java:31–44](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L31-L44>) [shell/src/main/java/com/anland/shell/ds/DsCli.java:59–68](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L59-L68>) [shell/src/main/java/com/anland/shell/ds/DsCli.java:374–382](<https://github.com/SuperTurtleDev/anland-shell/blob/920fcfc8159690f5232e2d9c7c3294d4c1481145/src/main/java/com/anland/shell/ds/DsCli.java#L374-L382>) |
| lfdevs Anland: Termux | Explicitly legacy-based; supports Termux native and PRoot/Chroot/LXC. Qualcomm acceleration uses **Freedreno OpenGL and Turnip Vulkan**. Native installation guide currently points to **26.3.0-devel-20260824**, except Weston to **26.2.0-devel-20260709-weston**; container guide points to mesa-for-android-container. Image package URLs pin the 26.3 snapshot. [Termux/README.md:7–13](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/README.md#L7-L13>) [Termux/docs/user-guide.md:148–152](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L148-L152>) [Termux/images/packages.json:1–16](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/images/packages.json#L1-L16>) |
| Termux Mesa build at that release tag | Package version **26.3.0**, Mesa commit **`98f3d6229d61452cef80f8563af7c56ae599dc14`**. `-Dgbm=enabled -Dopengl=true -Degl=enabled -Dgles2=enabled -Dglx=dri -Dllvm=enabled -Dplatforms=x11,wayland -Dglvnd=enabled`; Gallium **llvmpipe,softpipe,virgl,zink**, plus **freedreno** on ARM/aarch64; Vulkan **swrast**, plus **freedreno**; **`-Dfreedreno-kmds=msm,kgsl`**. [termux-packages/mesa/build.sh:6–35](https://github.com/lfdevs/termux-packages/blob/ccc9cc550efc604188242285f99884183596ebb0/packages/mesa/build.sh#L6-L35) [termux-packages/mesa/build.sh:177–185](https://github.com/lfdevs/termux-packages/blob/ccc9cc550efc604188242285f99884183596ebb0/packages/mesa/build.sh#L177-L185) |
| Container Mesa workflow at the same Mesa commit | `-Dplatforms=x11,wayland`, Gallium **freedreno,zink,virgl,llvmpipe**, Vulkan **freedreno**, EGL/GLES2/GLVND enabled, GLX DRI, **`-Dfreedreno-kmds=kgsl`**. These are build settings in the release-tag tree, not proof every installed package/device selects every built driver. [mesa-for-android-container/build-check.yml:57–81](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/.github/workflows/build-check.yml#L57-L81) |
| asveroid Plasma installer | Explicitly **Termux Native**, no root, Snapdragon/Adreno. Installs patched KWin/Xwayland and Freedreno; script still pins **26.2.0-devel-20260709** Mesa and `mesa-vulkan-icd-freedreno`, older than lfdevs' current guide. [Plasma/README.md:3–23](<https://github.com/asveroid/anland-plasma-termux/blob/099710d413b47d2f3f8c5091a95ee2732d3be0e6/README.md#L3-L23>) [Plasma/install-anland-plasma.sh:26–31](<https://github.com/asveroid/anland-plasma-termux/blob/099710d413b47d2f3f8c5091a95ee2732d3be0e6/install-anland-plasma.sh#L26-L31>) |
**Zink:** it is built in both Mesa configurations above, but neither main 6.x's defaults nor Termux's documented KGSL overrides establish it as the default path. Termux's manual examples use `GALLIUM_DRIVER=freedreno`, `TURNIP_KMD=kgsl`, `FD_FORCE_KGSL=1` and `XWAYLAND_FORCE_KGSL_SURFACELESS=1`; the native example also uses `ANLAND_NO_DRM_DEVICE=1 EGL_PLATFORM=surfaceless`. Do not conflate this legacy Termux surfaceless integration with the four-hunk main 6.x Xwayland patch. [termux-packages/mesa/build.sh:177–185](https://github.com/lfdevs/termux-packages/blob/ccc9cc550efc604188242285f99884183596ebb0/packages/mesa/build.sh#L177-L185) [mesa-for-android-container/build-check.yml:65–80](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/.github/workflows/build-check.yml#L65-L80) [Termux/docs/user-guide.md:251–262](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L251-L262>) [Termux/docs/user-guide.md:294–300](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L294-L300>) [anland/patches/xwayland/anland-kgsl-glamor.patch:1–109](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L1-L109>)
**Native vs containers:** Termux-native uses Android/Termux packages; container images install distro-appropriate Mesa into `/usr`. PRoot-Distro examples use `--shared-tmp`; the general guide bind-mounts Termux `$TMPDIR` to container `/tmp`, so the legacy daemon socket is shared. Official GitHub Termux APK variants use shared UID; the compatible variant instead holds the socket in Termux and transfers its fd over Binder. These are legacy transport arrangements, not the 6.x `/run/anland/wayland-0` setup. [Termux/README.md:31–34](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/README.md#L31-L34>) [Termux/docs/user-guide.md:60–96](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L60-L96>) [Termux/docs/user-guide.md:199–206](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L199-L206>) [Termux/images/debian-plasma.Dockerfile:55–58](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/images/debian-plasma.Dockerfile#L55-L58>) [Termux/docs/user-guide.md:294–300](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/docs/user-guide.md#L294-L300>)
**Non-Adreno GPUs:** lfdevs says MediaTek Dimensity, Google Tensor and Samsung Exynos currently use **LLVMpipe**, may fail to enter the desktop, and only **Tensor G1** had been personally tested successfully in PRoot/Chroot. The Plasma installer repeats the software-rendering limitation. This is not a verified hardware-accelerated Mali, PowerVR or Xclipse support statement. No specific accelerated PowerVR/Xclipse result or desktop GL-version claim was established in the reviewed docs/builds. **Inferred:** MobileGL may remove the Mesa KGSL dependency for rendering, but current 6.x's snapalloc-dependent buffer import is a separate obstacle on other vendors. [Termux/README.md:25–29](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/README.md#L25-L29>) [Plasma/README.md:59](<https://github.com/asveroid/anland-plasma-termux/blob/099710d413b47d2f3f8c5091a95ee2732d3be0e6/README.md#L59>) [termux-packages/mesa/build.sh:177–185](https://github.com/lfdevs/termux-packages/blob/ccc9cc550efc604188242285f99884183596ebb0/packages/mesa/build.sh#L177-L185) [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [waylandbridge/awl_ahb.cpp:264–274](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L264-L274>)
**GL versions and performance:** the evidence establishes Freedreno FD650 direct acceleration in a device issue, and waylandbridge requests an **OpenGL ES 3 context**; neither establishes that graphical Linux apps universally get desktop GL 4.6 or any other specific maximum. No `glxinfo`/`eglinfo` version result was found in the cited compatibility docs or device report. The concrete performance figures available in the current code are the **29 fps** callback-placement regression and **123 vs ~1300 fps** vkmark scanout/copy comparison, with the measurement limitations described in §3. [anland issue #84](https://github.com/SuperTurtleDev/anland/issues/84) [waylandbridge/awl_gl.cpp:102–105](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L102-L105>) [Termux/README.md:25–29](<https://github.com/lfdevs/anland-termux/blob/a70879f059a2e8439fdb132195c3dd10d4810106/README.md#L25-L29>) [waylandbridge/awl_sc.cpp:60–75](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L60-L75>) [waylandbridge/awl_sc.cpp:389–400](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L389-L400>)
### 5.2 What Xwayland glamor needs, and what the patch changes
The vendored source is Xwayland **24.1.10**. The patch README explicitly says the display GPU is reached via KGSL and that the chosen fallback `/dev/dri/renderD128` is redirected by the patched Mesa stack. It does not demonstrate that a stock distro GBM can open a working DRM render node on every phone. [anland/patches/xwayland/README.anland.md:1–9](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/README.anland.md#L1-L9>) [anland/patches/xwayland/README.anland.md:29–39](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/README.anland.md#L29-L39>)
| Requirement / interface | Actual pinned GBM implementation |
|---|---|
| GBM device and EGL platform | Opens/uses a device fd, calls `gbm_create_device`, gets `EGL_PLATFORM_GBM_MESA`, and initializes EGL. It tries desktop GL then GLES, requires a renderer and `GL_OES_EGL_image`, and rejects named llvmpipe/softpipe glamor renderers. [xserver/hw/xwayland/xwayland-glamor-gbm.c:1710–1766](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1710-1766) |
| dma-buf image import | Tests both dma-buf import and modifiers extension support for `dmabuf_capable`; constructs `EGL_LINUX_DMA_BUF_EXT` images with plane fds, offsets, pitches and modifier words. **Inferred:** a replacement provider for this path needs compatible GBM allocation/import and EGL image behavior, not only GL draw entry points. [xserver/hw/xwayland/xwayland-glamor-gbm.c:243–277](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L243-277) [xserver/hw/xwayland/xwayland-glamor-gbm.c:1769–1773](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1769-1773) |
| dma-buf export | The pinned GBM path obtains fds through **`gbm_bo_get_fd_for_plane` / `gbm_bo_get_fd`**. It does **not** require/call `EGL_MESA_image_dma_buf_export` in this file. That extension can be useful for an EGL-owned-image export design, but calling it a universal glamor prerequisite would overstate this implementation. [xserver/hw/xwayland/xwayland-glamor-gbm.c:486–533](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L486-533) [xserver/hw/xwayland/xwayland-glamor-gbm.c:844–881](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L844-881) |
| DRI3 | Registers `dri3_screen_init` and import/export callbacks for pixmaps/fds. The patch fixes a modifier problem specifically in imported DRI3 buffers. **Inferred:** accelerated X11 clients need a compatible GLX/DRI3/GBM integration (or another deliberate X11 route); a MobileGL Wayland EGL swap implementation alone does not transparently replace that client/server contract. [xserver/hw/xwayland/xwayland-glamor-gbm.c:1812–1826](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1812-1826) [anland/patches/xwayland/anland-kgsl-glamor.patch:22–37](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L22-L37>) |
The four patch changes are precise:
1. Submit **`DRM_FORMAT_MOD_INVALID`** via linux-dmabuf without requiring it to be found in the advertised modifier set. [anland/patches/xwayland/anland-kgsl-glamor.patch:3–21](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L3-L21>)
2. Rewrite bogus modifier **`1274ULL` (`0x4FA`) → DRM_FORMAT_MOD_LINEAR`** before GBM import, for the documented Mesa/KGSL DRI3 bug. This is not general tiled-buffer conversion. [anland/patches/xwayland/anland-kgsl-glamor.patch:22–37](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L22-L37>)
3. Wait for default feedback only when protocol version is **≥4**; initialize `main_dev=NULL`; when unavailable or without a render node, use **`XWAYLAND_GBM_DEVICE`**, default **`/dev/dri/renderD128`**. [anland/patches/xwayland/anland-kgsl-glamor.patch:38–91](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L38-L91>)
4. Permit glamor when **linux-dmabuf v3** exists despite no `wl_drm` or v4 feedback. [anland/patches/xwayland/anland-kgsl-glamor.patch:92–109](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L92-L109>)
**Inferred:** these changes solve device selection and buffer-routing assumptions in that Mesa-based stack. They do not implement a GBM backend for MobileGL, import full AHB handles into Anland, add AB24/UBWC support, or make container glibc load Android vendor GLES directly. [anland/patches/xwayland/anland-kgsl-glamor.patch:1–109](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/patches/xwayland/anland-kgsl-glamor.patch#L1-L109>) [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_ahb.cpp:334–340](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L340>)
## 6. waylandbridge's own renderer and Android driver loading
The build uses the Android NDK toolchain, **arm64-v8a**, platform **android-35**. Wayland/ffi are static dependencies, and Anland's protocol/adapter source is compiled into one executable. The graphics/system dependencies remain **`binder_ndk android log EGL GLESv3 nativewindow`**; there is no evidence here of a fully static libc+vendor-GLES binary. The README's “single statically-linked ELF” should be read in light of the executable's actual link configuration. [anland/Makefile:77–83](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/Makefile#L77-L83>) [waylandbridge/CMakeLists.txt:225–244](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L225-L244>) [waylandbridge/CMakeLists.txt:303–308](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L303-L308>) [waylandbridge/CMakeLists.txt:364–365](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L364-L365>) [anland/README.md:73–78](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/README.md#L73-L78>)
The renderer calls ordinary Android EGL entry points: `eglGetDisplay(EGL_DEFAULT_DISPLAY)`, `eglInitialize`, `eglChooseConfig` for ES3 and window/pbuffer use, and `eglCreateContext(...CLIENT_VERSION,3)`. It resolves EGLImage/native-buffer/fence procedures with `eglGetProcAddress`. Root executable startup and policy explicitly provide GPU/vendor-file access. **Inferred:** this is a bionic Android-native consumer of the platform EGL/GLES dispatch stack and device graphics implementation, not container Mesa doing the daemon's composition. [waylandbridge/awl_gl.cpp:34–106](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L34-L106>) [anland/module/service.sh:20–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L20-L26>) [anland/module/sepolicy.rule:111–120](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L111-L120>)
There is no custom graphics linker namespace creation or `android_dlopen_ext` bypass in these renderer files. The explicit `dlopen` shown in `waylandbridge.cpp` loads **`libbinder_ndk.so`** to resolve platform-only service-manager/thread-pool functions, not a vendor GLES driver. **Inferred:** a root bionic executable can follow this platform-EGL route without being an APK; root is not itself a linker-namespace bypass. This source does not prove that arbitrary vendor DSOs can be dlopened directly from arbitrary namespaces, nor that a glibc executable can load them. [waylandbridge/waylandbridge.cpp:100–119](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L100-L119>) [waylandbridge/awl_gl.cpp:34–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L34-L76>) [waylandbridge/CMakeLists.txt:364–365](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L364-L365>)
The target does not link Vulkan or call it in these rendering paths. **Inferred:** Anland provides concrete precedent for MobileGL's bionic GLES/Magma server deployment; an Espryt/Vulkan server using Android's Vulkan loader is a separate validation task. Linker/loader policy, extension support and buffer interoperability for that Vulkan path cannot be certified from Anland's GLES implementation. [waylandbridge/CMakeLists.txt:350–365](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L350-L365>) [waylandbridge/awl_gl.cpp:34–106](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L34-L106>)
## 7. What the derivatives reveal about other frame paths
**E7G wlroots-anland.** This is a wlroots **0.20.x** native backend for the **E7G private Anland protocol**, not a backend for 6.x linux-dmabuf Wayland ingress. It connects to `ANLAND_SOCKET` (default `/tmp/display_daemon.sock`) and imports a **consumer-owned pool**. Its allocator gets the selected pool fd/info, wraps a one-plane `wlr_dmabuf_attributes` with offset/stride/modifier, and advertises `WLR_BUFFER_CAP_DMABUF`. Commit locks the submitted buffer until consumer-ready, then sends a wlroots presentation event and a new frame opportunity. **Inferred:** this demonstrates an alternative allocation direction useful for MobileGL—Android supplies render targets—but does not supply native-AHB ingress to current waylandbridge. Its `VSYNC|ZERO_COPY` presentation flags are backend claims based on consumer-ready, not proof of a physical scanout timestamp. [wlroots/README.anland.md:3–19](<https://github.com/E7G/wlroots-anland/blob/351399132de0b208ddb36d0ea2fa74cbe668fb72/README.anland.md#L3-L19>) [wlroots/backend/anland/allocator.c:33–58](<https://github.com/E7G/wlroots-anland/blob/351399132de0b208ddb36d0ea2fa74cbe668fb72/backend/anland/allocator.c#L33-L58>) [wlroots/backend/anland/allocator.c:64–76](<https://github.com/E7G/wlroots-anland/blob/351399132de0b208ddb36d0ea2fa74cbe668fb72/backend/anland/allocator.c#L64-L76>) [wlroots/backend/anland/output.c:21–34](<https://github.com/E7G/wlroots-anland/blob/351399132de0b208ddb36d0ea2fa74cbe668fb72/backend/anland/output.c#L21-L34>) [wlroots/backend/anland/output.c:67–76](<https://github.com/E7G/wlroots-anland/blob/351399132de0b208ddb36d0ea2fa74cbe668fb72/backend/anland/output.c#L67-L76>)
**Yizhou147 anland-drm.** This replaces the legacy Android APK consumer with a DRM/KMS consumer. The implemented main display allocation uses **DRM dumb buffers**, exports PRIME fds, sends a pool with format **1 (RGBA8888)**, modifier **0**, offset **0**, selects a producer render target, then page-flips it. Although the README lists dma-heap/ion helpers, those are not the allocation calls used by this main display path. Its README says untested, but the newer progress note reports a binary running on **houji/Snapdragon 8 Gen 3**, detecting a **5120×2560@60** connector/input, while `drmModeSetCrtc` still fails **Permission denied**, even after stopping SF and trying root. **Inferred:** it demonstrates the protocol's independence from Android windowing, not a working universal replacement for HWC. Its loop closes the returned render fence before page-flip without waiting/passing it onward, so it is also not a reliable synchronization precedent. [anland-drm/consumer/drm_display.c:146–196](<https://github.com/Yizhou147/anland-drm/blob/850f7ca0e0f25d34dd04baa7e12e8751bcff4951/consumer/drm_display.c#L146-L196>) [anland-drm/consumer/drm_consumer.c:242](<https://github.com/Yizhou147/anland-drm/blob/850f7ca0e0f25d34dd04baa7e12e8751bcff4951/consumer/drm_consumer.c#L242>) [anland-drm/consumer/drm_consumer.c:275–320](<https://github.com/Yizhou147/anland-drm/blob/850f7ca0e0f25d34dd04baa7e12e8751bcff4951/consumer/drm_consumer.c#L275-L320>) [anland-drm/README.md:3–13](<https://github.com/Yizhou147/anland-drm/blob/850f7ca0e0f25d34dd04baa7e12e8751bcff4951/README.md#L3-L13>) [anland-drm/PROGRESS.md:14–34](<https://github.com/Yizhou147/anland-drm/blob/850f7ca0e0f25d34dd04baa7e12e8751bcff4951/PROGRESS.md#L14-L34>)
**Legacy 5.x “Anland Display Protocol.”** `common/protocol.h` defines packed control/data messages (`CONSUMER_HELLO=1`, `PRODUCER_HELLO=2`, `SCREEN_INFO=7`, `PICKUP_FDS=9`, `FDS_READY=10`; data `BUF_READY=100`, `REFRESH_DONE=101`, `BUFS_READY=200`; `MAX_BUFS=8`) and `buf_info {uint32_t stride,width,height,format; uint64_t modifier; uint32_t offset;}`. Android consumer dequeues **real ANativeWindow buffers**, retains the original `ANativeWindowBuffer*`, exports only native-handle **fd[0]** plus descriptor, and sets modifier/offset **0**. It waits on dequeue fences before selecting a producer target; producer completion returns a sync_file that is passed to `queueBuffer` for SF to wait on. There is no need to forge a return-path AHB because the Android consumer kept the original native buffer. **Inferred:** this is a strong pattern for a native-handle extension or Android-owned target pool, but the existing private protocol itself does **not** transmit a complete arbitrary native_handle/AHB; compressed storage cannot safely be represented just by claiming modifier 0. [legacy/common/protocol.h:7–49](<https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/common/protocol.h#L7-L49>) [legacy/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c:133–188](<https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L133-L188>) [legacy/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c:778–813](<https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L778-L813>) [legacy/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c:953–1003](<https://github.com/SuperTurtleDev/anland/blob/9ab13eb146bc9f3dd3cc44cabc268659d9eb0a46/consumers/anland_v5/android_consumer/app/src/main/jni/native_consumer.c#L953-L1003>)
## 8. Concrete changes for MobileGL AHB output and avoiding the forge
This section is **inferred design**, not existing Anland functionality. The starting constraint is that MobileGL allocates genuine Android AHBs, commonly **`AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM`**, with DRM identity **ABGR8888/AB24** for little-endian RGBA bytes. Current Anland admits AR24/XR24 and then overwrites the storage description with BGRA donor metadata. In contrast, Anland's **own SC copy pool already allocates RGBA AHBs** with `GPU_COLOR_OUTPUT | GPU_SAMPLED_IMAGE | COMPOSER_OVERLAY` and imports/displays them. Thus the output side is already capable of consuming suitable genuine RGBA AHBs; ingress, representation and lifetime are the missing pieces. [drm-uapi/drm_fourcc.h:193–199](https://github.com/lfdevs/mesa-for-android-container/blob/98f3d6229d61452cef80f8563af7c56ae599dc14/include/drm-uapi/drm_fourcc.h#L193-L199) [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_ahb.cpp:334–336](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L336>) [waylandbridge/awl_sc.cpp:279–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L279-L300>)
### 8.1 Smallest paths, and exactly what each buys
| Path | Changes and limits — all inferred |
|---|---|
| **No Anland change, functional baseline** | MobileGL finishes/readbacks or otherwise converts to packed BGRA/XRGB in `wl_shm`, and its client-side Wayland WSI commits it. This avoids forge/vendor metadata dependence but pays readback/transfer/upload and does not preserve compressed zero-copy buffers. Anland's shm upload/release path already exists. [waylandbridge/awl_gl.cpp:331–379](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L331-L379>) [anland/include/awl.h:325–342](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L325-L342>) |
| **No Anland change, narrow dma-buf experiment** | Produce actual linear, zero-offset AR24/XR24 storage, send modifier **0**, correct byte pitch and sync_files. Requires a device where the snapalloc calibration/import works; allocating an RGBA/UBWC AHB and sending fd[0] is not this path. There is no generic portability guarantee. [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [waylandbridge/awl_ahb.cpp:264–274](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L264-L274>) [waylandbridge/awl_ahb.cpp:334–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L395>) |
| **Smallest meaningful native-AHB change** | Add a native-AHB registration/import side channel and a way to create/associate a Wayland `wl_buffer` with the registered object. Keep the existing renderers/SC transactions, but resolve registered buffers directly to retained AHBs instead of `awl_ahb_wrap`. Add correct RGBA/AB24 format handling and propagate lifetime/fences. This avoids donor allocation/calibration/patching and the fabricated local socketpair round-trip. [waylandbridge/awl_internal.h:71–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L71-L81>) [waylandbridge/awl_surface.c:184–218](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L184-L218>) [waylandbridge/awl_ahb.cpp:534–569](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L534-L569>) [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) [waylandbridge/awl_sc.cpp:727–743](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L743>) |
| **Clean native buffer path** | Versioned Wayland extension or authenticated broker protocol, typed buffer payload, resource-owned native import and stable generation/identity; preserve linux-dmabuf as a separate raw-layout importer. Full AHB metadata stays on Android; the glibc client carries an opaque resource/token and fences. This makes native compressed layouts opaque to Anland's logical layer and leaves import interpretation to gralloc/EGL/SF. [waylandbridge/awl_bufferqueue.h:5–8](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L5-L8>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>) [anland/include/awl.h:309–323](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/include/awl.h#L309-L323>) [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) |
Adding only AB24 to `k_supported` is **insufficient**: `awl_ahb_wrap` still forces HAL BGRA, the cache does not include format, and no original native metadata is available. Extending the forge to another HAL format could help a narrowly compatible linear layout but would still retain the vendor-layout dependency and would not preserve UBWC. [waylandbridge/awl_dmabuf.c:17–22](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L17-L22>) [waylandbridge/awl_ahb.cpp:334–336](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L336>) [waylandbridge/awl_ahb.cpp:546–568](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L546-L568>)
### 8.2 Concrete insertion points
| Function / file | Small viable edit → clean design direction — all inferred |
|---|---|
| `awl_dmabuf_setup`, `params_*`, `dmabuf_buffer_create` in `awl_dmabuf.c`; `awl_server_start`; protocol generation in `CMakeLists.txt` | Add a distinct native-buffer factory/registration protocol and register it at startup; do not overload linux-dmabuf plane fds with arbitrary gralloc metadata. For the existing raw path, fix INVALID and explicitly validate supported modifier, zero offset, format, dimensions and extent instead of discarding layout information. [waylandbridge/awl_dmabuf.c:49–85](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L49-L85>) [waylandbridge/awl_dmabuf.c:105–175](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L105-L175>) [waylandbridge/awl_server.c:385–401](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_server.c#L385-L401>) [waylandbridge/CMakeLists.txt:309–349](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/CMakeLists.txt#L309-L349>) [waylandbridge/awl_internal.h:35–40](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L35-L40>) |
| `host_on_transact` / Binder transaction enum in `waylandbridge.cpp`, if choosing an Android broker | Add a **new** registration operation for complete AHB handle/descriptor import, returning a scoped token. Reuse Binder only as transport/identity infrastructure: `T_CONNECT` currently transfers a Wayland socket. Bind a token to the right client/resource or an explicit delegated registration capability, accounting for the different container and Android-server UIDs. [waylandbridge/waylandbridge.cpp:184–196](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L184-L196>) [waylandbridge/waylandbridge.cpp:1528–1567](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1528-L1567>) [waylandbridge/waylandbridge.cpp:1820–1847](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1820-L1847>) |
| `struct awl_buffer` / destruction in `awl_internal.h`, `awl_surface.c`, `awl_dmabuf.c` | Add payload kind and opaque native import reference, with acquire/release ownership, descriptor and stable identity. Keep the imported AHB alive after `wl_buffer.destroy` while queue/SF references exist, following the existing wrapper lifetime pattern. [waylandbridge/awl_internal.h:65–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L65-L81>) [waylandbridge/awl_dmabuf.c:33–46](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_dmabuf.c#L33-L46>) [waylandbridge/awl_surface.c:58–67](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L58-L67>) |
| `awl_surface_apply_buffer` and `struct awl_bq_buffer` | Carry an opaque native-buffer reference/token with each frame; preserve acquire/release fences and format/geometry. The current queue treats **`dmabuf_fd<0` as NULL/unmap**, so merely adding an AHB pointer while leaving fd=-1 silently breaks readiness/render selection. Introduce a tagged payload or retain a real backing fd in the narrow implementation; update readiness, close/unref, and all fd-based branches. [waylandbridge/awl_surface.c:184–218](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L184-L218>) [waylandbridge/awl_bufferqueue.h:63–76](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.h#L63-L76>) [waylandbridge/awl_bufferqueue.c:184–210](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L184-L210>) [waylandbridge/awl_sc.cpp:896–898](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L896-L898>) |
| `awl_ahb_cache_get` / `awl_ahb_wrap` | Resolve native registrations directly to an acquired AHB, bypassing the forge. An inode-only association can be a controlled prototype, but the clean key is an imported-buffer identity/generation plus complete descriptor; include format and distinguish native handles from reconstructed raw buffers. Multiple metadata handles or reuse must not collide. Cache the real AHB/EGLImage per swapchain buffer. [waylandbridge/awl_ahb.cpp:534–569](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L534-L569>) [waylandbridge/awl_ahb.hpp:53–86](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.hpp#L53-L86>) |
| `awl_gl_tex_import`, `render_frame` in `awl_renderer.cpp` | `awl_gl_tex_import` already takes any suitable AHB through native-buffer EGL import. Feed it the real AHB; extend source selection and format/opacity handling for AB24/XB24. Keep shm's BGRA byte uploader separate; do not apply an unnecessary R/B swap to native RGBA. [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) [waylandbridge/awl_gl.cpp:331–345](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L331-L345>) [waylandbridge/awl_renderer.cpp:292–334](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_renderer.cpp#L292-L334>) |
| `sc_render_window`, `latch_scanout`, `latch_egl` in `awl_sc.cpp`; `awl_ahb_hwc_scanout_ok` | The pitch oracle is specifically for forged linear BGRA and must not decide native opaque/compressed AHB eligibility. Submit a real allocation with its original metadata; require suitable AHB usage and let Android import/composition determine support. Retain a real-AHB-to-real-AHB GPU-copy mode for pacing or unsupported direct composition where source sampling succeeds. [waylandbridge/awl_ahb.cpp:60–91](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L60-L91>) [waylandbridge/awl_sc.cpp:279–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L279-L300>) [waylandbridge/awl_sc.cpp:727–796](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L796>) [waylandbridge/awl_sc.cpp:903–935](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L903-L935>) |
| `awl_esync.c`, queue readiness/release, SC release callbacks | Reuse sync_file per-commit semantics. Minimal implementation can keep current ready-before-latch behavior. A later SC optimization can pass the producer fence directly to SF instead of waiting for readiness, but requires redesigning mailbox selection, ownership and fence lifetime; it is not a one-line replacement of `-1`. Native payloads without a raw dma-buf need explicit release, not the current implicit-reservation fallback. [waylandbridge/awl_esync.c:121–170](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L121-L170>) [waylandbridge/awl_bufferqueue.c:184–192](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_bufferqueue.c#L184-L192>) [waylandbridge/awl_surface.c:84–108](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L84-L108>) [waylandbridge/awl_sc.cpp:329–353](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L329-L353>) [waylandbridge/awl_sc.cpp:727–740](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L740>) |
| `module/service.sh`, `module/sepolicy.rule`, service contexts; container mount/session configuration | Launch the bionic server and expose the local socket/broker, with domain-specific communication, allocator/GPU and fd-origin consumer permissions as in §4. Keep normal Wayland surface ownership tied to the Linux app unless explicitly choosing an APK-owned client architecture. [anland/module/service.sh:20–26](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/service.sh#L20-L26>) [anland/module/sepolicy.rule:76–87](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L76-L87>) [anland/module/sepolicy.rule:111–161](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L111-L161>) [waylandbridge/waylandbridge.cpp:1528–1567](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1528-L1567>) |
### 8.3 Native handle transport, UBWC and the remaining MobileGL responsibilities
**Inferred native transport design:** use **`AHardwareBuffer_sendHandleToUnixSocket` → `AHardwareBuffer_recvHandleFromUnixSocket`** on a **dedicated local Unix channel** between the two bionic processes, or design a Binder HardwareBuffer/native-handle transport. The two Unix APIs are documented for `AF_UNIX` and available since **API 26**. Preserve **all** handle fds and integer metadata, original descriptor/usage and allocation identity; do not copy the forge's hardcoded two-fd/13-word reconstruction as a universal format. Associate the imported object with a Wayland `wl_buffer` by a scoped token/protocol object. Do not interleave AHB's byte stream into the ordinary Wayland socket without a defined protocol envelope. A real interprocess AHB receive/import remains, but the fabricated local donor round-trip and its calibration are eliminated. [android/hardware_buffer.h:528–549](https://android.googlesource.com/platform/frameworks/native/+/refs/heads/main/libs/nativewindow/include/android/hardware_buffer.h#528) [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [waylandbridge/awl_ahb.cpp:398–464](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L398-L464>) [waylandbridge/awl_internal.h:65–81](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_internal.h#L65-L81>) [waylandbridge/waylandbridge.cpp:1820–1847](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/waylandbridge.cpp#L1820-L1847>)
For SC presentation, allocate with the intended usage from the start. Android's header specifically says **`AHARDWAREBUFFER_USAGE_COMPOSER_OVERLAY`** is needed for `ASurfaceTransaction_setBuffer` when the buffer was allocated directly with `AHardwareBuffer_allocate`; the framework normally adds it for framework-managed buffers. **Inferred:** MobileGL's native target pool should use the render/sample/composer combination already used by `pool_create`, subject to device support, rather than changing usage words after allocation. [android/hardware_buffer.h:270–290](https://android.googlesource.com/platform/frameworks/native/+/refs/heads/main/libs/nativewindow/include/android/hardware_buffer.h#270) [waylandbridge/awl_sc.cpp:284–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L284-L300>)
**Inferred UBWC policy:** allow only genuine native allocations that the consuming Android stack can import/sample/present with their original metadata. The extension should carry them opaquely, not assert a linear DRM modifier or patch a donor blob. Unsupported native import should fail clearly before claiming the buffer usable; an optional GPU blit requires the native source to be importable first. RGBA support and compressed-layout support are related but distinct. Anland's own RGBA target pool demonstrates the right allocation/import pattern, not proof of every vendor's compression support. [waylandbridge/awl_sc.cpp:279–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L279-L300>) [waylandbridge/awl_gl.cpp:257–285](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L285>) [waylandbridge/awl_ahb.cpp:334–395](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L334-L395>)
**Inferred MobileGL work items:** client-side Wayland EGL/GL window-system integration must create/resize buffers, attach/damage/commit the correct `wl_surface`, obey configure/viewport geometry, pace frames, and retain each buffer until release plus fence completion. Android-side rendering must allocate/import the target, export a real completion sync_file, and honor consumer release before reuse. The Anland changes above solve buffer handoff, not missing GL entry points or X11 GLX/DRI3/GBM integration. A staged validation should first prove `wl_shm`, then native uncompressed RGBA AHB import, then fence/lifetime/resize/parking correctness, and only then vendor compression and direct-SC behavior. These stages are recommendations grounded in the existing independent paths, not tests performed in this audit. [waylandbridge/awl_surface.c:184–218](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L184-L218>) [waylandbridge/awl_surface.c:825–859](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_surface.c#L825-L859>) [waylandbridge/awl_viewport.c:1–17](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_viewport.c#L1-L17>) [waylandbridge/awl_esync.c:121–200](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_esync.c#L121-L200>) [waylandbridge/awl_sc.cpp:279–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L279-L300>) [waylandbridge/awl_sc.cpp:727–796](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L796>) [xserver/hw/xwayland/xwayland-glamor-gbm.c:1710–1773](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1710-1773) [xserver/hw/xwayland/xwayland-glamor-gbm.c:1812–1826](https://gitlab.freedesktop.org/xorg/xserver/-/blob/5a873951579f1ff894f71489f3651a55d6dde250/hw/xwayland/xwayland-glamor-gbm.c#L1812-1826)
**Preferred integration, inferred:** keep Linux applications as ordinary Wayland clients, keep MobileGL's render server bionic, and add a small native-buffer bridge into Anland's existing AHB-consuming renderers. This addresses both RGBA/opaque gralloc layouts and the donor-forge dependency without making the glibc client understand vendor gralloc internals. The exact viable compressed formats, enforced SELinux policy and release-depth behavior remain device-validation questions. [waylandbridge/awl_gl.cpp:257–289](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_gl.cpp#L257-L289>) [waylandbridge/awl_sc.cpp:279–300](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L279-L300>) [waylandbridge/awl_sc.cpp:727–743](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_sc.cpp#L727-L743>) [waylandbridge/awl_ahb.cpp:139–146](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/services/waylandbridge/awl_ahb.cpp#L139-L146>) [anland/module/sepolicy.rule:18](<https://github.com/SuperTurtleDev/anland/blob/cc1691805fe6aca6851b6fcccb6a1e33019026c1/module/sepolicy.rule#L18>)
@@ -1,591 +0,0 @@
# MobileGL → Anland:只读代码盘点
审计日期:2026-09-30。对象:Arch WSL 的 ~/w7/p9-main。实际分支为 **p8/main**,HEAD **648800a2da231e8152760d400da33e500d511ca6**,提交说明为关闭 P8;不是任务描述里的 feat/disaggregated。依据是本次只读 git status / git branch --show-current / git log -1;未切换分支。路线图也将 P8 标记完成(docs/Disaggregated/ROADMAP.md:3、:36)。所有源码引用均为此目录、此时点的树根相对路径。未构建、未执行测试或设备操作;“已验证”只指被引用的历史证据。Anland 的格式、modifier、同步和部署条件采用题目给定前提,未另行审计 Anland。
**结论:拆分 GL 执行已存在,Anland 所需的窗口系统与帧交付链尚不存在。** 已有 Linux client → Android vendor-driver server 的历史跨机验收;当前 P12 在服务器自己的 SurfaceView 上呈现,未把每帧 dma-buf / fence fd 交回 Linux client。要作为通用 Linux desktop GL provider,仍需 Linux WSI、可交付图像、跨进程同步、buffer 生命周期、多会话/多窗口与真实 context/share-group 支持。前两句有代码/验收直接依据;“所需工作”是 **inferred**,详见各节。(docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:3–17;MobileGL/MG_Remote/CONTRACT-P12.md:7、:16、:44;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:17252–17283;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:14528–14547)
先纠正名称:**Espryt = DirectGLES(原生 EGL/GLES);Magma = DirectVulkan(Vulkan)**。题目第 3 项括号中的两个后端正好相反。(MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:812–822;MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:543–551)
## 1. Linux client 构建、库与加载器
### 1.1 哪些平台能确认
| 平台 / 问题 | 当前结论及依据 |
|---|---|
| Linux glibc x86_64 / WSL | **有实际 client 证据**。P6.5 明确部署 x86_64 WSL client ↔ aarch64 Redmi server;P12 的 WSL trace client 对 Android DirectGLES/stream 已验收 OpenRA 和 rd12。它证明 glibc client 可运行和跨 ABI 的特定组合可工作,不证明任意 Linux GUI 程序已可在 Anland 上显示。(docs/Disaggregated/notes/p65/README.md:61、:93;docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:3–17) |
| Linux CI | 存在 Ubuntu split build,clang/clang++ 20、Release、disaggregated/inproc/push 和 integration tests 配置。这里没有重跑 CI。(.github/workflows/test.yml:1206、:1260、:1269–1296) |
| Linux glibc aarch64 | **未找到该组合的现成 preset 或运行验收,不能写成“已验证支持”**。Linux POSIX transport 路径不限制 x86,wire 已为 x86_64↔aarch64 固定宽度/布局校验;因此原生或交叉 glibc aarch64 构建是合理移植目标,属于 **inferred**。Android arm64 NDK 构建不能替代 glibc arm64 验收。(CMakeLists.txt:177–183、:503–545、:783–835、:954–962;MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:61–77;docs/Disaggregated/notes/p65/README.md:93;docs/Disaggregated/notes/OPEN-QUESTIONS.md:28) |
| “纯 client”库 | 当前是同一 libMobileGL 内编入两种角色,remote client 不构造本地具体 backend;server 可执行文件也链接这份库。没有产品级 client-only / server-only CMake 目标开关。**inferred**:精简 glibc client 的链接依赖是可选后续工作,而不是使用 split 的先决条件。(MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:22–26、:98–105;CMakeLists.txt:594–623、:783–785、:1033–1040;docs/Disaggregated/design/07-memory-readback-verification.md:27) |
可复用的配置形状(仅示意,本次未执行):
cmake -S . -B build-split -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DMOBILEGL_BUILD_DISAGGREGATED=ON
-DMOBILEGL_PIPE_PUSH=ON
-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_BUILD_BENCHMARK=OFF
INPROC 是测试/同进程两角色形态的超集,不是跨进程 client 必需项;INPROC 隐含 DISAGGREGATED,后者隐含 PIPE_PUSH。FlatBuffers submodule runtime header 缺失时 CMake 会警告并把 split 在本次配置中关闭。根目录/两层查找未见 CMakePresets 文件;项目说明和 CI 都用直接 CMake 选项。(CMakeLists.txt:23–30、:503–545;docs/Disaggregated/guide/build-and-run.md:7–12;.github/workflows/test.yml:1275–1293)
工具链最低 CMake 3.22.1、C++23;glslang、SPIRV-Cross、SPIRV-Tools、SPIRV-Reflect、xxHash、VMA、Threads 被链接,非 Android 配置还查找 Vulkan。CI 安装 Mesa EGL/GLES/Vulkan 开发依赖用于主机后端和测试。aarch64 glibc 应用同样需合适 sysroot/工具链并验证 wireFingerprint;目前没有证据可指定一个现成 arm64 glibc preset。(CMakeLists.txt:1、:177–183、:277–283、:719–731、:954–962;.github/workflows/test.yml:1260;docs/Disaggregated/design/06-transport.md:64;最后一句为 **inferred**。)
### 1.2 产物与 SONAME
| 产物 | 源码能确认的事实 |
|---|---|
| libMobileGL.so | CMake 项目 MobileGL 的 SHARED target;GL、EGL、Linux GLX 都在这份库里。(CMakeLists.txt:3、:401–406、:783–785) |
| libEGL.so、libEGL.so.1 | 非 Android、非 Apple UNIX 构建后创建的符号链接,指向上述 target。代码实际只列了这两个 alias。(CMakeLists.txt:825–834) |
| libGL.so / libGL.so.1、libOpenGL.so、libGLESv2.so | 未见同等构建 alias / 独立输出目标。不能因为 comment 写“GL/EGL loaders”就称已经产出 libGL;使用 libGL 的系统程序还需 loader 包装/部署策略,后半句为 **inferred**。(CMakeLists.txt:825–834;GL 导出见 MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:26–59) |
| libMobileGL_s.a | NOT ANDROID 下另建 MobileGL_s STATIC,同一源集合。(CMakeLists.txt:849–881) |
| libMobileGLServer.so | 实际是 PIE **executable**,不是供应用当 EGL vendor dlopen 的库;Android 与 desktop 都强制 lib 前缀、.so 后缀,链接 libMobileGL 并设 $ORIGIN RPATH。(CMakeLists.txt:1033–1085) |
| ELF SONAME | 未发现 VERSION/SOVERSION 或 SONAME override。按 CMake Linux 默认,预期 SONAME 为 libMobileGL.so(**inferred,未读 ELF 动态段**);libEGL.so.1 是文件系统 alias,不证明 ELF SONAME 变成 libEGL.so.1。(CMakeLists.txt:783–835) |
### 1.3 API 导出与 GLVND
GL/EGL/GLX 导出宏都是 extern C + default visibility;EGL 导出覆盖 display/config/context/current、window/pbuffer/pixmap、swap、查询、EGL 1.5 sync/image/platform 接口,以及 eglGetPlatformDisplayEXT。除通用 glXGetProcAddress/ARB 包装外,具体 GLX 导出在 Linux 非 Android 条件下编译,包含 glXCreateContextAttribsARB(MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp:12–23;MobileGL/MG_Impl/GLXImpl/GLXImpl.h:38)。大量 GL core/extension 名称也被导出,但部分明确是 stub;“dlsym 成功”不能当能力证明。(MobileGL/Defines.h:33–44;MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp:12–25、:112–155、:198–240;MobileGL/MG_Impl/GLXImpl/GLXImpl.h:12–39;MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:26–35、:152、:395–429)
两个 backend 的目标/报告 GL 和 GLSL 都为 **4.6.0**,静态 identity 的 IsCompatibilityProfile=false;前端 GL_VERSION 直接从这个 TargetGLVersion 生成,而不是按当前 EGL bound API 切换 OpenGL ES 字符串。README 仍写短期目标 OpenGL 4.2 Core,不能把这些数字解释为“已完成 GL 4.6 conformance”。GLX 可接受 compatibility context 请求,但固定功能不完整,见第 7 节。(MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:817–822;MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:546–551;MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:619–638;README.md:49–56)
**没有查到 libglvnd vendor 集成**:全项目检索没有 MobileGL 的 __egl_Main / __glx_Main 定义或 MobileGL egl_vendor.d JSON;目前是直接导出入口、libEGL alias / 由应用指定 MobileGL 加载的形态。CI/测试出现 egl_vendor.d 是选择服务器侧 Mesa/NVIDIA 原生 EGL vendor,不是注册 MobileGL 为 vendor。(CMakeLists.txt:825–834;MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp:12–127;MobileGL/MG_IntegrationTest/CMakeLists.txt:280–299;MobileGL/MG_Benchmark/Driver/run_driver_bench.sh:19)
**inferred:**Anland 集成需要决定“私有库搜索路径下直接替代 GL/EGL”还是实现 GLVND vendor ABI;已有 GLX 入口不会自动让系统 GLVND 将所有 GLX 应用派发给 MobileGL。依据为上述 alias 和导出形态。
## 2. EGL frontend 与 GLX 的实际能力
### 2.1 平台与 native window
- eglGetDisplay 将 native display key 登记为 EGL_NONE 平台;eglGetPlatformDisplay 将任意 platform/native key 放进显示表,前端并没有按 Wayland/X11/GBM 平台进行真实 WSI 分派。因此“返回非空 EGLDisplay”不构成该平台支持。(MobileGL/MG_State/EGLState/Core.cpp:316–324;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:708–715)
- window backend 按**编译平台**决定:Android / MetalLayer / Win32 / Linux X11;Linux 不是运行时看到 EGL_PLATFORM=wayland 就变为 Wayland。没有 wl_display、wl_egl_window 或 gbm_surface 的实现分支。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:64–75、:744–770;未发现这些 WSI 集成的检索结论也与 docs/Disaggregated/design/08-runtime-and-platform.md:32 一致。)
- EGL_PLATFORM=surfaceless 是 headless 测试 harness 在未显式设置时写给原生 Mesa loader 的环境变量;DirectGLES 后端仍调用原生 eglGetDisplay(EGL_DEFAULT_DISPLAY)。这不是 MobileGL frontend 完整实现了所有 EGL platform。(MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp:172–191;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16587–16593)
- Linux **并非源码只有 surfaceless**:有真实 X11 window 代码,GLX 查询 XGetGeometry,Magma 创建 VkXlibSurfaceKHR;而拆分验收/WSL/CI 的平台政策是不打开窗口。Linux-local X11 支持和“Linux client → Android server → Anland”是不同能力,后一个并未打通。(MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:258–327;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:16450–16500;docs/Disaggregated/design/08-runtime-and-platform.md:32)
- “surfaceless 平台”也应与 EGL_NO_SURFACE context 区分:backend MakeEGLCurrent 要求注册 surface、draw=read,并拒绝 draw/read 为 EGL_NO_SURFACE 的有效 context bind。因此不能宣称完整 EGL_KHR_surfaceless_context;其广告表也没有该扩展。(MobileGL/MG_Backend/BackendObject.cpp:314–338;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:465–473)
### 2.2 Config 与 surface
Config 是前端合成值:每 display 两个默认 config(stencil=0 和 8),RGBA 各 8、depth 24;surface bits 宣告 WINDOW|PBUFFER|PIXMAP,renderable bits 宣告 OPENGL|ES2|ES3;swap interval 0..4;Linux visual 来自 X11 查询,Android visual ID 是 RGBA8888 AHB format 常量。它们不是由每个 frontend config 对等选择的 vendor config 完整镜像。(MobileGL/MG_State/EGLState/Core.cpp:153–182;DirectGLES 实际另选 native config:MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16500–16531、:16593)
查询还固定返回 EGL_RGB_BUFFER、EGL_CONFIG_CAVEAT=EGL_NONE、EGL_NATIVE_RENDERABLE=TRUE、EGL_SAMPLES/SAMPLE_BUFFERS=0,EGL_CONFORMANT 直接等于 RenderableType;这些广告不补足尚缺的 ES/pixmap 实现。(MobileGL/MG_State/EGLState/Core.cpp:535–599;第 2.2、2.3 节实现证据)
| Surface / API | 实际路径 |
|---|---|
| Window / platform window | 创建前端状态后调用 backend CreateEGLWindowSurface;split 经 SurfaceOp。普通模式拒绝空 native window;ServerOwned 可为空,且丢弃客户端窗口 token。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:121–204、:718–776) |
| Pbuffer | 创建前端状态,随后真调用 backend CreateEGLPbufferSurface;缺省 width/height=1;原生实现见第 3 节。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:498–520) |
| Pixmap / platform pixmap | frontend 只建 SurfaceObject,EGLImpl 没有配套 backend pixmap 创建;没有 GLX pixmap / DRI pixmap 通路可据此认定。**inferred:**广告 EGL_PIXMAP_BIT 高于当前可证明的原生能力。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:580–587、:801–807;MobileGL/MG_State/EGLState/Core.cpp:886–915、:991–1020;MobileGL/MG_Backend/BackendObject.cpp:324–338) |
| PbufferFromClientBuffer | 只转给 EGLState 登记,并无对应 backend 导入调用。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:571–578;MobileGL/MG_State/EGLState/Core.cpp:922–956) |
| BindTexImage / ReleaseTexImage / CopyBuffers | 检查句柄/参数后直接成功,未进行纹理绑定或 native pixmap copy;不是可用的图像共享捷径。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:523–568) |
### 2.3 Context、GLES、sharing 与线程
eglBindAPI(EGL_OPENGL_ES_API) **返回成功**,EGL_CLIENT_APIS 也写 OpenGL OpenGL_ES;甚至 OPENVG_API 也被接受。CreateContext 只检查 display/config/shareCtx 的存在,存 ClientAPI、version、profile、flags,然后铸造 frontend handle;不会在这里选择 ES 实现或创建原生 share context。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:240–245、:414–428、:463–464;MobileGL/MG_State/EGLState/Core.cpp:615–689)
所以答案是:**有 ES API 接受和 ES2/ES3 config bits,但没有证据可称为完整 GLES provider、或给出一个经确认的 frontend GLES 最大版本。** Espryt 内部确实请求原生 ES 3.2→3.1→3.0,但那是实现 desktop GL 的 backend context,不等于应用请求的独立 GLES context。GL_VERSION 路径依然返回 desktop target;glGetShaderPrecisionFormat 还是 stub。(MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16591–16612;MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:619–626;MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:152)
前端 EGLState 有多个 context handle、per-thread current/error/API 表、context owner 检查;shareCtx 被存为 SharedContext。然而 GL 状态仍通过全局 pGLContext,active backend 是全局单例,Espryt 只有一套 g_Display/g_Context/g_Surface,Magma 只有全局 pVulkanRenderer。协议 SurfaceOp 没有 CreateContext/DestroyContext/share-group 消息。**inferred:**多个 EGL handle 可创建、context 可在线程间迁移,不等于 N 个独立 GL context 或正确 share-group 隔离;多线程同时发 GL 命令的通用实现不能视为已支持。(MobileGL/MG_State/EGLState/Core.cpp:101–126、:637–641、:1204–1278;MobileGL/MG_State/GLState/Core.h:701;MobileGL/GlobalObjects.cpp:23–24;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16183–16185、:16685;MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:38–39;MobileGL/MG_Remote/Protocol/protocol.fbs:240–258)
文档明确把多 context 留作未做;INPROC 角色隔离也明确不是给 1494 个 pGLContext 读点加 TLS。服务器 native context 常驻 apply 线程,客户端 ReleaseCurrent 只改变逻辑 live 状态,不真实解绑 native context。(MobileGL/MG_Remote/CONTRACT-P12.md:64;docs/Disaggregated/design/09-build-switches-counters.md:21;MobileGL/MG_Remote/Server/ServerLoop.cpp:330–391)
### 2.4 EGL extension 广告与真实实现
完整广告字符串如下,没有隐藏的 dma-buf/fence/damage/buffer-age 附加表:(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:458–473)
| QueryString 范围 | 广告 |
|---|---|
| EGL_NO_DISPLAY | EGL_EXT_client_extensions、EGL_EXT_platform_base、EGL_KHR_platform_base、EGL_MESA_platform_surfaceless |
| initialized display | EGL_KHR_create_context、EGL_MESA_platform_surfaceless |
以下均**未广告**:EGL_EXT_image_dma_buf_import、EGL_EXT_image_dma_buf_import_modifiers、EGL_MESA_image_dma_buf_export、EGL_KHR_fence_sync、EGL_ANDROID_native_fence_sync、EGL_KHR_swap_buffers_with_damage、EGL_EXT_swap_buffers_with_damage、EGL_EXT_buffer_age、EGL_KHR_surfaceless_context。(同上 MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:465–473)
eglCreateImage 虽有入口,却忽略 attribList,仅保存 target/buffer/context 到 map,不调用 driver 或解析 plane fd/offset/stride/modifier。eglCreateSync 只接受 EGL_SYNC_FENCE,创建时直接标 EGL_SIGNALED;ClientWaitSync 直接返回 EGL_CONDITION_SATISFIED,WaitSync 只验证归属。因此这些入口不能用作 Anland 的 dma-buf import/export 或 explicit sync。include/EGL/eglext.h 中的声明也不能改变这一事实。(MobileGL/MG_State/EGLState/Core.cpp:1336–1359、:1378–1394、:1430–1466;include/EGL/eglext.h:784–829、:1103–1111)
### 2.5 GLXImpl 是否是 stub
**不是纯 stub,也不是 Mesa DRI/DRI3 provider。** 它是 MobileGL EGL 上的一层 GLX facade;动态加载 libX11 并解析 XGetGeometry、XGetVisualInfo、XDefaultScreen/Visual、XVisualIDFromVisual、XFree、XSync;按 drawable 建立 EGL surface,swap 前最多每 250ms 查询一次几何并 resize,再调用 EGLImpl::SwapBuffers。没有查到实现 X11 GLX wire rendering、DRI loader driver 或 DRI3/Present buffer 交付的代码。(MobileGL/MG_Impl/GLXImpl/GLXImpl.h:12–17;MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:129–149、:258–327、:956–965、:1019–1023;后一个“没有”是对本实现及检索的结论。)
它的 QueryVersion 固定答 GLX 1.4,QueryServerString 复用本地 client string,并未向 X server 查询 GLX 能力。(MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:543–571)它广告 GLX_ARB_create_context、_no_error、_profile、ARB_get_proc_address,以及 EXT/MESA/SGI swap_control;CreateContextAttribsARB 把版本/profile/flags 翻译为 MobileGL EGL 属性。legacy CreateContext/NewContext 默认请求 3.3 compatibility。GLXWindow 就是输入 XID;draw/read 不同时实际用 draw 同时充当 read,却记录用户 read handle。(MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:84–86、:785–883、:941–953、:968–979)
**inferred:**这可作为将来 Linux X11 frontend 的复用起点;对 Anland/Xwayland,需要新增本地 X drawable 的图像提交机制(例如 DRI3/Present,或先用 CPU image copy),不能把 XID 发给 Android 后端当 ANativeWindow,更不能仅把 ServerOwned 打开就得到每个 X 窗口的内容。依据为该 facade、下面 Rule H / native window 路径。
## 3. Split 模式的 present 路径
### 3.1 共用调用链
1. 导出的 eglSwapBuffers → EGLImpl::SwapBuffers,检查 surface,进入 BackendObject_Remote::SwapEGLBuffers。
2. Remote 调用 base SwapEGLBuffers,验证当前线程、display、唯一 active surface,再通过 GetBackendFunctions().Present 调用 client emitter;不额外调用 ServerSwapEGLBuffers。
3. EmitPresent 先获取 present credit,铸造 FrameSerial,再 EmitAndWait(Present);MGPPresent 本体**只有 FrameSerial**。
4. server 解码至 ServerVerbSink::OnPresent → backend table Present();返回后更新 frame serial、报告已完成 GL fences、ReturnPresentCredit。
(MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp:112–117;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:207–228;MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:396–404;MobileGL/MG_Backend/BackendObject.cpp:369–397;MobileGL/MG_Remote/Client/EmitTables.cpp:1165–1188;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809;MobileGL/MG_Remote/Server/PipeApplier.cpp:388–438)
没有一条 present 回包包含 image/buffer fd、fence fd、surface id、format、stride、offset 或 modifier。它目前确认服务器本地 Present 已返回,不是“Linux compositor 已经采用或释放某个 buffer”。(上述 MGPPresent / OnPresent;后一语义解释为 **inferred**。)
### 3.2 三类 surface
| 模式 | 创建/目标/呈现 |
|---|---|
| (a) Client-owned native window | Remote 先 ServerSetWindowHandle 再 ServerCreateEGLWindowSurface;每个 EGL forwarder 先等已提交命令 apply,避免跨批次乱序。(MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:178–220)Rule H 永久禁止 peer-window-system-local value 跨进程,不是等 P12 将来补。(MobileGL/MG_Remote/CONTRACT-P6.md:31–45)wire 的 AndroidNativeWindow 和 MetalLayer 被 decoder 拒绝、以 UnmigratedSurface 闩会话。(MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:216–224、:303–314) |
| (a) X11/Win32/None 的残留例外 | decoder 仍接受这些 token,UnpackWindowHandle 强转 void*;Linux-local Magma 可用 XID+XOpenDisplay 建 Xlib surface,但 Android build 创建的是 VkAndroidSurfaceKHR。**inferred:**被 decoder 接受不是容器 XID 对 Android 有效,更不是 Anland 集成,存在未封住的窗口种类路径。(MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:124–143、:226–236;MobileGL/MG_Remote/Server/ServerLoop.cpp:1226–1264;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:16429–16500) |
| (b) MOBILEGL_IPC_SURFACE=server | Remote 不发 SetWindowHandle;WindowKind=ServerOwned,nativeToken=0。服务器从 ServerDisplay 取得窗口租约,替换为本进程 Android WindowHandle,调用同一 backend window 创建;reply 返回实际尺寸。Java SurfaceView 是目标,**不是 Anland wl_surface**。(MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:216–218;MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:160–169;MobileGL/MG_Remote/Server/ServerLoop.cpp:1600–1652;MobileGL/MG_Remote/Server/DisplayServerJni.cpp:11–24) |
| (c) Pbuffer / headless | CreatePbufferSurface → SurfaceOp → server backend pbuffer;会话闩为 Offscreen。Espryt 建原生 EGL pbuffer;Magma 创建带尺寸的 windowless renderer,在 Linux 用 VK_EXT_headless_surface,在 Android 无此扩展时使用 AImageReader 的 ANativeWindow 作为不可见 producer target。该 Android fallback 明确“never acquires images”,没有把图像回给 client。(MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:271–288;MobileGL/MG_Remote/Server/ServerLoop.cpp:1361–1381;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16667–16680;MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:378–387;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:16344–16410) |
即使 pbuffer 调用 eglSwapBuffers,也走同一 Present 命令,不是 readback-pixels 的自动交付。真正 EGL_NO_SURFACE 的应用 bind 受第 2 节限制;文档里的“surfaceless”通常描述 headless native display/pbuffer 路线。(MobileGL/MG_Backend/BackendObject.cpp:314–338、:369–397;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:498–520)
### 3.3 后端如何拿到 presentation target
**Espryt / DirectGLES:**窗口来自 backend 的 WindowHandle(ServerOwned 时为租约中的 ANativeWindow),用 vendor EGL 的 eglGetDisplay / eglInitialize / eglBindAPI(ES),创建一个原生 ES context 和 eglCreateWindowSurface;Present 在 native GL 命令流插 GLsync,然后原生 eglSwapBuffers(g_Display,g_Surface),轮询 GLsync 推进资源回收水位。这里不是 EGL native-fence fd。(MobileGL/MG_Remote/Server/ServerLoop.cpp:1623–1635;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16587–16612、:16639–16664、:17252–17290)
**Magma / DirectVulkan:**InitWindowSurface 将 m_windowHandle.Handle 放入全局 VulkanRenderer;Android CreateSurface 以此 ANativeWindow 调 vkCreateAndroidSurfaceKHR,再 vkCreateSwapchainKHR。Present 提交 VkCommandBuffer,调用 vkQueuePresentKHR,并继续 acquire/重建逻辑;VkFence/VkSemaphore 是该 renderer 内部同步。(MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:359–374;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:16429–16435、:14528–14569、:14634–14645;MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp:284)
### 3.4 AHB、dma-buf 与 fence fd:现有命中的真正用途
**未找到产品帧导出路径。** 有 AHB import、有 external-memory 扩展、有 fd passing;用途与“default framebuffer 导出给 client”不同。完整逐命中位置/用途索引见附录 A(主仓库 tracked tree 586 个匹配行、134 个文件;包含文档、历史日志、公共 EGL header,不把 submodule 内上游声明算作 MobileGL 实现)。
| 命中类别 | 用途与是否可直接用于 Anland present |
|---|---|
| P11 AdoptT0 | client 分配 **AHARDWAREBUFFER_FORMAT_BLOB**,height=1,usage=CPU read/write + GPU_DATA_BUFFER,持 CPU lock;通过 hop socket 传 handle,server 收到后校验 BLOB 宽度=size。是 persistent GL buffer 的 backing store,**不是颜色图像**。(MobileGL/MG_Remote/Transport/AdoptT0.cpp:35–75、:78–96、:108–125) |
| GLES eglGetNativeClientBufferANDROID | 用在 glBufferStorageExternalEXT(nativeClientBuffer),然后 MapBufferRange persistent/coherent。没有把该 AHB 做 EGLImage render target。(MobileGL/MG_Backend/DirectGLES/Managers.cpp:2799–2817、:2835–2857、:2875–2897) |
| Vulkan VK_ANDROID_external_memory_android_hardware_buffer | VkBufferManager 将 client 的 AHB BLOB 导入 **VkBuffer**、dedicated VkDeviceMemory,并处理持久映射/生命周期;VulkanRenderer 按条件启用扩展供它用。不是导出 VkImage / swapchain image。(MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp:631–742;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:15348–15362;MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h:75–80) |
| POST / T0 self-test | server bind 时按允许开关、平台 AHB、后端 import op 和 POST 决定 kCapAdoptT0;失败退 T2。产品 T1 已关闭;显式选择 T1 也退 T2。(MobileGL/MG_Remote/Server/ServerSession.cpp:1092–1143;MobileGL/MG_Remote/CONTRACT-P11.md:233–248) |
| extmem_probe 的 external_memory_fd / DMA_BUF / SCM_RIGHTS | 独立探针测试 T1 host-visible buffer fd 导出、T0 AHB BLOB 导入、T3 host memory。它确实有 vkGetMemoryFdKHR 与跨进程读写,但不是 renderer 的逐帧图像输出。(tools/spikes/extmem_probe/README.md:23–60;tools/spikes/extmem_probe/extmem_probe.cpp:8–20、:1718–1773、:2182–2188、:2356–2378、:3590–3664) |
| AImageReader / AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT | integration、CTS、piglit harness 创建 Android 测试窗口;Magma headless fallback 也使用 AImageReader window,但未 acquire 导出图像。(MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp:102;tools/cts/platform/tcuMobileGLPlatform.cpp:129–130;tools/cts/probe/mgprobe.c:158–159;tools/piglit-android/patches/waffle-mobilegl-android.patch:277–278;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:16355–16390) |
| SCM_RIGHTS 产品实现 | 共享段和门铃交付、supervisor 数据连接 handoff,以及 T0 的 hop;详见第 4 节。没有定义 FrameReady(bufferFd,fenceFd) 消息。(MobileGL/MG_Remote/Transport/FdPassing.cpp:108–149;MobileGL/MG_Remote/Client/ClientSession.cpp:1186–1251;MobileGL/MG_Remote/CONTRACT-P11.md:257) |
| include/EGL/eglext.h | Khronos 公共声明:AHB native client buffer、dma-buf import/modifiers/export。只有声明,不是 MobileGL 功能。(include/EGL/eglext.h:517–520、:784–829、:1103–1111) |
| 历史日志 | vendor Vulkan extension 枚举及运行诊断;证明该次 driver 暴露了名字,不证明 MobileGL 调用该扩展输出帧。逐行出处见附录 A。 |
对主仓库 tracked tree 的指定精确子串检索中,**sync_file 和 SYNC_FD 无命中**;另外检索未找到产品 vkGetSemaphoreFdKHR / eglDupNativeFenceFDANDROID 调用。EGL_ANDROID_native_fence_sync 未广告,前端 EGL sync 只是状态对象;GLsync / 完成水位也不是可交给 zwp_linux_explicit_synchronization_v1 的 sync_file fd。(检索范围见附录 A;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:465–473;MobileGL/MG_State/EGLState/Core.cpp:1336–1394;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:17252–17283)
**inferred:**Anland 需要新增 image 路线,不能把 P11 BLOB 改个名字就用。最直接候选是 server 分配/导入可渲染 AHB image(GLES:EGLImage+FBO;Vulkan:external-memory VkImage),输出单平面图像描述及真实同步;也可改造 AImageReader 为实际 acquire/release 的 buffer 消费链。必须验证四字符格式、stride/offset、LINEAR/INVALID modifier 以及 AHB/gralloc native handle 与普通 dma-buf 的可转换性,不能把 opaque fd 或 AHB 多 fd handle 默认当成 Anland 可导入的线性 ARGB/XRGB。依据:当前 BLOB/import 代码与题目给定 Anland buffer 限制;历史探针甚至在 Adreno/Mali 两台上都没得到 T1-dma-buf 导出(docs/Disaggregated/notes/p0/p0-device-findings.md:18–27),但该 buffer 探针也不能证明所有 image 路线必然失败。
## 4. Fd passing、transport 与 SELinux
### 4.1 今天实际可用的组合
| 拓扑/端点 | 实际数据面 | fd 能力 |
|---|---|---|
| inproc | 同进程共享段/双角色 | InProcessTransport 复制描述符给 peer,模拟所有权;不是跨 Android 安全域方案。(MobileGL/MG_Remote/Transport/InProcessTransport.cpp:200;docs/Disaggregated/guide/build-and-run.md:20–22) |
| spawn + fork | ShmLink | launch server 后连接本机 AF_UNIX;四段+三个门铃经 aux fd passing。配置只收 auto/shm。(MobileGL/MG_Remote/Client/ClientSession.cpp:954–1005、:1186–1251) |
| spawn + unix:path / @abstract | ShmLink | control 与 aux 配对;ShareFd/ReceiveFd 在 aux 上 SCM_RIGHTS。(MobileGL/MG_Remote/Client/ClientSession.cpp:928–950;MobileGL/MG_Remote/Transport/SocketTransport.cpp:849–868) |
| spawn + fd:control,aux | ShmLink | P11 B1 已连接的两根 AF_UNIX SOCK_STREAM socket;可由 Android helper/broker 交接。只收 auto/shm。(MobileGL/MG_Remote/CONTRACT-P11.md:110–125;MobileGL/MG_Remote/Client/ClientSession.cpp:877–924) |
| spawn + tcp://host:port | StreamLink | **不传 fd**。SocketTransport::ShareFd/ReceiveFd 对 m_tcp 返回 UNSUPPORTED;client 只接 auto/stream。(MobileGL/MG_Remote/Transport/SocketTransport.cpp:849–868;MobileGL/MG_Remote/Client/ClientSession.cpp:928–950、:1025–1026) |
| TCP control + shm data | 设计过,**当前未接线** | design/06 描述的 ShmLink 独立 AF_UNIX rendezvous 是第二波设计;同文件最后明确第一波仅 TCP+stream。当前上层按 IsTcp 选数据面并拒绝混搭,不能按愿景回答已经支持。(docs/Disaggregated/design/06-transport.md:54–67;MobileGL/MG_Remote/Client/ClientSession.cpp:928–936、:1025–1026) |
“控制面不传 fd”作为协议层概念成立:SCM_RIGHTS 不塞进 FlatBuffers 控制字节流。但当前实现的 **ITransport 接口仍保留 ShareFd / ReceiveFd**,由 SocketTransport 的 aux socket 实现;并非代码已经完全迁到 ShmLink 自有交付 API。新实现应尊重实际的 ITransport/aux 接口,而不是仅照旧契约措辞接线。(MobileGL/MG_Remote/Transport/ITransport.h:87–108;MobileGL/MG_Remote/Transport/SocketTransport.cpp:849–868;对照 MobileGL/MG_Remote/CONTRACT-P6.md:636–648)
### 4.2 “每帧 server → client buffer fd + fence fd”有多少现成基础
基础发送已是**双向**接口;server 现在就把共享段 fd 发给 client。SendFd 的 ancillary buffer 只放一个 int;ReceiveFd 虽收集至多四个以便关闭多余 fd,随后明确要求 receivedCount==1。sideband 最大 256 字节。不能原样把“两 fd”塞进一次现有消息。(MobileGL/MG_Remote/Transport/ITransport.h:89–108;MobileGL/MG_Remote/Transport/FdPassing.h:36–38、:48–73;MobileGL/MG_Remote/Transport/FdPassing.cpp:131–149、:253–267、:289–301;MobileGL/MG_Remote/Client/ClientSession.cpp:1194–1217)
**inferred:fd 搬运本身是局部扩展,完整呈现协议是显著工作。** 可选择两个带相同 frame/buffer serial、不同 fd-kind 的单 fd offer,或定义版本化 multi-fd packet;更有效的是 buffer pool 首次注册发 buffer fd,之后每次 present 仅发 buffer id + acquire fence。需要同时实现:
1. session/surface/buffer/generation/frame 关联、format/plane metadata、协商和协议修订;
2. client 持续接收 server aux offer 的 dispatcher,不能只在握手读取 7 个 fd;与已有 T0 aux 用途协调;
3. fence=-1 / 无 fence 情况、重复/陈旧/中断消息及 fd 关闭规则;
4. buffer release / release fence 的反向通道,在 compositor 未释放前禁止复用;
5. 有界发送和背压,避免 apply 线程在 client 不读 fd 时永久阻塞;
6. StreamLink 没有本地 fd 传递的 fallback:如同机额外 AF_UNIX,或显式像素传输模式。
这些是设计建议,不是现有功能。依据:一次一个 fd、有限 sideband、仅 serial 的 Present、T0 offer 协议及当前单独的阶段性接收逻辑。(MobileGL/MG_Remote/Transport/FdPassing.h:36–73;MobileGL/MG_Remote/Client/ClientSession.cpp:1186–1251;MobileGL/MG_Remote/CONTRACT-P11.md:257;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809;MobileGL/MG_Remote/Transport/SocketTransport.cpp:849–868)
### 4.3 谁创建共享段:文档 owner 不等于 allocation owner
**当前跨进程产品代码由 server 创建全部四段**:
- ServerSession::Accept 的非 stream 分支调用 Memory().Create(sizes, MemoryRole::Server)。
- SessionSegments::Create 枚举 mgl-cmd、mgl-stage、mgl-reply、mgl-event,并逐个 ShmSegment::Create / Map。
- client 收 7 个 fd(4 段+3 门铃),再 AdoptFromDescriptors(..., MemoryRole::Client)。
(MobileGL/MG_Remote/Server/ServerSession.cpp:713–737;MobileGL/MG_Remote/Transport/ShmSegment.cpp:236–284;MobileGL/MG_Remote/Client/ClientSession.cpp:1186–1251)
设计表把 SEG_CMD/SEG_STAGE “拥有者”写 client,适合解释写者/语义归属,**不能用于证明 fd 是 root container client 创建的**。Linux 的 ShmSegment::Create 用 raw memfd_create syscall + ftruncate,失败退 shm_open+立即 unlink;Android 用 ASharedMemory_create。若 server 是 Android app,其握手段走 Android 创建分支,而非 Linux client 的 memfd 分支。(docs/Disaggregated/design/06-transport.md:9–17;MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:51–109;上段调用点)
StreamLink 用私有段传字节,本地 memfd/存储并不以 fd 共享给对端;这与 ShmLink 的跨进程共享不同。(docs/Disaggregated/design/06-transport.md:17、:59;MobileGL/MG_Remote/Server/ServerSession.cpp:713–737)
**inferred:**因此题目担心的“Android untrusted_app 使用 root-client 创建的 CMD/STAGE fd”不是当前握手路径的准确模型;真正需要验证的是 root/container 域接收和映射 app-origin fds、AF_UNIX connectto/namespace 可达性,以及今后客户端返回 release fence 的 fd use。P11 T0 确实是相反方向的 client-origin AHB,但 glibc client 没有该 Android AHB allocator,不能自动享有 T0;可先明确选择 T2。(MobileGL/MG_Remote/Transport/AdoptT0.cpp:35–54;MobileGL/MG_Remote/Transport/AdoptT0.h:56–61;MobileGL/MG_Remote/CONTRACT-P11.md:233–248)
历史 SELinux 测量:shell 和另一个 untrusted_app 连 server 的 abstract unix socket 被 connectto 拒;root relay 打通连接后,另一 app mmap server 创建的段可用。B1 因而实现令牌 broker + app_process helper + Binder PFD 交接,helper exec 外部程序并设置 fd: 端点。**该证据不是 Droidspaces root glibc/chroot 域的测试结论**,不能承诺所有 ROM 或 root 策略都允许。(docs/Disaggregated/notes/p11/B0-CROSS-APP.md:3–15;MobileGL/MG_Remote/CONTRACT-P11.md:110–125、:195–197)
还有部署限制:目前 broker 只认离屏 MobileGLServerService,显示 Activity :mglwin 在跑时也可答 server-not-running;它不是 P12 onscreen server 的通用 broker。(MobileGL/MG_Remote/CONTRACT-P11.md:147)
## 5. Session / 多 client / 多窗口限制
### 5.1 哪些是宿主管理策略,哪些深入进程状态
| 限制 | 所在层与深度 |
|---|---|
| “同设备一个 server” | P12 APK 产品策略:DisplayActivity 启动会 stopService;Service 启动会结束显示 server。不是 kernel / wire 全设备唯一锁。(MobileGL/MG_Remote/CONTRACT-P12.md:46;android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLDisplayActivity.java:119;android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java:85–103、:139) |
| 这个口号已有例外 | B1 在主监听为 TCP 时另起一个私有 UNIX broker supervisor。两者是独立 process/supervisor;所以不能把“一个 device 最多一个 session”当已严格执行的全局不变量。**inferred:**不同 supervisor 各自的 Busy 不互相协调。(android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java:147–162;MobileGL/MG_Remote/CONTRACT-P11.md:113;MobileGL/MG_Remote/Server/ServerMain.cpp:1222、:1363、:1548) |
| 每 supervisor 一个 active session | TCP authenticated second Hello → Refuse Busy;UNIX/thread/fork 路线也做 Busy。单个慢启动但已配对 client 可占槽;很晚才发 Hello 的第二个 client 可能只看见发送失败。(MobileGL/MG_Remote/Server/ServerMain.cpp:915、:1222–1260、:1363、:1548–1565;MobileGL/MG_Remote/CONTRACT-P11.md:130) |
| Offscreen fork-per-session | Service exec libMobileGLServer.so endpoint --serve;每 session 子进程 fork,并设 PR_SET_PDEATHSIG 随 supervisor 退出。它本来具备进程隔离结构,但调度仍串行。(android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java:180–188;MobileGL/MG_Remote/Server/ServerMain.cpp:317、:1240、:1553;MobileGL/MG_Remote/CONTRACT-P12.md:45) |
| Onscreen 串行进程内 session | Activity 的 :mglwin 持 SurfaceView,server 用 Handoff::Thread;backend 按进程钉住,首次 session 可决定类型,之后不能换另一 backend。会话间重置 latch / 清 backend 状态。(MobileGL/MG_Remote/CONTRACT-P12.md:44;MobileGL/MG_Remote/Server/ServerMain.cpp:520–542、:1623;docs/Disaggregated/notes/p12/INTEGRATOR-DECISIONS-P12.md:10–14) |
| 一会话一个 surface mode | 首次成功 pbuffer → Offscreen;ServerOwned → OnScreen。另一类返回 SurfaceModeMismatch,拒绝不等于杀 session。X11/Win32/None 残留路径不闩模式,是未收紧的例外。(MobileGL/MG_Remote/CONTRACT-P12.md:17、:23;MobileGL/MG_Remote/Server/ServerLoop.cpp:1361–1381、:1539–1543、:1606–1614) |
| ServerOwned 只有一个实际窗口 | 多个 surface token 可以存在,但都指 server 的同一窗口;一个 extent 应用到全部。不是 M 个独立窗口。(docs/Disaggregated/notes/p12/INTEGRATOR-DECISIONS-P12.md:16;MobileGL/MG_Remote/Server/ServerLoop.cpp:1627–1648;MobileGL/MG_Remote/Client/ClientSession.cpp:449–463) |
| Session 单例 | ServerSession::Active 是一个进程全局 atomic 指针,ServerSessionInstance / ClientSessionInstance 为单例;segment resolver 也明确全进程唯一,反向 callback 表为全局。(MobileGL/MG_Remote/Server/ServerSession.cpp:527–543、:832–857;MobileGL/MG_Remote/Client/ClientSession.cpp:616–619;MobileGL/MG_Pipe/MGPipeCallbacks.h:76–77) |
| Backend / frontend 单例 | pGLContext、pActiveBackendObject、global function table;Espryt g_Display/g_Context/g_Surface;Magma pVulkanRenderer。切换 surface 会替换 renderer/运行时状态,不是独立 per-window/context renderer 集合。(MobileGL/MG_State/GLState/Core.h:701;MobileGL/GlobalObjects.cpp:23–24;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16183–16185;MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:38–39;MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:359–387) |
| 热路径缺乏多窗口身份 | Present 只有 FrameSerial;MGPSurfaceInfo 只有尺寸/格式/samples/layers/default 标记,没有 surface id。当前依赖 session 当前绑定目标,无法直接表达并行多窗口完成/释放。(MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809、:1999–2005) |
### 5.2 要服务 N clients × M windows,需要什么
以下均为 **inferred**,基于上表结构:
- **N 个 client 最小方案:每 client 一个 worker process。** 扩展 offscreen supervisor 为多 child/session registry、把控制/数据配对、认证、额度、日志、退出清理路由到对应 worker;这样可以暂时保留大量 backend process-global 状态。仅删一个 Busy 判断不够,当前 child/session handoff 和 Active 指针模型必须保持隔离。(基础:MobileGL/MG_Remote/Server/ServerMain.cpp:915、:1222–1260;MobileGL/MG_Remote/Server/ServerSession.cpp:527–543)
- **同一进程并发 N sessions** 则要 session 化 active backend、resolver、callback 表、latch、caps、backend resource/cache 全局,或完整线程/对象归属隔离;目前 P12 的结束清理只服务串行重用。(基础:MobileGL/MG_Remote/Server/ServerSession.cpp:832–857;MobileGL/MG_Pipe/MGPipeCallbacks.h:76–77;docs/Disaggregated/notes/p12/INTEGRATOR-DECISIONS-P12.md:12–14)
- **单 client 的 M windows** 要有每 surface 独立 framebuffer / image pool、尺寸世代、swap interval / credits、present/release identity;修改 active-surface backend 生命周期,避免换窗即丢弃该进程唯一 renderer。共享 GL 资源不能粗暴一窗口一进程来保证,还要真实 context/share-group。(基础:MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:359–387;MobileGL/MG_Backend/BackendObject.cpp:314–338;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809、:1999–2005)
- **Xwayland glamor** 应单独当系统级 EGL/GLES/图像共享消费者验证;应用 GLX facade 可用并不能保证 glamor 所需 EGLImage、dma-buf import/export、同步和 renderer 资源互通。可以先让 Xwayland 保持其他渲染/软件路径,仅给选定 GL 应用使用 MobileGL;这属于分阶段部署选择,不是现成配置承诺。(基础:第 2 节 image/sync/config 和 GLX 证据;Anland/Xwayland 场景取自题目。)
## 6. 窗口系统事件、几何、失效与 pacing
### 6.1 现有协议能表达什么
SurfaceOpKind 完整列表为 InitializeDisplay、CreateWindowSurface、CreatePbufferSurface、ResizeWindowSurface、ReleaseSurface、MakeCurrent、ReleaseCurrent、SetSwapInterval、ReleaseResources、SetWindowHandle、InitCapabilities(另有 None)。它有 seq/display/surface/windowKind/nativeToken/width/height/swapInterval/readSurface/context;没有 Wayland object、DRM format/modifier、damage region 或 buffer release。(MobileGL/MG_Remote/Protocol/protocol.fbs:240–305)
WindowKind 为 None=0、AndroidNativeWindow=1、X11=2、Win32Hwnd=3、Surfaceless=4、Pbuffer=5、MetalLayer=6、ServerOwned=7;**没有 Wayland/GBM**。ServerOwned 只合法于 CreateWindowSurface,token 必须 0。(MobileGL/MG_Remote/Protocol/protocol.fbs:261–276;MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:199–214)
SurfaceReply 含 seq/ok/EGL version/eventHead/width/height/refusal;defaultFb schema 字段还在,但 encoder 固定传 0,实际 framebuffer 几何通过 kEventSurfaceChanged。eventHead 保证更早的数据面事件先于 reply 被观察。(MobileGL/MG_Remote/Protocol/protocol.fbs:308–323;MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:242–260)
ServerOwned 的 resize 为 Java SurfaceHolder.setFixedSize / setSizeFromLayout 请求,窗口实际尺寸经 reply/event 返回;Activity 做等比 letterbox。kEventSurfaceChanged 带 Width/Height/InternalFormat/Samples/Layers/IsDefault,client 更新默认 framebuffer 和所有 ServerOwned surface extent。没有 frame callback、presentation timestamp 或 input forwarding。(MobileGL/MG_Remote/CONTRACT-P12.md:31–35、:64;MobileGL/MG_Remote/Server/DisplayServerJni.cpp:27–34;MobileGL/MG_Remote/Client/ClientSession.cpp:449–463;MobileGL/MG_Pipe/MGPipeTypes.h:1999–2005)
SurfaceProgress 是冷启动 SurfaceOp 心跳,不是“该帧可显示”的事件。P10 的 kEventFenceSignaled 是逻辑 GL fence 完成报告,不是 sync_file fd。(MobileGL/MG_Remote/Protocol/protocol.fbs:390–398;docs/Disaggregated/ROADMAP.md:38;MobileGL/MG_Remote/Server/PipeApplier.cpp:433–438)
### 6.2 device-lost:文档承诺与代码差异
现有 peer hangup 设置 ClientSession::m_deviceLost,一次日志后后续 wire verbs DECLINED;不自动 respawn。窗口被撤销时,先在 apply 线程释放 backend surface,再 Fatal ServerWindowLost,最后归还 lease;Detach 有界 3 秒。纯等待超时不应被当成 peer 死亡,但仍有自己的 Fatal timeout 失败家族。(MobileGL/MG_Remote/Client/ClientSession.cpp:595–613;MobileGL/MG_Remote/CONTRACT-P12.md:27–40;MobileGL/MG_Remote/Client/ClientSession.cpp:2432–2438)
**不能照抄“eglSwapBuffers 返回 EGL_CONTEXT_LOST”为当前已实现行为。** design/08:33 确实这样写,但实际 EGLImpl swap 只有 validation/backend false 时 SetError(EGL_BAD_SURFACE),成功则 TRUE;base 调 void Present() 后返回 true,Remote 直接复用 base。检索产品代码中 EGL_CONTEXT_LOST 仅见 enum-to-string,未见 lost→EGL error 映射。因此:GL reset/device-lost 机制存在,EGL swap 的标准错误映射有缺口;**inferred:**死亡后某些调用可仍沿现有 Bool 路径成功,具体时序未运行验证。(docs/Disaggregated/design/08-runtime-and-platform.md:33;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:207–228;MobileGL/MG_Backend/BackendObject.cpp:390–397;MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:396–404;MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp:77)
### 6.3 Present credit 与 compositor 怎样接
MOBILEGL_IPC_PRESENT_CREDIT 默认 1、范围 1..8;client 在编码 present **之前**等待 serial-credit 的 presentAckSerial。server 在 backend Present() 返回之后还 credit。它限制命令执行/本地 swap 的 run-ahead,独立于 CMD/STAGE 字节 credit。(docs/Disaggregated/design/08-runtime-and-platform.md:7–8;MobileGL/MG_Remote/Client/ClientSession.cpp:2385–2438;MobileGL/MG_Remote/Client/EmitTables.cpp:1165–1181;MobileGL/MG_Remote/Server/PipeApplier.cpp:426–438)
文档 design/08 仍称 credit>1 未测,但后来的 P12 跨机验收已测 1/2/3;报告应以后者为准,不能把它写成仍完全未知。(docs/Disaggregated/design/08-runtime-and-platform.md:8;docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:16、:23–34)
**inferred:接 Anland 至少要分开三种事件。**
| 事件 | 建议接线 / 不可混用的语义 |
|---|---|
| GPU frame ready | server 输出 image identity + acquire fence;client 可将 fd 交给 Wayland explicit synchronization。不能用现有 presentAckSerial 冒充 GPU fence。(基础:MobileGL/MG_Remote/Server/PipeApplier.cpp:426–438;题目给定 explicit sync) |
| compositor frame callback | 用于允许/建议下一帧节奏;可控制 frontend swap pacing、有限 credit 或单独 queue-depth。不能证明旧 buffer 已可重写。(基础:当前 credit 返回条件同上;新 compositor 语义为集成设计推断) |
| wl_buffer.release / explicit release fence | 发回 server 的 BufferReleased(surface,buffer,generation[,fence]),决定 image pool 的复用。多窗口要每窗/每池计数,不能只用全会话一个 frame serial。(基础:MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809、:1999–2005;题目给定 Anland buffer 路线) |
还需让隐藏/最小化/长期无 frame callback 的窗口不会触发现有“credit 长期不还→PresentCreditTimeout”而丢会话;必须定义暂停、丢帧、buffer pool 耗尽和断连回收规则。这是 **inferred** 的协议设计需求,不是现有行为。(MobileGL/MG_Remote/Client/ClientSession.cpp:2432–2438;MobileGL/MG_Remote/CONTRACT-P12.md:64)
## 7. 通用 Linux 桌面 GL 覆盖:一张短表
| 能力 | Espryt / DirectGLES | Magma / DirectVulkan | 对 Linux 应用的含义 |
|---|---|---|---|
| 最大报告/目标 GL、GLSL | 4.6.0 / 4.6.0,静态 core identity。(MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:817–822) | 同为 4.6.0,静态 core identity。(MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:546–551) | 是广告/目标,不是完整一致性认证;README 仍写 4.2 Core 目标。(README.md:49–56) |
| Compatibility / fixed function | 两者共用前端:compat profile 属性可保存并用于宽松语义,但不是完整 legacy pipeline。导出/lookup 未找到核心 glBegin/glEnd、glMatrixMode、glLightfv、glMaterialfv、glNewList、glDrawPixels 的真实链;一些 EXT matrix 名只是扩展符号表存在。 | 同左 | 无法承诺旧 GL1.x/2.x fixed-function 桌面程序。依据:MobileGL/MG_State/GLState/Core.h:703–708;MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:2426;MobileGL/MG_Impl/GetProcAddress.cpp:896–910、:1406–1407;MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:26–35;“无法承诺”为 **inferred**。 |
| GLES contexts | frontend BindAPI(ES) 接受;backend 内部 ES3.2→3.0 negotiation。 | frontend 同样接受 ES;backend 是 Vulkan。 | 没有完整 GLES profile/version 保证;GL_VERSION 不切 ES,GetShaderPrecisionFormat stub。(MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:414–428;MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16595–16612;MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:619–626;MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:152) |
| GLX_ARB_create_context | 共用 Linux GLX facade;广告并将版本/profile 转到 EGL。(MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:84–86、:818–883) | 同左 | 入口可用≠完整 share group / DRI3 / Xwayland present。(第 2、5 节证据;判断为 **inferred**。) |
| 已知非游戏专属缺口 | DebugMessageControl/Callback/GetDebugMessageLog、若干 robustness/GetnUniform 等导出仍 stub;EGL image/sync 属于状态壳。 | 同样共用前端 | 会影响调试、robustness、图像互通和 compositor 消费者。(MobileGL/MG_Impl/GLImpl/Exporting/Definitions.cpp:397–429;MobileGL/MG_State/EGLState/Core.cpp:1336–1466;影响判断为 **inferred**。) |
| docs 记录的未完项 | fp64 server 广告/client 编译配置不一致;driver-written texture 拒读仍可变 session Fatal;多 context 未做。 | 有 malformed indirect fixture 丢设备、设备对齐约束及 monolith 特有债。 | 应以真实目标应用/GL CTS 子集验收,不能用 Minecraft 或 trace 成功覆盖全部 desktop workloads。(docs/Disaggregated/notes/DEBTS.md:14、:22、:25–28、:42;验收建议为 **inferred**。) |
这里的固定功能否定基于实际入口/前端检索,不把“IsCompatibilityProfile=false”单独当证明;相反,EGL/GLX 的 compatibility 请求和 getter 确实存在,必须与完整功能实现区分。(MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:794–812;MobileGL/MG_State/EGLState/Core.cpp:657–659;MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:2426)
## 8. 文档是否已预见 Linux-on-Android / Wayland / AHB / 多 client / Termux
以下为原文节录。保留“历史设计”与“当前实现”的区别,不把旧推测当新验收。
| 主题 | 原文与出处 | 如何解读 |
|---|---|---|
| 容器 Linux / Termux | “**B 容器类 guest(DroidSpace / Winlator / Termux,共享内核)** … AF_UNIX … 同上(含 AHardwareBuffer_sendHandleToUnixSocket,API 26) … **整套 P6 机制逐字可用**。”(docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md:138) | 同内核 transport 的历史论证;并非 Anland WSI 或 glibc AHB API 验收。 |
| 后来场景变更 | “**形态 B 不再是操作性场景;终局是 D,P6.5 在关键路径上。**”(docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md:142) | 不能据历史容器表声称已为 Anland 交付。 |
| Linux client 范围 | “**默认先 Linux + Android(现有 CI 能验),Windows client 第二批 … macOS 仍不拆分。**”(docs/Disaggregated/notes/OPEN-QUESTIONS.md:28) | Linux client 是明确目标,但无 aarch64 glibc 专项验收。 |
| Wayland | “**Linux / WSL / CI 永不开窗(EGL_PLATFORM=surfaceless);Wayland 不支持。**”(docs/Disaggregated/design/08-runtime-and-platform.md:32) | 当前声明不支持;本次也没有查到 Wayland frontend/present 实现。 |
| 历史 X11 设想 | “**Window 是 XID,nativeToken:u64 直接送。backend 自己 XOpenDisplay(getenv(\"DISPLAY\")) 并构造 VkXlibSurfaceCreateInfoKHR … Wayland 今天不支持 … 维持。**”(docs/Disaggregated/notes/recovered/wf1/final/part3.md:80) | 是同 X server 的 XID 设想,不能移植成 Android ANativeWindow token。 |
| T0 与 T1 | “**T0 = client 分配 AHardwareBuffer、server 导入;T1 = server 导出 opaque fd;T2 = 拒绝,client 保留 shadow 并推送。**”(MobileGL/MG_Remote/CONTRACT-P11.md:3) | 全部处于 persistent-map store 议题;不是 frames。T1 当前关闭见 :239。 |
| AHB 帧输出方案曾被讨论 | “**Child renders into AHardwareBuffers, sends them over a unix socket, app presents.**” 并明确需 “**a new buffer-ring/fence protocol, an Espryt default-framebuffer-to-AHB FBO, Magma AHB import**”。(docs/Disaggregated/notes/p12/map-apk-process.md:87–90;另见 docs/Disaggregated/notes/p12/map-docs-contract.md:103、docs/Disaggregated/notes/p12/map-server-surface.md:113) | 这是 P12 调研中的备选方案,确实预见了帧 AHB 交付;并未落地为当前 present 路径。 |
| P12 为何没有交付该方案 | “**in-process server … renders straight into its SurfaceView; sessions run sequentially in that process. (Not fork + AHB.)**”(docs/Disaggregated/notes/p12/BRIEF-onscreen.md:14–15) | 最终范围明确选服务器进程内 SurfaceView,放弃这一阶段的 fork+AHB 帧桥。 |
| dma-buf 历史设想 | “**T0 — server 导入 client 分配:client 分配 AHardwareBuffer/dma-buf … 理想但可用性未知。**”(docs/Disaggregated/notes/recovered/wf1/final/part2.md:172) | 旧设计;落地是 AHB BLOB T0。 |
| Export 的实际局限 | “**T1-dma-buf … UNSUPPORTED**”两台设备;“**Tier decision for P11 (persistent maps across the process boundary)** … T0 … BLOB。”(docs/Disaggregated/notes/p0/p0-device-findings.md:21、:27) | 不要将 persistent VkBuffer opaque-fd 成功解释为线性 frame dma-buf 成功。 |
| Termux 消费者 | “**消费者:同一台手机上从另一个 app(Termux、adb shell)启动、没有 Java Context 的原生 GL 程序。**”(MobileGL/MG_Remote/CONTRACT-P11.md:104) | 已落地 helper/broker/fd: 路线的真实目标;历史验收用 retrace/shell/第二 APK,非真实 Termux glibc 图形桌面。 |
| 跨 app 限制 | “**SELinux 挡的是连接这一步。** … 一旦连上 … 现有代码就走 SharedSegments … **缺的是跨上下文的连接中介。**”(docs/Disaggregated/notes/p11/B0-CROSS-APP.md:7) | 之后 B1 已实现中介;root container 仍是不同待验证安全域。 |
| 多 session / context | “**DirectGLES g_Display / g_Surface / g_Context 仍是全局 … 多 context;server 窗口不转发输入**”,状态为“**等出现第二个会话 / 第二个 context 的需求**”。(docs/Disaggregated/notes/DEBTS.md:42) | 明确尚未进入通用桌面多上下文阶段。 |
| 唯一会话 / Busy | “**同时只服务一个会话,第二个连接 Refuse{Busy}**。”(docs/Disaggregated/design/08-runtime-and-platform.md:17) | 精确实施范围是每个 supervisor;B1 双 supervisor 例外见第 5 节。 |
| 跨机 client | “**client 是 trace 重放器,不是活的 Minecraft 实例**。”(docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:11) | 更不能扩大成已经验证任意 Linux desktop app。 |
本次对 docs、notes、contracts 检索未找到 “Anland” 的专项实施方案;也未找到“导出 framebuffer AHB/dma-buf 给 Wayland client”的已落地设计。P12 调研**确实提出过 AHB frame bridge**,列明 buffer-ring/fence、GLES FBO、Magma AHB import 等新增工作;最终 brief 明确选择进程内 SurfaceView,未选择 fork+AHB。P11 则另行落地 persistent-buffer T0。这三件事应分开阅读。(docs/Disaggregated/notes/p12/map-apk-process.md:87–94;docs/Disaggregated/notes/p12/BRIEF-onscreen.md:14–15;MobileGL/MG_Remote/CONTRACT-P11.md:227–257;MobileGL/MG_Remote/CONTRACT-P12.md:7、:16、:44)
### 8.1 对接 Anland 的可实施分解(全部 inferred)
这些是基于本次源码盘点的实现建议,尚未构建或原型验证:
| 工作包 | 应做的具体事 | 可复用基础 / 缺口依据 |
|---|---|---|
| A. glibc arm64 client 与加载器 | 增加 aarch64 glibc toolchain/CI 和与 Android arm64 server 的 wireFingerprint 验收;决定 libGL/libEGL 直接替代部署或 GLVND vendor;先锁定目标 API/profile,修正虚假成功/广告。 | CMakeLists.txt:503–545、:783–835;.github/workflows/test.yml:1269–1293;MobileGL/MG_State/EGLState/Core.cpp:615–689、:1336–1466 |
| B. Linux WSI | EGL Wayland native display/window + resize/current/swap;client 在 glibc 侧持 wl_surface / wl_buffer。X11 路线补 local drawable 的 buffer present;另列 Xwayland glamor 支持门。 | MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:64–75;MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:258–327、:956–965 |
| C. server image provider | 新增导出型 offscreen surface,而非重用 P12 fullscreen DisplayActivity 作为目标;GLES AHB-image/EGLImage/FBO 或 Vulkan external VkImage;初期只协商题设 Anland 接受的单平面格式/modifier。若图像导出不可行,可先显式 readback 到 wl_shm 建立正确性基线,不能称其零拷贝。 | MobileGL/MG_Remote/Server/ServerLoop.cpp:1600–1652;MobileGL/MG_Backend/DirectGLES/Managers.cpp:2799–2809;MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:14528–14547;现有 readback 入口/成本清单:docs/Disaggregated/design/07-memory-readback-verification.md:21–22 |
| D. transport 与同步 | 复用 UNIX/fd: aux,版本化 BufferCreate/FrameReady/BufferRelease;真实 acquire/release fence fd;buffer pool 与 fd 生命周期;compositor callback pacing。root-container 域做定向 fd/connect/mmap 验证。 | MobileGL/MG_Remote/Transport/FdPassing.cpp:108–149、:289–301;MobileGL/MG_Remote/Protocol/protocol.fbs:290–323;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809 |
| E. 多用户态 client / 多窗口 / context | N clients 先用 worker-per-client 隔离;M windows 要 per-surface image pool/事件/credit;context 状态、share groups、同步归属和并发 frontends 必须从全局状态拆出。 | MobileGL/MG_Remote/Server/ServerMain.cpp:1222–1260;MobileGL/GlobalObjects.cpp:23–24;MobileGL/MG_State/GLState/Core.h:701;MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:38–39 |
| F. 通用应用验收 | 最小现代 GL EGL/GLX 程序→多 context/thread/share/window→桌面 toolkit/browser/Electron/Qt/Gtk 等目标工作负载→Xwayland glamor;按实测决定 GLES、compatibility 和 GL 版本广告。验证 resize、遮挡、无 callback、断连、释放 fence 与内容正确性。 | 第 2、5、6、7 节的具体缺口;历史跨机验收仅 trace:docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:10–17、:39–44 |
**inferred 的优先级判断:**最先证明“单 glibc arm64 client、单窗口、现代 GL、Android server 渲染→Anland 接受图像和同步→release 后复用”这一闭环,再扩大到 N×M 与 legacy/GLES。现有代码最有价值的是完整 command/state split 和已工作的 vendor-driver backend;最关键的新边界是 **frame/image ownership**,它目前不在 Present 协议中。(MobileGL/MG_Remote/Client/EmitTables.cpp:1165–1181;MobileGL/MG_Remote/Server/PipeApplier.cpp:388–438;MobileGL/MG_Pipe/MGPipeTypes.h:1807–1809)
### 8.2 本次识别的文档/代码漂移
| 文档说法 | 当前应采用的结论 |
|---|---|
| CMD/STAGE owner=client | producer 语义仍可这样称呼;跨进程四段的 allocator 是 server。(docs/Disaggregated/design/06-transport.md:9–17;MobileGL/MG_Remote/Server/ServerSession.cpp:737;MobileGL/MG_Remote/Transport/ShmSegment.cpp:253–284) |
| ShmLink 独立 aux,可 TCP+shm | 这是第二波设计;当前 client 拒绝该组合,fd API 仍在 ITransport/SocketTransport。(docs/Disaggregated/design/06-transport.md:62、:67;MobileGL/MG_Remote/Client/ClientSession.cpp:928–936;MobileGL/MG_Remote/Transport/ITransport.h:98–108) |
| split 今天固定 T2 / T0 握手 Fatal | 已被 P11 B2 取代:默认尝试 T0、不可用退 T2,T1 关闭。(docs/Disaggregated/design/07-memory-readback-verification.md:13;MobileGL/MG_Remote/CONTRACT-P11.md:233–248) |
| D8 debt 将 MetalLayer 列为仍接受 | 映射函数有 MetalLayer,但 wire decoder 先明确拒绝它;X11/Win32/None 才是残留接受项。(docs/Disaggregated/notes/DEBTS.md:34;MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:216–236) |
| device-lost 时 swap 返回 EGL_CONTEXT_LOST | 未找到实现映射;当前 Bool/void Present 链不能据文档认定标准 EGL 错误正确。(docs/Disaggregated/design/08-runtime-and-platform.md:33;MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:207–228;MobileGL/MG_Backend/BackendObject.cpp:396–397) |
| present credit>1 从未测 | P12 已有 1/2/3 的跨机 trace 测量。(docs/Disaggregated/design/08-runtime-and-platform.md:8;docs/Disaggregated/notes/p12/CROSSHOST-ACCEPTANCE.md:23–34) |
| 一设备一个 server | 是 APK display/service 模式策略;B1 TCP service 同时有独立 UNIX broker supervisor,且进程全局不等于设备全局。(MobileGL/MG_Remote/CONTRACT-P12.md:46;android-plugin/app/src/main/java/top/mobilegl/plugin/MobileGLServerService.java:147–162) |
## 附录 A. 指定关键词逐命中用途索引(主仓库)
检索通过指定 WSL 入口进行;模式为大小写不敏感的 AHardwareBuffer / dma_buf / DMA_BUF / SCM_RIGHTS / sync_file / SYNC_FD / external_memory。先对主仓库 git tracked 内容做 git grep,再补查实际展开的 3rdparty 目录(附录 B)。主仓库结果 **586 个匹配行、134 个文件**,附录 B 另有 **2108 个匹配行、88 个文件**。同一行多个关键词只算一个匹配行。未把 build 产物、Git 元数据或二进制的任意字节串当产品代码;未执行被检索脚本。
下表列出每一个匹配行的位置;同文件同用途合并,连续数字表示区间内每一行均命中。引用方式为“文件路径 + 行号”。无主仓库 sync_file / SYNC_FD 命中;3rdparty 有标准 SYNC_FD 枚举,Asio 有 async_file 子串匹配,因此**不能说整个磁盘树完全没有这些字符串**。
| 文件(树根相对路径) | 所有命中行 | 用途 |
|---|---|---|
| CMakeLists.txt | 621 | 编译 P11 T0 store/hop 的产品源文件。 |
| CMakeLists.txt | 1138 | 将独立 extmem_probe 打包成 server 替身,测试 AHB 跨 app 路由。 |
| MobileGL/Config.h | 635 | T0 不可用时退 T2 的配置说明。 |
| MobileGL/MG_Backend/DirectGLES/Managers.cpp | 33, 1242, 1244, 2192, 2349, 2406, 2799, 2814, 2878, 3304, 3857, 4060 | P11 T0:导入 client AHB BLOB 为 GL buffer,persistent map / POST / GPU 完成后的引用释放;非 EGLImage 颜色目标。 |
| MobileGL/MG_Backend/DirectGLES/Managers.h | 1055, 1133, 1333 | P11 T0:导入 client AHB BLOB 为 GL buffer,persistent map / POST / GPU 完成后的引用释放;非 EGLImage 颜色目标。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.cpp | 266, 576, 631, 678, 683–684, 710, 742, 798, 920 | P11 T0:AHB BLOB→VkBuffer 的 external-memory import、映射、回读和延迟释放;非帧图像。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferManager.h | 145, 147, 265 | P11 T0:AHB BLOB→VkBuffer 的 external-memory import、映射、回读和延迟释放;非帧图像。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.cpp | 129 | P11 T0:AHB BLOB→VkBuffer 的 external-memory import、映射、回读和延迟释放;非帧图像。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VkBufferObject.h | 75–76, 80 | P11 T0:AHB BLOB→VkBuffer 的 external-memory import、映射、回读和延迟释放;非帧图像。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp | 15348, 15358, 15361–15362 | 仅为 P11 T0 buffer import 启用/记录 AHB external-memory device extensions。 |
| MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h | 171–172, 596 | 仅为 P11 T0 buffer import 启用/记录 AHB external-memory device extensions。 |
| MobileGL/MG_IntegrationTest/CMakeLists.txt | 2543, 2560, 4120, 4126 | T0 adoption 集成用例、主机没有 AHB 的回退断言和测试注册。 |
| MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp | 102 | AImageReader 测试窗口的 sampled-image/color-output usage;测试 harness,不向 Linux compositor 导出帧。 |
| MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp | 103, 328, 414, 704 | T0 adoption 集成用例、主机没有 AHB 的回退断言和测试注册。 |
| MobileGL/MG_Pipe/MGPipeTypes.h | 155 | P11 T0 capability / resource ImportExternal 边界声明;persistent buffer store。 |
| MobileGL/MG_Pipe/PipeApply.h | 131, 134, 1319 | P11 T0 capability / resource ImportExternal 边界声明;persistent buffer store。 |
| MobileGL/MG_Remote/CONTRACT-P11.md | 3, 227, 240, 257, 275, 311–312 | P11 persistent map tiers、T0 BLOB offer/hop/import/POST 与测试;不是 present。 |
| MobileGL/MG_Remote/CONTRACT-P11.md | 80 | PairBind 后 aux 是 SCM_RIGHTS 的配对纪律。 |
| MobileGL/MG_Remote/CONTRACT-P11.md | 116 | B1 外部 client 握手接收 7 fd 的契约。 |
| MobileGL/MG_Remote/CONTRACT-P6.md | 39, 644 | Rule G 描述符边界及 aux/未来 ShmLink delivery 设计;不是帧协议。 |
| MobileGL/MG_Remote/Client/ClientSession.cpp | 858, 1187 | Client 采用 server 创建并发送的匿名共享段 fd。 |
| MobileGL/MG_Remote/Client/ClientSession.cpp | 2734, 2746, 2832, 2845 | T0 AHB store 协商、导入回复和回退/成功日志。 |
| MobileGL/MG_Remote/Client/ClientSession.h | 153, 171 | 共享段 Attach/Adopt/SCM_RIGHTS 启动契约。 |
| MobileGL/MG_Remote/Client/PersistentMapTracker.h | 272 | T0 persistent GL buffer 的客户端 backing store、wire map_persistent 及 CPU shadow 语义。 |
| MobileGL/MG_Remote/Client/WireTables.cpp | 951 | T0 persistent GL buffer 的客户端 backing store、wire map_persistent 及 CPU shadow 语义。 |
| MobileGL/MG_Remote/Protocol/mg_protocol_base.h | 105 | 跨进程 fd 交付/句柄存活期的协议边界说明。 |
| MobileGL/MG_Remote/Protocol/protocol.fbs | 46, 155 | SegmentRef/PairBind:fd 在 aux 交付,不把 fd 数字当跨进程对象。 |
| MobileGL/MG_Remote/Protocol/protocol.fbs | 73 | Hello 的 persistent-map adoptTier 说明。 |
| MobileGL/MG_Remote/Server/InProcessServer.h | 38 | Server 连接配对与 UNIX aux/共享段启动;不是 per-present buffer 交付。 |
| MobileGL/MG_Remote/Server/PairAcceptor.h | 12 | Server 连接配对与 UNIX aux/共享段启动;不是 per-present buffer 交付。 |
| MobileGL/MG_Remote/Server/ServerMain.cpp | 357, 1327 | Server 连接配对与 UNIX aux/共享段启动;不是 per-present buffer 交付。 |
| MobileGL/MG_Remote/Server/ServerSession.cpp | 1101, 1127, 1143, 1172, 1182, 1192 | T0 grant / 平台判定 / POST / 收 hop AHB / import / 回退。 |
| MobileGL/MG_Remote/Server/ServerSession.h | 302, 318 | T0 grant / 平台判定 / POST / 收 hop AHB / import / 回退。 |
| MobileGL/MG_Remote/Transport/AdoptT0.cpp | 42, 46, 50–54, 56, 60–61, 63–64, 85–86, 88, 101–103, 110–111, 113, 116–118, 121, 129, 133, 138, 144, 155, 172 | T0 AHB BLOB 分配、lock、hop send/recv、引用释放、host fallback;tier 名称/测试钩子。 |
| MobileGL/MG_Remote/Transport/AdoptT0.h | 10, 13, 18, 56, 61, 75, 85, 95 | T0 AHB BLOB 分配、lock、hop send/recv、引用释放、host fallback;tier 名称/测试钩子。 |
| MobileGL/MG_Remote/Transport/AdoptTier.h | 12 | T0 AHB BLOB 分配、lock、hop send/recv、引用释放、host fallback;tier 名称/测试钩子。 |
| MobileGL/MG_Remote/Transport/FdPassing.cpp | 147, 259 | 通用 AF_UNIX SCM_RIGHTS 单 fd sendmsg/recvmsg 与平台可用性。 |
| MobileGL/MG_Remote/Transport/FdPassing.h | 9, 12, 40 | 通用 AF_UNIX SCM_RIGHTS 单 fd sendmsg/recvmsg 与平台可用性。 |
| MobileGL/MG_Remote/Transport/ITransport.h | 89 | ITransport::ShareFd 契约:通过 aux 交 fd,调用者保留原 fd。 |
| MobileGL/MG_Remote/Transport/InProcessTransport.cpp | 200 | 同进程 transport 复制 fd,模拟 SCM_RIGHTS 所有权。 |
| MobileGL/MG_Remote/Transport/SessionRings.h | 129, 153, 165 | 匿名共享段创建、Attach/Adopt、映射/大小校验和描述符交付职责。 |
| MobileGL/MG_Remote/Transport/ShmSegment.cpp | 349, 423 | 匿名共享段创建、Attach/Adopt、映射/大小校验和描述符交付职责。 |
| MobileGL/MG_Remote/Transport/ShmSegment.h | 22, 55 | 匿名共享段创建、Attach/Adopt、映射/大小校验和描述符交付职责。 |
| MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp | 84 | 匿名共享段创建、Attach/Adopt、映射/大小校验和描述符交付职责。 |
| MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp | 89 | Windows 没有 SCM_RIGHTS;共享 section 按名称访问的说明。 |
| MobileGL/MG_Remote/Transport/SocketTransport.cpp | 650, 892 | control/aux socket 组织与 POSIX fd passing,Windows 不支持的路径。 |
| MobileGL/MG_Remote/Transport/SocketTransport.h | 34, 63, 77, 131 | control/aux socket 组织与 POSIX fd passing,Windows 不支持的路径。 |
| MobileGL/MG_Remote/Wire/PipeWireCodec.cpp | 1830 | T0 persistent GL buffer 的客户端 backing store、wire map_persistent 及 CPU shadow 语义。 |
| MobileGL/MG_Remote/Wire/PipeWireCodec.h | 509 | T0 persistent GL buffer 的客户端 backing store、wire map_persistent 及 CPU shadow 语义。 |
| MobileGL/MG_State/EGLState/Core.cpp | 180 | Android EGL config NativeVisualId 使用 RGBA8888 AHB format 常量;不是 AHB 分配/导出。 |
| MobileGL/MG_State/GLState/BufferState/BufferObject.cpp | 594 | T0 persistent GL buffer 的客户端 backing store、wire map_persistent 及 CPU shadow 语义。 |
| MobileGL/MG_Test/Wire/AdoptInboxTest.cpp | 11 | T0 offer/order/回退测试,主机用测试钩子;不证明 host 有 AHB allocator。 |
| MobileGL/MG_Test/Wire/CMakeLists.txt | 4, 26, 40 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| MobileGL/MG_Test/Wire/FdPassingTest.cpp | 9 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| MobileGL/MG_Test/Wire/InProcessTransportTest.cpp | 191 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| MobileGL/MG_Test/Wire/RemoteClientAdoptT0.inc | 3, 246, 249, 292, 303 | T0 offer/order/回退测试,主机用测试钩子;不证明 host 有 AHB allocator。 |
| MobileGL/MG_Test/Wire/ServerSpawnTest.cpp | 33, 257, 939 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| MobileGL/MG_Test/Wire/SessionTest.cpp | 1124 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| MobileGL/MG_Test/Wire/SocketTransportTest.cpp | 470 | SCM_RIGHTS / 共享段 / socket transport / spawn 的单元测试及构建注册。 |
| docs/Disaggregated/design/06-transport.md | 17 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/design/07-memory-readback-verification.md | 11 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p0/BRIEF-DOCS.md | 16 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p0/README.md | 37 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p0/p0-device-findings.md | 21, 23–24, 27 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p11/B0-CROSS-APP.md | 7, 43 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p11/INTEGRATOR-DECISIONS-P11.md | 23 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p11/PLAN-P11.md | 33, 37 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p12/device/2-onscreen-magma/check.log | 76, 99 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p12/device/trial-a4a940db/2-onscreen-magma/check.log | 64, 87 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p12/map-apk-process.md | 87–88 | P12 调研备选方案:child 渲染到 AHB,经 UNIX/SCM_RIGHTS 交 APK presenter;最终未选(BRIEF-onscreen.md:14–15)。 |
| docs/Disaggregated/notes/p12/map-docs-contract.md | 103 | P12 调研备选方案:child 渲染到 AHB,经 UNIX/SCM_RIGHTS 交 APK presenter;最终未选(BRIEF-onscreen.md:14–15)。 |
| docs/Disaggregated/notes/p12/map-server-surface.md | 113 | P12 调研备选方案:child 渲染到 AHB,经 UNIX/SCM_RIGHTS 交 APK presenter;最终未选(BRIEF-onscreen.md:14–15)。 |
| docs/Disaggregated/notes/p5/p5-results/s1-v1.md | 126, 358 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p5/scout-backend-install-and-thread.md | 350 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p5/scout-transport.md | 89, 100, 261, 289, 321 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p5c/p5c-audit-v1.md | 99 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p6/P6-CONTRACT-DRAFT.md | 33, 58 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md | 30, 137, 139, 143, 186, 399 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p6/P6-ENDSTATE-REVIEW.md | 138 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/p6/P6-SPAWN-PLAN.md | 17 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p6/README.md | 7, 16, 46 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p6/a6-audit-rows.md | 28 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p65/README.md | 17 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/p7/device-window-1/E0-attribution/E0a-inproc.log | 94, 158, 359, 416, 480, 1550, 1607, 1671, 1809, 1986, 2043, 2107, 2248, 2743, 2800, 2864, 2960, 3024, 3178, 3235, 3299, 3397, 3461, 3613, 3670, 3734, 3830, 3894, 4145, 4209, 4336 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E0-attribution/E0a-monolith.log | 175, 232, 296, 1914, 1971, 2035 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E1-lockstep/E1.log | 94, 158, 330, 387, 451, 592, 766, 823, 887, 983, 1047, 1201, 1258, 1322, 1419, 1483, 1693, 1757, 1902 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E7-coldcache/E7-cold-ra1-1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E7-coldcache/E7-cold-ra1-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E7-coldcache/E7-cold-ra1-3.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/E7-coldcache/E8-cold-fif1-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/F1-verify/F1-ra0.log | 130, 194, 367, 431 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/F1-verify/F1-ra1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/audit-cold-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/audit-warm-1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/bsl-mem-inproc.log | 68, 132, 263 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/bsl-mem-spawn.log | 67, 131 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/bsl-spawn.log | 67, 131, 499, 563 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump-inproc-1.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump-inproc-2.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump-inproc-3.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump-inproc-4.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump2-inproc-1.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump2-inproc-2.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump2-inproc-3.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump2-spawn-1.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/dump2-spawn-2.log | 96, 160 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif1-cold-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif4-1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif4-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif4-3.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif8-1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif8-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/fif8-3.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/matrix-inproc.log | 94, 158, 359, 416, 480, 1978, 2035, 2099, 2362, 2426, 2571 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/openra-cold-2.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/openra-cold-3.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/plain-cold-5.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/plain-warm-1.log | 94, 158 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/spawn-1.log | 93, 157 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/spawn-3.log | 93, 157 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/W4-verify/stats-lockstep-cold-1.log | 100 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/device-window-1/X1-verify/X1-dv-inproc.log | 41, 98, 162 | 历史运行日志中的 vendor Vulkan extension 枚举;不是调用点,也不是 frame export。 |
| docs/Disaggregated/notes/p7/ph-f.md | 104 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf1/final/part1.md | 135 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf1/final/part2.md | 18 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf1/final/part2.md | 171–172 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/recovered/wf1/final/part3.md | 16 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf1/final/part4.md | 13, 20, 50, 176–177, 191 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf1/final/part4.md | 178 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/recovered/wf2/final/part3.md | 139 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf2/final/part4.md | 19, 256 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf2/final/part4.md | 233 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| docs/Disaggregated/notes/recovered/wf2/final/synth-part3.md | 89 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf2/final/synth-part4.md | 16, 182 | 文档/历史审查:共享段、SCM_RIGHTS、配对、线程或 transport 平台边界;不新增实现。 |
| docs/Disaggregated/notes/recovered/wf2/final/synth-part4.md | 161 | 文档/历史设计或探针结果:外部内存 tiers、AHB BLOB persistent-map 或同内核 transport 可行性;不能当成当前 frame export。 |
| include/EGL/eglext.h | 517–518, 520 | Khronos EGL_ANDROID_get_native_client_buffer 的 AHB 类型/入口声明。 |
| include/EGL/eglext.h | 784–786, 788–796, 808, 810–822, 829 | Khronos dma-buf import/modifiers token 和 guard 声明;非实现。 |
| include/EGL/eglext.h | 1103–1104, 1111 | Khronos EGL_MESA_image_dma_buf_export guard 声明;非实现。 |
| scripts/ci/fatal_census.py | 206 | CI fatal/refusal census 对 T0 不可用→T2 的说明。 |
| tools/cts/platform/tcuMobileGLPlatform.cpp | 129–130 | AImageReader 测试窗口的 sampled-image/color-output usage;测试 harness,不向 Linux compositor 导出帧。 |
| tools/cts/probe/mgprobe.c | 158–159 | AImageReader 测试窗口的 sampled-image/color-output usage;测试 harness,不向 Linux compositor 导出帧。 |
| tools/piglit-android/README.md | 94 | 构建 piglit 时明确关闭 DMA_BUF tests 与 GBM。 |
| tools/piglit-android/patches/waffle-mobilegl-android.patch | 277–278 | AImageReader 测试窗口的 sampled-image/color-output usage;测试 harness,不向 Linux compositor 导出帧。 |
| tools/spikes/extmem_probe/CMakeLists.txt | 28 | 独立 probe 的 host 构建关闭 Android-only T0;保留其它 tier 探测。 |
| tools/spikes/extmem_probe/README.md | 23, 33, 50, 52–53, 55, 59–60, 157, 197–198 | 独立探针的 T0/T1/T3、BLOB 跨进程与真实 route 说明;不接 renderer Present。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 8, 16, 18, 20 | 独立探针 T1 fd export / T0 AHB import / T3 host import 的范围说明。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 407, 428, 469 | 独立探针自己的 SCM_RIGHTS sendmsg/recvmsg。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 580–581, 609, 627–630, 646–649, 651, 916, 918, 920, 925, 1477, 1491, 1628–1630, 1653–1657, 1673–1674, 1676, 1678–1679, 1695 | 探针 extension 枚举、external buffer capabilities/配置/类型和报告;没有 renderer 帧交付。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 1718, 1721–1722, 1771, 1773, 1904, 1983–1984, 1991, 2012, 2015, 2116, 2182–2183, 2188 | T1:host-visible VkBuffer 外部内存导出/接收/导入,opaque fd 或 DMA_BUF,及 GL memory-object import 对照。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 2356, 2370, 2374–2378, 2384, 2388–2389, 2395, 2404, 2407–2408, 2414, 2420, 2428, 2432, 2479–2480, 2487–2488, 2501, 2504, 2507, 2513, 2522, 2535–2536, 2778 | T0:AHB BLOB client allocation、CPU lock、socket handoff、Vulkan/GL buffer import 与字节验证。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 2784, 2796, 2808, 2812–2816, 2822, 2826–2827, 2830–2831, 2837, 2842, 2846, 2853, 2884, 2886, 2891, 2943, 2972–2973, 2981, 2989, 2999, 3005–3006 | T0S:保持 AHB CPU lock 的持续 CPU/GPU 读写一致性 probe。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 3219, 3232–3233, 3360, 3364, 3372, 3381–3383, 3394, 3399, 3451, 3455–3459, 3463, 3467–3468, 3471, 3488, 3492, 3515, 3569, 3576 | B2 route probe:经真实 app/socket direct 或 hop 交 AHB BLOB;host SKIP 分支。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 3590, 3632, 3638–3639, 3664, 3793, 3925–3926 | T3:VK_EXT_external_memory_host 导入 memfd/host mapping 的测试。 |
| tools/spikes/extmem_probe/extmem_probe.cpp | 4199–4200, 4204, 4206, 4208 | 探针 main 调度 T1/T0/T0S/T3,各为 buffer 内存测试。 |
## 附录 B. 已展开第三方目录的全部匹配
这些文件随依赖源码在树内,但不是 MobileGL 已接通的帧协议。其中 DiligentCore 的 add_subdirectory 在主 CMake 中仍注释掉(CMakeLists.txt:277),VMA/Vulkan headers/utilities 则进入依赖(CMakeLists.txt:280–282)。任何上游函数声明、loader、sample 或 Win32 export 都没有改变第 3 节的产品 Present 调用链。
| 文件(树根相对路径) | 所有命中行 | 用途 |
|---|---|---|
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan.cppm | 525, 1233, 1237, 1242, 1304, 1309, 1315, 1320, 1504, 1522, 1690, 2246, 2271, 2441, 2619, 2738, 3554, 3557, 3562, 3585, 3592, 3724, 3868, 4293, 4327, 4480, 4683, 4847, 6534, 6538, 6545, 6589, 6600, 6801, 7038, 7693, 7746, 7982, 8311, 8583 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan.hpp | 3087, 3104, 3217, 3227, 3244, 3715, 3718, 3726, 4268, 5205, 5301, 6036, 6246, 7196, 7199, 7507–7511, 7513–7517, 7520–7524, 7589–7591, 7593–7595, 7598–7600, 7603–7605, 7791–7793, 7808–7810, 7977–7979, 8534–8536, 8559–8561, 8729–8731, 8904–8906, 9022–9024, 11655, 11675, 11755, 11775, 12124, 12706, 15066, 15163, 16070, 17299, 18135, 18780, 18784, 18817, 18821, 18829, 18967, 19073, 19338, 19377, 19544, 19596, 20088, 20093, 20152, 20159, 20165, 20325, 20472, 20843, 20876, 21084, 21157, 21469, 21491, 21496, 21599, 21739, 22053, 22086, 22276, 22341 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_android.h | 46–50, 80, 108–109, 114, 120 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_core.h | 311–312, 565, 730, 973, 1052, 1165, 1302–1303, 5241–5265, 5270–5276, 5284, 5288, 5321, 5328, 10023–10026, 10058–10061, 10071–10074, 13544–13547, 13550–13554, 13559–13562, 13587–13590, 14152–14155, 15239–15242, 18172–18173, 18175–18176, 19325–19328 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_enums.hpp | 450–453, 887, 1059, 1312, 1389, 1509, 4239–4246, 4248, 4250–4251, 4253, 4255, 4257, 4260–4262, 4302–4304, 4329, 4419, 6184, 6190–6193, 6214–6216 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_extension_inspection.hpp | 55–56, 58, 114, 116, 127, 129, 131, 170, 174, 219, 356, 362, 402, 446, 476, 512, 519, 696, 699, 702, 705, 710, 743, 749, 752, 756, 759, 763, 766, 773, 990, 993, 998, 1008, 1013, 1170, 1173, 1929, 1932–1933, 1945, 1950, 1974, 1977, 2199, 2202, 2263, 2432, 2437, 2563, 2566, 2663–2664, 2778, 2780, 2782, 2784, 2787, 2789, 2907, 2911, 3269–3270, 3272, 3302, 3304, 3309, 3311, 3313, 3335, 3338, 3362, 3432, 3434, 3455, 3477, 3492, 3523, 3528, 3576 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_fuchsia.h | 45–48 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_funcs.hpp | 7522, 13547, 13590, 13610, 13632, 14389, 14415, 14428, 14449, 14486, 14502, 14522, 14556, 17337, 17343, 17357, 17362, 17377, 17382, 17401, 17413, 17419, 17422, 20728, 20757, 26007, 26031, 26072, 26466, 26489, 30367, 30390, 30409, 31687, 31708, 31747 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_handles.hpp | 756, 759, 764, 787, 794, 926, 1070, 1495, 1529, 1682, 1885, 2049, 13578, 13625, 13656, 14084, 14090, 14098, 14103, 14110, 14116, 15099, 16209, 16405, 17308, 17628, 18716, 18912 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_metal.h | 192–195 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_raii.hpp | 190, 233, 507, 529, 983, 1005, 1010, 1113, 1253, 1567, 1600, 1790, 1855, 2147, 2162, 2170, 2258, 2363, 2575, 2614, 2767, 2815, 3694, 3770, 4606, 4617, 4735, 4740, 4746, 4750, 4985, 5252, 5289, 5499, 5577, 9559, 17061, 19577, 19590, 19608, 19613, 19993, 20001, 20012, 20018, 20034, 20048, 20053, 20067, 21222, 21227, 21230, 21244, 21247, 21261, 21265, 21267, 22737, 22745, 25028, 25035, 25051, 25213, 25219, 26994, 27002, 27019, 27488, 27494, 27511 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_screen.h | 52–55 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_static_assertions.hpp | 2897, 2905, 2920, 2979, 3006, 3497, 4214, 6021, 6170, 6826, 7729, 8586 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_structs.hpp | 56639, 56668, 56699, 56728 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_to_string.hpp | 2803, 8124 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/include/vulkan/vulkan_win32.h | 51–54, 216–219 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/registry/validusage.json | 2596, 7211, 7216, 7310, 7315, 8044, 8049, 8373, 8378, 8735, 20628, 20683, 20703, 20708, 20743, 20748, 20753, 20758, 20763, 20768, 20773, 20783, 20788, 20793, 20798, 20803, 20808, 20813, 20833, 20873, 20878, 20883, 20937, 20942, 20947, 20952, 20962, 20967, 21179, 21213, 21445, 21503, 21537, 21580, 21590, 21595, 21619, 21629, 21634, 21682, 21692, 21711, 21720, 21749, 21759, 21867, 21891, 21953, 22345, 22403, 22437, 22690, 22883, 23299, 23990, 23995, 24020, 24025, 24446, 24460, 24705, 24798, 27017, 27022, 27185, 27190, 27419, 27577, 27744, 27912, 103968, 104608, 105427, 106451 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/Vulkan-Headers/registry/vk.xml | 236, 2441, 2485, 2937, 2943, 4275, 4559, 6973, 9052, 9612, 11249–11252, 11255–11257, 11361–11367, 11370–11372, 11380, 11393, 14840, 14847, 17627, 17646–17648, 19478, 19480–19481, 19490, 19492–19494, 19500, 19502–19503, 19511, 19731, 19733–19734, 19743–19749, 19752–19754, 19764, 19766–19769, 19778, 19780–19781, 19794, 19796–19797, 19808, 19830, 20274, 20464, 20466–20468, 20471, 20524, 20526–20528, 20543, 21488, 21490–21491, 21494–21496, 24101, 24103–24104, 24108, 24129, 24213, 24215–24216, 24218, 24220, 24278, 24280–24281, 24286–24288, 25124, 25126–25128, 25432, 26388, 26390–26392, 26397, 27139, 27232, 27234–27235, 27239–27241 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/DiligentCore/ThirdParty/glew/auto/doc/log.html | 40 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/auto/extensions/gl/EGL_EXT_image_dma_buf_import | 1, 3, 5, 7–15 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/auto/extensions/gl/EGL_EXT_image_dma_buf_import_modifiers | 1, 3, 5–15 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/auto/extensions/gl/EGL_MESA_image_dma_buf_export | 1, 3 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/doc/log.html | 138 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/include/GL/eglew.h | 798, 800–801, 803, 805–813, 826, 828, 830, 832–833, 835–845, 853, 855, 1801, 1803–1804, 1812, 1814, 2506–2507, 2568 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/src/glew.c | 17567–17568, 17629, 17861, 17863, 17873, 18177, 18179, 18189, 18476–18482, 18684–18687, 27867–27868, 27870, 27874–27875, 27877, 28313–28314, 28316 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/glew/src/glewinfo.c | 10644, 10646, 10648, 10651, 10653, 10655, 10657, 10663, 11276, 11278, 11280, 11286, 15990–15995, 16176–16178 | 上游 GLEW EGL extension 元数据、入口加载和 glewinfo 枚举;不实现 image 导入/导出。 |
| 3rdparty/DiligentCore/ThirdParty/volk/volk.c | 329, 331, 416, 418, 677, 680, 749, 751–752, 755, 863, 866, 983, 986–987, 990, 1187, 1189–1190, 1192, 1261, 1263, 1621, 1624, 1693, 1695–1696, 1699, 1807, 1810, 1927, 1930–1931, 1934, 2131, 2133–2134, 2136, 2205, 2207, 2601, 2604, 2710, 2712–2713, 2716, 2835, 2838, 2983, 2985–2986, 2989–2990, 2993, 3264, 3266–3267, 3269–3270, 3272, 3342, 3344 | 上游 volk Vulkan entrypoint dispatch loader;有函数指针不代表 MobileGL 调用了它。 |
| 3rdparty/DiligentCore/ThirdParty/volk/volk.h | 477, 482, 579, 583–584, 589, 743, 748, 909, 914–915, 920, 1201, 1205–1206, 1210, 1307, 1311, 1753, 1756, 1862, 1864–1865, 1868, 1987, 1990, 2135, 2137–2138, 2141–2142, 2145, 2416, 2418–2419, 2421–2422, 2424, 2494, 2496 | 上游 volk Vulkan entrypoint dispatch loader;有函数指针不代表 MobileGL 调用了它。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan.cppm | 434, 438, 467, 471, 476, 601, 708, 955, 982, 1057, 1184, 1247 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan.hpp | 4111, 4128, 4241, 4251, 4270, 4751, 4754, 4762, 5385, 6397, 6493, 6818, 7462, 7728, 8839, 8842, 9158–9162, 9164–9168, 9171–9175, 9240–9242, 9244–9246, 9249–9251, 9254–9256, 9442–9444, 9459–9461, 9636–9638, 10203–10205, 10228–10230, 10407–10409, 10412–10414, 10629–10631, 10785–10787, 13505, 13525, 13605, 13625, 13992, 14702, 17015, 17112, 18058, 18132, 19895, 21009, 22090, 22094, 22127, 22131, 22139, 22277, 22395, 22666, 22705, 22783, 22913, 22980, 23495, 23500, 23559, 23566, 23572, 23732, 23892, 24273, 24306, 24403, 24565, 24652, 24988, 25010, 25015, 25118, 25269, 25593, 25626, 25714, 25859, 25933 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_android.h | 48–52, 82, 110–111, 117, 125 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_core.h | 292–293, 563, 744, 986, 1073, 1099, 1258, 1450–1451, 5481–5506, 5511–5517, 5525, 5529, 5562, 5569, 10409–10412, 10446–10449, 10459–10462, 14715–14718, 14721–14725, 14730–14733, 14760–14763, 15355–15358, 16916–16919, 20010–20011, 20013–20014, 21289–21292 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_enums.hpp | 421–424, 893, 1081, 1333, 1420, 1446, 1619, 4437–4451, 4453, 4455–4456, 4458, 4460, 4462, 4465, 4468–4470, 4513–4518, 4546–4547, 4645–4646, 6697, 6703–6706, 6727–6729 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_extension_inspection.hpp | 58–59, 61, 117, 119, 130, 132, 134, 173, 177, 224, 363, 369, 412, 414, 469, 507, 557, 564, 756, 759, 763, 766, 771, 807, 813, 816, 821, 824, 829, 832, 840, 1067, 1070, 1075, 1086, 1091, 1268, 1271, 2063, 2066–2067, 2079, 2084, 2109, 2112, 2367, 2372, 2381, 2384, 2447, 2724, 2729, 2943, 2946, 3164–3165, 3289, 3291, 3293, 3295, 3298, 3300, 3422, 3426, 3824–3825, 3827, 3857, 3859, 3864, 3866, 3868, 3890, 3893, 3917, 3987, 3989, 4012, 4014, 4041, 4059, 4097, 4102, 4158 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_fuchsia.h | 47–50 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_funcs.hpp | 6628, 12530, 12573, 12593, 12613, 13345, 13370, 13383, 13403, 13438, 13454, 13473, 13504, 16197, 16203, 16215, 16220, 16239, 16244, 16261, 16272, 16278, 16281, 19666, 19691, 24641, 24663, 24702, 25064, 25085, 26384, 26406, 26425, 26456, 30136, 30158, 30177, 31542, 31562, 31597 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_handles.hpp | 755, 758, 763, 786, 793, 925, 1102, 1523, 1557, 1715, 1724, 2027, 2244, 14312, 14361, 14393, 14853, 14860, 14869, 14878, 14886, 14893, 16022, 17210, 17422, 17790, 18888, 19245, 20392, 20616 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_metal.h | 196–199 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_ohos.h | 22–23, 25–26 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_raii.hpp | 179, 222, 526, 548, 1033, 1055, 1060, 1163, 1314, 1638, 1671, 1759, 1904, 1978, 2282, 2297, 2305, 2393, 2509, 2727, 2766, 2835, 2957, 3013, 3893, 3976, 4763, 4774, 4895, 4900, 4906, 4910, 5147, 5403, 5440, 5543, 5751, 5824, 9910, 17419, 19870, 19883, 19901, 19907, 20295, 20303, 20314, 20320, 20336, 20350, 20355, 20369, 21554, 21559, 21562, 21576, 21579, 21592, 21596, 21598, 23134, 23142, 25369, 25376, 25392, 25554, 25560, 26225, 26232, 26248, 26263, 27776, 27784, 27801, 28316, 28322, 28339 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_screen.h | 56–59 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_static_assertions.hpp | 2897, 2905, 2920, 2979, 3006, 3497, 4415, 6212, 6361, 7023, 7061, 8456, 9528 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_structs.hpp | 77061, 77096, 77102, 77130, 77157 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_to_string.hpp | 2907, 8814 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/include/vulkan/vulkan_win32.h | 55–58, 232–235 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/registry/validusage.json | 2846, 7751, 7756, 7850, 7855, 8529, 8534, 8883, 8888, 9163, 9168, 9603, 22402, 22462, 22482, 22487, 22522, 22527, 22532, 22537, 22542, 22547, 22552, 22562, 22567, 22572, 22577, 22582, 22587, 22592, 22612, 22652, 22657, 22662, 22716, 22721, 22726, 22731, 22741, 22746, 22833, 22982, 23016, 23248, 23306, 23340, 23383, 23393, 23398, 23422, 23432, 23437, 23485, 23495, 23514, 23523, 23552, 23562, 23670, 23694, 23756, 24148, 24206, 24240, 24503, 24691, 25132, 25939, 25944, 25969, 25974, 26425, 26556, 26801, 26894, 29264, 29269, 29513, 29518, 29752, 29920, 30097, 30270, 30575, 31144, 118352, 119055, 120081, 121223 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Headers/registry/vk.xml | 245, 2565, 2609, 3082, 3088, 4549, 4833, 7281, 9443, 10058, 10954, 12457–12460, 12463–12465, 12571–12577, 12580–12582, 12590, 12603, 16214, 16221, 19260, 19279–19281, 21272, 21274–21275, 21284, 21286–21288, 21294, 21296–21297, 21305, 21535, 21537–21538, 21547–21553, 21556–21558, 21568, 21570–21573, 21582, 21584–21585, 21598, 21600–21601, 21612, 21634, 22104, 22294, 22296–22298, 22301, 22354, 22356–22358, 22373, 23486, 23488–23489, 23492–23494, 26166, 26168–26169, 26173, 26194, 26278, 26280–26281, 26283, 26285, 26343, 26345–26346, 26351–26353, 27203, 27205–27206, 27220, 27225, 27227–27229, 27427, 27620, 28724, 28726–28728, 28733, 29676, 29678–29679, 29683–29685 | 上游 Vulkan C/C++ API 类型、常量、dispatch/RAII wrapper 或 registry/valid-usage 规则;含 AHB/external memory/SYNC_FD 声明,不是 MobileGL 接线。 |
| 3rdparty/Vulkan-Utility-Libraries/include/vulkan/utility/vk_safe_struct.hpp | 11766 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/include/vulkan/utility/vk_struct_helper.hpp | 106–107, 580, 701, 909, 998, 1023, 1124 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/include/vulkan/vk_enum_string_helper.h | 295–298, 837–838, 1175–1176, 1641–1642, 1805–1806, 1857–1858, 2169–2170, 7719–7754, 7778–7783, 7813–7814, 7923–7924, 9969–9976, 10000–10005, 11602, 11604, 12530, 12760, 13160, 13332, 13382, 13579 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/scripts/generators/safe_struct_generator.py | 53 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/src/vulkan/vk_safe_struct_core.cpp | 7708, 7756 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/src/vulkan/vk_safe_struct_ext.cpp | 5408, 14352 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/src/vulkan/vk_safe_struct_utils.cpp | 129, 132, 1064, 1292, 1707, 1915, 1963, 2157, 2508, 2511, 3443, 3671, 4086, 4294, 4342, 4536 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/Vulkan-Utility-Libraries/src/vulkan/vk_safe_struct_vendor.cpp | 799, 10627, 14544, 19724 | 上游 Vulkan enum→string、sType、safe-struct 拷贝和生成器支持;不创建或发送呈现 buffer。 |
| 3rdparty/VulkanMemoryAllocator/CHANGELOG.md | 27 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/Doxyfile | 2469–2470 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/README.md | 56 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/faq.html | 126 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/globals.html | 110 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/globals_eval.html | 103 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/group__group__alloc.html | 2500–2501, 2547–2552, 2561, 2565 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/group__group__init.html | 132, 359, 557, 569 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/other_api_interop.html | 84, 89–90, 97, 101, 153, 191, 195, 209, 212 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/quick_start.html | 162 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/search/all_14.js | 66 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/search/enumvalues_0.js | 29 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/vk__mem__alloc_8h.html | 220 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/docs/html/vk_khr_external_memory_win32.html | 8, 78, 81–82, 86–87, 89, 91, 96, 100, 147, 151, 177, 189, 193 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/include/vk_mem_alloc.h | 242–245, 247, 251–254, 256, 480, 486, 1062, 1136, 1147, 1711, 2169, 2195, 2199–2200, 2221–2226, 2234, 2249–2250, 2265, 6396, 6483, 6552, 6559, 6670, 6672, 10716, 10721, 10729, 10731, 11137, 11143, 11458, 11474, 12078, 12086, 13217, 13310, 13313, 13324, 13326, 13350, 13356, 13540, 13648, 13702, 13959, 13967, 15625–15626, 17112, 17118, 17124–17129, 17133, 17298, 17523, 18933, 18946, 18949, 18966, 18970, 19028, 19070, 19075, 19120, 19123 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/src/Tests.cpp | 42, 8490–8491, 8497, 8506, 8530, 8536, 8584–8585, 8591, 8600, 8624, 8632 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/VulkanMemoryAllocator/src/VulkanSample.cpp | 72, 1494, 1540, 1542, 1933–1934, 2095–2096 | 上游 VMA 通用 external-memory allocation/Win32 handle 导出支持、示例测试及文档;没有 MobileGL 帧 dma-buf/fence 协议。 |
| 3rdparty/apitrace/specs/dxgi.py | 732, 740 | DXGI DMA_BUFFER_BOUNDARY 枚举;与 Linux dma-buf 无关的子串匹配。 |
| 3rdparty/apitrace/specs/eglapi.py | 62, 302, 304–312, 318–329, 422, 477 | apitrace 的 EGL API/enum trace 描述及 Khronos header;不是 MobileGL EGL extension 实现。 |
| 3rdparty/apitrace/specs/eglenum.py | 275, 277–285, 340–350 | apitrace 的 EGL API/enum trace 描述及 Khronos header;不是 MobileGL EGL extension 实现。 |
| 3rdparty/apitrace/thirdparty/khronos/EGL/eglext.h | 517–518, 520, 770–772, 774–782, 794, 796–808, 815, 1089–1090, 1097 | apitrace 的 EGL API/enum trace 描述及 Khronos header;不是 MobileGL EGL extension 实现。 |
| 3rdparty/asio/asio.manifest | 6515 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/asio/src/examples/cpp11/Makefile.am | 32, 184 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/asio/src/examples/cpp11/files/.gitignore | 1 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/asio/src/examples/cpp11/files/async_file_copy.cpp | 2, 79 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/asio/src/examples/cpp11/local/fd_passing_stream_client.cpp | 71 | Asio 的独立 AF_UNIX SCM_RIGHTS 示例程序;非 MobileGL transport。 |
| 3rdparty/asio/asio/src/examples/cpp11/local/fd_passing_stream_server.cpp | 87 | Asio 的独立 AF_UNIX SCM_RIGHTS 示例程序;非 MobileGL transport。 |
| 3rdparty/asio/boost_asio.manifest | 6951 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/src/examples/cpp11/Makefile.am | 32, 184 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/src/examples/cpp11/files/.gitignore | 1 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/src/examples/cpp11/files/async_file_copy.cpp | 2, 79 | async_file_copy 示例/清单;sync_file 命中是 async_file 的子串,非 Linux GPU sync_file。 |
| 3rdparty/asio/src/examples/cpp11/local/fd_passing_stream_client.cpp | 71 | Asio 的独立 AF_UNIX SCM_RIGHTS 示例程序;非 MobileGL transport。 |
| 3rdparty/asio/src/examples/cpp11/local/fd_passing_stream_server.cpp | 87 | Asio 的独立 AF_UNIX SCM_RIGHTS 示例程序;非 MobileGL transport。 |
-178
View File
@@ -1,178 +0,0 @@
# Runbook:在这台 Windows 机器 + 平板上起 / 迭代 MobileGL + anland + KDE Plasma(2026-10-02)
> 操作手册,不是设计笔记。配套 Claude Code skill:`.claude/skills/anland-mobilegl-plasma/`(`SKILL.md` 是本文的精简版,`scripts/` 是本文引用的全部脚本,每个脚本开头注明了参数)。现状、提交与已知问题见 [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md),零拷贝的设计见 [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md)。下文路径与命令按 2026-10-02 设备上的实际状态核对过;标"未验证"的是没跑通过或只从旧记录推出来的。
**设备可能正被别的会话使用。** 凡是会重启 app / daemon / 容器会话、或替换库的操作,先跑只读的 `scripts/device/status.sh` 看一眼,确认没人在测;`ct.sh` 的通道名用自己的(别人在用 c、c2、c3、xd、xr、gbm)。
## 架构
- **平板**:Lenovo TB321FU(Y700,Adreno 750,Android 15,内核 6.1),**KernelSU** root(`/data/adb/ksu`、`ksud`;不是 Magisk),`su -c` 可用。USB 序列号 `HA27Q3LQ`,也常走无线调试 `ip:port`。
- **anland APK** `com.anland.consumer.mobilegl`(anland 分支 `legacy-mobilegl-unified` 的 debuggable 构建):UI 进程 + 私有 Service 进程 `:mobilegl`(`MobileGLWorker`)。后者加载 **server** `libMobileGL.so`(NDK 构建,MobileGL 分支 `feat/disaggregated`),从第一个 attach 上来的窗口起监听抽象 socket `@anland-mobilegl`。一个进程服务所有 client(KWin、plasmashell、ksplash、Chrome……),一个进程只服务一种后端。`MobileGLWorker` 是**前台服务**(通知"Linux 桌面正在运行",带"停止桌面"按钮),窗口隐藏、锁屏、从最近任务划掉之后 server 和会话都还在。
- **后端**:app 设置"渲染后端"(设置 > 连接 > MobileGL 桌面;存在 `/data/data/<包名>/files/mobilegl-backend`,默认 DirectGLES;启动参数 `--es mobilegl_backend X` 也会写它)。Espryt = DirectGLES,Magma = DirectVulkan。server 进程启动时读一次;app 把它发布成 `/data/local/tmp/anland-mobilegl/backend`,会话启动前拷进容器的 `/etc/mobilegl/backend`——所有 client 按这个文件报后端,不一致 server 拒绝("Hello.backendType disagrees with pinned backend")。如果有人设了 `debug.mobilegl.backend`,它在下次重启前仍然覆盖设置。
- **实验用 display daemon**:`/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock`(显示 / 输入中介),由 app 拉起(见下)。模块的 `service.sh` 开机只起原版那个(`/data/local/tmp/display_daemon.sock`),原版不要动。
- **容器** `arch-kde-mgl`:Droidspaces 6.4.5,Arch Linux ARM(aarch64 glibc),host 网络;Android 侧 `rootfs.img` 挂在 `/mnt/Droidspaces/arch-kde-mgl`。bind mount:`/data/local/tmp/anland-mobilegl` → `/run/anland-mobilegl`,`/data/local/tmp/display_daemon.sock` → `/run/display.sock`。
- **会话**:`desktop-session.service`(User=swung0x48,**已 enable**:容器一启动就进会话,和原版镜像一样)。drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`(anland 的 `producers/kde/Arch_v5/desktop-session-mobilegl.conf`,由 `sync-build-kwin.sh` 安装并 enable):先 `mobilegl-startup.sh wait`(等 `/proc/net/unix` 里 `@anland-mobilegl` 在监听、daemon socket 存在),再以 root 跑 `sync-backend`,`Restart=always`,ExecStart 是 `/opt/mobilegl/bin/mobilegl-startup.sh plasma`——它删掉粘滞的软件渲染键,整个会话设 `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0`(MobileGL 永不拒绝,glvnd 不会把 KWin / plasmashell 交给别的 GL 栈),它每次写 `plasma-kwin_wayland.service` 的用户 drop-in(经 `kwin_wayland_wrapper` 起 `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`,`ANLAND_MOBILEGL=1`、`MOBILEGL_IPC_SURFACE=server`),然后 `startplasma-wayland`。
- **KWin 6.7.4** 带 anland 的 `anland` 后端,经 server 直接画进 anland 的 Android Surface。窗口不在屏幕上(consumer 断开)时后端把输出关掉(DPMS Off + RenderLoop inhibit:不合成、不发 frame callback),窗口回来再打开。其余 GL 进程都是 `MOBILEGL_IPC_SURFACE=offscreen`,经 KWin 合成上屏。
- **client 库** `/opt/mobilegl/lib/libMobileGL.so`(glibc aarch64),是容器里**全系统**的 GL vendor(见下节):EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json`(排在发行版的 `50_*.json` 前面,MobileGL 拒绝时由它接手)、GLX vendor `/usr/lib/libGLX_mobilegl.so.0`,配置来自 `/etc/mobilegl/client.conf`(`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`)+ `/etc/mobilegl/backend`。进程不需要任何专门的环境变量。
- **零拷贝窗口**:client 的 `wl_egl_window` 帧是 server 分配的 AHardwareBuffer(shared image),以 dma-buf fd 经 `zwp_linux_dmabuf_v1` 交给 KWin;KWin 用 EGL 导入,server 按 inode 认出是自己发出的那块。KWin 的 DRM 设备来自 `GBM_BACKEND=mobilegl` → `/usr/lib/gbm/mobilegl_gbm.so`(节点只当身份,从不 ioctl)。没有 linux-dmabuf 时退回 wl_shm 回读。
- **X11**(Xwayland)走 MobileGL 的 GLX vendor,每帧回读 + `xcb_put_image`。Chrome 用 `mobilegl-startup.sh chrome`:ANGLE GLES 跑在 MobileGL EGL 上,GPU 在浏览器进程内。
## 首次准备(Windows 主机)
- Git Bash、`adb`(SDK platform-tools)、PATH 上有 CMake ≥ 3.22 与 Ninja(本机 CMake 3.27.6、Ninja 1.13.2);`rec.sh` 另需 Python 3 + Pillow。
- Android NDK **27.2.12479018**,默认 `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018`(`ANDROID_NDK` 可覆盖)。
- WSL 发行版 **archlinux**:clang、lld、llvm、cmake、ninja。**lld 的大版本必须等于 clang / llvm-libs**(曾经 lld 23 配 llvm 22,`ld.lld` 直接跑不起来;现在全是 22.1.6)。然后导出一次 sysroot:`bash scripts/xsysroot/refresh-sysroot.sh`(得到 WSL 里的 `~/sysroots/arch-kde-mgl` 与 `~/sysroots/aarch64-arch.cmake`,glibc 2.43、gcc 16;容器升级 glibc / gcc / vulkan / libdrm / gbm 后要重跑,再 `xbuild.sh --reconfigure`)。
- MobileGL 用独立 worktree(主 checkout 和它的构建目录是共用的,别动):`GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`,`git submodule update --init --recursive`,再从主 checkout 拷 `3rdparty/glslang/External/spirv-tools`(submodule 初始化后是空的,configure 会报找不到 SPIR-V tools)。
- anland worktree,分支 `legacy-mobilegl-unified`(KWin patch / 后端源码、启动脚本、`mobilegl-tools/`)。
- 设备侧已经存在、本 runbook **不负责从零搭**的:APK(按 anland `consumers/anland_v5/android_consumer/MOBILEGL.md`,gradle `:app:assemblePlainDebug`,`MOBILEGL_DIST` 指向 server 库,`-PmobileglApplicationId=com.anland.consumer.mobilegl`;换签名重装会换 UID,要重做 KernelSU 授权,见 `mobilegl-tools/ksu_grant.c`)、anland 的 KernelSU 模块(`anland-daemon`、`anland-awl`)、容器本身,以及容器里配置好的 KWin 构建目录 `/root/mobilegl-build/kwin-6.7.4/build` 和原版 KWin 源码 `/root/gbm-dev/kwin-a/src`。从零重建这些未验证。
- 装一次辅助脚本(容器要在跑):`bash scripts/push-tools.sh`——`device/*.sh` 到 `/data/local/tmp/anl`,`mgrun` 到容器 `/usr/local/bin`,`kwprep.sh` 到容器 `/root`。
主机脚本都在 Git Bash 里 `bash scripts/<x>.sh` 运行。直接敲 adb 时,带设备路径要加 `MSYS_NO_PATHCONV=1`(否则 `/data/...` 被改写成 Git 安装目录),root 命令整个包进一层引号:`adb shell 'su -c "..."'`。设备脚本也可以不推送直接跑:`MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`。
## 启动、闲置、停止——没有手动步骤
开机什么都不起(容器 `run_at_boot=0`)。**打开 app**(桌面图标,等同 `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`,不带参数;APK 的默认 socket 是实验 daemon 那个,见 `-PanlandDefaultSocket`)会调 `MobileGLDesktop.ensureStarted()`——唯一的触发点——启动前台服务,并以 root(`su`)跑 APK 里的 `assets/mobilegl-desktop.sh up`:daemon 不在就起(脚本先离开 app 的 cgroup,app 被杀时 daemon 和容器不受牵连)→ 发布后端 → 容器没跑就启动 → `systemctl start --no-block desktop-session`。窗口等 daemon socket 出现;Surface attach 后 server 开始监听;会话的 `wait` 放行,Plasma 起来。全都在跑时再打开只是重新挂上窗口。日志:`/data/local/tmp/anland-mobilegl/desktop.log`,logcat `AnlandMobileGL`("MobileGL desktop up done in N ms")。
窗口隐藏、熄屏、锁屏、从最近任务划掉:server 与会话都留着(前台服务);KWin 记 "viewer gone: output powered down" 然后闲置,回来时记 "viewer back"。**停止桌面**(通知按钮,或 `am startservice -n <包名>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`)跑 `mobilegl-desktop.sh down`(停会话、停容器,daemon 留着),关窗口,结束 `:mobilegl` 进程。
```sh
adb connect <ip>:<port> # 只有无线调试才需要
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # 重启后:点亮、解开锁屏、打开 app、打印各步耗时
bash scripts/desktop-verify.sh push cold reopen idle # 冷启动计时、划掉再打开、可见/隐藏/锁屏的 CPU 与 GPU
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` 没有 `status` 子命令,`droidspaces show` 列出在跑的容器。`device/run-plasma.sh` 是开发用的重启(会话 + logind 用户,再重开 app),重新构建之后需要——KWin 不会重连新的 server 进程。`device/desktop-reset.sh` 把一切停到"刚重启"的状态。以前的手动序列(setprop、起容器、`restart-daemon.sh`、`run-plasma.sh`)不再需要;`restart-daemon.sh` 只留作调试 daemon。
## 构建与部署循环
### server(Android,NDK)→ 替换 APK 里的库
```sh
bash scripts/build-android.sh # → <worktree>/build-android/libMobileGL.so(RelWithDebInfo,不 strip,约 320 MB,留着给 addr2line)
bash scripts/cyc.sh s1 # 删粘滞软件渲染键 → 推库 → 换进 APK → 重启会话 → 截图 $ANL_DIR/s1.png → 打印 fatal
```
`device/swap.sh` 直接 root 覆盖 `/data/app/.../lib/arm64/libMobileGL.so`,保留 owner / 权限 / SELinux context,第一次运行把原件存为 `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`;不重签 APK(重签要重装,UID 会变,授权要重做)。只换库不跑 cyc:推到 `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`,`su -c "sh /data/local/tmp/anl/swap.sh"`,之后照样要 `run-plasma.sh`——server 进程换了,KWin 不会重连。
### client + GBM 后端(容器 glibc),WSL 交叉编译
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64;全量约 150 秒,增量几秒
bash scripts/xdeploy.sh # 新 inode 改名就位安装 libMobileGL.so + mobilegl_gbm.so,刷新别名、GLX 链接、vendor JSON、client.conf 和 GLX/GBM 环境 drop-in
```
已经在跑的 client 继续映射旧 inode:重启那个 client,KWin / plasmashell 就 `run-plasma.sh`。server 与 client **同一个 commit** 构建(两边 stamp 都取 `git rev-parse HEAD`),否则日志里 WARN "compatible wire with different build"。容器内构建是备用路径(慢):`scripts/sync-src.sh` 同步改动的文件,再在容器里跑 anland 的 `mobilegl-tools/anland-build-client.sh`。
### KWin(anland 后端 / kwin.patch),在容器里编
```sh
bash scripts/kwin-stage.sh build # 去 CR 后暂存 patch + 后端 + 启动脚本,跑 kwprep.sh,后台起 sync-build-kwin.sh
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # 等到 "== ALL DONE =="
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
```
两处暂存是因为两个容器脚本读的地方不同:`kwprep.sh` 读容器 `/root/kwsync.tgz`,把 `kwin.patch` 重新打到原版 KWin 源码的副本上,只把内容变了的文件拷进活树(保持 ninja 增量);anland 的 `sync-build-kwin.sh`(`producers/kde/Arch_v5/`)读 `/run/anland-mobilegl/kwin-sync/`,只同步后端源码,`ninja -j4`,再以原子改名安装 `kwin_wayland`、`libkwin.so.6.7.4`、打过补丁的 screencast 插件和 `/opt/mobilegl/bin/mobilegl-startup.sh`,不重启合成器。仓库根的 `libdisplay_producer/` 两个脚本都不同步。`kwin-stage.sh` 是把这两步拼起来的,还没整体跑过。
### 单元测试(不用设备)
这个分支的 MSVC 构建是坏的,在 WSL 里编:
```sh
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` 编并跑全部单元测试。DirectVulkan 集成测试永远不要跑;两个 ctest 不要同时跑。
## 切换后端
在 app 里:设置 > 连接 > MobileGL 桌面 > 渲染后端,然后通知里"停止桌面",再打开 app。或者 `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`,做的是同一件事(用启动参数保存设置、STOP 服务、重新打开;顺手清掉残留的 `debug.mobilegl.backend`)。不用 setprop,也不用改容器里的文件:会话下次启动时把发布的后端拷进 `/etc/mobilegl/backend`。`cyc.sh` 不改后端。
## 全系统 vendor 与回退
`xdeploy.sh`(以及 anland 的 `anland-build-client.sh`)安装下面这些,容器里任何进程经原版 libglvnd / libgbm 就用上 MobileGL:
| 文件 | 作用 |
|---|---|
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor,先于系统的 `50_*.json` 被问 |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | 同一份 JSON,留给 `__EGL_VENDOR_LIBRARY_FILENAMES` 单进程强制 |
| `/etc/mobilegl/client.conf` | 所有 client 的 `KEY=value` 默认值(transport、data、endpoint),每次部署重写 |
| `/etc/mobilegl/backend` | 后端(`switch.sh` 写);比 client.conf 里的 `MOBILEGL_BACKEND_TYPE` 优先 |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`、`GBM_BACKEND=mobilegl`,给 systemd 用户管理器(所有 Plasma 服务及其子进程,包括终端) |
| `/etc/profile.d/mobilegl.sh` | 同样两项,给登录 shell(`bash -l`、`su -l`、ssh、`droidspaces run bash -l`) |
优先级:环境变量 > `/etc/mobilegl/backend`(只管后端)> client.conf > 内置默认;`MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` 可指向别的文件(设为空即关闭);server 进程不读这些文件。
**回退**:真正拉起之前 client 先探一次配置的 endpoint(一次非阻塞 connect,`MOBILEGL_IPC_PROBE_TIMEOUT_MS` 默认 250,0 关闭;被拒的抽象 socket 约 0.2 ms 就有结果)。没有 server 时:EGL vendor 返回 `EGL_NO_DISPLAY`、设备数 0(glvnd 换下一个 vendor),GLX vendor 的 `__glx_Main` 失败(libGLX 回退到 Xwayland 报的 vendor),`mobilegl_gbm` 的 `create_device` 失败(libgbm 回退到设备自己的后端)。客户端查询(`eglQueryString(EGL_NO_DISPLAY)`、`eglGetProcAddress`)从不拉起会话。对别的后端创建的 GBM 设备,MobileGL 同样让开。`LIBGL_ALWAYS_SOFTWARE=1` 不再让它让开(918d40dd):那只是 Mesa 的软件渲染器选择,不是关掉 MobileGL 入口的理由;只要有 server,带这个变量的进程(包括被 KWin 补丁设了它的 Xwayland)照样由 MobileGL 服务。不停真 server 的演示:
```sh
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor 变成另一个,约 0.1 s
```
## 跑测试 client 与验证
在容器里以 root 执行(`printf '...' | bash scripts/ct.sh <通道>`),`mgrun offscreen <命令>` 以 swung0x48 身份在当前会话里起一个 client(日志 `/tmp/mgl-run.client.log`,`MGLOG=/tmp/x.log` 可换):
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome:`bash scripts/chrome-launch.sh <tag> [参数]`(日志 `/tmp/mgl-chrome-<tag>.client.log`,截图 `ch-<tag>.png`)。探针:`scripts/probes/texfmt.c`(各纹理格式是否保住 alpha)、`dmahash.c`(对某进程的 dma-buf 取样哈希);放进容器 `/root` 编,例如 `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`,用 `mgrun offscreen` 跑(之前的确切编译参数未验证)。
视频:Chrome 走硬解(VaapiVideoDecoder)。容器的 VA-API 驱动(`msm_drm_drv_video.so`,V4L2 解码)自己分配 surface 并导出 NV12 dma-buf(单 fd,R8 + GR88 两层),Chrome 以 NV12/P010 EGLImage 绑到 `GL_TEXTURE_EXTERNAL_OES`。这种**外来** YUV dma-buf 平台导入不了,MobileGL 走明确标注的 CPU 拷贝兜底:server 映射它、每帧首次使用时拷进自己的 YUV AHB(`AHardwareBuffer_lockPlanes`),再在 GPU 上转换进纹理的 RGBA 层(Espryt:`GL_EXT_YUV_target` + 导入时的色彩 hint,`MOBILEGL_YUV_DRIVER_CONVERSION=1` 改用驱动的 external sampler;Magma:AHB external format 上的 `VkSamplerYcbcrConversion`)。server 自己分配的 NV12/P010(GBM)零拷贝。`MOBILEGL_CHROME_VIDEO_DECODE=software` 恢复 `--disable-accelerated-video-decode`。检查:容器里 `python3 scripts/probes/cdpmedia.py <port> <秒>`(CDP `Media.enable`,`kVideoDecoderName`),`scripts/probes/yuvprobe.c` 像素校验外来 NV12/P010 导入、逐帧刷新和 GBM NV12。KWin 自己的 NV12 路径(拆成 R8 + GR88 两个导入)不支持,KWin 不通告 YUV 格式。
结论成立要同时满足:
- client 日志是 `presented through linux-dmabuf shared images`,不是 `wl_shm`;出现 `could not be presented through a linux-dmabuf shared image` 说明退回了;
- client 在动的时候 `bash scripts/shot.sh g 2 1` 两张截图哈希不同——dma-buf 下分数"很快"可能是帧根本没上屏;
- `adb logcat -b crash -d` 与 `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` 干净——窗口冻住多半是 server 死了;
- 对 plasmashell 下结论前,粘滞软件渲染已关(见下),且 `/tmp/mgl-plasmashell.client.log` 是新写的。
参考(glmark2-es2-wayland 1280x720,零拷贝 vs wl_shm):Espryt 187 vs 49,Magma 158 vs 34。出乎意料的通过要复跑。
## 日志
| 内容 | 位置 |
|---|---|
| KWin 的 client 侧 | 容器 `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`、`/tmp/mgl-ksplash.client.log`(用户 drop-in `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`、`/tmp/mgl-chrome-<tag>.client.log`(+ `/tmp/chrome-<tag>-out.log`) |
| server | `adb logcat -s MobileGL`;文件 `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| native 崩溃 | `adb logcat -b crash -d`;容器里 `coredumpctl list` |
| 会话 / KWin stderr | 容器 `journalctl -u desktop-session --no-pager`、`journalctl --no-pager --since -2min \| grep kwin_wayland` |
| display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` 实际写 `/tmp/x.client.log`(server 转发的行进 `.server.log`),打开时截断,所以每个进程一个 base。client 构建的日志级别是 INFO。server 栈帧用 `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <偏移>` 符号化。
## 常见坑
- **pkill -f 误杀自己**:`ct.sh` / `droidspaces run bash -lc "<脚本>"` 的进程参数里带着整段脚本,`pkill -f chrome` 会先把执行者杀掉。用 `pkill -x`。
- **/tmp 里 root 的文件**:root 跑 client 留下的日志,之后 swung0x48 打不开(日志不出现)。先以 root `rm -f`。
- **Plasma 软件渲染是粘滞的**:server 一崩,Plasma 就把 `[QtQuickRendererSettings] SceneGraphBackend=software` 写进 `~swung0x48/.config/kdeglobals`,之后 plasmashell 全用 CPU 渲染,截图照样正常(托盘里有"正在使用的软件渲染器"图标)。`mobilegl-startup.sh plasma` 每次会话启动都会删掉它;一个已经崩过的会话里,下结论前先重启会话(`run-plasma.sh`)。
- **锁屏**:自动锁屏已关(`kscreenlockerrc` 的 `[Daemon] Autolock=false`);真锁上只能输密码——这种 logind 会话不支持 lock-session,`device/unlock.sh` 的 unlock-session 能否解开未验证。测 greeter 用 `/usr/lib/kscreenlocker_greet --testing` 再杀掉。
- **CRLF**:本机 `core.autocrlf=true`,anland 的后端源码和 `sync-build-kwin.sh` 在 Windows checkout 里是 CRLF。凡是要在 Linux / Android 上执行的先 `tr -d '\r'`(`ct.sh`、`push-tools.sh`、`kwin-stage.sh` 已处理);skill 目录有 `.gitattributes` 强制 LF。
- **display daemon**:由 app 拉起;会话的 `wait` 步骤保证 KWin 不会在它之前启动(连不上时 KWin 报 "failed to connect to display daemon at /run/anland-mobilegl/display.sock",约 3 秒退出)。从 app 的 `su` 拉起的东西必须先离开 app 的 cgroup(`echo $$ > /sys/fs/cgroup/cgroup.procs`),否则 force-stop 会一并杀掉。
- **电量 / 连接**:电量低或熄屏(默认 30 秒)可能让 SurfaceView 被回收——`svc power stayon true`;无线调试的端口每次配对都变——脚本里不要写死 `-s HA27Q3LQ`,多设备时设 `ANDROID_SERIAL`。
- **容器与 Android 路径**:容器的 `/tmp` 和平板的 `/data/local/tmp` 不是一个地方;文件经 `/mnt/Droidspaces/arch-kde-mgl/root`(= 容器 `/root`)或 `/data/local/tmp/anland-mobilegl`(= 容器 `/run/anland-mobilegl`)中转。
- **正在映射的 .so 不要原地覆盖**(容器里或运行中的 APK):拷成新 inode 再 `mv -f`;APK 换库前先 force-stop。
- **输入注入**:普通 `adb shell input` 被拦,要 `su -c input`;anland 抓了触摸屏,`input tap` 无效时用 `input mouse tap x y`。不要对容器进程用 `debuggerd -b`(信号 35 会直接杀掉 glibc 进程)。
- **linux-dmabuf**:client 必须 `create` + roundtrip,不能 `create_immed`(KWin 对失败的立即导入回协议错误,直接断开 client)。
- **跨会话 bug**:某个 client 起来或退出后出现噪点 / 别的窗口的残影,先怀疑多会话 server 里的进程级状态(handle 是每 client 各自的,FBO / VAO 是每 context 的)。复现用 `kscreenlocker_greet --testing`。
- `/etc/environment` 经 pam_env(以及 `/usr/lib/environment.d/99-environment.conf` 进用户管理器)带进基线会话的驱动变量,`mobilegl-startup.sh` 里把它们 unset 了,改脚本时保留。
- **共用日志路径**:Xwayland 继承 KWin 的 `MOBILEGL_LOG_FILE_PATH`,进程写第一行日志就会打开(截断)那个文件。MobileGL 在不服务的进程里不写日志,但额外起的 client 要给自己的 `MOBILEGL_LOG_FILE_PATH`。
- **残留的单进程覆盖**:unit drop-in 或用户管理器环境(`systemctl --user show-environment`)里遗留的 `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` 仍比系统文件优先;`run-plasma.sh`(terminate-user)会清掉管理器里的。
- handoff 里提到的 `kwin_wayland` 日志 shim 与 `/tmp/mgl-kwin-<pid>.log` 已过时:`/opt/mobilegl/kwin/bin/kwin_wayland` 现在就是真二进制,KWin 的 client 日志在 `/tmp/mobilegl-compositor.client.log`。
## 规矩
- 在 git worktree 里干活,不碰共用的主 checkout 和它的构建目录;worktree 要 `GIT_LFS_SKIP_SMUDGE=1`。
- 提交用 `git commit --no-verify`,一句话的短消息,**不加** `Co-Authored-By`。
- DirectVulkan 集成测试不跑(`ctest -E DirectVulkan`)。
- 源码和注释里不写外部项目名("Mesa"、"Zink" 等),只描述手法。
- 零拷贝路径只用厂商中立的设施:AHB、EGL_ANDROID_*、VK_ANDROID 的 AHB 导入、sync_file、dma-buf、libgbm ABI;不碰 kgsl / gralloc 私有布局 / 高通修饰符。
- 身份缓存用单调递增的生命期 id 作键,不用 GL 名字或指针。
@@ -1,6 +0,0 @@
# Review: B3 fix round 3 (pipe bd849b28 10220b56 3b8bf9ec c8fc4fb7) + B2 round 4 (e385ea96 4b96f55d) — integrator (Fable 5.1, main session)
**Verdict: land** (no findings that need a change).
B3 r3: `wire_declines_audit.py` — `#endif` crossings are counted; a same-indent log under an outstanding `#endif` is accepted only if every opener found by continuing upward is the whole-line `#if MOBILEGL_BUILD_DISAGGREGATED` (`PP_DISAGGREGATED` anchored `\s*$`, so `&& !defined(X)` is fail-closed — stated); `PP_STOP` matches `#else`/`#elif` at any indentation and returns False before the indent test; `#if*` with no outstanding `#endif` stays a plain skip. 16/16 fixtures; tree 53/53/0 (gate on 4b96f55d). `run_trace_case.cmake` message now carries `MOBILEGL_IPC_CONTROL` (message only). `retrace_pull_library_control.sh`: the `case` guard `*$'\n\n'*|*$'\n'|$'\n'*` refuses a blank middle line, a trailing and a leading newline; `stub_ctest.sh` is 100755 at HEAD. No product file touched.
B2 r4: the window leg primes the default framebuffer magenta / 0.625 (source 0.25/0.75, user FBO blue/0.5), probes expect exactly that, so reading the FBO or the source both fail (the package showed the wrong-surface red once); the gate is `!PeekSplitRuntime().transportResolved` after `Ready()`; `SplitLane.h` include dropped; note cites by symbol. Gate on 4b96f55d: unit 2436, magma 100/79/71, full 538, verify 1148/1082, the three `MsFlip.` entries Passed.
-22
View File
@@ -1,22 +0,0 @@
# B4 集成源码审查(2026-09-23)
## Verdict
**land**。审查针对 pipe 在 a4c1bc8b / b19297d5 后的源码;没有遗留正确性问题。
## 源码核对
- MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:13524-13593:WaitForFrameSerial 不再把首个匹配 fence 当成整个 frame serial 已完成。它复制 submit index、等待完整 fence 前缀、在记录被退休后从头重扫,并且只在 IsFrameSerialComplete 为真时返回成功;异常路径 drain graphics queue 后也重新检查完成地板。
- VulkanRenderer.cpp:13678-13694 与 Renderer/SubmitFencePrefix.h:16-38:聚合等待包含目标 submit 及其之前的所有在飞 fence,使用 waitAll=VK_TRUE;缺失目标记录或无效 fence 会拒绝等待,不会用更早的前缀冒充目标完成。只在整段 wait 成功后推进 submit 完成记录。
- VulkanRenderer.cpp:13602-13612:pending-work 判断同时包含主 command buffer 和 pre-pass command buffer 状态,避免把 pre-pass 录制漏出 pending 账本。
- VulkanRenderer.cpp:14272-14282、:14408-14422:在 FrameContext 等待并重置槽 fence、释放退休 command buffer 之前,先等待该槽覆盖的完整 submit 前缀;初次获取没有历史 submit,仍走原路径。
- VulkanRenderer.cpp:7064-7071、:7689-7695:wire draw 与 dispatch 路径都调用纹理垃圾回收;其内部 256 次调用节流保留,避免只在 frame boundary 清理导致长帧记录滞留。
- MobileGL/MG_Remote/Server/StagedTextureStore.h:234-252:extent 改变时清掉旧 bytes 与 coverage,再接收新 run;StagedTextureStoreTest.AdoptRunExtentMoveDoesNotInheritOldCoverage 覆盖旧坐标系内容不可沿用。
- MagmaAggregateWaitTest 通过生产 fence-prefix selector 对照两笔同 serial submit 与单 fence 反事实;单 fence 保留目标帧在飞,生产选择覆盖目标前缀并排除下一帧;另有缺失目标索引拒绝用例。
- MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp:177 的 ::Display* 是仅为 GCC -Wchanges-meaning 编译错误加的类型限定,未改变对象布局或执行逻辑。
## Luna 验收证据
B4 包树的 split 八车道均为 0 失败:unit 2439(2428 pass / 11 skip)、integration split 304(295 / 9)、spawn 220(211 / 9)、TCP 223(214 / 9)、Magma split 102(100 / 2)、spawn 81(79 / 2)、TCP 71(69 / 2)、full split 540(478 / 62)。Verify lanes:1152(865 / 287)与 1086(958 / 128),失败均为 0。G1 .text=0xa52203、符号增删 0/0;fatal census 79/0 未标记;ratchet 173;parity 通过;wire decline audit 53/53、0 未记录或未知;OpenRA inproc/spawn 均 SSIM 1.000000、0 mismatch、0 Fatal、0 unsound serial。
pipe 集成树合并后的完整门另由 Luna 续跑;这些包树结果不替代该集成门。
@@ -1,7 +0,0 @@
# Review: F fix round (pipe 0293aebb c69d8dfd 8c8fc319 9cea5140) — integrator (Fable 5.1, main session)
**Verdict: land** (two nits, recorded in CONTRACT-P7 §12 / ph-f.md; no code change required before push).
1. nit — `ServerMain.cpp` `RunSession`: between `session.Accept()` returning and `::close(sourceFd)` a DataBind the supervisor already forwarded is closed with the child's queue (the passed fd goes with it), so that one peer sees a bare close instead of `Refuse{Authentication}`. Window is microseconds; acceptable; record.
2. nit — `scripts/ci/protocol_revision_pin.py`: any byte change to `protocol.fbs` (comment/whitespace) forces a revision bump; strict by design, say so in the docstring.
Verified from the diffs: child closes `pair[1]` after Accept (supervisor's `SendFd` then fails ECONNREFUSED/ENOTCONN → TRANSPORT_CLOSED → named refusal on the extra connection; `MSG_NOSIGNAL` already set, `MSG_DONTWAIT` maps EAGAIN → TIMEOUT → same refusal); the refusal is written on the extra connection and its object is closed by the caller as before; listener closed only on the single-session (`ListenerSource`) path, `--serve` keeps the supervisor's listener; `DrainUnread` bounded 256×4 KiB; `MalformedHello` is a generated `RefuseCode` (already in the vocabulary, census unchanged 79/44/3); pin script LF-normalises, two-sided (stale row / missing row), `--write` refuses re-pin, CI step in flatc-check with nonzero exit; token compare width = max(64, configured length) with both operands zero-padded, the >64 length class leak stated; `MintNonce` failure now sends `Refuse{LinkTerms}` and still exits 67; PH-8 log line fires only when a peer asks for more than granted; no pull-build file touched (G1 holds by construction; gate on 9cea5140: unit 2436, tcp 223, spawn 220, magma 100/79/71, verify 1148/1082).
-15
View File
@@ -1,15 +0,0 @@
# review-f2(集成者审查,2026-09-23)
**Verdict: land.** `p7/f2-all@ec845daa`(29 提交 = 基底 4 + f2-event 7 + f2-latch 8 + f2-auth 8 + 合并收尾 2),`~/w7/p7-f2-driver`。
| 片 | 核对 | 结论 |
|---|---|---|
| 基底 PH-2/3/4/5(Luna 审查 land) | 只跑了 49 条定向测试;全车道下 PH-4 两个真缺陷(零尺寸 `glTexImage*` 被判 `ExtentCarrier` 腐坏、`TextureInternalFormat::R8 == 0` 被当「未声明」)+ PH-3 把 ID-49「DstSize≠tight 读 tight」改成拒绝 + PH-2 测试期望 | 由 f2-latch 片 A(`20b827c0`)与空层修复(`58835fa4`)一次修掉;**教训:F2 落地必须全车道 + 主机 CTS 子集** |
| PH-1 (3)(4) 闩 | `FatalFunnel.cpp` 首错胜出(mutex + release store)、未武装=`SessionFail` 原样;只有 spawn/TCP 会话子进程武装(`ServerMain` RunSession),inproc 保持 Fatal(同进程无 supervisor,理由在 `FatalFunnel.h`);DrainRing 入口 + 每记录后查闩、apply 线程离环、闩后控制帧不执行、`_exit(75)` | 正确;64 处闩调用 53 行有原始对端负控、8 处论证不可达、`ph_latch_sites.py --self-test` 丢行即红 |
| fuzz 臂 2 | 64 行表驱动 `PeerLatchTest`,每行独立 `--serve` supervisor、会话 A 伪造、会话 B 必须渲染;D11 五界全有行,PH-5 行已改为旧字节界放行、新界拒绝(RLIMIT_AS 红一次) | 正确 |
| PH-6 + fuzz 臂 3 | drop-with-latch、`MOBILEGL_IPC_EVENT_WAIT_MS`(默认 2000,测试用非默认值并回读)、Stop 结束等待、shm 服务端铃有死亡见证、闩后队列不再 drain | 正确 |
| PH-7 (5) + fuzz 臂 1 | supervisor 在 fork **之前**读 Hello 并先验令牌(未认证只回 `Refuse{Authentication}`,不泄漏指纹/stamp);预认证队列按地址共享 + 置换 + 退避;五种畸形控制帧 + `CtrlEnvelopeBufferHasIdentifier` | 正确;`MOBILEGL_IPC_PREAUTH_{MS,MAX}` / `AUTH_BACKOFF_{AFTER,MS}` 四旋钮已入文档 |
| 门 | G1 `.text` 0xa52203 / 符号 0/0;census 79;棘轮 173;parity 0;unit 2546、split 306、spawn 223、tcp 227、magma 104/83/73、full 542;verify 构建 1156/1090;OpenRA 两臂 1.0 | 全绿 |
**记 §12 的债**:PipeApplier / QueryServer 另有 29 处读对端字节的 `WireProtocolFatal` 仍是 Fatal(会话进程死、supervisor 照常服务下一个);生成门 `MGPipeWireProtocolFatal` 与 MG_Pipe D11 钩子按设计 Fatal(改闩会碰 pull 构建);被丢弃事件的记录仍回 `kStatusOk`,client 无 `DroppedEvents()` 消费者。
**协议变化**:`ControlSchemaRevision` 并入 respecify 尺寸载体修订 → wireFingerprint 变,设备 server 随下一个 APK 重部署。
-11
View File
@@ -1,11 +0,0 @@
# review-g5(集成者审查,2026-09-23):门 5 的三个 inproc × DirectVulkan CTS 缺陷
| 包 | 提交(只取这些) | Verdict | 核对 |
|---|---|---|---|
| g5-readback | `18d3f27d`、`60eabf7b`、`e521c72b`(跳过重复的 `01f95c7b`) | **land** | 客户端把超出一个回复槽的 ReadPixels 切成行带(单行超槽则切行内片),每带是一条普通 `read_pixels`、DstSize 取带的 tight 值,server 侧 PH-3 界不变;中性 pack 态保零拷贝、bounce 只一带、scatter 与整读同一函数;`Fatal{ReplyTooLarge}` 只剩「一个像素都放不下」。7 个场景含 2 MiB 整、2 MiB+1 行、17 MB > 整个 SEG_REPLY、pack 缝隙、PBO、深度/模板,red-once 72/86 红 |
| g5-imgwin | `e8869c2a`(跳过 `8cc63c0b`);F2 落地后按包注记把 view 窗口用例并入门控臂 | **land** | 图像单元指向纹理没有的 level/layer(GL 4.6 §8.26)= 空单元:绑存储占位(着色器声明维度、每次清零),格式无声明时取应用绑定格式;只由 `ResolveWireTextureStorage` 的「确实不在」答案触发,死记录/陈旧 view 仍是具名拒绝;monolith 不动(BASE 本就 Fail) |
| g5-msrbo | `7c1a3b74`(单采样深/模板 1:1 blit 改 copy)、`b10c8529`;`e80392ec`/`19c78bb9` 的**按 vendorID 选臂** | **rework → g5-msprobe** | 根因成立(Adreno 830 的 render-pass 深度/模板 resolve 不写目标),但按 Qualcomm vendorID 选臂违反用户 2026-08-22 规则「后端限制类判定一律 POST 探针、不按驱动名硬编码」→ 改为带对照组的运行时探针 + POST Known Driver Bugs 行(FIXED) |
| g5-msprobe(p7/g5-msrbo 续) | `52d1099b`、`d2f2971a`、`926869f0` | **land** | 删 `WirePrefersShaderDepthResolve(vendorID)`;`WireDepthResolveProbe` 在 server 侧 Magma 初始化跑一次:六种深度/模板格式 4×4×4 样本、清到 0.25/0x5A、目标预填哨兵,用**生产同一个**无 draw render-pass resolve,对照组 = 生产着色器臂;对照不过的面不计 → inconclusive 保默认序。红米 6 格式全 `render pass 0/16 / shader control 16/16` → verdict broken,CTS 3/3;强制 clean 同库即 Fail。POST Vulkan「Known Driver Bugs」加 FIXED 行(无 stencil export 为 UNFIXABLE)。补一条 should-fix:`elide-subject` 测试旋钮让 lavapipe 的真实测量路径判 broken,证明主机上探针的真测量能红 |
| w2-reducer(p7/devprep) | `fe4c8745..f93a9da9` | **land** | 门 3/门 5 化简器按合同公式、五块必齐、同一 boot_id、缺块/空读/子集不给 PASS,自测覆盖;设备共享锁 |
共同基底:三个包都带 PH-4 空层修复的重复拣选(补丁 id `92d463c8`);只随 F2 线落一次(`58835fa4`)。
@@ -1,14 +0,0 @@
# Review: M2 round 2 (pipe 4d207468 959ebc9e 6e18acaa 5ba34fcf + integrator f62703e9 1ab62ece) — integrator (Fable 5.1, main session)
**Verdict: land** (no change required before push; one cost recorded in CONTRACT-P7 §12 as M3's denominator).
The must-fix from round 1 (memo handle-reuse ABA) is closed the way the review asked: `VkBufferManager::m_wireStoreDestroyEpoch` is bumped on every path that destroys a wire store's VkBuffer and never reset; `UniformManager` folds it into the descriptor-reuse signature (`mix64(epoch)`) and records/compares it in `FastRebindMemo`, all under `MOBILEGL_BUILD_DISAGGREGATED`.
Verified on the pipe source, not the note:
1. Every destroy site is covered. `DeferWireRelease`'s immediate destroy (`buffer.Destroy(); ++epoch`), `SweepDeferredWireReleases` (returns at `dead == 0` BEFORE the bump, so a sweep that freed nothing does not defeat the memo on the next draw), `DestroyAllDeferredWireReleases` (bump iff non-empty), `Shutdown`'s `m_wireBuffers.clear()` (bump iff non-empty). `DestroyWireBuffer` moves the store into `DeferWireRelease` and only then erases the record, so the erase destroys nothing. The seven remaining `buffer.Destroy()` calls in the file are the non-wire records, which still die at the frame boundary that clears the memos.
2. The epoch is the destroy count's parity, not the slice epoch: `m_sliceEpochCounter` moves on every `WriteWireBuffer` and would miss on every `glBufferSubData`; the destroy epoch moves ~26k times over bsl's 1.3M calls. Cost = one extra descriptor-set acquire+write on the first draw after each destroy event, which also feeds §6.1's in-frame descriptor-set growth (M3).
3. `FastRebindMemo` aggregate initialiser order matches the struct (epoch last, guarded on both sides); the wire arm never takes the fast rebind (`SetupWireDraw` passes no sampler hint), so the signature half is the one the scenario reds through; the memo half is defensive and costs nothing.
4. The red-once is deterministic by construction (no sleep, no fence-poll race): orphan → flush-without-wait → a staged copy keeps the recording pending so the blocking wait cannot take the memo-clearing drain → the next park's sweep destroys S1 → a 64 KiB mint through the freed handle cannot alias the 256-byte hole a 1 MiB pin bounds → the draw reads the dead store's colour without the epoch. Both arms red at base, green with the guards; the note's static-link finding (the Split arm runs the backend compiled into the test binary) is worth keeping for every future red-once.
5. Integrator additions: `RespecifyWireBuffer` clears B3's `lastUseSubmitIndex` beside `lastUseSerial` (pipe-only field; the create path already zeroes it at :303, so 0 = never submitted); the parity exception list on pipe is keyed by exact tail (two merges ago) so the fourth Reclaim case is named there (the package branch excepts by prefix). `ServerSpawn` keeps `SPIN_US` / `SERVER_AFFINITY` / `AUDIT` for the child (tuning / audit values, no endpoint, role, path or size; ServerMain's catch (b) still verifies its four).
Gate on f62703e9: unit 2436, split 304, spawn 220, tcp 223, magma 101 / 80 / 71, full-split 539, G1 `.text` 0xa52203 nm +0/-0, census 79, ratchet 173, wire-declines 53/53/0 + 16/16, OpenRA DirectVulkan inproc + spawn ssim 1.000000 / 0 unsound lines, verify lanes 1150 / 1084; parity 0 errors after 1ab62ece.
-11
View File
@@ -1,11 +0,0 @@
# M3 集成者源码审查(pipe `9fd768d4`,2026-09-23)
**Verdict: land.** 审查对象是集成树源码,不以包 note 代替。`p7/m3` 包提交 `7acad6b8` 已拣为 pipe `9fd768d4`;下述注释措辞的一处小修在本轮文档提交中落地。
1. `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:7065` 与 `:7687` 只在 wire draw / compute 的外层入口触发,发生于本次 draw 的资源准备、descriptor 写入之前。触发条件来自 `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:678` 的当前帧槽实际 Acquire 次数;memo 命中不增加此数,因而不会因重复 draw 无谓地强制等待。
2. `MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:13580`:尚有当前录制就先 flush;之后 `vkQueueWaitIdle` 覆盖 graphics queue 上所有已提交工作,包括 B4 尚未修复的单 fence 站点可能提前从 tracker 移除的记录;再推进 `through` 并复核无待提交录制。此处在 `m_wirePreparationDepth` 进入前执行,不会回卷本次 draw 尚未录制的 slice。只看 tracker 的聚合 fence 仍有被旧退休站点漏掉记录的风险,因此保守队列等待是本轮必要的完成证明。原注释的“exact submit index”易误读为单 fence 等待,已改为说明 queue idle。
3. `MobileGL/MG_Backend/DirectVulkan/Renderer/UniformManager.cpp:683` 保留每个 layout 的 pool/set,只回卷 cursor;清 descriptor-reuse、fast-rebind、bind-dedup 与 sampler-resolve memo,旧 set 不会被新 draw 的 memo 当作未改写继续命中。image/buffer view 保留到原帧生命周期,避免提前销毁。`OnDescriptorSetLayoutDestroyed` 的逐 pool 释放规则仍在;活 layout 数量自身的增长不属于此游标界。
4. `MobileGL/MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp:525` 固定一个 SSBO store/layout,在同一帧切 2049 个对齐窗口,不触发 M2 的 orphan-store 上限;两臂最后像素正确,server `retiredSubmit=2 budget=2048 cached=2048`。仅删 draw 入口回卷调用并重建三目标,两臂都在“2049 distinct descriptor writes ... did not rewind”处红;恢复后绿。`MobileGL/MG_IntegrationTest/CMakeLists.txt:3479` 只把它注册到 split/spawn 私有 server 日志车道,`scripts/ci/spawn_lane_parity.py:91` 对 TCP 的例外是精确 tail。
5. 集成树门:pull G1 `.text=0xa52203`、符号 +0/−0;census 79、棘轮 173、parity 0、审计 53/53/0;unit 2436、split 304、spawn 220、tcp 223、magma 102/81/71、full-split 540、verify 1152/1086,全部 0 红。OpenRA inproc / spawn 1.000000、0 Fatal / 0 unsound。主机 bsl-esc-menu inproc / spawn 都是既有 `ssim=0.998402`、306892 mismatch、0 Fatal / 0 unsound;server 实际发出一次 M3 回卷(frame 0、submit 32、budget 2048、缓存 3555),spawn `wbuf[wlivepk=1052 wdefsync=22]`,与 M2 后的数量级一致。
**记录项:** queue idle 在极长帧每 2048 次 descriptor Acquire 后可能加一次 stall,性能按 CONTRACT-P7 的非门规则记录;固定活 layout 的游标增长已界定,多 layout 的缓存总量可高于 2048(bsl 的 3555 是各 layout 历史缓存),不把 2048 误写成进程全局上限。设备 p7w6 的 bsl 验收针对 M2,M3 的下一版 APK 仍要做设备回归。
@@ -1,51 +0,0 @@
# P7 收官异模型审查(codex,2026-09-23,`6afa077f`)· 集成者分诊
codex 结论:yes-with-fixes。原文附后(`codex-review.md`)。
| # | 发现 | 分诊 | 去向 |
|---|---|---|---|
| 1 | 暂存纹理:对端在已声明的大层末尾写 4 字节,server 按终点 `resize` 零填到整层(16384² RGBA8 ≈ 1 GiB) | **记债,不修**:上限是设备限制下的整层,已认证对端用合法 `glTexStorage` 也能要到同量内存(ID-P7-3 威胁模型 = 未认证远端,已由令牌 + fork 前认证关闭);稀疏存储是重设计 | CONTRACT-P7 §12 |
| 2 | 越界图像单元共用可写占位:单元 A 的写经 barrier 后可被单元 B 读到(GL 要求写丢、读 0) | **修**:有 `VK_EXT_robustness2` nullDescriptor 时绑空描述符(语义精确),否则每 (binding, unit) 私有占位 | cf-magma |
| 3 | PH-5 按 `sizeof(Element)` 限 count,`Vector<String>` 紧凑编码的合法归档(多个短名 + 空尾向量)被拒 → split 下丢合法程序产物 | **修**(真回归):按该类型最小编码宽度限 count | cf-core |
| 4 | 门 3 判读信 runner 自报的遍数 / 臂 / 分母,`--repeat 1` 也能 PASS | **修**:判读器硬编码 §7.2 常量(≥3 遍、四臂齐、规范 36 例集合) | cf-reducer |
| 5 | CTS `Pass/(Pass+Fail)` 看不见 Pass→NotSupported 的流失 | **修**:BASE Pass → AFTER 非 Pass/Fail 计入非通过并列出,未裁定即 FAIL;字面公式仍打印 | cf-reducer |
| 6 | 闩:控制线程在 apply 线程每记录检查之后闩住,apply 还会再弹一条;`appliedSeq` 对闩住的记录也前进 | **修前半**(每次 pop 前再查一次);后半记债 | cf-core / §12 |
| 7 | 深度 resolve 探针判定函数级 static,同进程换物理设备复用旧判定 | **修**:按物理设备身份缓存 | cf-magma |
| — | 跨臂 PNG 不同只报告:需收官时逐例裁定 | 判读器改为「未裁定即 PASS-NEEDS-ADJUDICATION」 | cf-reducer |
| — | 门 1 数字取自 M3、最终树未见完整门 | 最终树 `431bda67`(+ 仅文档到 `6afa077f`)已跑 v2 全门(`~/w7/logs/int2b/gate.log`);修复轮合入后再跑一次 | 记录 |
---
## codex 原文(`6afa077f`)
## P7 closeout review
**Verdict: yes-with-fixes.** Gates 3 and 5 turning green would not, by themselves, justify closeout at `6afa077f`. The findings below include two peer-facing correctness or resource failures and two ways the window reducer can report green without proving the promised result.
I reviewed the `14e1c8b9..6afa077f` comparison and files fetched at `6afa077f` read-only. I did not build or run tests. The local shell did not return commands, so the source review used the repository connector.
### Findings
1. **Major — a tiny texture upload can allocate a gigabyte in the session child.** [StagedTextureStore.h:673](../../../../MobileGL/MG_Remote/Server/StagedTextureStore.h:673) grows `Bytes` through the *end offset*, while [the new bound](../../../../MobileGL/MG_Remote/Server/StagedTextureStore.h:196) limits dimensions and arithmetic, not staging memory. On a server allowing a 16384×16384 RGBA8 level, a peer can declare that level and send a four-byte slab at its last pixel; the computed end makes `resize` zero-fill about 1 GiB. Repeating this can exhaust memory or kill the child and put the supervisor under system memory pressure. PH-4 raw-peer tests exercise out-of-bound records; this slab is **within** the declared bound.
2. **Major — invalid image stores can become observable through a shared placeholder.** [WirePlaceholderImages.inc:77](../../../../MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc:77) keys writable storage placeholders by format and target, and [line 173](../../../../MobileGL/MG_Backend/DirectVulkan/Renderer/WirePlaceholderImages.inc:173) clears them before descriptor use. Two invalid image units of the same shape therefore bind the same Vulkan image. A shader that stores through invalid unit A and, after an image memory barrier, loads invalid unit B can see A’s value; GL requires the invalid store to be discarded and the load to return zero. The new scenario loads its invalid source **before** storing to its invalid destination, so it cannot catch this alias. CONTRACT-P7 §12 records sharing as a debt; its classification is too mild for this within-dispatch correctness failure.
3. **Major — PH-5 can reject an archive produced by its own encoder.** [ProgramArtifactsCodec.cpp:309](../../../../MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp:309) requires remaining serialized bytes to cover `count * sizeof(Element)` before decoding every vector. A `Vector<String>` element can serialize as an eight-byte length plus a short name while occupying roughly 32 bytes in the vector. A valid `LinkArtifacts` archive with, for example, ten short [XFB interface names](../../../../MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h:593) and empty later vectors can fail decode solely because its compact wire form is smaller than its in-memory form. The round-trip test uses two longer names; the PH-5 negative controls test malicious counts, not this valid compact case. A split client then loses a valid program artifact that monolith can use.
4. **Major — gate 3 can report PASS without the specified repetitions or control arm.** [60-reduce.py:182](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:182) accepts the repeat count written by the runner; [line 251](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:251) checks against that count, not three. `30-gate3.sh --repeat 1` can thus yield PASS for 36 cases after one inproc and one spawn image each. [The `RUN_AHEAD=0` arm is optional to the reducer](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:279), and [the denominator check](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:298) tests only the number 36, not the canonical case set. The default runbook uses the intended inputs, but its green verdict does not prove they were used; reducer self-tests do not enforce these contract constants.
5. **Major — ID-P7-59’s CTS rate can hide loss of supported cases.** [60-reduce.py:410](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:410) implements the agreed `Pass/(Pass+Fail)` formula, but [line 525](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:525) only warns when `NotSupported` changes. If 123 of the 124 baseline-passing SSBO cases become `NotSupported` and one still passes, the block remains 100% by that formula, has no new crash, and can be marked PASS. This is a flaw in the **recorded gate criterion**, rather than a mismatch between code and contract. The reducer even lists Pass-to-other-status regressions but does not gate on them. The CTS tests cannot catch a loss that their verdict rule explicitly accepts.
6. **Minor — the latch’s “no further record” guarantee has a control/apply race.** [ServerLoop.cpp:673](../../../../MobileGL/MG_Remote/Server/ServerLoop.cpp:673) checks the latch before the batch and [line 711](../../../../MobileGL/MG_Remote/Server/ServerLoop.cpp:711) after applying each record. If the control thread latches a malformed SurfaceOp just after that latter check, the apply thread can pop and execute the next queued command before checking again. Separately, [SessionRings.h:465](../../../../MobileGL/MG_Remote/Transport/SessionRings.h:465) advances `appliedSeq` even when the record’s callback latched a refusal, so a waiter can briefly see that record as applied before the fault reaches it. The batch negative controls latch on the apply path; they do not arrange this cross-thread ordering. The child still exits, limiting the impact to that session.
7. **Minor — the depth-resolve verdict belongs to the first device in a process.** [VulkanRenderer.cpp:15840](../../../../MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:15840) stores a choice derived from `this` device in a function-static value. If inproc tears down a renderer and initializes another physical device or changed driver environment in the same process, it reuses the first verdict and skips the new device’s probe. The current device gate and host controls start with one device, so they do not exercise that lifetime.
### Gate evidence and decisions
The notes substantiate gates 2–7 on their stated host scopes, with **gate 4 achieved by reclassifying 86 still-linked symbols as `# P13`**, not by removing all 101 original Magma references. That deferral is explicit and defensible while the disaggregated monolith reference arm remains required. The closeout should preserve that wording.
Gate 1’s displayed Magma counts in [CURRENT_STAGE_STATUS.md:13](../../../../docs/Disaggregated/CURRENT_STAGE_STATUS.md:13) are from M3. F2 has a later full local gate, while the gate-5 review records targeted tests for subsequent changes. The notes do not establish a full gate run on the final `6afa077f` tree. Gates 3 and 5 are correctly shown as pending.
ID-P7-59’s **report-only cross-arm SHA** rule follows CONTRACT-P7 §7.2, but [the reducer can PASS a stable split-only pixel difference](../../../../docs/Disaggregated/notes/p7/device-window-2/tools/60-reduce.py:284) when its SSIM change stays within 0.0005. Any such mismatch needs explicit image-diff adjudication at closeout; a green summary alone is insufficient.
**Must fix before closing:** bound total staged texture allocation; make invalid image writes truly discard without aliasing loads; preserve valid compact artifact archives; make gate 3 enforce three repeats, the required arms, and the named 36 cases; gate or adjudicate Pass-to-NotSupported CTS regressions; and run the full host gate on the final code after those fixes. Then assess the device-window gates 3 and 5 on that same code.
@@ -1,11 +0,0 @@
# review-ratchet88(集成者审查,2026-09-23)
**Verdict: land.** p7/ratchet88 `6a488d90`(产品 + 基线)/ `989c53fb`(CONTRACT-P7 §4.1/§12)。
| 项 | 核对 | 结论 |
|---|---|---|
| 退役 `MGPipeTextureLegacyArmScope` | `VkTextureManager.cpp` SyncTexture 只在 monolith 臂被调用(唯一调用者 `SyncTextureAndGetDescriptor`);split 臂同步走 `SyncTextureResourceByHandle`;探针 + 检查者 78 行 split 子集 retrace 0 命中 | 正确;回归现由 `Fatal{RoleViolation,"texture-legacy-arm"}` 按访问器名拦截 |
| G1 | 替换注释与原块同行数(`__LINE__` 进 pull `.text`);`.text` sha256 与基线相同、符号 30570/30570 | 不动 |
| 余 86 标 `# P13` | disaggregated 构建里 monolith 是默认运行时且是门 3 / 门 5 的参照臂(`ConfigLoader.cpp:322-330`、`test.yml:1531`、`CMakeLists.txt:805-811`);改用 wire placeholder 会改变参照臂可观测行为 | 接受;**新裁定 ID-P7-55 取代 ID-P7-31 的「不标 # P13、归 wave 3」**,§4.2 加注 |
| 措辞 | 4 个 MGB_CTX 符号里 `PipeInputs::HasOpenTransformFeedbackSpan` / `InvalidateCompileEnv` 不在 monolith draw 路径,计入原因是 `PipeFill.cpp` 按路径规则归 FRONTEND(同 5 个 SlotAllocator 符号) | 集成文档更正 |
| 缺口 | 退役后无常驻负控证明 apply 线程 `SyncTexture` 会按名 abort(只有临时探针 + 棘轮 red-once) | 记 §12 债 |
@@ -1,11 +0,0 @@
# Review: V1 fix round 2 (pipe 403b5669 ffc4fb7d 07f4e3ac 9814bc74 336174e3) — integrator (Fable 5.1, main session)
**Verdict: land** (no change required before push).
Fix 1 — the read-side control counts to two. The server reads the pack field twice inside the ReadPixels window (the applier's saved-pack read before it installs the neutral pack, the backend's read after), and the hook perturbs its oracle once per read because the ID-49 overwrite inside the window wipes the first perturbation. A control satisfied by one report was satisfied by either block alone and falsified neither. Verified: `CountReadReports` is line-based (ReportDivergence is one MGLOG_F per report, so no report spans lines and none share one); the scenario's `EXPECT_GE(reads, 2)` and the CI step's `-lt 2` red are the same threshold; the CI count is `grep -c … || true` with `${reads:-0}`, so a missing file reads 0 and reds instead of erroring. The falsifier asymmetry is measured and stated (block 2 reverted: both backends drop to 1; block 1 reverted: DirectVulkan drops to 1, DirectGLES stays at 2 because its backend reads twice after the install), which is what justifies the CI step needing both backends.
Fix 2 — the FATAL=0 summary is written before `Debug::Close`. Cause was pre-existing: the namespace-scope `VerifyState` destructor ran from `__run_exit_handlers` after `DestroyImpl` had closed the sink, and Log.cpp's next write reopened the role's file with "w", so every FATAL=0 client half was the summary alone (121 B / 1 line). Verified: `FlushSummary` is idempotent through `Summarised` (the destructor after Close has nothing new and writes nothing; a process that never called Destroy keeps the old arm, where no Close ran); the two writers are sequential (DestroyImpl, then exit handlers), so the non-atomic `Summarised` is fine; the call sits before the `logLifecycle` check and before Close; the include, the declaration and the call are all under `MOBILEGL_PIPE_VERIFY`, so the pull build gains no byte (G1 checked on the gate). Client halves now carry the arming line, the entry report and the summary as the last line (121 B → 7461 / 7386 B).
Nits 3 and 4 are citation fixes. The CONTRACT-P7 §6 hunk conflicted with the integration tree's own version of that sentence; resolved by keeping the tree's sentence and applying the package's edit (the CMake line-number citation goes, the guard `if (MOBILEGL_PIPE_VERIFY)` stands alone). The note's lane-count tables were left for the integrator to re-measure: the gate on 336174e3 is the measurement (below).
Gate on 336174e3: unit 2436、split 304、spawn 220、tcp 223、magma 101 / 80 / 71、full 539、G1 恒等、普查 79、棘轮 173、parity 0、审计 53/53/0、OpenRA inproc + spawn ssim 1.0 / 0 unsound、verify 1150 / 1084.
-26
View File
@@ -1,26 +0,0 @@
# Brief: rewrite `docs/Disaggregated/` into a clear, concise design + architecture document set
## The user's instruction (verbatim, this is the goal)
"make documentations in `docs/Disaggregated` clear and concise. Make it just clearly describe the design and architecture, rather than decision dilemmas."
## What exists today (branch `feat/disaggregated`, WSL tree `~/w7/pipe`, HEAD 458ccde1)
- `docs/Disaggregated/PLAN.md` (~312 KB, Chinese): the single implementation plan for MGPipe — the gallium-style explicit frontend/backend interface + backend-owned state machine + two-process split. It grew out of a design competition and three adversarial review rounds, so it is full of "v1 said X, v2 corrected to Y", "(P0 实测修正)…" patches, rejected alternatives, risk tables arguing with reviewers, and "待证/已证" bookkeeping. The design itself is settled; the prose is not.
- `docs/Disaggregated/REVIEW.md`: the design-competition and adversarial-review record.
- `scripts/check_doc_citations.py`: CI lint (job `pipe-gates` in `.github/workflows/test.yml`) that requires every `file:line` citation in `docs/Disaggregated/*.md` to resolve on the baseline commit. Run it and keep it green (read the script for its exact contract, base commit and invocation; `python3 scripts/check_doc_citations.py --help` or read the CI job).
- Code that already exists and that the docs must describe accurately (read it, do not guess): `MobileGL/MG_Pipe/` (PipeCalls.def — 71 calls after P-2 — MGPipeTypes.h, MGPipeHandles.h, MGPipeCallbacks.h, MGPipeHostSpan.h, MGPipe.h, Coverage.def, generated/), `scripts/gen_pipe.py`, `scripts/gen_pipe_dirty_surface.py`, `scripts/gen_protocol.py`, `MobileGL/MG_Remote/{Protocol,Transport}` (protocol.fbs, Ring/RingControl, Doorbell, ShmSegment, FdPassing, InProcessTransport, Framing), `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}`, ConfigLoader's `MOBILEGL_PIPE_*` knobs, `tools/spikes/{server_stub,extmem_probe}`, `MobileGL/MG_Test/Pipe`, `MobileGL/MG_Test/Wire`, `MobileGL/MG_Test/Util/PipeStatsTest.cpp`.
- Measured facts from P0 (these replace every "待证 / 设备锁 / 欠着" sentence in the current PLAN.md): read `<this directory>/p0-device-findings.md` — spike A (exec a second native binary from the app's own `untrusted_app` process) is proven on both devices; spike B's external-memory tiers per device; the on-device MGPipe stats baselines (accessor calls per draw, bytes per frame per class, upload shapes, memo gates) for four traces on both backends; the harness facts (JSON dump never fires on device, `MOBILEGL_PIPE_STATS_PERIOD`, serial device runs); and that the create-indirect fixture's failure on Adreno 830 is pre-existing at `dev@81b17c0b` (verified with a baseline APK), i.e. not caused by this branch.
## What to produce
Rewrite `docs/Disaggregated/` so a reader who has never seen the reviews can understand the design and the architecture and go implement the next phase. Chinese, same as today (technical identifiers in English). Suggested shape — adjust if the material argues for something else, but keep it small:
1. `README.md` — one page: what MGPipe is, the one-paragraph architecture (client tracker → MGPipe verbs → server object table → backend; monolith vs split build shapes), the status line (P0 landed at 458ccde1; P0.5/P1/P2 next; GO/NO-GO at day 43), and a map of the other files.
2. `ARCHITECTURE.md` — the design as decided: handles and generations; the call catalogue by group (screen / ctx-query / CSO / state / object / verb) and the record layout conventions (payload structs, var-tails, host spans, chunking bound = half the ring); render state as blob + CSO identity subset + `set_dynamic_state`; push-at-validate; texture subdata accumulation and `resource_subdata` (union box + region list); shader state = SPIR-V + reflection (the P0.5 header extraction it depends on); the reverse channel (callbacks + `resource_subdata_complete`); persistent maps and the external-memory tiers as measured; restart rewrite / multi-draw tiering on the server with the index host mirror; the transport (SPSC shm ring, RingControl cursors, doorbells, FlatBuffers control plane, SCM_RIGHTS, present credit); process/EGL/Android delivery (lib*.so executable, fork+execve, marker files); build shapes and purity gates; the verification strategy (PipeInputs strangler, `MOBILEGL_PIPE_VERIFY`, generation poison, the CI gates that exist). State each decision as a decision with its one-line reason; do NOT reproduce the dilemma, the rejected options or the review argument. Where a number justifies a decision, cite the measured number (from the findings file or PLAN.md's measured sections), not the estimate it replaced.
3. `ROADMAP.md` — phases P0…P13 as a compact table (what lands, acceptance gate, dependency), the day-43 GO/NO-GO criteria as a checklist, and the genuinely open questions as a short list (drop the ones P0 answered).
4. `MEASUREMENTS.md` (or a section) — the P0 baselines and spike results as tables, with the exact device / commit / command so they can be reproduced.
5. Delete `REVIEW.md` from this directory (the review record lives in git history: mention the commit range in README). Delete `PLAN.md` once its content is fully carried over — nothing that is still true and still needed may be lost, so do the carry-over section by section and keep a checklist in your notes; what is lost on purpose is the dilemma prose, superseded estimates, "v1/v2" archaeology and reviewer back-and-forth.
6. Every `file:line` you keep or add must be verified against the tree (open the file, check the line) and must pass `scripts/check_doc_citations.py`. Prefer citing a symbol name plus file over a raw line number when the doc will outlive the line.
## Constraints
- Docs only. Do not touch code, scripts, workflows or tests (the citation checker's config is the one exception if a deleted file name must be dropped from it — then say so).
- Work in a git worktree: `cd ~/w7/pipe && git worktree add ~/w7/p0-docs -b p0/docs-rewrite feat/disaggregated`. Commit there in one or a few commits; do not push. Commit message: single-line subject `[Docs] (Disaggregated): …` + blank line + `- ` bullets; no Co-Authored-By.
- Long outputs go to files, never into your return value: write your carry-over checklist to `~/w7/p0-docs-notes.md` and return only the branch name, commit hashes, the doc file list with sizes, the citation-check result, and anything you deliberately dropped that a reader might miss.
- Keep it concise: the target is roughly a quarter of today's volume or less. Concise means no repetition and no argument, not omitted design.
-48
View File
@@ -1,48 +0,0 @@
# P0 implementation brief (MobileGL disaggregation, plan "MGPipe")
You are one implementer among several working IN PARALLEL on phase P0 of the plan in
`C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg/docs/Disaggregated/PLAN.md`
(that file is being consolidated by another agent right now; if it is missing or mid-rewrite, read
`PLAN-B-MGPipe.md` in the same directory - same content, older numbering). Read the plan's §0 (decisions D-B1..D-B8),
§4 (interface MGPipe), §6.2/§6.3 (PipeInputs, residual block), §7 (reverse channel), §8 (inherited transport design),
§10.3 (gates) and §11 P0 before touching code. Cite plan sections in your commit body.
## Where the code lives and how you build it (WSL "Arch", 28 cores)
- Reference tree: `~/w7/pipe` (branch `feat/disaggregated`, warm `build-linux` with clang+ccache, unit + integration tests ON,
lavapipe/llvmpipe ICD). DO NOT edit `~/w7/pipe` or `~/w7/base` - they are read-only references.
- Make YOUR OWN tree: `cp -a ~/w7/pipe ~/w7/p0-<slug>` (a few minutes; keeps the warm build dir), then work only there.
Single-writer rule: never touch another slug's tree.
- Run WSL commands from the Windows Bash tool as:
`MSYS_NO_PATHCONV=1 wsl.exe -d Arch -- bash -lc 'bash /mnt/c/<path-to-a-script>.sh' 2>&1 | tr -d '\0'`
Put every non-trivial command sequence into a script file under your scratchpad directory (inline quoting through wsl.exe
gets mangled). Your scratchpad is `C:/Users/GEEKER~1/AppData/Local/Temp/claude/C--Users-geekerwan-AndroidStudioProjects-FoldCraftLauncher-MobileGL/eb1a694b-6a85-4ee7-810d-692eabf8fda6/scratchpad/wf2/p0-<slug>/` (create it; Windows path `/mnt/c/Users/geekerwan/AppData/Local/Temp/claude/...` from WSL).
- You may read/edit tree files from Windows through the UNC path `//wsl.localhost/Arch/home/swung/w7/p0-<slug>/...` with the
Read/Edit/Write tools, or edit inside WSL with python/sed from a script. Keep LF line endings.
- Build: `cd ~/w7/p0-<slug> && export CCACHE_BASEDIR=/home/swung/w7 && cmake --build build-linux -j 24 2>&1 | tail -30`
(reconfigure with the same cmake line as `~/w7/prep.sh` only if you changed CMake files: copy that line, keep `-B build-linux`).
- Tests: `cd build-linux && ctest -L unit -j 12 --output-on-failure` (428 unit tests must stay green) and, when your change
touches a backend or MG_State, `ctest -L integration-gpu -j 8` (367 scenarios; compare the pass/fail NAME set with the
baseline recorded in `~/w7/pipe-itest-baseline.txt` if present - a failure that is already in the baseline is not yours).
Per the user's rule, run REPRESENTATIVE tests locally, not everything, unless your package says otherwise.
- Android (only if your package needs it): `~/w7/pipe/build-android` is warm; NDK `/home/swung/android-sdk/ndk/27.3.13750724`,
SDK `~/android-sdk`, gradle `~/gradle-8.10.2`, JDK `/usr/lib/jvm/java-21-openjdk`.
- Devices (only if your package needs them): `35d0befa` (Xiaomi 24129PN74C, Adreno 830) and `3B159D009VZ00000` (Oppo PLG110,
Mali). Both are SHARED with another running campaign. Protocol: acquire an atomic mkdir lock BEFORE any adb use:
`LOCK="C:/Users/GEEKER~1/AppData/Local/Temp/claude/C--Users-geekerwan-AndroidStudioProjects-FoldCraftLauncher-MobileGL/8ab6b3cb-c783-4826-9fd7-39ff3498af5b/scratchpad/w7/locks/<serial>"; until mkdir "$LOCK" 2>/dev/null; do sleep 60; done; echo "p0-<slug> $(date)" > "$LOCK/owner"` ... `rm -rf "$LOCK"` when done. Hold it at most 25 minutes per acquisition. If a lock is held for more than 60 minutes (check `cat "$LOCK/owner"`), do NOT break it: report "blocked by device lock" in your result and finish the non-device parts. Always pass `-s <serial>` to adb. Wake+unlock before launching apps (`adb -s X shell input keyevent KEYCODE_WAKEUP; input keyevent 82`). Read device logs from `/sdcard/MG/latest.log`, not logcat.
## Rules
- Commit on a branch named `p0/<slug>` in YOUR tree (`git checkout -b p0/<slug>`), one or a few focused commits.
Commit message format (mandatory): first line `[Type] (Scope): short lowercase description`; blank line; then `- ` bullets
with the rationale, plan section citations and evidence. Types: Feat / Fix / Test / Docs / Build. NEVER add Co-Authored-By
or any AI attribution trailer.
- Do not push anywhere. The coordinator fetches `p0/<slug>` from your tree by path.
- The default ALL target must build; unit tests must be green; no per-draw `fprintf`/`printf` instrumentation may be committed
(a CI grep gate for that is one of the P0 deliverables).
- Logging: `MGLOG_D` for non-critical lines (compiled out in INFO builds), never `MGLOG_I` for per-frame noise.
- Do not modify `3rdparty/` contents except adding the flatbuffers submodule where your package says so.
- Do not touch the fixtures under `tools/trace_replay/fixtures/` (they are real binaries in the tree; `git checkout` would
turn them into LFS pointers).
- When you are done, return: the branch name and tree path, the list of commits (hash + subject), what you tested (exact
commands and results), anything you could not finish and why, and any deviation from the plan with justification.
-94
View File
@@ -1,94 +0,0 @@
# P0 — 卫生、度量、门、骨架(2026-09-05)
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(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)。
## 摘要
- 落地:边界计数器(`PipeStats`)、`PipeCalls.def` 完整目录 + payload POD + 生成器 G1–G7 + CI `pipe-gates`、`gen_pipe_dirty_surface.py`、`check_doc_citations.py`、`MOBILEGL_PIPE_*` 开关、`MG_Remote/{Protocol,Transport}` 骨架 + `protocol.fbs` + `flatc-check`、retrace `--env` 透传。
- spike A:Android 应用进程 `fork`+`execve` 第二个原生可执行文件,两台设备都成立(P6 / P12 的交付链前提)。
- spike B:跨进程外部内存只有 **T0(AHB BLOB 交接)** 在两台设备、两个后端上都是完整读写;Adreno 另有 T1(Vulkan opaque fd)。
- 基线:真机稳态动态 accessor 每 draw 6.5–9.3 次;Magma 在 26.3 每帧重打包 331 KB 具名 UBO;同 185 次纹理发射 Espryt 整 box 635 KB/帧 vs Magma rect 40 KB("server 选上传形状"的依据)。
## 阶段表行(原 `ROADMAP.md`)
- **阶段**:**P0** 卫生、度量、门、骨架
- **状态**:✅
- **落地什么 / 范围**:边界计数器;`PipeCalls.def` 完整目录 + payload POD + 生成器 G1–G7 + CI `pipe-gates`;`gen_pipe_dirty_surface.py`;`check_doc_citations.py`;`MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架 + `protocol.fbs` + `flatc-check` + `MG_Test/Wire`;三个严格 no-op 收益;spike A / B;retrace `--env` 透传
- **验收门 / 证据**:单元 / 集成 / trace 逐名不变;wire 层测试绿;两台设备字节 / 调用基线在案;spike 结论。`MEASUREMENTS.md` §1
## 实测:P0(2026-09-05,trace APK `7ef7c7e5`,spike `8a239177`)(原 `MEASUREMENTS.md` §1)
### 1.1 Spike A — 从应用自身进程 exec 第二个原生可执行文件
| 设备 | 结果 |
|---|---|
| Adreno 830 | **OK**:`untrusted_app` 父进程 `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so`,子进程同域同 category、exit 0、`execErrno=0`、零 avc denial |
| Mali | **OK**,同形 |
主机侧已证:AGP 会把改名成 `lib*.so` 的 `add_executable` 打进 `lib/arm64-v8a/`(`RUNTIME_OUTPUT_DIRECTORY` 重定向到 `CMAKE_LIBRARY_OUTPUT_DIRECTORY`);`posix_spawn` 在 minSdk 26 不可用;应用进程 stdout/stderr 是 `/dev/null`,子进程用 marker 文件证明自己活过。代码:`tools/spikes/server_stub/`、`android-plugin/app/src/trace/cpp/spawn_spike.cpp`、`MOBILEGL_BUILD_SERVER_SPIKE`。
### 1.2 Spike B — 跨进程外部内存分档(`tools/spikes/extmem_probe/`,`shell` 域,4 MiB payload,每行一次真 GPU 访问 + 两侧字节校验)
| 路线 | Adreno 830 | Mali |
|---|---|---|
| T1-opaque-fd(server 导出 `VkDeviceMemory` fd,client `mmap` + 导入) | **OK** 完整往返 | UNSUPPORTED(`VK_ERROR_INVALID_EXTERNAL_HANDLE`) |
| T1-dma-buf | UNSUPPORTED | UNSUPPORTED |
| T1-gles-memobj-fd(`GL_EXT_memory_object_fd`) | **FAIL**:导入接受,但每次 `glMapBufferRange` → `GL_INVALID_OPERATION` | UNSUPPORTED |
| **T0-ahb-blob-transfer**(client 分配 `AHardwareBuffer` BLOB → socket 交接 → server Vulkan + GL 导入) | **OK** 全链 | **OK** 全链 |
| T3-external-memory-host | UNSUPPORTED | PARTIAL:GPU 写对宿主映射不可见(只读档) |
决定:唯一在两台设备、两个后端上都是完整读写的档是 **T0**;Adreno 另有 T1(Vulkan 路径);Mali 无任何 server 导出路线。Caveat:运行域是 `shell` 不是 `untrusted_app`(`ROADMAP.md` 开放问题 3)。
```sh
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 tools/spikes/extmem_probe/build_android.sh /tmp/extmem-build
adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/ && adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe"
```
### 1.3 边界计数器基线(`MOBILEGL_PIPE_STATS=1`,最后一个完整 120 帧窗口;accessor / memo 数字是软件确定的,两台设备相同)
| trace(窗口内帧数) | 后端 | draws/f | acc/draw | buf B/f | tex B/f(发射 box / rect) | ubo-global B/f | ubo-named B/f | memo 门(hit/miss) |
|---|---|---|---|---|---|---|---|---|
| `minecraft-1.21.4-in-world`(360) | Espryt | 91.6 | **9.28** | 13.5 K | **635 K**(185 box / 0 rect) | 16.7 K | 0 | ers 9257/2577,etl 10538/1296,eub 10720/1114 |
| 同上 | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K(97 box / 89 rect) | 16.7 K | 0 | mfp 0/10994,mpm 9240/1754,mdt 9120/1874 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world`(120,memo 冷) | Espryt | 23.2 | 21.04 | 32.6 K | 8.8 K | 1.8 K | 0 | ers 1958/1843,etl 722/3079 |
| 同上 | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0(vtxc 1.7 K) | mpm 1890/895,mdt 1573/1212 |
| `improved-transparency-minecraft-26.3`(1200) | Espryt | 1320 | **8.44** | 333 K | 0 | 0 | 0 | ers 156925/2791,etl 148606/11110,eub 148246/11470 |
| 同上 | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036,mpm 134421/2615,mdt 156611/1785 |
门缩写:ers = `EsprytRenderState`,etl = `EsprytTextureSyncList`,eub = `EsprytUnitBindingsEpoch`,mfp = `MagmaDrawFastPath`,mpm = `MagmaPipelineMemo`,mdt = `MagmaDynamicTail`。读法:真机稳态动态 accessor 成本是每 draw 6.5–9.3 次(预测区间 10–25 的下沿),推送要打败的是 ~8 次 accessor + memo 探测;Magma 在 26.3 每帧重打包 **331 KB** 具名 UBO 字节(D-B8);同样 185 次发射,Espryt 整 box 路径 635 K 纹素字节 / 帧、Magma rect 路径 40 K,16×("server 选上传形状"的依据)。
```sh
ANDROID_SERIAL=<serial> MSYS_NO_PATHCONV=1 python3 tools/trace_replay/run_android_retrace_local.py \
--case minecraft-1.21.4-in-world --backend DirectGLES --env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
# 数字在结果目录的 mobilegl.log 里,grep 'MGPipe stats:'
```
### 1.4 桌面数据点与语料事实
- llvmpipe / lavapipe 动态 accessor(`GuiBatchScenario`,memo 冷):Espryt 20.65 / Magma 15.54 次 / draw。
- dirty-surface 面:`MG_Impl/GLImpl` 41 个文件、926 次 mutator 调用、73 个不同 mutator(`RecordError` 占 836 次);92 次位于同函数内也到达后端的即时发布点,其余由紧随的 verb 发布。
- 读点覆盖:63 个 `PipeInputs` 字段;后端读点清单 → 调用映射 **0 UNMAPPED**。
- OOM 探测惯用法:41 个 trace fixture 中 0 例(9 次 `glRenderbufferStorage` 无一在 3 个调用内跟 `glGetError`)→ `glRenderbufferStorage*` 不 ack。
- `FramebufferSrgb` / `DepthClamp`:六个后端读点消费编译期常量 `false` / 零读点;`glEnable` 落到 `default:` 分支既不存储也不报错;41 个 fixture 无一开启。
- payload 尺寸(`MGPipeTypes.h` 的 `static_assert`,arm64 与 x86-64 一致):`MGPDrawInfo` 56、`MGHostSpan` 32、`MGPBindRenderState` 12、`MGPResourceDesc` 88、`MGPFramebufferState` 304、`MGPProgramDesc` 192、`MGPSubData` 72;`SEG_CMD` 按 56 B 头定尺,MC 帧 1000–4000 draw 时每帧 56–224 KiB 头字节。
- persistent map 采纳的既有基线(`dev`,MC 26.3,Adreno):p99 163→21 ms、稳态 40→115 fps、省 ~400 MB;P11 的回归上限对着它。Mali 上传作业数悬崖:~100 个精灵 rect 对一个 union box 是 +6 ms/frame。
### 1.5 Harness 事实与陷阱
- trace app 从不到达 `MobileGL::DestroyImpl`,`MOBILEGL_PIPE_STATS_FILE` 在设备上永远不会写;只有 `mobilegl.log` 的周期汇总行,`MOBILEGL_PIPE_STATS_PERIOD` 为此而加。
- `run_android_retrace_local.py` 每棵树共用一个 `.trace-work/android-retrace-result` 根并 `rmtree`,两台设备必须从一棵树**串行**跑。
- `--env` 值里的 `/data/...` 会被 MSYS 路径转换,用 `MSYS_NO_PATHCONV=1`。
- ColorOS 首次 `adb install` 卡在 `InstallGuideActivity` 确认页;streamed install 进行中点它会让设备掉线几秒。
- `minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败,`dev@81b17c0b` 基线 APK 复现——`dev` 侧问题(`ROADMAP.md` 开放问题 17)。
## 本目录
| 文件 | 内容 |
|---|---|
| [`BRIEF-DOCS.md`](BRIEF-DOCS.md) | Brief: rewrite `docs/Disaggregated/` into a clear, concise design + architecture document set |
| [`BRIEF-P0.md`](BRIEF-P0.md) | P0 implementation brief (MobileGL disaggregation, plan "MGPipe") |
| [`p0-device-findings.md`](p0-device-findings.md) | P0 device runs (2026-09-05 evening, both device locks taken over from the stale 00:39/00:43 holders) |
从失败的 workflow 捞回的 P0 期材料(与本目录重复的副本已删除,只留独有的):
[`../recovered/wf1/final/`](../recovered/wf1/final/)(设计定稿前的实施计划草案四部分)、
[`../recovered/wf2/`](../recovered/wf2/)(方案 B / MGPipe 实施计划 v2 的分部与综合稿、`p0-polish-findings.md`、citation lint 自测)。
@@ -1,30 +0,0 @@
# P0 device runs (2026-09-05 evening, both device locks taken over from the stale 00:39/00:43 holders)
Tree: feat/disaggregated @ 7ef7c7e5 (aa005720 code + docs 87ee17c6 + spike-a 8a239177/38d4c237 + spike-b b82dd350/39f982e6/7ef7c7e5).
Spike-A APK: p0-spike-a-android/trace-debug-spike-on.apk (built at 30d7595b pre-rebase; identical sources).
Spike-B binary: p0-spike-b-extmem/extmem_probe (39f982e6 sources, arm64, run as `adb shell` = u:r:shell:s0).
## Spike A — exec a second native binary from the app's own process
| device | result |
|---|---|
| 35d0befa Xiaomi 24129PN74C, Adreno 830, Android 16 | **OK**. Parent `u:r:untrusted_app:s0:c173,c257,c512,c768` fork+execve of `<nativeLibraryDir>/libMobileGLServer.so` → child pid 31348, exit 0, marker + stdout capture + report all present, child selinux = `u:r:untrusted_app:s0:c173,c257,c512,c768` (same domain, same categories), `execErrno=0`, **zero avc denials** in the window. |
| 3B159D009VZ00000 Oppo PLG110, Mali, Android 16 | (pending — filled below when the run returns) |
Harness notes: the foreign-signed trace APK (versionCode 26080769, signer bb517d49, installed 2026-08-30 by the GL4.6 session) blocked `install -r`/`install -r -d` with INSTALL_FAILED_UPDATE_INCOMPATIBLE; it was uninstalled (recorded in lock-takeover-notes.txt). The env-passthrough A/B leg of 42-device.sh could not be exercised because `run-as sh -c 'cat > files/trace-replay/dummy.trace'` is refused on this ROM; the passthrough is instead proven end-to-end by the stats baseline runs (`--env MOBILEGL_PIPE_STATS=1` must produce `MGPipe stats` lines in mobilegl.log).
## Spike B — external-memory tiers (extmem_probe, 4 MiB payload; same verdicts at 64 KiB)
| route | Adreno 830 (35d0befa) | Mali (3B159D009VZ00000) |
|---|---|---|
| T1-opaque-fd (server exports VkDeviceMemory fd, client mmaps raw + imports) | **OK** full round trip incl. GPU touch (`/dmabuf:system`, dedicatedOnly=1, child type 4 bits 0x13) | UNSUPPORTED (`vkCreateBuffer(external)=VK_ERROR_INVALID_EXTERNAL_HANDLE`, advertisedExportable=0) |
| T1-dma-buf | UNSUPPORTED (VK_EXT_external_memory_dma_buf absent) | UNSUPPORTED (exportable=0) |
| T1-gles-memobj-fd (GL_EXT_memory_object_fd import of the export) | **FAIL**: import + glBufferStorageMemEXT accepted (GL_NO_ERROR) but every glMapBufferRange → GL_INVALID_OPERATION (persistent and plain); GL_EXT_memory_object present, GL_DEVICE_UUID unreadable | UNSUPPORTED (GL_EXT_memory_object / _fd not advertised; entry points resolve but ext strings absent) |
| T0-ahb-blob-transfer (client-allocated AHardwareBuffer BLOB → socket handoff → server Vulkan import + GL import) | **OK** full chain: cpu-lock, vk-import+map, GPU copy/fill, GL map persistent+coherent, writeback to client all byte-verified | **OK** full chain, identical verdicts (glPersistentCoherent=1, gpuRan=1) |
| T3-external-memory-host (VK_EXT_external_memory_host) | UNSUPPORTED (extension absent) | PARTIAL: import+map round-trips, but **GPU writes are not visible through the host mapping** (read-only tier) |
| T3-memfd-cross-process / client-memfd-server-import | UNSUPPORTED | OK / PARTIAL (same read-only GPU caveat) |
**Tier decision for P11 (persistent maps across the process boundary):** the one route that is a full read/write tier on BOTH devices is **T0 — client-allocated AHardwareBuffer BLOB**, imported by the server with `VK_ANDROID_external_memory_android_hardware_buffer` (Magma) or `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT` (Espryt), and mapped persistent+coherent on both sides. Adreno additionally offers T1 (server-exported opaque fd) for Vulkan-only paths; Mali offers nothing server-exported and its host-pointer import is read-only. Caveat: run domain was `shell`, not `untrusted_app`; the AHB socket handoff itself is exercised by every app that shares buffers with SurfaceFlinger so the domain risk is on the memfd/opaque-fd legs, not T0.
## Stats baselines (dynamic accessor calls / draw, bytes per frame)
(pending — filled below)
-434
View File
@@ -1,434 +0,0 @@
# P0.5 implementation brief — value header + artifacts header + include-closure gate
Tree: `feat/disaggregated @ 6672778b` (worktree `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`; WSL `~/w7/pipe` and the three package worktrees `~/w7/p05-value-types` / `~/w7/p05-program-artifacts` / `~/w7/p05-include-gate`, branches `p05/<slug>`, all at `6672778b`, all with a configured `build-linux/` — Release, `/usr/sbin/clang++` (clang 22; there is no `clang++-20` in WSL), ccache, `MOBILEGL_BUILD_TEST=ON`, `MOBILEGL_BUILD_INTEGRATION_TEST=ON`, `MOBILEGL_BUILD_DISAGGREGATED=OFF`, log level INFO).
Every `file:line` below was re-opened at `6672778b`; the scouts were written at `6e0e3df3` and all their line numbers still hold. Corrections to the scouts are marked **[correction]**.
---
## A. Goal and acceptance gate (verbatim)
`docs/Disaggregated/ROADMAP.md:16`:
> **P0.5** 值头与制品头抽取 | 6–9 | `MG_Pipe/MGPipeValueTypes.h`(`RenderStateParameters`、`SamplerParameters`、`PixelStoreParameters`、`VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,更新 7 个 includer);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言 | **全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因** | P0;**P1 与 P7 的硬前置**
`docs/Disaggregated/ARCHITECTURE.md:260` is the manifest: value header = `MAX_DRAW_BUFFERS`, `PerBufferBlendState`, `StencilFaceState`, `PixelStoreParameters`, `RenderStateParameters`, `SamplerParameters`, `BorderColorForm`, `VertexAttribute`, `VertexBufferBindingPoint`, "它不 include `MG_State/GLState` 任何东西"; artifacts header = `TypeFacts`, `ResourceReflection`, `XfbVarying`, `LinkArtifacts`, `SpirvArtifacts`, "只 include `<Includes.h>` 与容器". `ARCHITECTURE.md:259`: archive = `Visit()` + `static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`, one field table for both directions. `ROADMAP.md:7`: **每个门必须能因它存在的理由变红**; Windows 机器不是正确性门. `ROADMAP.md:34`: P0.5 is one of the five boundaries that owes a full `gl44to46` caselist run (CI/device, off the critical path).
Decoded: (1) no behaviour change, every existing test name still present and green (adding tests is fine); (2) two closure probes green + a negative control that goes red; (3) every `nm --defined-only -S` delta on `libMobileGL.so` explainable per symbol.
---
## B. Package split
### B.0 Decisions shared by all three packages (fixed here; do not re-decide inside a package)
| # | decision | why |
|---|---|---|
| D1 | **`DynamicBackendParameters` does NOT move.** `MGPipeTypes.h:32` keeps `#include <MG_Backend/BackendObject.h>`; the compositional assertion `MGPipeTypes.h:144-145` stays. | Not in the manifest; it has `SizeT` fields (`BackendObject.h:317,322,545`) and two member functions taking `TextureTarget` (`:487-497`) that force `TextureEnum.h`; "fixed-width members" is a type change, not a move. Escalate to the plan owner as a P1/P7 item. Consequence: probe A asserts `MGPipeValueTypes.h`, not `MGPipeTypes.h`. |
| D2 | **All eleven enums `RenderState.h:15-189` move**, including `StencilFace`, `PixelStoreParam`, `CapabilityInput`. | A split (some enums here, some there) is the maintenance trap; namespace `MobileGL` is unchanged so no spelling changes anywhere. |
| D3 | **`VertexAttribute`, `VertexBufferBindingPoint`, `VertexAttributeVersion` keep namespace `MobileGL::MG_State::GLState`** (the value header reopens it and forward-declares `class BufferObject`). **[correction** to scout-value-types §7.2, which recommended re-homing them in `MobileGL` + a `using`]. | Zero mangled-name churn for the value-types package, so the symbol report attributes 100 % of P0.5 churn to the artifacts de-nesting. Gate A is a path check; namespaces are irrelevant to it. Re-homing is a P13 cosmetic. |
| D4 | `MAX_DRAW_BUFFERS` becomes `inline constexpr Uint kMGMaxDrawBuffers = 8;` in `namespace MobileGL` (value header); `FramebufferObject::MAX_DRAW_BUFFERS` is **defined from it** (`= MobileGL::kMGMaxDrawBuffers`), so the 80 existing spellings keep working and cannot drift. `RenderStateParameters::MAX_VIEWPORTS` moves with its struct unchanged. No other new constants (**[correction]**: scout-value-types §6 `kMGMaxViewports`/`kMGMaxVertexAttribs*` are dropped). | Single definition; no `static_assert`-in-a-.cpp indirection. |
| D5 | **`LinkArtifacts::program` (`SharedPtr<glslang::TProgram>`) and `uniformInitialValues` (`Vector<glslang::TIntermediate::TUniformInitializer>`) move VERBATIM.** No `UniformInitializer` replacement in P0.5 (**[correction]** to scout-program-artifacts §6.2 item 5; recorded as the P7 follow-up). | Pure move. Both are symbol-free: `shared_ptr`'s deleter is type-erased at construction (only `ProgramLinkTask.cpp` constructs one) and `TUniformInitializer` (`include/glslang/MachineIndependent/localintermediate.h:627-636`) is an aggregate of `std::string`/`std::vector`/an unscoped enum with implicit inline special members. `<Includes.h>:80-84` makes both types complete on every side anyway. The gate pins the count of `glslang::` tokens in the header at exactly 2 so nothing new sneaks in. |
| D6 | `ProgramArtifacts.h` includes exactly `<Includes.h>` and `<set>` (`std::set<String>` at `ProgramObject.h:1331,1336`; `<set>` is in no header on the path today). | `ARCHITECTURE.md:260`. |
| D7 | `sizeof` tripwires: exact literal for POD types on every ABI (`TypeFacts` 44, `PixelStoreParameters` 28, `PerBufferBlendState` 28, `StencilFaceState` 28, `RenderStateParameters` 1168 — `MEASUREMENTS.md:86`, `ARCHITECTURE.md:188`, consistent with `MGL_RESIDUAL_BLOCK_SIZE 1248` at `MGPipeTypes.h:535`); **per-STL** numbers for container-bearing structs, asserted only under `__GLIBCXX__ && !_GLIBCXX_DEBUG` (the CI toolchain) with a `_LIBCPP_VERSION` (NDK) branch pinned by the integrator; MSVC unasserted. | `sizeof(std::string/std::set)` differs between libstdc++ and libc++ (`ARCHITECTURE.md:259` assumes one number; it cannot be one number). |
| D8 | **No `PipeFields.def` field lists for the value types in P0.5** (they need `std::array<struct>` support in `gen_pipe.py`'s comparator; `std::array::operator==` is unconstrained, so the `requires (x == y)` branch in `PipeVerify.inc:230` would hard-error); only the two comments that promise them "in P0.5" (`scripts/gen_pipe.py:163-165`, generated into `generated/PipeVerify.inc:232-238`) are re-worded to say P1. | Pure move; `MOBILEGL_PIPE_VERIFY` is not live until P1. |
| D9 | Gate skip semantics: a probe whose header does not exist prints `SKIP` and is counted; `--require-all` turns a SKIP into a failure. The negative control does not depend on either new header. CI and the ctest run **without** `--require-all` until the integrator flips it after all three packages land. | The task's "tolerate a not-yet-existing header" requirement, reconciled with `ROADMAP.md:7` (an all-skip run must not be a pass once the headers exist). |
| D10 | `.github/workflows/test.yml:3-8` triggers are NOT changed (`feat/disaggregated` is not a push trigger). The integrator runs `gh workflow run test.yml --ref feat/disaggregated` and records the run URL. | Adding the branch to the trigger list also fires the retrace jobs on every push; that is the plan owner's call, not this phase's. |
**Header paths and probe TUs (fixed contract; the gate package codes against these before the other two land):**
| probe | header (repo-relative) | probe TU text (exact) | forbidden path prefixes (repo-relative, after `normpath`) |
|---|---|---|---|
| `value-header` | `MobileGL/MG_Pipe/MGPipeValueTypes.h` | `#include <MG_Pipe/MGPipeValueTypes.h>\n` | `MobileGL/MG_State/`, `MobileGL/MG_Impl/`, `MobileGL/MG_Backend/`, `MobileGL/MG_Remote/` |
| `artifacts-header` | `MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` | `#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>\n` | `MobileGL/MG_State/GLState/ProgramState/ShaderObject.h`, `MobileGL/MG_Util/ShaderTranspiler/`, `MobileGL/Config.h`, `MobileGL/MG_Backend/`, `MobileGL/MG_State/GLState/BufferState/`, `MobileGL/MG_State/GLState/ProgramState/Shader` (prefix; catches `ShaderStage.h`, `ShaderCompileTask.h`, `ShaderCompileAdoptionMap.h`, `ShaderPreprocessCache.h`, `ShaderSourceKey.h`); text limit: `glslang::` ≤ 2 occurrences |
| `wire-header` (bonus, green today) | `MobileGL/MG_Remote/Transport/ITransport.h` | `#include <MG_Remote/Transport/ITransport.h>\n` | `MobileGL/Includes.h` (`WireLog.h:9-24` states the rule; `ITransport.h:33,35` include only `../Protocol/mg_protocol_base.h` and `<cstdint>`) |
The forbidden sets deliberately exclude glslang / spirv-cross / vulkan: `MobileGL/Includes.h:53,56,59,80-84,130` pulls `ska`, `xxhash`, `spirv_cross_c.h`, five glslang headers and `vulkan.h` unconditionally, and both new headers are allowed `<Includes.h>`. A "no glslang in the closure" assertion is unsatisfiable by construction; P7's criterion is `nm -D | grep glslang` on the server binary, a symbol gate.
---
### B.1 Package `p05-value-types` (tree `~/w7/p05-value-types`, branch `p05/value-types`)
**Files**
| action | file | what |
|---|---|---|
| CREATE | `MobileGL/MG_Pipe/MGPipeValueTypes.h` | the value header (layout below) |
| MODIFY | `MobileGL/MG_State/GLState/RenderState/RenderState.h` | replace `:11-12` with `#include <MG_Pipe/MGPipeValueTypes.h>`; delete `:15-370` (keep `:14 namespace MobileGL {`); `namespace MG_State { namespace GLState { class RenderState … } }` (`:372-538`) untouched |
| MODIFY | `MobileGL/MG_State/GLState/RenderState/RenderState.cpp` | add `#include <MG_State/GLState/FramebufferState/FramebufferObject.h>` after `:9` — **[correction]**: no scout saw that `RenderState.cpp:460,488,524,542,572,586` spell `MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS` while the .cpp includes only `RenderState.h`, `Log.h`, `Types.h` (`:9-11`) |
| MODIFY | `MobileGL/MG_State/GLState/SamplerState/SamplerObject.h` | replace `:11` (`VectorTypes.h`) with `#include <MG_Pipe/MGPipeValueTypes.h>`; delete `:14-97` (keep `:13 namespace MobileGL {`; `class SamplerObject` from `:99` unchanged — it uses `SamplerParameters`, `FloatVec4/IntVec4/UintVec4`, `BorderColorForm`, all now supplied by the value header) |
| MODIFY | `MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h` | add `#include <MG_Pipe/MGPipeValueTypes.h>` after `:12`; delete `:17-70` (`VertexAttribute`, `VertexBufferBindingPoint`, `VertexAttributeVersion`); keep `:11 "../BufferState/BufferObject.h"` (the class at `:72+` uses `BufferObject` 6 times) |
| MODIFY | `MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h` | add `#include <MG_Pipe/MGPipeValueTypes.h>` after `:13`; `:106` → `static constexpr Uint MAX_DRAW_BUFFERS = MobileGL::kMGMaxDrawBuffers;` |
| MODIFY | `MobileGL/MG_Pipe/MGPipeTypes.h` | `:33` → `#include "MGPipeValueTypes.h"`; keep `:32` (D1); rewrite the debt comment `:24-31`: the `MG_State` half is repaid, `MGPCaps`/`DynamicBackendParameters` remains (D1) and is what still keeps gate A off `MGPipeTypes.h`; `:36-40` `using` lines unchanged except the comment `:37-38` ("now live in MGPipeValueTypes.h") |
| MODIFY | `scripts/gen_pipe.py:163-165` + regenerate `MobileGL/MG_Pipe/generated/PipeVerify.inc` | comment only: "field lists of their own in P1 (P0.5 moved the types)"; run `python3 scripts/gen_pipe.py` and commit the regenerated `.inc` (CI `pipe-gates` diffs it, `test.yml:819-827`) |
| MODIFY | `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` | add the tests in "Tests" below; **must not gain any include** (`:17-18` are `"Includes.h"` + `<MG_Pipe/MGPipe.h>`; adding `RenderState.h` there would defeat the gate) |
| MODIFY (only if the build demands) | any `.cpp` that relied on `RenderState.h` transitively supplying `FramebufferObject.h`/`TextureObject.h`/`RenderbufferObject.h`/`SamplerObject.h` | add the direct include **to that .cpp**, immediately after its existing `MG_State/GLState/...` include; never re-add anything to `RenderState.h`. Candidates: the TUs including the four `MG_Util/Converters/*/RenderStateEnumConverter.h` or `MG_Util/Texture/PixelStoreProcessor.h` (`DirectGLES.cpp`, `VulkanRenderer.cpp`, `GL_Buffer.cpp`, `GL_Getter.cpp`, `GL_RenderState.cpp`, `GL_Texture.cpp`, `TextureTest.cpp`) — all also include `Core.h`, whose `:20 FramebufferState.h:12` keeps `FramebufferObject.h`, so most likely none. List every such file in the commit message. |
**`MGPipeValueTypes.h` layout** (one `namespace MobileGL` block, dependency order; every moved region is a verbatim cut, comments included):
```
// banner: copy MGPipeTypes.h:1-7, path = MobileGL/MG_Pipe/MGPipeValueTypes.h
#pragma once
#ifndef MOBILEGL_MG_PIPE_VALUE_TYPES_H // belt and braces: this file is reachable both as
#define MOBILEGL_MG_PIPE_VALUE_TYPES_H // <MG_Pipe/…> and <…> (CMakeLists.txt:531,535)
#include <Includes.h>
#include <MG_Util/Math/VectorTypes.h> // includes only <Includes.h> + <cstring> (VectorTypes.h:11,13)
#include <cstddef> // offsetof
#include <type_traits>
// PURITY: nothing from MG_State/, MG_Impl/, MG_Backend/, MG_Remote/ — scripts/check_include_closure.py
// probe "value-header" (ROADMAP P0.5; ARCHITECTURE.md:260, :501 gate A). Adding one turns CI red.
namespace MobileGL {
// GL_MAX_DRAW_BUFFERS as MobileGL advertises it. FramebufferObject::MAX_DRAW_BUFFERS is defined
// from this constant, so the two cannot drift.
inline constexpr Uint kMGMaxDrawBuffers = 8;
// RenderState.h:15-189 verbatim (11 enums, D2)
// RenderState.h:191-370 verbatim, with :263 and :273 respelled
// Array<PerBufferBlendState, kMGMaxDrawBuffers> BlendStates;
// Array<BoolVec4, kMGMaxDrawBuffers> ColorMasks;
// MEMBER ORDER UNCHANGED (DirectGLES.cpp:2070-2081 offsetof spans; PipeSpanTable.inc:34-59 names 24 members)
// SamplerObject.h:14-97 verbatim (6 enums incl. BorderColorForm : Uint8, SamplerParameters)
namespace MG_State::GLState {
class BufferObject; // BufferObject.h:61 forward-declares it the same way
// VertexArrayObject.h:17-70 verbatim: VertexAttribute, VertexBufferBindingPoint, VertexAttributeVersion
// (SharedPtr<BufferObject> members: NOT trivially copyable by design; MGPipeTypes.h:247-253 carries them as a blob)
}
// ---- trip wires (new; none existed for these before) ----
static_assert(std::is_trivially_copyable_v<PixelStoreParameters> && sizeof(PixelStoreParameters) == 28);
static_assert(std::is_trivially_copyable_v<PerBufferBlendState> && sizeof(PerBufferBlendState) == 28);
static_assert(std::is_trivially_copyable_v<StencilFaceState> && sizeof(StencilFaceState) == 28);
static_assert(std::is_trivially_copyable_v<RenderStateParameters>);
static_assert(std::is_standard_layout_v<RenderStateParameters>); // offsetof legality
static_assert(sizeof(RenderStateParameters) == 1168, "RenderStateParameters changed size; MGL_RESIDUAL_BLOCK_SIZE and the Espryt spans depend on it");
static_assert(offsetof(RenderStateParameters, BlendStates) < offsetof(RenderStateParameters, LogicOp));
static_assert(std::tuple_size_v<decltype(RenderStateParameters::BlendStates)> == kMGMaxDrawBuffers);
static_assert(std::is_trivially_copyable_v<SamplerParameters> && sizeof(SamplerParameters) == /*measure: expected 100*/);
static_assert(std::is_trivially_copyable_v<MG_State::GLState::VertexAttributeVersion> && sizeof(MG_State::GLState::VertexAttributeVersion) == 6);
} // namespace MobileGL
#endif
```
Measure a number before pinning it: compile once with `== 1`; clang/gcc print `note: expression evaluates to 'N == 1'`. If `is_standard_layout_v<RenderStateParameters>` fails (it should not: all members public, `Vec4` adds no data to `VecBase`, `VectorTypes.h:17,161`), drop that one assertion and say so in the commit — do not touch the vector types.
**Ordered steps**
1. Create the header by cutting the regions above (cut, never copy; the ODR grep in step 6 proves it).
2. Apply the five `MG_State` header edits and the `RenderState.cpp` include.
3. Apply the `MGPipeTypes.h` edit; `gen_pipe.py` comment + regenerate.
4. Build: `cmake --build ~/w7/p05-value-types/build-linux --parallel $(nproc)`. Resolve any lost-transitive-include failure by adding the direct include at the consumer (table above).
5. Add the tests; rebuild; run.
6. Verification commands (below) all green; commit as ONE commit: `[Refactor] (Pipe, State): extract MGPipeValueTypes.h - move the render-state, sampler and vertex value types and their enums out of MG_State/GLState so MG_Pipe no longer reaches RenderState.h; pure move, member order and namespaces unchanged`.
**Constraints**
- Byte-for-byte identical behaviour: no member reorder (Espryt spans `DirectGLES.cpp:2070-2081, 2660-2683`; `PipeSpanTable.inc:34-59` names 24 members by string for P2's `offsetof` table), no enum underlying-type change (`BorderColorForm : Uint8` is load-bearing for `SamplerParameters` tail padding), no `SizeT`→fixed-width widening, no renames, no namespace changes (D3), no reformatting beyond the re-indentation the cut itself needs (do not run `scripts/format_code.sh` over the touched files unless it is a no-op on the untouched ones).
- `MobileGL/MG_Backend/**` is not touched: all 51 `RenderStateParameters` uses in `DirectGLES.cpp`, the 17 in `VulkanRenderer.cpp`, the `Managers.h:1087,1825` `SamplerParameters` shadows, `Managers.h:744,788` `VertexAttribute`/`VertexAttributeVersion` compile unchanged (P1's `sed` counts at `ARCHITECTURE.md:344` depend on this — verify `grep -rc "pGLContext" MobileGL/MG_Backend | sort` is identical before/after).
- No `#include <MG_State/…>` or `<MG_Backend/…>` in the new header, ever; MG_Pipe never includes MG_State back.
- Existing test names unchanged; only additions.
**Tests to add** (`MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp`, `TEST(PipeCatalogue, …)`, no new includes):
- `ValueTypeLayoutsArePinned`: runtime `EXPECT_EQ` twins of the `sizeof`/`offsetof` static assertions above (so the numbers show up in `ctest` output on every platform, including ones where a static assertion is skipped).
- `ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail`: `offsetof(ResidualValueBlock, Pack) == sizeof(RenderStateParameters)` and `offsetof(ResidualValueBlock, CapabilityBits) == 1200` — pins that the move did not alter the carrier.
**Verification (WSL, in `~/w7/p05-value-types`)**
```
cmake --build build-linux --parallel $(nproc)
ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
ctest --test-dir build-linux -N | grep -E '^\s+Test #' | sed 's/^ *Test *#[0-9]*: //' | sort > /tmp/vt-after.txt
ctest --test-dir ~/w7/pipe/build-linux -N | grep -E '^\s+Test #' | sed 's/^ *Test *#[0-9]*: //' | sort > /tmp/vt-before.txt
comm -23 /tmp/vt-before.txt /tmp/vt-after.txt # must print nothing (no test name lost)
# ODR: exactly one definition each, all in the new header
grep -rn "^\s*struct \(RenderStateParameters\|PixelStoreParameters\|PerBufferBlendState\|StencilFaceState\|SamplerParameters\|VertexAttribute\|VertexBufferBindingPoint\|VertexAttributeVersion\)\b\|^\s*enum class \(BlendFactor\|BorderColorForm\|CapabilityInput\)\b" MobileGL
# purity (text)
grep -n "MG_State/\|MG_Backend/\|MG_Impl/\|MG_Remote/" MobileGL/MG_Pipe/MGPipeValueTypes.h # empty
grep -n "#include" MobileGL/MG_State/GLState/RenderState/RenderState.h # Includes.h + MGPipeValueTypes.h only
# closure (compiler; header must be self-contained)
printf '#include <MG_Pipe/MGPipeValueTypes.h>\n' > /tmp/probe_vt.cpp
clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -H -E -o /dev/null /tmp/probe_vt.cpp 2>&1 | grep -E '^\.+ ' | grep -c 'MobileGL/MG_State/\|MobileGL/MG_Backend/' # 0
clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -fsyntax-only /tmp/probe_vt.cpp
# once p05/include-gate exists (rebase onto it, or copy the script in): python3 scripts/check_include_closure.py --mode both --probe value-header
# backend read sites untouched
git diff --stat 6672778b -- MobileGL/MG_Backend # empty
# symbol report expectation for this package alone: 0 added / 0 removed / 0 resized / 0 renamed (see B.3 symbol_report.py)
python3 scripts/symbol_report.py --before ~/w7/pipe/build-linux/libMobileGL.so --after build-linux/libMobileGL.so
python3 scripts/gen_pipe.py --check
```
---
### B.2 Package `p05-program-artifacts` (tree `~/w7/p05-program-artifacts`, branch `p05/program-artifacts`)
**Files**
| action | file | what |
|---|---|---|
| CREATE | `MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` | the five types + `kInvalidUniformOffset` + `VisitFields` tables + tripwires (layout below) |
| MODIFY | `MobileGL/MG_State/GLState/ProgramState/ProgramObject.h` | commit 1: add `#include "ProgramArtifacts.h"` after `:11`; delete `:41-104` and insert the alias block; `:545-547` → `static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;` (keep the comment); delete `:1144-1171` (XfbVarying + its 2-line comment) and `:1173-1449` (the LinkArtifacts comment block, `LinkArtifacts`, the SpirvArtifacts comment block, `SpirvArtifacts`). Commit 2: delete `:14 #include <MG_Util/ShaderTranspiler/SpvcSession.h>` (the header names no `spvc_*`/`SpvReflect*`/`SpvcMetadata` symbol — verified by grep, the include line is the only match) |
| CREATE | `MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp` | tests below |
| MODIFY | `MobileGL/MG_Test/Program/CMakeLists.txt` | append a `ProgramArtifactsTest` stanza copied from the complete `AsyncCompileTest` stanza (`add_executable`, `target_include_directories` with `${MGL_ROOT}/include ${MGL_ROOT}/MobileGL`, `target_link_libraries(... GTest::gtest_main ${LINK_LIBRARIES})`, its `gtest_discover_tests(... DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)`) |
| MODIFY (commit 2, only if the build demands) | any `.cpp` that got `<spirv_reflect.h>`/`SpvcSession.h` transitively via `ProgramObject.h` | add `#include <MG_Util/ShaderTranspiler/SpvcSession.h>` **immediately after that TU's `ProgramObject.h` include line**. Candidates: `UniformManager.cpp`, `VulkanRenderer.cpp`, `ProgramInterface.cpp`, `ProgramFactory.h`, `ProgramTranslationCache.h/.cpp`, `ProgramLinkTask.h/.cpp`, `ProgramObject.cpp` (the other 17 `SpvcSession.h` includers already include it themselves) |
Includers of `ProgramObject.h` **[correction]**: 8 non-self files, in two spellings — `MG_Backend/DirectVulkan/Renderer/ProgramFactory.h:13`, `UniformManager.cpp:13`, `VulkanRenderer.cpp:17`, `MG_Impl/GLImpl/Program/ProgramInterface.cpp:11`, `ProgramLinkTask.h:11`, `ProgramTranslationCache.h:11` (as `<MG_State/GLState/ProgramState/ProgramObject.h>`), `ProgramPipelineObject.h:11`, `ProgramState.h:12` (as `"ProgramObject.h"`), plus `ProgramObject.cpp:9`. The roadmap's "7" is `ShaderObject.h`'s count. **None of the 8 needs an edit**: the in-class aliases keep every spelling (`ProgramObject::LinkArtifacts`, `ProgramObject::TypeFacts`, `ProgramObject::kInvalidUniformOffset` at `VulkanRenderer.cpp:4659,8553`, `GL_Program.cpp:1249,1304,1522`, `Core.cpp:513-514`, `ProgramSpirvTask.cpp:353,357,454`, the tests) compiling unchanged. Narrowing `ProgramTranslationCache.h`/`ProgramInterface.cpp` to the new header is a P7 follow-up, not P0.5.
**`ProgramArtifacts.h` layout**
```
// banner (copy from ProgramObject.h:1-7, path adjusted)
#pragma once
#include <Includes.h> // String/Vector/Array/UnorderedMap/SharedPtr + GL enums. DEBT NOTE (MGPipeTypes.h:24-31 style):
// Includes.h:80-84 still pulls glslang and :59 spirv_cross_c.h; this header is glslang-free BY SYMBOL
// (what P7's `nm -D | grep glslang` measures), not by preprocessed text. A textually glslang-free
// closure needs MG_Util/Types.h split off Includes.h (Types.h:11 includes it back) - out of scope.
#include <set> // std::set<String> (LinkArtifacts); NOT provided by Includes.h
// PURITY: no ShaderObject.h, no SpvcSession.h, nothing under MG_Util/ShaderTranspiler/, no Config.h, no MG_Backend/.
// scripts/check_include_closure.py probe "artifacts-header". `glslang::` appears exactly twice below (D5) and the gate pins that.
namespace MobileGL::MG_State::GLState {
// ProgramObject.h:545-547 -> namespace scope (SpirvArtifacts::reservedNumSamplesOffset defaults to it)
inline constexpr Uint kInvalidUniformOffset = ~0u;
// ProgramObject.h:41-72 verbatim struct TypeFacts
// ProgramObject.h:74-99 verbatim struct ResourceReflection
// ProgramObject.h:101-104 verbatim using UniformReflection/BlockReflection/PipeInputReflection/PipeOutputReflection
// ProgramObject.h:1144-1171 verbatim struct XfbVarying
// ProgramObject.h:1173-1393 verbatim (comment block + struct LinkArtifacts; the I5 paragraph :1184-1188 gets one added
// sentence: "m_artifacts lives in ProgramObject and is private there; the type being
// namespace-scope changes nothing about that gate")
// ProgramObject.h:1395-1449 verbatim (comment block + struct SpirvArtifacts)
// ---- the archive field tables (ARCHITECTURE.md:259): ONE table per type, serving both directions ----
// Visitor contract: v(const char* name, Field&). A visitor recurses into TypeFacts/XfbVarying/ResourceReflection
// by calling VisitFields on the element itself; the tables never recurse.
template <class Self, class V> requires std::same_as<std::remove_const_t<Self>, TypeFacts>
void VisitFields(Self& a, V&& v) { v("isArray", a.isArray); … v("basicType", a.basicType); } // 20
…ResourceReflection (14, `type` visited as a value)… XfbVarying (11)…
…LinkArtifacts: every member EXCEPT `program` (52 of 53) - `program` is null for every archived instance by
construction (ProgramTranslationCache.h:42-50) and must never be serialized…
…SpirvArtifacts (8)…
// ---- trip wires ----
static_assert(std::is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44); // 13 Bool + 3 pad + 7 x 4
#if defined(__GLIBCXX__) && !defined(_GLIBCXX_DEBUG) && (SIZE_MAX == UINT64_MAX)
#define MGL_RESOURCEREFLECTION_SIZE <measure>
#define MGL_XFBVARYING_SIZE <measure>
#define MGL_LINKARTIFACTS_SIZE <measure>
#define MGL_SPIRVARTIFACTS_SIZE <measure>
#elif defined(_LIBCPP_VERSION) && (SIZE_MAX == UINT64_MAX) && defined(MGL_ARTIFACT_SIZES_LIBCXX_PINNED)
// integrator pins these from the NDK build (see C.4); until then this branch is inert
#endif
#ifdef MGL_LINKARTIFACTS_SIZE
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
"LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) (and its serializer when one exists), then update this number");
… same for the other three …
#endif
}
```
`VisitFields` is a free constrained template rather than a member so the moved struct bodies stay verbatim, and a single table serves `const` and non-`const` (`Self` deduces either).
**`ProgramObject.h` alias block** (replaces `:41-104`, stays in the public section; a fully qualified RHS is mandatory — an unqualified `TypeFacts` would self-reference):
```
using TypeFacts = MobileGL::MG_State::GLState::TypeFacts;
using ResourceReflection = MobileGL::MG_State::GLState::ResourceReflection;
using UniformReflection = ResourceReflection;
using BlockReflection = ResourceReflection;
using PipeInputReflection = ResourceReflection;
using PipeOutputReflection = ResourceReflection;
using XfbVarying = MobileGL::MG_State::GLState::XfbVarying;
using LinkArtifacts = MobileGL::MG_State::GLState::LinkArtifacts;
using SpirvArtifacts = MobileGL::MG_State::GLState::SpirvArtifacts;
```
`MAX_UNIFORM_LOCATIONS` (`:39`, glslang-bound) and the inline accessors that spell `glslang::ElmRowMajor` (`:489,:502`) / `glslang::TQualifier::layoutLocationEnd` (`:1476`) stay in `ProgramObject.h`.
**Ordered steps**
1. Commit 1 (pure move): create the header (types + `kInvalidUniformOffset` only), apply the `ProgramObject.h` cuts + aliases + re-export; build; add the test file + CMake stanza (alias/POD tests only); run. Message: `[Refactor] (Program): extract ProgramArtifacts.h - move TypeFacts, ResourceReflection, XfbVarying, LinkArtifacts and SpirvArtifacts to namespace scope with in-class aliases so every existing spelling compiles unchanged; pure move`.
2. Commit 2: drop `ProgramObject.h:14`; build; fix consumers per the table; message `[Refactor] (Program): stop ProgramObject.h including SpvcSession.h - it names no spirv-cross or SPIRV-Reflect symbol`.
3. Commit 3: the `VisitFields` tables + tripwires + the field-count tests. Message `[Feat] (Program): add the reflection-archive field tables and sizeof trip wires to ProgramArtifacts.h`.
(Three commits so the symbol report attributes churn to commit 1 only, and any `SpvcSession.h` fallout is bisectable.)
**Constraints**
- Struct bodies verbatim (D5): both glslang-typed members stay; no field added/removed/reordered/re-typed; comments move with their structs.
- Do not leave a second definition behind (the classic failure: `GLState::TypeFacts` and `GLState::ProgramObject::TypeFacts` would both compile and silently split the type). The ODR grep below must show exactly one `struct` per name.
- I5 (`ProgramObject.h:1622-1631`): `m_artifacts`/`m_spirv` stay private; `Artifacts()`/`Spirv()` remain the only readers. Nothing in this package touches `ProgramObject.cpp`, `ProgramLinkTask.*`, `ProgramSpirvTask.*`, `MG_Backend/**` (except commit 2's include lines).
- Existing test names unchanged; `MG_Test/Program/*` test binaries untouched except the new one.
**Tests** (`ProgramArtifactsTest.cpp`; first include is `<MG_State/GLState/ProgramState/ProgramArtifacts.h>`, then `<gtest/gtest.h>`, then — for the alias checks only — `<MG_State/GLState/ProgramState/ProgramObject.h>`):
- `AliasesAreTheSameTypes`: `static_assert(std::is_same_v<ProgramObject::LinkArtifacts, LinkArtifacts>)` ×5, and `ProgramObject::kInvalidUniformOffset == kInvalidUniformOffset`.
- `TypeFactsIsPodOf44Bytes`.
- `VisitFieldsCoversEveryMember` (commit 3): counting visitor → `TypeFacts` 20, `ResourceReflection` 14, `XfbVarying` 11, `LinkArtifacts` 52, `SpirvArtifacts` 8; `const` and non-`const` instances count the same.
- `SizesArePinnedOnThisToolchain`: `EXPECT_EQ(sizeof(LinkArtifacts), MGL_LINKARTIFACTS_SIZE)` under the same `#if`, plus an unconditional `RecordProperty("sizeof_LinkArtifacts", …)` (and the other three) so the number is visible in every `ctest -V` log on every platform (this is how the integrator obtains the libc++ numbers).
**Verification (WSL, in `~/w7/p05-program-artifacts`)**
```
cmake --build build-linux --parallel $(nproc)
ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
# test-name diff vs ~/w7/pipe exactly as in B.1 (comm -23 must be empty)
grep -rn "^\s*struct \(TypeFacts\|ResourceReflection\|XfbVarying\|LinkArtifacts\|SpirvArtifacts\)\b" MobileGL # 5 hits, all in ProgramArtifacts.h
grep -n "#include" MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h # <Includes.h> and <set> only
grep -c "glslang::" MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h # 2 (D5)
printf '#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>\n' > /tmp/probe_pa.cpp
clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -H -E -o /dev/null /tmp/probe_pa.cpp 2>&1 | grep -E '^\.+ ' | grep -E 'ShaderObject.h|SpvcSession.h|MG_Util/ShaderTranspiler/|MobileGL/Config.h|MobileGL/MG_Backend/|BufferState/' # empty
clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -fsyntax-only /tmp/probe_pa.cpp
git diff --stat 6672778b -- MobileGL/MG_Backend MobileGL/MG_Impl # empty after commit 1 (commit 2 may add include lines)
# symbol report expectation: 0 added / 0 removed / 0 resized after --strip-scope; all churn is ProgramObject::X -> X renames
python3 scripts/symbol_report.py --before ~/w7/pipe/build-linux/libMobileGL.so --after build-linux/libMobileGL.so \
--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::' --only-names 'TypeFacts,ResourceReflection,XfbVarying,LinkArtifacts,SpirvArtifacts'
```
---
### B.3 Package `p05-include-gate` (tree `~/w7/p05-include-gate`, branch `p05/include-gate`)
Compiles and passes against the base tree (probes A/B `SKIP`, C `OK`, self-test `OK`) and needs nothing from the other two packages.
**Files**
| action | file | what |
|---|---|---|
| CREATE | `scripts/check_include_closure.py` | the closure gate (spec below) |
| CREATE | `scripts/symbol_report.py` | the nm/`.text` attribution (spec below) |
| CREATE | `MobileGL/MG_Test/Purity/CMakeLists.txt` | ctest wiring (below) |
| MODIFY | `MobileGL/MG_Test/CMakeLists.txt` | after `:90 add_subdirectory(Pipe)`: a 2-line comment in the `:88-89` voice + `add_subdirectory(Purity)` |
| MODIFY | `.github/workflows/test.yml` | new job `include-graph-check` inserted after `flatc-check` (after `:323`, before `:325 benchmark`); name reserved by `ARCHITECTURE.md:568` |
**`scripts/check_include_closure.py` — specification**
- `REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))` (`gen_pipe.py:37` idiom); `argparse` with `description=__doc__`; every stdout line prefixed `include-closure: `; failures end with a GitHub `::error::` line and `sys.exit(1)` (`test.yml:829-846` house style).
- `PROBES` module-level table exactly as in B.0 (Name, Header, Tu, Forbidden, Allow=[] — policy: an allow entry needs a `Reason` string printed in the summary and a PR justification, `test.yml:831-835` — TextLimits, Why).
- Flags: `--mode {text,clang,both}` (default `text`), `--compiler PATH` (default order: `$CXX`, `clang++-20`, `clang++`, `c++`, `g++`; print which one was used), `--compile-commands PATH` (optional: take any entry whose `file` is under `MobileGL/`, keep `-D/-I/-isystem/-std`, drop `-o/-c/-MD/-MF/-MT` and the input), `--probe NAME` (repeatable), `--self-test`, `--require-all`, `--json PATH`.
- Default clang flags when no compile-commands (measured to reproduce the full-flag closure exactly, 666 lines): `-std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -H -E -o /dev/null`, cwd = `REPO_ROOT`. Before running clang mode, check `include/ska/flat_hash_map.hpp`, `3rdparty/xxHash/xxhash.h`, `3rdparty/Vulkan-Headers/include/vulkan/vulkan.h` exist; if not, fail with `run from a full checkout: git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers` (never silently downgrade).
- `-H` parsing: stderr only; accept only lines matching `^(\.+) (.*)$` (g++ appends a "Multiple include guards may be useful for:" trailer that must be discarded); depth = dot count, so the chain to a violation is reconstructed by walking back to depth−1; `path = os.path.normpath(os.path.join(cwd, path))` before prefix matching (today's output contains `TextureState/../SamplerState/SamplerObject.h`); paths outside the repo are ignored.
- Text mode: transitive walk of literal `#include` lines (there are no macro-driven includes under `MobileGL/`: 3377 includes, all literal). Resolution: `"…"` → including file's directory first; then `include/`, `MobileGL/`, `MobileGL/MG_Pipe/`, `3rdparty/xxHash/`, `3rdparty/Vulkan-Headers/include/`; unresolved = leaf. Blind to `#if`; the compiler mode is the arbiter. In `--mode both` a disagreement between the modes' violation sets is itself a failure that prints both sets.
- Second assertion per probe in clang mode: `-fsyntax-only` on the probe TU (self-contained header), reported on its own line.
- Missing header (D9): `include-closure: <probe> SKIP header not present yet: <path>`; counted in the summary; with `--require-all` a SKIP is a failure.
- Summary lines (always): `include-closure: <probe> OK|SKIP|FORBIDDEN <n> headers in closure, <m> forbidden (mode=…, cxx=…)`, then `include-closure: <k> probes, <s> skipped, <p> problem(s)`. On a violation print the chain, one hop per line, indented.
- Assert that the manifest contains both `value-header` and `artifacts-header` — deleting a probe is red.
- `--self-test` (always run by the ctest and the CI job; must not touch any tracked file — synthesize TUs in `tempfile.mkdtemp()`):
1. Negative control independent of P0.5: TU `#include <MG_Pipe/MGPipeHandles.h>\n#include <MG_State/GLState/RenderState/RenderState.h>\n` checked with the `value-header` forbidden list → must report ≥1 violation with `RenderState.h` at depth 1.
2. When `MGPipeValueTypes.h` exists: TU `#include <MG_Pipe/MGPipeValueTypes.h>\n#include <MG_State/GLState/RenderState/RenderState.h>\n` → must trip (the roadmap's literal "人为加回一个 MG_State include 能变红").
3. When `ProgramArtifacts.h` exists: TU with `#include <MG_State/GLState/ProgramState/ShaderObject.h>` added → must trip on the `artifacts-header` list; and a canned copy of the header with a third `glslang::` token must trip the text limit.
4. Parser unit checks: a canned g++ `-H` transcript with trailer lines parses to the dot lines only; a `../`-spelled path normalises and matches.
5. Each of 1-3 runs in every enabled mode. Zero violations anywhere → `::error:: negative control did not trip: the closure gate is not checking anything`, exit 1.
**`MobileGL/MG_Test/Purity/CMakeLists.txt`**
```
cmake_minimum_required(VERSION 3.14)
# The P0.5 include-closure assertions (ROADMAP P0.5; ARCHITECTURE.md:501 gate A): no MobileGL
# compilation, no GL context. Text mode only here, because this CTestTestfile also runs on the
# `test` job's runner (test.yml:149-203), which has no submodules; the compiler-backed run is the
# include-graph-check job and the developer's local `--mode both`.
find_package(Python3 COMPONENTS Interpreter)
if (Python3_Interpreter_FOUND)
add_test(NAME MobileGLPurity.IncludeClosure
COMMAND ${Python3_EXECUTABLE} ${MGL_ROOT}/scripts/check_include_closure.py --mode text --self-test)
# ctest -L unit (test.yml:198-203). NO ENVIRONMENT property: it replaces the job env (ARCHITECTURE.md:567).
set_tests_properties(MobileGLPurity.IncludeClosure PROPERTIES LABELS unit)
else()
message(STATUS "MobileGLPurity.IncludeClosure not registered: no python3 interpreter (Windows is not a correctness gate)")
endif()
```
The integrator later appends `--require-all` (C.3). The `test` job's path rewrite (`test.yml:185-196`) only touches the cmake binary path; the script's absolute path is the same on both runners.
**CI job** (insert after `test.yml:323`):
```
# P0.5 interface-purity gate A (ARCHITECTURE.md:501): the two extracted headers' include closure,
# asserted on `-H` output because `nm --undefined-only` is blind to "included but not called".
# Needs a preprocessor and three header submodules, no CMake configure, no glslang sources -
# independent of build-linux for the same reason pipe-gates is.
include-graph-check:
name: Include-closure purity gate
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Check out the three header submodules the closure needs
# ska/flat_hash_map.hpp, xxhash.h and vulkan/vulkan.h are the only submodule headers
# Includes.h reaches; glslang and spirv_cross are vendored under include/.
run: git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers
- name: Install clang
run: sudo apt-get update && sudo apt-get install -y clang-20
- name: Include-closure assertions and negative control
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test
```
(`--require-all` appended by the integrator, C.3.)
**`scripts/symbol_report.py` — specification** (informational; `ARCHITECTURE.md:507`; name `monolith-symbol-report` from `:568`; no nm/size tooling exists in the tree today)
- `--before <.so>` `--after <.so>` `[--nm nm] [--size size] [--cxxfilt c++filt] [--markdown out.md] [--json out.json] [--strip-scope PREFIX]* [--rename-map OLD=NEW]* [--only-names A,B,…] [--threshold 0] [--self-test]`.
- Per side: `nm --defined-only -S <so>` → `{mangled: (type, size)}`; demangle via `c++filt` for the normalised key; apply `--strip-scope`/`--rename-map` textual substitutions to the demangled name (this folds `ProgramObject::LinkArtifacts` → `LinkArtifacts` everywhere, including template arguments such as `std::vector<ProgramObject::ResourceReflection>`); `size --format=sysv <so>` for `.text`/`.data`/`.bss`/Total.
- Buckets sorted by |Δ|: removed, added, resized, renamed-only (same normalised name, byte-identical size — the pure-move signature), unchanged (count). `--only-names` restricts the listing to symbols whose demangled text contains one of the names.
- Prints `symbol-report: .text B -> A (+d, +p%)` and `symbol-report: N -> M defined symbols: a added, r removed, s resized, n renamed`, then a Markdown table; always exit 0; `--fail-on-added-bytes N` reserved for a future hard gate.
- Docstring guard rails: same `CMAKE_BUILD_TYPE` on both sides (visibility presets differ, `CMakeLists.txt:578-598`), `MOBILEGL_ENABLE_LTO` OFF on both (`:108,137-146`), same compiler; print both `.so` paths and sizes in the header.
- `--self-test`: two tiny canned `nm -S` transcripts → expected buckets.
**Verification (WSL, in `~/w7/p05-include-gate`, base tree — no new headers yet)**
```
python3 scripts/check_include_closure.py --mode both --self-test # value-header SKIP, artifacts-header SKIP, wire-header OK, self-test OK, exit 0
python3 scripts/check_include_closure.py --mode both --self-test --require-all; echo exit=$? # exit 1, message names the two missing headers
python3 scripts/check_include_closure.py --mode text --self-test # exit 0 (this is what the ctest runs)
python3 scripts/check_include_closure.py --mode both --compiler g++ --self-test # trailer-line handling, exit 0
# red-for-its-reason, by hand: temporarily add `#include <MG_State/GLState/RenderState/RenderState.h>` to
# MobileGL/MG_Remote/Transport/ITransport.h -> probe wire-header FORBIDDEN with the printed chain; revert.
cmake -S . -B build-linux && ctest --test-dir build-linux -R MobileGLPurity --output-on-failure
ctest --test-dir build-linux -L unit -N | grep -c MobileGLPurity # 1 (the label reaches ctest -L unit)
python3 scripts/symbol_report.py --self-test
python3 scripts/symbol_report.py --before ~/w7/pipe/build-linux/libMobileGL.so --after build-linux/libMobileGL.so # 0/0/0/0 on an unchanged tree
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/test.yml'))" # syntax only
```
Commits: `[CI] (Purity): add scripts/check_include_closure.py - the -H include-closure gate for the P0.5 headers with an always-on negative control, wired as a unit ctest and the include-graph-check job` and `[Tooling] (Purity): add scripts/symbol_report.py - per-symbol nm/.text attribution between two libMobileGL.so builds`.
---
### B.4 File-ownership table (the guarantee that the three trees never touch the same file)
| file / glob | value-types | program-artifacts | include-gate | integrator |
|---|---|---|---|---|
| `MobileGL/MG_Pipe/MGPipeValueTypes.h` (new) | owner | – | – | – |
| `MobileGL/MG_Pipe/MGPipeTypes.h`, `scripts/gen_pipe.py`, `MobileGL/MG_Pipe/generated/*.inc` | owner | – | – | – |
| `MobileGL/MG_State/GLState/{RenderState/RenderState.h, RenderState/RenderState.cpp, SamplerState/SamplerObject.h, VertexArrayState/VertexArrayObject.h, FramebufferState/FramebufferObject.h}` | owner | – | – | – |
| `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` | owner | – | – | – |
| `MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` (new), `ProgramObject.h` | – | owner | – | – |
| `MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp` (new), `MobileGL/MG_Test/Program/CMakeLists.txt` | – | owner | – | – |
| `scripts/check_include_closure.py`, `scripts/symbol_report.py` (new), `MobileGL/MG_Test/Purity/**` (new) | – | – | owner | – |
| `MobileGL/MG_Test/CMakeLists.txt`, `.github/workflows/test.yml` | – | – | owner | flips `--require-all` after all land |
| `MobileGL/MG_Backend/**`, `MobileGL/MG_Impl/**`, `MobileGL/MG_Util/**`, other `MG_State/**` | include-line additions only, only if the build demands, placed after the TU's existing `MG_State/GLState/...` include | include-line additions only (commit 2), placed after the TU's `ProgramObject.h` include line | – | – |
| `docs/Disaggregated/*.md` | – | – | – | owner |
| `tools/**`, `android-plugin/**`, root `CMakeLists.txt` | nobody | nobody | nobody | nobody |
The one shared-file possibility is the "include-line additions" row: the two packages add at different anchors of a TU, so the hunks do not overlap; the integrator resolves an adjacent-hunk conflict by keeping both lines.
---
## C. Integration order and what the integrator runs
C.0 Preconditions: all three branches green on their own verification lists; `~/w7/pipe` clean at `6672778b` (fixture binaries hidden as in `wsl_integrate.sh`); capture the base library: `cp ~/w7/pipe/build-linux/libMobileGL.so ~/w7/p05-before-libMobileGL.so` (Release, `/usr/sbin/clang++`, built at `6672778b` — verify with `git -C ~/w7/pipe log -1` and `grep CMAKE_BUILD_TYPE ~/w7/pipe/build-linux/CMakeCache.txt`); capture `ctest -N` names and `grep -rc "pGLContext" MobileGL/MG_Backend | sort > ~/w7/p05-before-pglcontext.txt`.
C.1 Order: **include-gate → value-types → program-artifacts.** The gate first, so probes A and B flip from SKIP to OK as each header lands and the integrator runs the real gate on each merge. The p05 trees are worktrees of `~/w7/pipe` (`git worktree list` shows them; branches are local), so the P0 helper's fetch step is unnecessary; per slug:
```
cd ~/w7/pipe && git checkout -q feat/disaggregated
git checkout -q p05/<slug> && git rebase feat/disaggregated # conflict => abort, report, stop
git checkout -q feat/disaggregated && git merge --ff-only p05/<slug>
cmake --build build-linux --parallel $(nproc) && ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
python3 scripts/check_include_closure.py --mode both --self-test # after value-types: A OK; after artifacts: A+B OK
```
(`scratchpad/wf2/wsl_integrate.sh` is the P0 shape of this loop; it hard-codes `p0/` and `~/w7/p0-<slug>` and fetches — copy it to `wsl_integrate_p05.sh` with the prefix changed and the fetch removed rather than editing it.)
C.2 After all three: `python3 scripts/check_include_closure.py --mode both --self-test --require-all` (exit 0); `ctest -N` name diff against the C.0 capture (`comm -23` empty; additions listed in the merge message); `git diff --stat 6672778b -- MobileGL/MG_Backend` shows include-line additions only (or nothing); `grep -rc "pGLContext" MobileGL/MG_Backend | sort | diff - ~/w7/p05-before-pglcontext.txt` empty; `python3 scripts/gen_pipe.py --check`.
C.3 Flip the ratchet (one commit, integrator-owned): append `--require-all` to the ctest COMMAND in `MobileGL/MG_Test/Purity/CMakeLists.txt` and to the CI step in `test.yml`; message `[CI] (Purity): require both P0.5 closure probes now that the headers exist`.
C.4 Symbol report: `python3 scripts/symbol_report.py --before ~/w7/p05-before-libMobileGL.so --after build-linux/libMobileGL.so --strip-scope 'MobileGL::MG_State::GLState::ProgramObject::' --markdown ~/w7/p05-symbol-report.md`. Expected: `.text` unchanged (the library gains no code: the `VisitFields` templates are uninstantiated outside the test binary, all moved types are declarations); 0 added / 0 removed / 0 resized after normalisation; N renamed. Anything else is attributed line by line in the report before the `dev` merge. Paste the two summary lines into the merge commit. Android/libc++ sizes: build the plugin APK once (`gradle -p android-plugin :app:assemblePluginRelease`, see `apk.yml:78`) with a temporary `static_assert(sizeof(LinkArtifacts) == 1)` (and the other three) to read the values from the NDK clang notes, then pin the `_LIBCPP_VERSION` branch of `ProgramArtifacts.h` and define `MGL_ARTIFACT_SIZES_LIBCXX_PINNED` there; message `[Test] (Program): pin the libc++ artifact sizes`.
C.5 CI and docs: `gh workflow run test.yml --ref feat/disaggregated` (and `apk.yml` if dispatchable) — record run URLs; `include-graph-check`, `pipe-gates`, `test` green. Then docs (integrator-owned): `docs/Disaggregated/README.md:3` status → P0.5 landed with the commit hash; `ROADMAP.md:16` ✅ with the "7 includers" wording corrected to "8 includers, none edited (in-class aliases)"; `ARCHITECTURE.md:260` note that `DynamicBackendParameters` stayed (D1) and its `:563` stale citation `MG_Test/CMakeLists.txt:93-95` → `:100-103`. Then `[Merge]` to `dev` per the milestone flow, and schedule the full `gl44to46` caselist (`ROADMAP.md:34`) on the two devices per the Wave-7 protocol — device time, not on the integration critical path.
---
## D. Risks and mitigations
| risk | mitigation |
|---|---|
| **ODR — a struct copied, not moved.** Two same-named definitions in `namespace MobileGL` (value types) compile per-TU and silently break Espryt's `offsetof` shadow if they drift; for the artifacts, `GLState::TypeFacts` vs `GLState::ProgramObject::TypeFacts` compile as two different types. | The per-package ODR greps (exactly one `struct X` per name) are mandatory verification, and the alias `static_assert(is_same_v<…>)` in `ProgramArtifactsTest.cpp` catches the artifacts case at compile time. |
| **Include cycles.** MG_State now includes MG_Pipe (`RenderState.h`, `SamplerObject.h`, `VertexArrayObject.h`, `FramebufferObject.h` → `MGPipeValueTypes.h`). | `MGPipeValueTypes.h` includes only `<Includes.h>`/`VectorTypes.h` and forward-declares `BufferObject`; the gate makes any MG_State include in it red; MG_Pipe is "永远进构建" and below MG_State (`ARCHITECTURE.md:550-561`), so the direction is the intended one. `ProgramArtifacts.h` includes nothing from `ProgramState/`, so `ProgramObject.h → ProgramArtifacts.h` cannot cycle. |
| **Lost transitive includes.** `RenderState.h` stops dragging `FramebufferObject.h → TextureObject.h → RenderbufferObject.h → SamplerObject.h …` (8 headers) into the 73 `Core.h` consumers and its 5 direct includers; `ProgramObject.h` (commit 2) stops dragging `SpvcSession.h`/`<spirv_reflect.h>`. | Only the compiler enumerates the fallout; the rule is "add the direct include at the consumer, never at the forwarding header", one anchor per package (B.4), listed in the commit message. `RenderState.cpp` is the one known case (fixed in the package). |
| **MSVC / Android not built locally.** `test.yml` builds Linux only (clang-20, `build-linux`); `apk.yml` builds the NDK APK; neither runs on `feat/disaggregated` pushes; the Windows box is not a correctness gate. | Nothing in P0.5 is platform-specific: no `SizeT` widening (D1/D5), no language feature beyond C++20 constraints (`requires std::same_as`; fine on MSVC 19.3x and NDK r27 clang), `offsetof` on `RenderStateParameters` is already compiled on all three toolchains by `DirectGLES.cpp`. Container-bearing `sizeof` assertions are libstdc++-only until the integrator pins libc++ (C.4); MSVC is unasserted. The integrator's `workflow_dispatch` of `apk.yml` is the Android build gate; a `cmake --build` in the Windows worktree before the `dev` merge is cheap and recommended, not required. |
| **glslang types leaking by value in the archive.** `LinkArtifacts::program` and `uniformInitialValues` keep glslang types (D5). | Symbol-free by construction (type-erased deleter; aggregate with inline implicit members); the gate pins `glslang::` at exactly 2 occurrences in `ProgramArtifacts.h`, so a third leak is red; `VisitFields(LinkArtifacts)` excludes `program` and the future deserializer must assert it null (`ProgramTranslationCache.h:42-50` already asserts that at insert). The `UniformInitializer` re-typing is the recorded P7 follow-up, together with narrowing `ProgramTranslationCache.h`/`ProgramInterface.cpp` to the new header and moving `MAX_UNIFORM_LOCATIONS`' glslang RHS behind a client-side assert. |
| **`PipeInputs` (P1) needs.** `ARCHITECTURE.md:328`: accessors return "exactly the types the backend reads today"; `:344`: 293 `pGLContext->` sites + 58 non-arrow lines are `sed`'d mechanically; `ResidualValueBlock` needs `offsetof` computability; `MOBILEGL_PIPE_VERIFY` keeps its `MG_State` includes (`:369`). | D3 keeps every type's qualified name; the value header keeps standard layout and asserts it; `MG_Backend/**` is diff-empty apart from possible include lines; the `pGLContext` count is captured before/after (C.0/C.2); the value header coexists with `MG_State` includes (no mutual exclusion). `PipeFields.def` field lists for the value types are P1 work (D8), and `MEMCMP_FALLBACK_TYPES` in `gen_pipe.py:166-171` stays as is until then. |
| **`RenderStateParameters` is about to change in P2** (`FramebufferSrgb`/`DepthClamp` storage, `ROADMAP.md:18,78`), moving `sizeof` off 1168. | After P0.5 every 1168-dependent number lives in exactly two places: the value header's assertions and `MGL_RESIDUAL_BLOCK_SIZE` (`MGPipeTypes.h:535`); the test twins name both. P2 updates both in one commit. |
| **A gate that is green because it never ran.** `feat/disaggregated` is not a trigger branch (`test.yml:3-8`); a mislabelled ctest never runs under `ctest -L unit`. | `LABELS unit` verified by `ctest -L unit -N`; the ctest runs in every local build; the integrator dispatches the workflow and records the URL (D10); `--self-test` is always on, so a broken parser cannot pass silently; `--require-all` after integration makes SKIP red. |
| **`#pragma once` with two include spellings** (`<MG_Pipe/X.h>` and `<X.h>`, `CMakeLists.txt:531,535`) on case-insensitive/symlinked Windows worktrees. | The macro guard in `MGPipeValueTypes.h`. |
| **`nm` attribution looks catastrophic** because de-nesting renames every mangled name mentioning the five artifact types (including every `std::vector<…>` instantiation). | `symbol_report.py --strip-scope` folds them into the renamed-only bucket with byte-identical sizes; the value-types package contributes zero churn by D3, so the two packages are separable in the report by construction. |
-24
View File
@@ -1,24 +0,0 @@
# P0.5 — 值头与制品头抽取(`5d99ee43`)
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(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)。
## 摘要
- 落地:`MG_Pipe/MGPipeValueTypes.h`(不 include `MG_State/GLState`)、`ProgramArtifacts.h`、`Visit()` 归档 + `sizeof` 绊线、CI `-H` include 闭包断言(`scripts/check_include_closure.py`)与 `scripts/symbol_report.py`。
- 门:测试名零删除、`.text` 不变、符号 0 增 / 0 删 / 42 重命名。没有这一步,server 不链 glslang 的判据不可达。
## 阶段表行(原 `ROADMAP.md`)
- **阶段**:**P0.5** 值头与制品头抽取
- **状态**:✅ `5d99ee43`
- **落地什么 / 范围**:`MG_Pipe/MGPipeValueTypes.h`;`ProgramArtifacts.h`;`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言(`scripts/check_include_closure.py`)+ `scripts/symbol_report.py`
- **验收门 / 证据**:测试名零删除、`.text` 不变、符号 0 增 / 0 删 / 42 重命名
## 本目录
| 文件 | 内容 |
|---|---|
| [`BRIEF-P05.md`](BRIEF-P05.md) | P0.5 implementation brief — value header + artifacts header + include-closure gate |
| [`p05-results/`](p05-results/) | 6 个文件:三个包(value-types / program-artifacts / include-gate)的实现与评审稿 |
捞回的独有材料:[`../recovered/wf3/`](../recovered/wf3/)(四份 scout)。
@@ -1,95 +0,0 @@
# Adversarial review — package `p05-include-gate` (v1)
Tree `~/w7/p05-include-gate`, branch `p05/include-gate` = `fe3dc1dd` + `2318f6ae` on `6672778b`.
Reviewer stance: refute "correct, complete, pure, meets the brief (A, B.0, B.3, C, D)".
Verdict: **approved — 0 majors, 9 minors.** Tree left exactly as found (`git status --porcelain --untracked-files=all | grep -v build-linux/` empty before and after every experiment).
All scripts used are under `scratchpad/wf3/p05-include-gate-review/s01..s07.sh`; long outputs under `~/w7/review-p05g/`.
## 1. Scope and purity of the diff
`git diff --stat 6672778b..HEAD` = exactly the five B.4-assigned files, 1089 insertions, 0 deletions:
`.github/workflows/test.yml` (+26), `MobileGL/MG_Test/CMakeLists.txt` (+3), `MobileGL/MG_Test/Purity/CMakeLists.txt` (+15), `scripts/check_include_closure.py` (+659), `scripts/symbol_report.py` (+386).
`git diff --stat 6672778b -- MobileGL/MG_Backend MobileGL/MG_State MobileGL/MG_Pipe` empty. No fixture binaries. No CRLF (`xargs grep -lU $'\r'` empty). No attribution trailers in either commit message (`git log --format=%B | grep -i "co-authored\|signed-off"` empty); subjects are `[Type] (Scope): …` with `- ` bullets.
No C++ was added, so "copied vs moved", member order, alignment, constexpr-ness and static_assert coverage are not applicable to this package; the symbol report on the rebuilt library is 0/0/0/0 (section 3).
## 2. Spec conformance (B.3) — line-by-line against `scripts/check_include_closure.py`
| B.3 item | where | verdict |
|---|---|---|
| `REPO_ROOT` idiom, `description=__doc__`, `include-closure: ` prefix, `::error::` + exit 1 | `:62,:64,:161-166,:544,:651-653` | as specified |
| `PROBES` table with Name/Header/Tu/Forbidden/Allow/TextLimits/Why, values identical to B.0 | `:72-118` | byte-compared against the B.0 table: identical (six forbidden prefixes for artifacts incl. the `ProgramState/Shader` prefix, `glslang::` ≤ 2) |
| flags `--mode/--compiler/--compile-commands/--probe/--self-test/--require-all/--json` | `:546-558` | all present; default mode `text` |
| compiler order `$CXX, clang++-20, clang++, c++, g++`, printed | `:141,:286-303,:591` | as specified |
| default flags = the measured 6, cwd = REPO_ROOT | `:125-132,:576` | identical to the brief's line |
| prereq check that refuses rather than downgrades | `:135-139,:340-345` | verified: without the submodules, `--mode clang` exits 1 with the exact `git submodule update --init …` message (s06) |
| `-H` parse: stderr only, `^(\.+) (.*)$`, normpath, outside-repo ignored | `:156,:169-194,:348-352` | as specified; g++ trailer verified live and by canned transcript |
| text mode: literal walk, `"…"` includer-dir first then the 5 search dirs, unresolved = leaf | `:145-151,:197-241` | as specified |
| `--mode both` disagreement is a failure printing both sets | `:624-631` | verified (experiment B3) |
| `-fsyntax-only` second assertion, own line | `:410-424,:632-634` | verified |
| SKIP semantics + `--require-all` | `:603-615` | verified (section 4) |
| manifest assertion for both required probes | `:121,:561-566` | present |
| self-test 1–5, tempdir only, never touches a tracked file | `:431-540,:572` | verified incl. controls 2/3 and the third-`glslang::` control, which only arm when the headers exist (experiments B1, C1) |
| CMake stanza | `MG_Test/Purity/CMakeLists.txt` | verbatim to the brief; inserted after `add_subdirectory(Pipe)` at `MG_Test/CMakeLists.txt:91-93` |
| CI job after `flatc-check`, before `benchmark`, steps verbatim | `test.yml:325-349` | YAML parses (Windows PyYAML): 13 jobs, neighbours `['flatc-check','include-graph-check','benchmark']`, no `needs`, triggers untouched (D10) |
`scripts/symbol_report.py`: every flag in the spec list exists (`:269-289`); `nm --defined-only -S` / `c++filt` / `size --format=sysv` (`:131-138`); buckets sorted by |Δ| (`:189-191`); the two summary lines + Markdown (`:324-363`); always exit 0 (`:382`); guard rails in the docstring (`:24-31`); `--self-test` with two canned transcripts (`:209-263`).
Declared deviations (result file §3, items 1–6) were each checked and are accepted: `--strip-scope` de-nesting is the only semantics that produces the fold the brief asks for (a literal prefix delete would put the two spellings in removed/added); one SKIP line per probe; disagreement check on probes only; the extra normalised-names line; local clang 22/g++ 16 vs CI clang-20; cosmetic YAML spacing.
## 3. Independent re-run of the verification list (all in `~/w7/p05-include-gate`, s04)
```
cmake --build build-linux -j 16 -> [47/47] … (incremental, clean)
ctest --test-dir build-linux -L unit --no-tests=error -j 8
-> 1459 tests, 0 failed (2 skipped LogLevel.* as on base)
ctest -N name diff vs ~/w7/pipe/build-linux -> lost: (none); added: MobileGLPurity.IncludeClosure ; 1327 -> 1328
ctest --test-dir build-linux -L unit -N | grep -c MobileGLPurity -> 1
python3 scripts/gen_pipe.py --check -> generated files are up to date, exit 0
python3 scripts/check_include_closure.py --mode both --self-test -> exit 0, "3 probes, 2 skipped, 0 problem(s)", 2 trips
python3 scripts/check_include_closure.py --mode text --self-test -> exit 0, 1 trip
python3 scripts/check_include_closure.py --mode both --self-test --require-all -> exit 1, two "SKIP is a failure under --require-all" lines
python3 scripts/check_include_closure.py --mode both --compiler g++ --self-test -> exit 0, 2 trips
python3 scripts/symbol_report.py --self-test -> OK (2 canned transcripts, 5 buckets)
python3 scripts/symbol_report.py --before ~/w7/pipe/build-linux/libMobileGL.so --after build-linux/libMobileGL.so
-> .text 10792579 -> 10792579 (+0); 27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed; 27060 unchanged
ODR grep (8 value-type structs) -> 8 hits, all still in MG_State (nothing moved by this package, as expected)
git status -> clean
```
Generated CTestTestfile (`build-linux/MobileGL/MG_Test/Purity/CTestTestfile.cmake`): `add_test(MobileGLPurity.IncludeClosure "<python3>" ".../MobileGL/MG_Test/../../scripts/check_include_closure.py" "--mode" "text" "--self-test")`, `LABELS "unit"`, no ENVIRONMENT. `build-linux/MobileGL/MG_Test/CTestTestfile.cmake:20 subdirs("Purity")`, and the runtime tarball packs `${BUILD_DIR}/MobileGL/MG_Test` whole (`test.yml:126`), so the `test` job's `ctest -L unit` reaches it the same way it reaches `Pipe`.
## 4. Gate semantics and red-for-its-reason (s05, every edit restored by an EXIT trap)
**A. Manual negative control on the live probe.** `sed` inserted `#include <MG_State/GLState/RenderState/RenderState.h>` after `ITransport.h:35`; `--mode both --probe wire-header` → exit 1, both modes `wire-header FORBIDDEN … 1 forbidden` with the identical 3-hop chain `ITransport.h → RenderState.h → Includes.h` (76 / 672 headers in closure vs 2 / 42 clean). `git checkout --` restored; 0 dirty lines.
**B. Probe A exercised with a scratch `MobileGL/MG_Pipe/MGPipeValueTypes.h`** (never run before; deleted afterwards):
- pure (`<Includes.h>`, `<MG_Util/Math/VectorTypes.h>`, `<cstddef>`, `<type_traits>`): `value-header OK 66 / 654 headers, 0 forbidden`, SELF-CONTAINED OK, **control-2 arms and trips in both modes** (9 violations, RenderState.h at depth 1), `3 probes, 1 skipped, 0 problem(s)`, exit 0. So the B.0 forbidden set is satisfiable with `Includes.h` in the closure, and the brief's layout will go green.
- `RenderState.h` added back: both modes `FORBIDDEN … 9 forbidden`, chain printed, exit 1 — the roadmap's literal red.
- `<MG_Backend/BackendObject.h>` (the D1 route): `FORBIDDEN … 2 forbidden` both modes — the gate would catch `DynamicBackendParameters` being moved after all.
**C. Probe B exercised with a scratch `ProgramArtifacts.h`** (`<Includes.h>`, `<set>`, exactly two `glslang::`): `artifacts-header OK 65 / 653, 0 forbidden`, SELF-CONTAINED OK, control-3 trips in both modes (16 violations, ShaderObject.h at depth 1), text-limit control trips (`glslang:: occurs 3 times, budget is 2`), exit 0. Then `#include "ShaderObject.h"` (quoted spelling) → `FORBIDDEN … 16 forbidden` with chain; a third `glslang::` in a comment → `FORBIDDEN`, `text limit: glslang:: occurs 3 times`. `MobileGL/Config.h` is not reached via `Includes.h` (0 forbidden), so the Config.h rule is satisfiable too.
**D. CI file-set simulation** (s06): tracked files of `MobileGL include scripts` only + the three submodules linked in (exactly what `include-graph-check` sees) → `--mode both --self-test` exit 0 with the same 42-header wire closure and 2 trips. Every header the closure reaches outside the submodules (`include/EGL/egl.h`, `include/GL/*`, `include/GLES3/gl32.h`, `include/glslang/*`, `include/spirv_cross/spirv_cross_c.h`) is `git ls-files`-tracked, so the job does not depend on `update_glslang_sources.py` or system GL headers. Same set without the submodules (the `test` job's checkout) → `--mode text --self-test` exit 0, and `--mode clang` refuses with the submodule message. `apt-get install -y clang-20` is the same mechanism `build-linux` already uses (`test.yml:61`), so the install step is no less reliable than the existing lane.
## 5. Majors
None.
## 6. Minors (none blocks; ordered by usefulness to the integrator)
1. **`flags_from_compile_commands` corrupts a two-token `-isystem DIR`** (`check_include_closure.py:327-330`): the path token is dropped and the bare `-isystem` then swallows the next flag. Repro: canned entry `-DFOO=1 -isystem /opt/sys/include -I…/MobileGL -std=gnu++23` → `['-DFOO=1', '-isystem', '-I/…/MobileGL', '-std=gnu++23']`. Latent here: this tree's `compile_commands.json` uses no `-isystem` (Init.cpp entry checked), and neither CI nor the ctest passes `--compile-commands`. Fix: treat `-isystem` like `-o` (keep both tokens).
2. **`--require-all` only inspects selected probes**: `--mode text --probe wire-header --require-all --self-test` → exit 0 (`1 probes, 0 skipped, 0 problem(s)`) with both P0.5 headers absent (`main:593-615`). Not reachable from the CI/ctest command lines, which never pass `--probe`, but the ratchet has a hole for a hand-run. `--probe value-header --require-all` correctly exits 1.
3. **Text-only ctest lane has no arbiter for `#if`-guarded includes**: an `#if 0`-guarded `RenderState.h` include in the value header gives `FORBIDDEN 9 forbidden` in text mode and `OK` in clang; `--mode both` reports MODE DISAGREEMENT (2 problems). This is the brief's design, but it means the unit ctest can go red for something the compiler never includes. Rule for the two header authors: no conditionally compiled includes of forbidden paths, ever.
4. **`count_text_limits` counts comments** (`:272-283`): the brief's own suggested comment line for `ProgramArtifacts.h` ("`glslang::` appears exactly twice below (D5)") is itself a third token and turns probe B red (experiment C2). The program-artifacts author must spell it without the `::`, or the integrator sees a spurious FORBIDDEN on the first real run.
5. **`tempfile.mkdtemp` is never removed** (`:572`); two `/tmp/mgl-include-closure-*` directories were left behind by the review runs. Harmless on CI runners; a `shutil.rmtree` in a `finally` would do.
6. **ERROR + disagreement interplay**: when clang-mode preprocessing fails for a probe, `run_probe` returns `set()` (`:387`) and the disagreement check (`:624-631`) can print a spurious MODE DISAGREEMENT on top of the ERROR. Cosmetic; the run is red either way.
7. **`symbol_report.py` folding hides a count change**: with `--strip-scope`, the copied-not-moved case (both `ProgramObject::LinkArtifacts::Reset()` and `LinkArtifacts::Reset()` present after) surfaces as `resized +16`, not `added`, and the per-name `count` (1→2) is not in any table (`:150-157,:161-192`). Visible, but mislabelled; the ODR grep stays the primary defence, as D says.
8. **`flags_from_compile_commands` takes the first entry whose absolute path contains `MobileGL/`** (`:311-313`): on the Windows main worktree (`…/FoldCraftLauncher/MobileGL/`) every entry matches, including 3rdparty TUs. WSL trees are unaffected; only matters for a developer using the flag on Windows.
9. **Manifest assertion is two literals in one file** (`:121` vs `:72-118`): it guards against accidental deletion of a probe, not against a deliberate edit that removes both. Acceptable for its stated purpose ("deleting a probe is red").
## 7. Notes for the integrator (not findings)
- Probes A and B have now been exercised end-to-end with scratch headers built from the brief's own include lists (section 4 B/C); both go OK, their controls arm, and each forbidden rule was tripped at least once. Expect `1 skipped` after value-types lands and `0 skipped` after program-artifacts.
- The `--require-all` flip (C.3) is one appended token in each of `MG_Test/Purity/CMakeLists.txt:10` and `test.yml:349`.
- `clang++-20` is absent in WSL; local runs use clang 22 / g++ 16 and produce the same 42-header wire closure as the brief's measured defaults.
@@ -1,280 +0,0 @@
# P0.5 package `p05-include-gate` — result (v1)
Tree `~/w7/p05-include-gate`, branch `p05/include-gate`, based on `feat/disaggregated @ 6672778b`.
Nothing pushed. Neither P0.5 header exists in this tree, so probes A and B are SKIP by design (D9).
## 1. Commits
| sha | subject |
|---|---|
| `fe3dc1dd` | `[CI] (Purity): add scripts/check_include_closure.py - the -H include-closure gate for the P0.5 headers with an always-on negative control, wired as a unit ctest and the include-graph-check job` |
| `2318f6ae` | `[Tooling] (Purity): add scripts/symbol_report.py - per-symbol nm/.text attribution between two libMobileGL.so builds` |
`git diff --stat 6672778b HEAD`:
```
.github/workflows/test.yml | 26 ++
MobileGL/MG_Test/CMakeLists.txt | 3 +
MobileGL/MG_Test/Purity/CMakeLists.txt | 15 +
scripts/check_include_closure.py | 659 +++++++++++++++++++++++++++++++++
scripts/symbol_report.py | 386 +++++++++++++++++++
5 files changed, 1089 insertions(+)
```
Exactly the five files the B.4 ownership table assigns to this package. Working tree clean apart
from the untracked `build-linux/`. All new/modified files are LF (`grep -lU $'\r'` empty).
Absolute paths:
- `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/scripts/check_include_closure.py`
- `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/scripts/symbol_report.py`
- `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/MobileGL/MG_Test/Purity/CMakeLists.txt`
- `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/MobileGL/MG_Test/CMakeLists.txt`
- `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/.github/workflows/test.yml`
## 2. Verification — every command and its actual output
Environment: WSL Arch, `python3` 3.14.4, `/usr/sbin/clang++` = clang 22.1.6, `/usr/sbin/g++` = GCC
16.1.1. `clang++-20` does not exist in WSL, so the local runs used clang 22 and g++ 16; the CI job
still pins `clang++-20` as the brief specifies.
### 2.1 `python3 scripts/check_include_closure.py --mode both --self-test` → exit 0
```
include-closure: compiler: /usr/sbin/clang++
include-closure: value-header SKIP header not present yet: MobileGL/MG_Pipe/MGPipeValueTypes.h
include-closure: artifacts-header SKIP header not present yet: MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h
include-closure: wire-header OK 2 headers in closure, 0 forbidden (mode=text, cxx=-)
include-closure: wire-header OK 42 headers in closure, 0 forbidden (mode=clang, cxx=/usr/sbin/clang++)
include-closure: wire-header SELF-CONTAINED OK (-fsyntax-only, cxx=/usr/sbin/clang++)
include-closure: self-test: negative controls (must trip) in mode(s) text, clang
include-closure: self-test parser: trailer discarded, `../` normalised and matched
include-closure: self-test control-1 (independent of P0.5) [text]: tripped, 9 violation(s), MobileGL/MG_State/GLState/RenderState/RenderState.h at depth 1
include-closure: self-test control-1 (independent of P0.5) [clang]: tripped, 9 violation(s), MobileGL/MG_State/GLState/RenderState/RenderState.h at depth 1
include-closure: self-test: 2 negative-control trip(s), parser checks OK
include-closure: 3 probes, 2 skipped, 0 problem(s)
```
This is the required shape: **A SKIP, B SKIP, C OK, self-test OK, exit 0**. The two modes agreed on
the wire-header violation set (empty), so the `--mode both` disagreement check stayed silent.
### 2.2 `--mode text --self-test` (what the ctest runs) → exit 0
Same three probe lines, `self-test: 1 negative-control trip(s), parser checks OK`,
`3 probes, 2 skipped, 0 problem(s)`. No compiler and no submodules touched.
### 2.3 `--mode both --self-test --require-all` → **exit 1**, names both missing headers
```
include-closure: value-header SKIP header not present yet: MobileGL/MG_Pipe/MGPipeValueTypes.h
include-closure: value-header SKIP is a failure under --require-all: MobileGL/MG_Pipe/MGPipeValueTypes.h does not exist
include-closure: artifacts-header SKIP header not present yet: MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h
include-closure: artifacts-header SKIP is a failure under --require-all: MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h does not exist
include-closure: wire-header OK 2 headers in closure, 0 forbidden (mode=text, cxx=-)
include-closure: wire-header OK 42 headers in closure, 0 forbidden (mode=clang, cxx=/usr/sbin/clang++)
include-closure: wire-header SELF-CONTAINED OK (-fsyntax-only, cxx=/usr/sbin/clang++)
...
include-closure: 3 probes, 2 skipped, 2 problem(s)
::error::include-closure gate failed: 2 problem(s); see the chains above
exit=1
```
### 2.4 `--mode both --compiler g++ --self-test` → exit 0 (trailer-line handling)
```
include-closure: compiler: /usr/sbin/g++
include-closure: wire-header OK 33 headers in closure, 0 forbidden (mode=clang, cxx=/usr/sbin/g++)
include-closure: wire-header SELF-CONTAINED OK (-fsyntax-only, cxx=/usr/sbin/g++)
include-closure: self-test: 2 negative-control trip(s), parser checks OK
include-closure: 3 probes, 2 skipped, 0 problem(s)
```
g++ appends a `Multiple include guards may be useful for:` paragraph of bare `/usr/include/...`
paths to its `-H` output (confirmed by hand: 702 stderr lines, of which the trailer is ~40). The
`^(\.+) (.*)$` filter discards it, and `--self-test`'s canned-transcript check pins that behaviour
independently of which compiler is installed.
### 2.5 Red-for-its-reason (the manual negative control on the one live probe)
Temporarily added `#include <MG_State/GLState/RenderState/RenderState.h>` to
`MobileGL/MG_Remote/Transport/ITransport.h`, then `python3 scripts/check_include_closure.py
--mode both --probe wire-header --self-test`:
```
include-closure: wire-header FORBIDDEN 76 headers in closure, 1 forbidden (mode=text, cxx=-)
include-closure: forbidden: MobileGL/Includes.h (rule MobileGL/Includes.h) reached by:
include-closure: MobileGL/MG_Remote/Transport/ITransport.h
include-closure: MobileGL/MG_State/GLState/RenderState/RenderState.h
include-closure: MobileGL/Includes.h
include-closure: wire-header FORBIDDEN 672 headers in closure, 1 forbidden (mode=clang, cxx=/usr/sbin/clang++)
include-closure: forbidden: MobileGL/Includes.h (rule MobileGL/Includes.h) reached by:
include-closure: MobileGL/MG_Remote/Transport/ITransport.h
include-closure: MobileGL/MG_State/GLState/RenderState/RenderState.h
include-closure: MobileGL/Includes.h
include-closure: 1 probes, 0 skipped, 1 problem(s)
::error::include-closure gate failed: 1 problem(s); see the chains above
exit=1
```
Both modes went red with an identical three-hop chain; the closure grew 2 → 76 (text) and 42 → 672
(clang). Reverted with `git checkout --`; `git status --porcelain` for that path is empty.
### 2.6 Other flag paths exercised
- `--probe wire-header --json /tmp/p05g.json`: exit 0, JSON carries `modes`, per-mode `results`
(`headers` 2 / 42, `forbidden` 0), `skipped`, `problems`, `self_test`, `require_all`.
- `--probe nope`: `::error::unknown probe(s): nope`, exit 1.
- `--compile-commands build-linux/compile_commands.json --mode clang --probe wire-header`:
`flags from build-linux/compile_commands.json (27 tokens), cwd=/home/swung/w7/p05-include-gate/build-linux`,
then `wire-header OK 42 headers in closure, 0 forbidden` — the same closure as the default flags,
which is the point of having measured the defaults.
- Manifest assertion: `REQUIRED_PROBE_NAMES = ("value-header", "artifacts-header")` is checked
before anything else runs, so deleting a probe is red.
### 2.7 ctest wiring
```
$ cmake -S . -B build-linux # exit 0
$ ctest --test-dir build-linux -R MobileGLPurity --output-on-failure
Start 1064: MobileGLPurity.IncludeClosure
1/1 Test #1064: MobileGLPurity.IncludeClosure .... Passed 0.08 sec
100% tests passed, 0 tests failed out of 1
Label Time Summary:
unit = 0.08 sec*proc (1 test)
$ ctest --test-dir build-linux -L unit -N | grep -c MobileGLPurity
1
```
Full unit suite (`ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure -j 8`):
```
100% tests passed, 0 tests failed out of 1459
Label Time Summary: unit = 106.08 sec*proc (1459 tests)
Total Test time (real) = 13.42 sec
```
Test-name diff against the read-only reference tree `~/w7/pipe`:
```
comm -23 before after -> (empty; no test name lost)
comm -13 before after -> MobileGLPurity.IncludeClosure
1327 before / 1328 after
```
### 2.8 `.github/workflows/test.yml`
`python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))"` was run with the **Windows**
interpreter against a copy of `//wsl.localhost/Arch/home/swung/w7/p05-include-gate/.github/workflows/test.yml`
(WSL's python has no `yaml`):
```
parsed OK; jobs: 13
order around flatc-check: ['integration', 'flatc-check', 'include-graph-check', 'benchmark']
include-graph-check present: True
name: Include-closure purity gate
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v6
- name: Check out the three header submodules the closure needs
run: git submodule update --init include/ska 3rdparty/xxHash 3rdparty/Vulkan-Headers
- name: Install clang
run: sudo apt-get update && sudo apt-get install -y clang-20
- name: Include-closure assertions and negative control
run: python3 scripts/check_include_closure.py --mode both --compiler clang++-20 --self-test
```
Inserted immediately after `flatc-check` and before `benchmark`, as B.3 requires. Triggers
untouched (D10).
### 2.9 `scripts/symbol_report.py`
`python3 scripts/symbol_report.py --self-test` → `symbol-report: self-test: OK (2 canned
transcripts, 5 buckets)`, exit 0. The canned pair pins removed / added / resized / renamed-only /
unchanged, including that a de-nested member folds to *renamed*, not to added+removed.
Two real builds — `~/w7/pipe/build-linux/libMobileGL.so` (read-only reference) vs this tree's
`build-linux/libMobileGL.so`, both Release / `/usr/sbin/clang++` / LTO off:
```
symbol-report: before: /home/swung/w7/pipe/build-linux/libMobileGL.so (19101056 bytes on disk)
symbol-report: after : build-linux/libMobileGL.so (19101056 bytes on disk)
symbol-report: .text 10792579 -> 10792579 (+0, +0.000%)
symbol-report: .data 76824 -> 76824 (+0)
symbol-report: .bss 1296872 -> 1296872 (+0)
symbol-report: .rodata 1534938 -> 1534938 (+0)
symbol-report: Total 17209819 -> 17209819 (+0)
symbol-report: 27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed
symbol-report: 27060 -> 27060 normalised names, 27060 unchanged (name, size and mangling all identical)
```
**0 added / 0 removed / 0 resized / 0 renamed** on an unchanged tree, as expected (this package
adds no C++). `--markdown` output was also written and checked.
Synthetic `--strip-scope` rename case (two purpose-built .so files, `N::Outer::Inner::f` de-nested
to `N::Inner::f`, plus an untouched `N::Stable`):
```
without --strip-scope : 17 -> 17 defined symbols: 1 added, 1 removed, 0 resized, 0 renamed
with --strip-scope 'N::Outer::' :
17 -> 17 defined symbols: 0 added, 0 removed, 0 resized, 1 renamed, 16 unchanged
### Renamed only (same size) (1)
| normalised symbol | before mangling | after mangling |
| N::Inner::f(int) | _ZN1N5Outer5Inner1fEi | _ZN1N5Inner1fEi |
```
That is exactly the shape the `p05-program-artifacts` package's report is supposed to have.
## 3. Deviations from the brief, with reasons
1. **`--strip-scope` semantics.** The brief describes it as a textual substitution that "folds
`ProgramObject::LinkArtifacts` → `LinkArtifacts`". A literal *delete the whole prefix* would
break the fold: the before side becomes `LinkArtifacts::Reset()` while the after side is
`MobileGL::MG_State::GLState::LinkArtifacts::Reset()`, so the two would land in the
removed/added buckets — the opposite of the intent. Implemented as a **de-nesting**:
`--strip-scope 'A::B::C::'` rewrites every `A::B::C::X` to `A::B::X` (it deletes only the final
class component), which produces the stated fold and works inside template arguments too. The
behaviour is documented in `--help` and pinned by `--self-test`; C.4's command line is unchanged.
2. **One SKIP line per probe, not one per mode.** The brief's summary-line template carries a
`mode=` field, but whether a header exists is mode-independent; printing it twice under
`--mode both` was noise and double-counted the skip. A SKIP is now emitted once, before either
mode runs, and counted once. `--require-all` adds its own explicit line naming the header.
3. **The mode-disagreement check is applied to probes only, not to the self-test controls.** For a
probe, a text/clang divergence is a real signal and fails the run (implemented, prints both
sets). For the negative controls, whose TUs deliberately drag in the whole `MG_State` tree, text
mode's blindness to `#if` can legitimately produce a different violation *set* from clang's; the
self-test therefore requires each control to trip **in every enabled mode** with the expected
depth-1 path, which is the property that matters, rather than comparing sets.
4. **Extra output line in `symbol_report.py`.** `27799 -> 27799 defined symbols` and
`27060 unchanged` are different denominators (several mangled symbols fold to one normalised
demangled name). Added an explicit `N -> M normalised names` line so the two numbers cannot be
misread as 739 lost symbols.
5. **Local compiler.** `clang++-20` does not exist in WSL, so the local `--mode both` runs used
clang 22 (default resolution order) and g++ 16 (explicit `--compiler g++`). The CI job pins
`clang++-20` exactly as the brief specifies; the default compiler search order in the script is
`$CXX, clang++-20, clang++, c++, g++` as written.
6. **Cosmetic YAML.** The CI job got a blank line between `runs-on:` and `steps:` and a longer
comment block than the brief's snippet, to match the voice and spacing of the neighbouring
`flatc-check` / `benchmark` jobs. Steps and commands are verbatim from the brief.
No other deviation. `--fail-on-added-bytes` is accepted and documented as reserved (spec says
"reserved for a future hard gate"), `Allow` entries require a printed `Reason`, and the
`--json` / `--markdown` / `--only-names` / `--threshold` / `--rename-map` flags are all implemented.
## 4. Unfinished / handed to the integrator
- **The `--require-all` ratchet (C.3) is deliberately not flipped.** Both call sites
(`MobileGL/MG_Test/Purity/CMakeLists.txt` `add_test` COMMAND and the `include-graph-check` step
in `test.yml`) are one appended flag away; verified today that the flag produces exit 1 with a
message naming both headers.
- **Probes A and B have never run against a real header** — they cannot, in this tree. Their
forbidden lists, the `glslang::` ≤ 2 text limit, self-test controls 2 and 3 and the
"third `glslang::` token" text-limit control are all coded and arm themselves automatically the
moment `MGPipeValueTypes.h` / `ProgramArtifacts.h` appear. The integrator should see A flip to OK
after merging `p05/value-types` and B after `p05/program-artifacts` (C.1), and should treat an
all-SKIP run at that point as a failure.
- **CI has not run** (D10): `feat/disaggregated` is not a push trigger, so the new job needs
`gh workflow run test.yml --ref feat/disaggregated` from the integrator, who records the URL.
- **The `gl44to46` caselist run** owed by `ROADMAP.md:34` is device time and out of this package.
- Nothing was pushed; both commits are local on `p05/include-gate`.
@@ -1,114 +0,0 @@
# p05-program-artifacts — adversarial review v1
Reviewed: tree `~/w7/p05-program-artifacts`, branch `p05/program-artifacts`, HEAD `77beb7d6` (3 commits on `6672778b`: `a180dc57`, `3ce1264c`, `77beb7d6`).
Against: `BRIEF-P05.md` (A, B.0, B.2, C, D) and `p05-results/program-artifacts-v1.md`.
Every command below was re-run by me in WSL Arch from the package tree (scripts `scratchpad/wf3/p05-review-pa/s1..s10.sh`, raw outputs under `~/w7/rv-pa/`). `~/w7/pipe` was used read-only (`ctest -N`, `nm`, one run of one existing integration binary). The tree was left exactly as found: `git status --short` (excluding `build-linux/`) empty before and after, HEAD unchanged, `~/w7/pipe` clean.
**Verdict: APPROVED — 0 majors, 6 minors.** The package is a pure move that meets the brief; every claim in the result file that matters reproduced, and every gate it adds goes red for its reason.
---
## 1. What I tried to refute, and what I found
### 1.1 "Pure move" — copied vs moved, verbatim bodies, semantics
| check | command | observed |
|---|---|---|
| Struct bodies verbatim | `git show 6672778b:ProgramObject.h` regions `:41-104`, `:1144-1171`, `:1173-1449` vs `git show a180dc57:ProgramArtifacts.h` `:28-400`, both with leading whitespace stripped and blank lines dropped, `diff` | 7 hunks, all declared: 5 comment lines `glslang::X` → `glslang X` (deviation 1 in the result file), the one added I5 sentence the brief itself prescribes (B.2 layout), and the closing `} // namespace` line. **No field added, removed, reordered, re-typed, no default changed.** |
| ODR: one definition per name | `grep -rn "^\s*struct \(TypeFacts\|ResourceReflection\|XfbVarying\|LinkArtifacts\|SpirvArtifacts\)\b" MobileGL` | exactly 5 hits, `ProgramArtifacts.h:31,63,95,160,359` |
| `kInvalidUniformOffset` one definition | `grep -rn "kInvalidUniformOffset\s*=" MobileGL` | `ProgramArtifacts.h:26` (`inline constexpr Uint = ~0u`) and `ProgramObject.h:496` defined *from* it; the old `= ~0u` in class is gone |
| Alias identity actually enforced | scratch farm `~/w7/rv-pa/neg4f`: replaced the `using TypeFacts = …` alias in a copy of `ProgramObject.h` with a second `struct TypeFacts {…}` body, compiled `ProgramArtifactsTest.cpp` by hand with the farm first on the include path | `ProgramArtifactsTest.cpp:29: error: static assertion failed … is_same_v<ProgramObject::TypeFacts, GLState::TypeFacts>` (plus `ProgramObject.h:464` no-viable-conversion). The copy-left-behind failure the brief's risk table names is caught at compile time. |
| Behaviour unchanged (strongest evidence) | own `symcmp.py` (`nm --defined-only -S` + `c++filt`, fold `GLState::ProgramObject::{5 names}` → `GLState::X`, multiset compare) on `~/w7/pipe/build-linux/libMobileGL.so` (built 22:06 at `6672778b`, commit time 22:13 — same tree state) vs `build-linux/libMobileGL.so` | `symbols 30511 -> 30511; 43 names folded on the before side, 0 on the after side; added 0 removed 0 resized 1`; `.text 10792579 -> 10792579`, `.rodata`, `.data`, `.bss`, `Total 17209819` all byte-identical. The 1 resized is `typeinfo name for std::_Sp_counted_ptr_inplace<…LinkArtifacts,…>` 122 → 107 bytes: the RTTI name *string* lost `13ProgramObject`. Fully attributed. Byte-identical `.text` also rules out any ADL/overload-resolution shift from de-nesting. |
| No mangled-name dependence anywhere | `grep -rn "typeid\|type_info\|__PRETTY_FUNCTION__\|__FUNCSIG__" MobileGL` (non-test) | empty |
| B.0 D5 (glslang members verbatim, count pinned) | `grep -o "glslang::" ProgramArtifacts.h \| wc -l` (occurrences, not lines) | `2` — `:165 SharedPtr<glslang::TProgram> program;`, `:232 Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;` |
| B.0 D6 (includes) | `grep -n "#include" ProgramArtifacts.h` | `:10 <Includes.h>`, `:15 <set>` — nothing else |
| B.0 D7 (sizes) | header `:179-203`; test prints `sizeof: TypeFacts=44 ResourceReflection=128 XfbVarying=128 LinkArtifacts=1056 SpirvArtifacts=88` | TypeFacts pinned unconditionally at 44 with `is_trivially_copyable_v`; the four container-bearing sizes under `__GLIBCXX__ && !_GLIBCXX_DEBUG && SIZE_MAX == UINT64_MAX`; libc++ `#elif` inert behind `MGL_ARTIFACT_SIZES_LIBCXX_PINNED`; MSVC unasserted. Exactly the brief's shape. |
| I5 untouched | `git diff --stat 6672778b..HEAD` | only 4 files: `ProgramArtifacts.h` (new), `ProgramObject.h`, `MG_Test/Program/CMakeLists.txt`, `ProgramArtifactsTest.cpp`. `MG_Backend/**`, `MG_Impl/**`, `ProgramObject.cpp`, `ProgramLinkTask.*`, `ProgramSpirvTask.*` untouched; `m_artifacts`/`m_spirv` and `Artifacts()`/`Spirv()` not in the diff. |
| Includers of `ProgramObject.h` | repo-wide grep (both spellings) | the 8 non-self includers the brief lists + `ProgramObject.cpp` + the new test; none edited; all compile through the in-class aliases. |
### 1.2 Tests exist, run under the right label, and can fail
| check | command | observed |
|---|---|---|
| Incremental build | `cmake --build build-linux -j 16` (`CCACHE_BASEDIR=/home/swung/w7`) | `ninja: no work to do.` |
| Unit suite | `ctest --test-dir build-linux -L unit --no-tests=error -j 12` | `100% tests passed, 0 tests failed out of 1463`; 3 `***Skipped` (`LogLevel.DebugBuildKeepsEverything`, `LogLevel.AssertIsLiveInDebugBuilds`, `XfbFrontendOrderInvarianceTest.DumpBSpirvForDisassembly`) — skip-by-design, same as base |
| No test name lost | `ctest -N` (pattern `'^\s+Test\s+#'` — the implementer is right that the brief's single-space pattern drops tests < #1000) on both trees, `comm -23 before after` | before 2326, after 2331, **lost: none**, added: exactly `ProgramArtifacts.{AliasesAreTheSameTypes,SizesArePinnedOnThisToolchain,TypeFactsIsPodOf44Bytes,VisitFieldsCoversEveryMember,VisitFieldsPassesConstnessThrough}` |
| Label reaches `ctest -L unit` | `ctest -L unit -N \| grep -c ProgramArtifacts` | `5` |
| CMake wiring executes | `MG_Test/CMakeLists.txt:85 add_subdirectory(Program)` unconditional; stanza `Program/CMakeLists.txt:280-297` = the `AsyncCompileTest` shape with `${LINK_LIBRARIES}`, `gtest_discover_tests(… DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit)`; CI `test` job configures `-DMOBILEGL_BUILD_TEST=ON` (`test.yml:86`) and runs `ctest -L unit` | wired, discovered, runs |
| **Red-for-its-reason C1** — member added without a table entry | scratch copy `~/w7/rv-pa/neg1/…/ProgramArtifacts.h` with `Int addedWithoutATableEntry = 0;` appended to `LinkArtifacts`; `clang++ -I~/w7/rv-pa/neg1 -std=gnu++23 -Iinclude -IMobileGL … -fsyntax-only probe_pa.cpp` | exit 1: `error: static assertion failed … 'sizeof(LinkArtifacts) == 1056': LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) …` / `note: expression evaluates to '1064 == 1056'` |
| **Red-for-its-reason C2** — table entry dropped | symlink farm `~/w7/rv-pa/neg2f` (`cp -rs MobileGL`, header replaced by a copy minus `v("linkStatus", a.linkStatus);`); compiled `ProgramArtifactsTest.cpp` with its exact `compile_commands.json` line + `-I<farm>` first, linked with the ninja link line, ran | `ProgramArtifactsTest.cpp:84: Failure … Which is: 56 / Which is: 57`, plus `:110` (`linkStatus` write-through) fails; exit 1. Same hand pipeline on the **unmodified** header (`~/w7/rv-pa/pos2f`): `[ PASSED ] 2 tests` — so the failure is the dropped entry, not the pipeline. |
| **Red-for-its-reason C3** — purity | farm `~/w7/rv-pa/fake2` with `#include "ShaderObject.h"` inserted at header `:15`; (a) manual `-H` probe with the brief's grep; (b) the real gate from `~/w7/p05-include-gate/scripts/check_include_closure.py` copied under the farm's `scripts/` | (a) hits: `MobileGL/Config.h`, `MG_Backend/BackendObject.h`, `MG_Backend/BackendObjects.h`, `…/BackendObject_DirectGLES.h`, `…/BackendObject_DirectVulkan.h`, `BufferState/BufferObject.h`; (b) `artifacts-header FORBIDDEN 89 headers in closure, 16 forbidden (mode=text)` / `FORBIDDEN 678 headers, 16 forbidden (mode=clang)`, chain printed one hop per line, `::error::…`, exit 1 |
| Table completeness vs struct bodies (independent of the counting test) | python cross-check: member names parsed from each struct body vs `v("name", a.name)` entries | TypeFacts 20/20, ResourceReflection 14/14, XfbVarying 11/11, LinkArtifacts members 58 / table 57 (**only `program` missing**), SpirvArtifacts 8/8; every `"name"` string equals its member, every table in declaration order |
### 1.3 The closure gate, on the real tree
The gate lives in `p05/include-gate`; I ran that script (copied, unmodified) against this tree through a symlinked root `~/w7/rv-pa/fake/{MobileGL,include,3rdparty → tree}` so the tree itself was not touched:
```
include-closure: value-header SKIP header not present yet: MobileGL/MG_Pipe/MGPipeValueTypes.h
include-closure: artifacts-header OK 65 headers in closure, 0 forbidden (mode=text, cxx=-)
include-closure: artifacts-header OK 653 headers in closure, 0 forbidden (mode=clang, cxx=/usr/sbin/clang++)
include-closure: artifacts-header SELF-CONTAINED OK (-fsyntax-only, cxx=/usr/sbin/clang++)
include-closure: wire-header OK …
include-closure: self-test control-3 (ShaderObject.h added back to the artifacts header's TU) [text]: tripped, 16 violation(s) … [clang]: tripped, 16 violation(s) …
include-closure: self-test text limit: tripped as expected (glslang:: occurs 3 times, budget is 2)
include-closure: 3 probes, 1 skipped, 0 problem(s) exit 0
--require-all: exit 1 (value-header absent — expected until p05/value-types lands)
--probe artifacts-header alone: OK/OK/SELF-CONTAINED OK, exit 0
```
Manual equivalent (brief B.2): `-H -E` probe → 653 closure lines, 0 matches for `ShaderObject.h|SpvcSession.h|MG_Util/ShaderTranspiler/|MobileGL/Config.h|MobileGL/MG_Backend/|BufferState/`; the only `MobileGL/` headers in the closure are `Defines.h`, `Includes.h`, `ProgramArtifacts.h`, `MG_Util/Debug/Log.h`, `MG_Util/GLExtensions.h`, `MG_Util/PlatformStubs.h`, `MG_Util/Types.h`. `-fsyntax-only` OK. `python3 scripts/gen_pipe.py --check` → `generated files are up to date`.
### 1.4 Lost transitive includes — TUs this configuration never compiled
`ProgramObject.h` sits behind `Core.h`, so commit 2 (dropping `SpvcSession.h`, which forwards `<spirv_reflect.h>` and `MG_Util/ShaderTranspiler/Types.h`) affects every `Core.h` consumer. The local build has `MOBILEGL_BUILD_BENCHMARK=OFF`; **CI's `test` job has it ON (`test.yml:87`)**, so the implementer's "no consumer needed an include" was proven only for the TUs in `compile_commands.json`. I enumerated every `.cpp` under `MobileGL/ tools/ android-plugin/` absent from `compile_commands.json` (48 files: benchmarks, MG_Remote transport, CGL/WGL/NSOpenGL, Wire tests, JNI, tools) and syntax-checked the ones that reach the program headers with the exact flag set of a compiled test TU plus `_deps/benchmark-src/include`:
```
OK MobileGL/MG_Benchmark/Buffer/BufferBench.cpp
OK MobileGL/MG_Benchmark/Container/UnorderedMapBench.cpp
OK MobileGL/MG_Benchmark/Program/ProgramBench.cpp
OK MobileGL/MG_Benchmark/SanityBench.cpp
OK MobileGL/MG_Benchmark/ShaderCache/TranslationCacheBench.cpp
OK MobileGL/MG_Benchmark/Transpile/TranspileProfile.cpp
OK MobileGL/MG_Util/Converters/SPIRVCrossToGL/SpvcTypeConverter.cpp (referenced by no CMakeLists; checked anyway)
```
Repo-wide, every TU that names `SpvReflect*`/`SpvcSession`/`SpvcMetadata` either includes `SpvcSession.h`/`spirv_reflect.h` itself or is in this build's compile set (and built). `MG_Test/Backend/DirectVulkan/SanityTest.cpp` fails only on `GLFW/glfw3.h` (not installed; unrelated). `DriverBenchJni.cpp` names none of those symbols. Conclusion: commit 2 is safe for the CI configuration as well; the binding check remains the integrator's `gh workflow run` (C.5).
### 1.5 The integration-gpu failure the result file reports
`DirectVulkan.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput`: one run each of the untouched base binary (`~/w7/pipe/build-linux/MobileGL/MG_IntegrationTest/MobileGLIntegrationTest`, DirectVulkan + lavapipe env as the ctest property injects) and the package's: **both `[ FAILED ]`**. Pre-existing on this machine, as the implementer's 3×3 A/B said; with byte-identical `.text` it cannot be this package.
---
## 2. Majors
None.
## 3. Minors (none blocks; listed for the integrator / a follow-up commit)
1. **File mode.** `MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp` is committed as `100755`; its 15 siblings in that directory are `100644` (`git ls-files -s MobileGL/MG_Test/Program/`). Cosmetic; `git update-index --chmod=-x` in the integrator's merge or a follow-up.
2. **Test/header `#if` drift risk.** `ProgramArtifactsTest.cpp:297` re-spells the header's guard (`__GLIBCXX__ && !_GLIBCXX_DEBUG && SIZE_MAX == UINT64_MAX`) instead of `#ifdef MGL_LINKARTIFACTS_SIZE`. The brief literally asked for "under the same `#if`", so this is by the letter — but when C.4 pins the libc++ branch, the header will assert on Android while the runtime twin silently records `sizes_pinned: no` there. Recommend `#ifdef MGL_LINKARTIFACTS_SIZE` in the test (one-line change, integrator-owned since C.4 touches the header anyway).
3. **Macro leak.** `MGL_RESOURCEREFLECTION_SIZE`/`MGL_XFBVARYING_SIZE`/`MGL_LINKARTIFACTS_SIZE`/`MGL_SPIRVARTIFACTS_SIZE` (`ProgramArtifacts.h:187-190`) are `#define`d in a header reached by every `Core.h` consumer and never `#undef`'d. No collision today (`grep -rn MGL_.*_SIZE` shows only `MGL_RESIDUAL_BLOCK_SIZE`); the brief prescribed this shape. Note only.
4. **Transitive-include reliance inside the header.** `std::same_as` (`<concepts>`), `std::remove_const_t` (`<type_traits>`), `SIZE_MAX`/`UINT64_MAX` (`<cstdint>`) are not included by the header; D6 pins the include list, and the `-H` closure shows `concepts` and `cstdint` arrive via `<Includes.h>` on libstdc++ (`-dM -E` confirms `SIZE_MAX`, `UINT64_MAX` defined). Two things worth knowing: (a) if `SIZE_MAX`/`UINT64_MAX` were ever *not* defined at that point, the preprocessor evaluates `0 == 0` → true, i.e. the guard fails open rather than closed; `-Wundef` is not in the project flags. (b) libc++ and MSVC supply `std::same_as` through `<string>`/`<set>` in C++20 mode in practice. Not a defect under D6; a `<cstdint>`/`<concepts>` pair would cost nothing and is not forbidden by the gate — the plan owner's call.
5. **Result-file correction (deviation 6).** The implementer warns that the brief's C.4 recipe `--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::'` would report "43 added / 43 removed". With the real `scripts/symbol_report.py` from `p05/include-gate` it does not: `--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::'` → `27799 -> 27799 defined symbols: 0 added, 0 removed, 1 resized, 42 renamed`, identical to the `--rename-map` fold and to `--strip-scope 'ProgramObject::'`. The warning was true of the implementer's hand-rolled `symcmp.py`, not of the script the integrator will run. The C.4 recipe can be used as written; the expected line is `0 added, 0 removed, 1 resized, 42 renamed` with the one resized = the RTTI name string (`-15` bytes).
6. **Cosmetic.** `ProgramArtifacts.h:18` starts a sentence in lowercase ("…asserts that closure. the glslang scope token appears…").
## 4. Deviations declared by the implementer — verified
| # | claim | verdict |
|---|---|---|
| 1 | 5 comment respellings `glslang::X` → `glslang X` | exactly those 5 lines differ (§1.1 verbatim diff); no code token changed; needed because the gate counts occurrences in the whole file (the include-gate self-test confirms "budget is 2") |
| 2 | LinkArtifacts 57 of 58, not 52 of 53 | independent count 58 members (§1.2 cross-check); the brief's 53 was stale |
| 3 | `ctest -N` grep pattern | correct; used here |
| 4 | extra test `VisitFieldsPassesConstnessThrough` | additive; fails when the table is broken (§1.2 C2 also tripped it at `:110`) |
| 5 | CMake stanza appended at file end | content matches the `AsyncCompileTest` shape; wired (§1.2) |
| 6 | `symbol_report.py` `--strip-scope` warning | **incorrect for the real script** — see minor 5 |
## 5. Not verified here (by design)
- The libc++/NDK sizes (integrator C.4) and the MSVC build; nothing in the package is platform-specific beyond the per-STL `#if`.
- The full `integration-gpu` suite (only the reported failure was A/B'd, §1.5).
- CI dispatch of `test.yml` on `feat/disaggregated` (D10, integrator).
## 6. State of the trees after review
`~/w7/p05-program-artifacts`: HEAD `77beb7d6`, `git status --short` (minus `build-linux/`) empty, `build-linux/` rebuilt to no-op. `~/w7/pipe`: untouched, `6672778b`, clean. All scratch work under `~/w7/rv-pa/` (farms `fake`, `fake2`, `neg1`, `neg2f`, `pos2f`, `neg4f`, logs) and `scratchpad/wf3/p05-review-pa/`.
@@ -1,66 +0,0 @@
# p05-program-artifacts — results v1
Tree `~/w7/p05-program-artifacts`, branch `p05/program-artifacts`, base `feat/disaggregated @ 6672778b`.
Build: `build-linux/` Release, `/usr/sbin/clang++` (clang 22), ccache (`CCACHE_BASEDIR=/home/swung/w7`), libstdc++ (`__GLIBCXX__ 20260625`), `MOBILEGL_BUILD_TEST=ON`, `MOBILEGL_BUILD_INTEGRATION_TEST=ON`.
Not pushed. `~/w7/pipe` untouched (only read: `ctest -N`, `nm`, and a direct run of one existing test binary for the A/B below).
## Commits (oldest first)
| hash | subject |
|---|---|
| `a180dc57` | `[Refactor] (Program): extract ProgramArtifacts.h - move TypeFacts, ResourceReflection, XfbVarying, LinkArtifacts and SpirvArtifacts to namespace scope with in-class aliases so every existing spelling compiles unchanged; pure move` |
| `3ce1264c` | `[Refactor] (Program): stop ProgramObject.h including SpvcSession.h - it names no spirv-cross or SPIRV-Reflect symbol` |
| `77beb7d6` | `[Feat] (Program): add the reflection-archive field tables and sizeof trip wires to ProgramArtifacts.h` |
Files touched (vs `6672778b`): `MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` (new, 576 lines), `MobileGL/MG_State/GLState/ProgramState/ProgramObject.h` (-386/+15), `MobileGL/MG_Test/Program/ProgramArtifactsTest.cpp` (new), `MobileGL/MG_Test/Program/CMakeLists.txt` (+21). Nothing else — commit 2 needed no consumer include additions (all 8 includers + `ProgramObject.cpp` built without `SpvcSession.h` forwarded).
## What landed (B.2 checklist)
- `ProgramArtifacts.h`: banner copied from `ProgramObject.h`; includes exactly `<Includes.h>` and `<set>`; PURITY comment; `namespace MobileGL::MG_State::GLState { inline constexpr Uint kInvalidUniformOffset = ~0u; TypeFacts; ResourceReflection; the 4 `using` reflections; XfbVarying; LinkArtifacts (+ its comment block, with the one added I5 sentence); SpirvArtifacts (+ comment block); VisitFields x5; trip wires }`. Struct bodies moved verbatim (re-indented one level), no field added/removed/reordered/re-typed; `program` and `uniformInitialValues` keep their glslang types (D5).
- `ProgramObject.h`: `#include "ProgramArtifacts.h"` after `"ShaderObject.h"`; old `:41-104` replaced by the 9-line alias block (fully qualified RHS); `kInvalidUniformOffset` re-exported as `static constexpr Uint kInvalidUniformOffset = MobileGL::MG_State::GLState::kInvalidUniformOffset;` (comment kept); old `:1144-1171` and `:1173-1449` deleted; commit 2 drops `#include <MG_Util/ShaderTranspiler/SpvcSession.h>`. `MAX_UNIFORM_LOCATIONS` and the `glslang::` accessors stay. `m_artifacts`/`m_spirv` private, `Artifacts()`/`Spirv()` untouched (I5).
- `VisitFields`: free constrained templates (`requires std::same_as<std::remove_const_t<Self>, T>`), contract `v(const char*, Field&)`, tables never recurse; counts TypeFacts 20, ResourceReflection 14 (`type` as a value), XfbVarying 11, LinkArtifacts 57 (every member except `program`), SpirvArtifacts 8.
- Trip wires: `static_assert(is_trivially_copyable_v<TypeFacts> && sizeof(TypeFacts) == 44)`; under `#if defined(__GLIBCXX__) && !defined(_GLIBCXX_DEBUG) && (SIZE_MAX == UINT64_MAX)`: `MGL_RESOURCEREFLECTION_SIZE 128`, `MGL_XFBVARYING_SIZE 128`, `MGL_LINKARTIFACTS_SIZE 1056`, `MGL_SPIRVARTIFACTS_SIZE 88` (measured on this build via `== 1` notes); `#elif defined(_LIBCPP_VERSION) && (SIZE_MAX == UINT64_MAX) && defined(MGL_ARTIFACT_SIZES_LIBCXX_PINNED)` left inert for the integrator (C.4); the four `static_assert`s guarded by `#ifdef MGL_LINKARTIFACTS_SIZE` with the "add the field to VisitFields(...) then update this number" messages.
- Tests (`ProgramArtifacts.*`, label `unit`, first include is the artifacts header): `AliasesAreTheSameTypes` (9 `is_same_v` static_asserts + `kInvalidUniformOffset`), `TypeFactsIsPodOf44Bytes`, `VisitFieldsCoversEveryMember` (20/14/11/57/8, const == non-const, names distinct), `VisitFieldsPassesConstnessThrough` (extra: const walk yields const refs, mutable walk writes through), `SizesArePinnedOnThisToolchain` (`EXPECT_EQ` under the same `#if`, `RecordProperty("sizeof_*")` x5 always, plus a printf line `sizeof: TypeFacts=44 ResourceReflection=128 XfbVarying=128 LinkArtifacts=1056 SpirvArtifacts=88`).
## Verification — every command and its actual result
All run in WSL Arch, cwd `~/w7/p05-program-artifacts`, after commit 3 (`77beb7d6`) unless stated.
| # | command | result |
|---|---|---|
| 1 | `cmake --build build-linux -j 16` (after each commit) | commit 1: OK (186 steps, whole ProgramObject.h fan-out); commit 2: OK (183 steps, zero consumer edits needed); commit 3: OK; post-commit re-run: `ninja: no work to do.` Logs `~/w7/pa-build{1,2,3}.log` |
| 2 | `ctest --test-dir build-linux -L unit --no-tests=error -j 8` | `100% tests passed, 0 tests failed out of 1463` (3 `***Skipped`: `LogLevel.DebugBuildKeepsEverything`, `LogLevel.AssertIsLiveInDebugBuilds`, `XfbFrontendOrderInvarianceTest.DumpBSpirvForDisassembly` — skip by design). Log `~/w7/pa-unit3.log`. (After commit 1 alone: 1460/1460.) |
| 3 | `ctest --test-dir build-linux -L integration-gpu --no-tests=error -j 8` | `99% tests passed, 5 tests failed out of 868`: the four `*IsActuallyArmedWhenTheEnvironmentPinsItOn*` (DirectGLES UnlocatedIoBlocks, DirectVulkan PrimGenReroute, DirectGLES + DirectVulkan PointSizeDemotion) — the known `-j` flake — and `DirectVulkan.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput`. Log `~/w7/pa-igpu3.log` |
| 4 | `ctest --test-dir build-linux -L integration-gpu --rerun-failed -j 1` | `80% tests passed, 1 tests failed out of 5`: the four flake lanes pass; `DirectVulkan.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput` still fails (`IterationRPProgram203Scenario.cpp:371,377`, golden half-float texel mismatch, `mismatchTexels` 1 vs 0). Log `~/w7/pa-igpu3-rerun.log` |
| 5 | A/B of #4's failure on the UNTOUCHED base binaries: `MOBILEGL_BACKEND_TYPE=DirectVulkan __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json ~/w7/pipe/build-linux/MobileGL/MG_IntegrationTest/MobileGLIntegrationTest --gtest_filter=IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput --gtest_also_run_disabled_tests` (cwd = that binary's `MG_IntegrationTest` dir; the env is what the ctest ENVIRONMENT property injects) x3, and the same for my binary x3 | base (built 22:06 at `6672778b`): FAILED 3/3; mine: FAILED 3/3. **Pre-existing on this WSL/lavapipe machine, independent of this package** (the package touches no code path — only type scope — and the library `.text` is byte-identical, see #13). Its DirectGLES twin is `***Skipped` on both. |
| 6 | `ctest -N` name diff: `ctest --test-dir build-linux -N \| grep -E '^\s+Test\s+#' \| sed 's/^ *Test *#[0-9]*: //' \| sort` vs the same on `~/w7/pipe/build-linux`; `comm -23 before after` | before 2326, after 2331; `comm -23` **empty**; `comm -13` = exactly the 5 new `ProgramArtifacts.*` names. NOTE: the brief's pattern `'^\s+Test #'` (single space) only matches test numbers >= 1000 (ctest right-aligns: `Test #984:` vs `Test #1234:`); with it the diff is garbage (two spurious `QueryTest.*` "additions", my tests invisible). Use `'^\s+Test\s+#'`. |
| 7 | `ctest --test-dir build-linux -L unit -N \| grep -c ProgramArtifacts` | `5` (the label reaches `ctest -L unit`) |
| 8 | ODR: `grep -rn "^\s*struct \(TypeFacts\|ResourceReflection\|XfbVarying\|LinkArtifacts\|SpirvArtifacts\)\b" MobileGL` | exactly 5 hits, all in `ProgramArtifacts.h` (lines 31, 63, 95, 160, 359) |
| 9 | `grep -n "#include" MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` | 2 lines: `<Includes.h>` (line 10), `<set>` (line 15) |
| 10 | `grep -c "glslang::" MobileGL/MG_State/GLState/ProgramState/ProgramArtifacts.h` | `2` (lines 165 `SharedPtr<glslang::TProgram> program;` and 232 `Vector<glslang::TIntermediate::TUniformInitializer> uniformInitialValues;`) |
| 11 | Closure probe: `printf '#include <MG_State/GLState/ProgramState/ProgramArtifacts.h>\n' > /tmp/probe_pa.cpp; clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -H -E -o /dev/null /tmp/probe_pa.cpp 2>&1 \| grep -E '^\.+ ' \| grep -E 'ShaderObject.h\|SpvcSession.h\|MG_Util/ShaderTranspiler/\|MobileGL/Config.h\|MobileGL/MG_Backend/\|BufferState/'` | **empty** (0 forbidden of 653 closure lines). The only `MobileGL/` headers in the closure: `Defines.h`, `Includes.h`, `ProgramArtifacts.h`, `MG_Util/Debug/Log.h`, `MG_Util/GLExtensions.h`, `MG_Util/PlatformStubs.h`, `MG_Util/Types.h`. |
| 12 | `clang++ ... -fsyntax-only /tmp/probe_pa.cpp` | OK (header self-contained; `ProgramArtifactsTest.cpp` includes it first as the in-tree proof) |
| 13 | `git diff --stat 6672778b -- MobileGL/MG_Backend MobileGL/MG_Impl` | **empty** (also after commit 2 — no include-line additions were needed) |
| 14 | Symbol attribution (no `scripts/symbol_report.py` in this tree — done by hand with `nm --defined-only -S` + `c++filt` + `size --format=sysv`, script `scratchpad/wf3/p05-program-artifacts/symcmp.py`; normalisation folds `GLState::ProgramObject::{TypeFacts,ResourceReflection,XfbVarying,LinkArtifacts,SpirvArtifacts}` -> `GLState::X` on both sides, multiset compare on (name, type, size)); before = `~/w7/pipe/build-linux/libMobileGL.so` (6672778b, 22:06), after = `build-linux/libMobileGL.so` | `symbol-report: .text 10792579 -> 10792579` (byte-identical); `30511 -> 30511 defined symbols: 0 added, 0 removed, 1 resized, 43 renamed`; identical after commit 1 alone (`~/w7/pa-sym1.txt`) and after commit 3 (`~/w7/pa-sym3.txt`). The 1 "resized" is `r [122] -> [107] typeinfo name for std::_Sp_counted_ptr_inplace<...LinkArtifacts, allocator<void>, _Lock_policy 2>` — the RTTI **name string** (rodata, not code), 15 bytes shorter because `13ProgramObject` left the mangled name. Fully attributed; commits 2 and 3 add no symbol (VisitFields is uninstantiated outside the test binary). |
| 15 | Red-for-its-reason (scratch copy of the header under `/tmp/pa-neg`, tree untouched): add `Int addedWithoutATableEntry = 0;` to `LinkArtifacts`, `clang++ -I/tmp/pa-neg ... -fsyntax-only /tmp/probe_pa.cpp` | exit 1: `error: static assertion failed ... 'sizeof(LinkArtifacts) == 1056': LinkArtifacts changed size: add the field to VisitFields(LinkArtifacts) ..., then update this number` / `note: expression evaluates to '1064 == 1056'` |
| 16 | `git status --short` (excluding `build-linux/`) after each commit | clean |
| 17 | Line endings: `grep -c $'\r'` on the two headers and the test | 0 / 0 / 0 |
## Deviations from the brief (with reasons)
1. **Comment respellings in the moved text (commit 1).** Five comment lines that moved verbatim spell `glslang::TType`, `glslang::TLayoutMatrix`, `glslang::TBasicType`, `glslang::TObjectReflection`, `glslang::TProgram`; with them the header had 8 `glslang::` lines, and the gate/D5 pins the count at exactly 2. Respelled comment-only as `glslang TType` etc. (no code token changed). The brief's "verbatim" and "exactly 2" cannot both hold literally; the gate wins (`ROADMAP.md:7`).
2. **`LinkArtifacts` field count is 57 of 58, not 52 of 53.** Counted mechanically from the moved source (`program` + 57 others; a lines-ending-in-`;` count under-reads because several members carry trailing comments). `VisitFieldsCoversEveryMember` expects 57. The brief's 53 is a stale scout number.
3. **`ctest -N` grep pattern** corrected to `'^\s+Test\s+#'` (see #6) — the brief's pattern silently drops tests numbered < 1000.
4. **One extra test** `VisitFieldsPassesConstnessThrough` (adds only; brief allows additions) — pins that the single table really serves both directions.
5. **`MobileGL/MG_Test/Program/CMakeLists.txt` stanza** appended at the end of the file (after the existing `gtest_discover_tests` block) rather than "copied next to" `AsyncCompileTest`; content is the `AsyncCompileTest` shape with `DISCOVERY_TIMEOUT 30 PROPERTIES LABELS unit` as specified.
6. **`scripts/symbol_report.py` absent** in this tree (owned by `p05-include-gate`); the by-hand equivalent above records the numbers. The brief's `--strip-scope 'MobileGL::MG_State::GLState::ProgramObject::'` as a plain textual strip is NOT the right normalisation (it leaves a bare `LinkArtifacts` on the before side versus `MobileGL::MG_State::GLState::LinkArtifacts` on the after side and reports 43 added / 43 removed); the integrator's run of the real script should fold `GLState::ProgramObject::X` -> `GLState::X` (or strip only `ProgramObject::`). Flagging so C.4 does not read a false 43/43.
## Notes for the integrator
- libc++ branch of the trip wires is inert until `MGL_ARTIFACT_SIZES_LIBCXX_PINNED` is defined with the NDK numbers (C.4); `ProgramArtifacts.SizesArePinnedOnThisToolchain` prints and `RecordProperty`s all five sizes on every platform for that purpose.
- The glibc++ guard is exactly the brief's (`__GLIBCXX__ && !_GLIBCXX_DEBUG && SIZE_MAX == UINT64_MAX`); it does not exclude `_GLIBCXX_USE_CXX11_ABI=0` (COW `std::string`, 8 bytes) — no MobileGL toolchain uses that, mentioned only for completeness.
- `DirectVulkan.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput` is red on this WSL machine at the base commit (#5) — expect it in the integrator's baseline too.
## Unfinished
Nothing in scope. Not done by design/ownership: libc++ size pin (integrator, C.4); `scripts/check_include_closure.py --probe artifacts-header` run (script lives in `p05/include-gate`; the manual `-H` probe #11 is its equivalent); narrowing `ProgramTranslationCache.h`/`ProgramInterface.cpp` to the new header and the `UniformInitializer` re-typing (P7 follow-ups per the brief).
@@ -1,83 +0,0 @@
# p05-value-types — adversarial review v1
Reviewed: `~/w7/p05-value-types`, branch `p05/value-types`, HEAD `92e925f9` (one commit on `6672778b`).
Reference: `~/w7/pipe` @ `6672778b` (read-only; untouched — `git status` clean apart from its build dirs).
Review scripts: `scratchpad/wf3/p05-review-vt/s1..s6.sh`; logs under `~/w7/logs/review-vt/`.
Tree state at exit: `git status --short` prints only `?? build-linux/` (identical to the implementer's state); the temporarily copied gate script was removed; the red-for-its-reason experiments were done on out-of-tree copies (`~/w7/logs/review-vt/red*/`), never in the tree.
**Verdict: APPROVED — 0 majors, 3 minors.** The claim "correct, complete, pure move meeting the brief" survived every attempt to refute it.
---
## 1. What I tried to refute, and what I observed
### 1.1 "Cut, not copy" — verbatim-move proof (s2)
Reconstructed the moved regions from the base commit and diffed them against the new header:
```
git show 6672778b:.../RenderState.h | sed -n '15,370p' -> old_rs.txt
git show 6672778b:.../SamplerObject.h | sed -n '14,97p' -> old_so.txt
git show 6672778b:.../VertexArrayObject.h | sed -n '17,70p' -> old_vao.txt
```
- Non-vertex part (`old_rs + old_so`, with the two `MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS` → `kMGMaxDrawBuffers` respellings applied) vs `MGPipeValueTypes.h:32-471`: `diff -B` **exit 0** — byte-exact including indentation and comments.
- Vertex part (`old_vao` de-indented by 4) vs `MGPipeValueTypes.h:474-529`: `diff -B` **exit 0**.
- Whole region `diff -w -B`: the only extra lines are the three the brief prescribes (`namespace MG_State::GLState {`, `class BufferObject;`, closing brace).
- Banner `MGPipeValueTypes.h:1-7` == `MGPipeTypes.h:1-7` with the path substituted (`banner-ok`).
- Deleted "extra" lines (`RenderState.h:371`, `SamplerObject.h:97`, `VertexArrayObject.h:71`) are all blank (`cat -A` shows `$`), so deviation 1 of the result file is whitespace-only as declared.
Member order, defaults, enum underlying types (`BorderColorForm : Uint8` at `MGPipeValueTypes.h:441`), `constexpr`-ness (`MAX_VIEWPORTS` at `:244`), namespaces (`MG_State::GLState` reopened at `:473`, D3) — all unchanged by construction of the diff above.
### 1.2 ODR — no second definition anywhere
- Brief grep (8 structs + 3 enums) over `MobileGL/`: **11 hits, all in `MobileGL/MG_Pipe/MGPipeValueTypes.h`** (lines 32, 168, 208, 219, 229, 239, 441, 447, 476, 517, 525).
- Extended grep for **all 17 moved enums** over `MobileGL tools android-plugin` (`*.h *.hpp *.cpp *.inc`, build dirs excluded): 17 hits, all in the new header.
- `MAX_DRAW_BUFFERS\s*=`: exactly one, `FramebufferObject.h:107` `= MobileGL::kMGMaxDrawBuffers` (D4). `kMGMaxDrawBuffers` appears 8 times (header, tests, FramebufferObject.h); none in `MG_Backend`.
### 1.3 Purity / closure (s3, s4)
- Text: `grep -n "MG_State/\|MG_Backend/\|MG_Impl/\|MG_Remote/" MGPipeValueTypes.h` → **empty** (exit 1).
- Includes of the four forwarding headers: `RenderState.h:10-11` = `<Includes.h>` + `<MG_Pipe/MGPipeValueTypes.h>` only; `SamplerObject.h:10-11` same; `VertexArrayObject.h:10-13` keeps `BufferObject.h`/`Types.h` + value header; `FramebufferObject.h:14` adds the value header.
- Compiler closure, brief's exact flags, `-H -E` on `#include <MG_Pipe/MGPipeValueTypes.h>`: **654 lines, 0 under `MobileGL/MG_State/ | MG_Backend/ | MG_Impl/ | MG_Remote/`**. `MobileGL/` headers in the closure: `Defines.h, Includes.h, MG_Pipe/MGPipeValueTypes.h, MG_Util/{Debug/Log.h, GLExtensions.h, Math/VectorTypes.h, PlatformStubs.h, Types.h}`. `-fsyntax-only` exit 0 (self-contained).
- **Real gate run** (copied `~/w7/p05-include-gate/scripts/check_include_closure.py` into `scripts/` for the run, removed afterwards; `REPO_ROOT` is derived from the script location so it had to be in-tree):
- `--mode both --self-test`: `value-header OK 66 headers (text)`, `OK 654 headers (clang)`, `SELF-CONTAINED OK`; `artifacts-header SKIP` (expected, other package); `wire-header OK`; `self-test: 4 negative-control trip(s), parser checks OK`; `3 probes, 1 skipped, 0 problem(s)`, exit 0. The 4 trips = self-test steps 1 and 2 (step 2 is the roadmap's literal "add an `MG_State` include back and it goes red", which only runs once `MGPipeValueTypes.h` exists — it does now, and it trips) in both modes.
- `--require-all`: exit 1, names only the missing `ProgramArtifacts.h` (D9 semantics correct; the value header itself is not the reason).
- `--probe value-header`: `1 probes, 0 skipped, 0 problem(s)`.
- **Red-for-its-reason, by hand** (out-of-tree shadow copy at `~/w7/logs/review-vt/red/MG_Pipe/MGPipeValueTypes.h` with `#include <MG_State/GLState/RenderState/RenderState.h>` inserted after `:15`, put first on the `-I` path): the same `-H` probe reports **1 forbidden line**. Restored = nothing to restore; the tree was never edited.
- **Tripwire fires**: shadow copy with `sizeof(RenderStateParameters) == 1` → `error: static assertion failed ... note: expression evaluates to '1168 == 1'`.
### 1.4 Behaviour unchanged (s3)
- `cmake --build build-linux -j 16`: `ninja: no work to do` (the implementer's build is current).
- `ctest -L unit --no-tests=error -j 8`: **100% passed, 0 failed of 1460**, 11.0 s. The two new tests ran inside the `-L unit` selection (`#1053`, `#1054` Passed) — they are labelled, not orphaned.
- `ctest -N` names, `comm -23 before after`: **lost: none**; added exactly `PipeCatalogue.ValueTypeLayoutsArePinned`, `PipeCatalogue.ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail` (1327 → 1329).
- `python3 scripts/gen_pipe.py --check`: `generated files are up to date`, exit 0; tree still clean afterwards.
- `nm --defined-only -S` keyed on mangled name (type+size): **30511 → 30511, diff empty**; with addresses included: diff empty. `size --format=sysv`: `.text 10792579` and `Total 17209819` on both sides. `objcopy --only-section=.text` + `cmp`: **`.text` byte-identical**. The baseline `.so` carries a `GIT@a4baa9d` configure stamp (implementer already attributed it); since `.text` is byte-identical to a build known to be at `6672778b`+header-move, the baseline is a valid `6672778b` build.
- `grep -rc "pGLContext" MobileGL/MG_Backend | sort` identical to pipe; `git diff --stat 6672778b -- MG_Backend MG_Impl MG_Util` empty.
### 1.5 Lost transitive includes on platforms that were not built
- 27 files include the touched headers; every `.cpp` among them (8) is in `build-linux/compile_commands.json`.
- 31 `.cpp` under `MobileGL/` are not built on Linux (Android JNI, WGL/CGL/NSOpenGL, MG_Remote split-only, benchmarks, a dead `VkTextureSamplerManager.cpp` referenced by no CMake file). Inspected their includes: none includes `RenderState.h`/`SamplerObject.h`/`VertexArrayObject.h` directly; the JNI ones go through `MG_Impl/GLImpl/*` and `Includes.h` (same path the Linux build exercises). The 18 split-only TUs (`~/w7/pipe/build-linux-split`) are all `MG_Remote/Transport/*` and `MG_Test/Wire/*`, which by the wire-header rule do not include `Includes.h` at all. No plausible Android/Windows/split fallout found.
### 1.6 Wiring, formatting, docs
- No CI format gate exists (`grep clang-format .github/workflows/*.yml` empty); `clang-format --dry-run` on the new header nevertheless reports **0 warnings** (clang-format 22.1.6, repo `.clang-format`). No trailing whitespace in the diff (`grep -P '^\+.*[ \t]+$'` count 0); LF endings, newline at EOF, mode 100644.
- `scripts/check_doc_citations.py` (base tree, the `test.yml:853-861` lint, which is `|| true` anyway) on the three `docs/Disaggregated/*.md`: `19 citations ... 0 problem(s)`. `MGPipeTypes.h` line numbers are unchanged (the comment rewrite is line-for-line), so `ARCHITECTURE.md:359`'s `MGPipeTypes.h:535` still points at `MGL_RESIDUAL_BLOCK_SIZE`.
- Commit message: prescribed subject verbatim + `- ` bullets explaining why; no attribution lines; not pushed; no fixture binaries touched.
- Deviations 1-7 in the result file were each checked and are as declared (whitespace, comment wording, additional-only test expectations, tooling not present in this tree).
## 2. Majors
None.
## 3. Minors
1. **`MGPipeTypes.h:16-18` overstates the repaid debt.** The rewritten comment says "this header no longer reaches into MG_State". The `-H` closure of `#include <MG_Pipe/MGPipeTypes.h>` still contains `.. MobileGL/MG_Backend/BackendObject.h` → `... MobileGL/MG_State/GLState/TextureState/TextureEnum.h` (`BackendObject.h:11`), exactly the D1 reason the brief gives for not moving `DynamicBackendParameters`. The following sentence ("that one include is what still keeps purity gate A off this header") is correct, so nothing functional depends on it, but the first sentence should read "no longer includes MG_State directly; the remaining MG_State reach is `TextureEnum.h` via `BackendObject.h`". Comment-only; fix in the integrator's docs/comment pass or a follow-up, not a reason to hold the package.
2. **`PipeCatalogue.ValueTypeLayoutsArePinned` is tautological on the CI toolchain** (`PipeCatalogueTest.cpp:117-135`): every `EXPECT_*` compares the same compile-time constants the header's `static_assert`s already pin, so on Linux/libstdc++ it cannot fail without the build already failing. This is exactly what the brief asked for ("runtime twins ... so the numbers show up in ctest output on every platform"), and it does earn its keep on MSVC/NDK where a static assertion might be `#if`'d out later; recorded so nobody counts it as an independent gate.
3. **Result-file line range `SamplerObject.h:14-96` vs brief `14-97`**: line 97 at `6672778b` is blank, so the cut is the brief's cut; only the description differs. No action.
## 4. Notes for the integrator (not findings against this package)
- The package's `build-linux/CMakeCache.txt` has `MOBILEGL_ITEST_VK_ICD=` empty (implementer's Program203 diagnosis checks out against `MG_IntegrationTest/CMakeLists.txt:284-298`); reconfigure the three p05 build dirs with the lavapipe ICD before relying on a plain `ctest -L integration-gpu`.
- With this package merged after include-gate, `check_include_closure.py --mode both --self-test` already reports `value-header OK` in both modes plus the step-2 negative control tripping; only `artifacts-header` remains `SKIP` until program-artifacts lands.
@@ -1,63 +0,0 @@
# p05-value-types — result v1
Tree `~/w7/p05-value-types`, branch `p05/value-types`, base `feat/disaggregated @ 6672778b`.
Build dir `build-linux/` (Release, `/usr/sbin/clang++` clang 22, ccache, tests + integration tests ON).
Logs: `~/w7/logs/p05vt-*.log`, `~/w7/logs/nm-*.raw`, `~/w7/logs/probe_vt_H.txt`, `~/w7/logs/p203-*.log`.
## Commits (one, as the brief asks)
- `COMMIT_HASH` `[Refactor] (Pipe, State): extract MGPipeValueTypes.h - move the render-state, sampler and vertex value types and their enums out of MG_State/GLState so MG_Pipe no longer reaches RenderState.h; pure move, member order and namespaces unchanged`
Files: `MobileGL/MG_Pipe/MGPipeValueTypes.h` (new, 550 lines), `MobileGL/MG_Pipe/MGPipeTypes.h`, `MobileGL/MG_Pipe/generated/PipeVerify.inc`, `MobileGL/MG_State/GLState/RenderState/RenderState.{h,cpp}`, `MobileGL/MG_State/GLState/SamplerState/SamplerObject.h`, `MobileGL/MG_State/GLState/VertexArrayState/VertexArrayObject.h`, `MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.h`, `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp`, `scripts/gen_pipe.py`. Nothing under `MG_Backend/`, `MG_Impl/`, `MG_Util/` touched. Not pushed.
## What was done (B.1, in order)
1. `MGPipeValueTypes.h` created by a Python script (`scratchpad/wf3/p05-value-types/cut.py`) that slices the exact line ranges the brief names, with the anchor line of every range asserted before the cut: `RenderState.h:15-189` (11 enums, D2), `:191-370` (4 structs; `:263`/`:273` respelled to `kMGMaxDrawBuffers`), `SamplerObject.h:14-96` (6 enums + `SamplerParameters`), `VertexArrayObject.h:17-70` (3 vertex types, re-indented by one level because they now sit in `namespace MG_State::GLState` inside `namespace MobileGL` instead of three nested namespaces; namespace unchanged, D3; `class BufferObject;` forward-declared). `inline constexpr Uint kMGMaxDrawBuffers = 8;` (D4). Macro guard + `#pragma once`; includes exactly `<Includes.h>`, `<MG_Util/Math/VectorTypes.h>`, `<cstddef>`, `<type_traits>`. Trip wires as in the brief layout; the two "measure" numbers came out as `SamplerParameters` 100 and `VertexAttributeVersion` 6 (the build accepted them first time; `is_standard_layout_v<RenderStateParameters>` holds, so that assertion is kept).
2. Forwarding edits: `RenderState.h` now includes `<Includes.h>` + `<MG_Pipe/MGPipeValueTypes.h>` only; `RenderState.cpp` gained the direct `FramebufferObject.h` include (it spells `FramebufferObject::MAX_DRAW_BUFFERS` six times); `SamplerObject.h` and `VertexArrayObject.h` include the value header and keep only their classes; `FramebufferObject.h` includes the value header and defines `MAX_DRAW_BUFFERS = MobileGL::kMGMaxDrawBuffers`.
3. `MGPipeTypes.h:33` → `#include "MGPipeValueTypes.h"`, `:32` kept (D1), debt comment rewritten to say the MG_State half is repaid and `DynamicBackendParameters` is what still keeps gate A off `MGPipeTypes.h`; `using` comment updated. `gen_pipe.py:163-165` and the MEMCMP FALLBACK comment (`:410-414`, the source of `PipeVerify.inc:232-238`) reworded to P1; `python3 scripts/gen_pipe.py` run and the regenerated `.inc` committed (comment-only diff).
4. Full build: **zero lost-transitive-include failures** — no consumer `.cpp` needed a direct include (the candidates all reach `FramebufferObject.h` through `Core.h`, as the brief predicted).
5. Tests `PipeCatalogue.ValueTypeLayoutsArePinned` and `PipeCatalogue.ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail` added to `PipeCatalogueTest.cpp`; **no include added** (`offsetof` reaches it through `MGPipe.h → MGPipeTypes.h → MGPipeValueTypes.h → <cstddef>`).
## Verification (every command, actual result)
| # | command | result |
|---|---|---|
| 1 | `cmake --build build-linux -j 16` (three times: after the cut, after the tests, after a comment reword) | exit 0 each time; first build 237 steps, `libMobileGL.so` + all test binaries link |
| 2 | `ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure -j 8` | **100% tests passed, 0 failed out of 1460** (log `p05vt-unit.log`; rerun after the final rebuild: see `p05vt-unit2.log`, UNIT2_RESULT) |
| 3 | new tests present in the run | `#1053 PipeCatalogue.ValueTypeLayoutsArePinned Passed`, `#1054 PipeCatalogue.ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail Passed` |
| 4 | `ctest -N` name lists, `comm -23 before after` (`~/w7/pipe/build-linux` vs mine) | before 1327 / after 1329 names; **lost: none**; added: the two tests above |
| 5 | ODR grep (`^\s*struct (RenderStateParameters\|…)\b \| ^\s*enum class (BlendFactor\|BorderColorForm\|CapabilityInput)\b` over `MobileGL/`) | 11 hits, **all in `MobileGL/MG_Pipe/MGPipeValueTypes.h`** (lines 32, 168, 208, 219, 229, 239, 441, 447, 476, 517, 525); no second definition anywhere |
| 6 | `grep -n "MG_State/\|MG_Backend/\|MG_Impl/\|MG_Remote/" MGPipeValueTypes.h` | **empty** (after rewording my own PURITY comment, which had matched with a slash spelling) |
| 7 | `grep -n "#include" RenderState.h` | `10:#include <Includes.h>`, `11:#include <MG_Pipe/MGPipeValueTypes.h>` only |
| 8 | closure probe: `clang++ -std=gnu++23 -Iinclude -IMobileGL -IMobileGL/MG_Pipe -I3rdparty/xxHash -I3rdparty/Vulkan-Headers/include -H -E` on `#include <MG_Pipe/MGPipeValueTypes.h>` | 654 closure lines, **0 under `MobileGL/MG_State/`, `MG_Backend/`, `MG_Impl/`, `MG_Remote/`** |
| 9 | same flags, `-fsyntax-only` on the probe TU | exit 0 (header is self-contained) |
| 10 | `git diff --stat 6672778b -- MobileGL/MG_Backend` | **empty** |
| 11 | `grep -rc "pGLContext" MobileGL/MG_Backend \| sort` before (pipe) vs after | **identical** |
| 12 | `python3 scripts/gen_pipe.py --check` | `generated files are up to date`, exit 0 |
| 13 | symbol report — `scripts/symbol_report.py` does not exist in this tree (it is the include-gate package's deliverable), so the equivalent was done by hand: `nm --defined-only -S` on `~/w7/pipe/build-linux/libMobileGL.so` vs `build-linux/libMobileGL.so`, keyed on mangled name, comparing type+size | 30511 → 30511 defined symbols; **0 added / 0 removed / 0 resized; 0 symbols changed address**; `size --format=sysv`: `.text` 10792579 B on both sides, Total 17209819 on both |
| 14 | attribution of every differing byte between the two `.so` files (`cmp -l`, mapped to sections) | 31 bytes total, length equal: 20 B in `.note.gnu.build-id`; 7 B in `.rodata` = the `GIT@a4baa9d` → `GIT@6672778` build stamp string (the reference build carries an older configure-time stamp); 4 B in `.rodata` = SPIRV-Tools' `Timestamp: 2026-09-05T22:05:41` → `22:36:04` string. `.text`, `.rodata` otherwise, `.data`, `.data.rel.ro`, `.eh_frame`, `.init_array`, `.dynsym`, `.dynstr`, `.gnu.hash` byte-identical (`objcopy --only-section` + `cmp`). Section table identical. **Zero code bytes differ.** |
| 15 | `ctest -L integration-gpu -j 8` (as configured) | 99%: 4 failed of 868 — three `*IsActuallyArmedWhenTheEnvironmentPinsItOn*` (the known `-j` flake) + `DirectVulkan.IterationRPProgram203Scenario.FixedCompleteInputProducesFixedCompleteGoldenOutput`; `--rerun-failed -j 1`: the three flakes pass, Program203 still fails (1 texel at (0,0), actual `(0x357a,0x4b65)` vs golden `(0x3a74,0x4821)`) — **diagnosed below as a configure difference of the build dir, not a regression** |
| 16 | `ctest -L integration-gpu -j 8` with `MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1 MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1 MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1` exported (what CI's integration-gpu job does, per `MG_IntegrationTest/CMakeLists.txt:286-291`), then `--rerun-failed -j 1` | ITEST2_RESULT |
### Program203 diagnosis (item 15)
- Passes 3/3 in `~/w7/pipe/build-linux`, fails 3/3 in mine, deterministically, same wrong bits every time.
- `.text` of `libMobileGL.so` **and** of `MobileGLIntegrationTest` are byte-identical between the two trees.
- `ctest -N -V` shows the reference registers the test with `ENVIRONMENT … VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json; MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH=1; MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS=1; MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER=1`; mine registers only `MOBILEGL_BACKEND_TYPE=DirectVulkan`.
- Cause: `MG_IntegrationTest/CMakeLists.txt:284-298` attaches the three repairs only when `MOBILEGL_ITEST_VK_ICD MATCHES "lvp_icd|lavapipe"`. `~/w7/pipe/build-linux/CMakeCache.txt` has `MOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json`; **`~/w7/p05-value-types/build-linux/CMakeCache.txt` has `MOBILEGL_ITEST_VK_ICD=` (empty)** — the package worktree was configured without the ICD pin. The only ICD on the machine is lavapipe, so the test runs on lavapipe either way, just without the repairs forced on — exactly the "Program 203 misses its golden output" case the CMake comment describes.
- Proof: my binary run by hand with the three variables → `mismatches=0/262656`, PASSED; the reference binary run by hand without them → `mismatches=1/262656`, the identical wrong bits, FAILED.
- I did not reconfigure `build-linux/` (the brief handed it to me as-is); I ran the lane with the variables exported instead (item 16). **Integrator note:** the other two p05 worktrees were presumably configured the same way and will show the same single failure on this test under a plain `ctest -L integration-gpu`; `cmake -DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json build-linux` on each fixes the registration.
## Deviations from the brief
1. **Deleted the blank separator line after each cut region** (`RenderState.h:371`, `VertexArrayObject.h:71`) in addition to the ranges the brief names, so the forwarding headers do not keep a stray empty line right after the opening `namespace` brace. Whitespace only.
2. **The value header's leading comment is not the brief's two-line PURITY text verbatim**: I spelled the forbidden directories without a trailing slash (`MG_State, MG_Impl, MG_Backend or MG_Remote`) because the brief's own text-purity grep (`grep -n "MG_State/\|…"`) matched the brief's suggested wording; the grep is now empty as the brief requires. Added a short paragraph saying what the header is and why the MG_State headers forward to it.
3. **`MGPipeTypes.h` debt comment / `gen_pipe.py` comments are my wording**, following the brief's content instructions (MG_State half repaid; `DynamicBackendParameters` remains and keeps gate A off `MGPipeTypes.h`; field lists in P1). `gen_pipe.py:165` was wrapped onto two lines to stay under the file's line width.
4. **`ResidualBlockIsExactlyItsTwoValueStructsPlusPatchTail` pins two more offsets than the brief lists** (`RenderState` at 0, `PatchVertices` at 1208) and `ValueTypeLayoutsArePinned` also checks `ColorMasks`' extent and the trivially-copyable/standard-layout predicates at runtime. Additions only.
5. **No `scripts/symbol_report.py`** in this tree (B.3 deliverable, not yet landed): the nm comparison was done by hand as the task allows; every byte of difference is attributed (item 14).
6. **`ctest -L integration-gpu` was run twice**: once as configured (item 15, one non-regression failure explained above) and once with CI's three environment variables exported (item 16). No test file, CMake file or build configuration was changed to get there.
7. `scripts/check_include_closure.py --mode both --probe value-header` (the "once p05/include-gate exists" line) was not run: the script is not in this tree. The equivalent manual `-H` probe (items 8-9) is green.
## Unfinished
UNFINISHED_TEXT
-572
View File
@@ -1,572 +0,0 @@
# P1 implementation brief — PipeInputs strangler, per-verb fill points, poison generations, MOBILEGL_PIPE_VERIFY comparator and its CI mode
Tree: `feat/disaggregated @ 087685d1` (worktree `C:/Users/geekerwan/AndroidStudioProjects/FoldCraftLauncher/MobileGL-disagg`; WSL `~/w7/pipe` is a worktree of the same repo at `087685d1`, branch `feat/disaggregated`, with a configured `build-linux/` — Release, `/usr/sbin/clang++` (clang 22.1.6; there is no `clang++-20` in WSL), ccache, `MOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO`, `MOBILEGL_BUILD_TEST=ON`, `MOBILEGL_BUILD_INTEGRATION_TEST=ON`, `MOBILEGL_BUILD_DISAGGREGATED=OFF`, `MOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/50_mesa.json`, `MOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json`, `MOBILEGL_ITEST_REQUIRE_GPU=OFF`; `build-retrace/` exists; 94 fixture files under `tools/trace_replay/fixtures/`; 28 cores). `~/w7/base` is `dev@81b17c0b` (the performance anchor, untouched). The P0.5 tooling in `scratchpad/wf2/` (`wsl_p05_trees.sh`, `wsl_integrate_p05.sh`, `retrace_gate.py`, `wsl_retrace_baseline.sh`) is the shape every WSL step below copies with `p05`→`p1`.
Every `file:line` below was re-opened at `087685d1` (the scouts were written at `5635e33f`; the one commit between is CI-only, `.github/workflows/test.yml`, and every scout line number still holds). Corrections to the scouts are marked **[correction]**; deliberate deviations from the design documents are marked **[deviation]** and carry their reason.
**Scout verification summary.** The pull-site catalogue (`scout-pull-sites.md` §1) was cross-checked mechanically against the live tree (`scratchpad/wf4/xcheck.py`): all 335 `pGLContext` occurrences on 332 lines in 12 files match the catalogue line-for-line and accessor-for-accessor (the script's one "DIFF" for `VulkanRenderer.cpp` is the parser swallowing the §5 sync tables that follow that heading; 158 − 26 = 132 = live). Spot-reads of 40+ sites across both backends confirmed the `enclosing function` / `verb path` columns with one exception, **[correction]** `VkRenderPassManager.cpp:1111` sits inside `VkRenderPassManager::GetOrCreateRenderPass` (the `ConvertTextureInternalFormatToVkEnum` the catalogue names at `:1044` is `MG_Util::`'s, a different function that reads nothing), so all three `VkRenderPassManager.cpp` reads are render-pass creation — reached from draws, clears and blits only. No `pGLContext` use exists in `MG_Backend` outside the 12 catalogued files (`Init.cpp`, `BackendObject.*`, `BackendObjects.h` and every other `.h` are clean); no other `MG_State::` global is reached (`MG_State::pGLContext` is the only one, 335/335). Other corrections: `scout-docs-spec.md` says "~34 `MOBILEGL_ASSERT`" — the live count is **43** (34 in `DirectVulkan.cpp` + 9 in `UniformManager.cpp`); `scout-pull-sites.md` cites `MGPipeHostSpan.h:119-154` — the file is 57 lines, `struct MGHostSpan` is `:28-37`; `scout-test-infra.md` B2/B4 gate the poison on `MOBILEGL_DEBUG`, which does not exist anywhere in the tree (D2 below). The MG_Impl boundary is **83 call statements over 69 distinct table entries**, not "~93 sites" (the 91-line grep counts null-checks and captured-pointer lines; §B.7). `Present` and `SetSwapInterval` are never called through `gBackendFunctionsTable` from `MG_Impl` — EGL goes through `BackendObject` virtuals (`EGLImpl.cpp:178,448`) — and neither backend's present path reads `pGLContext`, so they are not fill points.
---
## A. Goal and acceptance gate
`docs/Disaggregated/ROADMAP.md:17`, verbatim:
> **P1** `PipeInputs` 替换与 verify harness | 10–13 | `MG_Backend/MGPipe/PipeInputs.h`(Espryt 32 / Magma 55 访问器);`sed` 293 处 + 58 行非箭头清单逐条转换(显式交付物);逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5
Binding context: `ARCHITECTURE.md:325-340` (the `PipeInputs`/`MGB_CTX` sketch), `:342-347` (phase A/B/C table — P1 is phase A plus the phase-B scaffolding: verify harness, poison, fill points), `:348-349` (fill points per verb class; poison is a per-verb generation, `Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}`; gate C greps `pGLContext`, not `pGLContext->`), `:501` (gate 13.2-②: two state models in one address space, `SnapshotFromGLContext()`, field-wise, first differing field + draw serial, third CI mode 5–10× slower, never shipped, survives P13), `:369` (P13 keeps `MOBILEGL_PIPE_VERIFY` with the `MG_State` include it needs), `:553` (`MG_Backend/MGPipe/` is created in P1), `:583` (`MOBILEGL_PIPE_VERIFY` is a **CMake** option, planned P1). `ROADMAP.md:7`: **每个门必须能因它存在的理由变红**; Windows is not a correctness gate. `ROADMAP.md:38`: day 25 is the P1 exit, "verify harness 逐 draw 逐字段证明推送等价于拉取", not a GO/NO-GO. `ROADMAP.md:46`: the first GO/NO-GO item is P1's proof.
Decoded into checkable statements:
| # | statement | how it is checked |
|---|---|---|
| G1 | The **pull build** (`MOBILEGL_PIPE_PUSH=OFF`, `MOBILEGL_PIPE_VERIFY=OFF`, Release/INFO, LTO off) has an identical `nm --defined-only` symbol set before and after P1, and a `.text` byte delta of **zero**; if not zero, every differing symbol is attributed to one of the 58 converted non-arrow lines (D9). | `scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --json` → `added == removed == resized == renamed == 0`, `.text` delta 0 (C.integrator). |
| G2 | Every backend read of frontend state goes through `MGB_CTX` (335 occurrences → 0 `pGLContext` tokens in `MG_Backend/**` except inside the pull arm of the one switch header, which lives in `MG_Pipe/`). | `grep -rc "pGLContext" MobileGL/MG_Backend | grep -v ":0$"` is empty. |
| G3 | In the **verify build** (`MOBILEGL_PIPE_VERIFY=ON` ⇒ `MOBILEGL_PIPE_PUSH=ON`), with `MOBILEGL_PIPE_VERIFY=1` in the environment, the 40-trace retrace (79 desktop cases: 40 × 2 backends minus `iterationrp × DirectGLES`; `create-indirect` waived on device only) and **every** integration entry (the 742-entry `integration-verify` lane) run with zero `Fatal{PipeVerifyDiffer` and zero `Fatal{UnmigratedPipeInput` and the arming line present in every log. | `retrace_gate.py` under `MOBILEGL_PIPE_VERIFY=1` against the verify `.so`; `ctest -L integration-verify --no-tests=error`; `run_trace_case.cmake`'s arming/differ assertions (E). |
| G4 | Negative control A: `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` turns a green verify run red, naming that field and the verb serial. | unit (A), lane (E), CI step (E). |
| G5 | Negative control B: `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit` makes `glGenerateMipmap` abort with `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}` on **that** call, and no other verb is affected (a sibling field on the same verb stays fresh; the preceding draw does not abort). | unit (A), fork/waitpid scenario per backend (E). |
| G6 | Every one of the 20 `SyncPersistentMappedRange` and 6 `SyncGpuWrites` sites has a row in the ownership table (D10) stating the §5.7 reconcile class it must reproduce; the `*IndirectCount` rows record today's behaviour verbatim (open question #15, not fixed in P1). | the table is in this brief; the integrator lands it in `docs/Disaggregated/MEASUREMENTS.md`. |
| G7 | Existing test names unchanged (additions only); `pipe-gates`, `include-graph-check`, `test`, `integration`, `retrace` all green; the new generated file is regenerated by `gen_pipe.py` and diffed by CI. | `ctest -N` name diff (`comm -23` empty); `gh workflow run test.yml --ref feat/disaggregated`. |
| G8 | The gate itself can go red for its reason: the arming assertion fails a lane whose library never armed; `--no-tests=error` fails a lane whose label matched nothing; the two negative controls are always-on CI steps. | E. |
---
## B. Fixed design decisions (every package follows these; nothing here is re-decided inside a package)
### D1 — Counts, measured at `087685d1`
| what | value |
|---|---|
| `pGLContext` occurrences / lines in `MG_Backend/**` | **335 / 332** (3 lines hold two: `VulkanRenderer.cpp:9214,10719,10963`) |
| arrow sites `pGLContext->` | **277** occurrences on 274 lines: `DirectGLES.cpp` 91, `Managers.cpp` 15, `MultiDraw.cpp` 5, `Utils.cpp` 2, `BackendObject_DirectVulkan.cpp` 2, `DirectVulkan.cpp` 12, `UniformManager.cpp` 14, `VkClearManager.cpp` 1, `VkRenderPassManager.cpp` 3, `VkTextureManager.cpp` 2, `VulkanRenderer.cpp` 127 (130 occurrences) |
| non-arrow lines | **58**: 43 `MOBILEGL_ASSERT`, 7 null guards, 5 `!= nullptr` outside asserts, 1 `== nullptr`, 1 `.get()`, 1 comment (D9 lists every line) |
| distinct accessors read | **62** (Espryt 32, Magma 56) + one `.get()` |
| `PipeInputs` field set | **63** = the 61 rows of `Coverage.def:30-97` + `GetBoundTransformFeedbackLifetimeId` + `HasOpenTransformFeedbackSpan` (D13). `GetBoundTransformFeedbackName` (`Coverage.def:34`) is read by no backend; it stays as a field so the vendored inventory row `backend_read_inventory.md:594` keeps its mapping (`kMGPipeInventoryUnmapped == 0` is asserted at `PipeCatalogueTest.cpp:213-214`), and is marked dead in the header. |
| MG_Impl fill points | **83** call statements over **69** verbs (every function-pointer member of `GLFunctionsTable`, `BackendObject.h:117-289`); `CompileEnv.cpp:135,137` (`MG_Util`) also calls `GL.GetIntegeri_v` and gets no fill point (the entry reads no frontend state on either backend) |
| `SyncPersistentMappedRange` / `SyncGpuWrites` | 20 / 6, exactly as catalogued (D10) |
ROADMAP's "293 / 32+55 / ~93" are the vendored-inventory numbers; the integrator corrects them in the docs (C.5). `MEASUREMENTS.md:82`'s "61 个 PipeInputs 字段" becomes 63.
### D2 — Switches and their spellings
| switch | kind | default | meaning |
|---|---|---|---|
| `MOBILEGL_PIPE_PUSH` | CMake `option()` + compile definition `-DMOBILEGL_PIPE_PUSH=1` (in `MOBILEGL_COMPILE_DEF`, `CMakeLists.txt:523-525` shape) | OFF | `MGB_CTX` expands to `&gPipeInputs`; the fill points and the filler are compiled; `MobileGL/MG_Backend/MGPipe/PipeInputs.cpp` and `MobileGL/MG_Impl/Pipe/PipeFill.cpp` are appended to `SOURCE_FILES`. OFF is the pull build and must be byte-identical to today. |
| `MOBILEGL_PIPE_VERIFY` | CMake `option()` + `-DMOBILEGL_PIPE_VERIFY=1`; **forces `MOBILEGL_PIPE_PUSH` ON** (`set(MOBILEGL_PIPE_PUSH ON)` as a normal variable in the configure that sees VERIFY, with a `message(STATUS)`) | OFF | compiles the second snapshot, the entry comparator, the compare-at-read hook and the two negative-control knobs. Never shipped. |
| `MOBILEGL_PIPE_POISON` | derived preprocessor macro, defined **once**, in `MobileGL/MG_Backend/MGPipe/PipeInputs.h`: `#if MOBILEGL_PIPE_PUSH && (MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED || MOBILEGL_PIPE_VERIFY)` → `#define MOBILEGL_PIPE_POISON 1` else `0` | — | the per-verb generation stamps and the read-side `Fatal{UnmigratedPipeInput}` check. **[correction]** the docs' `MOBILEGL_DEBUG` does not exist; the repo's debug gate is `MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG` (`Defines.h:104-115`). The verify CI build is Release/INFO with `MOBILEGL_BUILD_DISAGGREGATED=OFF`, so the third arm is what arms the poison there without dragging `MG_Remote` in. |
| runtime `MOBILEGL_PIPE_PUSH` (`Features.PipePush`, `Config.h:326`, `ConfigLoader.cpp:245`) | Uint64 bitmask | 0 | **not consumed in P1.** The P1 filler fills every field in a verb's mask regardless of the bitmap; P2 makes the filler yield per bit to the tracker. Bit 63 stays reserved for the CSO negative control. Same name for the macro and the env var is deliberate (`ARCHITECTURE.md:334,367`): the macro selects the arm, the bitmap selects subsystems inside it. |
| runtime `MOBILEGL_PIPE_VERIFY` (`Features.PipeVerify`, `Config.h:332`, `ConfigLoader.cpp:246`) | Bool | 0 | arms the comparator at run time in a verify build. Ignored (with one `MGLOG_W_ONCE`) in a build where `MOBILEGL_PIPE_VERIFY` is not compiled in — that warning is what the arming assertion turns into red. |
| runtime `MOBILEGL_PIPE_VERIFY_FATAL` | Bool, new `Features.PipeVerifyFatal`, parsed `QueryEnvQuirkOverride(...) != ForceOff` (tri-state, like `PipeLegacyMemos`, `ConfigLoader.cpp:251-252`) | 1 | first divergence aborts (`MGLOG_F` + `std::abort()`); `0` logs and counts, for triage and for the lane that must survive to read its own log. |
| runtime `MOBILEGL_PIPE_VERIFY_CORRUPT` | String, new `Features.PipeVerifyCorrupt` (`QueryEnvVariable`) | "" | `<FieldName>` from `kMGPipeInputFieldNames[]`; perturbs that field in the **snapshot** arm before the entry compare. Unknown name → `Fatal{PipeVerifyBadKnob}`. |
| runtime `MOBILEGL_PIPE_POISON_OMIT` | String, new `Features.PipePoisonOmit` | "" | `<Verb>:<FieldName>` (`kMGPipeVerbNames[]` × `kMGPipeInputFieldNames[]`); the filler skips the **stamp** (not the value) of that field for that verb. Unknown name → `Fatal{PipeVerifyBadKnob}`. |
The three new `Features` members and their three parser lines are wrapped in `#if MOBILEGL_PIPE_PUSH` in `Config.h` and `ConfigLoader.cpp` so the pull build's `Features` object is unchanged in size (G1). `ConfigLoader.cpp:242-244`: no allow-list edit is needed for `MOBILEGL_*` names.
### D3 — `MGB_CTX` and the switch header (exact text; A creates it, B and C include it)
`MobileGL/MG_Pipe/PipeInputsSwitch.h` — lives under `MG_Pipe/`, never under `MG_Backend/`, so the pull arm's `pGLContext` spelling is outside what purity gate C greps (`ARCHITECTURE.md:500,349`):
```cpp
// MobileGL - MobileGL/MG_Pipe/PipeInputsSwitch.h
// (banner lines 2-7 as in MGPipe.h:2-7)
#pragma once
#ifndef MOBILEGL_MG_PIPE_INPUTS_SWITCH_H // belt and braces: reachable as <MG_Pipe/..> and <..> (CMakeLists.txt:531,535)
#define MOBILEGL_MG_PIPE_INPUTS_SWITCH_H
// The strangler switch (ARCHITECTURE.md 9.2). Every backend read of frontend state is spelled
// MGB_CTX->Accessor(...). Pull arm: the live GLContext, so the pull build is the tree before P1
// token for token. Push arm: the PipeInputs block the frontend fills at every verb boundary.
// The pull arm is the ONLY place under MobileGL/ outside MG_State and MG_Impl that may spell
// pGLContext; purity gate C greps MG_Backend/ for that token.
#if MOBILEGL_PIPE_PUSH
#include <MG_Backend/MGPipe/PipeInputs.h>
#define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#define MGB_CTX_LIVE (::MobileGL::MG_Pipe::gPipeInputs.IsLive())
#define MGB_CTX_IDENTITY (::MobileGL::MG_Pipe::gPipeInputs.ContextIdentity())
#else
#include <MG_State/GLState/Core.h>
#define MGB_CTX (::MobileGL::MG_State::pGLContext)
#define MGB_CTX_LIVE (::MobileGL::MG_State::pGLContext != nullptr)
#define MGB_CTX_IDENTITY (static_cast<const void*>(::MobileGL::MG_State::pGLContext.get()))
#endif
#endif
```
`MG_State::pGLContext` is `extern UniquePtr<GLState::GLContext>& pGLContext;` (`Core.h:596`), so `MGB_CTX->X()` in the pull arm is exactly today's `MG_State::pGLContext->X()`, and `MGB_CTX_LIVE` is `UniquePtr::operator bool`'s definition spelled out. Backend TUs include `<MG_Pipe/PipeInputsSwitch.h>` immediately after their `MG_State/GLState/Core.h` include (`DirectGLES.cpp:18`, `MultiDraw.cpp:11`, `Utils.cpp:19`, `BackendObject_DirectVulkan.cpp:14`, `DirectVulkan.cpp:12`, `UniformManager.cpp:12`, `VkClearManager.cpp:15`, `VkTextureManager.cpp:13`, `VulkanRenderer.cpp:16`; `Managers.cpp` gets it after its `Managers.h` include, `VkRenderPassManager.cpp` after its own include block — neither includes `Core.h` directly).
### D4 — `PipeInputs`: one struct, three field classes, types identical to today's reads
`MobileGL/MG_Backend/MGPipe/PipeInputs.h`, `namespace MobileGL::MG_Pipe`, `struct PipeInputs` + `inline PipeInputs gPipeInputs{};` (the single global the docs name, `ARCHITECTURE.md:331`; inline variable, no `.cpp` needed for the definition). Includes: `<MG_Pipe/MGPipe.h>` (field ids, verb enum, poison helpers), `<MG_State/GLState/Core.h>` (the frontend types the accessors return — allowed: P13 keeps the `MG_State` include for the verify arm, `ARCHITECTURE.md:369`; the header spells **no** `pGLContext` token). Every accessor has the **same name, parameters and return type** as its `GLContext` counterpart (`Core.h:66-506`), so the `sed` is type-neutral (phase A rule, `ARCHITECTURE.md:328`). Three storage classes:
- **V (value)** — copied out of `GLContext` at fill time by calling the same accessor (`m_x = ctx->GetX(...)`; no derivation logic is re-implemented in `PipeInputs`, which is what keeps the copy semantically identical by construction). Accessor returns the copy (by value or `const&`, whichever `Core.h` uses).
- **O (object reference)** — a `SharedPtr` copy, or a raw pointer to the live `GLContext`-owned slot/array for the accessors that return a non-const reference into the context. Same return type as today; identity is what phase C turns into a handle.
- **F (forwarded)** — argument-keyed lookups and reverse-channel calls. Declared in `PipeInputs.h`, **defined out of line in `MobileGL/MG_Impl/Pipe/PipeFill.cpp`** (the client side, where `pGLContext` may be spelled), forwarding to the live context. Sticky (D6).
| field (id from `kMGPipeInputFieldNames`) | class | storage in `PipeInputs` | accessor signature (= `Core.h`) | fill |
|---|---|---|---|---|
| GetActiveTextureUnit | V | `Int m_activeTextureUnit` | `Int GetActiveTextureUnit() const` | `ctx->GetActiveTextureUnit()` |
| GetBlendColor | V | `FloatVec4 m_blendColor` | `const FloatVec4& GetBlendColor() const` | |
| GetBlendEquationIndexed | V | `BlendEquation m_blendEquation[kMGMaxDrawBuffers][2]` | `void GetBlendEquationIndexed(Uint, BlendEquation&, BlendEquation&) const` | loop i<8 |
| GetBlendFuncIndexed | V | `BlendFactor m_blendFunc[kMGMaxDrawBuffers][4]` | `void GetBlendFuncIndexed(Uint, BlendFactor&×4) const` | loop i<8 |
| GetBoundTransformFeedbackName | V | `Uint m_boundTransformFeedbackName` | `Uint GetBoundTransformFeedbackName() const` | **dead field**: filled, never read (D1) |
| GetBoundVertexArray | O | `SharedPtr<VertexArrayObject> m_boundVertexArray` | `const SharedPtr<VertexArrayObject>& GetBoundVertexArray()` | copy |
| GetBufferBindingSlot | O | `BindingSlot<BufferObject>* m_bufferBindingSlot[BufferTargetCount]` | `BindingSlot<BufferObject>& GetBufferBindingSlot(BufferTarget)` | `&ctx->GetBufferBindingSlot(t)` for every `t` in `GlobalBufferTargets` (`BufferState.h:15`); others null; accessor asserts non-null |
| GetBufferBindingPoint | O | `BindingSlotRange1D<BufferObject>* m_bufferBindingPointBase[BufferTargetCount]` | `BindingSlotRange1D<BufferObject>& GetBufferBindingPoint(BufferTarget, Uint)` | `&ctx->GetBufferBindingPoint(t, 0)` for `t` in `BufferBindPointTargets` (`BufferState.h:20`); storage is `Array<Array<..., 84>, N>` (`BufferState.h:73`) so `base[index]` is the live slot |
| GetBufferBindingPointCount | F | — | `SizeT GetBufferBindingPointCount(BufferTarget) const` | forward (constexpr table) |
| GetTouchedBufferBindingPointCount | V | `SizeT m_touchedBindingPointCount[BufferTargetCount]` | `SizeT GetTouchedBufferBindingPointCount(BufferTarget) const` | loop over `BufferBindPointTargets` |
| GetClampReadColor | V | `GLenum` | `GLenum GetClampReadColor() const` | |
| GetClearColor / GetClearDepth / GetClearStencil | V | `FloatVec4` / `Float` / `Uint32` | as `Core.h:272-276` | |
| GetColorMaskIndexed | V | `BoolVec4 m_colorMask[kMGMaxDrawBuffers]` | `BoolVec4 GetColorMaskIndexed(Uint) const` | loop |
| GetCullFaceMode | V | `CullFaceMode` | | |
| GetCurrentVertexAttribute | V | `CurrentVertexAttributeValue m_currentVertexAttribute[VertexArrayObject::MAX_VERTEX_ATTRIBS]` (32, `VertexArrayObject.h:24`) | `const CurrentVertexAttributeValue& GetCurrentVertexAttribute(Uint) const` | loop |
| GetDepthFunc / GetDepthMask | V | `DepthTestFunc` / `Bool` | | |
| GetDepthRangeIndexed | V | `FloatVec2 m_depthRange[RenderStateParameters::MAX_VIEWPORTS]` | `const FloatVec2& GetDepthRangeIndexed(Uint) const` | loop 16 |
| GetFramebufferBindingSlot | O | `BindingSlot<FramebufferObject>* m_framebufferBindingSlot[FramebufferTargetCount]` | `BindingSlot<FramebufferObject>& GetFramebufferBindingSlot(FramebufferTarget)` | both targets |
| GetImageTextureBinding | O | `ImageTextureBinding* m_imageTextureBindingBase` | `ImageTextureBinding& GetImageTextureBinding(Int)` (+ const overload) | `&ctx->GetImageTextureBinding(0)`; storage `Array<ImageTextureBinding, 192>` (`TextureState.h:129`) |
| GetLineWidth / GetLogicOp / GetMaxTouchedTextureUnit / GetMinSampleShadingValue | V | `Float` / `LogicOperation` / `Int` / `Float` | | |
| GetPatchDefaultInnerLevel / GetPatchDefaultOuterLevel / GetPatchVertices | V | `FloatVec2` / `FloatVec4` / `Uint` | `const FloatVec2&` / `const FloatVec4&` / `Uint` | |
| GetPipelineStateVersion / GetRenderStateParametersVersion | V | `Uint` / `Uint` | | |
| GetPixelStoreParameters | V | `PixelStoreParameters m_pixelStore[2]` (`[0]`=pack, `[1]`=unpack) | `PixelStoreParameters GetPixelStoreParameters(Bool isUnpack) const` | both |
| GetPolygonModeFront / GetPolygonOffsetFactor / GetPolygonOffsetUnits / GetPrimitiveRestartIndex / GetProvokingVertexMode | V | `GLenum` / `Float` / `Float` / `Uint32` / `ProvokingVertexMode` | | |
| GetProgramForDispatch / GetProgramForDraw / GetTransformFeedbackProgram | O | `SharedPtr<ProgramObject>` ×3 | `const SharedPtr<ProgramObject>& ...()` | copy (`GetProgramForDraw()` is called once per fill; it has a guarded static, `DirectGLES.cpp:2965-2968`) |
| GetProgramObject | F | — | `const SharedPtr<ProgramObject>& GetProgramObject(Uint)` | forward |
| GetRenderStateParameters | V | `RenderStateParameters m_renderState` (1168 B) | `const RenderStateParameters& GetRenderStateParameters() const` | memcpy-by-assignment |
| GetSamplingResolutionGeneration / GetTextureBindGeneration / GetTextureContextId | V | `Uint64` ×3 | | |
| GetScissorBox | V | `IntVec4` | `const IntVec4& GetScissorBox() const` | |
| GetStencilState | V | `StencilFaceState m_stencil[2]` | `const StencilFaceState& GetStencilState(StencilFace) const` | both faces |
| GetTextureObject | F | — | `const SharedPtr<ITextureObject>& GetTextureObject(Uint)` | forward |
| GetTextureUnitObject | O | `TextureUnit* m_textureUnitBase` | `TextureUnit& GetTextureUnitObject(Int)` | `&ctx->GetTextureUnitObject(0)`; storage `Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS=192>` (`TextureState.h:41,128`) |
| GetTransformFeedbackCapturedVertices / GetTransformFeedbackGeneration / GetTransformFeedbackPausedPrimitiveCounter / GetBoundTransformFeedbackLifetimeId | V | `Uint64` ×4 | as `Core.h:334,349,357,441` | |
| GetViewport | V | `IntVec4` | `IntVec4 GetViewport() const` | |
| GetViewportIndexed | V | `FloatVec4 m_viewport[MAX_VIEWPORTS]` | `const FloatVec4& GetViewportIndexed(Uint) const` | loop 16 |
| IsCapabilityEnabled | V | `Bool m_capability[CapabilityInputCount]` (37 entries, `MGPipeValueTypes.h:168-204`) | `Bool IsCapabilityEnabled(CapabilityInput) const` | loop over all caps (`FramebufferSrgb` copies today's constant `false`, `MEASUREMENTS.md:84` — no change of value) |
| IsCapabilityEnabledIndexed | V | `Bool m_blendIndexed[kMGMaxDrawBuffers]`, `Bool m_scissorIndexed[MAX_VIEWPORTS]` | `Bool IsCapabilityEnabledIndexed(CapabilityInput, Uint) const` | `Blend` ×8, `ScissorTest` ×16; any other cap → `Fatal{UnmigratedPipeInput}` with the cap name (no backend asks; `VulkanRenderer.cpp:5437` asks `Blend`) |
| IsTransformFeedbackActive / IsTransformFeedbackPaused | V | `Bool` ×2 | | |
| HasOpenTransformFeedbackSpan | F | — | `Bool HasOpenTransformFeedbackSpan(Uint64) const` | forward |
| InvalidateCompileEnv / ValidateProgramName / RecordError | F | — | `void InvalidateCompileEnv()` / `Bool ValidateProgramName(Uint) const` / `void RecordError(ErrorCode, UniquePtr<ErrorInfo>)` | forward (`RecordError` forwards even when no context is live? no: `IsLive()` false → drop with `MGLOG_E_ONCE`; today's guarded sites (`Managers.cpp:8750`) never reach it without a context) |
Non-field members: `const void* m_contextIdentity` (the live `GLContext` address at fill; `ContextIdentity()` serves `MGB_CTX_IDENTITY`), `Bool m_live` (`IsLive()`), `MGPipeVerb m_currentVerb`, and under `MOBILEGL_PIPE_POISON` an `MGPipeFilledState m_filled` (`PipeFilled.inc:293-296`, `CurrentVerbSerial` + `FilledGen[63]`). Estimated size ≈ 3.5 KB (well under the docs' ~20 KB). `static_assert(sizeof(PipeInputs) < 20 * 1024)`.
Every accessor body is: `MGP_INPUT_CHECK(MGPipeInputField::X);` (poison, D6) → `MGP_INPUT_VERIFY_READ(MGPipeInputField::X, idx0, idx1);` (compare-at-read, D8) → return the storage. Both macros expand to nothing when their switch is off, so a plain `MOBILEGL_PIPE_PUSH` build's accessor is a load.
Known gap, closed in P2 (Espryt 0b deletes `g_fbSlotCache`, `ROADMAP.md:18`): the five reads through `GetFramebufferBindingSlotFast` (`DirectGLES.cpp:1611,1905,2743,2858,2933`) bypass the accessor once the cache is warm, so they are not poison-checked; the entry compare still covers `GetFramebufferBindingSlot`.
### D5 — Files created, and where the `pGLContext` spelling is allowed
| path | owner | contents | may spell `pGLContext`? |
|---|---|---|---|
| `MobileGL/MG_Pipe/PipeInputsSwitch.h` | A | D3 | only in the `#else` arm |
| `MobileGL/MG_Pipe/FillPoints.def` | A | D7 source of truth | no |
| `MobileGL/MG_Pipe/generated/PipeFillPoints.inc` | A (generated) | verb enum, class tables, masks | no |
| `MobileGL/MG_Backend/MGPipe/PipeInputs.h` | A | D4 struct, accessors, `gPipeInputs`, `MOBILEGL_PIPE_POISON`, the two accessor macros | **no** |
| `MobileGL/MG_Backend/MGPipe/PipeInputs.cpp` | A | `MGPipeInputPoisonFatal` wrapper with verb name, per-field equality (`MGPipeInputsFieldEqual`), corruption injector | **no** |
| `MobileGL/MG_Impl/Pipe/PipeFill.h` | A | `MGP_FILL(Verb)` macro; declarations of `MGPipeFillForVerb`, `SnapshotFromGLContext` | no (macro expands to nothing under pull) |
| `MobileGL/MG_Impl/Pipe/PipeFill.cpp` | A | the filler (non-verify branch), `SnapshotFromGLContext` (verify branch), the F-class forwarders, `IsLive`, compare-at-read hook, entry compare, knob parsing | yes (client side) |
**[deviation]** `ARCHITECTURE.md:553` lists `MGPipeImpl_DirectGLES.cpp` / `MGPipeImpl_DirectVulkan.cpp` as `[P1+]`. P1 creates neither: nothing backend-specific exists in `PipeInputs` until a backend installs an `MGPipeContext` table (P5). The directory `MG_Backend/MGPipe/` is created with `PipeInputs.h/.cpp`; the integrator records this in the docs.
### D6 — Poison: per-verb generation, seven sticky fields, the Fatal
- `MGPipeFillForVerb(MGPipeVerb v)` (`PipeFill.cpp`) does, in order: `++m_filled.CurrentVerbSerial` (starts at 1 so `FilledGen == 0` means never filled); `m_currentVerb = v`; if `pGLContext == nullptr` → `m_live = false`, return; else `m_live = true`, `m_contextIdentity = ctx`, then for every field in `kMGPipeClassFieldMask[kMGPipeVerbClass[v]]`: copy per D4 and `m_filled.FilledGen[f] = CurrentVerbSerial` — **unless** `(v, f)` equals the parsed `MOBILEGL_PIPE_POISON_OMIT` pair, in which case the value is still copied and only the stamp is skipped (an omission indistinguishable from a forgotten row). Cost is irrelevant in P1 (verify build) and is replaced by the tracker in P2.
- `MGP_INPUT_CHECK(f)` under `MOBILEGL_PIPE_POISON`: `if (!MGPipeInputFieldIsFresh(m_filled, f)) MGPipeInputPoisonFatalForVerb(f, m_currentVerb);` → `MGPipeInputPoisonFatal(f, kMGPipeVerbNames[v])` (`PipeFilled.inc:298-302`: `MGLOG_F("MGPipe: Fatal{UnmigratedPipeInput, \"%s@%s\"}")` + `std::abort()`; before the first verb the name is `"<none>"`). `MGLOG_F` is live at every log level (`Log.h:87-92`) and this is not `MOBILEGL_ASSERT` on purpose (inert in INFO builds, `Defines.h:113-115`).
- **Sticky = exactly the seven F-class fields**: `RecordError`, `InvalidateCompileEnv`, `ValidateProgramName`, `GetProgramObject`, `GetTextureObject`, `HasOpenTransformFeedbackSpan`, `GetBufferBindingPointCount`. Argument (goes into `Coverage.def` as the reason string): each takes an argument that is not verb state — a GL name, a lifetime id, a target — i.e. it is a lookup or a reverse-channel write, not a state read; there is no value the filler could copy and no verb whose fill could make it stale; phase C replaces them with handle tables/callbacks. They are stamped once (`FilledGen = 1`) by the first fill that sees a live context and pass `MGPipeInputFieldIsFresh` through the `Sticky → FilledGen != 0` branch (`PipeFilled.inc:304-307`). **None of the version/generation accessors is sticky** (`GetTextureContextId`, `GetTextureBindGeneration`, `GetSamplingResolutionGeneration`, `GetTransformFeedbackGeneration`, `GetPipelineStateVersion`, `GetRenderStateParametersVersion` change under verbs and are precisely what the poison must protect; scout R5).
- `kMGPipeInputFieldSticky[]` stops being all-`false`: `Coverage.def` gains `MGP_COVERAGE_STICKY_LIST(X)` with seven `X(Accessor, "reason")` rows; `gen_filled` emits `true` for those and `sys.exit`s on a name that is not an accessor.
- `IsCapabilityEnabledIndexed` with a cap other than `Blend`/`ScissorTest`, and `GetBufferBindingSlot`/`GetBufferBindingPoint` with a target the fill left null, are also `MGPipeInputPoisonFatal` (the message names the field and the verb; the cap/target goes in a preceding `MGLOG_E`).
### D7 — Fill points: 69 verbs, 9 classes, one `.def`, one generated `.inc`, one macro
`MobileGL/MG_Pipe/FillPoints.def` (hand-maintained, A):
```
// X(Verb, Class) — one row per function-pointer member of MG_Backend::GLFunctionsTable (BackendObject.h:117-289),
// in declaration order. gen_pipe.py parses that struct and refuses a row set that is not exactly its member set.
#define MGP_FILL_VERB_LIST(X) \
X(DrawArrays, kDraw) X(DrawElements, kDraw) ... X(GetGpuTimestampNs, kQuery)
// (must be followed by an empty line: gen_pipe.py's block regexes end at a blank line)
// X(Class) — the nine verb classes (ARCHITECTURE.md:153 names eight; kProgramOp is split out because
// ShaderStorageBlockBinding is the one non-draw verb that syncs Espryt's render state and textures).
#define MGP_FILL_CLASS_LIST(X) \
X(kDraw) X(kDispatch) X(kClear) X(kBlitOrCopy) X(kTextureOp) X(kReadback) X(kXfbSpan) X(kProgramOp) X(kQuery)
// X(Class, Field) — the may-read table. A field named here is filled and stamped at every verb of the class;
// a read of a field NOT named here is Fatal{UnmigratedPipeInput, "Field@Verb"} in a poison build.
#define MGP_FILL_FIELD_LIST(X) \
X(kDraw, GetBoundVertexArray) ...
```
Verb → class (69 rows; the count and the membership are the contract, verified by a `gen_pipe.py` parse of `struct GLFunctionsTable`):
| class | verbs |
|---|---|
| `kDraw` (20) | DrawArrays, DrawElements, DrawElementsBaseVertex, MultiDrawArrays, MultiDrawElements, MultiDrawElementsBaseVertex, MultiDrawElementsIndirect, MultiDrawArraysIndirect, MultiDrawElementsIndirectCount, MultiDrawArraysIndirectCount, DrawRangeElementsBaseVertex, DrawRangeElements, DrawElementsInstancedBaseVertexBaseInstance, DrawElementsInstancedBaseVertex, DrawElementsInstancedBaseInstance, DrawElementsInstanced, DrawElementsIndirect, DrawArraysInstancedBaseInstance, DrawArraysInstanced, DrawArraysIndirect |
| `kDispatch` (2) | DispatchCompute, DispatchComputeIndirect |
| `kClear` (9) | Clear, ClearBufferfi, ClearBufferfv, ClearBufferiv, ClearBufferuiv, ClearNamedFramebufferfv, ClearNamedFramebufferfi, ClearNamedFramebufferiv, ClearNamedFramebufferuiv |
| `kBlitOrCopy` (5) | BlitFramebuffer, BlitNamedFramebuffer, CopyTexImage2D, CopyTexSubImage2D, CopyImageSubData |
| `kTextureOp` (2) | GenerateMipmap, BindImageTexture |
| `kReadback` (3) | ReadPixels, GetTexImage, GetTextureImage |
| `kXfbSpan` (6) | BeginTransformFeedback, EndTransformFeedback, PauseTransformFeedback, ResumeTransformFeedback, BindTransformFeedback, DeleteTransformFeedback |
| `kProgramOp` (1) | ShaderStorageBlockBinding |
| `kQuery` (21) | BeginOcclusionQuery, EndOcclusionQuery, BeginTimeElapsedQuery, EndTimeElapsedQuery, QueryCounterTimestamp, BeginXfbPrimitivesQuery, EndXfbPrimitivesQuery, IsQueryResultAvailable, GetQueryResult64, DeleteBackendQuery, IsTimerQuerySupported, GetGpuTimestampNs, FenceSync, ClientWaitSync, WaitSync, GetSyncStatus, DeleteSync, MemoryBarrier, MemoryBarrierByRegion, PatchParameteri, GetIntegeri_v |
Class → may-read fields (from the verified reachability, `scout-pull-sites.md` §4.2, union over both backends; the seven sticky fields are implicit everywhere and are not listed). **The verify lane is the oracle: a `Fatal{UnmigratedPipeInput}` found there is fixed by adding the row to this table, never by marking the field sticky.**
| class | fields |
|---|---|
| `kDraw` | GetBoundVertexArray, GetProgramForDraw, GetBufferBindingSlot, GetBufferBindingPoint, GetTouchedBufferBindingPointCount, GetTextureUnitObject, GetTextureContextId, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetSamplingResolutionGeneration, GetImageTextureBinding, GetCurrentVertexAttribute, GetRenderStateParameters, GetRenderStateParametersVersion, GetPipelineStateVersion, GetViewport, GetViewportIndexed, GetDepthRangeIndexed, GetScissorBox, IsCapabilityEnabled, IsCapabilityEnabledIndexed, GetBlendColor, GetBlendFuncIndexed, GetBlendEquationIndexed, GetColorMaskIndexed, GetLogicOp, GetDepthFunc, GetDepthMask, GetStencilState, GetCullFaceMode, GetPolygonModeFront, GetPolygonOffsetFactor, GetPolygonOffsetUnits, GetLineWidth, GetMinSampleShadingValue, GetProvokingVertexMode, GetPatchVertices, GetPatchDefaultOuterLevel, GetPatchDefaultInnerLevel, GetPrimitiveRestartIndex, GetFramebufferBindingSlot, IsTransformFeedbackActive, IsTransformFeedbackPaused, GetTransformFeedbackProgram, GetTransformFeedbackGeneration, GetBoundTransformFeedbackLifetimeId, GetTransformFeedbackCapturedVertices |
| `kDispatch` | GetProgramForDispatch, GetBufferBindingSlot, GetBufferBindingPoint, GetTouchedBufferBindingPointCount, GetTextureUnitObject, GetTextureContextId, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetSamplingResolutionGeneration, GetImageTextureBinding, GetFramebufferBindingSlot, GetPatchVertices, GetPatchDefaultOuterLevel, GetPatchDefaultInnerLevel (Espryt `SyncCurrentProgram` → `AttachPassthroughTessControlStage`, `Managers.cpp:7193-7204`) |
| `kClear` | GetRenderStateParameters, GetRenderStateParametersVersion, GetViewport, IsCapabilityEnabled, GetFramebufferBindingSlot, GetClearColor, GetClearDepth, GetClearStencil, GetScissorBox, GetColorMaskIndexed, GetDepthMask, GetStencilState, GetTextureUnitObject, GetTextureContextId, GetSamplingResolutionGeneration, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetImageTextureBinding |
| `kBlitOrCopy` | GetFramebufferBindingSlot, IsCapabilityEnabled, GetScissorBox, IsTransformFeedbackActive, IsTransformFeedbackPaused, GetRenderStateParameters, GetRenderStateParametersVersion, GetViewport, GetActiveTextureUnit, GetTextureUnitObject, GetTextureContextId, GetSamplingResolutionGeneration, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetImageTextureBinding, GetColorMaskIndexed, GetDepthMask, GetStencilState |
| `kTextureOp` | GetActiveTextureUnit, GetTextureUnitObject, GetImageTextureBinding, GetTextureContextId, GetSamplingResolutionGeneration, GetTextureBindGeneration, GetMaxTouchedTextureUnit |
| `kReadback` | GetPixelStoreParameters, GetBufferBindingSlot, GetFramebufferBindingSlot, GetActiveTextureUnit, GetTextureUnitObject, GetClampReadColor, IsCapabilityEnabled, GetRenderStateParameters, GetRenderStateParametersVersion, GetViewport, GetTextureContextId, GetSamplingResolutionGeneration, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetImageTextureBinding |
| `kXfbSpan` | GetTransformFeedbackProgram, GetBufferBindingPoint, GetTouchedBufferBindingPointCount, GetTransformFeedbackCapturedVertices, IsTransformFeedbackActive, IsTransformFeedbackPaused, GetTransformFeedbackGeneration, GetBoundTransformFeedbackLifetimeId |
| `kProgramOp` | GetRenderStateParameters, GetRenderStateParametersVersion, GetViewport, IsCapabilityEnabled, GetFramebufferBindingSlot, GetTextureUnitObject, GetTextureContextId, GetSamplingResolutionGeneration, GetTextureBindGeneration, GetMaxTouchedTextureUnit, GetImageTextureBinding |
| `kQuery` | GetTransformFeedbackPausedPrimitiveCounter (`DirectVulkan.cpp:1237,1284`) — every other verb in the class reads nothing; the fill is a serial bump |
`gen_pipe.py` G5b (`gen_fill_points`) emits `generated/PipeFillPoints.inc`: `enum class MGPipeVerb : Uint8 { DrawArrays, ..., kVerbCount }` (69), `kMGPipeVerbNames[]`, `enum class MGPipeVerbClass : Uint8` (9), `kMGPipeVerbClassNames[]`, `kMGPipeVerbClass[kMGPipeVerbCount]`, `struct MGPipeFieldMask { Uint64 Words[2]; }` + `kMGPipeClassFieldMask[9]` with the seven sticky fields OR'ed in, `MGPipeFieldMaskHas(mask, field)`, and `static_assert(kMGPipeVerbCount == 69)`. It is included from `MGPipe.h` after `PipeFilled.inc` (`MGPipe.h:86`) — the banner's "included from MG_Pipe/MGPipe.h inside namespace MobileGL::MG_Pipe" stays true.
`MobileGL/MG_Impl/Pipe/PipeFill.h`:
```cpp
#pragma once
#if MOBILEGL_PIPE_PUSH
#include <MG_Pipe/MGPipe.h>
namespace MobileGL::MG_Pipe {
void MGPipeFillForVerb(MGPipeVerb verb); // PipeFill.cpp
}
#define MGP_FILL(Verb) ::MobileGL::MG_Pipe::MGPipeFillForVerb(::MobileGL::MG_Pipe::MGPipeVerb::Verb)
#else
#define MGP_FILL(Verb) ((void)0)
#endif
```
Placement rule (A applies it at the 83 statements of §B.7): `MGP_FILL(X);` is the statement **immediately before** the call through the table, after every early return the call is behind (the `ConditionalRenderDiscardsCommand()` check in the `*_Backend` wrappers, `GL_Drawing.cpp:529`; the `if (!f) { RecordError; return; }` null checks; the `if (const auto f = table.GL.X) {` guards). A verb whose table entry is null on this backend never bumps. For a call inside a loop (`GL_Query.cpp:915`, `GL_Sync.cpp:229`) the fill is inside the loop body before the call. In the pull build the macro is `((void)0)` (G1).
### D8 — The comparator: what it compares, where it runs, what it prints
Two mechanisms, both compiled only under `MOBILEGL_PIPE_VERIFY` and active only when `Features.PipeVerify` is set:
1. **Entry compare, once per verb** ("每 draw 比一次"): at the end of `MGPipeFillForVerb`, `SnapshotFromGLContext(gPipeSnapshot, mask)` fills a second, file-static `PipeInputs` from the live context (the "verify branch" that survives P13), applies `MOBILEGL_PIPE_VERIFY_CORRUPT` to **the snapshot** (perturb one field: flip a bool, `+1` a scalar, `^0x5A` the first byte of a struct, swap a pointer for `nullptr`), then `MGPipeVerifyInputs(gPipeInputs, gPipeSnapshot, mask, &field)` compares every field in the mask with `MGPipeInputsFieldEqual(field, a, b)` (`PipeInputs.cpp`: V via G4's `MGPipeFieldEqual` — bitwise floats, field-wise structs; O by identity). **Honest statement of what this proves in P1**: both arms come from the same context at the same instant, so it is tautological except under the CORRUPT knob (negative control A) — its oracle role starts in P2 when the tracker fills the first arm. It is nevertheless the mechanism the gate names, and P1 lands it complete.
2. **Compare-at-read, on every accessor**: `MGP_INPUT_VERIFY_READ(f, i0, i1)` calls `MGPipeVerifyReadHook(f, i0, i1)` (`PipeFill.cpp`), which re-reads the same accessor with the same indices from the live context and compares against the stored value. This is the arm that is **real in P1**: it catches any field whose value differs between the verb boundary (what push reads) and the moment of the read (what pull reads) — a frontend counter bumped by the backend mid-verb, a texture re-mint bumping `SamplingResolutionGeneration` between `TrySetupDrawFastPath` and `SetupDraw` (`VulkanRenderer.cpp:6304` vs `:6999`), an error recorded by a helper. Zero divergence here is the semantic statement "push equals pull for every value any backend read in the 40 traces and 808 integration entries".
Reporting: `MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}", field, verbName, serial, "entry"|"read")` then `std::abort()` unless `PipeVerifyFatal` is off (then a relaxed-atomic counter, summarised at teardown with `MGLOG_E`). Arming, once, at the first fill under verify: `MGLOG_I("MGPipe: verify armed - %u fields, %u verbs, fatal=%d")` (an `MGLOG_I` with a stated reason, per `PipeStats.cpp:224-231`; the lanes grep for `MGPipe: verify armed`). Knob acknowledgements: `MGLOG_I("MGPipe: verify corruption armed - %s")`, `MGLOG_I("MGPipe: poison omission armed - %s@%s")`. Never `MGLOG_D` (compiled out at INFO, `Log.h:59-64`); never stdio (`test.yml:862-869`).
G4 growth (A):
- `PipeFields.def` gains `MGP_FIELDS_RenderStateParameters` (all 67 members of `MGPipeValueTypes.h` `struct RenderStateParameters`, in declaration order, `Viewports` … `ScissorBoxes`), `MGP_FIELDS_PixelStoreParameters` (8), `MGP_FIELDS_PerBufferBlendState` (7), `MGP_FIELDS_StencilFaceState` (7), `MGP_FIELDS_DynamicBackendParameters` (every member of `BackendObject.h:316-…`, `Pad`-named members excluded), `MGP_FIELDS_MGHostSpan` (`Ptr`, `Seg`, `Size`, `Offset`; `Pad0` excluded), all appended to `MGP_VERIFY_PAYLOAD_LIST` → `kMGPipeVerifiedPayloadCount` 63 → **69**. `MEMCMP_FALLBACK_TYPES` (`gen_pipe.py:167-172`) becomes empty and the fallback branch in `gen_verify` becomes `static_assert(sizeof(T) == 0, "no field list in PipeFields.def")` — a future memcmp fallback is a compile error, not a padding false positive.
- Two overloads added to `gen_verify`'s emitted `MGPipeFieldEqual`, declared **before** the generic template: `template <class T, SizeT N> Bool MGPipeFieldEqual(const Array<T, N>&, const Array<T, N>&)` (element-wise; `Array` is `std::array`, `Types.h:57`), and `template <class D, class T, SizeT N> Bool MGPipeFieldEqual(const VecBase<D, T, N>& a, const VecBase<D, T, N>& b)` (element-wise over `data`, so `FloatVec4`/`FloatVec2` compare **bitwise** — `VecBase::operator==` at `VectorTypes.h:41` is IEEE `==`, under which a NaN patch level would differ from itself; the docs' stated policy is bitwise, `ARCHITECTURE.md:159`). Partial ordering picks these over `const T&`.
- Coverage assertion ("the comparator's coverage is itself asserted", `PipeFields.def:15-17`): `gen_pipe.py` gains `check_field_lists_cover_struct_members()`: for every payload in `MGP_VERIFY_PAYLOAD_LIST`, locate `struct <Name> {` in `MGPipeTypes.h`, `MGPipeValueTypes.h`, `MGPipeHostSpan.h`, `MG_Backend/BackendObject.h` (comments/strings masked, `gen_pipe_dirty_surface.py:46-85` shape), collect the direct data members (regex over one-member-per-line bodies; skip `static`, `constexpr`, member functions, nested types; a member named `Pad\d*` is padding), and `sys.exit` on any member absent from the `F(...)` list or any `F(...)` name that is not a member. Runs in both modes (`--check` included) and therefore in `pipe-gates`. Negative control inside `gen_pipe.py --self-test` (new flag): a canned struct/field-list pair that must trip.
### D9 — The 58 non-arrow lines: exact rewrite per category, and why the pull build does not move
| category | count | lines | rewrite | pull-build effect |
|---|---|---|---|---|
| `MOBILEGL_ASSERT(MG_State::pGLContext, "...")` | 34 | `DirectVulkan.cpp` 337,343,349,355,362,369,376,383,389,394,445,451,496,523,567,582,616,622,631,638,644,650,656,741,746,752,758,817,842,865,971,977,1001,1008 | `MOBILEGL_ASSERT(MGB_CTX_LIVE, "...")` | none (macro is empty at INFO, `Defines.h:113-115`) |
| `MOBILEGL_ASSERT(MG_State::pGLContext != nullptr, ...)` | 9 | `UniformManager.cpp` 812,837,992,1152,1266,1651,1806,1888,1968 | `MOBILEGL_ASSERT(MGB_CTX_LIVE, ...)` | none |
| bare guard `if (MG_State::pGLContext) {` | 5 | `Managers.cpp` 3644,3844,8750; `BackendObject_DirectVulkan.cpp` 388,786 | `if (MGB_CTX_LIVE) {` | `UniquePtr::operator bool` is `get() != nullptr`; identical codegen expected |
| compound guard | 2 | `Managers.cpp` 3773 (`&& MG_State::pGLContext &&`), 4735 (`if (MG_State::pGLContext && ...`) | `MGB_CTX_LIVE` in place | as above |
| ternary condition `MG_State::pGLContext != nullptr` | 3 | `Managers.cpp` 7192,7200,7203 (their `?` arms 7193,7201,7204 are arrow lines and take the sed) | `MGB_CTX_LIVE` | as above |
| `!= nullptr` in a condition | 2 | `DirectVulkan.cpp` 1236; `VulkanRenderer.cpp` 12766 | `MGB_CTX_LIVE` | as above |
| `== nullptr` | 1 | `VulkanRenderer.cpp` 11267 | `!MGB_CTX_LIVE` | as above |
| `.get()` raw capture + `decltype` alias | 1 (+ arrow line 143) | `DirectGLES.cpp` 143-155 | `:143` → `decltype(MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw))`; `:144` → `static const void* g_fbSlotCacheContext = nullptr;`; `:147` → `const void* ctx = MGB_CTX_IDENTITY;`; `:150` → `g_fbSlotCache[i] = &MGB_CTX->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));` | a `const void*` compare instead of `GLContext*`: identical codegen expected; if `.text` moves, this is the first suspect |
| comment | 1 | `VertexInputStateFactory.h:133` | "they live on the frontend context's VAOs" | none |
| ternary with arrow on one line | (arrow line) | `DirectVulkan.cpp` 1284 | `MGB_CTX_LIVE ? MGB_CTX->GetTransformFeedbackPausedPrimitiveCounter() : 0` | as above |
**[deviation]** `ARCHITECTURE.md:344` says the ~34 asserts are *deleted*. They are converted instead: identical in every INFO build (G1 is unaffected), preserves DEBUG-build parity, and gate C is satisfied equally (no `pGLContext` token remains). Under push, `MGB_CTX_LIVE` is always true once a context exists, which changes the guarded sites' semantics only in the push arm — that is the "空守卫/三元重写推迟到 P2" the gate names: P1 keeps them textually equivalent, P2 gives them real meaning.
Expected G1 result: `.text` delta **0**, symbols 0/0/0/0. The only code-bearing rewrites are the 13 guard/compare lines and the `.get()` block, and each is a like-for-like pointer test.
### D10 — `SyncPersistentMappedRange` (20) + `SyncGpuWrites` (6): per-site ownership table
Reconcile classes are `ARCHITECTURE.md:233-238` (§5.7): **R0** none; **R1** EBO/max-index scan = `SyncPersistentMappedRange` **+** `SyncGpuWrites`; **R2** `*IndirectCount` count = **only** `SyncPersistentMappedRange` (open question #15, `ROADMAP.md:88` — recorded, not fixed); **R3** server-side restart rewrite / multi-draw flatten = server reads its mirror, GPU-writer visibility by `OnGpuWritten`; **R4** UBO host payload, the immovable consumer (D-B8, `ARCHITECTURE.md:294`). In P1 every site stays exactly where it is (the `sed` touches none of these lines); the table records the class each site must reproduce when P8 moves the reconcile to the client.
| # | site (verified) | receiver | verb | today | class |
|---|---|---|---|---|---|
| P1 | `DirectGLES.cpp:263` | drawBuffer | draw indirect (CPU fallback `ResolveIndirectCommandBytes`) | SPMR only | R2 |
| P2 | `DirectGLES.cpp:4468` (+ G1 `:4469`) | indexBuffer | draw, restart substitution | SPMR + SGW | R1 |
| P3/P4 | `DirectGLES.cpp:4722,4723` | drawBuffer, parameterBuffer | `MultiDrawElementsIndirectCount` | SPMR only | R2 |
| P5/P6 | `DirectGLES.cpp:4824,4825` | drawBuffer, parameterBuffer | `MultiDrawArraysIndirectCount` | SPMR only | R2 |
| P7 | `Managers.cpp:1569` | bufferObject | buffer op (`EnsureBufferResource`) | SPMR | R0 (client array/staging: application memory, no GPU writer) |
| P8 | `MultiDraw.cpp:510` (+ G3 `:511`) | indexBuffer | multi-draw flatten `RunRebasedDrawElements` | SPMR + SGW | R3 |
| P9 | `DirectVulkan.cpp:280` | drawBuffer | draw indirect (CPU fallback) | SPMR only | R2 |
| P10 | `DirectVulkan.cpp:471` | parameterBuffer | `MultiDrawArraysIndirectCount` | SPMR only | R2 |
| P11 | `DirectVulkan.cpp:795` | indexBufferShared | `BuildClosedLineLoopIndices` (index read) | SPMR only | R1 (client pointer source → R0 when the indices are client memory) |
| P12 | `UniformManager.cpp:2023` | bufferObject | `ResolveUniformBufferPayload` | SPMR | **R4** — immovable |
| P13 | `VkBufferManager.cpp:628` | bufferObject | `AcquireResidentSlice` | SPMR | R0 |
| P14 | `VkBufferManager.cpp:679` | bufferObject | `AcquireStreamedSlice` | SPMR | R0 |
| P15 | `VulkanRenderer.cpp:3434` (+ G4 `:3433`) | indexBufferShared | `TryComputeMaxIndexFromHostBytes` | SPMR + SGW | R1 |
| P16 | `VulkanRenderer.cpp:3513` | bufferObject | `TryBindResolvedVertexBindings` | SPMR | R0 |
| P17 | `VulkanRenderer.cpp:3828` (+ G5 `:3827`) | sourceBufferShared | fp64 narrowing in `UploadAndBindVertexBuffers` | SPMR + SGW | R1 (a GPU-written vertex source) |
| P18 | `VulkanRenderer.cpp:7137` | indirectBuffer | `DispatchComputeIndirect` | SPMR only | R2 |
| P19/P20 | `VulkanRenderer.cpp:12132,12133` | drawBuffer, parameterBuffer | `MultiDrawElementsIndirectCount` | SPMR only | R2 |
| G2 | `Managers.cpp:2643` | bufferObject | `SyncFloat64AttributeAsFloat32` | SGW | R1 |
| G6 | `VulkanRenderer.cpp:4161` | indexBufferShared | `UploadAndBindIndexBuffer`, restart branch | SGW | R3 |
Split: SPMR 8 Espryt / 12 Magma; SGW 3 / 3. Comment-only mentions (not sites): `Managers.cpp:1461,2681`, `VulkanRenderer.h:1214`, `DirectGLES.cpp:459,509`. The integrator lands this table in `docs/Disaggregated/MEASUREMENTS.md` as a new §6.
### D11 — Deferred out of P1 (recorded, with the doc lines to correct)
| item | why not P1 | doc to fix |
|---|---|---|
| `gen_pipe_dirty_surface.py` mapping file + `--check` gate | `ROADMAP.md:18` puts "首轮映射成门" in **P2**; the table cells are the authority (scout R2) | `ARCHITECTURE.md:172,568`, `test.yml:871-874` comment, `scripts/gen_pipe_dirty_surface.py:21-22` say P1 → P2 (integrator) |
| `GetBufferBindingSlot` rows split by target in `Coverage.def` (`:36-40`) | needs the re-vendored inventory carrying the target argument; the extractor lives in MobileGL-CS, not here | note in `Coverage.def:36-40` ("when the inventory is re-vendored") |
| re-vendoring `scripts/data/backend_read_inventory.md` | same; D13's live cross-check is the stop-gap gate | `MEASUREMENTS.md:82` |
| `DynamicBackendParameters` fixed-width members (`MGPipeTypes.h:32`, scout R6) | not in the P1 row; the field list (D8) covers verify without it | — |
| semantic rewrite of the 13 guard/ternary sites | gate text: "推迟到 P2" | — |
| `MGPipeImpl_*.cpp` | D5 | `ARCHITECTURE.md:553` |
| `PipeStats` `AccessorCalls` tallies (`PipeStats.cpp:16-100`) | unchanged: the sites still call the same accessors through `MGB_CTX`; `RecordDrawPayloadBytes` stays unwired until a generator emits records | — |
### D12 — Coverage.def and the live accessor cross-check (A)
`Coverage.def` gains two rows after `:97`: `X(GetBoundTransformFeedbackLifetimeId, SetStreamOutputTargets)` and `X(HasOpenTransformFeedbackSpan, SetStreamOutputTargets)` (the D21 XFB counter-slot rekey's reads, `VulkanRenderer.cpp:11219,11236`; the calls they map to are the same as `GetTransformFeedbackGeneration`'s); `GetBoundTransformFeedbackName` keeps its row with a `/* dead: no backend reads it since D21; kept for inventory row 594 */` comment. `gen_pipe.py` gains `scan_live_accessors()`: walk `MobileGL/MG_Backend/**.{cpp,h}` (comments masked), collect every `\b(?:MGB_CTX|pGLContext)\s*->\s*(\w+)` name, `sys.exit` if a name has no `Coverage.def` row, and print (informational) rows no backend reads. Runs in both modes, so `pipe-gates` catches the next drift the day it lands. Zero-row accessors are already fine for `gen_coverage` (`gen_pipe.py:513-579` only iterates inventory rows).
### D13 — Namespaces, naming, generated-file discipline
Pipe code: `namespace MobileGL::MG_Pipe` (`MGPipe.h:26`); `PipeInputs`, `gPipeInputs`, `MGPipeVerb`, `MGPipeVerbClass`, `MGPipeFieldMask`, `kMGPipe*` constants, `MGPipeFillForVerb`, `SnapshotFromGLContext`, `MGPipeVerifyInputs`, `MGPipeVerifyReadHook`, `MGPipeInputsFieldEqual`. Generated files: `MobileGL/MG_Pipe/generated/*.inc`, LF, written through `write()` inside `main()` (`gen_pipe.py:619-652`) so `--check` and the CI `git diff -- MobileGL/MG_Pipe/generated` see them. Banner: `MGPipe.h:1-7`. Commits: `[Type] (Scope): description`, single line, no `Co-Authored-By`. Logging per D8. Test names: additions only.
---
## C. Packages
Four packages on four branches, four WSL worktrees. **Branch points:** `p1/core` from `feat/disaggregated@087685d1`. `p1/core`'s **first commit is the contract** (`c1`, tagged `p1/contract`): the switch header, `PipeInputs.h` with every accessor, `PipeFill.h`, `FillPoints.def` + regenerated `PipeFillPoints.inc`, the CMake options, `PipeInputs.cpp`/`PipeFill.cpp` with the forwarders and a `MGPipeFillForVerb` that only bumps (no field copies yet), `Config.h`/`ConfigLoader.cpp` knobs. `p1/espryt`, `p1/magma`, `p1/verify` branch from `p1/contract`, so their push builds compile and link while A finishes. `c1` is byte-neutral for the pull build (headers nobody includes, sources behind an OFF option).
WSL tree creation (copy `scratchpad/wf2/wsl_p05_trees.sh` → `wsl_p1_trees.sh`; `p05`→`p1`, slugs `core espryt magma verify`; the three non-core trees are created after `p1/contract` exists with `git worktree add -q ~/w7/p1-$slug -b p1/$slug p1/contract`). Each tree configures three build directories:
```
# pull (the G1 build; flags identical to ~/w7/pipe/build-linux)
cmake -S . -B build-linux -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_BUILD_TYPE=Release -DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_INTEGRATION_TEST=ON -DMOBILEGL_ITEST_EGL_VENDOR=/usr/share/glvnd/egl_vendor.d/50_mesa.json -DMOBILEGL_ITEST_VK_ICD=/usr/share/vulkan/icd.d/lvp_icd.json -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_POLICY_VERSION_MINIMUM=3.5
# push
cmake -S . -B build-push ... same ... -DMOBILEGL_PIPE_PUSH=ON
# verify (A and E, and the integrator)
cmake -S . -B build-verify ... same ... -DMOBILEGL_PIPE_VERIFY=ON -DMOBILEGL_ITEST_REQUIRE_GPU=ON
```
Before anything else the integrator captures the baseline: `cp ~/w7/pipe/build-linux/libMobileGL.so ~/w7/p1-before-libMobileGL.so` (confirm `git -C ~/w7/pipe log -1` is `087685d1` and the cache flags above), `ctest --test-dir ~/w7/pipe/build-linux -N | grep -E '^\s+Test #' | sed 's/^ *Test *#[0-9]*: //' | sort > ~/w7/p1-before-ctest-names.txt`, `grep -rc "pGLContext" MobileGL/MG_Backend | sort > ~/w7/p1-before-pglcontext.txt` (must equal the D1 per-file counts).
### C.1 Package A — `p1-core` (tree `~/w7/p1-core`, branch `p1/core`)
**Files**
| action | file | what |
|---|---|---|
| CREATE | `MobileGL/MG_Pipe/PipeInputsSwitch.h` | D3 verbatim |
| CREATE | `MobileGL/MG_Pipe/FillPoints.def` | D7: three macros, 69 verb rows in `GLFunctionsTable` declaration order, 9 classes, the field rows; blank line after each macro |
| CREATE | `MobileGL/MG_Backend/MGPipe/PipeInputs.h`, `PipeInputs.cpp` | D4, D6, D8 |
| CREATE | `MobileGL/MG_Impl/Pipe/PipeFill.h`, `PipeFill.cpp` | D7, D8; `PipeFill.cpp` includes `<MG_State/GLState/Core.h>`, `<MG_Backend/MGPipe/PipeInputs.h>`, `<Config.h>` |
| MODIFY | `MobileGL/MG_Pipe/Coverage.def` | D12 rows; `MGP_COVERAGE_STICKY_LIST(X)` with the seven D6 rows and reasons; note at `:36-40` |
| MODIFY | `MobileGL/MG_Pipe/PipeFields.def` | D8: six new field lists + payload-list entries |
| MODIFY | `scripts/gen_pipe.py` | parse `FillPoints.def` (`parse_fill_points()`, blank-line-terminated blocks, `sys.exit` on: a verb not in `GLFunctionsTable`, a table member without a row, a verb in two classes, a class with no verbs, an unknown field, a duplicate (class, field)); parse `MGP_COVERAGE_STICKY_LIST`; `gen_filled` emits real sticky flags; `gen_fill_points` → `PipeFillPoints.inc` written via `write()` in `main()` between the `PipeFilled.inc` and `PipeCoverage.inc` lines (`:650-651`); `gen_verify` overloads + empty `MEMCMP_FALLBACK_TYPES` + `static_assert` fallback; `check_field_lists_cover_struct_members()`; `scan_live_accessors()`; `--self-test` (canned negative controls for the struct-member check and the verb-set check; `trips == 0 → error`, `check_include_closure.py:541-543` shape); summary line gains "N verbs, M classes" |
| MODIFY | `MobileGL/MG_Pipe/generated/*.inc` | regenerate and commit (7 files change: `PipeFilled.inc` count 63 + sticky, `PipeVerify.inc`, new `PipeFillPoints.inc`, the banner of any file whose sources line changes) |
| MODIFY | `MobileGL/MG_Pipe/MGPipe.h` | `#include "generated/PipeFillPoints.inc"` after `:86` with a G5b comment |
| MODIFY | `CMakeLists.txt` | after `:24`: `option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)`, `option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)`; after `:451`: `if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH) message(STATUS ...) set(MOBILEGL_PIPE_PUSH ON) endif()`; `if (MOBILEGL_PIPE_PUSH) list(APPEND SOURCE_FILES MobileGL/MG_Backend/MGPipe/PipeInputs.cpp MobileGL/MG_Impl/Pipe/PipeFill.cpp) endif()`; after `:525`: the two `list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1 / -DMOBILEGL_PIPE_VERIFY=1)` |
| MODIFY | `MobileGL/Config.h:332` region, `MobileGL/ConfigLoader.cpp:246` region | D2's three knobs under `#if MOBILEGL_PIPE_PUSH` |
| MODIFY | `MobileGL/MG_Impl/GLImpl/{Drawing/GL_Drawing.cpp, Framebuffer/GL_Framebuffer.cpp, Texture/GL_Texture.cpp, Getter/GL_Getter.cpp, Program/GL_Program.cpp, Query/GL_Query.cpp, Sync/GL_Sync.cpp}` | `#include <MG_Impl/Pipe/PipeFill.h>` after each TU's last `MG_State`/`MG_Backend` include; the 83 `MGP_FILL(X);` statements of §B.7 |
| MODIFY | `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` | `:225` 61 → 63; new tests below; if the verify-only tests need it, `#if MOBILEGL_PIPE_VERIFY … #else GTEST_SKIP() << "verify not compiled in" #endif` (a visible skip, not a vanishing test) |
| MODIFY | `MobileGL/MG_Test/Pipe/CMakeLists.txt` | only if a `PipeInputs`-linking test target is added: `PipeInputsTest` linking `${LINK_LIBRARIES}` (the static library), registered `LABELS unit` |
**Ordered steps**
1. `c1` (contract): switch header; `FillPoints.def` + generator growth + regenerate; `PipeInputs.h` complete (all 63 accessors, storage, macros expanding to nothing until step 3); `PipeInputs.cpp` (equality, corruption injector); `PipeFill.h`; `PipeFill.cpp` with `IsLive`, `ContextIdentity`, the seven forwarders, and `MGPipeFillForVerb` = serial bump + `m_currentVerb` only; CMake options and conditional sources; `Config.h`/`ConfigLoader.cpp` knobs; `Coverage.def` + `PipeFields.def` edits; `PipeCatalogueTest.cpp` count. Build pull (identical), push, verify; commit; **tag `p1/contract`** and tell the other packages. Message: `[Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged`.
2. `c2`: the filler proper (per-class copies, stamps, omission knob), poison checks in the accessors (`MGP_INPUT_CHECK`), sticky stamping on first live fill. Message: `[Feat] (Pipe): fill PipeInputs per verb class from GLContext and stamp per-verb generations - a read of a field the verb did not fill is Fatal{UnmigratedPipeInput}`.
3. `c3`: the 83 fill points in `MG_Impl`. Message: `[Feat] (Impl): call MGP_FILL before every GLFunctionsTable entry - 83 statements over 69 verbs, a no-op in the pull build`.
4. `c4`: `SnapshotFromGLContext`, entry compare, compare-at-read, arming line, `CORRUPT`/`FATAL` knobs. Message: `[Feat] (Pipe): the MOBILEGL_PIPE_VERIFY shadow comparator - a per-verb entry compare over the fill set and a compare-at-read in every accessor, first differing field and verb serial, fatal by default`.
5. `c5`: G4 field lists for the six value types, the two overloads, the struct-member coverage check, `scan_live_accessors`, `--self-test`; regenerate. Message: `[Feat] (Pipe): give the six value structs G4 field lists and assert the lists cover their members - the memcmp fallback is now a compile error, floats in vector types compare bitwise`.
6. `c6`: unit tests. Message: `[Test] (Pipe): pin the P1 poison and verify shapes - 63 fields, 69 verbs, the seven sticky fields, an omitted GenerateMipmap field leaves exactly that field stale, a corrupted snapshot names its field, reading an unfilled field aborts with SIGABRT`.
**Tests to add** (`PipeCatalogueTest.cpp`; the fork/waitpid one follows `HeadlessGL.cpp:343-388` / `FdPassingTest.cpp:62-132`, never `EXPECT_DEATH` — the tree has none):
- `PipeInputFieldsStartUnfilled`: 63.
- `VerbTableIsTheFunctionTable`: `kMGPipeVerbCount == 69`; every class non-empty; the seven sticky bits are set in every class mask.
- `StickyFieldsAreExactlyTheSeven`: iterate `kMGPipeInputFieldSticky[]`.
- `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` (scout B4 layer 1, verbatim shape): fill twice with a fake context (`MG_State::pGLContext = MakeUnique<GLContext>()`, the `SanityTest.cpp:907` idiom), once clean, once with `SetPoisonOmission("GenerateMipmap", "GetActiveTextureUnit")`; assert `GetTextureUnitObject` fresh and `GetActiveTextureUnit` stale, and that a `DrawArrays` fill afterwards leaves `GetActiveTextureUnit` stale (not in `kDraw`'s mask) while `GetBoundVertexArray` is fresh.
- `ReadingAnOmittedFieldAbortsNamingTheVerb` (`#if MOBILEGL_PIPE_POISON` else `GTEST_SKIP`): fork; child fills `GenerateMipmap` with the omission then calls `gPipeInputs.GetActiveTextureUnit()`; parent expects `WIFSIGNALED && WTERMSIG == SIGABRT`; sibling test without the omission expects `_exit(0)`. Log assertion of the exact `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}` string via `MOBILEGL_LOG_FILE_PATH` set in the test's own `main()` (`LogLevelTest` shape, `MG_Test/Util/CMakeLists.txt:19-22`) — if that needs a separate target, `PipeInputsTest` links `GTest::gtest`.
- `CorruptedSnapshotFieldIsNamedWithItsSerial` (`#if MOBILEGL_PIPE_VERIFY`): clean arm true; `ApplyVerifyCorruption(snapshot, "GetRenderStateParameters")` → false, field string equal, serial equal to the fill serial.
- `FloatVectorsCompareBitwise`: `MGPipeFieldEqual(FloatVec4{NaN,...}, same bits)` true; `-0.0f` vs `0.0f` false; `Array<PerBufferBlendState, 8>` differing in element 3 → `MGPipeVerify(ResidualValueBlock…)` names `RenderState`.
- `SixValueStructsHaveFieldLists`: `kMGPipeVerifiedPayloadCount == 69u`.
**Constraints**: `PipeInputs.h` and `PipeInputs.cpp` never spell `pGLContext` (`grep -c pGLContext MobileGL/MG_Backend/MGPipe/*` == 0). `MG_Backend/DirectGLES/**` and `MG_Backend/DirectVulkan/**` are **not touched** (B and C own them). `PipeCalls.def` untouched. No renames of `PipeStats` counter names (`PipeStatsTest.cpp:260-279`). No `MGLOG_D` in verify reporting.
**Verification (WSL, `~/w7/p1-core`)**
```
python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test
cmake --build build-linux --parallel 28 && ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 # 0/0/0/0, .text +0
grep -rc "pGLContext" MobileGL/MG_Backend | sort | diff - ~/w7/p1-before-pglcontext.txt # empty (A converts no site)
grep -c "pGLContext" MobileGL/MG_Backend/MGPipe/PipeInputs.h MobileGL/MG_Backend/MGPipe/PipeInputs.cpp # 0 0
grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp | awk -F: '{s+=$2} END {print s}' # 83
cmake --build build-push --parallel 28 && ctest --test-dir build-push -L unit --no-tests=error
cmake --build build-verify --parallel 28 && ctest --test-dir build-verify -L unit --no-tests=error
# the poison is real: with the sites still on pGLContext (B/C not merged) nothing reads gPipeInputs, so run the
# integration suite in build-push to prove the fill points and filler are inert-correct (all green, no aborts):
ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4
ctest --test-dir build-linux -N | grep -E '^\s+Test #' | sed 's/^ *Test *#[0-9]*: //' | sort | comm -23 ~/w7/p1-before-ctest-names.txt - # empty
```
### C.2 Package B — `p1-espryt` (tree `~/w7/p1-espryt`, branch `p1/espryt`, from `p1/contract`)
**Files**: `MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp`, `Managers.cpp`, `MultiDraw.cpp`, `Utils.cpp` (nothing else; `Managers.h`, `Utils.h`, `DirectGLES.h`, `BackendObject_DirectGLES.cpp` have no sites).
**Site list it owns** — 122 lines = 113 arrow lines (113 occurrences) + 9 non-arrow:
| file | arrow lines (sed `MG_State::pGLContext->` → `MGB_CTX->`) | non-arrow (D9) |
|---|---|---|
| `DirectGLES.cpp` (91 + 1) | 143,261,357,370,429,462,465,474,481,518,662,892,987,1007,1244,1275,1373,1386,1414,1415,1515,1517,1518,1580,1728,1820,2026,2054,2083,2166,2842,2844,2846,2957,2968,3048,3212,3362,3482,3580,3662,3712,3821,3822,3826,4062,4088,4093,4156,4215,4380,4396,4397,4402,4442,4570,4609,4680,4711,4712,4782,4813,4814,4973,5014,5259,5260,6044,6045,6107,6108,6185,6375,6699,6700,6794,6795,6932,6933,7357,7358,7554,7562,8544,8774,9041,9084,9194,9197,9420,9510 | 147 (`.get()`, with the 143-155 block rewrite of D9) |
| `Managers.cpp` (15 + 8) | 3645,3646,3774,3775,3845,3846,4736,4737,5355,5460,6230,7193,7201,7204,8751 | 3644,3773,3844,4735 (guards), 7192,7200,7203 (ternary conditions), 8750 (guard) |
| `MultiDraw.cpp` (5) | 45,51,52,87,95 | — |
| `Utils.cpp` (2) | 2297,2301 | — |
**Ordered steps**
1. Add `#include <MG_Pipe/PipeInputsSwitch.h>` (D3 placement) to the four TUs.
2. Mechanical sed over the four files: `sed -i 's/MG_State::pGLContext->/MGB_CTX->/g'` — then confirm the per-file occurrence counts above (`grep -c 'MGB_CTX->'`), and that the only remaining `pGLContext` tokens are the 9 non-arrow lines.
3. Convert the 9 non-arrow lines per D9 (the `DirectGLES.cpp:143-155` block by hand).
4. Build pull; symbol report; build push; run.
5. One commit: `[Refactor] (Espryt): route every frontend read through MGB_CTX - 113 arrow sites sed'd, 9 non-arrow lines converted (the fb-slot cache keys on MGB_CTX_IDENTITY); pull build byte-identical`.
**Constraints**: no reordering, no reformatting beyond the touched lines; the `PipeStats` `AddCalls` literals near the sites (`DirectGLES.cpp:1416-2969`, `Managers.cpp:783-4433`, `MultiDraw.cpp:176-754`) are unchanged; the 8 + 3 sync sites of D10 are untouched; no new `pGLContext` spelling anywhere.
**Verification (WSL, `~/w7/p1-espryt`)**
```
grep -rc "pGLContext" MobileGL/MG_Backend/DirectGLES | grep -v ":0$" # empty
grep -c "MGB_CTX" MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp MobileGL/MG_Backend/DirectGLES/Managers.cpp MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp MobileGL/MG_Backend/DirectGLES/Utils.cpp # ≥ 92+23+5+2 (occurrences per file: DirectGLES 91 arrow + 4 in the cache block + 1 alias; Managers 15+8; MultiDraw 5; Utils 2)
cmake --build build-linux --parallel 28 && ctest --test-dir build-linux -L unit --no-tests=error
python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 # 0/0/0/0, .text +0 expected; any delta attributed to D9 lines in the commit message
ctest --test-dir build-linux -L integration-gpu -R '^DirectGLES\.' --no-tests=error -j 4 # by-name green
cmake --build build-push --parallel 28 && ctest --test-dir build-push -L integration-gpu -R '^DirectGLES\.' --no-tests=error -j 4 # links; runs (on the contract tree the filler does not copy yet, so under push this run is expected to hit Fatal{UnmigratedPipeInput} at the first read - that is the poison working; rerun after rebasing onto p1/core c2+ and it must be green)
```
### C.3 Package C — `p1-magma` (tree `~/w7/p1-magma`, branch `p1/magma`, from `p1/contract`)
**Files**: `MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp`, `BackendObject_DirectVulkan.cpp`, `Renderer/UniformManager.cpp`, `Renderer/VulkanRenderer.cpp`, `Renderer/VkClearManager.cpp`, `Renderer/VkRenderPassManager.cpp`, `Renderer/VkTextureManager.cpp`, `Renderer/VertexInputStateFactory.h` (comment only).
**Site list it owns** — 213 lines = 164 arrow occurrences on 161 lines + 49 non-arrow:
| file | arrow lines | non-arrow (D9) |
|---|---|---|
| `DirectVulkan.cpp` (12 + 35) | 270,273,278,402,465,533,586,716,786,910,1237,1284 (1284 also gets the `MGB_CTX_LIVE ?` rewrite) | 337,343,349,355,362,369,376,383,389,394,445,451,496,523,567,582,616,622,631,638,644,650,656,741,746,752,758,817,842,865,971,977,1001,1008 (asserts), 1236 (`!= nullptr`) |
| `BackendObject_DirectVulkan.cpp` (2 + 2) | 389,787 | 388,786 (guards) |
| `UniformManager.cpp` (14 + 9) | 506,555,782,821,847,1016,1189,1194,1295,1662,1850,1938,2013,2018 | 812,837,992,1152,1266,1651,1806,1888,1968 (asserts) |
| `VkClearManager.cpp` (1) | 57 | — |
| `VkRenderPassManager.cpp` (3) | 613,965,1111 | — |
| `VkTextureManager.cpp` (2) | 809,952 | — |
| `VulkanRenderer.cpp` (127 lines / 130 occurrences + 2) | 463,468,523,556,557,570,649,650,1243,1247,1303,2771,2988,3448,3449,3926,4060,4815,4816,4817,4828,4940,4984,5172,5173,5175,5178,5180,5181,5187,5199,5200,5202,5287,5288,5294,5298,5312,5317,5318,5354,5355,5407,5435,5436,5437,5442,5858,5919,5943,6019,6043,6091,6105,6110,6121,6122,6130,6304,6434,6464,6471,6472,6512,6526,6532,6650,6669,6830,6831,6972,6973,6999,7036,7088,7132,7206,7209,7257,7260,7268,7269,7270,7277,7300,7327,7340,7369,7373,7389,7410,7439,7481,7512,7514,7526,7583,7601,7609,7628,8586,8587,8619,8620,9214(×2),9222,9933,10688,10689,10719(×2),10963(×2),11219,11236,11268,11274,11277,11316,11347,11370,12018,12096,12106,12112,12197,12207,12277,12767 | 11267 (`== nullptr`), 12766 (`!= nullptr`) |
| `VertexInputStateFactory.h` (0 + 1) | — | 133 (comment) |
Steps, constraints and verification exactly as B, with `DirectVulkan` in place of `DirectGLES`, the seven TUs getting the switch include (D3 placement), and one commit: `[Refactor] (Magma): route every frontend read through MGB_CTX - 164 arrow sites sed'd, 49 non-arrow lines converted (43 asserts keep their meaning as MGB_CTX_LIVE); pull build byte-identical`. The 12 + 3 sync sites of D10 are untouched. `grep -rc "pGLContext" MobileGL/MG_Backend/DirectVulkan | grep -v ":0$"` is empty.
### C.4 Package E — `p1-verify` (tree `~/w7/p1-verify`, branch `p1/verify`, from `p1/contract`)
The third CI mode and the negative controls. Everything E codes against is a contract fixed above: option names (D2), env knobs (D2), log lines (D8), the `MGPipeVerb`/field vocabularies (D7/D4), `MGLOG_F` + `std::abort()` = SIGABRT.
**Files**
| action | file | what |
|---|---|---|
| MODIFY | `MobileGL/MG_IntegrationTest/CMakeLists.txt` | after `:393`: `set(MGL_ITEST_VERIFY_TIMEOUT 900)`; `if (MOBILEGL_PIPE_VERIFY)` block with two ambient registrations `DirectGLES.Verify.` / `DirectVulkan.Verify.` (env `MOBILEGL_PIPE_VERIFY=1` + `MOBILEGL_LOG_FILE_PATH=${CMAKE_CURRENT_BINARY_DIR}/pipe-verify-<backend>.log` + **`${MGL_ITEST_COMMON_ENV}` / `${MGL_ITEST_VULKAN_ENV}` appended** (`:339-344` rule), `LABELS integration-gpu integration-verify`, `TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}`, joined with `mgl_itest_join_environment` (`:307-317`)); two corrupted-field lanes `DirectGLES.VerifyCorrupted.` / `DirectVulkan.VerifyCorrupted.` (`TEST_FILTER "PipeVerifyArmingScenario.CorruptedFieldIsReported"`, env adds `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters MOBILEGL_PIPE_VERIFY_FATAL=0`, own log path); two omission lanes `DirectGLES.PoisonOmitted.` / `DirectVulkan.PoisonOmitted.` (`TEST_FILTER "PoisonOmissionScenario.*"`, env adds `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit`, own log path). Both scenario files added to the `add_executable` list (`:51-130`) |
| CREATE | `MobileGL/MG_IntegrationTest/Scenarios/PipeVerifyArmingScenario.cpp` | `Armed`: skips unless `AmbientQuirkFromEnvironment("MOBILEGL_PIPE_VERIFY") == On` (`ScenarioFixture.h:56-65`) and the log path is set (`UnlocatedIoBlockScenario.cpp:354-360` idiom, only bytes written after the case started); draws once; asserts `MGPipe: verify armed` present and no `Fatal{PipeVerifyDiffer`. `CorruptedFieldIsReported`: skips unless `MOBILEGL_PIPE_VERIFY_CORRUPT` is set; draws once; asserts `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@` present |
| CREATE | `MobileGL/MG_IntegrationTest/Scenarios/PoisonOmissionScenario.cpp` | `OmittedFieldAbortsOnThatVerb`: skips unless `MOBILEGL_PIPE_POISON_OMIT` is set; `fork()` **before** any scenario-local GPU work (`HeadlessGL.cpp:343-388`, `_exit` never `exit`, `:365-368`); child: create a 2-level texture, `glDrawArrays` once (must not abort), `glGenerateMipmap` (must abort), `_exit(0)`; parent: `WIFSIGNALED && WTERMSIG == SIGABRT`, log carries `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}` and no `@DrawArrays`. `WithoutOmissionCompletes`: same child without the knob → `WIFEXITED && WEXITSTATUS == 0` (registered in the same lane by filter; the env var is present, so this case clears it? No — env is process-wide: the clean arm runs in the ambient `Verify.` lanes instead, where the scenario asserts the knob is unset) |
| MODIFY | `tools/trace_replay/run_trace_case.cmake` | after `:129`: `if(DEFINED ENV{MOBILEGL_PIPE_VERIFY} AND NOT "$ENV{MOBILEGL_PIPE_VERIFY}" STREQUAL "0")` → `FATAL_ERROR` if `mobilegl.log` missing, if it lacks `MGPipe: verify armed`, or if it contains `Fatal\{PipeVerifyDiffer` / `Fatal\{UnmigratedPipeInput` (scout B2.2 text) |
| MODIFY | `tools/trace_replay/CMakeLists.txt:373-379` | add `LABELS retrace` to both `set_tests_properties` (the lane is otherwise selectable only by regex) |
| MODIFY | `tools/trace_replay/trace_cases.json`, `trace_cases.py` | `"verify": true` on eight cases: the five 180 s cases plus `minecraft-1.21.4-in-world`, `minecraft-1.21.4-fabric-sodium-in-world`, `improved-transparency-minecraft-26.3` (exact names from the file; E lists them in the commit); `--format github-verify-matrix` = `github_test_matrix(cases with verify)`; unit-check in `trace_cases.py`'s existing self-checks |
| MODIFY | `scripts/symbol_report.py` | implement `--fail-on-added-bytes N` (`:287-288` is reserved) and add `--fail-on-symbol-set-change`; default behaviour unchanged; `--self-test` covers both |
| MODIFY | `.github/workflows/test.yml` | (a) `build-linux-verify` (copy of `build-linux:12-147` with `BUILD_DIR=build-verify`, its own ccache key `${{ runner.os }}-test-${{ github.job }}-ccache-v1`, configure adds `-DMOBILEGL_PIPE_VERIFY=ON` and keeps `MOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO`, artifact `mobilegl-linux-runtime-verify`, `timeout-minutes: 120`); (b) `integration-verify` (`needs: build-linux-verify`; the `integration:205-296` shape incl. the path-normalisation step over `build-verify`, `ctest --output-on-failure -L integration-verify --no-tests=error`, core-dump upload, `timeout-minutes: 180`) plus the two negative-control steps: `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters ctest -L integration-verify -R 'DirectGLES\.Verify\..*ClearThenReadPixels' --no-tests=error` must **fail** (`if ctest ...; then echo "::error::..."; exit 1; fi`), and `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit ctest -L integration-verify -R 'DirectGLES\.Verify\..*Mipmap' --no-tests=error` must fail (job env reaches the process: no `ENVIRONMENT` property names these, `test.yml:274-288` precedent); (c) `retrace-verify` (`needs: [build-linux-verify, build-retrace, trace-cases, trace-fixtures]`, matrix from a new `trace-cases` output `verify-matrix` = `--format github-verify-matrix`, `max-parallel: 4`, `timeout-minutes: 240`; unpack the **verify** runtime so the verify `.so` lands at `build-linux/libMobileGL.so` (the path frozen into `CTestTestfile.cmake`), `test -f` + `nm -D build-linux/libMobileGL.so | grep -q MGPipeVerifyInputs`, `export MOBILEGL_PIPE_VERIFY=1` next to the per-case exports (`:700-723`), `ctest -V --no-tests=error --timeout 10800 -R '^MobileGLTraceReplay\.<case>\.<backend>$'`, artifacts suffixed `-verify`); one inverted `OpenRA` step with `MOBILEGL_PIPE_VERIFY_CORRUPT`; (d) `remove-artifact-clutter:787` `needs: [retrace-summary, retrace-verify]`; (e) `monolith-symbol-report` (`workflow_dispatch` input `baseline_sha`, default `087685d1`): two pull builds with identical flags (`-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF -DMOBILEGL_BUILD_INTEGRATION_TEST=OFF -DMOBILEGL_BUILD_TRACE_REPLAY=OFF -DMOBILEGL_BUILD_DISAGGREGATED=OFF -DMOBILEGL_PIPE_PUSH=OFF -DMOBILEGL_PIPE_VERIFY=OFF -DMOBILEGL_ENABLE_LTO=OFF`, clang-20), `symbol_report.py --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0 --markdown --json`, upload the markdown, and `nm --defined-only build-sym-head/libMobileGL.so | grep -q MG_Remote && exit 1` (`ARCHITECTURE.md:506`); (f) the `pipe-gates` dirty-surface comment `:871-872` → "in P2"; (g) `pipe-gates` gains `python3 scripts/gen_pipe.py --self-test` after `:851`. Triggers (`:3-9`) unchanged (P0.5 D10). |
**Constraints**: every new `ENVIRONMENT` list appends `${MGL_ITEST_COMMON_ENV}`/`${MGL_ITEST_VULKAN_ENV}`; the 11 filtered mode lanes are not doubled; no `ENVIRONMENT` property ever names `MOBILEGL_PIPE_VERIFY_CORRUPT`/`MOBILEGL_PIPE_POISON_OMIT` on the ambient `Verify.` lanes (the CI negative-control steps rely on job env reaching them); the verify lanes are registered only under `if (MOBILEGL_PIPE_VERIFY)` so `--no-tests=error` makes a mis-configured lane red; `MG_Test/**`, `MobileGL/**` outside `MG_IntegrationTest/`, `CMakeLists.txt` (root) untouched.
**Verification (WSL, `~/w7/p1-verify`)**
```
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/test.yml'))"
python3 tools/trace_replay/trace_cases.py --ci --format github-verify-matrix | python3 -c "import json,sys; m=json.load(sys.stdin); print(len(m['include']))" # 16 (8 cases x 2, minus any ci_backends restriction)
python3 scripts/symbol_report.py --self-test
cmake -S . -B build-verify ... -DMOBILEGL_PIPE_VERIFY=ON -DMOBILEGL_ITEST_REQUIRE_GPU=ON && cmake --build build-verify --parallel 28
ctest --test-dir build-verify -N -L integration-verify | grep -c 'Test #' # 742 + 2 + 2 (the 371 x 2 ambient entries plus the four control entries)
ctest --test-dir build-verify -N -L integration-gpu | sed 's/\.Verify\././' | sort -u | wc -l # parity with the pull registration set
# On the contract tree the library never arms (the comparator lands in p1/core c4), so the arming case must SKIP-or-FAIL, never pass:
ctest --test-dir build-verify -R 'DirectGLES\.Verify\.PipeVerifyArmingScenario' --output-on-failure # expected red (REQUIRE_GPU on) - the lane is falsifiable
```
Full functional verification of E happens on the integrated tree (D.3).
### C.5 File-ownership table (no two packages edit the same file)
| file / glob | A core | B espryt | C magma | E verify | integrator |
|---|---|---|---|---|---|
| `MobileGL/MG_Pipe/{PipeInputsSwitch.h, FillPoints.def, Coverage.def, PipeFields.def, MGPipe.h, generated/*.inc}` | owner | – | – | – | – |
| `scripts/gen_pipe.py` | owner | – | – | – | – |
| `MobileGL/MG_Backend/MGPipe/**` (new) | owner | – | – | – | – |
| `MobileGL/MG_Impl/Pipe/**` (new), `MobileGL/MG_Impl/GLImpl/{Drawing,Framebuffer,Texture,Getter,Program,Query,Sync}/*.cpp` | owner | – | – | – | – |
| root `CMakeLists.txt`, `MobileGL/Config.h`, `MobileGL/ConfigLoader.cpp` | owner | – | – | – | – |
| `MobileGL/MG_Test/Pipe/**` | owner | – | – | – | – |
| `MobileGL/MG_Backend/DirectGLES/{DirectGLES.cpp, Managers.cpp, MultiDraw.cpp, Utils.cpp}` | – | owner | – | – | – |
| `MobileGL/MG_Backend/DirectVulkan/{DirectVulkan.cpp, BackendObject_DirectVulkan.cpp, Renderer/UniformManager.cpp, Renderer/VulkanRenderer.cpp, Renderer/VkClearManager.cpp, Renderer/VkRenderPassManager.cpp, Renderer/VkTextureManager.cpp, Renderer/VertexInputStateFactory.h}` | – | – | owner | – | – |
| `MobileGL/MG_IntegrationTest/**`, `tools/trace_replay/{run_trace_case.cmake, CMakeLists.txt, trace_cases.json, trace_cases.py}`, `scripts/symbol_report.py`, `.github/workflows/test.yml` | – | – | – | owner | – |
| `docs/Disaggregated/*.md`, `scripts/gen_pipe_dirty_surface.py` (comment), `test.yml:871-872` comment (E lands it) | – | – | – | – | owner |
| everything else | nobody | nobody | nobody | nobody | nobody |
The contract (`p1/contract`) is the only shared code, and it is a single commit all three other branches descend from; the integrator's rebase of B/C/E onto the final `p1/core` therefore never sees an edit on both sides of one file.
---
## D. Integration order and what the integrator runs
D.0 Preconditions: baseline captures of §C (`~/w7/p1-before-*`); `~/w7/pipe` clean at `087685d1`; fixture binaries hidden as in `wsl_p05_trees.sh`.
D.1 Order: **core → espryt → magma → verify.** Copy `scratchpad/wf2/wsl_integrate_p05.sh` → `wsl_integrate_p1.sh` (`p05`→`p1`); per slug: rebase `p1/<slug>` onto `feat/disaggregated` in its worktree, `git merge --ff-only`, then in `~/w7/pipe`:
```
cmake --build build-linux --parallel 28 && ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure
python3 scripts/gen_pipe.py --check && python3 scripts/gen_pipe.py --self-test
python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --markdown ~/w7/p1-symbol-<slug>.md # 0/0/0/0 and .text +0 after every merge
```
After espryt and magma additionally: `grep -rc "pGLContext" MobileGL/MG_Backend | grep -v ":0$"` → empty (G2); `ctest --test-dir build-linux -L integration-gpu --no-tests=error -j 4` green by name (`comm -23 ~/w7/p1-before-ctest-names.txt` empty).
D.2 Push build on the integrated tree (`~/w7/pipe/build-push`, `-DMOBILEGL_PIPE_PUSH=ON`): unit + `integration-gpu` green — no `Fatal{UnmigratedPipeInput}` is possible here (the poison is off in a Release/INFO push build without VERIFY), so this run proves only that the filler and accessors are functionally equivalent under pull semantics.
D.3 Verify build (`~/w7/pipe/build-verify`, `-DMOBILEGL_PIPE_VERIFY=ON -DMOBILEGL_ITEST_REQUIRE_GPU=ON`):
```
ctest --test-dir build-verify -L unit --no-tests=error
ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4 --output-on-failure # 746 entries green: G3 half one
grep -c "MGPipe: verify armed" build-verify/MobileGL/MG_IntegrationTest/pipe-verify-*.log # > 0 in both
MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters ctest --test-dir build-verify -L integration-verify -R 'DirectGLES\.Verify\..*ClearThenReadPixels' ; echo rc=$? # non-zero: G4
MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit ctest --test-dir build-verify -L integration-verify -R 'Mipmap' ; echo rc=$? # non-zero: G5
ctest --test-dir build-verify -R 'PoisonOmitted\.|VerifyCorrupted\.' --no-tests=error # the four control entries green (they assert the red)
# 40 traces (79 desktop cases) under verify - the second half of G3:
cp scratchpad/wf2/retrace_gate.py ~/w7/retrace_gate.py
MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py --tree ~/w7/pipe --lib ~/w7/pipe/build-verify/libMobileGL.so --out ~/w7/retrace-out/p1-verify -j 4
grep -L "MGPipe: verify armed" ~/w7/retrace-out/p1-verify/*/mobilegl.log # empty: every case armed
grep -l "Fatal{" ~/w7/retrace-out/p1-verify/*/mobilegl.log # empty
```
A `Fatal{UnmigratedPipeInput, "F@V"}` here is a missing row in `FillPoints.def`'s class table: add it (never sticky), regenerate, rerun. A `Fatal{PipeVerifyDiffer, ..., where=read}` is a real push/pull semantic divergence: record it in `MEASUREMENTS.md` with the site and decide (plan owner) whether the pull value or the boundary value is the intended one; it is not silenced.
D.4 CI: `gh workflow run test.yml --ref feat/disaggregated` and, once green, `gh workflow run test.yml --ref feat/disaggregated -f baseline_sha=087685d1` for `monolith-symbol-report`; record both run URLs. Expected: `pipe-gates`, `include-graph-check`, `test`, `integration`, `integration-verify`, `retrace` (77), `retrace-verify` (16), `monolith-symbol-report` green.
D.5 Docs (integrator-owned, one commit; `scripts/check_doc_citations.py docs/Disaggregated/*.md` must stay clean): `README.md:3` status → P1 landed with the hash; `ROADMAP.md:17` ✅ with "277 处(274 行)+ 58 行", "Espryt 32 / Magma 56(63 字段)", "83 个填充点 / 69 个 verb"; `ROADMAP.md:18` unchanged (dirty-surface stays P2); `ARCHITECTURE.md:172,568` "P1 起成为门/P1 成门" → P2; `:344` counts; `:349` add `MOBILEGL_PIPE_POISON`'s definition and the `MOBILEGL_DEBUG` correction; `:553` note that `MGPipeImpl_*.cpp` land with the first installed table; `:583` `MOBILEGL_PIPE_VERIFY` → 已落地, add `MOBILEGL_PIPE_PUSH` (CMake) row and the three runtime knobs at `:588-597`; `MEASUREMENTS.md`: new §6 (the D10 table), §4 "61 字段" → 63 / "63 verify payload" → 69, the G1 symbol-report summary lines, the verify-lane wall times, and the P1 `.text` attribution (expected: "0 bytes; 58 lines converted, none code-bearing"). Then `[Merge]` to `dev` per the milestone flow (P1 is not one of the five full-CTS boundaries, `ROADMAP.md:34`, but the `dev` merge still triggers the full `gl44to46` run on both devices — schedule it, off the critical path).
---
## E. Risks and mitigations
| risk | mitigation |
|---|---|
| **A fill table that is too small turns the verify lane red for a reason that is not a bug.** The class masks in D7 are derived statically from `scout-pull-sites.md` §4.2; a helper reached through a path the reachability trace missed reads a field its class does not list. | This is exactly what the lane exists to find, and the fix is mechanical (add the row, regenerate). The rule in D7 — never sticky — keeps the fix honest. Budget one day of D.3 iteration for it; each Fatal names field and verb. |
| **A read outside any verb.** `InvalidateCompileEnv` at backend init (`BackendObject_DirectVulkan.cpp:389,787`) and `RecordError` from sampler sync run with no verb; texture-sync helpers (`Managers.cpp:3645-4737`) are reached only from verbs (callers verified: `DirectGLES.cpp:1325,1567,1632`, `Managers.cpp:5234`, all under `SyncNeccessaryTextures`/FBO sync); Magma's render-pass and view helpers are reached only from draw/clear/blit (`GetOrCreateRenderPass` callers `VulkanRenderer.cpp:6363,6833,6846,7196,7229,8491`); `Present` reads nothing. | The seven sticky fields cover the two genuine out-of-verb cases. If the lane finds another (`@<none>` in the Fatal), the answer is a fill point at that boundary (an EGL virtual in `EGLImpl.cpp`), never a sticky flag. |
| **`.text` moves in the pull build.** The 13 guard/compare rewrites and the `.get()` block are like-for-like, but a compiler may schedule a `const void*` compare differently. | `symbol_report.py --threshold 0` after every merge (D.1); any delta is attributed line by line in the commit message and in `MEASUREMENTS.md`; the gate text allows an attributed delta, not an unexplained one. |
| **The verify lane is green because it never armed.** A `MOBILEGL_PIPE_VERIFY=1` against a library without the option is a no-op that looks green. | `run_trace_case.cmake` fails a verify run whose log lacks the arming line; `PipeVerifyArmingScenario` fails the itest lane the same way; `--no-tests=error` fails a lane whose registration was skipped; the retrace-verify job asserts the swapped `.so` exports `MGPipeVerifyInputs`. |
| **The entry compare is tautological in P1 and someone reads "zero divergence" as more than it is.** | D8 states it; `MEASUREMENTS.md` records which arm proved what; the compare-at-read arm and the poison are the P1 semantic content, and the CORRUPT control keeps the entry compare falsifiable until P2 gives it a real first arm. |
| **Verify cost.** 5–10× on 742 itest entries and 79 traces; the 1800 s trace cases already sit above ctest's 1500 s default. | `TIMEOUT 900` on the verify lanes; `--timeout 10800` on retrace-verify; `timeout-minutes` on every new job (the file has none today); per-push CI runs the 8-case verify subset, the full 79 runs at the P1 exit on WSL (D.3) and on `workflow_dispatch`; `-j 4` locally (each case unpacks a multi-GB trace). |
| **`std::abort()` inside a scenario kills the whole `MobileGLIntegrationTest` process.** | ctest runs one case per process (`gtest_discover_tests`), so a Fatal reds exactly one entry and leaves a core; the omission scenario forks first (`HeadlessGL.cpp:323-343`). |
| **The `GetFramebufferBindingSlotFast` cache bypasses the accessor** for five Espryt reads. | Known and recorded (D4); the entry compare still covers the field; P2's Espryt 0b deletes the cache. |
| **`Coverage.def` and the stale vendored inventory disagree** (a 62-accessor tree, a 61-row table, a dead row). | D12's `scan_live_accessors()` is a `pipe-gates` gate from `c5` on; the dead row is labelled; re-vendoring is a recorded P2 item. |
| **Two `PipeInputs` field lists drift from `RenderStateParameters` when P2 adds `FramebufferSrgb`/`DepthClamp` storage** (`ROADMAP.md:18`). | D8's struct-member coverage check makes a member without an `F(...)` entry a `pipe-gates` failure; `MGL_RESIDUAL_BLOCK_SIZE` and the value-header `static_assert` catch the size. |
| **MSVC / Android.** `fork()`-based tests and the itest lanes are POSIX; the push/verify options are desktop-only in P1. | Windows is not a correctness gate (`ROADMAP.md:7`); the new sources compile on MSVC (no POSIX in `PipeInputs`/`PipeFill`); the fork tests are `#if !_WIN32` with a visible skip; `apk.yml` is unaffected (options OFF). |
| **A/B/C/E land against an outdated contract.** | The contract is one commit, tagged; A must not change any signature in `PipeInputs.h`, `PipeFill.h` or `PipeInputsSwitch.h` after `p1/contract` without telling B/C/E (a rebase-time compile error is the backstop). |
---
## B.7 (appendix) — the 83 fill-point statements at `087685d1`
Insert `MGP_FILL(<Verb>);` immediately before each statement (D7 placement):
- `GL_Drawing.cpp`: 530 Clear; 538 DrawElements; 547 MultiDrawElements; 556 MultiDrawElementsBaseVertex; 565 DrawArrays; 573 MultiDrawArrays; 582 DrawElementsBaseVertex; 591 MultiDrawElementsIndirect; 599 MultiDrawArraysIndirect; 608 MultiDrawElementsIndirectCount; 618 MultiDrawArraysIndirectCount; 628 DrawRangeElementsBaseVertex; 638 DrawRangeElements; 648 DrawElementsInstancedBaseVertexBaseInstance; 658 DrawElementsInstancedBaseVertex; 668 DrawElementsInstancedBaseInstance; 678 DrawElementsInstanced; 686 DrawElementsIndirect; 694 DrawArraysInstancedBaseInstance; 703 DrawArraysInstanced; 711 DrawArraysIndirect; 742 DispatchCompute (`dispatchCompute(numGroupsX, …)`); 794 DispatchComputeIndirect; 815 PatchParameteri; 885 MemoryBarrier; 907 MemoryBarrier (`TextureBarrier`); 919 MemoryBarrierByRegion; 1241 BeginTransformFeedback; 1323 EndTransformFeedback; 1331 FenceSync; 1332 ClientWaitSync; 1334 DeleteSync; 1352 PauseTransformFeedback; 1366 ResumeTransformFeedback; 1571 DeleteTransformFeedback; 1602 BindTransformFeedback — **36**
- `GL_Framebuffer.cpp`: 619 BlitFramebuffer; 632 BlitNamedFramebuffer; 643 ClearNamedFramebufferfv; 653 ClearNamedFramebufferfi; 663 ClearNamedFramebufferiv; 673 ClearNamedFramebufferuiv; 2732 ClearBufferfi; 2738 ClearBufferfv; 2744 ClearBufferuiv; 2750 ClearBufferiv; 2997 ReadPixels — **11**
- `GL_Texture.cpp`: 1079 ReadPixels; 1622 GenerateMipmap; 4027 CopyTexSubImage2D; 4043 CopyImageSubData; 4464 CopyTexImage2D; 5074 GetTexImage; 6456 GetTextureImage; 6660 BindImageTexture — **8**
- `GL_Getter.cpp`: 1176 GetIntegeri_v; 1356 GetGpuTimestampNs; 2266 GetGpuTimestampNs — **3**
- `GL_Program.cpp`: 3401 ShaderStorageBlockBinding — **1**
- `GL_Query.cpp`: 167, 274, 320, 444, 591, 915 DeleteBackendQuery; 181 EndTimeElapsedQuery; 261, 301 GetQueryResult64; 289 IsQueryResultAvailable; 425, 607 EndOcclusionQuery; 523 BeginXfbPrimitivesQuery; 530 BeginOcclusionQuery; 534 BeginTimeElapsedQuery; 582 EndXfbPrimitivesQuery; 646 QueryCounterTimestamp; 775 IsTimerQuerySupported — **18**
- `GL_Sync.cpp`: 59 FenceSync; 97 ClientWaitSync; 122 WaitSync; 142 DeleteSync; 177 GetSyncStatus; 229 DeleteSync — **6**
Not fill points: the 8 null-check/capture lines the 91-line grep also matches (`GL_Drawing.cpp:1010,1031,1255,1329`, `GL_Query.cpp:218,471,545,768`, `GL_Texture.cpp:6455,6715`, `ProgramInterface.h:27`), `CompileEnv.cpp:135,137` (D1), and the EGL virtuals (`EGLImpl.cpp:178,267,284,347,448`).
-65
View File
@@ -1,65 +0,0 @@
# P1 verify-lane findings on the integrated tree (`feat/disaggregated` @ 97b997d5, WSL `~/w7/pipe`)
All four P1 packages are merged. The pull build is clean: `symbol_report --threshold 0` reports 0 added / 0 removed / 0 resized / 0 renamed, `grep -rc pGLContext MobileGL/MG_Backend` is empty everywhere, `ctest -L unit` 1482/1482, `ctest -L integration-gpu` 878 with one known `-j` flake that reruns green, `ctest -N` names 0 removed / 26 added, `gen_pipe.py --check` and `--self-test` clean.
The push and verify builds are not clean. Three distinct problems, in the order they must be fixed.
## F1 — missing fill rows (mechanical, `MobileGL/MG_Pipe/FillPoints.def`)
The 79-case retrace under `MOBILEGL_PIPE_VERIFY=1` (`~/w7/retrace-out/p1-verify/`) aborts four cases with:
```
Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@GenerateMipmap"} x3 cases
Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@DispatchCompute"} x1 case
```
`IsCapabilityEnabled` is listed for `kDraw`, `kClear`, `kBlitOrCopy` and `kReadback` but not for `kTextureOp` (which owns `GenerateMipmap`) or `kDispatch`. Adding a row is the whole fix, but do not stop at these two: audit every class against the verb paths in `scratchpad/wf4/scout-pull-sites.md` §3b and add every row the lane can still miss, then prove it by re-running the lane and the retrace (they are the only real oracle).
One row of the same kind was already fixed at `9087f133` (`kReadback` was missing `IsTransformFeedbackActive` / `IsTransformFeedbackPaused`, which the depth/stencil read emulation reads through `ScopedEmulationDrawState`, `DirectGLES.cpp:5224`).
## F2 — the backend mutates frontend state inside a verb, so the pushed block goes stale mid-verb
Two retrace cases and eight `integration-verify` entries abort with:
```
Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", verb=3, where=read}
Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawElements", ..., where=read}
```
The failing entries are `DirectVulkan.Verify.UnboundImageDescriptorScenario.*` (6) and `DirectVulkan.Verify.SampledSetStalenessScenario.*` (2); the log line immediately before the divergence is `ResolveSamplerDescriptor: using fallback texture for unbound sampler ...`.
Mechanism, verified in the tree: `GetSamplingResolutionGeneration` counts frontend mutations. It is bumped from exactly two places, both frontend objects: `SamplerObject::BumpVersion()` (`MG_State/GLState/SamplerState/SamplerObject.cpp:34`) and `TextureObjectBase::BumpShapeVersion()` (`MG_State/GLState/TextureState/TextureObject.cpp:37`). During a draw, Magma's own paths (fallback-texture resolution for an unbound sampler, materialising a queued clear) write into frontend texture/sampler objects, so the counter moves between the verb's fill and the backend's read. This is the known "backend writes into frontend objects" surface (about 40 call sites; see `scout-pull-sites.md` §2).
This is a real finding, not a harness artefact, and it must not be silenced. Decide and implement one of:
- **(preferred) push on mutation.** The frontend mutator that changes a pushed value refreshes that field in the pushed block (and restamps it for the current verb) when `MOBILEGL_PIPE_PUSH` is on. This keeps the comparator's invariant ("the pushed block equals the live context at every read") literally true, and it is the shape P2's tracker needs anyway. Layering is fine: `MG_State` already includes `MG_Pipe/MGPipeValueTypes.h`.
- **(fallback) an explicit volatile-in-verb class.** The field is compared at verb entry only, the compare-at-read is skipped for it, the reason is written next to the field list, and the exact backend call sites that mutate it are listed in `docs/Disaggregated/MEASUREMENTS.md` as P2/P7 work.
Whichever you choose: enumerate every PipeInputs field a backend can invalidate through its own frontend writes (do not assume this one field is the only one; the lane found it because the fixture happened to hit it), handle them all the same way, add a unit test that reproduces the mid-verb mutation and would fail without the fix, and write the enumeration into the result file.
## F3 — unit tests that drive backend helpers with no verb
Eleven `ctest -L unit` entries fail in the push and verify builds and pass in the pull build:
```
DirectGLESSanity.BindsAMultisampleTextureDespiteTheDefaultMipmapFilter
DirectGLESSanity.BindingZeroClearsPreviousNativeTextureBinding
FramebufferTest.{DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff,
ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitutesOne,
ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffer,
DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere,
DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing,
DualSourceFactorsAreDeclinedEvenWithBlendingDisabled,
DualSourceFactorsWithBlendingDisabledStillReachACapableDriver}
DirectGLESTextureSync.* (1)
TextureTest.* (1)
```
These construct a `GLContext` by hand and call a backend helper directly (for example `DirectGLES::BindCurrentTextures()` at `MG_Test/SanityTest.cpp:366`), so no GL entry point runs and no `MGP_FILL` ever fires; the first accessor read hits the poison. The tests are right and the poison is right: what is missing is that a test which drives a backend helper directly must state which verb it is standing in.
Fix in the test layer only (`MobileGL/MG_Test/**`): give those tests a small, explicit helper that fills the block for the verb class the helper under test belongs to (a scoped RAII "as if we were inside verb X" object is the natural shape), so the test declares the verb rather than the poison being weakened. Do not add a global escape hatch, do not make the poison tolerant in unit builds, and do not change any test name. The pull build must stay byte-identical.
## F4 — corrections to the integrator's own commands (not code)
- The lane-level poison control is not `-R 'Mipmap'`: the four dedicated control entries are `DirectGLES|DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported` and `...PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb` (all four pass).
- The retrace arming line is `MGPipe verify: <case> <backend> armed, zero divergences, zero unmigrated reads` in the case log, not `MGPipe: verify armed`; 73 of 79 cases carry it, the other 6 are the F1/F2 aborts.
-57
View File
@@ -1,57 +0,0 @@
# P1 — `PipeInputs` 替换与 verify harness
> 本页是该阶段的**阶段汇总**:阶段表行、实测与落地形状的完整文本(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)。
## 摘要
- 落地:`MG_Backend/MGPipe/PipeInputs.h`(63 字段);277 处 `pGLContext->` 箭头 + 58 行非箭头逐条换成 `MGB_CTX`;逐 verb 类填充点(83 条 `MGP_FILL`)与逐 verb 世代 poison;G4 影子比对器 + verify CI 模式;push-on-mutation 三字段。
- 门:pull `nm` 不变;单元 1485×3;`integration-gpu` 878;`integration-verify` 818 零 `Fatal{`;79 例 retrace verify 79/79 armed 零分歧。verify 通道找出 9 处缺填充行与 3 个"verb 内后端改前端"字段。
## 阶段表行(原 `ROADMAP.md`)
- **阶段**:**P1** `PipeInputs` 替换与 verify harness
- **状态**:✅
- **落地什么 / 范围**:`MG_Backend/MGPipe/PipeInputs.h`(63 字段);277 处箭头 + 58 行非箭头逐条转换;逐 verb 类填充点;逐 verb 世代 poison;G4 影子比对器 + verify CI 模式;push-on-mutation 三字段
- **验收门 / 证据**:pull `nm` 不变;单元 1485×3;`integration-gpu` 878;`integration-verify` 818 零 Fatal;79 retrace verify 79/79 armed 零分歧。§2
## 实测:P1(lavapipe / llvmpipe)(原 `MEASUREMENTS.md` §2)
### 2.1 规模
| 量 | 值 |
|---|---|
| 后端 `pGLContext->` 箭头站点 | 277(Espryt 113、Magma 164) |
| 非箭头行 | 58(Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT`) |
| `PipeInputs` 字段 / 不同访问器 | 63 / 62(Espryt 32、Magma 56) |
| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 |
| `SyncPersistentMappedRange` / `SyncGpuWrites` | **21**(Espryt 9 + Magma 12;P5 b1 复核,原记 20 漏了共享 helper `ResolveIndirectCommandBytes`)/ 6 |
### 2.2 verify 通道发现的两类真问题
**缺填充行(9 处)**:`kReadback` 缺 `IsTransformFeedback{Active,Paused}`、`kTextureOp` 与 `kDispatch` 缺 `IsCapabilityEnabled`、`kBlitOrCopy`/`kTextureOp` 缺着色器 blit 用到的 viewport 与顶点 / 缓冲绑定。其中 8 行是静态过近似(代码路径可达但通道未跑到),有意保留——能退役它们的证据只能是动态的(完整 CTS caselist + `MOBILEGL_PIPE_POISON_OMIT`,未做)。
**verb 内后端改前端(3 个字段)**:Magma 在 draw 里写前端对象(合成回退纹理、材质化排队清除、覆写 sampler filter),8 条 DirectVulkan 用例与 2 条 trace 报 `Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@Draw*"}`。解法 **push-on-mutation**:前端计数器移动时用 `MGP_NOTE_MUTATION(Field)` 刷新推送块里那一个字段——钩子挂在三个计数器上(`BumpSamplingResolutionGeneration`、`BumpTextureBindGeneration` / `NoteUnitTouched`、高水位分支),不挂在四十个写入点上。
### 2.3 验收
| 门 | 结果 |
|---|---|
| pull 构建符号与 `.text` | 0 增 / 0 删 / 0 改尺寸 / 0 重命名,`.text` 不变 |
| `MG_Backend` 里的 `pGLContext` | 0 |
| 单元(pull / push / verify) | 1485 × 3 |
| `integration-gpu`(pull) / `integration-verify` | 878 / 818 零 `Fatal{` |
| 79 例 retrace(`MOBILEGL_PIPE_VERIFY=1`) | 79/79,79/79 armed,零 `Fatal{` |
| 两个阴性对照 | 篡改字段变红、抽掉一个填充戳记在那条 verb 上变红 |
| 测试名 | 0 删除,+29 |
verify 构建的代价:`integration-verify` 与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。
## 本目录
| 文件 | 内容 |
|---|---|
| [`BRIEF-P1.md`](BRIEF-P1.md) | P1 implementation brief — PipeInputs strangler, per-verb fill points, poison generations, MOBILEGL_PIPE_VERIFY comparator and its CI mode |
| [`P1-LANE-FINDINGS.md`](P1-LANE-FINDINGS.md) | P1 verify-lane findings on the integrated tree (`feat/disaggregated` @ 97b997d5, WSL `~/w7/pipe`) |
| [`p1-results/`](p1-results/) | 19 个文件:各包(core / espryt / magma / verify / fix-*)的实现与评审稿 |
捞回的独有材料:[`../recovered/wf4/`](../recovered/wf4/)(pull-site 目录分部、BRIEF-P1 第一部分、三份 scout)。
@@ -1,73 +0,0 @@
# P1 package A (p1-core) — c1, the contract commit
Tree `~/w7/p1-core`, branch `p1/core`, base `feat/disaggregated @ 087685d1`.
## Commit
- `bf86b1ede6f9a43b4114fe2603f68a0bc2aa233c` — `[Feat] (Pipe): land the P1 contract - PipeInputsSwitch.h with MGB_CTX, the 63-field PipeInputs block with type-identical accessors, FillPoints.def and its G5b generator, the MOBILEGL_PIPE_PUSH/VERIFY options and the three verify knobs; pull build unchanged`
- Tag: `p1/contract` (annotation-less tag on that commit; `git describe --tags` → `p1/contract`). Not pushed.
- 15 files, +1834 / −30. Working tree clean apart from the three untracked build directories.
| action | file |
|---|---|
| CREATE | `MobileGL/MG_Pipe/PipeInputsSwitch.h` (D3 verbatim, 28 lines) |
| CREATE | `MobileGL/MG_Pipe/FillPoints.def` (69 verb rows in `GLFunctionsTable` declaration order, 9 classes, 138 (class, field) rows) |
| CREATE | `MobileGL/MG_Pipe/generated/PipeFillPoints.inc` (generated: `MGPipeVerb` 69, `kMGPipeVerbNames`, `MGPipeVerbClass` 9, `kMGPipeVerbClassNames`, `kMGPipeVerbClass[]`, `MGPipeFieldMask{Words[2]}`, `MGPipeFieldMaskHas`, `kMGPipeClassFieldMask[9]` with the 7 sticky bits OR'ed in, `static_assert(kMGPipeVerbCount == 69)`) |
| CREATE | `MobileGL/MG_Backend/MGPipe/PipeInputs.h` (D4: all 63 accessors — 49 V, 7 O, 7 F — with GLContext-identical signatures; `gPipeInputs` inline; `MOBILEGL_PIPE_POISON` derived once; `MGP_INPUT_CHECK` / `MGP_INPUT_VERIFY_READ`; `MGP_INPUT_STORAGE_LIST` + `VisitStorage`; `static_assert(sizeof < 20 KiB)`) |
| CREATE | `MobileGL/MG_Backend/MGPipe/PipeInputs.cpp` (`MGPipeVerbName`, `MGPipeInputPoisonFatalForVerb`, `MGPipeFindInputField` / `MGPipeFindVerb`; under `MOBILEGL_PIPE_VERIFY`: `MGPipeInputsFieldEqual`, `MGPipeApplyVerifyCorruption`) |
| CREATE | `MobileGL/MG_Impl/Pipe/PipeFill.h` (D7 `MGP_FILL(Verb)`, `((void)0)` under pull) |
| CREATE | `MobileGL/MG_Impl/Pipe/PipeFill.cpp` (`PipeInputs::IsLive`, the seven F-class forwarders, `MGPipeFillForVerb` = serial bump + verb + identity + one-time sticky stamps; no field copies) |
| MODIFY | `MobileGL/MG_Pipe/Coverage.def` (D12: `GetBoundTransformFeedbackLifetimeId`, `HasOpenTransformFeedbackSpan` rows after `RecordError`; dead-row comment on `GetBoundTransformFeedbackName`; the `:36-40` note reworded to "when the inventory is re-vendored"; `MGP_COVERAGE_STICKY_LIST(X)` with the seven D6 rows and reasons) |
| MODIFY | `MobileGL/MG_Pipe/generated/PipeFilled.inc` (63 fields, 7 `true` sticky flags with reasons, `kMGPipeInputStickyFieldCount = 7`) |
| MODIFY | `MobileGL/MG_Pipe/MGPipe.h` (`#include "generated/PipeFillPoints.inc"` after `PipeFilled.inc`, G5b comment) |
| MODIFY | `scripts/gen_pipe.py` (`parse_function_table()`, `parse_fill_points()`, sticky-list parsing in `parse_coverage()`, `gen_filled` real sticky flags, `gen_fill_points()`, `PipeFillPoints.inc` written between `PipeFilled.inc` and `PipeCoverage.inc`, summary line "… 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes") |
| MODIFY | `CMakeLists.txt` (options after `:24`; VERIFY⇒PUSH block + conditional `SOURCE_FILES` append after the disaggregated fallback `endif()` at `:452`; the two `MOBILEGL_COMPILE_DEF` appends after `:525`) |
| MODIFY | `MobileGL/Config.h`, `MobileGL/ConfigLoader.cpp` (D2 `PipeVerifyFatal` / `PipeVerifyCorrupt` / `PipePoisonOmit` under `#if MOBILEGL_PIPE_PUSH`) |
| MODIFY | `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` (`:225` 61u → 63u) |
Not touched (per brief): `MG_Backend/DirectGLES/**`, `MG_Backend/DirectVulkan/**`, `PipeCalls.def`, `PipeFields.def`, `MG_Impl/GLImpl/**`, `tools/trace_replay/fixtures/**`.
## Verification (all run in WSL on `~/w7/p1-core`; logs under `~/w7/p1-core-verify-*.log`, `~/w7/p1-core-build-*-c1.log`)
Tree creation: `bash wsl_p1_tree.sh core feat/disaggregated verify` → worktree on `p1/core @ 087685d1`, submodules ok, `build-linux` / `build-push` / `build-verify` all rc=0 at the base (955/955).
Before any edit (sanity of the tooling): `symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` → `.text 10792579 -> 10792579 (+0)`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`.
After the edits, in this order:
1. `python3 scripts/gen_pipe.py` → wrote `PipeFilled.inc`, `PipeFillPoints.inc`; `python3 scripts/gen_pipe.py --check` → `gen_pipe: 71 calls (11 screen, 60 context), 63 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes` / `inventory 477 rows … 0 UNMAPPED` / `generated files are up to date`, rc=0.
2. `cmake --build build-linux -j 20` (reconfigured itself on the `CMakeLists.txt` change; `MOBILEGL_PIPE_PUSH=1` occurs 0 times in `build-linux/compile_commands.json`) → rc=0.
`python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` →
`.text 10792579 -> 10792579 (+0, +0.000%)`, `.data +0`, `.bss +0`, `.rodata +0`, `Total +0`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 normalised names unchanged`; file bytes identical (19100448). `nm --defined-only build-linux/libMobileGL.so | grep -c MGPipe` = 0, same as the baseline. **G1 holds: the pull build is symbol-identical with a .text delta of 0** (the new `.cpp` files are not listed in the pull build — D5/D13: appended to `SOURCE_FILES` only under `MOBILEGL_PIPE_PUSH`; the new headers are included by nobody in the pull build; `Config.h`'s three members are behind `#if MOBILEGL_PIPE_PUSH`; the `MGPipe.h` include of `PipeFillPoints.inc` adds only `inline constexpr` tables that no pull TU odr-uses).
3. `cmake --build build-push -j 20` → rc=0, links (`MOBILEGL_PIPE_PUSH=1` on all 496 compile commands; `nm -C build-push/libMobileGL.so` exports `MGPipeFillForVerb`, `PipeInputs::IsLive`, `gPipeInputs`). Configure log: `MobileGL: PipeInputs push ON, appending the MGPipe fill sources`.
4. `cmake --build build-verify -j 20` → rc=0, links (`MOBILEGL_PIPE_PUSH=1` and `MOBILEGL_PIPE_VERIFY=1` on all 496; exports `MGPipeInputsFieldEqual`, `MGPipeApplyVerifyCorruption`). Configure log: `MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure`.
5. `ctest -L unit --no-tests=error -j 12`: build-linux `100% tests passed, 0 tests failed out of 1466`; build-push `100% … out of 1466`; build-verify `100% … out of 1466`.
6. `(cd build-linux && ctest -N | grep -E '^\s+Test\s+#[0-9]+:' | sed 's/^ *Test *#[0-9]*: //' | sort)` → 2334 names; `comm -23 ~/w7/p1-before-ctest-names.txt -` → **0 removed**, `comm -13` → 0 added. (Note: the brief's regex `'^\s+Test #'` only matches test numbers ≥ 1000 — ctest pads smaller numbers with extra spaces — and would report 999 spurious removals; the baseline file itself holds all 2334 names, so the diff above uses the padding-tolerant regex.)
7. `grep -rc "pGLContext" MobileGL/MG_Backend | sort | diff - ~/w7/p1-before-pglcontext.txt` → only the two new files appear, both `:0` (`MG_Backend/MGPipe/PipeInputs.h:0`, `PipeInputs.cpp:0`); every catalogued file's count is unchanged (A converts no site). `grep -c pGLContext` on the two MGPipe files → `0 0`.
8. `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp` → 0 (fill points are c3).
9. Integration smoke in the push and verify libraries (`ctest -L integration-gpu -R 'ClearThenReadPixels|GenerateMipmap|Mipmap' -j 4`): build-push `100% tests passed, 0 failed out of 10`, build-verify `100% … out of 10`, zero `Fatal{` in either log — the inert filler and the unarmed poison do not disturb a run in which no backend site reads `gPipeInputs` yet.
Not run (not in the task's proof list; long): the full `ctest -L integration-gpu` in build-push (brief C.1 step 1 lists it for A's own tree — recommended after c3 when the fill points exist, since at c1 nothing calls `MGPipeFillForVerb`).
## Deviations from the brief (and brief-vs-tree facts)
1. **`MGP_INPUT_CHECK` is live at c1** (under `MOBILEGL_PIPE_POISON`) rather than "expanding to nothing until step 3" (C.1 step 1 wording). Reason: B's and C's verification text expects a push run on the contract tree to hit `Fatal{UnmigratedPipeInput}` at the first read ("that is the poison working"); with an empty check the first read of an O-class field would be a null dereference instead. The check costs nothing in a non-poison push build (`((void)0)`), and c2 now only has to add the filler copies. `MGP_INPUT_VERIFY_READ` expands to nothing as specified (c4 arms it).
2. **Sticky stamping is in c1** (three lines in `MGPipeFillForVerb`: the seven sticky fields get `FilledGen = 1` on the first live fill) rather than c2. It is generation bookkeeping, not a field copy, and it keeps the F-class forwarders usable under poison from the contract on.
3. **`MGPipeInputsFieldEqual` and `MGPipeApplyVerifyCorruption` are in c1** (`PipeInputs.cpp`, `#if MOBILEGL_PIPE_VERIFY`), per C.1 step 1's "`PipeInputs.cpp` (equality, corruption injector)"; the comparator proper (`SnapshotFromGLContext`, entry compare, compare-at-read hook, arming line, knob parsing) is NOT in c1, as the task instruction requires. Both are one call of `PipeInputs::VisitStorage` over `MGP_INPUT_STORAGE_LIST` (56 storage rows + 7 forwarded = 63, static_asserted) instead of two 63-way switches. `IsCapabilityEnabledIndexed`'s two arrays are one `IndexedCapabilities` struct so the list stays one row per field.
4. **`PipeFields.def` is untouched in c1.** C.1 step 1 says "`Coverage.def` + `PipeFields.def` edits" but step 5 (c5) owns the six G4 field lists / overloads / coverage check, and the task instruction for c1 does not list `PipeFields.def`. Deferred to c5. Consequence: `kMGPipeVerifiedPayloadCount` stays 63 at c1 and `RenderStateParameters` / `PixelStoreParameters` still go through the memcmp fallback inside `MGPipeInputsFieldEqual` (both arms are copied by assignment into zero-initialised globals, so padding is equal; c5 removes the fallback anyway).
5. **`PipeInputs::IsLive()` forwards to the live context** (defined in `PipeFill.cpp`, returns `pGLContext != nullptr`) instead of returning a fill-time `m_live` flag; `m_live` is still recorded at fill for the filler's own use. Reason: D5 lists `IsLive` among `PipeFill.cpp`'s contents and D9 says `MGB_CTX_LIVE` "is always true once a context exists" under push, which a fill-time flag cannot promise before the first verb (e.g. `InvalidateCompileEnv` at backend init, `BackendObject_DirectVulkan.cpp:389,787`). `ContextIdentity()` is the fill-time address as specified.
6. **`GetBufferBindingPointCount` is not `constexpr`** in `PipeInputs` (GLContext's is): it is an out-of-line forwarder per D4's F class. No backend uses it in a constant expression.
7. **`MGPipeFieldMask::Words[2]`** as the brief says, generated as `max(2, ceil(fields/64))` so a 129th field grows the mask instead of silently truncating (`static_assert(kMGPipeInputFieldCount <= 2 * 64)` in the `.inc`).
8. **Two extra `gen_pipe.py` checks beyond the brief's list**: verb rows must be in `GLFunctionsTable` declaration order (not just the same set), so `MGPipeVerb`'s numbering is the table's by construction; a class listed twice in `MGP_FILL_CLASS_LIST` is refused. `--self-test`, `scan_live_accessors()` and `check_field_lists_cover_struct_members()` are c5, as the brief's step 5 says.
9. **Tree-vs-brief line facts**: `CMakeLists.txt` "after :451" — the disaggregated fallback's `endif()` is at `:452`; the block was inserted after it (before the `if (MOBILEGL_BUILD_DISAGGREGATED)` sources block at `:454`). The `MOBILEGL_COMPILE_DEF` appends went after the `endif()` at `:525` as stated. The brief's ctest-name regex issue is item 6 above.
10. **Accessor namespaces**: `BufferTarget` and `FramebufferTarget` live in `namespace MobileGL` (not `MG_State::GLState`, where the brief's text implies all frontend types sit); the header's `using` aliases spell them accordingly. Everything else (`VertexArrayObject`, `ProgramObject`, `ITextureObject`, `BufferObject`, `FramebufferObject`, `TextureUnit`, `ImageTextureBinding`, `CurrentVertexAttributeValue`) is `MG_State::GLState::`; value types are `MobileGL::`.
11. `IsCapabilityEnabled` with a cap ≥ `CapabilityInputCount` returns `false` (defensive bound; GLContext would index its own table); indexed getters mirror `RenderState`'s bounds handling (`MOBILEGL_ASSERT` + element 0 / early return; `GetCurrentVertexAttribute` mirrors `Core.cpp:246-253` with `MGLOG_E_ONCE` + a static default).
## Unfinished / next for A
- c2: per-class copies and stamps in `MGPipeFillForVerb` (mask = `kMGPipeClassFieldMask[kMGPipeVerbClass[verb]]`), `MOBILEGL_PIPE_POISON_OMIT` parsing.
- c3: the 83 `MGP_FILL(X);` statements (§B.7).
- c4: `SnapshotFromGLContext`, entry compare, `MGPipeVerifyReadHook` behind `MGP_INPUT_VERIFY_READ`, arming/knob log lines, `PipeVerifyFatal`.
- c5: `PipeFields.def` field lists, `MGPipeFieldEqual` overloads, `check_field_lists_cover_struct_members`, `scan_live_accessors`, `--self-test`.
- c6: the unit tests listed in C.1.
- Full `ctest -L integration-gpu` in build-push (after c3).
@@ -1,103 +0,0 @@
# Adversarial review — p1-core v1 (tree `~/w7/p1-core`, branch `p1/core`, HEAD `440d3c52`)
Verdict: **NOT approved — 1 major, 8 minors.** The package is otherwise complete against C.1 and the D-decisions; every claim in `core-v1.md` that I could re-run reproduced. The one major is a defect in the delivered G5 negative-control unit tests, not in the filler/poison/comparator code.
Tree left exactly as found (`git status` clean apart from the three untracked build dirs; no file edited, nothing committed). All logs under `~/w7/p1-core-review/`.
## 1. What I re-ran (all on `440d3c52`; builds were `ninja: no work to do` in all three dirs)
| check | command | observed |
|---|---|---|
| G1 symbols | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --json` | `.text 10792579 -> 10792579 (+0)`, `Total +0`, `27799 -> 27799: 0 added, 0 removed, 0 resized, 0 renamed` |
| G1 bytes | `cmp -l before after \| wc -l`; `readelf -SW` diff; `strings` diff | 34 bytes; section table identical; the only differing strings are `GIT@5d99ee4 -> GIT@83b1656` and glslang's `Timestamp: 2026-09-05T23:14:30 -> 2026-09-06T02:05:06` (see minor 6 for the stamp value) |
| G2 (A's half) | `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only `MG_Backend/MGPipe/PipeInputs.{cpp,h}:0` appear; every catalogued file's count unchanged — A converts no site, touches no B/C file |
| `pGLContext` in A's backend files | `grep -c pGLContext MobileGL/MG_Backend/MGPipe/*` | `0 0` |
| fill points | `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp` | Drawing 36, Framebuffer 11, Texture 8, Getter 3, Program 1, Query 18, Sync 6 = **83**; no `MGP_FILL` outside `MG_Impl/GLImpl` |
| generator | `python3 scripts/gen_pipe.py --check` / `--self-test` | rc 0 / rc 0; `63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `generated files are up to date`; six controls trip, positive control OK |
| D7 table vs `FillPoints.def` | `d7check.py` (mechanical diff of the brief's verb→class and class→field tables against the .def) | 69/69 verbs identical; 139/139 (class, field) rows identical |
| D8 field lists vs the structs (independent oracle) | clang `-ast-dump=json -ast-dump-filter=<Struct>` on `PipeFill.cpp`'s compile command, `FieldDecl` names vs `PipeFields.def` (`ast.py`) | RenderStateParameters 65/65, PixelStoreParameters 8/8, PerBufferBlendState 7/7, StencilFaceState 7/7, DynamicBackendParameters 85/85, MGHostSpan 5 members of which `Pad0` is the excluded padding → 4/4. The regex-based `check_field_lists_cover_struct_members()` agrees with clang on every struct |
| accessor signatures | `Core.h:66-506` vs `PipeInputs.h` | all 63 identical in name, parameters, constness and return type (incl. the `const&` returns for `GetRenderStateParameters`, `GetBlendColor`, `GetViewportIndexed`, …; no by-value copy of the 1168-byte struct was introduced). Only difference: `GetBufferBindingPointCount` is `constexpr` on `GLContext` and a plain forwarder here — the three backend uses (`DirectGLES.cpp:474`, `UniformManager.cpp:1189,2013`) are runtime, so nothing breaks |
| fill-probe side effects | `BufferState.cpp:90-98`, `TextureState.cpp:157-173`, `Core.cpp:179,594` | `GetBindingPoint(t,0)`, `GetUnitObject(0)`, `GetImageTextureBinding(0)`, `GetBoundVertexArray()` are pure lookups; `TouchBindPoint` is only reached from `Core.h:84` (the setter). `GetProgramForDraw()` joins link+SPIR-V — same verb as today's first read |
| unit, 3 configs | `ctest -L unit --no-tests=error -j 12` in build-linux / build-push / build-verify | `100% tests passed, 0 tests failed out of 1475` × 3 |
| `ctest -N` names | `comm` against `~/w7/p1-before-ctest-names.txt` | 2343 names, **0 removed**, 9 added; identical name sets in push and verify |
| skip visibility | `ctest -R 'PipeInputsTest' -V` | build-linux: all 5 `***Skipped` ("push not compiled in"); build-push: 4 skipped (poison/verify), `ReadingAFilledFieldCompletes` passes; build-verify: all 5 pass |
| nm -D | `nm -D --defined-only <lib> \| grep -c MGPipeVerifyInputs` | push 0, verify 1 |
| integration-gpu, push | `ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4` | `100% tests passed, 0 tests failed out of 868`, `grep -c 'Fatal{'` = 0 |
| G4 lane smoke, verify | `MOBILEGL_PIPE_VERIFY=1 ctest -R 'DirectGLES\..*ClearThenReadPixels'` | 5/5 green; log has 1 × `MGPipe: verify armed - 63 fields, 69 verbs, fatal=1`, 0 `Fatal{` |
| G4 negative control A | same + `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` | rc 8, 0/5; `verify corruption armed - GetRenderStateParameters`; `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear", verb=1, where=entry}` |
| FATAL=0 | + `MOBILEGL_PIPE_VERIFY_FATAL=0` | 5/5 green, 0 `Fatal{PipeVerifyDiffer` aborts, `verify summary - 3 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0` |
| bad CORRUPT name | `MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField` | rc 8, `Fatal{PipeVerifyBadKnob, "MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField": no such field in kMGPipeInputFieldNames}` |
| POISON_OMIT env path, verify | `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit ctest -R 'DirectGLES\..*ClearThenReadPixels'` | green (no backend reads `gPipeInputs` on this tree), log has `poison omission armed - GetActiveTextureUnit@GenerateMipmap`, 0 `Fatal{` |
| POISON_OMIT bad verb | `MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit` | rc 8, `Fatal{PipeVerifyBadKnob, "MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit": no such verb in kMGPipeVerbNames}` |
| POISON_OMIT in a POISON-less push build | same in build-push | green, `MGPipe: poison omission GetActiveTextureUnit@GenerateMipmap requested but the poison is not compiled in (MOBILEGL_PIPE_POISON=0)` |
| `MOBILEGL_PIPE_VERIFY=1` in a push build | build-push | green, one `MGPipe: MOBILEGL_PIPE_VERIFY=1 requested but the comparator is not compiled in` |
| G5 unit, serial | `ctest --test-dir build-verify -R PipeInputsTest -j 1` | 5/5 |
| **G5 unit, parallel** | `ctest --test-dir build-verify -R PipeInputsTest -j 8` × 40 | **40/40 runs red** (major 1) |
| commit hygiene | `git log --format=%B` | 6 commits, `[Type] (Scope): …` subject + blank + `- ` bullets; 0 `Co-Authored-By`; nothing pushed |
| ownership | `git diff --stat feat/disaggregated..HEAD` (26 files) | every path is in A's C.5 rows; `DirectGLES/**`, `DirectVulkan/**`, `PipeCalls.def`, fixtures, `MG_IntegrationTest/**`, `test.yml`, docs untouched |
| logging discipline | grep `MGLOG_D`/stdio in `PipeFill.cpp`, `PipeInputs.{h,cpp}` | none |
| line endings | `git diff --name-only … \| xargs file \| grep -i crlf` | none (all LF) |
## 2. Major
### M1 — The G5 negative-control unit tests share one fixed log file and fail 100% under `ctest -j`
`MobileGL/MG_Test/Pipe/PipeInputsTest.cpp:255-270` (`main()`): every `PipeInputsTest` process computes the same path `temp_directory_path()/mobilegl-pipeinputs-test.log`, **unlinks it** (`fs::remove(path, ec)`, line 261) and points `MOBILEGL_LOG_FILE_PATH` at it. `gtest_discover_tests` registers the five cases as five separate processes, so under a parallel ctest all five start within milliseconds, each deleting the file the others are writing to and reading from (`ReadLog()` reopens by path, lines 49-54, 144/162, 175/191).
Reproduction (verify build, this tree):
```
ctest --test-dir build-verify -R 'PipeInputsTest' -j 1 # 100% tests passed, 0 tests failed out of 5
ctest --test-dir build-verify -R 'PipeInputsTest' -j 8 # red; 40 of 40 iterations red (~/w7/p1-core-review/stress2-*.log)
```
Observed failure text (iteration 1, `--output-on-failure`):
```
../MobileGL/MG_Test/Pipe/PipeInputsTest.cpp:192: Failure
Expected equality of these values:
log.find("Fatal{")
Which is: 49
std::string::npos
Which is: 18446744073709551615
[02:20:47] [Linux PipeInputsTest/FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}
[ FAILED ] PipeInputsTest.ReadingAFilledFieldCompletes (0 ms)
```
i.e. `ReadingAFilledFieldCompletes` reads the **sibling process's** Fatal line out of the shared file and goes red; in the first stress loop (`stress-*.log`) the other direction showed: `ReadingAnOmittedFieldAbortsNamingTheVerb` red 25/25 because a sibling's `main()` unlinked the file between the child's write and the parent's read, so its own line was gone.
Why major: these two cases are the package's G5 proof (`core-v1.md` "G5 unit"), and they go red for a reason that is not the poison — the opposite of `ROADMAP.md:7` (a gate goes red for its own reason) — whenever anyone runs the unit label with `-j` (the integrator's habit in D.1/D.3 is `-j 4`; my full `-L unit -j 12` run happened to pass only because the five cases were spread among 1470 others). The CI `test` job (`test.yml:192,194`) runs `ctest -L unit` serially today, which is why it has not shown up there; that is luck, not a property of the test. Fix is local to the test: a per-process file (e.g. `mobilegl-pipeinputs-test-<pid>.log`, removed at exit) — the forked child inherits the path, so nothing else changes.
## 3. Minors
1. **Undeclared 10th placement deviation — `GL_Sync.cpp:235` (`DeleteSync` in the orphan sweep).** `MGP_FILL(DeleteSync);` sits inside the loop body but **before** `if (backendDeleteSync && syncObject->backendHandle)`, so a backend with a null `DeleteSync` entry, or a sync without a backend handle, bumps the serial for every orphan. D7 says a null entry never bumps, and the sibling loop at `GL_Query.cpp:937` (`DeleteBackendQuery`) was placed inside the guard. Harmless (kQuery fills one field; nothing reads between two fills) — the same argument as the nine sites the result file declares — but it is not in the declared list (deviation 1 names 9 sites). Either move it inside the guard or add it to the record.
2. **The compare-at-read arm has zero execution coverage in this package.** `MGPipeVerifyReadHook` (`PipeFill.cpp:409-426`) is the arm the result file and D8 call "the arm that is real in P1", yet no unit test calls an accessor with `Features.PipeVerify` set after mutating the live context, and on this tree no lane can reach it (no backend spells `MGB_CTX->` yet, so no accessor is ever entered). It is exercised for the first time only in D.3 on the integrated tree, where a hook that silently never fires would look like "zero divergence". The code reads correctly (`&self == &gPipeInputs`, sticky skip, `InHook` guard, `CopyField` into `g_readScratch`, `MGPipeInputsFieldEqual`, `ReportDivergence(field, "read")`), but the brief's own mitigation for "green because it never ran" is a falsifier, and the entry compare got one (CORRUPT) while this arm got none. Cheap unit test in the same fork shape: `MG_Config::Features.PipeVerify = true; MGPipeFillForVerb(DrawArrays); pGLContext->SetLineWidth(2.f); (void)gPipeInputs.GetLineWidth();` → expect `SIGABRT` and `Fatal{PipeVerifyDiffer, "GetLineWidth@DrawArrays", verb=<serial>, where=read}`. (Not required by C.1's test list, hence minor.)
3. **`OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` does not prove "exactly".** `PipeInputsTest.cpp:106-132` asserts one sibling (`GetTextureUnitObject`) and one sticky field; "exactly that field" would be a loop over `kMGPipeClassFieldMask[kTextureOp]` asserting every other bit fresh (the shape `EveryVerbFillsItsClassAndNothingElse` already uses at `:238-247`). Two lines; makes the test name true.
4. **The two env-knob parsers are not covered by any registered test.** `ParsePoisonOmissionKnob` (`PipeFill.cpp:300-316`) and `ArmVerify`'s CORRUPT parse (`:351-371`) are only reachable through `MG_Config::Features`, which the unit binary never loads (I confirmed: `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit ctest -R ReadingAFilledFieldCompletes` in build-verify does not arm — the itest path does). I exercised both parsers manually through `DirectGLES.*ClearThenReadPixels` (table above: armed lines, `PipeVerifyBadKnob` on a bad field and a bad verb, the W_ONCE in a POISON-less push build) and they behave; the record is that this coverage lives in E's lanes on the integrated tree, not in A's tests.
5. **`IsCapabilityEnabledIndexed` semantic asymmetry to record.** Per D6 the push accessor aborts for a cap other than `Blend`/`ScissorTest` (`PipeInputs.h:448-460`), while the pull arm returns `false` with an `MGLOG_I` (`RenderState.cpp:471-488`: "a query must never be able to terminate the process"). No backend asks another cap today (`VulkanRenderer.cpp:5437` asks `Blend`), so it cannot trip in P1, but it is the one accessor where push and pull disagree on a *reachable* input by design; worth a line in `MEASUREMENTS.md` (integrator) so the next person who adds `glIsEnabledi`-driven backend reads knows the push arm is fatal there.
6. **`build-linux/libMobileGL.so` carries `GIT@83b1656`, not the `GIT@087685d` the result file cites.** The stamp is taken at configure time; the library in the tree was last linked at c5 (`83b16561`), and c6 (`440d3c52`) touches only `MG_Test/Pipe/**`, so the G1 comparison is of the right bytes (I confirmed `ninja: no work to do` and the section table identical). Purely a bookkeeping mismatch between `core-v1.md` and the artifact; the integrator's D.1 re-run on `~/w7/pipe` supersedes it.
7. **Generated-file "7 files change" (C.1) vs 3 — correctly recorded by the implementer**, and I confirm the reason: `Coverage.def`'s two new accessor rows change `PipeFilled.inc` (63 + sticky) but not `PipeCoverage.inc` (which iterates inventory rows only), and `PipeVerify.inc` changes at c5. `--check` is clean, so nothing is stale.
8. **`kMGPipeForwardedFieldCount = 7` is duplicated by hand** (`PipeInputs.h:133`) next to the generated `kMGPipeInputStickyFieldCount = 7` (`PipeFilled.inc:230`); the two are tied only by `StickyFieldsAreExactlyTheSeven`. A `static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount)` in `PipeInputs.h` would make the header refuse an eighth sticky row instead of the test.
## 4. Where the tree and the brief disagree (verified; the implementer's list is right)
- `GL_Drawing.cpp:906/907` `MemoryBarrier (TextureBarrier)`: the call is on 906, the brief's 907 is the closing brace — fill placed at (now) 932, before the call. ✓
- `RenderStateParameters` has **65** direct data members (clang FieldDecl count; brief D8 says 67). `DynamicBackendParameters` **85**. ✓
- `CapabilityInput`: storage is sized from the enum; the brief's "37" is not load-bearing anywhere in the code. (Not independently recounted.)
- The result file's `ctest -N` regex note (`'^\s+Test\s+#[0-9]+:'`) is right: ctest right-aligns the number; the brief's `'^\s+Test #'` would drop the padded rows. My name diff used the padded form and matched the baseline count (2343 = 2334 + 9).
## 5. Notes for the integrator (not A's findings, surfaced while reviewing)
- **The brief's CI negative-control step (C.4 (b)) as spelled is vacuous on this tree:** `ctest -L integration-verify -R 'DirectGLES\.Verify\..*Mipmap'` — no integration test has "Mipmap" in its name (`ctest -N | grep -i mipmap` finds 7 names, all `TextureTest.*`/`DirectGLESSanity.*` unit-label cases; `grep -rli generatemipmap MobileGL/MG_IntegrationTest/` is empty). With `--no-tests=error` a no-match run exits non-zero, so the inverted "must fail" check would pass without any process ever loading the library. E's `PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb` does not contain "Mipmap" either. Either the scenario's name or the regex needs to change, and the step should additionally grep the lane log for `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}` so it cannot be satisfied by "no tests found". Same applies to D.3's `-R 'Mipmap'` line.
- The compare-at-read hook (minor 2) is the thing to watch in D.3: if the 79 traces and 742 entries report zero `where=read` divergences, make sure at least one accessor was actually entered under verify (e.g. count `MGPipe: verify read of` lines with a deliberately mutated context, or land minor 2's unit test first).
## 6. Verdict
Not approved: fix M1 (per-process log path in `PipeInputsTest::main`, and re-run `ctest --test-dir build-verify -R PipeInputsTest -j 8` a few dozen times green), then re-review. Minors 1-3 and 8 are one-line-each and worth taking in the same pass; 4-7 are records.
@@ -1,95 +0,0 @@
# Adversarial review — p1-core v2 (tree `~/w7/p1-core`, branch `p1/core`, HEAD `d9f4698d`)
Verdict: **NOT approved — 1 major, 5 minors.** Every claim in `core-v2.md` that can be re-run reproduced on this HEAD, all eight v1 items are fixed or honestly recorded, and the four rework commits are correct for what they set out to do. The major is not in the rework; it is a gap in the poison mechanism itself that neither round's tests reach, and it contradicts a sentence of D6 verbatim. It is a one-line fix in a file this package owns.
Tree left exactly as found: `git status` shows only the three untracked build directories; no file edited, nothing committed, nothing pushed. Reviewer logs under `~/w7/p1-core-review2/`; scratch inputs under `scratchpad/wf4/core-review2/` (`s1.sh`, `s2.sh`, `s3.sh`, `prefill.cpp`, `d7check.py`).
## 1. What I re-ran (all on `d9f4698d`; all three build dirs were `ninja: no work to do`)
| check | command | observed |
|---|---|---|
| G1 symbols | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` | `.text 10792579 -> 10792579 (+0, +0.000%)`; `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed, 27060 unchanged` |
| G1 bytes | `cmp -l … \| wc -l`; `readelf -SW` diff | 34 bytes; section table identical; library stamp `GIT@83b1656` (configure-time, as the result file records) |
| G2 (A's half) | `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only `MG_Backend/MGPipe/PipeInputs.{cpp,h}:0` appear; every catalogued count unchanged — A converts no site, touches no B/C file |
| `grep -c pGLContext MobileGL/MG_Backend/MGPipe/*` | | `0 0` |
| fill points | `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp` | Drawing 36, Framebuffer 11, Getter 3, Program 1, Query 18, Sync 6, Texture 8 = **83**; none outside `MG_Impl/GLImpl` |
| fill placement | read of every hunk in `git diff feat/disaggregated..HEAD -- MobileGL/MG_Impl/GLImpl` | every statement is immediately before its table call and after the early returns it is behind; the frontend mutation that precedes the call (`BindImageTexture`'s `Bind`/`NoteTextureUnitTouched`, `SetPatchVertices`, `BeginTransformFeedback`, `SetTransformFeedbackPaused`, `BindTransformFeedbackObject`, `ScopedNeutralPackState`) is before the fill in every case; the nine guarded-expression sites are the nine declared; `GL_Sync.cpp:236` now inside its guard (v1 minor 1 fixed) |
| generator | `python3 scripts/gen_pipe.py --check` / `--self-test` | rc 0 / rc 0; `63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `generated files are up to date`; 6 controls trip, positive OK |
| **generator gates against real drift** (scratch copy `/tmp/gp-review`, never the tree) | 11 mutations, `--check` each | m1 `MGB_CTX->NotAnAccessor()` in `Utils.cpp` → rc 1 `Coverage.def: accessor(s) read by a backend with no row: NotAnAccessor (…/Utils.cpp)`; m2 the same inside a `//` comment → rc 0 (masked); m3 `MGB_CTX->GetBoundTransformFeedbackName()` → the "no backend reads" line disappears (the `MGB_CTX->` spelling IS collected — first time it has met one); m4 `pGLContext->NotAnAccessor2()` → rc 1; m5 `GenerateMipmap` verb row deleted → rc 1 `member(s) without a verb row: GenerateMipmap`; m6 verb class changed → rc 1 `OUT OF DATE: …PipeFillPoints.inc`; m7 `Int ReviewerExtraMember` added to `RenderStateParameters` → rc 1 `member(s) with no F(...)`; m8 field row swapped → rc 1 (stale inc); m9 generated file touched → rc 1; m10 duplicate `(kDraw, GetLineWidth)` → rc 1; m11 `X(kDraw, NotAField)` → rc 1 `not an accessor in Coverage.def` |
| D7 tables vs `FillPoints.def` | `d7check.py` (brief tables parsed mechanically) | 69/69 verb→class identical; 139/139 (class, field) rows identical, per class 47/14/18/18/7/15/8/11/1 |
| unit, 3 configs | `ctest -L unit --no-tests=error -j 12` in build-linux / build-push / build-verify | `100% tests passed, 0 tests failed out of 1481` × 3 |
| `ctest -N` names | padded regex, `comm` against `~/w7/p1-before-ctest-names.txt` | 2349 names in each dir; **0 removed**, 15 added (4 `PipeCatalogue.*` + 11 `PipeInputsTest.*`); linux/push/verify name sets identical |
| skip visibility | `ctest -R PipeInputsTest -V` | build-linux 11 `***Skipped`; build-push 9 skipped + `ReadingAFilledFieldCompletes`, `BadPoisonOmitKnobIsFatalNamingTheKnob` pass; build-verify 11/11 pass |
| **M1(v1) repro, fixed** | `ctest --test-dir build-verify -R PipeInputsTest -j 8` × 40 | **0/40 red**; build-push × 10 → 0/10; `ls /tmp/mobilegl-pipeinputs-test* \| wc -l` = 0 afterwards |
| nm -D | `nm -D --defined-only <lib> \| grep -c MGPipeVerifyInputs` | push 0, verify 1 |
| integration-gpu, push | `ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4` | `100% tests passed, 0 tests failed out of 868`; `grep -c 'Fatal{'` = 0 |
| G4 lane smoke, verify | `MOBILEGL_PIPE_VERIFY=1 MOBILEGL_LOG_FILE_PATH=… ctest --test-dir build-verify -R 'DirectGLES\..*ClearThenReadPixels'` | 5/5 green; 1 × `MGPipe: verify armed`, 0 `Fatal{`, 0 `verify read of` (no backend reads `gPipeInputs` on this tree — expected) |
| G4 negative control A | + `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters` | rc 8, 0/5; `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear", verb=1, where=entry}` |
| FATAL=0 | + `MOBILEGL_PIPE_VERIFY_FATAL=0` | 5/5 green; 1 × `verify summary - … survived MOBILEGL_PIPE_VERIFY_FATAL=0` |
| POISON_OMIT env path | `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit` | green (no reader on this tree), 1 × `poison omission armed` |
| accessor edge semantics | `Core.cpp:98-110,246-253,309-319,806,938-948,965-1041,1087,1180`; `RenderState.cpp:18-26,101-107,471-478,541-593,666,694-696,770-776`; `BufferState.cpp:43-50,90-97`; `TextureState.cpp:157-173` vs `PipeInputs.h:199-556` | bounds handling matches GLContext's for every indexed accessor (same `MOBILEGL_ASSERT`+fallback shapes, `GetColorMaskIndexed` unchecked on both sides, stencil `Back→1 else 0` = `GetStencilFaceIndex`); `GetRenderStateParameters` returns `const&` on both sides — no 1168-byte copy on a read path |
| O-class target domain | `grep -o 'GetBufferBindingSlot([^)]*)' MG_Backend`; `SyncBoundBuffer` callers `DirectGLES.cpp:664,691`; `GetBufferBindingPoint` targets | slots read: DrawIndirect ×16, PixelPack ×7, Parameter ×4, DispatchIndirect ×1 — all in `GlobalBufferTargets`; points read: Uniform/TransformFeedback/AtomicCounter/ShaderStorage = `BufferBindPointTargets`; the variable-target loop at `DirectGLES.cpp:369-370` is bounded by a touched count that is 0 for any other target. No read the fill leaves null is reachable today |
| commit hygiene | `git log --format=%B 440d3c52..HEAD` | 4 commits, `[Type] (Scope): …` + blank + `- ` bullets, 0 `Co-Authored-By`; nothing pushed; all LF |
| ownership | `git diff --stat feat/disaggregated..HEAD` (26 files) | every path in A's C.5 rows; the rework touched 4 files, all A's |
| logging discipline | grep `MGLOG_D`/stdio in `PipeFill.cpp`, `PipeInputs.{h,cpp}` | none |
## 2. Major
### M1 — The poison (and the compare-at-read hook) cannot trip on any read made before the first fill: `FilledGen == 0` is "fresh" while `CurrentVerbSerial == 0`
D6, verbatim: `++m_filled.CurrentVerbSerial` "(starts at 1 so `FilledGen == 0` means never filled)", and the Fatal "before the first verb the name is `<none>`" (`PipeInputs.h:29-30` repeats the promise: `"<none>" before the first verb`). The freshness predicate that every `MGP_INPUT_CHECK` (`PipeInputs.h:44-49`) evaluates is
```
MobileGL/MG_Pipe/generated/PipeFilled.inc:313-317 (emitted by scripts/gen_pipe.py gen_filled)
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
return kMGPipeInputFieldSticky[index] ? state.FilledGen[index] != 0
: state.FilledGen[index] == state.CurrentVerbSerial;
}
```
Before the first `MGPipeFillForVerb` both sides are 0 (`gPipeInputs` is value-initialised, `PipeInputs.h:612,678`), so `0 == 0` is **fresh**: every V-class accessor and both `SharedPtr` O-class accessors return their default-constructed storage (zeros, empty `SharedPtr`) with no Fatal. The serial only "starts at 1" *after* the first fill (`PipeFill.cpp:517-519`), which makes 0 distinguishable from every filled generation but does not make it stale. The second net has the same window: `ArmVerify()` runs only inside `MGPipeFillForVerb` (`PipeFill.cpp:505-507,361-366`), so `MGPipeVerifyReadHook` sees `g_verify.Enabled == false` (`PipeFill.cpp:421`) until the first fill even when `Features.PipeVerify` is already set. Only the four raw-pointer O-class accessors (`GetBufferBindingSlot`, `GetBufferBindingPoint`, `GetFramebufferBindingSlot`, `GetImageTextureBinding`/`GetTextureUnitObject`) trip before the first fill, through their null-base checks (`PipeInputs.h:489,499,512,521,552`) — 55 of 63 fields are silently served wrong values.
Reproduction (a 30-line program compiled and linked with `PipeInputsTest`'s own ninja commands against `build-verify`, nothing in the tree touched; `scratchpad/wf4/core-review2/prefill.cpp`, `s3.sh` section (a)):
```
$ MOBILEGL_LOG_FILE_PATH=~/w7/p1-core-review2/prefill-plain.log ~/w7/p1-core-review2/prefill
before-first-fill: serial=0 gen[GetLineWidth]=0 fresh=1 live=1
read completed: stored=0 live=7 # gPipeInputs.GetLineWidth() after pGLContext->SetLineWidth(7.f), no fill
GetRenderStateParameters completed: LineWidth=1
GetProgramForDraw completed: (nil)
rc=0 # expected per D6: SIGABRT, Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}
$ … prefill x # same with MG_Config::Features.PipeVerify = true before the reads
before-first-fill: serial=0 gen[GetLineWidth]=0 fresh=1 live=1
read completed: stored=0 live=7
rc=0 # no "verify armed", no "verify read of", no Fatal in the log
```
Why major: this is the "poison that can never trip" shape, on the exact window the brief's own risk table names (E: "A read outside any verb … If the lane finds another (`@<none>` in the Fatal), the answer is a fill point"). With this predicate the lane cannot find one for a V-class field: a backend read that lands before the first verb — an init-time capability probe, the first link, anything reached from `eglMakeCurrent` — is served zeros by the push arm, is not compared by the read hook, and reports nothing. `PipeCatalogue.PipeInputFieldsStartUnfilled` (`PipeCatalogueTest.cpp:225-228`, pre-P1) sets `state.CurrentVerbSerial = 1` by hand before asserting "unfilled", i.e. the P0.5 test already steps around serial 0; none of the 11 `PipeInputsTest` cases reads before a fill. Honest scope: **latent on this tree** — the scout catalogue (`scout-pull-sites.md:951`) finds exactly two out-of-verb pulls, both `InvalidateCompileEnv` (sticky, forwarded, unchecked), so D.3 will not hit it today. It is still a deviation from D6 that nobody declared, in a mechanism whose whole value is catching the case that is not catalogued yet.
Fix (A's files): emit `state.FilledGen[index] != 0 && state.FilledGen[index] == state.CurrentVerbSerial` for the non-sticky arm in `gen_filled` (`gen_pipe.py`, the `MGPipeInputFieldIsFresh` text) and regenerate; add the serial-0 assertion to `PipeInputFieldsStartUnfilled`; add one fork case (`ReadingBeforeAnyFillAbortsNamingNoVerb`: live context, no fill, `gPipeInputs.GetLineWidth()` → SIGABRT + `Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}`). With the poison fixed the read hook's arming-at-first-fill is harmless in a verify build (a pre-fill read is Fatal before the hook matters); if a plain push build without POISON is also meant to be covered, arm in the hook when `!g_verify.Parsed`.
## 3. Minors
1. **The seven F-class forwarders carry no `MGP_INPUT_CHECK`** (`PipeInputs.h:563-571`, `PipeFill.cpp:464-500`), while D4 says "Every accessor body is: `MGP_INPUT_CHECK(...)` → `MGP_INPUT_VERIFY_READ(...)`" and the header comment narrows it to "every non-forwarded accessor" (`PipeInputs.h:40,155`). This is the *right* behaviour — the two init-time `InvalidateCompileEnv` calls (`BackendObject_DirectVulkan.cpp:388-390,786-788`) run before any fill and would otherwise be `Fatal{…@<none>}` — but it is an undeclared deviation, and it means the sticky stamp (`FilledGen = 1`, `PipeFill.cpp:530-535`) and the `Sticky → FilledGen != 0` branch are consulted by no accessor at all: the sticky machinery is exercised only by tests that call `MGPipeInputFieldIsFresh` directly. Record it in the deviations list (and in `MEASUREMENTS.md`), so P2's tracker does not "fix" the missing check.
2. **`ArmVerify` re-arms only when `Features.PipeVerify` changes** (`PipeFill.cpp:362`); `PipeVerifyCorrupt` and `PipeVerifyFatal` are latched at that moment (`:367-374`) and a later change to either without a `PipeVerify` toggle is not seen. Deviation 14 and the `878db2c4` subject ("re-parse the POISON_OMIT and VERIFY knobs when their Features value changes") overstate this. Lane behaviour is unaffected (all three are loaded before the first fill); the unit tests set all three before the child's first fill. Say so precisely in the record.
3. **`GetBufferBindingSlot(BufferTarget::Index)` diverges between arms by design and no `FillPoints.def` row can repair it.** `GLContext::GetBufferBindingSlot` is polymorphic (`Core.cpp:98-106`: `Index` returns the bound VAO's element-buffer slot); the fill copies `GlobalBufferTargets` only and leaves `Index` null (`PipeFill.cpp:63-69`), so the push accessor is `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@…"}` there (`PipeInputs.h:489-492`). Unreachable today (every backend slot read is DrawIndirect/DispatchIndirect/Parameter/PixelPack; table above), same class as v1 minor 5 (`IsCapabilityEnabledIndexed`): both belong in the same `MEASUREMENTS.md` line for the integrator, because the D.3 rule "a Fatal is fixed by adding the row" is false for these two accessors.
4. **`VerifyFatalOffLogsTheDivergenceAndContinues` never observes the `verify summary` line** — the child `_exit(0)`s (`PipeInputsTest.cpp:446`), so `VerifyState::~VerifyState` (`PipeFill.cpp:348-354`) does not run; the summary path is covered only by my lane run (`summary=1` under `MOBILEGL_PIPE_VERIFY_FATAL=0`). Fine as a unit-level statement of "continues"; note that the teardown line's coverage lives in E's lanes.
5. **`MGPipeVerifyReadHook` re-reads the whole field on every accessor call** (`PipeFill.cpp:430`) — for `GetRenderStateParameters` that is a 1168-byte copy plus a 65-member compare per backend read, and `GetProgramForDraw` re-joins the link per read. Declared (deviation 2) and inside the 5–10× verify budget; recorded here only so the D.3 wall-time numbers are read with this in mind, and because the `InHook` guard (`:429-432`) is what keeps a `GetProgramForDraw`-triggered backend re-entry from recursing — worth a comment at the site.
## 4. Where the tree and the brief disagree (verified; the implementer's list stands)
Unchanged from v1: `GL_Drawing.cpp:906` vs 907; `RenderStateParameters` 65 members not 67; `CapabilityInput` 35 not 37; c5 regenerates 1 file not 7; `integration-gpu` 868 entries; the `ctest -N` regex needs the padded form. Nothing new this round.
## 5. Notes for the integrator (not A's findings)
- The v1 note on the vacuous `-R 'DirectGLES\.Verify\..*Mipmap'` CI regex still stands; the implementer's record of it in `core-v2.md` is accurate.
- Once M1 is fixed, D.3 gains a new failure mode worth expecting: any backend read reached from EGL/init before the first verb will now be `Fatal{UnmigratedPipeInput, "<Field>@<none>"}` in the verify lane — per E's risk table the answer is a fill point at that boundary (an EGL virtual), never a sticky flag or a `FillPoints.def` row.
- `GetBufferBindingSlot(Index)` and `IsCapabilityEnabledIndexed(other cap)` are the two accessors where "add the row" does not apply (minor 3): put both on one `MEASUREMENTS.md` line.
## 6. Verdict
Not approved: fix M1 (predicate emission in `gen_pipe.py` + regenerate, serial-0 assertion in `PipeInputFieldsStartUnfilled`, one fork case for the `@<none>` Fatal), then re-review. Minors 1–3 are records for the deviations list / `MEASUREMENTS.md`; 4–5 are notes.
@@ -1,324 +0,0 @@
# Adversarial review — p1-core v3 (tree `~/w7/p1-core`, branch `p1/core`, HEAD `44ffafb2`)
Verdict: **NOT approved — 1 major, 6 minors.**
Everything the round-2 review asked for is genuinely fixed and I reproduced each fix independently
rather than through the implementer's own tests: the serial-0 window is closed on **both** branches
of the freshness predicate (sticky included), the `PipeInputsTest` log-path race is gone (0/20 red
under `-j 8`, no leftovers), `ArmVerify` keys on all three knobs, and the FATAL=0 child now observes
its teardown summary. All eight G-gates that can be checked in this tree are green, the 83 fill
points are in the right places, and the 63 accessors are signature-identical to `GLContext`'s.
The major is not in the rework. It is a **hole in `FillPoints.def`'s `kReadback` class mask** that
the espryt reviewer raised against package A in round 1, that is still open at HEAD, and that turns
gate G3 red on the integrated tree — the first of the two cross-package items I was asked to settle.
The second (the `FilledGen == 0` window) is genuinely closed.
Tree left exactly as found: `git status` shows only the three untracked build directories, `git diff`
is empty, HEAD is `44ffafb2`. All reviewer artifacts under `~/w7/p1-core-review3/`; scratch inputs
under `scratchpad/wf4/core-review3/` (`gates.sh`, `nc.sh`, `lane.sh`, `probe.cpp`, `probe2.cpp`,
`sig.py`, `mut.sh`, `fixsim.sh`). Two generator mutation experiments ran in `/tmp` copies, never in
the tree, and were deleted afterwards.
---
## 1. What I re-ran (all on `44ffafb2`; all three build dirs were `ninja: no work to do`)
| check | command | observed |
|---|---|---|
| **G1 symbols** | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` | `.text 10792579 -> 10792579 (+0, +0.000%)`; `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed, 27060 unchanged`; Removed/Added/Resized/Renamed sections all `_none_` |
| **generator** | `python3 scripts/gen_pipe.py --check` / `--self-test` | rc 0 / rc 0; `63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `generated files are up to date`; 6 negative controls `tripped as expected`, `positive control OK` |
| generated files match their sources | `git status --porcelain` after `--check` | only the three untracked build dirs; no `.inc` drift |
| **ctest -L unit ×3** | `ctest --test-dir {build-linux,build-push,build-verify} -L unit --no-tests=error -j 12` | `100% tests passed, 0 tests failed out of 1482` in each |
| **`ctest -N` name diff** | padded regex `sed -E 's/^ *Test +#[0-9]+: //'`, `comm` vs `~/w7/p1-before-ctest-names.txt` | 2350 names in each of the three dirs; **0 removed**; 16 added (4 `PipeCatalogue.*` + 12 `PipeInputsTest.*`); linux/push/verify name sets byte-identical (`diff` empty) |
| fill points | `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp` | Drawing 36, Framebuffer 11, Getter 3, Program 1, Query 18, Sync 6, Texture 8 = **83** over **69** distinct verbs; none outside `MG_Impl/GLImpl` |
| every table call has a fill | all `gBackendFunctionsTable.GL.<Entry>` occurrences in `MG_Impl` classified | 89 occurrences; every one that is a *call* has an `MGP_FILL` on its path; the surplus occurrences are null-checks (`BeginOcclusionQuery` ×3, `GetTexImage`, `GetTextureImage`, `EndTransformFeedback`) and captures (`MemoryBarrier` ×2, `MemoryBarrierByRegion`, `DispatchCompute*`, `PatchParameteri`). No missing fill point |
| **negative control A, unit** | `PipeInputsTest.VerifyCorruptKnobNamesTheFieldAtEntry`, plus my own `probe2 corrupt` | see §2.2 — red for the stated reason |
| **negative control B, unit** | `PipeInputsTest.ReadingAnOmittedFieldAbortsNamingTheVerb` / `PoisonOmitKnobArmsTheOmission`, plus my own `probe2 omit` / `omit-other-verb` | see §2.2 — red on that verb only |
| negative control A, lane | `MOBILEGL_PIPE_VERIFY=1 MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters ctest --test-dir build-verify -R 'DirectGLES\..*ClearThenReadPixels'` | **red**: `1586 - DirectGLES.ClearThenReadPixelsScenario.ADrawIntoTheDefaultFramebufferSurvivesAnEarlierClear (Subprocess aborted)`; log: `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear", verb=1, where=entry}` |
| clean lane smoke | same without the knob | 5/5 green, `verify armed`=1, `Fatal{`=0, `verify read of`=0 |
| negative control B, lane | `+ MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit` | 5/5 green, `poison omission armed`=1, `Fatal{`=0 — vacuous on this tree by construction (minor 5) |
| M1(v1) log race, re-stressed | `ctest --test-dir build-verify -R PipeInputsTest -j 8` × 20 | **red = 0/20**; `ls /tmp/mobilegl-pipeinputs-test*` = 0 afterwards; log path is per-pid (`PipeInputsTest.cpp:525-526`) |
| skip visibility | `ctest -R PipeInputsTest` in the three dirs | build-verify 12/12 pass; build-push 4 skipped (verify-only + the poison-only new case); build-linux 12 skipped ("push not compiled in") |
| **M1(v2) fix, independently** | `probe2 prefill` (my own binary, linked against `build-verify`) | `serial=0: fresh fields = 0 of 63 (sticky RecordError fresh=0)`; `serial=5, all stamps 0: fresh fields = 0`; then rc **134** with `Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}` |
| accessor signature parity | `sig.py`: all 63 accessor declarations in `PipeInputs.h` vs `MG_State/GLState/Core.h` | **63/63 identical** in name, parameters, constness and return type. The nine my regex first reported as DIFF (`IsTransformFeedbackActive`, `GetTextureContextId`, …) are one-line inline definitions in `Core.h` (`:325,326,334,357,429,441,126,130,143`) that the regex skipped; re-read by hand, all match. **No `const&` became a copy**, on the draw path or anywhere else |
| build-configuration hygiene | `ninja -t commands` in the three dirs | build-verify/build-push: **0** `MobileGL/` TUs compiled without `-DMOBILEGL_PIPE_PUSH=1` (so the `#if MOBILEGL_PIPE_PUSH` members added to `MG_Config::Features` cannot produce an ODR/layout split); build-linux: 0 occurrences of either macro and `PipeInputs.cpp`/`PipeFill.cpp` not compiled |
| D3 verbatim | `cat MobileGL/MG_Pipe/PipeInputsSwitch.h` | identical to the brief's D3 text, including the `#else` arm being the only `pGLContext` spelling |
| generator gate on the M1 fix | `mut.sh` m1: delete `if (gen == 0) return false;` from the generated `PipeFilled.inc` in a `/tmp` copy | `gen_pipe.py --check` → **rc 1** (the predicate cannot silently regress) |
| commit hygiene / ownership | `git log`, `git diff --stat 087685d1..HEAD` | 14 commits, `[Type] (Scope): …` single-line subjects, 0 `Co-Authored-By`, nothing pushed; 26 files, every path in A's C.5 rows |
---
## 2. The two cross-package items I was asked to settle
### 2.1 (a) `kReadback` and the transform-feedback flags — **still open. This is the major.**
See §3 (M1). Answer to the question as posed: **no**, `FillPoints.def`'s `kReadback` class does not
carry `IsTransformFeedbackActive` / `IsTransformFeedbackPaused` at HEAD, and **yes**, HEAD's class
table still produces `Fatal{UnmigratedPipeInput, "IsTransformFeedbackActive@ReadPixels"}` — I
reproduced it directly against this tree's `build-verify` library, without the espryt patch.
On the second half of the question ("whether any other verb class is missing a field its verb path
reads"): I found **no second gap**, from three independent angles —
1. the espryt reviewer's full `DirectGLES` `integration-gpu` run under verify on an integrated tree
(441 entries) produced exactly 19 aborts, all this one Fatal (`espryt-review-v1.md:46`);
2. the magma reviewer's full `DirectVulkan` verify run produced **zero** `UnmigratedPipeInput` and
zero `@<none>` (`magma-review-v1.md:99`);
3. my own source audit of the helpers that serve more than one verb class against the pull-site
catalogue §4.2 and §1: `SyncRenderState` (kDraw/kClear/kBlitOrCopy/kProgramOp — all four masks
carry its four accessors), `SyncNeccessaryTextures` (its five texture accessors are in every
class that reaches it, kReadback included), `SyncCurrentFBO` /
`FramebufferImpl::GetReadColorAttachment` (`Managers.cpp:5355`) and
`IsFixedPointFallbackReadAttachment` (`:5460`) read `GetFramebufferBindingSlot`, which kReadback
has; `BoundImageUnitFormat` (`Managers.cpp:6230`, `GetImageTextureBinding`) is reached only
through `ImageUnitFormatsStillMatch` from `SyncCurrentProgram` (`DirectGLES.cpp:2812`), i.e. the
draw/dispatch closure, and both masks carry the field; `MultiDraw.cpp:45,51,52,87,95` are kDraw
accessors in kDraw's mask.
Residual exposure worth naming for D.3: **nobody has yet run the 40-trace retrace under
`MOBILEGL_PIPE_VERIFY=1`**, and that is the part of G3 that exercises paths the itest lanes do
not (MC shaders, XFB spans, indirect count). More `FillPoints.def` rows may fall out there; per
D7 the fix is always "add the row", never a sticky flag.
### 2.2 (b) The `FilledGen == 0` / `serial == 0` window — **closed, on both branches and for sticky fields.**
`MobileGL/MG_Pipe/generated/PipeFilled.inc:316-321` (emitted by `gen_pipe.py:813-818`):
```
inline Bool MGPipeInputFieldIsFresh(const MGPipeFilledState& state, MGPipeInputField field) {
const SizeT index = static_cast<SizeT>(field);
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
}
```
The `gen == 0` refusal precedes the branch, so it covers the sticky arm too. Verified three ways,
none of them by trusting the delivered tests:
```
$ MOBILEGL_LOG_FILE_PATH=~/w7/p1-core-review3/p2-prefill.log ~/w7/p1-core-review3/probe2 prefill
serial=0: fresh fields = 0 of 63 (sticky RecordError fresh=0)
serial=5, all stamps 0: fresh fields = 0
rc=134
[FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}
```
(the second line is the case the v2 fix's wording does not cover explicitly: a *non-zero* serial with
an unstamped sticky field is stale too, so a process that filled before a sticky field was ever
stamped cannot read it as fresh either).
The compare-at-read branch: the hook still arms only inside `MGPipeFillForVerb`
(`PipeFill.cpp:532`), so it is blind before the first fill — but `MGP_INPUT_CHECK` precedes
`MGP_INPUT_VERIFY_READ` in every accessor body (`PipeInputs.h:199-474`) and a verify build always
defines `MOBILEGL_PIPE_POISON` (`PipeInputs.h:21-26`), pinned by
`static_assert(MOBILEGL_PIPE_POISON, ...)` at `PipeFill.cpp:349`. The probe run above sets
`Features.PipeVerify = true` before the read and still aborts on the poison first, with no
`verify armed` and no `PipeVerifyDiffer` in the log — i.e. the window is closed on that branch by
construction, not by luck. In a push build without the poison the window is open by design (both
macros compile out); that is D2's definition, not a defect.
### 2.3 The two negative controls, exercised independently
Driving `MG_Config::Features` exactly as `ConfigLoader.cpp:247-252` does, from my own binary
(`~/w7/p1-core-review3/probe2`, linked against `build-verify`):
```
===== corrupt (Features.PipeVerify=1, PipeVerifyCorrupt="GetRenderStateParameters")
rc=134
MGPipe: verify armed - 63 fields, 69 verbs, fatal=1
MGPipe: verify corruption armed - GetRenderStateParameters
MGPipe: Fatal{PipeVerifyDiffer, "GetRenderStateParameters@DrawArrays", verb=1, where=entry}
===== omit (Features.PipePoisonOmit="GenerateMipmap:GetActiveTextureUnit")
draw read ok <- the preceding DrawArrays read is unaffected
sibling read ok <- GetTextureUnitObject, same verb, still fresh
rc=134
MGPipe: poison omission armed - GetActiveTextureUnit@GenerateMipmap
MGPipe: Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}
===== omit-other-verb (same knob, fill BindImageTexture — same kTextureOp class)
green: BindImageTexture (same kTextureOp class) is unaffected rc=0
```
Both controls turn something red, each for its stated reason, and G5's "on **that** verb and only
there" holds even against a sibling verb of the same class. The lane-level CORRUPT control is red as
well (§1).
---
## 3. Major
### M1 — `FillPoints.def`'s `kReadback` class lacks `IsTransformFeedbackActive` / `IsTransformFeedbackPaused`, which Espryt's `ReadPixels` **and** `GetTexImage` verb paths read; G3 is red on the integrated tree
**Where.** `MobileGL/MG_Pipe/FillPoints.def:225-240` — the `kReadback` block holds 15 rows and
neither flag. The generated mask says so:
`MobileGL/MG_Pipe/generated/PipeFillPoints.inc:290-291`, `// kReadback: 22 fields (15 own + 7 sticky)`,
`{{0x5c50f3a041300541ull, 0x0000000000000000ull}}`.
**The read, at `087685d1` and unchanged on `p1/espryt`:**
```
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5259-5260 (ScopedEmulationDrawState::ScopedEmulationDrawState, :5226)
if (MG_State::pGLContext->IsTransformFeedbackActive() &&
!MG_State::pGLContext->IsTransformFeedbackPaused() && g_GLESFuncs.glPauseTransformFeedback) {
```
reached as
`ReadPixels` (`:8999`, the `GLFunctionsTable.ReadPixels` entry)
→ `ReadPixelsDepthComponent` (call `:9066`, definition `:8140`)
→ `DepthStencilSamplingReadImpl::Read` (call `:8128`, definition `:8039`)
→ `ScopedEmulationDrawState emulationState;` (`:8061`)
→ the two reads at `:5259-5260`.
`GetTexImage` (`:9166`) is in the same class and reaches the same helper through its own pack path
(`:9498`, `:9502`), so the identical Fatal is reachable as `IsTransformFeedbackActive@GetTexImage`.
The catalogue's §4.2 row for Espryt's `ReadPixels` lists only the pack/format-conversion helpers and
misses this branch entirely (`scout-pull-sites.md:893`), which is how the row was lost; the
catalogue's own §1 entry for `:5259` attributes it to `ResolveThenBlit` / "blit" only
(`scout-pull-sites.md:188-189`), and `kBlitOrCopy` does carry both flags — which is why only the
readback class is short.
**Reproduction (this tree, `build-verify`, nothing in the tree touched).** A 40-line program
(`scratchpad/wf4/core-review3/probe.cpp`, built with `PipeInputsTest`'s own compile/link flags into
`~/w7/p1-core-review3/probe`) that does what `DirectGLES.cpp:5259` does under `MGB_CTX` on the
`ReadPixels` verb:
```
$ MOBILEGL_LOG_FILE_PATH=~/w7/p1-core-review3/probe.log ~/w7/p1-core-review3/probe
after ReadPixels fill: serial=1 verb=ReadPixels
fresh(IsTransformFeedbackActive)=0 fresh(IsTransformFeedbackPaused)=0 fresh(GetPixelStoreParameters)=1
Aborted rc=134
$ cat ~/w7/p1-core-review3/probe.log
[FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "IsTransformFeedbackActive@ReadPixels"}
```
**Corroboration on a real lane** (not mine, but the reason this was raised against core in round 1):
`espryt-review-v1.md:46,50` — `MOBILEGL_PIPE_VERIFY=1 ctest --test-dir build-verify -L integration-gpu -R '^DirectGLES\.' -j 4`
on an integrated tree gives **421/441**, the 19 failures all `DirectGLES.ForcedDepthStencilEmulation.*`
(`MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1`, cases 2274-2293), each aborting on exactly this
Fatal (`~/w7/p1-espryt-review-int-one.log`). I confirmed the row is still absent on `p1/espryt`
(`f4ea84c5`) too, since that branch inherits `FillPoints.def` from `p1/contract`.
**Why this is a major rather than a note.** G3 requires zero `Fatal{UnmigratedPipeInput}` across the
`integration-verify` lane; with this row missing, 19 known entries abort the moment core and espryt
are integrated (D.1 order: core → espryt → magma → verify), i.e. the package's own acceptance gate
cannot pass. D7 designates `FillPoints.def` — package A's file — as the *only* place such a Fatal
may be fixed ("a `Fatal{UnmigratedPipeInput}` found there is fixed by adding the row to this table,
never by marking the field sticky"), so the fix belongs to A, and the lane that is the designated
oracle has already spoken. It was reported before this round's rework began and `core-v3.md`
neither fixes it nor records it (its "Unfinished" section says "Nothing in A's scope").
**In fairness to the implementer:** the brief's own D7 table for `kReadback` has the same hole, and
`FillPoints.def` reproduces the brief's 139 (class, field) rows exactly — the round-2 review's
`d7check.py` verified 139/139, and `FillPoints.def` is untouched since. A followed the specification
faithfully; the specification was derived from a scout reachability trace that missed one branch.
That makes it a *correct* implementation of a *wrong* table, which is still an open defect in A's
deliverable, not somebody else's.
**Fix, verified end to end in a `/tmp` copy** (`scratchpad/wf4/core-review3/fixsim.sh`): two rows in
`FillPoints.def`'s `kReadback` block plus a regenerate. `gen_pipe.py --check` correctly goes rc 1 on
the un-regenerated tree, and the regenerated mask is the only change:
```
- // kReadback: 22 fields (15 own + 7 sticky)
- {{0x5c50f3a041300541ull, 0x0000000000000000ull}},
+ // kReadback: 24 fields (17 own + 7 sticky)
+ {{0x5f50f3a041300541ull, 0x0000000000000000ull}},
```
I did **not** apply it (the tree is left as found). After applying, re-run the espryt reviewer's
lane on an integrated tree to confirm 441/441, and re-run my `probe` (it must print
`fresh(IsTransformFeedbackActive)=1` and exit 0).
---
## 4. Minors
1. **A `MOBILEGL_PIPE_VERIFY_CORRUPT` field outside every executed verb's mask arms, logs, and never
reports** — `MobileGL/MG_Impl/Pipe/PipeFill.cpp:416`
(`if (g_verify.Corrupt && MGPipeFieldMaskHas(mask, *g_verify.Corrupt))`). Observed:
`probe2 corrupt-outside-mask` (CORRUPT=`GetPixelStoreParameters`, verb `DrawArrays`) → **rc 0**,
log carries `verify corruption armed - GetPixelStoreParameters` and nothing else. Negative
control A is therefore green-by-omission whenever it is pointed at a field no executed verb
fills; only the unknown-name case is Fatal (D2). The CI step's `GetRenderStateParameters` happens
to be in kDraw/kClear/kBlitOrCopy/kReadback/kProgramOp, so today it works — but the control
cannot report its own inapplicability. One counter and a teardown `MGLOG_E` when a corruption was
armed and applied zero times would make it falsifiable.
2. **The push arm loses the pull arm's DEBUG bounds assertion on two texture accessors.**
`PipeInputs.h:518-533` (`GetImageTextureBinding`, both overloads) and `:549-556`
(`GetTextureUnitObject`) index the raw base pointer with no range check, while
`MG_State/GLState/TextureState/TextureState.cpp:157-172` guards both with
`MOBILEGL_ASSERT(unit >= 0 && unit < MAX_TEXTURE_IMAGE_UNITS, ...)`. In an INFO build the assert
is inert (`Defines.h:113-115`), so G1/D9 are unaffected and behaviour is identical; a DEBUG push
build silently reads out of bounds where the pull build would have said so. (`GetBufferBindingPoint`'s
`index` is unchecked on both sides — `PipeInputs.h:506` vs `BufferState.cpp:93` — so it is
symmetric and not part of this.)
3. **Nine fill points still bump the serial for a null table entry** — declared deviation 1, and I
re-audited all 83 statements against appendix B.7 rather than trusting the record. The nine are
`GL_Query.cpp:263,294,306,531,543,575,583` and `GL_Sync.cpp:179` (plus `GL_Drawing.cpp:1358`,
which is actually exact — it sits inside the `FenceSync && ClientWaitSync` guard). The **dangerous**
direction is absent everywhere: there is no call that can be reached without its fill. The
round-1 finding (`GL_Sync.cpp` `DeleteSync` in the orphan sweep) is fixed — `GL_Sync.cpp:233` is
now inside `if (backendDeleteSync && syncObject->backendHandle)`, matching its `GL_Query.cpp:930`
sibling. Placement relative to frontend mutation is right in every case that matters
(`BindImageTexture` after `.Bind`/`NoteTextureUnitTouched`, `PatchParameteri` after
`SetPatchVertices`, `Pause`/`Resume` after `SetTransformFeedbackPaused`, `BindTransformFeedback`
after `BindTransformFeedbackObject`, `EndTransformFeedback` *before* the frontend's own
`EndTransformFeedback()`, `ReadPixels` inside the `ScopedNeutralPackState` scope at
`GL_Texture.cpp:1078`).
4. **The env-var forms of both knobs are inert in the unit binary.** `MG_ConfigLoader` never runs
there, so `MOBILEGL_PIPE_VERIFY_CORRUPT` / `MOBILEGL_PIPE_POISON_OMIT` in the environment do
nothing to `ctest -R PipeInputsTest`; the unit-level controls set `MG_Config::Features` directly
(the same fields `ConfigLoader.cpp:247-252` writes, through the same parsers). That is the right
design, but "the negative controls at unit level" should be understood as the two tests, and the
env→`Features` hop is proven only by the lane runs. Worth one sentence in `MEASUREMENTS.md` so
nobody later "checks" the controls by exporting the variables around a unit run and reads green.
5. **The lane-level POISON_OMIT control is vacuous in this package** (declared): 5/5 green with only
`poison omission armed` in the log, because no backend on `p1/core` spells `MGB_CTX->`. Its real
coverage is the unit fork case plus E's `PoisonOmitted.` lanes on the integrated tree. Combined
with the v1 note that the brief's CI regex `-R 'DirectGLES\.Verify\..*Mipmap'` matches nothing,
this control is the one the integrator should watch most closely at D.3.
6. **`core-v3.md`'s "Unfinished: Nothing in A's scope" is not accurate** while M1 stands, and the
result file's deviation list does not mention the `kReadback` row at all even though the espryt
review named it as a core item before this round started. Bookkeeping, but it is the kind of
bookkeeping that lets a known-open item reach D.3 unowned.
---
## 5. Where the tree and the brief disagree (verified; the implementer's list stands)
Unchanged from v1/v2: `GL_Drawing.cpp:906` vs 907; `RenderStateParameters` 65 members not 67;
`CapabilityInput` 35 not 37; c5 regenerates 1 file not 7; `integration-gpu` 868 entries; the
`ctest -N` regex needs the padded form. New this round: the brief's **D7 `kReadback` field list is
itself incomplete** (M1) — when the row is added, `BRIEF-P1.md`'s D7 table and
`scout-pull-sites.md:893` should be corrected in the same pass, or the next reader will "fix" the
`.def` back to the brief.
## 6. Notes for the integrator (not A's findings)
- The magma reviewer's verify lane found a genuine `where=read` divergence,
`Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", verb=3, where=read}` on
eight `DirectVulkan` scenarios (`magma-review-v1.md:99`): Magma reads that counter four times
inside one draw and the backend's own texture/sampler completion bumps it between the reads. Per
D.3 that is recorded in `MEASUREMENTS.md` and decided by the plan owner, not silenced — it is the
compare-at-read arm doing exactly its job, and it is not a core defect.
- Nobody has yet run the 40-trace retrace under `MOBILEGL_PIPE_VERIFY=1`. That is the half of G3
most likely to surface further `FillPoints.def` rows; budget the "one day of D.3 iteration" E's
risk table already names.
- `GetBufferBindingSlot(BufferTarget::Index)` and `IsCapabilityEnabledIndexed(cap ∉ {Blend, ScissorTest})`
remain the two accessors where "add the row" does not apply; the one-line `MEASUREMENTS.md` record
the implementer drafted is still owed.
## 7. Verdict
**Not approved.** One major: add `X(kReadback, IsTransformFeedbackActive)` and
`X(kReadback, IsTransformFeedbackPaused)` to `MobileGL/MG_Pipe/FillPoints.def`, regenerate, and
re-run the espryt verify lane (or, in this tree, `~/w7/p1-core-review3/probe`, which must then exit
0). Minors 1-2 are small code changes worth taking in the same pass; 3-6 are records.
@@ -1,82 +0,0 @@
# P1 package A (p1-core) — c2..c6 on top of the contract
Tree `~/w7/p1-core`, branch `p1/core`, base `feat/disaggregated @ 087685d1`, contract `bf86b1ed` (tag `p1/contract`, see `core-contract.md`). Nothing pushed. Working tree clean apart from the three untracked build directories.
## Commits (oldest first, all on `p1/core` after `p1/contract`)
| sha | subject |
|---|---|
| `3aa4d8af1ff2ca6f95c9ab3e047e70bc0e0cf65f` | `[Feat] (Pipe): fill PipeInputs per verb class from GLContext and stamp per-verb generations - a read of a field the verb did not fill is Fatal{UnmigratedPipeInput}` |
| `a196ada4c11b59f23587596486d265124ea06a6f` | `[Feat] (Impl): call MGP_FILL before every GLFunctionsTable entry - 83 statements over 69 verbs, a no-op in the pull build` |
| `275dd3edb4909fe2a23af0f9a2667fb8215d24be` | `[Feat] (Pipe): the MOBILEGL_PIPE_VERIFY shadow comparator - a per-verb entry compare over the fill set and a compare-at-read in every accessor, first differing field and verb serial, fatal by default` |
| `83b16561c20428dd15995ed3b2b4ad5496a97bab` | `[Feat] (Pipe): give the six value structs G4 field lists and assert the lists cover their members - the memcmp fallback is now a compile error, floats in vector types compare bitwise` |
| `440d3c52534e2c227b8b4d83251d8afcb0849e3f` | `[Test] (Pipe): pin the P1 poison and verify shapes - 63 fields, 69 verbs, the seven sticky fields, an omitted GenerateMipmap field leaves exactly that field stale, a corrupted snapshot names its field, reading an unfilled field aborts with SIGABRT` |
`git diff --stat p1/contract..HEAD`: 17 files changed, 1587 insertions(+), 72 deletions(-). Files touched (all inside A's ownership row of C.5): `MobileGL/MG_Backend/MGPipe/PipeInputs.{h,cpp}`, `MobileGL/MG_Impl/Pipe/PipeFill.{h,cpp}`, the seven `MobileGL/MG_Impl/GLImpl/{Drawing,Framebuffer,Texture,Getter,Program,Query,Sync}/*.cpp`, `MobileGL/MG_Pipe/PipeFields.def`, `MobileGL/MG_Pipe/generated/PipeVerify.inc`, `scripts/gen_pipe.py`, `MobileGL/MG_Test/Pipe/{CMakeLists.txt, PipeCatalogueTest.cpp, PipeInputsTest.cpp}`. Not touched: `MG_Backend/DirectGLES/**`, `MG_Backend/DirectVulkan/**`, `PipeCalls.def`, `Coverage.def`, `FillPoints.def`, `PipeInputsSwitch.h`, `CMakeLists.txt` (root), `Config.h`/`ConfigLoader.cpp`, fixtures. No signature in `PipeInputs.h` / `PipeFill.h` / `PipeInputsSwitch.h` that B/C/E code against changed (the accessor set, `MGB_CTX`, `MGP_FILL`, the knob names and log lines are as in the contract); the additions are listed under deviations 5-7.
## What each commit does
- **c2 filler** (`PipeFill.cpp`): `MGPipeFillForVerb` walks `kMGPipeClassFieldMask[kMGPipeVerbClass[verb]]`; `MGPipeFillAccess::CopyField` is one switch over the 56 stored fields, each branch calling the `GLContext` accessor of the same name (D4's fill column verbatim: loops of 8/16/32, `GlobalBufferTargets`/`BufferBindPointTargets` for the slot/point bases, `&ctx.GetTextureUnitObject(0)`/`&ctx.GetImageTextureBinding(0)` as array bases, Blend x8 + ScissorTest x16 for the indexed capabilities, all 35 capabilities incl. `FramebufferSrgb`'s constant false); every copied field is stamped with the new serial; the seven sticky fields get `FilledGen = 1` on the first live fill. `MOBILEGL_PIPE_POISON_OMIT` (`<Verb>:<Field>`) is parsed once at the first fill; `MGPipeSetPoisonOmission(verb, field)` is the programmatic form; the omitted pair keeps its value copy and loses only its stamp; unknown names are `Fatal{PipeVerifyBadKnob}`.
- **c3 fill points**: the 83 `MGP_FILL(X);` statements of B.7 (36/11/8/3/1/18/6 per TU) plus `#include <MG_Impl/Pipe/PipeFill.h>` after each TU's last `MG_State`/`MG_Backend` include, inserted by a patcher (`scratchpad/wf4/core/patch_c3.py`) that refused any target line not spelling its verb.
- **c4 comparator** (`PipeFill.cpp`, `PipeInputs.{h,cpp}`): entry compare at the end of every fill (`SnapshotFromGLContext` into a file-static second `PipeInputs`, `MOBILEGL_PIPE_VERIFY_CORRUPT` applied to the snapshot arm, `MGPipeVerifyInputs` over the mask, first differing field); compare-at-read through `MGP_INPUT_VERIFY_READ` -> `MGPipeVerifyReadHook(*this, field, i0, i1)` (re-reads the field from the live context into a scratch block and compares against the live block only); `MGLOG_F("MGPipe: Fatal{PipeVerifyDiffer, \"%s@%s\", verb=%llu, where=%s}")` + `abort()` unless `MOBILEGL_PIPE_VERIFY_FATAL=0` (counts, `MGLOG_E` summary at teardown); `MGLOG_I("MGPipe: verify armed - 63 fields, 69 verbs, fatal=N")`, `MGLOG_I("MGPipe: verify corruption armed - F")`, `MGLOG_I("MGPipe: poison omission armed - F@V")`; a push build without the comparator answers `MOBILEGL_PIPE_VERIFY=1` with one `MGLOG_W_ONCE`.
- **c5 G4 growth** (`PipeFields.def`, `gen_pipe.py`, regenerated `PipeVerify.inc`): the six field lists (RenderStateParameters 65, PixelStoreParameters 8, PerBufferBlendState 7, StencilFaceState 7, DynamicBackendParameters 85, MGHostSpan 4), `kMGPipeVerifiedPayloadCount` 63 -> 69; `MEMCMP_FALLBACK_TYPES` empty and the fallback branch `static_assert(sizeof(T) == 0, "no field list in PipeFields.def for this type")`; `Array<T,N>` overload; VecBase detected by a probe and compared bitwise over `data`; `check_field_lists_cover_struct_members()`, `scan_live_accessors()` (both modes), `--self-test` with six negative controls and a positive one.
- **c6 tests**: the four `PipeCatalogue.*` additions and the five `PipeInputsTest.*` cases (new unit target, own `main()` for `MOBILEGL_LOG_FILE_PATH`, fork/waitpid, never `EXPECT_DEATH`).
## Verification (WSL, `~/w7/p1-core`; logs under `~/w7/p1-core-*.log`)
Every command below was run on the final tree (`440d3c52`) unless a commit is named.
| command | actual output |
|---|---|
| `python3 scripts/gen_pipe.py --check` | `gen_pipe: 71 calls (11 screen, 60 context), 69 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes` / `inventory 477 rows: ... 0 UNMAPPED` / `1 accessor row(s) no backend reads: GetBoundTransformFeedbackName` / `generated files are up to date`, rc=0 |
| `python3 scripts/gen_pipe.py --self-test` | six controls "tripped as expected" (struct member without F, F that is not a member, payload with no struct, verb missing from FillPoints.def, verb not in GLFunctionsTable, field row naming a non-accessor), `positive control OK`, rc=0. Before the whitespace-tolerant fix the three FillPoints controls reported DID NOT TRIP and the run exited 1 - the "zero trips is an error" arm was exercised. |
| `cmake --build build-linux` + `ctest -L unit --no-tests=error` | rc=0; `100% tests passed, 0 tests failed out of 1475` (1466 before c6 + 9 additions) |
| `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` | `.text 10792579 -> 10792579 (+0, +0.000%)`, `.data +0`, `.bss +0`, `.rodata +0`, `Total +0`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 normalised names unchanged`. **G1 holds** after every commit (checked at c3, c4, c5, final). `readelf -S` section sizes identical. `cmp -l` shows 34 differing bytes: 20 in `.note.gnu.build-id`, 14 in `.rodata` = the `GIT@5d99ee4` -> `GIT@087685d` configure stamp and glslang's `Timestamp: 2026-09-05T23:14:30` -> `2026-09-06T01:32:05` string - build stamps of equal length, not code. (Markdown/JSON: `~/w7/p1-core-symbol-final.{md,json}`.) |
| `grep -rc "pGLContext" MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only the two contract files appear, both `:0` (`MG_Backend/MGPipe/PipeInputs.cpp:0`, `PipeInputs.h:0`); every catalogued file's count unchanged - A converts no site |
| `grep -c "pGLContext" MobileGL/MG_Backend/MGPipe/PipeInputs.h PipeInputs.cpp` | `0` `0` |
| `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp \| awk ...` | `83` (Drawing 36, Framebuffer 11, Texture 8, Getter 3, Program 1, Query 18, Sync 6) |
| `cmake --build build-push` + `ctest -L unit --no-tests=error` | rc=0; `100% tests passed, 0 tests failed out of 1475`; `MOBILEGL_PIPE_PUSH=1` on every compile command, `MOBILEGL_PIPE_VERIFY=1` on none |
| `cmake --build build-verify` + `ctest -L unit --no-tests=error` | rc=0; `100% tests passed, 0 tests failed out of 1475`; `MOBILEGL_PIPE_VERIFY=1` on all 497 compile commands |
| `ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4` (at c3, `a196ada4`) | `99% tests passed, 1 tests failed out of 868`; the one failure `DirectVulkan.PointSizeDemotion.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn` (0.23 s) passes alone in build-push AND in build-linux (`100% tests passed ... out of 4`) - a `-j 4` flake of an env-pinned lane, the case the integrator script's `--rerun-failed -j 1` exists for; zero `Fatal{` lines in the log |
| `ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4` (final tree) | ITEST_FINAL_PLACEHOLDER |
| `ctest -N` name diff (pull): `comm -23 ~/w7/p1-before-ctest-names.txt -` | 2343 names, **0 removed**, 9 added (`PipeCatalogue.{VerbTableIsTheFunctionTable, StickyFieldsAreExactlyTheSeven, FloatVectorsCompareBitwise, SixValueStructsHaveFieldLists}`, `PipeInputsTest.{OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale, ReadingAnOmittedFieldAbortsNamingTheVerb, ReadingAFilledFieldCompletes, CorruptedSnapshotFieldIsNamedWithItsSerial, EveryVerbFillsItsClassAndNothingElse}`); the same 9 names exist in all three configurations (skips are visible, not vanishing). The padding-tolerant regex `'^\s+Test\s+#[0-9]+:'` is used, as in the contract report. |
| **G4 unit** (`build-verify`, `PipeInputsTest.CorruptedSnapshotFieldIsNamedWithItsSerial`) | Passed: clean snapshot compares equal over `kDraw`'s mask; `MGPipeApplyVerifyCorruption(snapshot, GetRenderStateParameters)` -> `MGPipeVerifyInputs` false naming `GetRenderStateParameters`, whose stamp equals the fill serial; a corruption of a field outside the mask is not seen; a forwarded field cannot be corrupted. Skips visibly in build-push ("verify not compiled in") and build-linux ("push not compiled in"). |
| **G4 lane-level smoke** (`build-verify`, `MOBILEGL_PIPE_VERIFY=1 ctest -R 'DirectGLES\..*ClearThenReadPixels'`) | clean: `100% tests passed ... out of 5`, log has `MGPipe: verify armed - 63 fields, 69 verbs, fatal=1`, 0 `Fatal{`. With `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters`: `0% tests passed, 5 tests failed out of 5` (rc=8), log has `MGPipe: verify corruption armed - GetRenderStateParameters` and `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@Clear", verb=1, where=entry}` |
| **G5 unit** (`build-verify`, `PipeInputsTest.{Omitting..., ReadingAnOmittedFieldAbortsNamingTheVerb, ReadingAFilledFieldCompletes}`) | all Passed: with `SetPoisonOmission("GenerateMipmap","GetActiveTextureUnit")` the GenerateMipmap fill leaves `GetTextureUnitObject` fresh and `GetActiveTextureUnit` stale, a following DrawArrays fill leaves it stale while `GetBoundVertexArray` is fresh, and a BindImageTexture fill (same class, different verb) heals it; the forked child that draws, reads a filled field, fills GenerateMipmap, reads the sibling and then the omitted field dies with `WTERMSIG == SIGABRT` and the log carries exactly `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}`, no `@DrawArrays`, no `GetTextureUnitObject@`; the sibling child without the omission `_exit(0)`s with no `Fatal{`. In build-push (Release/INFO, no POISON) the two poison cases skip visibly and `ReadingAFilledFieldCompletes` passes. |
| `nm -D --defined-only build-verify/libMobileGL.so \| grep -c MGPipeVerifyInputs` / same on build-push | `1` / `0` - the symbol E's retrace-verify job probes exists only in the verify library |
| `EveryVerbFillsItsClassAndNothingElse` (build-verify) | Passed: after each of the 69 fills a field is fresh iff its class bit is set (sticky included); `ContextIdentity()` is the live context address |
Not run: the 40-trace retrace under verify and the `integration-verify` lane (E's files; D.3 on the integrated tree). The verify-build integration-gpu was only smoked (5 `ClearThenReadPixels` entries) - on this tree no backend reads `gPipeInputs`, so its full run would only repeat build-push's proof.
## Deviations from the brief (with reasons)
1. **Nine fill points precede a guarded expression rather than the bare call** (D7 says "after every early return the call is behind; a verb whose table entry is null never bumps"): `GL_Query.cpp` 260/300 (`if (handle && getQueryResult64 && !getQueryResult64(...))`), 289 (`outValue = (!f || f(h)) ? 1 : 0`), 522/533/645 (`handle = f ? f() : nullptr`), 774 (`supported = ... && f && f()`), `GL_Sync.cpp` 177 (`signaled = !f || !h || f(h)`) and `GL_Drawing.cpp` 1331 (`if (auto sync = backendGL.FenceSync())`, inside the `FenceSync && ClientWaitSync` guard so this one is exact). At the other eight the fill precedes the statement that holds both the null guard and the call, so a null entry bumps the serial once with nothing to read it (kQuery fills one field; no backend read happens between two fills without a fill). Chosen over a comma-expression (`(MGP_FILL(X), f(...))`) or splitting the statements because either would touch the code shape of the pull build, and G1 asks for byte identity.
2. **Compare-at-read re-reads the WHOLE field, not only the indexed element** (D8: "re-reads the same accessor with the same indices"). The hook copies the field from the live context into a scratch block through the same `CopyField` the filler uses and compares with `MGPipeInputsFieldEqual`; the indices decorate the report (`MGLOG_E` before the Fatal). A superset: a divergence in an index the backend did not ask for is still a boundary-vs-live divergence. One copy switch instead of a second 56-way index-aware switch.
3. **`MGPipeVerifyReadHook` takes the block as its first parameter** (`(const PipeInputs& self, field, i0, i1)`; the brief spells `(f, i0, i1)`): the macro expands inside member functions of any `PipeInputs` (the snapshot's accessors run in the tests), and only the live block is verified; without `self` the hook could not tell them apart. No backend code spells the hook.
4. **VecBase bitwise compare is a probe inside the generic `MGPipeFieldEqual`, not an overload** (D8 says "two overloads ... partial ordering picks these over `const T&`"). For `FloatVec4` the overload on `const VecBase<D,T,N>&` needs a derived-to-base conversion and LOSES overload resolution to the exact-match generic template before partial ordering is consulted; the `Array<T,N>` overload is exact and does win, and is emitted as the brief says. `PipeCatalogue.FloatVectorsCompareBitwise` pins the behaviour (NaN equals itself, -0.0f != 0.0f, `a == b` is IEEE and disagrees).
5. **`MGPipeInputsFieldEqual` takes `const PipeInputs&`** (the contract had `PipeInputs&`), with a const `VisitStorage` overload added; nobody outside A calls it.
6. **`MGPipeVerifyInputs` carries `__attribute__((visibility("default")))`** (not in the brief): the library is built with hidden visibility, so without it E's `nm -D build-linux/libMobileGL.so | grep -q MGPipeVerifyInputs` step (C.4 (c)) could never pass. No MGPipe symbol was in the dynamic table before; this is the only one now.
7. **Pointer corruption flips the low bits instead of nulling** (D8 says "swap a pointer for nullptr"): a pointer that is already null (an O-class field before the first fill, or a null `GetProgramForDraw`) would make the corruption invisible to the compare; a `SharedPtr` becomes an aliasing pointer with no control block. The snapshot is only ever compared, never dereferenced.
8. **`SnapshotFromGLContext` and `MGPipeFillForVerb` share `CopyField`** rather than being two copies of the copy logic; the snapshot is still its own function (the branch that survives P13) with its own file-static block.
9. **`CurrentVertexAttributeValue` compares bitwise** in `PipeInputs.cpp` with a `sizeof == 48` static_assert: it is `MG_State` storage (three scalar `Array`s, no padding), not a payload, so it gets no `PipeFields.def` row and the struct-member check does not search `Core.h`.
10. **`PipeInputsTest` is a separate unit target** (the brief allowed it "if the verify-only tests need it"): the fork cases need their own `main()` to point `MOBILEGL_LOG_FILE_PATH` at a file before anything logs, and the fake-context cases need `MobileGL_s`; `PipeCatalogueTest` stays context-free. Its five cases are `GTEST_SKIP` in a pull build and the poison/verify cases skip in a push build without those switches.
11. **A push build without POISON acknowledges `MOBILEGL_PIPE_POISON_OMIT` with `MGLOG_W_ONCE`** ("no stamp exists to omit") instead of the `MGLOG_I("poison omission armed")` line, which is emitted only where the omission can act.
12. **The sticky one-time stamp moved from an unconditional loop into the mask walk** (every class mask carries the seven sticky bits, so the result is identical).
13. `gen_pipe.py --self-test` also runs the field-list coverage check on the real inputs first (`main()` order: calls -> payloads -> coverage check -> coverage -> self-test), so a broken real list fails before the canned controls run.
## Where the tree contradicted the brief's numbers
- **B.7 `GL_Drawing.cpp:907 MemoryBarrier (TextureBarrier)`**: line 907 is the closing brace; the `memoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT | GL_FRAMEBUFFER_BARRIER_BIT);` call is at **906**. Every other B.7 line number held (the patcher checks each target line for its verb; the six XFB sites use abbreviated captured names `beginXfb`/`endXfb`/`pauseXfb`/`resumeXfb`/`deleteXfb`/`bindXfb`, `GL_Getter.cpp:1176` spells `getIntegeri(`).
- **B.7 continuation lines**: seven listed lines are the call's continuation line of a multi-line statement (`GL_Query.cpp` 261, 301, 523, 534, 646, 775; `GL_Sync.cpp` 178 for the 177 entry) - the fill goes before the statement start (260, 300, 522, 533, 645, 774, 177).
- **D8 "all 67 members of `struct RenderStateParameters`"**: the struct has **65** direct data members (`MGPipeValueTypes.h:239-387`, `MAX_VIEWPORTS` is static). `DynamicBackendParameters` has 85 (brief gives no count).
- **D4 `IsCapabilityEnabled` "37 entries, `MGPipeValueTypes.h:168-204`"**: `CapabilityInput` has **35** enumerators before `CapabilityInputCount`; the storage is sized from the enum, so nothing depends on the number.
- **C.1 "7 files change" for c5's regeneration**: only `PipeVerify.inc` changes at c5 (the field lists feed G4 alone); the contract commit already regenerated `PipeFilled.inc` and created `PipeFillPoints.inc`.
- **The integration-gpu count**: 868 entries in build-push's `integration-gpu` label (the brief's "371 + 3 new cases" is E's post-integration figure).
- **`ctest -N` regex** (`'^\s+Test #'`, C.1): as recorded in the contract report, ctest right-aligns the test number, so the padding-tolerant regex is what produces the 2334/2343 name lists.
## Unfinished / notes for the integrator
- Nothing in A's scope is unfinished. The verify library's full `integration-gpu`, the `integration-verify` lane and the 40-trace retrace under verify are D.3 items on the integrated tree (they need B/C's sites on `MGB_CTX` and E's lanes to mean anything).
- `MGB_CTX_LIVE`-guarded `RecordError` and the two `InvalidateCompileEnv` calls at backend init run before any fill; they are sticky and pass the poison from the first live fill on, as designed. If the verify lane finds an `@<none>` Fatal for any other field, the answer is a fill point at that boundary, never a sticky flag.
- The `-j 4` flake of `PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn` reproduced neither alone nor in the pull build; it is not caused by this branch but the integrator's `--rerun-failed -j 1` step stays worth keeping.
- `scan_live_accessors()` will start reporting `MGB_CTX->` reads the day B/C land; today it reports one unread row (`GetBoundTransformFeedbackName`, dead by design).
@@ -1,80 +0,0 @@
# P1 package A (p1-core) — rework round 2 (v2)
Tree `~/w7/p1-core`, branch `p1/core`, base `feat/disaggregated @ 087685d1`, contract `bf86b1ed` (tag `p1/contract`). v1 HEAD was `440d3c52` (reviewed in `core-review-v1.md`: 1 major, 8 minors). Four new commits on top; nothing pushed; working tree clean apart from the three untracked build directories. `core-v1.md` remains the record of c2..c6; this file records the rework and the full re-verification on the new HEAD.
## Commits (oldest first, on top of `440d3c52`)
| sha | subject | review item |
|---|---|---|
| `510ecd92` | `[Fix] (Impl): move the DeleteSync fill of the orphan sweep inside its null-entry guard` | minor 1 |
| `77ecde15` | `[Fix] (Pipe): refuse an eighth sticky row at compile time - the forwarded count equals the generated sticky count` | minor 8 |
| `878db2c4` | `[Fix] (Pipe): re-parse the POISON_OMIT and VERIFY knobs when their Features value changes, not once per process` | minor 4 (enabler) |
| `d9f4698d` | `[Test] (Pipe): give every PipeInputsTest process its own log file, and cover the compare-at-read arm, the three knob parsers and VERIFY_FATAL=0 with forked children` | **M1**, minors 2, 3, 4 |
`git diff --stat 440d3c52..HEAD`: 4 files, +283/-37 — `MobileGL/MG_Backend/MGPipe/PipeInputs.h` (+4), `MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp` (+1/-1), `MobileGL/MG_Impl/Pipe/PipeFill.cpp` (+15/-2), `MobileGL/MG_Test/Pipe/PipeInputsTest.cpp` (rewritten). All inside A's C.5 rows; no B/C/E file, no fixture, no generated file (`gen_pipe --check` clean), no signature that B/C/E code against changed (`MGB_CTX`, `MGP_FILL`, the 63 accessors, the knob names and every log line are as in the contract).
## What was done per review item
- **M1 (shared log path)** — `PipeInputsTest::main()` names the file `mobilegl-pipeinputs-test-<pid>.log` (`ProcessId()` = `getpid`/`_getpid`) and removes it after `RUN_ALL_TESTS`; a forked child inherits the path on purpose (it is the same file the parent reads the delta from). Proof: `ctest --test-dir build-verify -R PipeInputsTest -j 8` × 40 → **0/40 red** (was 40/40 red at v1 per the review); build-push × 10 → 0/10 red; `/tmp/mobilegl-pipeinputs-test*` leftover count after the whole suite: see the verification table.
- **Minor 1 (`GL_Sync.cpp` DeleteSync sweep)** — the fill moved inside `if (backendDeleteSync && syncObject->backendHandle)`, the shape of `GL_Query.cpp`'s `DeleteBackendQuery` sweep; D7 now holds there (a null entry never bumps). Fill count still 83 (Sync 6). Pull build unaffected (`MGP_FILL` is `((void)0)`): G1 re-proven below.
- **Minor 2 (compare-at-read arm had no falsifier)** — `PipeInputsTest.MutatedFieldIsNamedAtRead` (verify build, fork): child sets `Features.PipeVerify = true`, fills `DrawArrays`, reads `GetLineWidth()` (completes, boundary == live) and `GetRenderStateParameters()`, then `pGLContext->SetLineWidth(boundary + 1)` and reads `GetLineWidth()` again; parent asserts `SIGABRT`, the arming line `verify armed - 63 fields, 69 verbs, fatal=1`, `verify read of GetLineWidth (index 0, 0) differs from the live context`, exactly `Fatal{PipeVerifyDiffer, "GetLineWidth@DrawArrays", verb=<parent serial+1>, where=read}` and no `where=entry`. Passes in build-verify; visible skip elsewhere.
- **Minor 3 (name overclaimed)** — `OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` now loops all 63 fields after the omitted `GenerateMipmap` fill: fresh iff the bit is in `kMGPipeVerbClass[GenerateMipmap]`'s mask and the field is not `GetActiveTextureUnit`.
- **Minor 4 (knob parsers unreachable from a unit test)** — the two latches in `PipeFill.cpp` now key on the VALUE rather than "parsed once": `ParsePoisonOmissionKnob` re-parses when `Features.PipePoisonOmit` differs from the last parsed string; `ArmVerify` re-arms when `Features.PipeVerify` differs from `g_verify.Enabled` (and resets `Corrupt` on re-arm). A lane loads `Features` before its first fill, so lane behaviour is one parse/arm per process as before; a forked test child that sets `Features` after its parent filled gets its own parse. Cost: one `String ==` (two empty strings in every shipped configuration) and one `Bool ==` per fill, push/verify builds only. Five new fork cases cover: the OMIT knob arming through `Features` (arming line + the exact `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}`), a bad verb in OMIT (`Fatal{PipeVerifyBadKnob, "MOBILEGL_PIPE_POISON_OMIT=NoSuchVerb:GetActiveTextureUnit": no such verb in kMGPipeVerbNames}`; this one runs in build-push too — the parser is switch-free), the CORRUPT knob through `Features` (`verify corruption armed - GetRenderStateParameters` + `Fatal{PipeVerifyDiffer, "GetRenderStateParameters@DrawArrays", verb=N, where=entry}`), a bad CORRUPT field (`Fatal{PipeVerifyBadKnob, "MOBILEGL_PIPE_VERIFY_CORRUPT=NoSuchField": no such field in kMGPipeInputFieldNames}` and no arming line), and `PipeVerifyFatal = false` (child exits 0 after two fills, log has both `PipeVerifyDiffer` lines at consecutive serials, `fatal=0` in the arming line). The `MGLOG_W_ONCE` for OMIT in a POISON-less push build and for `MOBILEGL_PIPE_VERIFY=1` in a comparator-less build stay lane-verified only (the review's table) — they are one-shot log lines and the review already exercised them.
- **Minor 5 (`IsCapabilityEnabledIndexed` asymmetry)** — record only (below, for the integrator's `MEASUREMENTS.md`); no code change: the push arm's Fatal for a cap other than Blend/ScissorTest is D6's rule for a field the block does not carry, and no backend asks another cap.
- **Minor 6 (GIT@ stamp)** — record: all three libraries still carry `GIT@83b1656` (the stamp is a configure-time value; no reconfigure happened in this round, only incremental builds, which relinked `build-linux/libMobileGL.so` because `GL_Sync.cpp` changed). The G1 comparison below is of the relinked library.
- **Minor 7 (3 vs 7 generated files)** — record only; `gen_pipe --check` clean.
- **Minor 8 (hand-written 7)** — `static_assert(kMGPipeForwardedFieldCount == kMGPipeInputStickyFieldCount)` in `PipeInputs.h` (the generated constant is visible through `MGPipe.h` → `generated/PipeFilled.inc`).
- **Integrator note in the review (vacuous `-R '...Mipmap'` CI regex)** — not A's file; recorded under "for the integrator" below.
## Verification (WSL, `~/w7/p1-core`, HEAD `d9f4698d`; logs `~/w7/p1-core-v2-*.log`, summary `~/w7/p1-core-v2-summary.txt`)
Builds: `cmake --build {build-linux,build-push,build-verify} -j 12` all rc=0 (`~/w7/p1-core-v2-build.log`), `CCACHE_BASEDIR=/home/swung/w7`.
| command | actual output |
|---|---|
| `python3 scripts/gen_pipe.py --check` | rc=0, `gen_pipe: generated files are up to date` |
| `python3 scripts/gen_pipe.py --self-test` | rc=0, 6 controls `tripped as expected`, `positive control OK` |
| `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` | rc=0; `.text 10792579 -> 10792579 (+0, +0.000%)`; `Total 17209819 -> 17209819 (+0)`; `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed, 27060 unchanged` (`~/w7/p1-core-v2-symbol.md`) |
| `cmp -l before after \| wc -l` / `readelf -SW` diff | 34 bytes (build-id + the two build stamps, as at v1); section table identical |
| `strings ... \| grep GIT@` | `GIT@83b1656` in all three libraries (configure-time stamp; see minor 6) |
| `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only `MG_Backend/MGPipe/PipeInputs.{cpp,h}:0` appear; every catalogued count unchanged |
| `grep -c pGLContext MobileGL/MG_Backend/MGPipe/PipeInputs.{h,cpp}` | `0` `0` |
| `grep -c "MGP_FILL(" MobileGL/MG_Impl/GLImpl/*/*.cpp` | **83**: Drawing 36, Framebuffer 11, Getter 3, Program 1, Query 18, Sync 6, Texture 8 |
| `ctest --test-dir build-linux -L unit --no-tests=error -j 12` | rc=0, `100% tests passed, 0 tests failed out of 1481` |
| `ctest --test-dir build-push -L unit --no-tests=error -j 12` | rc=0, `100% tests passed, 0 tests failed out of 1481` |
| `ctest --test-dir build-verify -L unit --no-tests=error -j 12` | rc=0, `100% tests passed, 0 tests failed out of 1481` |
| `ctest -N` names vs `~/w7/p1-before-ctest-names.txt` (regex `'^\s+Test\s+#[0-9]+:'`) | 2349 names in each of the three dirs; `comm -23 before now` = **0 removed**; 15 added = the 4 `PipeCatalogue.*` of v1 + 11 `PipeInputsTest.*` (the v1 five + `MutatedFieldIsNamedAtRead`, `PoisonOmitKnobArmsTheOmission`, `BadPoisonOmitKnobIsFatalNamingTheKnob`, `VerifyCorruptKnobNamesTheFieldAtEntry`, `BadVerifyCorruptKnobIsFatalNamingTheKnob`, `VerifyFatalOffLogsTheDivergenceAndContinues`); push and verify name sets identical |
| `ctest -R PipeInputsTest -V`, skip visibility | build-linux 11 `***Skipped` ("push not compiled in"); build-push 9 skipped (5 "verify not compiled in", 4 "poison not compiled in"), `ReadingAFilledFieldCompletes` and `BadPoisonOmitKnobIsFatalNamingTheKnob` pass; build-verify 0 skipped, 11/11 pass |
| **M1 repro, fixed**: `ctest --test-dir build-verify -R PipeInputsTest -j 8` × 40 | **red=0/40** (`~/w7/p1-core-v2-stress-{1..40}.log`); v1 was 40/40 red per the review |
| `ctest --test-dir build-push -R PipeInputsTest -j 8` × 10 | red=0/10 |
| `nm -D --defined-only <lib> \| grep -c MGPipeVerifyInputs` | push 0, verify 1 |
| `ctest --test-dir build-push -L integration-gpu --no-tests=error -j 4` | PENDING-ITEST |
| leftover `/tmp/mobilegl-pipeinputs-test*` after the whole suite | PENDING-LEFTOVER |
| G4 unit (`CorruptedSnapshotFieldIsNamedWithItsSerial`, `VerifyCorruptKnobNamesTheFieldAtEntry`, `MutatedFieldIsNamedAtRead`) | pass in build-verify: entry compare names the corrupted field at its serial (block level and through the knob), and the compare-at-read arm names a mutated field `where=read` — both arms now have a falsifier |
| G5 unit (`OmittingOneField...`, `ReadingAnOmittedFieldAbortsNamingTheVerb`, `PoisonOmitKnobArmsTheOmission`, `ReadingAFilledFieldCompletes`) | pass in build-verify (the two poison-free ones also in build-push): the exact `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}`, no `@DrawArrays`, no `GetTextureUnitObject@`; the sibling exits 0 with no `Fatal{` |
| commit hygiene | 4 commits, `[Type] (Scope): ...` + blank + `- ` bullets, no Co-Authored-By/attribution, nothing pushed; `git status` clean; all LF |
Not run (unchanged from v1): the 40-trace retrace under verify and the `integration-verify` lane (E's files, D.3 on the integrated tree).
## Deviations from the brief (with reasons) — additions to v1's list 1-13
14. **The knob latches key on the value, not "once per process"** (`PipeFill.cpp`; the brief/contract said "parsed once at the first fill"). Reason: without it no unit test can reach the parsers or `Fatal{PipeVerifyBadKnob}` (review minor 4); lane behaviour is unchanged because `Features` is loaded before the first fill. Cost is one empty-string compare and one Bool compare per fill in push/verify builds only.
15. **`ArmVerify` resets `Corrupt` on re-arm** — a consequence of 14 (a stale corruption must not outlive the knob value that named it).
16. **v1 deviation 1's "nine guarded-expression sites" is now exactly nine**: the tenth, undeclared one (`GL_Sync.cpp` DeleteSync sweep) was moved inside its guard rather than added to the list, because the sibling `GL_Query.cpp` sweep already had the in-guard shape and D7 is satisfiable there at zero cost.
17. **`PipeInputsTest` has 11 cases, not the brief's/v1's 5**; the six additions are the review's minor 2 and minor 4 falsifiers. `PipeCatalogueTest` unchanged.
18. **`OmittingOneFieldForOneVerbLeavesExactlyThatFieldStale` asserts the whole class mask** (the brief's "verbatim shape" names two fields); superset of the brief's assertions.
## Where the tree contradicted the brief's numbers
Unchanged from v1 (`GL_Drawing.cpp:906` vs 907; `RenderStateParameters` 65 members not 67; `CapabilityInput` 35 not 37; c5 regenerates 1 file not 7; integration-gpu 868 not 371+3; the `ctest -N` regex needs the padded form). New in this round: none.
## Records for the integrator (not A's code)
- **`IsCapabilityEnabledIndexed` asymmetry (review minor 5)**: the push accessor is `Fatal` for a cap other than `Blend`/`ScissorTest` (`PipeInputs.h`, per D6 — the block carries only those two indexed caps), the pull arm returns `false` with `MGLOG_I` (`RenderState.cpp`). Unreachable today (`VulkanRenderer.cpp` asks `Blend` only); a `MEASUREMENTS.md` line is the right place, so that a future `glIsEnabledi`-driven backend read adds a field rather than tripping the Fatal.
- **CI negative-control regex (review section 5)**: `-R 'DirectGLES\.Verify\..*Mipmap'` matches no integration test name on this tree, and E's `PoisonOmissionScenario` name has no "Mipmap"; with `--no-tests=error` the inverted step passes vacuously. The step should also grep the lane log for `Fatal{UnmigratedPipeInput, "GetActiveTextureUnit@GenerateMipmap"}`. The unit-level G5 proof (`ReadingAnOmittedFieldAbortsNamingTheVerb`, `PoisonOmitKnobArmsTheOmission`) is independent of that regex.
- **Compare-at-read in D.3**: `MutatedFieldIsNamedAtRead` is the falsifier the review asked for; a D.3 run reporting zero `where=read` divergences is now known to have a hook that fires when a value diverges.
- The `GIT@` stamp in the three libraries will refresh at the integrator's reconfigure on `~/w7/pipe`.
## Unfinished
Nothing in A's scope. Every major and minor in `core-review-v1.md` is either fixed (M1, minors 1, 2, 3, 4, 8) or recorded above with the reason (minors 5, 6, 7 are bookkeeping/records by the reviewer's own classification).
@@ -1,62 +0,0 @@
# P1 package A (p1-core) — rework round 3 (v3)
Tree `~/w7/p1-core`, branch `p1/core`, base `feat/disaggregated @ 087685d1`, contract `bf86b1ed` (tag `p1/contract`). v2 HEAD was `d9f4698d` (reviewed in `core-review-v2.md`: 1 major, 5 minors). Four new commits on top; nothing pushed; working tree clean apart from the three untracked build directories. `core-v1.md` records c2..c6 and `core-v2.md` the round-2 rework; this file records round 3 and the full re-verification on the new HEAD.
## Commits (oldest first, on top of `d9f4698d`)
| sha | subject | review item |
|---|---|---|
| `30d72c5b` | `[Fix] (Pipe): refuse a stamp of 0 as fresh on both branches - a read before the first fill is Fatal{UnmigratedPipeInput, "<Field>@<none>"}, not default storage` | **M1** |
| `d0ff6475` | `[Fix] (Pipe): re-arm the verify comparator when any of its three knobs changes, not only when PipeVerify does` | minor 2 |
| `12b57055` | `[Docs] (Pipe): record why the forwarders carry no poison check, the InHook re-entry guard, and the Index slot no FillPoints.def row can fill` | minors 1, 3, 5 |
| `44ffafb2` | `[Test] (Pipe): pin the pre-fill window - every stamp of 0 is stale at serial 0, a read before any fill aborts naming "<none>", and the FATAL=0 child leaves through std::exit so the teardown summary is observed` | M1, minor 4 |
`git diff --stat d9f4698d..HEAD`: 6 files, +102/-15 — `scripts/gen_pipe.py` (+6/-2), `MobileGL/MG_Pipe/generated/PipeFilled.inc` (+6/-2, regenerated), `MobileGL/MG_Impl/Pipe/PipeFill.cpp` (+38/-11), `MobileGL/MG_Backend/MGPipe/PipeInputs.h` (+6), `MobileGL/MG_Test/Pipe/PipeCatalogueTest.cpp` (+6), `MobileGL/MG_Test/Pipe/PipeInputsTest.cpp` (+40/-2). All inside A's C.5 rows; no B/C/E file, no fixture. One generated file changed and is committed (`gen_pipe --check` clean). No signature B/C/E code against changed: `MGB_CTX`, `MGP_FILL`, the 63 accessors, the knob names and every log line are as in the contract; the only behavioural change visible to a backend is that a read before the first `MGP_FILL` is now the D6 Fatal instead of default storage.
## What was done per review item
- **M1 (serial-0 window read as fresh)** — `gen_pipe.py` `gen_filled` now emits, and `PipeFilled.inc` carries:
```
const Uint64 gen = state.FilledGen[index];
if (gen == 0) return false;
return kMGPipeInputFieldSticky[index] || gen == state.CurrentVerbSerial;
```
i.e. a stamp of 0 is "never filled" on both branches (the review's `FilledGen != 0 &&` in the non-sticky arm, written once for both). Proof, three layers: (1) the reviewer's own `prefill.cpp` recompiled and relinked against the rebuilt `build-verify` with the same ninja commands (`~/w7/p1-core-v3-prefill/`): `before-first-fill: serial=0 gen[GetLineWidth]=0 fresh=0 live=1`, **rc=134** (SIGABRT), log `Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}` — both plain and with `Features.PipeVerify = true` before the read (v2 was `fresh=1`, `stored=0 live=7`, rc=0, no log line); (2) `PipeCatalogue.PipeInputFieldsStartUnfilled` asserts every one of the 63 fields (sticky included) stale on a value-initialised `MGPipeFilledState` at serial 0, before its existing serial-1/serial-2 assertions; (3) the fork case `PipeInputsTest.ReadingBeforeAnyFillAbortsNamingNoVerb`: a live context with `SetLineWidth(7)`, no fill in the process (a visible `GTEST_SKIP` if another case of the same process already filled — `gtest_discover_tests` runs each case alone, so it never skips in ctest), the child reads `gPipeInputs.GetLineWidth()`; the parent asserts SIGABRT, exactly `Fatal{UnmigratedPipeInput, "GetLineWidth@<none>"}`, no `PipeVerifyDiffer`, no `verify armed`. In a verify build the child sets `Features.PipeVerify = true` first (the lane's shape). The read hook's arming-at-first-fill is left as is: `MGP_INPUT_CHECK` precedes `MGP_INPUT_VERIFY_READ` in every accessor and a verify build always defines `MOBILEGL_PIPE_POISON` (`PipeInputs.h:21-23`), so a pre-fill read is the poison's Fatal before the hook could matter; a `static_assert(MOBILEGL_PIPE_POISON, ...)` next to the hook in `PipeFill.cpp` pins that dependency so a future change to the POISON definition fails to compile rather than reopening the window. The comment at the serial bump now states the rule.
- **Minor 1 (forwarders carry no `MGP_INPUT_CHECK`)** — behaviour kept (correct per the review); the exception is now declared in the header above the seven forwarders (why: a forward is a live call, `InvalidateCompileEnv` is reached from backend initialisation before any fill and would be `Fatal{...@<none>}` on every start; the sticky stamp is consulted by no accessor and pinned only through `MGPipeInputFieldIsFresh` in tests), and recorded as deviation 19 below and in the integrator notes so P2's tracker does not "fix" it.
- **Minor 2 (`ArmVerify` re-armed only on a `PipeVerify` change)** — fixed rather than only recorded: `VerifyState` gained `CorruptKnob` (the last-parsed `PipeVerifyCorrupt` string) and the latch keys on all three values (`PipeVerify`, `PipeVerifyFatal`, `PipeVerifyCorrupt`). Lane behaviour unchanged (Features loaded before the first fill → one arm per process); cost two Bool compares and one String compare per fill in verify builds only. Deviation 14 is restated below to say exactly this; `878db2c4`'s subject is now true for all three verify knobs.
- **Minor 3 (`GetBufferBindingSlot(Index)`)** — no code change (the null-by-design Index slot and its poison Fatal are D6's rule); the `CopyField` comment now says why no `FillPoints.def` row can fill it (GLContext derives it through the bound VAO's element-buffer slot, `Core.cpp:98-106`) and that no backend reads it today. `MEASUREMENTS.md` is integrator-owned (C.5): the one-line record for both this and `IsCapabilityEnabledIndexed` is under "Records for the integrator" below.
- **Minor 4 (`VerifyFatalOffLogsTheDivergenceAndContinues` never saw the summary)** — the child now leaves through `std::exit(0)` instead of `_exit(0)`, so `VerifyState::~VerifyState` runs in the child, and the parent asserts `MGPipe: verify summary - 2 divergence(s) survived MOBILEGL_PIPE_VERIFY_FATAL=0` in the log delta. Passes in build-verify (the child's static destructors run against the forked copy of a fake context; stdio was flushed before the fork, so nothing is printed twice). The lane-level `summary=1` is still re-run below.
- **Minor 5 (hook re-reads the whole field; `InHook` guard)** — comment at the site: the per-read cost (`GetRenderStateParameters` re-copies and compares the struct, `GetProgramForDraw` re-joins the link) as a thing to keep in mind when reading the verify lane's wall time, and what `InHook` prevents (a `GetProgramForDraw`-triggered backend re-entry reaching another accessor becomes a plain load, never a second hook, never a report against a half-copied scratch). Recorded for D.3 below.
## Verification (WSL, `~/w7/p1-core`, HEAD `44ffafb2`; logs `~/w7/p1-core-v3-*.log`, summary `~/w7/p1-core-v3-summary.txt`, build/prefill log `~/w7/p1-core-v3-build.log`)
Builds: `cmake --build {build-linux,build-push,build-verify} -j 12` after the patch, all rc=0 (`CCACHE_BASEDIR=/home/swung/w7`); the verification pass re-ran them (`ninja: no work to do` × 3).
VERIFICATION_TABLE
Not run (unchanged from v1/v2): the 40-trace retrace under verify and the `integration-verify` lane (E's files, D.3 on the integrated tree). Not built per intermediate commit: the split of `PipeFill.cpp` between `30d72c5b` / `d0ff6475` / `12b57055` was checked hunk by hunk (each commit's hunks are self-contained: the static_assert and comments in A, the `CorruptKnob` member and its uses together in B, comments only in C); only the final tree was compiled.
## Deviations from the brief (with reasons) — additions/restatements to v1's 1-13 and v2's 14-18
14. **(restated)** The knob latches key on the value, not "once per process": `ParsePoisonOmissionKnob` on `Features.PipePoisonOmit`; `ArmVerify` on all three of `Features.PipeVerify`, `PipeVerifyFatal`, `PipeVerifyCorrupt` (as of `d0ff6475`; v2 keyed on `PipeVerify` alone and latched the other two). Reason and cost as in v2.
19. **The seven F-class forwarders carry no `MGP_INPUT_CHECK` / `MGP_INPUT_VERIFY_READ`** (D4: "every accessor body"). Reason: a forward is a live call with no stored value; `InvalidateCompileEnv` is reached from backend initialisation (`BackendObject_DirectVulkan.cpp:388-390,786-788`) before any verb fills, where a check would be `Fatal{InvalidateCompileEnv@<none>}` on every start. Consequence: the sticky stamp (`FilledGen = 1` on the first live fill) and the `Sticky → FilledGen != 0` branch are consulted by no accessor; they are pinned only by `PipeCatalogue.PipeInputFieldsStartUnfilled` / `StickyFieldsAreExactlyTheSeven` and `PipeInputsTest.OmittingOneField...` / `EveryVerbFills...` through `MGPipeInputFieldIsFresh` directly. Declared in the header (`12b57055`).
20. **`MGPipeInputFieldIsFresh` refuses a stamp of 0 on the sticky branch too** (D6 words the sticky branch as `FilledGen != 0`, which it still is; the non-sticky arm gained the same guard). No semantic change to the sticky branch.
21. **The read hook still arms at the first fill** (D8: "Arming, once, at the first fill under verify" — unchanged); the pre-fill window is covered by the poison, and `static_assert(MOBILEGL_PIPE_POISON)` in the verify block makes that dependency a build-time fact. The review's alternative (arm inside the hook when `!Parsed`) was not taken: in every build that compiles the hook the poison is on, so it would be dead code.
22. **`PipeInputsTest` has 12 cases** (v2: 11; brief: 5): `ReadingBeforeAnyFillAbortsNamingNoVerb` added.
23. **`VerifyFatalOffLogsTheDivergenceAndContinues`'s child exits via `std::exit(0)`**, the one child in the file that runs static destructors on purpose (to observe the summary line); every other child still `_exit`s.
## Where the tree contradicted the brief's numbers
Unchanged from v1/v2 (`GL_Drawing.cpp:906` vs 907; `RenderStateParameters` 65 members not 67; `CapabilityInput` 35 not 37; c5 regenerates 1 file not 7; integration-gpu 868 entries; the `ctest -N` regex needs the padded form). New in this round: none. One brief sentence that the tree did not honour before this round and does now: D6's "starts at 1 so `FilledGen == 0` means never filled" — true of the stamp values since c2, true of the read predicate only since `30d72c5b`.
## Records for the integrator (not A's code)
- **`MEASUREMENTS.md` one-liner for the two accessors where "add the row" does not apply**: `GetBufferBindingSlot(BufferTarget::Index)` (polymorphic on `GLContext` — the bound VAO's element-buffer slot, `Core.cpp:98-106`; null in the block by design) and `IsCapabilityEnabledIndexed(cap ∉ {Blend, ScissorTest})` (the block carries only those two indexed caps) are push-arm `Fatal{UnmigratedPipeInput, ...}` that no `FillPoints.def` row can repair; both unreachable today (every backend slot read is DrawIndirect/DispatchIndirect/Parameter/PixelPack; `VulkanRenderer.cpp` asks `Blend` only). A future backend read of either adds a field, not a row.
- **D.3 gains a failure mode** (the review's section 5, now real): any backend read reached from EGL/init before the first verb is `Fatal{UnmigratedPipeInput, "<Field>@<none>"}` in the verify lane. Per E's risk table the answer is a fill point at that boundary (an EGL virtual), never a sticky flag or a `FillPoints.def` row. The scout catalogue finds no such read on this tree (the two out-of-verb pulls are the forwarded, unchecked `InvalidateCompileEnv` calls), so no lane is expected to trip today.
- **D.3 wall-time reading**: the compare-at-read hook re-reads and compares the whole field per backend accessor call (`GetRenderStateParameters` = a struct copy + 65-member compare per read; `GetProgramForDraw` = a link join per read); within the 5-10× verify budget by design, but the verify lane's time is not the poison's cost.
- **The forwarders' missing check is deliberate** (deviation 19): P2's tracker should not add one.
- The v2 records stand: the vacuous `-R 'DirectGLES\.Verify\..*Mipmap'` CI regex (grep the lane log for the exact Fatal instead), the `GIT@` stamp refresh at reconfigure.
## Unfinished
Nothing in A's scope. M1 is fixed with the three proofs above; minors 1, 3, 5 are recorded in code comments and here (the `MEASUREMENTS.md` line is the integrator's file); minors 2 and 4 are fixed in code and covered by tests.
@@ -1,70 +0,0 @@
# P1 package B — p1-espryt — adversarial review v1
Reviewed: tree `~/w7/p1-espryt`, branch `p1/espryt`, HEAD `f4ea84c5` (one commit on top of `p1/contract` = `bf86b1ed`). Implementer's result: `p1-results/espryt-v1.md`. Brief: `BRIEF-P1.md` A, B (D1–D13), C.2, C.5, D, E.
Stance: I tried to refute correctness, completeness and the G1/G2/G7 gate claims. Every number below was re-derived by me; logs under `~/w7/p1-espryt-review-*` and `~/w7/p1-espryt-review-int-*`; scripts under `scratchpad/wf4/espryt-review/`. The tree was left exactly as found (`git status`: only `build-linux/`, `build-push/` untracked; `git diff --stat` empty). The one throwaway worktree I created (`~/w7/p1-espryt-review-int`, branch `tmp/p1-espryt-review-int`) was removed afterwards.
## Verdict
**Approved — zero majors.** The package is a textually pure conversion of exactly the 122 lines the brief assigns to it, in exactly the four files it owns, and the pull build is symbol- and size-identical to the `087685d1` baseline. The one deviation from D9 (the hoisted `auto& live = *MGB_CTX;` in the fb-slot cache refill) is declared, justified with a bisect, semantically equivalent in both arms, and is the shape that yields `.text +0` where the literal D9 text does not.
## What I re-ran (commands + observed output)
| check | command (in `~/w7/p1-espryt`) | observed |
|---|---|---|
| diff scope | `git diff --stat p1/contract..HEAD` | 4 files, +129/−124: `DirectGLES.cpp` 190, `Managers.cpp` 47, `MultiDraw.cpp` 11, `Utils.cpp` 5 — nothing outside C.5's B row; no fixture touched |
| textual purity of every changed line | `scratchpad/wf4/espryt-review/diffcheck.py` over `git diff p1/contract..HEAD` (pairs `-`/`+` lines per hunk; a pair is "pure" iff `+` with `MGB_CTX->`→`MG_State::pGLContext->` equals `-`) | 86 hunks, **112 pure arrow substitutions**; the only non-pure pairs are the 4 include insertions, the `:143` decltype line (an arrow inside `decltype`, pure by the same rule), the 143–155 block, and the 8 Managers.cpp D9 lines (`if (MG_State::pGLContext)`→`if (MGB_CTX_LIVE)` ×3, `&& MG_State::pGLContext &&`→`&& MGB_CTX_LIVE &&`, `if (MG_State::pGLContext && `→`if (MGB_CTX_LIVE && `, `!= nullptr`→`MGB_CTX_LIVE` ×3). No accessor renamed, no argument changed, no evaluation order moved, no `const&` binding lost. |
| the touched old-line set equals the brief's site list | `scratchpad/wf4/espryt-review/linecheck.py` (old-side line numbers of every `-` line vs C.2's table + D9's 144/147/150) | `DirectGLES.cpp` removed 94 = brief 94, `Managers.cpp` 23 = 23, `MultiDraw.cpp` 5 = 5, `Utils.cpp` 2 = 2; `removed-not-in-brief []` and `brief-not-removed []` for all four |
| G2 (this package's share) | `grep -rc "pGLContext" MobileGL/MG_Backend/DirectGLES \| grep -v ":0$"` | empty (all 10 files 0) |
| no site outside ownership touched | `grep -rc "pGLContext" MobileGL/MG_Backend \| sort \| diff ~/w7/p1-before-pglcontext.txt -` | only the four DirectGLES lines change (92/23/5/2 → 0) plus the two contract-created `MGPipe/PipeInputs.{h,cpp}:0` rows; every DirectVulkan count identical to the baseline |
| MGB_CTX counts | per-file `grep -o` | arrows 91/15/5/2 (brief: 91 15 5 2); `MGB_CTX` tokens DirectGLES.cpp 93 (= 91 + `*MGB_CTX` + `MGB_CTX_IDENTITY`), `MGB_CTX_LIVE` Managers.cpp 8 (brief: 8), `MGB_CTX_IDENTITY` 1 |
| include placement (D3) | `grep -n` | `DirectGLES.cpp:18` Core.h → `:19` switch; `MultiDraw.cpp:11`→`:12`; `Utils.cpp:19`→`:20`; `Managers.cpp:9` `"Managers.h"` → `:10` switch (Managers.cpp does not include Core.h directly, per D3) |
| CRLF | `grep -c $'\r'` ×4 | 0 0 0 0 |
| gen_pipe | `python3 scripts/gen_pipe.py --check` | rc=0, "generated files are up to date" (63 fields, 7 sticky, 69 verbs, 9 classes) |
| pull build | `cmake --build build-linux -j 12` | rc=0 |
| **G1** | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --json … --markdown …` | `.text 10792579 -> 10792579 (+0)`, `.data/.bss/.rodata +0`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 normalised names, 27060 unchanged` |
| nm set | `nm --defined-only … \| awk '{print $NF}' \| sort` before/after, `diff` | 0 lines |
| raw `.text` bytes | `objcopy -O binary --only-section=.text` ×2, `cmp -l` | sizes equal; **39 bytes differ**, deltas +1 (23 bytes) and +2 (16 bytes); mapped through `nm -S` to exactly the 6 functions the implementer names (`SyncMipmapsToBackend` 9, `SyncBuiltinSamplerToBackend` 8, `CopyTexSubImage2D` 7, `SyncTextureParamsToBackend` 6, `CopyTexImage2D` 5, `GetTexImage` 4) — the `__LINE__` immediates in `ErrorInfo` constructions shifted by the inserted include (+1) and, after `:148`, the hoisted line (+2). Sizes unchanged, so G1 as decoded ("`.text` byte delta of zero" = size) holds; see minor 1 for the wording |
| push build | `cmake --build build-push -j 12` | rc=0; `nm -C …/DirectGLES.cpp.o`: `gPipeInputs` refs 2, `pGLContext` 0 |
| **push arm at DEBUG level** (never built by anyone: both INFO builds compile out `DirectGLES.cpp:4200-…`, which holds the converted site at `:4217`) | `scratchpad/wf4/espryt-review/10_debugpush.sh`: the four TUs' `build-push/compile_commands.json` commands with `-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_DEBUG` and `-fsyntax-only` (this also arms `MOBILEGL_PIPE_POISON` and the `MGP_INPUT_CHECK` bodies) | `DirectGLES.cpp rc=0 errors=0`, `Managers.cpp 0/0`, `MultiDraw.cpp 0/0`, `Utils.cpp 0/0` (twice each: static + shared targets) |
| G7 names | width-tolerant regex `grep -E '^\s*Test\s+#[0-9]+:'` | 2334 = 2334, `comm -23` empty, `comm -13` empty. The brief's own regex `'^\s+Test #'` yields **1335** on this build (confirmed: ctest pads to `Test #1:` above 999 tests) — the implementer's "tree vs brief" item 2 is correct and the integrator's D.1 step needs the tolerant form |
| unit, pull | `ctest --test-dir build-linux -L unit --no-tests=error -j 8` | rc=0, 1466 (3 skipped by design) |
| unit, push (contract tree) | `ctest --test-dir build-push -L unit --no-tests=error -j 8` | rc=8, 18 failed / 1466 — the same 18 the implementer lists (contract filler copies nothing; expected per C.2 step 4) |
| integration lane, pull | `ctest --test-dir build-linux -L integration-gpu -R '^DirectGLES\.' --no-tests=error -j 4` | **441/441 passed, rc=0**. The implementer's run had 2 failures (`UnlocatedIoBlockScenario.TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn`, `PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn`); on the identical binary they pass here, which is consistent with the "shared per-lane log file under `-j 4`" flake they diagnosed (`UnlocatedIoBlockScenario.cpp:362-371`: the case trusts bytes after `LibraryLogSize()` in a file every process of the lane appends to). Pre-existing, not this package's |
| D10 / PipeStats constraints | `git diff -U0 p1/contract..HEAD \| grep -c "SyncPersistentMappedRange\|SyncGpuWrites"` / `\| grep -c AddCalls` | 0 / 0 |
| commit hygiene | `git log -1 --format=%B`; `git branch -r \| grep p1/` | subject exactly the C.2 step-5 text, blank line, four `- ` bullets; no attribution lines; no remote branch (not pushed) |
## Integrated-tree check (throwaway, `p1/core` HEAD `83b16561` = c1…c5 incl. the 83 fill points + `f4ea84c5` cherry-picked as `4c92f727`)
INTEGRATED_SECTION_PLACEHOLDER
## Deviations from the D-decisions, audited
1. **D9 `:150` shape** — declared. Committed: `auto& live = *MGB_CTX;` once, then `&live.GetFramebufferBindingSlot(i)` in the loop. Pull arm: `*MG_State::pGLContext` is `UniquePtr::operator*` → `GLContext&`, the same object the old `ctx` pointed at; behaviour when no context is live is identical to before (both the old `ctx->…` on a null `get()` and the new `*pGLContext` on a null pointer are only reached when the cached identity differs, i.e. never in that state). Push arm: `*(&gPipeInputs)` → `PipeInputs&`, whose `GetFramebufferBindingSlot` (`PipeInputs.h:494-500`) returns the live slot reference the filler stored; the identity key stays `MGB_CTX_IDENTITY` = `gPipeInputs.ContextIdentity()` so a recreated context still invalidates. I verified the committed shape is `.text +0` (above); I did **not** independently rebuild the literal D9 variant — the implementer's `~/w7/p1-espryt-bisect.txt` (v4/v5: +32, `SyncCurrentProgram` 1494→1510, `ForceBindCurrentFBO` 181→190, `BlitNamedFramebuffer` 592→593; v8 = committed: 189968 = baseline) is internally consistent and the TBAA explanation is the standard one for a pointer store next to a `unique_ptr` load. The deviation is the better reading of D9's own "if `.text` moves, this is the first suspect".
2. **`:143` keeps `std::remove_reference_t<…>`** — D9 abbreviated it; the alias must be the slot type, not a reference. Correct.
3. **Push lane not demonstrated in the package's own tree** — correct at the time (c3 had not landed); now demonstrated by me on the integrated throwaway tree, see above.
Undeclared deviations found: **none**.
## Majors
None.
## Minors
1. **Wording "pull build byte-identical" (commit subject, brief-dictated) vs. 39 differing `.text` bytes.** G1 is size/symbol based and holds (0/0/0/0, `.text +0`), and the body of the commit discloses the `__LINE__` immediates, but the integrator's `MEASUREMENTS.md` P1 attribution line ("expected: 0 bytes; 58 lines converted, none code-bearing", brief D.5) should say "sizes identical; 39 bytes of `__LINE__` immediates in 6 `DirectGLES.cpp` functions from the inserted include (+1) and the hoisted cache line (+2)" rather than "0 bytes". Integrator doc item, not a code change.
2. **Push-build unit tests that drive backend helpers directly** (`DirectGLESSanity.*`, `DirectGLESTextureSync.*`, `FramebufferTest.*`, `TextureTest.*` families, 18 on the contract tree) read `gPipeInputs` with no verb ever filled — they bypass `GLFunctionsTable` and therefore every `MGP_FILL`. Whatever their state on the integrated tree (recorded in the section above), this is a design consequence of D7's placement rule, not of this package's textual conversion; the implementer flagged it for A/integrator (D.2 expects "unit + integration-gpu green" under push). Recording so it is tracked.
3. **Brief D.1 / C `ctest -N` regex** (`'^\s+Test #'`) undercounts to 1335 on a 2334-test build; the implementer's tolerant form (`'^\s*Test\s+#[0-9]+:'`) is the one the integrator must use, or `comm -23` reports 999 phantom losses.
4. The two `ActuallyArmedWhenTheEnvironmentPinsItOn` itests are a `-j 4` flake in package E's file set (`MG_IntegrationTest/**`); the ambient lanes share one `MOBILEGL_LOG_FILE_PATH` per lane. Not this package's; noted for E/integrator.
## Where the tree contradicted the brief
- Only the `ctest -N` regex (minor 3). Every site count, line number and per-file total in C.2 matched the tree exactly (line check above).
## Hunted for and not found
- A converted site with a different accessor / dropped null check / moved evaluation / lost `const&`: none (112 pure substitutions + 1 decltype; the only `GetRenderStateParameters()` binding that is not `const auto&` is `DirectGLES.cpp:4158`'s `const FloatVec4& cc = …ClearColor`, a member reference — no 1168-byte copy).
- A site left on `pGLContext` behind a macro: the four files have 0 tokens; the one converted site under a preprocessor conditional (`DirectGLES.cpp:4217`, `LOG_LEVEL_DEBUG` block) compiles under the push arm at DEBUG.
- A site touched in a shared or unowned file: none (diff is the four owned files).
- Reformatting beyond touched lines: none (diff = 122 site lines + 4 includes + 1 hoisted line + the `:150` `&ctx->`→`&live.` line).
- A `PipeStats` literal or D10 sync site on a changed line: none.
@@ -1,79 +0,0 @@
# P1 package B — p1-espryt — result v1
Tree `~/w7/p1-espryt`, branch `p1/espryt`, branched from tag `p1/contract` (`bf86b1ed`). Working tree clean after the commit. Nothing pushed.
## Commits (p1/contract..p1/espryt)
| hash | subject |
|---|---|
| `f4ea84c5f786ff556e8c41508072ad3782bee3ca` | `[Refactor] (Espryt): route every frontend read through MGB_CTX - 113 arrow sites sed'd, 9 non-arrow lines converted (the fb-slot cache keys on MGB_CTX_IDENTITY); pull build byte-identical` |
Files: `MobileGL/MG_Backend/DirectGLES/{DirectGLES.cpp, Managers.cpp, MultiDraw.cpp, Utils.cpp}` only (4 files, +129/−124). No other file touched; no fixture binary touched.
## What was done (brief C.2 steps 1–5)
1. `#include <MG_Pipe/PipeInputsSwitch.h>` inserted per D3: `DirectGLES.cpp` after its `Core.h` include (old :18 → new :19), `MultiDraw.cpp` after :11 (→ :12), `Utils.cpp` after :19 (→ :20), `Managers.cpp` after its `Managers.h` include (old :9 → new :10). Includes were inserted after all line-number-addressed edits, so every brief line number was applied against the unshifted file.
2. Mechanical `MG_State::pGLContext->` → `MGB_CTX->` over the four files: 91 + 15 + 5 + 2 = **113 occurrences on 113 lines**, matching the brief's per-file arrow table exactly (verified line-for-line before the sed: DirectGLES.cpp 143…9510, Managers.cpp 3645…8751, MultiDraw.cpp 45,51,52,87,95, Utils.cpp 2297,2301).
3. The 9 non-arrow lines per D9: `Managers.cpp` 3644, 3844, 8750 (`if (MG_State::pGLContext) {` → `if (MGB_CTX_LIVE) {`), 3773 (`&& MG_State::pGLContext &&` → `&& MGB_CTX_LIVE &&`), 4735 (`if (MG_State::pGLContext && ` → `if (MGB_CTX_LIVE && `), 7192/7200/7203 (`MG_State::pGLContext != nullptr` → `MGB_CTX_LIVE`; their `?` arms 7193/7201/7204 took the sed); `DirectGLES.cpp` 143–155 block: `:144` → `static const void* g_fbSlotCacheContext = nullptr;`, `:147` → `const void* ctx = MGB_CTX_IDENTITY;`, `:150` → see deviation 1.
4. Pull build, symbol report, push build, lanes — below.
5. One commit, message per brief plus `- ` bullets.
## Verification (WSL, `~/w7/p1-espryt`; logs under `~/w7/p1-espryt-*`)
| command | result |
|---|---|
| `grep -rc "pGLContext" MobileGL/MG_Backend/DirectGLES \| grep -v ":0$"` | **empty** (all 10 files report 0) |
| `for f in …; do grep -o 'MGB_CTX->' … \| wc -l; done` | DirectGLES.cpp **91**, Managers.cpp **15**, MultiDraw.cpp **5**, Utils.cpp **2** (the brief's "91 15 5 2"; `MGB_CTX` tokens total in DirectGLES.cpp = 93 = 91 arrows + `*MGB_CTX` in the cache refill + `MGB_CTX_IDENTITY`) |
| `grep -c "MGB_CTX_LIVE" MobileGL/MG_Backend/DirectGLES/Managers.cpp` | **8** |
| `grep -c "MGB_CTX_IDENTITY" MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp` | 1 |
| `cmake --build build-linux -j 12` | rc=0 (`[49/49] Linking … MobileGLIntegrationTest`) |
| `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --json … --markdown ~/w7/p1-espryt-symbol.md` | `.text 10792579 -> 10792579 (+0)`, `.data/.bss/.rodata +0`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 normalised names, 27060 unchanged` — **G1: 0/0/0/0, .text +0** |
| `nm --defined-only` name set, before vs after | diff **empty** (27799 = 27799) |
| raw `.text` byte compare (`objcopy -O binary --only-section=.text` + `cmp -l`) | 39 bytes differ in 6 DirectGLES.cpp functions (`SyncMipmapsToBackend`, `SyncBuiltinSamplerToBackend`, `SyncTextureParamsToBackend`, `CopyTexSubImage2D`, `CopyTexImage2D`, `GetTexImage`), function sizes unchanged. Disassembly diff: every difference is a `movl $0xNNN,0x8(%rax)` immediate = a `__LINE__` value stored into an `ErrorInfo` (e.g. `0xf0e`→`0xf0f` = 3854→3855): 23 bytes are +1 (lines after the inserted include), 16 are +2 (DirectGLES.cpp lines after the include *and* the hoisted `auto& live` line). The object built with **only** the include already shows these; the seds and the D9 rewrites add no byte. Not a gate item (G1 is symbol/size based), recorded for completeness. |
| `ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure -j 8` | rc=0, **100% tests passed, 0 failed out of 1466** (on the final `.so`) |
| `ctest --test-dir build-linux -L integration-gpu -R '^DirectGLES\.' --no-tests=error --output-on-failure -j 4` | 441 tests; **439 passed, 2 failed**: `DirectGLES.UnlocatedIoBlocks.UnlocatedIoBlockScenario.TheEmulationIsActuallyArmedWhenTheEnvironmentPinsItOn`, `DirectGLES.PointSizeDemotion.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn` — both are "text appended to the lane's `MOBILEGL_LOG_FILE_PATH` log after the case started" assertions (`UnlocatedIoBlockScenario.cpp:516`, `PointSizeDemotionScenario.cpp:512`). Re-run of the 10 `ActuallyArmedWhenTheEnvironmentPinsItOn` cases: `-j 1` **10/10 pass**, `-j 4` **10/10 pass**. The same two cases also failed identically in the push lane of a tree that did **not** contain this commit (bare `p1/core` c2, see below), and the pull `.so` is symbol- and size-identical to the baseline, so this is a pre-existing parallel-lane flake of the shared per-lane log file, not a regression of this package. Lane label used: `integration-gpu` (the tree has only `integration-gpu` and `unit`; the `-R '^DirectGLES\.'` selects the DirectGLES entries). |
| `ctest -N` name diff vs `~/w7/p1-before-ctest-names.txt` | `comm -23` **empty**, `comm -13` **empty**, 2334 = 2334 — see "tree vs brief" item 2 for the regex. |
| `cmake --build build-push -j 12` (`MOBILEGL_PIPE_PUSH=ON`) | rc=0 (`[55/55] Linking …`); the DirectGLES.cpp object references `gPipeInputs` and no `pGLContext` (`nm`: 0 / 2) |
| `ctest --test-dir build-push -L unit` on the **contract** tree | 18 failed / 1466 (expected on the contract tree: the c1 filler copies nothing, C.2 step 4 note) |
| `ctest --test-dir build-push -L integration-gpu -R '^DirectGLES\..*ClearThenReadPixels'` on the contract tree | 0/5 pass, as the brief predicts for the contract tree |
| D10: `git diff -U0 … \| grep -c "SyncPersistentMappedRange\|SyncGpuWrites"` on changed lines | **0** — the 8 SPMR + 3 SGW Espryt sites are untouched (they are not on any converted line) |
| `git diff -U0 … \| grep -c AddCalls` on changed lines | 0 — `PipeStats` literals untouched |
| CRLF check on the four files | none |
### Push-build proof against the real filler (throwaway, removed afterwards)
Because `p1/core` had progressed to c2 (`3aa4d8af`, the per-class filler) while this package ran, `p1/espryt` was cherry-picked onto `p1/core@3aa4d8af` in a throwaway worktree (`~/w7/p1-espryt-onto-core`, branch `tmp/p1-espryt-onto-core`; **both deleted after the run**, logs kept as `~/w7/p1-espryt-onto-core-*.log`):
- cherry-pick applied cleanly (disjoint files); `cmake --build build-push` rc=0.
- `ctest -L unit` (push): 18 failed / 1466 — the same 18 as on the contract tree: `DirectGLESSanity.{BindsAMultisampleTextureDespiteTheDefaultMipmapFilter, BindingZeroClearsPreviousNativeTextureBinding}`, `NegativeApiErrorsTest.MultiBindTextures{RejectsBadRangesAndNames, BindsToTheTexturesOwnTarget}` (aborted), `FramebufferTest.{DrawIntoAWidenedDrawBuffer…, ClearIntoAWidenedDrawBuffer…, ApplicationAlphaMaskOff…, DualSourceBlendFactorsReachTheDriver…, DualSourceBlendIsDeclined…, DualSourceFactorsAreDeclinedEvenWithBlendingDisabled, DualSourceFactorsWithBlendingDisabledStillReachACapableDriver}`, `TextureTest.StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams` (aborted), + 6 more of the same families (full list in `~/w7/p1-espryt-onto-core-unit.log`).
- `ctest -L integration-gpu -R '^DirectGLES\.'` (push): **68 passed / 373 failed** (SegFault or `Subprocess aborted`). A single case run with `MOBILEGL_LOG_FILE_PATH` shows the reason: `MGPipe: Fatal{UnmigratedPipeInput, "GetImageTextureBinding@<none>"}` — the verb name is `<none>`, i.e. **no verb was ever filled in the process**. Cause: `p1/core` at c2 (and at its current head `275dd3ed`, c4) carries **0** `MGP_FILL(` statements in `MobileGL/MG_Impl/GLImpl` (c3, the 83 fill points, has not landed yet). So the push lane cannot be green on any tree yet; this is not a package-B defect and not a fill-table gap (those would name a real verb). **The brief's "rerun after rebasing onto p1/core c2+ and it must be green" has to be re-run by the integrator once c3 is on `p1/core`** (D.2).
- The 18 push-build unit failures are the same phenomenon in the unit binaries (backend helpers driven directly by the tests, never through a fill point); with c3 landed most should turn green, but tests that call backend entry points without going through `GLFunctionsTable` will still read `@<none>` and are an A/integrator item (recorded here so it is not a surprise in D.2).
## Deviations from the brief (with reason)
1. **D9 line 150 (`DirectGLES.cpp` cache refill loop).** D9 spells `g_fbSlotCache[i] = &MGB_CTX->GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));`. Built that way the pull build is **not** byte-identical: `.text +32`, 3 resized symbols (`PrgramImpl::SyncCurrentProgram` 1494→1510, `ForceBindCurrentFBO` 181→190, `BlitNamedFramebuffer` 592→593) — exactly the "first suspect" D9 names. Bisected by rebuilding only the DirectGLES.cpp object per cumulative variant (`~/w7/p1-espryt-bisect.txt`): include alone 0, +91 arrow seds 0, +`const void*` cache (144) 0, **+line 150 via `MGB_CTX->` = +32**, +line 147 `MGB_CTX_IDENTITY` 0. Reason: `g_fbSlotCache[i] = …` stores a pointer, and under clang's TBAA any pointer store may alias the `unique_ptr`'s own pointer, so `MGB_CTX->` inside the loop re-loads `pGLContext` per iteration (the original read through the local `ctx`). Committed shape hoists the dereference once:
```cpp
const void* ctx = MGB_CTX_IDENTITY;
if (ctx != g_fbSlotCacheContext) {
auto& live = *MGB_CTX;
for (SizeT i = 0; i < g_fbSlotCache.size(); ++i) {
g_fbSlotCache[i] = &live.GetFramebufferBindingSlot(static_cast<FramebufferTarget>(i));
}
g_fbSlotCacheContext = ctx;
}
```
Measured: `.text +0`, 0/0/0/0. Semantics identical in both arms (pull: `GLContext&`; push: `PipeInputs&`, the identity compare still uses `MGB_CTX_IDENTITY`, so a recreated context invalidates the cache under push too). One extra line inside the touched 143–155 block; no `pGLContext` token. If the integrator prefers the literal D9 text, the cost is the attributed +32 above (both variants' numbers are in the bisect file) — the "no reformatting beyond the touched lines" constraint is respected either way.
2. **Line 143** keeps `std::remove_reference_t<decltype(MGB_CTX->GetFramebufferBindingSlot(FramebufferTarget::Draw))>` (D9 abbreviates it as `decltype(MGB_CTX->…)`); it is the sed result and the `remove_reference_t` is required for the alias to be the slot type.
3. **Push-lane green not demonstrated** (see above): impossible on any tree until c3 lands; the brief's step-4 expectation of a `Fatal{UnmigratedPipeInput}` on the contract tree was reproduced instead.
## Where the tree contradicted the brief's numbers
1. None for the site counts: DirectGLES.cpp 92 lines / 91 arrows, Managers.cpp 23 / 15, MultiDraw.cpp 5 / 5, Utils.cpp 2 / 2, 9 non-arrow lines at exactly the listed line numbers; `~/w7/p1-before-pglcontext.txt` per-file numbers match.
2. **Baseline `ctest -N` name file vs the brief's own extraction regex.** `~/w7/p1-before-ctest-names.txt` holds 2334 names; the brief's pipeline `grep -E '^\s+Test #'` applied to the same build lists only 1335 (both in `~/w7/pipe/build-linux` today and in this tree) because with 2334 tests ctest pads the number to four columns (`Test #1:` … `Test #1000:`), so the single-space regex matches only #1000–#2334. Width-tolerant form used here: `ctest --test-dir build-linux -N | grep -E '^\s*Test\s+#[0-9]+:' | sed -E 's/^\s*Test\s+#[0-9]+: //' | sort` → 2334 names, `comm` both ways empty. The integrator's D.1 `comm -23` step needs the same regex or it reports 999 "lost" names (all of `TextureTest.*`, `ProgramUtilTest.*`, … i.e. tests #1–#999).
3. `SyncCurrentProgram` (the symbol the literal D9 line 150 grows) lives in `DirectGLES.cpp` (`PrgramImpl::SyncCurrentProgram`), not `Managers.cpp`; the brief's `Managers.cpp:7193-7204` reference in D7 is `AttachPassthroughTessControlStage`'s ternaries (converted per D9, byte-neutral).
4. `p1/core` moved during this package: c2 `3aa4d8af`, then c4 `275dd3ed` (comparator) — c3 (83 fill points) not yet present at the time of writing.
## Unfinished
- Push-lane green (`ctest --test-dir build-push -L integration-gpu -R '^DirectGLES\.'`) — blocked on `p1/core` c3; re-run at integration (D.2). Everything else in the package's verification list is done.
- The two `ActuallyArmedWhenTheEnvironmentPinsItOn` itests are a pre-existing `-j 4` flake (pass serially and in isolation, fail identically without this commit); not fixed here (`MG_IntegrationTest/**` is package E's file set).
@@ -1,260 +0,0 @@
# Adversarial review — package `fix-core` (P1 lane findings F1 + F2)
Reviewer ran everything independently in WSL `~/w7/p1-fix-core` (branch `p1/fix-core`, HEAD
`9bd6d394`, three commits on top of `feat/disaggregated@97b997d5`). Tree left exactly as found
(`git status --porcelain` clean apart from the three untracked build directories, HEAD unchanged).
**Verdict: approved — 0 majors, 7 minors.** Every claim in `fix-core-v1.md` reproduced. The
central attack ("zero divergences was bought by disabling the read hook / by silencing the
comparator") is refuted by direct experiment below.
---
## 1. What was re-run, and what came back
Diff under review (`git diff 97b997d5..HEAD`, 371 lines, 6 files, nothing under `MG_Backend/`,
nothing under `tools/trace_replay/fixtures/`):
```
MobileGL/MG_Impl/Pipe/PipeFill.cpp | 28 ++
MobileGL/MG_Pipe/FillPoints.def | 31 +-
MobileGL/MG_Pipe/PipeMutation.h | 42 ++ (new)
MobileGL/MG_Pipe/generated/PipeFillPoints.inc | 12 +-
MobileGL/MG_State/GLState/TextureState/TextureState.h | 31 +-
MobileGL/MG_Test/Pipe/PipeInputsTest.cpp | 105 ++
```
| check | command | observed |
|---|---|---|
| G1 pull build | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0` | `.text 10792579 -> 10792579 (+0, +0.000%)`, `.data/.bss/.rodata +0`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`; file size identical (19100448 both sides) |
| generator | `python3 scripts/gen_pipe.py --check` | `71 calls, 69 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `0 UNMAPPED`, `generated files are up to date`; `git status` clean afterwards |
| generator self-test | `python3 scripts/gen_pipe.py --self-test` | `6 negative-control trip(s), positive control OK` |
| unit, pull | `ctest --test-dir build-linux -L unit --no-tests=error -j 8` | `100% tests passed, 0 tests failed out of 1485` |
| unit, push | `ctest --test-dir build-push -L unit ...` | `11 failed out of 1485` — exactly the F3 list owned by `p1/fix-tests` (`DirectGLESSanity` ×2, `DirectGLESTextureSync` ×1, `FramebufferTest` ×7, `TextureTest` ×1) |
| unit, verify | `ctest --test-dir build-verify -L unit ...` | same 11, same names |
| `ctest -N` | `comm` vs `~/w7/p1-before-ctest-names.txt` | 0 removed, 29 added; vs `~/w7/pipe/build-linux` names: 0 removed, **exactly 3 added**, all `PipeInputsTest.*` from this package. `build-linux` and `build-push` name lists are identical (`diff` empty) |
| integration-verify | `ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4` | `rc=0`, `100% tests passed, 0 tests failed out of 818`, `grep -c "Fatal{UnmigratedPipeInput"` = 0, `grep -c "Fatal{PipeVerifyDiffer"` = 0. All eight formerly-aborting `DirectVulkan.Verify.{UnboundImageDescriptorScenario,SampledSetStalenessScenario}.*` entries pass |
| the four controls | `ctest --test-dir build-verify -R 'PoisonOmitted\.\|VerifyCorrupted\.'` | 4/4 pass (`DirectGLES/DirectVulkan.VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported`, `...PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb`) |
| 79-case retrace | `MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py --tree ~/w7/p1-fix-core --lib .../build-verify/libMobileGL.so --out ~/w7/fcr-retrace -j 4` | `passed 79 / 79; failed: []` |
| retrace arming | in `~/w7/fcr-retrace`: `ls *.log` = 79; `grep -l "armed, zero divergences, zero unmigrated reads" *.log` = **79**; `grep -l "Fatal{" *.log` = 0; `grep -rn "UnmigratedPipeInput\|PipeVerifyDiffer" .` = nothing |
| include closure | `python3 scripts/check_include_closure.py` | `3 probes, 0 skipped, 0 problem(s)` |
The arming assertion is not decorative: `tools/trace_replay/run_trace_case.cmake:164` does
`string(FIND "${pipe_verify_log}" "MGPipe: verify armed" ...)` and `:174` greps the case log for
`Fatal\{(PipeVerifyDiffer|UnmigratedPipeInput|PipeVerifyBadKnob)`. Spot-checked a case log
directly: `improved-transparency-minecraft-26.3/DirectGLES/output/mobilegl.log:329` carries
`MGPipe: verify armed - 63 fields, 69 verbs, fatal=1`.
Note the `build-push` incremental build had 50 targets to relink when I started
(`[50/50] Linking ... MobileGLIntegrationTest`), i.e. the implementer's `build-push` was one step
stale when their table (c) was produced. Re-running after a full rebuild reproduces their numbers
exactly, so the claim stands.
---
## 2. The attacks that were tried and failed
### 2a. "Zero divergences was achieved by disabling the read hook"
Refuted three ways.
1. `MobileGL/MG_Impl/Pipe/PipeFill.cpp` gains **only** the new `MGPipeNoteFrontendMutation`
(+ one `#include`). `MGPipeVerifyReadHook` (`PipeFill.cpp:439-462`) is byte-identical to
`97b997d5`: it still re-reads the whole field out of the live context
(`MGPipeFillAccess::CopyField(g_readScratch, *ctx, field)`) and still calls
`ReportDivergence(field, "read")`.
2. Every `integration-verify` entry still carries `MOBILEGL_PIPE_VERIFY=1` in its ctest
`ENVIRONMENT` property (verified in
`build-verify/MobileGL/MG_IntegrationTest/MobileGLIntegrationTest[17]_tests.cmake`), so the
comparator is armed for all 818.
3. **Direct falsification.** I inserted `return;` as the first statement of
`MGPipeNoteFrontendMutation` (`PipeFill.cpp:479`), rebuilt `build-verify`, and re-ran:
```
ctest --test-dir build-verify -L integration-verify \
-R 'UnboundImageDescriptorScenario|SampledSetStalenessScenario' -j 4
-> 8 tests failed out of 30, all Subprocess aborted:
DirectVulkan.Verify.SampledSetStalenessScenario.{AFilterChange,ASamplerObject}CompletesTheTexture
DirectVulkan.Verify.UnboundImageDescriptorScenario.* (6)
ctest --test-dir build-verify -R 'PipeInputsTest\.(AFrontendMutation|TheMutationNotice)'
-> 2 of 3 fail; child log carries
MGPipe: Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", verb=1, where=read}
```
That is the lane failure from `P1-LANE-FINDINGS.md` §F2, reproduced exactly, and it proves the
read hook is live and that the notice is the thing carrying the fix. The edit was reverted
(`git checkout --`), `build-verify` rebuilt, and the three tests are green again;
`git status --porcelain` is clean.
### 2b. "The F2 mechanism silences the comparator / makes it compare a value against itself"
The notice refreshes the pushed **value at the point of the frontend write**, not at read time.
The read hook still compares the stored value against a fresh live read at every accessor call, so
any mutation path added later without a notice still reds the lane — demonstrated by 2a.3. No
compare is skipped, no field is excluded, `kMGPipeInputFieldSticky[]` is untouched, and the poison
stamp is deliberately not written (`PipeFill.cpp:479-491` copies the value only), so negative
control B and `Fatal{UnmigratedPipeInput}` both survive — confirmed by the four control entries
passing and by `PipeInputsTest.TheMutationNoticeRefreshesTheValueButNotTheStamp`.
This is the findings' explicitly preferred option, so it is not an undeclared deviation.
### 2c. "The enumeration missed fields" — re-derived from the code, independently
The result file's Step 1–3 was not taken on trust. Re-derivation:
* **Backends cannot reach any `GLContext` mutator.** Gate G2 holds (`grep -rc pGLContext
MobileGL/MG_Backend` is empty on the integrated tree), so the only context-level calls a backend
can make are the `PipeInputs` accessors, and the only non-const ones there are the three
forwarders `RecordError`, `InvalidateCompileEnv`, `ValidateProgramName`
(`MG_Impl/Pipe/PipeFill.cpp:537-551`) — all F-class/sticky with no storage. So a backend can only
move a pushed value *through a frontend object that calls back into the context*.
* **That callback surface is exactly five call sites, on three methods.**
`grep -rn "pGLContext->" MobileGL/MG_State/ | grep -v Core.cpp` yields, excluding comments:
- `MG_State/GLState/SamplerState/SamplerObject.cpp:34` → `BumpSamplingResolutionGeneration()`
- `MG_State/GLState/TextureState/TextureObject.cpp:37` → `BumpSamplingResolutionGeneration()`
- `MG_State/GLState/TextureState/TextureObject.cpp:96` → `BumpTextureBindGeneration()`
- `MG_State/GLState/TextureState/TextureUnit.cpp:39` → `BumpTextureBindGeneration()`
plus `MG_State/GLState/TextureState/TextureState.cpp:142` → `BumpTextureBindGeneration()`.
All five land in `TextureState::BumpSamplingResolutionGeneration` (`TextureState.h:131`) or
`TextureState::BumpTextureBindGeneration` (`TextureState.h:109`), both hooked. The only other
writes to the three counters in the whole tree are inside `TextureState::NoteUnitTouched`
(`TextureState.h:78-100`), also hooked on both branches. Verified by
`grep -rn "m_textureBindGeneration\|m_samplingResolutionGeneration\|m_maxTouchedUnit"`: no write
outside `TextureState.h`.
* **Three fields re-derived, one field ruled out.**
1. `GetSamplingResolutionGeneration` — moved by `SamplerObject::BumpVersion`
(`SamplerObject.cpp:34`) and `TextureObjectBase::BumpShapeVersion` (`TextureObject.cpp:37`);
hook at `TextureState.h:131-134`. Covers all sampler-setter and storage-shape writers.
2. `GetTextureBindGeneration` — moved by `TextureUnit.cpp:39` (sampler unbind),
`TextureObject.cpp:96` (a context default texture becoming defined) and
`TextureState.cpp:142` (delete-unbind), plus the `bindingChanged` branch of `NoteUnitTouched`;
hooks at `TextureState.h:97-99` and `:109-112`.
3. `GetMaxTouchedTextureUnit` — moved only by the high-water branch of `NoteUnitTouched`
(`TextureState.h:79-86`); hook is inside that branch, after the assignment.
4. **`GetTouchedBufferBindingPointCount` ruled out** (the one V-class high-water counter the
result file does *not* discuss by name): its only mutator is
`GLContext::TouchBufferBindingPoint` (`Core.h:83`) and its only callers are
`MG_Impl/GLImpl/Buffer/GL_Buffer.cpp:1499,1622` — frontend entry points, never a backend.
* **Everything else checks out.** `GetRenderStateParameters*`/`GetPipelineStateVersion` move only
through `RenderState` setters (`RenderState.cpp`), and `RenderState` is a private member of
`GLContext` with no backend-reachable handle; `DirectGLES`'s `ScopedEmulationDrawState`
(`DirectGLES.cpp:5226-5345`) saves/restores **native** GL through `g_GLESFuncs.glGetIntegerv`
only — no frontend write, so the `IsTransformFeedbackPaused` worry the `kReadback` comment
raises is not a mutation. The transform-feedback counters move only through `GLContext` methods
(`Core.h:308-357`), unreachable from a backend. The `*Impl::` namespaces the backends use
(`BufferImpl`, `TextureImpl`, `SamplerImpl`, `FramebufferImpl`, `RenderStateImpl`) are
**backend-local** (`MG_Backend/DirectGLES/Managers.cpp:494,2751,5078,8490`,
`DirectGLES.cpp:352,1299,1882,1983`), not `MG_Impl`. The one genuine `MG_Impl` helper a backend
calls, `MG_Impl::GLImpl::CopyTextureImageToClientOrPBO_State` (`VulkanRenderer.cpp:10768`,
defined `GL_Texture.cpp:5375`), only reads and `RecordError`s. No `MGP_FILL` is reachable from
inside a verb (no nested fill).
Conclusion: the enumeration is complete for this tree. I could not find a fourth field.
### 2d. "A fill row that hides a divergence instead of fixing it"
A `FillPoints.def` row can only suppress `Fatal{UnmigratedPipeInput}` for that (class, field); the
value it then makes available is still compare-at-read verified against the live context, so a row
cannot hide a `PipeVerifyDiffer`. The two lane-driven rows are correctly derived:
`VkClearManager.cpp:58` reads `MGB_CTX->IsCapabilityEnabled(FramebufferSrgb)` inside
`PreCompensateSrgbClearColor`, reached from `VulkanRenderer::MaterializePendingClearForTexture`
(`:7865`, calls at `:7967,7995`), which `GenerateMipmap` calls at `:11000` (kTextureOp) and
`PrepareStorageImageTextures` (`:5763`) calls from `DispatchCompute` (`:7054`) /
`DispatchComputeIndirect` (`:7106`) (kDispatch). Verified. The other `IsCapabilityEnabled`
consumers (`MaterializeMultisamplePendingClear` `:8038`, `MaterializePendingClearForRenderbuffer`
`:8109`) are reached from `CopyImageSubData` (`:9513`), `ReadPixels` (`:9928`) and
`ReadDepthStencilPixels` (`:10307`) — classes `kBlitOrCopy`/`kReadback`, which already carry the
row. The `kReadback` "reformat" is genuinely row-neutral: the generated `kReadback` mask
(`0x5f50f3a041300541`) is unchanged in the `.inc` diff, and the class own-counts move by exactly
+1/+4/+3 = the 8 added rows.
### 2e. "A test helper that makes tests pass unconditionally / tests that cannot fail"
No helper was added or changed; the three tests use the existing `PipeInputsTest` fixture,
`Fresh()`, `RunInChild()`, `ExitedWith()`. Two of the three fail under 2a.3's perturbation. Both
value tests carry their own "the mutation actually took" guards
(`ASSERT_NE(ctx.GetSamplingResolutionGeneration(), samplingBefore)` at `PipeInputsTest.cpp:551`,
and `::_exit(7)` in the child at `:610`), so they cannot pass for the wrong reason. All three exist
as visible `GTEST_SKIP`s in the pull arm (`:173,176,179`) and skip visibly where their switch is
off — confirmed per build: `build-linux` 3 skipped, `build-push` 1 pass + 2 skipped (POISON and
VERIFY are both off there), `build-verify` 3 pass.
### 2f. "Something moved the pull build"
`.text`, `.data`, `.bss`, `.rodata`, total size and all 27799 symbols identical, and the `.so` is
the same number of bytes on disk. The `MG_State` header edit is three `MGP_NOTE_MUTATION`
statements plus an include that expands to nothing under `MOBILEGL_PIPE_PUSH=OFF`.
---
## 3. Majors
**None.**
---
## 4. Minors
1. **The notice keeps firing between verbs.** `m_currentVerb` is never reset at verb end
(`PipeInputs.h:616` initialises it to `kVerbCount`, and only `MGPipeFillForVerb` writes it), so
after any verb the last verb's mask stays installed and every subsequent frontend
`glBindTexture` / sampler edit made by the *application* runs the full notice
(`PipeFill.cpp:479-491`, including the `CopyField` switch) even though no verb is in flight.
Harmless for correctness — the value only becomes *more* equal to the live context and the stamp
is untouched — but `PipeMutation.h:33` ("a verb has been filled … in that verb class's may-read
mask") reads as if the guard meant "inside a verb", which it does not, and the push build now
pays the notice on the application's bind hot path. A `MGP_FILL`-side reset (or a
`verb-in-flight` flag) would make the comment true and the cost proportional. Worth naming for P2.
2. **Six of the eight added `FillPoints.def` rows are static over-approximation, not lane-driven.**
Only `X(kDispatch, IsCapabilityEnabled)` (`:179`) and `X(kTextureOp, IsCapabilityEnabled)`
(`:246`) were forced by an observed Fatal. `X(kBlitOrCopy, GetViewportIndexed|
GetDepthRangeIndexed|GetProvokingVertexMode|GetBufferBindingPoint)` (`:228-231`) and
`X(kTextureOp, GetFramebufferBindingSlot|GetBufferBindingPoint)` (`:247-248`) come from a
hand-verified but unexercised path (`TryBlitToDefaultFramebufferWithShader`,
`GenerateDepthMipmapWithShader`). Each such row permanently weakens the poison for that
(class, field) pair. The result file admits this for one row; it applies to five more.
3. **The notice's mask guard is untested.** `if (!MGPipeFieldMaskHas(mask, field)) return;`
(`PipeFill.cpp:489`) has no covering assertion.
`TheMutationNoticeRefreshesTheValueButNotTheStamp` (`PipeInputsTest.cpp:563-587`) says its
`FenceSync` half proves "the notice stamped a field the verb class never fills", but the notice
never stamps *any* field, so that half re-tests the stamp invariant rather than the mask branch;
removing the mask guard entirely would leave all three tests green.
4. **`MG_State` now has a link-time dependency on a symbol defined in `MG_Impl`.**
`TextureState.h:11` includes `MG_Pipe/PipeMutation.h`, whose `MGPipeNoteFrontendMutation` is
defined in `MG_Impl/Pipe/PipeFill.cpp`. Fine today (one static library, one `SOURCE_FILES`
list), and the header itself only depends on `MG_Pipe`, but it inverts the `MG_State < MG_Impl`
layering at link time and `PipeMutation.h` is not one of `check_include_closure.py`'s three
probed headers, so nothing gates what that include may drag in later.
5. **"Every case armed" is not provable per entry for the 818 integration entries.** All
`Verify` entries share one log file per backend
(`MOBILEGL_LOG_FILE_PATH=…/pipe-verify-DirectGLES.log`); after the full lane run that file
contains no `MGPipe` line at all (each process truncates it). Per-entry arming rests on the two
dedicated `PipeVerifyArmingScenario` entries plus the fact that the eight F2 aborts reproduce on
demand (2a.3). Pre-existing lane design, not this package's doing, but the result file's "(d)"
should not be read as per-entry arming evidence. The 79-case retrace *is* per-case armed (79/79).
6. **`ctest -L integration-verify` also reports 77 `Skipped` entries** among the 818 (741 real
passes). Not caused by this package (no `MG_IntegrationTest` file is touched), but the "818
passed" figure in the result file is 741 executed + 77 skipped.
7. **Two small undeclared deviations.** (a) `build-linux/CMakeCache.txt` in this tree carries
`CMAKE_CXX_COMPILER:UNINITIALIZED=clang++` while the reference `~/w7/pipe/build-linux` and this
tree's own `build-push`/`build-verify` carry `CMAKE_CXX_COMPILER:STRING=/usr/sbin/clang++`; the
`.text` byte-identity shows it resolves to the same compiler, but the G1 comparison was made
across a differently-spelled cache entry and deviation 2 does not mention it.
(b) `MobileGL/MG_Pipe/PipeMutation.h` is a new file in a directory the P1 brief's D5 table
enumerates; the new row is not called out as a deviation (it is implied by the findings'
preferred option, so this is bookkeeping only).
---
## 5. Follow-ups for the integrator (not blocking)
- The F2 enumeration in `fix-core-v1.md` still has to land in `docs/Disaggregated/MEASUREMENTS.md`
(the package correctly left it alone; the findings asked for the enumeration to exist, and it does).
- Minor 1 (verb-end reset) and minor 3 (mask-guard coverage) are natural P2 items when the tracker
takes over the filler.
- `GetFramebufferBindingSlotFast`'s five bypassing reads (`DirectGLES.cpp:1611,1905,2743,2858,2933`)
remain outside both the poison and the compare-at-read; unchanged, and correctly recorded.
@@ -1,284 +0,0 @@
# P1 lane fixes — package `fix-core` (F1 + F2)
Tree: WSL `~/w7/p1-fix-core`, branch `p1/fix-core`, branched from the integrated
`feat/disaggregated` @ `97b997d5`. Not pushed.
## Commits
| sha | subject |
|---|---|
| `80a6b390` | `[Fix] (Pipe): fill the capability set on a texture op and a dispatch, and the shader blit's viewport, provoking vertex and buffer bindings` |
| `6b681c4a` | `[Fix] (Pipe, State): refresh a pushed PipeInputs field when the frontend moves it inside a verb` |
| `9bd6d394` | `[Test] (Pipe): pin the push-on-mutation shape - a frontend write inside a verb refreshes the pushed field, and only its value` |
Files touched: `MobileGL/MG_Pipe/FillPoints.def`,
`MobileGL/MG_Pipe/generated/PipeFillPoints.inc` (regenerated),
`MobileGL/MG_Pipe/PipeMutation.h` (new), `MobileGL/MG_Impl/Pipe/PipeFill.cpp`,
`MobileGL/MG_State/GLState/TextureState/TextureState.h`,
`MobileGL/MG_Test/Pipe/PipeInputsTest.cpp`. No backend file, no other package's file.
---
## F1 — missing fill rows
### What the lane proved
`~/w7/retrace-out/p1-verify/` aborted four cases with
`Fatal{UnmigratedPipeInput, "IsCapabilityEnabled@GenerateMipmap"}` (3) and
`"IsCapabilityEnabled@DispatchCompute"` (1), all `DirectVulkan`.
Root cause, traced in the tree: `VulkanRenderer::MaterializePendingClearForTexture`
(`VulkanRenderer.cpp:7865`) calls `VkClearManager::PreCompensateSrgbClearColor`, which reads
`IsCapabilityEnabled(FramebufferSrgb)` (`VkClearManager.cpp:58`). It is reached from
`GenerateMipmap` (`:11000`, a `kTextureOp` verb) and from `DispatchCompute` through
`PrepareStorageImageTextures` (`:5763`, a `kDispatch` verb). `kDraw`, `kClear`, `kBlitOrCopy`
and `kReadback` already named the field; those two classes did not.
### The audit that followed
Not stopped at the two rows. A static verb-class → field reachability pass over `MG_Backend`
(name-based call graph, roots taken from both `GetBackendFunctions()` tables;
`scratchpad/wf4/fix-core/reach.py`) produced candidate rows with their call paths, and each
candidate was then hand-verified against the tree — the pass merges same-name functions, so
most candidates were collisions (`Clear`, `SyncToBackend`, `GetOrCreatePipeline` on
`PipelineFactory` vs `VulkanRenderer`) and were dropped. Two real families survived:
| class | rows added | verified path |
|---|---|---|
| `kDispatch` | `IsCapabilityEnabled` | `DispatchCompute` → `PrepareStorageImageTextures` → `MaterializePendingClearForTexture` → `PreCompensateSrgbClearColor` |
| `kTextureOp` | `IsCapabilityEnabled` | `GenerateMipmap` (`VulkanRenderer.cpp:11000`) → the same |
| `kTextureOp` | `GetFramebufferBindingSlot`, `GetBufferBindingPoint` | `GenerateMipmap` → `GenerateDepthMipmapWithShader` (`:11069`) → `BindProgramUniformBuffers` (`:4755`) → `ResolveSamplerDescriptor` (`UniformManager.cpp:555`, the feedback-loop check) / `ResolveStorageBufferDescriptor` (`:1194`) |
| `kBlitOrCopy` | `GetViewportIndexed`, `GetDepthRangeIndexed` | `BlitNamedFramebuffer` → `TryBlitToDefaultFramebufferWithShader` (`:8671`) → `ApplyGLViewportState` (`:8502`) → `ComputeGLViewport` |
| `kBlitOrCopy` | `GetProvokingVertexMode` | same → `GetOrCreateBlitPipeline` (`:8511`) → `SelectProvokingVertexMode` (`:4434`) |
| `kBlitOrCopy` | `GetBufferBindingPoint` | same → `BindProgramUniformBuffers` (`:8576`) → the buffer-block resolvers |
Every added row carries its derivation in a comment in `FillPoints.def`. The commit also
unfolds the `kReadback` transform-feedback rows that `9087f133` landed on one 1200-column
line; no row changed there.
---
## F2 — the backend mutates frontend state inside a verb
### Option chosen: **push on mutation** (the findings' preferred option)
Justification:
- It keeps the comparator's invariant — "the pushed block equals the live context at every
read" — literally true, so the push build's semantics stay equal to the pull build's. The
fallback (a volatile-in-verb class) would instead have made the push build read the
boundary value where the pull build reads the moved one, i.e. it would have *kept* the
behavioural difference and hidden it.
- The compare-at-read arm is the only comparator arm that is real in P1 (the entry compare is
tautological until P2's tracker fills the first arm, per brief D8). The fallback's mechanism
is precisely "skip compare-at-read for this field", which would blind the P1 gate on the one
field that found the bug.
- It is the shape P2's tracker needs anyway (findings, F2 bullet 1).
### Mechanism
`MobileGL/MG_Pipe/PipeMutation.h` (new, leaf layer that both `MG_State` and `MG_Impl` may
include) declares `MGP_NOTE_MUTATION(Field)`. In the pull build the macro is `((void)0)` and
the header includes nothing. Under `MOBILEGL_PIPE_PUSH` it calls
`MGPipeNoteFrontendMutation(field)`, defined in `MG_Impl/Pipe/PipeFill.cpp` next to the filler
whose `MGPipeFillAccess::CopyField` it reuses. The notice refreshes one field's **value** when
1. a context is live,
2. a verb has been filled (`m_currentVerb != kVerbCount`),
3. the field is not sticky (a forwarded field has no storage), and
4. the field is in the current verb class's `kMGPipeClassFieldMask`.
It never touches the poison stamp, so a stamp `MOBILEGL_PIPE_POISON_OMIT` withheld stays
withheld (negative control B survives) and a field the verb never filled stays
`Fatal{UnmigratedPipeInput}` on the next read instead of being healed by an unrelated write.
### The enumeration asked for
**Step 1 — what a backend can write into the frontend.** `scout-pull-sites.md` §2.6 lists the
sites; re-derived live on this tree by matching every non-const `MG_State` method declaration
against every call in `MG_Backend/**` (`scratchpad/wf4/fix-core/scan.py`). Nothing is reached
through `MGB_CTX` itself: the only non-read accessors the backends call are `RecordError`,
`InvalidateCompileEnv` and `ValidateProgramName`, all three F-class/sticky with no storage.
Everything else is a method call on an object the backend obtained by handle or created itself.
| written frontend object | mutators the backends call | pushed field it can move |
|---|---|---|
| `SamplerObject` | `SetMinFilter`, `SetMagFilter`, `SetMipmapMode`, `SetWrapS/T/R`, `SetLodRange`, `SetCompareMode`, `SetSamplerCompareFunc` (`VulkanRenderer.cpp:4270-4276`) — each ends in `SamplerObject::BumpVersion()` (`SamplerObject.cpp:34`) | **`GetSamplingResolutionGeneration`** |
| `ITextureObject` | `AllocateStorage`, `MarkStorageDirty`, `TruncateMipmapLevels`, `SetInternalFormat`, `SetSamples`, `SetFixedSampleLocations` (`UniformManager.cpp:1484-1498,1625-1635` fallback-texture creation; `DirectGLES.cpp:6326-6327`) — each ends in `TextureObjectBase::BumpShapeVersion()` (`TextureObject.cpp:37`); `SetInternalFormat` on a context default texture additionally bumps the bind generation (`TextureObject.cpp:96`) | **`GetSamplingResolutionGeneration`**, **`GetTextureBindGeneration`** |
| texture unit / texture state | any bind path (`TextureState::NoteUnitTouched`), `TextureUnit::SetSamplerObject`, `TextureState::MarkTextureObjectForDeletion` | **`GetTextureBindGeneration`**, **`GetMaxTouchedTextureUnit`** |
| `BufferObject` | `MarkGpuWritten`, `SetBackendResource`, `EnsureGpuResidentStorage`, `WritebackFromBackend`, `SyncPersistentMappedRange`, `SyncGpuWrites` | none — buffers are reached through the O-class binding slots/points, which are live pointers into the context, so a content or resource change is visible to the backend without any copy |
| `ProgramObject` | `SetDrawID`, `SetBaseVertex`, `SetBaseInstance`, `SetViewportPassMask`, `AttachShader`, `Link`, `MarkUBOContentDirty`, `SetBackendHashMemo` | none — `GetProgramForDraw`/`ForDispatch`/`TransformFeedbackProgram` are O-class `SharedPtr` copies; the pointee is shared, so its mutations are seen live and the handle itself never moves inside a verb |
| `VertexArrayObject` | `SetBackendHashMemo`, `SetBackendStateMemo`, `SetBackendAuxMemo` | none — same reason (`GetBoundVertexArray` is O-class) |
| `GLContext` | `RecordError`, `InvalidateCompileEnv` | none — both are sticky F-class with no storage |
**Step 2 — the frontend mutators that bump a pushed value.** Only six `PipeInputs` fields are
*derived* counters rather than plain state copies: `GetSamplingResolutionGeneration`,
`GetTextureBindGeneration`, `GetMaxTouchedTextureUnit`, `GetTextureContextId` (immutable per
context), `GetRenderStateParametersVersion` / `GetPipelineStateVersion` (moved only by
`RenderState`'s setters, which no backend calls — the scan finds no `RenderState` mutator in
`MG_Backend`, and `DirectGLES`'s `ScopedEmulationDrawState` / `ScopedRestartIndexSubstitution`
save and restore *native* GL state, never frontend state), plus the transform-feedback
counters (`AddTransformFeedback*`, `EndTransformFeedback` — `GLContext` mutators no backend
calls).
**Step 3 — the intersection, i.e. what is handled.** Three fields:
| field | hook site |
|---|---|
| `GetSamplingResolutionGeneration` | `TextureState::BumpSamplingResolutionGeneration()` |
| `GetTextureBindGeneration` | `TextureState::BumpTextureBindGeneration()` and the `bindingChanged` branch of `TextureState::NoteUnitTouched()` |
| `GetMaxTouchedTextureUnit` | the high-water branch of `TextureState::NoteUnitTouched()` |
The hooks sit on the **counters**, not on the individual writers, so every path in the table
above (and any writer added later) is covered by three notices rather than forty.
---
## Verification (all run in `~/w7/p1-fix-core`)
Build directories configured to the brief's C recipe: `build-linux` (pull),
`build-push` (`MOBILEGL_PIPE_PUSH=ON`), `build-verify` (`MOBILEGL_PIPE_VERIFY=ON`,
`MOBILEGL_ITEST_REQUIRE_GPU=ON`); `CCACHE_BASEDIR=/home/swung/w7`; all three build clean.
**(a) pull build symbol-identical**
```
python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so \
--after build-linux/libMobileGL.so --threshold 0
.text 10792579 -> 10792579 (+0, +0.000%)
.data/.bss/.rodata/Total all +0
27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed
Removed (0) / Added (0) / Resized (0) / Renamed only (0)
```
**(b) generator**
```
python3 scripts/gen_pipe.py --check -> "generated files are up to date"
71 calls, 69 verify payloads, 63 fields (7 sticky),
69 verbs, 9 classes, 0 UNMAPPED
python3 scripts/gen_pipe.py --self-test -> "6 negative-control trip(s), positive control OK"
```
**(c) `ctest -L unit`**
| build | result |
|---|---|
| `build-linux` | **100% tests passed, 0 failed out of 1485** |
| `build-push` | 11 failed out of 1485 |
| `build-verify` | 11 failed out of 1485 |
The 11 in push/verify are **exactly** the F3 list owned by the other package
(`DirectGLESSanity.{BindsAMultisampleTextureDespiteTheDefaultMipmapFilter,
BindingZeroClearsPreviousNativeTextureBinding}`, `DirectGLESTextureSync.UnitMemoRefusesToDrive
ATwinFromAnotherTexture`, the seven `FramebufferTest.*`, `TextureTest.StorePackedWordsToClient
CopiesWordsVerbatimUnderPackParams`) — they construct a `GLContext` by hand and call a backend
helper with no verb in flight, and they are not mine to fix.
**(d) `ctest -L integration-verify` in `build-verify`**
```
ctest --test-dir build-verify -L integration-verify --no-tests=error -j 4
exit=0
100% tests passed, 0 tests failed out of 818
grep -c "Fatal{UnmigratedPipeInput" -> 0
grep -c "Fatal{PipeVerifyDiffer" -> 0
```
The eight `DirectVulkan.Verify.{UnboundImageDescriptorScenario,SampledSetStalenessScenario}.*`
aborts of F2 are gone. The four poison/verify control entries
(`DirectGLES|DirectVulkan.{VerifyCorrupted.PipeVerifyArmingScenario.CorruptedFieldIsReported,
PoisonOmitted.PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb}`) still pass, i.e. the gate
can still go red for its reason.
**(e) full 79-case retrace under verify**
```
cp .../wf2/retrace_gate.py ~/w7/retrace_gate.py # byte-identical to the one already there
MOBILEGL_PIPE_VERIFY=1 python3 ~/w7/retrace_gate.py \
--tree ~/w7/p1-fix-core --lib ~/w7/p1-fix-core/build-verify/libMobileGL.so \
--out ~/w7/retrace-out/fix-core -j 4
79 tests selected of 79
passed 79 / 79; failed: []
```
```
cd ~/w7/retrace-out/fix-core
ls *.log -> 79
grep -l "armed, zero divergences, zero unmigrated reads" -> 79
grep -l "Fatal{" *.log -> 0
grep -r "Fatal{|UnmigratedPipeInput|PipeVerifyDiffer" . -> 0 each
```
Sample summary line: `-- MGPipe verify: improved-transparency-minecraft-26.3 DirectGLES armed,
zero divergences, zero unmigrated reads`.
**(f) `ctest -N` name diff**
```
ctest --test-dir build-linux -N | grep -E '^\s+Test +#[0-9]+: ' \
| sed -E 's/^ *Test +#[0-9]+: //' | sort > ~/w7/fc-ctest-names.txt
comm -23 ~/w7/p1-before-ctest-names.txt ~/w7/fc-ctest-names.txt | wc -l -> 0 (removed)
comm -13 ~/w7/p1-before-ctest-names.txt ~/w7/fc-ctest-names.txt | wc -l -> 29 (added)
```
26 of the 29 came in with the merged P1 packages; the 3 new ones are this package's.
**Extra: include-closure gate**
```
python3 scripts/check_include_closure.py -> 3 probes, 0 skipped, 0 problem(s)
```
**Extra: falsification of the new tests.** With `MGPipeNoteFrontendMutation`'s body
short-circuited and `build-verify` rebuilt,
`PipeInputsTest.AFrontendMutationInsideAVerbRefreshesThePushedField` and
`...DoesNotDivergeAtRead` both FAIL (the second by `SIGABRT` on
`Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@DrawArrays", where=read}` — the lane
failure reproduced in a unit test); `...TheMutationNoticeRefreshesTheValueButNotTheStamp` stays
green, which is correct for a guard case. The edit was reverted and the build restored before
committing (`git diff` clean on that file).
---
## Deviations
1. **Tree path.** The brief names the tree `~/w7/fix-core`; the worktree that exists is
`~/w7/p1-fix-core` on branch `p1/fix-core` (`git worktree list` at `97b997d5`). Tree wins
for facts; everything was done there.
2. **Build directories did not exist.** The brief says the tree "has three configured build
directories". `build-linux`, `build-push` and `build-verify` were absent; they were
configured from the brief's C recipe (matching `~/w7/p1-core`'s `CMakeCache.txt` flags:
Release, `/usr/sbin/clang++`, ccache, INFO, `MOBILEGL_BUILD_TEST=ON`,
`MOBILEGL_BUILD_INTEGRATION_TEST=ON`, `MOBILEGL_BUILD_BENCHMARK=OFF`,
`MOBILEGL_BUILD_TRACE_REPLAY=OFF`, mesa EGL vendor, lvp ICD;
`MOBILEGL_ITEST_REQUIRE_GPU=ON` only for verify).
3. **A fourth file under `MG_State`.** The brief allows `MG_State/**` "only if F2's chosen
mechanism needs a frontend hook" — it does; `TextureState.h` is the only `MG_State` file
touched, and only by three macro statements plus one include that expands to nothing in the
pull build (`.text` delta 0 confirms it).
4. **`ctest -L integration-verify` counts 818 entries**, not the brief's 742. The label's
membership grew with the merged P1 packages; nothing was excluded.
5. **Reformatting inside `FillPoints.def`.** The `kReadback` transform-feedback rows that
`9087f133` landed on a single 1200-column line were unfolded to the file's one-row-per-line
shape. No row was added, removed or moved between classes by that edit.
6. **The static reachability pass is a helper, not a gate.** `scratchpad/wf4/fix-core/reach.py`
over-approximates through same-name functions; it was used to generate candidates, each of
which was then confirmed or rejected by reading the tree. It is not wired into CI.
## Unfinished / known gaps
- **F3 is not fixed here** (it is the other package's): 11 `ctest -L unit` entries still fail
in `build-push` and `build-verify` on this branch, and they will keep failing until
`p1/fix-tests` lands. `build-linux` is 1485/1485.
- **`GetFramebufferBindingSlotFast`** (`DirectGLES.cpp:1611,1905,2743,2858,2933`) still
bypasses the accessor once its cache is warm, so those five reads are neither poison-checked
nor compare-at-read verified. That is the known P2 gap recorded in brief D4, untouched here.
- **The notice is single-threaded**, exactly like the filler it shares state with: both write
`gPipeInputs` with no synchronisation. Unchanged risk profile, but worth naming before P2
puts a tracker behind the same global.
- **`GetBufferBindingPoint` on `kTextureOp`/`kBlitOrCopy` is a "may-read" row**, not one the
lane exercised: the helper programs those two verbs draw with declare no SSBO today, so the
resolver that reads the binding points is reachable in code but not hit by the 79 traces or
the 818 integration entries. It was added because the path exists; if the table is ever
trimmed for fill cost, that is the first row to re-examine.
- Nothing was pushed, and `docs/Disaggregated/MEASUREMENTS.md` was not edited (not in this
package's file list); the F2 enumeration above is the material an integrator would land there.
@@ -1,272 +0,0 @@
# Adversarial review — P1 package `fix-tests` (F3), branch `p1/fix-tests` @ `c67c47f7`
Reviewer ran independently in WSL `~/w7/p1-fix-tests` (the tree; the task text's `~/w7/fix-tests`
does not exist — the implementer declared this, deviation 1, and it is correct).
Tree state at start and at end: `git status --porcelain` shows only the three untracked build
directories; `git log -1` is `c67c47f7`. Nothing was left changed (two temporary mutations were
made for negative controls and reverted with `git checkout --`; verified clean afterwards).
**Verdict: approved. 0 majors, 6 minors.**
I set out to refute "correct and complete" and could not. Every claim in
`fix-tests-v1.md` reproduced, and two of them reproduce *stronger* than claimed.
---
## 1. What was re-run independently (command + observed output)
### 1.1 The diff is what it says it is
```
$ git -C ~/w7/p1-fix-tests diff --name-only 97b997d5..HEAD
MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp
MobileGL/MG_Test/SanityTest.cpp
MobileGL/MG_Test/ScopedPipeVerb.h
MobileGL/MG_Test/Texture/TextureTest.cpp
```
128 insertions / 5 deletions, all inside `MobileGL/MG_Test/**`. No production source, no
`scripts/`, no CMake, no `.def`/generated file, no fixture, no `MG_Test/Pipe/**` (the other
package's area), no `MG_IntegrationTest/**`. The full diff was read line by line.
Commit message shape checked against the lane rule (subject + blank line + `- ` bullets, no
`Co-Authored-By`): conforms.
### 1.2 Build configurations are the reference ones (deviation 2/3 verified, not taken on trust)
Diffing every `MOBILEGL_*` / `CMAKE_BUILD_TYPE` / `CMAKE_CXX_COMPILER` cache entry against
`~/w7/pipe/<dir>/CMakeCache.txt`:
```
--- build-linux --- IDENTICAL to pipe/build-linux
--- build-push --- only CMAKE_CXX_COMPILER:STRING vs :UNINITIALIZED (same value)
--- build-verify --- only CMAKE_CXX_COMPILER:STRING vs :UNINITIALIZED (same value)
```
`MOBILEGL_BUILD_BENCHMARK:BOOL=OFF` is present in `~/w7/pipe`'s three caches too, so
deviation 3 reproduces the reference trees rather than changing them. Compile definitions
confirmed from `build.ninja`: `build-linux` has neither, `build-push` has
`-DMOBILEGL_PIPE_PUSH=1`, `build-verify` has `-DMOBILEGL_PIPE_PUSH=1 -DMOBILEGL_PIPE_VERIFY=1`.
`cmake --build {build-linux,build-push,build-verify} -j 12` → all three `ninja: no work to do`,
i.e. the committed tree is exactly what was tested.
### 1.3 Pull-build symbol report, `--threshold 0`
```
$ python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so \
--after build-linux/libMobileGL.so --threshold 0
symbol-report: .text 10792579 -> 10792579 (+0, +0.000%)
symbol-report: .data 76824 -> 76824 (+0) .bss 1296872 -> 1296872 (+0) .rodata 1534938 -> 1534938 (+0)
symbol-report: 27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed
```
G1 holds. (Trivially, by construction — the helper is header-only test code — but re-run.)
### 1.4 `gen_pipe.py --check` / `--self-test`
```
gen_pipe: 71 calls (11 screen, 60 context), 69 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes
gen_pipe: generated files are up to date rc=0
gen_pipe: self-test: 6 negative-control trip(s), positive control OK rc=0
```
`scripts/check_include_closure.py` also clean (3 probes, 0 forbidden) — the new header does not
break the layering gate.
### 1.5 `ctest -L unit` in all three build directories
```
build-linux rc=0 100% tests passed, 0 tests failed out of 1482
build-push rc=0 100% tests passed, 0 tests failed out of 1482
build-verify rc=0 100% tests passed, 0 tests failed out of 1482
```
### 1.6 `ctest -N` name diff
```
build-linux 2360 names, build-push 2360, build-verify 3178
removed vs ~/w7/p1-before-ctest-names.txt: 0 added: 26
vs the integrated tree ~/w7/pipe/build-linux (2360): 0 removed, 0 added <-- exact match
```
The 26 additions are the other P1 packages' (already at the branch point); **this package adds
none, removes none, renames none.** The `build-verify` surplus is confirmed by label census:
`integration-verify` 818 + `integration-gpu` 1696; `~/w7/pipe/build-verify` also lists 3178.
So the surplus is the configuration, not a name change.
---
## 2. The four things I tried to catch it on
### 2.1 "A test helper that makes tests pass unconditionally" — refuted, twice
**Control A (mine, stronger than the implementer's): neutralise the helper entirely.**
`sed -i 's/^#if MOBILEGL_PIPE_PUSH$/#if defined(MGL_REVIEW_NEUTRALISE) \&\& MOBILEGL_PIPE_PUSH/'`
on `MobileGL/MG_Test/ScopedPipeVerb.h` (all four guards, so ctor, `Renew()` and dtor become
no-ops in every build), rebuild `build-verify`, `ctest -L unit -j 8`:
```
99% tests passed, 11 tests failed out of 1482
5 DirectGLESSanity.BindsAMultisampleTextureDespiteTheDefaultMipmapFilter (Subprocess aborted)
6 DirectGLESSanity.BindingZeroClearsPreviousNativeTextureBinding
82 DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture
310 FramebufferTest.DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff
311 FramebufferTest.ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitutesOne
312 FramebufferTest.ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffer
313 FramebufferTest.DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere
314 FramebufferTest.DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing
315 FramebufferTest.DualSourceFactorsAreDeclinedEvenWithBlendingDisabled
316 FramebufferTest.DualSourceFactorsWithBlendingDisabledStillReachACapableDriver
471 TextureTest.StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams
```
Exactly eleven, and exactly F3's eleven — every declaration in the diff is load-bearing (none is
decorative), and no *other* test in the 1482 depends on the helper. So the helper cannot be
making anything pass that was not already failing for the F3 reason, and none of these eleven is
a test that cannot fail.
**Control B (mine): substitute the WIDEST verb class for a narrow one.** In
`MobileGL/MG_Test/Texture/TextureTest.cpp:4481` change `MGPipeVerb::ReadPixels` →
`MGPipeVerb::DrawArrays` (kDraw, 47 fields, the largest mask there is) and run that one test in
`build-verify` with `MOBILEGL_LOG_FILE_PATH` set:
```
exit=134 (SIGABRT)
[Linux TextureTest/FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "GetPixelStoreParameters@DrawArrays"}
```
This is the decisive anti-rubber-stamp result: the ctor runs the *production*
`MGPipeFillForVerb` (`MobileGL/MG_Impl/Pipe/PipeFill.cpp:528-575`), which stamps only
`kMGPipeClassFieldMask[class of verb]` — so a read outside the declared verb's may-read set is
still `Fatal{UnmigratedPipeInput}`, naming the field *and* the declared verb, even when the
declared verb is the broadest one. `FillPoints.def` remains the only thing that says what a verb
may read. It also proves the test binary and the backend share one `gPipeInputs`: the abort came
out of the backend's accessor, on the block the test's ctor stamped.
Both were reverted with `git checkout --` and the affected targets rebuilt; the restored tests
pass and `git status --porcelain -- MobileGL` is empty.
### 2.2 "Fields / call sites the enumeration missed" — refuted
Enumerated every direct backend-helper call in the unit tree myself
(`awk` over the three files, plus a repo-wide grep):
* `MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp` — 11 `SyncRenderState(` calls at
`:1238,1271,1283,1323,1356,1377,1395,1422,1440,1453,1471`; **every one** sits inside a
`ScopedPipeVerb` scope or after a `Renew()`.
* `MobileGL/MG_Test/SanityTest.cpp` — 4 `BindCurrentTextures()` (`:329,372,380,391`) and
2 `SyncNeccessaryTextures()` (`:3025,3046`); all covered.
* `MobileGL/MG_Test/Texture/TextureTest.cpp` — 2 `StorePackedWordsToClient(` (`:4482,4508`);
both covered.
* `grep -rlE 'MG_Backend::(DirectGLES|DirectVulkan)::[A-Za-z_]+::[A-Za-z_]+\('
MobileGL/MG_Test --include=*.cpp` returns exactly those three files and no others.
* The same grep over `MobileGL/MG_IntegrationTest` returns **nothing** — integration scenarios
drive GL entry points only, so `MGP_FILL` always fires there. The implementer's stated reason
for not touching `MG_IntegrationTest/**` is factually right, not an excuse.
### 2.3 "A mechanism that silences the comparator" — refuted, and the package under-claims
The unit lane in `build-verify` runs with `Features.PipeVerify` **off** unless
`MOBILEGL_PIPE_VERIFY=1` is in the environment, so the implementer's green `ctest -L unit` only
exercised the poison, not the compare-at-read hook. That was the one place a wrongly *placed*
declaration (fill before a mutation the helper must see) could hide: the push value would go
stale against the live context and nobody would look. So I armed it:
```
$ MOBILEGL_PIPE_VERIFY=1 ctest --test-dir build-verify -L unit --no-tests=error -j 8
rc=0 100% tests passed, 0 tests failed out of 1482
```
and proved the arming was real rather than a silent no-op:
```
$ MOBILEGL_PIPE_VERIFY=1 MOBILEGL_LOG_FILE_PATH=/tmp/mgl-verify.log \
./build-verify/.../FramebufferTest --gtest_filter='*DrawIntoAWidened*'
[Linux FramebufferTest/INFO]: MGPipe: verify armed - 63 fields, 69 verbs, fatal=1
# same run without the env var: 0 occurrences of "MGPipe: verify armed"
```
With the comparator genuinely armed, every accessor read in all eleven tests returned exactly
what a live pull would have returned. That is a stronger statement than the result file makes,
and it closes the placement question completely: no `ScopedPipeVerb` in this diff is placed
before a mutation its helper is meant to see.
### 2.4 "The pull build moves" / "a global escape hatch" / "the poison is weakened" — refuted
* Symbol report 0/0/0/0, `.text` +0 (§1.3). The class body is `#if MOBILEGL_PIPE_PUSH`-gated to
nothing (`MobileGL/MG_Test/ScopedPipeVerb.h:46,62,67`), the ctor signature is identical in
both arms, so call sites cannot rot.
* No global escape hatch: no `#define`, no env knob, no weakening of `MGP_INPUT_CHECK`, no
change to `PipeInputs.h`/`PipeFill.cpp`/`FillPoints.def`. The whole mechanism is "call the
production filler for a named verb".
* Whole-binary single-process runs reproduce (the destructor re-arm's stated purpose):
`SanityTest` exit=0 82 tests, `FramebufferTest` exit=0 61 tests, `TextureTest` exit=0
192 tests, 0 `[ FAILED ]` lines each. With the helper neutralised the same three binaries
are 134/1/1 with 11 and 3 failures — i.e. the re-arm is not covering anything up.
---
## 3. Minors
1. **Nested scopes silently invalidate the enclosing declaration, and the header does not say
so.** `MobileGL/MG_Test/ScopedPipeVerb.h:68` — the destructor bumps `CurrentVerbSerial`, so
closing an inner scope makes the *outer* scope's stamps stale. The two current sites survive
only because a `draw.Renew()` happens to follow
(`FramebufferTest.cpp:1453` after the `Clear` scope at `:1439-1441`). The header's guidance
at `:38-42` documents `Renew()` as "a second verb of the same kind begins" and nested scopes
as the way to handle another class, but never states the interaction. A future author who
nests a scope and then calls the outer helper again gets an abort. One sentence in the
`Renew()` comment would fix it.
2. **The exit fill makes the next poison Fatal name a misleading verb.**
`ScopedPipeVerb.h:74` picks `MGPipeVerb::GetGpuTimestampNs` as the leave-verb, so a test that
forgets a declaration *after* another scope closed aborts with
`Fatal{UnmigratedPipeInput, "<Field>@GetGpuTimestampNs"}` — reading as "a timestamp query
tried to read this", which is not what happened. The implementer's own demo A produced the
clearer `@<none>` only because that test declared nothing at all. A dedicated
"left the verb" sentinel (or documenting the string) would keep the diagnostic honest.
3. **Only one of the verb classes that reach each helper is exercised.**
`SanityTest.cpp:3025,3046` declare `DrawArrays` for `SyncNeccessaryTextures()`, but that
helper is reached in production from `kReadback` too — `DirectGLES.cpp:9020` (`ReadPixels`)
and `:9191` (`GetTexImage`), among 14 call sites. Choosing the class the helper "belongs to"
is exactly what F3 asks for, and the retrace / integration lanes are the oracle for the other
classes (that is F1's job), so this is not a defect — but the result file's table reads as if
the declared verb were the only class involved, and it is not.
4. **The known `GetFramebufferBindingSlotFast` poison gap is what produced the pre-fix push
SEGFAULT, and it is worth recording.** `DirectGLES.cpp:147-157`: when the block is unfilled,
`MGB_CTX_IDENTITY` is `nullptr` and `g_fbSlotCacheContext` is also `nullptr`, so the cache is
treated as warm and `*g_fbSlotCache[target]` dereferences null. This is the documented D4
gap (brief line 151, closed in P2), not this package's, but it means a push-build test that
reaches that path can crash or read a stale slot without the poison ever firing — the one
place where "green" is not proof. No such test exists today (§2.2).
5. **A brief-vs-tree fact, recorded per the standing instruction.** The brief (G3) calls
`integration-verify` "the 742-entry lane" and `P1-LANE-FINDINGS.md` says 878 integration
entries; the tree says **818** `integration-verify` and 1696 `integration-gpu`
(`ctest --test-dir build-verify -N -L <label>`), identical in `~/w7/pipe/build-verify`. The
result file's 818 is right; the brief's number is stale. Tree wins.
6. **The result file under-reports its own strongest evidence.** It does not mention that the
verify build's unit lane is also green with the comparator actually armed
(`MOBILEGL_PIPE_VERIFY=1`, §2.3). That is the result that rules out mis-placed declarations,
and it belongs in the record.
## 4. Declared deviations — all six check out
1. tree `~/w7/p1-fix-tests` (confirmed: `~/w7/fix-tests` does not exist; branch, branch point
`97b997d5` and ownership are as briefed);
2. the three build dirs did not exist and were configured by the implementer (confirmed
equivalent to `~/w7/pipe`'s, §1.2);
3. `-DMOBILEGL_BUILD_BENCHMARK=OFF` (confirmed present in all three reference caches);
4. `~/w7/p1-before-ctest-names.txt` as the `-N` baseline (I additionally diffed against the
integrated tree: 0/0);
5. the extra wrong-verb demonstration (I reproduced an independent, stronger variant);
6. the destructor's re-arm as an unbriefed design choice (reproduced; see minors 1 and 2).
No undeclared deviation found. F1, F2 and F4 are untouched, as stated; the
`integration-verify` lane and the 79-case retrace were not run here, which is correct — they are
red for F1/F2 reasons this package does not own, and nothing in this diff can reach them
(no `MG_IntegrationTest/**`, no production source, no fill table).
@@ -1,214 +0,0 @@
# P1 package `fix-tests` — F3 only (`p1/fix-tests`, WSL `~/w7/p1-fix-tests`)
Branch `p1/fix-tests`, branched from the integrated `feat/disaggregated @ 97b997d5`.
Owns finding **F3** and nothing else. F1, F2 and F4 belong to other packages and were not touched.
## Commits
| sha | subject |
|---|---|
| `c67c47f7` | `[Test] (Pipe): let a test that drives a backend helper directly declare the verb it stands in` |
Diff against the branch point (`git diff --stat 97b997d5..HEAD`):
```
MobileGL/MG_Test/Framebuffer/FramebufferTest.cpp | 30 ++++++++-
MobileGL/MG_Test/SanityTest.cpp | 18 +++++-
MobileGL/MG_Test/ScopedPipeVerb.h | 78 ++++++++++++++++++++++++
MobileGL/MG_Test/Texture/TextureTest.cpp | 7 +++
4 files changed, 128 insertions(+), 5 deletions(-)
```
Nothing outside `MobileGL/MG_Test/**` was touched; `MG_Test/Pipe/**` (the other package's) was
not touched either; no production source, no `scripts/`, no CMake, no fixture.
`MG_IntegrationTest/**` was not touched: the helper is a header-only class used only by unit
targets, and every one of them already has `${MGL_ROOT}/MobileGL` on its include path, so no
shared helper needed to move there.
## What the fix is
`MobileGL/MG_Test/ScopedPipeVerb.h` — `MobileGL::MG_Test::ScopedPipeVerb`, an RAII
"this test is standing inside verb X" object:
* **ctor** calls the real `MG_Pipe::MGPipeFillForVerb(verb)` — the exact call `MG_Impl` makes
before that verb reaches a backend. Nothing is bypassed and nothing is faked: the block is
filled out of the live `GLContext` through the production filler.
* **`Renew()`** re-fills the same verb, for a test that drives the helper again after moving
frontend state — that is what a second GL entry point's `MGP_FILL` would have done.
* **dtor** re-arms the poison with a `kQuery` fill (`GetGpuTimestampNs`, whose class mask is a
single field): the serial bumps and every field the scope stamped goes stale. This matters
when the suite runs as one process — a developer running `./FramebufferTest` directly rather
than one ctest entry per case — where otherwise one case's declaration would silently cover a
later case that forgot to make one.
* **pull build**: the whole class body is `#if MOBILEGL_PIPE_PUSH`-gated to nothing; the
constructor still takes an `MGPipeVerb` (the enum is unconditional in `MGPipe.h`), so call
sites are identical in both arms and cannot rot.
Placement rule, copied from the brief's D7 for `MGP_FILL`: the declaration goes **immediately
before** the backend call, after every frontend mutation that call is meant to see. A call of a
different verb *class* in the same test gets its own nested scope (`{ ScopedPipeVerb clear(Clear); ... }`).
It is not a global escape hatch, the poison was not weakened, and no test name changed.
### Where it is used (exactly the eleven failing entries, no others)
| file | test | verb declared | why |
|---|---|---|---|
| `MG_Test/SanityTest.cpp` | `DirectGLESSanity.BindsAMultisampleTextureDespiteTheDefaultMipmapFilter` | `DrawArrays` | `DirectGLES::BindCurrentTextures()` is the per-draw unit walk — it reads `GetProgramForDraw`, a `kDraw`-only field |
| `MG_Test/SanityTest.cpp` | `DirectGLESSanity.BindingZeroClearsPreviousNativeTextureBinding` | `DrawArrays` + 2×`Renew()` | three walks = three draws; the third stands after `glBindTexture(GL_TEXTURE_2D, 0)` |
| `MG_Test/SanityTest.cpp` | `DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture` | `DrawArrays` + `Renew()` | `SyncNeccessaryTextures()` is the per-draw unit walk; the second fill re-reads the memo keys, which is the premise (the silent slot swap moved none of them) |
| `MG_Test/Framebuffer/FramebufferTest.cpp` | `DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff` | `DrawArrays` | `SyncRenderState(forColorClear=false)` is the draw arm |
| `…` | `ClearIntoAWidenedDrawBufferKeepsAlphaWritableAndSubstitutesOne` | `DrawArrays`, then `Clear` (nested scopes) | the test issues a draw then a clear; the clear half must go through on `kClear`'s fill set alone |
| `…` | `ApplicationAlphaMaskOffIsStillHonouredOnANativeDrawBuffer` | `DrawArrays` | |
| `…` | `DualSourceBlendFactorsReachTheDriverWhenTheExtensionIsThere` | `DrawArrays` | |
| `…` | `DualSourceBlendIsDeclinedRatherThanThrownWhenTheExtensionIsMissing` | `DrawArrays` + `Renew()` | two draws, `glBlendFunc` between them |
| `…` | `DualSourceFactorsAreDeclinedEvenWithBlendingDisabled` | `DrawArrays`, `Clear` (nested), `Renew()` | draw / clear / draw |
| `…` | `DualSourceFactorsWithBlendingDisabledStillReachACapableDriver` | `DrawArrays` | |
| `MG_Test/Texture/TextureTest.cpp` | `TextureTest.StorePackedWordsToClientCopiesWordsVerbatimUnderPackParams` | `ReadPixels` + `Renew()` | `ReadbackImpl::StorePackedWordsToClient` reads the PACK block; two stores with `glPixelStorei` between |
## Verification — commands and actual output
Environment: WSL Arch, `CCACHE_BASEDIR=/home/swung/w7`, `cmake --build <dir> -j 12`.
### Baseline before the fix (the failure being fixed, reproduced)
```
ctest --test-dir build-linux -L unit --no-tests=error -j 8 # rc=0 100% passed, 0 failed out of 1482
ctest --test-dir build-push -L unit --no-tests=error -j 8 # rc=8 99% passed, 11 failed out of 1482
ctest --test-dir build-verify -L unit --no-tests=error -j 8 # rc=8 99% passed, 11 failed out of 1482
```
The 11 in both are exactly the F3 list (push reports `Failed` / one `SEGFAULT` — that build has
`MOBILEGL_PIPE_POISON == 0`, so it reads the *zeroed* block rather than aborting; verify reports
`Subprocess aborted`, the poison Fatal).
### (a) `ctest -L unit` green in all three build dirs
```
cmake --build build-linux -j 12 # 0
cmake --build build-push -j 12 # 0
cmake --build build-verify -j 12 # 0
ctest --test-dir build-linux -L unit --no-tests=error -j 8 # rc=0 100% tests passed, 0 tests failed out of 1482
ctest --test-dir build-push -L unit --no-tests=error -j 8 # rc=0 100% tests passed, 0 tests failed out of 1482
ctest --test-dir build-verify -L unit --no-tests=error -j 8 # rc=0 100% tests passed, 0 tests failed out of 1482
```
Additionally, each affected binary run **whole, in one process** (verify build), which is the
case the destructor's re-arm protects:
```
build-verify/MobileGL/MG_Test/SanityTest exit=0 82 tests, PASSED
build-verify/MobileGL/MG_Test/Framebuffer/FramebufferTest exit=0 61 tests, PASSED
build-verify/MobileGL/MG_Test/Texture/TextureTest exit=0 192 tests, PASSED
```
### (b) pull build symbol-identical to `~/w7/p1-before-libMobileGL.so`
```
python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so \
--after build-linux/libMobileGL.so --threshold 0
```
```
symbol-report: before: /home/swung/w7/p1-before-libMobileGL.so (19100448 bytes on disk)
symbol-report: after : build-linux/libMobileGL.so (19100448 bytes on disk)
symbol-report: .text 10792579 -> 10792579 (+0, +0.000%)
symbol-report: .data 76824 -> 76824 (+0) .bss 1296872 -> 1296872 (+0) .rodata 1534938 -> 1534938 (+0)
symbol-report: 27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed
```
**0 / 0 / 0 / 0, `.text` delta 0.** (Expected by construction: the library contains no test code,
and the helper is a no-op header in the pull build anyway.)
### (c) `ctest -N` name diff
```
ctest --test-dir build-linux -N > ft-N-raw.txt
sed -n 's/^ *Test *#[0-9]*: //p' ft-N-raw.txt | sort > ft-names-linux.txt # 2360 names
comm -23 ~/w7/p1-before-ctest-names.txt ft-names-linux.txt | wc -l # 0 removed
comm -13 ~/w7/p1-before-ctest-names.txt ft-names-linux.txt # 26 added
```
**Zero removed.** The 26 additions are all from the earlier P1 packages, already merged at the
branch point — **this package adds none**:
`DirectGLES|DirectVulkan.PipeVerifyArmingScenario.{Armed,CorruptedFieldIsReported}`,
`DirectGLES|DirectVulkan.PoisonOmissionScenario.{OmittedFieldAbortsOnThatVerb,TheSequenceThePoisonControlsRun,WithoutOmissionCompletes}`,
`PipeCatalogue.{FloatVectorsCompareBitwise,SixValueStructsHaveFieldLists,StickyFieldsAreExactlyTheSeven,VerbTableIsTheFunctionTable}`,
and the 13 `PipeInputsTest.*`.
`build-push` registers the same 2360 names (0 added / 0 removed vs `build-linux`). `build-verify`
registers 3178 — the extra 818 are the `integration-verify` lane that `MOBILEGL_ITEST_REQUIRE_GPU=ON`
turns on in that configuration, not a name change from this package.
### (d) the helper is not a rubber stamp
Two mutations of `FramebufferTest.DrawIntoAWidenedDrawBufferReachesTheDriverWithAlphaWritesMaskedOff`
in `build-verify`, run directly with `MOBILEGL_LOG_FILE_PATH` set so the Fatal line is readable:
**A — the declaration deleted from that one test:**
```
exit=134 (SIGABRT)
Fatal{UnmigratedPipeInput, "GetRenderStateParametersVersion@<none>"}
```
**B — the declaration kept but naming the WRONG verb class** (`GenerateMipmap`, `kTextureOp`,
whose mask does not carry the render-state fields):
```
exit=134 (SIGABRT)
Fatal{UnmigratedPipeInput, "GetRenderStateParametersVersion@GenerateMipmap"}
```
B is the stronger half: the helper does not make the poison tolerant, it fills exactly the named
verb's class mask, and a read outside that mask still aborts naming the field **and the declared
verb**. The fill table remains the only thing that says what a verb may read.
**C — restored:** `exit=0`, `100% tests passed, 0 tests failed out of 1`, and
`git status --porcelain -- MobileGL/ scripts/ CMakeLists.txt` is empty. The tree ends clean at
`c67c47f7`.
## F2 enumeration
Not owned by this package. F1, F2 and F4 were not touched.
## Deviations
1. **Tree path.** The tree is `~/w7/p1-fix-tests` (the task text said `~/w7/fix-tests`). Branch,
branch point (`97b997d5`) and ownership are as briefed. Tree wins.
2. **The three build directories did not exist.** The task said the tree has `build-linux`,
`build-push` and `build-verify` configured; it had none. I configured them myself, copying
`~/w7/pipe`'s cache flags exactly (Release, `/usr/sbin/clang++`, ccache, INFO, `BUILD_TEST=ON`,
`BUILD_INTEGRATION_TEST=ON`, mesa EGL vendor + lavapipe ICD, `CMAKE_POLICY_VERSION_MINIMUM=3.5`;
`MOBILEGL_PIPE_PUSH=ON` for push, `MOBILEGL_PIPE_VERIFY=ON` + `ITEST_REQUIRE_GPU=ON` for verify,
matching `~/w7/pipe/build-verify`).
3. **`-DMOBILEGL_BUILD_BENCHMARK=OFF` added to all three configures.** With benchmark left at its
default the configure dies in the fetched Google Benchmark sub-project
(`CMake Error: CMAKE_CXX_COMPILER not set, after EnableLanguage` from its
`check_cxx_compiler_flag`). `~/w7/pipe`'s three caches all carry
`MOBILEGL_BUILD_BENCHMARK:BOOL=OFF`, so this reproduces the reference trees rather than
changing anything. A transient `googletest` clone failure on the first `build-verify` configure
was cleared by deleting `build-verify/_deps/googletest-{src,subbuild}` and re-configuring.
4. **`ctest -N` baseline.** Acceptance (c) does not name a baseline file; I used
`~/w7/p1-before-ctest-names.txt` (the pre-P1 list), which is the strictest available and shows
both that nothing was removed and that the only 26 additions in the whole lane came from the
other packages.
5. **Extra demonstration.** Acceptance (d) asks only for the removal case (A). I also ran the
wrong-verb case (B), because "removing it makes the test fail" alone would also hold for a
rubber stamp that filled everything; B is what shows it does not.
6. **Destructor behaviour is a design choice not spelled out in the brief.** Leaving the scope
re-arms the poison (a one-field `kQuery` fill) rather than doing nothing. Rationale in the
header and above; without it a single-process run of a test binary would let one case's
declaration cover a later case that made none. It uses only the public filler — no production
source, and no new API, was needed.
## Unfinished
Nothing in this package's scope.
Out of scope, and still open where the findings file left them: **F1** (missing `FillPoints.def`
rows), **F2** (`GetSamplingResolutionGeneration` going stale mid-verb) and **F4** (the
integrator's command corrections). Consequently the `integration-verify` lane and the 79-case
verify retrace were **not** run here — they are still red for F1/F2 reasons this package does not
own, and running them would prove nothing about F3. The only lane this package's change can
affect is `ctest -L unit`, which is green in all three build directories.
@@ -1,87 +0,0 @@
# P1 package C — p1-magma — adversarial review v1
Reviewed: tree `~/w7/p1-magma`, branch `p1/magma` @ `fad8636cb6b423e27c815ca2e4ed2b6ef8613e92` (one commit on top of `p1/contract` = `bf86b1ed`). Result file: `scratchpad/wf4/p1-results/magma-v1.md`. Brief: `scratchpad/wf4/BRIEF-P1.md` sections A, B (D1–D13), C.3, C.5, D, E.
Review scripts and the saved patch: `scratchpad/wf4/magma-review/` (`01_state.sh` … `11_check.sh`, `classify.py`, `ppscan.py`, `dbgcompile.py`, `diff.patch`). Logs: `~/w7/p1-magma-review-*`. The tree was left exactly as found (`git status --porcelain` shows only the untracked `build-linux/`, `build-push/`); the only thing I created is my own throwaway worktree `~/w7/p1-magma-review-int` (detached merge of `p1/magma` + `p1/core`, see §5), which is not a package tree.
**Verdict: approved — zero majors, four minors (none code-affecting).**
---
## 1. What I re-ran independently (every gate reproduces)
| check | command (WSL, `~/w7/p1-magma`, `CCACHE_BASEDIR=/home/swung/w7`) | observed |
|---|---|---|
| tree state | `git log --oneline p1/contract..HEAD`; `git status --porcelain` | exactly one commit `fad8636c`; clean apart from `?? build-linux/ build-push/`; both build dirs are Release / INFO / clang / LTO off, `build-push` has `MOBILEGL_PIPE_PUSH=ON`, `build-linux` OFF/OFF |
| G1 symbol report | `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --markdown ~/w7/p1-magma-review-symbols.md --json ~/w7/p1-magma-review-symbols.json` (after `cmake --build build-linux -j 12` → `ninja: no work to do`) | `Total 17209819 -> 17209819 (+0)`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 normalised names, 27060 unchanged` |
| G1 raw | `readelf -S` `.text` size before/after; `nm --defined-only` name-set diff | `0xa4ae83` both; diff `0` lines |
| G2 (owned files) | `grep -rc "pGLContext" MobileGL/MG_Backend \| sort \| diff ~/w7/p1-before-pglcontext.txt -` | the only differences are the eight DirectVulkan files going `4/47/23/1/1/3/2/129 → 0` plus the two contract-created `MG_Backend/MGPipe/PipeInputs.{h,cpp}:0` lines; **every DirectGLES count is unchanged** (`DirectGLES.cpp:92, Managers.cpp:23, MultiDraw.cpp:5, Utils.cpp:2`) — no site outside the package's ownership was touched |
| ownership | `git diff --name-only p1/contract..HEAD` | exactly the eight files of C.5's Magma row; nothing else |
| gen_pipe | `python3 scripts/gen_pipe.py --check` | `generated files are up to date`, rc=0 (`--self-test` does not exist on the contract; it is core c5) |
| unit (pull) | `ctest --test-dir build-linux -L unit -j 12 --no-tests=error` | `100% tests passed, 0 tests failed out of 1466` |
| unit (push) | `ctest --test-dir build-push -L unit -j 12 --no-tests=error` | `100% tests passed, 0 tests failed out of 1466` |
| G7 name diff | `ctest -N \| grep -E "^\s+Test +#" \| sed -E "s/^ *Test +#[0-9]+: //" \| sort` vs `~/w7/p1-before-ctest-names.txt` | `after=2334 before=2334 removed=0 added=0` |
| DV integration lane (pull) | `ctest --test-dir build-linux -L integration-gpu -R '^DirectVulkan\.' -j 4 --no-tests=error --output-on-failure` | `100% tests passed, 0 tests failed out of 427` on the first `-j 4` pass (the implementer's two `*ArmedWhenTheEnvironmentPinsItOn` flakes did not reproduce for me; `~/w7/pipe-int-itest-failures.txt` confirms the PrimGenReroute one flaked on the baseline too) |
| push arm selected | `nm -C build-push/libMobileGL.so \| grep -c "MG_Pipe::PipeInputs::"` vs pull | `13` vs `0`; `ctest --test-dir build-push -N -L integration-gpu -R '^DirectVulkan\.'` → `Total Tests: 427` (the push build registers the lane) |
| include gate | `python3 scripts/check_include_closure.py --mode both --compiler clang++ --self-test --require-all` | rc=0, `6 negative-control trip(s)`, `3 probes, 0 skipped, 0 problem(s)` |
## 2. Independent classification of every changed line (`classify.py` over `git diff -U0 p1/contract..HEAD`)
The script pairs each removed line with its added line and accepts a pair only if applying the D9 rewrite set verbatim to the old line yields the new line; anything else is reported.
```
categories: {'guard': 2, 'arrow': 160, 'assert': 43, 'nenull': 2, 'arrow+ternary': 1, 'eqnull': 1}
pure additions: 7 (the seven #include <MG_Pipe/PipeInputsSwitch.h> lines)
BAD count: 1 (VertexInputStateFactory.h:133 — the comment rewrite, the one intentional non-mechanical line; text equals D9's wording)
arrow occurrences per file: BackendObject 2, DirectVulkan 12, UniformManager 14, VkClearManager 1, VkRenderPassManager 3, VkTextureManager 2, VulkanRenderer 130 — total 164 on 161 lines
non-arrow lines: 49 (DirectVulkan.cpp 34 asserts + 1236 + 1284; UniformManager 9 asserts; VulkanRenderer 11267, 12766; BackendObject 388, 786) + the comment
```
- Old line numbers of every arrow line and every non-arrow line equal the C.3 / D9 lists **verbatim** (printed by the script; compared list-for-list, including the three double-occurrence lines `VulkanRenderer.cpp:9214,10719,10963`).
- No pure deletions; no reordering; `git diff --check` clean; CR count 0 in all eight files (LF preserved).
- `PipeStats` `AddCalls` literals and every other non-`pGLContext` line untouched (follows from the classification: the only non-rewrite changes are the 7 includes and the comment).
- D10: `SyncPersistentMappedRange` / `SyncGpuWrites` counts identical to `~/w7/pipe` per file (`DirectVulkan.cpp 3/0, UniformManager.cpp 1/0, VulkanRenderer.cpp 6/3, VkBufferManager.cpp 2/0` = 12 + 3).
- Include placement matches D3: line immediately after the single `"MG_State/GLState/Core.h"` include in the six TUs that have one (`DirectVulkan.cpp:13`, `BackendObject_DirectVulkan.cpp:15`, `UniformManager.cpp:13`, `VkClearManager.cpp:16`, `VkTextureManager.cpp:14`, `VulkanRenderer.cpp:17`); `VkRenderPassManager.cpp:16` closes its only include block (`:10-15`) and is followed by the blank line and the namespace.
- The six code-bearing rewrites, verbatim (`git diff -U1`):
```
BackendObject_DirectVulkan.cpp:388,786 - if (MG_State::pGLContext) { → if (MGB_CTX_LIVE) {
DirectVulkan.cpp:1236 - !query->pausedPrimitivesCountedByGpu && MG_State::pGLContext != nullptr) { → && MGB_CTX_LIVE) {
DirectVulkan.cpp:1284 - MG_State::pGLContext ? MG_State::pGLContext->GetTransformFeedbackPausedPrimitiveCounter() : 0 → MGB_CTX_LIVE ? MGB_CTX->...() : 0
VulkanRenderer.cpp:11267 - if (!m_transformFeedbackFeatureEnabled || MG_State::pGLContext == nullptr || → || !MGB_CTX_LIVE ||
VulkanRenderer.cpp:12766 - return (MG_State::pGLContext != nullptr && → return (MGB_CTX_LIVE &&
```
Operand order, short-circuit order and the accessor called are unchanged in every one; no null check dropped, no `const&` lost (the sed cannot touch the left-hand side of a site and the classification proves nothing else moved).
## 3. Hunts that came back empty
| hunt | method | result |
|---|---|---|
| site left behind a macro / in a shared file | `grep -rn pGLContext MobileGL/MG_Backend` minus DirectGLES; `grep -rn "MG_State::"` in DirectVulkan excluding `MG_State::GLState` | nothing; no other `MG_State::` global is read |
| site inside a preprocessor region the INFO push build never compiled | `ppscan.py`: every `MGB_CTX` line with a non-empty `#if` stack | none — all 210 lines are unconditionally compiled, so the push build's successful compile checks every site |
| DEBUG-level build breaks on the new spelling (`MOBILEGL_ASSERT` becomes real) | `dbgcompile.py`: each of the seven TUs compiled `-fsyntax-only` with `-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_DEBUG` from `compile_commands.json`, in both `build-linux` and `build-push` | 14/14 `rc=0 errors=0` |
| an accessor Magma names that the D12 `scan_live_accessors()` gate (core c5) will reject | set of `MGB_CTX->(\w+)` in DirectVulkan (56 names) `comm -23` the `X(...)` rows of `MG_Pipe/Coverage.def` (65 rows) | empty — every one of the 56 has a row; the seven F-class names are among them, as the brief expects |
| the push-arm guards change meaning (`MGB_CTX_LIVE` at backend init before any fill) | read the contract: `PipeFill.cpp:33 Bool PipeInputs::IsLive() const { return LiveContext() != nullptr; }` | `MGB_CTX_LIVE` under push is literally `pGLContext.get() != nullptr`, so `BackendObject_DirectVulkan.cpp:388/786` (`InvalidateCompileEnv` at init) and the four XFB guards keep today's meaning in both arms; the brief's D9 [deviation] paragraph ("always true once a context exists") is true of the contract as landed |
| lane green because it never ran | `ctest -N` counts in both builds; `LastTest.log` timestamps | 427 entries registered and executed in both arms |
| other packages' trees touched | heads of `~/w7/p1-espryt` (`f4ea84c5`), `~/w7/p1-verify` (`bf86b1ed`), `~/w7/pipe` (`087685d1`, clean apart from its own untracked build dirs) | untouched by this package |
## 4. Where the tree and the brief disagree (and what wins)
- None on counts: every per-file count, line list and the D10 site count re-measured on the tree equals the brief and the result file.
- `ctest -N` recipe: the brief's `grep -E '^\s+Test #'` matches only four-digit test numbers on this ctest (`Test #1:` / `Test #999:` / `Test #1000:`). The baseline file `~/w7/p1-before-ctest-names.txt` nevertheless holds all 2334 clean names (verified head/tail and `grep -c '^ *[0-9]*:'` = 0), so the integrator's `wsl_p1_verify_all.sh` (which already uses `Test +#`) is fine; the brief's recipe line in D.1/C is the stale one. The implementer recorded this correctly.
- D13 says commits are single-line; the orchestrator's commit rule (subject + `- ` bullets) wins for this package and the implementer declared it. Subject line equals C.3's prescribed text character for character.
## 5. The one verification the result file could not do: the push-build DV lane on a tree that fills
The pull-build lane is a tautology for this package (the `.so` is byte-identical to the baseline), so the only functional test that can fail because of Magma's converted sites is the push build with core's filler and fill points present. The implementer deferred it because `p1/core` had no commit past the contract at 01:49; by review time `p1/core` was at `275dd3ed` (c2 filler `3aa4d8af`, c3 83 fill points `a196ada4`, c4 comparator `275dd3ed`), touching no Magma-owned file, `git merge-tree` conflict-free. I built a throwaway detached merge (`~/w7/p1-magma-review-int` @ `72a919b2`, `p1/magma` + `p1/core`, same configure flags as `wsl_p1_tree.sh`) and ran:
MERGED_RESULTS_PLACEHOLDER
## 6. Minors (none require a code change in this package)
1. **Push-smoke abort mischaracterised.** The result file (item 11) reports the contract-tree push lane as `12/12 Subprocess aborted` and calls it the expected poison. The actual reason (reproduced: `MOBILEGL_LOG_FILE_PATH=... ctest --test-dir build-push -R '^DirectVulkan\.OrientationScenario\.OffscreenPassRendersUpright$'`) is `[FATAL]: MGPipe: Fatal{UnmigratedPipeInput, "GetFramebufferBindingSlot@<none>"}` — the **unconditional** null-storage check in `PipeInputs.h:475-478` (`m_bufferBindingSlot[index] == nullptr → MGPipeInputPoisonFatalForVerb`), fired before any fill point ran (`@<none>`; c3 was not on the contract). The D6 per-verb poison (`MGP_INPUT_CHECK`) is compiled out in a Release/INFO push build (`MOBILEGL_PIPE_POISON 0`, `PipeInputs.h:25`), so "the poison working" (C.2 step 4's wording, which C.3 inherits) is not what was observed. Harmless for this package; worth correcting in the brief's C.2/C.3 text so the integrator does not read a `<none>` Fatal as a table gap.
2. **The result file's "Lane green by name: 427/427" rests on a serial rerun of two flaky entries** rather than a clean `-j 4` pass. My `-j 4` pass was 427/427 first time; the baseline's failure list carries the same PrimGenReroute entry. Not a package defect, but the integrator's D.1 lane should keep `--rerun-failed -j 1` in its recipe (it already does).
3. **Commit-message form** deviates from D13's single-line rule (declared; the orchestrator's rule wins). No action.
4. **Cross-package observation, not Magma's:** the contract keeps a `Bool m_live` that `MGPipeFillForVerb` writes (`PipeFill.cpp:84,88`) but `IsLive()` ignores it (`PipeFill.cpp:33`). Magma's six `MGB_CTX_LIVE` sites are correct *because* `IsLive()` forwards; if core ever switches `IsLive()` to `m_live`, `BackendObject_DirectVulkan.cpp:388/786` (backend init, before any verb) would silently skip `InvalidateCompileEnv` under push. Record for the core reviewer / integrator.
## 7. Majors
None.
@@ -1,72 +0,0 @@
# P1 package C — p1-magma — result v1
Tree `~/w7/p1-magma`, branch `p1/magma`, branched from tag `p1/contract` (= `bf86b1ed`, the contract commit).
Scripts used: `scratchpad/wf4/magma/00_tree.sh` … `10_pushsmoke.sh`; logs under `~/w7/p1-magma-verify-*`.
## Commits (not pushed)
| hash | subject |
|---|---|
| `fad8636cb6b423e27c815ca2e4ed2b6ef8613e92` | `[Refactor] (Magma): route every frontend read through MGB_CTX - 164 arrow sites sed'd, 49 non-arrow lines converted (43 asserts keep their meaning as MGB_CTX_LIVE); pull build byte-identical` (+ `- ` bullets: per-file counts, D9 categories, D10 untouched, the G1 proof) |
`git diff --stat p1/contract HEAD`: 8 files changed, 217 insertions(+), 210 deletions(-) — 210 rewritten lines (the brief's 210) + 7 `#include <MG_Pipe/PipeInputsSwitch.h>` lines.
## What was converted (all under `MobileGL/MG_Backend/DirectVulkan/`)
| file | `MGB_CTX->` (arrow occurrences) | `MGB_CTX_LIVE` | include placed |
|---|---|---|---|
| `DirectVulkan.cpp` | 12 | 36 (34 asserts + `:1236` + the `:1284` ternary) | after `Core.h` (`:13`) |
| `BackendObject_DirectVulkan.cpp` | 2 | 2 (`:388`, `:786` guards) | after `Core.h` (`:15`) |
| `Renderer/UniformManager.cpp` | 14 | 9 (asserts) | after `Core.h` (`:13`) |
| `Renderer/VulkanRenderer.cpp` | 130 (127 lines) | 2 (`:11267` → `!MGB_CTX_LIVE`, `:12766`) | after `Core.h` (`:17`) |
| `Renderer/VkClearManager.cpp` | 1 | 0 | after `Core.h` (`:16`) |
| `Renderer/VkRenderPassManager.cpp` | 3 | 0 | after its include block (`:16`, after `TextureMetrics.h`; never included `Core.h`) |
| `Renderer/VkTextureManager.cpp` | 2 | 0 | after `Core.h` (`:14`) |
| `Renderer/VertexInputStateFactory.h` | — | — | comment `:133` → "they live on the frontend context's VAOs" |
Total 164 arrow occurrences on 161 lines + 49 non-arrow lines = 213 occurrences / 210 lines, exactly D1/C.3. The old line numbers of every removed non-arrow line were extracted from the diff and equal the D9 lists verbatim (DirectVulkan.cpp 337…1008 + 1236; UniformManager 812…1968; VulkanRenderer 11267, 12766; BackendObject 388, 786).
Rewrites used (D9): `MG_State::pGLContext->` → `MGB_CTX->`; `MOBILEGL_ASSERT(MG_State::pGLContext, ` → `MOBILEGL_ASSERT(MGB_CTX_LIVE, `; `MG_State::pGLContext != nullptr` → `MGB_CTX_LIVE` (covers the 9 UniformManager asserts, `DirectVulkan.cpp:1236`, `VulkanRenderer.cpp:12766`); `MG_State::pGLContext == nullptr` → `!MGB_CTX_LIVE`; `if (MG_State::pGLContext) {` → `if (MGB_CTX_LIVE) {`; `MG_State::pGLContext ? ` → `MGB_CTX_LIVE ? ` (`:1284`). No other qualified spelling (`::MG_State::pGLContext`, `MobileGL::MG_State::pGLContext`) existed (checked before the sed).
D10: the 12 `SyncPersistentMappedRange` + 3 `SyncGpuWrites` Magma sites are untouched (token counts 13 / 3 before and after, identical to `~/w7/pipe`). `PipeStats` `AddCalls` literals untouched (no non-`MGB_CTX` line changed besides the 7 includes and the comment — verified by filtering the diff).
## Verification commands and actual output
All in WSL, `~/w7/p1-magma`, `CCACHE_BASEDIR=/home/swung/w7`, build flags = `wsl_p1_tree.sh` (Release/INFO, clang, ccache, tests+itests on, lavapipe ICD, `MOBILEGL_BUILD_BENCHMARK=OFF`).
1. Tree creation `wsl_p1_tree.sh magma p1/contract`: `build-linux build rc=0 [955/955]`, `build-push build rc=0 [959/959]`.
2. `grep -rc "pGLContext" MobileGL/MG_Backend/DirectVulkan | grep -v ":0$"` → **empty** (all 43 files report 0).
3. `cmake --build build-linux -j 12` (after `touch` of the 8 files, to prove the objects come from the converted sources): rc=0, 21 objects rebuilt. Same for `build-push`: rc=0, 21 objects rebuilt.
4. `python3 scripts/symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --markdown ~/w7/p1-magma-verify-symbols.md --json ~/w7/p1-magma-verify-symbols.json`:
```
.text 10792579 -> 10792579 (+0, +0.000%) .data +0 .bss +0 .rodata +0 Total 17209819 -> 17209819 (+0)
27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed
27060 -> 27060 normalised names, 27060 unchanged
```
Run twice (before and after the touch-rebuild): identical. `nm --defined-only` name-set diff: 0 lines. (The `.so` sha256 differs — build path / build-id — sections and symbols do not.) **G1: 0/0/0/0, .text delta 0; no attribution needed.**
5. `ctest --test-dir build-linux -L unit --no-tests=error --output-on-failure` → `100% tests passed, 0 tests failed out of 1466`.
6. `ctest --test-dir build-linux -L integration-gpu -R '^DirectVulkan\.' --no-tests=error -j 4 --output-on-failure` → 425/427 first pass; the two failures, `DirectVulkan.PrimGenReroute.PrimitivesGeneratedNoXfbScenario.TheRerouteIsActuallyArmedWhenTheEnvironmentPinsItOn` and `DirectVulkan.PointSizeDemotion.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnvironmentPinsItOn`, both "quirk pinned ON but the log never reported engaging it". Rerun serially (`-j 1`) twice → **both Passed** each time (0.2–0.3 s). The PrimGenReroute one is also the single entry in the baseline's own failure list `~/w7/pipe-int-itest-failures.txt` (same `-j 4` run shape); the PointSizeDemotion sibling passed in the baseline's run. Both are the "read the log file appended after the case started" scenarios and race under `-j 4`; not a Magma-conversion effect. **Lane green by name: 427/427.**
7. `ctest --test-dir build-push -L unit --no-tests=error` → 1466/1466 (push build compiles, links, and its unit set is green).
8. `ctest -N` name diff: `ctest --test-dir build-linux -N | grep -E '^\s+Test\s+#' | sed 's/^ *Test *#[0-9]*: //' | sort` → 2334 names; `comm -23 ~/w7/p1-before-ctest-names.txt -` → **empty**, `comm -13` → empty.
9. `python3 scripts/check_include_closure.py --mode both --compiler clang++ --self-test --require-all` → rc=0, `3 probes, 0 skipped, 0 problem(s)` (the new backend→`MG_Pipe/PipeInputsSwitch.h` include does not trip the include gate).
10. Push arm really selected: `nm -C build-push/libMobileGL.so | grep -c "MG_Pipe::PipeInputs::"` = 13 vs 0 in `build-linux`; `VulkanRenderer.cpp.o` in `build-push` references `gPipeInputs` (2).
11. Informational only (not a P1-C gate): `ctest --test-dir build-push -L integration-gpu -R '^DirectVulkan\.' -I 1,12 -j 4` → 12/12 `Subprocess aborted`. Expected on the contract tree: the contract's `MGPipeFillForVerb` only bumps the serial and copies no field (C.2 step 4 predicts this run is red until `p1/core` c2+ lands). `p1/core` currently has **no commit beyond `p1/contract`**, so the "rerun after rebasing onto p1/core c2+" half could not be done in this package.
## Where the tree contradicted the brief's numbers
None. Every count re-measured on the tree equals the brief: per-file `pGLContext` counts (4/47/23/1/1/3/2/129 = `~/w7/p1-before-pglcontext.txt`), arrow occurrences (12/2/14/1/3/2/130), the 49 non-arrow line numbers, the three double-occurrence lines (`VulkanRenderer.cpp:9214,10719,10963`), the D10 site count (12 + 3), the Core.h include line numbers used for D3 placement.
One brief-vs-brief detail: the C.3 header says "49 non-arrow lines" and the D9 table for C sums to 35 + 2 + 9 + 2 + 1 = 49 including the comment line — consistent; `:1284` is counted as an arrow line (it takes both the sed and the `MGB_CTX_LIVE ?` rewrite) and is not double-counted.
The brief's `ctest -N` recipe (`grep -E '^\s+Test #'`) misses tests #1–#999 on this ctest (they print as `Test #999`, padded); the baseline file `~/w7/p1-before-ctest-names.txt` has all 2334, so the diff was taken with `Test\s+#` (same `sed`). Noted so the integrator's D.1 step does not report a false 999-name loss.
## Deviations from the brief
None in the code. Process notes:
- The edits were applied while the initial `wsl_p1_tree.sh` build was still running (after `git worktree add`, before `build-linux/` existed); to remove any doubt about stale objects, all 8 files were `touch`ed and both configurations rebuilt (21 objects each) before the final symbol report and test runs.
- The commit message carries the prescribed subject plus `- ` bullets (per the task's commit rules), not the single-line form of D13.
## Unfinished / for the integrator
- The push-build DirectVulkan lane on a tree that actually fills (`p1/core` c2+) has not been run — `p1/core` had no commit past the contract at the time of this result. After the D.1 rebase, `cmake --build build-push && ctest --test-dir build-push -L integration-gpu -R '^DirectVulkan\.' -j 4` is the first thing to run on this branch.
- The two `*ArmedWhenTheEnvironmentPinsItOn` scenarios are `-j 4`-flaky on lavapipe (baseline included); if the integrator's D.1 lane run trips on one, a serial rerun of that entry is the check.
@@ -1,97 +0,0 @@
# Adversarial review — package E `p1-verify` (v1)
Tree `~/w7/p1-verify`, branch `p1/verify` = `1a5252b6`, four commits on `p1/contract` (`bf86b1ed`). Tree left exactly as found (only the untracked `build-*` directories the implementer created; no file touched). Scripts and raw outputs: `scratchpad/wf4/verify-review/s0*.sh`, `~/w7/p1-verify-review/`.
**Verdict: NOT approved — 3 majors.** The package's mechanical gates all hold (G1 0/0/0/0 +0, G2 untouched, G7 additions only, `gen_pipe --check` clean, unit 1466/1466 in all three builds, lane counts as claimed, `trace_cases.py` formats byte-identical to the contract, YAML parses, the `run_trace_case.cmake` block does what it says on synthetic logs). What fails is the CI mode itself: two of its always-on steps cannot pass on the integrated tree for reasons unrelated to the comparator, and the G1 job's gate is weaker than G1's check column.
## 1. What was re-run independently (command → observed)
| # | command (in `~/w7/p1-verify`) | observed |
|---|---|---|
| 1 | `git diff --name-only p1/contract..HEAD` | the 9 files of the result file; every one is in E's row of C.5. Nothing under `MG_Backend/**`, `MG_Test/**`, root `CMakeLists.txt`, `MG_Pipe/**` touched. |
| 2 | `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only the two `:0` lines for `MG_Backend/MGPipe/PipeInputs.{h,cpp}` (files the contract commit created after the baseline was captured; both 0). E converts no site and touches no site. |
| 3 | `python3 scripts/gen_pipe.py --check` | rc 0, `63 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `0 UNMAPPED`, up to date. |
| 4 | `cmake --build build-{linux,push,verify} -j 12` | `ninja: no work to do` ×3 (the trees were current). |
| 5 | `symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` | rc 0; `.text 10792579 -> 10792579 (+0)`, `27799 -> 27799: 0 added, 0 removed, 0 resized, 0 renamed`. G1 holds for E. |
| 6 | `ctest -L unit --no-tests=error -j 12` in build-linux / build-push / build-verify | `100% tests passed, 0 tests failed out of 1466` ×3. |
| 7 | `ctest --test-dir build-linux -N` vs `~/w7/p1-before-ctest-names.txt` | removed 0, added 10 (the five new cases × 2 backends). |
| 8 | `ctest --test-dir build-verify -N -L integration-verify` | 816; `VerifyCorrupted.`/`PoisonOmitted.` = #3157-3160; control-A filter selects 5. |
| 9 | `ctest --test-dir build-linux -L integration-verify --no-tests=error` | `Total Tests: 0`, rc 8 (G8 half two holds). |
| 10 | `ctest --test-dir build-retrace -N -L retrace` | 80. |
| 11 | `grep -ho "MOBILEGL_PIPE_[A-Z_]*=…" build-verify/MobileGL/MG_IntegrationTest/*.cmake \| uniq -c` | 816 `PIPE_VERIFY=1`, 2 each of `CORRUPT`, `FATAL=0`, `POISON_OMIT` — the ambient entries name neither control knob. |
| 12 | `trace_cases.py --format {cmake,names,fixture-files,github-test-matrix}` HEAD vs contract, same directory | all four byte-identical; `github-verify-matrix` = 16 (OpenRA first), `github-test-matrix` = 77. |
| 13 | `python3 scripts/symbol_report.py --self-test` | `OK (2 canned transcripts, 5 buckets, 2 gates)`. |
| 14 | Windows `yaml.safe_load` on a copy (BOM kept) | 17 jobs; `retrace-verify` needs/if as stated; `remove-artifact-clutter` needs `[retrace-summary, retrace-verify]`, `if: always()`; dispatch input `baseline_sha`; `trace-cases` outputs incl. `verify-matrix`; `pipe-gates` has both `--self-test` steps. Job-level diffs of `build-linux-verify`/`retrace-verify`/`integration-verify` against their originals show only the declared changes plus the two items in minors 5 and 6. |
| 15 | `run_trace_case.cmake:150-186` extracted and driven with `cmake -P` against 9 synthetic logs | unset→0, `=0`→0, armed→0, no-arming→1, missing log→1, `Fatal{PipeVerifyDiffer`→1, `Fatal{UnmigratedPipeInput`→1, `=true` armed→0, `Fatal{PipeVerifyBadKnob}`→0 (see minor 7). |
| 16 | `ctest --test-dir build-verify -R 'DirectGLES\.Verify\.PipeVerifyArmingScenario\|VerifyCorrupted\.\|PoisonOmitted\.\|Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes'` | `Armed` ***Failed, `CorruptedFieldIsReported` (ambient) ***Skipped, both `WithoutOmissionCompletes` Passed (0.43/0.45 s), the four control entries ***Failed — exactly the contract-tree shape the result file reports. |
| 17 | `python3 scripts/gen_pipe.py --self-test` | `unrecognized arguments: --self-test`, rc 2 — the declared dependency on core c5. |
## 2. Majors
### M1 — Both `nm -D` gates can never pass: the Release library exports no MGPipe symbol
`test.yml:393-394` (`build-linux-verify`, "The verify library really carries the comparator") and `test.yml:1161` (`retrace-verify`, "Unpack the VERIFY runtime") do `nm -D --defined-only libMobileGL.so | grep -q MGPipeVerifyInputs` / `MGPipeFillForVerb`. The library is built with `CXX_VISIBILITY_PRESET hidden` for every non-Debug configuration (root `CMakeLists.txt:600-604`), and the MGPipe functions are plain namespace functions with no export attribute, so they are local (`t`) symbols and absent from the dynamic table. Reproduced on this tree with the symbol that already exists:
```
$ nm -D --defined-only build-verify/libMobileGL.so | grep -c MGPipeFillForVerb
0
$ nm --defined-only build-verify/libMobileGL.so | grep MGPipeFillForVerb
0000000000648d80 t _ZN8MobileGL7MG_Pipe17MGPipeFillForVerbENS0_10MGPipeVerbE
$ nm -D --defined-only build-verify/libMobileGL.so | grep -c MGPipe # 0 of 11930 exported
0
```
Consequence: `build-linux-verify` fails at its check step on every run, so `integration-verify` (needs it) never runs and every `retrace-verify` matrix entry is skipped by its `if:`. The result file attributes the red to "`MGPipeVerifyInputs` does not exist until core c4"; that is not the failure mode — `MGPipeFillForVerb` exists today and is not found either, and c4 will not change visibility. The fix is one of: `nm --defined-only` (static symtab; the runtime artifact is not stripped, and the job's own MG_Remote check at `test.yml:1301` already uses that spelling), or an exported probe symbol. The step's comment ("a typo'd -D is not an error in CMake") is right about the need; the spelling defeats it.
### M2 — "Every verify process really armed" (`test.yml:504-517`) reds a healthy lane, and checks one process, not every process
The step greps `pipe-verify-*.log` for the arming line after the whole lane ran. Each ambient lane shares ONE log path (`MG_IntegrationTest/CMakeLists.txt:670,674`), and the library opens it with `fopen(path, "w")` at each process's first write (`MG_Util/Debug/Log.cpp:73`), so after 406 serial processes the file holds only the LAST one. The last registered entry of each ambient lane is `PoisonOmissionScenario.WithoutOmissionCompletes` (ctest #2750 / #3156, from `ctest -N -L integration-verify`), and that parent process never reaches a verb: its body is `Ready()` → `fork()+execve()` → `waitpid` → read files (`PoisonOmissionScenario.cpp:344-380`); the fixture `SetUp` brings up a context via `RunEglBringUp` (`HeadlessGL.cpp:205-297`: EGL + `glGetString`), which contains no `GLFunctionsTable` verb; the child writes its own `.poison-child` file. Arming is latched at the first fill (brief D8), so this process's log carries no arming line by construction.
Reproduced on this tree:
```
$ ctest --test-dir build-verify -R '^DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes$' # rc 0
$ wc -l < build-verify/MobileGL/MG_IntegrationTest/pipe-verify-DirectGLES.log
317
$ head -1 …/pipe-verify-DirectGLES.log ; tail -1 …/pipe-verify-DirectGLES.log
[…] Initializing MobileGL...
[…] MobileGL closing...
```
(317 lines of init/teardown; no verb, no fill.) On the integrated tree, therefore, `grep -q "MGPipe: verify armed"` on this file fails and the step exits 1 — a red for a non-reason, immediately after a green 816-entry run. Even with a different last entry the step proves only "the last process armed", not "every process" (the name and the `::error::` text both claim the latter); the per-process proof is `PipeVerifyArmingScenario.Armed`, which already exists. Either drop the step or make it honest (e.g. count processes that wrote, or give the arming entry its own log path and grep that).
### M3 — The G1 gate (`symbol_report.py:244-257`) passes a shrunk `.text` and resized symbols; G1's check column requires all four buckets and the delta to be zero
Brief A/G1: "`added == removed == resized == renamed == 0`, `.text` delta 0". `gate_failures` has no `resized` (or `renamed`) input at all, and `--fail-on-added-bytes N` fires only on `text_delta > N`:
```
$ python3 -c "import sys; sys.path.insert(0,'scripts'); import symbol_report as s
print(s.gate_failures([], [], -4096, 0, True)); print(s.gate_failures.__code__.co_varnames[:5])"
[]
('added', 'removed', 'text_delta', 'fail_on_added_bytes', 'fail_on_symbol_set_change')
```
So `monolith-symbol-report` with `--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` (`test.yml:1306-1314`) is green for a P1 tree whose pull `.text` shrank, or whose functions were resized with zero net delta (the exact shape the D9 guard rewrites can produce: "if `.text` moves, this is the first suspect"). Deviation 10 says "`--threshold` already governs resizes" — it governs only the report's bucket (`bucket(..., args.threshold)`), never the exit code. A one-line fix each: `text_delta != 0` under `--fail-on-added-bytes 0` (or a `--fail-on-text-delta`), and `resized` in the set-change gate; the self-test then needs the corresponding canned cases. (With `--strip-scope`, renames are additionally folded out of `added`/`removed` at `:193` and escape the gate; the CI invocation does not pass it, so that is a documentation hole only.)
## 3. Minors
1. **The re-exec'd child discards its own assertions.** `PoisonOmissionScenario.cpp:274-281`: `RunSequence(); … _exit(0);` unconditionally. If `ASSERT_NE(program, 0u)` (`:142`) or any earlier fatal assertion fires, `RunSequence` returns before the draw and the mipmap, the child exits 0, and `WithoutOmissionCompletes` passes without the sequence having run (the `"Fatal{"`-absent check is satisfied by any log). Fails safe for CI control B (it would go red), but the G5 sibling entry can pass vacuously. `_exit(::testing::Test::HasFailure() ? 1 : 0)` and a `glGetError()` after the mipmap close it.
2. **Shared per-lane log vs the brief's `-j 4`.** D.3 runs `ctest -L integration-verify -j 4`; with 405 siblings truncating `pipe-verify-<backend>.log`, `Armed`'s whole-file arming grep (`PipeVerifyArmingScenario.cpp:184`) can miss the line during a neighbour's bring-up window (false red), and `LibraryLogSince(before)` (`:192`) reads past EOF after a truncation (Fatal scan vacuous — harmless only because `FATAL=1` aborts anyway). The existing lanes that use this idiom are one-entry filtered lanes. `RUN_SERIAL`/`RESOURCE_LOCK` on the ambient registrations, or a dedicated arming lane with its own log, removes the race; at minimum the result file should tell the integrator to run the arming entries serially.
3. **`remove-artifact-clutter` sweeps a failed verify retrace's fixture.** `test.yml:1364` keeps `trace-fixture-*` only for jobs whose name `startswith("retrace (")`; `retrace verify (…)` failures are not consulted. The `needs:` was extended; the keep-on-failure filter was not (the commit message implies both).
4. **Retrace negative control overwrites the verified run's output.** `test.yml:1188-1200` reruns OpenRA into the same case directory; `Upload actual image` (`:1209`) then ships the corrupted run's images for OpenRA/DirectGLES.
5. **`integration-verify` drops the second `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` pass** the `integration` job runs (`test.yml:290-294`), so the staged-copy upload tier is never exercised under the comparator. Not required by C.4; worth a sentence in the docs so "every integration entry" is not read as "every configuration".
6. **`build-linux-verify` hardcodes Release** where `build-linux` switches to Debug under `ACTIONS_STEP_DEBUG` (the original's `BUILD_TYPE` block). Declared in the step comment and correct (a Debug build would flip visibility and the poison arm), but it is an undeclared difference from "copy of `build-linux:12-147`"; the "Show installed toolchain" step is also dropped.
7. **`run_trace_case.cmake:170-171` scans only `PipeVerifyDiffer|UnmigratedPipeInput`.** `Fatal{PipeVerifyBadKnob}` (a typo'd knob) is not in the regex; it is caught only because D2 makes it abort. Fine if that stays true; a comment would do.
8. **D.3's G5 line as written in the brief (`-R 'Mipmap'` without `--no-tests=error`) prints rc=0 on this tree** because nothing matches (`ctest -N -R Mipmap` → 0). Deviation 4 fixes CI but the result file does not hand the integrator the corrected local line (`-R 'DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes' --no-tests=error`).
9. Result-file wording: "`MGPipeVerifyInputs` … does not exist until core c4 … the check is the point" reads as if the check will turn green at c4; per M1 it will not.
## 4. Brief vs tree (facts recorded, tree wins)
- Lane size 816 (not 742/752), pull `integration-gpu` 878 — as the result file measured; the `comm` parity is trivially true (fold of `Verify.`/`VerifyCorrupted.`/`PoisonOmitted.`), not a statement about coverage.
- `LABELS "a\;b"` escaping, `include(GoogleTest)` at `:395`, `'^\s+Test #'` regex — confirmed as the result file states.
- `gen_pipe.py --self-test` does not exist on this tree (rc 2) — the `pipe-gates` step is red until core c5, as declared.
- `retrace` label count 80 (build-retrace registers the non-CI case pair too).
## 5. What was not re-run
The full 816-entry lane, any real retrace under `MOBILEGL_PIPE_VERIFY`, and any CI dispatch — same as the implementer, for the same reasons (no comparator on the contract tree; no push). M1 and M2 are demonstrable without them; M3 is a pure-function property.
@@ -1,69 +0,0 @@
# Adversarial review — package E `p1-verify` (v2)
Tree `~/w7/p1-verify`, branch `p1/verify` = `eec8cf2c`, eight commits on `p1/contract` (`bf86b1ed`): the four of v1 plus `6060690a`, `01300e6a`, `ce26029c`, `eec8cf2c`. Tree left exactly as found (the four untracked `build-*` directories the implementer created; no file touched; the only side effect of this review is a rebuilt-and-relinked `MobileGLIntegrationTest` in `build-linux`/`build-push`, see minor 4, and fresh `pipe-*.log*` files under `build-verify/MobileGL/MG_IntegrationTest/`). Scripts and raw outputs: `scratchpad/wf4/verify-review2/s01.sh … s11.sh`, `jobdiff.py`, and `~/w7/p1-verify-review2/`.
**Verdict: APPROVED — 0 majors, 5 minors.** The three v1 majors are fixed and the fixes were re-derived independently (not just re-read): the static-symtab gate finds `MGPipeFillForVerb` today and `MGPipeVerifyInputs` exists in `p1/core`; the arming assertion has a one-process lane and the CI step reads only those logs; `gate_failures` now fails on all four buckets and on a `.text` that moved either way. Everything else the package claims holds under re-run, and the deeper hunts (the `.so` swap's linkage, `ctest --timeout` vs a `TIMEOUT` property, log flushing before `std::abort()`, both backends reading `GetActiveTextureUnit` at `GenerateMipmap`, the ambient `Verify.` lanes being functionally green on this tree, the per-job YAML diffs against their originals) found nothing that can make a lane red or green for the wrong reason.
## 1. What was re-run independently (command → observed)
All in `~/w7/p1-verify` unless stated; `CCACHE_BASEDIR=/home/swung/w7`.
| # | command | observed |
|---|---|---|
| 1 | `git status --short`; `git log --oneline p1/contract..HEAD`; `git diff --stat p1/contract..HEAD` | clean apart from `build-*`; 8 commits; 9 files, +1738/−14 — exactly E's row of C.5. Nothing under `MG_Backend/**`, `MG_Test/**`, `MG_Pipe/**`, root `CMakeLists.txt`, `Config.h`, `ConfigLoader.cpp`. Every touched file is LF (`grep -c $'\r'` = 0 ×9). |
| 2 | `cmake --build build-{linux,push,verify} -j 12` | `build-verify`: no work; `build-linux`/`build-push`: `[2/2] Linking CXX executable MobileGL/MG_IntegrationTest/MobileGLIntegrationTest` (minor 4). The library itself did not change (row 3). |
| 3 | `symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` | rc 0; `.text 10792579 -> 10792579 (+0)`; `27799 -> 27799: 0 added, 0 removed, 0 resized, 0 renamed`. G1 holds under the now-strict gate. |
| 4 | `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only the two `:0` lines for the contract's `MGPipe/PipeInputs.{h,cpp}`. E converts no site, touches no site (G2 is B/C's). |
| 5 | `python3 scripts/gen_pipe.py --check` | rc 0: `63 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `0 UNMAPPED`, up to date. `--self-test` → rc 2 here (contract tree); on `~/w7/p1-core` (read-only, `d9f4698d`) → `6 negative-control trip(s), positive control OK`, rc 0 — the `pipe-gates` step E added will be green once core lands. |
| 6 | `python3 scripts/symbol_report.py --self-test` | `OK (2 canned transcripts, 5 buckets, 3 gates)`. |
| 7 | `ctest -L unit --no-tests=error -j 12` in build-linux / build-push / build-verify | `100% tests passed, 0 tests failed out of 1466` ×3 (after the relink of row 2). |
| 8 | `ctest --test-dir build-linux -N` vs `~/w7/p1-before-ctest-names.txt` (`comm`) | removed 0, added 10 (the five new cases × 2 backends). G7 additions only. |
| 9 | `ctest --test-dir build-verify -N -L integration-verify` / `-L integration-gpu`; build-linux `-L integration-gpu` | **818** / 1696 / 878. The 16 non-ambient-or-new names listed: `Verify.` ×10, `VerifyArming.` ×2, `VerifyCorrupted.` ×2, `PoisonOmitted.` ×2. |
| 10 | `ctest --test-dir build-linux -L integration-verify --no-tests=error` | `Total Tests: 0`, rc 8 — G8 half two holds. |
| 11 | `ctest --test-dir build-retrace -N -L retrace` | 80. |
| 12 | env-knob census over `build-verify/MobileGL/MG_IntegrationTest/*.cmake` | `818 MOBILEGL_PIPE_VERIFY=1`, `2 …_CORRUPT=GetRenderStateParameters`, `2 …_FATAL=0`, `2 …_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit`, `2 MGITEST_PIPE_ARMING_LANE=1`. No ambient entry names a control knob or the arming marker — the C.4 ENVIRONMENT constraint holds. |
| 13 | `ctest --test-dir build-verify -R 'PipeVerifyArmingScenario\|PoisonOmissionScenario'` (26 entries) | pull-prefixed ×10: all ***Skipped**; ambient `Verify.` ×10: 8 ***Skipped**, both `WithoutOmissionCompletes` **Passed** (0.44/0.39 s); `VerifyArming.` ×2, `VerifyCorrupted.` ×2, `PoisonOmitted.` ×2: all ***Failed**. **The arming case skips or fails on the contract tree; it never passes.** |
| 14 | `ctest … -R '^DirectGLES\.VerifyArming\.' --output-on-failure` | fails at `PipeVerifyArmingScenario.cpp:211` with "the library never reported arming the comparator" — the right reason (no comparator until core c4), not a GPU/harness failure. `pipe-verify-arming-{DirectGLES,DirectVulkan}.log` exist (318/330 lines, 0 arming lines) — so the CI arming step would red this tree for the right reason too. |
| 15 | `ctest --test-dir build-verify -L integration-verify -E 'VerifyArming\.\|VerifyCorrupted\.\|PoisonOmitted\.' -j 8 --no-tests=error` (the two ambient `Verify.` lanes, 812 entries, **not run by the implementer**) | rc 0, `100% tests passed, 0 tests failed out of 812`, 77 skipped. The registration itself (per-lane `MOBILEGL_LOG_FILE_PATH`, `TIMEOUT 900`, `-j 8` on a shared log path) breaks no existing scenario. |
| 16 | control filters: `ctest -N -L integration-verify -R 'DirectGLES\.Verify\..*ClearThenReadPixels'` / `-R 'DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes'` | 5 entries (all five `ClearThenReadPixelsScenario` cases, each issues a clear on DirectGLES) / 1 entry. Neither control can pass on an empty selection. |
| 17 | `trace_cases.py --ci --format github-verify-matrix` | 16 entries over exactly the 8 named cases; `github-test-matrix` 77. |
| 18 | `readelf -d build-verify/libMobileGL.so \| grep -E NEEDED\|RUNPATH` | NEEDED = `libvulkan.so.1 libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6 ld-linux-x86-64.so.2`; no RUNPATH. The `cp build-verify/libMobileGL.so build-linux/libMobileGL.so` swap in `retrace-verify` is sound — the library pulls nothing from its own build tree (`libSPIRV-Tools-shared.so`/`libEGL.so` in the tree are not NEEDED). `build-retrace`'s `CTestTestfile.cmake` freezes `MOBILEGL_LIBRARY=<tree>/build-verify/libMobileGL.so` here (local configure) / `build-linux/` in CI, as the step assumes. |
| 19 | `grep -c TIMEOUT build-retrace/tools/trace_replay/CTestTestfile.cmake` → 0; empirical: a scratch project with `TIMEOUT 1` on a 3 s test under `ctest --timeout 100` → `***Timeout 1.00 sec`, the property-less sibling passes | `--timeout` only governs tests WITHOUT a `TIMEOUT` property, and the retrace tests have none (`timeout_seconds` in the manifest is not emitted as a property), so `--timeout 10800` in `retrace-verify` is effective. Not a defect; recorded because the opposite would have been a lane that cannot pass. |
| 20 | `MG_Util/Debug/Log.cpp:81-84` | every write is `fputs` + `fflush(s_logFile)`, so the `MGLOG_F` line precedes `std::abort()` in the child log the omission scenario reads. `p1/core` prints `MGPipe: Fatal{PipeVerifyDiffer, "%s@%s", verb=%llu, where=%s}` unconditionally and aborts only when `g_verify.Fatal` (`PipeFill.cpp:384-391`), `Fatal{UnmigratedPipeInput, "%s@%s"}` at `PipeFilled.inc:308`, `MGPipe: verify armed - …` at `PipeFill.cpp:377`, `poison omission armed - %s@%s` at `:452` — every string E greps for is the string core prints. |
| 21 | `VulkanRenderer.cpp:10963` and `DirectGLES.cpp:6932` | both backends' `GenerateMipmap` read `pGLContext->GetActiveTextureUnit()` first, so `<backend>.PoisonOmitted.` can go green on both once the poison exists; `grep -rln glGenerateMipmap MobileGL/MG_IntegrationTest` → only `PoisonOmissionScenario.cpp` (the "only possible target" claim holds). |
| 22 | `jobdiff.py`: per-job YAML diffs `build-linux`→`build-linux-verify`, `integration`→`integration-verify`, `retrace`→`retrace-verify`; `yaml.safe_load` with BOM | only the declared changes: BUILD_DIR/artifact names, `timeout-minutes`, `-DMOBILEGL_PIPE_VERIFY=ON` + `BENCHMARK=OFF` + Release-always, the nm gate, the arming step, the two controls, the log upload; `retrace-verify` drops only the iterationrp exports (that case is not in the verify set) and adds `MOBILEGL_PIPE_VERIFY=1` + `--timeout 10800`. `retrace` (pull) is byte-identical to the contract's. `trace-cases`, `remove-artifact-clutter`, `pipe-gates` changed as declared. 17 jobs. |
| 23 | `bash -eo pipefail -c 'matched=$(echo "" \| grep -cE "^ *Test *#[0-9]+:"); echo reached'` | rc 1, nothing printed (minor 2). |
| 24 | `git ls-tree p1/contract scripts/symbol_report.py` vs `git ls-files -s` | `100755` → `100644` (minor 1). |
## 2. Majors
None.
The three v1 majors, each re-tested rather than re-read:
- **M1 (nm -D)** — `test.yml` `build-linux-verify` "The verify library really carries the comparator" and `retrace-verify` "Unpack the VERIFY runtime" now use `nm --defined-only`; `nm --defined-only build-verify/libMobileGL.so | grep -c MGPipeFillForVerb` → 1 here, and `MGPipeVerifyInputs` is a defined function in `p1/core` (`PipeInputs.cpp:160`), so both greps turn green at integration. The added "looks stripped" floor (`< 1000` defined symbols) closes the third silent way the gate could red.
- **M2 (arming step)** — `PipeVerifyArmingScenario.Armed` now runs only where `MGITEST_PIPE_ARMING_LANE` is set (`DirectGLES.VerifyArming.` / `DirectVulkan.VerifyArming.`, own log path), skips in the ambient lanes (row 13), and the CI step greps only `pipe-verify-arming-*.log` and requires one per backend. Reproduced the fix's premise (row 14) and the old failure (the ambient lane's last entry writes a 317-line log with no verb). The step's name and text now claim per-lane arming, which is what it proves.
- **M3 (gate_failures)** — `gate_failures([],[],[],[],-4096,0,True)` → one reason; resized-only and renamed-only under `--fail-on-symbol-set-change` → one reason each; the gates read threshold-0 buckets regardless of `--threshold`; the self-test drives all three shapes (row 6). The CI invocation is unchanged and now means G1.
## 3. Minors
1. **`scripts/symbol_report.py` lost its executable bit** (`100755` at `p1/contract` → `100644` at HEAD, introduced in `09804491`, carried by `01300e6a`). Undeclared; inert for CI (`python3 scripts/symbol_report.py` everywhere, `test.yml:1398,1506`) but a mode regression the integrator's rebase will carry into `feat/disaggregated`. `git update-index --chmod=+x` before or at integration.
2. **The two negative-control steps die before their diagnostic when the filter matches nothing.** `test.yml` control A/B: `matched=$(ctest -N … | grep -cE '^ *Test *#[0-9]+:')` under GitHub's default `bash -eo pipefail` — `grep -c` exits 1 on zero matches, the assignment fails, `set -e` ends the step before `if [ "${matched}" -lt 1 ]` prints `::error::negative control … selected 0 tests`. Reproduced (row 23). The outcome is still red (correct), so this is a message defect only; `grep -cE … || true` fixes it.
3. **The full 79-case verify sweep "on `workflow_dispatch`" (brief E, "Verify cost" row) is not implemented and not declared.** `retrace-verify`'s matrix is always `verify-matrix` (8 cases); there is no dispatch input or job that runs the 77-entry matrix under the verify library. C.4 itself asks only for the subset, so this is a documentation gap against the risk table rather than against the file list; the D.3 WSL run covers the P1 exit.
4. **`build-linux` and `build-push` were not current with HEAD on arrival** — `cmake --build` relinked `MobileGLIntegrationTest` in both (row 2), so the result file's row 1 "rc 0 ×3" and the unit rows were measured before the final scenario edit landed in those two trees (the library was unchanged, and the unit suites pass after the relink, row 7). Cosmetic; noted so the integrator does not read the implementer's table as the final-tree state for those two builds.
5. **Verify-cost arithmetic on the retrace subset.** The eight `"verify": true` cases include two `timeout_seconds: 1800` cases (`minecraft-1.21.4-fabric-sodium-in-world`, `improved-transparency-minecraft-26.3`) and one default-900 (`minecraft-1.21.4-in-world`); at the design's 5-10× a 1800 s case is 9000-18000 s against `--timeout 10800` and `timeout-minutes: 240` (14400 s). Both numbers are the brief's (C.4/E), and `--timeout` is effective here (row 19), so nothing is mis-wired — but the first real run may show the budget itself is short for those two cases, and the fix then is a `trace_cases.json` decision, not E's plumbing.
Carried from v1 as observations, not defects: deviation 5 (`integration-verify` does not repeat the `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` pass) is now written into the step comment; the `Fatal{PipeVerifyBadKnob` regex addition in `run_trace_case.cmake` is correct and matches core's spelling (`PipeFill.cpp:284`).
## 4. Brief vs tree (facts recorded; tree wins)
- `integration-verify` is **818** entries (2 × 406 ambient + 2 arming + 2 corrupted + 2 omitted), not the brief's 742/752; the pull `integration-gpu` set is 878; the fold parity holds trivially.
- `LABELS "integration-gpu\;integration-verify"` escaping, the verify block after `include(GoogleTest)` at `:395`, the `'^ *Test *#[0-9]+:'` count regex — as the result file states.
- The brief's D.3 G5 line `-R 'Mipmap'` selects nothing (row 16 shows the only matching name is `…WithoutOmissionCompletes`); the result file hands the integrator the corrected line.
- The brief's C.4 has the arming case ride the ambient lanes; the tree gives it a lane (deviation 3 in the result file). Justified: the ambient log path is truncated by every process, so the brief's own design would have been unsound.
- `gen_pipe.py --self-test` is a core c5 deliverable; present and green in `~/w7/p1-core` today (row 5).
- `timeout_seconds` in `trace_cases.json` is not a ctest `TIMEOUT` property (row 19) — the brief's "1800 s cases already sit above ctest's 1500 s default" is the right mental model and `--timeout 10800` is the right knob.
## 5. What was not re-run
The full 818-entry lane with the controls (six entries are red by design on this tree; the 812 ambient entries were run, row 15), any real retrace under `MOBILEGL_PIPE_VERIFY` (no comparator on the contract tree; `run_trace_case.cmake`'s block was driven by the implementer on synthetic logs and its `pipe_verify_fatals` regex and arming `string(FIND)` were re-read), and any CI dispatch (no push). None of the approved claims depends on those: the CI mode's correctness on the integrated tree is D.3's job, and every mechanism it needs from E has been shown here to red for its stated reason and to green under the strings core actually prints.
@@ -1,84 +0,0 @@
# P1 package E (p1-verify) — the third CI mode, its lanes and its negative controls
Tree `~/w7/p1-verify`, branch `p1/verify`, base `p1/contract` = `bf86b1ed` (`feat/disaggregated@087685d1` + package A's contract commit). Created with `wsl_p1_tree.sh verify p1/contract verify`; `build-linux` / `build-push` / `build-verify` all configured and green at the base (955/955, 959/959, 959/959). Not pushed.
## Commits (4, on `p1/verify`)
| sha | subject |
|---|---|
| `62783e4f` | `[Test] (Pipe): the integration-verify lanes and their two always-on negative controls` |
| `c4170e56` | `[Test] (Retrace): make a retrace under MOBILEGL_PIPE_VERIFY prove it armed, and give the lane a label` |
| `09804491` | `[Feat] (Tooling): give symbol_report.py the two hard gates G1 needs, with the report written first` |
| `1a5252b6` | `[CI] (Pipe): the third CI mode - a verify build, its integration and retrace lanes, and the two negative controls as always-on steps` |
`git diff --stat p1/contract..HEAD`: 9 files, +1498 / −12 — `.github/workflows/test.yml` (+574), `MobileGL/MG_IntegrationTest/CMakeLists.txt` (+134), `Scenarios/PipeVerifyArmingScenario.cpp` (new, 243), `Scenarios/PoisonOmissionScenario.cpp` (new, 395), `scripts/symbol_report.py` (+66/−12), `tools/trace_replay/{CMakeLists.txt, run_trace_case.cmake, trace_cases.json, trace_cases.py}`. Nothing outside E's row of the C.5 ownership table was touched; `git status` clean apart from the three untracked build directories and `build-retrace/` (configured for the label check below).
## What landed
**v1 — the lanes.** Six `gtest_discover_tests` registrations inside one `if (MOBILEGL_PIPE_VERIFY)` block at the end of `MG_IntegrationTest/CMakeLists.txt`, all labelled `integration-gpu;integration-verify`, all `TIMEOUT ${MGL_ITEST_VERIFY_TIMEOUT}` (900): ambient `DirectGLES.Verify.` / `DirectVulkan.Verify.` (`MOBILEGL_PIPE_VERIFY=1` + a per-lane `MOBILEGL_LOG_FILE_PATH`), `*.VerifyCorrupted.` (adds `MOBILEGL_PIPE_VERIFY_CORRUPT=GetRenderStateParameters MOBILEGL_PIPE_VERIFY_FATAL=0`, filter `PipeVerifyArmingScenario.CorruptedFieldIsReported`), `*.PoisonOmitted.` (adds `MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit`, filter `PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb`). Every list appends `${MGL_ITEST_COMMON_ENV}` / `${MGL_ITEST_VULKAN_ENV}`. Two new scenario files, added unconditionally to `add_executable`.
**v2 — retrace.** `run_trace_case.cmake` gains a verify block (arming line present; `Fatal{PipeVerifyDiffer` and `Fatal{UnmigratedPipeInput` absent; log must exist), inert unless `MOBILEGL_PIPE_VERIFY` is set to something other than `0`/`false`. `LABELS retrace` on both `set_tests_properties`. `"verify": true` on eight cases and `--format github-verify-matrix`.
**v3 — `symbol_report.py`.** `--fail-on-added-bytes N` implemented, `--fail-on-symbol-set-change` added, decision isolated in a pure `gate_failures()` the `--self-test` drives, report written before any gate fires, default behaviour (no flag → exit 0) unchanged.
**v4 — `test.yml`.** `build-linux-verify`, `integration-verify` (+2 always-on negative-control steps + an arming-line grep over the lane logs), `retrace-verify` (+ an inverted OpenRA step), `monolith-symbol-report` (dispatch-only, `baseline_sha` input, both gates, MG_Remote refusal), `remove-artifact-clutter` waits for `retrace-verify`, `trace-cases` exports `verify-matrix`, `pipe-gates` gains `gen_pipe.py --self-test` and `symbol_report.py --self-test` and its dirty-surface comment now says P2.
## Verification — every command and its actual output
All in WSL on `~/w7/p1-verify` unless marked. Logs under `~/w7/p1-verify-*.log` / `~/w7/p1-verify-*.txt`.
| # | command | result |
|---|---|---|
| 1 | `python -c "import yaml,sys; yaml.safe_load(open(sys.argv[1]))" test.yml.copy` (**Windows python**, on a copy) | parses; 17 jobs. `build-linux-verify` timeout 120 / 15 steps; `integration-verify` needs `build-linux-verify`, timeout 180, 12 steps; `retrace-verify` needs `[build-linux-verify, build-retrace, trace-cases, trace-fixtures]`, timeout 240, 13 steps; `monolith-symbol-report` timeout 180, 11 steps; `remove-artifact-clutter` needs `['retrace-summary','retrace-verify']`; dispatch inputs `['baseline_sha']`; `trace-cases` outputs `['matrix','names','verify-matrix']`; `pipe-gates` steps now include both `--self-test`s. The file's UTF-8 BOM is preserved byte for byte (`efbbbf 6e616d65...`). |
| 2 | `trace_cases.py --ci --format github-verify-matrix` | **16** entries (8 cases × 2 backends), first three `OpenRA/DirectGLES`, `OpenRA/DirectVulkan`, `minecraft-1.21.4-startup/DirectGLES`. `--format github-test-matrix` still **77**; `--format cmake` and `--format names` unchanged in content (the `verify` key is not one of the emitter's keys). |
| 3 | `python3 scripts/symbol_report.py --self-test` | `self-test: OK (2 canned transcripts, 5 buckets, 2 gates)`, rc 0. |
| 4 | `cmake --build build-{linux,push,verify} -j 12` | rc 0, rc 0, rc 0. |
| 5 | `ctest -L unit --no-tests=error -j 12` | `build-linux` `100% tests passed … out of 1466`; `build-verify` idem. |
| 6 | `ctest --test-dir build-verify -N -L integration-verify \| grep -c 'Test #'` | **816** = 2 × 406 ambient `Verify.` + 2 `VerifyCorrupted.` + 2 `PoisonOmitted.` |
| 7 | `ctest -N -L integration-gpu` folded (`s/\.(Verify\|VerifyCorrupted\|PoisonOmitted)\./\./`, `sort -u`) | verify build 878 unique names, pull build 878 — `comm` empty in **both** directions: exact parity with the pull registration set. Raw counts: verify 1694, pull 878. |
| 8 | `ctest --test-dir build-verify -R 'DirectGLES\.Verify\.PipeVerifyArmingScenario' --output-on-failure` (REQUIRE_GPU=ON) | rc 8. `…Armed ***Failed` with "MOBILEGL_PIPE_VERIFY=1 is set for this process and a frame was cleared, drawn and read back, and the library never reported arming the comparator"; `…CorruptedFieldIsReported ***Skipped` (its knob is not set in that lane). **The arming case fails and never passes on the contract tree** — G8 half one. |
| 9 | `ctest --test-dir build-verify -R 'VerifyCorrupted\.\|PoisonOmitted\.' --no-tests=error` | rc 8, **4/4 red**, each naming its own reason ("the comparator is not comparing…", "It exited with status 0 instead - the poison is not armed…"). Correct for the contract tree: the comparator lands in core `c4`, the filler in `c2`. |
| 10 | `ctest --test-dir build-verify -R 'Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes'` | **2/2 pass** (0.48 s / 0.42 s) — the sibling control is green on both backends, so control B has a green entry to turn red. |
| 11 | `ctest --test-dir build-linux -N -L integration-verify` / `ctest --test-dir build-linux -L integration-verify --no-tests=error` | `Total Tests: 0`; rc **8**. The lanes exist only under the option, and `--no-tests=error` reds a build that lost it — G8 half two. |
| 12 | the two CI control steps, simulated locally in `build-verify` | control A selects **5** entries, control B selects **1**. With the knob exported in the job environment both still exit 0 on the contract tree, i.e. **the CI step would `exit 1` and say "the comparator is not comparing" / "the poison is not armed"** — correct, and the check that the knob reaches the process is below. |
| 13 | `grep -ho "MOBILEGL_PIPE_[A-Z_]*=…" build-verify/MobileGL/MG_IntegrationTest/*.cmake \| sort \| uniq -c` | `816 MOBILEGL_PIPE_VERIFY=1`, `2 MOBILEGL_PIPE_VERIFY_CORRUPT=…`, `2 MOBILEGL_PIPE_VERIFY_FATAL=0`, `2 MOBILEGL_PIPE_POISON_OMIT=…`. Only the four dedicated entries name the two control knobs, so the job environment reaches the 812 ambient ones — the ENVIRONMENT-property trap is avoided by construction and measured. |
| 14 | `run_trace_case.cmake`'s verify block, driven standalone against seven synthetic logs | env unset → silent, rc 0 · `=0` → "no verify assertions", rc 0 · armed log → "armed, zero divergences, zero unmigrated reads", rc 0 · **no arming line → rc 1** · **missing log → rc 1** · **`Fatal{PipeVerifyDiffer` → rc 1** (offending line echoed) · **`Fatal{UnmigratedPipeInput` → rc 1**. |
| 15 | `cmake -S . -B build-retrace -DMOBILEGL_BUILD_TRACE_REPLAY=ON …` then `ctest -N -L retrace` | configure rc 0; **80** cases listed under the new label (was: not selectable by label at all). |
| 16 | `symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` | rc **0**. `.text 10792579 -> 10792579 (+0, +0.000%)`, `27799 -> 27799 defined symbols: 0 added, 0 removed, 0 resized, 0 renamed`, `27060 unchanged`. G1 holds — E adds no C++ to the pull build. |
| 17 | the same gates against `build-verify/libMobileGL.so` (a deliberately different library) | rc **1**, `FAIL --fail-on-symbol-set-change: 43 symbol(s) added, 0 removed`, `FAIL --fail-on-added-bytes 0: .text grew by 10416 bytes` — the gate can go red for its reason. Ungated run on the same pair: rc 0. |
| 18 | `python3 scripts/gen_pipe.py --check` | rc 0: `63 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes` / `0 UNMAPPED` / `generated files are up to date`. |
| 19 | `ctest --test-dir build-linux -N` vs `~/w7/p1-before-ctest-names.txt` | **removed 0**, added 10 — the four new scenario cases plus the child-worker case, × 2 backends. Additions only. |
## Deviations from the brief, and why
1. **`LABELS integration-gpu integration-verify` does not work; the list must be escaped.** With the brief's spelling (and with a plain `"a;b"`) the second label is lost: `ctest -N -L integration-verify` listed **0** entries while `-L integration-gpu` listed 1690. The file's own `mgl_itest_join_environment` documents the same hazard for `ENVIRONMENT`. Landed as `LABELS "integration-gpu\;integration-verify"`, measured 816.
2. **`set(MGL_ITEST_VERIFY_TIMEOUT 900)` and the whole block sit at the end of the file, not "after `:393`".** `gtest_discover_tests` needs `include(GoogleTest)`, which is at `:395`; a block at `:394` would not configure. The timeout is set inside the `if (MOBILEGL_PIPE_VERIFY)` block, next to the registrations that use it.
3. **The `PoisonOmitted.` lanes filter `PoisonOmissionScenario.OmittedFieldAbortsOnThatVerb`, not `PoisonOmissionScenario.*`.** The brief says both, in the same row; the single-case spelling is what its own "+2 PoisonOmitted" count requires, and `*` would register a guaranteed skip beside it.
4. **CI negative control B targets `DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes`, not `DirectGLES\.Verify\..*Mipmap`.** Measured: **no** integration entry in the tree matches `Mipmap` (`ctest -N -R 'DirectGLES\.Verify\..*Mipmap'` → 0), and `glGenerateMipmap` appears in exactly one scenario file — the new one. With the brief's filter the step would select nothing, `--no-tests=error` would exit non-zero, and the `if ctest …; then error` shape would read that as "the control worked". **Both** control steps therefore now count their selection with `ctest -N` first and fail loudly on an empty one.
5. **`WithoutOmissionCompletes` does not skip when `MOBILEGL_PIPE_POISON_OMIT` is set** (C.4 says it should). It is the only entry control B can turn red; an entry that skips itself reports green either way. Its failure message says explicitly when the failure is the control doing its job.
6. **The poison sequence runs in `fork()` + `execve()` of the test binary, not a bare `fork()`.** With a bare fork (the brief's shape, following `HeadlessGL.cpp`) the DirectVulkan child wedged: `DirectVulkan.…WithoutOmissionCompletes` took **121.57 s** and timed out while DirectGLES passed in 0.33 s — the pre-flight forks *before* any context exists, this case cannot, and a fork of a process holding a live Vulkan device inherits driver mutexes with no threads to release them. After the change both backends pass in ~0.45 s. The child gets its **own** `MOBILEGL_LOG_FILE_PATH` (`<lane log>.poison-child`) because `MG_Util/Debug/Log.cpp` opens the log with `fopen(path, "w")` and a shared path would truncate the file the parent is about to read.
7. **The arming assertion searches the whole log file**, not only the bytes appended after the case started (the `UnlocatedIoBlockScenario` idiom the brief cites). Two tree facts: the log is truncated at each process's first write (`fopen(…, "w")`), so the whole file *is* this process's; and the arming line is latched at the first fill, which may be the harness bring-up rather than the case's own draw. The divergence checks (`Fatal{PipeVerifyDiffer`, `Fatal{UnmigratedPipeInput`) stay scoped to the appended bytes.
8. **A third case, `PoisonOmissionScenario.TheSequenceThePoisonControlsRun`**, is the child's body (it skips in every process without the re-exec marker). Hence 10 new ctest names in the pull build rather than 8.
9. **`trace_cases.py` has no "existing self-checks"** to extend (the brief's C.4 wording). The `verify` flag is validated in `load_trace_case_manifest` instead — non-boolean, or `verify` on a case excluded from CI, is a `ValueError` in every consumer including the cmake emitter.
10. **`--fail-on-added-bytes N` is defined as ".text grew by more than N"**, and `--fail-on-symbol-set-change` as "added or removed is non-empty" (renamed-only folds and resized do not count; `--threshold` already governs resizes).
11. **`retrace-verify` artifacts are named `retrace-verify-result-*`**, which `remove-artifact-clutter`'s `retrace-result-*` sweep does not match, so a failed verify retrace keeps its images. The brief only asked for a `-verify` suffix.
12. **`build-linux-verify` has no `needs:`** (it is a copy of `build-linux`, which has none) and builds with `MOBILEGL_BUILD_BENCHMARK=OFF` — the verify lanes need no benchmark target.
13. **`monolith-symbol-report`** builds the baseline in a `git worktree` of `inputs.baseline_sha` (so `fetch-depth: 0` on the checkout) and spells the MG_Remote check as `if nm … | grep -q MG_Remote; then exit 1; fi` rather than the brief's `grep -q … && exit 1`, which under GitHub's `bash -e` fails the step on the *healthy* path.
## Where the tree contradicted the brief's numbers
| brief | tree (measured) |
|---|---|
| "the 742-entry `integration-verify` lane"; "752 = 2 × (371 + 3 new cases) + 2 + 2" | 406 cases per backend → **816** entries (812 ambient + 4 controls). The pull build's `integration-gpu` set is **878**, not 742. |
| C.4: `ctest -N -L integration-gpu \| sed 's/\.Verify\././' \| sort -u` "parity with the pull registration set" | holds exactly once `VerifyCorrupted.`/`PoisonOmitted.` are folded too: 878 = 878, `comm` empty both ways. |
| "40 trace + 79 desktop cases"; D.4 "retrace (77)" | `--ci --format github-test-matrix` = **77** entries (39 CI cases × 2 minus the DirectVulkan-only `iterationrp`). The verify subset is 16, as predicted. |
| `MGPipeHostSpan.h`, `MGL_ITEST_TIMEOUT` etc. line numbers | `set(MGL_ITEST_TIMEOUT 120)` is at `:393` and `include(GoogleTest)` at `:395`, as the brief says; `run_trace_case.cmake` is 129 lines, as the brief says. |
| brief's ctest-name regex `'^\s+Test #'` | matches only test numbers ≥ 1000 (ctest pads smaller ones); every count above uses `grep -cE '^ *Test *#[0-9]+:'`. Package A recorded the same. |
## Unfinished / known dependencies on package A
- **`nm -D … | grep -q MGPipeVerifyInputs`** (in `build-linux-verify`'s "The verify library really carries the comparator" step and in `retrace-verify`'s unpack step) is the contract name from D8/D13 and **does not exist until core `c4`**; the contract tree exports only `MGPipeInputsFieldEqual` / `MGPipeApplyVerifyCorruption`. Both steps will fail in CI until `p1/core` c4 lands. Deliberate: the check is the point.
- **`pipe-gates` runs `python3 scripts/gen_pipe.py --self-test`**, which core `c5` adds (brief C.1 step 5). The step is red until then.
- **The four control lanes and `PipeVerifyArmingScenario.Armed` are red on this tree by design** (no filler, no comparator). They go green with core `c2`+`c4` merged; the brief defers E's full functional verification to the integrated tree (D.3).
- **Not run here:** the full `ctest -L integration-verify` (816 entries; the ambient `Armed` entries are red by design), any retrace under `MOBILEGL_PIPE_VERIFY` (no trace-replay binary or fixtures budget in this tree — the gate was driven against seven synthetic logs instead), and any CI dispatch (no push).
- **`build-retrace/`** was configured in this tree only to prove the `retrace` label registers; it is untracked and no retrace binary was built.
@@ -1,202 +0,0 @@
# P1 package E (p1-verify) — v2, the rework of the three majors and eight minors
Tree `~/w7/p1-verify`, branch `p1/verify`, base `p1/contract` = `bf86b1ed`. Four **new** commits on top of
v1's four; nothing rebased, nothing pushed. `git status` clean apart from the four untracked build
directories (`build-linux`, `build-push`, `build-verify`, `build-retrace`).
Review answered: `scratchpad/wf4/p1-results/verify-review-v1.md`. Scripts and raw outputs of this round:
`scratchpad/wf4/verify2/s0*.sh` … `s19.sh`.
## Commits
| sha | subject |
|---|---|
| `62783e4f` | `[Test] (Pipe): the integration-verify lanes and their two always-on negative controls` (v1) |
| `c4170e56` | `[Test] (Retrace): make a retrace under MOBILEGL_PIPE_VERIFY prove it armed, and give the lane a label` (v1) |
| `09804491` | `[Feat] (Tooling): give symbol_report.py the two hard gates G1 needs, with the report written first` (v1) |
| `1a5252b6` | `[CI] (Pipe): the third CI mode - a verify build, its integration and retrace lanes, and the two negative controls as always-on steps` (v1) |
| **`6060690a`** | `[Test] (Pipe): give the arming assertion a lane and a log of its own, and stop the poison child calling a sequence it never ran a success` |
| **`01300e6a`** | `[Fix] (Tooling): gate symbol_report on all four buckets and on a .text that moved in either direction` |
| **`ce26029c`** | `[Test] (Retrace): scan a verify retrace's log for the third MGPipe Fatal too` |
| **`eec8cf2c`** | `[CI] (Pipe): read the verify library's static symtab, grep only the arming lane's log, and stop the two retrace lanes overwriting each other's evidence` |
`git diff --stat p1/contract..HEAD`: 9 files, +1738 / −14 — the same nine files as v1 (E's row of C.5); no file
outside it was touched in either round.
## What each finding got
### M1 — `nm -D` can never find an MGPipe symbol (fixed, `eec8cf2c`)
Confirmed on this tree, and the reviewer's diagnosis is exact:
```
nm --defined-only build-verify/libMobileGL.so | wc -l -> 30557
nm -D --defined-only build-verify/libMobileGL.so | wc -l -> 11930
MGPipeFillForVerb static:1 dynamic:0
MGPipeVerifyInputs static:0 dynamic:0 (does not exist until core c4)
```
Both gates (`build-linux-verify` "The verify library really carries the comparator", `retrace-verify`
"Unpack the VERIFY runtime as the library under test") now use `nm --defined-only`, name in their error
text what a miss means, and the build-side one first counts the defined symbols and refuses an artifact
that looks stripped — otherwise a stripped library would fail the greps for a third, silent reason. The
comment at each site says why `-D` is wrong (hidden visibility in every non-Debug configuration,
`CMakeLists.txt:600-604`), so nobody re-adds it. **The v1 result file's wording was wrong** and is corrected
below (see "Dependencies on package A"): `MGPipeFillForVerb` exists today and `-D` did not find it either.
### M2 — "Every verify process really armed" (fixed, `6060690a` + `eec8cf2c`)
Reproduced verbatim: after running the last ambient entry
(`DirectGLES.Verify.PoisonOmissionScenario.WithoutOmissionCompletes`, rc 0) the shared lane log is 317 lines
with **0** arming lines, because that parent forks/execve()s/waits and issues no verb. The step would have
red a healthy lane, and in any case spoke about the last writer.
Fix, in two halves:
* `PipeVerifyArmingScenario.Armed` moved into a lane of its own — `DirectGLES.VerifyArming.` /
`DirectVulkan.VerifyArming.`, `TEST_FILTER PipeVerifyArmingScenario.Armed`, own
`pipe-verify-arming-<backend>.log`, marker `MGITEST_PIPE_ARMING_LANE=1` (a harness variable; the library
never reads it). The case skips wherever the marker is unset, so it no longer runs in the ambient lane at
all. This is the shape every other log-reading scenario in the file already uses, and it also closes
**minor 2** (the `-j 4` truncation race): nothing else writes that path.
* the CI step is now "The verify lanes armed the comparator", greps only those two logs, requires one per
backend, and states what it proves — arming is a property of (library, environment), and these two
processes share both with their ~400 ambient siblings; a per-process census is not available through a
log every process truncates. The false claim is gone from the step name, the step text, the CMake comment
and the scenario's header comment.
Driven against real and synthetic logs (`s06.sh`): contract-tree logs (no comparator) → rc 1 naming the log;
same logs with the arming line appended → rc 0; one backend's log removed → rc 1 naming the count.
### M3 — `symbol_report.gate_failures` ignored `resized`/`renamed` and shrinks (fixed, `01300e6a`)
`gate_failures(added, removed, resized, renamed, text_delta, fail_on_added_bytes, fail_on_symbol_set_change,
fail_on_text_delta=False)`:
* `--fail-on-symbol-set-change` fires on any of the four buckets;
* `--fail-on-added-bytes 0` means byte-identical (a shrink fails it); a positive budget keeps the old
one-sided meaning; `--fail-on-text-delta` is the explicit spelling; a negative budget is a `parser.error`;
* the gates always read **threshold-0** buckets, whatever `--threshold` says (a gate that read the
thresholded resize list would weaken itself the day someone raised the threshold), and the run prints that
it did when `--threshold` is non-zero;
* the self-test drives the three shapes that used to pass. The reviewer's counterexample now answers:
`gate_failures([],[],[],[],-4096,0,True)` → `['--fail-on-added-bytes 0: .text moved by -4096 bytes …']`.
The CI invocation is unchanged (`--threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0`) — it is
the brief's spelling and it now means what G1 says.
### Minors
| # | disposition |
|---|---|
| 1 | **Fixed** (`6060690a`). The child exits `HasFailure() ? 1 : 0` and checks `glGetError()` after the mipmap; the parent's message explains that status 1 is the child's own assertion. A/B proven below. |
| 2 | **Fixed** by M2's dedicated arming lane (own log, one process). No `RUN_SERIAL` was needed and the ambient lane keeps `-j 4`. |
| 3 | **Fixed** (`eec8cf2c`). `remove-artifact-clutter` now selects `retrace (` **and** `retrace verify (` jobs and strips both prefixes, longest first (verified on both job-name shapes). |
| 4 | **Fixed** (`eec8cf2c`). The control copies the verified `OpenRA/` output to `${RUNNER_TEMP}` first, captures ctest's status, restores the good output, and only then decides — so the restore happens on both paths and `Upload actual image` ships the verified run. |
| 5 | **Documented, not implemented** (`eec8cf2c`, in the step comment, and here). The second `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` pass is 186 entries under `-L integration-verify` on this tree; at the 5-10x the comparator costs that is ~23% more wall time on a lane already budgeted at 180 min, and C.4 does not ask for it. "Every integration entry" is not "every configuration": the staged-copy tier stays covered by `integration`, unverified. |
| 6 | **Fixed/declared** (`eec8cf2c`). `build-linux-verify` regains `Show installed toolchain`; the deliberate Release-always divergence is now written into the Configure step's comment (a Debug build flips visibility and arms `MOBILEGL_PIPE_POISON` through a different `#if` arm, i.e. it would measure a different library). |
| 7 | **Fixed** (`ce26029c`), and more than a comment: `Fatal{PipeVerifyBadKnob` is in the regex. D2's abort is exactly what `MOBILEGL_PIPE_VERIFY_FATAL=0` — the supported triage configuration — takes away, so relying on it was a hole. |
| 8 | **Fixed here.** The corrected local G5 line for the integrator is in "For the integrator" below. |
| 9 | **Fixed here** (see M1 and "Dependencies on package A"). |
## Verification — every command and its actual output
WSL, `~/w7/p1-verify`, this round. (v1's table still holds for everything untouched; these are re-runs.)
| # | command | result |
|---|---|---|
| 1 | `cmake --build build-{verify,linux,push} -j 12` | rc 0 ×3 (scenario + CMake changes rebuilt). |
| 2 | `ctest -L unit --no-tests=error -j 12` in all three builds | `100% tests passed, 0 tests failed out of 1466` ×3. |
| 3 | `python3 scripts/gen_pipe.py --check` | rc 0 — `63 verify payloads, 63 PipeInputs fields (7 sticky), 69 verbs, 9 classes`, `0 UNMAPPED`, up to date. |
| 4 | `symbol_report.py --before ~/w7/p1-before-libMobileGL.so --after build-linux/libMobileGL.so --threshold 0 --fail-on-symbol-set-change --fail-on-added-bytes 0` | rc **0**. `.text 10792579 -> 10792579 (+0)`, `27799 -> 27799: 0 added, 0 removed, 0 resized, 0 renamed`. G1 holds with the *stricter* gate. |
| 5 | `symbol_report.py --self-test` | `OK (2 canned transcripts, 5 buckets, 3 gates)`, rc 0. |
| 6 | `gate_failures([],[],[],[],-4096,0,True)` / resized-only / renamed-only / clean | 1 reason / 1 / 1 / `[]` — the reviewer's three counterexamples now fail, a clean pair still passes. |
| 7 | `ctest --test-dir build-linux -N` vs `~/w7/p1-before-ctest-names.txt` | **removed 0, added 10** (the five new cases × 2 backends). Additions only (G7). |
| 8 | `ctest --test-dir build-verify -N -L integration-verify` | **818** = 812 ambient + 2 `VerifyArming.` + 2 `VerifyCorrupted.` + 2 `PoisonOmitted.` (was 816; the two arming entries are the delta). |
| 9 | `ctest --test-dir build-linux -L integration-verify --no-tests=error` | `Total Tests: 0`, rc **8** — G8 half two still holds. |
| 10 | `ctest -N -L integration-gpu` folded (`s/\.(Verify\|VerifyArming\|VerifyCorrupted\|PoisonOmitted)\./\./`) | verify **878** unique names, pull **878**, `comm` empty both ways. |
| 11 | `ctest --test-dir build-verify -R '…Arming…\|VerifyCorrupted\.\|PoisonOmitted\.\|WithoutOmissionCompletes'` | `DirectGLES.Verify.…Armed` ***Skipped**, `DirectGLES.Verify.…CorruptedFieldIsReported` ***Skipped**, both `VerifyArming.…Armed` ***Failed**, both `VerifyCorrupted.` ***Failed**, both `PoisonOmitted.` ***Failed**, both ambient `WithoutOmissionCompletes` Passed (0.45/0.41 s). **The arming case skips or fails; it never passes on the contract tree.** |
| 12 | the arming step body vs the real `pipe-verify-arming-*.log` | rc 1, `::error::…pipe-verify-arming-DirectGLES.log carries no arming line` (correct: no comparator until core c4). Both logs exist, 318/330 lines, 0 arming lines. |
| 13 | the same body with the arming line appended to both logs | rc 0, `arming line present in all 2 arming-lane log(s)`. |
| 14 | the same body with one backend's log deleted | rc 1, `::error::found 1 pipe-verify-arming-*.log (expected one per backend)`. |
| 15 | the old failure mode: run only the last ambient entry, then look at the shared log | entry rc 0; `pipe-verify-DirectGLES.log` = 317 lines, **0** arming lines — M2 reproduced, and the new step no longer reads that file. |
| 16 | **A/B on minor 1**: ablate the child's first `ASSERT_NE(program, 0u)` **and** restore v1's `_exit(0)` | both `WithoutOmissionCompletes` **Passed** — the vacuous green. |
| 17 | same ablation with the fix in place (`_exit(HasFailure()?1:0)`) | both **Failed**; ablation reverted, both Passed again (0.42/0.40 s). |
| 18 | `run_trace_case.cmake`'s verify block driven with `cmake -P` against 8 synthetic logs | unset→0 · `=0`→0 · armed→0 · no arming line→1 · missing log→1 · `Fatal{PipeVerifyDiffer`→1 · `Fatal{UnmigratedPipeInput`→1 · **`Fatal{PipeVerifyBadKnob`→1** (was 0). |
| 19 | `nm` census on `build-verify/libMobileGL.so` | 30557 static / 11930 dynamic defined symbols; `MGPipeFillForVerb` static 1, dynamic 0. M1 reproduced and the fix's premise measured. |
| 20 | Windows `python -c "import yaml,sys; yaml.safe_load(…)"` on a copy of `test.yml` | parses. 17 jobs; `build-linux-verify` 16 steps / timeout 120, `integration-verify` 12 steps / needs `build-linux-verify` / 180, `retrace-verify` 13 steps / needs `[build-linux-verify, build-retrace, trace-cases, trace-fixtures]` / 240, `remove-artifact-clutter` needs `[retrace-summary, retrace-verify]`. BOM preserved. |
| 21 | `bash -n` on all 33 `run:` blocks of the six touched jobs | 0 syntax errors. |
| 22 | `remove-artifact-clutter`'s prefix stripping, on both job-name shapes | `retrace (DirectGLES, OpenRA)` → `OpenRA`; `retrace verify (DirectVulkan, minecraft-1.21.4-startup)` → `minecraft-1.21.4-startup`. |
| 23 | `trace_cases.py --format {cmake,names,fixture-files,github-test-matrix}` HEAD vs the contract script, same directory and manifest | **byte-identical** ×4 (21145 / 1928 / 46 / 6422 bytes), and `--ci --format github-test-matrix` identical too. `github-verify-matrix` = **16**, `github-test-matrix` = **77**. |
| 24 | `ctest --test-dir build-retrace -N -L retrace` | **80** (the label from v1's `c4170e56` still registers). |
| 25 | `grep -rc pGLContext MobileGL/MG_Backend \| sort \| diff - ~/w7/p1-before-pglcontext.txt` | only the two `:0` lines for the contract's new `MGPipe/PipeInputs.{h,cpp}`. E converts no site (G2 is B and C's). |
| 26 | env-knob census in the generated `*.cmake` | `818 MOBILEGL_PIPE_VERIFY=1`, `2 MOBILEGL_PIPE_VERIFY_CORRUPT`, `2 MOBILEGL_PIPE_VERIFY_FATAL=0`, `2 MOBILEGL_PIPE_POISON_OMIT`, `2 MGITEST_PIPE_ARMING_LANE=1`. No ambient entry names a control knob — the ENVIRONMENT-property trap stays avoided, and the new marker is on the arming lane only. |
## Deviations from the brief (v1's, still standing, plus this round's)
v1's deviations 1-3, 5-9, 11-13 are unchanged and still apply; the ones this round touched or added:
1. *(v1 #4, restated)* CI negative control B targets
`DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes`, not the brief's
`DirectGLES\.Verify\..*Mipmap`: nothing in the tree matches `Mipmap`, and with an empty selection
`--no-tests=error` exits non-zero, which the `if ctest …; then error` shape would read as success. Both
controls count their selection with `ctest -N` first.
2. *(v1 #10, superseded)* `--fail-on-added-bytes N` is "grew by more than N" **only for N > 0**; `0` means
"byte-identical, in either direction", `--fail-on-symbol-set-change` covers all four buckets, and both
gates read threshold-0 buckets. v1's claim that "`--threshold` already governs resizes" was wrong —
`--threshold` only shortens the report.
3. **New.** `PipeVerifyArmingScenario.Armed` runs in a dedicated `VerifyArming.` lane per backend and skips
in the ambient `Verify.` lanes; the brief's C.4 registers only six lanes and expects the arming case to
ride the ambient ones. The lane count is therefore 8, and `integration-verify` is 818 rather than 816.
Reason: the shared lane log cannot be read soundly by a lane member (M2, minor 2).
4. **New.** The arming CI step proves "the arming lane's process armed", not "every verify process armed".
The honest per-process statement is not obtainable from a log every process truncates; the step, its
name and the CMake comment all say so.
5. **New.** `integration-verify` still does not run `integration`'s second
`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` pass (minor 5) — 186 entries at 5-10x, and C.4 does not ask
for it. Recorded in the step comment so a green lane is not read as "every configuration".
6. **New.** `run_trace_case.cmake` scans for three Fatal families, not the brief's two
(`Fatal{PipeVerifyBadKnob` added).
7. **New.** `build-linux-verify` keeps Release under `ACTIONS_STEP_DEBUG` where `build-linux` switches to
Debug, and this is now declared rather than implied by "copy of `build-linux:12-147`".
## Where the tree contradicted the brief's numbers
Unchanged from v1, with the lane count updated:
| brief | tree (measured) |
|---|---|
| "the 742-entry `integration-verify` lane"; "752 = 2 × (371 + 3 new) + 2 + 2" | **818** = 2 × 406 ambient + 2 arming + 2 corrupted + 2 poison-omitted. The pull build's `integration-gpu` set is 878. |
| C.4's `-L integration-gpu` fold "parity with the pull registration set" | holds once `Verify.`/`VerifyArming.`/`VerifyCorrupted.`/`PoisonOmitted.` are all folded: 878 = 878, `comm` empty both ways. |
| "40 trace + 79 desktop cases"; D.4 "retrace (77)" | `--ci --format github-test-matrix` = 77; `github-verify-matrix` = 16, as predicted. |
| brief's ctest-name regex `'^\s+Test #'` | matches only numbers ≥ 1000 (ctest pads); every count here uses `grep -cE '^ *Test *#[0-9]+:'`. |
| `LABELS integration-gpu integration-verify` (C.4 spelling) | must be `LABELS "integration-gpu\;integration-verify"` or the second label is silently dropped (v1 deviation 1, measured again: 818 under the label). |
| C.4 "after `:393`" for the verify block | must sit after `include(GoogleTest)` at `:395`; the block is at the end of the file. |
## For the integrator
* D.3's G5 line as written in the brief (`-R 'Mipmap'`) selects **nothing** on this tree and prints rc=0.
The line that works locally is
`MOBILEGL_PIPE_POISON_OMIT=GenerateMipmap:GetActiveTextureUnit ctest --test-dir build-verify -L integration-verify -R 'DirectGLES\.Verify\.PoisonOmissionScenario\.WithoutOmissionCompletes' --no-tests=error ; echo rc=$?`
(expect non-zero once the poison is armed).
* D.3's arming grep should read the arming lane's logs:
`grep -c "MGPipe: verify armed" build-verify/MobileGL/MG_IntegrationTest/pipe-verify-arming-*.log` (> 0 in
both). The ambient `pipe-verify-<backend>.log` holds only the lane's last process and must not be used as
evidence.
* D.3's lane count is 818, not 752.
## Dependencies on package A, and what is still unfinished
* `nm --defined-only … | grep -q MGPipeVerifyInputs` (both jobs) is red until core **c4** lands the
comparator, and **that is the only reason left**: with the `-D` removed the check now measures what it
claims, and `MGPipeFillForVerb` — which exists today — is found. (v1's wording implied `-D` would start
working at c4; it would not have.)
* `pipe-gates` runs `python3 scripts/gen_pipe.py --self-test`, which core **c5** adds; red until then
(`unrecognized arguments: --self-test`, rc 2, re-confirmed).
* The six control/arming entries are red and the two ambient arming entries skip on this tree by design (no
filler, no comparator). They go green with core c2+c4 merged; C.4 defers E's functional verification to
the integrated tree (D.3).
* **Not run here**: the full 818-entry lane, any retrace under `MOBILEGL_PIPE_VERIFY` (no trace binary or
fixtures in this tree — the gate was driven against synthetic logs), and any CI dispatch (no push). The CI
steps were exercised by extracting and running their exact bodies locally (rows 12-14, 21, 22).
* `build-retrace/` remains a throwaway configure used only to prove the `retrace` label registers.
-37
View File
@@ -1,37 +0,0 @@
# P10 A:fence 轮询本地作答——配对测量与 Espryt 发现
2026-09-29,`p10/main@8d0a1b6c`,主机 WSL Arch(Espryt = mesa llvmpipe ES 3.2;Magma = lavapipe)。协议与门见 [`CONTRACT-P10.md`](../../contracts/CONTRACT-P10.md) §1。只记录、不设门。
## 负载
`FencePollScenario.PacedFrameFenceWaitsBench`(`MGITEST_FENCE_PACE_BENCH=1`):每帧 clear + `glFenceSync` + 交换,再对 N 帧前(`MGITEST_FENCE_PACE_LAG`,默认 2)的 fence 做 `glClientWaitSync(FLUSH, 1 s)`;600 帧。也就是 Minecraft 的形状,这里单独拿出来测(回放语料里只有 `improved-transparency-minecraft-26.3` 带 timed wait,见 [`C-MEASUREMENTS.md`](C-MEASUREMENTS.md) §3)。A/B = `MOBILEGL_IPC_POLL_ESCALATE=0`(每次都往返,P10 之前的行为)对默认 64。
## 结果(3 轮,ms/帧 与 600 帧里的 fence 往返次数)
| 后端 · 传输 | esc=0 ms/帧 | esc=64 ms/帧 | esc=64 往返 / 本地作答 |
|---|---|---|---|
| Magma · inproc | 0.201 / 0.206 / 0.217 | 0.200 / 0.206 / 0.214 | 0 / 598(回复槽记录 601 → 3) |
| Magma · tcp | 1.017 / 1.007 / 1.004 | **0.631 / 0.651 / 0.606**(−37%) | 0 / 598 |
| Espryt · inproc | 0.816 / 0.672 / 0.816 | 0.808 / 0.843 / 0.848 | 595–598 / 0–3 |
| Espryt · tcp | 0.891 / 0.939 / 0.908 | 0.796 / 0.891 / 0.800 | 497–519 / 79–101 |
表中是 `625f4db4` 的配置。证据:`~/w7/notes/p10/evidence/bench-after-present-report.log`;改动前的一组在 `bench-before-present-report.log`,数字相近。
## present 时报告(`625f4db4`)
server 在 `OnPresent` 里、归还 credit **之前**查一次未报告的 fence(状态查询,不提交)。原因:credit 放行的 client 立刻去等一两帧前的 fence,批次结束后才发的报告会输掉这场竞争。实测只有 Espryt·tcp 有变化,本地作答从 36–54 升到 79–101;Magma 在改动前就已经是 0 往返。
## Espryt 发现:llvmpipe 上 fence 只有被阻塞等待过才可见
Espryt 的 timed wait 几乎全部过线,这**不是**报告晚到:
| 探针 | 结果 |
|---|---|
| lag 1 / 2 / 3 / 4 / 6(`benchlag-present-status.log`) | inproc 本地作答都在 0–6,与 lag 无关——六帧前的 fence 也看不到 |
| server 端追踪(临时 `P10DBG`) | 队首 fence 在约 4 帧里的每次检查(含 flush 检查)都是 pending;client 的 timed wait 过线、server 执行 `ClientWaitSync(…, 1 s)` 之后,下一批才报告它。Magma 同一追踪里报告全部早于等待 |
| flush 检查改成冲刷最新的 fence | 不变(0–24) |
| flush 检查改用 1 ns 的 timed wait | 不变(本地 17 / 596) |
结论:在这台主机的 mesa llvmpipe(ES 3.2)上,`glGetSynciv(GL_SYNC_STATUS)` 与零超时 `glClientWaitSync` 看不到早已完成的 fence,只有真正阻塞的等待才能让它变成 signaled。这是宿主驱动的行为,与传输无关。A 在这里只是**没有收益**,并没有错:过线的等待语义不变,esc=64 与 esc=0 持平。探针都已撤掉,`ReportSignaledFences` 保持门禁通过时的原样。
**真机上的答案(C,[`C-MEASUREMENTS.md`](C-MEASUREMENTS.md) §1)**:在红米(Adreno 830)上同一个 bench,Espryt esc=64 时 0 次往返、598 / 598 次本地作答,与 Magma 相同。上面的现象只出现在 llvmpipe 上。
-46
View File
@@ -1,46 +0,0 @@
# P10 C:真机测量与往返普查
2026-09-29。真机:Redmi `2f7cbe2e`(Adreno 830),代码 `8d0a1b6c`;CPU 定频 1555200 / 1958400 并回读核验,GPU 未定频。只记录、不设门。原始数据在 `~/w7/notes/p10/evidence/c/`(报告 `~/w7/notes/p10/c-report.md`)。
## 1. fence bench 在 Adreno 830 上(Split / inproc,600 帧,3 轮交错)
`FencePollScenario.PacedFrameFenceWaitsBench`,每帧对 2 帧前的 fence 做 timed wait。
| 后端 | esc=0 ms/帧 | esc=64 ms/帧 | esc=64 fence 往返 / 本地作答 | 回复槽记录 0 → 64 |
|---|---|---|---|---|
| Espryt | 0.163 / 0.153 / 0.145 | 0.156 / 0.140 / 0.157 | **0 / 598** | 601 → 3 |
| Magma(tier 2) | 0.167 / 0.176 / 0.181 | 0.191 / 0.175 / 0.174 | 0 / 598 | 601 → 3 |
| Magma(`.Fence.` 带状环境,与主机 bench 同) | 0.171 / 0.167 / 0.164 | 0.182 / 0.160 / 0.182 | 0 / 598 | 601 → 3 |
- 延迟 4 帧:Espryt 0 / 596;延迟 1 帧:两后端都是 0 / 599。
- **结论:真 GPU 上 Espryt 与 Magma 一样,每次等待都在本地作答。** [`A-FENCE-POLL.md`](A-FENCE-POLL.md) 里"fence 只有阻塞等待后才可见"是主机 llvmpipe 独有的行为。inproc 的每帧耗时在噪声内不变(每次运行约 0.1 s,10% 以内的差都是噪声);收益在有往返延迟的链路上(主机 Magma·tcp −37%)。
- Spawn 臂没跑:Android 测试框架总是画进 `ANativeWindow`,spawn 出来的 server 拒收(`Fatal{UnmigratedSurface}`)。
- **Android 上的 itest 本身跑不了这个 bench**:`SplitRuntimePeek` 在 `__ANDROID__` 下编译掉,itest 又链接的是隐藏符号的 `libMobileGL.so`。测量用的是本地构建补丁(`-DMGITEST_ANDROID_STATIC_PEEK=ON`:Android 上改链静态库并开启 peek,库代码与编译选项不变),未提交,存为 `evidence/c/peek-patch.diff`(ID-P10-14)。
## 2. `MOBILEGL_IPC_PRESENT_CREDIT` 1 / 2 / 3,设备上 spawn + tcp(127.0.0.1),trace APK,Espryt,3 次交错
| 臂 · 负载 | credit 1 | credit 2 | credit 3 | 散布 | credit 等待 ms/帧 | 传输等待 ms/帧 | client CPU ms/帧 |
|---|---|---|---|---|---|---|---|
| tcp · rd12(稳态 fps) | 51.4 | 51.6 | 52.4 | 1–3% | 0.18–0.24 | 11.2 | 9.9 |
| 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`](../../../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。
- P12 同款明细(`evidence/c/m2/m2.p12style.md`):rd12 加载帧 10.6–10.8 s、稳态 73–74 MB/s;`wait_replies` 全在第 1 帧(`ResourceCreate`)。
## 3. 主机 retrace 往返普查(`MOBILEGL_PIPE_STATS=1`,全部 SPLIT / SPAWN 条目)
集成头 `47bbf5e6`,156 条(39 个 trace × 两后端 × SPLIT / SPAWN),`P65LinkMetrics` 的逐帧 `wait_replies` 按 op 分。全表在 `~/w7/notes/p10/evidence/census.md`,脚本 `~/w7/notes/p10/census.py`。
| 稳态(第 2 帧起)的回包等待 | 全语料 | 出处 |
|---|---|---|
| draw / state / upload 路径 | **0** | 没有一条 draw、状态设置或 `ResourceSubData` 记录等待回复 |
| `ResourceCreate` | 243,172 | 每个资源一次(modernui-inventory 每帧约 6.5 次);P12 已裁定保持 `kWaitReply`([`../p12/DEVICELOST-AND-UPLOAD-WAITS.md`](../p12/DEVICELOST-AND-UPLOAD-WAITS.md) §2.4) |
| `FenceWait` | 10,685 | **只有** `improved-transparency-minecraft-26.3` × Espryt(每帧约 4.4 次 timed wait),即 llvmpipe 的现象(ID-P10-6);同一 trace 在 Magma 上是 0 |
| `MapPersistent` | 192 | 每个 trace 1–9 次 |
- 39 个 trace 里有 34 个建 fence(多数每帧一个),其中 33 个在任何一条臂上都**没有**发出 `FenceWait` / `FenceStatus` 记录,也就是轮询全部在本地作答;唯一的例外是上表中 llvmpipe 上那一条。
- iterationrp 的 6 条 retrace 因宿主 JIT 崩溃判红,但崩溃前的帧照常计数。
@@ -1,20 +0,0 @@
# P10 集成者裁定日志(sync / query / present 节奏)
> 形式沿用 [`../p9/INTEGRATOR-DECISIONS-P9.md`](../p9/INTEGRATOR-DECISIONS-P9.md):一条裁定一个 ID,只记「定了什么、为什么、影响哪些文件」。计划与范围重划见 [`PLAN-P10.md`](PLAN-P10.md),协议见 `MobileGL/MG_Remote/CONTRACT-P10.md`。
| ID | 日期 | 裁定 | 理由 | 影响 |
|---|---|---|---|---|
| ID-P10-1 | 2026-09-29 | 范围按计划 §1 重划:client 铸造 query handle、present 1:1、credit 默认 1 已是现状,关闭;"DirectGLES fence 逐 fence 退休"与"非 present fence tick"不单独做——线上不依赖帧水位(每批后 `RetireThrough(appliedSeq)`),帧水位只喂 Espryt 自己的 buffer 池回收,与 monolith 相同;A 的"空闲时也查 fence"就是线上的非 present tick。 | 路线图行写于 09-22,逐项对照树的现状核对过。 | `PLAN-P10.md` |
| ID-P10-2 | 2026-09-29 | A 的形状:server 按创建顺序查 fence,完成的发 `kEventFenceSignaled`(`EventKind` 追加 5,8 字节);client 已知 signaled 的轮询本地作答,不能等的未知轮询本地答"未完成",同一 fence 连续第 `MOBILEGL_IPC_POLL_ESCALATE`(默认 64)次改为一次往返;**可以等的调用(timeout > 0)仍然往返**,语义不变。旋钮为 0 就恢复 P10 之前的行为,作 A/B 控制;设备丢失后一律走往返臂。 | 本地作答只用 server 说过的事实,所以不会早报;升级保证"只轮询不发记录"的循环有界。timed wait 本地等报告需要另一套唤醒路径,往返本身就是那次等待。 | `EventRing.h`、`ServerSession.{h,cpp}`、`PipeApplier.{h,cpp}`、`ServerLoop.cpp`、`ClientSession.{h,cpp}`、`EmitTables.{h,cpp}`、`Config.h`、`ConfigLoader.cpp` |
| ID-P10-3 | 2026-09-29 | client 记"已 signaled"的表用独立互斥锁,不用 `g_fenceMutex`。 | 事件排空发生在持有 `g_fenceMutex` 的发射函数内部(`EmitAndWait`、`PollEntry` 都排空),用同一把锁会让 GL 线程自锁。 | `EmitTables.cpp` |
| ID-P10-4 | 2026-09-29 | server 将要空闲且有未报告 fence 时:对最老的 pending fence 做一次 `ClientWaitSync(GL_SYNC_FLUSH_COMMANDS_BIT, 0)`(两个后端都会借此提交所持命令的唯一后端无关方式),仍有未报告的就限时停车 1 ms 再查;否则照旧无限期停车。`SEG_EVENT` 满时不报告。 | red-once:去掉空闲复查 → Espryt 需 1 次升级,Magma 升级 15 次仍等不到(Magma 的 fence 不会自己提交)。每批都提交会打掉 Magma 的命令缓冲批处理,所以只在空闲时做。 | `ServerLoop.cpp`、`PipeApplier.cpp` |
| ID-P10-5 | 2026-09-29 | `OnPresent` 在归还 present credit **之前**报告一次(状态查询,不提交)。 | credit 放行的 client 会立刻去等一两帧前的 fence;批次结束后才发的报告会输给它。实测只在 Espryt·tcp 有变化(本地作答 36–54 → 79–101 / 598)。 | `PipeApplier.cpp`、`ServerSession.{h,cpp}`(`EventRingHasRoom`) |
| ID-P10-6 | 2026-09-29 | 主机 Espryt 上 A 没有收益:在 mesa llvmpipe(ES 3.2)上 `glGetSynciv` 与零超时 `glClientWaitSync` 看不到早已完成的 fence,只有阻塞等待才能让它变成 signaled。另外试了三种做法——空闲 / present 时冲刷最新的 fence、present 时 flush 检查、1 ns 的 timed wait——都没有效果,全部撤回,`ReportSignaledFences` 保持门禁通过时的原样。真 GPU 上的答案由 C 给出(ID-P10-11)。 | lag 1–6 的本地作答都是 0–6;server 端追踪显示队首 fence 在约 4 帧的每次检查里都是 pending,要等 client 的 timed wait 过线、server 阻塞等待之后才被报告。Magma(lavapipe)同一追踪里报告全部早于等待,tcp 每帧 −37%。 | `A-FENCE-POLL.md` |
| ID-P10-7 | 2026-09-29 | B:`GL_KHR_parallel_shader_compile` 由 client 的编译开关决定,caps mirror 在采纳快照时重算这一项。 | 编译池与 `GL_MAX_SHADER_COMPILER_THREADS_KHR` 都在 client;Split / Spawn 之所以一致,只是因为 server 继承了 client 的环境,tcp 的共享 fixture server 不继承,于是 Tcp 的 `AsyncOff` 用例两后端都红。 | `CapsMirror.cpp`、`RemoteClientTest.cpp` |
| ID-P10-8 | 2026-09-29 | B:发射表的 `SetSwapInterval` 槽改为 server 转发器,`SetSwapInterval_Unmigrated` 与 `BackendObject_Remote` 的覆盖删除;class C 清零(69 已实现 / 0 未迁移)。控制由单元用例 `ServerLoopTest.TheEmitTableSwapIntervalSlotCrossesAsOneDispatchedFrame` 钉住(split 车道观察不到交换间隔)。 | 路线图列为最后一个手写 class-C。原槽是一个永远到不了的具名 Fatal,实际调用一直走另一条路。 | `EmitTables.{h,cpp}`、`BackendObject_Remote.{h,cpp}`、`ServerLoopTest.cpp`、`RemoteClientTest.cpp` |
| ID-P10-9 | 2026-09-29 | B 的登记:`AsyncCompileScenario`(`AsyncOn` / `AsyncOff` / `OptimisticShaderStatus` 三形)与 `XfbPrimitiveQueryScenario` 在两后端的 Split / Spawn / Tcp 上登记;`PrimitivesGeneratedNoXfbScenario` 只登记 Magma(Espryt 上每例都按设计跳过),其中 `PrimGenReroute` 不登记 Magma Tcp(共享 fixture server 读不到这个开关,8 例在 `spawn_lane_parity.py` 里具名列出)。 | 保证跳过的登记只会让门看上去更大。 | `MG_IntegrationTest/CMakeLists.txt`、`spawn_lane_parity.py` |
| ID-P10-10 | 2026-09-29 | B 查出、**不在 P10 改**的两项:(1) `GL_ARB_gpu_shader_fp64` 按 server 的 `MOBILEGL_ADVERTISE_FP64` 宣告,但 fp64 收窄发生在 client 的编译里,tcp 下 server 环境不同时会不一致,属于同类问题(挂点是 `CapsMirror::Adopt`)→ 债;(2) Magma 没有注册 `SetSwapInterval`,monolith 与 split 下 `eglSwapInterval` 都是空操作 → dev 线独立任务。 | 前者无触发报告,挂点已有;后者是 dev 的缺陷,按规矩不顺手修。 | `DEBTS.md`;独立任务 |
| ID-P10-11 | 2026-09-29 | 真机(Adreno 830,`8d0a1b6c`)上 A 成立:同一 bench 在 esc=64 时,Espryt 与 Magma 都是 0 次 fence 往返、598 / 598 次本地作答,回复槽记录 601 → 3;lag 1 / 4 也一样。ID-P10-6 的现象只出现在 llvmpipe 上,A 的实现不需要按后端区分。 | 这正是 ID-P10-6 留给 C 的问题。 | `C-MEASUREMENTS.md` §1 |
| ID-P10-12 | 2026-09-29 | **P10 收官**。集成头(A + B,`47bbf5e6`)整套门:G1 符号增 0 减 0、`.text` `0xa52203`(同机 clang 22.1.6,与 P9 相同);fatal census 79、link ratchet 171、spawn parity 0、协议修订 3、wire-declines 53/53;卫生 12/12;unit 2644;integration-split / spawn / tcp 356 / 273 / 277;integration-magma-split / spawn / tcp / full-split 174 / 153 / 130 / 583;integration-gpu monolith / inproc 2348 / 2348。verify 构建 unit / integration-verify / integration-verify-split 2644 / 1238 / 1172。主机 retrace 230 / 236,6 项仍是 iterationrp 的宿主 llvmpipe JIT 崩溃(monolith 同挂,同 P9)。出口门"trace 上 draw / state / upload 往返读零"成立:`MOBILEGL_PIPE_STATS=1` 普查 156 条 SPLIT / SPAWN 条目,稳态回包等待只有 `ResourceCreate`(P12 已裁定保持)、`MapPersistent`,以及 llvmpipe 上 Espryt 的 `FenceWait`(`C-MEASUREMENTS.md` §3)。收官审查沿用 P12:不派 agent / Codex,契约断言由集成者对照代码核过(`kEventFenceSignaled = 5` 与 8 字节载荷、`PollEscalate` 默认 64 且范围 0..1000000、`kFenceIdlePollMs = 1`、控制协议修订 3、class C 无 `_Unmigrated` 桩)。 | — | 全阶段 |
| ID-P10-13 | 2026-09-29 | `PRESENT_CREDIT` 默认保持 1。设备上 tcp loopback 的 credit 1 / 2 / 3 在噪声内(rd12 51.4 / 51.6 / 52.4 fps,credit 等待 ~0.2 ms / 帧,传输停车 ~11.2 ms / 帧);跨机 Wi-Fi 的 +26%(P12)是按链路调的旋钮,不是默认值问题。present credit 的直方图不另做:`P65ClientPace` 每次 present 都打 `credit_wait_ms`,分布可以直接从日志得到。loopback 上的下一个开销是 stream 传输的停车:记为性能债,等路线图推完再回头做(用户 09-08:性能只记录)。 | 路线图说"credit > 1 从未量过",现在两条链路都量过了;推翻五臂报告的建议 #2(那组数据是旧构建、未定频)。 | `C-MEASUREMENTS.md` §2 |
| ID-P10-14 | 2026-09-29 | 让 Android itest 读到 split 运行时计数的构建补丁(`-DMGITEST_ANDROID_STATIC_PEEK=ON`:改链静态库并开启 `SplitRuntimePeek`)**不入库**,只存为证据(`~/w7/notes/p10/evidence/c/peek-patch.diff`)。 | 只是这一次测量要用;要让设备上的计数门成为一条车道,得先给 Android 框架加 pbuffer 路径(spawn 臂也缺这个),那是单独一项工作。 | 无(证据) |
-38
View File
@@ -1,38 +0,0 @@
# P10 计划:sync / query / present 节奏(重划范围)
> 2026-09-29,基于 `feat/disaggregated@de33d401`(P9 收官头)。路线图行(原文在 [`README.md`](README.md))写于 09-22,与树的现状逐项核对如下;**先读 §1**。
## 1. 路线图每一项的现状
| 路线图写的 | 现状(核对) | 处置 |
|---|---|---|
| 轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE` | **没做**:`glClientWaitSync` / `glGetSynciv` 都是 `kWaitReply`(`FenceWait` op 9、`FenceStatus` op 8),每次都要等 server 把它之前**所有**记录 apply 完再回复——run-ahead 下等于一次全管线同步。FCL 里的 Minecraft 稳态每帧约 1 次(P12 验收 A);TCP 上再加一个 RTT。旋钮 0 处 | **A**(核心) |
| DirectGLES fence 完成度逐 fence 退休;非 present fence tick | 线上**不依赖**帧水位:server 每批 apply 后 `RetireThrough(appliedSeq)`(`ServerLoop.cpp`),`SEG_STAGE` 不会在无 present 负载里饿死。帧水位只喂 Espryt 自己的 buffer 池回收,与 monolith 同 | 不单独做;A 需要"空闲时也检查 fence",那一点就是线上的非 present tick |
| `present` 1:1、credit 默认 1、client 铸造 query handle | 已是 | 关闭 |
| `PRESENT_CREDIT > 1` 从未量过 | 跨机 Wi-Fi 已量(P12:rd12 稳态 1→2 +26%、2→3 +4%,[`../p12/CROSSHOST-ACCEPTANCE.md`](../p12/CROSSHOST-ACCEPTANCE.md));**TCP loopback 未量**([`../perf-five-arm-20260928`](../perf-five-arm-20260928/README.md) 建议 #2 "未实测,待 A/B") | **C** |
| roundtrip 计数器与输入延迟直方图 | 部分已有:`P65LinkMetrics` 逐 op 回包等待 + 回复延迟直方图(P12);trace 稳态回包等待 0(五臂报告)。缺:present credit 等待的直方图、逐 trace 的往返普查记录 | **C**(记录);直方图若缺则补 |
| `SetSwapInterval`(最后一个手写 class-C) | `eglSwapInterval` 实际走 `BackendObject_Remote::SetEGLSwapInterval` → 控制帧 `SurfaceControlOp::SetSwapInterval`,跨进程可用;发射表槽 `SetSwapInterval_Unmigrated` 是永远到不了的具名 Fatal | **B**:槽改为同一转发,class-C 清零 |
| 门:query / XFB / `AsyncCompile` 场景在 split 下绿 | `SyncWire`、四个 XFB 场景已登记 split;**`AsyncCompileScenario`、`XfbPrimitiveQueryScenario`、`PrimitivesGeneratedNoXfbScenario` 未登记任何 split 臂** | **B** |
| 门:零 timeout 轮询有界退出 | 今天靠每次往返保证进度;A 之后由升级规则保证 | **A** 的门 |
## 2. 工作包
### A — fence 轮询本地作答(核心)
- **server**:apply 线程在每个 drain 批次之后、以及空闲等待时(有未报告的 fence 就限时停车,默认 1 ms 再查)按创建顺序用后端表 `GL.GetSyncStatus` 查尚未报告的 fence;新近 signaled 的发一条 `kEventFenceSignaled{handle}`(EventKind 只追加)。后端无关(两个后端都实现 `GL.FenceSync/GetSyncStatus/ClientWaitSync`)。
- **client**:排空事件时记下已 signaled 的 fence;`glClientWaitSync` / `glGetSynciv(GL_SYNC_STATUS)`:已知 signaled → 本地答 `GL_ALREADY_SIGNALED` / `GL_SIGNALED`;未知且 timeout = 0 → 先 publish + 敲门铃(轮询入口是门铃点;`GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 flush),排空事件,仍未知则本地答 `GL_TIMEOUT_EXPIRED` / `GL_UNSIGNALED`;同一 fence 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64;0 = 关闭本地作答,即 A/B 控制)无进展 → 升级成一次真往返;timeout > 0 且未知 → 真往返(语义不变)。
- **门**:零 timeout 轮询循环在 split / spawn / tcp × 两后端有界退出;轮询不产生回复槽记录(计数门,red-once = `POLL_ESCALATE=0`);`SyncWireScenario` 不变绿;停车的 server 仍会报告后来才 signaled 的 fence(red-once = 去掉空闲检查)。
### B — split 覆盖与最后一个 class-C
- `AsyncCompileScenario`、`XfbPrimitiveQueryScenario`、`PrimitivesGeneratedNoXfbScenario` 登记到两个后端的 Split / Spawn / Tcp 臂,红的修(两后端都修)。
- `SetSwapInterval` 发射表槽改为转发,删 `SetSwapInterval_Unmigrated`。
### C — 测量(A 落地之后)
- 红米 `2f7cbe2e`:`PRESENT_CREDIT` 1 / 2 / 3 × {spawn+tcp localhost, WSL → 手机 Wi-Fi},rd12 与 openra,每档 ≥ 3 次交错;`POLL_ESCALATE` 0 / 64 的配对。只记录、不设门(性能只记录,用户 09-08)。
- 主机 retrace 语料 `MOBILEGL_PIPE_STATS=1`:逐 trace 的稳态回包等待按 op 列表,draw / state / upload 路径应为 0。
## 3. 门与规矩
同 P9([`../p9/HANDOFF-P9.md`](../p9/HANDOFF-P9.md) §5–§6):`~/w7/notes/p9/gate.sh` 全套 + verify 构建三车道;G1 同机比较;改 wire 按规矩(EventKind / opcode 只追加);每个新门 red-once;设备只用红米;不拉 LFS;提交一行、无署名尾注。
-25
View File
@@ -1,25 +0,0 @@
# P10 — sync / query / present 节奏(已收官,2026-09-29)
> **2026-09-29 收官**。协议与行为:`MobileGL/MG_Remote/CONTRACT-P10.md`;裁定 [`INTEGRATOR-DECISIONS-P10.md`](INTEGRATOR-DECISIONS-P10.md);计划与范围重划 [`PLAN-P10.md`](PLAN-P10.md);测量 [`A-FENCE-POLL.md`](A-FENCE-POLL.md)、[`C-MEASUREMENTS.md`](C-MEASUREMENTS.md)。
>
> | 包 | 结果 |
> |---|---|
> | A fence 轮询 | server 报告完成的 fence(`kEventFenceSignaled`,含 present 时、归还 credit 之前),client 本地作答;第 `MOBILEGL_IPC_POLL_ESCALATE`(64)次未答的零超时轮询才往返;timed wait 语义不变。主机 Magma·tcp 每帧 −37%、fence 往返 598 → 0;主机 Espryt 无收益(llvmpipe 上只有阻塞等待才能看到 fence 完成);Adreno 830 上两后端都是 0 次往返、598 / 598 次本地作答 |
> | B 覆盖与 class C | `SetSwapInterval` 槽改为 server 转发,class C 清零;`GL_KHR_parallel_shader_compile` 改由 client 决定;AsyncCompile / XfbPrimitiveQuery / PrimitivesGeneratedNoXfb 登记到 split 各臂(Espryt 27 条、Magma 64 条) |
> | C 测量 | 设备上 tcp loopback 的 `PRESENT_CREDIT` 1/2/3 在噪声内(rd12 51.4 / 51.6 / 52.4 fps),默认保持 1,下一个开销是传输停车(性能债);主机 retrace 往返普查见 `C-MEASUREMENTS.md` §3 |
>
> 门(集成头):见 ID-P10-12。
>
> 下面是该阶段在路线图上的原始范围与出口门(2026-09-24 从 [`ROADMAP.md`](../../../Disaggregated/ROADMAP.md) 阶段表移入,原文照录)。
## 摘要
- 轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`;DirectGLES 的 fence 完成度改为逐 fence 退休;非 present fence tick;**`PRESENT_CREDIT > 1` 从未量过**(带延迟链路上的首要杠杆);roundtrip 计数器与输入延迟直方图;`SetSwapInterval`(最后一个手写 class-C)。
- 门:query / XFB / `AsyncCompile` 场景在 split 下绿;draw / state / upload 路径 roundtrip 读零;credit 1..3 在 TCP loopback 与跨机臂上的配对记录。
## 阶段表行(原 `ROADMAP.md`)
- **阶段**:**P10** sync / query / present 节奏
- **状态**:待排
- **落地什么 / 范围**:~~client 铸造 query handle~~(已是:`QueryCreate` 为 `kWaitNone`);轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`(0 处;TCP 上是首要杠杆之一);DirectGLES 的 fence 完成度来自逐 fence 退休(`g_completedFrameSerial` 仍只在 `Present` 前进;Magma 那半已由 run-ahead 落地);非 present fence tick(唯一记载处);`present` 1:1(已是);~~credit 默认 1~~(已是默认,`ConfigLoader.cpp:414`);**`PRESENT_CREDIT > 1` 从未量过**(重审 §8.7)——带延迟链路上的首要杠杆,inproc 臂上即可取;roundtrip 计数器与输入延迟直方图(今天只有 `MapPersistentRoundtrips`);`SetSwapInterval`(最后一个手写 class-C,`EmitTables.cpp:1665`)
- **验收门 / 证据**:query / XFB / `AsyncCompile` 场景在 split 下绿;trace 上 draw / state / upload 路径 roundtrip 读零;零 timeout 轮询有界退出;credit 1..3 在 TCP loopback 与跨机臂上的配对记录;配对 A/B 记录
-38
View File
@@ -1,38 +0,0 @@
# P11 A3 / A4:app 域外存探针与采纳基线(真机)
2026-09-29,Redmi `2f7cbe2e`(Adreno 830,GLES 3.2 V@0800.71,Vulkan 1.3.284 / 512.800.71)。代码 `ffeed0da` + 三个只动工具的提交(`p11/d`:`ea410991`、`1082ff5e`、`f4ead90a`)。CPU 定频并回读核验,GPU 未定频。只记录、不设门。原始数据 `~/w7/notes/p11/evidence/d/`,报告 `~/w7/notes/p11/d-report.md`。
## A3:T0 在 app 域能不能用(开放问题 3)
同一个探针 `.so`,shell 域(`u:r:shell:s0`)与经 trace app 的 exec 钩子在 `untrusted_app`(`u:r:untrusted_app:s0:c83,c257,c512,c768`)里各跑 5 组配置 × 2 次,交错进行。
| 路线 | shell / app |
|---|---|
| T0 AHB:交接、CPU 锁、Vulkan 导入、GPU 访问、GLES 导入、写回 client(64 KiB、128 MiB 两端区域) | OK / OK |
| **T0S 持续锁定**(client 从不解锁;8 轮 GPU 读 CPU 写、CPU 经持有的指针读 GPU 填充),Vulkan 与 GLES、64 KiB 与 128 MiB | OK / OK |
| T1 opaque-fd | OK / OK |
| T1-GLES memobj | FAIL / FAIL(与 spike B 同) |
| T3 | UNSUPPORTED / UNSUPPORTED |
**结论:两域逐行无差别;Vulkan 导入与 GLES `glBufferStorageExternalEXT` 导入都是 GO。** 局限:一台设备;持续锁定是顺序的乒乓,不是真正并发的 CPU / GPU 访问。
## A4:采纳基线(中位数,5 次交错)
只有 MC 26.3(`improved-transparency-minecraft-26.3`)会触发 ≥ 16 MiB 采纳;rd12 不会,Create 的 fixture 只有 2 帧。
| MC 26.3 | 单进程(采纳) | 单进程(关采纳) | spawn+shm(T2) | inproc(T2) |
|---|---|---|---|---|
| Espryt 稳态 p99 ms | 32.6 | 33.3 | 44.8 | 46.2 |
| Espryt 峰值 RSS MB(client + server) | 1098 | 1578 | 2205 | 2108 |
| Magma 稳态 p99 ms | 30.4 | 30.6 | 77.0 | 78.6 |
| Magma 峰值 RSS MB | 1150 | 1506 | 1751 | 1661 |
- **采纳开 / 关(Adreno)**:帧时没有差别(稳态 p99、世界内 p99、加载尖峰总和都在散布内),Mali 上的"163 → 21 ms"在这里不成立;RSS 省 480 MB(Espryt)/ 356 MB(Magma),与"~400 MB"相符。路线图的三个数由这张表取代。
- **split T2 相对单进程采纳**:RSS Espryt 约 2.0× / 1.9×、Magma 约 1.5× / 1.4×(rd12 上 +200–280 MB);p99 Espryt +37–42%、Magma 约 2.5×(世界内帧约 2.7× 慢)。
- **线程落位扭曲 split 的时间**:server apply 线程钉在 6–7 核,client GL 线程随之大多落到慢核(大核占比 0.19–0.23,单进程 0.87–0.90),每帧 CPU 约 2.3×;唯一落到 7 号核的一次 8.3 ms,与 P10 一致。RSS 不受影响。
- spawn 臂的共享段(约 40 MB)在两个进程的 RSS 里各算一次,去重后的和约少 40 MB。
## 没跑成
- Magma 单进程 rd12(两个单进程臂)在加载帧 abort:`vm.max_map_count`(65530)耗尽,其中 57–60k 是 `/dev/kgsl-3d0` 映射;不带任何 `MOBILEGL_*` 也复现,与采纳无关。split 的 Magma rd12 与单进程 Magma 的 MC 26.3 正常。已转 dev 独立任务(`../DEBTS.md`)。
- tcp 臂按计划不跑(P10 已有 loopback 数)。
-43
View File
@@ -1,43 +0,0 @@
# P11 B0:同机跨 app 的 client 能否走 unix + 共享内存(真机)
2026-09-29,Redmi `2f7cbe2e`(Adreno 830,Android 16 / HyperOS),代码 `82467823`。server = trace APK #1(`top.mobilegl.plugin.trace`,`untrusted_app:c83`);client = adb shell 里的 retrace 可执行文件(`shell`)与 trace APK #2(`top.mobilegl.plugin.b0.trace`,`untrusted_app:c84`)。只记录、不设门。原始数据 `~/w7/notes/p11/evidence/b0/`,报告 `~/w7/notes/p11/b0-report.md`。
## 结论
**SELinux 挡的是连接这一步。** 另一个安全上下文里的 client 连不上 server app 的 `@abstract` 端点:`unix_stream_socket connectto` 被拒,`shell → untrusted_app:c83` 与 `untrusted_app:c84 → c83` 都一样。一旦连上(用 root relay 代替 broker),现有代码就走 SharedSegments(7 个 fd 经 `SCM_RIGHTS`:4 个段、3 个门铃),c84 读写、mmap c83 的段都没有拒绝,ssim 1.0。**数据面与协议都不用改,缺的是跨上下文的连接中介。**
| client → endpoint | 连接 | 数据面 | ssim(openra / rd12) |
|---|---|---|---|
| shell → `@mglb0` | **拒**(`avc: denied { connectto } … scontext=u:r:shell:s0 tcontext=u:r:untrusted_app:s0:c83,…`) | — | — |
| APK #2 → APK #1 的 `@mglb0` | **拒**(类别不同) | — | — |
| APK #1 自己的 client → 自己的 server | OK | SharedSegments | 1.0 |
| shell / APK #2 → tcp loopback | OK | Stream | 1.0 / 0.99988 |
| root → `@mglb0`;shell、APK #2 → root relay → `@mglb0` | OK | SharedSegments,7 fd | 1.0 / 0.99988 |
## 线程落位决定 shm 是否更快(Espryt rd12 稳态 fps)
| 落位 | tcp | shm |
|---|---|---|
| 默认(5 次) | 45–52 | **26** |
| server apply 线程在 cpu6、client 在 cpu7(3 次) | 54.5 | **118.0** |
默认下 apply 线程亲和两个大核(`auto` = `0xc0`)且自旋,外部 client 的 GL 线程落到同一对核上。干净落位时 shm 与 P10 的 app 内 shm(120 fps)相同,openra Espryt 491 → 903。Magma rd12 在各臂都受 server 限制(默认 19 tcp / 23 shm)。
## B1 要做的(按代码)
| # | 件 | 依据 |
|---|---|---|
| 1 | server app 里导出、校验调用方的 bound Service,把 client 的两条已连接套接字经 Binder 交出(最简:Service 自己连两次自己的端点,同 app 允许) | `MobileGLServerService.onBind` 今天返回 null;唯一入口是 `Listen` + `AcceptPair` |
| 2 | client 端点接受已连接的 fd(如 `fd:<control>,<aux>`) | `ClientSession.cpp:846-855` 今天只收 `fork` / `tcp://` / `unix:` |
| 3 | client 侧能调用 `bindService` 的引导:有 Java 的 client(FCL、启动器)直接用;Termux 一类原生程序没有 `Context`,要伴生的 helper(如 `app_process`)——**待用户裁定** | 原生进程没有 `Context`;`AServiceManager` 只到系统服务 |
| 4 | 按身份配对控制与辅助连接,不按到达顺序 | F2(下) |
| 5 | 外部 client 使用期间 server 进程不被冻结 | F1(下) |
| 6 | 外部 client 会话的亲和 / 自旋策略 | 上表 |
| 7 | broker 上的调用方认证(unix 端点今天没有令牌) | `Handshake.h` `AuthenticatePeerToken` 无令牌即通过 |
## 顺带查出
- **F1 HyperOS 熄屏冻结整个 server uid**(`frozen uid = … reason=screen off`),FGS 进程、supervisor 与会话子进程同一 cgroup,在飞的会话 120 s 后 `Fatal{BarrierTimeout}`。FCL 的同机用法也会碰到。
- **F2 `AcceptPair` 按到达顺序配对**(`SocketTransport.cpp:373-420`;产品的 TCP 路径本来就按身份配对——一条控制连接、数据连接按 nonce 绑定——受影响的是 unix 端点与非 `--serve` 的一次性 TCP server;**已修**,P11 PAIR,ID-P11-12):`ServerControlActivity` 的就绪探测是一次空的 `connect()`+`close()`,2 s 内会与下一个真 client 的控制连接配成一对,会话读到探测的套接字后退出;两个 client 同时连也会交错。
- 构建:`-Pmobilegl.apkSuffix` 只改文件名;包名由 `-Pmobilegl.applicationIdSuffix` 决定。
- 工具提交 `72926a88`(`p11/b0`):`tools/device_bench/disagg/fdrelay.c`,root 的字节 + `SCM_RIGHTS` 中继,B1 broker 的替身。
-43
View File
@@ -1,43 +0,0 @@
# P11 B2:T0 零拷贝导入(真机)
2026-09-29,Redmi `2f7cbe2e`(Adreno 830),默认环境、两个 app 的 HyperOS 电池设「无限制」、CPU 定频并回读、无解冻器。协议与行为见 `MobileGL/MG_Remote/CONTRACT-P11.md` B2 节;报告 `~/w7/notes/p11/b2-report.md`,证据 `~/w7/notes/p11/evidence/b2/`。
## 开关
`MOBILEGL_IPC_ADOPT_TIER`:**不设(默认,用户 2026-09-29)= 请求 T0**,本会话用不了(server 自测不通过、server 端 `MOBILEGL_IPC_ALLOW_ADOPT_T0=0`、Stream 数据面、没有 AHB)时静默退回 T2(只记 MGLOG_D、计数);`0` = 同样请求 T0,但退回时两端各一行具名日志;`1` = `Refuse{AdoptTierClosed}` 后 T2;`2` = 与之前完全相同(共享段 + client 推送,T2)。不 Fatal。默认开后在红米上不设开关复跑 MC 26.3 inproc:两后端都导入 8 个缓冲(462,946,304 字节)、ssim 通过、无 T0 相关的 W/E 行。控制协议修订 4 → 5(Hello 带 client 请求的档位,Welcome 回显)。
## 先验:AHB 的 fd 跨过每条路线(先于任何构建)
| 路线 | client → server | 结果 |
|---|---|---|
| shell → server app(B1 helper 的 `fd:`) | `shell` → `untrusted_app:c87` | 64 KiB、32 MiB、128 MiB 都通;server 的 GPU 读到 client 在从不解锁的锁下写入的字节;0 个 avc |
| 另一个 app → server app | `c84` → `c87` | 通 |
| 同 app(经 helper / 直连 / spawn 的子进程) | `c87` → `c87` | 通 |
## MC 26.3:开关 0 vs 2(3 次交错,中位)
内存口径 = 两进程 VmHWM(去重共享页)+ kgsl 驱动分配 + kgsl 导入(AHB 页只出现在导入里,RSS 看不到)。
| 后端 | 臂 | 开关 | p99 ms | GL / apply CPU ms/帧 | **峰值总内存 MB** | 稳态 MB |
|---|---|---|---|---|---|---|
| Espryt | inproc | 2 | 46.7 | 10.9 / 11.3 | 3010 | 2992 |
| Espryt | inproc | 0 | 44.1 | 11.5 / 11.3 | **1758(−42 %)** | 1492 |
| Espryt | spawn+shm | 2 | 44.5 | 10.5 / 11.2 | 3049 | 3029 |
| Espryt | spawn+shm | 0 | 44.5 | 10.5 / 10.7 | **1789(−41 %)** | 1526 |
| Magma | inproc | 2 | 75.6 | 15.9 / 20.1 | 2677 | 2669 |
| Magma | inproc | 0 | 75.9 | 15.9 / 20.4 | **1971(−26 %)** | 1774 |
| Magma | spawn+shm | 2 | 76.3 | 15.9 / 20.4 | 2731 | 2713 |
| Magma | spawn+shm | 0 | 75.3 | 15.2 / 19.6 | **2244(−18 %)** | 1815 |
- ssim 开关 0 与 2 逐位相同:MC 26.3 Espryt 0.999684、Magma 0.999600;create-instancing 0.99998。每个开关 0 的会话 8 个大缓冲(441.5 MiB)全部导入。
- 帧时与 CPU 不变;每帧 stage 字节几乎不变(1071 → 1052 KiB)——T0 省的是内存,不是上传量。
- B1 的外部路线(另一个 app 经 helper)同样走 T0:Espryt 峰值 2830 → 1590 MB。
## T0 专属改动的 red-once(真机,Android 集成测试 + static-peek,inproc)
| 改动 | 变红的用例 | Espryt | Magma |
|---|---|---|---|
| 常驻写直接写进 AHB | `ADrawQueuedBeforeASubDataKeepsItsOwnBytes` | 红 | 红 |
| respecify 时立即释放导入 | `ARespecifyWithADrawQueuedKeepsThatDrawsStore` | 绿(驱动大概自己持有引用,未核实;带 fence 的释放保留) | 红 |
| 不发布 `kCapAdoptT0` | `TheArenaLandsInTheTierItsLaneDeclares` | 红 | 红 |
| CPU 读跳过回读往返 | `ReadbackSeesTheLatestCpuWrite`、`GpuWriteIntoTheArenaIsReadBack` | 红 | 红 |

Some files were not shown because too many files have changed in this diff Show More