Files
MobileGL/docs/Disaggregated/P6-ENDSTATE-REVIEW.md
T
swung0x48 e19095269b [docs, MG_Remote] (P7): the roadmap rows say what the tree says, and CONTRACT-P7 is drafted
ROADMAP.md: the Ph / P3b-P4b / P7 rows carry the audit's verdicts and the factual
corrections it found - the @P7 refusal list is 15 sites in 4 files (the row printed 8),
the 29-commit DirectVulkan shape wave was never recorded, the device divergence is two
observations with no per-case attribution and no lockstep control, SetupDrawSnapshot /
content-addressed CSO are declined for this stage, "XFB scatter to client" is retired
in favour of the two real holes, readback/pack state splits into closed and open
halves, the NDK size pin is obsolete, PACK-PBO readback landed in 70df57bb, the ABI
fingerprint landed in P6.5, and OQ-8 / OQ-10 / OQ-13 / OQ-21 get their current answers.
P6-ENDSTATE-REVIEW.md:284 qualifies an Init.cpp citation the lint reported ambiguous.

MG_Remote/CONTRACT-P7.md: rules I (no std::abort on the Magma wire arm) and J (every
fix reds once on a two-process arm), the five exit gates in testable form, the
retire-vs-decline table for every @P7 name, the ratchet's definition and P7 target,
the nine-subsystem denominator for the 40% checkpoint, the wave-2 file partition,
and the amendments to CONTRACT-P6. Census, ratchet and lane numbers are marked
待填 until wave 0's packages report them.
2026-09-22 13:26:34 -04:00

