diff --git a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md index 23822d3a..66d5c271 100644 --- a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md +++ b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md @@ -13,7 +13,7 @@ | **P5d** `inproc` 性能专项 | **已收官(2026-09-18,三轮)** | `cb06538c`、`56a77348`、`1f8de61b`;报告 [`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md);`MEASUREMENTS.md` §9 | | **P5e** 退役 Espryt draw path 的 lockstep | **已收官(2026-09-19)**,附一条具名未决(E1 对照,ID-122) | 契约 `MobileGL/MG_Remote/CONTRACT-P5E.md`;计划 `~/w7/notes/p5e/BRIEF-P5E.md`、裁定 `~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`(**ID-80..136**);**十二个包已全部落地合并**(§2.7),`kMGPipeP5eRunAheadReady` 与 `kMGPipeP5eClientWaitRuleLanded` 均已翻。strict 车道硬绿 179/179、`integration-gpu` 1357/1357、三个构建 flavour 全绿;设备上 VD32 已与 monolith 齐平。报告 [`P5E-RUNAHEAD.md`](P5E-RUNAHEAD.md),未完成项见 §5 | | **P5f** 一切状态上 wire | **已收官(2026-09-20)** | f0 / f1 / fc / fe / fm / fs / fr / fv 及分类收口均已完成;零 BARRIER_PULLED、两份 marker 空表、逐帧 rsp=0。主机全门、Claude 异族终审两项修复及 red-once、Redmi 六 clean-boot 臂通过。见 [`close-report`](notes/p5f/close-report.md)、[`close-review`](notes/p5f/close-review.md)、[`device-report`](notes/p5f/device-report.md) | -| **P6** spawn transport | **前提已解除,尚未实施** | 计划 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md)、草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md);下一步 a6 核验进程/链接边界,再 c6 和 spawn 包。传输原语与 P5f 值控制帧、静态世代/角色隔离已在树上;待做的是进程装配、socket 传输、握手/EOF/device-lost 等,不再重做已落地的控制帧与静态量 | +| **P6** spawn transport | **a6 已收官(2026-09-21)**,c6 开工中 | 计划 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md)、草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md);a6 见 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md) 与 [`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)。传输原语与 P5f 值控制帧、静态世代/角色隔离已在树上;待做的是进程装配、socket 传输、握手/EOF/device-lost 等,不再重做已落地的控制帧与静态量 | ## 2. P5f 最终验收 @@ -326,7 +326,7 @@ p50 181(约 1.10 倍)——那一次不可配对,只能作为方向性提 ## 6. 下一步 -1. **P6 a6 尚未执行**:按 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md) 核验 P5f 的 wire-only 结论在独立进程装配中成立,重点是角色链接闭包、段映射/句柄传递与生命周期边界;控制帧、静态世代与对象字段不重新施工。 +1. ~~**P6 a6 尚未执行**~~ **a6 已收官(2026-09-21)**,产出与十四项 c6 决定清单见 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md) §7。原计划文字::按 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md) 核验 P5f 的 wire-only 结论在独立进程装配中成立,重点是角色链接闭包、段映射/句柄传递与生命周期边界;控制帧、静态世代与对象字段不重新施工。 2. a6 后推进 **c6 契约与 spawn 实现**:SocketTransport、ServerMain、握手、EOF / device-lost 及子进程身份门。`integration-spawn` 应与现有 split 同名,且日志证明真正在子进程执行;这些代码和验收尚未实施。 3. P7/P8/P9 的 buffer、格式/placeholder、client arrays、大 blob、异步回读等功能债继续按阶段推进;79 trace 全集重新普查后再引用首阻塞,P5b 历史清单不当作当前结果。 4. **保留 P5e E1 历史债(ID-122)**与 VD12 面板上限、线程放置/负载平衡等测量问题。P5f 设备正确性通过不改写 P5e 的性能结论,也不声称解决上述性能问题。 diff --git a/docs/Disaggregated/P6-ENDSTATE-REVIEW.md b/docs/Disaggregated/P6-ENDSTATE-REVIEW.md new file mode 100644 index 00000000..54d66ae6 --- /dev/null +++ b/docs/Disaggregated/P6-ENDSTATE-REVIEW.md @@ -0,0 +1,393 @@ +# 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) 为准。** + +--- + +## 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`,交给 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 | + +**这是本次审查最令人愉快的一条结论:形态 B 的终局,从"P6 + 修订后的 P12"就能到达,不需要 P6.5。** +同内核意味着 `AF_UNIX` + memfd + `SCM_RIGHTS` 全部照旧。 +Winlator 在自己 app 内跑 VirGL / Vortek 渲染服务,是"终局减去 VM"的一个**已出货存在性证明**。 + +### 3.1 [外部未验证] 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 了** +> (`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` 值而不是新机器 | — | +| 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 之前。 + +--- + +## 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` 探针(一小时,设备上) + +从一个普通 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)。它不改结构,改的是终局能承诺什么。 +2. **[外部未验证]** pKVM 下手机上的 `AF_VSOCK` 吞吐与单向延迟。树里、文档里、脚本里对 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` 方法,构建变红。 diff --git a/docs/Disaggregated/ROADMAP.md b/docs/Disaggregated/ROADMAP.md index 10742ca2..5b5a6f01 100644 --- a/docs/Disaggregated/ROADMAP.md +++ b/docs/Disaggregated/ROADMAP.md @@ -32,7 +32,7 @@ | **P5d** `inproc` 性能专项 | ✅ `cb06538c`、`56a77348`、`1f8de61b` | 一二轮:persistent-map 推送三级臂(mprotect 脏页追踪 + 哈希抑制 + 绑定过滤 `PushDrawConsumers`)、barrier 批处理 `MOBILEGL_IPC_BATCH_WAITS` + 延期销毁队列、自旋时钟 64 合 1、零页注册 + shadow 页对齐分配。三轮(按符号化 profile 逐项切,四包 + 一份 lockstep 可行性研究):apply 空转轮询零锁(控制邮箱原子影子位)、`Doorbell::Wait` 校准自旋稳态不读时钟、wait/park 计数进 stats;`OnApplyThread()` 内联为线程指针比较、三个 scope 深度计数器退掉 `thread_local`;shadow 分配尺寸补页、追踪范围向外对齐、边块哈希消失、脏页排空修复;image unit 高水位、残余填充 memo、server stamp 常量表;`generate_mipmap` 不再免等(apply 读残余输入)、death 测试围栏 | 三轮设备(Redmi,CPU 定频):inproc **64.3 → 103-106 fps**(p50),client 线程 CPU 15.0 → 9.2 ms/帧、apply 12.4 → 7.0-7.4;monolith 同场景 115(120 Hz vsync 封顶的运行)/ 206(一次未封顶,渲染线程 5.0 ms/帧 大核);配对比较以逐线程 CPU/帧为主,见报告"数字"节;unit 2203/2203、`integration-split` 111/111(两条曾 flaky 的用例复跑 6/6);双生成器、include 闭包、dirty-surface、doc 引用绿。未完成:R-1 序列化(研究结论:留给 P3b/P4b 后的 P11 `gPipeInputs` 版本化)、线程放置(内核忽略亲和)、`SetHashSuppressor` 哈希等小项。报告:[`P5D-INPROC-PERFORMANCE.md`](P5D-INPROC-PERFORMANCE.md) | P5b | | **P5e** 退役 Espryt draw path 的 lockstep | ✅ **已收官(2026-09-19)**,附一条具名未决:E1 对照需按 ID-114 重新定义 | 契约 `MG_Remote/CONTRACT-P5E.md`(规则 F:**未设障**记录的 apply 不得读任何 client 内存;ID-84 限定它只约束未设障记录);裁定 **ID-80..136**(`~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`)。**十二个包,三波**:wave 1 **c0e**(契约 + 线上行:`PipeCalls.def` 第五列 `WaitClass`、`set_program_bindings` opcode 80、`kCapRunAheadApply` 位 10 仅 DirectGLES 臂、`kDrawClientArrays`、子系统位 13、两个旋钮)→ **id**(registry 按 `{slot, gen}` 重键、by-handle resolver、分配器守卫);wave 2 **vi / sb / pg / tx2 / fb** 五个 per-draw 家族 ∥ **ra**(等待规则、present credit、`gPipeInputs` 变 server 角色内存),加 **fix1**(设备发现的 monolith 臂回归,ID-107);wave 3 **pa**(program 家族离开前端)、**mv**(multi-draw + 五个档位的门禁条目)、**gl**(门禁机制本身 + 两处潜伏崩溃)、**ra2**(翻开关后暴露的竞态)。**`kMGPipeP5eRunAheadReady` 与 `kMGPipeP5eClientWaitRuleLanded` 均已翻**。最大的那次迁移(逐 draw 的 `GetProgramForDraw`)**没有改动任何线上结构**:记录早就带着逐 link 的 `ProgramArchive`,缺的是仍去问前端对象的读者。报告 [`P5E-RUNAHEAD.md`](P5E-RUNAHEAD.md) | 出口门已逐条跑过(BRIEF-P5E §3,2026-09-19 在集成树上,run-ahead 已武装):**item 1** unit 在 strict 两臂均 **2256/2256**;**item 2/3** `integration-split` **179/179**、逐条普查零 `Fatal{`、`Admitted{` 恰为八行;**item 4(a)** `RUN_AHEAD=0` 作为配对对照 **179/179 绿**;**item 4(b)** `VERB_BARRIER=0` **变红**(车道 62/179);**item 4(c)** Magma 逐字记下 `run-ahead requested, server does not publish kCapRunAheadApply - running lockstep`,而 Espryt 对照记下 `run-ahead ARMED`;**item 5** 在集成树上重跑 red-once(ID-121 重排后的形式):倒掉 pa 的臂得 `GetProgramForDraw@DrawArrays`、倒掉 mv 的臂得 `GetBoundVertexArray@DrawArrays`,**恰好各一个具名对**,还原后各自转绿;**item 7** `MOBILEGL_IPC_AUDIT=1` **179/179**。**唯一未过的是 E1 对照脚本本身**(ID-122):已修掉它两个遮蔽性缺陷(选中了一个在本车道永远跑不起来的阳性对照用例;ctest 与日志助手对“谁被选中”意见不一致),但它索要的 `Fatal{BarrierViolation}` 在整次运行里 **出现 0 次**:`BATCH_WAITS` 默认值 1 下那个重叠不发生,而 run-ahead 武装后 `VERB_BARRIER=0` 还会顺手关掉 run-ahead(ID-114),它产生的那条臂**既不是 lockstep 也不是 run-ahead**。E1 需要重新定义它要证伪什么,不是再调阈值;在想清楚之前不动它的计数规则,否则只是换一种伪绿。G1 仍由 CI 断言(ID-123) | P5d | | **P5f** 一切状态上 wire | ✅ **已收官(2026-09-20)**;验收代码 Windows `c42577a4` / WSL `cfca93c7885f` | f0 普查、f1 双块、fc 控制帧、fe Espryt 字段、fm Magma record consumers、fs 静态世代/角色及 liveness、fr registry、fv 反向回调均已合并。字段归属 **41 RECORD_SUPPLIED / 6 APPLIER_DERIVED / 0 BARRIER_PULLED / 16 FATAL**;strict / dualblock 预期 marker 集合均空。控制帧与静态量工作已完成,不再列为 P6 待实现项 | unit **2310 = 2300 PASS + 10 skip**;GPU **1377** 零失败;ordinary / strict split **180 = 176 PASS + 4 skip**;双块 **212 = 206 PASS + 6 精确具名 skip**;双后端 RSP 门 **2 PASS / 0 skip**、逐帧 rsp=0。G1 `.text` 10806051、symbols 27815 恒等且 0/0/0/0;G2 3016 同名;G14 3646→3744 无删名。逐包与收官修复均有 red-once;Claude 异族终审 P1/P2 已修;Redmi 六 clean-boot 臂 **60 PASS / 6 skip / 0 failed**、36 个窗口 rsp=0。详见 [`close-report`](notes/p5f/close-report.md) | -| **P6** spawn transport | **P5f 前提已解除,尚未实施**;a6 / c6 待开始 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch。消费 P5f 已落地的值控制帧与 server-owned 状态,不重做 EGL 帧化或静态世代隔离。依赖序见 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md):**a6**(核验进程装配/链接边界)→ **c6**(契约)→ **so / sm / cp / st / t6**。契约草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md)。边界仍为 pbuffer / surfaceless / 离屏;真窗口到达归 P12。需区分不等待的 client 与已死亡的 server,不以超时阈值代替 device-lost 判定 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99;`integration-spawn` 与 `integration-split` 名集合一致;每条私有日志证明子进程 pid 与 `transport=spawn`;socket 门铃代价实际测量,性能只记录 | +| **P6** spawn transport | **a6 已收官(2026-09-21)**,c6 开工中 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch。消费 P5f 已落地的值控制帧与 server-owned 状态,不重做 EGL 帧化或静态世代隔离。依赖序见 [`P6-SPAWN-PLAN.md`](P6-SPAWN-PLAN.md):**a6**(核验进程装配/链接边界)→ **c6**(契约)→ **so / sm / cp / st / t6**。契约草稿 [`P6-CONTRACT-DRAFT.md`](P6-CONTRACT-DRAFT.md)。**a6 产出**:[`a6-audit-v1.md`](notes/p6/a6-audit-v1.md)(198 行,七项各带对抗复审)、[`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)(**server 不能不链 `MG_Impl`**:184 个符号,债主 P7 101 / P3b-P4b 14 / 共有 60 / P6 自己 6);终局重审见 [`P6-ENDSTATE-REVIEW.md`](P6-ENDSTATE-REVIEW.md)。a6 推翻计划四处陈述:`spawn` 今天不是可观察状态、`cp` 不止传输、device-lost 闩锁不存在、"server 不链 MG_Impl" 为假。边界仍为 pbuffer / surfaceless / 离屏;真窗口到达归 P12。需区分不等待的 client 与已死亡的 server,不以超时阈值代替 device-lost 判定 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99;`integration-spawn` 与 `integration-split` 名集合一致;每条私有日志证明子进程 pid 与 `transport=spawn`;socket 门铃代价实际测量,性能只记录 | | **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 的 monolith 清理(transport 原生臂已由 P5f fs 落地);回读 / pack state;P4a 记下的 R-3 / R-4 / R-5 / R-7 / R-10 / R-11;`ProgramArtifacts.h` 的 NDK 尺寸钉 | 纹理 / program 场景;CTS `texture_*` / `shader_image_*` / `packed_pixels` 在 0.5 pp 内;每一个 Iris trace;`TextureUploadShapeScenario` 升级成门 | | **P7** DirectVulkan(Magma)全量迁移 | 部分已提前落地,阶段未收官 | `38919d45` 已补实际游戏的 server buffer consumers 和 Android 旋转 blit([报告](notes/p5f/magma-inproc-fix.md));`194382c9` 又完成 Magma run-ahead、GPU 延迟退休与 VAO 身份碰撞修复([报告](notes/p5f/magma-runahead.md));其余工作:其余子系统:`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化;具名 UBO host payload(D-B8);内部 shader 烘焙;`VertexInputStateFactory` 内容寻址 CSO;D18 容器纪律保留 | 集成 + trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;`nm -D libMobileGLServer.so \| grep glslang` 为空;CTS 0.5 pp 内。**再基线检查点:中点完成子系统 < 40% 立即重定基线** | | **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 待排 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,逐站点 reconcile);`Server/IndexHostMirror`;CopyImage 镜像搬到 client;viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;大 blob carrier(开放问题 11);无 present fence tick;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同;trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` fixture;`ClientArrayAfterComputeWriteScenario`;`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes` 逐用例发布 | diff --git a/docs/Disaggregated/notes/p6/a6-audit-rows.md b/docs/Disaggregated/notes/p6/a6-audit-rows.md new file mode 100644 index 00000000..ef5e69b2 --- /dev/null +++ b/docs/Disaggregated/notes/p6/a6-audit-rows.md @@ -0,0 +1,336 @@ +# a6 审计 — 全部行 +> 头 `b3b9c2ee`。七项各由一个审计 agent 产出、一个对抗复审 agent 逐条重新解析 `file:line`。 +> `复审` 列:空 = 复审确认;其余 = 复审的异议与更正,**以复审为准**。 +> 汇总与结论见 [`a6-audit-v1.md`](a6-audit-v1.md);第 5 项链接实验见 [`a6-link-experiment.md`](a6-link-experiment.md)。 + +--- + +## a6.1 裸指针 / 进程局部句柄 +**小结**:I opened every site below at b3b9c2ee. The honest headline is that the *byte-carrying* families are closed: `MGHostSpan::Ptr` is written non-null nowhere in the tree (the single producer, WireTables.cpp:603-607, writes nullptr explicitly), every `MGPBlobRef` that could hold a host address is caught at the ENCODER by CheckBlobIsHonest whose BlobSlotsFor table covers all eleven blobref members in MGPipeTypes.h, the only blobref resolve outside the codec (ResourceTracker.h:598-618) is a bounds-checked read of the client's own SEG_EVENT mapping, the reverse producers copy payload into SEG_EVENT rather than shipping pointers, MapPersistent declines by ruling, and the sixteen FATAL PipeInputs rows make every server read of a frontend pointer a named abort. What is NOT closed is *identity*: EGL display/surface/context still cross as bit-cast host pointers (ServerLoop.cpp:838-846 `TokenFromHandle`/`HandleFromToken`, consumed at :908-946), `nativeToken` is an `ANativeWindow*`/HWND as an integer, and the wire path that would refuse them (`ServerApplyWireSurfaceOp`) has **no production caller at all** - the live inproc path never reaches it. The single thing c6 most needs from this item is the answer to its own PENDING question: forcing `MG_Config::Transport = Monolith` in the child is **not** safe, because 151 sites in MG_Backend alone (208 tree-wide) select their server-role arm on `Transport != Monolith`, and flipping them selects frontend glue that does not exist in a second process. The anti-recursion control must be a separate flag from the role selector. Second-most important: `MGPipeServerBlockNoteIdentity()` is gated on the `MOBILEGL_IPC_ROLE_SPLIT_STATE` knob (default false), so under spawn with the knob off the server block's `ContextIdentity()` stays nullptr and DirectGLES' fb-slot memo reads that as a cache HIT against its own nullptr initialiser - an unnamed null-slot crash where the comment at PipeInputs.h:878-884 says a named one was intended. Third: the server cannot link without MG_Impl today - `PipeInputs::IsLive()` and the seven F-class forwarders live in MG_Impl/Pipe/PipeFill.cpp:2135-2230, and both role-refusal helpers live in MG_Impl/Pipe/SlotAllocator.cpp:29-57, so dropping MG_Impl from the server image drops the guards with it. + +复审结论:**sound-with-fixes**(重新解析 25 条,异议 13 条,另指出 6 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A1-1 | already-handled | MGHostSpan::Ptr - the one pointer field on the wire. Verified: NO site in the tree ever assigns it a non-null value. | MobileGL/MG_Pipe/MGPipeHostSpan.h:32 (struct MGHostSpan); MobileGL/MG_Remote/Client/WireTables.cpp:603-607 (Wire_Escape_SetProgramBindings - the only producer, writes `entry.Name.Ptr = nullptr` explicitly); MobileGL/MG_Impl/Pipe/ProgramEmit.h:336 (`MGPProgramStorageOverride entry{};` - the monolith producer value-initialises it) | Nothing. There is no non-null Ptr to break. I grepped every `.Ptr =` and `Ptr = ` in the tree outside MG_Test: the only assignment is the nullptr one. | CheckHostSpanIsHonest arm 1, PipeWireCodec.cpp:527-533, Fatal{ProtocolCorruption,"host-span"} on Ptr != nullptr - run at | a6 | | +| A1-2 | already-handled | Host spans ARE emitted today - exactly one family. The audit asked; the answer is yes, not zero. MGPProgramStorageOverride::Name is an MGHostSpan and set_program_bindings (opcode 80) emits one per storage-block override. | MobileGL/MG_Pipe/MGPipeTypes.h:1062-1067 (`MGHostSpan Name; // the block name, NUL included, staged whole`); MobileGL/MG_Pipe/PipeCalls.def:340 (`SetProgramBindings ... kVarTail/kHostSpan`); MobileGL/MG_Remote/Client/WireTables.cpp:589-607 (stages each name, fills the span); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1946-1975 (decoder resolves each) | Nothing in this row by itself - the span is already fully wire-shaped. It matters because it falsifies the standing claim that P5 emits zero host spans, so every host-span guard is LIVE code with real traffic rather than latent P8 code. | CheckHostSpanIsHonest(span, segments) at the decoder, PipeWireCodec.cpp:1954; plus the NUL-termination check at :1955-19 | a6 | | +| A1-3 | p6-must-handle | The ENCODER's blanket host-span honesty pass does not cover set_program_bindings' spans. It requires `layout.TailCount == 2 && layout.SecondTailIsHostSpans`; set_program_bindings has THREE tails and SecondTailIsHostSpans is deliberately false, so the only check on those spans runs at the decoder. | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1144-1155 (the encoder pass and its two conditions); :717-722 (`// NOT SecondTailIsHostSpans: the spans are MEMBERS of the third tail's elements`); :1946-1954 (the decoder-side per-element check that does fire) | A malformed override span is refused in the SERVER process. The client sees the peer die and takes the device-lost latch; the operator gets a server-side Fatal naming a record the client produced, and the producing call stack is in the other process. It is a diagnosis loss, not a correctness loss. | partial - decoder-only (PipeWireCodec.cpp:1954). No encoder-side arm. | c6 | | +| A1-4 | no-action | kDrawHasUserIndices is set by NO emitter in the tree, so MGPipeHostBytes - the only consumer of gMGPipeSegmentResolver - is unreachable today. | MobileGL/MG_Pipe/MGPipeHostSpan.h:50-56 (MGPipeHostBytes); MobileGL/MG_Remote/Server/PipeApplier.cpp:743 (the ONE caller); MobileGL/MG_Remote/Client/EmitTables.cpp:1916-1918 and :411 ("NO kDrawHasUserIndices: the reduced path draws from a VBO"); MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h:51 (clears the bit) | Nothing, and it would still be correct if it did fire: gMGPipeSegmentResolver is installed in the SERVER process by ServerSession::Accept, so a span resolved there names the server's own mapping of SEG_STAGE. This is the family that is genuinely finished. | CheckHostSpanIsHonest arm 4 (segment-range, PipeWireCodec.cpp:543-563) plus PipeApplier.cpp:745-750's null-resolve Fatal | P8 | | +| A1-5 | already-handled | MGPBlobRef carrying a raw host address: the monolith emitters still write one into Blob.Offset, and the split emitters overwrite it. The encoder catches any that is not overwritten. | MobileGL/MG_Impl/Pipe/ProgramEmit.h:227 (`Blob.Offset = reinterpret_cast(program->GetUBOData())`), :434, :438; MobileGL/MG_Impl/Pipe/TextureEmit.h:1458-1459, :1511, :1529; MobileGL/MG_Impl/Pipe/VertexInputEmit.h:409-411 (Seg=None with a NON-ZERO Size); guard at MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:488-500 and the encoder pass at :1122-1127 | Nothing new. The failure mode is identical to inproc and already aborts on the producing side, which is the process that can name the call site. | CheckBlobIsHonest, PipeWireCodec.cpp:459-512, called for every blob slot of every emitted record at :1123-1127 | a6 | **line-does-not-resolve** — Re-cite (a) as VertexInputEmit.h:410-412 and (b) as PipeWireCodec.cpp:456-475 for the two named arms (and note :495-505 as a THIRD arm - 'a content blob that names any segment but SEG_STAGE' - which t | +| A1-6 | later-stage | BlobSlotsFor is the coverage of the encoder's blob guard. I enumerated every MGPBlobRef member in MGPipeTypes.h (11: MGPCaps x2, MGPRenderStateDesc, MGPDynamicState, MGPVertexElements, MGPSamplerDesc, MGPProgramDesc Spirv[6]+Reflection, MGPGlobalConstants, MGPResidualValueState, MGPSubData, MGPStorageBlockBinding) against the table. Coverage is complete. | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:303-330 (BlobSlotsFor); MobileGL/MG_Pipe/MGPipeTypes.h:174,175,476,501,531,541,636,1096,1195,1292,1797 (every blobref member) | Nothing today. The standing hazard is append-only growth: a new record type with a blobref that nobody adds to this switch gets no encoder check, and under spawn the resulting Fatal lands in the peer process. | none for the table itself - there is no static_assert or generator tie between MGPipeTypes.h's blob members and this swi | P7 | | +| A1-7 | already-handled | MGPBlobRef resolved to an address OUTSIDE the codec: exactly one site in the tree - the client's writeback consumer resolving kSegEvent through ClientSession::Segments(). | MobileGL/MG_Impl/Pipe/ResourceTracker.h:598-618 (the kSegEvent arm, `session->Segments().Resolve(kSegEvent, bytes.Offset, bytes.Size)`); producer side MobileGL/MG_Remote/Client/ClientSession.cpp:285-300 (`blob.Offset = events.OffsetInSegment(bytes)`); the Seg==None arm's transport refusal at ResourceTracker.h:583-592 | Nothing, PROVIDED the spawn client maps SEG_EVENT and installs it at ClientSession.cpp:663-665 exactly as AttachInProcess does today. If the adopt path maps only three of the four segments, Resolve answers nullptr and the site aborts by name - which is the correct failure, not a silent one. | ResourceTracker.h:583-592 (Seg==None under transport) and :607-616 (out-of-segment run); both Fatal{ProtocolCorruption} | so | **line-does-not-resolve** — Re-cite as ClientSession.cpp:654-662 (all four Installs; SEG_EVENT at :660-662). Verdict already-handled / phase so unchanged and correct - the precondition 'spawn's adopt must map all four slots' is | +| A1-8 | already-handled | gMGPipeSegmentResolver - one plain non-atomic inline variable per process, installed by the SERVER role only, via a file-static table pointer. | MobileGL/MG_Pipe/MGPipeHostSpan.h:47; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:348-355 (g_processResolverTable + ProcessResolverThunk), :428-449 (Install/Uninstall); installed at MobileGL/MG_Remote/Server/ServerSession.cpp:566, uninstalled at :665; the client explicitly does NOT install (ClientSession.cpp:645-665) | Nothing - it becomes trivially correct. In the server process the server installs it; in the client process it stays nullptr and nothing calls MGPipeHostBytes. The double-install Fatal loses its reason to exist but stays harmless. Note for c6: PipeCatalogueTest.cpp:1122 asserts it is nullptr, which stays true in a client process and stays false in a server one - the test's home moves. | WireProtocolFatal at PipeWireCodec.cpp:434-438 | a6 | | +| A1-9 | already-handled | gMGPipeCallbacks - ten function pointers at namespace scope. Four are installed by ServerSession::Accept as PRODUCERS; the client's installer is monolith-gated; six entries have no caller at all. | MobileGL/MG_Pipe/MGPipeCallbacks.h:61 (the global); MobileGL/MG_Remote/Server/ServerSession.cpp:575-580 (installs OnGlError/OnBufferWriteback/OnGpuWritten/OnSurfaceChanged), :666-678 (releases only its own); MobileGL/MG_Impl/Pipe/ResourceTracker.h:679-695 (client installer) gated at MobileGL/MG_Impl/Pipe/PipeFill.cpp:866-869 | Nothing. Each process gets its own copy of the inline variable: the server's holds its four producers, the client's stays all-null and its consumers are called BY NAME from DrainEventRing (ClientSession.cpp:266-300), never through the table. What the producers hand the wire is a COPY: ServerOnBufferWriteback memcpys the backend's mapped bytes into SEG_EVENT (ServerSession.cpp:299-303) and ServerOnSurfaceChanged copies MGPSurfaceInfo field by field (:332-341). No host pointer reaches the ring. | ServerSession.cpp:356-366 and ResourceTracker.h:680-690, both Fatal{RoleViolation}; plus MGPipeAnnounceBufferGpuWritten' | a6 | | +| A1-10 | blocks-p6 | EGLDisplay / EGLSurface / EGLContext cross as bit-cast host pointers. TokenFromHandle is `reinterpret_cast` and HandleFromToken is its inverse; the frame's display/surface/readSurface/context are those bits. | MobileGL/MG_Remote/Server/ServerLoop.cpp:838-846 (TokenFromHandle / HandleFromToken); posted at :1063-1066, :1078-1082, :1107-1119, :1123-1128, :1139-1143; consumed at :908-909, :915, :923, :928, :943-946; field declarations at MobileGL/MG_Remote/Server/SurfaceControlFrame.h:72-76 | HandleFromToken(frame.display) hands the server's eglInitialize a pointer from the CLIENT process's EGL. There is no check: it is a reinterpret_cast into a driver call. Best case EGL_BAD_DISPLAY, worst case an address that happens to be mapped in the server. Every one of the five ops above is affected. | none. The only range check on the frame is WindowBackendFromFrameValue (ServerLoop.cpp:872-882), which validates the bac | cp | **line-does-not-resolve** — Re-cite as ServerLoop.cpp:839-848 (the pair), SurfaceControlFrame.h:75-78 (the four token fields), ServerLoop.cpp:864-873 (WindowBackendFromFrameValue). Verdict blocks-p6 / phase cp unchanged and corr | +| A1-11 | blocks-p6 | SurfaceControlFrame::nativeToken is `ANativeWindow*` / HWND / XID as an integer. | MobileGL/MG_Remote/Server/SurfaceControlFrame.h:78 (`Uint64 nativeToken = 0; // HWND / XID / ANativeWindow* as an integer`); packed at MobileGL/MG_Remote/Server/ServerLoop.cpp:864-869 (PackWindowHandle -> TokenFromHandle(handle.Handle)); unpacked at :876-881 into a WindowHandle handed to CreateEGLWindowSurface (:915) and SetWindowHandle; source type MobileGL/MG_Backend/BackendObject.h:579-584 (`void* Handle`) | A window surface created from a client-process ANativeWindow* in the server process. The named refusal for exactly this exists - SurfaceWireError::AndroidNativeWindowArrived -> Fatal{UnmigratedSurface,"AndroidNativeWindow@P12"} - but it lives on the wire decode path, which nothing calls (see A1-12). On the live frame path there is no refusal at all. | exists but unreachable: MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:157-166 (decode) and :197-215 (the Fatal) | cp | **line-does-not-resolve** — Re-cite as: SurfaceControlFrame.h:83 (the field); ServerLoop.cpp:853-858 (PackWindowHandle, the pack at :855); :875-882 (UnpackWindowHandle, the unpack at :878); :915 (handed to CreateEGLWindowSurface | +| A1-12 | p6-must-handle | The wire surface-op codec - the half that carries every named process-local refusal - has NO production caller. `ServerApplyWireSurfaceOp` and `EncodeSurfaceOpFrame` are referenced only by their own header and by MG_Test. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:116 (EncodeSurfaceOpFrame), :197 (ServerApplyWireSurfaceOp); the live path is MobileGL/MG_Remote/Server/ServerLoop.cpp:658-670 (RunSurfaceControlFrame) -> :886-960 (ApplySurfaceControlFrame), which never touches the codec | Not a break - an unarmed gate. Every rule-G refusal in this family (AndroidNativeWindow@P12, MetalLayer@P12, InprocOnlyOpOnTheWire, UnknownWindowKind) can only go red once cp routes the frame through the socket. A spawn lane that kept the by-value path would satisfy the exit gate while never once running the code that refuses a process-local window. | n/a - the guards ARE the unreached code | cp | | +| A1-13 | blocks-p6 | The client attaches to the server's shared memory by holding a C++ reference to the server's SessionSegments object, not by receiving descriptors. | MobileGL/MG_Remote/Client/ClientSession.cpp:533 (`Server::ServerSession& server = Server::ServerSessionInstance();`), :575-586 (Welcome cross-check reads `server.Shm().AnnouncedSize(slot)` directly), :599-601 (`m_shm.AttachInProcess(server.Shm(), MemoryRole::Client)`), :704 (`Server::ServerLoopInstance().Start(server)`); MobileGL/MG_Remote/Transport/SessionRings.h:160 (AttachInProcess signature); MobileGL/MG_Remote/Transport/ShmSegment.cpp:311-352 (dup + Adopt + Map off `owner.m_segments[index]->Fd()`) | There is no `server` object in the client process. Three separate things break: the Welcome geometry cross-check has nothing to compare against, AttachInProcess has no owner, and ServerLoopInstance().Start(server) is starting the other process's apply thread. SessionSegments has no constructor that takes received descriptors - only Create and AttachInProcess. | none - it is a compile-time coupling, so it fails to build rather than failing silently. That is the safe direction. | so | **line-does-not-resolve** — Fix :533 -> :534 and add the teardown sites to the row's `site` column: ClientSession.cpp:758, :802, :814 and MG_Backend/Init.cpp:255. ARCHITECTURE.md:504 already rules that under spawn the client's d | +| A1-14 | blocks-p6 | Segment descriptors never cross: ShareFd / ReceiveFd have no production caller, SegmentRef carries no fd, and on Android the segment name is not openable. | MobileGL/MG_Remote/Transport/ITransport.h:98,107 (the two virtuals); MobileGL/MG_Remote/Transport/InProcessTransport.cpp:187-226 (a real dup-based implementation, unused); MobileGL/MG_Remote/Protocol/protocol.fbs:48-53 (SegmentRef = id/kind/sizeBytes/name - no fd field); MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:62-65 (ASharedMemory_create - the label is not a resolvable name); MobileGL/MG_Remote/Transport/ShmSegment.h:22-24 says so | On Android the four segments cannot reach the child at all. ASharedMemory gives an anonymous fd; SegmentRef::name is a debug label; there is no shm_open path to it. SCM_RIGHTS is the only route and nothing calls it outside MG_Test. | none - the primitives are built and unit-tested (FdPassing.cpp:173, MG_Test/Wire/FdPassingTest.cpp) but unwired | so | | +| A1-15 | p6-must-handle | No pid crosses. Hello::pid is hardcoded 0 and Welcome::serverPid is filled from the CLIENT's Hello, so it is always 0. | MobileGL/MG_Remote/Client/ClientSession.cpp:519-521 (`/*pid=*/0u`); MobileGL/MG_Remote/Server/ServerSession.cpp:532-534 (`CreateWelcome(builder, abiMajor, abiMinor, static_cast(hello->pid()), ...)` - the fourth argument is serverPid); MobileGL/MG_Remote/Protocol/protocol.fbs:64 (Hello.pid), :80 (Welcome.serverPid) | Two things. (a) P6-SPAWN-PLAN §6.5's arm-proof gate wants each test's private log to carry the child pid plus `transport=spawn`; serverPid is the natural carrier and it currently echoes the peer's value, so a spawn lane could log `serverPid=0` and still look armed. (b) The process-tree gate (§6.4) counts children externally; a serverPid the client can assert against is the in-band version of the same check. This is a pid crossing that SHOULD exist and does not, rather than one that must be removed. | none - no consumer, so a wrong value is unobservable | sm | | +| A1-16 | blocks-p6 | ClientSession owns BOTH transport ends as concrete `unique_ptr` and reads the doorbell pair off the concrete type. The server's mirror of the same access is guarded; the client's is not. | MobileGL/MG_Remote/Client/ClientSession.h:357-358 (both members typed InProcessTransport); MobileGL/MG_Remote/Client/ClientSession.cpp:622-623 and :632 (`m_clientTransport->PeerDoorbell()` / `SelfDoorbell()`); the guarded server equivalent at MobileGL/MG_Remote/Server/ServerSession.cpp:709-740 (role check then Fatal{UnmigratedVerb,"ServerSession::ConsumerDoorbell"} for any non-InProcess role) | A SocketTransport cannot be stored in either member, and StartOverTransportPair's signature takes two InProcessTransport unique_ptrs. The client also cannot legitimately own the server end at all. It is a compile-time break, which is the honest direction - but note the asymmetry: ServerSession already has the named-Fatal shape for an unmigrated transport role and ClientSession has no equivalent. | partial - ClientSession.cpp:482-490 refuses non-inproc modes by name at Start, which is what keeps this from being reach | so | **line-does-not-resolve** — Re-cite the members as ClientSession.h:355-356. Verdict blocks-p6 / phase so unchanged and correct. | +| A1-17 | blocks-p6 | InitSplitRoles builds BOTH roles in the one process: the server's private BackendObject, its CallMask, its CapsSnapshot source, then the client session, then the client's BackendObject_Remote. | MobileGL/MG_Backend/Init.cpp:148-224 (the whole function); specifically :155-158 (`loop.CreateBackend(...)` - the server's backend, on the app thread), :162 (`ServerSessionInstance()`), :209-210 (`ClientSessionInstance().Start(...)`), :221 (`pActiveBackendObject = MakeUnique()`) | Steps 1 and 2 - backend creation, the consumer-mask honesty cross-check (AssertConsumerMaskIsHonest, :117-138) and the capability-bit derivation off `loop.Backend()->GetBackendFunctions()` (:183-205) - all have to run in the CHILD, after exec, before the Welcome. `g_resourceOpsAtStep2` (:145) is a pointer compared against MGPipeGetResourceOps() later, which only means anything inside one process. | the caller refuses to continue on false rather than falling back to monolith (the named refusal at :210-216) - so a fail | sm | **line-does-not-resolve** — Re-cite as: Init.cpp:151-230 (InitSplitRoles); :157-158 (loop.CreateBackend on the app thread); :119-139 (AssertConsumerMaskIsHonest); :146 (g_resourceOpsAtStep2); :163 (ServerSessionInstance); :184-2 | +| A1-18 | already-handled | gPipeInputs' O-class members are raw frontend pointers and SharedPtrs. All nine reachable getters are classified FATAL, and a server read under a stamped verb aborts by name. | MobileGL/MG_Backend/MGPipe/PipeInputs.h:808-818 (SharedPtr, BindingSlot*[15], BindingSlotRange1D*[15], BindingSlot*[2], ImageTextureBinding*, three SharedPtr, TextureUnit*); classification at MobileGL/MG_Pipe/FieldOwnership.def:28-43; enforcement at MobileGL/MG_Backend/MGPipe/PipeInputs.h:96-105 (MGP_INPUT_CHECK) and MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:382-414 (MGPipeInputUnfreshRead) | Nothing, and the failure direction is right: in a server process gPipeInputs is filled by nobody, every field is withdrawn, and any MGB_CTX read of any class aborts with the field and verb named. This is the family P5f genuinely closed. The exception is A1-20. | MGP_INPUT_CHECK / MGPipeInputUnfreshRead, Fatal{UnmigratedPipeInput} and Fatal{RoleViolation}; plus MGPipeStickyForwardP | a6 | | +| A1-19 | blocks-p6 | The dual-block selection and the server block's identity are gated on a KNOB (MOBILEGL_IPC_ROLE_SPLIT_STATE, default false), not on the transport. Under spawn the rehearsal is not optional - it is the reality - and the knob-off path has no server block identity. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (`MGPipeRoleSplitActive() = Ipc.RoleSplitState && Transport != Monolith`), :297-299 (MGPipeClientInputs), :314-315 (`MGPipeServerBlockNoteIdentity` returns early when the knob is off); default at MobileGL/Config.h:550 (`Bool RoleSplitState = false`); the hazard is written out at MobileGL/MG_Backend/MGPipe/PipeInputs.h:876-884 | With the knob off in a server process, nothing ever calls SetIdentity on gPipeInputs, so ContextIdentity() stays nullptr for the life of the process. DirectGLES' fb-slot memo compares identity FIRST against a nullptr-initialised static, reads a HIT, and returns `*g_fbSlotCache[target]` - a null FbBindingSlot* - WITHOUT running MGP_INPUT_CHECK, because the cache-hit path bypasses the accessor entirely. That is an unnamed null dereference in exactly the place PipeInputs.h:880-884 says a named abort was intended. It is reachable whenever MOBILEGL_PIPE_LEGACY_MEMOS is on (CMakeLists.txt:43 - ON by default) and kMGPipeSubsystemEsprytSlots (bit 5) is clear. | none on this path - the memo hit is the bypass. The Fatal it should produce lives behind a knob that spawn cannot assume | c6 | | +| A1-20 | p6-must-handle | The fb-slot memo caches raw FbBindingSlot* pointers into the frontend GLContext and is keyed on MGB_CTX_IDENTITY - a raw pointer compare with no generation. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:199-213 (GetFramebufferBindingSlotChecked: `const void* ctx = MGB_CTX_IDENTITY; if (ctx != g_fbSlotCacheContext) { ... }`), statics at :177-178; the push-arm bypass at :195-198 (`if (EsprytSlotTablesEnabled()) return MGB_CTX->GetFramebufferBindingSlot(target);`) | Two ways, both only on the legacy arm. (a) A1-19's null identity makes the very first call a false hit. (b) Even with a correct identity token, the cached pointers address the CLIENT's GLContext, which does not exist here. The comment at :180-186 already records that this path bypasses the per-verb poison stamp at all five call sites. | structural only: EsprytSlotTablesEnabled() (SlotTables.h:150-153) routes past it, and ResolveEsprytSlotTablesArm Fatals | c6 | | +| A1-21 | p6-must-handle | The frontend-keyed twin registry and slot allocator are guarded, but the predicate is `ServerLoop::OnApplyThread()` rather than "this is the server process". | MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-43 (MGPipeRefuseAllocatorFromApplyThread) and :45-57 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread), both `if (!ServerLoop::OnApplyThread()) return;`; call sites at MobileGL/MG_Backend/DirectGLES/SlotTables.h:281, :398, :410, :476, :505 and MobileGL/MG_Backend/DirectGLES/Managers.h:341, :406, :430, :436; the allocator's own three at SlotAllocator.cpp:217, :230, :239 plus the singleton accessor at :329 | They stop coinciding. Server-process code that is NOT on the apply thread - backend bring-up before Start, and any RunSurfaceControlFrame dispatch that takes the non-apply-thread branch (ServerLoop.cpp:669-670) - passes the guard. MGPipeSlots() in the server process is then a freshly constructed, EMPTY allocator (SlotAllocator.cpp:338-339): FindByLifetimeId answers the null handle and Acquire mints a slot the client knows nothing about. Both are silent wrong answers where the inproc build gives a named abort. | Fatal{RoleViolation,"MGPipeSlots"} - correct message, predicate too narrow for two processes | c6 | | +| A1-22 | blocks-p6 | The server cannot link without MG_Impl today. PipeInputs' out-of-line definitions and BOTH role-refusal helpers live in MG_Impl translation units. This is the direct answer to P6-SPAWN-PLAN §5 item 5 and P6-CONTRACT-DRAFT §3.3. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141 (PipeInputs::IsLive - what MGB_CTX_LIVE expands to, read at 20+ MG_Backend sites), :2160-2230 (the seven F-class forwarders: GetBufferBindingPointCount, GetProgramObject, GetTextureObject, HasOpenTransformFeedbackSpan, InvalidateCompileEnv, ValidateProgramName, RecordError); MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-57 (both refusal helpers) and :324-340 (MGPipeSlots); the claim it contradicts is at MobileGL/MG_Backend/MGPipe/PipeInputs.h:281-284 and docs/Disaggregated/ARCHITECTURE.md:516 (§16 lists PipeInputs.{h,cpp} under MG_Backend/MGPipe/) | A server image that drops MG_Impl loses PipeInputs::IsLive (an undefined symbol at 20+ MGB_CTX_LIVE sites) and, worse, loses MGPipeRefuseAllocatorFromApplyThread and MGPipeRefuseFrontendKeyedRegistryFromApplyThread - so the guards of A1-21 vanish along with the code they guard. The realistic P6 answer is that the server links the whole library and the separation is enforced at run time, not at link time; a6 should say so rather than leave the claim standing. | none; check_include_closure.py does not probe MG_Backend headers (SlotTables.h:80-82 says so) | sm | | +| A1-23 | blocks-p6 | MG_Config::Transport is a process global that 208 sites switch on to pick the server-role arm, 151 of them in MG_Backend. Forcing it to Monolith in the child - which ARCHITECTURE.md §15.1 instructs - selects frontend glue. | MobileGL/Config.h:564 (the idiom named); representative server-arm selectors: MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:30 and :49 (the two role refusals), MobileGL/MG_Pipe/PipeApply.cpp:2286-2291 (R-6's MapPersistent decline), MobileGL/MG_Impl/Pipe/PipeFill.cpp:2136-2138 (PipeInputs::IsLive -> MGPipeServerContextIsLive), MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:293 (MGPipeRoleSplitActive), MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:19 (`const Bool server = Transport != Monolith`), MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2212 and :2257 (the XFB key selector), MobileGL/MG_Impl/Pipe/ResourceTracker.h:585 and :704 | If the child sets Transport = Monolith to stop recursive spawning, all 151 MG_Backend selectors flip to the monolith arm: the applier stops declining MapPersistent, IsLive stops being server-owned and calls LiveContext() (a client GLContext that is not there), ContextEpoch stops treating itself as the server, and both role refusals in SlotAllocator.cpp return early - the guards go quiet in exactly the process that needs them. If instead the child leaves Transport = Spawn, ConfigLoader/Init could try to become a client again. The two controls are one variable today and must become two. | none - the recursion fuse and the role selector are the same field | c6 | **overstated** — Replace the count with: 179 `Transport != ...Monolith` comparisons tree-wide (MG_Backend 138, MG_Impl 25, MG_Pipe 6, MG_State 4, MG_Remote 4), plus 45 further `TransportMode::Monolith` mentions of whi | +| A1-24 | no-action | Server-side addresses used as map keys that never cross - verified clean, recorded so c6 does not re-open them. | MobileGL/MG_Remote/Server/StagedTextureStore.h:214-217 (KeyForTwinAddress: a server-side twin address, tagged so it cannot alias the handle key space minted at :207-210); MobileGL/MG_Remote/Server/ServerLoop.h:161-163 (CurrentThreadKey via __builtin_thread_pointer, apply-thread identity only); MobileGL/MG_Pipe/PipeApply.cpp:1677 (`BoundStreamOutputLifetimeId = values.BoundTransformFeedbackLifetimeId` - a CLIENT-minted opaque integer that arrives in the set_context_values record, used as a server map key at DirectGLES.cpp:2007, :2213, :2258) | Nothing. None of these leaves the server process, and the XFB identity is the shape P5f fs chose precisely because a lifetime id is an opaque integer rather than an address. This is a family where the honest answer is "nothing left". | KeyForTwinAddress's top-bit tag (`Uint64{1} << 63` on the handle key) makes the two key spaces non-aliasing by construct | a6 | | +| A1-25 | already-handled | MGPipeApplyMapPersistent's host-pointer return is declined by ruling under any transport, on both the monolith applier path and the codec path. | MobileGL/MG_Pipe/PipeApply.cpp:2256-2302, specifically :2285-2292 (`if (Transport != Monolith && AdoptTierIsEmulate()) return nullptr;`); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1616-1638 (the codec arm never calls the applier and posts kStatusDeclined) | Nothing. The pointer that would be meaningless across a process boundary is never produced. Note for c6: the record payload is a bare MGPHandleOnly and carries neither size nor seedBytes, so the phase that lands a real map_persistent needs an append-only payload change before it can even call the applier (:1618-1622). | the two decline arms above; AdoptTierIsEmulate aborts by name at T0/T1 rather than returning | P11 | | + +**复审另行指出的遗漏**: +- MobileGL/MG_Remote/Client/WireTables.cpp:87 - `Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }`. This is A1-21's exact defect (an apply-thread predicate standing in for 'this process is the server') on a far larger and more consequential surface: it is the selector between the WIRE EMITTER and the MONOLITH ADAPTER for every routed row. Consulted at WireTables.cpp:156 (MGP_WIRE_SIMPLE), :166 (MGP_WIRE_BLOB), :180 (MGP_WIRE_TAIL), :256, :276, :295, :315, :328 and :576 (the five hand-written escapes including Wire_Escape_SetProgramBindings). In a spawn SERVER process any routed call that is not on mgl-srv-apply takes the CLIENT arm and reaches RequireSession (WireTables.cpp:106-119), i.e. Fatal{NoClientSession} - loud, but for the wrong reason and in the wrong process. The audit's c6_ask #4 names only the two SlotAllocator helpers; this site must be in the same ruling. +- MobileGL/MG_Impl/Pipe/PipeFill.cpp:358, :976, :1115, :1206, :1256, :3598 and MobileGL/MG_Remote/Client/GpuWritePending.cpp:227 - six MG_Impl call sites and one MG_Remote one that select their role arm through RunsAsTheServerRole(), i.e. through ServerLoop::OnApplyThread(). Same ask as above, and they are additionally a second MG_Impl -> MG_Remote::Server link that A1-22's build-layout row does not count. +- MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:535 (`PersistentMapTracker::OnServerRole()`), MobileGL/MG_Remote/Client/ClientSession.cpp:1357 and :1376, MobileGL/MG_Pipe/PipeApply.cpp:1355, MobileGL/MG_Backend/DirectGLES/Managers.cpp:230 and :2868, MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:56 and :116 - eight further production sites where ServerLoop::OnApplyThread() IS the role predicate. The MG_State pair is notable on its own: the frontend state layer calls into MG_Remote::Server to ask which role it is in. +- MobileGL/MG_Remote/Client/ClientSession.cpp:758, :802, :814 and MobileGL/MG_Backend/Init.cpp:255 - the TEARDOWN half of A1-13. The client calls ServerSessionInstance().Close() twice and ServerLoopInstance().Stop() once, and MG_Backend::Init's shutdown calls ServerLoopInstance().Stop() unconditionally. Under spawn none of these can reach the other process; ARCHITECTURE.md:504 already rules that the server exits on EOF instead. A1-13 covers only bring-up and its natural_carrier column says nothing about shutdown. +- MobileGL/MG_Impl/Pipe/PipeFill.cpp:348 - `GLContext* LiveContext() { return MG_State::pGLContext.get(); }`. A1-22's link-closure argument is right (I confirmed there are exactly 8 out-of-line `PipeInputs::` member definitions in PipeFill.cpp: IsLive at :2135 and the seven forwarders at :2160, :2178, :2184, :2190, :2202, :2217, :2223), but it names only MG_Impl. Those eight bodies all resolve through LiveContext(), so the transitive dependency is MG_Impl AND MG_State::pGLContext, and MG_State itself calls back into MG_Remote::Server (MipmapStorage.cpp:56, :116). The c6 ruling on 'does the server image link MG_Impl' is really 'does it link MG_Impl + MG_State'. +- MobileGL/MG_Backend/MGPipe/PipeInputs.h:743-745 and :956-958 - two more statements of the 'MG_Impl is not in the server' premise that A1-22 and doc_drift bullet 3 do not name (they cite only :281-284). :744 says 'MGPipeFillAccess lives in MG_Impl, which is the role the server does not have'; :957 says 'NOT enough for a spawned server, where MG_Impl is not in the process'. All three have to move together with whatever c6 rules. + +--- + +## a6.2 进程级静态 +**小结**:I re-read the fs-era mechanisms in the tree rather than in the reports, and they hold: `ContextEpoch` (native generation + served serial + execution arm) really does gate both the unpack shadow and the render-state shadow; the raw-depth sampler really is a pure native object rebuilt on native generation; XFB really is role-split and keyed on `BoundStreamOutputLifetimeId`; server liveness really is held by `g_serverContextLive` and driven only from make-current / release / destroy / stop. None of those needs redoing. The delta is elsewhere, and it has one shape: **almost every one of these keys, guards and lifecycles is selected by `MG_Config::Transport != Monolith` (155 sites under `MG_Backend` alone) or by the presence of a `ClientSession` in this process — two facts that stop meaning what they mean today the moment the roles are in two processes.** Concretely: `MGPipeApplierReset`'s layer-2 role guard (PipeApply.cpp:1353-1355) is disarmed in a server-only process because it requires `ClientSession::Active() != nullptr`; `MGPipeServerBlockNoteIdentity` (PipeInputs.cpp:314-315) is a no-op unless `MOBILEGL_IPC_ROLE_SPLIT_STATE=1`, which is OFF by default, so under spawn the server block's `ContextIdentity()` is nullptr with nothing left to fill it; `CountBarrierPull`'s unconditional Fatal arm (PipeInputs.cpp:174) is likewise knob-gated; and `PipeInputs::IsLive()` — the very answer fs re-owned — is **defined in `MG_Impl/Pipe/PipeFill.cpp:2135`**, which answers §3.3's `PENDING a6` question "can the server link without MG_Impl" with a flat no. The new exit boundary adds two owners that do not exist: nothing in a server-only process starts or stops `ServerLoop` (`ClientSession.cpp:704`/`:802` own that today, and the comment at :702 says so), and nothing owns the resolver/callback uninstall order that `ClientSession::Stop` currently sequences. The one thing c6 most needs from this: **write down, as a contract row, which single process-wide fact each of these arms actually keys on under spawn, because `Transport != Monolith` is true in BOTH processes and `ClientSession::Active()` is true in neither of the two the server lives in.** + +复审结论:**sound-with-fixes**(重新解析 25 条,异议 9 条,另指出 7 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A2-1 | blocks-p6 | `ContextEpoch::Server` — the execution-arm third of the epoch key — is `MG_Config::Transport != Monolith`, a process-global config value, not a role fact | MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:18-21 (CurrentContextEpoch); consumers MobileGL/MG_Backend/DirectGLES/Managers.cpp:6497 (ScopedDefaultUnpackState::EnsureShadowSynced) and MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5044-5049 (RenderStateImpl::SyncRenderState) | Nothing, IF the server child keeps `Transport != Monolith`. If ARCHITECTURE.md:488's rule is implemented literally (`mobilegl_server_main` hard-sets `MG_Config::Transport = Monolith` before `Init()`), the epoch's arm bit flips to `false` in the server process and, with it, 155 other `MG_Config::Transport` reads under MG_Backend select the frontend/monolith arm inside the server. | none at the epoch itself; the epoch merely notices the flip and re-publishes once. The damage is in the other 155 sites, | c6 | **overstated** — Say '155 `MG_Config::Transport` reads under MG_Backend, 151 of them the Monolith comparison'. In c6_ask #1, add one sentence: MG_Impl (35) and MG_Remote (23) are deliberately NOT in the re-key set bec | +| A2-2 | already-handled | `ScopedDefaultUnpackState::s_synced` + the six `s_*` pixel-store shadows + `s_epoch`, class-level `static inline` in a header-less .cpp scope | MobileGL/MG_Backend/DirectGLES/Managers.cpp:6472-6549 (class), :6495-6519 (EnsureShadowSynced), :6539 (s_synced), :6541 (s_epoch) | nothing. Under spawn only the server process ever constructs this scope; the native generation and served serial are both server-owned numbers. The only way to break it is A2-1's arm flip. | `s_epoch != epoch` (Managers.cpp:6498) | st | | +| A2-3 | already-handled | render-state shadow family (`g_forceFullRenderStateResync`, `g_hasSyncedRenderState`, `g_syncedRenderStateVersion`, `g_syncedBackendViewport/ScissorBox`, `g_syncedColorMaskAlphaWidenMask`, `g_dualSourceDeclinedBlendStates`) plus the function-local `static ContextEpoch syncedEpoch` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5021-5049 (statics + the epoch gate), :5036-5041 (InvalidateSyncedRenderState), :16209 (the MakeCurrent invalidation that predates fs) | nothing. Both keys are server-owned. `MGB_CTX->GetRenderStateParametersVersion()` at :5054 reads the SERVER block of gPipeInputs, which the applier writes. | `syncedEpoch != epoch` (DirectGLES.cpp:5046) | st | **line-does-not-resolve** — Widen the cite to DirectGLES.cpp:5002-5049. Verdict already-handled is correct. | +| A2-4 | already-handled | raw-depth-fetch sampler: the transport arm's function-local `static UniquePtr native` + `static Uint generation`, and the LEGACY arm's two file statics that still hold `SharedPtr` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:313-344 (GetRawDepthFetchSampler), :316-321 (the transport statics and the generation rebuild), :82-83 (the legacy MG_State SharedPtr pair); destructor gate at MobileGL/MG_Backend/DirectGLES/Managers.cpp:14690-14708 | nothing on the transport arm. But it is one of the concrete things A2-1 costs: a server child forced to `Monolith` takes the :333-343 branch and constructs an `MG_State::GLState::SamplerObject` — a frontend object — inside the server process, and calls `SyncToBackend` on it, which is the client-registry identity lookup fs deliberately removed. | `generation != g_backendContextGeneration` (DirectGLES.cpp:318) and the destructor's generation compare (Managers.cpp:14 | st | | +| A2-5 | p6-must-handle | `g_Display` / `g_Context` / `g_Surface` / `g_Config` — ONE EGL tuple per process, file-static, and `InitDisplayAndContext` unconditionally destroys the previous one before creating a new one | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:15668-15671 (the four statics), :16012-16013 (`InitDisplayAndContext` opens with `DestroyEGLContext()`), :16079-16095 (InitWindowSurface), :16097-16112 (InitPbufferSurface) | This is the hard answer to "can a server process hold more than one surface or session": **no, and the refusal is not named** — a second `InitWindowSurface`/`InitPbufferSurface` silently destroys the first surface, its context, and (see A2-6) the display. A spawn server that accepted a second session, or a client that created a pbuffer beside its window, loses the first without a log line. | none. There is no `Fatal{...}` and no `if (g_Surface != EGL_NO_SURFACE) refuse` — `DestroyEGLContext()` is simply called | c6 | | +| A2-6 | p6-must-handle | `DestroyEGLContext()` ends with `eglTerminate(g_Display)` — a PROCESS-wide EGL teardown, not a context teardown | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16730-16774, specifically :16761-16773 (`eglMakeCurrent(NO_SURFACE)`, `eglDestroyContext`, `eglDestroySurface`, `eglTerminate`, `g_Display = EGL_NO_DISPLAY`) | A spawn server that serves a second session after a `ReleaseResources` has had its `EGLDisplay` terminated; any EGL object another part of the server process still held (a Magma swapchain, a probe context) is invalidated with it. It also means the server process cannot be 'reset between clients' — `eglTerminate` + `eglInitialize` on the default display is the only path back, and nothing sequences it. | the `MGPipeServerSetContextLive(false)` at :16732-16733 and the generation bumps at :16750/:16754 make every STALE HANDL | sm | | +| A2-7 | blocks-p6 | the served-context serial's two halves: the client's `g_applierResetContextSerial` (a namespace static that is NEVER reset anywhere) and the server's `PipeApplier::m_applierResetSerial`, a member of the LEAKED `ServerSessionInstance()` singleton that `Close()` does not reset either | MobileGL/MG_Remote/Client/WireTables.cpp:642 (declaration, comment says 'the client's own count'), :656 (`record.ContextSerial = g_applierResetContextSerial++`); MobileGL/MG_Remote/Server/PipeApplier.cpp:1149-1157 (the Fatal assertion), MobileGL/MG_Remote/Server/PipeApplier.h:322 (`m_applierResetSerial = 0`), MobileGL/MG_Remote/Server/ServerSession.h:188 (`PipeApplier m_applier`), MobileGL/MG_Remote/Server/ServerSession.cpp:376-382 (the leaked singleton), :656-689 (`Close()`, which resets neither) | A RECONNECTING CLIENT (new client process, same long-lived server process) sends ContextSerial 0 while the server expects N -> `Fatal{ProtocolCorruption, "ApplierReset.ContextSerial"}` and `std::abort()` IN THE SERVER. The mirror case (same client process, respawned server after `MOBILEGL_IPC_RESPAWN=1`) sends N while the new server expects 0 -> same abort. So `MOBILEGL_IPC_RESPAWN` as specified (ARCHITECTURE.md:504, 'restart and re-push everything') cannot work without deciding what this serial means across a reconnect. | the Fatal at PipeApplier.cpp:1150 — it fires, loudly and by name. The problem is that it fires on a LEGITIMATE reconnect | c6 | | +| A2-8 | p6-must-handle | `g_sessionOwner` / `g_active` single-session enforcement: a SECOND OBJECT is a named Fatal, but a second `Accept()` on the SAME object — which is what `ServerSessionInstance()` makes the only reachable shape — returns an UNLOGGED error code | MobileGL/MG_Remote/Server/ServerSession.cpp:368 (`g_active`), :372 (`g_sessionOwner`), :421-439 (`Accept`'s CAS; `:425` is the silent `return MOBILEGL_ERR_INVALID_ARGUMENT`, `:426-428` the Fatal), :376-382 (`ServerSessionInstance()`); the one production caller is MobileGL/MG_Backend/Init.cpp:163 | P6 is the first phase in which a second client can physically arrive at a running server. It is refused — but through the `expectedOwner == this` branch, which returns `MOBILEGL_ERR_INVALID_ARGUMENT` with **no MGLOG line at all**. A ServerMain that logs only the rc gets a bare number; the project's 'always a NAMED refusal' rule is not met at exactly the point P6 makes reachable. (The other branch, a genuinely different `ServerSession*`, is correctly `Fatal{RoleViolation, "callback-double-install"}`.) | the CAS itself; but the same-object arm has no name and no log | c6 | **claim-false** — Replace with: 'the only production caller of Accept() is ClientSession.cpp:535; ServerSessionInstance() itself has four production callers (Init.cpp:163, ClientSession.cpp:534, :758, :814)'. Verdict p | +| A2-9 | p6-must-handle | `ServerSession::Close()` re-enterability, and the asymmetry between how it releases the four reverse callbacks (owner-checked) and the process segment resolver (NOT owner-checked) | MobileGL/MG_Remote/Server/ServerSession.cpp:656-689; :663 `Wire::SegmentTable::UninstallProcessResolver()` unconditional, :666-677 the four `== &ServerOnX` identity checks; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:444-449 (Uninstall nulls whoever owns it), :430-442 (Install DOES check, and Fatals) | Nothing about re-entry. What breaks is the ORDER: the resolver's own comment (:661-662 and PipeWireCodec.cpp:445-446) says 'uninstall AFTER the apply thread has joined, never before', and the only thing that sequences that today is `ClientSession::Stop()` — which calls `ServerLoopInstance().Stop()` (join) at ClientSession.cpp:802 and only then `ServerSessionInstance().Close()` at :814. In a server-only process there is no ClientSession to impose that order. See A2-17. | the ordering is a comment, not a check. `Close()` does not assert that the apply thread has joined. | sm | | +| A2-10 | already-handled | `gMGPipeSegmentResolver` + `g_processResolverTable`: one function pointer and one table pointer per process, and `MGPipeHostBytes` returns a SILENT nullptr when the resolver is unset | MobileGL/MG_Pipe/MGPipeHostSpan.h:47 (`inline MGPipeSegmentResolver gMGPipeSegmentResolver = nullptr`), :54-55 (the silent-null arm); MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:348 (`g_processResolverTable`), :350-355 (thunk), :430-449 (install/uninstall); installed at MobileGL/MG_Remote/Server/ServerSession.cpp:565, released at :663 | nothing about ownership — the split makes the ruling trivially true, since the client process has no resolver to race. The residual risk is purely the teardown window of A2-9: after `UninstallProcessResolver()` the thunk is null and any still-decoding record resolves to a silent nullptr whose escalation lives in the caller rather than here. | `InstallProcessResolver`'s Fatal (PipeWireCodec.cpp:436-438) on the install side; caller escalation on the resolve side | c6 | | +| A2-11 | later-stage | `gMGPipeCallbacks` has TEN entries; `ServerSession::Accept` installs FOUR and `Close` releases those four. The other six stay null for the life of the server process, and 'null means send nothing' is the documented default | MobileGL/MG_Pipe/MGPipeCallbacks.h:27-52 (the ten), :56-58 (`kMGPipeCallbackCount = 10` + static_assert), :61 (`inline MGPipeCallbacks gMGPipeCallbacks{}` with the 'Null-initialized: a backend that installs nothing sends nothing' comment); installs at MobileGL/MG_Remote/Server/ServerSession.cpp:575-580, releases at :666-677 | nothing new — but note the asymmetry for c6: the install/release identity check is per-entry and owner-checked, so the table survives a session cycle correctly, while the six unfilled rows will silently no-op the first time a spawn lane reaches a P8/P9 path. That is a phase boundary, not a spawn defect. | fv's named refusal on OnGlError; `InstallReverseCallback`'s `Fatal{RoleViolation, "callback-double-install"}` (ServerSes | P9 | | +| A2-12 | blocks-p6 | `MOBILEGL_IPC_ROLE_SPLIT_STATE` is OFF by default and is NOT implied by a real transport — yet spawn IS the dual-block arrangement, structurally and unavoidably | MobileGL/Config.h:540-550 (`Bool RoleSplitState = false`, and the comment calling 0 'the negative control'); MobileGL/ConfigLoader.cpp:400-406 (plain env flag, forced off under PipeVerify); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (`MGPipeRoleSplitActive() = knob && Transport != Monolith`), :297-299 (`MGPipeClientInputs`), :861 in PipeInputs.h (`gPipeInputsClientBlock`) | In the server process there is no client to fill anything, so the arrangement the knob simulates is the ONLY arrangement — but `MGPipeRoleSplitActive()` still answers FALSE unless an operator sets the env var. Three named mechanisms are therefore disarmed in the very configuration they were written for: (a) `CountBarrierPull`'s unconditional Fatal arm (PipeInputs.cpp:174-176); (b) `MGPipeServerBlockNoteIdentity` (see A2-13); (c) `MGPipeClientClearVerbBoundary` selecting the client block. A spawn lane run with the default config is a spawn lane with the role-split detector switched off. | none — the knob is the guard, and it defaults to the disarmed position | c6 | | +| A2-13 | blocks-p6 | `MGPipeServerBlockNoteIdentity()` early-returns unless the role-split knob is armed, so under spawn the server block's `m_contextIdentity` stays nullptr and `m_live` is never written by anyone | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:314-328 (`if (!MGPipeRoleSplitActive()) return;` at :315, the odd-encoded token at :324-327); the rationale is spelled out at MobileGL/MG_Backend/MGPipe/PipeInputs.h:876-885; the consumer is MobileGL/MG_Pipe/PipeInputsSwitch.h:21 (`MGB_CTX_IDENTITY`) read at MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:201-209 | In the server process nothing writes it: the client's SetIdentity is in MG_Impl and never runs there, and this function returns early. `MGB_CTX_IDENTITY` is then nullptr forever, and at DirectGLES.cpp:202 `ctx != g_fbSlotCacheContext` is FALSE on the very first call (both nullptr) so :209 dereferences a null `FbBindingSlot*` — the unnamed crash the rehearsal exists to replace with a named one. Reachable whenever `EsprytSlotTablesEnabled()` is false, i.e. on the supported `MOBILEGL_PIPE_PUSH` A/B arm with bit 5 clear (ARCHITECTURE.md §9.6). Not reachable on the default mask 0x3fff, which sets bit 5 (MG_Pipe/MGPipe.h:164) — and `MOBILEGL_PIPE_LEGACY_MEMOS` defaults ON (CMakeLists.txt:43) so the legacy arm IS compiled. | none in the default spawn config. The knob is what arms it. | c6 | **wrong-verdict** — Downgrade to p6-must-handle. Keep the c6 ask (the identity should not be knob-gated; its token is derived from MGPipeApplierContextSerial(), a server-owned clock) but state honestly that the crash at | +| A2-14 | blocks-p6 | `MGPipeApplierReset`'s layer-2 role guard is armed by `ClientSession::Active() != nullptr` — i.e. by a CLIENT existing IN THIS PROCESS | MobileGL/MG_Pipe/PipeApply.cpp:1353-1360 (`Transport != Monolith && ClientSession::Active() != nullptr && !ServerLoop::OnApplyThread()` -> `Fatal{RoleViolation, "g_applier"}`); the documented exception is at :1348-1351; the state it protects is `g_applier` at :436 | In a spawn SERVER process, `ClientSession::Active()` is permanently nullptr — so the fixture exception becomes the permanent state and the guard NEVER fires. Any direct off-apply-thread `MGPipeApplierReset()` in the server silently resets server state that the wire is supposed to own. This is a gate that can no longer go red for the reason it exists. | the Fatal itself, disarmed by its own first conjunct | c6 | | +| A2-15 | blocks-p6 | `PipeInputs::IsLive()` — the server-liveness answer fs re-owned — and 17 other `PipeInputs::` member definitions are compiled in `MG_Impl/Pipe/PipeFill.cpp`, the CLIENT-role translation unit | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141 (the definition; the transport arm at :2137-2138 forwards to `MGPipeServerContextIsLive()`), :2160-2175 and following (the seven sticky forwarders); the declaration is MobileGL/MG_Backend/MGPipe/PipeInputs.h:262; the claim it contradicts is at PipeInputs.h:281 and :957 ('MG_Impl is not in the server at all') | This is the concrete answer to P6-SPAWN-PLAN §5 / P6-CONTRACT-DRAFT §3.3's `PENDING a6` question: **the server image cannot link without MG_Impl today**, and not because of some far-off glue but because the backend's own `PipeInputs` member functions are defined there. The spawn plan's own §2.3 worry ('blindly forcing Monolith would select frontend glue') is the same fact seen from the other end. | none — it is a link-time fact, and today it always resolves | c6 | **overstated** — Replace '17 other' with 'seven further (the seven F-class sticky forwarders)' and cite PipeFill.cpp:2135, :2160, :2178, :2184, :2190, :2202, :2217, :2223. Keep the verdict blocks-p6 — eight backend-cl | +| A2-16 | p6-must-handle | `PipeInputs::IsLive()` answers `MGPipeServerContextIsLive()` for ANY non-monolith transport, and `g_serverContextLive` is only ever written from server-role code | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2137-2138; MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:305 (`namespace { Bool g_serverContextLive = false; }`), :307-310 (setter), :312 (getter); the six writers are MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16733 and MobileGL/MG_Remote/Server/ServerLoop.cpp:290, :295, :314, :475, :761, :1006 | In the spawn CLIENT process `Transport != Monolith` is also true, so any `MGB_CTX_LIVE` evaluated there would consult a global no one in that process ever sets -> permanently FALSE. I verified that every `MGB_CTX_LIVE` site is under `MG_Backend` (DirectGLES/Managers.cpp and DirectVulkan), which the client process should not be running — so this is latent rather than live, and it becomes live the moment A2-18's `InitSplitRoles` builds a DirectGLES backend in the client process. Separately: `g_serverContextLive` is a plain non-atomic `Bool` written from the apply thread (ServerLoop.cpp:475) and from whatever thread calls `Stop()` (:761). | none; it is a plain Bool with no role or thread assertion | c6 | **overstated** — Say 'seven writers'. No other change; verdict p6-must-handle stands. | +| A2-17 | blocks-p6 | `ServerLoop` — the apply thread, the server's private BackendObject, the current-tuple and the liveness clear — is STARTED AND STOPPED BY CLIENT-ROLE CODE | MobileGL/MG_Remote/Client/ClientSession.cpp:700-704 (Start; the comment at :702 reads 'Under `inproc` the client is what starts the server role, which is why this call is here rather than in some server-side main'), :802 (Stop, step 3 of table 3's order); MobileGL/MG_Backend/Init.cpp:244-255 (`ShutdownSplitRoles`); the singleton is MobileGL/MG_Remote/Server/ServerLoop.cpp:810-812; the teardown it owns is :463-478 (Detach, backend reset on the context-owning thread, `MGPipeServerSetContextLive(false)`, `ForgetCurrentTuple`) | A server-only process has no `ClientSession` and therefore no driver for any of this. Every process-level static that the teardown at ServerLoop.cpp:463-478 is responsible for clearing — the backend object, `g_serverContextLive`, the EGL tuple, and (through `~BackendObject_DirectGLES` -> `DestroyEGLContext`) `g_backendContextGeneration`, the frame-fence ring and `g_backendContextOwnerThread` — has no owner on the EOF exit path ARCHITECTURE.md:504 specifies ('client dies -> server reads EOF -> destroy the native context immediately and exit'). | `ServerLoop::Stop`'s bounded join and its `Fatal{ApplyThreadJoinTimeout}` (ServerLoop.cpp:790-799) are the right mechani | sm | | +| A2-18 | blocks-p6 | `InitSplitRoles()` builds the SERVER's BackendObject (and registers the process-wide resource-ops table) in whatever process has `Transport != Monolith`, then starts a CLIENT session in the same breath | MobileGL/MG_Backend/Init.cpp:151-229; :157-159 (`ServerLoopInstance().CreateBackend`), :163 (`ServerSessionInstance()`), :216 (`ClientSessionInstance().Start`), :228 (`pActiveBackendObject = BackendObject_Remote`); the backend construction is MobileGL/MG_Remote/Server/ServerLoop.cpp:166-191, whose :185-189 comment says `Initialize()` 'registers the resource op table' | Run unchanged in the spawn CLIENT process, this constructs a full `BackendObject_DirectGLES` there and calls `Initialize()`, which loads the driver entry points and installs `g_resourceOps` process-wide — in the process that must not have a DirectGLES backend at all. It is also the function that decides which process holds `ServerSessionInstance()` and `ServerLoopInstance()`. This is the concrete place where 'the child must not become a client' (P6-CONTRACT-DRAFT §3.1) and 'the parent must not become a server' meet, and it is the site a6.3's question lands on too. | `CreateBackend`'s `m_backend != nullptr` guard (ServerLoop.cpp:167-169) only prevents a second one in the same process | sm | | +| A2-19 | already-handled | `PipeInputs::m_serverStampedVerb` — the latch that arms the whole split read path — is cleared by the APPLIER, not only by the client, exactly as the P6 comment demands | MobileGL/MG_Backend/MGPipe/PipeInputs.h:278-288 (the comment that names the P6 hazard by name); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:330-375 (stamp), :377 (`MGPipeServerClearVerbBoundary`); the server-side caller is MobileGL/MG_Remote/Server/PipeApplier.cpp:1328 (stamp) and :1331 (`LeaveApplier` -> clear) | nothing. I checked this specifically because PipeInputs.h:281-287 predicts the failure; the fix is already in the tree. | `PipeApplier::LeaveApplier` (PipeApplier.cpp:1331) | st | | +| A2-20 | already-handled | XFB `g_roleXfbState` — a two-element array indexed by `Transport != Monolith`, per-arm native generation, server keys = `MGPipeApplier().BoundStreamOutputLifetimeId` | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1629-1656 (`RoleXfbState`, `ActiveXfbState()`; the role index at :1645, the generation reset at :1647-1650, the role-change `NeedsBind` at :1651-1653), :1659-1677 (`CurrentXfb`, the lifetime key at :1663), :2210-2245 (bind), :2280-2287 (the server's virtual-name-0 arm), :2295-2306 (`OnBackendContextDestroyed`, which clears both arms) | nothing, except through A2-1: the arm index at :1645 is the same `Transport != Monolith` expression, so a forced-monolith server child would silently switch to the GL-name-keyed map and to `scatterProgram`, i.e. to frontend objects that do not exist in that process. | `state.NativeGeneration != g_backendContextGeneration` (:1647); `g_activeXfbRole != role` (:1651) | st | | +| A2-21 | p6-must-handle | `EsprytSlotTablesEnabled()` — a function-local `static const Bool` latched ONCE per process at the first twin lookup, read off `MG_Config::Features.PipePush`, NOT off the transport | MobileGL/MG_Backend/DirectGLES/SlotTables.h:150-153 (the latch), :140 (`ResolveEsprytSlotTablesArm`); the body is MobileGL/MG_Backend/DirectGLES/Managers.cpp:431-471 (the `Fatal{PipeLegacyMemosDisabled}` NoArm stop at :454-458); the default mask is MobileGL/MG_Pipe/MGPipe.h:164 (`0x3fff`, bit 5 set) applied at MobileGL/ConfigLoader.cpp:266 | The two processes parse their own environments. ARCHITECTURE.md:488 specifies scrubbing `MOBILEGL_TRANSPORT` and all `MOBILEGL_IPC_*` from the child's envp — `MOBILEGL_PIPE_PUSH` is NOT in that set, so it inherits through envp but NOT through any value the parent set programmatically after its own parse. If the two arms disagree the client mints `{slot, gen}` handles the server's legacy address-keyed registry cannot resolve, with no gate that compares them. Note also that the `Hello`/`Welcome` fingerprint is build-only (CapsCodec.h) and carries no config. | the build fingerprint does not cover the subsystem mask; `ConfigLoader` logs it but nothing cross-checks | c6 | **line-does-not-resolve** — Cite ARCHITECTURE.md:489. Verdict p6-must-handle is right. Worth adding that MOBILEGL_PIPE_VERIFY is in the same unscrubbed class and interacts with A2-12: ConfigLoader.cpp:401-406 forces RoleSplitSta | +| A2-22 | p6-must-handle | `g_processTeardown` / `EnsureProcessTeardownSentinel()` — the at-exit sentinel every twin destructor consults before touching the driver, registered LAZILY at first registry use | MobileGL/MG_Backend/DirectGLES/Managers.cpp:188-197 (`Bool g_processTeardown`, `std::atexit` registration at :195-196); readers at Managers.cpp:217, :4783, :6068, :9895, :12173, :14691, :15031 and MobileGL/MG_Backend/DirectGLES/SlotTables.h:65; registration sites Managers.h:358, SlotTables.h:275, :355 | The server process's exit path is brand new and `sm` chooses it. If it is `_exit()` (which ARCHITECTURE.md:493's 'EOF detection exits immediately and unconditionally' points at), no atexit handler and no static destructor runs at all — which is safe but means nothing in the tree is responsible for the EGL teardown, so A2-17's owner is mandatory. If it is `exit()`/return-from-main, the lazy registration order relative to the function-local statics registered later (e.g. `GetRawDepthFetchSampler`'s `native`, DirectGLES.cpp:316) decides whether a destructor calls `glDeleteSamplers` after `eglTerminate`. I could not settle the ordering by reading alone. | `InProcessTeardown()` early-outs in seven destructors, plus the independent generation gate at Managers.cpp:14704 which | sm | **line-does-not-resolve** — Change the citation to ARCHITECTURE.md:498. Keep verdict p6-must-handle and confidence uncertain — the open question about atexit ordering is genuinely unsettled by reading, and :498 makes `_exit` the | +| A2-23 | p6-must-handle | `ClientSession::Stop()` reaches into the SERVER role's two process singletons directly, on both its arms | MobileGL/MG_Remote/Client/ClientSession.cpp:758 (`Server::ServerSessionInstance().Close()` on the failed-Start arm), :802 (`Server::ServerLoopInstance().Stop()`), :814 (`Server::ServerSessionInstance().Close()` on the started arm); the ordering argument is at :746-755 and :768-805 | In the spawn client process these calls still compile and still run. `ServerSessionInstance()` would construct a never-accepted `ServerSession` there (Close is then a near no-op), and `ServerLoopInstance().Stop()` would construct a `ServerLoop` in the client process and take its `!m_thread.joinable()` arm (ServerLoop.cpp:755-766), which writes the client process's own `g_serverContextLive = false` and `ForgetCurrentTuple()`. Harmless today, but it is two server-role singletons instantiated in the wrong process, and it is the same function that must instead send a shutdown frame over the socket. | none — the calls are unconditional on the arm | cp | | +| A2-24 | no-action | `g_backendContextGeneration` starts at 1, only ever increments, and `ContextEpoch{}`'s default is `{0, 0, false}` — so the first `CurrentContextEpoch()` in any process can never equal the default-constructed shadow key | MobileGL/MG_Backend/DirectGLES/Managers.cpp:69 (`Uint g_backendContextGeneration = 1`); the single bump is MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:16750 inside `DestroyEGLContext`; the default key is MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:11-16 | nothing. Worth recording because it is the reason `st` does not need a 'prime the shadows at server start' step: the epoch's own zero value does it. | the epoch compare itself | st | | +| A2-25 | already-handled | `StagedShadowStore` — 'ONE PER PROCESS', and its copying arm is latched at first use from `MG_Config::Transport != Monolith` | MobileGL/MG_Backend/DirectGLES/Managers.cpp:1154-1165 (the comment and `ServerStaged()`; the constructor argument at :1164 is `MG_Config::Transport != MG_Config::TransportMode::Monolith`); the drop-all at MobileGL/MG_Pipe/PipeApply.cpp:1487-1490 | nothing — it is server-private and its key is server-owned. It is a third transport-latched arm (with A2-21 and A2-1) that a forced-monolith child would flip, and since it latches at FIRST USE rather than at config parse, flipping `MG_Config::Transport` after the first staged resource would strand everything already staged. | the latch itself | st | | + +**复审另行指出的遗漏**: +- MobileGL/MG_Remote/Client/ClientSession.cpp:1300, :1397, :1402 — the FOURTH and FIFTH production consumers of MGPipeRoleSplitActive(), which A2-12 does not list. A2-12 names three mechanisms gated on the knob ((a) PipeInputs.cpp:174, (b) :315, (c) :297). The complete production set is five sites: `grep -rn MGPipeRoleSplitActive MobileGL/ | grep -v MG_Test` returns PipeInputs.cpp:174, :298, :315 AND ClientSession.cpp:1300 (`ApplyThreadIsInsideApplier`: `if (MGPipeRoleSplitActive()) return g_roleInsideApplier;`), :1397 and :1402 (`NoteApplyThreadEnteredApplier` / `NoteApplyThreadLeftApplier`: `if (!MGPipeRoleSplitActive()) g_applyThreadInsideApplier.store(...)`). The state behind them is `std::atomic g_applyThreadInsideApplier{false}` at ClientSession.cpp:119 — a process-shared atomic WRITTEN BY THE SERVER ROLE AND READ BY THE CLIENT ROLE, which is the purest instance of this item's own subject. With the knob at its default 0, a spawn client process reads an atomic nobody in that process ever writes, so it is permanently false and the R-1 mutual-exclusion Fatal it feeds (RefusePipeInputsTouchWhileApplierOwnsIt, ClientSession.cpp:1304-1394) can never go red there. Honest framing for c6: under spawn that gate SHOULD be vacuous (there is no shared gPipeInputs to race), but it is vacuous by accident of a default-off knob rather than by a role fact — which is A2-12's thesis exactly. This is not a new discovery either: docs/Disaggregated/notes/p5f/f0-statics.md §S13 already classed this scalar 【A】 and predicted it in words ('拆进程后 client 那份恒 false … 否则会漏掉「client 读自己那份永远 false」的假绿'). VERIFIED by reading. +- MobileGL/MG_Remote/Client/CapsMirror.cpp:166-171 (the leaked singleton), CapsMirror.h:95-98 ('Per client context in principle; one per process in P5'), CapsMirror.cpp:91 (`Valid()` is `m_generation != 0`), CapsMirror.cpp:55-87 (Adopt), sole adoption site ClientSession.cpp:448 — a process-scoped singleton holding the SERVER's session-scoped answers (CallMask, FormatCapabilityCache, RendererInfo, apiVersion, BackendType) that ClientSession::Stop() never resets. I checked Stop() line by line: :819-826 resets m_runAheadArmed / m_runAheadLatched / m_presentsSent under the explicit P5e argument 'the LATCH GOES DOWN WITH THE SESSION … a session that starts again must take it again from that session's own first snapshot rather than inherit a verdict about a server that is gone' — and does NOT extend that argument to the mirror the latch was derived from. `grep CapsMirror MobileGL/MG_Remote/Client/ClientSession.cpp` returns only :392, :448, :677, :840; there is no reset anywhere. This is the same shape as A2-7 on the caps side: under MOBILEGL_IPC_RESPAWN=1 (ARCHITECTURE.md:504) the reconnecting client answers R-8's only legal client-side question — CapsMirror::ServerConsumes, whose own comment at CapsMirror.cpp:126-136 says a placeholder mirror consuming nothing is 'the safe direction' and emitting to a server with no consumer is 'the one that loses uploads' — from the DEAD server's CallMask, and Valid() stays true so WarnPlaceholder never fires. The asymmetry worth naming to c6: f0-statics §S13 classed BOTH `g_applierResetContextSerial` (WireTables.cpp:642) and `CapsMirror.cpp:169` as 【B】 'client-side, per-process is fine'; a6.2 correctly reopened the first (A2-7) and did not reopen the second, although the respawn boundary is new to P6 for both. VERIFIED by reading; the consequence is inferred and depends on what MOBILEGL_IPC_RESPAWN's 're-push everything' is specified to do. +- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:312-322 (`MGPipeSlotAllocator::Reset()`) and :324-341 (`MGPipeSlots()`) — the client's {slot, gen} handle allocator is a process-lifetime singleton whose own comment at :331-338 says 'NEVER DESTROYED, deliberately (one allocation for the life of the process)', and `Reset()` has ZERO callers in the whole tree (`grep -rn 'Slots()\.Reset\|SlotAllocator.*Reset'` finds only the definition). Same family as A2-7: a process-scoped counter/space that no session boundary resets, and here the reset that exists is dead code. Under MOBILEGL_IPC_RESPAWN=1 the client keeps its entire slot and generation space across the reconnect while the fresh server's twin registries start empty, so whether the respawn works at all depends on 're-push everything' re-emitting every resource_create with the SAME client-minted handles. c6 should say which, and either wire Reset() into the respawn path or delete it so it is not mistaken for a mechanism. VERIFIED that the allocator is process-scoped and Reset() is uncalled; the respawn consequence is inferred. +- MobileGL/MG_Backend/MGPipe/PipeInputs.h:496, :515, :526 — the three `m_serverStampedVerb`-gated dual-arm getters (GetSamplingResolutionGeneration at :496, GetTextureBindGeneration at :515, GetTextureContextId at :526) each read `if (m_serverStampedVerb) return MGPipeApplier*Serial();` and otherwise fall back to a CLIENT-FILLED member (m_textureBindGeneration :517, m_textureContextId :528). This is A2-13's shape one level down and A2-13 does not cover it: in a server-only process the fallback member is never written by anyone, and PipeApplier::LeaveApplier (PipeApplier.cpp:1331) clears the stamp on every applier exit, so ANY backend read outside a stamped verb — a control-request path, InitCapabilities, a surface op, the sticky-forward exemption family PipeInputs.h:955-958 names — takes the non-stamped arm and keys off a permanently-zero value. These three answers key the whole DirectGLES context-scoped memo family: g_observedUnitBindingsContextId (DirectGLES.cpp:3179), g_unitTextureSyncListByHandleContextSerial (:3415), g_unitTextureSyncListContextId (:3423), g_fboTextureSyncListContextId (:3461), g_fboAttachmentSyncListContextSerial (:3487). Those memos are each double-keyed on native generation as well (:3419, :3425, :3462, :3488), so a stale read is bounded rather than catastrophic — which is why I rate it p6-must-handle for the contract rather than blocks-p6. VERIFIED the code shape by reading; reachability outside a stamped verb is inferred, and settling it needs the ServerMain control path sm has not written. +- MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp:55-70 and MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp:34-35 — the query and sync registries (g_queryObjectsMutex / g_liveQueryObjects / g_nextQueryId / the four active-query ids / g_activePipelineStatisticsQueryIds; g_syncObjectsMutex / g_liveSyncObjects) are per-context BY SPEC and stored per-process. This is the literal statement of the item's title and a6.2 has no row for it at all, although docs/Disaggregated/notes/p5f/f0-statics.md §S12 already recorded it as a multi-context debt. Under spawn they stay in the CLIENT process, which is the right process, and P6-CONTRACT-DRAFT §9.2 declines multi-context (it moves to P12), so the correct verdict is later-stage/P12 — but the item should CLASSIFY them rather than omit them, so a6's successor does not re-discover them. Note the audit also used file paths that no longer exist: f0-statics cites `GL_Query.cpp:55-66` and `GL_Sync.cpp:34-37` without the Query/ and Sync/ subdirectories they now live in. VERIFIED by reading. +- MobileGL/MG_Backend/Init.cpp:82 — a tree comment the audit had every reason to catch while working in this exact function and did not: it asserts 'MGPipeSetResourceOps has exactly one caller in the whole tree and it is Managers.cpp:2594'. At b3b9c2ee there are TWO callers and neither is at that line: DirectGLES/Managers.cpp:3137 and DirectVulkan/Renderer/VkBufferManager.cpp:170 (with the matching owner-checked unsets at Managers.cpp:3149-3150 and VkBufferManager.cpp:429-430). Low severity — it does not change any verdict — but it sits in the middle of InitSplitRoles' rationale block, which is the text c6 will read when splitting the function per A2-18. Two smaller instances of the same rot, both in comments the audit quotes from: PipeApplier.cpp:1160 cites 'the g_applier this role owns (PipeApply.cpp:409)' and PipeFill.cpp:822 cites 'a PROCESS-WIDE global (PipeApply.cpp:402)', while `MGPipeApplierState& g_applier` is at PipeApply.cpp:436 and `const MGPipeResourceOps* g_resourceOps` at :442. VERIFIED by reading. +- NEGATIVE RESULT worth recording so c6 does not chase it, and so the integrator knows this was checked rather than skipped: g_resourceOps (PipeApply.cpp:442) is NOT a gap. It is the most obvious candidate in this family — a process-wide table the server registers that client code reads — but c1's R-8 work already moved every client-side consumer to the caps mirror, and the code names the spawn case by name: PipeFill.cpp:819-828 ('under spawn the client process has no backend at all, so the read answers null and five record families stop emitting - silently'), with the moved arms at :834-835 (MGPipeResourceSubsystemEnabled), :843-848 (MGPipeResourceOpsHaveSubDataResident) and :1427-1431 (P4aFamilyHasItsConsumer), and the server-side honesty check at Init.cpp:119-131 (Fatal{ConsumerMaskLie}). Likewise g_clientTablesUninstalled (WireTables.cpp:104) IS session-cycle-safe: it is reset to false at WireTables.cpp:730 and :800, unlike the two serials in A2-7. Both were checked and found already-handled. + +--- + +## a6.3 gPipeInputs 角色归属与反递归 +**小结**:Today a record consumer selects its server arm with ONE predicate spelled 238 non-test times: `MG_Config::Transport != MG_Config::TransportMode::Monolith`. It is process-global, latched at `ConfigLoader::Init`, never thread-dependent. Beside it live exactly two other real predicates - `ServerLoop::OnApplyThread()` (ServerLoop.h:265, one relaxed load of `g_applyThreadKey` plus a thread-pointer compare) and `ClientSession::Active() != nullptr` - plus nine aliases/wrappers of those three. The decisive finding is that ARCHITECTURE.md:488's instruction ("`mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`") is not merely suboptimal, it is unsound at named sites I opened: it flips `ContextEpoch::Server` to false and `Served` to 0 (ContextEpoch.h:19), constructs BOTH server staged stores with `copies=false` (Managers.cpp:1164, StagedTextureStore.h:475 - latched function-statics, unrecoverable after first use), re-enables the frontend VAO walk in the server (DirectGLES.cpp:2483/2576), disarms three role Fatals (SlotAllocator.cpp:30/50, MipmapStorage.cpp:55, BufferObject.cpp:63), and - worst, because it is SILENT - makes `PipeInputs::IsLive()` (PipeFill.cpp:2137) answer from the child's null `pGLContext` instead of `MGPipeServerContextIsLive()`, turning all 58 `MGB_CTX_LIVE` guards into a quiet "no context" in a process that has one. `Dial = No` is the right structural catch, but it is NOT sufficient at one site: `Init.cpp:266` is a two-way `Transport != Monolith` whose else-arm is the monolith backend switch at `Init.cpp:305`, so a child with `Dial=No` and `Transport=Spawn` still needs a THIRD `Role == Server` arm there or it falls into exactly the trap. gPipeInputs itself is already clean: the P5f dual-block census (`Harness/dualblock-expected-fatals.txt`) is EMPTY, which is in-process proof that the server block is self-sufficient - so the one thing c6 most needs from this audit is the decision at Init.cpp:266, expressed as a three-valued Role and not as a boolean Dial. + +复审结论:**sound-with-fixes**(重新解析 27 条,异议 11 条,另指出 8 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A3-1 | blocks-p6 | `MG_Config::Transport != TransportMode::Monolith` - the dominant server-arm selector. 238 non-test occurrences (311 with tests); 67 in DirectGLES.cpp, 27 VulkanRenderer.cpp, 24 Managers.cpp, 19 PipeFill.cpp, 16 DirectVulkan.cpp, 11 PipeApply.cpp, 9 ClientSession.cpp, 8 in MG_State. | MobileGL/Config.h:455 (declaration); MobileGL/ConfigLoader.cpp:33 (definition, plain non-atomic, default Monolith); representative consumers MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2483, MobileGL/MG_Backend/DirectGLES/Managers.cpp:2867 | In the child the two facts part company. If the child forces Monolith (ARCHITECTURE.md:488), all 238 sites select FRONTEND GLUE in the one process that has no frontend. If the child keeps non-Monolith, the same 238 sites are right but the predicate now also says "dial out" at Init.cpp:266, which is the fork chain. | none - it is a plain enum load, and the pull build folds it to constexpr Monolith (Config.h:566), so no gate sees it | c6 | **overstated** — Replace 238/311 with 178 non-test predicate sites (179 incl. the integration harness; 177 excluding the Config.h:564 comment), and 208 total comparisons against Monolith counting the 30 `== Monolith` | +| A3-2 | p6-must-handle | `ServerLoop::OnApplyThread()` - the thread-identity predicate. Body is `g_applyThreadKey.load(relaxed) != 0 && key == CurrentThreadKey()`; the key is the thread pointer (TPIDR_EL0) on aarch64. | MobileGL/MG_Remote/Server/ServerLoop.h:265-271; key published at MobileGL/MG_Remote/Server/ServerLoop.cpp:344 (ApplyThreadMain) and cleared at :528 | In the server process the ROLE is the whole process. If ServerMain drives the applier on its main thread rather than through ServerLoop::Start, g_applyThreadKey stays 0 and every OnApplyThread() guard answers FALSE in the server - the guards silently stop guarding. If ServerMain does start a ServerLoop, the guards are right on that thread but still answer FALSE on the server's control-pump / socket-reader threads, which are also the server role. | none. The header argues the key is never 0 for a LIVE thread, not that the role implies a thread. | sm | | +| A3-3 | p6-must-handle | `MGPipeRoleSplitActive()` = `MG_Config::Ipc.RoleSplitState && Transport != Monolith`. Deliberately NOT latched (two global loads per verb). | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 | Nothing breaks - it becomes MOOT, which is its own problem. Under spawn the two blocks are in two address spaces whatever the knob says, so the knob can no longer change behaviour in either process, and scripts/ci/dualblock_negative_control.sh's red form (ROLE_SPLIT_STATE=0 must turn the lane red) cannot go red in a spawn lane. This is a FOURTH question - "do the two roles share one storage object" - that Role/Dial/SplitActive cannot express. | scripts/ci/dualblock_negative_control.sh - and that is exactly the gate that stops being able to fail | t6 | | +| A3-4 | already-handled | gPipeInputs storage and the dual-block selection. `gPipeInputs` is the SERVER-role block; `gPipeInputsClientBlock` exists only in a disaggregated build; `MGPipeClientInputs()` returns the client block iff MGPipeRoleSplitActive(). Both are leak-at-exit `*new PipeInputs()` because their O-class members are SharedPtrs to MG_State frontend objects. | MobileGL/MG_Backend/MGPipe/PipeInputs.h:837 (gPipeInputs), :861 (gPipeInputsClientBlock), :895 (the non-disagg fold); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:297-299 (selection) | Nothing, and that is the useful answer. In the SERVER process gPipeInputs is the only block and is wholly server-written; in the CLIENT process gPipeInputs (or the client block) is fill-written and read by NOBODY, because no backend is linked into the reading path there. The `gPipeInputsClientBlock` allocation and the selection branch become dead weight in both processes. | MG_IntegrationTest/Scenarios/DualBlockScenario.cpp asserts blocksDistinct and that a client verb boundary does not move | c6 | | +| A3-5 | already-handled | Who WRITES gPipeInputs in the server role: the applier through MGPipeApplyAccess, unconditionally on `gPipeInputs` (never through MGPipeClientInputs). | MobileGL/MG_Pipe/PipeApply.cpp:1619 (BindRenderState), :1647 (SetDynamicState), :1656 (SetPixelPackState), :1668 (SetContextValues), :1682 (SetPatchState), :1741, :1822 | nothing - these are already address-space-local server writes. They are the reason gPipeInputs is self-sufficient in a second process. | n/a | c6 | | +| A3-6 | already-handled | Who STAMPS gPipeInputs: MGPipeStampAccess, from PipeApplier. The stamp fills FilledGen[i] with the new serial for RECORD_SUPPLIED/APPLIER_DERIVED fields inside the verb's class mask and ZERO for everything else; FilledGen==0 is "never filled" and is Fatal on read. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:330-375 (stamp), :377 (clear), :325 (SetIdentity); called from MobileGL/MG_Remote/Server/PipeApplier.cpp:1328 (StampVerbBoundary) and :1331 (LeaveApplier); freshness rule MobileGL/MG_Pipe/generated/PipeFilled.inc:421-426 | nothing structurally, but it is the ONLY writer of the server block's stamps in the child. Every read before the first stamp has FilledGen==0 and therefore Fatals - which is correct and loud. PipeInputs.h:277-285 already states that latching ServerStampedVerb TRUE is wrong "for P6, where MG_Impl is not in the server at all", and the clear at PipeApplier.cpp:1331 is what makes that true. | MGPipeInputUnfreshRead (PipeInputs.cpp:381-413) - poison Fatal or Fatal{RoleViolation} | st | | +| A3-7 | already-handled | Who READS gPipeInputs: 383 MGB_CTX* occurrences in MG_Backend - 310 `MGB_CTX->`, 58 `MGB_CTX_LIVE`, 3 `MGB_CTX_IDENTITY`. All resolve to `&gPipeInputs`. | MobileGL/MG_Pipe/PipeInputsSwitch.h:19-21; densest consumers MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp (112), MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp (135) | nothing on the read side. The P5f dual-block census is EMPTY (MobileGL/MG_IntegrationTest/Harness/dualblock-expected-fatals.txt, "the expected set is empty after fc -> fe -> fm integration"), i.e. the whole integration-split + integration-magma-split lane already runs green with the two blocks separated and MOBILEGL_IPC_STRICT_ERRORS=1. That is in-process PROOF that no backend read reaches the client fill. | dualblock-expected-fatals.txt is two-sided (split_log_paths.py expect-fatal fails on a new pair AND on a listed pair tha | st | **overstated** — Keep the verdict (already-handled) and the 58. Recompute the other four numbers, or drop them - they are decoration on a conclusion the empty census already carries. Same recomputation invalidates doc | +| A3-8 | blocks-p6 | `PipeInputs::IsLive()` - the liveness forwarder, whose BODY branches on Transport and whose definition lives in MG_Impl. Non-monolith -> `MGPipeServerContextIsLive()` (server-owned, P5f fs). Monolith -> `LiveContext() != nullptr`, i.e. `MG_State::pGLContext`. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135-2141; consumed as MGB_CTX_LIVE at MobileGL/MG_Pipe/PipeInputsSwitch.h:20, 58 read sites in MG_Backend | THE SHARPEST force-Monolith failure. With the child forced Monolith, IsLive() reads `MG_State::pGLContext`, which is null in the server process, so all 58 MGB_CTX_LIVE guards answer FALSE and every context-guarded backend path silently early-returns. IsLive() carries NO MGP_INPUT_CHECK (PipeInputs.h:259-262, it is "not a field"), so this is a silent wrong picture, never a named Fatal. | none - IsLive is outside the poison, by design | sm | | +| A3-9 | p6-must-handle | Eight out-of-line `PipeInputs::` member definitions live in MG_Impl, not MG_Backend: IsLive, GetBufferBindingPointCount, GetProgramObject, GetTextureObject, HasOpenTransformFeedbackSpan, InvalidateCompileEnv, ValidateProgramName, RecordError. Six of them re-ask `Transport != Monolith` in their own bodies. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:2135, :2160, :2178, :2184, :2190, :2202, :2217, :2223 | A server image that does not link MG_Impl has eight undefined symbols reachable from the backend's own MGB_CTX reads. Under force-Monolith the ones that DO link take the client path and answer from a null GLContext: GetBufferBindingPointCount returns 0 instead of kMGPipeMaxBufferBindingPoints, GetProgramObject/GetTextureObject return an empty SharedPtr, RecordError MGLOG_E_ONCEs and DROPS the error instead of firing the reverse-channel Fatal at PipeFill.cpp:2231-2235. | none. MGPipeStickyForwardPull (PipeInputs.cpp:442-452) only counts these inside a server-stamped verb, and with 0 BARRIE | a6 | **overstated** — Say FIVE re-ask the predicate, and split the other three out as 'null-GLContext in any child, independent of the force-Monolith decision' - they argue for the link/ownership question (a6.5), not for t | +| A3-10 | blocks-p6 | `Init.cpp`'s single hook: `if (Transport != Monolith) { InitSplitRoles(); ... return; }` and the ELSE-arm, which is the monolith backend switch. | MobileGL/MG_Backend/Init.cpp:266 (the hook), :151-230 (InitSplitRoles), :216 (ClientSession::Start - the dial), :305-317 (the monolith switch) | THE ANTI-RECURSION SITE, and a boolean `Dial` is not enough here. `Dial = No` + `Transport = Spawn` makes the condition at :266 still TRUE, so the child dials. Making the condition `ShouldDial()` instead makes it FALSE in the child, which drops it into the monolith switch at :305 and constructs `BackendObject_DirectGLES` on the app thread with no ServerLoop, no applier and no stamp - the exact frontend-glue trap. This site needs a THREE-VALUED Role, not a boolean. | none. InitSplitRoles' failure path sets pActiveBackendObject=nullptr (Init.cpp:270-273) and is loud, but the Role==Serve | c6 | | +| A3-11 | blocks-p6 | `ContextEpoch::Server` - `Transport != Monolith` used as a CACHE KEY VALUE, not a branch. The epoch is {native generation, served-context serial, Server}; with Server false the Served half is forced to 0. | MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:17-21 | Force-Monolith gives the SERVER `Server=false, Served=0`, so the served-context serial stops invalidating anything and a replaced context reuses the previous one's sync state - which is exactly what negative control S4 exists to falsify. Separately, if c6 rewrites this as `MGPipeServerArm()` carrying the `OnApplyThread()` disjunct, the value becomes THREAD-dependent and the same static memo is keyed two ways under inproc. | P6-SPAWN-PLAN §7 S4 ("临时关闭 spawn 接线中继承的 P5f epoch 失效") - but S4 tests the epoch, not the Server bit that keys it | c6 | | +| A3-12 | blocks-p6 | `ServerStaged()` - the server's buffer staged-shadow store, a function-static constructed ONCE with `Transport != Monolith` as its `CopiesIntoServerStorage` constructor argument. | MobileGL/MG_Backend/DirectGLES/Managers.cpp:1161-1166 (argument at :1164); class at MobileGL/MG_Remote/Server/StagedShadow.h:34-35 | Force-Monolith constructs it with `false` in the server process, so the server keeps NO authoritative bytes and falls back to a frontend MappedData that does not exist there. It is LATCHED, so nothing later can correct it. | none at the construction site. StagedShadowStore takes the flag as a parameter precisely so a unit case can compare the | c6 | **line-does-not-resolve** — Change the why-it-works cite from Managers.cpp:2247-2252 to Managers.cpp:1155-1157. The verdict (blocks-p6, latched, unrecoverable) is correct and survives. | +| A3-13 | blocks-p6 | `ServerStagedTexture()` - the same shape for textures: a function-static constructed once with `Transport != Monolith`, consumed by BOTH backends (Espryt's Managers.cpp and Magma's VulkanRenderer.cpp). | MobileGL/MG_Remote/Server/StagedTextureStore.h:473-477 (argument at :475) | same as A3-12, and it is worse because two backends share the one store - a wrong latch in the child silently disables the texture staging both of them read. | none in production wiring (StagedTextureStore.h:92-95 says so out loud) | c6 | **claim-false** — The guard is not 'none' - it is MG_Test/Wire/StagedTextureStoreTest.cpp:740-743 + :445-453, an in-process red-once that WOULD catch a force-Monolith latch. The spawn-specific problem is therefore shar | +| A3-14 | p6-must-handle | `ActiveXfbState()` - `const Int role = Transport != Monolith` used as an ARRAY INDEX into a 2-element role-partitioned XFB state table, with `g_activeXfbRole` tracking transitions and forcing a rebind on change. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1641-1655; consumers at :1662 (CurrentXfb), :1807 (BindScatterCaptureBuffer), :1848 (ScatterCapturedRecords), :2212 (BindTransformFeedback), :2257 (DeleteTransformFeedback) | Force-Monolith puts the server on index 0, whose keys are GL NAMES it never receives (the server's identity is `MGPipeApplier().BoundStreamOutputLifetimeId`, DirectGLES.cpp:1664). Also a decomposition misfit: rewriting `role` as `MGPipeServerArm()` with the OnApplyThread disjunct would make the index thread-dependent under inproc and split one process's XFB objects across both slots. | none - `g_activeXfbRole != role` only forces a rebind, it does not refuse | c6 | | +| A3-15 | p6-must-handle | The "MONOLITH ONLY" frontend-glue gates: `if (Transport != Monolith) return true;` guarding client-side-vertex-array staging, which reads `MGB_CTX->GetBoundVertexArray()` and the frontend binding slots. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2483 (SyncClientSideVertexArraysForFetch), :2544 (ForIndirectFetch), :2576 (ForIndexedFetch); same shape at :4297, :4416, :8964 | Force-Monolith re-enables the whole family in the server. Here the failure IS named: GetBoundVertexArray is a FATAL row (MG_Pipe/FieldOwnership.def:27), so the read aborts with Fatal{UnmigratedPipeInput,"GetBoundVertexArray@"} - but only once the applier has stamped a verb. Between stamps FilledGen==0 also Fatals, so this class is loud rather than silent. It is still 40-odd bodies of dead-and-dangerous code re-armed in the wrong process. | MGP_INPUT_CHECK -> MGPipeInputUnfreshRead (PipeInputs.cpp:381) -> Fatal | c6 | **wrong-verdict** — Move DirectGLES.cpp:8962-8970 out of A3-15 and into A3-16's disarmed-Fatal family (it is the fifth member there, alongside the one named in `missed`). Fix FieldOwnership.def:27 -> :28. The remaining f | +| A3-16 | p6-must-handle | Role-violation Fatals of the shape `if (Transport == Monolith) return; if (!OnApplyThread()) return; Fatal;` - the tree's canonical "SplitActive AND ServerArm" conjunction. | MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:29-43 (MGPipeRefuseAllocatorFromApplyThread) and :46-56 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread); MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:54-67 (RefuseLegacyTextureArmFromApplyThread); MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:62-66 (RefuseLegacyBufferArmFromApplyThread, via PushIsArmed/OnServerRole) | Force-Monolith makes the FIRST conjunct false, disarming every one of these in the process where the client allocator genuinely does not exist. With Transport kept non-Monolith, the SECOND conjunct is only true on a ServerLoop apply thread - the server's control-pump and socket-reader threads slip through. The review's `MGPipeServerArm() = Role==Server // (Role==Client && OnApplyThread())` FIXES both halves: in the child Role==Server makes the guard fire on every thread. This is the decomposition's best fit. | these ARE the guards | sm | | +| A3-17 | p6-must-handle | `MGPipeApplierReset`'s three-conjunct guard: `Transport != Monolith && ClientSession::Active() != nullptr && !OnApplyThread()`. The middle conjunct is a FOURTH axis - "is there a live wire" as distinct from "is a transport configured". | MobileGL/MG_Pipe/PipeApply.cpp:1354-1362; the axis is documented at PipeApply.cpp:41-45 and ClientSession.cpp:455-458 | `ClientSession::Active()` is null in the SERVER process for the whole of its life - there is no client session there - so this guard is unconditionally disarmed in the child. It is a concrete counterexample to the three-predicate decomposition: SplitActive/ServerArm/ShouldDial cannot express "a peer session is live", and the tree already needs that axis in two places. | this is the guard; it stops being able to go red in the server process | c6 | | +| A3-18 | p6-must-handle | `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` - the gPipeInputs role guard. Opens with `if (Transport == Monolith) return;` then consults `ApplyThreadIsInsideApplier()`, a process-global atomic raised by the applier. | MobileGL/MG_Remote/Client/ClientSession.cpp:1303-1392 (entry at :1303, SplitActive test at :1320, the two Fatals at :1358 and :1387); flag at :119 and :1396-1404 | In the CLIENT process `g_applyThreadInsideApplier` is never set by anyone, so `ApplyThreadIsInsideApplier()` is permanently false and both Fatals become unreachable - a gate that can no longer go red for the reason it exists. It must be retired BY ROLE ("this process has no applier") and named, not left silently satisfied by an always-false global. | itself - and that is the problem | c6 | **overstated** — Keep verdict p6-must-handle. Restate the mechanism as: the flag is thread_local under role-split (:123) and a never-written process atomic otherwise (:119) - both answer false in a spawn client. State | +| A3-19 | p6-must-handle | `OnFrontendStateObjectDestroyed`'s emit fork: `Transport != Monolith && !OnApplyThread()` -> emit object_death as a RECORD; else fall through to the direct twin-release switch. | MobileGL/MG_Backend/DirectGLES/Managers.cpp:229-231 | In the SERVER process any non-apply thread satisfies both conjuncts and would try to EmitObjectDeathRecord from the server - publishing into the ring the server is consuming. Rewriting `!OnApplyThread()` as `!MGPipeServerArm()` with the Role disjunct fixes it; rewriting it as `!OnApplyThread()` alone does not. | none at this site | sm | | +| A3-20 | p6-must-handle | `MGL_BACKEND_SLOT_CAP` / `_LOCAL` / `_PTR_CAP` / `_PTR_LOCAL` - `Transport != Monolith` selecting "ask the CapsMirror (the REMOTE server's published caps)" vs "read the local backend function table". | MobileGL/MG_Remote/Client/SlotCaps.h:110-136; users MobileGL/MG_Impl/GLImpl/{Drawing,Getter,Query,Sync,Texture}, MobileGL/MG_Backend/Init.cpp | This predicate is asking `Role == Client`, not SplitActive. In the child (Role==Server, Transport non-Monolith) it answers "ask the mirror", and the child's CapsMirror is a PLACEHOLDER - CapsMirror.cpp:44-52 warns once and answers placeholder at generation 0. Reachability in the child depends on whether any MG_Impl GL entry point runs there; I did not establish that it does. | CapsMirror::WarnPlaceholder (CapsMirror.cpp:44-52) - a warning, not a refusal | c6 | | +| A3-21 | p6-must-handle | The raw `g_applyThreadKey != 0` read - a THIRD question neither SplitActive nor ServerArm can ask: "has an apply thread ever run in this process". | MobileGL/MG_Backend/DirectGLES/Managers.cpp:2865-2884 (the buffer-mutation-epoch pin); the reasoning is written out at :2843-2855 | In the server process EVERY bump is legitimate before the applier starts and none is after, but `g_applyThreadKey != 0` still only answers "is a ServerLoop apply thread up", not "has this process finished bring-up". If ServerMain does not use ServerLoop, the pin never arms. The decomposition needs a bring-up/steady-state axis or this pin silently stops covering the server. | g_bufferMutationEpochBumpsOffTheApplyThread counter + Fatal under MOBILEGL_IPC_STRICT_ERRORS | st | | +| A3-22 | p6-must-handle | The aliases: three wrapper functions whose whole body is one of the two real predicates, so a c6 rewrite must find them by BODY, not by name. | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526-528 (PushIsArmed == Transport != Monolith) and :535 (OnServerRole == OnApplyThread); MobileGL/MG_Remote/Client/WireTables.cpp:87 (RunsAsTheServerRole == OnApplyThread); MobileGL/MG_Remote/Client/GpuWritePending.cpp:125-127 (GpuWriteSetIsClientSide == Transport != Monolith) | Each inherits its underlying predicate's failure exactly. `PushIsArmed()` in particular gates BufferObject's legacy-arm refusal (BufferObject.cpp:63), so force-Monolith disarms that Fatal through an alias rather than through the obvious spelling. | none | c6 | | +| A3-23 | p6-must-handle | The subsystem-bit wrappers: `Transport != Monolith && ` - six helpers that conjoin the server-arm question with a feature bit so an operator can put a family back on the frontend walk. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:272-274 (FramebufferRecordArmIsMandatory), :3385-3388 (UnitTexturesByHandle), :660-661 (ResolveBufferBindingSubsystemArm); MobileGL/MG_Backend/DirectGLES/Managers.h:740-742 (ProgramHandleArm), :1146-1150 (VertexInputReadsRecords) | Force-Monolith makes all six answer "frontend walk" in the server. Under the decomposition they become `MGPipeServerArm() && bit`, which is right - but note that these are the sites where the server arm is legitimately OPTIONAL, so `ServerArm()` must not be conflated with "the record path is mandatory". | ResolveBufferBindingSubsystemArm refuses bit 13 without bit 7 by name (DirectGLES.cpp:663-672) - a good model for the sh | c6 | | +| A3-24 | blocks-p6 | `MG_Config::TransportMode` itself: 5 enumerators in one Uint8, plus `TransportEndpoint` beside it. It conflates four independent facts and stores two different lifetimes. | MobileGL/Config.h:443-449 (enum), :455-457 (Transport + TransportEndpoint); MobileGL/ConfigLoader.cpp:33-34 (definition, default Monolith "so an early read sees Monolith rather than a torn enum") | The four conflated facts are: (1) does a wire exist; (2) WHICH ROLE this process plays - not encoded at all, inferred from (1); (3) whether and HOW this process dials (Spawn=fork+exec, UnixSocket=connect, NamedPipe=connect, InProcess=construct the peer here) - and P6-ENDSTATE-REVIEW:144-146 says that under AVF the SERVER dials and the CLIENT listens, which no enumerator can express; (4) the MEDIUM, carried half in the enum and half in TransportEndpoint. Adding Spawn costs exactly ONE predicate (ClientSession.cpp:482); adding the ROLE axis costs the other 238 nothing ONLY IF the child keeps Transport != Monolith. | ConfigLoader.cpp:347-350 names an unknown value; there is no guard on the conflation | c6 | | +| A3-25 | p6-must-handle | `MOBILEGL_TRANSPORT=spawn` parses and is then RESET to Monolith - so `MG_Config::Transport` can never hold TransportMode::Spawn at this head, and the sequencing of the integration commit matters. | MobileGL/ConfigLoader.cpp:339-346 (the named refusal inside InitTransport, which spans :319-351) | If ConfigLoader stops refusing spawn BEFORE `so`/`sm` land, all 238 sites flip to the server arm in a process that has no working session: Init.cpp:266 enters InitSplitRoles, which creates the server's private backend at :158, registers the resource op table, and only then hits the refusal at ClientSession.cpp:482 and returns false - leaving pActiveBackendObject null with a half-built server behind it. P6-SPAWN-PLAN §5 already sequences the ConfigLoader change last; this row is the concrete blast radius for why. | ClientSession.cpp:482-489 named refusal; Init.cpp:270-277 refuses to fall back | c6 | | +| A3-26 | p6-must-handle | Config is a SNAPSHOT of `environ` taken at ConfigLoader::Init, filtered to LIBGL_/MOBILEGL_ prefixes, and the map is destroyed immediately after. So the envp scrub is total for the child - and totally ineffective for anything the child later execs. | MobileGL/ConfigLoader.cpp:42-59 (IsAcceptedPrefix + the environ walk), :430-445 (Init order: InitBackendType, InitFeatures, InitTransport, InitIpc, then map.reset()); called once from MobileGL/Init.cpp:118 | This is what sizes the Dial=No-vs-envp-scrub question. `Dial` is in-process state and is NOT inherited; `MOBILEGL_TRANSPORT` IS. So the scrub catches exactly the cases Dial=No cannot: a GRANDCHILD that loads libMobileGL fresh (ARCHITECTURE.md:498 already names one - HeadlessGL's fork pre-check forks a child that runs full EGL bring-up), a respawn by MOBILEGL_IPC_RESPAWN (registered for P6+ at ARCHITECTURE.md:639), and a platform restart of the child. Dial=No catches what the scrub cannot: a second exec path added later that forgets to scrub, and any configuration source that is not envp. Also: Dial must be established BEFORE Init.cpp:118, because after ConfigLoader::Init the map is gone and a later setenv is invisible. | P6-SPAWN-PLAN §7 S3 ("子进程 envp 未剔除" -> 第二道保险挡住递归 spawn 并具名) | sm | | +| A3-27 | p6-must-handle | The DualBlock scenario asserts an IN-PROCESS fact about the server block - `serverIdentity != nullptr` - that is only ever set by the server's own stamp/attach. | MobileGL/MG_IntegrationTest/Scenarios/DualBlockScenario.cpp:69-73 (the assertion); MobileGL/MG_IntegrationTest/Harness/DualBlockPeek.cpp:34-41 (the peek); identity written at MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:314-328 | Run from the CLIENT process under spawn, `gPipeInputs.ContextIdentity()` is nullptr - nothing in that process ever calls MGPipeServerBlockNoteIdentity - so the case fails for a reason that is not a defect. P6-SPAWN-PLAN §6 G2/G14 requires the integration-spawn case-name set to be name-for-name identical to integration-split, so this case cannot simply be dropped. | the assertion itself - it goes red for the wrong reason | t6 | | + +**复审另行指出的遗漏**: +- MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:23-27 - `MGPipeApplierIsUnbarrieredApply()`, a FIFTH member of A3-16's `if (Transport == Monolith) return false; if (!OnApplyThread()) return false;` family, in the same file the auditor opened and 6 lines above the row's own first cite. It is the only member of that family CALLED FROM THE BACKEND: MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2608 guards the Fatal{RoleViolation,"MGPipeSlots"} at :2609-2614 for a draw carrying client-memory vertex arrays applied unbarriered - 'the bytes below are the application's and are moving'. Declared MobileGL/MG_Impl/Pipe/SlotAllocator.h:178. Force-Monolith in the child disarms it at its first conjunct; a ServerMain that does not publish g_applyThreadKey disarms it at its second. It is also a NINTH MG_Impl symbol the backend needs at link time, which falsifies A3-9's and doc_drift #2's 'eight'. +- MobileGL/MG_Remote/Server/ServerLoop.cpp:667 (`if (OnApplyThread()) return ApplySurfaceControlFrame(frame);`) and :735 (same, with hook/user) - two `OnApplyThread()` sites that are not guards at all but the re-entrancy arms of RunSurfaceControlFrame. With the key unpublished (a ServerMain that drives the applier without ServerLoop::Start) these answer FALSE on the thread that IS the applier, the frame goes into the one-slot control channel and waits on m_controlDone 'for an answer only THIS thread could ever give' - the hang described in as many words at ServerLoop.cpp:517-527, which also says the integration lane 'would catch that as a hang, not as a failed assertion'. This is the strongest concrete case for c6_ask #6 and the item never names it: it is a deadlock, not a disarmed guard. +- MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:112-126 - `g_textureLegacyArmScopeDepth` is a NON-thread_local process global (:112, and the comment at :103-111 argues that is safe precisely because OnApplyThread() gates both ends), and MGPipeTextureLegacyArmScope's constructor counts only `if (m_counted)` where `m_counted = ServerLoop::OnApplyThread()` (:115-118). The Fatal at :60 and its named Magma exemption at :59 read the same predicate, so A3-16's recommendation to rewrite :56 as `MGPipeServerArm()` (Role==Server on EVERY server thread) WITHOUT also rewriting :116 silently kills the declared texture-sync exemption and converts it into a spurious Fatal on every non-apply server thread. A3-16 asserts the rewrite 'FIXES both halves' and never opens the scope. +- MobileGL/MG_Remote/Client/ClientSession.cpp:123 (`thread_local Bool g_roleInsideApplier = false;`) and :1299-1302 (`ApplyThreadIsInsideApplier()` returning g_roleInsideApplier under MGPipeRoleSplitActive(), the atomic otherwise) - A3-18 describes only the process atomic at :119. The comment at :120-122 is P5f's half-landed answer to this row and should be quoted in it. +- MobileGL/MG_Remote/Client/ClientSession.cpp:1357 and :1376 - two further `Server::ServerLoop::OnApplyThread()` guards inside the very function A3-18 opens, both omitted from c6_ask #6's 'nine guards' list. Full census at this head: 14 non-comment OnApplyThread() call sites outside the definition at ServerLoop.h:265-271 - Managers.cpp:230, Managers.cpp:2868, SlotAllocator.cpp:25/:31/:50, PipeApply.cpp:1355, ClientSession.cpp:1357/:1376, PersistentMapTracker.cpp:535, WireTables.cpp:87, ServerLoop.cpp:667/:735, MipmapStorage.cpp:56/:116. Not nine. +- MobileGL/MG_Remote/Transport/ITransport.h:40-44 - `enum class TransportRole { Server = 1 /* accepts the client connection */, Client = 2 /* connects to the server endpoint */, InProcess = 3 }`, consumed via ITransport::Role() (:124, implemented InProcessTransport.h:62, tested ServerSession.cpp:715/:733) - and MobileGL/MG_Remote/Transport/RoleMemory.h:42-46 `enum class MemoryRole { Client, Server }` (used ClientSession.cpp:600/:1490, ServerSession.cpp:486/:806). The item's headline recommendation is 'a three-valued Role and not a boolean Dial', and the tree already carries two Client/Server role vocabularies, one of which ALREADY encodes the accept-vs-dial distinction the review wants split out. c6 will reuse, rename or collide with these and the audit gives it no warning. +- MobileGL/MG_Backend/DirectGLES/Managers.h:341, :406, :430, :436 and MobileGL/MG_Backend/DirectGLES/SlotTables.h:281, :398, :410, :476 - eight backend call sites of `MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread`, defined in MG_Impl at SlotAllocator.cpp:48; plus `MG_Pipe::MGPipeSlots()` at SlotTables.h:285 and :481, declared MG_Impl/Pipe/SlotAllocator.h:165. doc_drift #2 sizes the MG_Impl link debt as 'eight undefined symbols reachable from the backend's own MGB_CTX reads'; the real surface is larger and is not MGB_CTX-shaped at all. MG_Backend also includes MG_Impl headers at 12 places (MobileGL/MG_Backend/DirectGLES/SlotTables.h:15, Managers.h:25, DirectGLES.cpp:38, DirectVulkan/Renderer/VulkanRenderer.cpp:20/:26/:27/:30/:48, UniformManager.cpp:16, VkRenderPassManager.cpp:11, SwapchainObject.cpp:11, DirectVulkan.cpp:15) and MG_State headers on 55 lines across 20+ files. +- MobileGL/MG_Backend/DirectGLES/Managers.h:708-723 and :1012-1019 - six process-lifetime function-static latches (FramebufferSubsystemEnabled / TextureResourceSubsystemEnabled / SamplerSubsystemEnabled / ProgramSubsystemEnabled / ResourceSubsystemEnabled / VertexInputSubsystemEnabled), resolved at Managers.cpp:2939 and :2966 off `MG_Config::Features.PipePush`. Same latch-at-first-use hazard A3-12/A3-13 name, on the OTHER config table: in the child these must equal the client's, the envp scrub A3-26 discusses touches only MOBILEGL_TRANSPORT and MOBILEGL_IPC_*, and the only thing carrying Features to the child is the handshake configBlob (ARCHITECTURE.md:489). The item examines only the Transport-keyed latches and so its `open` item on which env vars must be scrubbed/inherited/negotiated is missing the MOBILEGL_PIPE_* family entirely. + +--- + +## a6.4 控制面 +**小结**:All twelve Server* forwarders are value frames today - I opened every one (ServerLoop.cpp:1063-1178) and each packs a SurfaceControlFrame and posts it through RunSurfaceControlFrame; nothing carries a pointer. The contract's three "not on the wire" claims are two-thirds right: ServerSwapEGLBuffers has zero production callers (only MG_Test) and ServerInitWindowSurface has zero callers of any kind, so neither needs a socket form - but ServerInitCapabilities has a LIVE production caller on the first make-current of every surface (BackendObject_Remote.cpp:126, reached from BackendObject.cpp:341-347), and SurfaceOpCodec refuses to encode its kind (SurfaceOpCodec.cpp:119-122). "Answered by CapsSnapshot" describes the answer, not the request; under spawn there is no wire op by which a client can ask, and the first eglMakeCurrent fails. That is the one thing that makes "cp is TRANSPORT ONLY" false, and it is what c6 most needs from this item. The review's SurfaceReply.defaultFb claim is CONFIRMED at both ends (encoder hardcodes 0 at SurfaceOpCodec.cpp:184, decoder never reads it at :190-195, no test touches it). The one-slot mailbox needs no protocol change at all - it needs a second poster, because the apply thread never reads the control transport (ServerLoop.cpp:404-454) and PumpControlPlane drops SurfaceOp/SurfaceReply by name (ClientSession.cpp:1425-1432). Two further surprises: the EGL handles crossing the frame are ALREADY client-minted dense tokens (MG_State/EGLState/Core.h:212-215, counters at :247-250), so the token work P6-CONTRACT-DRAFT table 0 plans is already done; and the window-kind accept set for a window op is exactly {None, X11, Win32Hwnd} - I followed nativeToken all the way to eglCreateWindowSurface (DirectGLES.cpp:16084), which confirms the review's 2.1 hole end to end and adds a third member to it, WindowKind::None. + +复审结论:**sound-with-fixes**(重新解析 34 条,异议 4 条,另指出 8 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A4-01 | p6-must-handle | ServerInitializeEGLDisplay(dpy, major*, minor*) - packs kind=InitializeDisplay + display token; the two out-pointers come back as frame.eglMajor/eglMinor. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::InitializeDisplay(1), reply carries eglMajor/eglMinor (already in SurfaceReply). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1063 (ServerInitializeEGLDisplay); dispatch at :902-913 (ApplySurfaceControlFrame); client caller MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:206 | Nothing in the payload. The forwarder itself calls ServerLoopInstance() with no role branch - see A4-13. | Detail::SurfaceControlFrameHasOnlyValues() static_assert (SurfaceControlFrame.h:118); wire-kind alignment static_assert | cp | | +| A4-02 | p6-must-handle | ServerCreateEGLWindowSurface(surface, WindowHandle&) - packs kind=CreateWindowSurface, surface token, and PackWindowHandle (windowBackend/nativeToken/width/height). VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::CreateWindowSurface(2) - but every production windowKind is refused at the server (A4-23/A4-24). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1078 (ServerCreateEGLWindowSurface); pack at :853-858; dispatch :914-921 | handle.Handle is a client-process value. Android dies by name at the decoder; X11/Win32 do NOT (A4-24). P6 is offscreen only, so this op should never legitimately cross in P6. | SurfaceWireError::AndroidNativeWindowArrived / MetalLayerArrived -> Fatal at SurfaceOpCodec.cpp:201-217 | cp | | +| A4-03 | already-handled | ServerResizeEGLWindowSurface(surface, w, h) - packs kind=ResizeWindowSurface + surface + width/height. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::ResizeWindowSurface(4), pure scalars. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1087; dispatch :922-926; client caller BackendObject_Remote.cpp:230 | nothing - it is four scalars | none needed | cp | | +| A4-04 | already-handled | ServerCreateEGLPbufferSurface(surface, w, h) - packs kind=CreatePbufferSurface + surface + width/height; the encoder stamps windowKind=Pbuffer from the op, and the decoder never reads windowKind for this kind. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::CreatePbufferSurface(3). THIS IS P6's ONLY WORKING SURFACE PATH. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1097; encoder windowKind at SurfaceOpCodec.cpp:124-126; decoder skips it because OpNeedsAWindowBackend is false (SurfaceOpCodec.cpp:55-57, :156) | nothing. This is the op P6's exit gate rides on. | none needed | cp | | +| A4-05 | p6-must-handle | ServerMakeEGLCurrent(dpy, draw, read, ctx) - ONE forwarder, TWO kinds: it self-classifies the three-NO_* release shape into SurfaceControlOp::ReleaseCurrent and everything else into MakeCurrent. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::MakeCurrent(6) / ReleaseCurrent(7); the four-tuple needs readSurface+context, which fc already appended. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1107-1121 (classification at :1112-1114); dispatch :935-983; wire fields protocol.fbs:208-209 | nothing in the payload. But the MakeCurrent arm republishes CapsSnapshot from inside the dispatch (:959-969) - see A4-30 for the socket ordering that creates. | static_asserts pin MakeCurrent=6/ReleaseCurrent=7 (SurfaceOpCodec.cpp:39-42); ID-67 republish counter (ServerLoop.h:311) | cp | | +| A4-06 | already-handled | ServerSwapEGLBuffers(dpy, draw) - packs kind=SwapBuffersInprocOnly. VALUE FRAME today. INPROC-ONLY. Socket form: NONE, and none is needed: it has ZERO production callers. The only callers in the tree are MG_Test/Wire/ServerLoopTest.cpp:1674 and :1693 (the N-3 recreate control, which needs a swap that is not a record). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1123; dispatch :1032-1038; the reason at Client/BackendObject_Remote.cpp:279-288 | nothing. Verified: grep over MobileGL/ finds no non-test caller. | SurfaceControlOpHasWireKind returns false (SurfaceControlFrame.cpp:47-48) -> EncodeSurfaceOpFrame answers InprocOnlyOpOn | cp | | +| A4-07 | p6-must-handle | ServerSetEGLSwapInterval(interval) - packs kind=SetSwapInterval + swapInterval; returns void, so the rc is discarded at the call site. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::SetSwapInterval(8), one int. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1132 (note the `(void)` at :1136); dispatch :984-987; client caller BackendObject_Remote.cpp:297 | nothing in the payload, but the discarded rc means a client whose server died sees no error at all from eglSwapInterval - the void forwarders drop MOBILEGL_ERR_NOT_INITIALIZED on the floor. | none - `(void)ServerLoopInstance().RunSurfaceControlFrame(frame)` | cp | | +| A4-08 | already-handled | ServerReleaseEGLSurface(surface) - packs kind=ReleaseSurface + surface token; void, rc discarded. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::ReleaseSurface(5). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1139; dispatch :988-998 (which calls ForgetCurrentTupleIfItNames); client caller BackendObject_Remote.cpp:301 | nothing | ForgetCurrentTupleIfItNames (ServerLoop.cpp:333-336) | cp | | +| A4-09 | p6-must-handle | ServerReleaseEGLResources() - packs kind=ReleaseResources, no arguments; void, rc discarded. BLOCKING BY CONTRACT. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::ReleaseResources(9). | MobileGL/MG_Remote/Server/ServerLoop.cpp:1146-1157 (the contract note at :1147-1153); dispatch :1000-1014; client caller BackendObject_Remote.cpp:309 | The two posters split across two PROCESSES. In the server process the inline arm still works. In the client process the call must become a socket round trip that BLOCKS until the server's DestroyEGLContext has run, because MobileGL::Destroy() walks on the moment it returns (Init.cpp:68, cited at ServerLoop.h:489-493). A fire-and-forget socket send would reinstate scout-install 5.3's hazard across a process boundary. | OnApplyThread() re-entrancy arm (ServerLoop.cpp:667); Fatal{ApplyThreadNotRunning} (:690-696) | cp | **claim-false** — Keep the row and keep verdict p6-must-handle - the BLOCKING requirement is real and correctly argued (MG_Impl/EGLImpl/EGLImpl.cpp:328 calls ReleaseEGLResources and :336 walks straight into MobileGL::D | +| A4-10 | blocks-p6 | ServerInitCapabilities() - packs kind=InitCapabilitiesInprocOnly. VALUE FRAME today. Marked INPROC-ONLY. Socket form: THERE IS NONE, AND IT NEEDS ONE. This is the single claim in the contract that the code contradicts. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1159-1164; dispatch :1019-1031; LIVE client caller MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:126 (`if (!Server::ServerInitCapabilities())`), reached from MG_Backend/BackendObject.cpp:341-347 on the first make-current per surface | EncodeSurfaceOpFrame REFUSES this kind (SurfaceOpCodec.cpp:119-122 -> InprocOnlyOpOnTheWire), so the spawned client cannot ask. With no wire form the forwarder runs in the CLIENT process against an empty ServerLoop and returns MOBILEGL_ERR_NOT_INITIALIZED -> BackendObject_Remote::InitCapabilities returns false -> the base class's MakeEGLCurrent fails -> the FIRST eglMakeCurrent under spawn fails. Not an edge case: it is on the bring-up path of every surface. BackendObject_Remote.cpp:119-125 argues explicitly why the client may not assume ServerMakeEGLCurrent already asked (a surface can be replaced without a new make-current). | SurfaceControlOpHasWireKind=false (SurfaceControlFrame.cpp:47-48) - the guard is doing its job; the gap is that a live c | c6 | | +| A4-11 | already-handled | ServerInitWindowSurface() - packs kind=InitWindowSurfaceInprocOnly. VALUE FRAME today. INPROC-ONLY. Socket form: NONE needed - it has ZERO callers anywhere in the tree, tests included. The client-side virtual is a bare `return true`. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1166-1171; dispatch :1039-1044; the client-side no-op at Client/BackendObject_Remote.cpp:145-150 | nothing | SurfaceControlOpHasWireKind=false; SurfaceControlFrameTest.cpp:269-275 | cp | | +| A4-12 | p6-must-handle | ServerSetWindowHandle(WindowHandle&) - packs kind=SetWindowHandle + windowBackend/nativeToken/width/height; void, rc discarded. VALUE FRAME today. NOT inproc-only. Socket form: SurfaceOpKind::SetWindowHandle(10) - same windowKind refusal surface as CreateWindowSurface. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1173-1178; dispatch :1015-1018; client caller BackendObject_Remote.cpp:215 (posted BEFORE ServerCreateEGLWindowSurface) | It is a second full socket round trip for something BackendObject::ActivateEGLSurface already does for itself from the registered surface state (BackendObject.cpp:283, `SetWindowHandle(surfaceState->Window)`). Worse, the dispatch stores the decoded nativeToken as a raw pointer with no backend check when windowKind is None - see A4-25. | WindowBackendFromFrameValue range check (ServerLoop.cpp:864-873) - but it ACCEPTS Unknown(-1) | cp | | +| A4-13 | blocks-p6 | THE TWELVE FORWARDERS HAVE NO ROLE BRANCH. Every one of them ends in `ServerLoopInstance().RunSurfaceControlFrame(frame)` unconditionally. There is no `if (MG_Config::Transport == Spawn)`, no ClientSession lookup, nothing. This is the whole of cp's client-side work and no line of it exists. | MobileGL/MG_Remote/Server/ServerLoop.cpp:1063-1178 (all twelve); the singleton at :810-814; the declarations and their 'the seam is twelve signatures' rationale at ServerLoop.h:495-522 | In the client process ServerLoopInstance() has never been Start()ed and m_backend is null, so PostSurfaceControlFrameWithCallerLock takes the `!m_running && !backendAlive` arm and returns MOBILEGL_ERR_NOT_INITIALIZED (ServerLoop.cpp:698-703) with NO LOG LINE. Every EGL call under spawn would fail as an unnamed false - the exact silent-fallback shape this project forbids. The Fatal{ApplyThreadNotRunning} next door (:690-696) does NOT fire, because it is gated on backendAlive. | none on the path that is actually taken; the named Fatal is on the other arm | cp | | +| A4-14 | already-handled | THE ONE-SLOT BLOCKING MAILBOX, protocol in full. Poster: takes m_callerMutex (serialises every non-apply-thread poster), takes m_controlMutex, reads m_running under that same lock, copies the frame into m_controlFrame, sets m_controlPending=true / m_controlFinished=false, publishes the release-store shadow m_controlPosted LAST, rings the CONSUMER doorbell (publish-then-ring), then waits on m_controlDone. Taker: the apply thread's PumpControlRequest does a fast negative on the shadow, takes the lock, copies the frame OUT, clears the shadow (when TAKEN, not when finished), runs ApplySurfaceControlFrame OUTSIDE the lock, then republishes frame+result under the lock, sets m_controlFinished, notify_all. Poster copies the reply half out of m_controlFrame. | MobileGL/MG_Remote/Server/ServerLoop.cpp:658-671 (RunSurfaceControlFrame), :673-727 (PostSurfaceControlFrameWithCallerLock), :542-582 (PumpControlRequest); members at ServerLoop.h:394-423; the park predicate at ServerLoop.cpp:398-402 | NOTHING IN THE PROTOCOL. The slot already carries a pure-value frame; the handshake, the shadow, the doorbell order and the C2 exit block are all address-space-local to the server. What changes is only WHO POSTS: today the app/GL thread, under spawn a server-side reader. The minimal cp shape is an mgl-srv-io thread that ReceiveFrame -> DecodeWireSurfaceOp -> ServerApplyWireSurfaceOp -> EncodeSurfaceReplyFrame -> SendFrame, and that thread simply becomes the poster this code already supports. The alternative - a fourth arming term in `ready` so the apply thread polls the socket itself - would be the only change that touches this protocol. | C2 exit block answers a stranded request NOT_INITIALIZED rather than leaving it blocking (ServerLoop.cpp:490-502); m_con | cp | | +| A4-15 | blocks-p6 | THE POSTER'S WAIT HAS NO DEADLINE. `m_controlDone.wait(lock, [this]{ return m_controlFinished; })` - an unbounded predicate wait. Compare the ring side, which uses kBarrierTimeoutMs and dies Fatal{BarrierTimeout} by name. | MobileGL/MG_Remote/Server/ServerLoop.cpp:724; the bounded counterpart at Client/ClientSession.cpp:1198-1214 (WaitForApplyToCatchUp, Fatal{BarrierTimeout}) | A SIGSTOP'd / Android-freezer'd server produces NO EOF and NO reply. The GL thread then blocks forever inside eglMakeCurrent with no diagnostic. P6-SPAWN-PLAN §4 names the frozen-server case but only for the record/death path; this is the wait that actually wedges a GL thread, and it is not in that table. | none | cp | **overstated** — Soften to: 'P6-SPAWN-PLAN §4's table already requires a bounded wait + named diagnostic for the freezer case, but its chosen mechanism - SocketDoorbell::m_dead latched on EOF (Transport/Doorbell.h:388 | +| A4-16 | blocks-p6 | THE APPLY THREAD NEVER READS THE CONTROL TRANSPORT. Its loop is PumpControlRequest / DrainRing / park, and its `ready` predicate has exactly three terms (stop, ControlIsPending, cmdHead != LocalTail) plus the eventRingFull veto. No socket, no ITransport, no PeekFrameSize. The server's ONLY ITransport reads in the whole tree are the one handshake receive and the sends. | MobileGL/MG_Remote/Server/ServerLoop.cpp:398-454 (the loop and `ready`); the server's only transport reads/sends at Server/ServerSession.cpp:450 (handshake), :538 (Welcome), :653 (PublishCapsSnapshot) | A SurfaceOp arriving on the socket is read by nobody. There is no server-side control reader after the handshake. cp must add one (mgl-srv-io) or add the socket to the park predicate. This is the largest single piece of cp's actual work and the plan's 'cp 只剩传输' does not size it. | none - there is nothing to go red | cp | | +| A4-17 | blocks-p6 | THE CLIENT'S CONTROL PUMP DROPS SurfaceOp AND SurfaceReply BY NAME. PumpControlPlane consumes only CtrlMsg::CapsSnapshot; anything else is logged at WARN and `continue`d. The comment names SurfaceOp/SurfaceReply as 'P6's and P7's' precisely so a later phase does not discover the loop swallowing them - and that is now. | MobileGL/MG_Remote/Client/ClientSession.cpp:1406-1444, the drop at :1425-1432 | A SurfaceReply that lands while some other code path is pumping caps is WARNed and discarded, and the forwarder waiting for it never wakes. The control socket carries two interleaved streams (server-pushed CapsSnapshot, and replies correlated to requests) and needs a demultiplexer with a per-seq rendezvous. Today it has a single-consumer drain loop that understands one message type. | the MGLOG_W at :1429 - it will fire, which is better than silence, but the request still hangs (A4-15) | cp | | +| A4-18 | p6-must-handle | ClientSession holds BOTH ENDS of the transport and its start path is typed to the concrete InProcessTransport, not to ITransport. StartOverTransportPair takes two unique_ptr and refuses if either is null. | MobileGL/MG_Remote/Client/ClientSession.cpp:474-496 (Start, the named non-InProcess refusal at :482-488) and :498-511 (StartOverTransportPair) | A spawn client owns ONE end. The signature must be generalised to ITransport (or a second entry point added), and the handshake stops being drivable by one thread - Hello and Welcome cross a real socket. ITransport already has the right shape (ITransport.h:46-128); nothing there needs to change. | the named refusal at :482-488 is exactly right and is what so/cp replace | so | | +| A4-19 | p6-must-handle | SurfaceReply.defaultFb IS DEAD AT BOTH ENDS - the review's claim CONFIRMED. The encoder passes a literal 0 as the DefaultFramebufferInfo offset (flatbuffers AddOffset skips a null offset, so the field is ABSENT in the buffer, not zero-valued); the decoder writes only seq/ok/eglMajor/eglMinor and never touches it; no test in the tree reads it. | encoder MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:181-188 (the `/*defaultFb=*/0` at :184); decoder :190-195; schema MobileGL/MG_Remote/Protocol/protocol.fbs:212-218; generated accessor protocol_generated.h:1225-1226; the round-trip test that covers everything BUT it, MG_Test/Wire/SurfaceControlFrameTest.cpp:239-261 | Nothing breaks - but the slot is a second truth source waiting to be revived by someone who reads the schema and not the code. | none; nothing asserts it stays empty | c6 | | +| A4-20 | p6-must-handle | SurfaceReply CARRIES NO MobileGLResult. The frame's reply half is {ok, eglMajor, eglMinor}; ServerApplyWireSurfaceOp RETURNS the rc but the wire has nowhere to put it. So 'the apply thread is gone' (NOT_INITIALIZED) and 'the driver said no' (ok=false) are the same byte on the wire. | schema MobileGL/MG_Remote/Protocol/protocol.fbs:212-218; encoder MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:183-184; the rc that is dropped, :219-221; the inproc collapse the forwarders do, ServerLoop.cpp:1075 (`return rc == MOBILEGL_OK && frame.ok`) | A client cannot tell a server that died mid-op from a legitimate refusal, so it cannot decide between arming the device-lost latch and returning EGL_FALSE. The test that exercises this collapse is MG_Test/Wire/ServerLoopTest.cpp:474-495: rc==NOT_INITIALIZED, wireReply->ok()==false, and the reply cannot say which. | none | c6 | | +| A4-21 | p6-must-handle | A seq==0 SurfaceOp IS SILENTLY RENUMBERED, not refused. RunSurfaceControlFrame mints a local seq when frame.seq is 0, and its own comment says a wire request 'is already numbered by its client and MUST retain that number' - but nothing enforces that the wire request actually had one. The encoder copies frame.seq verbatim and mints nothing. | MobileGL/MG_Remote/Server/ServerLoop.cpp:659-662; the encoder's verbatim copy at Protocol/SurfaceOpCodec.cpp:133-135 | Under spawn the CLIENT must mint, because the reply is correlated by seq. A client that forgets gets a server-minted number it has never seen - an uncorrelatable reply, silently. This is the same shape as Fatal{ProtocolCorruption}: a value the peer could not have written. | none - the renumbering is unconditional | c6 | | +| A4-22 | p6-must-handle | DecodeWireSurfaceReply DOES NOT CORRELATE. It overwrites seq/ok/eglMajor/eglMinor into whatever frame it is handed and returns void - no comparison against the request's seq, no error channel at all. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:190-195; header contract at SurfaceOpCodec.h:87 | The correlation has to live somewhere. If it lives in the pump rather than here, say so in the contract; if it lives here, this signature has to grow a SurfaceWireError like its SurfaceOp twin. | none | c6 | | +| A4-23 | blocks-p6 | THE WINDOW-KIND ACCEPT SET, EXACTLY. windowKind is only consulted for the two ops OpNeedsAWindowBackend answers true for: CreateWindowSurface and SetWindowHandle. For those: ACCEPTED = {None(0) -> WindowBackend::Unknown, X11(2) -> X11, Win32Hwnd(3) -> Win32}. REFUSED = {AndroidNativeWindow(1) -> AndroidNativeWindowArrived, MetalLayer(6) -> MetalLayerArrived, Surfaceless(4) and Pbuffer(5) -> WindowKindNamesNoBackend, any value 7..255 -> UnknownWindowKind}. All four refusals reach std::abort at the wire entry, the first two with UnmigratedSurface@P12 messages and the rest with Fatal{ProtocolCorruption,"SurfaceOp"}. For every OTHER op windowKind is ignored entirely, including CreatePbufferSurface, whose encoder stamps Pbuffer that nobody reads. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:55-57 (OpNeedsAWindowBackend), :104-114 (WindowBackendForWireWindowKind), :156-176 (the decode arm), :197-222 (the Fatals); schema protocol.fbs:181-192 | X11 and Win32Hwnd are ACCEPTED, and that is the review's 2.1 hole - I confirm it and add a third member: WindowKind::None is accepted too (:110), and the decode still copies nativeToken through (:175) even though the backend tag says Unknown. | MG_Test/Wire/SurfaceControlFrameTest.cpp:295 (Surfaceless), :308 (Android), :327/:350/:364 (the aborts). NOTHING tests X | c6 | **claim-false** — Replace the guard sentence with: 'X11, Win32Hwnd and None are each pinned as ACCEPTED, both directions, at SurfaceControlFrameTest.cpp:162-174, and X11 + a nonzero nativeToken is pinned as a successfu | +| A4-24 | blocks-p6 | nativeToken FOLLOWED ALL THE WAY INTO THE DRIVER, as asked. For an accepted windowKind: op.nativeToken() -> frame.nativeToken -> UnpackWindowHandle's HandleFromToken (an unconditional reinterpret_cast) -> WindowHandle::Handle -> RegisterEGLWindowSurface stores it in EGLSurfaceState::Window -> ActivateEGLSurface calls SetWindowHandle(surfaceState->Window) -> BackendObject_DirectGLES::InitWindowSurface does `reinterpret_cast(m_windowHandle.Handle)` -> DirectGLES::InitWindowSurface calls `eglCreateWindowSurface(g_Display, g_Config, window, nullptr)`. There is no validation anywhere on that chain except the Backend-tag range check and the `!handle.Handle` null test. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:174-175 -> Server/ServerLoop.cpp:875-882 (UnpackWindowHandle) -> MG_Backend/BackendObject.cpp:227-238 (RegisterEGLWindowSurface) -> :282-283 (ActivateEGLSurface) -> DirectGLES/BackendObject_DirectGLES.cpp:899-907 -> DirectGLES/DirectGLES.cpp:16079-16095 (the eglCreateWindowSurface at :16084) | On a Linux/Windows build an X11 XID or an HWND reaches eglCreateWindowSurface in the SERVER process. An XID and an HWND happen to be system-scoped and might even work, which is worse than crashing: it makes the refusal look unnecessary while a guest with a private window system hands the server a wild value. This is the review's 2.1, confirmed end to end at this head. | BackendObject.cpp:227's `handle.Backend == Unknown // !handle.Handle` - a soft `return false` with an MGLOG_E, not a nam | c6 | | +| A4-25 | p6-must-handle | WindowKind::None ON A WINDOW OP is accepted, maps to WindowBackend::Unknown(-1), and the raw nativeToken is copied through anyway. WindowBackendFromFrameValue's range check passes -1 (its lower bound IS Unknown), so UnpackWindowHandle builds {Unknown, , w, h}. CreateWindowSurface then fails softly at BackendObject.cpp:227; SetWindowHandle does not fail at all - it just STORES the foreign pointer. | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:110 (None -> Unknown), :174-175 (token copied regardless); MobileGL/MG_Remote/Server/ServerLoop.cpp:864-873 (the range check that admits -1), :875-882; MG_Backend/BackendObject.cpp:227-230 and :473-476 | A corrupt or hostile SurfaceOp with windowKind=None and a nonzero nativeToken gets a client-process pointer STORED in the server's m_windowHandle with no refusal of any kind. It is later overwritten by ActivateEGLSurface, so it is latent rather than immediately fatal - but 'latent' is exactly what the whitelist exists to remove. | none for SetWindowHandle; a soft false for CreateWindowSurface | c6 | | +| A4-26 | already-handled | THE EGL HANDLES ARE ALREADY CLIENT-MINTED DENSE TOKENS. MG_State fabricates every EGLDisplay/EGLConfig/EGLSurface/EGLContext as `EncodeHandle(m_next*Handle++)` over counters that all start at 1; they are never driver handles. The server's BackendObject uses them purely as std::map keys and identity comparisons; the REAL EGL objects are DirectGLES' process globals g_Display/g_Surface/g_Context, created from the native window and never named on the wire. | MobileGL/MG_State/EGLState/Core.h:212-215 (EncodeHandle), :247-250 (the four counters, each = 1); mint sites Core.cpp:143 (display), :163 (config), :687 (context), :830/:868/:905/:945/:977/:1010 (surfaces); server-side use as map keys MG_Backend/BackendObject.cpp:198, :232, :265-268, :324-335; the real objects at DirectGLES/DirectGLES.cpp:16031-16056, :16084 | NOTHING. This is the best news in the item: P6-CONTRACT-DRAFT table 0's DisplayToken/SurfaceToken/ContextToken ('minted by the client, dense from 1') is a description of what MG_State already does, not work to do. The server needs no token->native map, because it never had native handles to map to. | the static_asserts in TokenFromHandle/HandleFromToken that the type is a pointer | c6 | | +| A4-27 | already-handled | ForgetCurrentTuple AND ITS N-3 RULE LIVE INSIDE THE DISPATCH, so the frame path preserves them by construction. Six call sites: ApplyThreadMain's exit (after ~backend), Stop()'s never-started arm, and four dispatch arms - CreateWindowSurface on success, CreatePbufferSurface on success, ReleaseSurface via ForgetCurrentTupleIfItNames, and ReleaseResources. The rule itself: forget after any event that may have destroyed the native context the held tuple names, because EGL handles are recycled (on this host they are literally 0x1). | MobileGL/MG_Remote/Server/ServerLoop.cpp:325-336 (the two functions); call sites :478, :764, :919, :933, :996, :1012; the rule stated at ServerLoop.h:325-337; the red-once control named at ServerLoop.cpp:1007-1011 | NOTHING. Because the calls are on the dispatch side of the seam, an io-thread poster changes nothing about them. P6-CONTRACT-DRAFT §4.4 and P6-SPAWN-PLAN:83 both claim this; I verified it at all six sites and the claim holds. | ServerLoopTest's C7/N-3 recreate control reads NativeBindCount()==1 where 2 is required if :1012 is deleted (stated at S | cp | | +| A4-28 | already-handled | WHICH CONTROL OPS NEED A GL/EGL CONTEXT TO COMPUTE - all of them are on the SERVER side, on the apply thread, which already owns the context for life. NEED IT: CreateWindowSurface / CreatePbufferSurface (they CREATE the context via InitDisplayAndContext), MakeCurrent (native eglMakeCurrent + the base class's InitCapabilities GL queries), SetSwapInterval (eglSwapInterval needs a current context - DirectGLES defers when there is none), ReleaseSurface (may reach DestroyEGLContext), ReleaseResources (DestroyEGLContext), and the three inproc-only kinds. DO NOT: InitializeDisplay (two constants), ResizeWindowSurface (bookkeeping only, not overridden by DirectGLES), ReleaseCurrent (the ClientRelease arm records and does not forward), SetWindowHandle (a struct store). | MG_Backend/BackendObject.cpp:182-187 (InitializeDisplay constants), :196-210 (Resize, bookkeeping); ServerLoop.cpp:282-293 (ClientRelease, no native call); BackendObject.cpp:473-476 (SetWindowHandle); DirectGLES/DirectGLES.cpp:16031-16056 + :16079-16095 (context creation), :16065-16077 (swap interval), :16178-16185 (MakeCurrent); BackendObject_DirectGLES.h:31-50 (the override list, which is how I know Resize and SetSwapInterval are not overridden) | nothing - the split is already drawn in the right place. Under spawn the apply thread stays the context owner in its own process, which is, in ServerLoop.h's words, 'exactly the shape P6's spawned server inherits'. | the 32 existing context-owner guards | cp | | +| A4-29 | already-handled | NOTHING IN THE CLIENT'S CONTROL PATH NEEDS A GL CONTEXT. The nine EGL virtuals on BackendObject_Remote do exactly three things besides forwarding: a run-ahead wait on a shm watermark, a caps-mirror refresh, and a SEG_EVENT drain that writes MG_State texture shadows. No GL call, no driver entry point. | MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:190-194 (WaitForApplyBeforeEglForwarder), :83-92 (RefreshFormatCapabilities, reads CapsMirror), :224/:233/:242/:272 (DrainPublishedEvents); the event consumer at Client/ClientSession.cpp:225-257, which only calls MG_State AllocateStorage/SetInternalFormat | nothing. The answer to the item's last question is: no. | CONTRACT-P5C rule E / the RoleViolation guards | cp | | +| A4-30 | blocks-p6 | CapsSnapshot IS PUSHED FROM INSIDE THE DISPATCH, on the apply thread, mid-op. The MakeCurrent arm and the InitCapabilities arm both call ServerSession::PublishCapsSnapshot(), which builds a FlatBuffer and calls SendEnvelope on the same ITransport the reply will use. | MobileGL/MG_Remote/Server/ServerLoop.cpp:959-969 (MakeCurrent arm) and :1025-1030 (InitCapabilities arm); Server/ServerSession.cpp:609-654 (the send at :653); ITransport's threading note at Transport/ITransport.h:27-29 ('callers serialize sends') | Two consequences. (1) ORDERING: the snapshot must reach the client BEFORE the reply that its adoption depends on, because the client pumps and adopts right after the RPC returns (BackendObject_Remote.cpp:268-273). Same-socket FIFO gives that for free ONLY if the same thread sends both - i.e. if the io thread does not send the reply while the apply thread is still sending the snapshot. (2) SERIALISATION: under spawn the server has two potential senders (the apply thread's PublishCapsSnapshot and the io thread's SurfaceReply) on an interface that is explicitly not internally synchronised for send. | none - ITransport documents the requirement but does not enforce it | cp | | +| A4-31 | no-action | MGPipeServerSetContextLive, called from five sites in the control dispatch, writes a PROCESS-LOCAL static g_serverContextLive. Both the setter and the only reader are server-side: MGPipeServerContextIsLive is read from PipeInputs::IsLive, which is reached only through MGB_CTX_LIVE, and every MGB_CTX_LIVE use site is under MG_Backend/. | MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:305-312; writers at Server/ServerLoop.cpp:290, :295, :314, :475, :761, :1006; reader chain MG_Impl/Pipe/PipeFill.cpp:2135-2141 -> MG_Pipe/PipeInputsSwitch.h:20 -> MG_Backend/DirectGLES/Managers.cpp:7304 etc. | NOTHING. I checked specifically because the control dispatch touches it: there is no client-side reader of MGB_CTX_LIVE anywhere under MG_Impl or MG_State, so the static stays inside the server process and keeps meaning what it means. (Flagging for the integrator in case a6.2's statics item counts it - it is server-local and needs no carrier.) | purity gate C's ban on pGLContext outside MG_State/MG_Impl keeps MGB_CTX_LIVE server-side | cp | | +| A4-32 | p6-must-handle | ServerSetWindowHandle IS A REDUNDANT BLOCKING ROUND TRIP. The client posts it immediately before ServerCreateEGLWindowSurface, but the server's own ActivateEGLSurface sets the window handle from the surface it just registered. | client MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:212-216; the server's own set at MG_Backend/BackendObject.cpp:282-283 (`SetWindowHandle(surfaceState->Window)`), fed from RegisterEGLWindowSurface's store at :232-238 | Two socket round trips instead of one, each with the reply wait of A4-15, at exactly the moment the plan's §6.8 wants to measure socket-doorbell cost against inproc condvar. Also doubles the surface at which A4-25's unvalidated SetWindowHandle store is reachable. | none | cp | | +| A4-33 | already-handled | THE TEST PROBE'S RAW FUNCTION POINTER IS A MEMBER, NOT SLOT CONTENT, and ProbeForTesting has no wire kind - so the one remaining pointer in the control channel cannot cross. Worth recording because P5c row G4 was exactly 'a function pointer in the mailbox'. | MobileGL/MG_Remote/Server/ServerLoop.h:228-235 and :424-428 (m_controlProbeHook/m_controlProbeUser as atomics beside the slot); SurfaceControlFrame.cpp:47-48 (no wire kind); the probe dispatch at ServerLoop.cpp:892-895 | nothing. The pointer never leaves the process it was taken in, and the codec refuses the kind. | SurfaceControlOpHasWireKind=false; SurfaceControlFrameTest.cpp:269-275 covers all four inproc-only kinds | cp | | +| A4-34 | p6-must-handle | THE CONTROL PUMP'S LOOP BOUND IS PeekFrameSize()==0, which means something different on a stream socket than on a message queue. On InProcessTransport a message is whole or absent; on a socketpair a partially reassembled frame also answers 0. | MobileGL/MG_Remote/Client/ClientSession.cpp:1415-1418; Transport/Framing.h:9-28 and :79-80 (FrameReader is a byte-stream reassembler, explicitly 'it never assumes a read() returned a whole frame'); ITransport.h:83-85 | A reply-wait loop that breaks on PeekFrameSize()==0 would spin out of a half-arrived frame and report no reply. The pump's bound and the reply wait need different shapes: the pump stays non-blocking, the reply wait must block with a deadline (A4-15). | Framing.h latches PROTOCOL_MISMATCH on a violated frame, so corruption is named - but a short read is not corruption | so | | + +**复审另行指出的遗漏**: +- MobileGL/MG_Remote/Client/ClientSession.cpp:704 - `const MobileGLResult running = Server::ServerLoopInstance().Start(server);`. The CLIENT's Start() is what starts the apply thread; the comment at :700-703 says so ('Under `inproc` the client is what starts the server role, which is why this call is here rather than in some server-side main'). Under spawn this line IS the server-side main that does not exist. It is the same no-role-branch shape as A4-13 and it is not in a6.4 at all; it is also absent from P6-ENDSTATE-REVIEW.md:264's sm list (which names :534-539, :577, :599-600, :758 but not this). +- MobileGL/MG_Remote/Client/ClientSession.cpp:802 - `Server::ServerLoopInstance().Stop();` inside the client's teardown, between transport->Shutdown() (:797) and the emitter release (:808-813). The bounded join, the private-backend destruction and the ordering argument at :800-805 all live on the client's thread today. Same gap, same omission from both a6.4 and the review's list. +- MobileGL/MG_Remote/Client/ClientSession.cpp:534-535, :577, :582, :599-600, :758, :814 - the remaining direct reaches from client code into the server singletons: `Server::ServerSessionInstance()` + `server.Accept(*m_serverTransport)` (:534-535), the Welcome cross-check that reads `server.Shm().AnnouncedSize(slot)` (:577, :582), `m_shm.AttachInProcess(server.Shm(), ...)` (:599-600), and two `ServerSessionInstance().Close()` (:758, :814). A4-18 covers only the InProcessTransport typing of StartOverTransportPair and does not enumerate these even to assign them to sm/so. The review has four of the six; the audit should have closed the set with file:line so c6 can see the whole seam at once. +- MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:156-176 - `nativeToken` is SILENTLY DROPPED for the eight non-window ops. `decoded.nativeToken = op.nativeToken()` (:175) is inside the `if (OpNeedsAWindowBackend(kind))` block opened at :156, so a wire SurfaceOp with kind=ReleaseSurface (or MakeCurrent, or ReleaseResources) carrying nativeToken=0xDEADBEEF decodes to frame.nativeToken == 0 with no log, no SurfaceWireError and no Fatal. A4-23 caught the windowKind half of this ('for every OTHER op windowKind is ignored entirely') but not the token half - and nativeToken is the field that actually carries a foreign address. By the project's own rule (and by A4-21's framing of seq==0), a value the peer could not legitimately have written is Fatal{ProtocolCorruption}, not a silent zero. c6's Rule G should cover both fields, not just the kind. +- MobileGL/Config.h:443-449 and :455 - c6_asks item 2 offers `MG_Config::Transport == Spawn` as one of three candidate role predicates. TransportMode::Spawn = 2 does exist (:446) and Transport is a runtime `extern TransportMode` under MOBILEGL_BUILD_DISAGGREGATED (:455, constexpr Monolith only in the non-disaggregated build at :566), so the predicate is mechanically viable - but P6-ENDSTATE-REVIEW.md:264 has ALREADY ruled against the Transport dial as the role predicate: it requires two orthogonal axes `Role{Monolith, Client, Server}` x `Dial{No, Fork, Connect}` and names forcing the child to Monolith as the trap ('后者会选中 frontend glue,正是 CONTRACT §3.1 点名的陷阱'). The review is listed as key reading and is what AMENDED a6, so the ask should have been framed as 'confirm the review's MGPipeServerArm() against these twelve sites', not as an open three-way choice. +- MobileGL/MG_Backend/BackendObject.h:616-620 - `enum class SurfaceKind { None, Window, Pbuffer }`. There is no surfaceless surface kind anywhere in BackendObject, and `WindowKind::Surfaceless` (protocol.fbs:186) is refused by the decoder (SurfaceOpCodec.cpp:168-172 -> WindowKindNamesNoBackend, pinned at SurfaceControlFrameTest.cpp:181, :295). A4-04 states correctly that pbuffer is 'P6's ONLY WORKING SURFACE PATH' but never draws the doc consequence: P6-SPAWN-PLAN.md:40 and P6-CONTRACT-DRAFT.md:107 both promise 'pbuffer / surfaceless / 离屏', and surfaceless has no backend implementation and no wire op at this head. That belongs in doc_drift beside the other two P6-SPAWN-PLAN entries. +- In-code citation drift in the exact comments a6.4 quotes, none of it flagged: (a) MobileGL/MG_Remote/Server/ServerLoop.h:491 and MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:307 cite 'MobileGL/Init.cpp:68' for pActiveBackendObject.reset(); at this head that call is MobileGL/Init.cpp:77 and :68 is a comment about the live-sync registry. (b) MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:257 cites 'ServerLoop.cpp:613-628' for the caps republish; the republish is ServerLoop.cpp:959-969. (c) MobileGL/MG_Remote/Server/ServerLoop.cpp:1148 and ServerLoop.h:489 cite 'EGLImpl.cpp:326' for the ReleaseEGLResources call; it is MG_Impl/EGLImpl/EGLImpl.cpp:328. A4-09 reproduces (a) and (c) verbatim as if they resolved. +- P6-CONTRACT-DRAFT.md:46 cites `protocol.fbs:234-242` for 'FatalCode::ServerCrashed / DeviceLost ... Already allocated'. At this head `enum FatalCode` is protocol.fbs:248-256 (ServerCrashed = 5 at :254, DeviceLost = 4 at :253); lines 234-242 are `enum AuxRequestKind` plus the `table AuxRequest` header. Same table 0 the audit combed for drift (it caught :43's protocol.fbs:186-196 and :44's correct 166-179) - this row was skipped. + +--- + +## a6.6 Transport 站点分类 +**小结**:The 238-site census is CONFIRMED exactly, but only with the integrator's own counting rule: 238 = grep-lines matching `MG_Config::Transport` (any suffix, so `TransportMode` and `TransportEndpoint` count) in non-test dirs, distributed 155 MG_Backend / 35 MG_Impl / 21 MG_Remote / 12 MG_Pipe / 8 MG_State / 7 Config. Twelve of the 238 are COMMENT lines, so there are 226 live code lines; counting occurrences of the bare symbol instead gives 228. The eight MG_State sites are NOT the sample the plan thinks they are: two of the eight (BufferObject.cpp:60, BufferObject.h:194) are comments, five of the remaining six are single-question `SplitActive` tests of the R-6 / third-state shape, and only ONE (MipmapStorage.cpp:55) literally asks both questions in one function. The "asks both questions" shape is not MG_State-local at all - it appears at 18 sites across MG_Impl/Pipe/SlotAllocator.cpp, MG_Impl/Pipe/PipeFill.cpp, MG_Pipe/PipeApply.cpp, MG_Backend/DirectGLES/Managers.cpp and MG_Remote/Client, and it already has two spellings in the tree (`Transport!=Monolith && !RunsAsTheServerRole()` for the client arm, `Transport!=Monolith && OnApplyThread()` for the server refusal). The three proposed predicates DO cover everything I opened, but a fourth axis exists and it is not a predicate: WHEN the question is asked. Most sites ask per-draw; VulkanRenderer.cpp:14593/:14616 and StagedTextureStore.h:475 ask ONCE, during device creation or at first use, which means the spawn server's Role must be established before MG_Backend::Init() rather than at session start. The one thing c6 most needs from this: decide whether the spawn SERVER process carries `Transport != Monolith` or `Transport == Monolith`, because 148 backend code lines and both Fatal role guards flip together on that single bit, and a server process left at Monolith selects the frontend glue arm everywhere - which P6-ENDSTATE-REVIEW.md:264 already names as the trap but which nothing in the tree currently prevents. + +复审结论:**sound-with-fixes**(重新解析 26 条,异议 11 条,另指出 7 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A6-S1 | no-action | Comment line documenting that PushIsArmed() is a read of MG_Config::Transport and OnServerRole() a relaxed load of the apply-thread key. NOT a code site. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:60 (comment inside RefuseLegacyBufferArmFromApplyThread) | Nothing at this line. But the comment's cost argument ("one relaxed load each") is what a rename has to preserve: if MGPipeSplitActive() stops being one load of a process global it regresses a per-draw guard that simpleperf measured at 2.66% self on the client thread (ServerLoop.h:90-96). | none (comment) | c6 | | +| A6-S2 | p6-must-handle | `if (MG_Config::Transport != MG_Config::TransportMode::Monolith) return;` - R-6's "second door": refuse the LEGACY backend persistent-map mint when a transport is live. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:308 (BufferObject::TryAdoptLargeStorage) | Nothing, if the client process keeps Transport != Monolith. This is a pure SplitActive question and it never asks who is running. In the SERVER process the function is unreachable (no frontend BufferObject exists there), so the answer is irrelevant on that side. | the R-6 decline is doubled at PipeApply.cpp:2287 and at PipeWireCodec.cpp:1636; PipeStats' persistent-map-push counter i | sm | | +| A6-S3 | p6-must-handle | `if (MG_Config::Transport != Monolith && atOffset == 0 && data.size >= m_size) { m_gpuWritePending = false; }` - the third state's only exit. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:559 (BufferObject::WritebackFromBackend) | Nothing structural - it is SplitActive only. One latent sharpness: the condition `data.size >= m_size` is how a whole-buffer writeback is recognised, and the SLICED readback path at :605-618 has to clear m_gpuWritePending by hand for exactly that reason. Under spawn the slice size (BufferWritebackSliceBytes) is driven by ring capacity, so more buffers will take the sliced path and the by-hand clear at :614 becomes the common case rather than the exception. | AwaitBufferWriteback's third-state Fatal (BufferObject.cpp:619-628, MG_Remote/Client/GpuWritePending.cpp) | sm | **line-does-not-resolve** — Sliced path :594-612; by-hand clear :612; whole-buffer emit :614; AwaitBufferWriteback call :620; no-route clear :627-629. Drop 'BufferObject.cpp:619-628' from the guard field and keep only MG_Remote/ | +| A6-S4 | p6-must-handle | `if (MG_Config::Transport != Monolith) { ... }` - the WHOLE split arm of SyncGpuWrites: emit readback (sliced or whole), then block in AwaitBufferWriteback. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:590 (BufferObject::SyncGpuWrites) | This is the one MG_State site whose failure mode is a HANG rather than a Fatal. AwaitBufferWriteback blocks on a reply slot. If the spawn SERVER process is configured with Transport != Monolith and any thread there reaches SyncGpuWrites on a frontend BufferObject (there should be none, but the guard that would catch it is the accessor guard, not this function), it emits a readback with no ClientSession and waits. Today inproc is protected only because the apply thread aborts earlier in the accessor. | indirect: RefuseLegacyBufferArmFromApplyThread on the accessors; BufferWritebackIsReachable's no-route clear at :627 | sm | | +| A6-S5 | p6-must-handle | `if (MG_Config::Transport != Monolith) return false;` - R-6's second door again, on the shader-storage residency path. | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:879 (BufferObject::EnsureGpuResidentStorage) | nothing - pure SplitActive, one direction, and the fallback (stay shadow-backed) is the spawn-correct answer | same as A6-S2 | sm | | +| A6-S6 | p6-must-handle | `MG_Config::Transport == MG_Config::TransportMode::Monolith &&` as a CONJUNCT inside a multi-line map-persistent condition (not a statement of its own). | MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:958 (BufferObject::AcquireMemoryRange, the legacy AcquirePersistentMap arm) | nothing semantically; it is the textual shape that resists a mechanical rewrite | same as A6-S2 | sm | | +| A6-S7 | no-action | Comment line: "Everything here is behind the build option AND behind `MG_Config::Transport != Monolith` at the call site". NOT a code site. | MobileGL/MG_State/GLState/BufferState/BufferObject.h:194 (the "P5 b1" block declaring PushMappedSpanBlock / NotePersistentMapStateChanged / HasLiveHostWritesForWire / HasOutstandingGpuWrite) | nothing at this line, but the comment is the thing c6 will read to decide which members are client-only, and it understates where the gate actually sits | none (comment) | c6 | **line-does-not-resolve** — Call sites are BufferObject.cpp:227 (ResizeShadow), :378 (ReleaseMemory), :833 (glMapBuffer arm), :923 (AcquireMemoryRange), :947 (the MOBILEGL_PIPE_PUSH persistent arm). The callee's own gate is :489 | +| A6-S8 | p6-must-handle | `if (MG_Config::Transport == Monolith) return;` immediately followed by `if (!MG_Remote::Server::ServerLoop::OnApplyThread()) return;` then a named-scope exemption, then Fatal{RoleViolation, "texture-legacy-arm"}. THE two-question sample. | MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:55-70 (RefuseLegacyTextureArmFromApplyThread) | Both halves become structurally constant, in opposite directions, and that is the finding. In the spawn CLIENT process g_applyThreadKey is永 0 (ServerLoop.h:265-271: `key != 0 && key == CurrentThreadKey()`), so OnApplyThread() can never be true and the guard can never fire - a gate that cannot go red. In the spawn SERVER process there is no frontend TextureObjectMipmap to call it on, so it also cannot fire. The guard's whole value evaporates at exactly the moment spawn makes the violation impossible - which is fine as an outcome but must be a DECLARED retirement, not a silently dead abort. | itself; the pinned surface list in MG_Test/Wire/RemoteClientTest.cpp's RemoteGuards is what keeps the list honest | sm | **line-does-not-resolve** — RefuseLegacyTextureArmFromApplyThread :54-67; the Monolith test :55; the OnApplyThread test :56; the MGPipeTextureLegacyArmScope exemption :59; the Fatal :60-66. RefuseLegacyTextureShapeReadFromApplyT | +| A6-P1 | blocks-p6 | PATTERN: the backend body's server-arm test. 148 code lines (150 total minus 2 comments) across MG_Backend/DirectGLES (98 lines) and MG_Backend/DirectVulkan (52 lines). Predominant form is the bare `if (MG_Config::Transport != Monolith) {` / `... == Monolith) return;`. | Representatives opened: MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:315 (GetRawDepthFetchSampler), :442 (indirect command resolve), :876 (shader-storage GPU-write walk), :2483 (SyncClientSideVertexArraysForFetch), :8398-8417 (multi-draw indirect), :15600 (GetTexImage PBO); MobileGL/MG_Backend/DirectGLES/Managers.cpp:3289, :3482; MobileGL/MG_Backend/DirectGLES/Utils.cpp:55 (ActiveBackendFormatCaps), :2363; MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:1054; MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:685 (ApplyLineWidthState), :3162, :13148 | This is the headline. All 148 flip together on ONE bit in the server process. If c6 spells "the server does not dial" as `Transport = Monolith` (the trap P6-ENDSTATE-REVIEW.md:264 names), every one of these selects the frontend glue arm inside a process that has no frontend. The failure is NOT uniformly loud: DirectGLES.cpp:2483 would read a null GetBoundVertexArray() and `return true` silently; Managers.cpp:3482 would call `bufferObject->SyncPersistentMappedRange()` on a null pointer's guard and skip; VulkanRenderer.cpp:13148 would read MGB_CTX and hit MGP_INPUT_CHECK's poison Fatal by FIELD name, which names the field and not the wrong-arm decision. | partial and at the wrong altitude: MGPipeInputPoisonFatalForVerb names a field, not a role; no gate exists that says "th | c6 | | +| A6-P1a | later-stage | SUB-PATTERN of P1: role-INDEXED statics. `Array g_roleXfbState; Int g_activeXfbRole = -1;` with `const Int role = MG_Config::Transport != Monolith;` used as the subscript, plus four `const Bool server = ...` selectors that pick between a per-role member and the shared one. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:1641-1656 (ActiveXfbState), :1645 (the subscript), :1662 (CurrentXfb), :1807-1809 (BindScatterCaptureBuffer), :1848-1850 (ScatterCapturedRecords), :2212 (BindTransformFeedback), :2257 (DeleteTransformFeedback) | Nothing breaks - but the whole construct is a same-address-space artifact. Under spawn each process has exactly one role and `g_roleXfbState[0]` is dead in the server, `[1]` dead in the client. Leaving it is harmless; collapsing it is the kind of cleanup that silently changes which key a test observes, so it should be a named decision rather than a drive-by. | none; g_activeXfbRole's NeedsBind reset is the only thing that notices a role change | cp | | +| A6-P1b | blocks-p6 | SUB-PATTERN of P1: bring-up-time, ONCE-per-process reads of Transport inside device/instance creation and inside a function-static initialiser. | MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:14593 (descriptorIndexingCore, inside CreateDevice's feature selection), :14616 (wireDepthResolveEnabled); MobileGL/MG_Remote/Server/StagedTextureStore.h:475 (ServerStagedTexture's `static StagedTextureStore& store = *new StagedTextureStore(Transport != Monolith);`); MobileGL/MG_Backend/DirectGLES/ContextEpoch.h:19 (CurrentContextEpoch) | These decide BEFORE any session exists, so the spawn server's Role must be established at process entry - before MG_Backend::Init(), before VulkanRenderer's vkCreateDevice - not at ClientSession/ServerSession handshake time. Any design that derives ServerArm from "a session is attached" or "the apply thread is running" gets the wrong answer here, because on Magma the descriptor-indexing and depth-resolve feature set is baked into the VkDevice at this point and cannot be changed later. | none - a wrong answer here is a device created without the extensions the wire path needs, surfacing much later as a mis | c6 | **claim-false** — Drop ContextEpoch.h:19 from P1b and file it under A6-P1 (ordinary per-draw server-arm test). The bring-up-once family is then VulkanRenderer.cpp:14593, :14616, StagedTextureStore.h:475 and the missed | +| A6-P2 | p6-must-handle | PATTERN: the client-arm emit/decide test, single question. ~48 code lines: MG_Impl (35 lines, 1 of them comment) + MG_State (6 code lines, rows S2-S6) + MG_Remote/Client's own predicates. | MobileGL/MG_Impl/Pipe/PipeFill.cpp (19 lines); MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6079, :6543, :6572, :6832; MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp:1347, :1409; MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp:782; MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp:1186; MobileGL/MG_Impl/Pipe/VertexInputEmit.h:389, :396; MobileGL/MG_Impl/Pipe/ResourceTracker.h:585, :704 | Nothing, in the client process. In the server process these TUs are linked (same libMobileGL.so) but should be unreachable. The two Fatals at ResourceTracker.h:585/:704 are the ones worth keeping: they fire on `Transport != Monolith` alone with no role term, so they stay able to go red in either process. | the two named Fatals above; MGPipeInputPoison for the fill side | sm | | +| A6-P2a | blocks-p6 | PATTERN: the TWO-QUESTION sites - `Transport != Monolith` conjoined with a role probe in the same function. 18 sites, in TWO spellings that mean opposite things: the CLIENT arm `Transport != Monolith && !RunsAsTheServerRole()` and the SERVER refusal `Transport != Monolith && OnApplyThread()`. | CLIENT arm: MobileGL/MG_Impl/Pipe/PipeFill.cpp:357 (ClientRunsAhead), :975 (respecify+subdata follow-up), :1114 (flush-range follow-up), :1255 (MGPipeClientFinish), :3597 (applier reset); MobileGL/MG_Remote/Client/GpuWritePending.cpp:226 (MGPipeStageChunkBytes). SERVER refusal: MobileGL/MG_Impl/Pipe/SlotAllocator.cpp:24 (MGPipeApplierIsUnbarrieredApply), :30 (MGPipeRefuseAllocatorFromApplyThread), :49 (MGPipeRefuseFrontendKeyedRegistryFromApplyThread); MobileGL/MG_Impl/Pipe/PipeFill.cpp:1205 (MGPipeDeferDestroyAndFreeIfOnApplyThread); MobileGL/MG_Pipe/PipeApply.cpp:1353 (MGPipeApplierReset's Fatal); MobileGL/MG_Backend/DirectGLES/Managers.cpp:229 (OnFrontendStateObjectDestroyed), :2867 (mutation-epoch bump pin); MobileGL/MG_Remote/Client/ClientSession.cpp:1320+:1357 (RefusePipeInputsTouchWhileApplierOwnsIt); MobileGL/MG_State/GLState/TextureState/MipmapStorage.cpp:55; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:62-67; MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526/535 | The role term becomes a constant in each process and the conjunction degenerates - but in two DIFFERENT directions, which is why these cannot be rewritten uniformly. In the client process `!RunsAsTheServerRole()` is always true (g_applyThreadKey==0), so the six CLIENT-arm sites reduce to plain SplitActive - mechanical. In the server process `OnApplyThread()` is true only on the apply thread, so the twelve SERVER refusals still have a live second term, and a non-apply thread in the server (the EGL/control thread, the main thread) that reaches PipeFill.cpp:975 or :1114 would take the CLIENT branch and emit client wire records with no ClientSession. That is a NEW reachability that inproc never had, because in inproc every thread except the apply thread genuinely was the client. | PipeFill.cpp:984-995's ClientWireRecordsEmitted() self-check would abort by name (Fatal{InitialBytesNotCarried}) on the | c6 | **claim-false** — Retire the two-spelling taxonomy. Reclassify by POLARITY, not by name, into three buckets: (a) server-arm gates that proceed when split AND on the apply thread - SlotAllocator.cpp:24-25, :30-31, :49-5 | +| A6-P3 | blocks-p6 | PATTERN: the four SlotCaps macros - `MGL_BACKEND_SLOT_CAP`, `MGL_BACKEND_SLOT_LOCAL`, `MGL_BACKEND_SLOT_PTR_CAP`, `MGL_BACKEND_SLOT_PTR_LOCAL`. Four DEFINITION lines that govern 14 non-test expansion sites. | MobileGL/MG_Remote/Client/SlotCaps.h:111, :116, :127, :134. Expansions: MG_Impl/GLImpl/Query/GL_Query.cpp (7), MG_Impl/GLImpl/Texture/GL_Texture.cpp (2), MG_Impl/GLImpl/Getter/GL_Getter.cpp (2), MG_Impl/GLImpl/Sync/GL_Sync.cpp (1), MG_Impl/GLImpl/Drawing/GL_Drawing.cpp (1), MG_Backend/Init.cpp (1) | The LOCAL forms are the interesting ones. `MGL_BACKEND_SLOT_LOCAL` answers `Transport == Monolith && slot != nullptr`, i.e. "I may call the backend directly". In the spawn SERVER process that is exactly what the server SHOULD do, and it would answer false if the server carries Transport != Monolith. One expansion is in MG_Backend/Init.cpp - bring-up code that runs in BOTH processes under spawn. c6 must decide whether LOCAL means "monolith" or "the backend is in this process", because under spawn those stop being the same statement. | none; a wrong LOCAL answer is a silently skipped call, not a Fatal | c6 | **overstated** — Restate as '4 definition lines governing 12 live expansion sites, all of them in MG_Impl/GLImpl, none in MG_Backend'. Delete the MG_Backend/Init.cpp sentence - it is the row's main argument for urgenc | +| A6-P4 | p6-must-handle | PATTERN: role-NEUTRAL split-active tests in the shared applier and codec. 9 code lines in MG_Pipe/PipeApply.cpp (11 minus 2 comments) plus MG_Remote/Wire/PipeWireCodec.cpp:1636 and MG_Backend/MGPipe/PipeInputs.cpp:292-294. | MobileGL/MG_Pipe/PipeApply.cpp:1022, :1043, :1311 (NoP4aConsumer), :1487 (MGPipeApplierReleaseObjectRecords), :2139 (the flush-range fault rule), :2245 (staged-texture drop), :2287 (MGPipeApplyMapPersistent's R-6 decline), :3462; MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1636 (AdoptTier consult) | Nothing I can find. These are the most purely mechanical group in the census: `Transport != Monolith` -> `MGPipeSplitActive()` with no second question and no behaviour change. The one to watch is PipeApply.cpp:1311 (NoP4aConsumer), which reaches `ServerSession::Active()` - a singleton whose liveness in the spawn server process is a bring-up ordering question, not a role question. | Fatal{ProtocolCorruption} / Fatal{StageSnapshotTooNarrow} downstream | sm | **overstated** — State it as '8 role-neutral code lines in MG_Pipe/PipeApply.cpp (11 census lines minus 2 comments minus PipeApply.cpp:1353, which is A6-P2a's)'. The verdict p6-must-handle and the 'most purely mechani | +| A6-P5 | blocks-p6 | PATTERN: DIAL / bring-up. The sites that decide whether this process brings up a split at all. 3 code lines in MG_Backend/Init.cpp plus ClientSession::Start's signature and mode refusal. | MobileGL/MG_Backend/Init.cpp:266 (Init's THE SINGLE HOOK), :216 (ClientSessionInstance().Start(MG_Config::Transport, MG_Config::TransportEndpoint)), :236 (ShutdownSplitRoles' early-out); MobileGL/MG_Remote/Client/ClientSession.cpp:474 (Start's signature), :482 (the `mode != InProcess` named refusal); MobileGL/MG_Remote/Client/ClientSession.h:70 | This is where MGPipeShouldDial() has to be born, and the split of InitSplitRoles into a server half and a client half is the actual P6 work. Under spawn the CLIENT must run steps 3-4 without step 1 (no local ServerLoop, so g_applyThreadKey stays 0 - which is what makes every role probe in the client answer correctly for free), and the SERVER must run step 1-2 without step 3. Note Init.cpp:236's ShutdownSplitRoles calls ServerLoopInstance().Stop() UNCONDITIONALLY for M-6's reason; in a client that never created a ServerLoop that is a no-op, but it is a no-op by accident rather than by design. | Init.cpp:271-276 refuses to fall back to monolith when InitSplitRoles fails - the one gate that is already spawn-shaped | sm | | +| A6-P6 | blocks-p6 | The parse itself: `spawn` / `unix:` / `pipe:` are RECOGNISED, logged as "names a transport P6 implements and P5 does not", and then forced back to `MG_Config::Transport = Monolith`. | MobileGL/ConfigLoader.cpp:339-346 (MG_ConfigLoader::InitTransport); the four writes are :328, :332, :344, :350 | This is the first line c6 changes, and the trap is what replaces it. The refusal currently ends in `Transport = Monolith`, i.e. the refusal and the server's own resting state have the SAME spelling. The moment spawn is accepted, "I refused to dial" and "I am the server and do not dial" must stop being the same value or the 148 P1 sites cannot tell them apart. | the build-level gate CONTRACT-P5.md table 3 names (`nm --defined-only libMobileGL.so / grep -i MG_Remote` non-empty in b | c6 | **line-does-not-resolve** — Change the guard cite from Config.h:530-540 to Config.h:435-442. (The row's sibling cite in A6-P8, 'Config.h:424-440 argues it at length', DOES resolve - the 'WHY Transport IS NOT A FeaturesTable MEMB | +| A6-P7 | p6-must-handle | The sites that are ALREADY the natural carrier: named predicates whose entire body is one Transport test. Rewriting these is a one-line change each and it converts a large number of call sites for free. | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:526-528 (PushIsArmed); MobileGL/MG_Remote/Client/GpuWritePending.cpp:125-127 (GpuWriteSetIsClientSide); MobileGL/MG_Backend/DirectGLES/Managers.h:740-742 (ProgramHandleArm), :1146-1152 (VertexInputReadsRecords); MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:272-274 (FramebufferRecordArmIsMandatory), :661 (ResolveBufferBindingSubsystemArm), :3386 (the sampler twin); MobileGL/MG_Backend/MGPipe/PipeInputs.cpp:292-295 (MGPipeRoleSplitActive) | Nothing - but MGPipeRoleSplitActive (PipeInputs.cpp:292) is a NAME COLLISION waiting to happen: it means `Ipc.RoleSplitState && Transport != Monolith` (the P5f dual-block rehearsal), which is strictly narrower than the proposed MGPipeSplitActive(). Two predicates one word apart, one of which is gated on an env knob, is the kind of pair that drifts. | none | sm | | +| A6-P8 | blocks-p6 | The G1 constraint on the rewrite: in a build without MOBILEGL_BUILD_DISAGGREGATED, `inline constexpr TransportMode Transport = TransportMode::Monolith;` | MobileGL/Config.h:566 (the #else arm of the MOBILEGL_BUILD_DISAGGREGATED block); the comment stating why is at Config.h:563-565 | Not a spawn break - a REWRITE break. MGPipeSplitActive() / MGPipeServerArm() / MGPipeShouldDial() must each be `inline constexpr`-foldable to false in the pull build, or 226 sites become real branches and G1 goes red across the whole tree. The SlotCaps.h precedent (:139-147) shows the shape: the pull build gets a DIFFERENT macro expansion, not a runtime-false predicate. | G1 (the pull build's symbol set and codegen must not move) - a build-level gate that WILL go red for this reason | c6 | | +| A6-P9 | p6-must-handle | The FOURTH predicate that already exists and is not one of the three: MGPipeInstalledArm() -> {kNone, kMonolith, kClientWire}, "what the table actually does" as opposed to "what was ASKED for". | MobileGL/MG_Pipe/PipeRoute.h:234-239 (the enum, MGPipeInstalledArm, MGPipeNoteInstalledArm) with the contrast spelled at :235 | It is the honest answer to "am I the client arm" and it has no server value: kClientWire and kMonolith, no kServerApply. If c6 adds Role, this enum either grows a third value or becomes redundant with MGPipeShouldDial(). Leaving BOTH is how the tree ends up with two selectors that can drift apart - exactly what Managers.h:1142-1145 warns about for VertexInputReadsRecords. | MGPipeTablesAreInstalled() (PipeRoute.h:232) | c6 | | +| A6-P10 | later-stage | The backend reaching the ServerLoop singleton to find ITSELF: `if (Transport != Monolith) { if (BackendObject* server = ServerLoopInstance().Backend()) ... }` with the monolith arm reading MG_Backend::pActiveBackendObject. | MobileGL/MG_Backend/DirectGLES/Utils.cpp:50-62 (ActiveBackendFormatCaps); MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:685-699 (ApplyLineWidthState) | Not a break, an unnecessary indirection that becomes a constraint: it means the spawn server process MUST construct a ServerLoop singleton and register its backend there even though nothing in that process needs a loop to route through. If c6 lets the server process put its backend in pActiveBackendObject (there being no BackendObject_Remote to compete for the slot), these two sites stop needing the Transport test entirely - a behaviour-preserving simplification, but only if BOTH are changed together. | Utils.cpp:59-63's named refusal of the mirror fallback | cp | **overstated** — Widen the site list to Utils.cpp:55, VulkanRenderer.cpp:685, DirectVulkan.cpp:868, VertexInputStateFactory.cpp:256, :599, plus the ungated VertexInputStateFactory.cpp:464. Change the framing from 'an | +| A6-P11 | already-handled | Census correction: 12 of the 238 lines are COMMENTS, not code. 226 code lines remain. Counting occurrences of the bare symbol `MG_Config::Transport` (excluding TransportMode/TransportEndpoint) gives 228 non-test occurrences on 226 lines. | Comment-only lines: MobileGL/Config.h:564; MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:2431; MobileGL/MG_Backend/DirectGLES/Managers.cpp:10389; MobileGL/MG_Backend/Init.cpp:263; MobileGL/MG_Impl/Pipe/ResourceTracker.h:55; MobileGL/MG_Pipe/PipeApply.cpp:20, :2134; MobileGL/MG_Pipe/PipeRoute.h:235; MobileGL/MG_Remote/Server/StagedShadow.h:34; MobileGL/MG_Remote/Server/StagedTextureStore.h:93; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:60; MobileGL/MG_State/GLState/BufferState/BufferObject.h:194 | nothing - but a rewrite package sized at 238 will overshoot by 12, and two of the twelve are inside the MG_State sample c6 is told to treat as representative | none | a6 | | +| A6-P12 | blocks-p6 | The MobileGLServer target that MOBILEGL_IPC_SERVER_PATH points at is the P0 spike STUB - an Android-only, OFF-by-default executable built from tools/spikes/server_stub/main.cpp that links no MobileGL code at all. | CMakeLists.txt:31 (the option), :926-949 (the target); MobileGL/MG_IntegrationTest/CMakeLists.txt:1931 (MGL_ITEST_SPLIT_SERVER_PATH = ${CMAKE_BINARY_DIR}/libMobileGLServer.so); tools/trace_replay/CMakeLists.txt:467 (IPC_SERVER_PATH = same); MobileGL/Config.h:452-457 (IpcTable::ServerPath, "P6 consumes it; P5 lands the parse") | a6 item 5's "link it once for real" experiment has no target to start from: the spawn server is not a variant of this executable, it is a NEW target that has to link MG_Backend + MG_Pipe + MG_Remote/Server and answer the question of whether MG_Impl and MG_State come along. Until that target exists, every claim in this audit about "what MG_Config::Transport is in the server process" is a design choice and not an observation. | none - a ctest ENVIRONMENT block naming a nonexistent .so is currently silent | c6 | **line-does-not-resolve** — Change the Config.h cite from :452-457 to :468-473 (struct IpcTable open at :468, comment :469-472, `String ServerPath;` at :473), and add MobileGL/ConfigLoader.cpp:358 as the sole parse site. Everyth | +| A6-P13 | p6-must-handle | Sites where the Transport test is spelled as a ?: SELECTOR across a #if boundary, splitting one expression over preprocessor arms rather than guarding a statement. Mechanically the hardest group to rewrite without changing shape. | MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:8396-8407 and :8413-8421 (useNative / resource selection), :8496-8513 (the twin block), :9361, :9518, :9782, :9831, :13574, :14590, :14831, :15153, :15600; MobileGL/MG_Backend/DirectGLES/Utils.cpp:2363-2366; MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp:3162-3167; MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:958 | Not a semantic break, but a sed-and-commit rewrite will mangle these. I count ~20 of them in DirectGLES.cpp alone by form. They are the concrete reason the answer to "is the rewrite mechanical" is NO for MG_Backend even though every one of them is a pure ServerArm question. | G1 | sm | | +| A6-P14 | p6-must-handle | SUSPECTED ALREADY-WRONG-FOR-INPROC: the client-arm respecify and flush-range follow-ups guard against the apply thread with `!RunsAsTheServerRole()`, but only ONE of the two has a counter that can notice if the guard is wrong. | MobileGL/MG_Impl/Pipe/PipeFill.cpp:975-1000 (respecify: has the ClientWireRecordsEmitted() before/after self-check and Fatal{InitialBytesNotCarried}) vs MobileGL/MG_Impl/Pipe/PipeFill.cpp:1114-1116 (flush-range: `if (Transport != Monolith && !RunsAsTheServerRole() && size != 0) MGPipeEmitResourceSubData(...)` with no check at all) | In the server process a NON-apply thread answers false to RunsAsTheServerRole() and would take the client arm here. The respecify site aborts by name. The flush site emits a resource_subdata into nothing and the picture is wrong until StageSnapshotTooNarrow fires on a later flush. I am flagging this as the asymmetry, not as a live bug: I did not find a path that reaches PipeFill from a non-apply thread in a server-role process today, because today there is no server-only process. | asymmetric: Fatal{InitialBytesNotCarried} at :987 for the respecify, only the downstream Fatal{StageSnapshotTooNarrow} f | sm | | +| A6-P15 | blocks-p6 | ANSWER TO "do the three predicates cover everything": yes for role, no for one axis. Three sites ask a question that is neither SplitActive nor ServerArm nor ShouldDial - they ask "does the backend live in THIS process". | MobileGL/MG_Remote/Client/SlotCaps.h:116 (MGL_BACKEND_SLOT_LOCAL) and :133-136 (MGL_BACKEND_SLOT_PTR_LOCAL), expanded at MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6837 and 13 other call sites including MobileGL/MG_Backend/Init.cpp | Under spawn there are two processes, and in the SERVER process the backend IS local while a transport IS active. The macro's two terms stop being the same question. This is the fourth pattern the task asked me to name if the three did not cover it: LOCALITY, not role. It is small - 2 macro definitions, 14 expansions - but it is real, and folding it into ShouldDial ("if I do not dial, the backend is local") is a coincidence in the current topology rather than a definition. | none - a wrong answer is a silently skipped backend call | c6 | **overstated** — Restate as '2 macro definitions, 2 live expansion sites, both at MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6552 and :6837'. Keep the verdict - the locality/role conflation is real and c6 must rul | + +**复审另行指出的遗漏**: +- MobileGL/ConfigLoader.cpp:408 - `if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;`. This is the SEVENTH Config-dir census line and NO row in the item names it (A6-P6 accounts only for :326, :328, :332, :344, :350 and Config.h:564). It gates the one-line resolved-IPC-configuration log at :411-417 ('Config: IPC ring=%uMiB stage=%uMiB ... role-split-state=%d affinity=...'). It matters directly to this item's own open question #2, which asks for 'a first-boot log line in the server that prints the resolved Role so a wrong answer is visible in the first 20 lines of logcat': the site that would carry that line already exists and is gated on the exact bit under dispute, so a spawn server left at Transport==Monolith prints NOTHING and silences the very diagnostic the audit asks for. +- MobileGL/MG_Backend/DirectGLES/Managers.cpp:1161-1166 - `MG_Remote::Server::StagedShadowStore& ServerStaged() { static ... store = *new StagedShadowStore(MG_Config::Transport != MG_Config::TransportMode::Monolith); return store; }` (the Transport read is census line :1164). This is the exact family twin of StagedTextureStore.h:475, which A6-P1b names as one of its three bring-up-once sites - and MobileGL/MG_Remote/Server/StagedTextureStore.h:466-467 literally says it follows 'ServerStaged() ruling verbatim'. Its own comment at :1155-1157 gives the same once-and-for-all argument ('an answer that changed mid-run would strand every resource already staged'). A6-P1b names the derivative and misses the original. It is also the wider of the two: ServerStaged() governs MGL_SERVER_STAGED_ADOPT/DROP/DROP_ALL/REQUIRE_COVERAGE at Managers.cpp:1191, :1201-1205, :3052, :3398, :3533. +- MobileGL/MG_Impl/Pipe/PipeFill.cpp:2868-2869 and :2913-2914 - the g_fillPlan MEMO KEY: `&& g_fillPlan.CapsGeneration == (MG_Config::Transport != Monolith ? MG_Remote::Client::CapsMirrorInstance().Generation() : 0)` and the matching store. No row names either line. This is the ONLY shape in the whole 226-line census where the Transport answer feeds a CACHE KEY rather than a branch, which makes it the one site where a predicate rename can change WHEN the answer is computed and therefore change cache-invalidation semantics rather than just control flow. It is also a direct counterexample to A6-P7's organising quote from Managers.h:737-739 ('NOT LATCHED... Transport is configuration read once at bring-up'): here the Transport answer selects between a latched generation counter and a constant 0. And CapsMirrorInstance() is client-side structure, so if c6 gives the server process Transport != Monolith this code consults a mirror that has no meaning there. +- MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp:6552 - `MGL_BACKEND_SLOT_LOCAL(GetTextureImage)` (in the `pActiveBackendObject->GetBackendType() == BackendType::DirectVulkan` arm). A6-P15 names only its twin at :6837 and then pads the list with 13 sites that belong to a different macro family. :6552 is the OTHER of the two real locality sites and it is the more interesting one, because it conjoins the locality question with a backend-type question against pActiveBackendObject - which A6-P10 establishes holds BackendObject_Remote on the client (Init.cpp:225-228), so in a spawn client this arm asks 'is the backend local' of an object that by construction is not. +- MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp:256-260, :599-603 and :464, plus MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp:868-873 - four more sites in A6-P10's family (backend reaching ServerLoopInstance().Backend() for its own dynamic parameters). P10 asserts there are exactly two and that they must 'be changed together'. VertexInputStateFactory.cpp:464 is the sharpest of the four: it reads `ServerLoopInstance().Backend()` with NO Transport test at all (inside `#if MOBILEGL_BUILD_DISAGGREGATED` opened at :438) and silently treats a null answer as 'fp64 unsupported' at :465-467. +- MobileGL/MG_Remote/Client/WireTables.cpp:87 - `Bool RunsAsTheServerRole() { return Server::ServerLoop::OnApplyThread(); }`. Not a census line, but the single fact that collapses A6-P2a's two-spelling taxonomy, and the item never opens it despite building a blocks-p6 row on the premise that the two names differ in meaning. c6 needs this on the record: there is exactly ONE role predicate in the tree under two names, so the Role axis has one implementation to redirect, not two. +- MobileGL/MG_Impl/GLImpl/Sync/GL_Sync.cpp:66-67 - the comment `// This is the same pointer expression MGL_BACKEND_SLOT_PTR_LOCAL had in a pull build.` followed at :67 by the RAW `if (const auto backendFenceSync = MG_Backend::gBackendFunctionsTable.GL.FenceSync)`. A6-P3 counts this as an expansion; it is the opposite - a site where the macro was deliberately declined and the locality question left spelled with no Transport term at all. It is a third instance of A6-P15's locality question and the only one that would be invisible to a rewrite that greps for the macro names. + +--- + +## a6.7 abort 普查 +**小结**:At b3b9c2ee there are 92 executable `std::abort()` statements under MobileGL/MG_Remote/ (94 grep hits minus two inside comments: EmitTables.h:25, WireLog.h:49); one of the 92, ClientSession.cpp:44's MGP5_C0_STUB, is defined at :39 and #undef'd at :1549 with zero expansions, so 91 can fire. Roughly 48 of them are reachable from bytes the PEER wrote, split ~27 server-side (records, SEG_STAGE, ctrl frames, the shared control page) and ~21 client-side (SEG_EVENT, reply slots, Welcome, the watermarks the server advances); the other ~44 are self-checks on our own invariants. Two shared funnels multiply that: Wire::WireProtocolFatal/At (PipeWireCodec.cpp:377/382) has 54 call sites, and the generated MGPipeWireProtocolFatal (PipeWire.inc:1177) has 84 MGP_WIRE_CHECK_BOUNDS arms plus the `` default at :1472 - every one of those 139 is peer-reachable by construction. ANSWER TO THE DECISION QUESTION: funnelling into one `Session::Fail(FatalCode, detail)` that is still `[[noreturn]]` and still aborts is a RENAME for all 92 sites plus the ~90 MG_Pipe ones - no site loses anything, because nothing after an abort is reachable today. The expense is entirely in the LATER latch flip, and it is 13 sites where a returning Fail is undefined behaviour with no honest value to return (8 of them reference-returning: EmitTables.cpp:216, WireTables.cpp:120, ServerSession.cpp:272/:698/:713/:724/:731/:739; 5 last-statement non-void: PipeWireCodec.cpp:927/:2312, ServerSession.cpp:262, ServerLoop.cpp:1056, StagedTextureStore.h:344, PersistentMapTracker.cpp:1059) plus ~9 where falling through IS the defect the Fatal exists to prevent (ServerSession.cpp:158's guessed CallMask is the named "marquee defect"; EmitTables.cpp:843's silent zero bpp; PipeWire.inc:1472's unknown-opcode dispatch). The remaining ~70 need one inserted `return`. THE ONE THING c6 MOST NEEDS: `ServerSession::ConsumerDoorbell()`/`ProducerDoorbell()` abort `Fatal{UnmigratedVerb}` for every TransportRole that is not InProcess (ServerSession.cpp:719-724, :735-739), and ServerLoop::ApplyThreadMain calls ConsumerDoorbell() unconditionally at ServerLoop.cpp:373 - so the first spawn session aborts on the apply thread before it pops a single record. That is a blocks-p6 row, not a hardening row. Separately, three guest-reachable unbounded allocations are real and verified: PipeApply.cpp:1566-1568 (CreateRenderState resizes RenderStateCsos to a peer-supplied Cso.Slot+1 with NO slot limit, unlike every sibling family which goes through RecordAt's bound), PipeApplier.cpp:439-440 (readback scratch from peer w*h*bpp), and ProgramArtifactsCodec.cpp:219-220 (count bounded by remaining BYTES then multiplied by sizeof(element)) - and there is not one try/catch anywhere under MG_Remote/, so each one ends in an uncaught std::bad_alloc and an unnamed std::terminate. + +复审结论:**sound-with-fixes**(重新解析 30 条,异议 11 条,另指出 8 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A7-1 | blocks-p6 | ServerSession::ConsumerDoorbell() / ProducerDoorbell() abort Fatal{UnmigratedVerb} for every TransportRole except InProcess; ApplyThreadMain calls ConsumerDoorbell() unconditionally | MobileGL/MG_Remote/Server/ServerSession.cpp:709-724 (ServerSession::ConsumerDoorbell), :727-739 (ServerSession::ProducerDoorbell), reached from MobileGL/MG_Remote/Server/ServerLoop.cpp:373 (ServerLoop::ApplyThreadMain) | The spawn transport's role is not InProcess, so mgl-srv-apply aborts at ServerLoop.cpp:373 before it pops its first record. The abort text says 'that is P6's SocketTransport' - it is the phase's own tripwire, correctly armed, and it fires first. | The Fatal itself is the guard; it goes red for exactly the reason it exists. | so | | +| A7-2 | blocks-p6 | MGPipeApplyCreateRenderState resizes g_applier.RenderStateCsos to a peer-supplied Cso.Slot+1 with NO slot limit; every sibling create-family goes through RecordAt(records, slot, slotLimit) | MobileGL/MG_Pipe/PipeApply.cpp:1563-1569 (MGPipeApplyCreateRenderState) vs :471-476 (RecordAt) and its bounded callers at :1884, :2397, :2650, :2752, :2811, :1210/:1216; decode arm MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1713 | A record with Cso.Slot = 0xFFFFFFFE asks for ~4.29e9 entries of MGPipeRenderStateCsoRecord (8 + kMGPipePipelineChunkBytes = 396, so ~1.7 TB). std::length_error / std::bad_alloc, uncaught, std::terminate - no Fatal line, no family tag. | none - MOBILEGL_ASSERT at :1564 checks only slot != 0 and is inert at INFO | dl | | +| A7-3 | already-handled | Wire::WireProtocolFatal / WireProtocolFatalAt: the [[noreturn]] funnel this layer already has, 54 call sites | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:377-380, :382-386 (definitions); declarations MobileGL/MG_Remote/Wire/PipeWireCodec.h:110-111 | nothing - this is the shape dl wants, one level down. It is the proof that the funnel is a rename: 54 sites already route through two functions. | n/a | dl | **claim-false** — State 93 call sites tree-wide, 54 of them in PipeWireCodec.cpp. Keep the verdict already-handled and the rename argument - it gets STRONGER, not weaker - but c6 must be told the rename touches six fil | +| A7-4 | p6-must-handle | Generated MGPipeWireProtocolFatal: 84 MGP_WIRE_CHECK_BOUNDS arms plus the `` default, all peer-reachable, all [[noreturn]]-relying | MobileGL/MG_Pipe/generated/PipeWire.inc:1177-1181 (definition), :1183-1188 (MGP_WIRE_CHECK_BOUNDS), :1472 (the default arm, inside a Bool function that continues to gMGPipeWireRecordApply dispatch at :1474-1477) | Nothing breaks; it is exactly right. But it is GENERATED, so the funnel rename must be a generator edit, and if the latch ever flips, :1472 needs `return false` or an unknown opcode is handed to the real decoder. | the check itself | dl | **claim-false** — 83 arms plus the default = 84 peer-reachable sites behind MGPipeWireProtocolFatal. Total peer-reachable death sites behind the two named funnels is 177, not 139. | +| A7-5 | later-stage | Eight reference-returning Fatal sites: a returning Fail is a null dereference or a missing return, and there is no value to invent | MobileGL/MG_Remote/Client/EmitTables.cpp:208-217 (RequireSession -> ClientSession&, `return *session;` at :217); MobileGL/MG_Remote/Client/WireTables.cpp:106-121 (RequireSession); MobileGL/MG_Remote/Server/ServerSession.cpp:267-273 (ReverseCallbackOwner -> ServerSession&), :695-701 (Control -> RingControl&, `return *control;` at :701), :713 and :724 (ConsumerDoorbell -> Doorbell&), :731 and :739 (ProducerDoorbell -> Doorbell&) | nothing today. These are the sites that make the LATER latch flip a redesign: a Doorbell& or a RingControl& has no null, so each caller must be restructured to ask a Bool question first. | none needed while Fail is [[noreturn]] | Ph | **line-does-not-resolve** — EmitTables.cpp:208-218, return at :218. ServerSession.cpp:693-701, abort at :698, return at :700. Verdict later-stage / phase Ph unchanged - the row's argument (a Doorbell& or RingControl& has no null | +| A7-6 | later-stage | Five last-statement non-void Fatal sites: a returning Fail is UB (falls off the end of a value-returning function) | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:927 (PipeWireEncoder::StageAllocate -> Uint8*), :2312 (PipeWireDecoder::ApplyChecked -> Bool, the switch default); MobileGL/MG_Remote/Server/ServerSession.cpp:262 (ReserveEventOrBlock -> void*); MobileGL/MG_Remote/Server/ServerLoop.cpp:1056 (ServerLoop::ApplySurfaceControlFrame -> MobileGLResult); MobileGL/MG_Remote/Server/StagedTextureStore.h:344 (RequireLevelBytes -> const Uint8*); MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:1059 (AdoptTierIsEmulate -> Bool) | nothing today. Each has a plausible-but-lying return (nullptr, false, an error code) that a caller would then have to check - StagedTextureStore.h:344's callers dereference the result, so nullptr is the crash one line later. | none | Ph | **claim-false** — Say six sites. Move PipeWireCodec.cpp:2312 into A7-3's funnel bucket where it belongs. Re-file StagedTextureStore.h:344 as its own finding: the function ALREADY has a nullptr return path its two produ | +| A7-7 | later-stage | ServerSession::CallMask() falls through FatalUnsetCallMask to `m_capBits / MGCapsConsumerBits(...)` - the fall-through IS the defect the Fatal names | MobileGL/MG_Remote/Server/ServerSession.cpp:147-159 (FatalUnsetCallMask, [[noreturn]]), called from :415-420 (ServerSession::CallMask, returns Uint64) | nothing today; but it is the clearest example of a site where a returning Fail silently re-creates a named historical defect. Any latch flip must give this one a refusal path, not a return value. | none after the Fatal | Ph | **line-does-not-resolve** — ServerSession.cpp:412-417, call at :414, fall-through at :416, Bool twin at :410. Verdict later-stage / phase Ph unchanged. | +| A7-8 | p6-must-handle | Ring watermark advance: Fatal{ProtocolCorruption,"watermark"} fires on a value read out of the SHARED control page | MobileGL/MG_Remote/Transport/Ring.cpp:364-377 (AdvanceMonotonic), call sites :390 submittedSeq, :394 appliedSeq, :403 retiredSeq, :407 completedFrameSerial, :411 presentAckSerial; page layout MobileGL/MG_Remote/Transport/Ring.h:135-139 | Both processes map the control page read-WRITE (ShmSegment.cpp:283 and :346 both call Map(false); the readOnly parameter at ShmSegment.h:64 has no true caller). A client that stores a huge appliedSeq into the shared page makes the SERVER abort here - a peer-written byte reaching a Fatal that was written as a self-check. | the Fatal itself, but it is aimed at our own bug, not at the peer | cp | **claim-false** — Both roles write cursors into the SAME page: RingControl is one 4096-byte object (static_assert at Ring.h:150), so the row's natural_carrier - 'map the control page read-only on the side that does not | +| A7-9 | p6-must-handle | Four SEG_EVENT decode Fatals: the client aborts on a malformed reverse-channel record | MobileGL/MG_Remote/Client/ClientSession.cpp:268-370 (DrainEventRing): :277 corrupt record stream, :290 kEventBufferWriteback head->Size vs tail, :316 kEventGpuWritten RangeCount vs tail, :362 kEventGlError MessageBytes / missing NUL | Correct as written - these are real peer gates. What changes is the consequence: a server that dies mid-record now kills the app's GL thread with an abort instead of latching the context lost. | the four checks themselves, all named | dl | | +| A7-10 | already-handled | Nine decoder honesty Fatals on peer blobrefs and spans - the densest peer-reachable cluster in the tree | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:455-514 (CheckBlobIsHonest, aborts at :466 :474 :504 :512), :516-523 (RequireDeclaredBlob, :521), :543-565 (CheckHostSpanIsHonest+segments, :562), :1498-1521 (ApplyChecked's tail cross-check, :1520), :1799 (CreateShaderState.Spirv declared), :2074 (ResourceSubData union box) | nothing breaks - this is the part of the census that is already right. It is the bulk evidence that the funnel is a rename: every one is `if (bad) { MGLOG_F(...); std::abort(); }` inside a void or continuing function. | the checks themselves | dl | | +| A7-11 | p6-must-handle | PipeApplier peer-record Fatals: Present.FrameSerial, ApplierReset.ContextSerial, ObjectDeath.Handle/.Kind, the two sampler-window checks | MobileGL/MG_Remote/Server/PipeApplier.cpp:325-334 (OnPresent, FrameSerial 0 under run-ahead), :1144-1157 (OnApplierReset, ContextSerial), :1168-1183 (OnObjectDeath, null handle and Kind >= KindCount), :550-572 (CheckUnitWindows, both arms) | All six are exactly the shape a merely-unusual peer produces - a restarted client whose reset count disagrees, a present serial of 0 from a peer built before P5e. Each is a remote kill of the server. | the checks themselves, all named ProtocolCorruption | dl | | +| A7-12 | already-handled | ServerLoop::DrainRing aborts on a corrupt SEG_CMD record header - the canonical peer gate | MobileGL/MG_Remote/Server/ServerLoop.cpp:617-626 (ServerLoop::DrainRing), detection at MobileGL/MG_Remote/Transport/Ring.cpp:246-262 (RingConsumer::Pop sets *outCorrupt and returns false, no abort) | nothing breaks. Note the good split already present: the TRANSPORT returns a flag, the SESSION decides to die. That is the shape the funnel wants everywhere. | Ring.cpp:251-253's bounds test | dl | | +| A7-13 | p6-must-handle | Wire SurfaceOp validation aborts on the server: an ANativeWindow*, a CAMetalLayer*, an unknown wire kind, an out-of-range windowBackend tag, an undispatchable op | MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:197-218 (ServerApplyWireSurfaceOp); MobileGL/MG_Remote/Server/ServerLoop.cpp:864-871 (WindowBackendFromFrameValue), :1053-1057 (ApplySurfaceControlFrame's unknown-kind tail) | Under spawn the same frame arrives from the peer's flatbuffer, so ServerLoop.cpp:860-863's premise ('an out-of-range tag is a memory-corruption shape, not bad input') is false and the comment must be corrected in the same change. WindowBackendFromFrameValue also falls through to a cast of an out-of-range enum if the Fatal ever returns. | the three checks, all named | sm | | +| A7-14 | p6-must-handle | Handshake aborts on both sides: FatalAbiMismatch on a peer's Hello / Welcome | MobileGL/MG_Remote/Server/ServerSession.cpp:92-108 (FatalAbiMismatch), called at :471-477 (protocol version) and :479-482 (struct fingerprint); MobileGL/MG_Remote/Client/ClientSession.cpp:83-92, called at :566-568 | A mismatched or hostile peer kills the other by sending a Hello/Welcome. Note the refusal the review points at is real and eight lines away: ServerSession.cpp:456-466 already answers MOBILEGL_ERR_PROTOCOL_MISMATCH for a malformed frame without aborting. Also: both FatalAbiMismatch calls fall through into Accept()/Start() continuing, so a returning Fail would mount shared memory for an ABI-mismatched peer. | the fingerprint comparison itself | so | | +| A7-15 | p6-must-handle | Reply-path aborts on the client: the server's status byte and reply size can kill the app | MobileGL/MG_Remote/Client/ClientSession.cpp:1104-1110 (ReplyMissing), :1121-1127 (ReplyTooLarge); MobileGL/MG_Remote/Client/EmitTables.cpp:883-909 (RequireReadbackReplyComplete, four arms at :887 :894 :898 :907); MobileGL/MG_Remote/Client/WireTables.cpp:441-446 and :487-493 and :500-505 | A peer that posts status 2, a short OK payload, or a slot whose header stamps the wrong seq turns a glReadPixels into an abort. The client-side read already refuses an over-cap header.Size by returning false (ReplySlot.h:295-302), which is the non-abort shape to copy. | ReplySlot.h:295-302 bounds header.Size before the memcpy at :314 | dl | | +| A7-16 | p6-must-handle | Four timeout/credit aborts on the client: a peer that simply stops is indistinguishable from a wedge, and the answer is abort | MobileGL/MG_Remote/Client/ClientSession.cpp:1071-1078 (BarrierTimeout), :1206-1214 (forced run-ahead wait), :1271-1279 (PresentCreditTimeout), :911-916 (RetirementWaitFailed, SEG_CMD); and the encoder's twin at MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:921-928 (RetirementWaitFailed, SEG_STAGE) | A server process that is merely slow, swapped out or SIGSTOPped kills the client after kBarrierTimeoutMs. Under spawn 'the server is gone' becomes an ordinary event (SIGKILL, OOM-kill, a crash) and it must latch, not abort - this is precisely the S2 gate the review re-aims at P6.5. | the bounded wait itself | P6.5 | **line-does-not-resolve** — protocol.fbs:254. Trivial, but the item's own doc_drift section corrects a fourteen-line drift in P6-CONTRACT-DRAFT.md for this same enum, so the census should not introduce a one-line one of its own. | +| A7-17 | later-stage | SEG_EVENT overflow policy: the server parks 30 s on a client's bell and then aborts | MobileGL/MG_Remote/Server/ServerSession.cpp:212-263 (ReserveEventOrBlock): :218 non-run-ahead immediate Fatal, :230 record too big for the ring, :244-251 the 30 s wait Fatal, :259-262 the emptied-ring Fatal; constant kEventBacklogWaitMs at :210 | A guest that simply never calls Drained() parks the host's renderer for 30 s and then kills it. The review names this; it is real and the 30 s constant is the whole of the policy. | kEventBacklogWaitMs = 30000 (ServerSession.cpp:210) - bounded, but bounded at a number that is already a hosting outage | Ph | **line-does-not-resolve** — Use :216. And add the cross-reference the row misses: ReserveEventOrBlock takes `Transport::Doorbell& bell = session.ConsumerDoorbell();` at ServerSession.cpp:234, so the whole SEG_EVENT backpressure | +| A7-18 | p6-must-handle | Readback scratch is resized from a peer-supplied w*h*bpp with no absolute cap, and the resize happens BEFORE any size gate | MobileGL/MG_Remote/Server/PipeApplier.cpp:416-441 (ServerVerbSink::OnReadPixels): `tight` computed at :425-430 from info.Box.W/H and the format pair, `m_readbackScratch.resize(tight)` at :439-440 | A record with Box.W = Box.H = 65535 and RGBA8 asks for a 17 GB resize on the apply thread. bad_alloc, uncaught, terminate. The server also then calls PostReply with `tight`, which ReplySlot.h:235-246 would refuse - but only after the allocation. | none on the server side; the only bound is the client's own pre-emit check, which a peer does not have to run | dl | | +| A7-19 | p6-must-handle | Program archive decode: a count bounded by remaining BYTES is then multiplied by sizeof(element) | MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp:166-175 (TakeCount), :216-223 (ArchiveVector arm: `value.resize(count)`); reached from MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1814-1818 (DecodeProgramArchive on peer-staged bytes) | With SEG_STAGE at its default 32 MiB, a forged archive can declare a count of ~32e6 for a vector whose element is tens of bytes, so the resize is one to two orders of magnitude larger than the archive. The outer-then-inner nesting lets a second such resize live at the same time. The top-level DecodeProgramArchive stage count IS bounded (:335-338, kProgramArchiveMaxStages); the recursive arm is not. | TakeCount's byte bound (real, but pre-multiplication) | dl | | +| A7-20 | p6-must-handle | No try/catch anywhere under MG_Remote/, and exceptions are not disabled for MobileGL - every peer-driven allocation ends in an unnamed std::terminate | grep for try/catch over MobileGL/MG_Remote/**.{cpp,h} returns zero handlers (only struct names match); -fno-exceptions appears only under 3rdparty/apitrace. Allocation sites: MobileGL/MG_Pipe/PipeApply.cpp:1567, MobileGL/MG_Remote/Server/PipeApplier.cpp:440, MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp:220, MobileGL/MG_Remote/Server/StagedShadow.h:72, MobileGL/MG_Remote/Server/StagedTextureStore.h:441, MobileGL/MG_Remote/Server/ServerSession.cpp:68, MobileGL/MG_Remote/Client/ClientSession.cpp:67 | Every resource-exhaustion row above lands as std::terminate with no Fatal line, no family tag and nothing on stderr for a death control to match - the exact failure WireLog.h:35-52 was written to end. | none | dl | **claim-false** — Four verified unbounded peer-driven allocations, not three. Add StagedTextureStore.h:441 as its own blocks-p6 row with the StagedTextureRunImageOffset analysis, and note that StagedTextureStore::Adopt | +| A7-21 | later-stage | MOBILEGL_ASSERT compiles out at INFO (the production default), so every assert-only diagnostic on a peer-supplied handle is silent in shipping builds | MobileGL/Defines.h:104-115 (MOBILEGL_ASSERT is live only when MOBILEGL_LOG_ACTIVE_LEVEL <= DEBUG, and expands to TRAP, not abort); peer-reachable users MobileGL/MG_Backend/DirectGLES/SlotTables.h:347-351 and :370-376, MobileGL/MG_Pipe/PipeApply.cpp:1564-1565 | A forged handle takes the silent refusal path. IMPORTANT NEGATIVE RESULT: in SlotTables.h the REFUSAL is a real statement (`return m_nullTwin;` at :351 and :376) outside the assert, and PipeApply.cpp:660-700's resolvers count every refusal into RefusedResourceCalls / RefusedObjectCalls - so the BOUND holds in release and the drop is observable. What is missing is the NAME, not the bound. | the real `return` statements beside the asserts, plus the Refused* counters (PipeApply.cpp:663-700) | Ph | | +| A7-22 | p6-must-handle | Staged-shadow coverage Fatals: the server aborts because a record the peer should have sent did not arrive | MobileGL/MG_Remote/Server/StagedShadow.h:99-116 (RequireCoverage), MobileGL/MG_Remote/Server/StagedTextureStore.h:326-345 (RequireLevelBytes) | This is the purest 'merely unusual peer message' case in the census: a peer that draws from a buffer it never filled kills the host renderer with Fatal{StageSnapshotTooNarrow}. RequireLevelBytes returns const Uint8* and its callers dereference it, so it is also an A7-6 site. | the coverage set itself (StagedShadow.h CoverageHas) | dl | | +| A7-23 | p6-must-handle | Two abort sites carry NO Fatal{...} family tag, so a verbatim funnel leaves them invisible to the family grep | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:1040-1060 (AdoptTierIsEmulate: two MGLOG_F lines, neither with a Fatal{} marker, then abort at :1059); MobileGL/MG_Remote/Transport/WireLog.cpp:34-51 (WireLogFatal formats whatever its caller passed - four ReplySlot.h callers do carry Fatal{}, Ring.cpp:370 does too) | nothing breaks, but the review's promise that the funnel keeps the log 'verbatim' means these two arrive at Session::Fail with no FatalCode to map to. AdoptTierIsEmulate is also a value-returning function whose abort is its last statement (A7-6). | none - a CI grep for Fatal{ would not see either | dl | | +| A7-24 | already-handled | Control-frame receive resizes a vector from a peer-declared frame length - bounded, and the bound is a named refusal already | MobileGL/MG_Remote/Server/ServerSession.cpp:58-70 (ReceiveEnvelope, `out.resize(size)` at :68) and MobileGL/MG_Remote/Client/ClientSession.cpp:59-69 (:67); the bound is MobileGL/MG_Remote/Transport/Framing.h:48 (kMaxFramePayloadSize = 64 MiB) enforced at :173-179 with a WireLogError and MOBILEGL_ERR_PROTOCOL_MISMATCH, plus the send-side check at :58-63 | Only that 64 MiB per frame is still a large peer-driven allocation on a control plane whose real messages are kilobytes. Not a blocker. Recording it so c6 does not re-derive it: the framing layer is the ONE place in the tree that already does refusal-not-abort on a peer-supplied size, and it is the model for the rest. | Framing.h:173-179, a real named refusal that can go red | dl | | +| A7-25 | no-action | MultiDraw arrays resized from a peer-supplied NumDraws - bounded transitively by the record-size cross-check | MobileGL/MG_Remote/Server/PipeApplier.cpp:676-710 (ServerVerbSink::OnDrawVbo's multi-draw arm), resizes at :687 :690 :699 :700 from info.NumDraws; the bound is MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1511-1521 (size == layout.TotalBytes) composed with MobileGL/MG_Remote/Transport/Ring.cpp:250-262 (size <= available && offset+size <= capacity) | nothing. NumDraws is capped at (SEG_CMD capacity)/sizeof(MGPDrawRange), ~350k at the 8 MiB default, so the four resizes are at most a few MB. A legitimate and useful negative result: the tail cross-check at :1520 is load-bearing as an ALLOCATION bound, not just a decode bound, and anything that weakens it re-arms this row. | PipeWireCodec.cpp:1511-1521, and it can go red for exactly this reason | dl | | +| A7-26 | p6-must-handle | Encoder-side Fatals in PipeWireCodec are CLIENT self-checks, not peer-reachable - but two of them wait on a watermark the peer advances | MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:834-928 (PipeWireEncoder::StageAllocate): :865 blob larger than SEG_STAGE at any occupancy (self), :912 and :927 the retirement wait (peer-paced); :973-1145 (EncodeRecord): :1030 record over MaxRecordBytes (self), :1140 a declared CreateShaderState.Spirv run (self) | :912 and :927 are the sites where a server that stops retiring kills the client after 5 s; the other two stay honest self-checks that never involve a peer byte. Worth keeping the distinction in the funnel's detail string so an operator can tell which half was wrong. | the 5000 ms bell wait at :917 | dl | | +| A7-27 | no-action | Dead abort site: MGP5_C0_STUB is defined and #undef'd with zero expansions | MobileGL/MG_Remote/Client/ClientSession.cpp:39-44 (definition, abort at :44), :1549 (#undef); no expansion anywhere in the tree | nothing. Recording it so the census count is honest: 92 statements, 91 that can execute. | n/a | dl | | +| A7-28 | p6-must-handle | PipeRoute's six reply-mailbox Fatals and PipeApply's three role/emulation Fatals are applier-reachable and must be in the funnel's scope, but are NOT peer-reachable | MobileGL/MG_Pipe/PipeRoute.cpp:85-97 (MGPipePostReply), :105-120 (TakeReply), :137-143 and :148-153 (TakeReplyBool/Pointer), :258-267 (Mono_SetResidualValueState); MobileGL/MG_Pipe/PipeApply.cpp:1353-1360 (MGPipeApplierReset off the apply thread), :3460-3467 (MGPipeUnmigratedEmulation), :63-70 (MGP_TRIP_WIRE_REPORT); MobileGL/MG_Pipe/generated/PipeFilled.inc:412-416 (MGPipeInputPoisonFatal) | Nothing peer-driven. Flagging two things c6 needs: (a) MGP_TRIP_WIRE_REPORT at PipeApply.cpp:63-70 only aborts under MOBILEGL_PIPE_POISON/VERIFY and is MGLOG_E otherwise, so it is a third policy alongside abort and assert and the funnel must not silently absorb it; (b) MGPipeInputPoisonFatal is a second [[noreturn]] funnel in generated code with its own family word. | n/a | c6 | | +| A7-29 | p6-must-handle | Both processes will map every segment read-write; the readOnly parameter exists and has no true caller | MobileGL/MG_Remote/Transport/ShmSegment.h:64 (Map(bool readOnly)), :71 (MappedReadOnly); the only two callers are MobileGL/MG_Remote/Transport/ShmSegment.cpp:283 and :346, both Map(false); POSIX prot at ShmSegmentPosix.cpp:159, Win32 at ShmSegmentWin32.cpp:129 | Every shared cursor, watermark and parked flag (Ring.h:102-147) is writable by both sides, which is what makes A7-8 a peer-reachable abort and what turns SEG_REPLY and SEG_EVENT into surfaces the peer can scribble on. The plumbing to do better is already there and unused. | none today | cp | **claim-false** — Restate as: the RO path is built AND tested (FdPassingTest.cpp:116, :188); only the two production call sites pass false, deliberately and with a comment saying why. Then add the contradiction the row | +| A7-30 | blocks-p6 | The schema's FatalCode has 7 values; the tree's Fatal{} vocabulary is 30 distinct family words plus 2 untagged sites | MobileGL/MG_Remote/Protocol/protocol.fbs:248-256 (FatalCode: None, ProtocolCorruption, RingOverrun, SegmentMismatch, DeviceLost, ServerCrashed, AbiMismatch) and :258-261 (table Fatal); the only reference to the enum in C++ is MobileGL/MG_Test/Wire/ProtocolSmokeTest.cpp:125 | The funnel's signature Session::Fail(FatalCode, detail) cannot carry today's 30 families in its first argument. Counts by family across the 92 sites: ProtocolCorruption 37, RoleViolation 8, RingOverrun 4, UnmigratedVerb 4, EventRingOverflow 4, NoClientSession 3, ReplyError 3, AbiMismatch 2, RetirementWaitFailed 2, BarrierTimeout 2, StageSnapshotTooNarrow 2, and 20 families with one site each, plus 2 untagged. Only 3 of the 30 map onto an existing FatalCode; SegmentMismatch has zero producers and PipeWireCodec.cpp:495-509 argues explicitly AGAINST minting a family for the case it names. | none - nothing today can notice the mismatch | c6 | **overstated** — Give c6 the family LIST (31 words, reproducible by `grep -rn 'Fatal{[A-Za-z]' MobileGL/MG_Remote/ / grep -v '^[^:]*:[0-9]*: *//' / grep -o 'Fatal{[A-Za-z]*' / sort -u`) and drop the invented per-famil | + +**复审另行指出的遗漏**: +- MobileGL/MG_Remote/Server/StagedTextureStore.h:441 - a FOURTH unbounded peer-driven allocation the item lists as an allocation site (in A7-20) but never flags. `shadow.Bytes.resize(needed, 0)` in CopyRunInto, needed = StagedTextureRunImageOffset(upload, regions) + byteSize. The offset function at MobileGL/MG_Remote/Server/StagedTextureStore.h:178-193 guards only `upload.Blob.Size != boxBytes` (:190); with box.D == box.H == 1 that reduces to Blob.Size == box.W * texelBytes and leaves box.Z, box.Y, regions[0].SrcSliceStride and SrcRowStride entirely free. Returned offset at :192-193. Reached from MobileGL/MG_Pipe/PipeApply.cpp:1037-1039 and MobileGL/MG_Backend/DirectGLES/Managers.cpp:2738-2740. Deserves its own blocks-p6 row beside A7-2 / A7-18 / A7-19. +- MobileGL/MG_Remote/Server/StagedShadow.h:72 - a FIFTH candidate. `if (shadow.Bytes.size() < needed) shadow.Bytes.resize(needed, 0);` with `needed = std::max(width, offset + size)` at :71. Reached through the macro at MobileGL/MG_Backend/DirectGLES/Managers.cpp:1200-1201. MobileGL/MG_Pipe/PipeApply.cpp:707-711 (BufferRangeFault) bounds offset+size against `width`, but nothing I found bounds `width` itself - it is the resource's peer-declared storage. INFERRED, not verified: I did not trace create_resource's width validation to the end, so c6 should resolve this one before acting on it. +- MobileGL/MG_Remote/Server/QueryServer.inc:5, :12, :16, :21, :34, :50, :80, :91, :111, :118, :130, :144 - twelve server-side Wire::WireProtocolFatal arms on peer-supplied query records (handle, Kind, lifecycle state, `request.Wait > 1`, `request.Reserved`). Peer-reachable by construction, in the funnel's rename scope, named by no row. A7-3 swallows them in a count that is itself wrong. +- MobileGL/MG_Remote/Server/PipeApplier.cpp:104, :111, :116, :121, :132, :144, :155, :168, :209, :239, :255, :349, :354, :359, :618, :745, :1066 - seventeen more server-side funnel Fatals on peer fence / clear / readback / draw / stream-output records. A7-11 covers only six PipeApplier std::abort sites (:333, :1156, :1176, :1182, :562, :571) and none of these seventeen, because the census grepped `std::abort` and these route through Wire::WireProtocolFatal. +- MobileGL/MG_Remote/Transport/ReplySlot.h:196, :227, :232, :239 and MobileGL/MG_Remote/Transport/Ring.cpp:370 - the five WireLogFatal call sites. Every one is a death site that carries a Fatal{ tag, and NONE is visible to the `std::abort` grep the whole census is built on. The item's c6 ask #4 asks the right question (count aborts, or Fatal{ tags, or both?) but the item's own answer - 92 statements / 91 reachable - only works for the abort grep. The two greps disagree by far more than the two sites the item names: ~98 extra Fatal{-tagged death sites (93 WireProtocolFatal/At + 5 WireLogFatal) are invisible to it. +- NEGATIVE RESULT the item should have recorded, and the direct answer to A7-19's natural_carrier: MobileGL/MG_Remote/CapsCodec.cpp:318 (`count > r.RoomFor(sizeof(Int32))`) and :426 (`extensionCount > r.RoomFor(sizeof(Uint32))`) already do exactly what A7-19 asks ProgramArtifactsCodec to do - bound the count by remaining-bytes-divided-by-element-size BEFORE the resize at :324 and :430. RoomFor is defined at MobileGL/MG_Remote/CapsCodec.cpp:162-164 as `m_left / elementBytes`. This is the in-tree model for the fix and neither A7-19 nor any c6 ask cites it. +- NEGATIVE RESULT worth recording so c6 does not re-derive it: MobileGL/MG_Remote/Wire/PipeWireCodec.cpp:1948 `overrideNames.reserve(static_cast(count))` is SAFE. count is `layout.TailBytes[2] / sizeof(MGPProgramStorageOverride)` at :1947 - a division, not a multiplication - and TailBytes is bound by the tail cross-check at :1511-1521. Same shape as A7-25's MultiDraw negative result, same load-bearing guard. +- The design contradiction behind A7-29 and c6 ask #6: MobileGL/MG_Remote/Transport/ShmSegment.h:62-63 states the peer gets SEG_CMD read-only on the server side, but the RingControl page lives inside SEG_CMD (MobileGL/MG_Remote/Transport/SessionRings.h:165) and the server writes four of the five watermarks into it (SessionRings.h:439, Ring.cpp:593, :637, :645). The header's stated P6 design cannot be implemented as written. Neither A7-8 nor A7-29 notices, and c6 ask #6 asks the question in a form that presumes the design works. + +--- + +## a6.8 窗口句柄与分片 +**小结**:PART A: there are exactly FOUR store statements to `m_windowHandle` in the whole tree (`BackendObject.cpp:475`, `:365`, `:206`, `:207`) and exactly FIVE reads (`BackendObject_DirectGLES.cpp:901`, `:963`, `:964`, `BackendObject_DirectVulkan.cpp:338`, `:343`). The corpus's "SetWindowHandle is the sole injection point" is false in the way the integrator said, but the fix is smaller than the review implies: the only full store, `SetWindowHandle` (`:473-475`), is reached from TWO callers - the wire op (`ServerLoop.cpp:1016`) and `ActivateEGLSurface` (`:283`) - and `ActivateEGLSurface` takes its value from `m_eglSurfaces[surface].Window`, written at exactly one place, `RegisterEGLWindowSurface` (`:232-238`). So a server-owned window needs ONE substitution at `:235`, ONE refusal at `ServerLoop.cpp:1015-1017`, ONE preserve in `ResetEGLRuntimeState` (`:365`), and - the non-obvious fourth - a new dedup key for `BackendObject_DirectGLES.cpp:962-964`, whose `sameHandle` compares the CLIENT's window pointer against the server's latched one and would therefore destroy and rebuild the native context on every `eglCreateWindowSurface`. The `Width`/`Height` writes at `:206-207` clobber nothing today because NOTHING READS THEM: `m_windowHandle.Width`/`.Height` have zero readers at this head. PART B: `resource_subdata`/`buffer_subdata_resident` (buffer ranges), texture levels (three-arm slab walk, not just whole-width), the persistent-map push and `GetTextureImage` all HAVE chunking walks; `create_shader_state`'s reflection+SPIR-V archive, `set_global_constants`' whole default UBO, `set_program_bindings`' three tails, `draw_vbo`'s `MGPDrawRange[NumDraws]` tail and `read_pixels`' single reply slot DO NOT. The corpus's "host index span" is wrong: `kDrawHasUserIndices` has NO producer at this head - the only occurrence in an emitter is the CLEAR at `OwnedDrawInputs.h:51` - so the largest unchunked client->server blob is the program archive and the largest unchunked TAIL is a multi-draw range array. The one thing c6 most needs: a stream link window must be sized against `create_shader_state`'s archive (the only unbounded blob) and `draw_vbo`'s range tail (the only unbounded tail), not against the index span the corpus names. + +复审结论:**sound-with-fixes**(重新解析 31 条,异议 6 条,另指出 6 处遗漏) + +| # | 判定 | 事实 | 站点 | spawn 下如何坏 | 守卫 | 归属 | 复审 | +|---|---|---|---|---|---|---|---| +| A8-1 | p6-must-handle | `m_windowHandle = handle;` - THE ONLY full store to the window handle in the tree | MobileGL/MG_Backend/BackendObject.cpp:473-475 (BackendObject::SetWindowHandle) | the `void*` is an ANativeWindow*/HWND/XID in the CLIENT process. The server's copy would name client memory. Nothing in the server can dereference it | SurfaceOpCodec.cpp:161 / :202 `Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"}` and :165 / :208 `MetalLayer@P12` - b | cp | | +| A8-2 | p6-must-handle | `backend->SetWindowHandle(UnpackWindowHandle(frame))` - the wire seam the corpus calls the SOLE injection point | MobileGL/MG_Remote/Server/ServerLoop.cpp:1015-1017 (ApplySurfaceControlFrame, case SurfaceControlOp::SetWindowHandle); unpacker at :875-882; only poster is ServerLoop.cpp:1173-1178 (ServerSetWindowHandle), whose only caller is MG_Remote/Client/BackendObject_Remote.cpp:215 | the token is a foreign address. It IS a seam a server-owned window must refuse - but deleting it is NOT sufficient, because A8-3 re-injects the same value | WindowBackendFromFrameValue's Fatal{ProtocolCorruption,"SurfaceOp.windowBackend"} (ServerLoop.cpp:864-873) covers a bad | cp | | +| A8-3 | blocks-p6 | `SetWindowHandle(surfaceState->Window)` - THE SECOND SEAM the corpus missed | MobileGL/MG_Backend/BackendObject.cpp:282-283 (BackendObject::ActivateEGLSurface, Window arm) | with the server owning its own window, this overwrites the server's latched handle with the client-supplied copy held in m_eglSurfaces, on every activation - including both MakeEGLCurrent activation paths, which have no SetWindowHandle op in front of them at all | none | P12 | **wrong-verdict** — Demote A8-3 to `p6-must-handle` at phase P12, matching A8-4/A8-5/A8-6/A8-7/A8-9/A8-11/A8-15. Reserve `blocks-p6` for the rows that genuinely gate spawn bring-up: A8-13 (the refusal's side of the wire) | +| A8-4 | p6-must-handle | `.Window = handle` - the ONLY write to the per-surface copy that A8-3 re-derives from; the smallest latch point | MobileGL/MG_Backend/BackendObject.cpp:232-238 (BackendObject::RegisterEGLWindowSurface); field declared at BackendObject.h:632 | holds the client's window pointer for the life of the surface, so every later activation re-clobbers | RegisterEGLWindowSurface refuses `handle.Backend == Unknown // !handle.Handle` (:227-229) - a null/untagged handle, not | P12 | | +| A8-5 | p6-must-handle | MakeEGLCurrent path 1: first activation when no surface is initialised - reaches A8-3 with no SetWindowHandle op in front of it | MobileGL/MG_Backend/BackendObject.cpp:317-322 (`if (!m_eglSurfaceInitialized) { if (draw != read // !ActivateEGLSurface(draw)) ... }`) | a window handle reaches the driver through a path the wire's SetWindowHandle op never touched - so a P12 design that only guards the wire op has a hole here | none | P12 | | +| A8-6 | p6-must-handle | MakeEGLCurrent path 2: switching the current surface - the second no-op-free route to A8-3 | MobileGL/MG_Backend/BackendObject.cpp:332 (`if (draw != m_eglSurface && !ActivateEGLSurface(draw))`) | same as A8-5; ActivateEGLSurface's early-out at :278-280 only covers the identical-surface repeat | none | P12 | | +| A8-7 | p6-must-handle | `m_windowHandle = {};` - the ZEROING clobber | MobileGL/MG_Backend/BackendObject.cpp:359-367 (ResetEGLRuntimeState), reached from BackendObject.cpp:429 (DestroyPendingEGLSurfaceIfUnused), :461 (ReleaseEGLSurface), :467 (ReleaseEGLResources), BackendObject_DirectGLES.cpp:969-972 (destroy-recreate inside CreateEGLWindowSurface) and :1000-1004 (inside CreateEGLPbufferSurface) | a server-owned window is zeroed by a client-driven release or by a pbuffer create, and the next InitWindowSurface (A8-9/A8-11) then builds on a null window. DirectVulkan's InitWindowSurface names that refusal (`:338-341`); DirectGLES does not check at all | BackendObject_DirectVulkan.cpp:338-341 logs and returns false on a null handle. DirectGLES has no equivalent | P12 | | +| A8-8 | later-stage | `m_windowHandle.Width/.Height = width/height` - the geometry writes, and the finding that NOTHING READS THEM | MobileGL/MG_Backend/BackendObject.cpp:205-208 (ResizeEGLWindowSurface, gated on `m_eglSurface == surface`); the surface-state twin at :203-204; reached from EGLImpl.cpp:734, ServerLoop.cpp:922-925, BackendObject_Remote.cpp:234, BackendObject_DirectVulkan.cpp:430 | nothing - these two stores are dead today. They become a clobber only on the day the server owns its own extent, which is exactly P12 | n/a | P12 | | +| A8-9 | p6-must-handle | reader 1: `reinterpret_cast(m_windowHandle.Handle)` -> DirectGLES::InitWindowSurface(nativeWindow) | MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:899-906 | dereferences a client-process ANativeWindow*. No null check and no tag check - a garbage pointer reaches eglCreateWindowSurface | none at this site; only the wire decoder's Fatal (A8-1) | P12 | | +| A8-10 | p6-must-handle | reader 2: the `sameHandle` dedup key compares the INCOMING client handle against the latched one - the fourth edit point a server-owned window forces | MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:962-967, with the destroy-recreate it gates at :969-972 | with the server owning the window these NEVER match, so `sameHandle` is always false and every create runs DestroyEGLContext + ResetEGLRuntimeState - a full native context rebuild per surface create, plus the A8-7 zeroing. The dedup key must become the client's SURFACE token, not the window pointer | none - it degrades silently into a rebuild, which is a performance and lifetime fault, not a refusal | P12 | | +| A8-11 | p6-must-handle | reader 3: DirectVulkan's InitWindowSurface null-checks then casts the handle | MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:337-352 | same as A8-9. Note the null check (`:338`) is the ONLY guard of its kind in either backend, and it catches A8-7's zeroing but not a foreign pointer | MGLOG_E + return false at :339-341 | P12 | | +| A8-12 | no-action | the CLIENT's own m_windowHandle instance (BackendObject_Remote inherits the base) is WRITE-ONLY | MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:210-226 (CreateEGLWindowSurface calls the base at :225, which reaches BackendObject.cpp:283 -> :475) and :145-150 (InitWindowSurface is a no-op that reads nothing) | nothing. The client's copy is harmless dead state and needs no P6 or P12 work | n/a | P6.5 | | +| A8-13 | blocks-p6 | the AndroidNativeWindow@P12 refusal fires in the SERVER process, after the record has crossed; the client emits and the ENCODER does not refuse | encoder: MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp:130-136 (EncodeSurfaceOpFrame) + :99-108 (WireWindowKindForWindowBackend maps Android -> AndroidNativeWindow and returns true). decoder refusal: :159-166, :199-213. client emitter with no pre-check: MobileGL/MG_Remote/Client/BackendObject_Remote.cpp:214-216 | on the rooted Redmi the first eglCreateWindowSurface from FCL/Minecraft will `std::abort()` the SERVER process. The CLIENT sees a dead peer / EOF, not a named refusal in its own log. The discipline says 'always a NAMED refusal'; here the name is in the corpse | the Fatal exists (SurfaceOpCodec.cpp:202-206) but is on the wrong side of the wire for diagnosis | c6 | **overstated** — Keep the verdict blocks-p6 - the decision is real and c6 must make it - but restate the mechanism: at b3b9c2ee the wire surface-op codec is reachable only from unit tests, so the Fatal has never run a | +| A8-14 | already-handled | the two client-side WindowHandle construction sites and the compile-time backend tag | MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:135-140 (CreateWindowSurface) and :692-697 (CreatePlatformWindowSurface); DetectWindowBackend at :59-71 | DetectWindowBackend is `#if defined(__ANDROID__)` - it answers for the CLIENT's compile target and is carried as truth about a window the SERVER will try to use. Under spawn both processes are the same build so the tag is right; it is the POINTER beside it that is wrong | the decode-side refusal (A8-13) | P6.5 | | +| A8-15 | p6-must-handle | the release paths that reach the zeroing store | MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp:313-315 (DestroySurface -> ReleaseEGLSurface) and :326-329 (Terminate -> ReleaseEGLResources); forwarded at BackendObject_Remote.cpp:300-311; dispatched at ServerLoop.cpp:986-1010 | a client-driven eglTerminate zeroes the server's window handle (A8-7) and tears down the server's context. With a server-owned window whose lifetime is the Service's (P12), the client must not be able to zero it | the blocking-by-contract handshake preserves ORDER, not ownership | P12 | | +| B8-1 | already-handled | resource_subdata / buffer_subdata_resident: CHUNKED. The walk cuts at MGPipeStageChunkBytes() = clamp(stage/4, 4096, stage) = 8 MiB by default | MobileGL/MG_Impl/Pipe/PipeFill.cpp:1011-1017 (MGPipeContentChunkCap), :1020-1052, :1054-1085; walk at MobileGL/MG_Impl/Pipe/ResourceTracker.h:250-267; budget at MobileGL/MG_Remote/Client/GpuWritePending.cpp:205-233 | nothing | PipeWireCodec.cpp:855-864 and :918-927 Fatal{RingOverrun, "SEG_STAGE"} as the backstop | P8 | | +| B8-2 | already-handled | texture level upload: CHUNKED, and with THREE arms, not just whole-width slabs - depth-slices, then whole-row runs, then sub-row texel runs | MobileGL/MG_Impl/Pipe/TextureEmit.h:446-504 (MGPipeForEachTextureSlab; :472-479 slices, :480-491 rows, :492-503 texels), driven at :1499-1517 | nothing | same SEG_STAGE Fatal | P8 | | +| B8-3 | p6-must-handle | the texture walk's SILENT FALLBACK to an uncut whole-level record when the pitch does not tile the level exactly | MobileGL/MG_Impl/Pipe/TextureEmit.h:457-459 (`if (sliceStride * depth != levelBytes) return false;`) and the caller's `if (!split)` arm at :1518-1531 | a level larger than the stage arena that also fails the tiling test falls back to one whole blob and dies at PipeWireCodec.cpp:858 Fatal{RingOverrun,"SEG_STAGE"} with a message that says 'report the record type with no cut' - which is wrong here: the type HAS a cut and declined to use it | the SEG_STAGE Fatal fires, but names the wrong cause. There is no log line at the `return false` itself | P8 | | +| B8-4 | p6-must-handle | create_shader_state: NO CHUNKING WALK. The whole LinkArtifacts + SpirvArtifacts archive (up to 6 SPIR-V modules) is staged as ONE blob | MobileGL/MG_Remote/Client/WireTables.cpp:514-559, staged whole at :556 (`record.Reflection = session.Encoder().StageBytes(archive.data(), archive.size())`); catalogue row MobileGL/MG_Pipe/PipeCalls.def:201; payload MobileGL/MG_Pipe/MGPipeTypes.h:610-641 | unchanged mechanically, but it is the LARGEST unbounded client->server blob and therefore the number a stream link window must be sized against. Worst case = sizeof(all six generatedSpirv modules) + the reflection archive; there is no ceiling in the code and no measurement of the distribution (MEASUREMENTS.md:342 says the post-chunking per-blob byte distribution is unmeasured) | PipeWireCodec.cpp:855-864 Fatal{RingOverrun,"SEG_STAGE"} at stage capacity (32 MiB default); WireTables.cpp:552-555 Fata | P8 | **line-does-not-resolve** — Fatal{ArchiveEmpty} is at WireTables.cpp:549-552 (MGLOG_F at :550). The six per-stage blobrefs are MGPipeTypes.h:635 (`MGPBlobRef Spirv[6]; // per stage`), zeroed at WireTables.cpp:555. Note also that | +| B8-5 | later-stage | set_global_constants: NO CHUNKING WALK. The whole default uniform block is one blob | MobileGL/MG_Impl/Pipe/ProgramEmit.h:202-241 (size from `program->GetUBOSize()` at :209, routed with that size at :233-234); staged via MobileGL/MG_Remote/Client/WireTables.cpp:221 (MGP_WIRE_BLOB); catalogue MobileGL/MG_Pipe/PipeCalls.def:215; payload MGPipeTypes.h:1092-1099 | unchanged mechanically. Realistic worst case is bounded by what the shader compiler produced for the default block - practically <= 64 KiB (GL_MAX_UNIFORM_BLOCK_SIZE's floor) - so it is far below the program archive and should NOT drive the link window size, contrary to the corpus which names it alongside the index span | the SEG_STAGE Fatal | P8 | | +| B8-6 | no-action | set_program_bindings: NO CHUNKING WALK on its three tails; the NAMES are staged one at a time so each name is small, but the three tails are one reservation | MobileGL/MG_Remote/Client/WireTables.cpp:575-625 (per-name StageBytes at :597, the three tails at :611-621, emitted at :622-623); catalogue MobileGL/MG_Pipe/PipeCalls.def:340; header MGPipeTypes.h:1077-1085 | nothing mechanically; the tails are bounded by reflection, not by application data | the record bound, PipeWireCodec.cpp:1019-1031 Fatal{RingOverrun,""} at MaxRecordBytes (half the SEG_CMD ring, 4 MiB | P8 | | +| B8-7 | p6-must-handle | draw_vbo's MGPDrawRange[NumDraws] tail: NO CHUNKING WALK. The largest unbounded TAIL in the catalogue | MobileGL/MG_Remote/Client/EmitTables.cpp:536-545 (`tails[0] = {ranges, numDraws * sizeof(MGPDrawRange)}`); MGPDrawRange is 12 bytes (MGPipeTypes.h:1620-1624); catalogue PipeCalls.def:261 | unchanged mechanically, but this is the ONE hot-path record whose size is application-controlled and unchunked. A glMultiDrawElements with ~350k sub-draws exceeds a default 4 MiB MaxRecordBytes. MEASUREMENTS.md:340 itself flags 'oversized DrawVbo tail' as a named unit refusal and :133 says the index/indirect tails 'still must be recorded' | PipeWireCodec.cpp:1019-1031 Fatal{RingOverrun,"DrawVbo"} - a named refusal, with no cut | P8 | **line-does-not-resolve** — Drop the MEASUREMENTS.md:133 cite - it names nothing. Keep MEASUREMENTS.md:340, which carries the whole load by itself. In the `open` block, the measurement instrument is at EmitTables.cpp:1067-1073 ( | +| B8-8 | already-handled | CORRECTION: draw_vbo's host index span (kDrawHasUserIndices MGHostSpan) has NO PRODUCER at this head - the corpus names it as a remaining unchunked type and that is wrong | the only occurrence of kDrawHasUserIndices in any emitter is the CLEAR at MobileGL/MG_Impl/Pipe/OwnedDrawInputs.h:51; client index arrays become owned buffer resources at :46-56 and travel as ordinary chunked resource_subdata. EmitTables.cpp:536-543 only ever fills tails[1] with MGPDrawIndirect. The decode-side path (PipeWireCodec.cpp:2157-2178, :565-585) is live but unreachable | nothing - there are no host spans on the wire at all (kCapNeedsHostIndexBytes/kCapNeedsHostUboBytes are 0 for the whole of P5 by ruling, MG_Backend/Init.cpp:171-173) | CheckHostSpanIsHonest's four arms (PipeWireCodec.cpp:542-563) including the segment-range arm, plus CheckDrawUserIndices | P8 | | +| B8-9 | already-handled | set_shader_buffers' OPTIONAL second host-span tail: no producer | MobileGL/MG_Remote/Client/WireTables.cpp:227-232 (the generic MGP_WIRE_TAIL carries the FIRST tail only, and says so); the bit that would ask for it is kCapNeedsHostUboBytes, never published (MG_Backend/Init.cpp:171-173, :184) | nothing | the cap-bit ruling | P7 | | +| B8-10 | no-action | the six structurally bounded blobs with no chunking walk and no need for one | MobileGL/MG_Pipe/PipeCalls.def:187 CreateRenderState (RenderStateParameters chunk bytes, <= 1168 B - MGPipeTypes.h:471-478), :190 CreateVertexElements (MGPVertexAttribWire[<=32] + binding points - :522-532), :197 CreateSamplerState (SamplerParameters POD - :539), :205 SetDynamicState (dynamic chunks of the same 1168 B, staged OPTIONALLY - WireTables.cpp:255-268), :224 SetResidualValueState (MGL_RESIDUAL_BLOCK_SIZE == 8 - MGPipeTypes.h:1185-1197), :297 SetStorageBlockBinding (one NUL-terminated block name) | nothing | the SEG_STAGE Fatal as a backstop | P8 | **line-does-not-resolve** — Cite MG_Pipe/MGPipeValueTypes.h:660 for the 1168-byte bound (or MGPipeRenderStateSpans.h:36 for the span layout); MGPipeTypes.h:471-478 only establishes the 48-byte record. Downgrade the '<=32' attrib | +| B8-11 | p6-must-handle | server->client bulk: read_pixels is UNCHUNKED (one SEG_REPLY slot); get_texture_image IS chunked; buffer writeback IS chunked | read_pixels: MobileGL/MG_Remote/Transport/ReplySlot.h:171-207 (RequireReadPixelsFits -> Fatal{ReplyTooLarge}), slot cap = ReplyBytes/SlotCount = 16 MiB/8 = 2 MiB (SessionRings.h:91-97, kDefaultReplySlotCount = ReplySlot.h:105), emitter check at EmitTables.cpp:955. get_texture_image: MobileGL/MG_Remote/Client/TextureReadbackEmit.inc:84-95 slices at session.MaxReplyBytes(). buffer writeback: MobileGL/MG_Remote/Client/GpuWritePending.cpp:189-203 slices at EventRingCapacity/4 (64 KiB by default) | a full-HD or 4K glReadPixels exceeds 2 MiB and Fatal{ReplyTooLarge} fires at the CLIENT before emission - a named refusal, correct, but a hard functional ceiling on the Redmi (1080p RGBA8 = 8.3 MB). The refusal text says this is 'a P6 debt (chunked readback), not a bigger pool' | Fatal{ReplyTooLarge} at ReplySlot.h:196 and the last-line-of-defence in ClientSession.cpp:1119-1126 | P9 | | +| B8-12 | already-handled | the persistent-map push is chunked twice: at the block granularity, then at the content cap | MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:523 (block bytes = MG_Config::Ipc.PersistentBlockKb * 1024, default 64 KiB) -> :735 PushMappedSpanBlock -> MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:456-486 -> NotifySubData -> the chunked MGPipeEmitResourceSubData | nothing | both walks | P11 | | +| B8-13 | already-handled | get_caps' two blobrefs do not ride a record at all; the caps snapshot crosses the control plane as a bounded RLE table | MobileGL/MG_Remote/CapsCodec.cpp:178-248 (EncodeFormatCapabilities, RLE over kFormatCapabilityTargetCount x kFormatCapabilityFormatCount cells declared at MG_Backend/BackendObject.h:102-103); PipeCalls.def:154 carries kHasBlob but the row is answered by CapsSnapshot, not by a record (ServerLoop.cpp:1017-1030, ROADMAP.md:126) | nothing about its SIZE. ROADMAP.md:126 notes it still owes a server->client carrier RULE - it may not be squeezed into SEG_STAGE - and under spawn it already crosses as a control frame, which is that rule | the ABI fingerprint assertion (ServerSession.cpp:91-108 FatalAbiMismatch) | cp | | +| B8-14 | blocks-p6 | what bounds an ANNOUNCED segment size today: ShmSegment::Adopt's fstat check, which is skippable and has no upper bound | MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp:120-141 | the guard is `if (::fstat(fd,&st) == 0 && st.st_size > 0 && st.st_size < size) refuse`. If fstat FAILS, or st_size is 0, the announced size is TRUSTED and Map() mmaps that many bytes - a read past the real end is SIGBUS, not a named refusal. There is also NO UPPER bound: a peer announcing 4 GiB with a 4 GiB memfd is adopted and mapped | MOBILEGL_ERR_INVALID_ARGUMENT with a log line at :129-133 - only in the three-way-true case | so | | +| B8-15 | blocks-p6 | the Welcome geometry cross-check has NO spawn counterpart: it compares against an in-process ServerSession | MobileGL/MG_Remote/Client/ClientSession.cpp:574-593 (`agrees()` compares welcome->cmdRing()->sizeBytes() against server.Shm().AnnouncedSize(slot), where `server` is Server::ServerSessionInstance() obtained at :534 in the same process) | there is no ServerSession object in the client process, so this whole block must be replaced. Its comment at :570-573 says the cross-check exists precisely so 'the assertion that would otherwise first run in P6' is not first exercised on the expensive day - but what it asserts (server-announced == server-mapped) is trivially true and is NOT the assertion spawn needs, which is 'announced <= what I am willing to map' | MOBILEGL_ERR_PROTOCOL_MISMATCH + Stop() at :590-593 | so | | +| B8-16 | blocks-p6 | CORRECTION: MOBILEGL_IPC_RING_MB / _STAGE_MB ceilings are 1024 / 4096 MiB as claimed, but they are consumed in the SERVER process, not the client | ceilings: MobileGL/ConfigLoader.cpp:363-364 (`QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024)`, `QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096)`). consumption: MobileGL/MG_Remote/Server/ServerSession.cpp:120-129 (SizesFromConfig) applied at :442-445. ownership: MobileGL/MG_Remote/Transport/SessionRings.h:126-131 ('WHO CREATES THEM: the SERVER') | the CHILD's env decides. P6-CONTRACT-DRAFT.md:69-70 says 'every role-selecting MOBILEGL_IPC_* removed from the child's envp' - if the scrub is by prefix, the child falls back to 8/32 MiB while the client was launched with e.g. STAGE_MB=256, silently. ServerSession.cpp:113-119 is a written-down precedent for exactly this class of bug ('a knob that REPORTS a value it does not use is worse'). The chunking budget itself stays correct because ClientSession::StageCapacityBytes() (ClientSession.cpp:1479) reads the ATTACHED segment, not the client's own config | none - there is no cross-check that the client's requested sizes and the server's applied sizes agree, and no log line n | sm | | + +**复审另行指出的遗漏**: +- MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp:955-960 - the DirectGLES backend's own named refusal on the INCOMING WindowHandle (`handle.Backend` allow-list of Android/X11/MetalLayer/Win32, plus `!handle.Handle`), MGLOG_E at :958 and `return false` at :959. It sits four lines above the `sameHandle` dedup key A8-10 names (:962-967) and A8-10 never mentions it. It matters for c6_ask #2 and #4: under P12 option (b) - substitute the server's window once at RegisterEGLWindowSurface (BackendObject.cpp:235) - this guard still inspects the CLIENT's handle, so a P12 design that stops shipping a client window handle at all makes this arm refuse every create unless it is re-keyed or moved. +- MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp:415-419 - the DirectVulkan twin of the same incoming-handle refusal (MGLOG_E at :417), which runs BEFORE the RegisterEGLWindowSurface call at :421. A8-4 cites the enclosing :409-422 as 'registers WITHOUT activating' but never names the guard inside it, so an integrator reading A8-4 alone would not know that Magma's create path has a second client-handle gate ahead of the latch point the audit recommends substituting at. +- MobileGL/MG_Backend/BackendObject.cpp:180 (`m_eglDisplay = dpy;`) and :469 (`m_eglDisplay = EGL_NO_DISPLAY;`), field declared at MG_Backend/BackendObject.h:649 - one line below m_windowHandle at :648. This is the EGLDisplay twin of the whole A-family and PART A never mentions it. I checked it: the client's EGLDisplay is latched in the server's backend and is NEVER dereferenced - the only uses are equality comparisons (InitializeEGLDisplay :175, MakeEGLCurrent :315, SwapEGLBuffers :385) - exactly the write-only shape A8-12 found on the client's own m_windowHandle. That is a POSITIVE finding c6 needs: of the EGL handles the server backend latches, the window is the only one that must migrate under P12, and the audit's claim to have covered the family is incomplete without saying so. +- MobileGL/MG_Pipe/PipeCalls.def:207, :210, :211, :212, :214, :216 - six kVarTail catalogue rows (SetVertexBuffers, SetSamplerViews, BindSamplerStates, SetShaderImages, SetStreamOutputTargets, SetVertexAttribDefaults) that PART B never examines, although the item's stated scope is 'every record type with no chunking walk'. None has one. B8-10 enumerates only the kHasBlob rows and B8-6/B8-7 only two tails, so six of the catalogue's ten tail-bearing rows go unnamed. I checked each: all carry the bounded `Start, Count` binding-range shape (MGPipeTypes.h:868-869 MGPVertexBuffers, :916-917 MGPSamplerViews, :923-924 MGPSamplerStates, :940 MGPShaderImages, :1029 MGPStreamOutputTargets, :1125 MGPVertexAttribDefaults) and are bounded by GL binding-point limits, so every verdict is no-action - but the enumeration as written is not the exhaustive sweep the summary claims, and c6 should not read 'the only unbounded tail is draw_vbo's' as having been proved across all ten. +- MobileGL/MG_Pipe/PipeCalls.def:227 - ResourceSubData carries `kHasBlob|kVarTail|kReplySlot`, and B8-1 covers only the blob half. Its var tail is MGPSubRegion[], sized from the dirty-rect count at MG_Impl/Pipe/TextureEmit.h:1506 (`m_regions.assign(std::max(rectCount, 1), MGPSubRegion{})`) and :1525-1527, and critically the slab walk does NOT cut it: each slab piece re-derives up to rectCount regions (TextureEmit.h:1506-1509), so the region tail rides every piece uncut. I chased the bound and it holds - MipmapStorage::kMaxDirtyRects = 96 at MG_State/GLState/TextureState/MipmapStorage.h:111, enforced at MipmapStorage.cpp:399 and read with that cap at TextureEmit.h:1421-1425 - so this is a no-action. But B8-2 as written would let an integrator conclude the texture path is fully cut when one of its two carriers is not cut at all and is bounded only by a constant in another subsystem. +- MobileGL/MG_Remote/Client/PersistentMapTracker.cpp:850 - a SECOND `buffer.PushMappedSpanBlock(...)` call site. B8-12 names only :735. Both funnel into MG_State/GLState/BufferState/BufferObject.cpp:456 -> NotifySubData -> the chunked MGPipeEmitResourceSubData, so the already-handled verdict is unchanged, but the row's 'the persistent-map push' is two producers, not one, and a c6 reader auditing that path would miss one of them. diff --git a/docs/Disaggregated/notes/p6/a6-audit-v1.md b/docs/Disaggregated/notes/p6/a6-audit-v1.md new file mode 100644 index 00000000..9985277d --- /dev/null +++ b/docs/Disaggregated/notes/p6/a6-audit-v1.md @@ -0,0 +1,270 @@ +# a6-audit-v1 — spawn 之前,进程/角色边界还差什么 + +> 只读静态审计,头 `b3b9c2ee`(`git rev-parse HEAD`,2026-09-21)。无 git 变更。 +> 八项中的第 5 项(能否不链 `MG_Impl`)是唯一的实验,单独成篇:[`a6-link-experiment.md`](a6-link-experiment.md)。 +> 全部 198 行逐条清单、复审异议与遗漏补充:[`a6-audit-rows.md`](a6-audit-rows.md)。 +> 形状照 [`../p5c/p5c-audit-v1.md`](../p5c/p5c-audit-v1.md)。 +> +> 方法:七项各由一个审计 agent 读树产出,再由一个对抗复审 agent **逐条重新解析 `file:line`** 并反向找遗漏。 +> 198 行里复审对 65 行提出异议,本文只采用**存活的**结论;被判为假的行保留在逐行清单里,标着为什么假。 + +--- + +## 0 一句话 + +**P6 的传输那一半比计划以为的少(fc/fs 确实做完了),进程/角色那一半比计划以为的多得多。** + +P6-SPAWN-PLAN 与 P6-CONTRACT-DRAFT 有**四处陈述在树上是假的**(§2), +其中两处(device-lost 闩锁"已实现"、`cp` "只剩传输")各自欠着一个完整的包。 +而真正的头号发现是:**`spawn` 今天在代码里根本不是一个可观察的状态**, +所以计划里"子进程强制 monolith"这条反递归规则,**执行它就等于让 server 进程整体选中前端臂**。 + +--- + +## 1 头号发现:`Transport` 这一个 bit 同时回答三个问题,而 spawn 让它们分家 + +### 1.1 `spawn` 现在连一个可观察的值都不是 + +`ConfigLoader.cpp:339-346` 精确识别 `spawn` / `unix:` / `pipe:`,然后写 + +```cpp +MG_Config::Transport = MG_Config::TransportMode::Monolith; +``` + +并记一行"P5 不实现,退回 monolith"。**于是"子进程被强制成 monolith"和"父进程解析失败退回 monolith"是同一个状态**, +`ConfigLoader.cpp` 那条具名拒绝**证伪不了自己**——它落在与 server 静息态相同的值上。 + +### 1.2 `Transport != Monolith` 有 238 处,它选的是**server 臂** + +复审精确确认了 238 这个数(`MG_Config::Transport` 的非测试 grep 行数,含 `TransportMode` / `TransportEndpoint` 后缀), +分布 155 `MG_Backend` / 35 `MG_Impl` / 21 `MG_Remote` / 12 `MG_Pipe` / 8 `MG_State` / 7 Config; +其中 **12 行是注释**,所以活代码是 226 行。 + +**`ARCHITECTURE.md:488` 的"子进程强制 monolith"如果按字面实现,148 行 `MG_Backend` 活代码会一起翻到前端臂**—— +在那个唯一没有前端的进程里。最锋利的一处是 `PipeInputs::IsLive()`(`MG_Impl/Pipe/PipeFill.cpp:2135`): +强制 monolith 后它读 `MG_State::pGLContext`,在 server 进程里是 null,于是 **58 个 `MGB_CTX_LIVE` 守卫全部答"不活"**。 + +> **给 c6 的第一个决定**:反递归的保险丝与 server 角色选择器**必须是两个独立的事实**。 +> 推荐形状:子进程保持 `Transport == Spawn`(于是 238 个站点原样选中 server 臂), +> 反递归另设 `Dial = No`。这样 c6 只需重写真正在问 Dial / Role 的那一小撮,而不是 238 个。 + +### 1.3 三个谓词覆盖得了,但审计找出了**第四、第五个问题**和一个命名冲突 + +复审推翻了我在 `P6-ENDSTATE-REVIEW.md` 里的一个判断(见 §5),并给出更准的分类: + +| 模式 | 活代码行 | 应成为 | +|---|---:|---| +| 后端体内的 server 臂测试(DirectGLES 98 + DirectVulkan 50) | 148 | `MGPipeServerArm()` | +| client 臂 emit/decide 测试(`MG_Impl` 34 + `MG_State` 6) | ~48 | `MGPipeSplitActive()` | +| 四个 `SlotCaps` 宏 | 4 | 局部性,见下 | +| 共享 applier / codec 里角色中立的测试 | ~11 | `MGPipeSplitActive()`,**最机械的一组** | +| Dial / bring-up(`Init.cpp` + `ClientSession`) | 3+ | `MGPipeShouldDial()`,**P6 的真活在这里** | + +**没被三个谓词覆盖的**: + +- **第四个问题——"对端会话是否活着"**。已有两处在用:`PipeApply.cpp:1355`(`ClientSession::Active() != nullptr`)、 + `ClientSession.cpp:1320`/`:1383`(`ClientSessionInstance().Started()`)。在 server-only 进程里前者**永远是 null**, + 于是 `MGPipeApplierReset` 的第二层角色守卫**在最需要它的进程里永远武装不起来**。 +- **第五个问题——"两个角色是否共用一份存储对象"**,即已经存在的 `MGPipeRoleSplitActive()`(`PipeInputs.cpp:292`)。 + spawn 下它结构上恒为假(两个地址空间),但它的定义是 `Ipc.RoleSplitState && Transport != Monolith`,**默认关**。 +- **局部性**:"后端在不在本进程"。`SlotCaps.h:116`/`:133-136` 今天拼成 `Transport == Monolith && slot != nullptr`。 +- **命名冲突**:我提议的 `MGPipeSplitActive()` 与既有的 `MGPipeRoleSplitActive()` 只差一个词、含义严格更窄。**先改名,再引入。** + +### 1.4 一条 G1 约束 + +非 disaggregated 构建里 `Transport` 是 `inline constexpr Monolith`(`Config.h:566`), +所以三个新谓词**都必须 `inline constexpr` 可折叠为 false**,否则 pull 构建的 `.text` 恒等门会红。 + +--- + +## 2 计划文档里在树上为假的四处 + +### 2.1 `cp` **不是**"只剩传输"——`InitCapabilities` 的**请求**没有 wire 形式 + +`P6-CONTRACT-DRAFT:98-101` 与 `P6-SPAWN-PLAN:76` 都写 "`InitCapabilities` 由 `CapsSnapshot` 回答"。 +**答案**是 `CapsSnapshot` 没错,但**请求**不是:`BackendObject_Remote.cpp:126` 有活的 client 调用者, +而 `EncodeSurfaceOpFrame` 对这个 kind **具名拒绝**(`SurfaceOpCodec.cpp:119-122`,`InprocOnlyOpOnTheWire`)。 +spawn 的 client **问不出来**。这是 `cp` 必须先决的第一件事,不是传输。 + +### 2.2 device-lost 闩锁不存在(上轮已发现,a6 逐条坐实) + +复审确认:`FatalCode::DeviceLost`/`ServerCrashed` 零 producer 零 consumer; +`glGetGraphicsResetStatus` 恒返回 `GL_NO_ERROR`;`MOBILEGL_IPC_RESPAWN`/`IDLE_EXIT_S` 除 `Config.h:466` 一句注释外无人解析。 +另外 `P6-CONTRACT-DRAFT:46` 引 `protocol.fbs:234-242` 指 `FatalCode`,实际在 `:248-256`——**漂了十四行**。 + +### 2.3 "server 不链 `MG_Impl`"——`PipeInputs.h:281` 与 `ARCHITECTURE.md:9` 都在断言,都是假的 + +见 [`a6-link-experiment.md`](a6-link-experiment.md):184 个符号。 +a6.2 独立给出同一结论的另一半证据:`PipeInputs::IsLive()` 与另外 **17 个 `PipeInputs::` 成员定义**编译在 `MG_Impl/Pipe/PipeFill.cpp`, +**两个角色拒绝助手**(`MGPipeRefuseAllocatorFromApplyThread` / `...FrontendKeyedRegistryFromApplyThread`)编译在 `MG_Impl/Pipe/SlotAllocator.cpp:29-57`。 +**把 `MG_Impl` 从 server 镜像里拿掉,等于把守卫一起拿掉。** + +### 2.4 大量行号漂移 + +`ClientSession.cpp:468`→`:482`;`BackendObject.h:561-566`(`WindowHandle`)→`:579-584`; +`protocol.fbs:186-196`(`SurfaceOp`)→`:194-210`;`ServerSession.cpp:670-690`(门铃访问器)→`:709-740`; +`ConfigLoader.cpp:310-348`→`:319-351`;`CONTRACT-P5C.md:262`、`PipeInputs.h:854` 的计数(379→383)等。 +`c6` 落地时逐条重解,`check_doc_citations.py` 应当覆盖这些。 + +--- + +## 3 阻塞 spawn 的硬发现(复审存活) + +### 3.1 apply 线程会在弹出第一条记录之前 abort + +`ServerSession::ConsumerDoorbell()`(`ServerSession.cpp:709-724`)对**任何非 `InProcess` 的 role** +`Fatal{UnmigratedVerb}` + `std::abort()`,`ProducerDoorbell()` 同(`:727-739`)。 +`ApplyThreadMain` 无条件调用它。注释自陈这是按**契约 §3.9** 把访问器留在 session 上"好让它保持一处 switch"。 + +> 这正好是 `P6-ENDSTATE-REVIEW` 里 `lk` 包要**废止**而不是**修补**的那条裁定——两条独立得出的结论对上了。 + +### 3.2 控制面在 server 侧**没有读者**,在 client 侧**被丢弃** + +- apply 线程的 `ready` 谓词只有三项(stop / `ControlIsPending()` / `cmdHead != LocalTail()`)—— + **控制 socket 不在其中**(`ServerLoop.cpp:398-402`)。socket 上到达的 `SurfaceOp` **没有任何人读**。 +- client 的控制泵只消费 `CapsSnapshot`,其余(含 `SurfaceOp`/`SurfaceReply`)记一条 WARN 然后 `continue` + (`ClientSession.cpp:1425-1432`,注释写明"是 P6 的")。 + +### 3.3 发帖方的等待**没有期限** + +`m_controlDone.wait(lock, pred)`(`ServerLoop.cpp:724`)是无期限谓词等待。 +对照 ring 侧有 `kBarrierTimeoutMs`。**被 SIGSTOP / Android freezer 冻住的 server 不产生 EOF 也不产生 reply**, +GL 线程就永远卡在 `eglMakeCurrent` 里,没有任何诊断。这正是 `P6-SPAWN-PLAN §4` 点名却没落到代码上的那个状态。 + +### 3.4 `SurfaceReply` 装不下一个 `MobileGLResult` + +wire 只有 `{seq, ok, eglMajor, eglMinor}`(`protocol.fbs:212-218`),`ServerApplyWireSurfaceOp` 的 rc 被丢掉。 +**client 因此分不清"server 半路死了"和"一次合法的拒绝"**,也就无法决定是武装 device-lost 闩锁还是返回 `EGL_FALSE`。 + +### 3.5 `seq == 0` 被静默重编号,且 reply 不做关联 + +`RunSurfaceControlFrame`(`ServerLoop.cpp:659-662`)在 `frame.seq == 0` 时自己铸号, +而它自己的注释说 wire 请求必须保留 client 的号。`DecodeWireSurfaceReply`(`SurfaceOpCodec.cpp:190-195`) +把 seq/ok 直接覆盖进交给它的帧并返回 void——**不做任何关联**。 + +### 3.6 server 进程只能有一套 EGL,且第二套**不具名** + +`g_Display` / `g_Context` / `g_Surface` / `g_Config` 是每进程 file-static, +`InitDisplayAndContext` 开头无条件 `DestroyEGLContext()`(`DirectGLES.cpp:16012-16013`), +而后者最后一步是 `eglTerminate(g_Display)`——**进程级** EGL 拆除。 +一 guest 一进程因此不是设计偏好,是**今天唯一成立的形状**;而它现在**没有具名拒绝**。 +`ServerSession::Accept` 的同对象臂返回 `MOBILEGL_ERR_INVALID_ARGUMENT` **且没有任何日志**(`ServerSession.cpp:425`)。 + +### 3.7 角色守卫在 server 进程里**武装不起来** + +| 守卫 | 武装条件 | 在 server-only 进程里 | +|---|---|---| +| `MGPipeApplierReset` 第二层 | `ClientSession::Active() != nullptr`(`PipeApply.cpp:1353-1355`) | 恒 null → **永远不武装** | +| `MGPipeServerBlockNoteIdentity` | `MGPipeRoleSplitActive()`(默认关) | 早退 → `ContextIdentity()` 恒 nullptr | +| `CountBarrierPull` 无条件 Fatal 臂 | 同上 | 关 | +| 两个 `RefuseFromApplyThread` | `ServerLoop::OnApplyThread()` | 与"是 server 进程"**不再重合** | + +**其中 `ContextIdentity()` 恒 nullptr 是一个会崩的洞**:`DirectGLES.cpp:199-213` 的 fb-slot memo 以 +`MGB_CTX_IDENTITY` 做裸指针比较、无世代,nullptr 会被读成**缓存命中**, +于是第一次调用就用上一个没初始化的槽——`PipeInputs.h:878-884` 的注释说这里本该是一条具名 Fatal。 + +--- + +## 4 a6.7:abort 普查——funnel 是改名还是重设计 + +`MG_Remote` 下 `std::abort()` 共 **94 处 grep 命中,其中 2 处在注释里,实际 92 个站点** +(上轮我写的 93 少一个,更正见 §5)。 + +**家族分布**:`Fatal{}` 词汇有 **30 个不同的家族词**(`ProtocolCorruption` 37、`RoleViolation` 8、`RingOverrun` 4、 +`UnmigratedVerb` 4、`EventRingOverflow` 4、`NoClientSession` 3、`ReplyError` 3 …), +而 schema 的 `FatalCode` 只有 **7 个值**(`protocol.fbs:248-256`)。 +**`Session::Fail(FatalCode, detail)` 的第一个参数装不下今天的 30 个家族**——这是 c6 必须先决的签名问题。 + +**两个站点完全没有 `Fatal{` 标记**(`PersistentMapTracker.cpp:1052-1058`、`WireLog.cpp:41-50`), +所以"日志逐字不变"这个承诺对它们不成立,家族 grep 也看不见它们。 + +**从对端字节可达、且今天会 abort 的无界分配**(三处,`MG_Remote` 下**没有任何 try/catch**): + +| 站点 | 形状 | +|---|---| +| `PipeApply.cpp:1563-1569` | `MGPipeApplyCreateRenderState` 按对端给的 `Cso.Slot+1` resize,**无槽位上限**;兄弟家族全走 `RecordAt` 的有界路径。`Slot = 0xFFFFFFFE` → 约 1.7 TB | +| `PipeApplier.cpp:416-441` | readback scratch 按 `Box.W*Box.H*bpp` resize,**resize 发生在任何尺寸门之前**。65535² RGBA8 → 17 GB | +| `ProgramArtifactsCodec.cpp:166-223` | count 只按**剩余字节**设界,然后乘 `sizeof(element)`。32 MiB stage 可伪造出高一两个数量级的 resize | + +--- + +## 5 更正我自己上一轮的四处 + +复审逐条核了 `P6-ENDSTATE-REVIEW.md`,以下四处我写错了,按树更正: + +1. **`MG_State` 那 8 个站点不是我说的那个形状。** 我写"唯一不按目录成形的一批,同一个函数里同时问两个问题"。 + 实际:8 个里 **2 个是注释**(`BufferObject.cpp:60`、`BufferObject.h:194`), + 剩下 6 个里 **5 个是单问题的 `SplitActive` 测试**,**只有 1 个**(`MipmapStorage.cpp:55-56`)真的同时问两个。 + 而"同时问两个"的形状**根本不是 `MG_State` 局部的**——它散在 `SlotAllocator.cpp` / `PipeFill.cpp` / `PipeApply.cpp` / + `Managers.cpp` / `MG_Remote/Client` 的 **18 个站点**上,而且树里已经有两种拼法。 + **结论不变**(a6 仍应先分类这一族),**理由和取样对象要换**。 +2. **abort 数是 92 不是 93**(94 命中减 2 处注释)。 +3. **`BackendObject.cpp:283` 是一次调用,不是 store**;全树对 `m_windowHandle` 的 store 只有 **4 处**(`:475`、`:365`、`:206`、`:207`), + 读只有 **5 处**。而且 `.Width` / `.Height` **没有任何读者**,所以 `:206-207` 今天 clobber 不了任何东西。 + **"两道缝"的结论仍然成立且更精确**:真正的第二道缝是 `ActivateEGLSurface → SetWindowHandle(surfaceState->Window)`, + 而 `surfaceState->Window` 只在 `RegisterEGLWindowSurface`(`:232-238`)写一处——**那才是最小的 latch 点**。 +4. **`MGPipeSplitActive()` 这个名字不能直接用**,与既有 `MGPipeRoleSplitActive()` 冲突(§1.3)。 + +另有一处我从 P6 文档继承下来的失效引用:`BackendObject.h:561-566` 指 `WindowHandle`,实际在 `:579-584`。 + +--- + +## 6 a6.8:窗口句柄与分片 + +**窗口句柄**(复审给出的精确形状,比我上轮写的更小也更清楚): + +server 端拥有自己的窗口需要**四处**改动,不是一处: +(a) `RegisterEGLWindowSurface`(`BackendObject.cpp:232-238`)的一次替换; +(b) `ServerLoop.cpp:1015-1017` 的一次拒绝; +(c) `ResetEGLRuntimeState`(`:359-367`)的一次保留; +(d) **非显然的第四处**——`BackendObject_DirectGLES.cpp:962-967` 的 `sameHandle` 去重键拿 client 的窗口指针比 server latch 的那个, +server 拥有窗口后**永不相等**,于是每次 `eglCreateWindowSurface` 都会 `DestroyEGLContext` + `ResetEGLRuntimeState` 重建原生 context。 + +**一条 P6 当下就该决的**:`BackendObject_Remote.cpp:214-216` **无条件**发 `SetWindowHandle`, +而 `AndroidNativeWindow@P12` 的拒绝在 **server 进程**里 abort。 +所以在这台 Redmi 上跑 FCL/Minecraft,**第一次 `eglCreateWindowSurface` 就会把 server 进程 abort 掉**, +client 只看到对端死了。要么 P6 在 **client 侧**镜像这条拒绝,要么接受这个形状并写进契约。 + +**分片**:`resource_subdata` / `buffer_subdata_resident`(buffer 范围)、纹理层级(**三臂**走查:整深度切片、整行 run、 +以及**次行 texel run**——比文档说的"整宽 slab"更细)、持久映射推送、`GetTextureImage` **都有**走查。 +**没有走查的**按大小排:program archive(`WireTables.cpp:556`,唯一无界 blob)、 +`draw_vbo` 的 `NumDraws * 12` range 尾(`EmitTables.cpp:536`,唯一无界尾)。 +**ROADMAP 开放问题 11 点名的"宿主索引 span"在本头没有 producer**——`kDrawHasUserIndices` 唯一出现是 +`OwnedDrawInputs.h:51` 的清零,client 索引已经变成 owned buffer 资源。**这条可以从开放问题里划掉。** + +> 所以 stream 链路窗口应当对着 **program archive** 和 **draw_vbo range 尾**定尺,不是对着 `set_global_constants`。 + +--- + +## 7 给 c6 的决定清单(按该先决的顺序) + +1. **`Transport` 在 spawn 的 server 进程里读作什么?** 148 行 `MG_Backend` 活代码和两个 Fatal 角色守卫一起翻在这一个 bit 上。 + 推荐:子进程保持 `Spawn`,反递归另设 `Dial = No`。 +2. **`Session::Fail` 的第一个参数**:留 7 值 wire 枚举,还是引入一个更宽的内部家族枚举?今天有 30 个家族词。 +3. **`InitCapabilities` 请求的 wire 形式**(§2.1)。这是 `cp` 的前置,不是传输。 +4. **控制 socket 谁读**:新 `mgl-srv-io` 线程投进现有单槽邮箱(`ServerLoop.cpp:673-727` 一行不改), + 还是给 apply 线程的 `ready` 谓词加第四项? +5. **控制回复等待的期限与名字**(§3.3)。 +6. **`SurfaceReply` 是否追加一个 result 字段**(§3.4)。 +7. **spawn 下谁铸 `seq`,`seq == 0` 在 wire 上什么意思**(§3.5)。 +8. **窗口种类:黑名单还是白名单**;以及 `WindowKind::None` 带非零 token 今天被接受这件事怎么办。 +9. **一 server 进程一 guest 写进契约,第二个具名拒绝**(§3.6)。 +10. **四个角色守卫的重新武装条件**(§3.7)——尤其 `ContextIdentity()` 恒 nullptr 那个会崩的洞。 +11. **三处无界分配的绝对上限**(§4),以及 `MG_Remote` 下要不要有 try/catch。 +12. **server 镜像链不链 `MG_Impl`**:按 a6 的答案,P6 链,契约把 `PipeInputs.h:281` 与 `ARCHITECTURE.md:9` 的断言改成记账。 +13. **`MGPipeRoleSplitActive()` 先改名**,再引入 `MGPipeSplitActive()`。 +14. **三个新谓词必须 `inline constexpr` 可折叠**(§1.4),否则 G1 红。 + +--- + +## 8 未决(读不出来的) + +1. **spawn 的 server 进程里谁跑 applier、在哪个线程上。** 十余处 `ServerLoop::OnApplyThread()` 守卫全部取决于它; + 若 `ServerMain` 在主线程驱动 applier 而不经 `ServerLoop::Start`,`g_applyThreadKey` 恒 0,**全部守卫静默失效**。 +2. **server 的退出原语是 `_exit` 还是 `exit`。** 它决定 `g_processTeardown`(`Managers.cpp:188-197`)是否被置位, + 进而决定七个 twin 析构是走早退还是真调进驱动。 +3. **握手要不要断言子系统掩码。** `EsprytSlotTablesEnabled()` 每进程从 `Features.PipePush` latch, + 两个进程各自解析自己的环境,而 `ARCHITECTURE.md:488` 的 envp 剔除清单**不含** `MOBILEGL_PIPE_*`。 +4. **P6 是否把任何段映射成只读。** `ShmSegment::Adopt` 已有 `Map(bool readOnly)`,但两个调用点都传 `false`。 +5. **`DualBlockScenario.cpp:69-73` 断言 `serverIdentity != nullptr`**,这是一个进程内事实; + 而 G2/G14 要求 `integration-spawn` 与 `integration-split` 逐名相同。这条用例在 spawn 下怎么办。 diff --git a/docs/Disaggregated/notes/p6/a6-link-experiment-data.md b/docs/Disaggregated/notes/p6/a6-link-experiment-data.md new file mode 100644 index 00000000..92f8d560 --- /dev/null +++ b/docs/Disaggregated/notes/p6/a6-link-experiment-data.md @@ -0,0 +1,249 @@ +# a6 item 5 - link experiment raw data + +Head b3b9c2ee. Build: cmake -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_PIPE_PUSH=ON, Release, clang 22.1.6, target MobileGL. +Method: nm over the per-object output of the single MobileGL target. No CMake edit (there is no module target to edit - see the report). + +SERVER set = MG_Backend/** + MG_Pipe/** + MG_Remote/{Transport,Protocol,Wire,Server}/** + MG_Remote/CapsCodec.o (45 objects) +FRONTEND set = MG_Impl/** + MG_State/** + MG_Remote/Client/** (71 objects) +SHARED set = MG_Util/** + Init/GlobalObjects/ConfigLoader (87 objects) + +server undefined symbols total : 1486 + not satisfied by SERVER or SHARED : 552 + OF WHICH defined by FRONTEND : 184 + +## The 184, demangled, grouped by defining file + +### MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp.o + - MobileGL::MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo + +### MG_Impl/GLImpl/Texture/GL_Texture.cpp.o + - MobileGL::MG_Impl::GLImpl::CopyTextureImageToClientOrPBO_State(std::shared_ptr const&, MobileGL::TextureUploadTarget, int, unsigned int, unsigned int, int, void*, char const*) + +### MG_Impl/Pipe/PipeFill.cpp.o + - MobileGL::MG_Pipe::MGPipeNoteAggregate(MobileGL::MG_Pipe::MGPipeAggregate) + - MobileGL::MG_Pipe::PipeInputs::GetBufferBindingPointCount(MobileGL::BufferTarget) const + - MobileGL::MG_Pipe::PipeInputs::GetProgramObject(unsigned int) + - MobileGL::MG_Pipe::PipeInputs::GetTextureObject(unsigned int) + - MobileGL::MG_Pipe::PipeInputs::HasOpenTransformFeedbackSpan(unsigned long) const + - MobileGL::MG_Pipe::PipeInputs::InvalidateCompileEnv() + - MobileGL::MG_Pipe::PipeInputs::IsLive() const + - MobileGL::MG_Pipe::PipeInputs::RecordError(MobileGL::ErrorCode, std::unique_ptr >) + - MobileGL::MG_Pipe::PipeInputs::ValidateProgramName(unsigned int) const + +### MG_Impl/Pipe/SlotAllocator.cpp.o + - MobileGL::MG_Pipe::MGPipeApplierIsUnbarrieredApply() + - MobileGL::MG_Pipe::MGPipeRefuseAllocatorFromApplyThread(char const*) + - MobileGL::MG_Pipe::MGPipeRefuseFrontendKeyedRegistryFromApplyThread(char const*) + - MobileGL::MG_Pipe::MGPipeSlotAllocator::Acquire(MobileGL::MG_Pipe::MGPipeKind, unsigned long) + - MobileGL::MG_Pipe::MGPipeSlotAllocator::FindByLifetimeId(MobileGL::MG_Pipe::MGPipeKind, unsigned long) const + - MobileGL::MG_Pipe::MGPipeSlotAllocator::Free(MobileGL::MG_Pipe::MGPipeKind, MobileGL::MG_Pipe::MGPipeHandle) + - MobileGL::MG_Pipe::MGPipeSlots() + +### MG_Remote/Client/BackendObject_Remote.cpp.o + - MobileGL::MG_Remote::Client::BackendObject_Remote::BackendObject_Remote() + +### MG_Remote/Client/ClientSession.cpp.o + - MobileGL::MG_Remote::Client::ClientSession::Active() + - MobileGL::MG_Remote::Client::ClientSessionInstance() + - MobileGL::MG_Remote::Client::ClientSession::NoteApplyThreadEnteredApplier() + - MobileGL::MG_Remote::Client::ClientSession::NoteApplyThreadLeftApplier() + - MobileGL::MG_Remote::Client::ClientSession::Start(MobileGL::MG_Config::TransportMode, std::__cxx11::basic_string, std::allocator > const&) + - MobileGL::MG_Remote::Client::ClientSession::Stop() + +### MG_Remote/Client/PersistentMapTracker.cpp.o + - MobileGL::MG_Remote::Client::AdoptTierIsEmulate() + +### MG_Remote/Client/WireTables.cpp.o + - MobileGL::MG_Remote::Client::EmitObjectDeathRecord(MobileGL::MG_Pipe::MGPipeKind, unsigned long) + +### MG_State/GLState/BufferState/BufferObject.cpp.o + - MobileGL::MG_State::GLState::BufferObject::DownloadSubData(void*, unsigned long, unsigned long) const + - MobileGL::MG_State::GLState::BufferObject::EnsureGpuResidentStorage() + - MobileGL::MG_State::GLState::BufferObject::GetBackendResource() const + - MobileGL::MG_State::GLState::BufferObject::GetChangeSerial() const + - MobileGL::MG_State::GLState::BufferObject::GetExternalIndex() const + - MobileGL::MG_State::GLState::BufferObject::GetSize() const + - MobileGL::MG_State::GLState::BufferObject::GetUsage() const + - MobileGL::MG_State::GLState::BufferObject::HasDefinedContent() const + - MobileGL::MG_State::GLState::BufferObject::IsBackendPersistentMapped() const + - MobileGL::MG_State::GLState::BufferObject::IsMapped() const + - MobileGL::MG_State::GLState::BufferObject::MappedData() const + - MobileGL::MG_State::GLState::BufferObject::MarkGpuWritten() + - MobileGL::MG_State::GLState::BufferObject::SetBackendResource(std::shared_ptr) + - MobileGL::MG_State::GLState::BufferObject::SyncGpuWrites() + - MobileGL::MG_State::GLState::BufferObject::SyncPersistentMappedRange() + - MobileGL::MG_State::GLState::BufferObject::WritebackFromBackend(MobileGL::DataPtr, unsigned long) + - MobileGL::MG_State::GLState::GetBufferBackendOps() + - MobileGL::MG_State::GLState::SetBufferBackendOps(MobileGL::MG_State::GLState::BufferBackendOps const*) + +### MG_State/GLState/Core.cpp.o + - MobileGL::MG_State::GLState::ClassifyVertexAttribType(unsigned int) + +### MG_State/GLState/FramebufferState/FramebufferObject.cpp.o + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetRenderbuffer() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetSize() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetTexture() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetTextureLayer() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetTextureLevel() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::GetTextureUploadTarget() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsComplete() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsEmpty() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsLayered() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsRenderbuffer() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsTexture() const + - MobileGL::MG_State::GLState::FramebufferAttachmentObject::IsValid() const + - MobileGL::MG_State::GLState::FramebufferObject::GetAllAttachmentObjects() const + - MobileGL::MG_State::GLState::FramebufferObject::GetAttachment(MobileGL::FramebufferAttachmentType) const + - MobileGL::MG_State::GLState::FramebufferObject::GetDrawBuffers() const + - MobileGL::MG_State::GLState::FramebufferObject::GetExternalIndex() const + +### MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp.o + - MobileGL::MG_State::GLState::DecodeProgramArchive(unsigned char const*, unsigned long, MobileGL::MG_State::GLState::ProgramArchive&) + +### MG_State/GLState/ProgramState/ProgramObject.cpp.o + - MobileGL::MG_State::GLState::ProgramObject::AllocateLifetimeId() + - MobileGL::MG_State::GLState::ProgramObject::AttachShader(std::shared_ptr const&) + - MobileGL::MG_State::GLState::ProgramObject::JoinPendingLink() const + - MobileGL::MG_State::GLState::ProgramObject::JoinPendingSpirv() const + - MobileGL::MG_State::GLState::ProgramObject::Link(bool) + - MobileGL::MG_State::GLState::ProgramObject::~ProgramObject() + +### MG_State/GLState/ProgramState/ShaderObject.cpp.o + - MobileGL::MG_State::GLState::ShaderObject::Compile() + - MobileGL::MG_State::GLState::ShaderObject::DropCompileNode() const + - MobileGL::MG_State::GLState::ShaderObject::JoinPendingCompile() const + - MobileGL::MG_State::GLState::ShaderObject::ReleaseCompileNode() + - MobileGL::MG_State::GLState::ShaderObject::SetShaderSource(std::__cxx11::basic_string, std::allocator >&&) + +### MG_State/GLState/RenderbufferState/RenderbufferObject.cpp.o + - MobileGL::MG_State::GLState::RenderbufferObject::GetExternalIndex() const + - MobileGL::MG_State::GLState::RenderbufferObject::GetHeight() const + - MobileGL::MG_State::GLState::RenderbufferObject::GetInternalFormat() const + - MobileGL::MG_State::GLState::RenderbufferObject::GetSamples() const + - MobileGL::MG_State::GLState::RenderbufferObject::GetWidth() const + - MobileGL::MG_State::GLState::RenderbufferObject::IsAllocated() const + +### MG_State/GLState/SamplerState/SamplerObject.cpp.o + - MobileGL::MG_State::GLState::SamplerObject::GetAllSamplerParameters() const + - MobileGL::MG_State::GLState::SamplerObject::GetBorderColor() const + - MobileGL::MG_State::GLState::SamplerObject::GetBorderColorI() const + - MobileGL::MG_State::GLState::SamplerObject::GetBorderColorUI() const + - MobileGL::MG_State::GLState::SamplerObject::GetCompareMode() const + - MobileGL::MG_State::GLState::SamplerObject::GetExternalIndex() const + - MobileGL::MG_State::GLState::SamplerObject::GetLifetimeId() const + - MobileGL::MG_State::GLState::SamplerObject::GetLodBias() const + - MobileGL::MG_State::GLState::SamplerObject::GetMagFilter() const + - MobileGL::MG_State::GLState::SamplerObject::GetMaxAnisotropy() const + - MobileGL::MG_State::GLState::SamplerObject::GetMaxLod() const + - MobileGL::MG_State::GLState::SamplerObject::GetMinFilter() const + - MobileGL::MG_State::GLState::SamplerObject::GetMinLod() const + - MobileGL::MG_State::GLState::SamplerObject::GetMipmapMode() const + - MobileGL::MG_State::GLState::SamplerObject::GetSamplerCompareFunc() const + - MobileGL::MG_State::GLState::SamplerObject::GetVersion() const + - MobileGL::MG_State::GLState::SamplerObject::GetWrapR() const + - MobileGL::MG_State::GLState::SamplerObject::GetWrapS() const + - MobileGL::MG_State::GLState::SamplerObject::GetWrapT() const + - MobileGL::MG_State::GLState::SamplerObject::~SamplerObject() + - MobileGL::MG_State::GLState::SamplerObject::SamplerObject(unsigned int) + - MobileGL::MG_State::GLState::SamplerObject::SetCompareMode(MobileGL::SamplerCompareMode) + - MobileGL::MG_State::GLState::SamplerObject::SetLodRange(float, float) + - MobileGL::MG_State::GLState::SamplerObject::SetMagFilter(MobileGL::SamplerFilterMode) + - MobileGL::MG_State::GLState::SamplerObject::SetMinFilter(MobileGL::SamplerFilterMode) + - MobileGL::MG_State::GLState::SamplerObject::SetMipmapMode(MobileGL::SamplerMipmapMode) + - MobileGL::MG_State::GLState::SamplerObject::SetSamplerCompareFunc(MobileGL::SamplerCompareFunc) + - MobileGL::MG_State::GLState::SamplerObject::SetWrapR(MobileGL::SamplerWrapMode) + - MobileGL::MG_State::GLState::SamplerObject::SetWrapS(MobileGL::SamplerWrapMode) + - MobileGL::MG_State::GLState::SamplerObject::SetWrapT(MobileGL::SamplerWrapMode) + +### MG_State/GLState/TextureState/MipmapStorage.cpp.o + - MobileGL::MG_State::GLState::MGPipeTextureLegacyArmScope::~MGPipeTextureLegacyArmScope() + - MobileGL::MG_State::GLState::MGPipeTextureLegacyArmScope::MGPipeTextureLegacyArmScope() + +### MG_State/GLState/TextureState/TextureObject1D.cpp.o + - MobileGL::MG_State::GLState::TextureObject1D::TextureObject1D(unsigned int) + +### MG_State/GLState/TextureState/TextureObject2D.cpp.o + - MobileGL::MG_State::GLState::TextureObject2D::TextureObject2D(unsigned int) + +### MG_State/GLState/TextureState/TextureObject2DCube.cpp.o + - MobileGL::MG_State::GLState::TextureObject2DCube::TextureObject2DCube(unsigned int) + +### MG_State/GLState/TextureState/TextureObject3D.cpp.o + - MobileGL::MG_State::GLState::TextureObject3D::TextureObject3D(unsigned int) + +### MG_State/GLState/TextureState/TextureObjectBuffer.cpp.o + - MobileGL::MG_State::GLState::TextureObjectBuffer::GetBufferBindingSlot(MobileGL::TextureUploadTarget) + +### MG_State/GLState/TextureState/TextureObject.cpp.o + - MobileGL::MG_State::GLState::SamplesAsIncompleteTexture(MobileGL::MG_State::GLState::ITextureObject const*, MobileGL::MG_State::GLState::SamplerObject const*) + - MobileGL::MG_State::GLState::TextureObjectBase::GetAllSwizzleParams() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetBorderColor() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetBorderColorForm() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetBorderColorI() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetBorderColorUI() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetContentVersion() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetExternalIndex() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetFormat() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetImmutableLevels() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetLevelRange() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetLifetimeId() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetSamplerObject() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetSamples() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetShapeVersion() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetSwizzleParam(MobileGL::TextureSwizzleParam) const + - MobileGL::MG_State::GLState::TextureObjectBase::GetTarget() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetTextureParamsVersion() const + - MobileGL::MG_State::GLState::TextureObjectBase::GetViewStorageOwner() const + - MobileGL::MG_State::GLState::TextureObjectBase::HasFixedSampleLocations() const + - MobileGL::MG_State::GLState::TextureObjectBase::IsImmutable() const + - MobileGL::MG_State::GLState::TextureObjectBase::IsMipmapCompleteForFilterCached(bool) const + - MobileGL::MG_State::GLState::TextureObjectBase::PipePublishParams() + - MobileGL::MG_State::GLState::TextureObjectBase::SetBaseLevel(unsigned int) + - MobileGL::MG_State::GLState::TextureObjectBase::SetBorderColorI(MobileGL::Vec4 const&) + - MobileGL::MG_State::GLState::TextureObjectBase::SetBorderColor(MobileGL::Vec4 const&) + - MobileGL::MG_State::GLState::TextureObjectBase::SetBorderColorUI(MobileGL::Vec4 const&) + - MobileGL::MG_State::GLState::TextureObjectBase::SetFixedSampleLocations(bool) + - MobileGL::MG_State::GLState::TextureObjectBase::SetImmutableLevels(unsigned int) + - MobileGL::MG_State::GLState::TextureObjectBase::SetInternalFormat(MobileGL::TextureInternalFormat) + - MobileGL::MG_State::GLState::TextureObjectBase::SetMaxLevel(unsigned int) + - MobileGL::MG_State::GLState::TextureObjectBase::SetSamples(int) + - MobileGL::MG_State::GLState::TextureObjectBase::SetSwizzleParam(MobileGL::TextureSwizzleParam, MobileGL::TextureSwizzleParam) + - MobileGL::MG_State::GLState::TextureObjectBase::SetSwizzleParamRGBA(MobileGL::Vec4 const&) + - MobileGL::MG_State::GLState::TextureObjectBase::~TextureObjectBase() + - MobileGL::MG_State::GLState::TextureObjectBase::TextureObjectBase(MobileGL::TextureTarget, unsigned int) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::AllocateStorage(MobileGL::TextureUploadTarget, unsigned int, MobileGL::MG_State::GLState::MipmapInput) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetBaseSize() const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapByteSize(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapCompressedByteSize(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapCompressedFormat(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapLevelCount() const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapRequestedCompressedFormat(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetMipmapTexelSize(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetStorageDirtyRects(MobileGL::TextureUploadTarget, unsigned int, MobileGL::MG_State::GLState::MipmapDirtyRegion*, unsigned long) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::GetStorageDirtyRegion(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::IsComplete() const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::IsStorageDirty(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::MapMipmapCompressedImage(MobileGL::TextureUploadTarget, unsigned int) const + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::MapMipmapData(MobileGL::TextureUploadTarget, unsigned int) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::MarkStorageDirty(MobileGL::TextureUploadTarget, unsigned int, bool) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::MarkStorageDirtyRegion(MobileGL::TextureUploadTarget, unsigned int, MobileGL::Vec3, MobileGL::Vec3) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::SetMipmapCompressedImage(MobileGL::TextureUploadTarget, unsigned int, unsigned int, void const*, unsigned long) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::SetMipmapRequestedCompressedFormat(MobileGL::TextureUploadTarget, unsigned int, unsigned int) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::TruncateMipmapLevels(MobileGL::TextureUploadTarget, unsigned int) + - MobileGL::MG_State::GLState::TextureObjectWithOneMipmap::UpdateMipmapSubData(MobileGL::TextureUploadTarget, unsigned int, MobileGL::DataPtr) + - typeinfo for MobileGL::MG_State::GLState::TextureObjectBase + - typeinfo for MobileGL::MG_State::GLState::TextureObjectWithOneMipmap + - vtable for MobileGL::MG_State::GLState::TextureObjectWithOneMipmap + +### MG_State/GLState/TextureState/TextureUnit.cpp.o + - MobileGL::MG_State::GLState::TextureUnit::GetAllBindingSlots() + - MobileGL::MG_State::GLState::TextureUnit::GetBindingSlot(MobileGL::TextureTarget) + - MobileGL::MG_State::GLState::TextureUnit::GetSamplerObject() const + +### MG_State/GLState/VertexArrayState/VertexArrayObject.cpp.o + - MobileGL::MG_State::GLState::VertexArrayObject::GetAllAttributes() const + - MobileGL::MG_State::GLState::VertexArrayObject::GetAllAttributeVersions() const + - MobileGL::MG_State::GLState::VertexArrayObject::GetAttribute(unsigned int) const + - MobileGL::MG_State::GLState::VertexArrayObject::GetIndexBufferBindingSlot() + - MobileGL::MG_State::GLState::VertexArrayObject::GetIndexBufferBindingSlot() const + diff --git a/docs/Disaggregated/notes/p6/a6-link-experiment.md b/docs/Disaggregated/notes/p6/a6-link-experiment.md new file mode 100644 index 00000000..7798abe4 --- /dev/null +++ b/docs/Disaggregated/notes/p6/a6-link-experiment.md @@ -0,0 +1,219 @@ +# a6 第 5 项:server 能否不链 `MG_Impl`——实验结果 + +> 头 `b3b9c2ee`。`P6-SPAWN-PLAN.md` §5 的 a6 第 5 项、`P6-CONTRACT-DRAFT.md` §3.3 都把这条挂着; +> `MG_Backend/MGPipe/PipeInputs.h` 与 `P5F-WIRE-COMPLETENESS.md` §1.4 **断言**它成立。 +> 三个阶段没人试过。这次试了。 +> +> 原始数据(184 个符号逐个按定义文件分组)见 [`a6-link-experiment-data.md`](a6-link-experiment-data.md)。 + +## 结论 + +**不能。** 今天没有任何一种配置能让 server 镜像不链 `MG_State` / `MG_Impl` / `MG_Remote/Client`。 + +而且这个问题**在今天根本不是一个链接行能回答的问题**,这是第一个发现。 + +--- + +## 1 结构性发现:构建系统里一道缝都没有 + +`CMakeLists.txt` 里 `SOURCE_FILES` 是**一张平的源文件列表**(211 个 `.cpp`), +`add_library(MobileGL SHARED ${SOURCE_FILES})`(`:695`)与 +`add_library(MobileGL_s STATIC ${SOURCE_FILES})`(`:762`)都从它构建。 + +`MG_Impl` / `MG_State` / `MG_Backend` / `MG_Pipe` / `MG_Remote` 之间**没有任何 target 边界**—— +没有 object library、没有 static lib、没有 interface target。 + +唯一叫 `MobileGLServer` 的 target(`:926`)是 P0 spike 桩:Android-only、默认 OFF、 +只编 `tools/spikes/server_stub/main.cpp`,**一个 MobileGL 库都不链**。 +`mobilegl_server_main` 这个符号在整棵树上**不存在**,只存在于 `ARCHITECTURE.md:488-496` 的描述里。 + +> 所以"改链接行试一次"这个做法本身不成立:没有可分的 target。 +> `c6` 必须把"建立模块 target 边界"当成一件**有成本的具名工作**,而不是 `sm` 包里顺手的一行。 + +## 2 方法(保持 a6 只读) + +不加 target、不改一行源码。构建 `MobileGL` 单个 target,然后对**逐个 object 文件**做符号闭包: + +``` +SERVER 集 = MG_Backend/** + MG_Pipe/** + MG_Remote/{Transport,Protocol,Wire,Server}/** + CapsCodec.o (45 个 object) +FRONTEND 集 = MG_Impl/** + MG_State/** + MG_Remote/Client/** (71 个 object) +SHARED 集 = MG_Util/** + Init/GlobalObjects/ConfigLoader (87 个 object) +``` + +取 SERVER 集的未定义符号,减去 SERVER 自己定义的,减去 SHARED 定义的, +与 FRONTEND 定义的取交集——**这就是一次真链接会报出来的那份未定义符号表**。 + +构建:`clang 22.1.6`、Release、`DISAGGREGATED=ON`、`DISAGGREGATED_INPROC=ON`、`PIPE_PUSH=ON`。 + +## 3 数字 + +| | | +|---|---:| +| SERVER 集未定义符号总数 | 1531 | +| 其中 SERVER + SHARED 自己满足不了的 | 552 | +| **其中由 FRONTEND 定义的** | **184** | + +184 个里: + +| 类别 | 数量 | +|---|---:| +| **真函数调用** | **177** | +| 析构函数 | 4 | +| typeinfo | 2 | +| vtable | 1 | + +**耦合是行为性的,不是类型性的。** 不是"server 持有一个 `SharedPtr` 所以需要 vtable", +是 server 在**调用前端对象的方法**。 + +### 3.1 按定义方 + +| 定义模块 | 符号数 | +|---|---:| +| `MG_State/GLState` | **157** | +| `MG_Impl/Pipe`(`PipeFill`、`SlotAllocator`) | 16 | +| `MG_Remote/Client` | 9 | +| `MG_Impl/GLImpl` | 2 | + +前五个文件:`TextureObject.cpp` 59、`SamplerObject.cpp` 30、`BufferObject.cpp` 18、 +`FramebufferObject.cpp` 16、`PipeFill.cpp` 9。 + +### 3.2 按引用方——**债主是 P7** + +| 引用方 | 符号引用数 | +|---|---:| +| `DirectVulkan`(Magma) | **227** | +| `DirectGLES`(Espryt) | 111 | +| `MG_Remote/{Wire,Server}` + `MG_Pipe` | 6 | + +去重后按后端归属: + +| | 符号数 | +|---|---:| +| **只有 Magma 要** | **101** | +| 两个后端都要 | 60 | +| 只有 Espryt 要 | 14 | + +单个最大引用方是 `DirectVulkan/Renderer/UniformManager.cpp`(90),其次 +`VulkanRenderer.cpp`(60)、`DirectGLES/Managers.cpp`(53)、`DirectGLES/DirectGLES.cpp`(51)。 + +**这份分布精确地落在路线图已经说没做完的两个阶段上**:P7(Magma 全量迁移)和 P3b/P4b(Espryt 深化)。 +链接耦合不是一笔新债,它是那两笔旧债在链接器上的影子。 + +## 4 这和 "P5c/P5f 已经把跨角色读归零" 矛盾吗?不矛盾 + +两句话都成立,因为它们量的不是一回事: + +- P5c / P5f 关掉的是**运行时**跨角色内存访问:`rsp` 逐帧为 0、`BARRIER_PULLED` 分类为 0。 +- 这次量的是**链接时**符号引用。 + +但**这也不只是"死臂的链接残留"**。取样核实过一个站点: + +`DirectVulkan/Renderer/UniformManager.cpp:1039` 一带是 + +```cpp +const auto& samplerOverride = MGB_CTX->GetTextureUnitObject(unit).GetSamplerObject(); +const auto* effectiveSampler = samplerOverride ? samplerOverride.get() + : texture->GetSamplerObject().get(); +…effectiveSampler->GetMagFilter() == SamplerFilterMode::Linear… +``` + +`MGB_CTX` 在 `PIPE_PUSH` 下是 `&gPipeInputs`,所以这是 `PipeInputs::GetTextureUnitObject`—— +而它在 `MG_Pipe/FieldOwnership.def:33` 被分类为 **FATAL**: + +> `X(GetTextureUnitObject, FATAL, "-", "server reads sampler/view records or verb texture handles; never a client TextureUnit")` + +也就是说:**split 下走到这里是一条具名 Fatal,不是一次错误的读**。P5f 的归零是真的。 +但"具名拒绝"意味着**这条功能没有迁移**,代码还在、符号还在、链接还需要它。 + +所以准确的说法是: + +> 184 个符号是 P5f 那 25 条 FATAL 分类 + 未迁移的对象类字段在链接层面的残留。 +> 运行时是干净的(要么走记录臂,要么 Fatal);链接时不是,而且**链接时是 server 镜像能不能存在的那个判据**。 + +## 5 传输层自己的 6 个——单独拎出来 + +其余 178 个是后端的债,这 6 个不是,它们在 `MG_Remote` / `MG_Pipe` 内部: + +| 引用方 | 需要的符号 | 说明 | +|---|---|---| +| `Server/PipeApplier.cpp` | `Client::ClientSession::NoteApplyThreadEnteredApplier()` / `NoteApplyThreadLeftApplier()` | **server 侧 applier 在调 client 的会话对象**。inproc 下同进程所以成立;spawn 的 server 进程里根本没有 `ClientSession` | +| `Server/PipeApplier.cpp` | `MG_Pipe::MGPipeApplierIsUnbarrieredApply()` | 定义在 `MG_Impl/Pipe` | +| `Wire/PipeWireCodec.cpp` | `MG_State::GLState::DecodeProgramArchive(...)` | 解码器需要一个住在 `MG_State` 里的解码助手 | +| `Wire/PipeWireCodec.cpp`、`MG_Pipe/PipeApply.cpp` | `Client::AdoptTierIsEmulate()` | 采纳档位判据住在 client 侧 | +| `MG_Pipe/PipeApply.cpp` | `Client::ClientSession::Active()` | 同上 | + +**这 6 个是 P6 自己的账**,不能推给 P7/P3b/P4b。它们数量小、形状清楚,应当在 `sm` 或 `lk` 里解决: +前三个是"把函数搬到正确的模块",后三个是"server 不该问 client 要答案"。 + +## 6 给 `c6` 的结论 + +1. **契约不能再写 "server 不链 `MG_Impl`"**。`PipeInputs.h` 与 `P5F-WIRE-COMPLETENESS.md` §1.4 + 的断言按今天的树是**假的**,`c6` 要按 `CONTRACT` §3.3 自己写的规矩"做不到就记账"把它记成债。 +2. **债的归属是 P7(101)+ P3b/P4b(14)+ 两者共有(60)+ P6 自己(6)**,不是一笔含糊的"以后再说"。 +3. **P6 不需要为此阻塞**。P6 的 server 是 fork 出来的同一个二进制,本来就链着整份库—— + `ARCHITECTURE.md:496` 的"一份共享库、两个角色"正是这个意思。 + 所以 P6 可以照常推进,但 `c6` 必须明确写下:**P6 的 server 镜像 = 整份 `libMobileGL.so`**, + 而"一个不含前端的 server 镜像"是 P7 / P3b/P4b 完成之后才谈得上的东西。 +4. **`c6` 要新增一条具名工作**:建立模块 target 边界(object library 或 static lib)。 + 没有它,这个数字永远只能靠 `nm` 事后量,不能变成一道会变红的门。 + 建议形状:`lk` 包顺带把 `MG_Remote/Transport` 切成自己的 target(它本来就是纯度门 A/B/C 的对象), + 其余模块边界记成 P13 的账。 +5. **一道可以现在就立的门**:把本文的 184 记成棘轮,CI 上重算并断言"只减不增"。 + 它能因 P7/P3b/P4b 的真实进展而下降,也能因新写的后端代码又去读前端对象而变红—— + 这正是这个项目要的那种门。 + +## 7 附带发现:ABI 指纹的 build stamp 会**静默变成空串** + +这是实验过程中撞出来的,不在 a6 的清单上,但它直接打在 `hs` 包上,而且是个真洞。 + +**机制**。`CMakeLists.txt:188-195`: + +```cmake +execute_process( + COMMAND git rev-parse HEAD + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL + OUTPUT_STRIP_TRAILING_WHITESPACE +) +string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT) +``` + +**没有 `RESULT_VARIABLE`,没有 `ERROR_QUIET` 之外的任何处理,没有兜底。** +`git` 失败时 `GIT_COMMIT_HASH_FULL` 是空的,`SUBSTRING` 给出空串,配置继续,构建成功。 + +本次实测:配置阶段打印 `fatal: not a git repository: (null)`, +生成的 `generated/MGGitHash.h` 是 + +```c +#define GIT_COMMIT_HASH_SHORT "" +#define GIT_COMMIT_HASH_FULL "" +``` + +**为什么会失败**:这棵树是 git worktree,`.git` 是一个文件,内容是 +`gitdir: C:/Users/geekerwan/.../FoldCraftLauncher/.git/modules/MobileGL/worktrees/MobileGL-disagg` +——一条 **Windows 路径**。WSL 的 git 解析不了它(要 `/mnt/c/...`)。 +本项目正是用 worktree 并行推进各阶段的,所以"从 WSL 构建某个 worktree"这件事会**稳定复现**。 + +**为什么要紧**。`AbiFingerprintInputs::BuildStamp`(`SessionRings.h:563-565`)就是它,注释写明它的职责: + +> *"GIT_COMMIT_HASH_SHORT: two builds of the same sizes can still disagree about a FIELD ORDER, +> which no sizeof can see. nullptr and "" are distinct inputs and neither equals a real stamp."* + +设计时**想过**空值——但只想到"空串是一个与真 stamp 不同的输入"。 +没想到的是**构建会静默地把所有构建的 stamp 都变成同一个空串**。 +于是:**两份来自不同 commit、都在 git 取不到版本的环境里构建的库,ABI 指纹相同**, +而指纹里唯一能看见字段重排的那一项就此失效——没有任何日志、任何门会说一声。 + +这不是一个奇异场景。会命中它的至少有:worktree 跨 WSL 构建(已实测)、 +源码 tarball 构建、CI 上不带 `.git` 的 checkout、把 repo 拷进去的 Docker 构建。 + +**给 `hs` 的要求**(在拆分 `wireFingerprint` / `buildFingerprint` 之外新增一条): +取 stamp 失败必须是**具名的**——要么配置期失败并说明,要么写进一个明确表示"本构建无版本标识"的哨兵值 +并让握手对它具名拒绝(`Dial == Fork` 下两端本来就该是同一二进制,无版本标识意味着这个前提无法验证)。 +**不能继续是空串。** +red-once:在一个 `git` 不可用的环境里构建,握手必须因为"build stamp 缺失"而具名变红,而不是悄悄通过。 + +## 8 另一件顺带记录的事 + +`.gitmodules` 里 `3rdparty/DiligentCore` 有重复配置(`path` 与 `url` 各两份), +`git fetch` 会 warn 并跳过第二份。不影响构建,但迟早要清。