Files
MobileGL/docs/Disaggregated/notes/p5f/P5F-WIRE-COMPLETENESS.md
T
swung0x48 43b5cfa94e [Docs] (Disaggregated): the four index documents keep only summaries, and every phase's detail lives in its own notes/<phase>/README.md
- 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.
2026-09-24 22:21:09 +00:00

16 KiB
Raw Blame History

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 的每一条都靠同一个机制证伪,而不是靠审计:

  1. PipeInputs 按角色分区:gPipeInputs 变成"取当前角色的块"。client 的 fill 写 client 的块, backend 的读取 server 的块。任何今天依赖"两者是同一个对象"的路径,立刻变成 Fatal{UnmigratedPipeInput, "<Field>@<Verb>"},带字段名和 verb 名。
  2. per-context 静态按角色 + 世代分区,同理。
  3. 两者都由一个旋钮武装(建议 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 均不能过门。

  1. 双块车道(§4)integration-split 全绿。
  2. MOBILEGL_IPC_STRICT_ERRORS=1 下 strict-expected-markers.txt 为空,且两侧棘轮仍在 (空表 + 出现任何一对即红)。
  3. FieldOwnership.def 零 BARRIER_PULLED 行;rsp 计数器逐帧读零。
  4. Magma 与 Espryt 跑同一套判据,且 Magma 发布 kCapRunAheadApply——或者明确说明它为什么 不发布,并给出不发布也不掩盖缺陷的理由(ID-132)。
  5. G1 0/0/0/0;G2 名集合不变;G14 测试名只增不删。
  6. 逐包 red-once。
  7. 设备: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 回答。