407 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P6 重审:面向 gfxstream 形状的终局
> 审查头 `b3b9c2ee`(`feat/disaggregated`,与 `origin` 同步)。本文是对 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md)
> 与 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md) 的逐行重审,判据是一个尚未写进路线图的终局:
> **client 在别处(AVF pVM / 容器类虚拟机)产生 GL 命令流,server 在一个 Android app 里解析、执行、渲染并显示。**
>
> 本文不推翻 P6,**也不改变 P6 的形状**。它给出:P6 哪些行在终局下是错的(四行,其中两行在树上已实测为假)、
> P6 必须现在付但很便宜的几项、以及终局在路线图上的落点。所有 `file:line` 在本头解析。
> 标 **[实测]** 的是在树上核对过的;标 **[外部未验证]** 的必须先做探针再当成前提。
>
> **2026-09-21 更正(a6 收官后)**:a6 逐条复核了本文,推翻四处——`MG_State` 八站点的形状(§2.5)、
> abort 计数 93→92、`m_windowHandle` 的注入点形状、以及 `MGPipeSplitActive()` 的命名冲突。
> 正文已就地更正。a6 另发现四处本文未及的阻塞项(apply 线程在第一条记录前 abort、控制面两端都没接、
> 控制回复等待无期限、`SurfaceReply` 装不下 result)。**以 [`notes/p6/a6-audit-v1.md`](notes/p6/a6-audit-v1.md) 为准。**
>
> **2026-09-22 更正(终局改判)**:本文假设的终局——client 在 AVF pVM、链路 `AF_VSOCK`——**撤回**。现定终局:
> client 与 server 可在**不同机器、不同 OS / 架构**上,经 **TCP** 连接;传输栈拆成控制面(`ITransport`)× 数据面(`ILink`)
> 两根独立可选、握手协商、可混搭的轴(同机可控制面 TCP + 数据面共享段)。后果:§3 加一行 **D TCP**(约束集与 C 相同——
> 无 fd 传递、无共享页——但 RTT 毫秒级、带宽 30–100 MB/s);§3.1、§7.3、§8.1、§8.2 的 vsock 条目撤回;§6 第 3 行
> "P7 不变、定宽只是协商值"改为**定宽 + 布局摘要是 P6.5 的第一个包**;"形态 B 不需要 P6.5"不再是有效场景,**P6.5 上关键路径**;
> §5.2 的 `LinkTerms` / `Refuse` / 指纹拆分经树上核实**未由 hs 落地**,归 P6.5。以 `ROADMAP.md` 的 P6.5 / Ph 行与
> `ARCHITECTURE.md` §11.9 为准;本文其余部分作为审查记录不再改。
---
## 0 一句话结论
P6 的**进程那一半**(fork/exec、envp 剔除、EOF 死亡、`ServerMain`、控制帧传输、静态量核验、整个出口门)
在终局下**几乎逐字成立**——它讲的是"两个角色在两个地址空间",对"哪个内核"一个字都没说。
P6 的**传输那一半**(`SCM_RIGHTS` + `ShmSegment::Adopt` + 共享 `RingControl` 页)**过不了 pVM 边界**,
这没关系——**前提是 P6 在它下面留一道缝**,而当前计划一道都没有:`ITransport.h:16-20` 自己写明热路径绕开它,
`Ring.h:259` / `ReplySlot.h:117` / `EventRing.h:125-128` 直接收 `void* base + capacity`。
所以重审的结论不是"重排 P6",而是:**P6 的形状不动,范围加五件,其中三件机械、一件是纠错。**
---
## 1 P5e 已经替终局把最贵的账付了
这一点值得单独写一段,因为它决定了终局到底可不可行。
跨 VM 边界唯一致命的不是带宽,是**每帧会合次数**。侧写记录在案(`MEASUREMENTS.md` §11):
lockstep 下客户端每帧进入 doorbell 等待约 **916 次**(对 ~849 draw),VD32 下约 **3600 次**,
`WaitForAppliedOrEventBacklog` 占客户端 GL 线程 **31.58%** 自身时间。
**任何一个 916×/帧 的会合结构,放到一条 RTT 以 µs 计的链路上都是死的。**
P5e 的 run-ahead 把它降到**每帧一次结构性会合**(present credit),`ARCHITECTURE.md` §13.1 的零 roundtrip
清单是它的正式表述。于是链路延迟的乘数从 ~900 变成 ~1:**1 ms 的 RTT 是 16.7 ms 帧的 6%,不是 900 ms。**
> gfxstream 花了很多年才走到"生成式编码器 + 可用/已消费计数器 + 门铃抑制 + 一帧一次同步"。
> 这套形状 MobileGL 已经有了,而且是为别的理由做的。终局要抄的是 gfxstream 的**协议**,不是它的**传输**(§4)。
带宽侧的账相形之下是小事:实测持久映射线流量 364.7 KB/帧,`ARCHITECTURE.md` §14 预算 ~1 MB/帧,
60 fps 即 22–64 MB/s。**但这里有一个必须先量的缺口,见 §7。**
---
## 2 在树上实测出的四个硬洞(本次审查的最高价值产出)
这四条不是设计意见,是在 `b3b9c2ee` 上核对出来的事实,其中两条与两份 P6 文档的**明文陈述相反**。
### 2.1 [实测] X11 / Win32 窗口种类今天是**被接受**的,而终局的首要 guest 正好走这条路
`P6-CONTRACT-DRAFT` §4.3 与 §8 的 S5 认为"真窗口在 server 端具名拒绝"。实际上只拒两种:
- `WindowBackendForWireWindowKind`(`Protocol/SurfaceOpCodec.cpp:104-113`)**接受** `WindowKind::X11 → WindowBackend::X11`
和 `Win32Hwnd → Win32`;`DecodeWireSurfaceOp`(`:141-179`)只拒 `AndroidNativeWindow` 和 `MetalLayer`。
- 解码后 `op.nativeToken()` 原样抄过,`UnpackWindowHandle`(`Server/ServerLoop.cpp:875-882`)无条件
`HandleFromToken<void*>`,交给 backend 当 `NativeWindowType` 用。
- `DetectWindowBackend()` 是编译期 `#if` 链(`MG_Impl/EGLImpl/EGLImpl.cpp:59-70`)。
**后果**:终局的首要 guest 是一个 Linux pVM,它编译 `__linux__`、宣告 X11、送一个 XID——
一个**完全正确、毫无恶意**的 guest,做 EGL 里最普通的一次调用,就穿过了计划认为已经关上的那道拒绝,
在 server 进程里变成一次野指针解引用。今天它是潜伏的(P6 只跑离屏),终局里它在用户的 app 里。
**改法**:拒绝改成**白名单**——只接受 `Pbuffer` / `Surfaceless` / `None`,凡是命名了 client 进程窗口对象的
种类一律具名拒绝。理由也要改:不是"P12 之前不支持",而是 **Rule G 的重述——一个含义来自对端窗口系统的值永远不过线**。
S5 随之加宽:`WindowKind::X11` 带一个像样的 XID 到达 server 必须具名变红,且门要能证伪"token 到达了 `UnpackWindowHandle`"。
今天的 S5 只测 `AndroidNativeWindow`,**洞开着它照样绿**。
### 2.2 [实测] device-lost 闩锁**不存在**
`P6-CONTRACT-DRAFT` §5.1 写的是 "The latch is already designed and **is implemented unchanged**",
`P6-SPAWN-PLAN` §4 写的是"照抄"。两句都是假的:
- `EGL_CONTEXT_LOST` / `GL_UNKNOWN_CONTEXT_RESET` 在 `MG_Test` 之外只出现在两个枚举转字符串函数里。
- `FatalCode::DeviceLost` / `ServerCrashed` 只是生成的 schema 枚举值,**零 producer、零 consumer**。
- `glGetGraphicsResetStatus` 无条件返回 `GL_NO_ERROR`(`MG_Impl/GLImpl/Getter/GL_Getter.cpp:2855-2861`)。
- `MOBILEGL_IPC_RESPAWN` 与 `MOBILEGL_IPC_IDLE_EXIT_S` **在任何地方都没有被解析**。
**今天 server 死掉,client 会永远静默地 DECLINE 每一条 verb,`glGetError` 干净。**
这是当前计划**最大的一处尺寸错误**:它被记成了"搬运",实际是一个完整的包。P6 必须写它,并按包计价。
它也是唯一能把 S2(`kill -9` 之后客户端能否区分"慢"与"死")做成一个能因错误实现变红、而不只是因缺失变红的门的前提。
### 2.3 [实测] `Fatal{}` 是 `std::abort()`,`MG_Remote` 下有 92 个非测试站点
`Transport/WireLog.cpp:50`:记日志 → echo 到 stderr → `std::abort()`。
今天这完全正确:server 是 client fork 出来的子进程,一条坏记录意味着"我们自己有 bug",崩掉是对的。
**终局里 server 是一个长期存活的普通 Android app,替用户自己装的 VM 渲染。**
此时每一个从对端字节可达的 abort 站点都是一个远程 kill——而且很多不需要恶意字节:
一个仅仅**不排空 `SEG_EVENT`** 的 guest 就能在 60 秒内用 `Fatal{EventRingOverflow}` 杀掉宿主渲染 app。
P6 该做的**不是**把它改成闩锁(那需要在每个 `[[noreturn]]` 站点发明一条真实返回路径,不是"一个函数翻一下"),
而是**收口**:一个 `Session::Fail(FatalCode, detail)`,P6 里仍然 `[[noreturn]]`、仍然 abort、日志逐字不变,
但换来三件当下就值的东西——(a) 一个地方在死之前向对端发一帧 `SessionFault{family, detail, seq, op}`,
让 guest 的日志知道是哪条记录杀了会话,而不是读到一个裸 EOF;(b) 一个遥测点;
(c) 一道 CI 普查门,禁止在收口之外新增 abort 站点。**策略翻转留给 Ph(§6)。**
### 2.4 [实测] 段尺寸由 **client 的环境变量**决定
`ConfigLoader.cpp:363-364`:`MOBILEGL_IPC_RING_MB` / `STAGE_MB` 从 **client 进程**的 env 读,量程到 1024 / 4096 MiB。
终局里这意味着**一个 guest 里的环境变量决定渲染 app 内部一次几百 MB 的分配**。
配套的另一半:`ShmSegment::Adopt` 的 `fstat`"不信对端宣告的尺寸"检查(`ShmSegmentPosix.cpp:127-133`)
是目前**唯一**的边界,而它在没有 fd 的链路上没有对应物。
**改法(P6,便宜)**:四个尺寸变成 **server 陈述**的协商字段,client 的请求可被 server 钳制并在 `Welcome` 里报告钳制后的值。
### 2.5 另外三条尺寸性事实
| 事实 | 出处 | 影响 |
|---|---|---|
| `mobilegl_server_main` **在树上不存在**,只存在于 `ARCHITECTURE.md:488-496`;唯一的 `MobileGLServer` target 是 P0 spike 桩 | 实测 | `a6` 第 5 项(server 能否不链 `MG_Impl`)必须**真去链一次**,而不是再读一遍断言。三个阶段的文档都在断言它,没人试过 |
| `MG_Config::Transport != Monolith` 有 **238 个**非测试站点:155 `MG_Backend` / 35 `MG_Impl` / 21 `MG_Remote` / 12 `MG_Pipe` / **8 `MG_State`** / 7 Config | 实测 | 此前每一次普查都漏了 `MG_State` 那 8 个。**~~它们是唯一不按目录成形的一批、同一个函数里同时问两个问题~~ a6 已推翻这个判断**:8 个里 2 个是注释,其余 6 个里 5 个是单问题测试,只有 `MipmapStorage.cpp:55-56` 一个真的同时问两个;而"同时问两个"的形状散在 `SlotAllocator` / `PipeFill` / `PipeApply` / `Managers` / `MG_Remote/Client` 的 **18 个站点**上。`a6` 仍应先分类这一族,但取样对象是那 18 个,不是 `MG_State` 的 8 个 |
| `m_windowHandle` **不止一个注入点**。a6 给出精确形状:全树 store 只有 4 处(`BackendObject.cpp:475`、`:365`、`:206`、`:207`),read 只有 5 处;`:283` 是一次**调用**不是 store;`.Width`/`.Height` **没有任何读者** | 实测 | "server 拥有显示"确实不是一处改动,但最小 latch 点比原先写的更清楚:`surfaceState->Window` 只在 `RegisterEGLWindowSurface`(`:232-238`)写一处。连同 `sameHandle` 去重键共四处,见 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md) §6 |
---
## 3 三种部署形态,以及每种真正可用的东西
终局不是一个场景,是三个,约束差别很大。
| 形态 | 边界 | 可用传输 | 可用 bulk | P6 机制是否够用 |
|---|---|---|---|---|
| **A 同内核两进程**(P6 的 spawn) | 进程 | `AF_UNIX` | `SCM_RIGHTS` + memfd/ASharedMemory + mmap | ✅ 今天就成立 |
| **B 容器类 guest**(DroidSpace / Winlator / Termux,共享内核) | 命名空间 / 沙箱 | `AF_UNIX` | 同上(含 `AHardwareBuffer_sendHandleToUnixSocket`,API 26) | ✅ **整套 P6 机制逐字可用** |
| **C AVF pVM** | 内核 | `AF_VSOCK`,**仅此** | 无。无 SCM_RIGHTS、无共享页、无 dma-buf | ❌ 需要 P6.5 |
| **D 跨机 TCP**(2026-09-22 定的终局) | 机器 / OS / 架构 | `TCP`,仅此 | 无 | ❌ 需要 P6.5;且两端是**不同二进制**,wire 定宽 + 布局摘要成硬前提 |
**这是本次审查最令人愉快的一条结论:形态 B 的终局,从"P6 + 修订后的 P12"就能到达,不需要 P6.5。**(2026-09-22:形态 B 不再是操作性场景;终局是 D,P6.5 在关键路径上。)
同内核意味着 `AF_UNIX` + memfd + `SCM_RIGHTS` 全部照旧。
Winlator 在自己 app 内跑 VirGL / Vortek 渲染服务,是"终局减去 VM"的一个**已出货存在性证明**。
### 3.1 [外部未验证] AVF 形态有一个可能致命的外部约束
> **撤回(2026-09-22)**:终局不再是 AVF,本节只作历史记录,探针不做。
审查给出的判断是:**一个普通(untrusted)Android app 根本无法参与 AVF。**
两道独立的闸,任一成立即致命:
1. `android.system.virtualmachine` 是 `@SystemApi`,`MANAGE_VIRTUAL_MACHINE` 限预装 / 平台签名(开发期可 `adb shell pm grant`)。
2. AOSP sepolicy 带 `neverallow all_untrusted_apps *:vsock_socket ~{ getattr getopt read write };`
——即 `socket(AF_VSOCK)` 被 MAC 拒绝,不是权限门。app 只能读写**别人递给它的** vsock fd,而那只有 VM 拥有者拿得到。
若这条成立,"在一个普通 Android app 里跑 server"对形态 A / B **为真**,对 AVF **为假**——
AVF 下 server 必须**链进 VM 启动器 app 本身**,或者由第三方桥接后走 IP 到达。
另外一条方向性事实:`VirtualMachine.connectVsock(port)` 是 **host → guest** 拨号。
所以在 AVF 形态里 **MobileGL 的 server 是 socket 的拨号方,client 是监听方**——
这正是 §5.2 要把 Role 与 Dial 拆成两根轴的直接理由。
> **这条必须先做探针再当前提。** 一小时的设备实验:从一个普通 app 调 `socket(AF_VSOCK)`,抓 avc denial。
> 它不改变任何结构,但改变终局能承诺什么。**没有人应该靠口碑给一条链路定尺寸。**
---
## 4 抄 gfxstream 的协议,不抄它的传输
gfxstream 的传输——virtio-gpu MMIO 设备、256 M–8 G 的 host memory window、`RESOURCE_MAP_BLOB`、ASG 共享环——
**每一样都要 VMM 级权限加 hypervisor memslot 配合**,一个拿不到内核的 app 永远得不到。
virgl 同理:它们 present 的方式是把一个 guest 资源变成 virtio-gpu 的 scanout。
**纯用户态、可以抄、而且 MobileGL 已经有的**:生成式 1:1 编码器 / 解码器、available/consumed 计数器、
门铃抑制、blob-vs-inline 的载体选择、fence 时间线、每上下文的 capset 协商。
这条应当写进契约作为一条禁令:**任何需要 guest 可见的 host 内存窗口、virtio-gpu context/blob、
MMIO 门铃或 DRM 设备的设计,都不在本项目的可达集内。**
同理,presentation 侧不要再用 gfxstream 类比推理——它的 present 路径我们结构上就够不着。
---
## 5 P6 的重新定范围
**判决:P6 的形状不变**——fork/execve、socketpair、`SCM_RIGHTS`、只落 pbuffer / surfaceless / 离屏。
理由:P6 昂贵且不可逆的那一半是**进程/角色**那一半,它不在乎对端是 fork 出来的孩子还是 vsock 那头的 pVM,
而且**只有一个真的第二进程能逼它诚实**。传输那一半是死路,但那可以接受——只要下面有缝。
净新增两个包(`lk`、`hs`),一个是计划本来就欠、记错了科目的(`dl`),三个已有包各加一条。
### 5.1 `lk`(新)——数据面的缝
`Transport/ILink.h` + `ShmLink`(今天的 `SessionSegments` / `Ring` / `ReplySlotPool` / `EventRing` 逐行搬到接口后面,
行为零变化)。`SessionProducer` / `SessionConsumer` / `ServerLoop` / `PipeWireEncoder` 收 `ILink*`,其余不动。
**`ITransport` 不扩、不改**——它自己的头(`ITransport.h:16-20`)把自己限定在控制面,而那个范围对 vsock 本来就是对的。
一条实测出来的反例值得记下:**不要把一个 progress 块别名到 `RingControl` 上面**。
`Ring.h:134-147` 的 watermark 那条 cache line 是 `{appliedSeq, submittedSeq, retiredSeq, completedFrameSerial, presentAckSerial}`,
`eventRingFull` / `eventDropped` 在整整一条线之后的另一个 `alignas(64)` 组里,
而且 `submittedSeq` 是 **producer 写**、其余四个是 consumer 写。
任何声称"单写者 64 字节块别名它"的设计在三个独立的点上不成立。
**改法是在 P6 里重组 `RingControl`**:把 `submittedSeq` 挪到 producer 自己那条线(`Ring.h:45-50` 写明它是诊断量、无人等待),
把 `eventRingFull` / `eventDropped` 提进 watermark 组,整组包成具名 `LinkProgress` 成员并加逐字段 `offsetof` 静态断言。
`ShmLink::Progress()` 于是直接返回共享页里的指针——自旋循环里零额外 store、零虚调用,
而 `SessionRings.h` 的每一条规则(`Watermark::Reached` 的 acquire + `>=`、`AdvanceMonotonic` 的 backwards-is-Fatal,`Ring.cpp:364-386`)
在**源码层面保持唯一实现**。
今天重组是免费的(两端同一二进制,由指纹强制);**有了第二份构建的那天它就是一次 wire break。**
缝必须覆盖两个方向:client 把 `kSegEvent` 装到自己的映射上(`ClientSession.cpp:661`),
而 `MG_Impl/Pipe/ResourceTracker.h:606` 通过它解析 writeback payload——
那个站点也要走缝访问器,否则缝的纯度门是假的。
**门**:G1 `.text` 差异具名认领;inproc 车道结果数与逐线程 CPU 与改缝前逐字相同
(`RingProducer::Reserve` 在 ~850–3550 记录/帧上失去内联的风险在这里**实测**,不靠论证);
外加一道机械 grep 纯度门——`RingControl` / `RingProducer` / `ReplySlotPool` / `EventRingConsumer` / `m_shm.`
只允许出现在 `Transport/ShmLink.*` 与传输自己的测试里。
red-once:任一站点绕过缝则 grep 变红;回退 `RingControl` 重组则 `offsetof` 静态断言变红。
**代价**:约一周加半天。**不做的代价**:五个包加两个阶段的重写——
P9 要在 `SEG_REPLY` 里建 slot 池、P11 要建 `SEG_ADOPT`(一块 client 写 server 读、绑 `completedFrameSerial` 的映射),
"以后再说"意味着移植三个映射形状的机制而不是一个。
### 5.2 `hs`(新)——握手身份,以及一次不是 abort 的拒绝
把 `abiFingerprint` 拆成 `wireFingerprint`(永远比较)与 `buildFingerprint`(仅在 `Dial == Fork` 时比较)。
**有一个广为流传的改法是错的**:`AbiFingerprintInputs`(`SessionRings.h:544-566`)自己的注释写明
`GIT_COMMIT_HASH_SHORT` 在里面是因为"两份 sizeof 相同的构建仍然可能对**字段顺序**有分歧,而那是任何 sizeof 看不见的"。
所以"去掉 git stamp、加一个目录 hash"**如其所述是错的**——
一个覆盖 `(opcode, payload 结构名, flags, WaitClass)` 的目录摘要,既看不见 `MGPResourceDesc` 内部的成员重排,
也看不见一次宽度变化;去掉 stamp 而不补上那道守卫,就是开了一个静默读错状态的洞。
`wireFingerprint` 因此要混入:**有序目录摘要** + **payload 布局摘要**(由 `PipeFields.def` 派生,逐成员的
name / `offsetof` / `sizeof`——`gen_pipe.py` 已经在 CI 里强制字段表点名每个成员,所以这是生成器加法,不是工具链)
+ 记录头布局 + 两个 blob codec 版本 + `kOpCount` + `MOBILEGL_ABI_VERSION` + 字节序 + 指针宽度,
**并且保留** `sizeof(MGPCaps)` 与 `sizeof(DynamicBackendParameters)`——
因为 `MGPWireRec_GetCaps` 就是由 `sizeof(MGPCaps)` 复合出来的(`PipeWire.inc:529-535`),它们是 **wire 事实**,不是构建事实。
**删掉** `sizeof(GLFunctionsTable)`:它从不过线,今天加一个 backend 槽就作废所有对端,毫无 wire 理由。
独立的另一半:握手分歧变成一条 `Refuse{code, detail, 对端的值}` 控制消息 + 两端
`MOBILEGL_ERR_PROTOCOL_MISMATCH`——server 发 Refuse 并 exit 0,client 回收子进程并**具名拒绝,不回落 monolith**。
**握手路径上不留任何 abort。** 现成的写法就在同一个函数八行之外:`ServerSession.cpp:456-466` 已经为畸形帧
返回 `MOBILEGL_ERR_PROTOCOL_MISMATCH` 并带正确的 `msg_as_Hello() != nullptr` 守卫。
顺带修一个实测出的不对称:**client 只检查 `abiFingerprint`(`ClientSession.cpp:566-568`),从不检查 `abiMajor` / `abiMinor`。**
附 `LinkTerms{dataPlane, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` 追加进 Hello / Welcome,
`dataPlane` 在 P6 里**只有一个合法值** `SharedSegments`,其余具名拒绝;四个尺寸由 **server 陈述**(§2.4)。
**注意 P6 的策略反而更严**:`buildFingerprint` 在 `Dial == Fork` 下照样比较——
fork 下子进程本来就是同一二进制,比较它能抓住一个指向旧 `libMobileGLServer.so` 的陈旧 `MOBILEGL_IPC_SERVER_PATH`,
那是 P6 真实的失败模式。**终局是一次策略翻转,不是一次重新设计。**
**门(三条 red-once,各证伪一件不同的事)**:
(1) 在临时树里交换某个 payload 结构里两个同宽成员 → 布局摘要变化而 git stamp **不变**(这是今天的指纹想红都红不了的门);
(2) `wireMajor = 99` → 两端具名拒绝,server exit 0 不留 core,client 不回落 monolith;
(3) 两份**仅 git stamp 不同**的对端在 `Dial != Fork` 下连接成功——**正控制**,没有它,拆分可以被报成"已落地"而 stamp 比较悄悄活着。
### 5.3 `dl`(计划已欠、记错科目)——device-lost 闩锁 + fail 收口
写 §2.2 那个闩锁:会话作用域,**从 `Doorbell::Dead()` 置位,永不从超时置位**;
此后 GL 入口 no-op、`eglSwapBuffers` 返回 `EGL_FALSE` + `EGL_CONTEXT_LOST`、
`glGetGraphicsResetStatus` 返回 `GL_UNKNOWN_CONTEXT_RESET`。首次产出 `FatalCode::ServerCrashed` / `DeviceLost`。
解析 `MOBILEGL_IPC_RESPAWN` 与 `IDLE_EXIT_S`,`RESPAWN=1` 在有阶段实现重推之前是具名拒绝。
加上 §2.3 的 `Session::Fail` 收口与 CI 普查门。
**门**:S2 重新瞄准——`kill -9` server → client 必须闩锁(不挂起、不继续提交、不 abort)。
red-once:**改成从超时置位闩锁,则配对的正控制"server 落后一帧是正常态"必须变红**。
这一对正是 §5.2 要求的"慢 vs 死"判别,也是这里**唯一能证伪错误实现、而不只是证伪缺失**的门。
### 5.4 三个已有包各加一条
| 包 | 加什么 |
|---|---|
| `a6` | 第 5 项从"读"改成**真链一次**(加 CMake target,试着不链 `MG_Impl`,失败就记下真实的未定义符号表)。新增:(6) 按 Role / Dial / SplitActive 分类 238 个站点,**先做 `MG_State` 那 8 个**;(7) 普查从对端字节可达的 abort 站点(`MG_Remote` 下 92 个),它决定 `dl` 的收口是改名还是重设计;(8) 枚举 `m_windowHandle` 的**每一处**写入(§2.5)与每一个**没有分片走查**的记录类型 |
| `cp` | 窗口种类改**白名单**(§2.1);`SurfaceReply.defaultFb` 标 `(deprecated)`——编码器硬写 0、解码器从不读,P5f/fc 已裁定 `kEventSurfaceChanged` 是唯一载体,现在烧掉这个槽以免它被复活成第二真相源 |
| `sm` | `MG_Config::Role{Monolith, Client, Server}` × `Dial{No, Fork, Connect}` 两根正交轴 + 三个具名谓词(`MGPipeSplitActive()` / `MGPipeServerArm()` / `MGPipeShouldDial()`;**注意 `MGPipeSplitActive` 与既有的 `MGPipeRoleSplitActive()` 命名冲突,后者先改名**,且三者都必须 `inline constexpr` 可折叠否则 G1 红)。反递归变成**结构性的 `Dial = No`**,而不是强置 Monolith——后者会选中 frontend glue,正是 `CONTRACT` §3.1 点名的陷阱;envp 剔除留作第二道独立保险,S3 照样证伪它。`ClientSession::Start` 拆出 `Connect(ITransport&)`:client 不再在自己线程上调 `ServerSessionInstance().Accept()`(`ClientSession.cpp:534-539`)、不再按引用 attach(`:599-600`)、不再在失败路径上关 server 单例(`:758`)、不再拿 `Welcome` 去跟本地 server 对象对账(`:577`,对任何真对端都是空检查——四个 `SegmentRef` 本来就在线上,改成**每条车道都只对线上值校验**)。`MGPipeServerArm()` 写成 `Role == Server \|\| (Role == Client && ServerLoop::OnApplyThread())`:第二个析取项是迁移路径,独立 server 出现的那天第一项点亮、线程判据处处答否,于是它是**可证的死代码**而不仅仅是没人用 |
> `MGPipeRunAheadCapBitsFor(...)` 与 `ConsumedSubsystemsFor(ActiveBackendType)` 要挪到 server 侧。
> 注意一个被多处写错的细节:**CallMask 的能力位那一半已经读 server 自己的 backend 了**
> (`MG_Backend/Init.cpp:184-211` 读 `loop.Backend()->GetBackendFunctions()`,代码里带了理由)。只有那两个函数需要搬。
### 5.5 `t6` 的出口门加三条负控制、两个必测数字
- **S6**:对端宣告 `LinkTerms.dataPlane = StreamOnly` → 具名拒绝,且 **server 活下来继续服务下一个连接**。
- **S7**:在 `Session::Fail` 之外新增一个裸 abort → 普查门变红。
- **S8**:`integration-spawn` 全程断言 `SessionFaultCount() == 0`——**好路径**上的断言,
正是 E1 / ID-122 想要而表达不出来的那种,而且 S5 / S6 能证伪它。
- **两个数字**(都可在现有 inproc 车道上取,不需要第二个进程):
`SEG_STAGE` 字节/帧与**分片后**的记录数/帧;以及 server 自己的 `PipeStats` 经控制通道回传——
`PipeStats.h:255-263` 自己写明 spawn 下 client 打印 `srv=0 srvpark=0`,
**一个意思是"在另一个进程里"的零,长得和一个意思是"从没等过"的零一模一样**,正是本项目到处禁止的那种假绿。
- 第 8 项(socket 门铃相对 condvar 的代价)的措辞要能**接受一个负向回归而不显得可疑**:
今天是 ~1600 次自旋/帧打在一条共享 cache line 上(99.8% 靠自旋解决),
socket 链路没有那条 line,但把它换成 ~4 次阻塞读/帧**可能更便宜**。
---
## 6 终局在路线图上的落点
终局不是一个阶段,是四件能力,家不一样。
| | 阶段 | 内容 | 顺序约束 |
|---|---|---|---|
| 1 | **P6(修订)** | 十个包,仍然可交付,仍然只落离屏 | — |
| 2 | **P6.5(新)stream 数据面** | `StreamLink` 作为 `ILink` 的第二实现:分块封帧、watermark 变消息(**规则逐字保留**——late-never-early、`>=` 不是 `==`、倒退是 Fatal,只有机制变)、reply 变成自带 seq 的真消息(于是 **删掉** slot 池、`seq & mask` 取模、stamp 自检与 `ReplySlot.h:56-62` 的 P9 漂移风险)、`SEG_STAGE` 的编码器局部分配器变成发送窗口、**每方向一条专用读线程**(否则 event-ring 的流控死锁以 write/write 互阻的形式原样复活) | **必须在 P11 之前**,且**约束 P9 的机制选择** |
| 3 | **P7 不变** | 定宽 payload 重写是**跨架构**前提,不是跨进程前提;`hs` 让它变成一个协商出来的 `wireForm` 值而不是新机器。**2026-09-22 改判:跨架构现在就是终局,定宽 + 布局摘要归 P6.5 wf,P7 不再持有它** | — |
| 4 | **P9 重定范围** | 从"`SEG_REPLY` 异步 slot 池"改成"**异步 reply 语义**",机制推到 P6.5 之后 | 否则 P9 造一个阶段的工作量给 stream 删掉 |
| 5 | **P11 加一条具名拒绝** | `SEG_ADOPT` 的档位是**只对共享映射成立**的;在非 shm 链路上点名 T0/T1 是具名拒绝并强制 T2 | 在 P6.5 之后 |
| 6 | **P12 修订,而且更小** | **删掉 Surface 传递那一半**(Java `Surface` → Messenger/AIDL → `MobileGLServerService` → `ANativeWindow_fromSurface`,以及那 ~15–25 MB 的额外 ART)。server app 拥有自己的 SurfaceView,client 根本没有窗口,**于是 minSdk 26 没有扁平化 `ANativeWindow` 的 NDK API——那条逼得 P5–P11 必须无窗口的约束——彻底不再成立,因为什么都不需要被扁平化。** 新增:一个追加的 `WindowKind::ServerOwned`、两个后端各一条 `CreateEGLWindowSurface` 臂、**第二个 `m_windowHandle` 注入点**(§2.5;a6 精确化为:`ActivateEGLSurface` 每次 make-current 用 `surfaceState->Window` 覆写 server 的窗口,最小 latch 点在 `RegisterEGLWindowSurface` `BackendObject.cpp:232-238`,另需改 `sameHandle` 去重键 `BackendObject_DirectGLES.cpp:962-967`)、DirectGLES 进程全局 `g_Display`/`g_Surface`/`g_Context` 的去全局化、以及 DirectGLES 的 `kEventSurfaceChanged` Width/Height 缺口(Espryt 只发布格式,`DirectGLES.cpp:15793-15805`,server 拥有显示时 client 的默认 FBO 会永远停在 `MG_Impl/Init.cpp` 的 512×512 占位值)。**Magma 近得多**:它的旋转 / preTransform 补偿本来就整个在 server 侧、按 server 自己的 surface 取键 | 形态 C 需要 P6.5 + Ph;**形态 B 两个都不需要** |
| 7 | **Ph(新)不可信 guest 加固** | 当前路线图里**没有家**。内容:把 `Session::Fail` 的**策略**翻成每会话闩锁(P6 收口站点,Ph 改它们做什么——注意这需要在每个站点发明真实返回路径,不是一个函数翻一下);handle slot 预算(guest 能跨 13 个 kind 各逼一次 `1<<20` 条目的 vector resize,而 `BackendSlotTable` 的两条拒绝是 `MOBILEGL_ASSERT`,release 下展开为空,留下一个**静默的 null twin**);readback 尺寸门挪到 server 侧、且在 resize **之前**加绝对上限;`StagedTextureStore` 的 resize 按该 level 宣告的范围设界;`ArchiveVector` 按 `count * sizeof(element)` 设上限;`SEG_EVENT` 溢出策略不能再是"一个仅仅在自己门铃上 park 的 guest 60 秒内杀掉宿主渲染器" | **必须在任何出货的 server app 接受一个不是它自己产生的 guest 的连接之前** |
**顺序小结**:`P6 → {P7 ∥ P6.5 ∥ Ph}`;P6.5 在 P11 与 P9 机制选择之前;Ph 在任何出货 server 之前。**2026-09-22**:IPC 跑道改为 `P6 → P6.5 → Ph → P12 → P9 → P10 → P11(同机臂)`,P7 与 P3b/P4b 在 monolith 跑道上并行。
---
## 7 本周就能做、而且最省钱的三件事
### 7.1 发布 `SEG_STAGE` 字节/帧与**分片后**记录数/帧(数小时,零新代码)
计数器已经接好了(`stage-buffer`、`stage-texture`、`stage-ubo-global`、`stage-ubo-named`、
`stage-vertex-client`、`stage-index-client`、`stage-indirect-cmd`、`pmap`、`csob-blob`)。
跑一个 MC VD12 窗口和一个 VD32 窗口,`MOBILEGL_PIPE_STATS_PERIOD=1`,发布按字节类分的那一行。
**为什么是最值钱的一小时**:`MEASUREMENTS.md:342` 明文写着分片后的逐 blob 分布**不存在**,
而 `SEG_STAGE` 大约是链路预算里其余部分的 **10 倍**——它是决定 stream 链路到底可不可行的**那一个**数字。
今天这一类唯一实测到的量是 364.7 KB/帧的持久映射流量,而 `ARCHITECTURE.md:477` **预算**是 ~1 MB/帧,
整个"26 还是 64 MB/s 可行性"的论证就卡在这两个数中间。
记录数/帧同样从没直接量过(只量过 draw/帧与头字节/帧),而它是"stream 链路的反向通道每帧要 ~1 条消息还是 ~900 条"的分母——
也就是 **P6.5 到底是一次传输练习还是一次协议重设计**。
它还可以**在 `c6` 写下第一行之前**取到,而它的答案会改变 `c6` 对 `LinkTerms` 默认窗口尺寸的写法。
### 7.2 `a6` 的链接实验(同一周)
加上 `MobileGLServer` target,试着不链 `MG_Impl` 链一次。这是 P6 本来就欠的一个硬是非题,
三个阶段的断言都没有解决它,而它的答案同时决定 **guest 侧产物能做到多小**。
### 7.3 `AF_VSOCK` 探针(一小时,设备上)
> **撤回(2026-09-22)**:终局改为 TCP 跨机;对应的测量变成 TCP loopback 与跨机臂上的 RTT / 吞吐(`ROADMAP.md` 开放问题 19)。
从一个普通 app 调 `socket(AF_VSOCK)`,抓 avc denial;量一次 guest↔host 的 1 MiB 吞吐与单向延迟。
§3.1 那条外部约束不做这个探针就不能当前提。
> **明确不要先做**的一件事:几份草案提议在门铃 notify 路径上加一个合成延迟做链路扫描。
> `SessionConsumer::ApplyOne` **每应用一条记录**就调一次 `NotifyClient()`(`SessionRings.h:465`),
> 所以 1 ms 的延迟在 VD12 上是 ~850 ms/帧,臂根本跑不完;而且 `CondVarDoorbell::Notify` 取的是
> `Park` 持有的同一把 mutex,那个 sleep 是在**串行化等待者**而不是在延迟它。
> 要扫延迟,只放在 present-credit 返回路径上(一个站点,结构上每帧一次),或者在双进程 socketpair 彩排上正经做。
---
## 8 仍然未决(不要当成已答)
1. ~~**[外部未验证]** 普通 Android app 能否成为 pVM 的 vsock 对端(§3.1)。它不改结构,改的是终局能承诺什么。~~ 撤回(2026-09-22,终局改为 TCP)。
2. ~~**[外部未验证]** pKVM 下手机上的 `AF_VSOCK` 吞吐与单向延迟。~~ 撤回(2026-09-22;换成 TCP 跨机的 RTT 与吞吐,`ROADMAP.md` 开放问题 19)。树里、文档里、脚本里对 vsock / AVF / virtio / gfxstream / virgl
**零引用**(已 grep);公开数字都是服务器级 x86 的 iperf3,这个配置下的延迟数字**根本不存在**。
3. server image 能否不链 `MG_Impl`。`PipeInputs.h:281/:744/:957` 与 `P5F-WIRE-COMPLETENESS` §1.4 断言它,
`SPAWN-PLAN:77` 与 `CONTRACT` §3.3 挂着它。读不出来,只能试链(§7.2)。
4. **`appliedSeq` 能否懒传输。** 这**不是**一个开放问题,是一条 P6.5 必须**修正案**处理的已关闭契约行:
`Ring.h:71-77` 写的是"批处理只能让 watermark 变**晚**。除 `appliedSeq` 外的五个都可以懒发布",
`Ring.h:52-58` 称它是"P5 **禁止**批处理的那一个 watermark"。
几份设计拿 late-never-early 去论证懒传输合法;那条规则**明文把 `appliedSeq` 排除在外**。
真正未决的是修正它的**代价**:修正案需要一条 flush-on-idle 规则(apply 线程将要 park 时、以及 post 一次 reply 时 flush),
否则一帧里最后一条 `kWaitReply` 记录会把两端一起挂住;而且它把合并间隔压到全部 14 条 `kWaitReply` 加 10 条 `kWaitApplied` 行的延迟上。
在 P6.5 的契约里用一条**专门挑起"帧内最后一条记录"情形**的 red-once 定它,不要靠推理。
5. 缝的热路径代价。`ILink::ReserveRecord` 在 VD32 下是 ~850–3550 次/帧的虚调用,
而客户端线程已经是 ~94% 占用(16.7 ms 帧里 15.72 ms)。预期在噪声里,**但 `lk` 的门要量,不要论证**。
具体风险是 `RingProducer::Reserve` 失去向编码器的内联。若不在噪声里,答案是把 reserve/commit 对做成
link 自有 arena 上的非虚函数,而不是加一个开关。
6. **并发多 guest**:一 guest 一进程,还是会话索引式解析器?`ServerSession` 在四个独立的点上是单会话
(泄漏的函数静态单例;`g_sessionOwner` 的 CAS,第二个取用者是 `Fatal{RoleViolation}`;四个 `gMGPipeCallbacks`
进程全局;`gMGPipeSegmentResolver` 一个每进程非原子 inline 变量,拒绝第二次安装);
DirectGLES 额外是**每进程单 surface**。`Close()` 写得可重入,顺序重连接近可达,并发不可达。
**显式决定它,而不是等第二个 guest 连上来的时候发现它**——注意 Android 上第二个进程很便宜、
而且免费换来崩溃隔离,这在这里比省内存重要得多;但数量被 manifest 卡死,因为 `android:process` 名是静态的。
7. `MOBILEGL_IPC_PRESENT_CREDIT > 1` 似乎**从没量过**。`ARCHITECTURE.md:475` 给了 1..8 的量程和一段反对调高的论证,
同时说只有实测吞吐收益能抵掉延迟代价——这暗示那个测量没做。**在一条带延迟的链路上它是首要杠杆**,
而它的代价因此未知;叠加 Magma 的 `Present` 本来就在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧。
在现有 inproc 臂上取它很便宜。
8. 哪些记录类型仍然没有分片走查。在 stream 链路上,一条超出发送窗口的未分片记录是 `Fatal{RingOverrun}` 而不是一次拆分,
所以那份枚举是选 P6.5 默认窗口尺寸的**前提**,不是后续。`a6` 第 8 项产出它。
---
## 9 `c6` 要改的契约措辞
1. §1 的 ABI 指纹行 → `hs`(§5.2)。
2. §5.1 停止声称 device-lost 闩锁已实现(§2.2)。
3. §7 停止声称 "a transport swap and nothing else"——**这句话在本次重审之后不再为真**,
§7 要把四项新增列出来,而不是把它们夹带过去。
4. Rule G 第二句改成传输中立:
*"Descriptors cross only by a mechanism the transport declares it supports, and where the transport declares none
they do not cross at all and the bytes are transferred instead; a transport may not invent a third answer."*
(当前措辞把 `SCM_RIGHTS` 硬编进了一条通用规则。)
5. 新增两条规则:**Rule H——一个 client 命名的窗口永不过线,永久**(不是"直到 P12");
以及一条对 `Ring.h:71-77` 的显式**修正案登记**:`appliedSeq` 按名被排除在懒发布之外,
任何未来的 stream 链路**修正**这条规则而不是绕着它推理。P6 不改行为,它只是让这个问题不必三个阶段之后再凭记忆重吵一遍。
6. 声明 `ILink`、`StreamLink`(未实现,每个方法是具名拒绝 `Fatal{UnmigratedDataPlane, "StreamLink@P6.5"}`)与 `LinkTerms`。
**门**:声明而未实现的 `StreamLink` 必须能对着 `ILink` **编译通过**——
这是"缝能不能说出一条流需要的东西(progress 传输、窗口字节、flush)"的廉价充分性证明,一行实现都不用写。
red-once:删掉 `StreamLink` 声明需要的任意一个 `ILink` 方法,构建变红。