a6 is the read-only audit P6-SPAWN-PLAN puts first. Seven static items, each
produced by one auditor and re-resolved line by line by one adversarial
verifier; 198 rows, 65 of them contested, and only the surviving ones carry
into the report. Item 5 - can the server link without MG_Impl - is the one
experiment, and it is answered.
The headline is that P6's transport half is smaller than the plan assumed
(fc and fs really did land) and its process/role half is much larger. Four
statements in P6-SPAWN-PLAN / P6-CONTRACT-DRAFT are false at this head:
- `spawn` is not an observable state at all. ConfigLoader.cpp:339-346
recognises it and then writes Transport = Monolith, so "the child was
forced to monolith" and "the parent fell back" are the same value and
that named refusal cannot falsify itself. Meanwhile 226 live lines test
`Transport != Monolith` to select the SERVER arm, 148 of them in
MG_Backend - so ARCHITECTURE.md:488's rule, implemented literally, flips
the whole backend to frontend glue in the one process with no frontend.
- `cp` is not transport-only: the InitCapabilities REQUEST has no wire form,
and the codec refuses to encode the kind a live client still calls.
- the device-lost latch is not implemented; it was booked as carriage.
- the server cannot link without MG_Impl: 184 symbols, 177 of them real
function calls, owed 101 to P7 / 14 to P3b-P4b / 60 shared / 6 to P6.
There is also no module target to link, so the question could not have
been answered by editing a link line.
Four more blockers the earlier review missed: the apply thread aborts in
ConsumerDoorbell before popping its first record (contract 3.9's ruling, which
lk should retire rather than amend); neither side of the control plane is
wired to a socket; the control-reply wait has no deadline, so a frozen peer
wedges the GL thread inside eglMakeCurrent; and SurfaceReply cannot carry a
MobileGLResult, so a client cannot tell a dead server from a legitimate
refusal.
The abort census makes dl's funnel a redesign rather than a rename: 92 sites,
30 distinct family words against a 7-value wire enum, and three unbounded
peer-driven allocations with no try/catch anywhere under MG_Remote.
P6-ENDSTATE-REVIEW.md is the re-examination of P6 against the gfxstream-shaped
end state that produced a6's three added items. a6 overturned four of its own
claims - the MG_State eight's shape, the abort count, m_windowHandle's
injection points and a predicate name collision - and those are corrected in
place rather than left to be rediscovered.
Docs only. No code, no gate, no behaviour.
18 KiB
a6-audit-v1 — spawn 之前,进程/角色边界还差什么
只读静态审计,头
b3b9c2ee(git rev-parse HEAD,2026-09-21)。无 git 变更。 八项中的第 5 项(能否不链MG_Impl)是唯一的实验,单独成篇:a6-link-experiment.md。 全部 198 行逐条清单、复审异议与遗漏补充:a6-audit-rows.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:,然后写
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: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,以下四处我写错了,按树更正:
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 仍应先分类这一族),理由和取样对象要换。- abort 数是 92 不是 93(94 命中减 2 处注释)。
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 点。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 的决定清单(按该先决的顺序)
Transport在 spawn 的 server 进程里读作什么? 148 行MG_Backend活代码和两个 Fatal 角色守卫一起翻在这一个 bit 上。 推荐:子进程保持Spawn,反递归另设Dial = No。Session::Fail的第一个参数:留 7 值 wire 枚举,还是引入一个更宽的内部家族枚举?今天有 30 个家族词。InitCapabilities请求的 wire 形式(§2.1)。这是cp的前置,不是传输。- 控制 socket 谁读:新
mgl-srv-io线程投进现有单槽邮箱(ServerLoop.cpp:673-727一行不改), 还是给 apply 线程的ready谓词加第四项? - 控制回复等待的期限与名字(§3.3)。
SurfaceReply是否追加一个 result 字段(§3.4)。- spawn 下谁铸
seq,seq == 0在 wire 上什么意思(§3.5)。 - 窗口种类:黑名单还是白名单;以及
WindowKind::None带非零 token 今天被接受这件事怎么办。 - 一 server 进程一 guest 写进契约,第二个具名拒绝(§3.6)。
- 四个角色守卫的重新武装条件(§3.7)——尤其
ContextIdentity()恒 nullptr 那个会崩的洞。 - 三处无界分配的绝对上限(§4),以及
MG_Remote下要不要有 try/catch。 - server 镜像链不链
MG_Impl:按 a6 的答案,P6 链,契约把PipeInputs.h:281与ARCHITECTURE.md:9的断言改成记账。 MGPipeRoleSplitActive()先改名,再引入MGPipeSplitActive()。- 三个新谓词必须
inline constexpr可折叠(§1.4),否则 G1 红。
8 未决(读不出来的)
- spawn 的 server 进程里谁跑 applier、在哪个线程上。 十余处
ServerLoop::OnApplyThread()守卫全部取决于它; 若ServerMain在主线程驱动 applier 而不经ServerLoop::Start,g_applyThreadKey恒 0,全部守卫静默失效。 - server 的退出原语是
_exit还是exit。 它决定g_processTeardown(Managers.cpp:188-197)是否被置位, 进而决定七个 twin 析构是走早退还是真调进驱动。 - 握手要不要断言子系统掩码。
EsprytSlotTablesEnabled()每进程从Features.PipePushlatch, 两个进程各自解析自己的环境,而ARCHITECTURE.md:488的 envp 剔除清单不含MOBILEGL_PIPE_*。 - P6 是否把任何段映射成只读。
ShmSegment::Adopt已有Map(bool readOnly),但两个调用点都传false。 DualBlockScenario.cpp:69-73断言serverIdentity != nullptr,这是一个进程内事实; 而 G2/G14 要求integration-spawn与integration-split逐名相同。这条用例在 spawn 下怎么办。