mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[Docs, Chore] (docs, tools, MobileGL, scripts, CONTRIBUTING.md, .gitignore): translate the documentation and use US spelling
This commit is contained in:
@@ -1,36 +1,36 @@
|
||||
# 设计与架构(索引)
|
||||
# Design and architecture (index)
|
||||
|
||||
> 这一页只有设计要点和章节去向;每一部分的完整设计在 [`design/`](design/)。章节编号 §1–§17、附 A / B 沿用至今,代码注释里写的 `ARCHITECTURE.md §N` 按下表找到对应文件。各阶段的 wire 契约原文见 git 历史,与设计文字冲突时以更新的契约为准。
|
||||
> This page holds only the design points and where each chapter lives; the full design of each part is in [`design/`](design/). Section numbers §1–§17 and appendices A / B are still in use; `ARCHITECTURE.md §N` written in code comments maps to a file in the table below. The original wire contracts for each phase are in git history; where they conflict with the design text, the newer contract wins.
|
||||
|
||||
## 一句话
|
||||
## In one sentence
|
||||
|
||||
后端本来就维护着一台贴着 GLES / Vulkan 的状态机,缺的只是"我被告知了什么"。MGPipe 让前端在每条做事的命令之前,把状态变化以**记录**的形式推给后端;后端只认这些记录、不读前端内存,所以两者之间可以隔一个线程、一个进程或一根网线。
|
||||
The backend already maintains a state machine that tracks GLES / Vulkan; all that is missing is an explicit statement of "what I was told". MGPipe has the frontend push state changes to the backend as **records** before each command that does work; the backend recognizes only these records and does not read frontend memory, so a thread, a process, or a network cable can sit between the two.
|
||||
|
||||
## 设计要点
|
||||
## Key design points
|
||||
|
||||
1. **一份调用目录生成一切**:`PipeCalls.def` 一行一个调用,线上编号就是行序、只追加;两张函数指针表、编解码、校验器都由它生成。
|
||||
2. **对象用句柄 `{slot, gen}` 寻址**:由前端分配,后端从不回传,所以创建对象不需要往返。
|
||||
3. **状态在命令之前一次性推送**,不是每个 GL setter 都推;整块数据过线、按内容去重,没变就不发。
|
||||
4. **同一份后端实现,三种拓扑**:单线程直调 / 同进程两线程 / 两个进程(可跨机)。
|
||||
5. **反向消息是具名回调**(错误、GPU 写回、窗口尺寸……),走一条与命令同样有序的事件通道。
|
||||
6. **着色器以 SPIR-V + 反射信息过线**:编译在前端,转译与按状态特化在后端。
|
||||
7. **传输分两层**:控制面(握手、EGL 控制)与数据面(命令、批量数据),各自可选本机共享内存或字节流 / TCP,可以混搭;上层代码不区分链路种类。
|
||||
8. **client 发完就走(run-ahead)**:只有需要答案的命令才等;靠 present 配额限速。
|
||||
9. **出错不崩**:server 崩溃或断线时 client 进入"设备丢失"状态;外来连接要令牌,畸形输入被拒绝而不是让 server 退出。
|
||||
10. **可证明的迁移**:每个门都必须能变红;单进程构建的二进制在整个拆分过程中逐字节不变(G1)。
|
||||
1. **One call catalog generates everything**: `PipeCalls.def` has one call per line; the wire number is the line order and is append-only; the two function pointer tables, the codec, and the validator are all generated from it.
|
||||
2. **Objects are addressed by handle `{slot, gen}`**: allocated by the frontend, never sent back by the backend, so creating an object needs no round trip.
|
||||
3. **State is pushed once before a command**, not by every GL setter; whole blocks cross the wire and are deduplicated by content, and unchanged data is not sent.
|
||||
4. **One backend implementation, three topologies**: direct calls in one thread / two threads in one process / two processes (possibly across machines).
|
||||
5. **Reverse messages are named callbacks** (errors, GPU writeback, window size…), over an event channel ordered like commands.
|
||||
6. **Shaders cross the wire as SPIR-V + reflection information**: compiled in the frontend, translated and specialized by state in the backend.
|
||||
7. **The transport has two layers**: control plane (handshake, EGL control) and data plane (commands, bulk data), each independently choosing local shared memory or a byte stream / TCP, and they can be mixed; upper-layer code does not distinguish link types.
|
||||
8. **The client runs ahead (run-ahead)**: only commands that need an answer wait; present credit paces it.
|
||||
9. **Errors do not crash**: when the server crashes or disconnects, the client enters a "device lost" state; external connections need a token, and malformed input is rejected rather than making the server exit.
|
||||
10. **Provable migration**: every gate must be able to turn red; the monolith build's binary stays byte-identical throughout the split (G1).
|
||||
|
||||
## 章节去向
|
||||
## Where the chapters live
|
||||
|
||||
| § | 内容 | 文件 |
|
||||
| § | Content | File |
|
||||
|---|---|---|
|
||||
| 1–2 | 边界:两张表、三种拓扑;对象模型:句柄、两种代数、CSO | [`design/01-boundary-and-objects.md`](design/01-boundary-and-objects.md) |
|
||||
| 3–4 | 调用目录、生成器 G1–G8、能力位;记录与 payload 格式 | [`design/02-catalogue-and-records.md`](design/02-catalogue-and-records.md) |
|
||||
| 5–7 | 前端 tracker(何时推、推什么、怎么去重);纹理上传;着色器 | [`design/03-frontend.md`](design/03-frontend.md) |
|
||||
| 8 | 反向通道:七个回调、有序性、错误与 ack、纹理重铸(server 本地,无拉取)、XFB | [`design/04-reverse-channel.md`](design/04-reverse-channel.md) |
|
||||
| 9–10 | 后端改造(`PipeInputs`、memo 重键、A/B 臂);server 侧 applier | [`design/05-backend-and-server.md`](design/05-backend-and-server.md) |
|
||||
| 11 | 传输:共享段、环、门铃、控制面、背压、等待规则、事件、两根轴 | [`design/06-transport.md`](design/06-transport.md) |
|
||||
| 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 | 各阶段落地时的形状与仍生效的规则(原文见 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) |
|
||||
| 1–2 | Boundary: two tables, three topologies; object model: handles, two generations, CSO | [`design/01-boundary-and-objects.md`](design/01-boundary-and-objects.md) |
|
||||
| 3–4 | Call catalog, generators G1–G8, capability bits; record and payload format | [`design/02-catalogue-and-records.md`](design/02-catalogue-and-records.md) |
|
||||
| 5–7 | Frontend tracker (when to push, what to push, how to deduplicate); texture upload; shaders | [`design/03-frontend.md`](design/03-frontend.md) |
|
||||
| 8 | Reverse channel: seven callbacks, ordering, errors and ack, texture re-mint (server-local, no pull), XFB | [`design/04-reverse-channel.md`](design/04-reverse-channel.md) |
|
||||
| 9–10 | Backend rework (`PipeInputs`, memo re-key, A/B arms); server-side applier | [`design/05-backend-and-server.md`](design/05-backend-and-server.md) |
|
||||
| 11 | Transport: shared segment, ring, doorbell, control plane, backpressure, wait rules, events, two axes | [`design/06-transport.md`](design/06-transport.md) |
|
||||
| 12–13 | Persistent map and large-buffer adoption; readback, round-trip list, five-part verification gate | [`design/07-memory-readback-verification.md`](design/07-memory-readback-verification.md) |
|
||||
| 14–15 | Frame pacing and threads; process, handshake, EGL, Android (including the on-screen presentation server), crash handling | [`design/08-runtime-and-platform.md`](design/08-runtime-and-platform.md) |
|
||||
| 16, Appendix A, Appendix B | Source directory and build options; all switches; boundary counters | [`design/09-build-switches-counters.md`](design/09-build-switches-counters.md) |
|
||||
| 17 | The shape each phase landed in and the rules still in force (originals in git history) | [`design/10-phase-shapes.md`](design/10-phase-shapes.md) |
|
||||
| — | State ownership: multiple sessions / multiple contexts / share groups (prerequisite for the anland unified server) | [`design/11-state-ownership.md`](design/11-state-ownership.md) |
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
# 当前阶段:P13 已收官(2026-10-07);下一阶段 P14
|
||||
# Current phase: P13 closed (2026-10-07); next phase P14
|
||||
|
||||
> **更新 2026-10-07**。这一页只有摘要;计划与裁定见 git 历史。上一阶段 P8 于 09-30 收官。
|
||||
> **Update 2026-10-07**. This page holds only a summary; plans and rulings are in git history. The previous phase P8 closed on 09-30.
|
||||
|
||||
## 目标
|
||||
## Goal
|
||||
|
||||
退役 pull 路径:单进程(FCL 内嵌库)也走 P8 修好的记录臂,删掉旧的「后端直接读前端」路径与它的全部编译期 / 运行期开关。
|
||||
Retire the pull path: the monolith (the library embedded in FCL) also runs on the record arm fixed in P8, and the old "backend reads the frontend directly" path and all its compile-time / run-time switches are deleted.
|
||||
|
||||
## 完成情况
|
||||
## Completion
|
||||
|
||||
| 项 | 状态 |
|
||||
| Item | Status |
|
||||
|---|---|
|
||||
| W1 CI 地基(skip 普查、零 `MG_Remote` 符号检查、运行期形态证明) | ✅ |
|
||||
| W2–W3 默认翻转与去宏(`MOBILEGL_PIPE_PUSH` / `LEGACY_MEMOS` 删除,位 0–13 固定开) | ✅ |
|
||||
| W4–W5 单进程换到记录臂,记录臂移出 `MG_Remote` | ✅ |
|
||||
| W6 删后端的前端对象臂(Espryt / Magma),`# P13` 债务清零 | ✅ |
|
||||
| W7 七个 OBJECT 模块 + server-only 链接检查 | ✅ |
|
||||
| W8 recorder 金标(首跑抓到并修掉两个单进程推送内容缺陷) | ✅ |
|
||||
| W9 缓存容量重调 | 移到 P13 之后的性能轮(用户 10-07) |
|
||||
| W10 文档与收官 | ✅;FCL 真机两后端冒烟通过 |
|
||||
| W1 CI foundation (skip census, zero `MG_Remote` symbol check, runtime shape proof) | ✅ |
|
||||
| W2–W3 default flip and macro removal (`MOBILEGL_PIPE_PUSH` / `LEGACY_MEMOS` deleted, bits 0–13 fixed on) | ✅ |
|
||||
| W4–W5 the monolith switches to the record arm, the record arm moves out of `MG_Remote` | ✅ |
|
||||
| W6 delete the backend's frontend-object arm (Espryt / Magma), `# P13` debts zeroed | ✅ |
|
||||
| W7 seven OBJECT modules + server-only link check | ✅ |
|
||||
| W8 recorder golden (the first run caught and fixed two monolith push-content defects) | ✅ |
|
||||
| W9 cache capacity re-tuning | moved to the performance round after P13 (user 10-07) |
|
||||
| W10 documentation and close-out | ✅; FCL on-device smoke passes on both backends |
|
||||
|
||||
## 下一步
|
||||
## Next steps
|
||||
|
||||
**P14**:状态归属(同进程多 session / 多 context / share group),设计见 [`design/11-state-ownership.md`](design/11-state-ownership.md)。性能轮(基线 `dev` 构建,定频,FCL MC + anland glmark2,两后端)也排在 P13 之后。
|
||||
**P14**: state ownership (multiple sessions / multiple contexts / share groups in one process), design in [`design/11-state-ownership.md`](design/11-state-ownership.md). The performance round (baseline `dev` build, pinned clocks, FCL MC + anland glmark2, both backends) is also queued after P13.
|
||||
|
||||
**待用户决策**:[`DEBTS.md`](DEBTS.md) 中「需裁定」各项;FCL 无声死亡留给用户(细节见 git 历史)。
|
||||
**Awaiting user decisions**: the "needs a ruling" entries in [`DEBTS.md`](DEBTS.md); FCL's silent death is left to the user (details in git history).
|
||||
|
||||
+46
-46
@@ -1,52 +1,52 @@
|
||||
# 仍开放的债务(跨阶段)
|
||||
# Still-open debts (cross-phase)
|
||||
|
||||
> **2026-10-07(P14 收官)**:W9 关闭(读数,见下);ID-P13-2(Magma monolith)更新为 trace 已基本追平、FCL 仍差的现状;新增三行:Espryt monolith 记录臂的逐项构成与结构原因、driver 项的 cache 压力推断、测过未做的前端变更 epoch 快路径。下一阶段是 draw-packet 缓存重设计。
|
||||
> **2026-10-07 (P14 close)**: W9 closed (readout, see below); ID-P13-2 (Magma monolith) updated to the current state where traces are largely caught up but FCL still trails; three rows added: the per-item composition and structural cause of the Espryt monolith record arm, the cache-pressure inference for the driver item, and the tested-but-not-done frontend change-epoch fast path. The next phase is the draw-packet cache redesign.
|
||||
>
|
||||
> **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` 构建。
|
||||
> **2026-10-07 (P13 close), three rows closed**: ID-P8-13 (9 monolith defects — after the monolith moved to the record arm, the corresponding monolith registrations all pass on the integration lane; the old monolith frontend arm was deleted in W6), ID-P8-14 (the whole-buffer readback in `SyncClientSideVertexArraysForIndirectFetch` and the depth mip view type in Magma monolith both belong to the deleted monolith frontend arm), `MOBILEGL_PIPE_TEXEL_RETAIN_MB` (W3b deleted with the pull build). Two rows added: W9 cache capacity re-tuning and Magma monolith performance, both queued for the performance round after P13, with the `dev` build as baseline.
|
||||
>
|
||||
> 路线图只列"有债务、看这里"。每条写明去向;已关闭的历史债与当时的完整债务表见 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 不再丢设备)。去向「需裁定」= 等用户定;「可进行」= 随时可开的小修,不依赖别的阶段。
|
||||
> The roadmap lists only "there are debts, look here". Each entry states its disposition; closed historical debts and the full debt table of that time are in git history. Open questions are in [`OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md). Updated: 2026-09-30 (P8 second wave; reordered per the debt review. Same day: create-indirect switched to the derived fixture `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024` (not the original capture; the 16-byte-aligned SSBO range bindings moved to 1024 alignment), ID-P8-14 and ID-P8-19 closed — Redmi passes on both backends for monolith / spawn, and Magma spawn no longer loses the device). Disposition "needs a ruling" = waiting for the user to decide; "can proceed" = a small fix that can start anytime and does not depend on another phase.
|
||||
|
||||
| 债务 | 去向 |
|
||||
| Debt | Disposition |
|
||||
|---|---|
|
||||
| CI 覆盖:`test.yml` / `apk.yml` 按分支名(`feat/disaggregated`、`codex/fix-github-ci`)切 `MOBILEGL_CI_DISAGGREGATED` 与车道(本次核对 `test.yml` 14 处、`apk.yml` 4 处);pull 构建只有 G1 与 monolith 对照,没有集成测试门(`TextureRemintPullScenario` 在 pull 上自 P9 红到 P11 M 才修);合入 `dev` 后 inproc 验收与 APK split 两臂随分支名关掉,build-linux-split / integration-split / retrace-split 无条件 | 需裁定:合入前是否加 pull 集成车道;合入 `dev` 后各车道的触发条件(顺带删 `feat/disaggregated` 触发器) |
|
||||
| P9 转出:`TCP_USER_TIMEOUT` = 5000 ms——client 停读控制超过 5 s 且有待发日志字节时内核断开连接;`PublishSessionFault` 的阻塞写对停读的 client 最多等同样 5 s(ID-P9-10)。风险场景:FCL 退后台暂停 + 跨机 TCP | 需裁定:建议调到几十秒、`PublishSessionFault` 改有界写;取值由用户定 |
|
||||
| P11 转出(B0 F1):HyperOS 熄屏冻结 server app 整个 uid。B1 已靠用户设置解决(server app 与 client 所在 app 电池都设「无限制」,ID-P11-16);剩 FCL 同机用法的保活 | 需裁定:只写须知 / app 内检测省电策略并引导 / 不管 |
|
||||
| P11 转出(B2):T0 只在红米一台 Adreno 上实测;Mali 等只有 spike B 的 shell 域证据(server 自测会在不支持的设备上退回 T2)。「respecify 时立即释放导入」的变异在 Espryt 上不红(驱动大概自己持有引用,未核实),带 fence 的释放保留 | 需裁定(受设备规矩所限):开一台 Mali 复测,或长期接受自测退回 |
|
||||
| P11 转出(B1):外部 client 的 apply 线程策略只在红米一台(两大核)上量过;Magma openra 两个数据面都卡在 server(约 285 fps),共享内存无收益 | 同上一行 |
|
||||
| P12 未做:TLS(开放问题 21 当时「令牌够,TLS 留作 P12 开放项」;P12 已收官) | 需裁定:正式放弃,或继续留作开放项 |
|
||||
| `MOBILEGL_IPC_IDLE_EXIT_S` 未解析(client EOF 时 server 已即时退出) | 需裁定:建议废弃;点头后删 `design/09-build-switches-counters.md:67` 与 `Config.h` 注释等处 |
|
||||
| P10 转出:`GL_ARB_gpu_shader_fp64` 按 server 的 `MOBILEGL_ADVERTISE_FP64` 宣告,fp64 收窄却在 client 的编译里;tcp 下 server 环境不同时两者不一致(同类的 `GL_KHR_parallel_shader_compile` 已修,做法见 `CapsMirror.cpp:72-101`,ID-P10-10) | 可进行(小修) |
|
||||
| P11 转出:client 与 server 共用日志前缀时,client 以 `"w"` 打开 `<base>.server.log` 写转发来的 server 日志(`MobileGL/MG_Util/Debug/Log.cpp:89`),截断 server 自己的文件;同机两个进程用同一前缀时同样互相截断 | 可进行(小修) |
|
||||
| P11 转出(B1):会话位被慢启动程序占着时,第二个 client 若 2 s 内发 Hello 得具名 `Refuse{Busy}`,更慢的只见发送失败(那条路径不读待发的拒绝);已写进契约 | 可进行(小修) |
|
||||
| P11 转出(B1):会话启动失败后 client 还多报一个误导性的 `Fatal{CapsBeforeFirstSnapshot}`(`CapsMirror.cpp:247`,早于 B1,helper 用户每次被拒都会看到) | 可进行(小修) |
|
||||
| P11 转出(B2):handle 臂的 CPU 读者读采纳 / T0 映射前先落排队常驻写——split 半已由 P8-C 实现(`SplitHostBytesForCpuRead` 的 T0 臂:落写 + `glFinish`,ID-P8-9),但主机无 T0、未能证红 | 需设备验证(红米 Android 集成测试 + static-peek) |
|
||||
| P11 转出(B2):server 准了 T0 而 client 选了 T2 时,每个存储的 `map_persistent` 等 2 s 再具名拒绝(产品里 grant 与 caps 一致,只在变异下见过) | 可进行(小修:每会话一个「client 不要 T0」提示) |
|
||||
| P9 转出:Espryt 重铸保留路径上 framebuffer clear 后 `imageLoad` 偶读旧值(主机 3/30,加 barrier 后 0/30;`TextureRemintPullScenario` case C 暂钉 barrier),疑 llvmpipe(ID-P9-11);P11 合并 dev 后 push 单进程臂上也见过一次,`ImageBindableRemintScenario` 同样钉 barrier(ID-P11-6) | 可进行(在红米上不加 barrier 归因) |
|
||||
| P8 转出(D):应用省略 `glMemoryBarrier` 时,两臂在计算写与顶点着色器读之间都没有依赖(GL 未定义,但真实应用依赖它);D 的每轮屏障在真机上的开销未测;create-indirect(现为派生的 `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024`,内容相同)验证不了这两件事(ID-P8-14),需要一个「计算写命令、无应用屏障、不重写」的 fixture | 待归因(需新 fixture) |
|
||||
| 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 等待;数字见 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;证据与机理见 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 记录 |
|
||||
| P8 转出(MF):两条 flush 阶梯附近几处注释仍写旧的冻结 / 钉住提交(`5cb826b0`、`3e298c9a`) | 文档小修 |
|
||||
| P8-0 转出:设备上基线就有的 SSIM < 0.99——1.21.11-main-menu(Espryt 0.890、Magma 0.165)、derivative-main(Espryt 0.851);两臂都有 | 待归因 |
|
||||
| P12 未做:D8 窗口种类白名单——X11 / MetalLayer / Win32Hwnd / None 仍被接受(`SurfaceOpCodec.cpp:138-141`),`HandleFromToken` 把 token 强转为指针(`ServerLoop.cpp:1228`,经 `UnpackWindowHandle`;下游是否交给 EGL 未追到底) | 可进行(Ph 类小修) |
|
||||
| P12 未做:修后的横屏 server 窗口未重测 | 可进行(红米只读检查) |
|
||||
| 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(见 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 不会传播;事实上的门脚本见 git 历史 | 需要时入库 |
|
||||
| CI coverage: `test.yml` / `apk.yml` switch `MOBILEGL_CI_DISAGGREGATED` and lanes by branch name (`feat/disaggregated`, `codex/fix-github-ci`); this check covers 14 places in `test.yml` and 4 in `apk.yml`; the pull build has only G1 and the monolith comparison, no integration test gate (`TextureRemintPullScenario` was red on pull from P9 until P11 M before it was fixed); after merging into `dev`, the inproc acceptance and the two APK split arms are turned off by branch name, while build-linux-split / integration-split / retrace-split are unconditional | Needs a ruling: whether to add a pull integration lane before merging; the trigger conditions of each lane after merging into `dev` (and delete the `feat/disaggregated` trigger while at it) |
|
||||
| P9 handoff: `TCP_USER_TIMEOUT` = 5000 ms — the kernel disconnects when the client stops reading control for more than 5 s and there are pending log bytes to send; the blocking write in `PublishSessionFault` waits at most the same 5 s for a client that stopped reading (ID-P9-10). Risk scenario: FCL backgrounded and paused + cross-machine TCP | Needs a ruling: recommend raising it to tens of seconds and making `PublishSessionFault` a bounded write; the user sets the value |
|
||||
| P11 handoff (B0 F1): HyperOS freezes the entire uid of the server app when the screen goes off. B1 is already solved by a user setting (both the server app and the app hosting the client set battery to "unrestricted", ID-P11-16); what remains is keep-alive for the same-machine FCL use case | Needs a ruling: document it only / detect the power-saving policy in the app and guide the user / do nothing |
|
||||
| P11 handoff (B2): T0 was measured only on one Redmi with an Adreno; Mali and others have only shell-domain evidence from spike B (the server self-test falls back to T2 on unsupported devices). The mutation "release the import immediately on respecify" does not turn red on Espryt (the driver probably holds its own reference, unverified); release with a fence is kept | Needs a ruling (limited by device rules): bring up a Mali and retest, or accept the self-test fallback long term |
|
||||
| P11 handoff (B1): the apply-thread policy for external clients was measured only on one Redmi (two big cores); Magma openra stalls at the server on both data planes (~285 fps), and shared memory brings no gain | Same as the row above |
|
||||
| P12 not done: TLS (open question 21 at the time said "the token is enough; TLS is kept as a P12 open item"; P12 has closed) | Needs a ruling: formally drop it, or keep it as an open item |
|
||||
| `MOBILEGL_IPC_IDLE_EXIT_S` unresolved (the server already exits immediately on client EOF) | Needs a ruling: recommend retiring it; after approval delete `design/09-build-switches-counters.md:67` and the `Config.h` comments and so on |
|
||||
| P10 handoff: `GL_ARB_gpu_shader_fp64` is advertised according to the server's `MOBILEGL_ADVERTISE_FP64`, but fp64 narrowing is in the client's compilation; under tcp, when the server's environment differs the two disagree (the similar `GL_KHR_parallel_shader_compile` is already fixed; the approach is in `CapsMirror.cpp:72-101`, ID-P10-10) | Can proceed (small fix) |
|
||||
| P11 handoff: when client and server share a log prefix, the client opens `<base>.server.log` with `"w"` to write forwarded server logs (`MobileGL/MG_Util/Debug/Log.cpp:89`), truncating the server's own file; two processes on the same machine with the same prefix truncate each other the same way | Can proceed (small fix) |
|
||||
| P11 handoff (B1): when the session slot is held by a slow-starting program, a second client that sends Hello within 2 s gets a named `Refuse{Busy}`, and a slower one only sees the send fail (that path does not read the pending refusal); already written into the contract | Can proceed (small fix) |
|
||||
| P11 handoff (B1): after a session start failure the client also reports a misleading extra `Fatal{CapsBeforeFirstSnapshot}` (`CapsMirror.cpp:247`, predates B1; every rejected helper user sees it) | Can proceed (small fix) |
|
||||
| P11 handoff (B2): the handle arm's CPU reader lands queued resident writes before reading the adoption / T0 mapping — the split half is already implemented by P8-C (`SplitHostBytesForCpuRead`'s T0 arm: land the write + `glFinish`, ID-P8-9), but the host has no T0 and could not prove red | Needs device verification (Redmi Android integration test + static-peek) |
|
||||
| P11 handoff (B2): when the server grants T0 but the client chose T2, every storage's `map_persistent` waits 2 s and then is refused by name (in production grant and caps agree; seen only under mutation) | Can proceed (small fix: one "client does not want T0" hint per session) |
|
||||
| P9 handoff: on the Espryt re-mint retain path, `imageLoad` occasionally reads an old value after a framebuffer clear (host 3/30, 0/30 after adding a barrier; `TextureRemintPullScenario` case C pins the barrier for now), suspected llvmpipe (ID-P9-11); seen once on the push monolith arm after P11 merged dev, and `ImageBindableRemintScenario` also pins the barrier (ID-P11-6) | Can proceed (attribute on Redmi without the barrier) |
|
||||
| P8 handoff (D): when the application omits `glMemoryBarrier`, neither arm has a dependency between compute writes and vertex shader reads (undefined by GL, but real applications rely on it); the per-round barrier cost of D on device is unmeasured; create-indirect (now the derived `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024`, same content) cannot verify these two things (ID-P8-14); a fixture is needed for "a compute write command, no application barrier, no rewrite" | Pending attribution (needs a new fixture) |
|
||||
| P8 handoff (E): the driver-written mark is applied to the whole texture (`GenerateMipmap` also marks it); when the driver refuses reads, even the base level is refused by name, which is conservative; cross-format copy stores are not followed; a refusal is still a session Fatal (the reply state would have to change to answer `GL_INVALID_OPERATION`) | Small fix (mark per level); the rest as needed |
|
||||
| P8 handoff (B): `PipeCalls.def:260` `GenerateMipmap` is still `kWaitApplied`; the reason for ruling 14 (CPU filtering touches the client shadow) no longer holds, so it can become `kWaitNone` | Performance work (protocol behavior change, needs a separate ruling) |
|
||||
| P8 handoff (C): server SSBO marking is over-approximate (every SSBO bound in the window is marked); a buffer that stays bound to an SSBO and is also used as a restart EBO is read back on every draw | Performance work |
|
||||
| P13 handoff (ID-P13-2, P14 update): Magma monolith record arm vs dev (pull) — traces are now largely caught up: rd12 83.8 vs 87.0 fps (−4%), openra 747 vs 293 (dev waits on the GPU there); P14's largest item is `553eb86d` (texture-layer uploads no longer wait on the CPU for the GPU to drain, rd12 70→83). But **still behind on FCL MC**: monolith 171 vs dev 232 (−26%), inproc 195; the presentation thread's CPU is only about 2.8 / 5.8 ms of the frame, the rest is waiting — another wait different from the traces | First item of the next phase: off-CPU profile of FCL Magma monolith (`--trace-offcpu`, the same method that found the upload wait; neither capture at P14 close caught a game frame), find that 3 ms wait; numbers in git history |
|
||||
| ~~P13 handoff (W9): cache capacity re-tuning~~ **P14 closed (readout)**: `MOBILEGL_PIPE_STATS` on rd12 / openra shows render-state CSO steady state mints 0 new per frame, Magma pipeline memo hit rate 99.0% / 92.7%, no cache thrash, P8 capacity kept | Closed (W9 section, see git history) |
|
||||
| P14 handoff: Espryt monolith record arm vs dev (pull) rd12 91.1 vs 115.2 fps (−21%); FCL MC 198 vs 229 (−13%). Render thread CPU per frame 11.56 vs 9.07 ms: client half +1.39 (validate 0.92: EmitGlobalConstants 0.17, EmitVertexElements 0.13, Tracker::Update 0.12, CopyField 0.10, validate itself 0.10, EmitVertexBuffers 0.07; verb port about 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. Each item is below 2% of frame time; together they are the record arm's structural cost: pull's backend reads frontend objects by pointer at draw time, while push must detect change (tracker), serialize (emitter, uniform span), apply (applier), and then the backend resolves again by handle through the slot table, and because the backend cannot see frontend dirty bits it does a clean check for every bound object — tens of ns per step per draw, each a separate cold cache line, multiplied by 1471 draws/frame | Next phase: per-(VAO, program) draw-packet cache (design draft (see git history), estimated to recover 1.0–1.6 ms/frame for rd12 Espryt monolith, about 91→100–106 fps); first prototype and measure only the client half |
|
||||
| P14 handoff: Espryt monolith's driver item +0.33 ms/frame (2.9%), all inside `glDrawElements`, with the GL call combination the same as dev. `simpleperf stat` (whole run, render thread): feat executes 16.6% more instructions than dev, L1D refill 56% more, L2D 23% more (IPC 3.73 vs 3.86). Inference: the record arm's larger per-draw working set (record, applier state, slot table, twin) pushes the driver's state out of cache — this is a counter inference, not measured inside the driver | Verify together with the draw-packet cache: measure L1D refill and driver ms/frame before and after landing; if the driver item does not drop, the inference does not hold, and investigate separately |
|
||||
| P14 handoff (tested, not done): frontend change-epoch fast path for Tracker / residual — bump a context epoch at every `DECLARE_GL_FUNCTION_END` entry (except draw, `glBindVertexArray`, floating-point `glUniform*`), as well as handwritten entries and EGL entries; when the epoch has not moved, validate only recomputes the VAO / program-derived shutter (cap about 0.07 ms), then, after auditing which residual fields depend on VAO / program binding, skips the residual copy (0.15–0.25 ms total, 1.5–2%) | Each is below the 2% threshold, and missing the bump at one entry renders stale state; merge into the draw-packet design (its §3 context epoch is exactly this), do not do it separately |
|
||||
| P13 handoff (FCL soak, user ruling: FCL is not changed for now, and MobileGL does not work around it): 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). Soak: P13 library 30 runs (Magma 22 / Espryt 8) died 18 times, P12 library 10 runs died 2; a P12 / P13 alternating control run by run (same scenario, same network period) P12 4/10, P13 2/10, the timeline (`Renderer:` line to MobileGL's first line about 6.2 s, the late death fixed about 9.4 s after it) identical for both libraries, with no evidence that MobileGL raises the probability; all status=34, no tombstone, no kgsl; evidence and mechanism in git history | fix belongs in FCL (save/restore libcore's SIGRTMIN+2 sigaction around JVM creation); left for the user |
|
||||
| P8 handoff (MD): dev's `RingAllocateSlow` drains without limit on a context in the error state (hangs in `glFinish` on device) | Small fix (dev) |
|
||||
| P8 handoff: `LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime` times out under load (twice), always passes when run alone | Small fix (relax the deadline or isolate) |
|
||||
| P8 handoff (SV): `gen_pipe_dirty_surface.py` scans integration test sources; a test member with the same name as a GLContext member (e.g. `m_parameters`) silently breaks its self-check; `.Mip3DValidation.` is a failure rather than a skip on lanes without the validation layer | Small fix / CI record |
|
||||
| P8 handoff (MF): near the two flush ladders, a few comments still name the old freeze / pin commits (`5cb826b0`, `3e298c9a`) | Documentation small fix |
|
||||
| P8-0 handoff: SSIM < 0.99 already present in the device baseline — 1.21.11-main-menu (Espryt 0.890, Magma 0.165), derivative-main (Espryt 0.851); present on both arms | Pending attribution |
|
||||
| P12 not done: D8 window-kind whitelist — X11 / MetalLayer / Win32Hwnd / None are still accepted (`SurfaceOpCodec.cpp:138-141`), `HandleFromToken` casts the token to a pointer (`ServerLoop.cpp:1228`, via `UnpackWindowHandle`; whether downstream hands it to EGL was not traced to the end) | Can proceed (Ph-class small fix) |
|
||||
| P12 not done: the fixed landscape server window was not retested | Can proceed (Redmi read-only check) |
|
||||
| P11 handoff (M2, to dev): dev's `DirectVulkan.SplitRecording.` lane has no `RESOURCE_LOCK`, and its cases read a literal log path — under `-j` they may truncate each other; the copy on the P11 line is already fixed (the duplicate fix for Magma's empty uniform was synthesized into one entry in ID-P8-17) | Bring it along on the next back-merge to dev |
|
||||
| P11 handoff (M2): large layers beyond the stage chunk budget are sent as shards, and shards not covered by a write box still carry the whole box (the server needs it to assemble the layer image), which paints the shadow over places the GPU wrote; the fix needs a "locate-only" region form that both servers skip, control protocol revision +1 (split-only, predates P11) | Small package or P8 |
|
||||
| P11 handoff (M2): uploads still have two lossy shapes, each logged once — when a layer's precise write boxes exceed 4096, it is sent covered by the storage's rectangle table; when an applier entry accumulates more than 4096 regions, they are merged into a union box. When the texture has GPU writes, both paint the shadow into the gaps | Small fix, no trigger reported |
|
||||
| P10 handoff (performance): on device, of the 19.4 ms per frame of tcp loopback rd12, ~11.2 ms is the producer parking in the stream transport (send + ring / stage / ack waits), credit waiting only ~0.2 ms (see git history §2, ID-P10-13) | Performance work after the roadmap completes |
|
||||
| P11 handoff (performance): on device with MC 26.3, split T2 relative to monolith adoption p99 Espryt +37–42%, Magma about 2.5×; part of that is thread placement — the server apply thread pinned to cores 6–7, the client GL thread mostly lands on slow cores, about 2.3× CPU per frame (see git history). Half of RSS is already delivered by B2 (T0): peak memory Espryt −41–42%, Magma −18–26% (ID-P11-17) | Performance work after the roadmap completes (p99 and thread placement) |
|
||||
| P11 handoff (B2, performance): adoption of `glBufferStorage(NULL)` first sends the entire zeroed storage once as `resource_subdata` (both T2 and T0; MC 26.3 8 storages, 441.5 MiB) | Performance work after the roadmap completes |
|
||||
| P12 not done: DirectGLES `g_Display` / `g_Surface` / `g_Context` are still globals (not needed when there is one session per process); full handling of the cached-app freezer (the docs never defined it); multiple contexts; the server window does not forward input | Wait for the need for a second session / second context |
|
||||
| P9 handoff: Magma sends a whole-buffer `OnGpuWritten` for writable SSBOs / texel buffers / XFB targets on every draw (7,093 on create-indirect's first frame); the client already marks the same set itself, redundant and harmless; `OnMipLevelsGenerated` and `OnCapsInvalidated` still have no producer. `OnTextureWriteback` also has no producer, but do not delete it: the CPU three-channel mip fallback needs it (`CONTRACT-P5B.md:255`) | Future narrowing / cleanup candidates (ID-P9-7) |
|
||||
| Out-of-tree scripts: the census runner, `wsl_p5_gate.sh`, and so on are in `~/w7/notes/`; a git merge does not propagate them; the de facto gate scripts are in git history | Put them in the repository when needed |
|
||||
|
||||
已关闭的历史债(P5 的 27 个 wrong-answer、`rsp` 残余读、P5b 的 inproc 依赖、ABI 指纹、默认 staging 装不下 128 MiB 上传、184 符号棘轮 CI 门)及当时的完整债务表见 git 历史。
|
||||
Closed historical debts (P5's 27 wrong answers, `rsp` residual reads, P5b's inproc dependency, the ABI fingerprint, the default staging being unable to hold a 128 MiB upload, the 184-symbol ratchet CI gate) and the full debt table of that time are in git history.
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
# 实测(索引)
|
||||
# Measured results (index)
|
||||
|
||||
> 这一页只有结论和去向;完整的表、命令、协议与读法在各阶段的"实测"节(原文见 git 历史)。代码和旧笔记里的 `MEASUREMENTS.md §N` 按下表"§"列找到对应阶段(小节编号在那里原样保留)。
|
||||
> This page holds only conclusions and pointers; the full tables, commands, protocols, and how to read them are in each phase's "measured results" section (originals in git history). `MEASUREMENTS.md §N` in code and old notes maps to the phase in the "§" column below (subsection numbering is preserved there as-is).
|
||||
|
||||
**口径**:性能只记录、不设门;看**逐线程 CPU 时间**,不看整机帧率。性能对比只用一台手机(Redmi `2f7cbe2e`,Adreno 830),定频、同热窗口、两臂交替跑([`guide/pin-verification-2026-09-07.md`](guide/pin-verification-2026-09-07.md))。
|
||||
**Basis**: performance is recorded only, never gated; look at **per-thread CPU time**, not whole-machine frame rate. Performance comparisons use only one phone (Redmi `2f7cbe2e`, Adreno 830), with pinned clocks, the same thermal window, and the two arms run alternately ([`guide/pin-verification-2026-09-07.md`](guide/pin-verification-2026-09-07.md)).
|
||||
|
||||
## 关键结论
|
||||
## Key conclusions
|
||||
|
||||
- **接口本身的代价**:单进程里改成"前端推、后端收",CPU 多约 6–12%(P2),已接受;顶点数组切换密集的场景更高(P3a,+27–30%)。
|
||||
- **拆成两线程**:起初游戏里只有 7–13 fps;优化后 103–106 fps(P5d);client 发完就走以后,重负载(渲染距离 32)下与单线程持平(P5e)。
|
||||
- **拆成两进程**:真机上与两线程成本持平;两者的 GL 线程 CPU 都比单线程少约 30%(后端工作挪到了另一线程 / 进程)(P6)。
|
||||
- **跨机**:电脑经 TCP 连手机,102 个集成用例全过;server 被杀后 133 ms、Wi-Fi 断开后约 5 s 干净报"设备丢失"(P6.5)。
|
||||
- **纹理上传的等待粒度(P12,handoff §4)**:wire 臂上纹理那半 `resource_subdata` 不再逐条买回包,等待点从「每条记录」落到「SEG_STAGE 窗口用尽」。
|
||||
单条纯上传占用的回包等待 **1 → 0**(`wait_replies`,正是 §3.2 里"一帧 55,428"的那个计数器)。
|
||||
按形状实测(`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`,主机 inproc,按 op 计数):**图集拼接**(一张 level 上 64 次 `glTexSubImage2D`)**64 → 0** 次回包等待;
|
||||
**每 sprite 一张纹理**(64 张纹理 + 256 次 `glTexParameteri`)**384 → 320**,剩下的全是 per-resource / per-call 的三条 row(create 76 / respecify 130 / params 130)——
|
||||
**这两条后来被"首次使用确认"取代了**(见下面那条):对象的存在性由它自己的 create 确认为事实之后,
|
||||
params 与 texture respecify 都不再逐条买答案,而是只在**首次使用**时付一次阻塞的 create。
|
||||
同一帧的累计实测:**43,232 waits / 221.5 s → 4,543 waits / 31.3 s(7.1×)**,staged 字节与发出的记录数(103,094)都不变。
|
||||
真机(TrebleDroid GSI,MC 26.2 经 TCP 入世界)那一帧的构成为 `ResourceRespecify` 20,633 + `SetTextureParams` 18,056 + `ResourceCreate` 4,536 条**各等一次**,
|
||||
而 `ResourceSubData` 12,671 条**零等待**(B 的实机证据);这些记录都是真实状态变化,客户端没有可去重的(同一值的 `glTexParameteri` 本来就不产生记录)。
|
||||
- **单次往返那 ~5 ms 是设备侧的深度空闲退出,不是链路也不是客户端**(P12,本轮实测):同一帧、同一批记录,
|
||||
客户端自旋 40× 只快 ~10%;换 `adb forward`(USB)快 ~25–30%;而把设备 8 核钉 `performance` 并关掉 `cpuoff_l`/`clusteroff_l`/`mcusysoff`/`s2idle` 后,
|
||||
单次等待 **5,040 → 3,715 µs**、那一帧墙钟 **221.5 s → 166.5 s(−25%)**——记录数与字节数都不变。
|
||||
这解释了 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` 自证。
|
||||
见 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)。全文见 git 历史。
|
||||
- **Cost of the interface itself**: in the monolith, changing to "frontend pushes, backend receives" costs about 6–12% more CPU (P2), which was accepted; scenes with dense vertex-array switching are higher (P3a, +27–30%).
|
||||
- **Split into two threads**: at first only 7–13 fps in the game; after optimization 103–106 fps (P5d); after the client runs ahead, under heavy load (render distance 32) it matches single-threaded (P5e).
|
||||
- **Split into two processes**: on device the cost matches two-thread; the GL thread CPU of both is about 30% lower than single-threaded (backend work moved to the other thread / process) (P6).
|
||||
- **Cross-machine**: a PC connects to the phone over TCP, all 102 integration cases pass; after the server is killed, 133 ms; after Wi-Fi disconnects, about 5 s to cleanly report "device lost" (P6.5).
|
||||
- **Texture upload wait granularity (P12, handoff §4)**: on the wire arm, the texture half of `resource_subdata` no longer buys a reply per record; the wait point moves from "every record" to "SEG_STAGE window exhausted".
|
||||
A single pure upload occupies a reply wait of **1 → 0** (`wait_replies`, exactly the counter that was "55,428 per frame" in §3.2).
|
||||
Measured by shape (`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`, host inproc, counted per op): **atlas assembly** (64 `glTexSubImage2D` calls on one level) **64 → 0** reply waits;
|
||||
**one texture per sprite** (64 textures + 256 `glTexParameteri` calls) **384 → 320**, and the rest is entirely the three per-resource / per-call rows (create 76 / respecify 130 / params 130) —
|
||||
**these two were later replaced by "first-use confirmation"** (see the item below): once an object's existence is confirmed as fact by its own create,
|
||||
params and texture respecify no longer buy an answer per row; instead a blocking create is paid once at **first use**.
|
||||
Cumulative measured result for the same frame: **43,232 waits / 221.5 s → 4,543 waits / 31.3 s (7.1×)**, with staged bytes and the number of records sent (103,094) both unchanged.
|
||||
On device (TrebleDroid GSI, MC 26.2 entering the world over TCP), that frame consisted of `ResourceRespecify` 20,633 + `SetTextureParams` 18,056 + `ResourceCreate` 4,536 records **each waiting once**,
|
||||
while `ResourceSubData` 12,671 records **waited zero times** (B's on-device evidence); these records are all real state changes, and the client had nothing to deduplicate (a `glTexParameteri` with the same value produces no record in the first place).
|
||||
- **The ~5 ms per round trip is the device-side deep idle exit, not the link and not the client** (P12, this round's measured result): same frame, same set of records,
|
||||
the client spinning 40× was only ~10% faster; switching to `adb forward` (USB) was ~25–30% faster; and after pinning the device's 8 cores to `performance` and turning off `cpuoff_l`/`clusteroff_l`/`mcusysoff`/`s2idle`,
|
||||
a single wait went **5,040 → 3,715 µs** and that frame's wall time **221.5 s → 166.5 s (−25%)** — with record count and byte count unchanged.
|
||||
This explains handoff §3.6 "switching to a link 3.4× faster leaves wall time unchanged": the money is spent on each wake-up of the device; the 249 s in handoff §3.2 was taken on a machine **without pinned clocks** and includes this idle tax.
|
||||
**The same diagnosis yields a portable fix**: raise the server's spin budget (the Activity's `env` extra → `MOBILEGL_IPC_SPIN_US=2000`, no root and no code change needed),
|
||||
and that frame goes **221.5 s → 182.8 s (−17%)**; pinning device clocks + turning off deep idle gives **166.5 s (−25%)**. The server log prints `spin 2000 us` as proof.
|
||||
See git history §2.6–2.10.
|
||||
- **Frames are correct**: the Vulkan backend's on-device frame check passes 36/36; the five CTS blocks show no regression beyond 0.5 percentage points against baseline and no new crashes (P7); on-screen presentation in the server's own window gives SSIM 1.0 on both backends (P12).
|
||||
- **Five-arm transport comparison (2026-09-28, unpinned-clock interleaved A/B)**: under heavy load (rd12) inproc ≈ spawn+shm is **about 1/3 faster** than monolith (run-ahead pipeline parallelism + fewer applier-path probes); under light load (openra) split pays only fixed overhead; **tcp localhost is ~8× slower, the bottleneck being present credit=1 serial waiting (~74%) + futex wake-up tax**, `MOBILEGL_IPC_SPIN_US=2000` alone measured 21.7→63.6 fps; the cross-host TCP "`eglMakeCurrent` hang" **was caused by the test environment** (the host's v2rayN `xray_tun` proxy terminated TCP and the relay hung, not a source bug); after bypassing it P12 exit gate (b) passes: the WSL client replays an in-world rd12 trace over Wi-Fi, ssim 0.999883, steady state 17–22 fps (`PRESENT_CREDIT` 1/2/3 = 16.9 / 21.3 / 22.1), openra steady state 105–133 fps; the 2.7 GB loading frame is bandwidth-bound (server measured 52 MB/s), steady state is round-trip-latency-bound (20–24 MB/s). Full text in git history.
|
||||
|
||||
## 按阶段
|
||||
## By phase
|
||||
|
||||
| § | 阶段 | 一句结论 | 全文 |
|
||||
| § | Phase | One-line conclusion | Full text |
|
||||
|---|---|---|---|
|
||||
| 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 | P0 | A second process can be launched from the application process; cross-process GPU memory sharing has only one path available on all devices | git history |
|
||||
| 2 | P1 | Verify pushed state per draw; 79 traces with zero divergence | git history |
|
||||
| 3 | P2 | Desktop benchmark: the new tracker is cheaper than the old fill it replaced | git history |
|
||||
| 4 | P3a | Two devices, on-device Release: interface cost +6–12%, scenes with dense VAO switching +27–30% | git history |
|
||||
| 5 | P4a | Switched to Redmi; P4a itself adds only 3–6 percentage points | git history |
|
||||
| 6 | P5 | The first frame draws correctly in two threads; the on-device two-thread arm still cannot run to completion | git history |
|
||||
| 7 | P5b | Four game traces run to completion in two threads on device; two-thread extra cost +6–18% | git history |
|
||||
| 8 | P5c | With zero shared memory between the two threads, all tests still green | git history |
|
||||
| 9 | P5d | In-game 7–13 → 103–106 fps, client CPU 15.0 → 9.2 ms/frame | git history |
|
||||
| 10–11 | P5e | Running ahead is 69% faster than waiting on every command; matches single-threaded under heavy load | git history |
|
||||
| 12 | P6 | Two-process matches two-thread; first measured data volume and record count per frame | git history |
|
||||
| 13 | P3b/P4b | Texture upload shapes are identical under all four topologies | git history |
|
||||
| 14 | P6.5 | Cross-machine TCP 102/102; disconnect detected in about 5 s | git history |
|
||||
| 15 | P7 | On-device frames 36/36; unbounded server memory growth reined in (host 825 → 546 MiB) | git history |
|
||||
| 16 | P12 | On-device retest after review: Espryt SSIM 1.0; Magma 1.0 / 0.999999511; P12 targeted CTest 46/46; texture upload reply waits 1→0 (host mechanism gate, on-device wall time pending); **P12 closed (09-29)**: cross-machine TCP gate (b) passes (rd12 ssim 0.999883, steady state 17–22 fps); FCL gate (a) passes (Minecraft presented on screen in the render server window; after killing the server, device-lost latches in 33–54 ms) | git history |
|
||||
| 17 | Five-arm transport measurement (2026-09-28, not a phase) | inproc/shm under heavy load is ~1/3 faster than monolith; tcp localhost bottleneck is credit=1 + wake-up tax (SPIN_US=2000 → 2.9×); the cross-host hang was actually caused by the host's xray TUN proxy; direct Wi-Fi steady state rd12 17–22 fps, openra 105–133 fps | git history |
|
||||
| 18 | P11 | Same-machine zero-copy (T0) MC 26.3 peak memory Espryt −41–42%, Magma −18–26%, ssim / frame time / CPU unchanged; external client over shared memory rd12 Espryt about 119 fps (TCP 40–53); split T2 relative to monolith adoption p99 Espryt +37–42%, Magma about 2.5×. Not measured: after M2, on the split server, when a texture has no GPU writes, Espryt chooses the precise write box by its own storage threshold; the non-ring path may send a few extra small boxes | git history |
|
||||
| 19 | P8 | On real content only Magma wire indirect hit (MC 26.3 × Magma inproc 13 million draws expanded on CPU) → after going native, the per-create-indirect full-GPU wait 321 → 0; Redmi create-instancing × Espryt × monolith 0.870 → 0.99998 (Adreno drops queued ring copies, the dev + feat two ladders); largest program archive 355 KB = 4.2% of arena/4 (no chunking); exit device matrix 77 cells / arms, none worse | git history |
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
# 开放问题全文(原 `ROADMAP.md`「开放问题」,2026-09-24 移入)
|
||||
# Open questions, full text (formerly the "Open questions" section of `ROADMAP.md`, moved here 2026-09-24)
|
||||
|
||||
> [`ROADMAP.md`](ROADMAP.md) 只留每题一行的状态;这里是每一题的完整陈述、已答部分的出处与未答部分的判据。编号稳定,引用写 "开放问题 N"。
|
||||
> [`ROADMAP.md`](ROADMAP.md) keeps only a one-line status per question; here is the full statement of each question, the source for the answered parts, and the criteria for the unanswered parts. Numbering is stable; cite as "open question N".
|
||||
|
||||
P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。
|
||||
Questions answered in P0 are no longer listed (spike A's domain, spike B's tiers, `posix_spawn` unavailable, the OOM probing idiom, retirement of `GetInteger64i_v`/`GetProgramiv`, D21 and the `RenderbufferObject` lifetime id, the dynamic accessor baseline).
|
||||
|
||||
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` 已删(见 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。
|
||||
6. **具名 UBO host payload 的形状(D-B8)。** Magma 在 26.3 世界每帧重打包 331 KB 具名 UBO 字节,Espryt 为 0。要么冻结第二变长尾形状,要么走备选(Magma 直接描述符绑定常驻 `VkBuffer` range,独立 `dev` PR)。P7。
|
||||
7. **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross / ESSL 转译缓存 / 格式处理器 / POST 探针,client 需要 glslang 与反射层;`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。P7。
|
||||
8. **一份反射归档能否服务三个消费者。** Espryt 那一半已答(P4a):能且不需要复制。Magma 的第一个消费者已由 P5f fm 答:`MagmaProgramSource` 全部从 wire `ProgramArchive` 取,无前端对象、无复制。第二个(storage block 表)今天是**否**且变差了:split 下 `MagmaProgramSource::EnsureStorageBlocks` **每 draw** 重跑 SPIRV-Reflect(monolith 臂 `DirectVulkan.cpp:177-200` 至少有持久缓存);答案是往 `LinkArtifacts` 加有序 `storageBlocks`(数据已在 `blockReflection` 里),两个消费者都改读它。**已答(P7 wave 2-C,`dce9953d..cf7ca59f`)**:`storageBlocks` 入归档、两个消费者改读、每 draw SPIRV-Reflect 4→0;`g_programResourceCaches` 的 monolith 半边归 P13(G1)。
|
||||
9. **viewport-array 回放能否塞进一次 `draw_vbo`**:**已答(2026-09-30,ID-P8-1)**:能——server 在一次 draw 内按状态分组逐遍回放(`DirectGLES.cpp:8421-8465`),`ViewportArrayScenario` 在 split 各臂绿。
|
||||
10. **`ResidentSubData` 的不对称。** 已答一半(2026-09-22 核):**Magma 的实现已补**(`VkBufferManager.cpp:123` monolith 表、`:148` wire 表,`fff9d639f`)。**仍未答:`kCapResidentSubData` 从未被 server 发布**——`MG_Backend/Init.cpp:192-219` 是全树唯一的 `SetCapabilityBits` 生产调用点,只 OR 了 XFB、三个 query 位与 run-ahead 位;于是 `PipeFill.cpp:848` 在 split 下恒 false,**两个后端在拆分模式下都退回 `BufferObject.cpp:711-716` 的就地 memcpy**,Magma 的实现在 split 里是死代码。**已答(P7 wave 2-C,ID-P7-20)**:`InitServerRoleCommon` 按 server 自己的 wire 资源表发布 `kCapResidentSubData`,镜像收到即验收;白盒「发出的是 opcode 49」在 P11 采纳档落地前不可达(§5.4 债)。
|
||||
11. **`SEG_STAGE` 的上限。** 内容侧已答(2026-09-20):**分块已实现**——buffer 范围走查(`1e7c372e`)与纹理整宽 slab(`9469d48e`)按 `MGPipeStageChunkBytes()`(默认 32 MiB 的 1/4 = 8 MiB)切成多条 `resource_subdata`,所以单次 128 MiB 上传不再需要显式 `MOBILEGL_IPC_STAGE_MB=256`;单片仍超 arena 或该 record 类型未接入分片时仍是 `Fatal{RingOverrun, "SEG_STAGE"}` 兜底。**仍未答(2026-09-22 收窄)**:a6 §6 实测没走查的只剩 **program archive**(唯一无界 blob)与 **`draw_vbo` 的 `NumDraws*12` range 尾**;宿主索引 span 无 producer(划掉),`set_global_constants` 有走查。二者的分片是 P6.5 sl 与 P8 共用的同一件事,stream 发送窗口对着它们定尺。MC in-world / Create 的占用分布仍未量(门 8 已给出两条负载的字节/帧与记录/帧,逐 blob 分布只在 integration 车道取到)。 **P8-0 实测(2026-09-30,ID-P8-5)**:最大 program archive 354,846 B、最大 `draw_vbo` 记录 1,936 B,都远低于 arena/4;**不做分片,关闭**。
|
||||
12. **P13 之后 split-only 渲染 bug 的 server 侧第二意见。** verify 构建 + recorder 只覆盖推送内容,不覆盖后端对它的解释。
|
||||
13. **烘焙后的内部 shader 能否在没有活 `ProgramObject` 的情况下表达 uniform location 与 UBO 布局。** 颜色 blit 已答(P5f fv:`WireColorBlit.{vert,frag}` 用 push constant,`WireColorBlitSpirv.h` 签进树);窄化到深度 mip 程序(今天在 split 下被编译掉、其 split 路径是具名 Fatal)与直通 TCS(`ProgramFactory.cpp:4060-4262`,第三个未盘点的内部 shader 生成器)。**已答(P7 wave 2-B / B2)**:深度 mip 烘焙 (A)(D)(`WireDepthMipmap.*`,push constant,`MOBILEGL_BAKED_INTERNAL_SHADERS` 新鲜度门表驱动)与 (B′)(disaggregated 构建的 monolith 臂改用烘焙模块)落地;multisample resolve 第三套烘焙 shader 随 B2;直通 TCS 仍未盘点。
|
||||
14. **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。一条线索:设备上 26.3 Espryt 的 `sve` ≈ draw 数(每 draw 重发一次 sampler-view 集合),桌面只有 ~0.07/draw;列入 P3b/P4b 优化清单。
|
||||
15. **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口。** 独立 `dev` 问题,拆分不得借机顺手修。**已答(2026-09-30):是**——P8-C / P8-D 主机 red-once 证实(Espryt `*IndirectCount`、Magma `glMultiDrawArraysIndirectCount` 读到 0);split 半已修(ID-P8-9),monolith 半在 P8 第二波的 dev 包里修(ID-P8-3)。
|
||||
16. **索引宿主镜像的实际内存占用。** MC / Sodium / Iris 语料里 element-array buffer 总量未测;若显著超 64 MiB,退化路径的频率与代价必须实测。P8。
|
||||
17. **`create-indirect` fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;`rd12` + Magma 在两台 Adreno 830 上的 `scudo::reportMapError` 崩溃同样是 `dev` 侧。两者排除在设备 A/B 之外、留在桌面语料里;P8 要在 `create-indirect` 上断言 `roundtrips-per-frame == 0`,所以 `dev` 的修复在别人的关键路径上。**create-indirect 部分已答(2026-09-30)**:原 fixture 在 `GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT` = 16 的桌面驱动上抓,Flywheel 按 16 摆 scatter 列表;Adreno 830 两后端与 Mali 的 Vulkan 要 64,1,270 次范围绑定全被 `GL_INVALID_VALUE` 拒掉。原 fixture 退役,换成派生的 `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024`(**不是同一个 trace**:`tools/trace_replay/fixture_tools/realign_buffer_ranges.cpp --align 1024` 把这些绑定挪到 1024 对齐的槽,插入 3,812 个调用,目标调用 504631 → 508443;主机 SSIM 与原 fixture 9 位一致)。红米两后端 monolith / spawn 全过;Mali-G1-Ultra 上 Magma 全过(split 臂另修了 T0 POST,见 `CONTRACT-P11.md` POST 节),Espryt 仍 0.848:该机 GLES 驱动顶点阶段 SSBO 上限为 0,Flywheel indirect 的顶点程序链接不了(能力缺失,不是分离缺陷)。
|
||||
18. **split 两侧的负载平衡。** P5e 解锁出来的新问题:VD32(~3550 draws/帧)下 apply 线程 **11-12 ms**、client **15.7 ms**,**client 是瓶颈且打满 ~94%**。把工作从 client 挪到 apply 会直接降瓶颈——这在 lockstep 下毫无意义(两侧串行相加),**run-ahead 才使它成为一个杠杆**。供选:编码侧的 emitter、tracker 走查、持久映射的脏页追踪(`change_protection` 在侧写里占 1.3%)。需先重新侧写:等待消失后尾部各项的分母都变了。排在 P5e 之后;无主,2026-09-22 建议挂在路线图推完后的优化阶段,P6.5 的 2×2 矩阵车道顺带重新侧写
|
||||
19. **链路带宽预算与压缩。** TCP 终局下 `SEG_STAGE` 字节/帧对的是 Wi-Fi / USB(量级 30–100 MB/s),不是内存;`ARCHITECTURE.md` §14 的 ~1 MB/帧预算在 60 fps 是 60 MB/s,已贴着 Wi-Fi 上限。是否对 stage 载荷做 LZ4 类压缩、纹理上传是否按内容哈希在 server 侧去重,要等门 8 的分布出来再定。P6.5 契约写下第一行之前必答。
|
||||
20. **大单次上传的停顿。** 128 MiB 一次上传在 50 MB/s 上是 2.5 s;内容侧分块解决的是"装不装得下",不是"要等多久"。跨机臂上要么接受并记录,要么给上传做异步化(与 P9 的语义一起想)。不假装解决。
|
||||
21. **配对 / 认证的形式。** **已裁定(ID-P7-3,2026-09-22):令牌足够,不做 TLS。** 威胁模型是「任何人都能连的远程代码路径」而不是窃听:令牌常量时间比较、≥16 字节、无令牌只 loopback、数据面以 `Welcome.dataNonce` 绑定到已认证的控制连接、fork 前认证。TLS 作为 P12 的开放项保留。
|
||||
22. **client 平台范围。** 默认先 Linux + Android(现有 CI 能验),Windows client 第二批(`ShmSegmentWin32` 在;`pipe:` 具名拒绝作废、TCP 替代),macOS 仍不拆分。这决定 `ARCHITECTURE.md` §15.3 "Windows 机器不是正确性门"要不要改。
|
||||
23. **半开连接的检测。** Wi-Fi 掉线不产生挂断事件;dl 的闩取自描述符挂断,TCP 上需要 keepalive / 心跳超时并**由传输层报告为挂断**——这与"绝不取自 apply 超时"不矛盾,写清楚免得重吵。P6.5 ct。
|
||||
24. **E1 对照的重定义(ID-122)。** **已关闭(2026-09-29 债务审查)**:`10bfd29f` 把 E1 的像素竞速换成观测协议等待的负控 `scripts/ci/wait_boundary_negative_control.py`。2026-10-18 该负控退役:探针看得见缺失的等待,改由常驻单元用例 `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` 证明(同一个 `ApplierReset`,发布后不等待,必须读到 parked=0)。
|
||||
1. **The real per-draw CPU cost of the client-side dirty walk.** Answered (P2–P4a): pushing did not net a reduction below the pull baseline; under Release the P2 boundary is +6–12%, P3a adds another +17–19 pt on rd12 where VAO switching is dense, P4a +3.4–5.7 pt on Espryt (`MEASUREMENTS.md` §3–§5). The user accepted on 2026-09-08, and performance has been recorded only since then.
|
||||
2. **The incidence of texture re-mint pulls on real corpora.** Answered (P4a): negligible; the LRU is kept and the default stays 0 — 2 times in 780 statistics windows over 79 cases × two backends (`iris-photon` and `iris-derivative` one each, only in DirectGLES). But under P5b this path was a named Fatal, exactly the first block of these two traces plus `create-indirect` GLES (P9). **P9 W2 closed**: re-mint needs no pull — the server reads the GPU level, the store fills the pending box (Espryt), GPU→GPU copy (Magma); the census over 77 rows × {monolith, inproc} is still the same two times (both are render targets whose store has no bytes), and `OnTexturePullRequest` is deleted (see git history).
|
||||
3. **Re-check of spike B's `untrusted_app` domain.** The tiers for both devices were measured in the `shell` domain; run `extmem_probe` once more from the application process (spike A's exec hook is already available). Do before P11. **2026-09-22 downgraded**: only meaningful for the same-machine arm's T0 / T1; the stream data plane does not need it.
|
||||
4. **Wire granularity of render state.** Answered (P2): 16 boundaries / 15 chunks, 7 pipeline 396 B + 8 dynamic 772 B; CSO LRU 64 provisional, retuned in P13.
|
||||
5. **Capabilities without storage.** Answered (P2): `FramebufferSrgb`, `DepthClamp`, `TextureCubeMapSeamless` all gained real storage, and `sizeof(RenderStateParameters)` is still 1168.
|
||||
6. **The shape of the named UBO host payload (D-B8).** Magma repacks 331 KB of named UBO bytes per frame in the 26.3 world; Espryt 0. Either freeze the second variable-length tail shape, or take the alternative (Magma binds a resident `VkBuffer` range directly at the descriptor, a separate `dev` PR). P7.
|
||||
7. **The cut seam in `MG_Util`.** The server needs SPIRV-Cross / ESSL translation cache / format handlers / POST probe; the client needs glslang and the reflection layer; whether `MG_Util` has a clean Transpile-vs-Reflect seam inside has not been audited. P7.
|
||||
8. **Can one reflection archive serve three consumers.** The Espryt half is answered (P4a): yes, and no copy is needed. Magma's first consumer was answered by P5f fm: `MagmaProgramSource` takes everything from the wire `ProgramArchive`, no frontend object, no copy. The second (storage block table) today is **no** and has gotten worse: under split, `MagmaProgramSource::EnsureStorageBlocks` **re-runs SPIRV-Reflect every draw** (the monolith arm's `DirectVulkan.cpp:177-200` at least has a persistent cache); the answer is to add ordered `storageBlocks` to `LinkArtifacts` (the data is already in `blockReflection`), and both consumers read it instead. **Answered (P7 wave 2-C, `dce9953d..cf7ca59f`)**: `storageBlocks` enters the archive, both consumers read it, per-draw SPIRV-Reflect 4→0; the monolith half of `g_programResourceCaches` goes to P13 (G1).
|
||||
9. **Can viewport-array replay fit into one `draw_vbo`**: **answered (2026-09-30, ID-P8-1)**: yes — the server replays it pass by pass within one draw, grouped by state (`DirectGLES.cpp:8421-8465`), and `ViewportArrayScenario` is green on each split arm.
|
||||
10. **The `ResidentSubData` asymmetry.** Half answered (checked 2026-09-22): **Magma's implementation is complete** (`VkBufferManager.cpp:123` monolith table, `:148` wire table, `fff9d639f`). **Still unanswered: `kCapResidentSubData` is never published by the server** — `MG_Backend/Init.cpp:192-219` is the only production `SetCapabilityBits` call site in the whole tree, OR-ing only XFB, the three query bits, and the run-ahead bit; so `PipeFill.cpp:848` is always false under split, and **both backends under split fall back to the in-place memcpy at `BufferObject.cpp:711-716`**, making Magma's implementation dead code in split. **Answered (P7 wave 2-C, ID-P7-20)**: `InitServerRoleCommon` publishes `kCapResidentSubData` from the server's own wire resource table, and the mirror accepts it on receipt; the white-box check "what is emitted is opcode 49" is unreachable before the P11 adoption tier lands (§5.4 debt).
|
||||
11. **The `SEG_STAGE` limit.** Content side answered (2026-09-20): **chunking is implemented** — the buffer range walk (`1e7c372e`) and the texture full-width slab (`9469d48e`) split into multiple `resource_subdata` records per `MGPipeStageChunkBytes()` (default 1/4 of 32 MiB = 8 MiB), so a single 128 MiB upload no longer needs an explicit `MOBILEGL_IPC_STAGE_MB=256`; when a single piece still exceeds the arena or that record type is not wired into chunking, the `Fatal{RingOverrun, "SEG_STAGE"}` fallback remains. **Still unanswered (narrowed 2026-09-22)**: in a6 §6's measured results, the only things not walked are the **program archive** (the only unbounded blob) and the **`draw_vbo` `NumDraws*12` range tail**; the host index span has no producer (struck out); `set_global_constants` has a walk. Chunking both is the same work shared by P6.5 sl and P8, and the stream send window is sized against them. The occupancy distribution of MC in-world / Create is still unmeasured (gate 8 has given bytes/frame and records/frame for the two loads; the per-blob distribution was taken only on the integration lane). **P8-0 measured result (2026-09-30, ID-P8-5)**: largest program archive 354,846 B, largest `draw_vbo` record 1,936 B, both far below arena/4; **no chunking, closed**.
|
||||
12. **A second opinion on the server side for split-only rendering bugs after P13.** The verify build + recorder cover only what is pushed, not the backend's interpretation of it.
|
||||
13. **Whether baked internal shaders can express uniform locations and UBO layouts without a live `ProgramObject`.** Color blit answered (P5f fv: `WireColorBlit.{vert,frag}` uses push constants, and `WireColorBlitSpirv.h` is checked into the tree); narrowed to the depth mip program (today compiled out under split, whose split path is a named Fatal) and pass-through TCS (`ProgramFactory.cpp:4060-4262`, the third uninventoried internal shader generator). **Answered (P7 wave 2-B / B2)**: depth mip baking (A)(D) (`WireDepthMipmap.*`, push constant, `MOBILEGL_BAKED_INTERNAL_SHADERS` freshness gate table-driven) and (B′) (the disaggregated build's monolith arm switches to the baked module) landed; the third baked shader set, for multisample resolve, comes with B2; pass-through TCS is still uninventoried.
|
||||
14. **Which pull-mode-tuned cache hit rates the push model changes.** Survivor capacity is retuned in P13. One clue: on device, 26.3 Espryt's `sve` ≈ draw count (the sampler-view set is re-sent once per draw), while desktop is only ~0.07/draw; on the P3b/P4b optimization list.
|
||||
15. **Whether monolith's `*IndirectCount` not calling `SyncGpuWrites()` is a latent gap.** A separate `dev` issue; the split must not fix it opportunistically. **Answered (2026-09-30): yes** — P8-C / P8-D host red-once confirms (Espryt `*IndirectCount`, Magma `glMultiDrawArraysIndirectCount` read 0); the split half is fixed (ID-P8-9), and the monolith half is fixed in the P8 second wave's dev package (ID-P8-3).
|
||||
16. **Actual memory footprint of the index host mirror.** The total element-array buffer in the MC / Sodium / Iris corpora is unmeasured; if it significantly exceeds 64 MiB, the frequency and cost of the fallback path must be measured. P8.
|
||||
17. **The `create-indirect` fixture's failure on Adreno 830** predates this branch (reproduces with the baseline APK, `dev@81b17c0b`); it is not caused by this branch. The `scudo::reportMapError` crash with `rd12` + Magma on both Adreno 830s is also on the `dev` side. Both are excluded from device A/B and stay in the desktop corpus; P8 must assert `roundtrips-per-frame == 0` on `create-indirect`, so the `dev` fix is on someone else's critical path. **The create-indirect part is answered (2026-09-30)**: the original fixture was captured on a desktop driver with `GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT` = 16, and Flywheel laid out the scatter list for 16; Adreno 830 on both backends and Mali's Vulkan require 64, so all 1,270 range bindings were rejected with `GL_INVALID_VALUE`. The original fixture is retired and replaced by the derived `minecraft-1.21.1-neoforge-create-indirect-in-world-align1024` (**not the same trace**: `tools/trace_replay/fixture_tools/realign_buffer_ranges.cpp --align 1024` moves these bindings to 1024-aligned slots, inserting 3,812 calls, target calls 504631 → 508443; host SSIM matches the original fixture to 9 digits). Redmi passes on both backends for monolith / spawn; on Mali-G1-Ultra Magma passes (the split arm separately fixed the T0 POST, see the POST section of `CONTRACT-P11.md`), while Espryt is still 0.848: that machine's GLES driver vertex-stage SSBO limit is 0, so Flywheel indirect's vertex program cannot link (a missing capability, not a split defect).
|
||||
18. **Load balancing between the two split sides.** A new question unlocked by P5e: under VD32 (~3550 draws/frame) the apply thread is **11-12 ms**, the client **15.7 ms**, and **the client is the bottleneck, pinned at ~94%**. Moving work from the client to apply would directly lower the bottleneck — meaningless under lockstep (the two sides add serially); **run-ahead is what makes it a lever**. Options: the emitter on the encode side, the tracker walk, dirty-page tracking of the persistent map (`change_protection` was 1.3% in the profile). Must re-profile first: with the waits gone, the denominators of all the tail items have changed. Queued after P5e; unowned; on 2026-09-22 suggested to attach to the optimization phase after the roadmap completes, with the 2×2 matrix lane of P6.5 re-profiling along the way.
|
||||
19. **Link bandwidth budget and compression.** In the TCP endgame, `SEG_STAGE` bytes/frame is against Wi-Fi / USB (on the order of 30–100 MB/s), not memory; the ~1 MB/frame budget in `ARCHITECTURE.md` §14 at 60 fps is 60 MB/s, already at the Wi-Fi limit. Whether to LZ4-style compress the stage payload and whether to deduplicate texture uploads by content hash on the server side must wait for gate 8's distribution. Must be answered before the first line of the P6.5 contract is written.
|
||||
20. **The stall of a large single upload.** A 128 MiB single upload at 50 MB/s is 2.5 s; content-side chunking solves "does it fit", not "how long must we wait". On the cross-machine arm, either accept and record it, or make uploads asynchronous (think it through together with P9's semantics). Do not pretend it is solved.
|
||||
21. **The form of pairing / authentication.** **Ruled (ID-P7-3, 2026-09-22): a token is enough, no TLS.** The threat model is "a remote code path anyone can connect to", not eavesdropping: token comparison in constant time, ≥16 bytes, no token means loopback only, the data plane bound to the authenticated control connection by `Welcome.dataNonce`, authentication before fork. TLS is kept as a P12 open item.
|
||||
22. **Client platform scope.** The default is Linux + Android first (existing CI can verify), the Windows client in the second batch (`ShmSegmentWin32` exists; the `pipe:` named refusal is retired, TCP replaces it), and macOS is still not split. This decides whether `ARCHITECTURE.md` §15.3 "Windows machines are not a correctness gate" needs changing.
|
||||
23. **Detection of half-open connections.** A Wi-Fi disconnect does not produce a hang-up event; dl's latch comes from the descriptor hang-up, while TCP needs keepalive / heartbeat timeout and **reporting as a hang-up by the transport layer** — this does not contradict "never take it from an apply timeout"; write it down to avoid re-arguing. P6.5 ct.
|
||||
24. **Redefinition of the E1 control (ID-122).** **Closed (2026-09-29 debt review)**: `10bfd29f` replaced E1's pixel race with the negative control `scripts/ci/wait_boundary_negative_control.py` that observes protocol waits. On 2026-10-18 that negative control retired: the probe could see the missing wait, so the resident unit case `RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait` proves it instead (the same `ApplierReset`, no wait after publishing, must read parked=0).
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
# MGPipe:MobileGL 前后端拆分
|
||||
# MGPipe: MobileGL frontend/backend split
|
||||
|
||||
**做什么**:在 MobileGL 的前端(处理应用的 GL 调用)和后端(调用 GLES / Vulkan 驱动)之间立一份显式接口——前端把状态变化**推**给后端,后端不再直接读前端的内存。有了这份接口,前后端可以放在两个线程、两个进程,甚至两台机器上运行。
|
||||
**What it does**: MGPipe puts an explicit interface between MobileGL's frontend (which handles the application's GL calls) and backend (which calls the GLES / Vulkan driver) — the frontend **pushes** state changes to the backend, and the backend no longer reads the frontend's memory directly. With this interface, the frontend and backend can run on two threads, two processes, or even two machines.
|
||||
|
||||
**做到哪了**:见 [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)。当前阶段与下一步只在那一页维护,这里不再复述。
|
||||
**How far along**: see [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md). The current phase and next steps are maintained only on that page and are not repeated here.
|
||||
|
||||
## 一张图
|
||||
## One diagram
|
||||
|
||||
```
|
||||
应用的 GL 调用
|
||||
→ 前端 MG_Impl:处理 GL 语义;在每条"做事的命令"(draw、clear…)之前,把状态变化整理成记录
|
||||
→ MGPipe 接口(两张函数指针表,由调用目录 PipeCalls.def 生成)
|
||||
→ 三选一:直接调用(单线程)│ 队列 → 另一个线程 │ 队列 → 另一个进程 / 另一台机器
|
||||
→ 后端 MG_Backend:Espryt(GLES)/ Magma(Vulkan),按记录更新自己的状态并调用驱动
|
||||
← 反向消息(错误、GPU 写回、窗口尺寸)按顺序回到前端
|
||||
The application's GL calls
|
||||
→ frontend MG_Impl: handles GL semantics; before every command that does work (draw, clear…), turns state changes into records
|
||||
→ MGPipe interface (two function pointer tables, generated from the call catalog PipeCalls.def)
|
||||
→ one of three: direct call (single-threaded) │ queue → another thread │ queue → another process / another machine
|
||||
→ backend MG_Backend: Espryt (GLES) / Magma (Vulkan), updates its own state from the records and calls the driver
|
||||
← reverse messages (errors, GPU writeback, window size) come back to the frontend in order
|
||||
```
|
||||
|
||||
## 文档导航
|
||||
## Documentation map
|
||||
|
||||
| 想知道 | 看 |
|
||||
| To learn | See |
|
||||
|---|---|
|
||||
| 现在做到哪、下一步 | [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md) |
|
||||
| 设计要点与各部分设计 | [`ARCHITECTURE.md`](ARCHITECTURE.md) → [`design/`](design/) |
|
||||
| 分几个阶段、每个阶段做了什么 | [`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) |
|
||||
| Where things stand, next steps | [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md) |
|
||||
| Key design points and the design of each part | [`ARCHITECTURE.md`](ARCHITECTURE.md) → [`design/`](design/) |
|
||||
| How many phases there are, what each phase did | [`ROADMAP.md`](ROADMAP.md) |
|
||||
| Key numbers for performance and verification | [`MEASUREMENTS.md`](MEASUREMENTS.md) |
|
||||
| How to build, run, and test | [`guide/build-and-run.md`](guide/build-and-run.md) |
|
||||
| Where the code is / terminology / engineering discipline | [`guide/code-map.md`](guide/code-map.md), [`guide/glossary.md`](guide/glossary.md), [`guide/discipline.md`](guide/discipline.md) |
|
||||
|
||||
整理记录:2026-09-24 两次整理——顶层只留索引,设计正文拆到 `design/`,上手与参考放 `guide/`,阶段细节放 `notes/<阶段>/`(这些笔记已从树中删除,见 git 历史);整理前的版本见提交 `1fb18d9`。更早的设计竞赛与评审稿在 git 历史(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`)。
|
||||
Organization history: two cleanups on 2026-09-24 — the top level keeps only indexes, the design body moved into `design/`, getting-started and reference material live in `guide/`, and phase details live in `notes/<phase>/` (those notes have been deleted from the tree; see git history); the pre-cleanup version is commit `1fb18d9`. Earlier design contests and review drafts are in git history (`8b31de2f`, `1794ac94`, `8349babe`, `87ee17c6`).
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
# 路线图(索引)
|
||||
# Roadmap (index)
|
||||
|
||||
> **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 历史。
|
||||
> **2026-10-07**: P0 through P7, P12, P9, P10, P11, P8, and **P13** are done — both the split route and the monolith route are complete. P13 (retire the pull path): the monolith also runs only on the record arm; the pull / push builds, the `MOBILEGL_PIPE_PUSH` and `MOBILEGL_PIPE_LEGACY_MEMOS` compile-time switches, the residual value blocks, and the backend's frontend-object arm are all deleted; the library splits into seven modules partitioned by the link ratchet, and in the split build the server half is truly linked on its own; G1 is retired, succeeded by zero `MG_Remote` symbols, the skip census, and the recorder golden. Performance work is queued after it, with the `dev` build as baseline. The next item is **P14** (state ownership). The plan, acceptance results, measured results, and reports for each phase are in git history.
|
||||
|
||||
## 目标
|
||||
## Goals
|
||||
|
||||
client(跑应用的一方)与 server(跑驱动的一方)可以在**不同机器、不同系统**上经 TCP 相连;在同一台机器上则用两个进程 + 共享内存。传输分控制面与数据面两层,可各自选择、混搭。
|
||||
The client (the side that runs the application) and the server (the side that runs the driver) can connect over TCP across **different machines and different operating systems**; on the same machine they use two processes + shared memory. The transport has two layers, control plane and data plane, each independently selectable and mixable.
|
||||
|
||||
## 两条路线
|
||||
## Two routes
|
||||
|
||||
- **单进程路线**:先把接口建起来,让单进程版本也受益(后端拥有自己的状态、可以挪到渲染线程)。P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → **P13**(走完)。
|
||||
- **拆分路线**:在接口之上逐步拆开前后端。P5 → P5b → P5c → P5d → P5e → P5f → P6 → P6.5 → Ph → P12 → P9 → P10 → **P11**(走完)。
|
||||
- **Monolith route**: build the interface first, so the monolith benefits too (the backend owns its own state and can move to the render thread). P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → **P13** (complete).
|
||||
- **Split route**: split the frontend and backend step by step on top of the interface. P5 → P5b → P5c → P5d → P5e → P5f → P6 → P6.5 → Ph → P12 → P9 → P10 → **P11** (complete).
|
||||
|
||||
## 阶段一览
|
||||
## Phase list
|
||||
|
||||
| 阶段 | 做什么 | 状态 |
|
||||
| Phase | What it does | Status |
|
||||
|---|---|---|
|
||||
| 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),实现待做 |
|
||||
| P0 | Lay the foundation: counters, call catalog and generators, CI gates; verify Android can launch a second process | ✅ 09-05 |
|
||||
| P0.5 | Extract the data type header shared by frontend and backend, cutting the backend's dependency on frontend headers | ✅ |
|
||||
| P1 | The backend reads an "input block" instead, and can verify per draw that it matches the frontend's real state | ✅ |
|
||||
| P2 | The frontend starts actively pushing render state; interface cost measured at +6–12%, decision to continue | ✅ 09-08 |
|
||||
| P3a | GLES backend's buffers and vertex arrays switch to handles | ✅ 09-08 |
|
||||
| P4a | GLES backend's framebuffers, textures, samplers, programs switch to handles | ✅ 09-08 |
|
||||
| P5 | First split into two threads: command queue + backend thread (every command waits) | ✅ 09-16 |
|
||||
| P5b | Move the remaining commands onto the queue; first on-device run of the target game with a separate backend thread | ✅ 09-16 |
|
||||
| P5c | The two threads no longer secretly share memory; they exchange only through the queue | ✅ 09-17 |
|
||||
| P5d | Two-thread performance: in-game 7–13 fps → 103–106 fps | ✅ 09-18 |
|
||||
| P5e | The client runs ahead and no longer waits for the backend on every command; matches single-threaded under heavy load | ✅ 09-19 |
|
||||
| P5f | All state passes through the queue, clearing the last obstacle to splitting into processes; the Vulkan backend also runs ahead | ✅ 09-20 |
|
||||
| P6 | Split into two processes; on device the two-process cost matches two-thread | ✅ 09-22 |
|
||||
| P6.5 | Transport splits into control plane / data plane; a PC as client and a phone as server run over TCP | first wave ✅, remainder to CI |
|
||||
| Ph | The server safely accepts external connections: token, quotas, bad data does not crash | required items ✅ (with P7) |
|
||||
| P3b / P4b | Deepening and wrap-up of the GLES backend | mostly ✅, remainder in parallel |
|
||||
| P7 | Full Vulkan backend migration; on-device frame check 36/36 passes | ✅ 09-23 |
|
||||
| **P12** | **The server opens its own window and presents on screen; the client needs no window** | ✅ 09-29: exit gate B (cross-machine TCP) ✅ 09-28, A (FCL + killing the server) ✅ 09-29; contract in git history; G1 holds; remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P8** | ~~Move the remaining emulation paths to the correct side and complete the protocol~~ → **Close the server-side emulation gaps and split coverage** | ✅ 09-30: re-scoped (6 of the original 10 items were already done as prerequisites); Espryt mip generation / staging copies with GPU writes / driver refusing reads, Magma wire-native indirect and dispatch; 246 cases brought onto the split three arms (`split_coverage.py`); the Create dropped-copy fix on Adreno landed in dev and feat; contract in git history; remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P9** | **Reverse channel: readback, logs, and re-mint no longer wait synchronously for the client** | ✅ 09-29: PACK-PBO readback fire-and-forget (both backends), graded log forwarding, re-mint without pull (`OnLog` / `OnTexturePullRequest` deleted, callbacks 9 → 7), F1 / F2; contract in git history; G1 holds; event-volume batching not done, no measured need; remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P10** | **Synchronization objects, queries, and frame pacing** | ✅ 09-29: fence polling answered locally by the server's own report (`kEventFenceSignaled`, `MOBILEGL_IPC_POLL_ESCALATE`; host Magma·tcp −37%, zero round trips for both backends on Adreno 830); `SetSwapInterval` forwarded, class C zeroed; query / XFB / AsyncCompile registered on each split arm; credit 1/2/3 device loopback within noise; contract in git history; G1 holds; remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P11** | **Same-machine large-buffer zero-copy sharing** | ✅ 09-29: T0 zero-copy import (both backends, on by default, silently falls back to T2 when the self-test fails or on Stream); same-machine external client goes through shared memory via helper + token broker (B1); connections paired by `PairBind` nonce (revision 5); adoption tier fixed at handshake; contract in git history; remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P13** | **Delete the old "backend reads the frontend directly" path** | ✅ 10-07: the monolith runs only on the record arm; the pull / push builds and the `PIPE_PUSH` / `LEGACY_MEMOS` compile-time switches, the residual value blocks, and the backend's frontend-object arm are deleted; seven OBJECT modules and server-only real linking; link ratchet 172 → 27 named debts; recorder golden; G1 retired; FCL on-device smoke passes on both backends; performance round (baseline `dev`) and remainder into [`DEBTS.md`](DEBTS.md) |
|
||||
| **P14** | **State ownership: multiple sessions / multiple contexts / share groups in one process** (prerequisite for a unified server; anland's built-in renderer does not isolate per process) | design ✅ [`design/11-state-ownership.md`](design/11-state-ownership.md), implementation pending |
|
||||
|
||||
## 另见
|
||||
## See also
|
||||
|
||||
- 工程纪律与验证门:[`guide/discipline.md`](guide/discipline.md)
|
||||
- 仍开放的债务:[`docs/Disaggregated/DEBTS.md`](DEBTS.md);开放问题:[`docs/Disaggregated/OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)
|
||||
- Engineering discipline and verification gates: [`guide/discipline.md`](guide/discipline.md)
|
||||
- Still-open debts: [`docs/Disaggregated/DEBTS.md`](DEBTS.md); open questions: [`docs/Disaggregated/OPEN-QUESTIONS.md`](OPEN-QUESTIONS.md)
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
# 边界与对象模型(原 ARCHITECTURE §1–§2)
|
||||
# Boundary and Object Model (formerly ARCHITECTURE §1–§2)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design highlights and an index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; `ARCHITECTURE.md §N` in code comments resolves by number here.
|
||||
|
||||
## 1. 边界
|
||||
## 1. Boundary
|
||||
|
||||
### 1.1 一句话
|
||||
### 1.1 In one sentence
|
||||
|
||||
`MG_Backend` 本来就是一台贴着目标 API 的状态机(Espryt = DirectGLES,Magma = DirectVulkan),缺的是一份"我被告知了什么"的显式声明。MGPipe 就是这份声明:前端在每条 verb 之前把变化**推**过去,后端不再拉 `MG_State::pGLContext`。接口从两个后端自己维护的关键结构反推(`SetupDrawSnapshot`、`DrawTextureSyncKeys`、`ResolvedVertexBindings`、`g_syncedRenderStateParameters`、`BufferBackendOps` …);gallium 是词汇的目的地,不是推导前提。
|
||||
`MG_Backend` was already a state machine glued to the target API (Espryt = DirectGLES, Magma = DirectVulkan); what it lacked was an explicit declaration of "what I was told". MGPipe is that declaration: the frontend **pushes** changes across before every verb, and the backend no longer pulls `MG_State::pGLContext`. The interface is derived backward from the key structures the two backends maintain themselves (`SetupDrawSnapshot`, `DrawTextureSyncKeys`, `ResolvedVertexBindings`, `g_syncedRenderStateParameters`, `BufferBackendOps` …); gallium is where the vocabulary is headed, not a premise of the derivation.
|
||||
|
||||
### 1.2 两张函数指针表
|
||||
### 1.2 Two function pointer tables
|
||||
|
||||
`MGPipeScreen`(share-group 作用域:caps、resource、persistent map、fence)与 `MGPipeContext`(其余全部),由 `PipeCalls.def` 生成。
|
||||
`MGPipeScreen` (share-group scope: caps, resource, persistent map, fence) and `MGPipeContext` (everything else), generated from `PipeCalls.def`.
|
||||
|
||||
- 函数指针 struct 而非虚基类:null 项已表示"未实现,前端回退",`MG_Test` 整表替换即可 mock。
|
||||
- 两张表从第一天分开,事后拆会给记录重新编号;v1 只有一个 screen、一个 context、一条 flow。
|
||||
- EGL 生命周期与 caps 面留在 `pActiveBackendObject` 的虚函数上(罕见路径,走控制面)。
|
||||
- A struct of function pointers instead of a virtual base class: a null entry already means "not implemented, the frontend falls back", and `MG_Test` can mock the whole thing by replacing the table.
|
||||
- The two tables are separate from day one; splitting them later would renumber the records; v1 has only one screen, one context and one flow.
|
||||
- EGL lifecycle and the caps side stay on the virtual functions of `pActiveBackendObject` (a rare path, on the control plane).
|
||||
|
||||
### 1.3 三种形态,一份后端
|
||||
### 1.3 Three forms, one backend
|
||||
|
||||
| 形态 | 表里装的是什么 | 用途 |
|
||||
| Form | What the tables hold | Purpose |
|
||||
|---|---|---|
|
||||
| `monolith`(默认) | 后端自己的函数,回调直调 `MG_State` | 出货 |
|
||||
| `inproc` | 发射器 → 同进程第二个线程上的 applier | CI 主验收;也就是 monolith 的**渲染线程** |
|
||||
| `spawn` | 发射器 → 数据面 → 另一个进程的 applier → 同一批后端函数 | 两进程形态;控制面可以是 `fork` / `unix:` / `tcp://`(§11.9),server 可在另一台机器上 |
|
||||
| `monolith` (default) | the backend's own functions; callbacks call `MG_State` directly | ship |
|
||||
| `inproc` | emitter → applier on a second thread in the same process | CI main acceptance; that is, the monolith **render thread** |
|
||||
| `spawn` | emitter → data plane → applier in another process → the same set of backend functions | two-process form; the control plane can be `fork` / `unix:` / `tcp://` (§11.9), and the server can be on another machine |
|
||||
|
||||
唯一 hook 点是 `MG_Backend::Init()` 里一个 `#if MOBILEGL_BUILD_DISAGGREGATED` 分支:`MG_Config::Transport != Monolith` 时装 `MG_Remote::BackendObject_Remote`。`MG_Impl` 的边界调用点零 `#ifdef`。
|
||||
The only hook point is one `#if MOBILEGL_BUILD_DISAGGREGATED` branch inside `MG_Backend::Init()`: when `MG_Config::Transport != Monolith` it installs `MG_Remote::BackendObject_Remote`. Zero `#ifdef`s at `MG_Impl`'s boundary call sites.
|
||||
|
||||
## 2. 对象模型
|
||||
## 2. Object model
|
||||
|
||||
### 2.1 句柄 = `{slot, gen}`(`MG_Pipe/MGPipeHandles.h`)
|
||||
### 2.1 Handle = `{slot, gen}` (`MG_Pipe/MGPipeHandles.h`)
|
||||
|
||||
- 8 字节 POD;**client 铸造,server 永不返回句柄** → 整份目录零创建 round trip(对 gallium 的偏离 D1)。
|
||||
- slot 按 kind 稠密分配(free list + 高水位),server 对象表是数组;`gen` 只在 slot 复用时 ++。
|
||||
- kind:`Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context`。保留:`{0,0}` = null;`Framebuffer {0,1}` = 默认帧缓冲;`ShaderCso` 高段给 program pipeline 合成体。
|
||||
- GL name 只作诊断(`GlNameForDiag`),永不做身份、memo 键或 content hash。
|
||||
- An 8-byte POD; **the client mints it, the server never returns a handle** → the whole catalog has zero creation round trips (deviation D1 from gallium).
|
||||
- Slots are allocated densely per kind (free list + high-water mark), the server's object table is an array; `gen` is incremented only when a slot is reused.
|
||||
- kind: `Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context`. Reserved: `{0,0}` = null; `Framebuffer {0,1}` = the default framebuffer; the high segment of `ShaderCso` is for program pipeline composites.
|
||||
- GL names are for diagnostics only (`GlNameForDiag`), never for identity, memo keys or content hashes.
|
||||
|
||||
### 2.2 两种世代,严格分开
|
||||
### 2.2 Two kinds of generation, strictly separated
|
||||
|
||||
| | 拥有者 | 回答 | 过线 |
|
||||
| | Owner | Answers | Crosses the wire |
|
||||
|---|---|---|---|
|
||||
| `MGPipeHandle::Gen` | client | "还是同一个 GL 对象吗?" | 是 |
|
||||
| `MGGen`(后端纪元,如 `g_bufferMutationEpoch`) | server | "我自己是否重铸了驱动对象?" | **永不**(只以纹理拉取请求的形式出现,§8.4) |
|
||||
| `MGPipeHandle::Gen` | client | "Is it still the same GL object?" | yes |
|
||||
| `MGGen` (backend epoch, e.g. `g_bufferMutationEpoch`) | server | "Did I re-mint the driver object myself?" | **never** (it appears only in the form of a texture pull request, §8.4) |
|
||||
|
||||
client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明:过线时要么加宽,要么与 `{slot, gen}` 同行。
|
||||
The client's wrapping `Uint16` version counter is never the sole proof of freshness: when it crosses the wire, either widen it or send it alongside `{slot, gen}`.
|
||||
|
||||
### 2.3 CSO 与可变对象
|
||||
### 2.3 CSO and mutable objects
|
||||
|
||||
- CSO(render state、vertex elements、sampler、sampler view、shader)在 **client 侧内容寻址**(Mesa `cso_cache` 先例),LRU 淘汰时发 `delete_*`;render-state 容量 64(P13 重调)、sampler state 256。
|
||||
- Buffer / Texture / Renderbuffer 走 create / respecify / subdata / destroy;Framebuffer / Xfb 是 per-context 身份 + `set_*` payload。
|
||||
- 两条偏离:**D-G1** Espryt 的 vertex-elements 按 VAO 身份寻址(twin 持驱动 VAO 名,不能共享);**D-F2** Espryt 的 sampler view 按纹理身份寻址,sampler state 内容寻址且带引用计数(被 `BuiltinSampler` 指着的项不许被 LRU 挤掉)。
|
||||
- CSO (render state, vertex elements, sampler, sampler view, shader) is **content-addressed on the client side** (Mesa `cso_cache` precedent), and sends `delete_*` on LRU eviction; render-state capacity 64 (retuned in P13), sampler state 256.
|
||||
- Buffer / Texture / Renderbuffer go through create / respecify / subdata / destroy; Framebuffer / Xfb are per-context identity + `set_*` payload.
|
||||
- Two deviations: **D-G1** Espryt's vertex elements are addressed by VAO identity (the twin holds the driver VAO name and cannot be shared); **D-F2** Espryt's sampler view is addressed by texture identity, sampler state is content-addressed and reference-counted (an entry pointed to by a `BuiltinSampler` must not be evicted by the LRU).
|
||||
|
||||
#### 2.3.1 sampler state 缓存:引用计数与值生命周期
|
||||
#### 2.3.1 sampler state cache: reference counting and value lifetime
|
||||
|
||||
sampler state 在 client 侧按值寻址(`MGPipeSamplerCsoCache`,容量 256):哈希、探测、再 memcmp 确认;只凭 64 位哈希相等会把两种状态并进一个 CSO,造成没有门看得见的错误过滤。
|
||||
- 活句柄躲开 LRU 靠引用计数而不是容量:已发布的 `MGPTextureParams` 在纹理参数不动时一直指着它的 `BuiltinSampler`,applier 不解析这个句柄,而淘汰不算参数变化,既不会重发也没人拒绝。所以每次 `Acquire` 取一个引用、`Release` 还一个;LRU 只考虑引用为 0 的项;全被钉住时超容量铸造并计入 `OverCapacityMints`(多一个槽费内存,句柄被抽走费正确性)。
|
||||
- 持有引用的是纹理的内建 sampler(纹理死亡或参数变化重取时释放)和 `bind_sampler_states` 当前绑定的每个 CSO;凡在返回后还会指名该句柄的调用方都必须持有。容量大于 `kMGPipeMaxTextureUnits` 只是尺寸界:装不下一整轮发射的缓存会每轮都超容量铸造。
|
||||
- 槽不带 lifetime id:CSO 属于值而不属于前端对象(别的 `SamplerObject` 可能持有同值),`~SamplerObject` 不释放任何槽,唯一死亡路径是 client 侧的 LRU 淘汰。因此来自 `~SamplerObject` 的 `NotifyStateObjectDestroyed(SamplerCso, lifetimeId)` 背后永远没有句柄;后端不得按 `SamplerObject` 的 lifetime id 键控 sampler twin,twin 的生命只是 `create_sampler_state` 到 LRU 发出的 `delete_sampler_state`。
|
||||
sampler state is value-addressed on the client side (`MGPipeSamplerCsoCache`, capacity 256): hash, probe, then confirm with memcmp; trusting a 64-bit hash match alone would merge two states into one CSO, causing incorrect filtering that no gate can see.
|
||||
- Live handles avoid the LRU by reference count rather than by capacity: a published `MGPTextureParams` keeps pointing at its `BuiltinSampler` while the texture parameters do not change, the applier does not resolve this handle, and eviction does not count as a parameter change, so the handle is neither re-sent nor rejected. Therefore every `Acquire` takes a reference and `Release` returns one; the LRU considers only entries with zero references; when everything is pinned and capacity is exceeded, mint beyond capacity and count it in `OverCapacityMints` (an extra slot costs memory, a stolen handle costs correctness).
|
||||
- The holders are the texture's built-in sampler (released when the texture dies or when parameters change and it re-acquires) and every CSO currently bound by `bind_sampler_states`; any caller that will still name the handle after returning must hold one. Capacity greater than `kMGPipeMaxTextureUnits` is only a size bound: a cache that cannot hold one full round of emission will mint beyond capacity every round.
|
||||
- Slots carry no lifetime id: a CSO belongs to a value, not to a frontend object (another `SamplerObject` may hold the same value), `~SamplerObject` frees no slot, and the only death path is client-side LRU eviction. So a `NotifyStateObjectDestroyed(SamplerCso, lifetimeId)` coming from `~SamplerObject` never has a handle behind it; the backend must not key sampler twins by a `SamplerObject`'s lifetime id; a twin's life is only from `create_sampler_state` to the `delete_sampler_state` issued by the LRU.
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
# 调用目录与记录格式(原 ARCHITECTURE §3–§4)
|
||||
# Call Catalog and Record Format (formerly ARCHITECTURE §3–§4)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design highlights and an index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; `ARCHITECTURE.md §N` in code comments resolves by number here.
|
||||
|
||||
## 3. 调用目录
|
||||
## 3. Call catalog
|
||||
|
||||
### 3.1 单一真相源
|
||||
### 3.1 Single source of truth
|
||||
|
||||
`MobileGL/MG_Pipe/PipeCalls.def`:一行一个 `X(Name, Payload, Class, Flags, WaitClass)`。**线上 opcode 就是行序**,只能追加、退役保留槽位;行数由 `MGP_CALL_LIST_DOCUMENTED_COUNT`(今天 81)与 `PipeCatalogueTest` 钉住。生成器产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`:
|
||||
`MobileGL/MG_Pipe/PipeCalls.def`: one `X(Name, Payload, Class, Flags, WaitClass)` per line. **The on-wire opcode is the line order**; you can only append, and retired lines keep their slot; the line count is pinned by `MGP_CALL_LIST_DOCUMENTED_COUNT` (81 today) and `PipeCatalogueTest`. Generator outputs are committed to the tree; CI `pipe-gates` regenerates them and runs `git diff --exit-code`:
|
||||
|
||||
| | 产物 | 内容 |
|
||||
| | Output | Content |
|
||||
|---|---|---|
|
||||
| G1 | `PipeTables.inc` | 两张函数指针表 |
|
||||
| G2 | `PipeThunks.inc` | monolith 直调 thunk |
|
||||
| G3 | `PipeWire.inc` | wire 记录、尺寸 `static_assert`、applier 边界检查 |
|
||||
| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 逐字段比对器(`PipeFields.def`) |
|
||||
| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id 与逐 verb 世代 poison |
|
||||
| G6 | `PipeCoverage.inc` | 后端读点 → 调用映射(`Coverage.def`),0 UNMAPPED 是门 |
|
||||
| G7 | `PipeSpanTable.inc` | render-state pipeline 子集成员表 |
|
||||
| G8 | `PipeFieldOwnership.inc` | 每个 `PipeInputs` 字段恰属 `RECORD-SUPPLIED / APPLIER-DERIVED / BARRIER-PULLED / FATAL` 之一(P5f 起 0 BARRIER-PULLED) |
|
||||
| G1 | `PipeTables.inc` | the two function pointer tables |
|
||||
| G2 | `PipeThunks.inc` | monolith direct-call thunks |
|
||||
| G3 | `PipeWire.inc` | wire records, size `static_assert`s, applier bounds checks |
|
||||
| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` field-by-field comparator (`PipeFields.def`) |
|
||||
| G5 | `PipeFilled.inc` | `PipeInputs` field ids and per-verb generation poison |
|
||||
| G6 | `PipeCoverage.inc` | backend read sites → call mapping (`Coverage.def`); 0 UNMAPPED is the gate |
|
||||
| G7 | `PipeSpanTable.inc` | render-state pipeline subset membership table |
|
||||
| G8 | `PipeFieldOwnership.inc` | each `PipeInputs` field belongs to exactly one of `RECORD-SUPPLIED / APPLIER-DERIVED / BARRIER-PULLED / FATAL` (0 BARRIER-PULLED since P5f) |
|
||||
|
||||
另有 `FillPoints.def`(填充点)与 `DirtySurface.def`(§5.2)。G1 codegen 规则:capability gate 替换指针表达式时保留**表达式形状**,否则 pull `.text` 漂移。
|
||||
There are also `FillPoints.def` (fill points) and `DirtySurface.def` (§5.2). G1 codegen rule: when the capability gate replaces a pointer expression, preserve the **expression shape**, otherwise the pull `.text` drifts.
|
||||
|
||||
### 3.2 分组与 flag
|
||||
### 3.2 Groups and flags
|
||||
|
||||
- Class:`kScreen`(caps、resource、persistent map、fence、`ApplierReset`)、`kCtxQuery`、`kCtxCso`(create/bind/delete × 五种 CSO)、`kCtxState`(`Set*` 状态、`SetContextValues`、`SetProgramBindings`、迁移期的 `SetResidualValueState`)、`kCtxObject`(subdata、readback、mip、`ObjectDeath` …)、`kCtxVerb`(blit、clear、`DrawVbo`、dispatch、XFB、`Present` …)。
|
||||
- Flags:`kNeedsAck`、`kHasBlob`、`kVarTail`、`kHostSpan`、`kReplySlot`、`kOptional`。第五列 `WaitClass`(P5e)决定 run-ahead 下 client 等不等(§11.6)。
|
||||
- 20 个 draw 入口塌成 `DrawVbo` 一条;`Clear` 一条判别式;sampler 集合**没有 stage 维度**(192 单元合并空间);`SetTextureParams` 按资源寻址、与 sampler view 分开(D10);`SetIndexBuffer` 独立于 VAO 配置(D5)。
|
||||
- 显式不移植:`GetIntegeri_v` / `GetInteger64i_v` / `GetProgramiv`、`set_pixel_unpack_state`(前端已解析)、压缩格式概念、`pipe_transfer`。
|
||||
- Class: `kScreen` (caps, resource, persistent map, fence, `ApplierReset`), `kCtxQuery`, `kCtxCso` (create/bind/delete × five kinds of CSO), `kCtxState` (`Set*` state, `SetContextValues`, `SetProgramBindings`, `SetResidualValueState` during migration), `kCtxObject` (subdata, readback, mip, `ObjectDeath` …), `kCtxVerb` (blit, clear, `DrawVbo`, dispatch, XFB, `Present` …).
|
||||
- Flags: `kNeedsAck`, `kHasBlob`, `kVarTail`, `kHostSpan`, `kReplySlot`, `kOptional`. The fifth column, `WaitClass` (P5e), decides whether the client waits under run-ahead (§11.6).
|
||||
- The 20 draw entry points collapse into the single `DrawVbo`; `Clear` is a single discriminative call; the sampler set **has no stage dimension** (a merged 192-unit space); `SetTextureParams` is resource-addressed and separate from sampler views (D10); `SetIndexBuffer` is independent of VAO configuration (D5).
|
||||
- Deliberately not ported: `GetIntegeri_v` / `GetInteger64i_v` / `GetProgramiv`, `set_pixel_unpack_state` (the frontend already resolves it), the compressed format concept, `pipe_transfer`.
|
||||
|
||||
#### 3.2.1 flag 与 WaitClass 的含义
|
||||
#### 3.2.1 Meaning of the flags and WaitClass
|
||||
|
||||
- `kHasBlob` 严格表示 payload 拥有一个 `MGPBlobRef` 成员,而不是"这条调用带字节":解码器信任它就必须有成员可读。`ResourceRespecify` 的初始内容与 `ResourceFlushRange` 的字节不走 blobref,而以相邻的 `resource_subdata` 记录过线。
|
||||
- `kNeedsAck` 只表示这条调用的记录**可能**要 ack,由逐记录谓词决定(`MGPipeResourceRespecifyNeedsAck`,§8.3)。
|
||||
- `kReplySlot`:applier 返回 client 会据以动作的答案(清 level 脏标、闩参数)。flag 只导出一份 `kMGPipeCallFlags[]`(`generated/PipeWire.inc`),消费者不另抄。
|
||||
- 第五列 `WaitClass` 是值不是 opcode,生成为 `MGPipeWaitClassFor(op)`,是 `MGPipeBarriered`(§11.6)的静态一项;client 只在 run-ahead 武装(server 发布 `kCapRunAheadApply`)时据它决定等不等,否则走 lockstep:
|
||||
- `kWaitReply`:答案不可推导且 client 要用,阻塞在 reply slot。与 `kReplySlot` 同在同缺,`gen_pipe.py` 拒绝不一致的行:不等的 reply 行会让 client 读一个没人填的 slot,没有 slot 的等待行会等一个没人发的答案。逐记录 route 谓词可再放宽(如 `MGPipeSubDataWantsItsReply`)。
|
||||
- `kWaitPresent`:节奏由 encode 之前取的 present credit 决定,publish 之后不再等。
|
||||
- `kWaitApplied`:client 停到记录 apply 完(今天只有 `applier_reset`)。
|
||||
- `kWaitNone`:apply 不读 client 内存,发布即走。
|
||||
- `kHasBlob` strictly means the payload owns an `MGPBlobRef` member, not "this call carries bytes": if the decoder trusts it, there must be a member to read. The initial contents of `ResourceRespecify` and the bytes of `ResourceFlushRange` do not go through a blobref; they cross the wire as an adjacent `resource_subdata` record.
|
||||
- `kNeedsAck` only means this call's record **may** need an ack, decided by a per-record predicate (`MGPipeResourceRespecifyNeedsAck`, §8.3).
|
||||
- `kReplySlot`: the applier returns an answer the client will act on (clearing level dirty bits, latching parameters). The flag exports only one `kMGPipeCallFlags[]` (`generated/PipeWire.inc`); consumers do not copy it.
|
||||
- The fifth column, `WaitClass`, is a value rather than an opcode, generated as `MGPipeWaitClassFor(op)`, one static entry of `MGPipeBarriered` (§11.6); the client uses it to decide whether to wait only when run-ahead is armed (the server publishes `kCapRunAheadApply`); otherwise it goes lockstep:
|
||||
- `kWaitReply`: the answer is not derivable and the client needs it, so block on the reply slot. It is present and absent together with `kReplySlot`, and `gen_pipe.py` rejects inconsistent lines: a reply line that does not wait would make the client read a slot nobody fills; a waiting line with no slot would wait for an answer nobody sends. A per-record route predicate can relax this further (e.g. `MGPipeSubDataWantsItsReply`).
|
||||
- `kWaitPresent`: the pace is decided by the present credit taken before encoding, and there is no waiting after publish.
|
||||
- `kWaitApplied`: the client stops until the record has been applied (today only `applier_reset`).
|
||||
- `kWaitNone`: apply does not read client memory, so publish and go.
|
||||
|
||||
### 3.3 能力位(`MGPCapBit`)
|
||||
### 3.3 Capability bits (`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 起发布,后者契约见 git 历史)。**不存在"归属开关"(D-B7)**:multi-draw 分档与 restart 重写永远由 server 拥有,client 只在 caps 要求时提供索引字节。
|
||||
`CallMask` replaces the implicit capability probe of "is the slot null"; `MGPCaps` = `DynamicBackendParameters` + `CallMask` + two blobs, the format capability table and the renderer string; snapshotted after the handshake, republished per generation after `MakeCurrent` / `InitCapabilities`. The bits include viewport array, fp64 vertices, `kCapResidentSubData`, the XFB / query family, `kCapNeedsHostIndexBytes`, `kCapNeedsHostUboBytes`, `kCapBackendOwnsXfbCapture`, `kCapRunAheadApply` (`MGPipeRunAheadCapBitsFor`: Espryt since P5e, and Magma run-ahead since P5f, whose contract is in the git history). **There is no "ownership switch" (D-B7)**: multi-draw tiering and restart rewriting always belong to the server, and the client supplies index bytes only when the caps ask for them.
|
||||
|
||||
## 4. 记录与 payload 约定(`MG_Pipe/MGPipeTypes.h`)
|
||||
## 4. Record and payload conventions (`MG_Pipe/MGPipeTypes.h`)
|
||||
|
||||
- 每个 payload 是平坦 POD、显式 padding、`static_assert` 精确尺寸;**永不含指针**。
|
||||
- `MGPBlobRef{Offset, Size, Seg}` 指向 blob 区;`MGHostSpan` 是唯一随传输而变的形状(monolith 下是指针,split 下字节在 `SEG_STAGE`),只进变长尾。
|
||||
- wire 记录头 `MGPWireRecHeader{Op, Flags, Size}`(8 B);**没有逐记录序号**,seq 就是记录序数。
|
||||
- 单条记录上界 = ring 一半,**记录本身永不分块**(R-10);**内容侧分块**:会超出 `SEG_STAGE` 的 blob 按 `MGPipeStageChunkBytes()`(默认 segment/4 = 8 MiB)切成多条记录(buffer 范围走查、纹理整宽 slab,server 拼回整级)。
|
||||
- Each payload is a flat POD with explicit padding and a `static_assert` on the exact size; **it never contains pointers**.
|
||||
- `MGPBlobRef{Offset, Size, Seg}` points into the blob area; `MGHostSpan` is the only shape that changes with the transport (a pointer under monolith, bytes in `SEG_STAGE` under split), and it only goes into the variable-length tail.
|
||||
- The wire record header `MGPWireRecHeader{Op, Flags, Size}` (8 B); **there is no per-record sequence number**, the seq is the record ordinal.
|
||||
- The upper bound of a single record is half the ring, and **a record itself is never chunked** (R-10); **content is chunked**: a blob that would exceed `SEG_STAGE` is split by `MGPipeStageChunkBytes()` (default segment/4 = 8 MiB) into several records (buffer range walks, full-width texture slabs; the server reassembles the whole level).
|
||||
|
||||
### 4.1 关键 payload
|
||||
### 4.1 Key payloads
|
||||
|
||||
| payload | 要点 |
|
||||
| payload | Key points |
|
||||
|---|---|
|
||||
| `MGPResourceDesc` | buffer / 纹理 / renderbuffer 一个判别式 create/respecify 形状;`ImageBindableHint`、`ViewOf`(视图的存储属主)、`HasDefinedContent` |
|
||||
| `MGPResourceDesc` | one discriminative create/respecify shape for buffer / texture / renderbuffer; `ImageBindableHint`, `ViewOf` (the storage owner of a view), `HasDefinedContent` |
|
||||
| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | §5.3 |
|
||||
| `MGPVertexElements` | blob 同时带解析后的属性与绑定点视图(记录自洽) |
|
||||
| `MGPSamplerView` / `MGPTextureParams` | view 只带视图限制;纹理参数挂纹理对象,`BuiltinSampler` 句柄不许为空(D-E1) |
|
||||
| `MGPProgramDesc` | 逐 stage SPIR-V + 反射归档(§7) |
|
||||
| `MGPFramebufferState` | 8 color + depth + stencil + client 解析后的 read surface;`ContentHash` 同时是 render-pass memo 键与发射抑制器;`Target` 含 `Named = 3`,applier 按 framebuffer 句柄存 |
|
||||
| `MGPVertexElements` | the blob carries both the parsed attributes and the binding-point views (the record is self-consistent) |
|
||||
| `MGPSamplerView` / `MGPTextureParams` | a view carries only view restrictions; texture parameters hang off the texture object, and the `BuiltinSampler` handle must not be null (D-E1) |
|
||||
| `MGPProgramDesc` | per-stage SPIR-V + reflection archive (§7) |
|
||||
| `MGPFramebufferState` | 8 color + depth + stencil + the client-resolved read surface; `ContentHash` is both the render-pass memo key and the emission suppressor; `Target` includes `Named = 3`, and the applier stores by framebuffer handle |
|
||||
| `MGPSubData` / `MGPSubRegion` | §6 |
|
||||
| `MGPDrawInfo` / `MGPDrawRange` | `DrawVbo`;multi-draw 范围与用户索引在变长尾,indirect 的 `DrawCount` 由 client 解析 |
|
||||
| `MGPDrawInfo` / `MGPDrawRange` | `DrawVbo`; multi-draw ranges and user indices in the variable-length tail, indirect's `DrawCount` resolved by the client |
|
||||
|
||||
每条 `kVarTail` 的 `set_*` 带 `ContentHash`,hash 未变就不发(§5.4)。
|
||||
Every `kVarTail` `set_*` carries a `ContentHash`; if the hash is unchanged, nothing is sent (§5.4).
|
||||
|
||||
### 4.2 split 下的诚实规则
|
||||
### 4.2 Honesty rules under split
|
||||
|
||||
同一地址空间里 host 指针能解引用、以 host 地址为 `Offset` 的 blobref 能解析、persistent map 返回的地址能直接用;`inproc` 若借这些捷径变绿,`spawn` 无法复现。所以 split 下编解码一律按跨进程规则执行,`inproc` 也不例外(`MG_Remote/Wire/PipeWireCodec.h`):
|
||||
In one address space a host pointer can be dereferenced, a blobref with a host address as `Offset` can be resolved, and an address returned by persistent map can be used directly; if `inproc` turns green by borrowing these shortcuts, `spawn` cannot reproduce it. So under split, encoding and decoding always follow cross-process rules, `inproc` included (`MG_Remote/Wire/PipeWireCodec.h`):
|
||||
|
||||
1. encoder 把 `MGHostSpan::Ptr` 恒写 `nullptr`,字节放进 `SEG_STAGE`,由 `Seg` / `Offset` 指名。
|
||||
2. `MGPBlobRef` 要么完整声明(真实 `Seg`、段内 `Offset`、非零 `Size`),要么三个字段全零。`Seg` 或 `Offset` 非零而 `Size == 0` 正是 monolith emitter 的形状(`Offset` 是 host 地址),split 下按损坏拒绝,不能读成"无 blob",否则未改造 emitter 的字节会被静默丢弃。
|
||||
3. decoder 对 `Ptr != nullptr`、内容记录的 `Size == 0`、`Size != 0` 而 `Seg == kSegNone`、`Offset + Size` 越过段尾一律报 `ProtocolCorruption`(`CheckBlobIsHonest`、`RequireDeclaredBlob`、`CheckHostSpanIsHonest`)。记录是否带内容由 payload 的其他字段决定,所以"内容记录须有 blob"由 decoder 的逐 op 分支调用 `RequireDeclaredBlob` 检查。
|
||||
4. T2 档下 `MGPipeApplyMapPersistent` 在 split 下恒返回 `nullptr`(§12),client 不拿 server 的地址。
|
||||
5. `MOBILEGL_IPC_AUDIT=1` 时 server 把已退休记录的 `SEG_STAGE` 字节填成 `0xDD`,apply 之后仍持有指针的实现下一帧会读到 `0xDD`。
|
||||
1. The encoder always writes `nullptr` for `MGHostSpan::Ptr`, puts the bytes in `SEG_STAGE`, and names them by `Seg` / `Offset`.
|
||||
2. `MGPBlobRef` either fully declares itself (real `Seg`, an in-segment `Offset`, nonzero `Size`), or all three fields are zero. `Seg` or `Offset` nonzero with `Size == 0` is exactly the shape of a monolith emitter (`Offset` is a host address); under split it is rejected as corruption and must not be read as "no blob", otherwise the bytes of an unmigrated emitter would be silently dropped.
|
||||
3. The decoder reports `ProtocolCorruption` for `Ptr != nullptr`, for `Size == 0` on a content record, for `Size != 0` with `Seg == kSegNone`, and for `Offset + Size` running past the end of the segment (`CheckBlobIsHonest`, `RequireDeclaredBlob`, `CheckHostSpanIsHonest`). Whether a record carries content is decided by other fields of the payload, so "a content record must have a blob" is checked by each per-op branch of the decoder calling `RequireDeclaredBlob`.
|
||||
4. In the T2 tier, `MGPipeApplyMapPersistent` always returns `nullptr` under split (§12); the client does not take the server's address.
|
||||
5. With `MOBILEGL_IPC_AUDIT=1` the server fills the `SEG_STAGE` bytes of retired records with `0xDD`; an implementation that still holds a pointer after apply reads `0xDD` on the next frame.
|
||||
|
||||
@@ -1,121 +1,121 @@
|
||||
# 前端:state tracker、纹理上传、着色器(原 ARCHITECTURE §5–§7)
|
||||
# Frontend: state tracker, texture upload, shaders (formerly ARCHITECTURE §5–§7)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design highlights and an index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; `ARCHITECTURE.md §N` in code comments resolves by number here.
|
||||
|
||||
## 5. 前端 state tracker(`MG_Impl/Pipe/Tracker`)
|
||||
## 5. Frontend state tracker (`MG_Impl/Pipe/Tracker`)
|
||||
|
||||
### 5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里
|
||||
### 5.1 Pushing happens at validate time before a verb, not in GL setters
|
||||
|
||||
per-setter 推送会把 Blaze3D 每个 batch 的冗余 `glEnable/glDisable` 变成接口调用,严格慢于今天;正确形态是 gallium `st_validate_state`。落地为唯一的 `MGPipeValidateForVerb(MGPipeVerb)`(九个 validate 类,`FillPoints.def`),恰在每次经表调用之前、每个提前返回之后。只有今天就在 GL 调用时刻分发的资源 op(`BufferBackendOps` 的七个 hook)在调用时刻推送。
|
||||
Per-setter pushing would turn Blaze3D's redundant `glEnable/glDisable` in every batch into interface calls, strictly slower than today; the correct form is gallium's `st_validate_state`. It lands as the single `MGPipeValidateForVerb(MGPipeVerb)` (nine validate classes, `FillPoints.def`), exactly before every call through the tables and after every early return. Only the resource ops that are already dispatched at GL call time today (the seven hooks of `BufferBackendOps`) push at call time.
|
||||
|
||||
### 5.2 dirty 位:值类零新增记账,对象类 5 个聚合世代
|
||||
### 5.2 Dirty bits: zero new bookkeeping for value classes, five aggregate generations for object classes
|
||||
|
||||
- 值类组(render state、pixel pack、patch、attrib defaults、shader bindings …)直接复用既有版本计数器作快门。
|
||||
- 对象类组(vertex buffers、framebuffer、sampler views / samplers / images、const / shader buffers、SO targets)靠 5 个**聚合世代**把快门降成一次 `Uint64` 比较——没有聚合就回答不了"有没有哪张已绑定纹理动了"。
|
||||
- 完整性由 `scripts/gen_pipe_dirty_surface.py` 保证(每个 mutator → 必须 bump 的世代,`--check` + `--self-test` 进 CI)。**快门看不见自己的主体**是踩过三次的坑:新增字段必须带"记录字段 → setter → 快门"一行,优先混入已有世代(新计数器会撑大 pull 对象,G1 不允许)。
|
||||
- Value-class groups (render state, pixel pack, patch, attrib defaults, shader bindings …) reuse the existing version counters directly as the shutter.
|
||||
- Object-class groups (vertex buffers, framebuffer, sampler views / samplers / images, const / shader buffers, SO targets) reduce the shutter to a single `Uint64` comparison through five **aggregate generations** — without aggregation there is no way to answer "did any bound texture move".
|
||||
- Completeness is guaranteed by `scripts/gen_pipe_dirty_surface.py` (every mutator → the generation that must be bumped, `--check` + `--self-test` in CI). **A shutter that cannot see its own subject** is a trap we have hit three times: a new field must come with a "record field → setter → shutter" line, and mixing it into an existing generation is preferred (a new counter would bloat the pull object, which G1 does not allow).
|
||||
|
||||
#### 5.2.1 dirty-surface 映射的答案(`DirtySurface.def`)
|
||||
#### 5.2.1 Answers of the dirty-surface mapping (`DirtySurface.def`)
|
||||
|
||||
`DirtySurface.def` 为每个后端可观察的前端 mutator 写明"谁发布这次修改"。漏发布不报错、只会静默渲染陈旧状态,所以清单由 `scripts/gen_pipe_dirty_surface.py` 机械枚举:扫描 `MG_Impl/GLImpl` 与 `MG_State/GLState` 里按 `MUTATOR_PREFIXES` 匹配的 `pGLContext->` 调用和 `MGP_NOTE_MUTATION` 站点(后者带字段名,行写在外层函数上),`--check` 双向核对行集。
|
||||
`DirtySurface.def` states, for every frontend mutator observable by the backend, "who publishes this change". A missing publication does not error, it only silently renders stale state, so the list is mechanically enumerated by `scripts/gen_pipe_dirty_surface.py`: it scans `MG_Impl/GLImpl` and `MG_State/GLState` for `pGLContext->` calls matching `MUTATOR_PREFIXES` and for `MGP_NOTE_MUTATION` sites (the latter carry a field name, and the line is written on the enclosing function); `--check` cross-checks the line set in both directions.
|
||||
|
||||
答案只列在**每条发生修改的路径**上都触发的发布者(值未变而提前返回不算修改);只在部分路径触发的位若单列,按表构造的快门会欠触发,而欠触发是危险方向。
|
||||
- `NEW_*`:`Tracker.h` 的 dirty 位,下一个 verb 发布;`kImmediate`:同一函数体直达后端;`kReverseChannel`:后端写回前端;`kNoBackendRead`:没有后端读点。
|
||||
- `kExplicitDestroy`:最后一个引用释放时,client 的 `MGPipeEmit*DestroyAndFree` 依次做 wire 删除(该类有删除行时)、后端通知、释放 slot;`kUnpublishedDestroy`:program pipeline 与 shader 没有 wire 句柄,无从删除。
|
||||
- `kPulledEveryVerb`:没有位移动,字段在其 verb 类的 may-read 表里,残余填充每个 verb 都拷贝;`kPulledPartialShutter|NEW_*`:同样靠拉取兜底,所附的位只在部分修改路径上触发。
|
||||
The answers list only publishers that fire on **every path that makes a modification** (an early return with an unchanged value does not count as a modification); a bit that fires only on some paths, if listed separately, would make the table-constructed shutter under-trigger, and under-triggering is the dangerous direction.
|
||||
- `NEW_*`: a dirty bit in `Tracker.h`, published at the next verb; `kImmediate`: straight to the backend from the same function body; `kReverseChannel`: the backend writes back to the frontend; `kNoBackendRead`: there is no backend read site.
|
||||
- `kExplicitDestroy`: when the last reference is released, the client's `MGPipeEmit*DestroyAndFree` in turn does the wire delete (when the class has a delete line), the backend notification, and frees the slot; `kUnpublishedDestroy`: program pipelines and shaders have no wire handle, so there is nothing to delete.
|
||||
- `kPulledEveryVerb`: no bit movement; the field is in the may-read table of its verb class, and the residual fill copies it at every verb; `kPulledPartialShutter|NEW_*`: likewise backed by the pull, with the attached bit firing only on some modification paths.
|
||||
|
||||
位答案由脚本推导:RenderState 族与 `RenderState.cpp` 双向比对(`BumpVersions()` 动两个计数器,裸 `++m_version` 只动 `NEW_RENDER_STATE`);其余位把快门读的成员 / 字段与 mutator 传递写入的成员 / 字段求交,只能查出"该位的快门在任何路径上都不动"这一种错答,证明不了每条路径都动。写入根定位不了的函数被污染,依赖它的答案为 UNDECIDED,须列入 `MGP_DIRTY_SURFACE_UNDECIDED_LIST`,列了却已可判定同样报红。散文答案不经推导(`kPulledPartialShutter` 所附的位除外);扫描器按文本匹配函数体,lambda 里的 mutator 记到外层函数上,经 helper 发布的修改读作延迟发布,语义证明仍是 `MOBILEGL_PIPE_VERIFY`。
|
||||
Bit answers are derived by the script: the RenderState family is compared both ways with `RenderState.cpp` (`BumpVersions()` moves two counters, a bare `++m_version` moves only `NEW_RENDER_STATE`); for the remaining bits, the members / fields the shutter reads are intersected with those the mutator transitively writes, which can only detect the one wrong answer "this bit's shutter never moves on any path", and cannot prove that every path moves. Functions whose write root cannot be located are tainted, and answers that depend on them are UNDECIDED and must be listed in `MGP_DIRTY_SURFACE_UNDECIDED_LIST`; listing one that is already decidable also turns red. Prose answers are not derived (except the bits attached to `kPulledPartialShutter`); the scanner matches function bodies by text, mutators inside lambdas are attributed to the enclosing function, and a change published through a helper is read as a deferred publication; the semantic proof is still `MOBILEGL_PIPE_VERIFY`.
|
||||
|
||||
#### 5.2.2 记录字段 → setter → 快门对照表
|
||||
#### 5.2.2 Record field → setter → shutter table
|
||||
|
||||
由 `Tracker.h` 的 dirty 位触发的每条记录,其每个字段只能经由会推动该位快门输入的 setter 改变,否则必须在 setter 处无条件发射(`resource_*` 族、`set_texture_params`)。`DirtySurface.def` 只把 mutator 映射到位,看不出派生字段依赖另一族的 mutator,所以下表手工维护;记录新增字段时在此加一行。
|
||||
Every field of a record triggered by a dirty bit in `Tracker.h` may only change through a setter that moves that bit's shutter input; otherwise it must be emitted unconditionally at the setter (the `resource_*` family, `set_texture_params`). `DirtySurface.def` only maps mutators to bits and cannot see that a derived field depends on a mutator of another family, so the table below is maintained by hand; when a record gains a field, add a row here.
|
||||
|
||||
| 位 | 记录与字段 | 改变字段的 setter | 快门输入 |
|
||||
| Bit | Record and fields | Setters that change the fields | Shutter inputs |
|
||||
|---|---|---|---|
|
||||
| 6 | `create/bind_shader_state`、`set_draw/dispatch_program`:Cso、StageMask、GlobalUboSize、artefact blob ref、两个已绑定句柄 | `glUseProgram`、`glLinkProgram`、`glBindProgramPipeline`、`glUseProgramStages` | lifetime id × link version;pipeline 下为 `stageLinks`(pipeline 名 × 各 stage 的 lifetime id 与 link version) |
|
||||
| 7 | program 绑定:image unit、block binding、uniform 写集合 | opaque uniform 上的 `glUniform1i`、`glUniformBlockBinding`、`glShaderStorageBlockBinding`、任意 `glUniform*` | 四个 per-program 计数器;pipeline 下再混入 `stageLinks` |
|
||||
| 8 | `set_global_constants`:ShaderCso、Version、默认 uniform 块镜像 | 默认块上的 `glUniform*`、`glUseProgram` | lifetime id × UBO content version;pipeline 下为 `stageState` |
|
||||
| 11 | `set_framebuffer_state`:Fbo、Color[8]/Depth/Stencil/ReadSurface(Res、Kind、InternalFormat、TextureTarget、Layered、Level、Layer、UploadTarget)、DrawBuffers[8]、Width/Height/Layers/Samples/FixedSampleLocations、IsDefault、Complete、Target | `glFramebufferTexture*`、`glFramebufferRenderbuffer`、`glDrawBuffer(s)`、`glReadBuffer`、`glFramebufferParameteri`、`glBindFramebuffer`;已挂接纹理或 renderbuffer 的存储重定义(格式、纹理目标、尺寸、采样数、完备性在发射时内联,所以 `PipePublishDescriptor` bump attachment 聚合) | attachment 聚合 × draw / read 绑定槽版本 |
|
||||
| 12 | `set_sampler_views`:每单元 {View, Texture} | `glBindTexture`、`glActiveTexture`、纹理或 sampler 参数、定义 level 的上传、默认纹理出现;在用 program(`glUseProgram`、relink、sampler uniform 的 `glUniform1i`) | content × params × bind generation × `opaqueUnits` |
|
||||
| 13 | `bind_sampler_states`:每单元 sampler CSO 句柄 | `glBindSampler`、`glDeleteSamplers`、`glSamplerParameter*`、`glTexParameter*` | params × sampling resolution × bind generation |
|
||||
| 14 | `set_shader_images`:program image-unit 窗口内每单元 {Res, InternalFormat, Layer, Level, Layered, Access} | `glBindImageTexture`、纹理 content / params、image uniform 的 `glUniform1i`;在用 program(`glUseProgram`、relink) | content × params × bind generation × `programImages` |
|
||||
| 6 | `create/bind_shader_state`, `set_draw/dispatch_program`: Cso, StageMask, GlobalUboSize, artifact blob ref, the two bound handles | `glUseProgram`, `glLinkProgram`, `glBindProgramPipeline`, `glUseProgramStages` | lifetime id × link version; under a pipeline, `stageLinks` (pipeline name × each stage's lifetime id and link version) |
|
||||
| 7 | program bindings: image units, block bindings, uniform write set | `glUniform1i` on opaque uniforms, `glUniformBlockBinding`, `glShaderStorageBlockBinding`, any `glUniform*` | the four per-program counters; under a pipeline, `stageLinks` is mixed in as well |
|
||||
| 8 | `set_global_constants`: ShaderCso, Version, default uniform block mirror | `glUniform*` on the default block, `glUseProgram` | lifetime id × UBO content version; under a pipeline, `stageState` |
|
||||
| 11 | `set_framebuffer_state`: Fbo, Color[8]/Depth/Stencil/ReadSurface (Res, Kind, InternalFormat, TextureTarget, Layered, Level, Layer, UploadTarget), DrawBuffers[8], Width/Height/Layers/Samples/FixedSampleLocations, IsDefault, Complete, Target | `glFramebufferTexture*`, `glFramebufferRenderbuffer`, `glDrawBuffer(s)`, `glReadBuffer`, `glFramebufferParameteri`, `glBindFramebuffer`; storage redefinition of an attached texture or renderbuffer (format, texture target, size, sample count, completeness are inlined at emission, so `PipePublishDescriptor` bumps the attachment aggregate) | attachment aggregate × draw / read binding slot version |
|
||||
| 12 | `set_sampler_views`: per unit {View, Texture} | `glBindTexture`, `glActiveTexture`, texture or sampler parameters, uploads defining a level, appearance of the default texture; the program in use (`glUseProgram`, relink, `glUniform1i` of a sampler uniform) | content × params × bind generation × `opaqueUnits` |
|
||||
| 13 | `bind_sampler_states`: per unit, the sampler CSO handle | `glBindSampler`, `glDeleteSamplers`, `glSamplerParameter*`, `glTexParameter*` | params × sampling resolution × bind generation |
|
||||
| 14 | `set_shader_images`: per unit within the program's image-unit window, {Res, InternalFormat, Layer, Level, Layered, Access} | `glBindImageTexture`, texture content / params, `glUniform1i` of an image uniform; the program in use (`glUseProgram`, relink) | content × params × bind generation × `programImages` |
|
||||
|
||||
位 11–14 还在 share group 的孪生聚合时钟变化时触发(setter 经兄弟 context 调用);最大触及纹理单元变化时 12、13 也触发。
|
||||
Bits 11–14 also fire when the share group's twin aggregate clock changes (the setter is invoked through a sibling context); when the maximum touched texture unit changes, 12 and 13 also fire.
|
||||
|
||||
### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集(D-B1)
|
||||
### 5.3 Render state: one whole blob crosses the wire, identity takes only the pipeline subset (D-B1)
|
||||
|
||||
```
|
||||
create_render_state(cso, pipelineSubsetChunks) // 只带 pipeline 子集
|
||||
bind_render_state(cso, version, pipelineVersion) // 稳态 12 B
|
||||
set_dynamic_state(dynamicChunks, version) // 只带变化的动态 chunk
|
||||
create_render_state(cso, pipelineSubsetChunks) // pipeline subset only
|
||||
bind_render_state(cso, version, pipelineVersion) // 12 B in steady state
|
||||
set_dynamic_state(dynamicChunks, version) // only the changed dynamic chunks
|
||||
```
|
||||
|
||||
- `RenderStateParameters`(1168 B)整块过线:Espryt 做三段 memcmp,字段顺序承重;拆成多个 CSO 要手工维护 ~150 字段划分且无绊线。
|
||||
- 身份只取 pipeline 子集,否则 `glViewport` / `glScissor` 每次都铸新 CSO、冲掉 server 的 pipeline memo。划分只写在一处:`MGPipeRenderStateSpans` 的 chunk 表(7 pipeline chunk 396 B + 8 dynamic chunk 772 B,`static_assert` 完整性),G7 断言"子集 hash 变 ⟺ `m_pipelineStateVersion` 变"。
|
||||
- Espryt 的 `SyncRenderState` 一行不动;Magma 的 pipeline memo 键是 `cso.slot`。
|
||||
- `RenderStateParameters` (1168 B) crosses as one whole block: Espryt does a three-segment memcmp, and field order is load-bearing; splitting it into several CSOs would require hand-maintaining a ~150-field partition with no tripwire.
|
||||
- Identity takes only the pipeline subset, otherwise `glViewport` / `glScissor` would mint a new CSO every time and flush the server's pipeline memo. The partition is written in one place only: the chunk table in `MGPipeRenderStateSpans` (7 pipeline chunks 396 B + 8 dynamic chunks 772 B, `static_assert` completeness), and G7 asserts "the subset hash changes ⟺ `m_pipelineStateVersion` changes".
|
||||
- Espryt's `SyncRenderState` is untouched; Magma's pipeline memo key is `cso.slot`.
|
||||
|
||||
### 5.4 验证不变式、合并与抑制器
|
||||
### 5.4 Validation invariants, merging and suppressors
|
||||
|
||||
规范(D-B3):**一条 verb 的全部 `set_*` / `bind_*` 在该 verb 之前完成;server 在 verb 处从已推送状态惰性特化 shader 与 pipeline。**除"资源 create 先于对它的 bind"外,`set_*` 之间无顺序要求。合并:整块优于逐字段;高水位直接是 `count` 实参;只发 program 解析过的集合;**集合 hash 抑制器**在 client 侧去抖(原先在后端的 ~175 行)。
|
||||
The rule (D-B3): **all `set_*` / `bind_*` of a verb complete before that verb; the server lazily specializes shaders and pipelines at the verb from the pushed state.** Apart from "a resource is created before it is bound", there is no ordering requirement among `set_*`. Merging: whole blocks beat per-field; the high-water mark is directly the `count` argument; only the sets the program resolved are sent; the **set hash suppressor** debounces on the client side (previously ~175 lines in the backend).
|
||||
|
||||
### 5.5 sampler view 在 client 侧解析
|
||||
### 5.5 Sampler views are resolved on the client
|
||||
|
||||
shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与未定义默认纹理;解析留在 client(带 memo),gallium "每槽一个 view" 就是解析后的形态。两处后端特定后处理留在 server:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。
|
||||
Which texture a shader sees depends on the sampler uniform type, mipmap completeness and the undefined default texture; resolution stays on the client (with a memo), and gallium's "one view per slot" is the resolved form. Two backend-specific post-processing steps stay on the server: Espryt's raw-depth-fetch sampler substitution and Magma's feedback-loop detection.
|
||||
|
||||
### 5.6 生命周期、共享组、composite program
|
||||
### 5.6 Lifetime, share groups, composite programs
|
||||
|
||||
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义,`resource_destroy` 在析构时发;死亡转发给每个 emitter(只释放 slot 曾导致 UAF)。view 先于存储属主销毁、FBO attachment 钉住纹理、buffer texture 钉住 buffer。
|
||||
- program pipeline 合成体(`CompositeResolver`)完全在前端合成、推一个句柄;释放恰好一次(resolver 换掉或 `~ProgramObject`,D-H7);记忆按 `(ContextId, 管线 GL 名)` 键控。
|
||||
- `resource_create` is sent when the frontend object is constructed, storage is lazily defined by `resource_respecify`, and `resource_destroy` is sent at destruction; the death is forwarded to every emitter (freeing only the slot previously caused a UAF). A view is destroyed before its storage owner, an FBO attachment pins its texture, and a buffer texture pins its buffer.
|
||||
- The program pipeline composite (`CompositeResolver`) is fully composed on the frontend and pushes a single handle; it is released exactly once (the resolver swaps it out, or `~ProgramObject`, D-H7); the memo is keyed by `(ContextId, pipeline GL name)`.
|
||||
|
||||
#### 5.6.1 program pipeline 合成体的句柄、释放与记忆键
|
||||
#### 5.6.1 Handle, release and memo key of the program pipeline composite
|
||||
|
||||
- 合成体由 `GLContext::GetProgramForDraw()` 在前端合成、缓存在各自的 `ProgramPipelineObject` 里;program 发射器按合成体自己的 lifetime id 从 `ShaderCso` 保留高段(§2.1)给它一个句柄,照普通 program 发 `create_shader_state`,server 不知道它是合成体。`MGPipeSlotAllocator::Allocate` 拒绝该段,`AllocateComposite` 是唯一入口;用保留段而不用句柄 flag,是因为 flag 要被两侧每个 `ShaderCso` 消费者携带和屏蔽,而保留段让"普通 program 拿不到合成体 slot"由分配器保证。
|
||||
- slot 有两条释放路径:签名变化时 `MGPipeCompositeResolver::Observe` 释放旧合成体,以及合成体自己的 `~ProgramObject`。两条都走 `MGPipeEmitShaderCsoDestroyAndFree`,后到的一条是空操作(`MGPipeSlotAllocator::Free` 拒绝不在该世代 live 的 slot,也不推进世代)。只靠析构不够:前端一旦多持一份引用,签名变化时析构就不会先到。
|
||||
- 签名取 `ComputeDrawProgramSignature()`(每个图形 stage 的 lifetime id 与 link version),不含 `GetBackendStateVersion()`:后者在 sampler uniform 的 `glUniform1i` 时也会变,会让合成体每次 draw 都重新编译。`Tracker.h` 的位 6 快门读同一组值。
|
||||
- 记忆键是 `(GLContext::GetTextureContextId(), 管线 GL 名)`:resolver 是进程单例,而 pipeline 名按 context 分配,只按名键控会在 make-current 后命中另一 context 的同名项、释放一个仍活着的合成体,每次切换都重建 program;context 地址也不行,销毁重建的 context 会落在旧地址上,而这个 id 永不复用。
|
||||
- context 销毁时其合成体经 `~ProgramObject` 释放,残留项再也查不到;make-current 时的 `Reset()` 丢弃它们而不释放,表长因此只受合成体 slot 仍 live 的项限制。
|
||||
- The composite is composed on the frontend by `GLContext::GetProgramForDraw()` and cached in its own `ProgramPipelineObject`; the program emitter reserves the high segment of `ShaderCso` by the composite's own lifetime id (§2.1) and gives it a handle, sending `create_shader_state` like an ordinary program, and the server does not know it is a composite. `MGPipeSlotAllocator::Allocate` rejects that segment and `AllocateComposite` is the only entry; the reserved segment is used instead of a handle flag because a flag would have to be carried and masked by every `ShaderCso` consumer on both sides, whereas the reserved segment makes "an ordinary program cannot get a composite slot" guaranteed by the allocator.
|
||||
- A slot has two release paths: `MGPipeCompositeResolver::Observe` releases the old composite when the signature changes, and the composite's own `~ProgramObject`. Both go through `MGPipeEmitShaderCsoDestroyAndFree`, and the later one is a no-op (`MGPipeSlotAllocator::Free` rejects a slot not live in that generation and does not advance the generation either). Relying on the destructor alone is not enough: once the frontend holds an extra reference, the destructor will not come first when the signature changes.
|
||||
- The signature is `ComputeDrawProgramSignature()` (each graphics stage's lifetime id and link version), excluding `GetBackendStateVersion()`: the latter also changes on `glUniform1i` of a sampler uniform, which would make the composite recompile on every draw. Bit 6's shutter in `Tracker.h` reads the same set of values.
|
||||
- The memo key is `(GLContext::GetTextureContextId(), pipeline GL name)`: the resolver is a process singleton, while pipeline names are allocated per context; keying by name alone would hit the same-named entry of another context after make-current and release a composite that is still alive, rebuilding the program on every switch; the context address is also wrong, because a destroyed and recreated context can land at the old address, whereas this id is never reused.
|
||||
- When a context is destroyed, its composite is released through `~ProgramObject`, and leftover entries can never be found again; `Reset()` at make-current discards them without releasing, so the table length is bounded only by the entries whose composite slots are still live.
|
||||
|
||||
### 5.7 emulation 的归属
|
||||
### 5.7 Ownership of emulation
|
||||
|
||||
规则:**驱动表达不了的变换在 tracker 里 lowering,硬件 / 驱动强加的变换在 driver 里 lowering。**
|
||||
Rule: **transformations the driver cannot express are lowered in the tracker; transformations forced by the hardware / driver are lowered in the driver.**
|
||||
|
||||
| 归 client(读前端字节的纯 CPU 变换) | 归 server |
|
||||
| Falls to the client (pure CPU transformations that read frontend bytes) | Falls to the server |
|
||||
|---|---|
|
||||
| client 顶点 / 索引数组(字节过线,永不是指针)、最大索引扫描、`*IndirectCount` 计数解析、CopyImage shadow 镜像、XFB CPU 图元计数、压缩纹理 / unpack 规整 | primitive-restart 重写、multi-draw 分档与展平(从索引宿主镜像读,§10.3)、viewport-array 回放、fp64 顶点窄化、image-bindable 存储加宽、生成 mipmap(client 分配 level 存储) |
|
||||
| client vertex / index arrays (bytes cross the wire, never pointers), maximum index scan, `*IndirectCount` count resolution, CopyImage shadow mirror, XFB CPU primitive count, compressed texture / unpack normalization | primitive-restart rewriting, multi-draw tiering and flattening (read from the index host mirror, §10.3), viewport-array replay, fp64 vertex narrowing, image-bindable storage widening, mipmap generation (the client allocates level storage) |
|
||||
|
||||
**陈旧索引纪律是逐站点表**:client 侧扫描前的 reconcile 必须逐字复现 monolith 的集合(最大索引扫描 = `SyncPersistentMappedRange()` + `SyncGpuWrites()`;`*IndirectCount` 只前者)。
|
||||
**Stale index discipline is a per-site table**: the reconcile before a client-side scan must reproduce monolith's set verbatim (maximum index scan = `SyncPersistentMappedRange()` + `SyncGpuWrites()`; `*IndirectCount` only the former).
|
||||
|
||||
## 6. 纹理 subdata 与 dirty 归属
|
||||
## 6. Texture subdata and dirty ownership
|
||||
|
||||
- 纹理上传由 Espryt 在 sync 时刻按**累积**区域做(Mali 按作业数计价,~100 个 rect 对一个 union box 实测 +6 ms/帧),逐 `glTexSubImage` 发记录会复现这个悬崖。
|
||||
- 因此 client 在自己的 rect 模型里累积,在下一个 validate / flush 点发**一条** `ResourceSubData`,同时携带 union box 与 region 列表,**由 server 选上传形状**(决策留在付 GPU 代价的一侧)。步长显式携带,不再靠指针比较。
|
||||
- **dirty 归属反转**:client 维护发射游标、发射后清自己的标志;清 dirty 要等 applier 的 acceptance(被拒的 level 留在脏表里);server 自己的重新变脏是 server 的事。
|
||||
- `TextureUploadShapeScenario` 把上传形状录成金标(P3b/P4b D2 起是门;Mali 帧时增量无设备,按 Adreno 记录)。
|
||||
- Texture uploads are done by Espryt at sync time over the **accumulated** region (Mali prices by job count; ~100 rects against one union box measured +6 ms/frame), so sending a record per `glTexSubImage` would reproduce this cliff.
|
||||
- So the client accumulates in its own rect model and sends **one** `ResourceSubData` at the next validate / flush point, carrying both the union box and the region list, and **the server picks the upload shape** (the decision stays on the side that pays the GPU cost). The stride is carried explicitly, no longer relying on pointer comparison.
|
||||
- **Dirty ownership is reversed**: the client maintains the emission cursor and clears its own flags after emission; clearing dirty waits for the applier's acceptance (a rejected level stays in the dirty table); the server's own re-dirtying is the server's business.
|
||||
- `TextureUploadShapeScenario` records the upload shape as golden (a gate since P3b/P4b D2; Mali frame-time deltas have no device here, recorded on Adreno).
|
||||
|
||||
### 6.1 server 侧待上传集合
|
||||
### 6.1 Server-side pending upload set
|
||||
|
||||
- client 在 applier 接受某 level 的 `ResourceSubData` 时就清掉自己的 dirty 标志;后端的上传循环有提前返回的臂(Espryt:纹理不完备、multisample 目标只刷新),所以已接受的形状由 applier 按 `(uploadTarget, level)` 累积在 `MGPipeResourceRecord::PendingUploads`,只在后端真正上传处消费并清除,任意次 bail 都不丢纹素。
|
||||
- 累积沿用 client 的 rect 模型:union box 取并、rect 列表拼接;任一侧只有 box,或列表将超过 `kMGPipeMaxPendingUploadRegions`,该项退化为只有 box(仍覆盖全部纹素,只多传空隙)。
|
||||
- respecify 只丢弃它替换掉的那份存储上的待上传(盒子在那份存储的坐标系里,跨缩小保留会越界上传):
|
||||
- level 为空 = 整资源重定义(`glBufferData` / `glBufferStorage`、`glTexStorage*`、纹理视图),全部清空;
|
||||
- 具名 level = 一次 `glTexImage*D`,只丢该 `(uploadTarget, level)`,与描述符是否变化无关。整体清空会丢掉别的 level 已被接受、尚未上传的纹素:`glTexImage2D(0)` → draw(纹理不完备,后端 bail)→ `glTexImage2D(1)`;
|
||||
- 只换元数据(`RespecifyRedefinesNoStorage`:sticky 的 `BindMask` / `ImageBindableHint` 变化时重发的描述符)不丢任何项。
|
||||
- buffer 没有待上传项,也永远不算只换元数据。
|
||||
- The client clears its dirty flag for a level as soon as the applier accepts that level's `ResourceSubData`; the backend's upload loop has early-return arms (Espryt: incomplete texture, multisample target only refreshes), so accepted shapes are accumulated by the applier per `(uploadTarget, level)` in `MGPipeResourceRecord::PendingUploads`, consumed and cleared only where the backend actually uploads, and no number of bails loses texels.
|
||||
- Accumulation reuses the client's rect model: the union box is taken as a union, rect lists are concatenated; if either side has only a box, or the list would exceed `kMGPipeMaxPendingUploadRegions`, that entry degrades to a box-only entry (still covering every texel, just transferring extra gaps).
|
||||
- respecify discards only the pending uploads on the storage it replaces (the boxes are in that storage's coordinate system; preserving them across a shrink would upload out of bounds):
|
||||
- an empty level = a whole-resource redefinition (`glBufferData` / `glBufferStorage`, `glTexStorage*`, texture views), clearing everything;
|
||||
- a named level = one `glTexImage*D`, dropping only that `(uploadTarget, level)`, regardless of whether the descriptor changed. Clearing everything would lose texels of other levels that were already accepted but not yet uploaded: `glTexImage2D(0)` → draw (texture incomplete, backend bails) → `glTexImage2D(1)`;
|
||||
- a metadata-only change (`RespecifyRedefinesNoStorage`: the descriptor re-sent when the sticky `BindMask` / `ImageBindableHint` changes) drops nothing.
|
||||
- A buffer has no pending uploads and never counts as metadata-only either.
|
||||
|
||||
#### 6.1.1 region 列表是精确脏足迹
|
||||
#### 6.1.1 The region list is the exact dirty footprint
|
||||
|
||||
纹理 `resource_subdata` 的 region 列表是该 level 自上次变干净以来的**精确脏足迹**:互不相交的盒子,覆盖每个写过的纹素,不多一个(`MGPipeTextureEmitter::EmitOneLevel`)。原因:server 端图像一旦可能含 GPU 写入,超出应用写入范围的上传就会盖掉它们。Magma 总把列表当作要上传的纹素;Espryt 在驱动纹理可能含 GPU 写入时同样如此,否则仍自己在 box 与 rect 之间选上传形状(Mali 作业数照旧由 server 决定)。
|
||||
- `RegionCount == 0` 只表示整 level 写;其余写入一律带盒子(只有一个也带),所以 applier 的累积不会因为某一侧「只有 box」而把精确贡献放宽成并集盒。
|
||||
- 例外:足迹超过一条记录能带的 `kMGPipeMaxPendingUploadRegions` 个盒子时,退回存储的 rect 列表(只保证覆盖,日志记一次);该列表又被扣下(rect 填满并集盒的 3/4)时,0 是一个**不精确**的部分盒。把「只有 box」读作「client 拥有盒内每个纹素」的读者(`AdoptDriverLevelIntoShadow` 的覆盖盒规则)只在这种情况下不精确。
|
||||
The region list of a texture `resource_subdata` is the **exact dirty footprint** of that level since it last became clean: disjoint boxes that cover every written texel and not one more (`MGPipeTextureEmitter::EmitOneLevel`). Reason: once a server-side image may contain GPU writes, an upload beyond the application's write range would overwrite them. Magma always treats the list as the texels to upload; Espryt does the same when the driver texture may contain GPU writes, and otherwise still chooses the upload shape itself between the box and the rects (the Mali job count is still decided by the server).
|
||||
- `RegionCount == 0` only means a whole-level write; every other write carries boxes (even a single one), so the applier's accumulation never relaxes an exact contribution into a union box because one side is "box only".
|
||||
- Exception: when the footprint exceeds the `kMGPipeMaxPendingUploadRegions` boxes one record can carry, it falls back to the storage's rect list (coverage only, logged once); when that list is in turn withheld (the rects fill 3/4 of the union box), 0 is an **inexact** partial box. Readers that read "box only" as "the client owns every texel in the box" (the coverage-box rule of `AdoptDriverLevelIntoShadow`) are inexact only in this case.
|
||||
|
||||
#### 6.1.2 纹理视图的上传记在存储属主上
|
||||
#### 6.1.2 Texture view uploads are recorded on the storage owner
|
||||
|
||||
`glTextureView` 与源纹理共用纹素,所以 client 把经视图或经属主名字做的每次上传都记在**存储属主**上:`TextureObjectView` 把 dirty 标记换算成属主坐标(`ToOwnerLevel` / `ToOwnerUploadTarget` / `ToOwnerRegionOffset`)转给属主,`TextureEmit.h` 的排空列表按(存储属主, upload target, level)键控,每条纹理 `resource_subdata` 都指向属主句柄;applier 因此只推进属主记录的 `Serial` 与 `PendingUploads`。
|
||||
- Espryt 的逐纹理 clean 门 `BackendTextureObject::IsDrawSyncCleanByRecord` 对两个存储条款读 `Desc.ViewOf` 指向的属主记录(`PipeTextureStorageRecordForRecord`),三个参数条款仍读视图自己的记录:视图有自己的纹理参数与内建 sampler。这与前端 `TextureObjectView::GetContentVersion()` 转发给属主是同一条规则。
|
||||
- 若存储条款读视图自己的记录,视图第一次被采样之后,经属主名字的上传在视图上永不可见。门是 `TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary`,monolith 与三个传输臂都跑。
|
||||
`glTextureView` shares texels with the source texture, so the client records every upload made through a view or through the owner's name on the **storage owner**: `TextureObjectView` converts the dirty mark into owner coordinates (`ToOwnerLevel` / `ToOwnerUploadTarget` / `ToOwnerRegionOffset`) and forwards it to the owner, the drain list in `TextureEmit.h` is keyed by (storage owner, upload target, level), and every texture `resource_subdata` points at the owner handle; the applier therefore advances only the owner record's `Serial` and `PendingUploads`.
|
||||
- Espryt's per-texture clean gate `BackendTextureObject::IsDrawSyncCleanByRecord` reads the owner record pointed to by `Desc.ViewOf` (`PipeTextureStorageRecordForRecord`) for the two storage clauses, while the three parameter clauses still read the view's own record: a view has its own texture parameters and built-in sampler. This is the same rule as the frontend's `TextureObjectView::GetContentVersion()` forwarding to the owner.
|
||||
- If the storage clauses read the view's own record, uploads through the owner's name would never become visible on the view after it is first sampled. The gate is `TextureViewAliasScenario.TheOwnerUploadDrainRunsAgainAfterADrawBoundary`, run under monolith and all three transport arms.
|
||||
|
||||
## 7. Shader state = SPIR-V + 反射归档
|
||||
## 7. Shader state = SPIR-V + reflection archive
|
||||
|
||||
- `CreateShaderState` 过线的是逐 stage SPIR-V + 反射归档,**不是源码**:glslang 全在 client、SPIRV-Cross 全在 server,文件级切割;server 侧无编译池。归档序列化器 `ProgramArtifactsCodec`,monolith 只在 verify 构建里调它(D-H3)。
|
||||
- server 在 verb 时刻从已推送状态**惰性特化**(D-B2:draw FBO clamp mask、fragColor 广播数、storage-block 绑定签名、image 格式、patch 参数等)——正是两个后端原来的做法。
|
||||
- link / compile 失败不需要同步返回:`GL_LINK_STATUS` 由 client 从 `ProgramObject` 回答,后端失败以 server 日志的 ≥ERROR 行(控制面 `LogLine`,无损,`MG_Remote/Transport/LogForward.h`)呈现。
|
||||
- 内部 shader(颜色 blit、深度 mip、multisample resolve)烘焙成签进树的 SPIR-V(`Wire*Spirv.h`),新鲜度门 `MOBILEGL_BAKED_INTERNAL_SHADERS`(`MG_Test/SelfTest/BakedInternalShadersTest.cpp`)重跑树内 glslang 逐字节比对(P7)。
|
||||
- `CreateShaderState` sends per-stage SPIR-V + a reflection archive across the wire, **not source**: glslang entirely on the client, SPIRV-Cross entirely on the server, a file-level split; no compile pool on the server. The archive serializer is `ProgramArtifactsCodec`, and monolith calls it only in verify builds (D-H3).
|
||||
- The server **lazily specializes** at the verb from the pushed state (D-B2: draw FBO clamp mask, fragColor broadcast count, storage-block binding signature, image formats, patch parameters, etc.) — exactly what the two backends did before.
|
||||
- link / compile failure needs no synchronous return: `GL_LINK_STATUS` is answered by the client from the `ProgramObject`, and backend failures surface as ≥ERROR lines in the server log (control-plane `LogLine`, lossless, `MG_Remote/Transport/LogForward.h`).
|
||||
- Internal shaders (color blit, depth mip, multisample resolve) are baked into SPIR-V signed into the tree (`Wire*Spirv.h`), and the freshness gate `MOBILEGL_BAKED_INTERNAL_SHADERS` (`MG_Test/SelfTest/BakedInternalShadersTest.cpp`) reruns in-tree glslang and compares byte for byte (P7).
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
# 反向通道(原 ARCHITECTURE §8)
|
||||
# Reverse Channel (formerly ARCHITECTURE §8)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design highlights and an index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; `ARCHITECTURE.md §N` in code comments resolves by number here.
|
||||
|
||||
## 8. 反向通道
|
||||
## 8. Reverse channel
|
||||
|
||||
### 8.1 `MGPipeCallbacks`(`MG_Pipe/MGPipeCallbacks.h`)
|
||||
### 8.1 `MGPipeCallbacks` (`MG_Pipe/MGPipeCallbacks.h`)
|
||||
|
||||
七个具名回调,取代后端直接 poke 前端对象的 95 个调用点(具名化是有意偏离 D8)。monolith 下直调,split 下是 `SEG_EVENT` 上的记录。
|
||||
Seven named callbacks replace the 95 call sites where the backend directly pokes frontend objects (the naming is a deliberate deviation D8). Under monolith they are direct calls; under split they are records on `SEG_EVENT`.
|
||||
|
||||
| 回调 | 作用 |
|
||||
| Callback | Role |
|
||||
|---|---|
|
||||
| `OnGlError` | 延迟的 GL 错误;**必须对命令流有序** |
|
||||
| `OnGpuWritten` | GPU 写过的 buffer 范围(收窄 client 保守自建的 pending 集) |
|
||||
| `OnBufferWriteback` | PBO 回读、XFB 捕获结果;必须与 epoch bump 有序 |
|
||||
| `OnTextureWriteback` | CPU 回退生成 mip 的纹素 |
|
||||
| `OnMipLevelsGenerated` | 只带形状 |
|
||||
| `OnSurfaceChanged` | 默认帧缓冲的格式与尺寸(server 拥有显示时 client 靠它得知窗口尺寸) |
|
||||
| `OnCapsInvalidated` | 取代 `InvalidateCompileEnv` |
|
||||
| `OnGlError` | deferred GL errors; **must be ordered with respect to the command stream** |
|
||||
| `OnGpuWritten` | buffer ranges the GPU has written (narrowing the client's conservatively built pending set) |
|
||||
| `OnBufferWriteback` | PBO readback, XFB capture results; must be ordered with the epoch bump |
|
||||
| `OnTextureWriteback` | texels of CPU-fallback generated mips |
|
||||
| `OnMipLevelsGenerated` | shape only |
|
||||
| `OnSurfaceChanged` | format and size of the default framebuffer (when the server owns the display, this is how the client learns the window size) |
|
||||
| `OnCapsInvalidated` | replaces `InvalidateCompileEnv` |
|
||||
|
||||
`OnLog` 已删除(P9 W3;与 W2 删除的 `OnTexturePullRequest` 合计,回调 9 → 7):零生产者、零消费者、无 `EventKind`。server 日志走控制面 `LogLine`(带 `level`),由 `MG_Remote/Transport/LogForward.h` 的队列 + 发送线程转发:≤WARN 有损(队列满即丢、计数、流内 `LogForward{Dropped}` 标出缺口),≥ERROR 无损 + 限速(超速排队延迟;超出错误字节上限时合并成一条 `LogForward{Coalesced}`),FATAL 有界等待写出;日志线程永不等 socket。
|
||||
`OnLog` has been removed (P9 W3; together with `OnTexturePullRequest` removed in W2, callbacks 9 → 7): zero producers, zero consumers, no `EventKind`. Server logs go through the control-plane `LogLine` (with `level`), forwarded by the queue + sending thread in `MG_Remote/Transport/LogForward.h`: ≤WARN is lossy (dropped when the queue is full, counted, the gap marked in-stream by `LogForward{Dropped}`), ≥ERROR is lossless + rate-limited (over-rate lines queue with delay; when the error byte cap is exceeded they are merged into one `LogForward{Coalesced}`), FATAL waits with a bound to be written out; the logging thread never waits on the socket.
|
||||
|
||||
后端凭空造的前端对象(Magma 占位纹理、swapchain 默认 FB 占位)改为 server 原生;pull 构建里的 `m_backend` 类成员真删会动 `sizeof`(G1),随 P13 退役(D-K)。
|
||||
Frontend objects invented out of thin air by the backend (Magma placeholder textures, the swapchain default FB placeholder) become server-native; truly deleting the `m_backend`-style members in pull builds would change `sizeof` (G1), so they retire with P13 (D-K).
|
||||
|
||||
### 8.2 有序性是正确性要求
|
||||
### 8.2 Ordering is a correctness requirement
|
||||
|
||||
每次 writeback 之后的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。**
|
||||
The epoch bump after each writeback must be applied by the server before any later command that reads that handle. **The reverse channel needs the same ordering guarantee as the forward channel.**
|
||||
|
||||
### 8.3 错误、ack 与日志
|
||||
### 8.3 Errors, acks and logs
|
||||
|
||||
- **唯一允许同步 ack 的入口是 `glBufferStorage`**:`ResourceRespecify` 的 `kNeedsAck` 由逐记录谓词收窄到"不可变 **buffer**"(D-A2),纹理 OOM 在 monolith 里本来就推迟到 sync 时刻。respecify 带逐 level 作用域与"只换元数据"的形式,不误丢已接受的待上传。
|
||||
- 其余错误一律晚到,走有序的 `OnGlError`。
|
||||
- **The only entry allowed to ack synchronously is `glBufferStorage`**: `ResourceRespecify`'s `kNeedsAck` is narrowed by a per-record predicate to the "immutable **buffer**" (D-A2); texture OOM was already deferred to sync time under monolith. respecify carries per-level scope and a "metadata-only" form, and does not mistakenly drop accepted pending uploads.
|
||||
- All other errors are late, going through the ordered `OnGlError`.
|
||||
|
||||
#### 8.3.1 server 日志的转发队列
|
||||
#### 8.3.1 The server log forwarding queue
|
||||
|
||||
`MG_Remote/Transport/LogForward.h` 的 `LogForwardChannel`:`Offer` 只在通道互斥锁下拷贝一行入队,唯一的发送线程按入队顺序写帧,写 socket 时不持锁,所以 FATAL 以下的行花在转发上的只是一次加锁加一次拷贝(`LogForwardCounters::maxOfferNs`),与对端是否在读无关。server 自己的日志文件在转发之前写(`Log.cpp`),总有全部行。
|
||||
- DEBUG / INFO / WARN 有损:使有损字节超过 `lossyQueueBytes` 的行丢弃并计数;缺口闭合时(下一条放得下的 ≤WARN 行、`LogFlush` ack 或 `Close`)通道自己插一条 WARN `LogForward{Dropped}`,写明条数、字节与起始时间。
|
||||
- ERROR 无损、限速:令牌桶(`errorBurstLines` 起步、`errorLinesPerSecond` 补充),超速的行按序排队延迟、不丢;排队的错误字节超过 `errorQueueBytes`(有损上限的 8 倍)时不再入队,由排在下一个能入队条目之前的一条 ERROR `LogForward{Coalesced}` 代替。
|
||||
- FATAL = ERROR + 等待:最多等 `fatalFlushMs`,直到发送线程写出它及之前的一切;一次等待超时后,在发送线程写出任何东西之前,后续 FATAL 不再等(`MGLOG_F` 也标记可存活的情形,可能连发)。`Log.cpp` 在日志锁外交出 FATAL 行。
|
||||
- `LogFlush` ack(`OfferControlFrame`)排在之前提交的每一行之后,`MGPipeSyncPeerLog` 的「ack 到达时之前的行都已在文件里」因此成立;它排队期间发送线程不限速。
|
||||
- `Close()` 不再收行,给队列 `closeDrainMs` 不限速发完;发送线程仍卡在写里时调用属主提供的 `unblock`(`ServerMain`:对控制 socket `shutdown(2)`),以便 join 而不是泄漏线程,剩余条目计为 abandoned。
|
||||
- 只有 TCP server 形态转发(`ServerMain.cpp`,`MOBILEGL_IPC_LOG_FORWARD`);unix spawn server 与 inproc apply 线程自己写 `<base>.server.log`。
|
||||
`LogForwardChannel` in `MG_Remote/Transport/LogForward.h`: `Offer` only copies one line into the queue under the channel mutex, and the single sending thread writes frames in enqueue order, holding no lock while writing the socket, so for lines below FATAL, forwarding costs only one lock plus one copy (`LogForwardCounters::maxOfferNs`), regardless of whether the peer is reading. The server's own log file is written before forwarding (`Log.cpp`), so it always has all lines.
|
||||
- DEBUG / INFO / WARN are lossy: a line that would push the lossy bytes over `lossyQueueBytes` is dropped and counted; when the gap closes (the next ≤WARN line that fits, a `LogFlush` ack, or `Close`), the channel itself inserts a WARN `LogForward{Dropped}` stating the count, bytes and start time.
|
||||
- ERROR is lossless and rate-limited: a token bucket (`errorBurstLines` to start, replenished by `errorLinesPerSecond`); over-rate lines queue in order with delay and are not dropped; when queued error bytes exceed `errorQueueBytes` (8 times the lossy cap), further lines are not enqueued, replaced by one ERROR `LogForward{Coalesced}` placed before the next entry that can be enqueued.
|
||||
- FATAL = ERROR + waiting: wait at most `fatalFlushMs`, until the sending thread has written it and everything before it; after one wait times out, later FATALs do not wait until the sending thread writes anything (`MGLOG_F` also marks survivable cases, which may fire in succession). `Log.cpp` hands off FATAL lines outside the log lock.
|
||||
- The `LogFlush` ack (`OfferControlFrame`) is ordered after every previously submitted line, so `MGPipeSyncPeerLog`'s "when the ack arrives, all preceding lines are already in the file" holds; while it is queued, the sending thread is not rate-limited.
|
||||
- `Close()` accepts no more lines and gives the queue `closeDrainMs` to drain without rate limiting; if the sending thread is still stuck in a write, it calls the owner-supplied `unblock` (`ServerMain`: `shutdown(2)` on the control socket) so the thread can be joined rather than leaked, and remaining entries count as abandoned.
|
||||
- Only the TCP server form forwards (`ServerMain.cpp`, `MOBILEGL_IPC_LOG_FORWARD`); the unix spawn server and the inproc apply thread write `<base>.server.log` themselves.
|
||||
|
||||
### 8.4 纹理重铸:字节全在 server 手里,没有拉取(P9 W2 撤销原「唯一的新停顿类」D-B6)
|
||||
### 8.4 Texture re-mint: all bytes are in the server's hands, no pull (P9 W2 withdraws the original "only new stall class" D-B6)
|
||||
|
||||
原设计假设 server 不保留纹素、重铸要把 level 从 client 拉回来。P5c 之后 server 有 `StagedTextureStore`,GPU 写过的层本来就在 GPU 图像里——**两个后端的重铸都不需要 client 的任何字节**:
|
||||
The original design assumed the server kept no texels and re-minting had to pull levels back from the client. After P5c the server has `StagedTextureStore`, and GPU-written layers are already in the GPU image — **neither backend's re-mint needs any bytes from the client**:
|
||||
|
||||
| | split 下重铸的字节来源 | monolith |
|
||||
| | Source of re-mint bytes under split | monolith |
|
||||
|---|---|---|
|
||||
| Espryt | `RequireImageBindableStorageByHandle`:驱动上确实存在的层逐层读回旧 GPU 纹理,pending 盒用 store 合并;驱动没有的层(最后一次 sync 之后才定义)不读,store 覆盖就重放 store,否则跳过;驱动拒读而 store 覆盖时退回 store;已 immutable 且无需加宽的原样保留 | 前端臂重放 client 影子 |
|
||||
| Magma | 重建带 `STORAGE` 的 VkImage,`PreserveTextureContentsOnRecreate` GPU→GPU 拷贝 | 同左 |
|
||||
| Espryt | `RequireImageBindableStorageByHandle`: layers that really exist on the driver are read back from the old GPU texture one by one, with pending boxes merged using the store; layers the driver does not have (defined only after the last sync) are not read, store coverage replays the store, otherwise they are skipped; when the driver refuses the read while the store covers it, fall back to the store; layers already immutable and needing no widening are kept as they are | the frontend arm replays the client shadow |
|
||||
| Magma | rebuild the VkImage with `STORAGE`, `PreserveTextureContentsOnRecreate` GPU→GPU copy | same as left |
|
||||
|
||||
- 只改读 store 不够:GPU 写过的层 store 里是旧字节或没有字节;语料里 Espryt 的两次重铸(`iris-photon`、`iris-derivative`)都是 store 无字节的渲染目标。
|
||||
- 「整格式再生」两臂都只上传 pending 层、不回读也不拉取;「view 源重铸」只剩源需要加宽,走同一条 ByHandle 路径。
|
||||
- 删除:`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:见 git 历史。
|
||||
- Changing only what the store reads is not enough: GPU-written layers have old bytes or no bytes in the store; in the corpus both Espryt re-mints (`iris-photon`, `iris-derivative`) are render targets with no bytes in the store.
|
||||
- "Whole-format regeneration" on both arms uploads only the pending layers, with no readback and no pull; "view source re-mint" leaves only the source needing widening and goes through the same ByHandle path.
|
||||
- Removed: `OnTexturePullRequest` (together with `OnLog` removed in W3, callbacks 9 → 7), the `texture-remint-pull` flag. op 50 `ResourceSubDataComplete` is append-only and kept as a retired line (decoding rejects it). `MOBILEGL_PIPE_TEXEL_RETAIN_MB` has no consumers left, but in pull builds it is deleted with P13 (G1).
|
||||
- `ImageBindableHint` is still a precaution: if the hint arrives before the first sync, storage is allocated image-bindable directly, saving one server readback + retransfer.
|
||||
- Known dev defect (not P9): monolith Espryt's frontend arm replays the client shadow, overwriting GPU-written texels.
|
||||
- Census, coverage matrix, gates and red-once: see the git history.
|
||||
|
||||
### 8.5 XFB scatter 留在 server
|
||||
### 8.5 XFB scatter stays on the server
|
||||
|
||||
补丁循环需要"捕获前的字节",而拆分后权威影子归 server(`StagedShadow`),所以 scatter 在 server 原地跑完;回程只有一条 `OnBufferWriteback`,载补好的整段,按 `SEG_EVENT` 容量的四分之一切片(`MGPipeBufferWritebackSliceBytes`),最后一片落地即蕴含之前每一片。孤儿目标(`HasDefinedContent == 0`)零填充后照常散射。`OnXfbScatterReady` 已删除(回调 10 → 9)。落地论证见 git 历史(espryt D1)。
|
||||
The patch loop needs "the bytes before capture", and after the split the authoritative shadow belongs to the server (`StagedShadow`), so scatter runs to completion in place on the server; the return trip is a single `OnBufferWriteback` carrying the patched whole range, sliced to a quarter of `SEG_EVENT` capacity (`MGPipeBufferWritebackSliceBytes`), and landing the last slice implies every preceding one. Orphan targets (`HasDefinedContent == 0`) are zero-filled and scattered as usual. `OnXfbScatterReady` has been removed (callbacks 10 → 9). For the landing argument see the git history (espryt D1).
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
# 后端改造与 server 侧(原 ARCHITECTURE §9–§10)
|
||||
# Backend Changes and the Server Side (formerly ARCHITECTURE §9–§10)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design highlights and an index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; `ARCHITECTURE.md §N` in code comments resolves by number here.
|
||||
|
||||
## 9. 后端状态机改造
|
||||
## 9. Backend state machine changes
|
||||
|
||||
### 9.1 原样不动的东西
|
||||
### 9.1 Things left untouched
|
||||
|
||||
Espryt 的 persistent ring、buffer pool、fallback-repack、scratch FBO、驱动绑定影子、Adreno / Mali workaround、SPIRV-Cross 会话、restart 重写与 multi-draw 分档;Magma 的 `VulkanRenderer` memo、`PipelineFactory`、`ProgramFactory`、`UniformManager`、五个 `Vk*Manager`、`SwapchainObject`、D18 的节点式容器纪律。"输入变了、算法不许动"的那一类由 **G5 字节一致门**把关(`scripts/p3a_untouched_regions.sh` 十一函数、`scripts/p4a_untouched_regions.sh` 17 区),re-pin 必须两份同改。
|
||||
Espryt's persistent ring, buffer pool, fallback-repack, scratch FBO, driver binding shadow, Adreno / Mali workarounds, SPIRV-Cross session, restart rewriting and multi-draw tiering; Magma's `VulkanRenderer` memo, `PipelineFactory`, `ProgramFactory`, `UniformManager`, the five `Vk*Manager`s, `SwapchainObject`, and D18's node-style container discipline. The "inputs changed, algorithm must not" category is guarded by the **G5 byte-identity gate** (`scripts/p3a_untouched_regions.sh` eleven functions, `scripts/p4a_untouched_regions.sh` 17 regions), and a re-pin must change both together.
|
||||
|
||||
### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1)
|
||||
### 9.2 strangler scaffolding: `PipeInputs` + per-verb fill + poison generation (P1)
|
||||
|
||||
```cpp
|
||||
// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,访问器类型与后端原来读到的完全一致
|
||||
// MG_Backend/MGPipe/PipeInputs.h — organized by memo key, with accessor types exactly matching what the backend read before
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
|
||||
#### 9.2.1 server 侧的 verb 戳与读检查
|
||||
#### 9.2.1 Server-side verb stamps and read checks
|
||||
|
||||
client 的残余填充在自己的 verb 边界给全部字段盖戳,server 读 `PipeInputs` 前必须换成自己的戳:与 client 共用一块时,不清零则每次读都“新鲜”、`rsp` 恒为 0,门形同虚设;两块分开时,没有戳则第一次读就中止。
|
||||
The client's residual fill stamps every field at its own verb boundary, and the server must swap in its own stamp before reading `PipeInputs`: when it shares one block with the client, without zeroing every read is "fresh" and `rsp` is always 0, making the gate a no-op; when the blocks are separate, without a stamp the first read aborts.
|
||||
|
||||
- 盖戳点:`PipeApplier::StampVerbBoundary` 只在 `MGPipeVerbForWireOp`(`FieldOwnership.def` 的 `MGP_VERB_OP_LIST`)映到 verb 的 op 上调用 `MGPipeServerStampVerbBoundary`:递增 `CurrentVerbSerial`、记下 verb,给该 verb 类可读掩码(`FillPoints.def`)内的 `RECORD_SUPPLIED` / `APPLIER_DERIVED` 字段盖新戳,其余字段戳清零(“从未填充”先于 sticky 判定,所以清零也取消 sticky 豁免)。
|
||||
- 非边界记录(如两次 draw 之间的 `set_dynamic_state`)不盖戳,沿用前一边界的序号、掩码与 verb 名,但盖戳标志已在上一条记录后清除,读检查按盖戳之外处理;20 个 draw 入口都盖成 `DrawArrays`,掩码正确,Fatal 里的名字是泛称。
|
||||
- 读检查只在 server 盖戳的 verb 内按归属类(§3.1 的 G8)分流:`RECORD_SUPPLIED` / `APPLIER_DERIVED` 读到旧值是 `Fatal{RoleViolation}`,`FATAL` 直接中止,`BARRIER_PULLED`(仅供生成器对照,没有生产行)在该 verb 类内计入 `rsp`,由 `CountBarrierPull` 决定何时改为中止(记录未 barrier、两角色用不同块、strict 下未准入)。盖戳之外一律是普通 poison `Fatal{UnmigratedPipeInput}`。
|
||||
- 盖戳标志由 `PipeApplier::LeaveApplier` 在每条记录之后清除;server 进程里没有 client 半边替它清,漏清会让之后的读都按上一个 verb 的掩码判定。
|
||||
- Stamp points: `PipeApplier::StampVerbBoundary` calls `MGPipeServerStampVerbBoundary` only on ops that `MGPipeVerbForWireOp` (`MGP_VERB_OP_LIST` in `FieldOwnership.def`) maps to a verb: it increments `CurrentVerbSerial`, records the verb, stamps `RECORD_SUPPLIED` / `APPLIER_DERIVED` fields within that verb class's readable mask (`FillPoints.def`) with the new stamp, and zeroes the stamps of the rest ("never filled" precedes the sticky test, so zeroing also cancels the sticky exemption).
|
||||
- Non-boundary records (such as `set_dynamic_state` between two draws) are not stamped and carry over the previous boundary's serial, mask and verb name, but the stamped flag has been cleared after the previous record, so the read check treats them as outside a stamp; all 20 draw entry points are stamped as `DrawArrays`, the mask is correct, and the name in the Fatal is generic.
|
||||
- The read check splits by ownership class (§3.1's G8) only inside a server-stamped verb: reading a stale value in `RECORD_SUPPLIED` / `APPLIER_DERIVED` is `Fatal{RoleViolation}`, `FATAL` aborts outright, and `BARRIER_PULLED` (for generator cross-check only, no production rows) counts into `rsp` within that verb class, with `CountBarrierPull` deciding when to abort instead (record not barriered, two roles using different blocks, not admitted under strict). Outside a stamp everything is the ordinary poison `Fatal{UnmigratedPipeInput}`.
|
||||
- The stamped flag is cleared by `PipeApplier::LeaveApplier` after each record; the server process has no client half to clear it, and a missed clear makes every later read judged by the previous verb's mask.
|
||||
|
||||
#### 9.2.2 run-ahead 下 client 对 `PipeInputs` 的写
|
||||
#### 9.2.2 Client writes to `PipeInputs` under run-ahead
|
||||
|
||||
run-ahead 下,unbarriered 记录 apply 期间 `PipeInputs` 是 server 角色内存:对这类 verb,client 不填充、不盖戳、不撤 server 的戳,只跑 tracker 与 emitter(`MGPipeValidateForVerb` 的 `fillOwed`)。GL 线程唯一合法的写是 barriered verb 的填充,而且写的那一刻就得成立:
|
||||
Under run-ahead, while unbarriered records are being applied, `PipeInputs` is server-role memory: for these verbs the client does not fill, does not stamp, and does not withdraw the server's stamp; it only runs the tracker and the emitter (`MGPipeValidateForVerb`'s `fillOwed`). The only legal write by the GL thread is the fill of barriered verbs, and it must hold at the moment of writing:
|
||||
|
||||
- 验证点的顺序是填充 → 发射 → park,填充时 apply 线程可能还在执行先前发布的 unbarriered 记录,所以每次写之前 `QuiesceApplierBeforeFill` 先等 applier 追上最后发布的序号(`ClientSession::WaitForApplyToCatchUp`,与 `glFinish` 同一种强制等待)。
|
||||
- 块的写分两段、夹着记录发布:发射前写 verb 序号、撤 server 戳与 verb 名,发射后做残余字段遍历;发出的记录 apply 时要读这块,一次等待盖不住两段。
|
||||
- `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` 查的是事实(apply 线程不在 applier 内),不采信调用方的声明;漏掉等待即 `Fatal{RoleViolation, "gPipeInputs"}`。
|
||||
- lockstep 下 `appliedSeq` 已在最后发布的序号上,等待立即返回。client 与 server 用不同块时(`MGPipeBlocksAreDistinct`:分角色演练或 spawn),client 只写自己的块、不必等待,server 块的戳只由 `PipeApplier::LeaveApplier` 清除。
|
||||
- The validate order is fill → emit → park; during fill the apply thread may still be executing previously published unbarriered records, so before every write `QuiesceApplierBeforeFill` first waits for the applier to catch up with the last published seq (`ClientSession::WaitForApplyToCatchUp`, the same kind of forced wait as `glFinish`).
|
||||
- The block's write is split into two parts with the record publication in between: before emission write the verb serial and withdraw the server stamp and the verb name; after emission do the residual field walk; the emitted record reads this block when it is applied, and one wait cannot cover both parts.
|
||||
- `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` checks the fact (the apply thread is not inside the applier) and does not trust the caller's declaration; a missing wait is `Fatal{RoleViolation, "gPipeInputs"}`.
|
||||
- Under lockstep `appliedSeq` is already on the last published seq, so the wait returns immediately. When client and server use different blocks (`MGPipeBlocksAreDistinct`: role drills or spawn), the client writes only its own block and need not wait, and the server block's stamp is cleared only by `PipeApplier::LeaveApplier`.
|
||||
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
|
||||
#else
|
||||
# define MGB_CTX (::MG_State::pGLContext)
|
||||
#endif
|
||||
```
|
||||
|
||||
三步:A 别名(P1,机械替换 + 逐 verb 类填充点,`nm` 不变)→ B 推送(P2,tracker 填 `gPipeInputs`,verify 构建逐 draw 比对快照版)→ C 句柄化(P3a–P4a、P7,`SharedPtr<前端对象>` → 句柄 + 描述符,写回变回调)。poison 是**逐 verb 世代**:读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "<Getter>@<Verb>"}`。
|
||||
Three steps: A alias (P1, mechanical replacement + per-verb-class fill points, `nm` unchanged) → B push (P2, the tracker fills `gPipeInputs`, verify builds compare against the snapshot version draw by draw) → C handle conversion (P3a–P4a, P7, `SharedPtr<frontend object>` → handle + descriptor, writeback becomes a callback). Poison is **per-verb generation**: reading a field not filled for the current verb is `Fatal{UnmigratedPipeInput, "<Getter>@<Verb>"}`.
|
||||
|
||||
**P13 收尾**:pull 分支与宏一起删掉(W3b,`MGB_CTX` 不再存在,后端只经 `gPipeInputs` 与 applier 记录取状态);单进程也走记录臂(W4–W5),后端的前端对象臂与前端键 twin 入口删除(W6)——Espryt 的 twin 表只收 client 铸好的句柄,对象死亡通知带句柄、按句柄释放(与 server 的 `object_death` 同一释放)。
|
||||
**P13 wrap-up**: the pull branch is deleted together with the macro (W3b, `MGB_CTX` no longer exists, and the backend takes state only through `gPipeInputs` and applier records); the monolith also goes through the record arm (W4–W5), and the backend's frontend-object arm and frontend-keyed twin entries are deleted (W6) — Espryt's twin table only accepts client-minted handles, object death notifications carry a handle, and release is by handle (the same release as the server's `object_death`).
|
||||
|
||||
### 9.3 Track V / Track H
|
||||
|
||||
Track V(值类型:render state、pixel store、capability 位、标量)是机械迁移;Track H(167 个 `SharedPtr<MG_State…>` 读点)是真活。74% 的读点是翻译输入,所以"bump 一个版本让 server 自己拉"行不通,值本身必须过去。
|
||||
Track V (value types: render state, pixel store, capability bits, scalars) is a mechanical migration; Track H (167 `SharedPtr<MG_State…>` read sites) is the real work. 74% of the read sites are translation inputs, so "bump a version and let the server pull" does not work; the values themselves must go over.
|
||||
|
||||
### 9.4 残余值块
|
||||
### 9.4 Residual value block
|
||||
|
||||
迁移期临时调用 `SetResidualValueState`(`ResidualValueBlock`):尺寸只降不升(1248 → 8,只剩 `CapabilityBits`),P13 W3c 删除——op 46 退役为拒绝行,capability 交叉核对交给 verify 比较器;`set_context_values` 携带剩下的值类字段(单进程也发,W4d / W8)。
|
||||
During migration, `SetResidualValueState` (`ResidualValueBlock`) is called temporarily: its size only shrinks, never grows (1248 → 8, only `CapabilityBits` left), and it is deleted in P13 W3c — op 46 retires as a rejecting line, and the capability cross-check goes to the verify comparator; `set_context_values` carries the remaining value-class fields (also sent under monolith, W4d / W8).
|
||||
|
||||
### 9.5 身份 memo 的重键
|
||||
### 9.5 Re-keying identity memos
|
||||
|
||||
`{slot, gen}` + 显式 destroy 让 21 条身份 memo 中 **11 条直接删除**、2 条的去抖搬到 client(§5.4)、7 条重键成更便宜的比较、1 条(D18)不动。统一事实:进 memo 键的版本计数器要么会回绕,要么根本不被它害怕的 mutation bump,身份比较是堵洞的补丁。
|
||||
`{slot, gen}` + explicit destroy means that of the 21 identity memos, **11 are deleted outright**, 2 move their debounce to the client (§5.4), 7 are re-keyed to a cheaper comparison, and 1 (D18) is untouched. The unified fact: a version counter that enters a memo key either wraps or is simply not bumped by the mutation it fears; identity comparison is a patch over the hole.
|
||||
|
||||
### 9.6 A/B 与口径收窄
|
||||
### 9.6 A/B and narrowing the criteria
|
||||
|
||||
`MOBILEGL_PIPE_PUSH` 子系统位图在阶段 B 是真 A/B;阶段 C 之后位清零时后端仍跑重键后的代码,所以**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS` 曾保留 pre-handle 臂。P13 结束了这段:`LEGACY_MEMOS` 与 pre-handle 臂删除,位 0–13 固定开、清位即启动期具名拒绝(原 Off 车道改为这条拒绝的阴性对照),只剩位 63 可设。位依赖两侧都拒(客户端族门里**根本不发射**,否则会"客户端已清 dirty、服务端却走旧臂"而丢上传);没有消费者的后端一条不发。
|
||||
`MOBILEGL_PIPE_PUSH`'s subsystem bitmap is a real A/B in phase B; after phase C, when the bits are cleared, the backend still runs the re-keyed code, so the **compile-time** `MOBILEGL_PIPE_LEGACY_MEMOS` once preserved the pre-handle arm. P13 ended this: `LEGACY_MEMOS` and the pre-handle arm are deleted, bits 0–13 are permanently on, and clearing a bit is a named refusal at startup (the original Off lane becomes the negative control for this refusal), leaving only bit 63 settable. Dependencies between bits are refused on both sides (the client family gate **does not emit at all**, otherwise "the client has cleared dirty while the server takes the old arm" would drop uploads); a backend with no consumers sends nothing.
|
||||
|
||||
## 10. server 侧
|
||||
## 10. Server side
|
||||
|
||||
### 10.1 对象表与 applier
|
||||
### 10.1 Object tables and the applier
|
||||
|
||||
`MG_Remote/Server/PipeApplier`:解码 → 更新对象表与 `PipeInputs` → 调后端。单进程(P13 起)是同一记录臂少了编解码与环:状态记录经 `PipeRoute` 的单进程表直进 applier,verb 记录由 `MG_Impl/Pipe/Verb` 的发射器交给本进程的 `MG_Backend/Record/RecordVerbSink`。server 不持有 buffer 的完整副本(只有 staged shadow)、不持有前端对象图;任何传输下都不得有 `SharedPtr` 或裸前端指针跨过 applier 边界(P5c 角色守卫、P5e 规则 F)。`InProcessTransport` 与 spawn 走完全相同的编解码路径。
|
||||
`MG_Remote/Server/PipeApplier`: decode → update the object tables and `PipeInputs` → call the backend. Monolith (since P13) is the same record arm minus the codec and the ring: state records go straight into the applier through `PipeRoute`'s monolith table, and verb records are handed by the emitters in `MG_Impl/Pipe/Verb` to this process's `MG_Backend/Record/RecordVerbSink`. The server does not hold a full copy of buffers (only the staged shadow) and does not hold the frontend object graph; under any transport, no `SharedPtr` or raw frontend pointer may cross the applier boundary (P5c role guard, P5e rule F). `InProcessTransport` and spawn go through exactly the same codec path.
|
||||
|
||||
### 10.2 monolith 侧的净收益
|
||||
### 10.2 Net gain on the monolith side
|
||||
|
||||
即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;`SwapchainObject` 写 `MG_Impl` 的分层倒置消失;`inproc` = 渲染线程。monolith 净代码量是增加的,论据只看逐线程 CPU。
|
||||
Even if IPC never ships: a whole class of address-reuse ABA becomes inexpressible; the FBO → program ordering hazard disappears; the layering inversion of `SwapchainObject` writing into `MG_Impl` disappears; `inproc` = the render thread. The monolith's net line count increases; the argument looks only at per-thread CPU.
|
||||
|
||||
### 10.3 索引宿主镜像(`Server/IndexHostMirror`,P8,待重裁)
|
||||
### 10.3 Index host mirror (`Server/IndexHostMirror`, P8, pending a new ruling)
|
||||
|
||||
设计:`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 历史)。
|
||||
Design: under `kCapNeedsHostIndexBytes`, the server incrementally maintains a host mirror of element-array buffers from the `ResourceCreate/Respecify/SubData` stream it has to receive anyway, with zero extra traffic; the budget is `MOBILEGL_PIPE_INDEX_MIRROR_MB` (default 64), and beyond the budget it degrades to transporting via `MGHostSpan` per draw. **P6's a6 audit measured that host index spans have no producer and client indices are already owned buffers**, so P8 must be ruled again against real consumers rather than copying this section (see the git history).
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
# 传输与数据面(原 ARCHITECTURE §11)
|
||||
# Transport and data plane (formerly ARCHITECTURE §11)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design points and the index of all chapters are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; an `ARCHITECTURE.md §N` in a code comment is found by its number here.
|
||||
|
||||
## 11. 传输与数据面(`MobileGL/MG_Remote/`)
|
||||
## 11. Transport and data plane (`MobileGL/MG_Remote/`)
|
||||
|
||||
### 11.1 段
|
||||
### 11.1 Segments
|
||||
|
||||
| 段 | 拥有者 | 默认 | 内容 |
|
||||
| Segment | Owner | Default | Contents |
|
||||
|---|---|---|---|
|
||||
| `SEG_CMD` | client(server 只读) | 8 MiB | `RingControl` 页 + POD 记录 + 小内联负载 |
|
||||
| `SEG_STAGE` | client | 32 MiB | bulk 字节:buffer / 纹理 subdata、UBO、client 数组、persistent-map 脏块 |
|
||||
| `SEG_REPLY` | server | 16 MiB,8 × 2 MiB slot | readback 像素、acceptance 答案(ID-47) |
|
||||
| `SEG_EVENT` | server | 256 KiB SPSC ring | 回调事件 |
|
||||
| `SEG_SHADOW` / `SEG_ADOPT` | client / server | — | 零拷贝 shadow(Phase 2)/ ≥16 MiB 采纳(P11);stream 数据面上具名拒绝(`MG_Remote/Transport/AdoptTier.cpp`,握手期,P11 A1) |
|
||||
| `SEG_CMD` | client (server read-only) | 8 MiB | `RingControl` page + POD records + small inline payloads |
|
||||
| `SEG_STAGE` | client | 32 MiB | bulk bytes: buffer / texture subdata, UBOs, client arrays, persistent-map dirty blocks |
|
||||
| `SEG_REPLY` | server | 16 MiB, 8 × 2 MiB slots | readback pixels, acceptance answers (ID-47) |
|
||||
| `SEG_EVENT` | server | 256 KiB SPSC ring | callback events |
|
||||
| `SEG_SHADOW` / `SEG_ADOPT` | client / server | — | zero-copy shadow (Phase 2) / ≥16 MiB adoption (P11); refused by name on the stream data plane (`MG_Remote/Transport/AdoptTier.cpp`, during the handshake, P11 A1) |
|
||||
|
||||
段在同机上由 `ShmSegment` 创建(Android `ASharedMemory`、Linux `memfd`、Windows `CreateFileMappingW`),描述符经 `SCM_RIGHTS` 传递;stream 数据面上两端各持一份同尺寸的私有段(§11.9)。
|
||||
On the same machine the segments are created by `ShmSegment` (Android `ASharedMemory`, Linux `memfd`, Windows `CreateFileMappingW`), and descriptors are passed with `SCM_RIGHTS`; on the stream data plane each end holds a private segment of the same size (§11.9).
|
||||
|
||||
### 11.2 `RingControl`(`Ring.h`)
|
||||
### 11.2 `RingControl` (`Ring.h`)
|
||||
|
||||
一页 4 KiB,每个争用组一条 cache line,按写者分组(P6 lk):producer 线 `cmdHead` / `submittedSeq`;consumer 线两个 tail;四个 consumer 写的水位收成 `Progress`(`appliedSeq` / `retiredSeq` / `completedFrameSerial` / `presentAckSerial`);最后是 epoch、generation、park 标志与 `eventRingFull` / `eventDropped`。游标是单调字节计数、2 的幂掩码、永不重置;pad 记录处理 wrap;不可能的头 → `Fatal{ProtocolCorruption}`。
|
||||
One 4 KiB page, one cache line per contention group, grouped by writer (P6 lk): the producer line holds `cmdHead` / `submittedSeq`; the consumer line holds two tails; the four watermarks written by the consumer are collected into `Progress` (`appliedSeq` / `retiredSeq` / `completedFrameSerial` / `presentAckSerial`); finally the epoch, generation, park flags and `eventRingFull` / `eventDropped`. Cursors are monotonic byte counts with a power-of-two mask and are never reset; pad records handle the wrap; an impossible head → `Fatal{ProtocolCorruption}`.
|
||||
|
||||
#### 11.2.1 五个水位
|
||||
#### 11.2.1 The five watermarks
|
||||
|
||||
| 水位 | 由谁推进 | 含义 |
|
||||
| Watermark | Advanced by | Meaning |
|
||||
|---|---|---|
|
||||
| `submittedSeq` | producer,发布之后 | 诊断量(client 领先 server 多少),没有人等它;放在 producer 的 cache line 上,`LinkProgress` 因此单写者 |
|
||||
| `appliedSeq` | consumer,每 apply 一条记录 +1 | verb barrier 与每个 reply 等待读它,所以不得批量:批量会让等待方卡在已完成的工作上,或在未完成的工作上醒来 |
|
||||
| `retiredSeq` | consumer,记录引用的 `SEG_STAGE` 字节用完后 | staging 分配器只在它之后回收;不超过 `appliedSeq` |
|
||||
| `completedFrameSerial` | server,present 完成时 | 回收与老化等它;比 `appliedSeq` 落后一个 GPU 深度,不得混用 |
|
||||
| `presentAckSerial` | server,归还 present credit 时 | present 节流等它;唯一限制延迟而非字节的背压 |
|
||||
| `submittedSeq` | producer, after publishing | a diagnostic (how far the client is ahead of the server), nobody waits on it; it sits on the producer's cache line, so `LinkProgress` stays single-writer |
|
||||
| `appliedSeq` | consumer, +1 per applied record | verb barriers and every reply wait read it, so it must not batch: batching leaves a waiter stuck on work that has already completed, or wakes it on work that has not |
|
||||
| `retiredSeq` | consumer, once the `SEG_STAGE` bytes a record references are done with | the staging allocator reclaims only after it; never beyond `appliedSeq` |
|
||||
| `completedFrameSerial` | server, when present completes | reclamation and aging wait on it; it trails `appliedSeq` by one GPU depth and must not be mixed with it |
|
||||
| `presentAckSerial` | server, when the present credit is returned | present throttling waits on it; the only backpressure that limits latency rather than bytes |
|
||||
|
||||
规则(`SessionRings.h` 的 `Watermark` 是唯一实现):所有等待用 `>=`,两侧都可能一次前进多步,`==` 会错过唤醒。水位只许晚发布、不许早发布:晚只是多等,早会让等待方静默使用尚未发生的工作,环上没有校验能发现;除 `appliedSeq` 外都可惰性发布,回退是 `Fatal{ProtocolCorruption}`。wrap 填充(`kRecPad`)不推进 seq:seq 就是 reply slot 的 id,只有一侧计数填充时两侧序号每次 wrap 偏一、静默读到别的调用的答案,所以两侧都在计数前跳过它(`RingConsumer::Pop`)。
|
||||
The rule (`Watermark` in `SessionRings.h` is the only implementation): every wait uses `>=`, since either side may advance several steps at once and `==` would miss the wakeup. A watermark may only be published late, never early: late just means waiting longer, while early makes a waiter silently use work that has not happened, and no check on the ring can detect it; all but `appliedSeq` may be published lazily, and going backward is `Fatal{ProtocolCorruption}`. The wrap padding (`kRecPad`) does not advance seq: seq is the reply slot id, and if only one side counted the padding the two sides' sequence numbers would be off by one on every wrap and silently read another call's answer, so both sides skip it before counting (`RingConsumer::Pop`).
|
||||
|
||||
### 11.3 双向 doorbell(`Doorbell.h`)
|
||||
### 11.3 Bidirectional doorbell (`Doorbell.h`)
|
||||
|
||||
两个方向都是"先自旋、再置 parked、再阻塞",发布方仅当对端 parked 时敲;丢失唤醒由两个 `seq_cst` fence 关闭。`CondVarDoorbell`(同进程,带 `Kill()`)与 `SocketDoorbell`(跨进程,对端关闭即死亡检测)。stream 数据面上门铃是本地条件变量,由读线程敲。
|
||||
Both directions do "spin first, then set parked, then block", and the publisher rings only when the peer is parked; lost wakeups are closed off by two `seq_cst` fences. `CondVarDoorbell` (same process, with `Kill()`) and `SocketDoorbell` (across processes, where the peer closing is the death detection). On the stream data plane the doorbell is a local condition variable, rung by the read thread.
|
||||
|
||||
#### 11.3.1 丢失唤醒为什么靠两道 fence
|
||||
#### 11.3.1 Why lost wakeups rest on two fences
|
||||
|
||||
等待方:置 park 标志 → `std::atomic_thread_fence(seq_cst)` → 重测条件(`Doorbell::Wait`);通知方:发布水位 → 同样的 fence → 读 park 标志(`NotifyIfParked`)。两道 fence 都在 seq_cst 全序里、必有先后,按 [atomics.order] 至少一侧看得见另一侧的写,所以标志自身的读写可以是 relaxed。
|
||||
Waiter: set the park flag → `std::atomic_thread_fence(seq_cst)` → re-test the condition (`Doorbell::Wait`); notifier: publish the watermark → the same fence → read the park flag (`NotifyIfParked`). Both fences are in the seq_cst total order and must have an order, so by [atomics.order] at least one side sees the other side's writes, and the flag's own reads and writes can be relaxed.
|
||||
|
||||
只把标志的 store / load 改成 seq_cst 不够:Dekker 论证要求四个访问都在全序里,而水位发布是 release store(`RingProducer::Publish`)、条件重测是 acquire load。x86 上 release store 是普通 MOV,还停在 store buffer 里时,对标志的普通 MOV 读就可能读到 0,通知方于是不敲门铃,等待方在旧水位上永久 park;ARMv8 上 STLR→LDAR 是 RCsc,只是碰巧没事。所以两道 fence 都不可删,且 `NotifyIfParked` 必须在水位发布之后调用:fence 只约束它之前的访问。
|
||||
Making only the flag's store / load seq_cst is not enough: the Dekker argument requires all four accesses to be in the total order, while the watermark publication is a release store (`RingProducer::Publish`) and the condition re-test is an acquire load. On x86 a release store is a plain MOV, and while it is still in the store buffer a plain MOV read of the flag may read 0, so the notifier does not ring the doorbell and the waiter parks forever on the old watermark; on ARMv8 STLR→LDAR is RCsc and merely happens to be fine. So neither fence can be removed, and `NotifyIfParked` must be called after the watermark publication: a fence only orders the accesses before it.
|
||||
|
||||
### 11.4 控制面(`protocol.fbs`、`Framing.h`、`ITransport.h`)
|
||||
### 11.4 Control plane (`protocol.fbs`, `Framing.h`, `ITransport.h`)
|
||||
|
||||
- 一份 FlatBuffers schema:热路径是 `struct` 直接进 ring(与 POD 逐条 `static_assert` 对齐);罕见 / 变长 / 需演进的走控制连接的 `CtrlMsg`(`Hello`、`Welcome`、`Refuse`、`CapsSnapshot`、`SurfaceOp/SurfaceReply`、`SessionFault`、`LogLine` …),union tag 只追加,控制修订号钉在 `sha256(protocol.fbs)` 上。
|
||||
- 生成物提交进树,CI `flatc-check` 重生成并 diff;codegen 不进默认构建图。
|
||||
- 封帧 `[u32 'MGLF'][u32 len][payload]`,64 MiB 上限,读时校验,坏帧立即闩住失败。
|
||||
- One FlatBuffers schema: the hot path uses `struct`s that go straight into the ring (aligned one by one with the POD records by `static_assert`); the rare / variable-length / evolving cases go through `CtrlMsg` on the control connection (`Hello`, `Welcome`, `Refuse`, `CapsSnapshot`, `SurfaceOp/SurfaceReply`, `SessionFault`, `LogLine` …), union tags are append-only, and the control revision is pinned to `sha256(protocol.fbs)`.
|
||||
- The generated output is committed to the tree, and the CI `flatc-check` regenerates it and diffs; codegen is not in the default build graph.
|
||||
- Framing is `[u32 'MGLF'][u32 len][payload]`, with a 64 MiB cap, validated on read, and a bad frame latches the failure immediately.
|
||||
|
||||
### 11.5 WAR 危害、拷贝账与背压
|
||||
### 11.5 WAR hazards, the copy accounting and backpressure
|
||||
|
||||
Phase 1(今天):GL 调用时刻把字节拷进 stage slot,slot 到 apply 越过它为止不可变,危害按构造消除,代价一次 memcpy;Phase 2(`SEG_SHADOW` 零拷贝)未做。server 没有第二份 `BufferObject`,不存在中间拷贝。分配失败升级:扩容 → 对最老未退休批次有界等待 → 硬 drain + generation bump(之后 tracker 全部重推)。
|
||||
Phase 1 (today): the bytes are copied into a stage slot at the moment of the GL call, the slot is immutable until apply passes it, hazards are eliminated by construction, and the cost is one memcpy; Phase 2 (`SEG_SHADOW` zero copy) is not done. The server has no second `BufferObject`, so there is no intermediate copy. Allocation failure escalates: grow → bounded wait on the oldest unretired batch → hard drain + generation bump (after which the tracker re-pushes everything).
|
||||
|
||||
### 11.6 publish、序号与 credit
|
||||
### 11.6 Publish, sequence numbers and credit
|
||||
|
||||
- 每条记录 release-store `cmdHead`,仅当 consumer parked 时敲门铃;显式门铃点:`present`、`kNeedsAck`、`eglMakeCurrent`、`glFlush`、stage 余量不足、轮询类入口。
|
||||
- seq = 记录序数;字节 credit 与 present credit 两个独立窗口。
|
||||
- **等待规则由 `WaitClass` 列决定(P5e)**:`kWaitReply` / `kWaitPresent` / `kWaitApplied` / `kWaitNone`;run-ahead 武装时 `kWaitNone` 发布即返回。barriered 谓词(`MGPipeBarriered`)client 与 server 用同一个函数、同一份数据算。
|
||||
- 强制等待点:`MapBuffer(READ)` / `GetBufferSubData` 源走 `SyncGpuWrites`;`glFinish` = 等 `appliedSeq` 追上再排空反向通道;每个 `Server*` EGL forwarder 之前等一次。`glGetError` 最多晚一个 present credit。
|
||||
- Every record release-stores `cmdHead`, and the doorbell is rung only when the consumer is parked; explicit doorbell points: `present`, `kNeedsAck`, `eglMakeCurrent`, `glFlush`, insufficient stage headroom, and polling-style entry points.
|
||||
- seq = the record ordinal; byte credit and present credit are two independent windows.
|
||||
- **The wait rule is decided by the `WaitClass` column (P5e)**: `kWaitReply` / `kWaitPresent` / `kWaitApplied` / `kWaitNone`; with run-ahead armed, `kWaitNone` returns as soon as it publishes. The barriered predicate (`MGPipeBarriered`) is computed with the same function and the same data on the client and on the server.
|
||||
- Forced wait points: `MapBuffer(READ)` / the `GetBufferSubData` source goes through `SyncGpuWrites`; `glFinish` = wait until `appliedSeq` catches up, then drain the reverse channel; wait once before every `Server*` EGL forwarder. `glGetError` may lag by at most one present credit.
|
||||
|
||||
#### 11.6.1 barriered 谓词的条款
|
||||
#### 11.6.1 The terms of the barriered predicate
|
||||
|
||||
`MGPipeBarriered(op, payload, applierState)`(`MG_Pipe/PipeApply.h`)由 server 的 `ApplyOne` 计算,决定这条记录能否读 client 内存;client 不调用它,而是在发布前对同样的条款求值(`ClientSession` 的等待规则读 `WaitClass` 列,`PipeFill.cpp` 的 `ClientVerbIsBarriered` 另判 XFB)。两侧答案一旦不同,失败是静默的(client 继续跑,server 读到已经变了的值),所以条款只读线上可见的数据:静态 `WaitClass` 列、记录自身的 payload、ring 上更早的记录写进 applier 的状态。条款只有三条:
|
||||
`MGPipeBarriered(op, payload, applierState)` (`MG_Pipe/PipeApply.h`) is computed by the server's `ApplyOne` and decides whether this record may read client memory; the client does not call it, but evaluates the same terms before publishing (the wait rule in `ClientSession` reads the `WaitClass` column, and `ClientVerbIsBarriered` in `PipeFill.cpp` separately decides XFB). If the two sides' answers ever differ, the failure is silent (the client keeps running and the server reads a value that has already changed), so the terms read only data visible on the wire: the static `WaitClass` column, the record's own payload, and the state that earlier records on the ring wrote into the applier. There are only three terms:
|
||||
|
||||
1. `WaitClass != kWaitNone`;
|
||||
2. 打开的 XFB span 内的 `kCtxVerb`(XFB 保持 lockstep);
|
||||
3. 带 `kDrawClientArrays` 的 `draw_vbo`:run-ahead 下 client 在发布前就拒绝它,此条只为 lockstep 臂而在,使谓词完备。
|
||||
1. `WaitClass != kWaitNone`;
|
||||
2. a `kCtxVerb` inside an open XFB span (XFB stays lockstep);
|
||||
3. a `draw_vbo` with `kDrawClientArrays`: under run-ahead the client refuses it before publishing, so this term exists only for the lockstep arm, to make the predicate complete.
|
||||
|
||||
新条款必须在同一次改动里同时加进这个函数与 client 侧的对应判断,不能只在一侧加。评判候选条款要问**哪条臂为它付代价**,而不是它让哪条车道变绿:曾为让 Espryt 间接 multi-draw 档读 `GL_DRAW_INDIRECT_BUFFER` 绑定合法化,把所有普通 multi-draw 升级为 barriered,于是默认档的每次 `glMultiDraw*` 都多一次会合,而这次读只发生在两个可选档里;最终改由做绑定的一侧按句柄回答(`MultiDraw.cpp` 的 `BoundDrawIndirectBufferId`),撤回了该条款。
|
||||
A new term must be added to this function and to the corresponding client-side decision in the same change, never on one side only. Judging a candidate term means asking **which arm pays for it**, not which lane it turns green: once, to make the Espryt indirect multi-draw tier's read of `GL_DRAW_INDIRECT_BUFFER` bindings legitimate, every ordinary multi-draw was upgraded to barriered, so every `glMultiDraw*` in the default tier gained one more rendezvous, while that read happens only in two optional tiers; in the end the side that does the binding answers by handle instead (`BoundDrawIndirectBufferId` in `MultiDraw.cpp`), and the term was withdrawn.
|
||||
|
||||
### 11.7 事件回传与溢出
|
||||
### 11.7 Event return and overflow
|
||||
|
||||
client 在 `glGetError`、query / sync 查询、`eglSwapBuffers`、buffer 回读与每轮等待循环中排空 `SEG_EVENT`。日志有损;语义事件无损:环满时 server 置 `eventRingFull`、在记录边界停 apply、敲 client,client 排空后清标志并敲回(两半都在,才不死锁)。run-ahead 下 producer 阻塞等排空,整笔预算 `MOBILEGL_IPC_EVENT_WAIT_MS`;client 不排空 / 已走 / 会话停止 → `ReverseChannelForfeit{…}` 弃投并闩住(Ph PH-6),而不是 `Fatal` 打掉 server。不发布 `kCapRunAheadApply` 的 server(lockstep 臂)保留 P5c 的 Fatal。
|
||||
The client drains `SEG_EVENT` in `glGetError`, query / sync queries, `eglSwapBuffers`, buffer readback, and each wait-loop round. Logs are lossy; semantic events are lossless: when the ring is full the server sets `eventRingFull`, stops applying at a record boundary, and rings the client; after draining, the client clears the flag and rings back (both halves are present, so there is no deadlock). Under run-ahead the producer blocks waiting for the drain, with a whole budget of `MOBILEGL_IPC_EVENT_WAIT_MS`; if the client does not drain / has gone away / the session stops → `ReverseChannelForfeit{…}` forfeits delivery and latches (Ph PH-6), instead of a `Fatal` taking down the server. A server that does not publish `kCapRunAheadApply` (the lockstep arm) keeps the P5c Fatal.
|
||||
|
||||
### 11.8 fence 与无 present 负载(P10)
|
||||
### 11.8 Fences and present-free workloads (P10)
|
||||
|
||||
fence 完成度必须来自真的逐 fence 退休,不是 present 水位(DirectGLES 的 `g_completedFrameSerial` 今天只在 `Present` 前进);无 present 循环需要 server 插非 present fence tick。Magma 那半已随 run-ahead 落地。
|
||||
Fence completion must come from real per-fence retirement, not from the present watermark (`g_completedFrameSerial` in DirectGLES today only advances in `Present`); a present-free loop needs the server to insert a non-present fence tick. The Magma half has landed with run-ahead.
|
||||
|
||||
### 11.9 传输栈的两根轴(P6.5)
|
||||
### 11.9 The two axes of the transport stack (P6.5)
|
||||
|
||||
终局(2026-09-22):client 与 server 可在不同机器、不同 OS / 架构上经 TCP 连接,同机 `spawn` 保留。传输栈拆成**两根独立可选、握手协商、可混搭**的轴,上层只见两个接口:
|
||||
The end goal (2026-09-22): the client and the server may connect over TCP from different machines and different OSes / architectures, and same-machine `spawn` is kept. The transport stack is split into **two independently selectable, handshake-negotiated, mixable** axes, and the layers above see only two interfaces:
|
||||
|
||||
| 轴 | 接口 | 实现 | 选项(`MOBILEGL_IPC_CONTROL` / `MOBILEGL_IPC_DATA`) |
|
||||
| Axis | Interface | Implementations | Options (`MOBILEGL_IPC_CONTROL` / `MOBILEGL_IPC_DATA`) |
|
||||
|---|---|---|---|
|
||||
| 控制面 | `ITransport`(只承载控制帧,不承载描述符) | `SocketTransport` | `fork`(继承 fd)、`unix:<path>`、`tcp://host:port` |
|
||||
| 数据面 | `ILink`(记录预留 / 提交、水位、reply、event、span 解析) | `ShmLink`(共享段 + `RingControl` + 门铃)、`StreamLink`(分块封帧,水位与 reply 变消息,发送窗口) | `shm`、`stream`、`auto` |
|
||||
| control plane | `ITransport` (carries only control frames, never descriptors) | `SocketTransport` | `fork` (inherited fd), `unix:<path>`, `tcp://host:port` |
|
||||
| data plane | `ILink` (record reservation / commit, watermarks, reply, event, span resolution) | `ShmLink` (shared segment + `RingControl` + doorbell), `StreamLink` (chunked framing, watermarks and replies become messages, send window) | `shm`, `stream`, `auto` |
|
||||
|
||||
- 配对在会话建立时选定一次;`LinkTerms`(数据面、窗口尺寸、`maxReplyBytes` …)由 **server 陈述**,client 请求可被钳制。
|
||||
- 共享段交付与控制面解耦:`ShmLink` 自带 AF_UNIX aux 汇合名,在 `Welcome` 里公布,所以同机"TCP 控制 + 共享段数据"是合法配对。`auto` 的判据是**段实际交付成功**,回落到 `stream` 必须具名进日志。
|
||||
- **上层零链路种类分支**:`ShmLink` / `StreamLink` / `RingControl` 等只出现在 `Transport/` 与其测试(纯度 grep 门);角色谓词回答"谁在哪个进程",不回答"用什么链路"。
|
||||
- 两端是不同二进制:`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)。
|
||||
- The pairing is chosen once at session setup; `LinkTerms` (data plane, window sizes, `maxReplyBytes` …) is **stated by the server**, and the client's request may be clamped.
|
||||
- Shared-segment delivery is decoupled from the control plane: `ShmLink` carries its own AF_UNIX aux rendezvous name, announced in `Welcome`, so "TCP control + shared-segment data" on the same machine is a valid pairing. The `auto` criterion is **that the segment actually delivered successfully**, and a fallback to `stream` must be named in the log.
|
||||
- **Zero link-kind branches above**: `ShmLink` / `StreamLink` / `RingControl` and the like appear only in `Transport/` and its tests (a purity grep gate); role predicates answer "who is in which process", not "what link is used".
|
||||
- The two ends are different binaries: `wireFingerprint` (the per-member layout digest derived from `PipeFields.def` + the catalog digest + byte order / pointer width) is always compared; `buildFingerprint` is compared only under `Dial == Fork` (or `MOBILEGL_IPC_REQUIRE_SAME_BUILD=1`); a mismatch is a `Refuse`, not an abort.
|
||||
- Over TCP every `kWaitReply` is one RTT, and the `SEG_STAGE` bytes face a 30–100 MB/s link: these two columns turn from "record items" into feasibility numbers (open questions 19, 20).
|
||||
|
||||
第一波(全 TCP 控制 + stream 数据)已落地;同机混搭(nd `auto` / sd)是第二波。细节与契约见 git 历史。
|
||||
The first wave (all-TCP control + stream data) has landed; same-machine mixing (nd `auto` / sd) is the second wave. Details and contracts are in the git history.
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
# persistent map、回读与验证(原 ARCHITECTURE §12–§13)
|
||||
# Persistent map, readback and verification (formerly ARCHITECTURE §12–§13)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design points and the index of all chapters are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; an `ARCHITECTURE.md §N` in a code comment is found by its number here.
|
||||
|
||||
## 12. persistent map 与 ≥16 MiB 采纳
|
||||
## 12. Persistent map and ≥16 MiB adoption
|
||||
|
||||
`AcquirePersistentMap` 是永久的地址空间捐赠(≥16 MiB 可变 store 自动采纳,MC 26.3 p99 163 → 21 ms),整个 monolith 改造期不动(D-B4)。拆分下三档由 POST 探针选择(spike B 实测):
|
||||
`AcquirePersistentMap` is a permanent address-space donation (a mutable store of ≥16 MiB is adopted automatically, MC 26.3 p99 163 → 21 ms), unchanged through the whole monolith rework (D-B4). Under the split, the POST probe chooses among three tiers (measured in spike B):
|
||||
|
||||
| 档 | 形态 | 结论 |
|
||||
| Tier | Shape | Conclusion |
|
||||
|---|---|---|
|
||||
| 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` §1) |
|
||||
| T0 | the client allocates an `AHardwareBuffer` BLOB, the server imports it (Vulkan / GLES) | the only tier that reads and writes fully on both devices and both backends (the P11 main thrust) |
|
||||
| T1 | the server exports an opaque fd | only the Adreno Vulkan path |
|
||||
| T2 | refuse, push on the client side | the permanently correct fallback; **the tier today's split uses** (`MOBILEGL_IPC_ADOPT_TIER=2`), forced on the stream data plane (P11 A1: at handshake `Transport::SettleAdoptTierAtHandshake` prints one `Refuse{AdoptTierOnStream}` line for 0/1 and goes T2; on a shared segment the client gets `Fatal{UnimplementedAdoptTier}` during the handshake, `CONTRACT-P11.md` §1) |
|
||||
|
||||
T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `hasLiveHostWrites`);在每个 validate 点按 64 KiB 块推送可达的已映射 buffer,脏页追踪 + 哈希抑制只发变化的块(P5d);门 `PersistentCoherentMapScenario`。`mpr` 数的是每一次 `MapPersistent` 发射(铸成或拒绝都算),所以在 monolith 下也可断言。
|
||||
Under T2 the client-side push has three parts: no map/unmap command pair (the payload carries `hasLiveHostWrites`); at every validate point, push the reachable mapped buffers in 64 KiB blocks, with dirty-page tracking + hash suppression sending only the changed blocks (P5d); the gate is `PersistentCoherentMapScenario`. `mpr` counts every `MapPersistent` emission (a mint or a refusal both count), so it can be asserted under the monolith too.
|
||||
|
||||
### 12.1 采纳档的握手期结算
|
||||
### 12.1 Settling the adoption tier at handshake
|
||||
|
||||
档在握手期、第一条 `map_persistent` 之前,由知道数据面的一侧结算(`MG_Remote/Transport/AdoptTier.h`)。T0 用不了从不是 `Fatal`,而是 T2 加一行:`MOBILEGL_IPC_ADOPT_TIER=0` 时是具名行,未设时只打 MGLOG_D,照样计数。
|
||||
The tier is settled during the handshake, before the first `map_persistent`, by the side that knows the data plane (`MG_Remote/Transport/AdoptTier.h`). T0 being unusable is never a `Fatal`; it is T2 plus one line: a named line when `MOBILEGL_IPC_ADOPT_TIER=0`, and only an MGLOG_D when unset, counted either way.
|
||||
|
||||
| 数据面 | 一侧 | knob 0 / 未设 | knob 1 |
|
||||
| Data plane | Side | knob 0 / unset | knob 1 |
|
||||
|---|---|---|---|
|
||||
| stream | 两端 | 只有 T2(不共享内存);knob 0 打 `Refuse{AdoptTierOnStream, "T0"}` | `Refuse{AdoptTierOnStream, "T1"}`,T2 |
|
||||
| 共享段 | client | 在 `Hello` 的 `LinkTerms.adoptTier` 请求 T0(0;未设为 `kAdoptAskT0Default`);是否真跑 T0 在第一条 `map_persistent` 时读 `kCapAdoptT0`(`ClientSession::AdoptT0`) | `Refuse{AdoptTierClosed, "T1"}`,请求 T2 |
|
||||
| 共享段 | server | 不看自己的 knob:按 client 的请求,在第一次原生 bind 时批准(`ServerSession::SettleAdoptT0AtBind`,之后的 `CapsSnapshot` 带 `kCapAdoptT0`)或具名说明原因 | 同左 |
|
||||
| stream | both ends | only T2 (no shared memory); knob 0 prints `Refuse{AdoptTierOnStream, "T0"}` | `Refuse{AdoptTierOnStream, "T1"}`, T2 |
|
||||
| shared segment | client | requests T0 in `Hello`'s `LinkTerms.adoptTier` (0; `kAdoptAskT0Default` when unset); whether T0 really runs is read from `kCapAdoptT0` at the first `map_persistent` (`ClientSession::AdoptT0`) | `Refuse{AdoptTierClosed, "T1"}`, requests T2 |
|
||||
| shared segment | server | does not look at its own knob: it follows the client's request, approving it at the first native bind (`ServerSession::SettleAdoptT0AtBind`, with the later `CapsSnapshot` carrying `kCapAdoptT0`) or stating the reason by name | same as left |
|
||||
|
||||
T2 路径上的使用时检查 `Client::AdoptTierIsEmulate` 读回结算结果:已结算的会话照常走 T2;knob 0 / 1 下没有任何握手结算过的 `map_persistent`(无会话的调用方)具名中止为 `Fatal{UnimplementedAdoptTier}`,不静默当成 T2。
|
||||
The use-time check on the T2 path, `Client::AdoptTierIsEmulate`, reads back the settlement result: a settled session takes T2 as usual; under knob 0 / 1, a `map_persistent` that no handshake settled (a caller with no session) aborts by name as `Fatal{UnimplementedAdoptTier}`, rather than silently counting as T2.
|
||||
|
||||
## 13. 回读、roundtrip 清单与验证
|
||||
## 13. Readback, the roundtrip list and verification
|
||||
|
||||
### 13.1 稳态零 roundtrip 与不可避免的阻塞点
|
||||
### 13.1 Steady-state zero roundtrips and the unavoidable blocking points
|
||||
|
||||
- 零 roundtrip:全部 draw / clear / blit / copy / dispatch / XFB / bind / CSO / `set_*` / 上传 / `present`;全部 caps 站点;`glGetError` / `glFinish` / `glFlush`;fence 与 query 的创建和非阻塞轮询;`eglSwapBuffers`;restart / multi-draw。
|
||||
- 不可避免(罕见):握手;surface 生命周期;`glReadPixels` 到客户内存;GPU-write pending 的 buffer 首次 CPU 读;带超时的 sync / query 结果;`glBufferStorage` 的 ack;纹理拉取;ring / stage 耗尽与 present credit。
|
||||
- 跨机上每条 `kWaitReply`(`ResourceCreate`、`SetTextureParams`、纹理 `ResourceSubData`)都是一个 RTT:逐帧次数是 P6.5 必测数,减少它是 P9 / P10 的活。
|
||||
- Zero roundtrips: all draw / clear / blit / copy / dispatch / XFB / bind / CSO / `set_*` / upload / `present`; all caps sites; `glGetError` / `glFinish` / `glFlush`; fence and query creation and non-blocking polling; `eglSwapBuffers`; restart / multi-draw.
|
||||
- Unavoidable (rare): the handshake; the surface lifetime; `glReadPixels` into client memory; the first CPU read of a buffer with GPU writes pending; sync / query results with a timeout; the `glBufferStorage` ack; texture pulls; ring / stage exhaustion and present credit.
|
||||
- Across machines every `kWaitReply` (`ResourceCreate`, `SetTextureParams`, a texture `ResourceSubData`) is one RTT: the per-frame count is a mandatory measurement for P6.5, and reducing it is P9 / P10 work.
|
||||
|
||||
#### 13.1.1 kWaitReply 行的逐记录豁免
|
||||
#### 13.1.1 Per-record exemptions from the kWaitReply row
|
||||
|
||||
`kWaitReply` 是行级属性;一条记录是否真的阻塞由逐记录谓词决定,route 与 wire emit 表读同一个函数,避免一边等另一边没要的回包。因此上面第三条所列的纹理 `ResourceSubData` 在 wire 臂上已不逐条付 RTT。
|
||||
- `resource_subdata`(`MGPipeSubDataWantsItsReply`):buffer 那半的 Bool 无人读,不等。wire 臂上纹理那半也不等:纹理记录只在 server 消费纹理族时才发(caps 镜像里的 consumer 位,与 applier 的接受检查读同一事实),所以 applier 唯一的合法拒绝"无 consumer"落不到这条记录上;其余拒绝(如目标不是纹理、无纹素或无字节、未知句柄)都是 bug,server 侧会 trip wire 报告或计数具名。client 自己取消的记录(device lost、stage 耗尽)仍答"未接受",level 留脏重试。等待由此从每条记录移到 `SEG_STAGE` 窗口回收,与窗口成正比而不是与调用数成正比(实测见 `MEASUREMENTS.md`「纹理上传的等待粒度」一条)。
|
||||
- `resource_respecify`(`MGPipeResourceRespecifyWantsItsReply`):wire 臂上 buffer 那半不等;纹理那半等到对象的 create 被接受(`MGPipeHandleIsPublished`),之后 acceptance 已是 client 知道的事实,首次使用时付一次阻塞的 create;create 被拒则闩不置位,后续照常等真答案,代价是吞吐而不是静默。
|
||||
- monolith 下答案在调用线程上直接返回、没有代价,谓词保持原答案(纹理那半照收)。
|
||||
`kWaitReply` is a row-level property; whether a record actually blocks is decided by a per-record predicate, and the route and the wire emit table read the same function, so one side does not wait for a reply the other never asked for. That is why the texture `ResourceSubData` listed in the third point above no longer pays an RTT per record on the wire arm.
|
||||
- `resource_subdata` (`MGPipeSubDataWantsItsReply`): nobody reads the Bool in the buffer half, so it does not wait. On the wire arm the texture half does not wait either: a texture record is sent only when the server consumes the texture family (the consumer bit in the caps mirror, reading the same fact as the applier's acceptance check), so the applier's only legitimate refusal, "no consumer", cannot fall on this record; the other refusals (such as the target not being a texture, no texels or no bytes, an unknown handle) are bugs, and the server side trips a wire report or counts them by name. A record the client itself cancels (device lost, stage exhaustion) still answers "not accepted", and the level stays dirty for a retry. The wait thus moves from each record to the `SEG_STAGE` window reclamation, proportional to the window instead of to the number of calls (measured under "the wait granularity of texture uploads" in `MEASUREMENTS.md`).
|
||||
- `resource_respecify` (`MGPipeResourceRespecifyWantsItsReply`): on the wire arm the buffer half does not wait; the texture half waits until the object's create has been accepted (`MGPipeHandleIsPublished`), after which acceptance is already a fact the client knows, so first use pays one blocking create; if the create is refused the latch is not set, and later calls wait for the real answer as usual, at the cost of throughput rather than silence.
|
||||
- Under the monolith the answer returns directly on the calling thread at no cost, so the predicate keeps its original answer (the texture half still takes it).
|
||||
|
||||
#### 13.1.2 client 侧保守 GPU 写集合
|
||||
#### 13.1.2 The client-side conservative GPU write set
|
||||
|
||||
`BufferObject::SyncGpuWrites` 在应用线程上、于 `glMapBuffer` / `glGetBufferSubData` 时回答"GPU 是否可能写过这块 buffer"。后端的标记点在 server 侧,逐次去问是稳态禁止的 roundtrip,所以 client 在每条 draw / dispatch 记录发出之前,从自己持有的绑定构建这个集合(`MG_Impl/Pipe/Verb/GpuWriteSet.h`)。
|
||||
At `glMapBuffer` / `glGetBufferSubData` on the application thread, `BufferObject::SyncGpuWrites` answers "may the GPU have written this buffer". The backend's mark points are on the server side, and asking each time is a roundtrip forbidden in the steady state, so the client builds this set from the bindings it holds before each draw / dispatch record is sent (`MG_Impl/Pipe/Verb/GpuWriteSet.h`).
|
||||
|
||||
- 宁多勿少:多标一次的代价是一次回读,漏标则静默读到陈旧 shadow。每个生产者有计数器(`ProducerMarkCount`),单元用例断言它确实触发。
|
||||
- 前四个生产者对应后端标记点:SSBO 绑定点、atomic counter 绑定点(标全部 touched 点,比后端宽)、`Access != GL_READ_ONLY` 的 image buffer texture、XFB 捕获目标。后端故意窄的两处照搬:只读 image 绑定不标;image 遍历在 draw 准备时做,不在 `glBindImageTexture` 时做。
|
||||
- 后端没有的两个生产者:读进 pack PBO 的 `glReadPixels` 发出即返回、只做标记,拷贝推迟到第一次真要读时;传输下 `glEndTransformFeedback` 不再无界等 fence,只标记捕获目标(monolith 仍等)。
|
||||
- 收窄尚未实现:`MGPipeClientOnGpuWritten` 只接受一个整 buffer 范围;零个范围留给将来的"已完全收窄",不能当作整块已写。
|
||||
- 不覆盖后端自己的 scratch buffer(Espryt 的转换顶点流与 primitive-restart 替换 buffer、Magma 的 UBO ring):后端那边同样没有 `MarkGpuWritten`,是两边共有的洞。
|
||||
- Better too many than too few: marking once too often costs one readback, while a missing mark silently reads a stale shadow. Each producer has a counter (`ProducerMarkCount`), and a unit test asserts that it really fires.
|
||||
- The first four producers correspond to backend mark points: SSBO binding points, atomic counter binding points (marking every touched point, wider than the backend), image buffer textures with `Access != GL_READ_ONLY`, and XFB capture targets. Two places where the backend is deliberately narrow are copied as they are: a read-only image binding is not marked; the image walk happens at draw preparation, not at `glBindImageTexture`.
|
||||
- Two producers the backend does not have: a `glReadPixels` reading into a pack PBO returns as soon as it is issued, only marking, and the copy is deferred to the first time a read really happens; under transport `glEndTransformFeedback` no longer waits for a fence without bound, only marking the capture targets (the monolith still waits).
|
||||
- Narrowing is not implemented yet: `MGPipeClientOnGpuWritten` accepts only one whole-buffer range; zero ranges is reserved for a future "fully narrowed" and must not be taken as the whole buffer written.
|
||||
- It does not cover the backend's own scratch buffers (Espryt's conversion vertex stream and primitive-restart replacement buffer, Magma's UBO ring): the backend side has no `MarkGpuWritten` for them either, so it is a hole the two sides share.
|
||||
|
||||
#### 13.1.3 glReadPixels:线上只有矩形,pack 状态留在 client
|
||||
#### 13.1.3 glReadPixels: only the rectangle goes on the wire, the pack state stays on the client
|
||||
|
||||
`read_pixels` 记录带的是矩形而不是布局:`MGPReadbackInfo::DstSize` 是紧凑的 `w*h*bytesPerPixel`,server 用中性 pack 状态读进这段紧凑行(就是 reply payload),client——唯一持有应用 pack 状态的一侧——再按应用的 `GL_PACK_*` 把各行散射到应用指针,行间空隙不写。若 `DstSize` 取打包后的尺寸,server 按它分配、后端又按实时 pack 状态写,就会越界(4x3 RGBA8、`PACK_ROW_LENGTH=8`、`SKIP_ROWS=1`、`SKIP_PIXELS=2`:分配 80 B,驱动写到第 120 B)。规则只关乎布局,深度与深度模板读同样适用。
|
||||
- 紧凑答案大于一个 reply slot(`LinkTerms.maxReplyBytes`)时切的是读,不是 reply:一行放得下就切成 `floor(maxReplyBytes / rowBytes)` 行的整宽行带,否则把每行切成 `floor(maxReplyBytes / bpp)` 像素的列段。每段是一条普通 `read_pixels` 记录,带自己的框与紧凑 `DstSize`,各自按 pack 状态散射;段按行主序恰好覆盖矩形一次,且在紧凑布局里连续,中性 pack 的快路径因此仍可直接读进应用指针(`PlanReadbackBands` / `ForEachReadbackBand`)。
|
||||
- 只有连一个像素都放不进 reply(含没有 reply 池的链路)时,client 才在发射前具名拒绝为 `Fatal{ReplyTooLarge}`:像素不能拆得比自身更小。
|
||||
The `read_pixels` record carries the rectangle, not the layout: `MGPReadbackInfo::DstSize` is the tight `w*h*bytesPerPixel`, and the server reads into these tight rows with a neutral pack state (that is the reply payload), after which the client—the only side that holds the application pack state—scatters the rows to the application pointer according to the application's `GL_PACK_*`, not writing the gaps between rows. If `DstSize` were the packed size, the server would allocate by it while the backend writes by the live pack state, and the write would overflow (4x3 RGBA8, `PACK_ROW_LENGTH=8`, `SKIP_ROWS=1`, `SKIP_PIXELS=2`: 80 B allocated, the driver writes up to byte 120). The rule is about layout only, and the same applies to depth and depth-stencil reads.
|
||||
- When the tight answer is larger than one reply slot (`LinkTerms.maxReplyBytes`), what is split is the read, not the reply: if one row fits, split into full-width row bands of `floor(maxReplyBytes / rowBytes)` rows; otherwise split each row into column runs of `floor(maxReplyBytes / bpp)` pixels. Each band is an ordinary `read_pixels` record with its own box and tight `DstSize`, each scattered according to the pack state; the bands cover the rectangle exactly once in row-major order and are contiguous in the tight layout, so the neutral-pack fast path can still read straight into the application pointer (`PlanReadbackBands` / `ForEachReadbackBand`).
|
||||
- Only when not even one pixel fits in a reply (including a link with no reply pool) does the client refuse by name as `Fatal{ReplyTooLarge}` before emitting: a pixel cannot be split smaller than itself.
|
||||
|
||||
### 13.2 五部分验证门(取代 monolith 的字节一致门)
|
||||
### 13.2 The five-part verification gate (replacing the monolith's byte-identity gate)
|
||||
|
||||
1. **接口纯度**:A 门 include 图(`scripts/check_include_closure.py`)、B 门 server 镜像不引用 `MG_State::GLState` 与 glslang(P6 的 a6 实测 server 仍链 `MG_Impl`,由 `scripts/link_ratchet.py` 的下行棘轮追踪;P13 把 172 条降到 27 条具名债务、`# P13` 注解清零并由脚本拒绝回潮,W7 的 `MobileGL_server_linkcheck` 把它变成真链接)、C 门 `MG_Backend` 里零 `pGLContext`;外加"每个后端 memo 键都是 `{slot, gen}`"(`HandleRecycleScenario`)。
|
||||
2. **语义影子比对** `MOBILEGL_PIPE_VERIFY=1`:tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 逐字段每 draw 比对,打印第一个分歧;verify 构建永不出货,活过 P13。
|
||||
3. **行为 A/B**:trace 语料在 monolith / split 下 SSIM ≥ 0.99(P13 前还有 monolith-pull / monolith-push 两臂);测试名只增不删(G14),skip 普查守住「跳过」与名集合;CTS 逐后端 0.5 pp 内;上传形状金标与 recorder 金标(§13.3)。
|
||||
4. **性能**:Redmi `2f7cbe2e` reboot-clean、同热窗口配对 A/B,指标是**逐线程 CPU 时间**;**只记录、不设门**(用户 2026-09-08)。P13 之后的性能轮以 `dev` 构建为基线(用户 2026-10-07)。
|
||||
5. **覆盖 + poison + 句柄纪律**:G6 0 UNMAPPED、dirty-surface 0 未映射、G8 归属完备、逐 verb poison、G7、残余块棘轮。
|
||||
1. **Interface purity**: gate A checks the include graph (`scripts/check_include_closure.py`), gate B that the server image references neither `MG_State::GLState` nor glslang (P6's a6 measured that the server still links `MG_Impl`, tracked by the downward ratchet in `scripts/link_ratchet.py`; P13 brings 172 down to 27 named debts, zeroes the `# P13` annotations, and a script refuses regressions, while W7's `MobileGL_server_linkcheck` turns it into a real link), and gate C zero `pGLContext` in `MG_Backend`; plus "every backend memo key is `{slot, gen}`" (`HandleRecycleScenario`).
|
||||
2. **Semantic shadow comparison** `MOBILEGL_PIPE_VERIFY=1`: the tracker fills a second `PipeInputs` with `SnapshotFromGLContext()`, and G4 compares field by field at every draw, printing the first divergence; verify builds never ship and survive P13.
|
||||
3. **Behavior A/B**: the trace corpus has SSIM ≥ 0.99 under monolith / split (before P13 there were also the monolith-pull / monolith-push arms); test names only grow, never shrink (G14), and the skip census guards "skips" and the name set; CTS is within 0.5 pp per backend; the upload-shape goldens and the recorder goldens (§13.3).
|
||||
4. **Performance**: Redmi `2f7cbe2e`, reboot-clean, paired A/B in the same thermal window, with **per-thread CPU time** as the metric; **recorded only, no gate** (user, 2026-09-08). Performance rounds after P13 use the `dev` build as the baseline (user, 2026-10-07).
|
||||
5. **Coverage + poison + handle discipline**: G6 0 UNMAPPED, dirty-surface 0 unmapped, G8 ownership complete, per-verb poison, G7, the residual-block ratchet.
|
||||
|
||||
幸存的字节级等式:`MOBILEGL_BUILD_DISAGGREGATED=OFF` 时库里零 `MG_Remote` 符号(P13 起它就是出货库)。**G1**(pull 构建的符号集与 `.text` 对基线恒等)随 pull 构建在 P13 退役,由零 `MG_Remote` 符号、skip 普查与 recorder 金标接班。**每个门必须能因它存在的理由变红**(R-16):带阴性对照并真跑过一次红;公共 GL 看不见的改白盒断言;拒绝普查逐用例读私有日志(`ctest -V` 是假零)。
|
||||
The byte-level identity that survives: with `MOBILEGL_BUILD_DISAGGREGATED=OFF` the library has zero `MG_Remote` symbols (since P13 it is the shipping library). **G1** (the pull build's symbol set and `.text` identical to the baseline) retires with the pull build in P13, and zero `MG_Remote` symbols, the skip census and the recorder goldens take over. **Every gate must be able to go red for the reason it exists** (R-16): it has a negative control and has really run red once; a change the public GL cannot see gets a white-box assertion; the refusal census reads the private log per case (`ctest -V` is a false zero).
|
||||
|
||||
### 13.3 长期语义门:MGPipe recorder(P13)
|
||||
### 13.3 The long-term semantic gate: the MGPipe recorder (P13)
|
||||
|
||||
`MG_Test/Pipe/RecorderGoldenTest.cpp`(P13 W8):记录器装在记录离开 client 的每一扇门上——两张 MGPipe 接口表与路由 escape(全部状态 / 对象记录)、verb 会话(全部 verb 记录)。固定的 GL 调用序列(缓冲 + VAO + 采样纹理 + program + 四种 draw;两张纹理的 framebuffer + 两种 clear + blit + mipmap)逐条录下,规范化只做一件事:指向调用方内存的 `MGPBlobRef` / `MGHostSpan` 地址清零、长度保留、所指字节入哈希。金标是一行一条记录(序号、调用名、字节数、哈希),在 `MG_Test/Pipe/goldens/recorder/`;`MOBILEGL_RECORDER_GOLDEN_WRITE=1` 才重写,且重写的那次必红。阴性对照:改一条 verb 记录的一个字段必须在那条红;改一个 GL 输入必须在资源记录处红。两种库形态(有无 `MG_Remote`)对同一金标——它第一次跑就发现无传输库的单进程不发 `set_context_values`(已修)以及 `RenderStateParameters` 的填充字节未清零(会让内容寻址的 CSO 在两个状态相同的 context 间不复用,已修)。它只覆盖推送内容,不覆盖后端对它的解释(开放问题 12)。
|
||||
`MG_Test/Pipe/RecorderGoldenTest.cpp` (P13 W8): the recorder sits at every gate through which a record leaves the client—the two MGPipe interface tables and the route escapes (all state / object records), and the verb session (all verb records). A fixed GL call sequence (a buffer + a VAO + a sampled texture + a program + four kinds of draw; a framebuffer with two textures + two kinds of clear + blit + mipmap) is recorded record by record, and normalization does exactly one thing: the addresses of `MGPBlobRef` / `MGHostSpan` pointing at caller memory are zeroed, the lengths kept, and the bytes they point at hashed. A golden is one line per record (ordinal, call name, byte count, hash), under `MG_Test/Pipe/goldens/recorder/`; it is rewritten only with `MOBILEGL_RECORDER_GOLDEN_WRITE=1`, and the rewriting run must go red. Negative controls: changing one field of a verb record must go red on that record; changing one GL input must go red at the resource record. Both library shapes (with and without `MG_Remote`) compare against the same golden—on its first run it found that the monolith library without a transport did not send `set_context_values` (fixed), and that `RenderStateParameters`'s padding bytes were not zeroed (which would stop a content-addressed CSO from being reused between two contexts with the same state; fixed). It covers only the pushed content, not the backend's interpretation of it (open question 12).
|
||||
|
||||
@@ -1,87 +1,87 @@
|
||||
# 帧节奏、进程、EGL 与平台(原 ARCHITECTURE §14–§15)
|
||||
# Frame pacing, processes, EGL and platform (formerly ARCHITECTURE §14–§15)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design points and the index of all chapters are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; an `ARCHITECTURE.md §N` in a code comment is found by its number here.
|
||||
|
||||
## 14. Present、线程与帧节奏
|
||||
## 14. Present, threads and frame pacing
|
||||
|
||||
- `eglSwapBuffers` → `present{frameSerial}` → publish + 敲门铃 → 返回,除非超出 credit;**`present` 与 `eglSwapBuffers` 严格 1:1**(后端的帧边界排空只在 `Present` 内发生)。
|
||||
- `MOBILEGL_IPC_PRESENT_CREDIT` 默认 1(range 1..8):credit 是 run-ahead 唯一的稳态背压,`FrameSerial` 由 client 铸造、1 起算,server 在 `Present()` 返回后归还 credit;等待发生在**编码之前**。credit > 1 从未量过(P10)。
|
||||
- 预算(~850 draw/帧):`SEG_CMD` ~70 KB/帧,`SEG_STAGE` ~1 MB/帧;跨机时这 ~1 MB/帧对着的是 Wi-Fi。
|
||||
- 线程:client v1 不加线程,编码在 GL 线程直接写 ring;server 有 io 线程与**终身持有原生 context** 的 `mgl-srv-apply`。全库无有效亲和性控制(Redmi 内核忽略 app 线程的 `sched_setaffinity`)。
|
||||
- 拆机顺序:publish + server 排空 → 停 apply 线程 → 关传输 → client 排空编译池 → `MobileGL::Destroy()`(落地形状见 §17)。
|
||||
- `eglSwapBuffers` → `present{frameSerial}` → publish + ring the doorbell → return, unless the credit is exceeded; **`present` and `eglSwapBuffers` are strictly 1:1** (the backend's frame-boundary drain happens only inside `Present`).
|
||||
- `MOBILEGL_IPC_PRESENT_CREDIT` defaults to 1 (range 1..8): the credit is the only steady-state backpressure for run-ahead, `FrameSerial` is minted by the client and starts at 1, and the server returns the credit after `Present()` returns; the wait happens **before encoding**. credit > 1 has never been measured (P10).
|
||||
- Budget (~850 draws/frame): `SEG_CMD` ~70 KB/frame, `SEG_STAGE` ~1 MB/frame; across machines that ~1 MB/frame faces Wi-Fi.
|
||||
- Threads: client v1 adds no thread, and encoding writes the ring directly on the GL thread; the server has an io thread and `mgl-srv-apply`, which **holds the native context for life**. The whole library has no effective affinity control (the Redmi kernel ignores `sched_setaffinity` for app threads).
|
||||
- Teardown order: publish + server drain → stop the apply thread → close the transport → the client drains the compile pool → `MobileGL::Destroy()` (the landed shape is in §17).
|
||||
|
||||
### 14.1 apply 线程的亲和与自旋(`auto`)
|
||||
### 14.1 Affinity and spin of the apply thread (`auto`)
|
||||
|
||||
`MOBILEGL_IPC_SERVER_AFFINITY=auto`(缺省)按会话对端(`Hello` 的 `DialMode`)与数据面选择 apply 线程的亲和与 park 前自旋(`MG_Remote/Server/ApplyThreadPolicy.h`,纯函数):同进程、fork 出的、拨入但走 stream 的对端 → 全部大核与 `MOBILEGL_IPC_SPIN_US`;拨入且走共享段的对端 → 一个核(`ReservedApplyCore`:多个 prime 核时取最低的;只有一个 prime 核时取另一个大核;否则不钉,绝不给唯一的 prime 核)与 `kConnectedPeerSpinUs`(除非显式设了 `MOBILEGL_IPC_SPIN_US`)。显式掩码或 `off` 对任何对端优先。
|
||||
- 拨入的共享段 client 直接写 apply 线程自旋的那条环,两边又落在同两颗 prime 核上:Redmi 上共享段 23–26 fps、TCP 45–54,手工分开后 118。stream 数据面由 io 线程喂 apply 线程,apply 不在 client 的写上自旋(apply CPU 7.9 ms/帧,共享段 26.8),所以维持全部大核。
|
||||
- 取最低而非最高的 prime 核:调度器把前台应用的热 GL 线程放在最高的 prime 核(44 次采样 43 次在 cpu7);apply 线程钉在那里 6.5 fps,钉在最低的 120 fps。
|
||||
- 短自旋保留:立即 park(0)在所有量过的格子里都更慢(helper 路径 120 → 74 fps,shell 27 → 17)。
|
||||
- 经 `ExternalClientHelper` 启动的 client 也被挡在保留核之外:supervisor 启动时公布该核(`ReservedApplyCoreForConfig`),broker 交给 helper;操作员给了掩码或 `off` 时不保留任何核。
|
||||
`MOBILEGL_IPC_SERVER_AFFINITY=auto` (the default) chooses the apply thread's affinity and pre-park spin from the session peer (`Hello`'s `DialMode`) and the data plane (`MG_Remote/Server/ApplyThreadPolicy.h`, pure functions): a same-process peer, a forked peer, or a dialed-in peer on stream → all big cores and `MOBILEGL_IPC_SPIN_US`; a dialed-in peer on a shared segment → one core (`ReservedApplyCore`: the lowest when there are several prime cores; another big core when there is only one prime core; otherwise no pinning, and never the only prime core) and `kConnectedPeerSpinUs` (unless `MOBILEGL_IPC_SPIN_US` is set explicitly). An explicit mask or `off` takes precedence for any peer.
|
||||
- A dialed-in shared-segment client writes directly to the ring the apply thread spins on, and the two land on the same two prime cores: on the Redmi the shared segment gives 23–26 fps and TCP 45–54, and 118 after separating them by hand. On the stream data plane the io thread feeds the apply thread, and apply does not spin on the client's writes (apply CPU 7.9 ms/frame, versus 26.8 on the shared segment), so all big cores are kept.
|
||||
- Taking the lowest rather than the highest prime core: the scheduler puts the foreground app's hot GL thread on the highest prime core (43 of 44 samples on cpu7); pinning the apply thread there gives 6.5 fps, while pinning it to the lowest gives 120 fps.
|
||||
- The short spin is kept: parking immediately (0) is slower in every measured cell (the helper path 120 → 74 fps, the shell 27 → 17).
|
||||
- A client started through `ExternalClientHelper` is kept off the reserved core too: the supervisor announces the core at startup (`ReservedApplyCoreForConfig`) and the broker passes it to the helper; when the operator gives a mask or `off`, no core is reserved.
|
||||
|
||||
## 15. 进程、EGL 与平台(P6 / P6.5 / P12)
|
||||
## 15. Processes, EGL and platform (P6 / P6.5 / P12)
|
||||
|
||||
### 15.1 启动与握手
|
||||
### 15.1 Startup and handshake
|
||||
|
||||
- 端点:`fork`(`socketpair` + `fork`/`execve`,fd 3 = socket;server 由 `MOBILEGL_IPC_SERVER_PATH` 或 `dladdr` 同目录的 `libMobileGLServer.so` 定位)、`unix:<path>`、`tcp://host:port`。设备侧 supervisor `mobilegl_server_main <endpoint> --serve`:listen → 认证 → 每会话 fork 一个子进程(或进程内线程,P12),同时只服务一个会话,第二个连接 `Refuse{Busy}`。
|
||||
- server 进程里 `Transport` 读作 `Spawn`,**不强制 monolith**(两百多处 `Transport != Monolith` 判断靠它选 server 臂;`CONTRACT-P6.md` §3.1 修订了原"子进程强制 monolith"的规则)。防无界 fork 链靠不继承的 `Dial == No`(`MOBILEGL_IPC_DIAL=no`),spawn 时构造的 envp 剔除 `MOBILEGL_TRANSPORT` 与 `MOBILEGL_IPC_*` 作第二道独立保险;server 主程序自设 `MOBILEGL_IPC_ROLE=server`。
|
||||
- `Hello`(abi、后端类型、指纹、配置、令牌、`LinkTerms` 请求)→ `Welcome`(server 陈述的尺寸、段 / aux 名、`dataNonce`、真实 pid)或 `Refuse{code, detail, 对端值}`;握手路径零 abort。非 loopback 监听必须有令牌(≥16 字节、常量时间比较、数据面按 `dataNonce` 绑定到已认证的控制连接、fork 前认证;Ph,OQ-21 裁定不做 TLS)。
|
||||
- TCP keepalive / 心跳失败**由传输层报告为挂断**,进 device-lost 闩;"闩绝不取自 apply 超时"不因此松动。冷启动期 server 发 `SurfaceProgress`,client 预算这段沉默(控制修订 2)。
|
||||
- Endpoints: `fork` (`socketpair` + `fork`/`execve`, fd 3 = socket; the server is located by `MOBILEGL_IPC_SERVER_PATH` or by `libMobileGLServer.so` in the same directory via `dladdr`), `unix:<path>`, `tcp://host:port`. The device-side supervisor is `mobilegl_server_main <endpoint> --serve`: listen → authenticate → fork one child process per session (or an in-process thread, P12), serving one session at a time, with a second connection getting `Refuse{Busy}`.
|
||||
- Inside the server process `Transport` reads as `Spawn`, and **this does not force the monolith** (the two hundred-odd `Transport != Monolith` checks rely on it to pick the server arm; `CONTRACT-P6.md` §3.1 revises the original "a child process forces the monolith" rule). An unbounded fork chain is prevented by the non-inherited `Dial == No` (`MOBILEGL_IPC_DIAL=no`), and the envp built at spawn strips `MOBILEGL_TRANSPORT` and `MOBILEGL_IPC_*` as a second, independent safeguard; the server main program sets `MOBILEGL_IPC_ROLE=server` itself.
|
||||
- `Hello` (abi, backend type, fingerprints, configuration, token, `LinkTerms` request) → `Welcome` (the sizes the server states, segment / aux names, `dataNonce`, the real pid) or `Refuse{code, detail, peer value}`; the handshake path has zero aborts. A non-loopback listen requires a token (≥16 bytes, constant-time comparison, the data plane bound to the authenticated control connection by `dataNonce`, authentication before fork; Ph, with OQ-21 ruling out TLS).
|
||||
- A TCP keepalive / heartbeat failure **is reported by the transport as a hang-up** and enters the device-lost latch; "the latch is never taken from an apply timeout" is not loosened by this. During cold start the server sends `SurfaceProgress`, and the client budgets that silence (control revision 2).
|
||||
|
||||
#### 15.1.1 TCP supervisor 的预认证
|
||||
#### 15.1.1 Pre-authentication in the TCP supervisor
|
||||
|
||||
TCP `--serve` 的 supervisor(`ServerMain.cpp` 的 `TcpSupervisor`,判定部分在 `PreAuthGate.h`)先认证、再把会话交出去(fork 子进程,或进程内线程);未认证的对端最多占一个有界的 pending 槽:
|
||||
The supervisor of TCP `--serve` (`TcpSupervisor` in `ServerMain.cpp`, with the decision part in `PreAuthGate.h`) authenticates first and only then hands the session over (a forked child, or an in-process thread); an unauthenticated peer holds at most one bounded pending slot:
|
||||
|
||||
- 新连接在首帧完整之前是 pending。supervisor 在一个 poll 循环里同时从所有 pending 连接非阻塞地读,任何未认证对端都拖不住别人;只读到首帧末尾,之后的字节属于会话。
|
||||
- 每个 pending 连接有 `MOBILEGL_IPC_PREAUTH_MS`(默认 2000)读完首帧,同时最多 `MOBILEGL_IPC_PREAUTH_MAX`(默认 8)个。队列按地址共享:满时新连接挤掉占槽最多的地址最老的那个;它自己的地址已占同样多时,被拒 `Busy` 的是它。
|
||||
- Hello 在 supervisor 里认证、令牌先于指纹:未认证的对端只收到 `Refuse{Authentication}`,得不到本构建的 wire / build 指纹。只为已认证的 Hello 交出会话,并把这一帧交给会话。
|
||||
- 失败(令牌错、首帧畸形、超时、静默占槽超过 `kPreAuthProbeGraceMs` 后关闭或被挤掉)记在对端地址上;满 `MOBILEGL_IPC_AUTH_BACKOFF_AFTER`(默认 5,0 = 关)次后该地址在 accept 时、读任何字节之前即被拒,窗口自 `MOBILEGL_IPC_AUTH_BACKOFF_MS`(默认 1000)起逐次翻倍、上限一分钟;该地址认证成功一次即清零。宽限期内无字节就关闭的端口探测、指向无活会话的 DataBind、退避本身发出的拒绝都不算失败。
|
||||
- DataBind 经 hand-off 连同 nonce 转给活会话(会话比较 nonce);没有活会话时由 supervisor 具名拒绝。已认证的第二个 Hello 收到 `Busy`。
|
||||
- 判定部分不含 I/O(`MG_Remote/Server/PreAuthGate.h`),由 `PreAuthGateTest` 覆盖;早于 `kAuthBackoffForgetMs` 的失败不再计入退避。
|
||||
- A new connection is pending until its first frame is complete. The supervisor reads from all pending connections non-blocking in a single poll loop, so no unauthenticated peer can hold up the others; it reads only up to the end of the first frame, and the bytes after that belong to the session.
|
||||
- Each pending connection has `MOBILEGL_IPC_PREAUTH_MS` (default 2000) to finish the first frame, and there are at most `MOBILEGL_IPC_PREAUTH_MAX` (default 8) at a time. The queue is shared by address: when it is full, a new connection evicts the oldest one of the address holding the most slots; if its own address already holds as many, it is the one refused with `Busy`.
|
||||
- Hello is authenticated in the supervisor, token before fingerprints: an unauthenticated peer only receives `Refuse{Authentication}` and never gets this build's wire / build fingerprints. The session is handed over only for an authenticated Hello, and that frame is passed to the session.
|
||||
- A failure (wrong token, malformed first frame, timeout, a silent slot-holder closed after `kPreAuthProbeGraceMs`, or being evicted) is recorded against the peer address; after `MOBILEGL_IPC_AUTH_BACKOFF_AFTER` (default 5, 0 = off) failures that address is refused at accept time, before any byte is read, with the window doubling from `MOBILEGL_IPC_AUTH_BACKOFF_MS` (default 1000) up to one minute; one successful authentication of that address clears it. A port probe that closes without sending a byte within the grace period, a DataBind aimed at no live session, and a refusal issued by the backoff itself do not count as failures.
|
||||
- DataBind is handed off with the nonce to the live session (which compares the nonce); with no live session the supervisor refuses by name. A second authenticated Hello receives `Busy`.
|
||||
- The decision part contains no I/O (`MG_Remote/Server/PreAuthGate.h`) and is covered by `PreAuthGateTest`; failures older than `kAuthBackoffForgetMs` no longer count toward the backoff.
|
||||
|
||||
### 15.2 Android
|
||||
|
||||
- 交付链(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 一退后台游戏就暂停(见 git 历史)。一台设备同一时刻一个 server;一个会话一种表面模式(`SurfaceModeMismatch`);无显示的 server 对 ServerOwned 具名拒绝(`NoServerDisplay`)。
|
||||
- 失窗:`surfaceDestroyed` 有界(3 s)阻塞,直到 apply 线程(批内每条记录前也检查)放掉后端表面并以 `Fatal{ServerWindowLost}` 闩住会话;server 继续监听。进程内会话结束时手动做进程退出本来白给的清理(Espryt twin 与单元影子、swap interval、Magma renderer)。
|
||||
- `HeadlessGL` 的 fork 预检会产生孤儿 server:server 的 EOF 检测即时且无条件退出。
|
||||
- Delivery chain (proven in spike A): the only executable location in an APK is `lib/<abi>/`, and the server is built with `add_executable` + a `lib*.so` name; `fork`+`execve` from an `untrusted_app` process stays in the same domain with zero avc denials; `posix_spawn` is unavailable at minSdk 26. One shared library, two roles (the stub does `dlopen` + `dlsym("mobilegl_server_main")`).
|
||||
- Offscreen server: the foreground `MobileGLServerService` (`:mglsrv`), exec'd from `nativeLibraryDir`, fork-per-session, with pbuffer / surfaceless EGL.
|
||||
- **On-screen server (P12 subset)**: the window belongs to **the server itself** (the client's window does not cross processes). `MobileGLDisplayActivity` (`:mglwin`) holds a full-screen SurfaceView and runs a TCP server on one thread in its own process (sessions serial, the latch reset between sessions, the backend pinned per process); `ServerDisplay` holds the window through three hooks—acquire / release / geometry—and the apply thread uses it under a **lease**. After the client sets `MOBILEGL_IPC_SURFACE=server` it is **headless**: `eglCreateWindowSurface(NULL)` sends one `WindowKind::ServerOwned` frame (control revision 3) and does not send `SetWindowHandle`, the size comes back in the reply, and a resize becomes a geometry request to the server window; when the client gives no `EGL_WIDTH/HEIGHT` (SDL), the native window size is taken as the request, and the server fixes the window buffer to it and letterboxes it with black bars (otherwise a landscape game would be cropped in a portrait window). FCL can only attach to an external server (its own spawn would send the window pointer over, which Rule H refuses), and it needs the version setting "do not pause the game when it goes to the background", otherwise the game pauses as soon as FCL goes to the background (see git history). One server per device at a time; one surface mode per session (`SurfaceModeMismatch`); a server with no display refuses ServerOwned by name (`NoServerDisplay`).
|
||||
- Window loss: `surfaceDestroyed` blocks with a bound (3 s) until the apply thread (which also checks before every record in a batch) drops the backend surface and latches the session with `Fatal{ServerWindowLost}`; the server keeps listening. When an in-process session ends, the cleanup that a process exit would give for free is done by hand (Espryt twins and unit shadows, swap interval, the Magma renderer).
|
||||
- `HeadlessGL`'s fork pre-check produces orphan servers: the server's EOF detection exits immediately and unconditionally.
|
||||
|
||||
### 15.3 Linux / Windows / 崩溃
|
||||
### 15.3 Linux / Windows / crashes
|
||||
|
||||
- Linux / WSL / CI 永不开窗(`EGL_PLATFORM=surfaceless`);Wayland 不支持。Windows / 桌面 client 在 TCP 终局下是真实形态(Windows 是第二批,开放问题 22);macOS 不拆分;Windows 机器不是正确性门。
|
||||
- server 死:client 读到挂断 → **device-lost 闩**(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`、reset status `GL_UNKNOWN_CONTEXT_RESET`);`Session::Fail` 漏斗向对端发 `SessionFault` 帧命名家族(`FatalFamilies.def`)。闩之前创建的 context 永久丢失(`eglMakeCurrent` / `eglSwapBuffers` 返回 `EGL_CONTEXT_LOST`,`glGetError` 报一次 `GL_CONTEXT_LOST`,销毁时不往新 session 发任何记录)。**进程可恢复**(`MOBILEGL_IPC_RECOVER`,默认 1):应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 拆掉丢失的 session、重新拨号得到新 session,client 侧描述旧 server 的缓存全部清空(发布闩、CSO/sampler 缓存、emitter 闩、create window、applier-reset 序号、persistent map),wire epoch 前进使旧 GLContext 一律视为丢失;surface 保留并在下次 make-current 时于新 session 重建(EGL 对 power-management 事件的语义),Wayland 窗口的 shared image 在新 session 重新分配。in-process server 与 `fd:` 对不可重拨,仍是永久丢失。`MOBILEGL_IPC_RESPAWN`(server 重启后全量重推)仍是具名拒绝。client 死:server 读到 EOF 立即销毁 context 并退出(supervisor 继续服务下一连接)。
|
||||
- Linux / WSL / CI never open a window (`EGL_PLATFORM=surfaceless`); Wayland is not supported. A Windows / desktop client is a real shape under the TCP end goal (Windows is the second batch, open question 22); macOS is not split; a Windows machine is not a correctness gate.
|
||||
- Server death: the client reads a hang-up → **device-lost latch** (GL calls no-op, `eglSwapBuffers` returns `EGL_CONTEXT_LOST`, reset status `GL_UNKNOWN_CONTEXT_RESET`); the `Session::Fail` funnel sends the peer a `SessionFault` frame naming the family (`FatalFamilies.def`). A context created before the latch is permanently lost (`eglMakeCurrent` / `eglSwapBuffers` return `EGL_CONTEXT_LOST`, `glGetError` reports `GL_CONTEXT_LOST` once, and destruction sends no record to a new session). **The process can recover** (`MOBILEGL_IPC_RECOVER`, default 1): the application's next `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` tears down the lost session and dials again for a new one, every client-side cache describing the old server is cleared (the publish latch, the CSO/sampler caches, the emitter latch, create window, the applier-reset sequence number, the persistent map), and the wire epoch advances so that every old GLContext is treated as lost; surfaces are kept and rebuilt in the new session at the next make-current (EGL's semantics for power-management events), and a Wayland window's shared image is reallocated in the new session. An in-process server and the `fd:` pair cannot be re-dialed and remain permanently lost. `MOBILEGL_IPC_RESPAWN` (a full re-push after a server restart) is still refused by name. Client death: the server reads EOF and immediately destroys the context and exits (the supervisor keeps serving the next connection).
|
||||
|
||||
#### 15.3.1 Fatal 家族与 wire FatalCode
|
||||
#### 15.3.1 Fatal families and the wire FatalCode
|
||||
|
||||
内部 Fatal 家族(`MG_Pipe/FatalFamilies.def`)远多于 wire 上 `FatalCode` 的 7 个值,所以家族是内部枚举:对端只收到粗粒度的码,细粒度的词留在本地日志与 `SessionFault` 帧的 `family` 字符串里。码只回答「这是哪一类结束」:
|
||||
The internal Fatal families (`MG_Pipe/FatalFamilies.def`) are far more numerous than the 7 values of the wire `FatalCode`, so a family is an internal enum: the peer only receives the coarse code, while the fine-grained word stays in the local log and in the `family` string of the `SessionFault` frame. The code answers only "which kind of end this is":
|
||||
|
||||
- `ProtocolCorruption`:无法正确执行的东西——畸形记录、未实现的 verb / surface、两个角色必须守住的不变式、不合契约的回复。对端读到的是「双方对协议的理解不一致」。
|
||||
- `RingOverrun`:共享环或事件通道溢出,是传输容量问题。
|
||||
- `SegmentMismatch`:共享段的尺寸放不下某个角色必须放进去的东西(尺寸错,不是一时满)。
|
||||
- `AbiMismatch`:两端的布局或协议版本不一致。
|
||||
- `ServerCrashed`:等待方归咎于对端的活性结束:排空未完成、apply 线程不在、server 拿不到必需的原生资源(`ResourceUnavailable`)、server 窗口被销毁(`ServerWindowLost`)、GPU 设备丢失(`BackendDeviceLost`,只闩住该 session)。拿不到资源不报 `SegmentMismatch`,那会把错归到共享段尺寸上;wire 上也没有「拿不到资源」的值,新增会改 `wireFingerprint`。
|
||||
- `DeviceLost`:只由 device-lost 闩设置,没有家族投影到它。
|
||||
- `ProtocolCorruption`: things that cannot be executed correctly—malformed records, an unimplemented verb / surface, an invariant the two roles must uphold, a reply that breaks the contract. What the peer reads is "the two sides disagree on the protocol".
|
||||
- `RingOverrun`: the shared ring or the event channel overflowed, a transport capacity problem.
|
||||
- `SegmentMismatch`: the shared segment's size cannot hold what a role must put in it (a wrong size, not a transient fill).
|
||||
- `AbiMismatch`: the two ends' layout or protocol version disagree.
|
||||
- `ServerCrashed`: a liveness end the waiter blames on the peer: an incomplete drain, the apply thread gone, the server unable to get a required native resource (`ResourceUnavailable`), the server window destroyed (`ServerWindowLost`), GPU device loss (`BackendDeviceLost`, which latches only that session). An unavailable resource is not reported as `SegmentMismatch`, since that would blame the shared segment's size; the wire has no "resource unavailable" value either, and adding one would change `wireFingerprint`.
|
||||
- `DeviceLost`: set only by the device-lost latch; no family projects onto it.
|
||||
|
||||
### 15.4 共享图像(`shared_image`)
|
||||
### 15.4 Shared images (`shared_image`)
|
||||
|
||||
server 分配并以 dma-buf 描述符导出的颜色缓冲,让一个 client 的帧不离开 GPU 内存就进入另一个 client 的纹理。一行记录(`MGPSharedImageOp`),每个操作都经 reply slot 以 `MGPSharedImageReply` 回答;server 交给 client 的描述符走 aux socket,先于 reply 到达;`ImageId` 由 server 铸造,server 生命期内不复用。`Damage` 是 GL 窗口坐标、左下原点的矩形,0 个 = 整个表面。
|
||||
A color buffer that the server allocates and exports as a dma-buf descriptor, letting one client's frame enter another client's texture without leaving GPU memory. One record (`MGPSharedImageOp`), and every operation is answered through a reply slot with `MGPSharedImageReply`; the descriptor the server hands the client goes over the aux socket and arrives before the reply; `ImageId` is minted by the server and is not reused for the server's lifetime. `Damage` is a rectangle in GL window coordinates with a bottom-left origin, and 0 of them = the whole surface.
|
||||
|
||||
| 操作 | 语义 |
|
||||
| Operation | Semantics |
|
||||
|---|---|
|
||||
| `Allocate` / `Release` | 新建 `Width×Height` 的 `Format` 图像,reply 给出 id,描述符经 aux socket 送达 / 会话放掉对 `ImageId` 的引用 |
|
||||
| `Import` | 认领 client 在 aux socket 上排队的描述符(`MGPSharedImageFdOffer` 带本记录的 seq);须是本 server 导出的图像,尺寸与格式一致 |
|
||||
| `Present` | 当前 context 的默认帧缓冲按顶行在先拷进 `ImageId`(窗口帧成为 wl_buffer 的内容),只拷 `Damage`:client 按图像记录它自上次写入后缺的区域;GPU 拷完才回 reply |
|
||||
| `Attach` | 纹理 `Texture` 的存储变成 `ImageId`(`glEGLImageTargetTexture2DOES`),直到它下一次定义存储的 respecify |
|
||||
| `QueryBufferAge` | 在流中回答当前 draw surface 的 `EGL_BUFFER_AGE_EXT`(0 = 未知);应用先声明绘制区域(`EGL_KHR_partial_update`)时 `Format = kMGPBufferAgeDamageRegionFollows` |
|
||||
| `SetDamageRegion` | `eglSetDamageRegionKHR`:本帧将画的区域 |
|
||||
| `Flush` | 往图像里渲染的会话(X server 的 glamor)的 `glFlush` / `glFinish`:自上个边界用过的图像按 flush 的 fence 发布为已写、已读(隐式同步),发布后回答 |
|
||||
| `NativeFence` | `EGL_ANDROID_native_fence_sync`:会话已提交工作完成时 signal 的 sync_file(写图像的会话同时做 `Flush` 的发布),经 aux socket 先于 reply 送达,`Format = kMGPNativeFenceFdFollows`;`Format = 0` 表示 server 给不出描述符,reply 发出时工作已完成 |
|
||||
| `Allocate` / `Release` | create a new `Width×Height` image of `Format`, the reply gives the id, and the descriptor is delivered over the aux socket / the session drops its reference to `ImageId` |
|
||||
| `Import` | claim the descriptor the client queued on the aux socket (`MGPSharedImageFdOffer` carries this record's seq); it must be an image this server exported, with matching size and format |
|
||||
| `Present` | the current context's default framebuffer is copied into `ImageId` top row first (the window frame becomes the wl_buffer's contents), copying only `Damage`: the client records, per image, the regions missing since the last write; the reply comes only after the GPU copy completes |
|
||||
| `Attach` | the storage of texture `Texture` becomes `ImageId` (`glEGLImageTargetTexture2DOES`), until its next respecify that defines storage |
|
||||
| `QueryBufferAge` | answers in the stream with the current draw surface's `EGL_BUFFER_AGE_EXT` (0 = unknown); when the application first declares the drawing region (`EGL_KHR_partial_update`), `Format = kMGPBufferAgeDamageRegionFollows` |
|
||||
| `SetDamageRegion` | `eglSetDamageRegionKHR`: the region that will be drawn this frame |
|
||||
| `Flush` | `glFlush` / `glFinish` of a session rendering into the image (the X server's glamor): images used since the last boundary are published as written and read at the flush's fence (implicit sync), and the answer follows the publication |
|
||||
| `NativeFence` | `EGL_ANDROID_native_fence_sync`: a sync_file signaled when the session's submitted work completes (a session that writes the image does the `Flush` publication at the same time), delivered over the aux socket before the reply, `Format = kMGPNativeFenceFdFollows`; `Format = 0` means the server cannot give a descriptor and the work was already done when the reply was sent |
|
||||
|
||||
### 15.5 EGL 归属:apply 线程终身持有原生 context
|
||||
### 15.5 EGL ownership: the apply thread holds the native context for life
|
||||
|
||||
split 下应用线程从不调用驱动的 `eglMakeCurrent`:`BackendObject_Remote` 的 EGL 虚函数调用 `ServerLoop.h` 的 `Server*` 转发函数,每个都经 `RunSurfaceControlFrame` 成为阻塞控制请求,由 `mgl-srv-apply` 对 server 私有的后端对象执行。
|
||||
- 为什么是这条线程:DirectGLES 的 `IsBackendContextCurrentOnThisThread()` 与 `Managers.cpp` 的 `CanTouchGLNow()` 守卫答「否」时静默降级——fence 恒为已 signal、query 返回空句柄、`Present` 不建帧 fence(buffer pool 的回收水位不再前进)、persistent map 拒绝采纳。apply 线程终身持有 context,这些守卫在 server 上都答「是」。
|
||||
- 原生 `eglMakeCurrent` 只在 apply 线程当前绑定的 tuple 真正改变时运行,去重分两层:apply 线程上的 `ServerLoop::ApplyMakeCurrent` 用 `ClassifyEglMakeCurrent` 与上次绑定的 tuple 比较,相同即 no-op、不重发 caps,不同则转发并重发 caps 快照,client 的 release-current 只记录(`ClientReleaseCount`)不转发;`BackendObject_DirectGLES::MakeEGLCurrent` 在有传输时,若目标 draw surface 已是本线程原生当前、且虚 tuple 正是那次绑定所为(或那次绑定是 surface 自己的创建),跳过原生调用。同一 surface 上换虚 context 仍原生重绑,因为 `MakeCurrent` 的失效针对的是变了的前端 context。只记一个 tuple:线程上同时只有一个 current context,A → B → A 是三次原生绑定。
|
||||
- 在原生绑定可能已改变、或 tuple 指向的 context 可能已不存在的每个事件之后遗忘 tuple(`ServerLoop::ForgetCurrentTuple`):`ReleaseEGLResources`、释放它指向的 surface、surface(重)建(创建时原生绑定新 surface)、后端销毁。EGL 句柄值会复用;记住死 tuple 的代价是静默地没有 current context,遗忘的代价只是一次多余的转发。
|
||||
- `ReleaseEGLResources` 与 `~BackendObject_DirectGLES` 必须阻塞:前者本身是控制请求,后者在 apply 线程退出前于该线程执行,`ServerLoop::Stop()` 等它结束;否则 `MobileGL::Destroy()` 会在 server 仍持有 context 时继续往下拆。
|
||||
- 对照 `ServerLoopEglTest.MakeCurrentBindsNativelyOncePerContextAndNeverForwardsAClientRelease` 在 EGL 函数表处计数:surface 创建 + 两次相同 make-current + 一次 client release + release 后再绑定 = 一次原生 `eglMakeCurrent`、零次原生 release。
|
||||
Under the split the application thread never calls the driver's `eglMakeCurrent`: the EGL virtual functions of `BackendObject_Remote` call the `Server*` forwarders in `ServerLoop.h`, each of which becomes a blocking control request through `RunSurfaceControlFrame` and is executed by `mgl-srv-apply` on the server's private backend object.
|
||||
- Why this thread: when DirectGLES's `IsBackendContextCurrentOnThisThread()` and `CanTouchGLNow()` in `Managers.cpp` answer "no", the guards degrade silently—fences are always signaled, queries return a null handle, `Present` creates no frame fence (the buffer pool's reclamation watermark stops advancing), and the persistent map refuses adoption. The apply thread holds the context for life, so these guards all answer "yes" on the server.
|
||||
- The native `eglMakeCurrent` runs only when the tuple currently bound on the apply thread really changes, deduplicated in two layers: on the apply thread, `ServerLoop::ApplyMakeCurrent` compares against the last bound tuple with `ClassifyEglMakeCurrent`; a match is a no-op with no caps resent, a difference is forwarded with a resent caps snapshot, and the client's release-current is only counted (`ClientReleaseCount`), never forwarded; `BackendObject_DirectGLES::MakeEGLCurrent`, when there is a transport, skips the native call if the target draw surface is already natively current on this thread and the virtual tuple is exactly what that binding produced (or that binding was the surface's own creation). Switching virtual contexts on the same surface still binds natively again, because a `MakeCurrent` invalidation targets the frontend context that changed. Only one tuple is kept: a thread has a single current context at a time, so A → B → A is three native bindings.
|
||||
- The tuple is forgotten (`ServerLoop::ForgetCurrentTuple`) after every event in which the native binding may have changed or the context the tuple points at may no longer exist: `ReleaseEGLResources`, releasing the surface it points at, a surface (re)creation (which natively binds the new surface), and backend destruction. EGL handle values are reused; remembering a dead tuple costs a silent absence of a current context, while forgetting costs only one redundant forward.
|
||||
- `ReleaseEGLResources` and `~BackendObject_DirectGLES` must block: the former is itself a control request, and the latter runs on the apply thread before that thread exits, with `ServerLoop::Stop()` waiting for it to finish; otherwise `MobileGL::Destroy()` would keep tearing down while the server still holds the context.
|
||||
- The control `ServerLoopEglTest.MakeCurrentBindsNativelyOncePerContextAndNeverForwardsAClientRelease` counts at the EGL function table: surface creation + two identical make-currents + one client release + a bind after the release = one native `eglMakeCurrent`, zero native releases.
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
# 构建布局、开关与计数器(原 ARCHITECTURE §16、附 A、附 B)
|
||||
# Build layout, switches and counters (formerly ARCHITECTURE §16, appendices A and B)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design details. The design points and the index of all chapters are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; an `ARCHITECTURE.md §N` in a code comment is found by its number here.
|
||||
|
||||
## 16. 构建布局
|
||||
## 16. Build layout
|
||||
|
||||
```
|
||||
MobileGL/MG_Pipe/ 永远进构建:*.def 目录与表、MGPipe*.h payload / 句柄 / 回调、PipeApply、PipeRoute、generated/*.inc
|
||||
MobileGL/MG_Impl/Pipe/ Tracker、PipeFill、SlotAllocator、CsoCache、CompositeResolver、ResourceTracker、SetHashSuppressor、*Emit.h
|
||||
MobileGL/MG_Pipe/ always in the build: the *.def catalog and tables, MGPipe*.h payloads / handles / callbacks, PipeApply, PipeRoute, generated/*.inc
|
||||
MobileGL/MG_Impl/Pipe/ Tracker, PipeFill, SlotAllocator, CsoCache, CompositeResolver, ResourceTracker, SetHashSuppressor, *Emit.h
|
||||
MobileGL/MG_Backend/MGPipe/ PipeInputs.{h,cpp}
|
||||
MobileGL/MG_Remote/ 仅 MOBILEGL_BUILD_DISAGGREGATED:CONTRACT-*.md、CapsCodec、FatalFamilies.def / FatalFunnel、Handshake.h
|
||||
Protocol/ protocol.fbs、generated/、SurfaceOpCodec、mg_protocol_base.h
|
||||
Transport/ ITransport、SocketTransport、InProcessTransport、ILink、ShmLink、StreamLink、Ring、SessionRings、ReplySlot、EventRing、
|
||||
Doorbell、ShmSegment(+Posix/Win32)、FdPassing、Framing、AuthToken、ControlInbox、LinkMetrics、WireLog
|
||||
Wire/ PipeWireCodec(记录编解码、WireVerbSink)
|
||||
Client/ BackendObject_Remote、ClientSession、EmitTables、WireTables、CapsMirror、PersistentMapTracker、GpuWritePending
|
||||
Server/ PipeApplier、ServerLoop、ServerSession、ServerMain / ServerEntry / ServerSpawn、PreAuthGate、StagedShadow、StagedTextureStore、
|
||||
SurfaceControlFrame、ServerDisplay、InProcessServer、DisplayServerJni
|
||||
MobileGL/MG_Remote/ only with MOBILEGL_BUILD_DISAGGREGATED: CONTRACT-*.md, CapsCodec, FatalFamilies.def / FatalFunnel, Handshake.h
|
||||
Protocol/ protocol.fbs, generated/, SurfaceOpCodec, mg_protocol_base.h
|
||||
Transport/ ITransport, SocketTransport, InProcessTransport, ILink, ShmLink, StreamLink, Ring, SessionRings, ReplySlot, EventRing,
|
||||
Doorbell, ShmSegment(+Posix/Win32), FdPassing, Framing, AuthToken, ControlInbox, LinkMetrics, WireLog
|
||||
Wire/ PipeWireCodec (record codec, WireVerbSink)
|
||||
Client/ BackendObject_Remote, ClientSession, EmitTables, WireTables, CapsMirror, PersistentMapTracker, GpuWritePending
|
||||
Server/ PipeApplier, ServerLoop, ServerSession, ServerMain / ServerEntry / ServerSpawn, PreAuthGate, StagedShadow, StagedTextureStore,
|
||||
SurfaceControlFrame, ServerDisplay, InProcessServer, DisplayServerJni
|
||||
```
|
||||
|
||||
- `MOBILEGL_BUILD_DISAGGREGATED`(默认 OFF)追加 `MG_Remote/**`;OFF 时 `MG_Config::Transport` 是 `constexpr Monolith`。`MOBILEGL_BUILD_DISAGGREGATED_INPROC` 隐含前者并加角色隔离:两个角色靠 apply 线程与控制邮箱分开,而不是给 1494 个 `pGLContext->` 读点加 TLS。
|
||||
- `MOBILEGL_TRANSPORT = monolith | inproc | spawn` 是**拓扑**(G1 相关);控制面与数据面由 §11.9 的两个变量选。**split build 不设 `MOBILEGL_TRANSPORT` 时是 monolith 对照臂**。
|
||||
- 三个构建 flavour(P13 起):默认(单进程记录臂,不编 `MG_Remote`,出货形态)、verify、split;Android 两份 APK flavour(单进程 / split)。pull 构建与 `MOBILEGL_PIPE_PUSH` CMake 选项在 P13 删除。
|
||||
- 模块(P13 W7):`SOURCE_FILES` 按 link ratchet 的 PARTITION 拆成七个 OBJECT 库,`MobileGL` / `MobileGL_s` 由它们拼成;split 构建的 `MobileGL_server_linkcheck` 只链 SERVER 与 SHARED 模块并 `--no-undefined`,基线外的前端引用在构建时就失败(`scripts/ci/server_link_check.py`)。记录臂的 server 端代码在 `MG_Backend/Record/`(`RecordVerbSink`、`StagedShadow`、`StagedTextureStore`),客户端 verb 发射在 `MG_Impl/Pipe/Verb/`,不再依赖 `MG_Remote`。
|
||||
- 测试接线:ctest `ENVIRONMENT` 用 `mgl_itest_join_environment(...)` 构造;每条 split / spawn / tcp 条目带独立日志路径,日志按角色分文件(`<base>.client.log` / `<base>.server.log`);spawn、tcp 车道与 split 名集合一致(`scripts/ci/spawn_lane_parity.py`),每条目记 arm 证明。
|
||||
- CI(`.github/workflows/test.yml`、`apk.yml`):`pipe-gates`(G1–G8、生成器 self-test、符号报告、dirty-surface、字段归属、G5、文档引用 lint)、`flatc-check`、include 闭包、各 flavour 的 build / integration / retrace 车道、APK + AVD。两份 workflow 里的 `feat/disaggregated` 触发器是临时的,合入 `dev` 前移除。
|
||||
- `MOBILEGL_BUILD_DISAGGREGATED` (default OFF) adds `MG_Remote/**`; when OFF, `MG_Config::Transport` is a `constexpr Monolith`. `MOBILEGL_BUILD_DISAGGREGATED_INPROC` implies the former and adds role isolation: the two roles are separated by the apply thread and the control mailbox, rather than by adding TLS to the 1494 `pGLContext->` read points.
|
||||
- `MOBILEGL_TRANSPORT = monolith | inproc | spawn` is the **topology** (G1-relevant); the control plane and data plane are selected by the two variables in §11.9. **A split build with `MOBILEGL_TRANSPORT` unset is the monolith control arm**.
|
||||
- Three build flavors (since P13): default (the monolith record arm, with `MG_Remote` not compiled, the shipping shape), verify, and split; Android has two APK flavors (monolith / split). The pull build and the `MOBILEGL_PIPE_PUSH` CMake option are deleted in P13.
|
||||
- Modules (P13 W7): `SOURCE_FILES` is split into seven OBJECT libraries according to the link ratchet's PARTITION, and `MobileGL` / `MobileGL_s` are assembled from them; in a split build `MobileGL_server_linkcheck` links only the SERVER and SHARED modules with `--no-undefined`, so a frontend reference outside the baseline fails at build time (`scripts/ci/server_link_check.py`). The record arm's server-side code is in `MG_Backend/Record/` (`RecordVerbSink`, `StagedShadow`, `StagedTextureStore`), and client verb emission is in `MG_Impl/Pipe/Verb/`, no longer depending on `MG_Remote`.
|
||||
- Test wiring: the ctest `ENVIRONMENT` is built with `mgl_itest_join_environment(...)`; every split / spawn / tcp entry has its own log path, and logs are split per role (`<base>.client.log` / `<base>.server.log`); the spawn and tcp lanes match the split name set (`scripts/ci/spawn_lane_parity.py`), and each entry records its arm proof.
|
||||
- CI (`.github/workflows/test.yml`, `apk.yml`): `pipe-gates` (G1–G8, the generator self-test, the symbol report, dirty-surface, field ownership, G5, the documentation-reference lint), `flatc-check`, the include closure, the build / integration / retrace lanes of each flavor, APK + AVD. The `feat/disaggregated` triggers in both workflows are temporary and are removed before merging into `dev`.
|
||||
|
||||
### 16.1 集成测试车道的接线规则
|
||||
### 16.1 Wiring rules for the integration test lanes
|
||||
|
||||
`MG_IntegrationTest/CMakeLists.txt` 的每条车道都要能因它存在的理由变红(§13.2),接线规则:
|
||||
- 读库日志的用例必须独占日志:库以 `fopen(path, "w")` 打开日志,同一路径上的每个进程启动时都截断它,`ctest -j` 下读者会读到邻居截断后的文件,症状与「计数从未发出」相同。条目因此带私有 `MOBILEGL_LOG_FILE_PATH`,过滤器只选这一个用例;车道必须跑整个 scenario(重新过滤会改测试名)时,加以日志命名的 `RESOURCE_LOCK`,ctest 不会同时运行持同一把锁的条目。
|
||||
- 选臂的每个变量都写成条目自己的 ctest `ENVIRONMENT` 属性:属性只覆盖它写出的变量,其余从 job 环境漏进来(job 级 `MOBILEGL_TRANSPORT=inproc` 会把没钉 `monolith` 的 monolith 条目悄悄变成又一条 split)。
|
||||
- ctest 不把 SKIP 算失败,跳光的车道和全过的车道一样绿:必须真跑的车道钉 `MOBILEGL_ITEST_REQUIRE_GPU=1`,让不可用的 harness 变成失败;功能 opt-in(如 `MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1`)必须到达每个臂里读它的进程,tcp 臂的 server 由 `TcpServer.Start` fixture 预先启动、不继承 client 条目的环境,所以也要设在 fixture 上。
|
||||
- `ctest -L` 按正则子串匹配:新标签不得包含任何被不加锚点选择的已有标签,所以限定词放在中间(`integration-magma-all-spawn` 不含 `integration-magma-spawn`,`integration-magma-spawn-buffers` 不含 `integration-magma-buffers`);只选一个标签时写 `'^label$'`。
|
||||
- 门控车道带一个私有的第二标签并以 `--no-tests=error` 运行:构建缺少该选项时匹配为空即失败,而不是零条目的绿。
|
||||
- 三条传输臂:`Split.` 为 inproc,server 是测试进程里的 apply 线程;`Spawn.` 的 server 子进程继承 `::environ`(`ServerSpawn.cpp` 只去掉 `MOBILEGL_TRANSPORT` 与非 server 自有的 `MOBILEGL_IPC_*`);`Tcp.` 的 server 是整条车道共享的 `TcpServer.Start` fixture,由 `scripts/ci/tcp_server_fixture.py` 以它自己的环境启动一次,每个 Hello fork 一个 session 子进程。
|
||||
- 条目 ctest `ENVIRONMENT` 里由 server 读取的旋钮只到达 inproc 与 spawn 的 server;tcp 副本会以默认臂运行却顶着声称旋钮的名字,永远不会红。这类条目只注册 split + spawn(Magma 的列入 `scripts/ci/spawn_lane_parity.py` 的 `MAGMA_SERVER_ENV_KNOB_NO_TCP`),旋钮取默认值的孪生条目保留三条臂;要 tcp 臂就得让 fixture 带上旋钮:对整条车道无害的追加到 `TcpServer.Start` 的 `ENVIRONMENT`,否则另起一个带旋钮的 supervisor(`P8Coverage.cmake`)。只能在本进程读取的探针(描述符池普查、server loop 挂钩、server 缓存窥视、验证层输出)只注册 inproc(`MAGMA_INPROC_ONLY`)。
|
||||
- Magma 分三层:门控的 `integration-magma-{split,spawn,tcp}` 是手工布置、各带 flavour(strict + 双块、adopt 档、run-ahead credit、cache 钉定)的车道,测绿后才进 CI 门;`integration-magma-all-*` 重放每次 `mgl_itest_register_split_arms` 调用;`integration-magma-full-*` 跑整个集成二进制减去 `MGL_ITEST_MONOLITH_MECHANISM_FILTER` 与 `DualBlockScenario`。后两层不进任何 CI 门,用例随修复其阻塞点的提交升入门控层。
|
||||
- `MGITEST_SPLIT_LANE=1` 武装 `ScenarioFixture` 的析构断言:case 必须推进 client 编码器的记录序号,tcp 臂还要证明没有本地 server 子进程。它只适合每条都绘制的精选车道;整二进制集合(`integration-magma-full-*`、`integration-verify-split`)不设它,因为纯查询 case 由 `CapsMirror` 回答、不产生记录,代价是这些集合不证明记录过了线。
|
||||
Every lane in `MG_IntegrationTest/CMakeLists.txt` must be able to go red for the reason it exists (§13.2); the wiring rules:
|
||||
- A case that reads the library log must own the log exclusively: the library opens the log with `fopen(path, "w")`, and every process starting on the same path truncates it, so under `ctest -j` a reader sees a neighbor's truncated file, with the same symptom as "the counter was never emitted". An entry therefore carries a private `MOBILEGL_LOG_FILE_PATH`, and the filter selects only that case; when a lane must run a whole scenario (re-filtering would change the test names), it adds a `RESOURCE_LOCK` named after the log, and ctest will not run entries holding the same lock at the same time.
|
||||
- Every variable that selects an arm is written as the entry's own ctest `ENVIRONMENT` property: the property overrides only the variables it states, and the rest leak in from the job environment (a job-level `MOBILEGL_TRANSPORT=inproc` quietly turns a monolith entry that does not pin `monolith` into another split).
|
||||
- ctest does not count SKIP as a failure, so a lane that skips everything is as green as a lane that passes everything: a lane that must really run pins `MOBILEGL_ITEST_REQUIRE_GPU=1`, so an unavailable harness becomes a failure; a feature opt-in (such as `MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1`) must reach the process that reads it in each arm, and the tcp arm's server is started ahead of time by the `TcpServer.Start` fixture and does not inherit the client entry's environment, so it must be set on the fixture as well.
|
||||
- `ctest -L` matches by regular-expression substring: a new label must not contain any existing label that is selected without anchors, so the qualifier goes in the middle (`integration-magma-all-spawn` does not contain `integration-magma-spawn`, and `integration-magma-spawn-buffers` does not contain `integration-magma-buffers`); selecting a single label is written `'^label$'`.
|
||||
- A gated lane carries a private second label and runs with `--no-tests=error`: when the build lacks the option the match is empty and fails, rather than a zero-entry green.
|
||||
- Three transport arms: `Split.` is inproc, and the server is the apply thread in the test process; `Spawn.`'s server child inherits `::environ` (`ServerSpawn.cpp` removes only `MOBILEGL_TRANSPORT` and the `MOBILEGL_IPC_*` that are not server-owned); `Tcp.`'s server is the `TcpServer.Start` fixture shared by the whole lane, started once by `scripts/ci/tcp_server_fixture.py` with its own environment, forking one session child per Hello.
|
||||
- A knob read by the server in an entry's ctest `ENVIRONMENT` reaches only the inproc and spawn servers; a tcp copy would run the default arm under a name claiming the knob and would never go red. Such entries register split + spawn only (for Magma they are listed in `MAGMA_SERVER_ENV_KNOB_NO_TCP` in `scripts/ci/spawn_lane_parity.py`), and the twin entry with the knob at its default keeps all three arms; a tcp arm requires the fixture to carry the knob: append it to `TcpServer.Start`'s `ENVIRONMENT` when it is harmless to the whole lane, otherwise start a separate supervisor that carries it (`P8Coverage.cmake`). Probes that can only be read in this process (the descriptor-pool census, the server loop hooks, the server cache peeking, validation-layer output) register for inproc only (`MAGMA_INPROC_ONLY`).
|
||||
- Magma has three layers: the gated `integration-magma-{split,spawn,tcp}` lanes are hand-arranged, each with its flavor (strict + dual block, adoption tier, run-ahead credit, cache pinning), and enter the CI gate only after passing; `integration-magma-all-*` replays every `mgl_itest_register_split_arms` call; `integration-magma-full-*` runs the whole integration binary minus `MGL_ITEST_MONOLITH_MECHANISM_FILTER` and `DualBlockScenario`. The last two layers enter no CI gate, and a case is promoted to the gated layer by the commit that fixes its blocking point.
|
||||
- `MGITEST_SPLIT_LANE=1` arms the `ScenarioFixture` destructor assertion: the case must advance the client encoder's record sequence number, and the tcp arm must also prove there is no local server child process. It suits only curated lanes where every case draws; the whole-binary sets (`integration-magma-full-*`, `integration-verify-split`) do not set it, because pure query cases are answered by `CapsMirror` and produce no records, at the cost that these sets do not prove records crossed the wire.
|
||||
|
||||
## 附 A:开关
|
||||
## Appendix A: switches
|
||||
|
||||
CMake:`MOBILEGL_BUILD_DISAGGREGATED`(OFF)、`MOBILEGL_BUILD_DISAGGREGATED_INPROC`(OFF,隐含前者)、`MOBILEGL_PIPE_VERIFY`(OFF,永不出货)、`MOBILEGL_BUILD_SERVER_SPIKE`(仅 Android spike)、`MOBILEGL_FLATC_EXECUTABLE`(只服务 `flatc-check`)。`MOBILEGL_PIPE_PUSH` 与 `MOBILEGL_PIPE_LEGACY_MEMOS` 在 P13 删除(旧缓存里的 OFF 只告警并忽略)。
|
||||
CMake: `MOBILEGL_BUILD_DISAGGREGATED` (OFF), `MOBILEGL_BUILD_DISAGGREGATED_INPROC` (OFF, implies the former), `MOBILEGL_PIPE_VERIFY` (OFF, never ships), `MOBILEGL_BUILD_SERVER_SPIKE` (Android spike only), `MOBILEGL_FLATC_EXECUTABLE` (serves `flatc-check` only). `MOBILEGL_PIPE_PUSH` and `MOBILEGL_PIPE_LEGACY_MEMOS` are deleted in P13 (an OFF in an old cache only warns and is ignored).
|
||||
|
||||
运行时,MGPipe(`MobileGL/Config.h`、`ConfigLoader.cpp`):
|
||||
At runtime, MGPipe (`MobileGL/Config.h`, `ConfigLoader.cpp`):
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_PIPE_PUSH` | `0x3fff` | 子系统位图(`MG_Pipe/MGPipe.h`,位永不复用);P13 起位 0–13 固定开,清任何一位都在启动时具名拒绝;只剩位 63("关 CSO 内容寻址"的行为对照)可设 |
|
||||
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` | 0 / 1 / 空 | 影子比对与它的阴性对照。比对器只编进 verify 构建,其他构建里 `MOBILEGL_PIPE_VERIFY=1` 只告警;`_VERIFY_CORRUPT` 不进 `Config.h`,只有 verify 构建在比对器武装时读它(`PipeFill.cpp` 的 `ArmVerify`,同样经环境与系统配置文件)。poison 的阴性对照没有运行时开关:集成测试的 harness 标记 `MGITEST_POISON_OMIT` 由重新 exec 的子进程经 verify 构建才有的 `MGPipeSetPoisonOmission` 武装(`Harness/PoisonOmissionArm.h`) |
|
||||
| `MOBILEGL_PIPE_STATS` / `_STATS_PERIOD` / `_STATS_FILE` | 0 / 120 / 空 | 边界计数器(附 B);dump 按角色写 `<base>.client.json` / `<base>.server.json` |
|
||||
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64 | 索引镜像预算(P8);`_LEGACY_MEMOS`、`_TEXEL_RETAIN_MB`、`_HANDLE_ABA_CONTROL` 随各自的臂在 P13 删除 |
|
||||
| `MOBILEGL_PIPE_PUSH` | `0x3fff` | subsystem bitmap (`MG_Pipe/MGPipe.h`, bits are never reused); since P13 bits 0–13 are fixed on, and clearing any of them is refused by name at startup; only bit 63 ("the behavior control for turning off CSO content addressing") can still be set |
|
||||
| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` | 0 / 1 / empty | the shadow comparison and its negative controls. The comparator is compiled only into the verify build; in other builds `MOBILEGL_PIPE_VERIFY=1` only warns; `_VERIFY_CORRUPT` is not in `Config.h`, and only the verify build reads it when the comparator is armed (`ArmVerify` in `PipeFill.cpp`, likewise through the environment and the system configuration file). The poison negative control has no runtime switch: the integration test's harness flag `MGITEST_POISON_OMIT` is armed by a re-exec'd child process through `MGPipeSetPoisonOmission`, which only the verify build has (`Harness/PoisonOmissionArm.h`) |
|
||||
| `MOBILEGL_PIPE_STATS` / `_STATS_PERIOD` / `_STATS_FILE` | 0 / 120 / empty | boundary counters (Appendix B); the dump is written per role as `<base>.client.json` / `<base>.server.json` |
|
||||
| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64 | the index mirror budget (P8); `_LEGACY_MEMOS`, `_TEXEL_RETAIN_MB` and `_HANDLE_ABA_CONTROL` are deleted with their arms in P13 |
|
||||
|
||||
运行时,传输与 IPC:
|
||||
At runtime, transport and IPC:
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `MOBILEGL_TRANSPORT` | `monolith` | 拓扑:`monolith` / `inproc` / `spawn` |
|
||||
| `MOBILEGL_IPC_CONTROL` / `MOBILEGL_IPC_DATA` | `fork` / `auto` | 控制面端点与数据面(§11.9);`MOBILEGL_IPC_SERVER_PATH` 定位 fork 出的 server |
|
||||
| `MOBILEGL_IPC_TOKEN` / `MOBILEGL_IPC_REQUIRE_SAME_BUILD` | 空 / 0 | 令牌(≥16 字节;无令牌只允许 loopback);要求两端同一构建(车道纪律) |
|
||||
| `MOBILEGL_IPC_PREAUTH_*` / `MOBILEGL_IPC_AUTH_BACKOFF_*` | — | server 预认证的上限与失败退避(Ph) |
|
||||
| `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` / `MOBILEGL_IPC_COLD_START_MS` | 5000 / 20000 | 控制回复的稳态上限与冷启动预算 |
|
||||
| `MOBILEGL_IPC_LOG_FORWARD` | 远端时开 | server 日志经控制连接前送,client 写进自己的 `<base>.server.log` |
|
||||
| `MOBILEGL_IPC_SURFACE` | `offscreen` | `server` = 使用 server 自有窗口,client 无头(P12) |
|
||||
| `MOBILEGL_IPC_RING_MB` / `MOBILEGL_IPC_STAGE_MB` | 8 / 32 | `SEG_CMD` / `SEG_STAGE`;stage 的 1/4 是内容分块预算 |
|
||||
| `MOBILEGL_IPC_RUN_AHEAD` | 1 | P5e run-ahead;只是合取式的一半(server 须发布 `kCapRunAheadApply`);`0` 是 A/B 对照 |
|
||||
| `MOBILEGL_FRAMES_IN_FLIGHT` | 3 | 1..64,两个后端共用(旧名 `MOBILEGL_MAGMA_FRAMESINFLIGHT` 为弃用别名):Magma 的帧上下文数;Espryt 在 present S 后等待帧 S+1-N 完成;split 的 present credit 由它导出为 max(1, N-2)(P15) |
|
||||
| `MOBILEGL_IPC_PRESENT_CREDIT` | 由 `MOBILEGL_FRAMES_IN_FLIGHT` 导出(默认 1) | 1..8,§14;显式设置覆盖导出值,仅供实验 |
|
||||
| `MOBILEGL_IPC_POLL_ESCALATE` | 64 | 未报告 fence 的零超时轮询本地答"未完成",同一 fence 连续第 N 次才往返(P10,`CONTRACT-P10.md` §1);`0` = 每次轮询都往返,是 A/B 对照 |
|
||||
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | lockstep 下值类记录发布即返回;verify 构建里 `MOBILEGL_PIPE_VERIFY=1` 强制 0(`RUN_AHEAD` 同样强制 0,`ROLE_SPLIT_STATE` 强制关;其他构建不改它们) |
|
||||
| `MOBILEGL_IPC_EVENT_WAIT_MS` | 2000 | 反向事件遇满环时 server 等 client 排空的整笔预算(§11.7) |
|
||||
| `MOBILEGL_IPC_WIRE_DEFERRED_MB` | 64 | Magma wire 臂延迟回收的水位线;`0` 是 M2 阴性对照 |
|
||||
| `MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS` | 1 | persistent-map 推送只推变化的块(粒度固定 64 KiB,`PersistentMapTracker::BlockBytes()`);`0` 是整段推送的 A/B 对照 |
|
||||
| `MOBILEGL_IPC_ADOPT_TIER` | 未设 = T0 | 未设(用户 2026-09-29 定为缺省)= 能用就 T0,不能用(stream、无 AHB、POST 失败、server 不允许)安静回退 T2:只 MGLOG_D、照样计数;`0` = 点名要 T0,回退有具名行(A1 的 `Refuse{AdoptTierOnStream}` 等);`1` = T1 已关,具名拒绝后 T2;`2` = T2(P11 前的缺省)。`CONTRACT-P11.md` B2 |
|
||||
| `MOBILEGL_IPC_ALLOW_ADOPT_T0` | 1 | server 自有:`0` = 永不批准 T0(会话走 T2),经 `ServerSpawn` 保留 |
|
||||
| `MOBILEGL_IPC_STRICT_ERRORS` / `MOBILEGL_IPC_AUDIT` / `MOBILEGL_IPC_ROLE_SPLIT_STATE` | 0 / 0 / 1 | 残余输入读升级为 Fatal / 退休 staging 填 `0xDD` / 双块(P5f;P15 起默认开,`0` 关闭,阴性对照显式设 0;双块下 barriered fill 写 client 块,不再 quiesce applier) |
|
||||
| `MOBILEGL_IPC_SPIN_US` / `MOBILEGL_IPC_SERVER_AFFINITY` | 50 / `auto` | park 前自旋预算 / apply 线程亲和(Redmi 内核忽略) |
|
||||
| `MOBILEGL_IPC_RECOVER` | 1 | session 因 device loss 结束后,应用下一次 `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` 重新拨号得到新 session(旧 context 保持丢失,surface 在新 session 重建);`0` = 丢失对整个进程永久 |
|
||||
| `MOBILEGL_IPC_RESPAWN` | — | 具名拒绝:没有阶段实现 server 重启后的全量重推 |
|
||||
| `MOBILEGL_TRANSPORT` | `monolith` | topology: `monolith` / `inproc` / `spawn` |
|
||||
| `MOBILEGL_IPC_CONTROL` / `MOBILEGL_IPC_DATA` | `fork` / `auto` | the control-plane endpoint and the data plane (§11.9); `MOBILEGL_IPC_SERVER_PATH` locates the forked server |
|
||||
| `MOBILEGL_IPC_TOKEN` / `MOBILEGL_IPC_REQUIRE_SAME_BUILD` | empty / 0 | the token (≥16 bytes; without a token only loopback is allowed); require the same build on both ends (lane discipline) |
|
||||
| `MOBILEGL_IPC_PREAUTH_*` / `MOBILEGL_IPC_AUTH_BACKOFF_*` | — | the server pre-authentication limits and the failure backoff (Ph) |
|
||||
| `MOBILEGL_IPC_CONTROL_TIMEOUT_MS` / `MOBILEGL_IPC_COLD_START_MS` | 5000 / 20000 | the steady-state cap on control replies and the cold-start budget |
|
||||
| `MOBILEGL_IPC_LOG_FORWARD` | on when remote | server logs are forwarded over the control connection, and the client writes them into its own `<base>.server.log` |
|
||||
| `MOBILEGL_IPC_SURFACE` | `offscreen` | `server` = use the server's own window, with the client headless (P12) |
|
||||
| `MOBILEGL_IPC_RING_MB` / `MOBILEGL_IPC_STAGE_MB` | 8 / 32 | `SEG_CMD` / `SEG_STAGE`; a quarter of stage is the content chunking budget |
|
||||
| `MOBILEGL_IPC_RUN_AHEAD` | 1 | P5e run-ahead; only one half of the conjunction (the server must publish `kCapRunAheadApply`); `0` is the A/B control |
|
||||
| `MOBILEGL_FRAMES_IN_FLIGHT` | 3 | 1..64, shared by both backends (the old name `MOBILEGL_MAGMA_FRAMESINFLIGHT` is a deprecated alias): the number of Magma frame contexts; Espryt waits for frame S+1-N to complete after presenting S; the split derives the present credit from it as max(1, N-2) (P15) |
|
||||
| `MOBILEGL_IPC_PRESENT_CREDIT` | derived from `MOBILEGL_FRAMES_IN_FLIGHT` (default 1) | 1..8, §14; an explicit setting overrides the derived value, for experiments only |
|
||||
| `MOBILEGL_IPC_POLL_ESCALATE` | 64 | a zero-timeout poll for an unreported fence answers "not complete" locally, and only the Nth consecutive poll of the same fence makes a roundtrip (P10, `CONTRACT-P10.md` §1); `0` = a roundtrip on every poll, the A/B control |
|
||||
| `MOBILEGL_IPC_BATCH_WAITS` | 1 | under lockstep, value-class records return as soon as they publish; in the verify build `MOBILEGL_PIPE_VERIFY=1` forces 0 (`RUN_AHEAD` likewise forces 0, `ROLE_SPLIT_STATE` forces it off; other builds do not change them) |
|
||||
| `MOBILEGL_IPC_EVENT_WAIT_MS` | 2000 | the whole budget for the server to wait for the client to drain when a reverse event meets a full ring (§11.7) |
|
||||
| `MOBILEGL_IPC_WIRE_DEFERRED_MB` | 64 | the watermark for deferred reclamation on the Magma wire arm; `0` is the M2 negative control |
|
||||
| `MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS` | 1 | persistent-map pushes send only the changed blocks (fixed 64 KiB granularity, `PersistentMapTracker::BlockBytes()`); `0` is the A/B control that pushes the whole range |
|
||||
| `MOBILEGL_IPC_ADOPT_TIER` | unset = T0 | unset (the user made this the default on 2026-09-29) = T0 when it can be used, with a quiet fallback to T2 when it cannot (stream, no AHB, POST failure, server does not allow it): only MGLOG_D, counted as usual; `0` = ask for T0 by name, and the fallback has a named line (A1's `Refuse{AdoptTierOnStream}` and so on); `1` = T1 is off, a named refusal and then T2; `2` = T2 (the pre-P11 default). `CONTRACT-P11.md` B2 |
|
||||
| `MOBILEGL_IPC_ALLOW_ADOPT_T0` | 1 | server-owned: `0` = never approve T0 (the session goes T2), preserved through `ServerSpawn` |
|
||||
| `MOBILEGL_IPC_STRICT_ERRORS` / `MOBILEGL_IPC_AUDIT` / `MOBILEGL_IPC_ROLE_SPLIT_STATE` | 0 / 0 / 1 | residual input reads escalate to Fatal / retired staging is filled with `0xDD` / dual block (P5f; on by default since P15, `0` turns it off, and the negative control sets 0 explicitly; under dual block the barriered fill writes the client block and no longer quiesces the applier) |
|
||||
| `MOBILEGL_IPC_SPIN_US` / `MOBILEGL_IPC_SERVER_AFFINITY` | 50 / `auto` | pre-park spin budget / apply-thread affinity (ignored by the Redmi kernel) |
|
||||
| `MOBILEGL_IPC_RECOVER` | 1 | after a session ends from device loss, the application's next `eglCreateContext` / `eglCreate*Surface` / `eglInitialize` dials again for a new session (old contexts stay lost, surfaces are rebuilt in the new session); `0` = the loss is permanent for the whole process |
|
||||
| `MOBILEGL_IPC_RESPAWN` | — | refused by name: no phase implements a full re-push after a server restart |
|
||||
|
||||
显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`。计划中未接线:`MOBILEGL_IPC_IDLE_EXIT_S`(未解析)。
|
||||
Explicitly not introduced: `MOBILEGL_IPC_PROGRAM` (there is no relink tier), `MOBILEGL_IPC_VALIDATE_SERVER`. Planned but not wired: `MOBILEGL_IPC_IDLE_EXIT_S` (unresolved).
|
||||
|
||||
## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`)
|
||||
## Appendix B: boundary counters (`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 仍有效(见 git 历史 §12.1)。站点清单(哪些路径**没有**接线)写在 `PipeStats.cpp` 头部,那份清单是契约。
|
||||
When off, each site costs one global load + one branch that never hits. The byte kinds (stage buffer / texture / UBO / client arrays, `pmap`, `resid`, `csob-blob`, `seg` = bytes written into `SEG_STAGE`), the call kinds (`draws`, `accessor-calls`, texture upload emit / box / rect / jobs, `csom` / `csob`, `mpr`, `trp`, `rsp`, `wrec`, server-applied control records `ar` (applier_reset) / `od` (object_death), `maxrec` / `ringwraps` / `ringpads` / `ringwaits`, the doorbells `srv` / `srvpark` / `cli` / `clipark`), and the hit/miss of the six memo gates. Every `MOBILEGL_PIPE_STATS_PERIOD` frames there is one `MGPipe stats:` summary line, written to the log per role. Reading traps: `accessor-calls` is a static lower bound, and judging performance looks only at CPU time; under spawn the client does not advance the frame window (`frames=0`), so windowed fields are invalid on a spawn client while the run total stays valid (see git history §12.1). The site list (which paths are **not** wired) is at the top of `PipeStats.cpp`, and that list is a contract.
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# 各阶段的落地形状(原 ARCHITECTURE §17)
|
||||
# Landing shapes of each phase (formerly ARCHITECTURE §17)
|
||||
|
||||
> 设计细节。设计要点与全部章节的索引见 [`../ARCHITECTURE.md`](../ARCHITECTURE.md)。章节编号沿用原 `ARCHITECTURE.md`,代码注释里的 `ARCHITECTURE.md §N` 按编号在这里找到。
|
||||
> Design detail. The design points and the index of all sections are in [`../ARCHITECTURE.md`](../ARCHITECTURE.md). Section numbers follow the original `ARCHITECTURE.md`; an `ARCHITECTURE.md §N` in a code comment is found here by that number.
|
||||
|
||||
## 17. 阶段落地形状(索引)
|
||||
## 17. Phase landing shapes (index)
|
||||
|
||||
各阶段落地时的形状、偏差与论证原文在 notes 里;下表只列仍然生效的规则。
|
||||
The shapes, deviations and rationale text for each phase's landing are in the notes; the table below lists only the rules that still apply.
|
||||
|
||||
| 原节 | 主题 | 仍生效的规则 | 全文 |
|
||||
| Original section | Topic | Rules that still apply | Full text |
|
||||
|---|---|---|---|
|
||||
| 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) | 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 历史 |
|
||||
| 17.1 | verb barrier and honest same-address-space transport (P5) | The same address space must not become a bypass (R-2): the encoder always writes `nullptr` for `MGHostSpan::Ptr`; replies use the record sequence number as the slot id (R-3); the 120 s wait watchdog only judges deadlock, it is not a performance gate | git history |
|
||||
| 17.2 | Three backend slot classes, caps, tight readback (P5) | A answers locally / B emits / C refuses by name, never falling back to the monolith (R-4); capabilities read only from `CallMask` (R-8); `ReadPixels` is always tight on the wire, and the client scatters by its own pack state | same as above |
|
||||
| 17.3 | The server role, shadow and exit (P5) | The apply thread holds the native context for its lifetime; rare EGL operations enter the apply thread through the control mailbox; staged shadow only hands over server-owned copies; bounded exit order | same as above |
|
||||
| 17.4 | lane isolation (P5) | Each split entry has a private log; the E1 vs E3(a) comparison is a permanent case (`RemoteWaitBoundaryControl.AnUnwaitedPublishIsSeenAsNoWait`, `PersistentCoherentMapScenario.ThePushIsTheOnlyRouteAWriteTakesToTheServer`), the lane is not rerun | same as above |
|
||||
| 17.5 | class-C verb migration (P5b) | The migration order is decided by the dynamic census; one migration = one emitter + one sink body; records carry GL call arguments verbatim, and unmeasured forms are refused by name (rule D) | git history |
|
||||
| 17.6 | Shared-address-space access reduced to zero (P5c) | Server-side texture staged shadow; `SEG_EVENT` is the only reverse channel; sink / twin resolve by record handle; `applier_reset` / `object_death` control records; role guard `Fatal{RoleViolation}` | git history |
|
||||
| 17.7 | run-ahead (P5e) | Rule F: applying an unbarriered record reads no client memory (a violation is a named Fatal, independent of the strict switch); `gPipeInputs` is server-role memory; `RunAheadArmed()` latches at the first caps adoption and can only be turned off, not on; `RUN_AHEAD=0` is the A/B control | git history |
|
||||
|
||||
@@ -1,271 +1,271 @@
|
||||
# 状态归属:session / context / share group / thread current(P14 设计)
|
||||
# State ownership: session / context / share group / thread current (P14 design)
|
||||
|
||||
> 为 anland 内置统一 server 而做。动因与否定项见 git 历史:不许用「每应用 fork 一个渲染进程」掩盖全局状态归属。本文行号对 `2f64ede8`(+ `4156ca6e` 文档提交),代码移动后以符号名检索。
|
||||
> Written for the unified server built into anland. The motivation and the rejected alternatives are in git history: "fork one render process per application" must not be used to paper over global state ownership. The line numbers here refer to `2f64ede8` (+ the `4156ca6e` documentation commit); after code moves, search by symbol name.
|
||||
|
||||
## 结论
|
||||
## Conclusion
|
||||
|
||||
今天整条栈的正确性建立在一个等式上:**一个进程 = 一个 server = 一个 session = 一个 context = 一个 share group**。`ServerLoopInstance`/`ServerSessionInstance`/`g_applyThreadKey`/`pGLContext`/`pEGLContext`/`g_Display`/`g_Context`/`g_Surface`/`g_applier`/`gMGPipeContext` 全是单实例。`2f64ede8` 的 `--serve --max-sessions` 用 fork 把等式搬进每个 worker 进程,绕开而不是解决归属;anland 统一 server 要求同进程多 session,等式必须在代码里真正拆开。
|
||||
Today the correctness of the whole stack rests on one equation: **one process = one server = one session = one context = one share group**. `ServerLoopInstance`/`ServerSessionInstance`/`g_applyThreadKey`/`pGLContext`/`pEGLContext`/`g_Display`/`g_Context`/`g_Surface`/`g_applier`/`gMGPipeContext` are all single-instance. `2f64ede8`'s `--serve --max-sessions` moves the equation into each worker process with fork, bypassing ownership instead of solving it; anland's unified server requires multiple sessions in one process, so the equation must actually be split apart in the code.
|
||||
|
||||
拆成四层归属:
|
||||
Split into four ownership layers:
|
||||
|
||||
| 层 | 拥有什么 | 今天的错误位置 |
|
||||
| Layer | What it owns | Where it is wrong today |
|
||||
|---|---|---|
|
||||
| **Session** | SHM rings/segments、staged stores、reply/event 路由、failure、对象 handle 空间、PipeApplier、apply thread、backend runtime 绑定 | `ServerSessionInstance()`/`ServerLoopInstance()`/`g_applier` 单例(`ServerSession.cpp:535`、`ServerLoop.cpp:1191`、`PipeApply.cpp:484`) |
|
||||
| **Context** | 独立 GLContext:bindings/render state/errors、VAO/FBO/TF/pipeline/query(容器对象不共享)、cache/scratch/owner/generation、native EGLContext | 唯一 `pGLContext`(`MG_State/GLState/Core.cpp:1565`);`eglCreateContext` 不建 GL 状态(`MG_State/EGLState/Core.cpp:625-700`);后端唯一 `g_Context`(`DirectGLES.cpp:16184`) |
|
||||
| **Share group** | buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 registry 与名称分配;后端 twin/native 对象映射 | 混在唯一 `pGLContext` 各 State 成员里(`MG_State/GLState/Core.h:610-692`);`shareCtx` 只存不用(`MG_State/EGLState/Core.cpp:650`) |
|
||||
| **Thread current** | TLS 指向 (session, context);owner 关系不进 TLS,context 可跨线程移交;apply 显式设置 runtime scope | `m_threadCurrents`/`m_contextOwners` 已有雏形但绑死单实例表(`MG_State/EGLState/Core.h:283-284`);`g_applyThreadKey` 进程级(`ServerLoop.h:129`) |
|
||||
| **Session** | SHM rings/segments, staged stores, reply/event routing, failure, the object handle space, PipeApplier, apply thread, backend runtime binding | The `ServerSessionInstance()`/`ServerLoopInstance()`/`g_applier` singletons (`ServerSession.cpp:535`, `ServerLoop.cpp:1191`, `PipeApply.cpp:484`) |
|
||||
| **Context** | An independent GLContext: bindings/render state/errors, VAO/FBO/TF/pipeline/query (container objects are not shared), cache/scratch/owner/generation, native EGLContext | The single `pGLContext` (`MG_State/GLState/Core.cpp:1565`); `eglCreateContext` creates no GL state (`MG_State/EGLState/Core.cpp:625-700`); the backend's single `g_Context` (`DirectGLES.cpp:16184`) |
|
||||
| **Share group** | The object registry and name allocation for buffer/texture/renderbuffer/sampler/shader/program/sync; the backend's twin/native object mapping | Mixed into the single `pGLContext`'s State members (`MG_State/GLState/Core.h:610-692`); `shareCtx` is stored but never used (`MG_State/EGLState/Core.cpp:650`) |
|
||||
| **Thread current** | TLS points at (session, context); the owner relation does not go into TLS, a context can be handed over across threads; apply sets the runtime scope explicitly | `m_threadCurrents`/`m_contextOwners` already have a prototype but are tied to single-instance tables (`MG_State/EGLState/Core.h:283-284`); `g_applyThreadKey` is process-level (`ServerLoop.h:129`) |
|
||||
|
||||
关闭一个 session 不得破坏另一个;同名 handle 在不同 session/share group 互不干扰;shared contexts 看到同一份共享对象。
|
||||
Closing one session must not break another; the same handle name in different sessions/share groups must not interfere; shared contexts see the same shared objects.
|
||||
|
||||
## wire 变更
|
||||
## Wire changes
|
||||
|
||||
1. **逐记录 context 身份**。`MGPWireRecHeader` 8 B(`generated/PipeWire.inc:35-65`)不含身份;`MGPApplierReset::ContextSerial` 只做断言(`MGPipeTypes.h:1953-1960`)。设计:context 族记录(`PipeTables.inc:32-45` 的 71 个)在 header 与 payload 之间插入 `Uint32 ContextSlot`(flags 加 `kRecHasContext` 位,生成器在 `PipeCalls.def` 的族声明处统一加,不逐 verb 手改)。`ContextSlot=0` 保留给「session 默认 context」,兼容只有一个 context 的旧形状。screen 族记录操作的对象 handle 本身属 share group,server 经「记录所属 context → share group」解析,不另加字段。
|
||||
- 为什么不用「每 context 一条 ring」:ring 是 session 的资源/信用单位,每 context 一条会把 swap pacing、反向事件、segment 生命周期全部复制一份;逐记录身份 4 B 解决同一问题。
|
||||
- 交错正确性:多线程 client 的记录在一条 ring 上交错,apply 以记录自带的 context 为准,不得依赖「最后一个 MakeCurrent」。不可拆分批次(一次 emit 的多记录)同 context 才许合并,生成器在 batch 边界断言。
|
||||
2. **EGL 生命周期上 wire**。`eglCreateContext`/`eglDestroyContext` 今天是纯前端(`EGLImpl.cpp:327-333`、`:428-434`)。`SurfaceOpKind`(`protocol.fbs:240-267`)新增 `CreateContext{contextSlot, shareWithSlot, api, version, flags}` 与 `DestroyContext{contextSlot}`;`MakeCurrent` 的 `context` 字段从「恒 0/忽略」变成真实 slot。frontend 仍在本地建 EGL 元数据,wire 只负责让 server 建对应 native context 与 share group。
|
||||
3. **反向事件带 context**。`EventFenceSignaledHead`(`ServerSession.cpp:1324-1338`)、GL error、surface-changed 事件头加 `ContextSlot`(surface 事件已有 surface handle,context 便于 client 直接派发)。fence handle 属 context 的 share group,client 按 (slot, gen) 原样核对。
|
||||
4. **wireFingerprint 随以上变更 bump**;两端同 commit + 显式 `-DMOBILEGL_BUILD_STAMP=<commit>` 重建,不接受 drift(交接文档「构建产物」节)。
|
||||
1. **Per-record context identity**. `MGPWireRecHeader` 8 B (`generated/PipeWire.inc:35-65`) carries no identity; `MGPApplierReset::ContextSerial` only asserts (`MGPipeTypes.h:1953-1960`). Design: context-family records (the 71 in `PipeTables.inc:32-45`) insert `Uint32 ContextSlot` between the header and the payload (the flags gain a `kRecHasContext` bit; the generator adds it once at the family declaration in `PipeCalls.def`, not per verb by hand). `ContextSlot=0` is reserved for "the session's default context", compatible with the old shape that has only one context. For screen-family records the object handle they operate on itself belongs to the share group; the server resolves it through "the record's context → share group" and adds no separate field.
|
||||
- Why not one ring per context: a ring is the session's resource/credit unit; one per context would duplicate swap pacing, reverse events and segment lifetime in full; per-record identity solves the same problem with 4 B.
|
||||
- Interleaving correctness: records from a multi-threaded client interleave on one ring; apply follows the record's own context and must not rely on "the last MakeCurrent". An indivisible batch (several records from one emit) may be merged only if the records share a context; the generator asserts this at batch boundaries.
|
||||
2. **EGL lifetime on the wire**. `eglCreateContext`/`eglDestroyContext` are pure frontend today (`EGLImpl.cpp:327-333`, `:428-434`). `SurfaceOpKind` (`protocol.fbs:240-267`) gains `CreateContext{contextSlot, shareWithSlot, api, version, flags}` and `DestroyContext{contextSlot}`; `MakeCurrent`'s `context` field goes from "always 0/ignored" to a real slot. The frontend still builds EGL metadata locally; the wire is only responsible for making the server create the corresponding native context and share group.
|
||||
3. **Reverse events carry a context**. `EventFenceSignaledHead` (`ServerSession.cpp:1324-1338`), the GL error and surface-changed event headers gain `ContextSlot` (surface events already carry a surface handle; the context lets the client dispatch directly). A fence handle belongs to the context's share group; the client checks it verbatim by (slot, gen).
|
||||
4. **wireFingerprint is bumped with the changes above**; both ends rebuild from the same commit + an explicit `-DMOBILEGL_BUILD_STAMP=<commit>`, and drift is not accepted (the "Build artifacts" section of the handover document).
|
||||
|
||||
## S1 已落地形状(context 身份上 wire)
|
||||
## S1 landed shape (context identity on the wire)
|
||||
|
||||
上面第 1、2 条是 P14a 的目标形状;S1 只落地了「身份能跨线、server 能归属」这一层,实际形状如下,后续切片按此续建。
|
||||
Items 1 and 2 above are P14a's target shape; S1 landed only the layer "identity can cross the wire and the server can attribute it", with the actual shape below, and later slices continue from it.
|
||||
|
||||
**data plane:`bind_context`,opcode 84**(`MG_Pipe/PipeCalls.def`,`kScreen` 族)。
|
||||
**data plane: `bind_context`, opcode 84** (`MG_Pipe/PipeCalls.def`, the `kScreen` family).
|
||||
|
||||
| 项 | 值 |
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| payload | `MGPBindContext{ Uint64 ClientContextToken }`(8 B,`MGP_ASSERT_POD`) |
|
||||
| flags / wait | `kNone` / `kWaitNone`(apply 不读客户端内存,记录的 ring 位置就是它的全部次序) |
|
||||
| apply 入口 | 无 `MGPipeApply*`;与 `applier_reset`/`object_death` 同为 control record,只到 `WireVerbSink` |
|
||||
| monolith 生产者 | 无(G1 不变) |
|
||||
| sink | `ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`) |
|
||||
| payload | `MGPBindContext{ Uint64 ClientContextToken }` (8 B, `MGP_ASSERT_POD`) |
|
||||
| flags / wait | `kNone` / `kWaitNone` (apply reads no client memory; the record's ring position is its entire ordering) |
|
||||
| apply entry | no `MGPipeApply*`; like `applier_reset`/`object_death` it is a control record and reaches only `WireVerbSink` |
|
||||
| monolith producer | none (G1 unchanged) |
|
||||
| sink | `ServerVerbSink::OnBindContext` (`MG_Remote/Server/PipeApplier.cpp`) |
|
||||
|
||||
**token 位宽:两端都是 `Uint64`。** 记录里是 `ClientContextToken`,控制帧里是 `SurfaceOp.context`;同宽是刻意的,否则必有一端要截断。`0` 在两处都表示「无 context」(release),也正是一个从未 bind 过的 session 已有的答案。token 由 `EGLState::CreateContext` 铸造(`m_nextClientContextToken` 从 1 起、稠密、不复用),随 `ContextObject` 存放,`EGLState::CurrentContextClientToken()` / `GetContextClientToken()` 读出。
|
||||
**token width: `Uint64` on both ends.** In the record it is `ClientContextToken`, in the control frame it is `SurfaceOp.context`; the equal width is deliberate, otherwise one end would have to truncate. `0` means "no context" (release) in both places, which is also the answer a session that has never bound already has. The token is minted by `EGLState::CreateContext` (`m_nextClientContextToken` starts at 1, dense, never reused), stored with `ContextObject`, and read out by `EGLState::CurrentContextClientToken()` / `GetContextClientToken()`.
|
||||
|
||||
**control plane:`SurfaceOpKind.CreateContext = 12` / `DestroyContext = 13`**(`protocol.fbs`),不是两个新的 CtrlMsg——context 的创建/销毁/make-current 与 surface 是同一个调用者、同一个线程、同一条阻塞 frame channel,另开一条只会多一个次序会打架的地方。`SurfaceOp` 追加 `shareGroupToken: ulong` 与 `contextFlags: uint`(保留扩展位,今天恒 0)。`MOBILEGL_PROTOCOL_CONTROL_REVISION` 5 → 6,`scripts/ci/protocol_revision_pins.json` 同步加行。
|
||||
**control plane: `SurfaceOpKind.CreateContext = 12` / `DestroyContext = 13`** (`protocol.fbs`), not two new CtrlMsgs—creating, destroying and make-current of a context have the same caller, the same thread and the same blocking frame channel as a surface, and opening another one would only add another place where ordering fights. `SurfaceOp` appends `shareGroupToken: ulong` and `contextFlags: uint` (reserved for extension, always 0 today). `MOBILEGL_PROTOCOL_CONTROL_REVISION` 5 → 6, with the line added to `scripts/ci/protocol_revision_pins.json` in sync.
|
||||
|
||||
- `CreateContext`:`context` = 新 context 的 token,`shareGroupToken` = 它并入的 share group(`shareCtx` 的族,或新建),`contextFlags` = 保留位。`display`/`surface` 为 0(server 的 display 就是 session 自己的)。
|
||||
- `DestroyContext`:`context` = 要销的 token;server 销表并在它当前 bind 时解绑。
|
||||
- 两者都用既有的 `SurfaceReply` 回答(ok + refusal),S1 建的是占位 runtime,没有别的可回。
|
||||
- `CreateContext`: `context` = the new context's token, `shareGroupToken` = the share group it joins (`shareCtx`'s family, or a new one), `contextFlags` = reserved bits. `display`/`surface` are 0 (the server's display is the session's own).
|
||||
- `DestroyContext`: `context` = the token to destroy; the server removes the entry and unbinds it if it is currently bound.
|
||||
- Both are answered with the existing `SurfaceReply` (ok + refusal); S1 builds a placeholder runtime and has nothing else to reply with.
|
||||
|
||||
**`bind_context` 与 MakeCurrent 控制帧的关系:不合并,各管一件事。**
|
||||
**Relationship between `bind_context` and the MakeCurrent control frame: not merged, each handles one thing.**
|
||||
|
||||
- `MakeCurrent` 控制帧仍是 **native tuple 的 op**:它让 server 的 apply thread 决定要不要真的 `eglMakeCurrent`,并参与 `m_haveCurrentTuple` 的去重(ID-54/ID-67 的口径一个字没动)。它的 `context` 字段与 `bind_context` 的 token 是同一个值,但那是「同一个身份」,不是「同一条通路」。
|
||||
- `bind_context` 是 **ring 上的归属记录**:它告诉 server「这条 ring 上从今往后的记录属于哪个 context」。它必须在 ring 上,因为 GL 记录就在 ring 上,而控制帧与 ring 之间没有全局次序可依赖。
|
||||
- 发射点只有一个:`EGLImpl::MakeCurrent` 在切换成功之后、返回之前调 `EmitContextBinding`(`MG_Impl/EGLImpl/EGLImpl.cpp`)。这给出「先于后续 GL 记录」的构造性保证——app 线程在 `eglMakeCurrent` 返回前不可能发出新 context 的任何记录,而 ring 是 FIFO。token 未变则不发(identical make-current 是 EGL 合法 no-op)。
|
||||
- `DestroyContext` 帧在客户端 `EGLState::DestroyContext` 接受之后才发,所以 EGL 自己拒掉的那种销毁(context 仍被某线程 current)不会到达 server,两端不会因为「token 悄悄消失」而对不上。
|
||||
- The `MakeCurrent` control frame is still an **op on the native tuple**: it lets the server's apply thread decide whether to actually `eglMakeCurrent`, and takes part in `m_haveCurrentTuple` deduplication (the ID-54/ID-67 wording is untouched by a single character). Its `context` field is the same value as `bind_context`'s token, but that is "the same identity", not "the same path".
|
||||
- `bind_context` is an **ownership record on the ring**: it tells the server "the records from now on, on this ring, belong to which context". It must be on the ring because GL records are on the ring, and there is no global order between a control frame and the ring to rely on.
|
||||
- There is exactly one emission point: `EGLImpl::MakeCurrent` calls `EmitContextBinding` (`MG_Impl/EGLImpl/EGLImpl.cpp`) after the switch succeeds and before returning. That gives the constructive guarantee "before any later GL record"—the app thread cannot emit any record for the new context before `eglMakeCurrent` returns, and the ring is FIFO. If the token is unchanged nothing is emitted (an identical make-current is a legal EGL no-op).
|
||||
- The `DestroyContext` frame is sent only after the client's `EGLState::DestroyContext` accepts, so a destroy that EGL itself refuses (the context is still current on some thread) never reaches the server, and the two ends cannot disagree through "the token quietly vanished".
|
||||
|
||||
**与第 1 条的取舍差异(明确记录)**:S1 走的是「在边沿宣告身份」而不是「在 71 个 context 族记录的 header 与 payload 之间插 `Uint32 ContextSlot`」。代价是每个 make-current 一条记录(而不是每条记录 4 B),收益是 payload 布局与 `MGPWireRec_*` 尺寸全部不动、`kRecHasContext` 位与 batch 边界的生成器改动全部不必;单 context 的既有形状(token 0 / 从未 bind 过)零改动即兼容。多线程交错下的逐记录身份若将来确有需要,再按第 1 条补 slot——那时两者可以并存,slot 为 0 仍读当前 token。
|
||||
**Difference in trade-off from item 1 (recorded explicitly)**: S1 takes "announce the identity at the edge" instead of "insert `Uint32 ContextSlot` between the header and payload of the 71 context-family records". The cost is one record per make-current (instead of 4 B per record); the benefit is that the payload layout and every `MGPWireRec_*` size stay unchanged, and the `kRecHasContext` bit and the batch-boundary generator changes are all unnecessary; the existing single-context shape (token 0 / never bound) is compatible with zero changes. If per-record identity under multi-thread interleaving really is needed later, add the slot per item 1 then—the two can coexist, and slot 0 still reads the current token.
|
||||
|
||||
**server 端**(`MG_Remote/Server/ServerSession.{h,cpp}`):每 session 一张 `{token → ContextRuntime}`(`ContextRuntime{ Token, ShareGroupToken, Flags }`,S1 里是占位,S4 给它装 native context)加一个当前 token(atomic,0 = 无)。`CreateContext`/`DestroyContext` 控制帧经 `ServerLoop::ApplySurfaceControlFrame` 的 `CreateContext`/`DestroyContext` 两个 arm 建表/销表;`bind_context` 经 codec 到 `ServerVerbSink::OnBindContext` 切当前 token。写入全部发生在 apply thread,表因此不需要锁。`bind_context` 指向一个本 session 从未创建的 token 是 `Fatal{ProtocolCorruption, "BindContext.ClientContextToken"}`(0 例外,永远合法);表的存在正是为了让这条判定有依据。
|
||||
**Server side** (`MG_Remote/Server/ServerSession.{h,cpp}`): each session gets a `{token → ContextRuntime}` table (`ContextRuntime{ Token, ShareGroupToken, Flags }`, a placeholder in S1, S4 gives it a native context) plus a current token (atomic, 0 = none). The `CreateContext`/`DestroyContext` control frames create/destroy the table through the `CreateContext`/`DestroyContext` arms of `ServerLoop::ApplySurfaceControlFrame`; `bind_context` goes through the codec to `ServerVerbSink::OnBindContext` to switch the current token. All writes happen on the apply thread, so the table needs no lock. A `bind_context` pointing at a token this session never created is `Fatal{ProtocolCorruption, "BindContext.ClientContextToken"}` (0 excepted, always legal); the table exists precisely so that this ruling has a basis.
|
||||
|
||||
**S1 的 token 只有写入者,没有读者**:server 端目前没有任何 apply 分支读 `CurrentContextToken()`(`grep` 可证:除 `ServerSession` 自己的访问器外无人调用),所以整条渲染路径的行为与 S1 之前逐字相同——这一层先把「归属」记下来,读它做事是 S4(native context)和 P14b/c 的事。没有 `bind_context` 的 session 行为同样不变:`ServerLoopTest` 的 MakeCurrent/epoch/liveness 用例(`MG_Test/Wire/ServerLoopTest.cpp:1780-1941`)仍全部通过,它们走的正是这条路径。
|
||||
**S1's token has a writer but no readers**: no apply branch on the server reads `CurrentContextToken()` today (provable by `grep`: nothing outside `ServerSession`'s own accessor calls it), so the whole rendering path behaves character for character as before S1—this layer records ownership first; reading it to act is S4's job (native context) and P14b/c's. A session without `bind_context` behaves identically too: the `ServerLoopTest` MakeCurrent/epoch/liveness cases (`MG_Test/Wire/ServerLoopTest.cpp:1780-1941`) all still pass, and they go through exactly this path.
|
||||
|
||||
**S1 的验收证据**(本次提交的测试):`PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` 钉 opcode 84 与 `kNone`/`kScreen`/`kWaitNone`;`SurfaceControlFrameTest.TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext` 钉 12/13 的值与 `shareGroupToken`/`contextFlags` 的往返;`SessionHandshakeTest.ARevisionFivePeerIsRefusedByNameAtRevisionSix` 钉 revision bump;`FuzzArm2/PeerLatchSite/.../BindContextUnknownToken` 是 data plane 的端到端控制——真实 `--serve` supervisor 上,一个伪造的、指向本 session 从未创建过的 token 的 `bind_context` 让 session 按名 latch(`exit=75`),且同一 supervisor 的下一个 session 正常渲染。
|
||||
**S1's acceptance evidence** (the tests of this commit): `PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering` pins opcode 84 and `kNone`/`kScreen`/`kWaitNone`; `SurfaceControlFrameTest.TheContextLifecycleFramedOpsAreCreateContextAndDestroyContext` pins the values 12/13 and the `shareGroupToken`/`contextFlags` round trip; `SessionHandshakeTest.ARevisionFivePeerIsRefusedByNameAtRevisionSix` pins the revision bump; `FuzzArm2/PeerLatchSite/.../BindContextUnknownToken` is the data-plane end-to-end control—on a real `--serve` supervisor, a forged `bind_context` that points at a token this session never created makes the session latch by name (`exit=75`), and the next session of the same supervisor renders normally.
|
||||
|
||||
## 前端(MG_State / MG_Impl)
|
||||
## Frontend (MG_State / MG_Impl)
|
||||
|
||||
1. **拆 GLContext**。`MG_State/GLState/Core.h:77-698` 的 `GLContext` 拆成 `ShareGroupState`(buffer/texture/renderbuffer/sampler/shader/program/sync 的对象 store + 名称分配)与 `GLContext`(bindings、error、viewport/scissor、render state、VAO/FBO/TF/pipeline/query store、texture unit 绑定、current program)。容器对象(VAO/FBO/TF/pipeline/query)**不共享**,留在 per-context;这是 GL 4.6 §5 的口径,不要为省事把 FBO 放进 share group。
|
||||
2. **per-EGLContext 实例化**。`EGLState::CreateContext` 建 `GLContext`,`shareCtx != EGL_NO_CONTEXT` 时挂到对方 `ShareGroupState`(首次共享时把对方的对象 store 迁移进新 share group,双方同指)。`eglMakeCurrent` 切 TLS 的 (session→) GLContext 指针;`eglDestroyContext` 只在 owner 不存在时允许(沿用 `MG_State/EGLState/Core.cpp:714-718`),销毁 context 不销毁 share group,最后一个 context 离开时 share group 对象全删。
|
||||
3. **Tracker 判据改显式身份**。`Pipe/Tracker.h:306-309` 的指针比较换成 (session, contextSlot);换 context 的首个 verb 重推完整状态的语义不变。`MGPipeTrackerInstance()`(`Tracker.h:956-963`)从单例改为 per-context(tracker 缓存的是 per-context 推送快门)。
|
||||
4. **eglMakeCurrent 流程**:`EGLImpl::MakeCurrent` 除调 `backendObject->MakeEGLCurrent` 外,先切 TLS GLContext 再发 `SurfaceOp::MakeCurrent{contextSlot}`;client 线程后续的 context 族记录都带这个 slot。
|
||||
1. **Split GLContext**. `MG_State/GLState/Core.h:77-698`'s `GLContext` splits into `ShareGroupState` (the object store + name allocation for buffer/texture/renderbuffer/sampler/shader/program/sync) and `GLContext` (bindings, error, viewport/scissor, render state, VAO/FBO/TF/pipeline/query store, texture unit bindings, current program). Container objects (VAO/FBO/TF/pipeline/query) are **not shared** and stay per-context; that is the GL 4.6 §5 wording—do not put FBO into the share group for convenience.
|
||||
2. **Per-EGLContext instantiation**. `EGLState::CreateContext` creates a `GLContext`; when `shareCtx != EGL_NO_CONTEXT` it attaches to the other side's `ShareGroupState` (at the first share, migrate the other side's object store into the new share group, both pointing at it). `eglMakeCurrent` switches the TLS (session→) GLContext pointer; `eglDestroyContext` is allowed only when no owner exists (following `MG_State/EGLState/Core.cpp:714-718`); destroying a context does not destroy the share group, and when the last context leaves all share group objects are deleted.
|
||||
3. **The tracker criterion becomes explicit identity**. The pointer comparison in `Pipe/Tracker.h:306-309` becomes (session, contextSlot); the semantics of re-pushing the full state on the first verb after a context switch are unchanged. `MGPipeTrackerInstance()` (`Tracker.h:956-963`) goes from a singleton to per-context (the tracker caches the per-context push shutter).
|
||||
4. **eglMakeCurrent flow**: besides calling `backendObject->MakeEGLCurrent`, `EGLImpl::MakeCurrent` first switches the TLS GLContext and then sends `SurfaceOp::MakeCurrent{contextSlot}`; later context-family records from the client thread all carry this slot.
|
||||
|
||||
## S3 已落地形状(前端 per-context / share group)
|
||||
## S3 landed shape (frontend per-context / share group)
|
||||
|
||||
上面「前端」一节是目标形状;S3 落地了第 1、2 条(第 3、4 条的 tracker/slot 部分未做),实际形状如下。
|
||||
The "Frontend" section above is the target shape; S3 landed items 1 and 2 (the tracker/slot parts of items 3 and 4 are not done), and the actual shape is below.
|
||||
|
||||
**`ShareGroupState`(`MG_State/GLState/ShareGroupState.{h,cpp}`,新)** 持有一个 share group 的全部对象注册表与名字生成器:
|
||||
**`ShareGroupState` (`MG_State/GLState/ShareGroupState.{h,cpp}`, new)** holds all object registries and name generators of one share group:
|
||||
|
||||
| 共享(ShareGroupState) | 每 context(GLContext) |
|
||||
| Shared (ShareGroupState) | Per context (GLContext) |
|
||||
|---|---|
|
||||
| buffer / texture / renderbuffer 的对象表与 `IndexGenerator`;sampler 对象表(`SamplerState` 整体);program+shader 表与共用名字生成器(`ProgramState` 整体);`token`(S1 的 `ShareGroupToken`) | error 列表;buffer binding slot 与 indexed binding point、touched 高水位、各 aggregate generation;texture unit / image unit 数组、active unit、bind/sampling generation、per-target 默认纹理(name 0);renderbuffer binding slot;VAO/FBO/TF/pipeline/query;`current program` |
|
||||
| The object tables and `IndexGenerator` for buffer / texture / renderbuffer; the sampler object table (all of `SamplerState`); the program+shader tables and the shared name generator (all of `ProgramState`); `token` (S1's `ShareGroupToken`) | The error list; buffer binding slots and indexed binding points, touched high-water marks, the per-aggregate generations; texture unit / image unit arrays, the active unit, bind/sampling generation, the per-target default texture (name 0); renderbuffer binding slots; VAO/FBO/TF/pipeline/query; `current program` |
|
||||
|
||||
`BufferState`/`TextureState`/`RenderbufferState` 因此只剩 binding 侧:删除走「先查 share group 的对象、再解本 context 的绑定、最后 free 名字」,即删对象是全局的、解绑定是逐 context 的(GL 4.6 core 6.1/8.1)。`ProgramState` 的 `UseProgram`/`glDeleteProgram`/`ReleaseShaderNameIfOrphaned` 现在把调用方的 current program 当参数传入,current program 本身落在 `GLContext`。`GLContext` 由 `GLContext(SharedPtr<ShareGroupState>)` 构造,`GLContext()` 建一个只属于它的 share group(测试与进程默认 context 用)。
|
||||
`BufferState`/`TextureState`/`RenderbufferState` therefore keep only the binding side: deletion goes "first look up the share group's object, then unbind this context, finally free the name", i.e. deleting an object is global and unbinding is per-context (GL 4.6 core 6.1/8.1). `ProgramState`'s `UseProgram`/`glDeleteProgram`/`ReleaseShaderNameIfOrphaned` now take the caller's current program as a parameter, and the current program itself lives in `GLContext`. `GLContext` is constructed from `GLContext(SharedPtr<ShareGroupState>)`; `GLContext()` creates a share group that belongs only to it (used by tests and the process default context).
|
||||
|
||||
**`pGLContext` 改成 TLS**:`extern thread_local SharedPtr<GLState::GLContext> pGLContext`(`GLState/Core.h`)。`EGLState::MakeCurrent` 成功后把它指向该 context 的 `GLContext`,`ReleaseThread`/`eglTerminate`/释放路径恢复线程进入 EGL 之前的那个值(`ThreadCurrentState::RestoreThreadGLState`)——所以「一个 context 同时只被一个线程拥有」的既有语义(`m_contextOwners`,EGL_BAD_ACCESS)一个字没动,owner 也不进 TLS:context 跨线程移交后状态仍在它自己的 `GLContext` 里。
|
||||
**`pGLContext` becomes TLS**: `extern thread_local SharedPtr<GLState::GLContext> pGLContext` (`GLState/Core.h`). After `EGLState::MakeCurrent` succeeds it points at that context's `GLContext`; `ReleaseThread`/`eglTerminate`/the release path restore the value the thread had before entering EGL (`ThreadCurrentState::RestoreThreadGLState`)—so the existing semantics "one context is owned by one thread at a time" (`m_contextOwners`, EGL_BAD_ACCESS) are untouched by a single character, and the owner does not go into TLS either: after a context is handed over across threads its state is still in its own `GLContext`.
|
||||
|
||||
没有 EGL context 的线程落到**进程默认 context**(`ProcessDefaultGLContext()`,`MG_State::Init()` 建,构造时 `pGLContext` 的初值就是它)——这正是拆分前「全局唯一 context」的语义,所以单 context 应用与所有直接操作 `pGLContext` 的既有测试行为不变。
|
||||
A thread without an EGL context falls to the **process default context** (`ProcessDefaultGLContext()`, created by `MG_State::Init()`; `pGLContext` is initialized to it at construction)—this is exactly the pre-split "one global context" semantics, so single-context applications and all existing tests that operate on `pGLContext` directly behave unchanged.
|
||||
|
||||
**每个 `eglCreateContext` 都建独立 `GLContext`**(`ContextObject::GLStateObject`);`shareCtx != EGL_NO_CONTEXT` 时 `GLContext` 挂到对方 `ShareGroupState`,否则新建一个并把 S1 铸的 `ShareGroupToken` 记在里面。这里不需要「迁移对方 store」:group 在第一个 context 诞生时就存在,后来者直接并入。`eglDestroyContext` 丢掉 `ContextObject`,共享指针归零才销毁 `GLContext` / `ShareGroupState`。
|
||||
**Every `eglCreateContext` creates an independent `GLContext`** (`ContextObject::GLStateObject`); when `shareCtx != EGL_NO_CONTEXT`, the `GLContext` attaches to the other side's `ShareGroupState`, otherwise it creates a new one and records the `ShareGroupToken` minted in S1 in it. There is no need here to "migrate the other side's store": the group exists when the first context is born, and later ones simply join. `eglDestroyContext` drops the `ContextObject`; `GLContext` / `ShareGroupState` are destroyed when the shared pointer reaches zero.
|
||||
|
||||
**默认帧缓冲(明确的偏差)**:GL framebuffer 0 今天仍是**进程一个对象**(`MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo`,后端按它判断「这是默认 FBO」)。它在 `MG_Impl::Init()` 建好之后经 `MG_State::SetDefaultFramebuffer` 交给 MG_State,此后每个新建 `GLContext` 构造时把它装成自己的 name 0 并绑到 Draw/Read(`GLContext::InstallDefaultFramebuffer`)。也就是说:每个 context 有**自己的 FramebufferState 表项与绑定**(`glBindFramebuffer` 互不影响),但那个对象暂时是共用的。真正的 per-context 默认帧缓冲属于「后端 per-context native tuple」那一期(见「后端」§1),在那之前若各 context 各建一份,后端的 `pDefaultFramebufferInfo` 身份判定会对第二个 context 失效。
|
||||
**The default framebuffer (an explicit deviation)**: GL framebuffer 0 is still **one object per process** today (`MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo`; the backend uses it to decide "this is the default FBO"). It is handed to MG_State through `MG_State::SetDefaultFramebuffer` after `MG_Impl::Init()` creates it; after that every newly constructed `GLContext` installs it as its own name 0 and binds it to Draw/Read (`GLContext::InstallDefaultFramebuffer`). That is: every context has **its own FramebufferState entry and binding** (`glBindFramebuffer` calls do not affect each other), but the object is shared for now. A real per-context default framebuffer belongs to the "backend per-context native tuple" phase (see "Backend" §1); until then, if each context created its own, the backend's `pDefaultFramebufferInfo` identity test would fail for the second context.
|
||||
|
||||
**monolith 与 split 走同一份结构**:两端共用一个 `MG_State`,`eglCreateContext`/`eglMakeCurrent`/`eglDestroyContext` 都先落这套前端状态,`MOBILEGL_PIPE_PUSH=0` 与 `=1` 的区别只是后端从哪儿读状态(`MGB_CTX` 是 `pGLContext` 还是 `gPipeInputs`)。`MGB_CTX_IDENTITY` 因此天然按 context 变化,post-S3 的 tracker 只需照旧比较(改成显式 slot 是后续切片)。
|
||||
**Monolith and split use the same structure**: both ends share one `MG_State`; `eglCreateContext`/`eglMakeCurrent`/`eglDestroyContext` all write this frontend state first, and the only difference between `MOBILEGL_PIPE_PUSH=0` and `=1` is where the backend reads state from (`MGB_CTX` is either `pGLContext` or `gPipeInputs`). `MGB_CTX_IDENTITY` therefore naturally varies per context, and the post-S3 tracker only has to compare as before (turning it into an explicit slot is a later slice).
|
||||
|
||||
**验收证据**(本次):`MG_Test/State/ContextStateOwnershipTest.cpp` 六个用例——不共享的两个 context 名字空间独立(同名不同对象、绑定不互见);共享对看到同一对象而各自持有绑定;`eglMakeCurrent` 切换后 error/viewport/FBO binding 各自独立且切回来还在;`eglDestroyContext` 后另一个 context 对象与绑定不受影响;释放后线程回到进入 EGL 前的 GL 状态;无 current context 的线程落到进程默认 context。
|
||||
**Acceptance evidence** (this commit): six cases in `MG_Test/State/ContextStateOwnershipTest.cpp`—two nonshared contexts have independent name spaces (same name, different objects, bindings not visible to each other); a shared pair sees the same object while each holds its own bindings; after an `eglMakeCurrent` switch, error/viewport/FBO binding are independent and still there when switching back; after `eglDestroyContext` the other context's objects and bindings are unaffected; after release the thread returns to the GL state it had before entering EGL; a thread without a current context falls to the process default context.
|
||||
|
||||
## server(MG_Remote/Server)
|
||||
## Server (MG_Remote/Server)
|
||||
|
||||
1. **session registry 取代单例**。`ServerSessionInstance`/`ServerLoopInstance` 改为 `ServerProcess` 持有的 session 表(slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread})。in-process server(`ServerMain.cpp:1593-1651` 的 `mobilegl_server_serve_inprocess`)从「单 active + RefuseBusy」改为接受至上限(默认 8,可配);`--serve` 的 unix 路径同样改为同进程多 session,**移除 fork-worker 分发**(`ServerMain.cpp:1566-1584`),`--max-sessions` 语义改为同进程上限。TCP 的 `TcpSupervisor` 单 active(`:1220-1223`)同样让位。
|
||||
2. **apply thread per session**。每 session 一个 apply thread;`g_applyThreadKey`(`ServerLoop.h:129`)改为 per-session 成员,`OnApplyThread()` 语义不变(同 thread 比较)。session 之间顺序调度由线程天然完成,不需要全局锁;native driver 本来就支持多线程各自 current 不同 context。
|
||||
3. **g_applier per session**。`PipeApply.cpp:484` 的进程级 `MGPipeApplierState` 改为 session 持有;`MGPipeApplierReset` 的 role guard(`PipeApply.cpp:1474-1481`)按本 session 的 apply thread 判定。`PipeApply.h:558-617` 已有的 share-group 对象记录 / working-state 二分是地基:对象记录表按 share group 分桶,working state 按 context 分桶,reset 只清本 context 的 working state。**(S5 已落地,形状见下面「S5 已落地形状」:这一条最终不是「per session 一个 applier」,而是 per {session, context} 一个 applier、其对象记录挂到 {session, shareGroup} 的组上。)**
|
||||
4. **反向路由按 session + context**。`ReverseCallbackOwner`(`ServerSession.cpp:408-418`)的 `Active()` 单活性改为「按发起线程/显式 session 参数解析」;事件头加 `ContextSlot`(见 wire 3)。`gMGPipeCallbacks`(`MGPipeCallbacks.h:77`)从进程级变量改为 per-session 表,Accept 时挂到 session 而非全局。
|
||||
5. **session 模式闩保留在 session 层**。`SessionSurfaceMode`/`SurfaceModeMismatch`(`ServerLoop.h:199-206`、`ServerLoop.cpp:1539-1544`)口径不变:compositor session 闩 OnScreen、app session 闩 Offscreen;统一 server 同时持有两种 session 是合法形状,不许把闩提升到进程级。
|
||||
1. **Session registry replaces the singletons**. `ServerSessionInstance`/`ServerLoopInstance` become a session table held by `ServerProcess` (slot → {transport, ServerSession, ServerLoop, PipeApplier, apply thread}). The in-process server (`ServerMain.cpp:1593-1651`'s `mobilegl_server_serve_inprocess`) goes from "single active + RefuseBusy" to accepting up to a limit (default 8, configurable); the unix path of `--serve` likewise becomes multiple sessions in the same process, **removing fork-worker dispatch** (`ServerMain.cpp:1566-1584`), and `--max-sessions` becomes the same-process limit. TCP's single-active `TcpSupervisor` (`:1220-1223`) gives way as well.
|
||||
2. **One apply thread per session**. `g_applyThreadKey` (`ServerLoop.h:129`) becomes a per-session member; `OnApplyThread()` semantics unchanged (same-thread comparison). Ordering between sessions is naturally done by the threads, with no global lock; the native driver already supports multiple threads each current with a different context.
|
||||
3. **`g_applier` per session**. The process-level `MGPipeApplierState` in `PipeApply.cpp:484` becomes session-owned; `MGPipeApplierReset`'s role guard (`PipeApply.cpp:1474-1481`) judges by this session's apply thread. The existing share-group object records / working-state split in `PipeApply.h:558-617` is the foundation: object record tables are bucketed by share group, working state by context, and reset clears only this context's working state. **(S5 has landed; for the shape see "S5 landed shape" below: this item ultimately is not "one applier per session" but one applier per {session, context}, whose object records hang on the {session, shareGroup} group.)**
|
||||
4. **Reverse routing by session + context**. `ReverseCallbackOwner`'s (`ServerSession.cpp:408-418`) single-activity `Active()` becomes "resolve by the originating thread / an explicit session parameter"; event headers gain `ContextSlot` (see wire 3). `gMGPipeCallbacks` (`MGPipeCallbacks.h:77`) goes from a process-level variable to a per-session table, attached to the session at Accept rather than globally.
|
||||
5. **The session mode latch stays at the session layer**. `SessionSurfaceMode`/`SurfaceModeMismatch` (`ServerLoop.h:199-206`, `ServerLoop.cpp:1539-1544`) keep their wording: a compositor session latches OnScreen, an app session latches Offscreen; a unified server holding both kinds of session at once is a legal shape, and the latch must not be raised to process level.
|
||||
|
||||
### S2 已落地形状(同进程多 session)
|
||||
### S2 landed shape (multiple sessions in one process)
|
||||
|
||||
上面第 1、2、4、5 条是 P14c 的目标形状;本切片落地的是「归属拆开、路由按线程」这一层,实际形状如下。
|
||||
Items 1, 2, 4 and 5 above are P14c's target shape; this slice landed the layer "ownership split, routing by thread", and the actual shape is below.
|
||||
|
||||
**每 session 一个 runtime,singleton 变成 fallback**(`MG_Remote/Server/SessionRuntime.h`)。`SessionRuntime` 持有该 session 的 `ServerSession`、`ServerLoop`、`PipeInputs` 块,加一个 latch domain 与一个 segment resolver 绑定;`ThreadSessionScope` 把它挂到 **该 session 的两个线程**上——控制线程(`ServerMain::RunSession` 自己的线程,supervisor 里每 session 一条 `std::thread`)与 apply 线程(`mgl-srv-apply`,`ApplyThreadMain` 第一件事就是开同一个 scope)。`ServerSessionInstance()` / `ServerLoopInstance()` / `ServerSession::Active()` 因此先问「本线程属于哪个 session」,只有**不属于任何 session 的线程**才拿进程级单例。这一条让 ~900 个 `gPipeInputs` / `MGB_CTX` / `ServerSession::Active()` 调用点一行不改就有正确归属。inproc 的「同进程 client+server」形状刻意不开 scope(`ClientSession` 直接调单例),所以那个形状的 verb barrier、单块 PipeInputs 与逐字行为完全不变。
|
||||
**One runtime per session, the singleton becomes a fallback** (`MG_Remote/Server/SessionRuntime.h`). `SessionRuntime` holds the session's `ServerSession`, `ServerLoop` and `PipeInputs` block, plus a latch domain and a segment resolver binding; `ThreadSessionScope` attaches it to **the session's two threads**—the control thread (`ServerMain::RunSession`'s own thread, one `std::thread` per session in the supervisor) and the apply thread (`mgl-srv-apply`, whose first act in `ApplyThreadMain` is to open the same scope). `ServerSessionInstance()` / `ServerLoopInstance()` / `ServerSession::Active()` therefore first ask "which session does this thread belong to", and only **a thread that belongs to no session** gets the process-level singleton. This item gives ~900 `gPipeInputs` / `MGB_CTX` / `ServerSession::Active()` call sites correct attribution without changing a line. The inproc "client+server in one process" shape deliberately does not open a scope (`ClientSession` calls the singleton directly), so that shape's verb barrier, single PipeInputs block and character-for-character behavior are completely unchanged.
|
||||
|
||||
**gPipeInputs 是每线程解析的引用**(`MG_Backend/MGPipe/PipeInputs.h`)。`gPipeInputs` 从「一个进程级对象」变成 `thread_local` 引用,经 `g_pipeInputsThreadResolver` 这个由 server 层装一次的探针解析到本线程 session 的块,无 session 时落到 `gPipeInputsDefault`(即原对象)。**为什么是这个形状**:`MGB_CTX` 是 `(&gPipeInputs)`,要让 ~380 个点位不动而目标随线程变,只有「目标随线程变的引用」可行。代价是每个访问点一次 TLS 地址计算(不是每个字段,且无 guard:每线程第一次使用时绑定一次),这是本切片的已知成本。
|
||||
**`gPipeInputs` is a per-thread resolved reference** (`MG_Backend/MGPipe/PipeInputs.h`). `gPipeInputs` goes from "one process-level object" to a `thread_local` reference, resolved through `g_pipeInputsThreadResolver`, a probe the server layer installs once, to this thread's session's block, falling back to `gPipeInputsDefault` (the original object) when there is no session. **Why this shape**: `MGB_CTX` is `(&gPipeInputs)`; to leave ~380 sites unchanged while making the target vary by thread, only "a reference whose target varies by thread" works. The cost is one TLS address computation per access site (not per field, and with no guard: bound once on first use per thread), which is this slice's known cost.
|
||||
|
||||
**apply 线程身份改成 TLS 一位,进程级那个字改成计数。** `OnApplyThread()` 的答案现在只可能是「本线程是不是 apply 线程」,所以它是 `thread_local Bool`(零初始化,无 guard、无原子);`Detail::g_applyThreadKey` 保留原名与类型但装的是**本进程活着的 apply 线程数**——`MG_Backend/DirectGLES/Managers.cpp`(本切片文件集之外)读这个 `!= 0` 问「有没有 apply 线程在跑」,计数正好回答那个问题,热路径没有人再读它。
|
||||
**The apply-thread identity becomes one TLS bit; the process-level word becomes a counter.** `OnApplyThread()`'s answer can now only be "is this thread an apply thread", so it is a `thread_local Bool` (zero-initialized, no guard, no atomic); `Detail::g_applyThreadKey` keeps its name and type but holds **the number of live apply threads in this process**—`MG_Backend/DirectGLES/Managers.cpp` (outside this slice's file set) reads `!= 0` to ask "is any apply thread running", and the count answers exactly that; no hot path reads it anymore.
|
||||
|
||||
**latch 是每 session 一个 domain**(`MG_Remote/FatalFunnel.*`)。原来的四个进程级静态变量变成按 domain 存的 `LatchState`,线程绑定一个 domain(`ThreadSessionScope`),不绑定就是 domain 0(进程默认 = 老行为)。理由不是整洁而是正确性:`SessionLatched()` 的三个读者都在 session 自己的线程上(apply 循环、`DrainRing`、`RunSession` 的控制循环),一个进程共用一个 latch 时 session A 的坏字节能停掉 B 的 apply 线程;`ResetSessionLatch()` 也有镜像问题(一个 session 的 reap 会清掉活着的那个的 latch)。现在 A 的关闭/故障都到不了 B。
|
||||
**The latch is one domain per session** (`MG_Remote/FatalFunnel.*`). The original four process-level statics become per-domain `LatchState`; a thread binds to a domain (`ThreadSessionScope`), and without a binding it is domain 0 (the process default = the old behavior). The reason is correctness, not tidiness: all three readers of `SessionLatched()` are on the session's own threads (the apply loop, `DrainRing`, `RunSession`'s control loop), and when one latch is shared by the process a bad byte in session A can stop B's apply thread; `ResetSessionLatch()` has the mirror problem (one session's reap clearing the live one's latch). Now A's close/fault cannot reach B.
|
||||
|
||||
**segment resolver 是每线程的表 + 进程级的 hook**(`MG_Remote/Wire/PipeWireCodec.*`)。`gMGPipeSegmentResolver` 仍是一个进程级函数指针(装一次,不再置空),但 thunk 先查**调用线程绑定的表**,其次才是进程级 fallback。同一线程上出现第二张表仍然是 `Fatal{...already installed...}`(既有 `APartiallyZeroUploadBoxIsFatalRatherThanASilentRace` 与 `ASecondProcessResolverIsFatalRatherThanASilentRace` 的口径不变);不同线程上的第二个 session 不再是错误。
|
||||
**The segment resolver is a per-thread table plus a process-level hook** (`MG_Remote/Wire/PipeWireCodec.*`). `gMGPipeSegmentResolver` remains a process-level function pointer (installed once, never cleared again), but the thunk first checks **the calling thread's bound table**, and only then the process-level fallback. A second table on the same thread is still `Fatal{...already installed...}` (the wording of the existing `APartiallyZeroUploadBoxIsFatalRatherThanASilentRace` and `ASecondProcessResolverIsFatalRatherThanASilentRace` is unchanged); a second session on a different thread is no longer an error.
|
||||
|
||||
**全局回调表:装一次、按线程路由、最后一个 session 关才卸。** `gMGPipeCallbacks` 的五个条目仍是进程级函数指针(每个 session 装的是同一批函数),`Close()` 只有在 `ServerSessionCount()` 归零时才清;`g_sessionOwner` 的 CAS 抢占对**有 runtime 的 session 不再生效**(共表是合法的),只对「进程就是一个 session」的形状(inproc client+server 角色、`ServerLoopTest` 的 `FvSession` 及其对等对象)保留原样的 `Fatal{RoleViolation,"callback-double-install"}`。
|
||||
**The global callback table: installed once, routed by thread, removed only when the last session closes.** The five entries of `gMGPipeCallbacks` remain process-level function pointers (every session installs the same set); `Close()` clears them only when `ServerSessionCount()` reaches zero; `g_sessionOwner`'s CAS claim **no longer applies to sessions with a runtime** (a shared table is legal), and keeps the original `Fatal{RoleViolation,"callback-double-install"}` only for shapes where "the process is one session" (the inproc client+server roles, `ServerLoopTest`'s `FvSession` and its peers).
|
||||
|
||||
**surface mode 仍是 D4 的 per-session 闩**(`ServerLoop::m_surfaceMode`)。本切片一度引入 `SetDeclaredSurfaceMode`,让 `RunSession` 按**进程自己的** `MOBILEGL_IPC_SURFACE` 给 session 声明模式——**已撤掉**,两条理由:(a) 进程级的环境变量说不了统一 server 同时持有的多个 session(§4 第 5 条要的是口径不变、闩不许提升到进程级),(b) 它对 dial 进来的 client 是错的——pre-started server 不继承 client 的 env(`ServerOwnedSurfaceTest` 的 TCP 形状),于是 session 被声明成 offscreen,ServerOwned 的建面先拿 `SurfaceModeMismatch`,永远走不到「本 server 没有 display」那条具名拒绝。**而且它不需要**:`m_surfaceMode` 是 `ServerLoop` 的成员,每个 session 有自己的 loop,两个 session 各自闩各自的模式——「每 session 独立 surface mode」正是这个形状,不需要任何声明。真正想在第一个 surface 之前声明模式的形状(P14d 的 KWin on-screen + Konsole offscreen 同一个 server)必须**让模式上 wire**(Hello 或一条 SurfaceOp),那是 P14d 的账;在那之前,谁先建 surface 谁闩定,是每个 session 各自的答案。**`ServerDisplay` 的 lease 天然多路复用**:holder 是 loop 指针,`AcquireFor` 只会把窗口租给一个 session,第二个 on-screen session 拿到的是**具名拒绝** `NoServerWindow`(日志里点名 LeasedElsewhere),不再是注释里那句「sessions are sequential: a bug if seen」;offscreen session 完全不取 lease,可以任意多。
|
||||
**The surface mode is still D4's per-session latch** (`ServerLoop::m_surfaceMode`). This slice once introduced `SetDeclaredSurfaceMode`, letting `RunSession` declare a mode for the session from the **process's own** `MOBILEGL_IPC_SURFACE`—**it has been withdrawn**, for two reasons: (a) a process-level environment variable cannot speak for the multiple sessions a unified server holds at once (§4 item 5 asks for unchanged wording and a latch that must not be raised to process level), and (b) it is wrong for a client that dials in—a pre-started server does not inherit the client's env (the TCP shape of `ServerOwnedSurfaceTest`), so the session is declared offscreen, ServerOwned's surface creation hits `SurfaceModeMismatch` first, and it never reaches the named refusal "this server has no display". **And it is not needed**: `m_surfaceMode` is a member of `ServerLoop`, each session has its own loop, and the two sessions latch their own modes—"an independent surface mode per session" is exactly this shape and needs no declaration at all. A shape that really wants to declare the mode before the first surface (P14d's KWin on-screen + Konsole offscreen on the same server) must **put the mode on the wire** (in Hello or a SurfaceOp); that is P14d's debt; until then, whoever creates the first surface latches it, and that is each session's own answer. **`ServerDisplay`'s lease is naturally multiplexed**: the holder is a loop pointer, `AcquireFor` leases the window to only one session, and a second on-screen session gets the **named refusal** `NoServerWindow` (naming LeasedElsewhere in the log), no longer the comment's "sessions are sequential: a bug if seen"; offscreen sessions take no lease at all and can be arbitrarily many.
|
||||
|
||||
**接入层**:`UnixInProcessSupervisor`(`mobilegl_server_serve_inprocess` 的 unix 分支)从「一次一个 + 第二个 Refuse{Busy} one session is already active」改成**并发至多 `MOBILEGL_IPC_INPROC_MAX_SESSIONS`(默认 16)**,每个 session 一条控制线程,超限时用 fork 路径同一句话 `Refuse{Busy} the session limit is reached`;embedded ABI 的签名不变。**没动的三处**:`--serve --max-sessions` 的 fork worker 路径(进程隔离仍是可选形态,`UnixSupervisor.ConcurrentSharedMemoryClientsHaveSeparateWorkersAndAnExitedSlotIsReused` 与 `...RequiresAnExplicitLimit` 照旧)、TCP 的 in-process 形状(仍是一 session 一次,`InProcessServer.AnAuthenticatedSecondHelloWhileASessionIsLiveIsRefusedBusy` 照旧)、`TcpSupervisor` 的 fork 形状。与本文 §4 第 1 条写的「默认 8」差一个数:本切片取 16,且上限是环境变量而非新 ABI 参数。
|
||||
**The access layer**: `UnixInProcessSupervisor` (the unix branch of `mobilegl_server_serve_inprocess`) goes from "one at a time + a second gets Refuse{Busy} one session is already active" to **concurrent up to `MOBILEGL_IPC_INPROC_MAX_SESSIONS` (default 16)**, one control thread per session, and over the limit it uses the same sentence as the fork path, `Refuse{Busy} the session limit is reached`; the embedded ABI signature is unchanged. **Three places untouched**: the `--serve --max-sessions` fork worker path (process isolation remains an optional shape; `UnixSupervisor.ConcurrentSharedMemoryClientsHaveSeparateWorkersAndAnExitedSlotIsReused` and `...RequiresAnExplicitLimit` stay as they are), the in-process TCP shape (still one session at a time; `InProcessServer.AnAuthenticatedSecondHelloWhileASessionIsLiveIsRefusedBusy` stays), and the fork shape of `TcpSupervisor`. One number differs from "default 8" written in §4 item 1 of this document: this slice takes 16, and the limit is an environment variable rather than a new ABI parameter.
|
||||
|
||||
**验收证据**(本切片新增):`MG_Test/Wire/MultiSessionTest.cpp` 三个用例——`MultiSessionFixture.TwoShmSessionsInOneProcessAreServedAtOnce`(同进程两个 SHM session 并发:两个 Welcome 同 pid、日志里两条 `in-process unix session #N started`、各自 clear 各自读回自己的颜色)、`MultiSessionFixture.SameNamesInTwoSessionsDoNotCollide`(同一 client context token 与同一 render state handle 在两个 session 里都被接受——名字是 session 内的名字)、`MultiSessionFixture.ClosingOneSessionLeavesTheOtherServing`(A 关掉并 reap 之后 B 继续 apply 三条记录,进程级 resolver/回调表没被 A 卸掉)。既有套件不回归:本切片点名的 `InProcessServerTest`、`ServerDisplayTest`、`UnixSupervisor`、`SupervisorChildren`、`ServerLoopTest`/`ServerLoopEglTest`/`ServerLoopLatchTest`、`EmbeddedServerTest`、`PeerLatchTest`(含 `PeerLatch.SiteMap`,其 FatalFunnel 的 check 正则已跟着 domain 改写更新)、`ServerOwnedSurfaceTest`、`SessionHandshakeTest`、`SurfaceControlFrameTest` 全绿(host ctest 里剩下的失败只有 `3rdparty/glslang` 那个 dirty submodule 造成的编译/链接期基线失败,与本切片无关)。
|
||||
**Acceptance evidence** (added by this slice): three cases in `MG_Test/Wire/MultiSessionTest.cpp`—`MultiSessionFixture.TwoShmSessionsInOneProcessAreServedAtOnce` (two SHM sessions concurrently in one process: two Welcomes with the same pid, two `in-process unix session #N started` lines in the log, each clears and reads back its own color), `MultiSessionFixture.SameNamesInTwoSessionsDoNotCollide` (the same client context token and the same render state handle are both accepted in two sessions—names are names within a session), `MultiSessionFixture.ClosingOneSessionLeavesTheOtherServing` (after A is closed and reaped, B keeps applying three records; the process-level resolver/callback table was not removed by A). The existing suites do not regress: `InProcessServerTest`, `ServerDisplayTest`, `UnixSupervisor`, `SupervisorChildren`, `ServerLoopTest`/`ServerLoopEglTest`/`ServerLoopLatchTest`, `EmbeddedServerTest`, `PeerLatchTest` (including `PeerLatch.SiteMap`, whose FatalFunnel check regex was updated with the domain rewrite), `ServerOwnedSurfaceTest`, `SessionHandshakeTest`, `SurfaceControlFrameTest` are all green (the only remaining failures in host ctest are the compile/link-time baseline failures caused by the dirty `3rdparty/glslang` submodule, unrelated to this slice).
|
||||
|
||||
**修正(`HealthySessionWithColor` 的颜色解包)**:本切片把 `P12ServerRig.h` 的绿色 clear 参数化时,一度把 `rgba` 按 `A<<24|B<<16|G<<8|R` 的逆序解包(把最高字节当 R),于是 `0xFF00FF00`(绿)被清成 `0x00FF00FF`(品红),`InProcessServer.SequentialSessions...` 的读回断言因此挂掉。正确的解包就是读回自己的字节序——`PeerReport::pixel` 是 GL_RGBA/GL_UNSIGNED_BYTE 三元组的 little-endian `Uint32`,故 `R = rgba & 0xFF … A = rgba >> 24`。**生产路径(clear → read_pixels)一个字节没动**:`PeerLatchTest` 用同一绿色的字面量 `{0,1,0,1}` 且断言同一个 `0xFF00FF00`,它的对应用例全程是绿的,就是这条结论的控制。红色 `0xFF0000FF` 在字节反转下是回文,所以当时只有绿色 session 失败——这不是两个 bug。
|
||||
**Correction (the color unpacking of `HealthySessionWithColor`)**: when this slice parameterized the green clear in `P12ServerRig.h`, it once unpacked `rgba` in the reversed order `A<<24|B<<16|G<<8|R` (treating the top byte as R), so `0xFF00FF00` (green) was cleared to `0x00FF00FF` (magenta), and `InProcessServer.SequentialSessions...`'s readback assertion failed. The correct unpacking is simply readback's own byte order—`PeerReport::pixel` is a little-endian `Uint32` of a GL_RGBA/GL_UNSIGNED_BYTE triple, so `R = rgba & 0xFF … A = rgba >> 24`. **The production path (clear → read_pixels) did not change a byte**: `PeerLatchTest` uses the same green literal `{0,1,0,1}` and asserts the same `0xFF00FF00`, and its corresponding case was green throughout, which is the control for this conclusion. Red `0xFF0000FF` is a palindrome under byte reversal, which is why only the green session failed at the time—this is not two separate bugs.
|
||||
|
||||
**本切片明确没做(下一层的账)**:DirectGLES 的 `g_Display/g_Config/g_Context/g_Surface` 与六个 twin registry 仍是进程级(P14b 的 per-context native tuple),所以两个并发 session 的**原生上下文并不隔离**:一个 session 的 teardown 仍会拆掉进程的 EGL context,本切片的渲染验收因此只在两个 session 都活着时做,关掉一个之后的证据是记录级的(记录照常 apply 并有回包)。`MGPipeServerSetContextLive` / `MGPipeServerSetOwnedWindow` 与 `PipeInputs.cpp` 同属进程级,不在本切片的文件范围内。`MG_Util::PipeStats::Init/Shutdown` 仍是进程级,多 session 下是遥测噪声不是正确性。`SessionFail`(未武装时的真 Fatal)仍会带走整个进程——那是 `--serve` fork 形状存在的理由。
|
||||
**What this slice explicitly did not do (the next layer's debt)**: DirectGLES's `g_Display/g_Config/g_Context/g_Surface` and the six twin registries are still process-level (P14b's per-context native tuple), so the **native contexts of two concurrent sessions are not isolated**: one session's teardown still tears down the process's EGL context, so this slice's rendering acceptance is done only while both sessions are alive; the evidence after closing one is at the record level (records are still applied and get replies). `MGPipeServerSetContextLive` / `MGPipeServerSetOwnedWindow` and `PipeInputs.cpp` are likewise process-level and outside this slice's file set. `MG_Util::PipeStats::Init/Shutdown` is still process-level; under multiple sessions it is telemetry noise, not correctness. `SessionFail` (a real Fatal when not armed) still takes the whole process down—that is the reason the `--serve` fork shape exists.
|
||||
|
||||
## 后端(DirectGLES)
|
||||
## Backend (DirectGLES)
|
||||
|
||||
1. **native tuple per context**。`g_Display` 保留进程级(native display 一次 initialize 合理);`g_Context`/`g_Surface`/`g_Config`(`DirectGLES.cpp:16183-16186`)改为 per-(session, contextSlot) 表,native context 按 share group 用 `eglCreateContext(share=…)` 建。
|
||||
2. **拆掉无条件 teardown**。`InitDisplayAndContext` 首句 `DestroyEGLContext()`(`DirectGLES.cpp:16569`)与 `BackendObject_DirectGLES` 建 surface 前的 `DestroyEGLContext()+ResetEGLRuntimeState()`(`BackendObject_DirectGLES.cpp:972-982`、`:1007-1014`)改为:目标 context 已存在则复用其 native context,仅换绑 surface;pbuffer→window→pbuffer 不丢资源。`DestroyEGLContext` 只拆指定 context;最后一个 context 走了才 `eglTerminate`。
|
||||
3. **Managers 按 share group / context 分表**。六个 twin registry(`Managers.cpp:6895`、`:11035`、`:12835`、`:13188`、`:16255`、`:16580`)按 share group 实例化;`g_activeTextureUnit`/`g_boundTexturesCache`/sampler cache(`Managers.cpp:11031-11032` 等)按 context 实例化;`DropEveryTwinForEndedServerSession`(`Managers.cpp:425-443`)只清本 session。`g_backendContextGeneration` 改为 per-context 世代。**(twin 键与 per-session 清理已落地,见 S6;单元影子与 context 世代仍欠。)**
|
||||
4. **DirectVulkan 同步审视**。Magma 的 per-device/per-queue 结构也要按 session 分;本阶段先保证不编译退化,多 session 下 Magma 的验收单列。
|
||||
1. **Native tuple per context**. `g_Display` stays process-level (initializing the native display once is reasonable); `g_Context`/`g_Surface`/`g_Config` (`DirectGLES.cpp:16183-16186`) become a per-(session, contextSlot) table; native contexts are created per share group with `eglCreateContext(share=…)`.
|
||||
2. **Remove the unconditional teardown**. `InitDisplayAndContext`'s first statement `DestroyEGLContext()` (`DirectGLES.cpp:16569`) and `BackendObject_DirectGLES`'s `DestroyEGLContext()+ResetEGLRuntimeState()` before surface creation (`BackendObject_DirectGLES.cpp:972-982`, `:1007-1014`) become: if the target context already exists, reuse its native context and only rebind the surface; pbuffer→window→pbuffer loses no resources. `DestroyEGLContext` tears down only the given context; `eglTerminate` only when the last context is gone.
|
||||
3. **Managers: one table per share group / context**. The six twin registries (`Managers.cpp:6895`, `:11035`, `:12835`, `:13188`, `:16255`, `:16580`) are instantiated per share group; `g_activeTextureUnit`/`g_boundTexturesCache`/the sampler cache (`Managers.cpp:11031-11032` etc.) per context; `DropEveryTwinForEndedServerSession` (`Managers.cpp:425-443`) clears only this session. `g_backendContextGeneration` becomes a per-context generation. **(The twin key and per-session cleanup have landed, see S6; the unit shadow and the context generation are still owed.)**
|
||||
4. **Review DirectVulkan alongside**. Magma's per-device/per-queue structures also need to be split by session; this phase only ensures no compile regression, and Magma's acceptance under multiple sessions is listed separately.
|
||||
|
||||
### S4 已落地形状(每 context 原生 tuple + share-group 键控)
|
||||
### S4 landed shape (native tuple per context + share-group keying)
|
||||
|
||||
上面第 1、2 条是 P14b 的目标形状;S4 落地了「native tuple 按 context 归属、创建第二个 context/surface 不再拆 context、share group 进原生」这一层,实际形状如下。
|
||||
Items 1 and 2 above are P14b's target shape; S4 landed the layer "native tuple attributed by context, creating a second context/surface no longer tears down the context, share group enters native", and the actual shape is below.
|
||||
|
||||
**native tuple 按 (session, token) 键控**(`MG_Backend/DirectGLES/DirectGLES.cpp` 的 `NativeContextTuple` / `NativeSessionState` 注册表)。`g_Display` 仍是进程级一份、`eglInitialize` 一次,只有最后一个原生 context 走了才 `eglTerminate`(`g_displayUsers` 计数)。`EGLContext`/`EGLConfig` 随 tuple;**原生 surface 按 session 一份**(EGL 里 surface 不属于 context,只有 draw/read 绑定属于),tuple 记自己当前画到哪个 surface。键由 server 层装的探针回答(`SetNativeContextKeyResolver`,`MG_Remote/Server/SessionRuntime.cpp` 经 `ThreadSessionScope` 装一次):答案是**本线程的 session 身份 + `ServerSession::CurrentContextToken()`**;没有探针的线程(monolith、纯前端进程、unit case、inproc 单进程形状)答案恒为 `{0,0}`——这正是被替换掉的进程单例,所以单 context 世界逐字不变。
|
||||
**The native tuple is keyed by (session, token)** (`MG_Backend/DirectGLES/DirectGLES.cpp`'s `NativeContextTuple` / `NativeSessionState` registry). `g_Display` is still one per process with a single `eglInitialize`, and `eglTerminate` only when the last native context is gone (`g_displayUsers` counter). `EGLContext`/`EGLConfig` follow the tuple; **the native surface is one per session** (in EGL a surface does not belong to a context, only the draw/read binding does), and the tuple records which surface it currently draws to. The key is answered by a probe the server layer installs (`SetNativeContextKeyResolver`, installed once in `MG_Remote/Server/SessionRuntime.cpp` through `ThreadSessionScope`): the answer is **this thread's session identity + `ServerSession::CurrentContextToken()`**; on a thread without a probe (monolith, pure frontend process, unit case, inproc single-process shape) the answer is always `{0,0}`—which is exactly the process singleton being replaced, so the single-context world is unchanged character for character.
|
||||
|
||||
**创建不再拆 context**。`InitDisplayAndContext()`(首句无条件 `DestroyEGLContext()`)换成 `EnsureNativeContext(tuple, surfaceBit, window)`:tuple 已有原生 context 就复用,只按需要选 config;没有才建。`BackendObject_DirectGLES::CreateEGL{Window,Pbuffer}Surface` 里的 `DestroyEGLContext()+ResetEGLRuntimeState()` 同样去掉 teardown,只留 base class 的 per-surface bookkeeping。因此 pbuffer→window→pbuffer 只是往 session 的 surface 集合里加/换,tuple 的 `EGLContext` 一字不动——对象(后端 twin 直接活在原生 context 里)自然不丢。`DestroyEGLContext()` 现在是**本 session** 的全量 teardown(`ReleaseEGLResources`/`~BackendObject_DirectGLES` 的语义),单个 context 的销毁是新的 `DestroyNativeContextFor(token)`。
|
||||
**Creation no longer tears down the context**. `InitDisplayAndContext()` (whose first statement was the unconditional `DestroyEGLContext()`) is replaced by `EnsureNativeContext(tuple, surfaceBit, window)`: if the tuple already has a native context, reuse it and only choose a config if needed; create one only if there is none. `BackendObject_DirectGLES::CreateEGL{Window,Pbuffer}Surface` likewise drops the teardown of `DestroyEGLContext()+ResetEGLRuntimeState()` and keeps only the base class's per-surface bookkeeping. So pbuffer→window→pbuffer merely adds to or switches within the session's surface set, and the tuple's `EGLContext` does not change a character—so objects (backend twins live directly in the native context) are naturally not lost. `DestroyEGLContext()` is now the **session's** full teardown (the semantics of `ReleaseEGLResources`/`~BackendObject_DirectGLES`); destroying a single context is the new `DestroyNativeContextFor(token)`.
|
||||
|
||||
**share group 进原生**(`CreateNativeContextFor(token, shareGroup)`)。`CreateContext` 控制帧到达时,server 为该 token 建原生 context:**同 session 内、同一 shareGroupToken 且已有活 context 的 tuple 的 `EGLContext` 作为 `eglCreateContext` 的 share 参数**,否则 `EGL_NO_CONTEXT`。组内后续 context 因此共享原生对象命名空间。建失败则回滚该 session 表项并把 `ok=false` 回给客户端。
|
||||
**The share group enters native** (`CreateNativeContextFor(token, shareGroup)`). When the `CreateContext` control frame arrives, the server creates a native context for the token: **within the same session, the `EGLContext` of the tuple with the same shareGroupToken that already has a live context is used as the share parameter of `eglCreateContext`**, otherwise `EGL_NO_CONTEXT`. Later contexts in the group therefore share the native object namespace. If creation fails, it rolls back that session entry and returns `ok=false` to the client.
|
||||
|
||||
**bind_context 是原生切换点**。MakeCurrent 控制帧到的时刻 `CurrentContextToken()` 还是旧值(S1 刻意让客户端在 make-current 成功后才发绑定记录),所以在那里切原生会绑到刚离开的 context。`ServerVerbSink::OnBindContext`(`MG_Remote/Server/PipeApplier.cpp`)在 `session->BindContext(token)` 之后调 `DirectGLES::MakeNativeContextCurrentForBoundToken()`:按新 token 解析 tuple,与 `t_boundNativeContext` 相同就返回(单 context 的每次绑定零成本),不同才做原生 `eglMakeCurrent`。
|
||||
**`bind_context` is the native switch point**. When the MakeCurrent control frame arrives, `CurrentContextToken()` is still the old value (S1 deliberately lets the client emit the binding record only after make-current succeeds), so switching native there would bind to the context just left. `ServerVerbSink::OnBindContext` (`MG_Remote/Server/PipeApplier.cpp`) calls `DirectGLES::MakeNativeContextCurrentForBoundToken()` after `session->BindContext(token)`: resolve the tuple by the new token, return if it is the same as `t_boundNativeContext` (each bind of a single context costs zero), and do a native `eglMakeCurrent` only when different.
|
||||
|
||||
**验收证据**(本次新增,`MG_Test/Wire/ServerLoopTest.cpp`,真实 headless EGL):`ServerLoopEglTest.CreatingASecondContextLeavesTheFirstNativeContextAlive`(同一 session 三个 context token,三个原生 EGLContext 同时活着且互不相同——还原 S4 前的无条件 teardown 时只剩一个);`ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext`(建第二个 pbuffer 后 tuple 的 `EGLContext` 句柄不变、原生 context 数仍为 1)。既有集合零回归:本切片点名的 97 个用例全绿(86 基线 + 11 `ServerLoopEglTest` + 上面两个)。
|
||||
**Acceptance evidence** (added this commit, `MG_Test/Wire/ServerLoopTest.cpp`, real headless EGL): `ServerLoopEglTest.CreatingASecondContextLeavesTheFirstNativeContextAlive` (three context tokens in one session; three native EGLContexts alive at the same time and distinct—restoring the pre-S4 unconditional teardown leaves only one); `ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext` (after creating a second pbuffer the tuple's `EGLContext` handle is unchanged and the native context count is still 1). The existing set does not regress: the 97 cases this slice names are all green (86 baseline + 11 `ServerLoopEglTest` + the two above).
|
||||
|
||||
**本切片明确没做(下一层的账)**:
|
||||
**What this slice explicitly did not do (the next layer's debt)**:
|
||||
|
||||
- **`g_applier` 没有改成 {shareGroup → applier}**(上面 server 第 3 条)。原因不是时间,是**形状**:`MGPipeApplierState` 把「share group 的对象记录」和「每 context 的 working state」混在一个结构里(`PipeApply.h:515-933`,其自身注释第 101 行已指出该二分),照字面把它按 share group 键控会让**同一 share group 的两个 context 共用 working state**——bindings、current program、unit 集互相覆盖,比不改更错。正确的做法是先按该二分拆结构(对象表进 share group、working state 进 context),那是独立的一刀。今天 `g_applier` 与 `gMGPipeApplierReset` 逐字未动。**(S5 已做,见下节。)**
|
||||
- **六个 twin registry 与 twin 键 `{shareGroup, slot, gen}` 未改**(`Managers.cpp`)。twin 仍按引用活着的后端 context 复用,多 context 下的正确性依赖「一个原生 context 一个 twin 世代」,这在两个 context 都活着时成立(本次测到的),在跨 context 迁移 twin 的路径上还没有证据。**(S5 仍未做;S6 已做,见「S6 已落地形状」。)**
|
||||
- **share group 的驱动级证明未落地**。`eglCreateContext(share=…)` 已接线,但用 headless EGL 直接问 `glIsBuffer` 的探针在本机没有得到可用的控制(同 context 内 `glGenBuffers` 后 `glIsBuffer` 即返回 false),没有作为「跑了个空的绿」收进来。要证到驱动层,需要先弄清该探针,再补一个用例。**(S5 也没弄清:见 S5 的未做项与「为什么没做」。)**
|
||||
- **Wire 第 1 条的逐记录 `ContextSlot` 未做**,仍是 S1 的边沿宣告 + `bind_context`。
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration` 仍是进程级**:销毁单个 context 会推进全局世代,其他 context 的旧 fence/query 句柄因此读作「已 signal」。安全方向(最坏是多等),但不是每 context 世代,属 P14b/c。
|
||||
- **ServerLoop 的 MakeCurrent 去重仍是「当前绑定的那一个 tuple」**,不是 per-context 表。理由写在 `ServerLoop.h:560-573`:ID-67 的钉死用例(A→B→A 必须三次原生绑定)定义「不同 tuple」为**当前绑定**的变化,而 EGL 里一个线程同时只有一个 current context,回到 A 确实必须重跑那七条前端 context 失效。「每 context 持有自己的 native triple」由后端的 {session, token} 注册表满足,去重这一层的单位仍是线程的当前绑定。
|
||||
- **`g_applier` was not changed to {shareGroup → applier}** (server item 3 above). The reason is not time but **shape**: `MGPipeApplierState` mixes "the share group's object records" and "the per-context working state" in one structure (`PipeApply.h:515-933`; its own comment at line 101 already points out this split), and keying it by share group literally would make **two contexts of the same share group share working state**—bindings, current program and unit sets overwrite each other, which is worse than not changing. The right approach is to first split the structure along that line (object tables into the share group, working state into the context), which is a separate cut. Today `g_applier` and `gMGPipeApplierReset` are untouched character for character. **(S5 did it, see the next section.)**
|
||||
- **The six twin registries and the twin key `{shareGroup, slot, gen}` are unchanged** (`Managers.cpp`). A twin is still reused by a backend context that is alive by reference; correctness under multiple contexts rests on "one native context, one twin generation", which holds while both contexts are alive (measured this time) but has no evidence on paths that migrate a twin across contexts. **(S5 still did not do it; S6 did, see "S6 landed shape".)**
|
||||
- **Driver-level proof of the share group did not land**. `eglCreateContext(share=…)` is wired, but the probe that asks `glIsBuffer` directly through headless EGL got no usable control on this machine (`glIsBuffer` returns false right after `glGenBuffers` in the same context), so it was not taken in as "an empty run that was green". Proving it at the driver layer requires understanding that probe first and then adding a case. **(S5 did not figure it out either: see S5's not-done items and "why it was not done".)**
|
||||
- **Wire item 1's per-record `ContextSlot` is not done**; it is still S1's edge announcement + `bind_context`.
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration` are still process-level**: destroying a single context advances the global generation, so other contexts' old fence/query handles read as "already signaled". The safe direction (at worst waiting longer), but not a per-context generation; belongs to P14b/c.
|
||||
- **ServerLoop's MakeCurrent deduplication is still "the one currently bound tuple"**, not a per-context table. The reason is in `ServerLoop.h:560-573`: ID-67's pinned case (A→B→A must be three native binds) defines "different tuple" as a change of **the current binding**, and in EGL a thread has only one current context at a time, so returning to A really must rerun those seven frontend context invalidations. "Each context holds its own native triple" is satisfied by the backend's {session, token} registry; the unit of this deduplication layer is still the thread's current binding.
|
||||
|
||||
### S5 已落地形状(applier 按 {session, context} / {session, shareGroup} 键控)
|
||||
### S5 landed shape (applier keyed by {session, context} / {session, shareGroup})
|
||||
|
||||
S4 留下的最后一块:`PipeApply.cpp` 的进程级 `g_applier` 现在是一个**每 (session, context) 的注册表**,它的对象记录按 {session, share group} 分桶。
|
||||
The last piece S4 left: `PipeApply.cpp`'s process-level `g_applier` is now a **per-(session, context) registry**, whose object records are bucketed by {session, share group}.
|
||||
|
||||
**结构拆分(`MG_Pipe/PipeApply.h`)**。原来一个 `MGPipeApplierState` 按两个寿命切成两个结构:
|
||||
**Structure split** (`MG_Pipe/PipeApply.h`). The former single `MGPipeApplierState` is split into two structures by two lifetimes:
|
||||
|
||||
| `MGPipeObjectRecords`(share group) | `MGPipeWorkingState`(context) |
|
||||
| `MGPipeObjectRecords` (share group) | `MGPipeWorkingState` (context) |
|
||||
|---|---|
|
||||
| `Resources`、`VertexElementsCsos`、`TextureResources`、`RenderbufferResources`、`SamplerCsos`、`SamplerViewCsos`、`ShaderCsos`、`CompositeShaderCsos`、`FramebufferRecords` | `RenderStateCsos`、`BoundRenderStateCso`、`Residual`、`ScatteredChunkBits`、四个 wire 计数器与四个 refusal 计数器、`BoundVertexElements`/`VertexBuffers*`/`IndexBuffer*`、`MapPersistentRoundtrips`、`BoundFramebuffer`/`FramebufferSerial`/`StaleFramebufferRecordLookups`、三个 unit 窗口、三个 program handle、三个 binding-point 窗口、`IsTransformFeedbackActive`、`TextureShutterSerial`/`ContextSerial`、`#if DISAGGREGATED` 的 verb handle 组 |
|
||||
| `Resources`, `VertexElementsCsos`, `TextureResources`, `RenderbufferResources`, `SamplerCsos`, `SamplerViewCsos`, `ShaderCsos`, `CompositeShaderCsos`, `FramebufferRecords` | `RenderStateCsos`, `BoundRenderStateCso`, `Residual`, `ScatteredChunkBits`, the four wire counters and the four refusal counters, `BoundVertexElements`/`VertexBuffers*`/`IndexBuffer*`, `MapPersistentRoundtrips`, `BoundFramebuffer`/`FramebufferSerial`/`StaleFramebufferRecordLookups`, the three unit windows, the three program handles, the three binding-point windows, `IsTransformFeedbackActive`, `TextureShutterSerial`/`ContextSerial`, the verb handle group under `#if DISAGGREGATED` |
|
||||
|
||||
`RenderStateCsos` 在 working 一侧不是笔误:它今天是**被 `MGPipeApplierReset` 清掉的**(每个 make-current 一份新 CSO 表),照旧规则留在 per-context 那半。`FramebufferRecords` 留在对象一侧也是照旧规则(reset 不清它),且它的槽来自全局分配器,所以一个 share group 装两个 context 的记录是「更宽的表」而不是别名。
|
||||
`RenderStateCsos` on the working side is not a typo: it is **cleared by `MGPipeApplierReset`** today (a fresh CSO table per make-current), and by the existing rule it stays in the per-context half. `FramebufferRecords` staying on the object side is also by the existing rule (reset does not clear it), and its slots come from the global allocator, so one share group holding the records of two contexts is "a wider table" rather than an alias.
|
||||
|
||||
`MGPipeApplierState` 现在**继承** `MGPipeWorkingState` 并持有一个 `MGPipeObjectRecords& Objects`,另把九张对象表**按原名字绑成引用成员**(`Resources`、`TextureResources`…)。理由写在头里:MG_Backend / MG_IntegrationTest / MG_Test 里 ~200 个调用点一直把 `st.Resources` 读作「这个对象的 share group 记录」,绑引用让这些读点**一个不改**且按各自 context 所属的 group 正确解析;把键控改动变成改名改动只增风险不增正确性。头文件里可以同时用 `Objects.X`(在**创建**两半的地方显式点出群体)与 `X`(后端一直的拼法),两者是同一张表。
|
||||
`MGPipeApplierState` now **inherits** `MGPipeWorkingState` and holds a `MGPipeObjectRecords& Objects`, and also binds the nine object tables **as reference members under their original names** (`Resources`, `TextureResources`…). The reason is written in the header: the ~200 call sites in MG_Backend / MG_IntegrationTest / MG_Test have always read `st.Resources` as "this object's share group record"; binding references leaves those read sites **unchanged** while resolving correctly by the group their context belongs to; turning a keying change into a renaming change adds risk without adding correctness. The header can use both `Objects.X` (making the group explicit where the two halves are **created**) and `X` (the spelling the backend has always used); they are the same table.
|
||||
|
||||
**注册表与解析链(`PipeApply.cpp`)**。三层,和 S2 的 `gPipeInputs`、S4 的 native tuple key 同构:
|
||||
**Registry and resolution chain** (`PipeApply.cpp`). Three layers, isomorphic to S2's `gPipeInputs` and S4's native tuple key:
|
||||
|
||||
- `MGPipeApplierKey{ SessionKey, ContextToken }`,由 `MGPipeSetApplierKeyResolver` 装的探针回答;`SessionRuntime.cpp` 在 `InstallSessionRuntimeHooks` 里装(`ThreadSessionScope` 一开就装)。探针答案是**本线程 session 的 `ServerSession*` 地址 + `CurrentContextToken()`**;不是 session 线程的答案 false,拿到进程级 applier —— 这正是被替换掉的单例,所以 monolith / 纯客户端 / inproc 单进程形状 / 单元用例逐字不变。
|
||||
- `MGPipeApplierRegisterContext(session, token, groupToken)` / `MGPipeApplierUnregisterContext` / `MGPipeApplierReleaseSession`。**注册而不是推断**:`ServerSession::CreateContext` / `DestroyContext` 在 apply 线程上调用它们,所以解析探针永远是「读本 session 已经在维护的当前 token」两字,不必从别的线程去读 session 的 map;`Accept` 与 `~SessionRuntime` 释放整 session。
|
||||
- **group 键**:`shareGroupToken != 0 ? shareGroupToken : (contextToken | 高位哨兵)`。前端铸的 share group token 与 context token 来自两个计数器,不隔离就会让 group 7 和 context 7 变成一组;`0` 是 S1 注释里「本 context 自己起了一个组」的哨兵(也是 `ServerCreateEGLContext(token, 0, 0)` 这类手工建 context 传的值),落成「自己的组」而不是「和别的 0 一组」。
|
||||
- **最后一个 context 走才释放该组的对象记录**(`MGPipeApplierReleaseObjectRecords` 的 per-group 版);一组的另一个 context 还在时记录一条不丢。
|
||||
- **线程局部缓存 + 世代号**。`MGPipeApplier()` 是后端按状态读调的,所以快路径是「本线程上次的 (session, token) 与注册表世代都对得上就直接返回」;注册表每次变动 `++generation`,缓存指针只有在世代相等时才被解引用 —— 这就是「另一个线程销毁了 context」不会留下悬垂缓存的原因。快路径开销是三个 TLS 读、一次原子读、两次比较;探针为 null(monolith)时是一次函数指针比较。
|
||||
- `MGPipeApplierKey{ SessionKey, ContextToken }`, answered by a probe installed by `MGPipeSetApplierKeyResolver`; `SessionRuntime.cpp` installs it in `InstallSessionRuntimeHooks` (installed as soon as `ThreadSessionScope` opens). The probe's answer is **the address of this thread's session's `ServerSession*` + `CurrentContextToken()`**; a thread that is not a session thread answers false and gets the process-level applier—which is exactly the singleton being replaced, so monolith / pure client / inproc single-process shape / unit cases are unchanged character for character.
|
||||
- `MGPipeApplierRegisterContext(session, token, groupToken)` / `MGPipeApplierUnregisterContext` / `MGPipeApplierReleaseSession`. **Register rather than infer**: `ServerSession::CreateContext` / `DestroyContext` call these on the apply thread, so the resolution probe is always just "read the current token this session already maintains", without reading the session's map from another thread; `Accept` and `~SessionRuntime` release the whole session.
|
||||
- **Group key**: `shareGroupToken != 0 ? shareGroupToken : (contextToken | high-bit sentinel)`. The frontend's share group tokens and context tokens come from two counters; without isolation, group 7 and context 7 would become one group; `0` is the sentinel for "this context started its own group" from the S1 comment (and the value passed by hand-built contexts such as `ServerCreateEGLContext(token, 0, 0)`), landing as "its own group" rather than "the same group as the other 0".
|
||||
- **The group's object records are released only when the last context leaves** (the per-group version of `MGPipeApplierReleaseObjectRecords`); while the other context of a group is still there, not one record is lost.
|
||||
- **Thread-local cache + generation number**. `MGPipeApplier()` is called by the backend on every state read, so the fast path is "if this thread's last (session, token) and the registry generation both match, return directly"; every registry change does `++generation`, and a cached pointer is dereferenced only when the generations are equal—that is why "another thread destroyed the context" leaves no dangling cache. The fast path costs three TLS reads, one atomic read and two comparisons; with a null probe (monolith) it is one function pointer comparison.
|
||||
|
||||
**前端/会话接入**:`EGLImpl::MakeCurrent` 的 `EmitContextBinding` 一字未动 —— S1 的边沿宣告本来就是「这条 ring 往后归谁」的全部信息,S5 只是第一次**读**它。
|
||||
**Frontend/session integration**: `EGLImpl::MakeCurrent`'s `EmitContextBinding` is untouched—S1's edge announcement was always all the information "this ring belongs to whom from now on", and S5 is simply the first time **reading** it.
|
||||
|
||||
**验收证据**(本次新增):
|
||||
**Acceptance evidence** (added this commit):
|
||||
|
||||
- `MG_Test/Wire/ApplierOwnershipTest.cpp`,5 个用例,直接驱动注册表与解析(无 EGL、无 wire):两个 nonshared context 同名 handle 是两条记录且互不覆盖(reset 也不丢);同一 share group 的两个 context **共用对象表而各持 working state**(当前 program、vertex-buffer 窗口不互见,而一张表上的 create 两边可见);两个 session 用同名 group/context token 不共享任何东西;组的记录活到最后一个 context 才释放、重建的 token 不继承死组;无 session 的线程仍拿到进程级 applier。
|
||||
**负控**(必做,否则是空绿):把测试装的解析探针改成 null(即回到 S5 前的「谁都拿进程级 applier」),5 个用例**全红**;换回即全绿。
|
||||
- `MultiSessionTest.TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk`:真实 `--serve` 前身(in-process unix supervisor)上两个 session,各自两个 context(同名 token 与同名 group token),**8 轮交错**:每轮先各发一条 `bind_context` 切 context,再跑一整趟 clear + 真读回。绿/红两种颜色互为证人;日志里 0 条 `SessionLatch{`。这条用例同时压的是**生产**解析链(`ThreadSessionScope` 装的探针)与并发(两个 apply 线程同时跑)。
|
||||
- 既有集合零回归:本切片点名的 97 个用例与 `MultiSessionFixture` 原有的 4 个全绿(含 `ServerLoopEglTest` 的 11 个、`ContextStateOwnershipTest` 6 个)。`SanityTest` 的两个用例因 `MGPipeApplierState` 不再可拷贝改为保存/恢复它真正改动的 working 字段。
|
||||
- **全量 host ctest 与基线的逐名对比**(不是"看起来差不多"):先 `git stash` 掉本切片的全部改动、重建、跑一遍全量(`ctest -j 8`,2735 个用例)留下失败名单,再恢复本切片、重建、跑同一遍,两份名单 `comm`:
|
||||
- `MG_Test/Wire/ApplierOwnershipTest.cpp`, 5 cases, driving the registry and resolution directly (no EGL, no wire): same-named handles in two nonshared contexts are two records that do not overwrite each other (nor does reset lose them); two contexts of one share group **share the object table while each holds working state** (current program and vertex-buffer windows are not visible to each other, while a create on that one table is visible to both); two sessions with the same group/context token names share nothing; a group's records live until the last context leaves, and a re-minted token does not inherit a dead group; a thread without a session still gets the process-level applier.
|
||||
**Negative control** (mandatory, otherwise it is an empty green): change the resolution probe the test installs to null (i.e. back to the pre-S5 "everyone gets the process-level applier"), and all 5 cases **go red**; switch it back and all are green.
|
||||
- `MultiSessionTest.TwoSessionsInterleaveTwoContextsEachWithoutCrossTalk`: on the real `--serve` predecessor (the in-process unix supervisor), two sessions with two contexts each (same token names and same group token names), **8 interleaved rounds**: each round first sends one `bind_context` per session to switch context, then runs a full clear + real readback. The green and red colors witness for each other; 0 `SessionLatch{` lines in the log. This case stresses both the **production** resolution chain (the probe installed by `ThreadSessionScope`) and concurrency (two apply threads running at once).
|
||||
- The existing set does not regress: the 97 cases this slice names and the original 4 cases of `MultiSessionFixture` are all green (including `ServerLoopEglTest`'s 11 and `ContextStateOwnershipTest`'s 6). Two `SanityTest` cases were changed to save/restore the working fields they really modify, because `MGPipeApplierState` is no longer copyable.
|
||||
- **Name-by-name comparison of the full host ctest against the baseline** (not "looks about the same"): first `git stash` away all of this slice's changes, rebuild, run the full suite (`ctest -j 8`, 2735 cases) and keep the failure list; then restore this slice, rebuild, run the same pass, and `comm` the two lists:
|
||||
|
||||
| | 数量 |
|
||||
| | Count |
|
||||
|---|---|
|
||||
| 基线失败 | 266 |
|
||||
| 本切片失败 | 265 |
|
||||
| **本切片新增失败** | **0** |
|
||||
| 基线失败而本切片不失败 | 1(`ProgramTest.CompileVertex`,glslang 那批的已知抖动) |
|
||||
| Baseline failures | 266 |
|
||||
| This slice's failures | 265 |
|
||||
| **New failures in this slice** | **0** |
|
||||
| Baseline failures that this slice does not fail | 1 (`ProgramTest.CompileVertex`, a known flake in the glslang batch) |
|
||||
|
||||
失败名单全部落在 glslang / 着色器编译族(`ProgramTest`、`ProgramInterfaceTest`、`AsyncLinkTest`、`GlslangCaptureProbeTest`、`TranslationCacheTest`…),与 `3rdparty/glslang` 这个 dirty submodule 的既有基线一致。
|
||||
The failure lists all fall in the glslang / shader compilation family (`ProgramTest`, `ProgramInterfaceTest`, `AsyncLinkTest`, `GlslangCaptureProbeTest`, `TranslationCacheTest`…), consistent with the existing baseline of the dirty `3rdparty/glslang` submodule.
|
||||
|
||||
**S5 明确没做(下一层的账)**:
|
||||
**What S5 explicitly did not do (the next layer's debt)**:
|
||||
|
||||
- **六个 twin registry 与 twin 键仍未改**(`MG_Backend/DirectGLES/Managers.cpp` 的六个 `g_backend*` 静态注册表;键在 `Managers.h:315` 的 `StateBackendObjectRegistry` → `SlotTables.h` 的 `BackendSlotTable<MGPipeHandle{Slot,Gen}>`)。**这是 S5 之后"两个 nonshared context 的同名 handle"剩下的那一半**:applier 里两条记录是对的(有测试),但 twin 层按 `{slot, gen}` 键,两个 context 的同一个 handle 值会解析到同一个 twin;反向也一样,跨 context 复用 twin 的路径没有按 share group 分组。S4 报告里那条 `glIsBuffer` 探针本机仍无可用控制,所以**驱动级的 shared / nonshared 证据仍然是缺的**,本次没有以"跑了个空的绿"收进来(这是本切片最该被下一刀补上的证据)。改形状是把 `BackendSlotTable` 加一个 group 维度或按组实例化那六个注册表,触及 `Managers.cpp` 里全部调用点,不是本轮时间能稳妥做完的一刀。**(S6 已做,见下节;驱动级探针仍未弄清,S6 走的是 twin 身份这条替代证据。)**
|
||||
- **pbuffer→pbuffer→pbuffer 的资源保留**只到 S4 已落地的程度(`ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext`:换 surface 不动 tuple 的 `EGLContext` 句柄)。真正的"三个 pbuffer 之间来回且资源都在"要 window 半边,本机 headless 验不了;按 S4 的口径只记录。
|
||||
- **wire 第 1 条的逐记录 `ContextSlot`** 仍未做,仍是 S1 的边沿宣告 + `bind_context`。所以"同一 ring 上两个 context 的记录交错"只在 client 按 make-current 顺序发射时成立(本轮的交错压力用例走的正是这个顺序)。
|
||||
- **`MGPipeApplierShareGroupCountForTesting`** 是测试观测面,不是生产 API。
|
||||
- **`MGPipeApplierState` 的九张表名与 `Objects.X` 两种拼法并存**是刻意换取 ~200 个读点不动的代价;改名的机会在 twin registry 一起做的那一刀。
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration`、ServerLoop 的 MakeCurrent 去重**同 S4 一节所列,仍未动。
|
||||
- **The six twin registries and the twin key are still unchanged** (`MG_Backend/DirectGLES/Managers.cpp`'s six `g_backend*` static registries; the key is in `Managers.h:315`'s `StateBackendObjectRegistry` → `SlotTables.h`'s `BackendSlotTable<MGPipeHandle{Slot,Gen}>`). **This is the remaining half of "same-named handles in two nonshared contexts" after S5**: the two records in the applier are right (tested), but the twin layer is keyed by `{slot, gen}`, so the same handle value from two contexts resolves to the same twin; the reverse also holds—paths that reuse a twin across contexts are not grouped by share group. The `glIsBuffer` probe from the S4 report still has no usable control on this machine, so **driver-level shared / nonshared evidence is still missing**, and it was not taken in this time as "an empty run that was green" (this is the evidence this slice most owes the next cut). Changing the shape means adding a group dimension to `BackendSlotTable` or instantiating the six registries per group, touching every call site in `Managers.cpp`; that is not a cut that can be completed safely in this round's time. **(S6 did it, see the next section; the driver-level probe is still unsolved, and S6 took the twin identity route as substitute evidence.)**
|
||||
- **Resource retention across pbuffer→pbuffer→pbuffer** is only at the level S4 landed (`ServerLoopEglTest.CreatingAnotherSurfaceKeepsTheSameNativeContext`: switching surfaces does not change the tuple's `EGLContext` handle). The real "back and forth among three pbuffers with all resources intact" needs the window half and cannot be verified headless on this machine; recorded only, per S4's wording.
|
||||
- **Wire item 1's per-record `ContextSlot`** is still not done, still S1's edge announcement + `bind_context`. So "records of two contexts interleaved on one ring" holds only when the client emits in make-current order (the interleaving stress case this round goes through exactly that order).
|
||||
- **`MGPipeApplierShareGroupCountForTesting`** is a test observation surface, not a production API.
|
||||
- **`MGPipeApplierState`'s two spellings—the nine table names and `Objects.X`—coexisting** is the deliberate price for leaving ~200 read sites unchanged; the chance to rename comes in the same cut as the twin registry.
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration` and ServerLoop's MakeCurrent deduplication** are as listed in the S4 section, still untouched.
|
||||
|
||||
### S6 已落地形状(twin 表按 {session, shareGroup} 键控)
|
||||
### S6 landed shape (twin tables keyed by {session, shareGroup})
|
||||
|
||||
S5 留下的最后一块:八个后端 twin 表(六个 `StateBackendObjectRegistry`,加 `BackendBufferResourceTable` 与 `BackendSamplerViewTable`)从 `{slot, gen}` 的进程级表变成**按 `{session, shareGroup}` 分桶**的表。这是「两个 nonshared context 的同名 handle」剩下的那一半——applier 的记录已经分开了(S5),但记录指向的 twin 还是一条。
|
||||
The last piece S5 left: the eight backend twin tables (the six `StateBackendObjectRegistry`, plus `BackendBufferResourceTable` and `BackendSamplerViewTable`) go from process-level `{slot, gen}` tables to tables **bucketed by `{session, shareGroup}`**. This is the remaining half of "same-named handles in two nonshared contexts"—the applier's records are already separated (S5), but the twin those records point at was still one.
|
||||
|
||||
**键结构(`MG_Backend/DirectGLES/DirectGLES.h`)**。`TwinKey{ SessionKey, ShareGroupKey }`,与 applier 的 `MGPipeApplierKey` 是**同一个二元组的后一半**:`SessionKey` 是 `ServerSession*` 的地址(S5 注册时用的也是同一个值,两侧按构造一致),`ShareGroupKey` 走 `MGPipeApplierShareGroupKeyFor(session, currentToken)`——**共享一个函数而不是各写一遍规则**,因为记录与它描述的 twin 必须永远落在同一个桶里,而「0 表示这个 context 自己起了一个组」的第二处拼写正是它们会分开的原因。`{0,0}` 保留给「不属于任何 session 的线程」,也就是被替换掉的进程单例。
|
||||
**Key structure** (`MG_Backend/DirectGLES/DirectGLES.h`). `TwinKey{ SessionKey, ShareGroupKey }`, **the second half of the same pair** as the applier's `MGPipeApplierKey`: `SessionKey` is the address of the `ServerSession*` (the same value S5 registers with, so the two sides agree by construction), and `ShareGroupKey` goes through `MGPipeApplierShareGroupKeyFor(session, currentToken)`—**sharing one function instead of writing the rule twice**, because a record and the twin it describes must always land in the same bucket, and a second spelling of "0 means this context started its own group" is exactly how they would come apart. `{0,0}` is reserved for "a thread that belongs to no session", i.e. the process singleton being replaced.
|
||||
|
||||
**解析链(`MG_Remote/Server/SessionRuntime.cpp`)**。第三层同构探针:`ResolveThreadTwinKey` 读 `ThreadSessionScope` 装好的 `t_runtime` → `runtime->Session().CurrentContextToken()` → `MGPipeApplierShareGroupKeyFor`;由 `InstallSessionRuntimeHooks` 用 `SetTwinKeyResolver` 装一次(与 S2 的 PipeInputs 探针、S4 的 native tuple 键、S5 的 applier 键并列)。没有探针的线程(monolith、纯客户端、inproc 单进程形状、单元用例)答案恒为 `{0,0}`。**探针只读 server session 侧的状态,不碰 `MG_State`**:`CurrentContextToken()` 是 apply 线程自己维护的原子,`MGPipeApplierShareGroupKeyFor` 读的是 applier 的注册表,所以 apply 线程那条「禁止前端键控查找」的约束(`Managers.h:341/406/430-436`)原样成立。
|
||||
**Resolution chain** (`MG_Remote/Server/SessionRuntime.cpp`). The third isomorphic probe: `ResolveThreadTwinKey` reads the `t_runtime` installed by `ThreadSessionScope` → `runtime->Session().CurrentContextToken()` → `MGPipeApplierShareGroupKeyFor`; installed once by `InstallSessionRuntimeHooks` with `SetTwinKeyResolver` (parallel to S2's PipeInputs probe, S4's native tuple key and S5's applier key). On a thread without a probe (monolith, pure client, inproc single-process shape, unit case) the answer is always `{0,0}`. **The probe reads only server-session-side state and does not touch `MG_State`**: `CurrentContextToken()` is an atomic the apply thread maintains itself, and `MGPipeApplierShareGroupKeyFor` reads the applier's registry, so the apply thread's constraint "no frontend keyed lookup" (`Managers.h:341/406/430-436`) stands as written.
|
||||
|
||||
**表形状(`SlotTables.h` 的 `SlotTableRegistry`)**。桶是 `SharedPtr<BackendSlotTable<…, kKind>>`,按 key 存在一张 unordered_map 里,带一个「上次的 key → 桶」的线程本地 memo(热路径是一次比较)。**~150 个调用点一行没动**:`SlotTableRegistry` 把表上每一个 handle-keyed 入口(`GetOrCreate` / `FindByHandle` / `LiveGenAt` / `HandleOf` / `StateForHandle` / `NoteStateForHandle` / `ReleaseByHandle` / `ForEachLive` / `LiveCount` / 两个 capacity 观测)按**调用线程的桶**转发一遍,所以 `g_backendTextureObjects.FindByHandle(h)` 还是那行字,答的却是本 context 的名字空间;`StateBackendObjectRegistry` 内同样把 `m_slotTable` 换成 `m_slotTables` 并转发。两处刻意的取舍:
|
||||
**Table shape** (`SlotTables.h`'s `SlotTableRegistry`). A bucket is a `SharedPtr<BackendSlotTable<…, kKind>>` stored in an unordered_map by key, with a thread-local memo "last key → bucket" (the hot path is one comparison). **~150 call sites are unchanged**: `SlotTableRegistry` forwards every handle-keyed entry on the table (`GetOrCreate` / `FindByHandle` / `LiveGenAt` / `HandleOf` / `StateForHandle` / `NoteStateForHandle` / `ReleaseByHandle` / `ForEachLive` / `LiveCount` / the two capacity observations) to **the calling thread's bucket**, so `g_backendTextureObjects.FindByHandle(h)` is still that line of text but answers with this context's name space; inside `StateBackendObjectRegistry` `m_slotTable` likewise becomes `m_slotTables` and is forwarded. Two deliberate trade-offs:
|
||||
|
||||
- **holder 列表仍是进程级**。notice 是关于**对象**的,不是关于桶的:`OnFrontendObjectDestroyed` / `ReleaseTwinByHandle` 必须走到 fixture 或 context reset 持有的 by-value 拷贝,无论它在哪个桶里——「哪个桶持有它」正是当初造成双 holder 泄漏的那个问题。每个共享 holder 就是一个桶,所以死亡通知天然覆盖全部桶。
|
||||
- **`ForEachLive` 走调用线程的桶**,不是全部桶。调用者问的是「我能看见的对象里哪些需要这个」,而它能看见的就是它自己的 share group;遍历全部桶会走到邻居 session 的对象。需要全进程视野的两处(死亡、诊断)走新的 `ForEachBucket(fn)`(回调带 key)。
|
||||
- **The holder list stays process-level**. A notice is about the **object**, not the bucket: `OnFrontendObjectDestroyed` / `ReleaseTwinByHandle` must reach the by-value copy held by the fixture or the context reset, whichever bucket it is in—"which bucket holds it" is precisely the problem that caused the double-holder leak. Every shared holder is one bucket, so death notices naturally cover all buckets.
|
||||
- **`ForEachLive` walks the calling thread's bucket**, not all buckets. The caller asks "which of the objects I can see need this", and what it can see is its own share group; walking all buckets would reach a neighbor session's objects. The two places that need a whole-process view (death, diagnostics) use the new `ForEachBucket(fn)` (the callback carries the key).
|
||||
|
||||
**新增的控制面**:`Drop(key)`(一个组)、`DropSession(sessionKey)`(一个 session 的全部组)、`ResetBuckets()`(全部,即 `= {}` 在旧形状下的意思)、`BucketCount()` / `BucketSnapshot()` / `BucketForCallingThread()`(测试与 fixture 的观测面)。`BackendSlotTable::Reset()` 是「把两个空间先移出去再析构」的写法,理由与 `ReleaseTwinAt` 相同:twin 的析构是驱动调用,可能重入 `GetOrCreate`。
|
||||
**New control surfaces**: `Drop(key)` (one group), `DropSession(sessionKey)` (all groups of a session), `ResetBuckets()` (all, i.e. what `= {}` meant in the old shape), `BucketCount()` / `BucketSnapshot()` / `BucketForCallingThread()` (observation surfaces for tests and fixtures). `BackendSlotTable::Reset()` is written as "move both spaces out first, then destruct", for the same reason as `ReleaseTwinAt`: a twin's destructor is a driver call and may reenter `GetOrCreate`.
|
||||
|
||||
**`DropEveryTwinForEndedServerSession` 变成 per-session**(`Managers.cpp`)。它跑在**正在结束的那个 session 的 apply 线程**上,而 S2 之后同一个进程里可能有别的 session 还在渲染,所以现在是「按调用线程的 session 删它全部桶」;没有 session 的线程(单元用例、`SanityTest` 的 `Transport=Spawn` 形状、fork worker)执行全量清理,与旧形状逐字相同。`g_boundTexturesCache` / `g_boundSamplersCache` / `g_activeTextureUnit` 这三个单元影子仍是进程级、仍在这里整体清空(见下面的欠债)。
|
||||
**`DropEveryTwinForEndedServerSession` becomes per-session** (`Managers.cpp`). It runs on **the apply thread of the session that is ending**, and after S2 other sessions in the same process may still be rendering, so it is now "delete all buckets of the calling thread's session"; a thread without a session (unit cases, `SanityTest`'s `Transport=Spawn` shape, fork workers) performs the full cleanup, character for character as the old shape. The three unit shadows `g_boundTexturesCache` / `g_boundSamplersCache` / `g_activeTextureUnit` are still process-level and are still cleared wholesale here (see the debt below).
|
||||
|
||||
**`Managers.h:2190` 的注释一并改正**:那里过去写着「a texture is owned by exactly one context (share groups are not implemented)」。真正成立的说法是**texture 属于一个 share group**(S3 的 `ShareGroupState` 持有对象表,S6 之后 twin 表也按组键控),同组两个 context 都会合法地改它;`m_syncedShapeContextId` 因此只是廉价门禁的一半——同组的另一个 context 改了 shape 会 stamp 一个新 id 并(多余地、但安全地)重推一次,而真正兜住变化的是那几个 per-mutation version,它们与 context 无关。
|
||||
**The comment at `Managers.h:2190` is corrected as well**: it used to say "a texture is owned by exactly one context (share groups are not implemented)". What really holds is that **a texture belongs to a share group** (S3's `ShareGroupState` holds the object table, and after S6 the twin table is keyed by group as well), and both contexts of a group may legally modify it; `m_syncedShapeContextId` is therefore only half of a cheap gate—another context in the same group changing shape stamps a new id and (redundantly, but safely) re-pushes once, while what really catches the change is the per-mutation versions, which do not depend on the context.
|
||||
|
||||
**验收证据**(本次新增):
|
||||
**Acceptance evidence** (added this commit):
|
||||
|
||||
- `MG_Test/Wire/TwinOwnershipTest.cpp`,5 个用例,直接驱动**真实** twin 表(无 EGL、无 wire):同 session 两个不共享的组、同名 handle 是两个 twin 且互不覆盖;同组两个 context 解析**同一个** twin(驱动级共享在 twin 层的等价物——twin 就是持有 driver id 的那个对象);一个 session 的 `DropBucketsOfSession` 只带走自己的 twin,邻居 session 的不受影响;不属于任何 session 的线程仍拿进程级那张表且与 session 的互不串;第六个用例在 `TwinRegistry` 模板(六个注册表同型)上重复前三条并顺带钉住 per-bucket 的世代门禁。
|
||||
**负控**(必做):把 `SlotTableRegistry::ForCallingThread` 改成恒答 `{0,0}`(即恢复 S6 前的进程级表),5 个用例里**4 个变红**;`TwoContextsOfOneGroupShareTheGroupsTwins` 保持绿——一个键时两种形状答案相同,这正是该用例断言的是「共享必须发生」而不是「存在两个桶」的原因。改回即全绿。
|
||||
- 既有集合零回归:本切片点名的 103 个用例(`EmbeddedServerTest`、`ServerDisplayTest`、`UnixSupervisor`、`InProcessServer`、`SupervisorChildren`、`ServerLoopTest`、`ServerLoopEglTest`、`MultiSessionFixture`、`PeerLatch`、`SurfaceControlFrameTest`、`ContextStateOwnershipTest`、`ApplierOwnershipTest`)全绿,其中 `MultiSessionFixture` 的两个真渲染用例现在跑在按 session 隔离的 twin 表上。
|
||||
- **全量 host ctest 与基线的逐名对比**(同 S5 的做法:`git stash push -u -- MobileGL docs` 掉本切片、重建、跑全量留失败名单,恢复后再跑一遍,两份名单按**用例名**`comm` 而不是按行号——本切片多了一个测试文件,行号会整体错位):
|
||||
- `MG_Test/Wire/TwinOwnershipTest.cpp`, 5 cases, driving the **real** twin tables directly (no EGL, no wire): two nonshared groups in one session—same-named handles are two twins that do not overwrite each other; two contexts of one group resolve **the same** twin (the twin-layer equivalent of driver-level sharing—a twin is the object that holds the driver id); one session's `DropBucketsOfSession` takes only its own twins and leaves the neighbor session's untouched; a thread that belongs to no session still gets the process-level table and does not cross with the sessions'. A sixth case repeats the first three on the `TwinRegistry` template (the six registries are the same shape) and also pins the per-bucket generation gate.
|
||||
**Negative control** (mandatory): change `SlotTableRegistry::ForCallingThread` to always answer `{0,0}` (i.e. restore the pre-S6 process-level table), and 4 of the 5 cases **go red**; `TwoContextsOfOneGroupShareTheGroupsTwins` stays green—with one key the two shapes answer the same, which is why that case asserts "sharing must happen" rather than "two buckets exist". Switch back and all are green.
|
||||
- The existing set does not regress: the 103 cases this slice names (`EmbeddedServerTest`, `ServerDisplayTest`, `UnixSupervisor`, `InProcessServer`, `SupervisorChildren`, `ServerLoopTest`, `ServerLoopEglTest`, `MultiSessionFixture`, `PeerLatch`, `SurfaceControlFrameTest`, `ContextStateOwnershipTest`, `ApplierOwnershipTest`) are all green, and two real-rendering cases of `MultiSessionFixture` now run on session-isolated twin tables.
|
||||
- **Name-by-name comparison of the full host ctest against the baseline** (the same method as S5: `git stash push -u -- MobileGL docs` away this slice, rebuild, run the full suite and keep the failure list; restore and run again; `comm` the two lists by **case name** rather than by line number—this slice added a test file, so line numbers would shift throughout):
|
||||
|
||||
| | 数量 |
|
||||
| | Count |
|
||||
|---|---|
|
||||
| 基线失败 | 277 |
|
||||
| 本切片失败 | 278 |
|
||||
| **本切片新增失败** | **0** |
|
||||
| 基线失败而本切片不失败 | 0 |
|
||||
| Baseline failures | 277 |
|
||||
| This slice's failures | 278 |
|
||||
| **New failures in this slice** | **0** |
|
||||
| Baseline failures that this slice does not fail | 0 |
|
||||
|
||||
两份名单的差集只有一个名字:`LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime`,它**基线也抖**——同一台机器上单独重复跑,基线 4/60 失败、本切片 2/40 失败,两边的失败断言逐字相同(`LogForwardTest.cpp:446` 的 `Dropped` 行数与 `counters.lossyDropped` 不等、`:448` 的 `counters.notices == 2` 而非 1),是那个用例自己的日志通道竞态:它让对端停止读取然后断言「掉线通知只发一条」,而通知本身由后台线程按队列水位发。该用例不建 backend、不碰 twin,与本切片无关,按既有基线口径记为抖动。其余失败名单全部落在 glslang / 着色器编译族(`ProgramTest` 80、`ProgramInterfaceTest` 32、`AsyncLinkTest` 17、`GlslangCaptureProbeTest` 15、`ShaderCompileAdoptionTest` 14…),与 `3rdparty/glslang` 这个 dirty submodule 的既有基线一致。
|
||||
The difference between the two lists is exactly one name: `LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime`, which **is also flaky on the baseline**—run alone repeatedly on the same machine, the baseline failed 4/60 and this slice 2/40, with identical failing assertions on both sides (the `Dropped` line count vs `counters.lossyDropped` mismatch at `LogForwardTest.cpp:446`, and `counters.notices == 2` instead of 1 at `:448`), a race in that case's own log channel: it makes the peer stop reading and then asserts "only one disconnect notice is sent", while the notice itself is sent by a background thread according to the queue level. That case builds no backend and touches no twin; it is unrelated to this slice and is recorded as a flake under the existing baseline wording. The remaining failures all fall in the glslang / shader compilation family (`ProgramTest` 80, `ProgramInterfaceTest` 32, `AsyncLinkTest` 17, `GlslangCaptureProbeTest` 15, `ShaderCompileAdoptionTest` 14…), consistent with the existing baseline of the dirty `3rdparty/glslang` submodule.
|
||||
|
||||
**S6 明确没做(下一层的账)**:
|
||||
**What S6 explicitly did not do (the next layer's debt)**:
|
||||
|
||||
- **单元影子仍是进程级**:`g_activeTextureUnit`、`g_boundTexturesCache`、`g_boundSamplersCache` 与 sampler cache(`Managers.cpp:11031-11032` 等)是 §3 第 3 条留给 P14b/c 的另一半。它们与 twin 键控**不冲突但也不互补**:缓存做的是指针同一性比较,所以按组分开的 twin 只会让「另一组的 twin」比较不相等并重新绑定(安全方向);真正的残留问题是「同一个 twin 在一次原生 context 切换之后被缓存命中而跳过绑定」,那在 S4 把原生 context 变成 per-context 时就已经存在,属于同一笔账。
|
||||
- **驱动级的 `glIsBuffer` 探针仍未弄清**(S4/S5 都记录的同一件事)。S6 因此没有走「真驱动上问一句」那条路,而是按 S5 报告的允许口径在 twin 层断言身份:twin 就是持有 driver id 的对象,两个 context 拿到不同的 twin 就是两个 driver 对象。要做成驱动级证据,需要先把 S4 报告里「同 context 内 `glGenBuffers` 后 `glIsBuffer` 即返回 false」这个探针问题弄清楚。
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration` 仍是进程级**,同 S4/S5 一节所列。
|
||||
- **`MGPipeApplierState` 的两种拼法(`Objects.X` 与表名)仍未收敛**;S5 说「改名的机会在 twin registry 一起做的那一刀」,S6 选择了不动它(那一刀的价值是整洁,而 S6 的 diff 已经在改 8 张表的行为,把重命名捆进来只增风险)。
|
||||
- **wire 第 1 条的逐记录 `ContextSlot`** 仍未做。
|
||||
- **The unit shadows are still process-level**: `g_activeTextureUnit`, `g_boundTexturesCache`, `g_boundSamplersCache` and the sampler cache (`Managers.cpp:11031-11032` etc.) are the other half that §3 item 3 leaves to P14b/c. They **neither conflict with nor complement** the twin keying: the caches compare pointer identity, so twins separated by group only make "the other group's twin" compare unequal and rebind (the safe direction); the real residual problem is "the same twin is hit by the cache after a native context switch and the bind is skipped", which already existed when S4 made the native context per-context and belongs to the same debt.
|
||||
- **The driver-level `glIsBuffer` probe is still unsolved** (the same thing S4/S5 recorded). S6 therefore did not take the "ask the real driver" route, and instead asserted identity at the twin layer under the allowance of the S5 report: a twin is the object holding the driver id, so two contexts getting different twins means two driver objects. Making it driver-level evidence requires first solving the probe problem from the S4 report ("`glIsBuffer` returns false right after `glGenBuffers` in the same context").
|
||||
- **`g_backendContextGeneration`/`g_syncContextGeneration` are still process-level**, as listed in the S4/S5 sections.
|
||||
- **`MGPipeApplierState`'s two spellings (`Objects.X` and the table names) are still not converged**; S5 said "the chance to rename comes in the same cut as the twin registry", and S6 chose not to touch it (that cut's value is tidiness, while S6's diff already changes the behavior of 8 tables; bundling a rename in would only add risk).
|
||||
- **Wire item 1's per-record `ContextSlot`** is still not done.
|
||||
|
||||
|
||||
## 分期与验收
|
||||
## Phases and acceptance
|
||||
|
||||
| 期 | 内容 | 验收 |
|
||||
| Phase | Content | Acceptance |
|
||||
|---|---|---|
|
||||
| P14a | wire context 身份 + 前端 GLContext/ShareGroupState 拆分 + CreateContext/DestroyContext 上 wire | shared contexts 访问同一 buffer/texture;nonshared 同名资源隔离;每 context 的 viewport/FBO/bindings/error 独立;context 跨线程移交后状态保留 |
|
||||
| P14b | 后端 per-context native tuple + surface 生命周期 | pbuffer→window→pbuffer 资源不丢;Qt offscreen probe + 实窗的 context 序列不丢首窗内容 |
|
||||
| P14c | server 同进程多 session(registry、per-session applier/apply-thread/反向路由),移除 fork-worker 依赖 | 相同 handle 值的两个 session 隔离且可独立关闭;一个 session 崩不拖死另一个;in-process server 并发两 client 渲染互不串台 |
|
||||
| P14d | anland 切统一 embedded server | KWin(OnScreen session)+ Konsole(Offscreen session)同 server 同上屏;见交接文档验收链 |
|
||||
| P14a | wire context identity + frontend GLContext/ShareGroupState split + CreateContext/DestroyContext on the wire | shared contexts access the same buffer/texture; nonshared same-named resources are isolated; per-context viewport/FBO/bindings/error are independent; state survives a context handover across threads |
|
||||
| P14b | backend per-context native tuple + surface lifetime | pbuffer→window→pbuffer loses no resources; the Qt offscreen probe + real-window context sequence does not lose the first window's content |
|
||||
| P14c | server multiple sessions in one process (registry, per-session applier/apply-thread/reverse routing), removing the fork-worker dependency | two sessions with the same handle values are isolated and can be closed independently; one session crashing does not drag the other down; the in-process server renders two concurrent clients without cross talk |
|
||||
| P14d | anland switches to the unified embedded server | KWin (OnScreen session) + Konsole (Offscreen session) on the same server, both presented on screen; see the acceptance chain in the handover document |
|
||||
|
||||
每期都过既有门:host ctest 全量、`split_coverage.py` 三臂、monolith G1 符号不变;wire 变更期两端同 stamp 重构建。monolith 路径(`MOBILEGL_PIPE_PUSH=0`)下多 context 语义同样必须成立——拆分前后端共用一个 `MG_State`。
|
||||
Every phase passes the existing gates: the full host ctest, `split_coverage.py`'s three arms, the unchanged monolith G1 symbols; in a phase with wire changes both ends rebuild with the same stamp. Multi-context semantics must also hold on the monolith path (`MOBILEGL_PIPE_PUSH=0`)—before and after the split both ends share one `MG_State`.
|
||||
|
||||
## 明确不做
|
||||
## Explicitly not done
|
||||
|
||||
- 跨 session 共享 GL 对象(EGL 语义本就不允许跨连接共享;需要时另立 import 设计)。
|
||||
- per-context ring(见 wire 1 的取舍)。
|
||||
- 把 FBO/VAO 等容器对象放进 share group。
|
||||
- 保留 `--serve --max-sessions` 的 fork 分发作为任何验收路径;工具本身可留作调试,但 anland 与多 session 验收不得依赖它。
|
||||
- Sharing GL objects across sessions (EGL semantics do not allow sharing across connections anyway; if needed, a separate import design).
|
||||
- per-context ring (see the trade-off in wire 1).
|
||||
- Putting container objects such as FBO/VAO into the share group.
|
||||
- Keeping `--serve --max-sessions`'s fork dispatch as any acceptance path; the tool itself may stay for debugging, but anland and multi-session acceptance must not depend on it.
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
# 构建、运行与测试
|
||||
# Build, run and test
|
||||
|
||||
> 上手说明。文档导航见 [`../README.md`](../README.md);全部开关见 [`../design/09-build-switches-counters.md`](../design/09-build-switches-counters.md) 附 A。
|
||||
> Getting started. For the documentation index see [`../README.md`](../README.md); for every switch see [`../design/09-build-switches-counters.md`](../design/09-build-switches-counters.md) appendix A.
|
||||
|
||||
## 三种构建
|
||||
## Three builds
|
||||
|
||||
P13 起只有一种单进程形态:后端只从前端推来的记录取状态(记录臂),旧的「后端直接读前端」路径与 pull / push 两套构建一起删掉了(`MOBILEGL_PIPE_PUSH` 不再是 CMake 选项)。
|
||||
Since P13 there is only one single-process shape: the backend takes its state solely from the records the frontend pushes (the record arm); the old "backend reads the frontend directly" path went away together with the pull / push builds (`MOBILEGL_PIPE_PUSH` is no longer a CMake option).
|
||||
|
||||
| 构建目录 | CMake | 用途 |
|
||||
| Build directory | CMake | Purpose |
|
||||
|---|---|---|
|
||||
| `build-linux` | 默认 | 出货形态(FCL 内嵌库就是它):单进程,记录臂,不编 `MG_Remote` |
|
||||
| `build-verify` | `-DMOBILEGL_PIPE_VERIFY=ON` | 每个 draw 把推来的状态与前端实际状态逐字段比对;慢,永不出货 |
|
||||
| `build-split` | `-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON` | 可以拆成两线程 / 两进程 / 两台机器;不设 `MOBILEGL_TRANSPORT` 时是同一记录臂的单进程对照 |
|
||||
| `build-linux` | default | The shipped shape (the FCL embedded library is this one): single process, record arm, `MG_Remote` not built |
|
||||
| `build-verify` | `-DMOBILEGL_PIPE_VERIFY=ON` | Every draw compares the pushed state field by field with the frontend's actual state; slow, never shipped |
|
||||
| `build-split` | `-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON` | Can split into two threads / two processes / two machines; with `MOBILEGL_TRANSPORT` unset it is the single-process control on the same record arm |
|
||||
|
||||
库由七个 OBJECT 模块拼成(`mg_util` / `mg_pipe` / `mg_backend` / `mg_frontend` / `mg_remote_transport` / `mg_remote_client` / `mg_remote_server`,按 `scripts/link_ratchet.py` 的 PARTITION 划分);split 构建另有 `MobileGL_server_linkcheck`,证明 server 那一半不带前端模块也能链接(剩下的前端引用是 link ratchet 基线里具名的债务)。
|
||||
The library is assembled from seven OBJECT modules (`mg_util` / `mg_pipe` / `mg_backend` / `mg_frontend` / `mg_remote_transport` / `mg_remote_client` / `mg_remote_server`, partitioned by `scripts/link_ratchet.py`); the split build also has `MobileGL_server_linkcheck`, which proves the server half links without the frontend modules (the remaining frontend references are named debts in the link-ratchet baseline).
|
||||
|
||||
## 选拓扑
|
||||
## Choosing a topology
|
||||
|
||||
split 构建在运行时用 `MOBILEGL_TRANSPORT` 选拓扑;**不设就是单进程(monolith)对照**:
|
||||
The split build selects the topology at run time with `MOBILEGL_TRANSPORT`; **unset means the single-process (monolith) control**:
|
||||
|
||||
| `MOBILEGL_TRANSPORT` | 形态 |
|
||||
| `MOBILEGL_TRANSPORT` | Shape |
|
||||
|---|---|
|
||||
| 不设 / `monolith` | 单线程直调 |
|
||||
| `inproc` | 同一进程两个线程(CI 的主验收形态) |
|
||||
| `spawn` | 两个进程;再用 `MOBILEGL_IPC_CONTROL`(`fork` / `unix:<path>` / `tcp://host:port`)与 `MOBILEGL_IPC_DATA`(`auto` / `shm` / `stream`)选控制面与数据面,跨机时还要 `MOBILEGL_IPC_TOKEN`(≥16 字节) |
|
||||
| unset / `monolith` | Single thread, direct calls |
|
||||
| `inproc` | Two threads in one process (the main CI acceptance shape) |
|
||||
| `spawn` | Two processes; `MOBILEGL_IPC_CONTROL` (`fork` / `unix:<path>` / `tcp://host:port`) and `MOBILEGL_IPC_DATA` (`auto` / `shm` / `stream`) then choose the control and data planes; across machines also set `MOBILEGL_IPC_TOKEN` (at least 16 bytes) |
|
||||
|
||||
```text
|
||||
MOBILEGL_TRANSPORT=inproc MOBILEGL_ITEST_REQUIRE_GPU=1 ctest --test-dir build-split -L integration-split --output-on-failure
|
||||
ctest --test-dir build-split -L 'integration-(spawn|tcp)' --output-on-failure # 条目自带 spawn / tcp 环境与私有日志
|
||||
ctest --test-dir build-split -L 'integration-(spawn|tcp)' --output-on-failure # the entries carry their own spawn / tcp environment and private logs
|
||||
```
|
||||
|
||||
其他常用旋钮:`MOBILEGL_IPC_RUN_AHEAD`(默认 1,client 发完就走)、`MOBILEGL_IPC_STAGE_MB`(默认 32,其 1/4 是大块上传的分块预算)、`MOBILEGL_IPC_SURFACE=server`(使用 Android server 自己的窗口)、`MOBILEGL_PIPE_STATS=1`(边界计数器,日志里的 `MGPipe stats:` 行)。
|
||||
Other common knobs: `MOBILEGL_IPC_RUN_AHEAD` (default 1, the client sends and moves on), `MOBILEGL_IPC_STAGE_MB` (default 32; a quarter of it is the chunk budget for large uploads), `MOBILEGL_IPC_SURFACE=server` (use the Android server's own window), `MOBILEGL_PIPE_STATS=1` (boundary counters, the `MGPipe stats:` lines in the log).
|
||||
|
||||
## Android
|
||||
|
||||
- 两份 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` 备用路线)见 git 历史中的"两边怎么配"。
|
||||
- 性能对比只用 Redmi `2f7cbe2e`,按 [`pin-verification-2026-09-07.md`](pin-verification-2026-09-07.md) 定频,reboot-clean、同热窗口、配对 A/B。P13 之后的性能工作以 `dev` 分支构建为「之前」基线(用户 2026-10-07)。
|
||||
- Two APK flavors: single-process / split (Gradle properties `mobilegl.buildDisaggregated`, `mobilegl.buildDisaggregatedInproc`; a standalone plugin build defaults to split, the FCL embedding defaults to single-process). The split APK also needs `MOBILEGL_TRANSPORT`; unset means the single-process control.
|
||||
- The on-device server has two shapes: an off-screen foreground Service (`MobileGLServerService`) and the on-screen `MobileGLDisplayActivity`; only one at a time. Both the plugin and trace flavor APKs carry them (standalone builds enable `mobilegl.buildDisaggregated` by default); the entry point is the "Render Server" page in the APK's bottom navigation. In the plugin flavor these three components are not exported; only the trace build allows adb to start them directly by component name.
|
||||
- For the cross-machine setup with the computer as client and the phone as server (starting the server, setting the token, the `adb forward` fallback), see "configuring the two sides" in the phase notes (in git history).
|
||||
- Performance comparisons use only the Redmi `2f7cbe2e`, pinned as described in the pin-verification record (in git history): reboot-clean, same thermal window, paired A/B. Performance work after P13 uses the `dev` build as its "before" baseline (user, 2026-10-07).
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# 代码地图
|
||||
# Code map
|
||||
|
||||
> 参考。文档导航见 [`../README.md`](../README.md);目录布局与构建选项见 [`../design/09-build-switches-counters.md`](../design/09-build-switches-counters.md) §16。
|
||||
> Reference. For the documentation index see [`../README.md`](../README.md); for the directory layout and build options see [`../design/09-build-switches-counters.md`](../design/09-build-switches-counters.md) §16.
|
||||
|
||||
| 路径 | 作用 |
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `MobileGL/MG_Pipe/` | 接口本身:调用目录 `PipeCalls.def` 与其他 `.def` 表、payload / 句柄 / 回调头、`PipeApply`、`PipeRoute`、`generated/*.inc`(生成物,提交进树) |
|
||||
| `MobileGL/MG_Impl/Pipe/` | 前端:Tracker、`PipeFill`、`SlotAllocator`、`CsoCache`、`CompositeResolver`、`ResourceTracker`、各族 `*Emit.h` |
|
||||
| `MobileGL/MG_Backend/MGPipe/` | `PipeInputs`:后端读的、被前端推来的状态块 |
|
||||
| `MobileGL/MG_Remote/` | 拆分专用(仅 `MOBILEGL_BUILD_DISAGGREGATED`):`Protocol/`(控制面 schema)、`Transport/`(控制面 `ITransport` × 数据面 `ILink`)、`Wire/`(记录编解码)、`Client/`、`Server/`;各阶段的 wire 契约 `CONTRACT-*.md` |
|
||||
| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | 边界计数器(`MOBILEGL_PIPE_STATS=1`) |
|
||||
| `MobileGL/MG_Test/Pipe/`、`MG_Test/Wire/`、`MG_IntegrationTest/Harness/*Peek` | 目录 / tracker / 发射器单元测试、wire 与 remote 单元测试、白盒断言 |
|
||||
| `scripts/` | 生成器(`gen_pipe.py`、`gen_pipe_field_ownership.py`、`gen_pipe_dirty_surface.py`、`gen_protocol.py`)、门脚本(`symbol_report.py`、`check_include_closure.py`、`link_ratchet.py`、G5 保护区脚本、`check_doc_citations.py`)、`ci/` |
|
||||
| `tools/trace_replay/`、`tools/device_bench/` | trace 回放(含 TCP 矩阵跑器)、设备定频与配对 A/B 工具 |
|
||||
| `MobileGL/MG_Pipe/` | The interface itself: the call catalog `PipeCalls.def` and the other `.def` tables, the payload / handle / callback headers, `PipeApply`, `PipeRoute`, `generated/*.inc` (generated, committed to the tree) |
|
||||
| `MobileGL/MG_Impl/Pipe/` | Frontend: Tracker, `PipeFill`, `SlotAllocator`, `CsoCache`, `CompositeResolver`, `ResourceTracker`, the per-family `*Emit.h` |
|
||||
| `MobileGL/MG_Backend/MGPipe/` | `PipeInputs`: the state block the backend reads and the frontend pushes |
|
||||
| `MobileGL/MG_Remote/` | Split-only (`MOBILEGL_BUILD_DISAGGREGATED`): `Protocol/` (control-plane schema), `Transport/` (control plane `ITransport` × data plane `ILink`), `Wire/` (record encoding and decoding), `Client/`, `Server/`; the per-phase wire contracts `CONTRACT-*.md` |
|
||||
| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | Boundary counters (`MOBILEGL_PIPE_STATS=1`) |
|
||||
| `MobileGL/MG_Test/Pipe/`, `MG_Test/Wire/`, `MG_IntegrationTest/Harness/*Peek` | Catalog / tracker / emitter unit tests, wire and remote unit tests, white-box assertions |
|
||||
| `scripts/` | Generators (`gen_pipe.py`, `gen_pipe_field_ownership.py`, `gen_pipe_dirty_surface.py`, `gen_protocol.py`), gate scripts (`symbol_report.py`, `check_include_closure.py`, `link_ratchet.py`, the G5 protected-region scripts, `check_doc_citations.py`), `ci/` |
|
||||
| `tools/trace_replay/`, `tools/device_bench/` | Trace replay (including the TCP matrix runner), device clock pinning and paired A/B tooling |
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
# 工程纪律与验证门
|
||||
# Engineering discipline and verification gates
|
||||
|
||||
> 参考。验证门的设计见 [`../design/07-memory-readback-verification.md`](../design/07-memory-readback-verification.md) §13.2;各阶段实际跑门的结果在 `../notes/<阶段>/README.md`。
|
||||
> Reference. The design of the verification gates is in [`../design/07-memory-readback-verification.md`](../design/07-memory-readback-verification.md) §13.2; the per-phase gate results are in the phase notes (in git history).
|
||||
|
||||
## 每个提交
|
||||
## Every commit
|
||||
|
||||
- 默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门)。
|
||||
- **每个门必须能因它存在的理由变红**:带阴性对照并真跑过一次红(R-16);公共 GL 看不见的改用白盒断言。
|
||||
- 正确性门是硬门;**性能只记录、不设门**(用户 2026-09-08),但数字必须真的采到。专门的优化阶段排在路线图推完之后;P13 之后的性能轮以 `dev` 构建为基线(用户 2026-10-07)。
|
||||
- **拆分不顺手修 `dev` 的 bug**:发现的 `dev` 侧缺陷记成独立条目、独立 PR。
|
||||
- The default ALL target must build completely; instrumenting hot paths is not allowed in commits (a CI grep gate).
|
||||
- **Every gate must be able to turn red for the reason it exists**: it has a negative control and has really run red once (R-16); what public GL cannot see is asserted white-box instead.
|
||||
- Correctness gates are hard gates; **performance is recorded, not gated** (user, 2026-09-08), but the numbers must actually be measured. Dedicated optimization phases come after the roadmap; performance rounds after P13 use the `dev` build as the baseline (user, 2026-10-07).
|
||||
- **Do not fix `dev` bugs along the way**: a defect found on the `dev` side becomes its own entry and its own PR.
|
||||
|
||||
## 每个阶段出口
|
||||
## Every phase exit
|
||||
|
||||
- 跑一次五部分门:接口纯度、语义影子比对、行为 A/B、性能记录、覆盖与句柄纪律。
|
||||
- 字节 / 名字级等式:**G5** 受保护区域逐字节一致;**G14** 测试名只增不删。**G1**(pull 构建的符号集与 `.text` 对基线恒等)与 **G2**(pull 与 push 的测试名集合相同)随 pull 构建在 P13 退役;接班的是:出货库零 `MG_Remote` 符号检查、skip 普查(新出现的跳过与消失的名字都要在基线里具名)、recorder 金标(`MG_Test/Pipe/RecorderGoldenTest.cpp`:固定 GL 调用序列的记录流逐条比对,改金标要在提交里说为什么),以及 link ratchet(server 一半对前端的引用只减不增,不允许 `# P13` 注解)。
|
||||
- 性能判据是**逐线程 CPU 时间**;设备对比走 reboot-clean + 同热窗口配对 A/B,只用 Redmi `2f7cbe2e`([`pin-verification-2026-09-07.md`](pin-verification-2026-09-07.md));Windows 机器不是正确性门。
|
||||
- 收官由异模型族审查一次,发现进入下一阶段首轮;实现方与审查方错开模型族;不过度验证(用户 2026-09-16,ID-66)。
|
||||
- Run the five-part gate once: interface purity, semantic shadow comparison, behavioral A/B, performance record, coverage and handle discipline.
|
||||
- Byte and name level equalities: **G5** protected regions are byte-identical; **G14** test names only grow. **G1** (the pull build's symbol set and `.text` identical to the baseline) and **G2** (the pull and push builds have the same set of test names) retired with the pull build in P13; their successors are: the zero-`MG_Remote`-symbol check on the shipped library, the skip census (newly appearing skips and disappearing names must be named in the baseline), the recorder golden (`MG_Test/Pipe/RecorderGoldenTest.cpp`: record streams from a fixed GL call sequence compared one by one; changing the golden requires saying why in the commit), and the link ratchet (the server half's references to the frontend only fall, and `# P13` annotations are not allowed).
|
||||
- The performance criterion is **per-thread CPU time**; device comparisons use reboot-clean + same-thermal-window paired A/B, on the Redmi `2f7cbe2e` only (pin-verification record, in git history); a Windows machine is not a correctness gate.
|
||||
- The close-out is reviewed once by a different model family; findings enter the first round of the next phase; implementers and reviewers use different model families; do not over-verify (user, 2026-09-16, ID-66).
|
||||
|
||||
## CTS
|
||||
|
||||
`gl44to46` 约 56,271 例,单独计价:逐阶段只跑该阶段可能影响的具名块;完整 caselist 只在架构边界与合并 `dev` 之前跑,放 CI 不放关键路径。
|
||||
`gl44to46` has about 56,271 cases and is priced separately: each phase runs only the named blocks it can affect; the full caselist runs only at architecture boundaries and before merging into `dev`, in CI rather than on the critical path.
|
||||
|
||||
@@ -1,49 +1,59 @@
|
||||
# 在宿主 APK 中嵌入 split server
|
||||
# Embedding the split server in a host APK
|
||||
|
||||
宿主应用可直接加载 `libMobileGL.so`,把自己持有的 `ANativeWindow` 交给 server。
|
||||
窗口、输入和 Android Service 的生命周期由宿主 APK 管理;不需要安装 MobileGL plugin APK。
|
||||
公共接口是 [`EmbeddedServer.h`](../../../MobileGL/MG_Remote/Server/EmbeddedServer.h),
|
||||
只使用 C 类型,也可通过 `dlsym` 调用。构建必须开启 `MOBILEGL_BUILD_DISAGGREGATED`。
|
||||
A host application can load `libMobileGL.so` directly and hand the server an `ANativeWindow` it owns.
|
||||
The window, input and the Android Service lifecycle stay with the host APK; no MobileGL plugin APK is
|
||||
needed. The public interface is
|
||||
[`EmbeddedServer.h`](../../../MobileGL/MG_Remote/Server/EmbeddedServer.h), uses only C types, and can
|
||||
also be called through `dlsym`. The build must enable `MOBILEGL_BUILD_DISAGGREGATED`.
|
||||
|
||||
## 有窗口的合成器服务
|
||||
## Compositor service with a window
|
||||
|
||||
在加载库前设置 `MOBILEGL_IPC_ROLE=server`、`MOBILEGL_IPC_DIAL=no`,清除
|
||||
`MOBILEGL_TRANSPORT`、`MOBILEGL_IPC_SERVER_PATH`、`MOBILEGL_IPC_RING_MB` 和
|
||||
`MOBILEGL_IPC_STAGE_MB`。建议放在宿主 APK 的独立服务进程中,以免渲染故障结束 UI 进程。
|
||||
Before loading the library, set `MOBILEGL_IPC_ROLE=server` and `MOBILEGL_IPC_DIAL=no`, and clear
|
||||
`MOBILEGL_TRANSPORT`, `MOBILEGL_IPC_SERVER_PATH`, `MOBILEGL_IPC_RING_MB` and
|
||||
`MOBILEGL_IPC_STAGE_MB`. Running in a separate service process of the host APK is recommended, so that
|
||||
a rendering fault does not take down the UI process.
|
||||
|
||||
1. 调用 `mobilegl_server_display_install_android(request_geometry, user)`。
|
||||
回调可为空;有回调时,它在 apply 线程运行,必须快速返回,把尺寸变更交给宿主线程。
|
||||
2. 从宿主的 `Surface` 获取 `ANativeWindow`,调用
|
||||
`mobilegl_server_display_attach_android(window, width, height)`。
|
||||
server 独立持有引用,调用者随后释放自己的引用。同一窗口再次 attach 只更新尺寸。
|
||||
3. 在服务线程调用 `mobilegl_server_serve_inprocess("@anland-mobilegl-compositor")`。
|
||||
这会在当前进程接受一个 client 会话。AF_UNIX 控制连接用 `SCM_RIGHTS` 传递共享内存段,
|
||||
数据通过 SHM 环传输。客户端设置 `MOBILEGL_TRANSPORT=spawn`、
|
||||
`MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl-compositor`、`MOBILEGL_IPC_DATA=shm` 和
|
||||
`MOBILEGL_IPC_SURFACE=server`。
|
||||
4. Surface 销毁时调用 `mobilegl_server_display_detach(3000)`。
|
||||
返回值 `0/1/2/3` 分别为无窗口、直接释放、会话释放后完成、超时。
|
||||
超时会保留引用,直到 apply 线程停止使用窗口;当前失窗会结束活动会话。
|
||||
5. 服务停止时调用 `mobilegl_server_stop_inprocess()`,等待 serve 线程返回,再调用
|
||||
`mobilegl_server_display_uninstall(3000)`。几何回调的 user 对象应至少活到此时。
|
||||
1. Call `mobilegl_server_display_install_android(request_geometry, user)`.
|
||||
The callback may be null; when present it runs on the apply thread, must return quickly, and hands
|
||||
size changes to the host thread.
|
||||
2. Take an `ANativeWindow` from the host's `Surface` and call
|
||||
`mobilegl_server_display_attach_android(window, width, height)`.
|
||||
The server holds its own reference; the caller then releases its own. Attaching the same window
|
||||
again only updates the size.
|
||||
3. On the service thread, call `mobilegl_server_serve_inprocess("@anland-mobilegl-compositor")`.
|
||||
This accepts one client session in the current process. The AF_UNIX control connection passes the
|
||||
shared-memory segment with `SCM_RIGHTS`, and data travels over a SHM ring. The client sets
|
||||
`MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl-compositor`,
|
||||
`MOBILEGL_IPC_DATA=shm` and `MOBILEGL_IPC_SURFACE=server`.
|
||||
4. When the Surface is destroyed, call `mobilegl_server_display_detach(3000)`.
|
||||
The return values `0/1/2/3` mean no window, released directly, completed after the session
|
||||
released it, and timed out. On timeout the reference is kept until the apply thread stops using the
|
||||
window; losing the window currently ends the active session.
|
||||
5. When the service stops, call `mobilegl_server_stop_inprocess()`, wait for the serve thread to
|
||||
return, then call `mobilegl_server_display_uninstall(3000)`. The geometry callback's user object
|
||||
must live at least until then.
|
||||
|
||||
`width/height` 为零时,Android attach 从 `ANativeWindow` 查询尺寸。几何回调收到正尺寸表示
|
||||
请求固定缓冲尺寸,收到 `0/0` 表示恢复宿主布局尺寸;宿主应通过后续 attach 报告实际结果。
|
||||
安装、attach、detach 和 uninstall 由宿主串行调用。库拒绝替换已安装的 display 或尚未结束
|
||||
的旧窗口租约,避免晚到的释放使用另一组引用回调。
|
||||
When `width/height` are zero, the Android attach queries the size from the `ANativeWindow`. A positive
|
||||
size in the geometry callback requests fixed buffer dimensions; `0/0` means restore the host's layout
|
||||
size, and the host reports the actual result through a later attach. Install, attach, detach and
|
||||
uninstall are serialized by the host. The library refuses to replace an installed display or an old
|
||||
window lease that has not ended, so that a late release cannot use another set of reference callbacks.
|
||||
|
||||
## 并发的离屏应用服务
|
||||
## Concurrent off-screen application service
|
||||
|
||||
一个进程仍只有一套 backend/GL 状态。有窗口的服务只接一个合成器;其他 EGL client 可连接
|
||||
宿主 APK 打包的 `libMobileGLServer.so` 可执行文件,由另一个服务进程负责启动和结束:
|
||||
A process still has only one set of backend / GL state. A service with a window accepts a single
|
||||
compositor; other EGL clients can connect to the `libMobileGLServer.so` executable packaged by the
|
||||
host APK, which another service process starts and stops:
|
||||
|
||||
```text
|
||||
<nativeLibraryDir>/libMobileGLServer.so @anland-mobilegl --serve --max-sessions 16
|
||||
```
|
||||
|
||||
`--max-sessions` 是活动会话上限,接受 `1..64`,默认仍为 `1`。大于 `1` 只适用于
|
||||
AF_UNIX 的 `--serve` 离屏 supervisor。每个 client 单独 fork worker,拥有独立 backend,
|
||||
共享内存与其他会话隔离;worker 退出后归还名额,supervisor 结束时所有 worker 一起结束。
|
||||
这些 worker 没有 display,客户端应清除 `MOBILEGL_IPC_SURFACE`,由客户端 WSI 提交窗口图像。
|
||||
`--max-sessions` is the limit on active sessions; it accepts `1..64` and still defaults to `1`. Values
|
||||
above `1` apply only to the AF_UNIX `--serve` off-screen supervisor. Each client forks its own worker
|
||||
with an independent backend, isolated from other sessions in shared memory; a worker returning frees
|
||||
its slot, and all workers end when the supervisor ends. These workers have no display; the client
|
||||
should clear `MOBILEGL_IPC_SURFACE` and submit window images through its own WSI.
|
||||
|
||||
进程隔离解决多个应用同时连接的问题;同一应用中的真正多 context/share-group 支持仍需另行实现。
|
||||
Process isolation solves multiple applications connecting at the same time; true multi-context /
|
||||
share-group support inside one application still needs separate work.
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
# 术语
|
||||
# Terms
|
||||
|
||||
> 参考。文档导航见 [`../README.md`](../README.md)。
|
||||
> Reference. For the documentation index see [`../README.md`](../README.md).
|
||||
|
||||
| 术语 | 意思 |
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| 前端 / 后端 | 前端 = `MG_State` + `MG_Impl`,处理应用的 GL 调用;后端 = `MG_Backend`,真正调用驱动 |
|
||||
| Espryt / Magma | 两个后端:Espryt 走 OpenGL ES(DirectGLES),Magma 走 Vulkan(DirectVulkan) |
|
||||
| client / server | 拆分后的前端角色 / 后端角色;可以是两个线程、两个进程或两台机器 |
|
||||
| monolith / inproc / spawn | 三种拓扑:单线程直调 / 同进程两线程 / 两个进程(可跨机) |
|
||||
| pull / push | 后端自己去读前端状态(旧做法)/ 前端把状态推给后端(MGPipe) |
|
||||
| verb | 会让后端真正做事的命令:draw、dispatch、clear、blit、回读、XFB、query、纹理操作。状态只在 verb 之前推送 |
|
||||
| 句柄 `{slot, gen}` | 对象的身份:client 分配的槽号 + 复用代数,server 从不回传 |
|
||||
| CSO | 常量状态对象(渲染状态、顶点格式、sampler、sampler view、shader):client 按内容去重,server 按句柄缓存 |
|
||||
| 控制面 / 数据面 | 握手与罕见控制消息走的连接 / 命令记录与批量数据走的通道;两者可分别选(共享内存或字节流、本机或 TCP) |
|
||||
| lockstep / run-ahead | client 每条命令都等 server 做完 / client 发完就走,只有需要答案的命令才等 |
|
||||
| barrier tax | 拆成两线程后多出来的逐线程 CPU 代价(split − push) |
|
||||
| device-lost | server 崩溃或断线后 client 进入的状态:GL 调用变空操作、交换缓冲返回"上下文丢失",而不是崩溃或卡死 |
|
||||
| class A / B / C | 后端函数表的槽:A 本地回答,B 发成记录,C 还没迁移、按名拒绝 |
|
||||
| Track V / Track H | 值类读点的迁移(整块数据过线)/ 对象类读点的迁移(对象指针换成句柄) |
|
||||
| G1 / G2 / G5 / G14 | 验证门:单进程构建二进制逐字节不变 / pull 与 push 测试名集合相同 / 受保护的后端函数逐字节不变 / 测试名只增不删 |
|
||||
| red-once(R-16) | 每个门都必须真的跑红过一次,证明它能因为它要抓的问题而失败 |
|
||||
| `MGGen` | server 私有的"我重建了驱动对象"纪元,永不过线;与句柄代数严格分开 |
|
||||
| frontend / backend | frontend = `MG_State` + `MG_Impl`, handling the application's GL calls; backend = `MG_Backend`, calling the driver for real |
|
||||
| Espryt / Magma | The two backends: Espryt goes through OpenGL ES (DirectGLES), Magma through Vulkan (DirectVulkan) |
|
||||
| client / server | The frontend role / backend role after the split; they can be two threads, two processes or two machines |
|
||||
| monolith / inproc / spawn | The three topologies: single thread direct calls / two threads in one process / two processes (optionally across machines) |
|
||||
| pull / push | The backend reads the frontend's state itself (the old way) / the frontend pushes state to the backend (MGPipe) |
|
||||
| verb | A command that makes the backend actually do work: draw, dispatch, clear, blit, readback, XFB, query, texture operations. State is pushed only before a verb |
|
||||
| handle `{slot, gen}` | An object's identity: a client-allocated slot plus the reuse generation; the server never sends it back |
|
||||
| CSO | Constant state object (render state, vertex format, sampler, sampler view, shader): the client deduplicates by content, the server caches by handle |
|
||||
| control plane / data plane | The connection carrying the handshake and rare control messages / the channel carrying command records and bulk data; each can choose shared memory or a byte stream, local or TCP |
|
||||
| lockstep / run-ahead | The client waits for the server after every command / the client sends and moves on, waiting only for commands that need an answer |
|
||||
| barrier tax | The extra per-thread CPU cost of splitting into two threads (split − push) |
|
||||
| device-lost | The state the client enters after the server crashes or disconnects: GL calls become no-ops and swap buffers returns "context lost" instead of crashing or hanging |
|
||||
| class A / B / C | Slots in the backend function table: A answers locally, B becomes a record, C is not migrated yet and is refused by name |
|
||||
| Track V / Track H | Migration of value-class read points (a whole block crosses the wire) / migration of object-class read points (object pointers become handles) |
|
||||
| G1 / G2 / G5 / G14 | Verification gates: single-process build binary byte-identical / pull and push have the same test-name set / protected backend functions byte-identical / test names only grow |
|
||||
| red-once (R-16) | Every gate must really have run red once, proving it can fail for the problem it is meant to catch |
|
||||
| `MGGen` | The server's private "I rebuilt the driver object" epoch; it never crosses the wire and is kept strictly separate from the handle generation |
|
||||
|
||||
@@ -6,7 +6,7 @@ thread records and applies. Inproc: the GL thread records and `mgl-srv-apply` ap
|
||||
|
||||
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 (section pwab).
|
||||
The mechanism is analysed in depth in PRESENT-WAIT.md.
|
||||
The mechanism is analyzed in depth in PRESENT-WAIT.md.
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -264,7 +264,7 @@ the display. Both are separate decisions and are not measured here.
|
||||
images, and a request of 7 (extra 2) gives 7 (logcat "Swapchain created ... imageCount", all runs). The
|
||||
platform rounds the MAILBOX request up to at least 6, so extra=2 is only one image more than the
|
||||
default. extra=1 would probably give no change; that was not run.
|
||||
- **Memory:** each image costs a full-size RGBA8 colour buffer plus a D24S8 depth/stencil buffer of the
|
||||
- **Memory:** each image costs a full-size RGBA8 color buffer plus a D24S8 depth/stencil buffer of the
|
||||
same size (Magma's default framebuffer has one per image). At FCL's 1280x800 surface in these runs that
|
||||
is about 4.1 + 4.1 = 8.2 MB per image. At a native 2560x1600 surface it would be about 33 MB. Driver
|
||||
compression metadata adds a little.
|
||||
@@ -317,7 +317,7 @@ Nothing outside the throttle moved: frame = CPU + throttle still holds (2.43 + 0
|
||||
- The +24 % bound came from the older traces' CPU of 2.86-2.9 ms (3.60 -> 2.9). At this baseline the
|
||||
presenting-thread CPU is 2.39 ms (reps) or 2.49 ms (trace). So removing the throttle alone allows up to
|
||||
+32 % (trace: 3.29 -> 2.49).
|
||||
- The after arm also has lever A, and A did more than modelled. The model charged the window pass a full
|
||||
- The after arm also has lever A, and A did more than modeled. The model charged the window pass a full
|
||||
0.8 ms start latency plus the pass, about 0.95 ms after the release. Measured E-R is 0.69 ms, so the
|
||||
start for the small final batch is shorter than for a whole-frame batch. The GPU-after-release wait fell
|
||||
from 0.50 to 0.11 ms per frame. The display-before-release wait fell from 0.28 to 0.11, from fewer gated
|
||||
|
||||
@@ -20,7 +20,7 @@ them wrong.
|
||||
| 2026-10-06 | `DirectGLES.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.Legacy.*`, `DirectVulkan.HandleRecycle.AbaControl.*` | Test / integration-gpu (`integration-monolith-control`) | they ran `MOBILEGL_PIPE_PUSH=0`, the pre-handle arm, which P13 W3b removed with `MOBILEGL_PIPE_LEGACY_MEMOS` (ID-P13-3) | `HandleRecycle.Handles.*` and `DirectVulkan.HandleRecycle.AbaControlHandles.*` (the ABA control against the handle arm that ships) stay |
|
||||
| 2026-10-06 | `DirectGLES.ResourceSubsystemControl.Off.*`, `DirectGLES.ResourceSubsystemOff.*`, `DirectGLES.ObjectSubsystemControl.{Off,Refused,RefusedTexture}.*` | Test / integration-gpu | they cleared subsystem bits (masks 0x7f / 0x1ff / 0x9ff / 0x5ff) to run legacy arms; since W3b bits 0-13 are fixed on (ID-P13-3) | one named-refusal control per lane, `DirectGLES.SubsystemMaskRefusal.<Lane>`: the same scenario and mask must go red with `Fatal{PipeSubsystemsFixedOn}` naming that mask (`Harness/RefusedMaskControl.cmake`); the `On` lanes now run 0x3fff |
|
||||
| 2026-10-06 | `SanityTest`: `DirectGLESSlotTable.{AnArmlessKnobCombinationStopsInsteadOfSkippingTheLane, TheArmlessCasesLeaveTheLogPathAndTheConfigAsTheyFoundThem, EglBringUpUnderTheArmlessKnobPairReturnsInsteadOfStopping}` | Test / unit | they pinned the `MOBILEGL_PIPE_LEGACY_MEMOS=0` + cleared-bit knob pair; the knob is gone and the mask is refused at startup | the SubsystemMaskRefusal controls above |
|
||||
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView` | Test / unit | **not a behaviour gap.** At 0x3fff the parameter push is record-addressed (`SyncTextureParamsToBackend` reads the texture's applier record; nothing in it consults a sampler view), but the unit lane cannot create that record (`NoP4aConsumer`, as above), so the probe's handle half never had a record to read; its legacy half pinned the pre-handle push that W3b removed | `TextureParamsWithoutASamplerViewScenario` (G9): its four cases take a WHITE-BOX reading of the driver-side parameters before any sample (`TakeTheWhiteBoxReadingBeforeAnySample`, ID-19), at the shipping mask on every lane - the same property, observed where records exist |
|
||||
| 2026-10-06 | `SanityTest`: `DirectGLESTextureSync.AnAttachmentOnlyTexturesParametersReachTheDriverWithNoSamplerView` | Test / unit | **not a behavior gap.** At 0x3fff the parameter push is record-addressed (`SyncTextureParamsToBackend` reads the texture's applier record; nothing in it consults a sampler view), but the unit lane cannot create that record (`NoP4aConsumer`, as above), so the probe's handle half never had a record to read; its legacy half pinned the pre-handle push that W3b removed | `TextureParamsWithoutASamplerViewScenario` (G9): its four cases take a WHITE-BOX reading of the driver-side parameters before any sample (`TakeTheWhiteBoxReadingBeforeAnySample`, ID-19), at the shipping mask on every lane - the same property, observed where records exist |
|
||||
| 2026-10-06 | unit `RenderStateSpans.{ResidualTripWireIsSilentUntilTheApplierOwnsTheBytes, ResidualTripWireHoldsAcrossAVerbClassThatDoesNotPublishTheBlock, ResidualTripWireFiresNamingTheCapability}` and PeerLatchTest's `SetResidualValueStateBlobSize` row | Test / unit | P13 W3c deleted ResidualValueBlock and its applier trip wire (op 46 is a retired, declined row) | integration-verify's new `Negative control A2 (corrupted capability)`: the verify comparator must go red on a corrupted `IsCapabilityEnabled`; `PipeWireCodecTest.ResidualValueBlockCrossesAsItsOwnBlob` now pins that op 46 is declined; `TrackerTest...AClipDistanceEnableReArmsTheResidualBlock` now asserts the capability reaches the applier's block |
|
||||
| 2026-10-06 | unit `DirectGLESBufferDrawProbe.ALiveHostMapKeepsTheHandleArmProbeDirtyBetweenTwoDraws` | Test / unit | P13 W6: it pinned the frontend arm's draw-time `SyncPersistentMappedRange` keeping a live host map's probe dirty; that per-draw hook is deleted - the record arm publishes a live map's span at push time (`PersistentMapTracker`) | `PersistentCoherentMapScenario` and `SplitBufferTest`'s persistent-map push / live-host-writes cases, on every arm |
|
||||
| 2026-10-06 | unit `DirectGLESSanity.{BindsAMultisampleTextureDespiteTheDefaultMipmapFilter, BindingZeroClearsPreviousNativeTextureBinding}` | Test / unit | P13 W6: GL 4.6 8.17 / 8.17.1 rules, but asserted through the deleted frontend arm's unit walk (`BindCurrentTextures` over frontend binding slots); every library walks the units by handle now | moved, not dropped: `TextureUnitSamplingScenario` (integration-gpu on both backends plus the split arms) asserts both rules on a real context |
|
||||
@@ -70,7 +70,7 @@ them wrong.
|
||||
### `minecraft-1.17-main-menu-854` (Test retrace / retrace-split / retrace-verify, APK retrace), 2026-10-06
|
||||
|
||||
- **Symptom**: every lane, both backends, SSIM 0.0186 against the golden; the frame is Minecraft's
|
||||
red loading-screen colour. Local repro on WSL llvmpipe: same SSIM, monolith and inproc.
|
||||
red loading-screen color. Local repro on WSL llvmpipe: same SSIM, monolith and inproc.
|
||||
- **Root cause**: the trace was captured on an implementation that gave shaders and programs
|
||||
**overlapping names** (shader 1 and program 1 live at once, likewise shader 5 / program 5,
|
||||
calls 159-209). GL 4.6 core and ES 3.2 §7.1 put shader and program objects in one shared name
|
||||
@@ -100,7 +100,7 @@ them wrong.
|
||||
### `SanityTest` `DirectGLESTextureSync.UnitMemoRefusesToDriveATwinFromAnotherTexture`, 2026-10-06 (removed in W3b, then RESTORED)
|
||||
|
||||
- **First call (wrong)**: retired with the P2-mask probes as "its arm is gone". The orchestrator's
|
||||
review asked whether its failure at the shipping mask was a behaviour gap.
|
||||
review asked whether its failure at the shipping mask was a behavior gap.
|
||||
- **Finding**: the code it pins is LIVE. The monolith arm (DISAGG builds under
|
||||
`MOBILEGL_TRANSPORT=monolith`, and the FCL build) still walks the frontend units with the
|
||||
borrowed-slot work list and its `PairingsIntact` check (`UnitTexturesByHandle()` is false on
|
||||
|
||||
Reference in New Issue
Block a user