- ROADMAP, MEASUREMENTS and ARCHITECTURE are rewritten as summaries (333 KB of index text -> 84 KB). ROADMAP's phase table is one line per phase plus a link; MEASUREMENTS keeps headline numbers under its old §1-§13 numbering (plus §14-§16 for P6.5, P7 and P12); ARCHITECTURE keeps every decided rule under the same §1-§17 / appendix numbering, so the `ARCHITECTURE.md §N` references in code still land, and §17 becomes an index of the phase landing shapes. - CURRENT_STAGE_PROGRESS now describes the current stage (the P12 on-screen-server subset) and absorbs CURRENT_STAGE_STATUS, which is removed. The P7 status tables, the P6 closing snapshot and the P6.5 first-wave snapshot move to notes/p7, notes/p6 and notes/p65. - Each phase gets notes/<phase>/README.md: a short summary, then the verbatim material that left the index documents (its ROADMAP row, its MEASUREMENTS section with subsection numbers kept, its ARCHITECTURE §17 part, milestones) and a file index. P8-P11 and P13 get their planned scope; the full open-question list moves to notes/OPEN-QUESTIONS.md. - The phase reports that sat at the top level move into their phase folders with unchanged names: P5D-INPROC-PERFORMANCE, P5E-RUNAHEAD, P5F-WIRE-COMPLETENESS, P6-SPAWN-PLAN, P6-CONTRACT-DRAFT, P6-ENDSTATE-REVIEW. Their relative links, CONTRACT-P6.md's links to them and one tool docstring are updated; notes/README.md maps every old path and section number to its new place. - 71 files under notes/recovered/ that were byte-identical copies of phase-folder files are removed. - Corrections made while condensing: the server process reads Transport as Spawn (CONTRACT-P6 §3.1 amended the old "child forces monolith" rule); both backends now publish kCapRunAheadApply; MOBILEGL_IPC_RESPAWN is a named refusal and MOBILEGL_IPC_IDLE_EXIT_S is not parsed; the Redmi pin row is in tools/device_bench/pin_device.sh. check_doc_citations over docs/Disaggregated/*.md: 0 problems. No relative link is newly broken.
16 KiB
P5f — 一切状态上 wire:跨进程前的最后一次归零
已收官(2026-09-20):f0 / f1 / fc / fe / fm / fs / fr / fv、字段归属收口、 主机出口门、Redmi 双后端六个 clean-boot 臂与跨模型族终审均完成。 最终行为验收头
cfca93c7;收口报告为当前结论,交接为下一阶段入口。P6 的 a6 / c6 / spawn 尚未实施。 当前字段:41 RECORD_SUPPLIED / 6 APPLIER_DERIVED / 0 BARRIER_PULLED / 16 FATAL; 两份 marker 棘轮为空,双后端逐帧rsp=0。旧指针 getter 保持 FATAL,不以改名消债。以下 §0–4 保留立项背景与设计,§1–2 的读点、行号及 15 字段数字均是 历史基线
feat/disaggregated @ 7f1d2134,其中“今天”“尚未”等不代表当前状态。 路径在MobileGL/下;各包报告保留其提交时快照,后续闭环见收口报告。
0 目标与出口判据
目标:两个角色之间,除 SEG_CMD / SEG_STAGE / SEG_REPLY / SEG_EVENT / caps 快照 / 控制帧
之外零交换。不得有任何一方读写另一方的内存、也不得依赖"两者其实是同一个对象"。
P5c 立过同样的规则(规则 E),但它的判据是审计——人读 59 行清单。P5f 的判据是机器:
出口判据:
inproc下给两个角色各自一份PipeInputs(§1),以及各自一份所有 per-context 的进程级静态,整条integration-split仍然全绿,且MOBILEGL_IPC_STRICT_ERRORS=1下Harness/strict-expected-markers.txt为空。
这条判据的价值在于它不需要第二个进程就能证伪跨进程的假设。今天两个角色共享一份
gPipeInputs,所以"server 读到了 client 填的值"和"server 读到了记录送来的值"在运行时完全
无法区分——这正是 P6 第一天会踩的坑。把块拆成两份,前者立刻变成 poison Fatal。
1 gPipeInputs 是什么,拆进程后它会怎样
1.1 它是什么
MG_Backend/MGPipe/PipeInputs.h:835:
inline PipeInputs& gPipeInputs = *new PipeInputs();
一个普通的进程级全局对象,不在任何共享内存段里。它是“后端被推送的状态块”——backend 不再每
draw 去读 MG_State::pGLContext,改为读这个块。每个字段带一个归属类
(CONTRACT-P5 表 2 / R-7,手维护的一半在 MG_Pipe/FieldOwnership.def):
| 归属类 | 量 | 含义 |
|---|---|---|
RECORD_SUPPLIED |
41 个字段(P5c rv 之前 32);由推导得出,不列在表里 | 推送的记录供给整个字段,server 永远不需要 client |
APPLIER_DERIVED |
4 行 | applier 从它已经应用的记录里写出它 |
BARRIER_PULLED |
21 行 / 15 个字段 | P5 的债:server 读的是 client 的残余填充留在“那唯一一份共享的 gPipeInputs”里的值 |
FATAL |
4 行 | 没有载体,缩减路径也不读;读到即 abort |
最要紧的一点,先说:BARRIER_PULLED 里好几个字段存的不是值,是指向 client 对象的指针。
// MG_Impl/Pipe/PipeFill.cpp:274 —— 裸指针,指进 client 的纹理单元数组
dst.m_textureUnitBase = &ctx.GetTextureUnitObject(0);
// MG_Impl/Pipe/PipeFill.cpp:245 —— 前端对象的 SharedPtr
dst.m_programForDraw = ctx.GetProgramForDraw();
读侧照着解引用(PipeInputs.h:669):
TextureUnit& GetTextureUnitObject(Int unit) { return m_textureUnitBase[unit]; }
PipeFill.cpp:2591-2592 对 set_shader_buffers 那组的说法是同一件事:该字段是
“four raw bases into the frontend's binding-point table”。所以这不是一块照搬过去就能用的状态。
1.2 一次 draw 的完整生命周期
以 glBindTexture(GL_TEXTURE_2D, 7) 之后一次 glDrawArrays(GL_TRIANGLES, 0, 3) 为例。
① glBindTexture:MG_Impl 更新 MG_State 的影子状态,tracker 置脏位。什么都不发。
推送只发生在 verb 边界。
② glDrawArrays → MGPipeValidateForVerb(kVerbDrawArrays)(MG_Impl/Pipe/PipeFill.cpp:3347),
四步:
| 步 | 做什么 | 碰 gPipeInputs 吗 |
|---|---|---|
| 1 | 判定本条记录设不设障(barriered / fillOwed) |
决定第 4 步填不填 |
| 2 | 脏位走查 tracker.Update(*ctx, verbClass) |
否 |
| 3 | 发射:14 个 emitter 把脏子系统变成记录;纹理绑定变成 set_texture_bindings,带的是句柄 |
否 |
| 4 | 残余填充:记录没供给的字段,CopyField(inputs, *ctx, field) 从活的 GLContext 抄进块里,盖上本 verb 的世代戳 |
是——BARRIER_PULLED 就是这一步 |
随后 draw 记录本身写进 SEG_CMD ring 并敲门铃。设障就等 appliedSeq;不设障就直接返回(P5e)。
③ server apply 线程:从 ring 取记录 → PipeApplier::ApplyOne → 分发到 verb sink → 调后端
DirectGLES::DrawArrays。
④ 后端 PrepareForDraw() 取它要的东西,两条路:
- 走记录:用记录带来的句柄查 server 自己的对象表。跨进程成立。
- 走
gPipeInputs:GetTextureUnitObject(0)返回m_textureUnitBase[0],而那个指针指进 client 的数组。今天成立,只因为两个角色同地址空间、且 barrier 保证此刻 client 停着。
⑤ 后端发出真正的 glDrawArrays 给厂商驱动。
字段的寿命:第 4 步填入并盖上 verb N 的戳;后端在 verb N 的 apply 期间读;到 verb N+1 若不
重填,读到的就是旧戳 → Fatal{UnmigratedPipeInput, "<Field>@<Verb>"}(MOBILEGL_PIPE_POISON)。
一个 verb 一次,不跨 verb 存活。
同一个例子今天分两条臂,正好是 §2.1 那张表的缩影:
| 纹理绑定怎么到后端 | |
|---|---|
| Espryt | 走记录:set_texture_bindings 带句柄 → server 自己的表。P5e 已退役 GetTextureUnitObject。跨进程成立。 |
| Magma | 仍读 gPipeInputs.GetTextureUnitObject(0) → client 的数组。跨进程不成立。 |
FieldOwnership.def 里这一行的原文就是 "P5e (Espryt unbarriered), P7 (Magma)"。
1.3 为什么今天能工作
因为两个角色在同一个进程、同一个 gPipeInputs 对象上,而 verb barrier 保证同一时刻
{GL 线程, apply 线程} 至多一个可运行——所以这个块任何瞬间只有一个写者
(PipeInputs.h:904-908 的裁定)。client 填,server 读,中间不经过任何载体。
BARRIER_PULLED 这一类的定义就是这句话:“server 在 barrier 把两个线程隔开时,读 client 的残余
填充留在 gPipeInputs 里的值。合法,并且被计数(PipeStats::CallClass::ResidualPulls)。”
1.4 拆进程后它会怎样
client 进程和 server 进程各有一个 gPipeInputs,是两个不同的对象。
RECORD_SUPPLIED/APPLIER_DERIVED的 45 个字段没问题:值本来就由记录或 server 自身产生, server 侧的块会被正常填上。BARRIER_PULLED的 15 个字段没有填充者。第 4 步写的是 client 那一份;server 那一份从来 没人写过,FilledGen恒为 0 → 后端每次读都Fatal{UnmigratedPipeInput}。MG_Impl根本不在 server 进程里,所以连“让 server 自己去 fill”这条退路都没有。
而且不能靠“把这个块也放进共享内存”解决(这是最容易想歪的一步):里面存的是裸指针和
SharedPtr(§1.1)。就算两个进程映射同一段内存,m_textureUnitBase 指向的 client 堆地址在
server 进程里没有意义,SharedPtr 的引用计数更跨不过去。
头文件自己写着这件事(PipeInputs.h:279):当前 verb 边界的清理“对 inproc 足够——两个角色共享
一个进程、一个 gPipeInputs——而对 P6 是误导的,那里 MG_Impl 根本不在 server 里”。
以及 :902:rsp 的值“就是 P6/P7/P8 债务的大小”。
所以 P5f 对 gPipeInputs 的要求:不是共享它,而是让这 15 个字段各自换一个不依赖对方地址
空间的来源——升成 RECORD_SUPPLIED(值随记录过线)、升成 APPLIER_DERIVED(server 自己推)、
或确认缩减路径根本不读它而降成 FATAL。
这也正是 §4 双块演练为什么有效:inproc 下就给两个角色各一份,第 4 步写 client 的、后端读
server 的,今天所有靠“其实是同一个对象”蒙混过去的路径立刻变成具名 Fatal,带字段名和 verb 名
——不用拆进程就拿到完整清单。
2 清单(在基线头上实测)
2.1 BARRIER_PULLED 的 15 个字段
| 字段 | 退役阶段(FieldOwnership.def 原文) |
|---|---|
GetBoundVertexArray |
P5e (Espryt unbarriered), P7 (Magma) |
GetBufferBindingPoint |
P5e (Espryt unbarriered), P7 (Magma) |
GetFramebufferBindingSlot |
P5e (Espryt unbarriered), P7 (Magma) |
GetImageTextureBinding |
P5e (Espryt unbarriered), P7 (Magma) |
GetProgramForDraw |
P5e (Espryt unbarriered), P7 (Magma) |
GetProgramForDispatch |
P5e (Espryt unbarriered), P7 (Magma) |
GetTextureUnitObject |
P5e (Espryt unbarriered), P7 (Magma) |
GetTextureObject |
P7 |
GetBufferBindingPointCount |
P7/P13 |
HasOpenTransformFeedbackSpan |
P7/P9 |
GetTransformFeedbackProgram |
P3b/P4b (Espryt), P7 (Magma) |
GetProgramObject |
P9 |
ValidateProgramName |
P9 |
RecordError |
P9 |
GetBufferBindingSlot |
P8 (indirect), P9 (readback), P13 (transfer) |
八个字段 P5e 只为 Espryt 的未设障路径退役了,Magma 一条都没动。 这是清单里最大的一块。
strict 车道当前实测活着 10 对 <field>@<verb>(Harness/strict-expected-markers.txt),
其中两对是 escalation-barriered 的 client vertex array 条目(P8)。
2.2 Magma(DirectVulkan)
P7 原本是"全量迁移 10 个子系统",里面混着与拆分无关的工作(内容寻址 CSO、内部 shader 烘焙、
定宽重写)。P5f 只取其中会阻碍跨进程的部分:§2.1 里带 "P7 (Magma)" 的那 9 个字段,以及
Magma 侧一切直接读前端对象 / 写 client 存储的站点。P5c 审计的 T5 是已知的一条:
Magma 生成 mip 直接写 client 的 level 存储(DirectVulkan/Renderer/VulkanRenderer.cpp:1562-1590)。
Magma 今天不发布 kCapRunAheadApply,所以它跑 lockstep——lockstep 掩盖的正是这类缺陷,
与 P5e 的 ID-132 是同一条教训。
2.3 EGL / surface 控制面(P5c 审计 G4)
十二个 Server* forwarder(MG_Remote/Server/ServerLoop.cpp:802-1022)经单槽邮箱传
函数指针 + 栈上 void*(ServerLoop.cpp:533-568、:645)。protocol.fbs:166-205 的
SurfaceOp / SurfaceReply / WindowKind schema 已定但无人使用。
跨进程后函数指针与栈地址都无意义。
fc 已落地:行号以基线
7f1d2134为准(本节是"在基线头上实测"的清单);落地后的形态、 逐 op 映射与契约草稿见notes/p5f/fc-report.md与notes/p5f/fc-contract-draft.md。
2.4 进程级静态,语义却是 per-context
已知两个:g_syncedRenderStateParameters(MG_Backend/DirectGLES/DirectGLES.cpp:4586)与
ScopedDefaultUnpackState::s_synced(CONTRACT-P5C.md:538-539 记为 P6 的)。
完整清单未知——这类缺陷 inproc 结构性看不见,必须普查。
2.5 P5c 留下的具名豁免 scope
MGPipeFrontendKeyedRegistryScope 在 DirectGLES.cpp 还有约 12 个活跃站点
(:2542、:4505、:5540、:5659、:5940、:6490、:6576、:7092、:7682、:8213、:11231 …),
背 P3b/P4b 的债:registry 仍按前端地址 / lifetime-id 键控。
2.6 反向通道(P9 的一部分)
XFB scatter 对 client shadow 读改写、OnXfbScatterReady、OnTexturePullRequest 与终止符、
OnGlError 有序化。其中凡是直接触碰 client 内存的归 P5f;纯粹的异步化 / 批处理 / 排序
(SEG_REPLY 异步槽池、PBO fire-and-forget、epoch 排序)仍归 P9。
2.7 class C
MG_Remote/Client/EmitTables.cpp 里 10 处具名拒绝。不归 P5f:拒绝是诚实的,
Fatal{UnmigratedVerb} 在两个进程下行为一致。它们限制的是 spawn 车道的覆盖面,不是它的正确性。
3 范围边界
| 取 | 不取 |
|---|---|
| §2.1 全部 15 个字段(含 Magma 的 9 个) | P7 的性能与架构项:内容寻址 CSO、内部 shader 烘焙、DynamicBackendParameters 定宽重写 |
| §2.2 Magma 侧一切跨角色读写 | Magma 的功能补齐(占位纹理原生化、具名 UBO host payload 等),除非它们本身是跨角色读写 |
| §2.3 控制面帧(原 P6 的) | 真窗口到达(P12) |
| §2.4 静态量按世代/角色分区 | — |
| §2.5 registry 按句柄重键 | P3b/P4b 的其余项(view 索引重映射、workaround 删除) |
| §2.6 反向通道里触碰 client 内存的部分 | P9 的异步化与排序 |
| — | class C(§2.7)、chunked readback(P9)、persistent map 分档(P11) |
判断准则只有一条:它在另一个地址空间里还成立吗? 不成立 → P5f。成立但慢/丑 → 留给原阶段。
4 中心机制:双块演练
P5f 的每一条都靠同一个机制证伪,而不是靠审计:
PipeInputs按角色分区:gPipeInputs变成"取当前角色的块"。client 的 fill 写 client 的块, backend 的读取 server 的块。任何今天依赖"两者是同一个对象"的路径,立刻变成Fatal{UnmigratedPipeInput, "<Field>@<Verb>"},带字段名和 verb 名。- per-context 静态按角色 + 世代分区,同理。
- 两者都由一个旋钮武装(建议
MOBILEGL_IPC_ROLE_SPLIT_STATE=1),默认关,CI 开一条车道。
这等于在不拆进程的前提下把进程拆分的语义先跑一遍。代价是一个旋钮和一次分区; 回报是 P6 那天不会有任何一条"第一次执行"的路径。
5 包(全部完成;ID-137 分工)
f0 普查(已集成):§2.4 的完整静态清单 + §2.6 的逐站点清单 + 字段载体判定
└─ f1 双块机制 + 旋钮 + 车道(已集成;初始红清单已全部退役)
├─ fe Espryt 字段读者 + XFB 快照 + 共用记录接线(已集成)
├─ fm Magma 的 9 个字段 + T5 + Magma 侧跨角色读写(已集成)
├─ fc 控制面帧(§2.3,已集成)
├─ fs 静态量按角色/世代分区、XFB lifetime、server liveness(已集成)
├─ fr frontend registry / allocator 无 apply-side 豁免(已集成)
└─ fv 反向事件 callback 所有权与 frontend fallback 退役(已集成)
└─ 收口(完成):字段归属零残余、两份空棘轮、逐帧 rsp 门、设备及异族审查
f1 先落是故意的:让红先出现,再逐包消红,而不是各包自称做完之后再找一个判据。
6 出口门
七项均已完成,数字与原始证据路径见 close-report.md。 双块 212 项为 206 PASS + 6 个按名称和原因锁定的既有 skip;RSP 独立门必须为 双后端 2 PASS / 0 skip。缺失、重复、未运行的 JUnit 条目或额外 skip 均不能过门。
- 双块车道(§4)
integration-split全绿。 MOBILEGL_IPC_STRICT_ERRORS=1下strict-expected-markers.txt为空,且两侧棘轮仍在 (空表 + 出现任何一对即红)。FieldOwnership.def零BARRIER_PULLED行;rsp计数器逐帧读零。- Magma 与 Espryt 跑同一套判据,且 Magma 发布
kCapRunAheadApply——或者明确说明它为什么 不发布,并给出不发布也不掩盖缺陷的理由(ID-132)。 - G1 0/0/0/0;G2 名集合不变;G14 测试名只增不删。
- 逐包 red-once。
- 设备:Redmi
2f7cbe2e,双后端,reboot-clean 配对 A/B。性能只记录。
阴性对照:关掉双块(回到共享一块)必须让至少一条具名用例由绿转红——否则这个机制 什么也没证明。这是 ID-122 的教训:对照要证伪的是机制本身。
7 与 P6 的关系
P5f 已完成跨角色内存依赖归零的机器门,P6 的前置条件解除。
P6 的 a6 仍须核对独立进程的启动、句柄/映射和配置边界;本阶段的 inproc 演练不代替
spawn 实现与跨进程设备验证。P6 尚未开工。
P6-SPAWN-PLAN.md 与 P6-CONTRACT-DRAFT.md 里凡写"PENDING a6"的行,
其中属于 §2 的部分改由 P5f 回答。