Files
MobileGL/docs/Disaggregated/P5E-RUNAHEAD.md
T
swung0x48 be345d711c [Docs] (Disaggregated): P5e progress, in full - the landed-content table, open items, ruling index and phase report
- New P5E-RUNAHEAD.md: P5e's phase report, a sibling of P5D-INPROC-PERFORMANCE.md. It carries
  the twelve-package list, the three defect classes and the lesson each one cost (a runtime arm
  no gate covered / build flavours no gate covered, where the FIRST gate added for them was
  itself a false green / a guard that could never fire), the profile and both device tiers, the
  unfinished items, and the next opportunity run-ahead unlocked.
- CURRENT_STAGE_PROGRESS.md: the stage table's P5e row becomes twelve packages + run-ahead
  armed + ID-80..136; new section 2.7, the P5e landed-content table, in the same shape as
  P5c's and P5b's; section 2.5 retitled as historical; two sections both numbered 4 fixed; five
  P5e entries added to the open items; next-step item 1 rewritten from c0e to the evidence the
  close still lacks; the ruling index extended from ID-79 to the load-bearing entries of
  ID-136.
- ROADMAP.md: the P11 row gains P5e's correction - retiring the draw lockstep did NOT need
  gPipeInputs versioning, because that race was in the metadata scalars. The row still holds
  for the general case, and contract section 3.2's per-role stamp storage is still unlanded.
- README.md: the status line gains P5e and run-ahead, and "next is P6" becomes "P6 comes after
  P5e".

Doc citation check: 3 problems, all pre-existing entries in CONTRACT-P5.md, none related to
this change.
2026-09-18 21:30:29 -04:00

13 KiB
Raw Blame History

P5e:退役 Espryt draw path 的 lockstep(run-ahead)

头 5c007b22,feat/disaggregated。契约 MobileGL/MG_Remote/CONTRACT-P5E.md,裁定 ~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md(ID-80..136)。run-ahead 已武装,阶段未收官(见"什么没完成")。

目标

P5c 让 inproc 成为只经 wire 交换的诚实两角色,P5d 把它的 CPU 代价压下来,但 R-1 的 lockstep 还在: 客户端每条 kCtxVerb 都要等 appliedSeq == emitSeq。P5e 退役 draw path 上的这道等待——客户端发了就走, 只有"设障"的记录才等。

一条记录能否未设障,取决于应用它时会不会读客户端内存。所以这个阶段的绝大部分工作不是改等待逻辑, 而是把后端仍在读前端对象的那些点迁到记录上;等待规则本身是最后一步。

落地了什么

十二个包,三波:

波 包 一句话
1 c0e 契约与线上行:PipeCalls.def 第五列 WaitClass + MGPipeWaitClassFor、set_program_bindings(opcode 80)、kCapRunAheadApply(位 10,只在 DirectGLES 臂)、kDrawClientArrays、子系统位 13、两个旋钮、跨包 seam
1 id 身份:registry 按 {slot, gen} 重键、by-handle resolver、分配器守卫。ID-96 的答案是空集
2 vi 顶点输入 / VAO / 索引
2 sb 缓冲绑定点;MGPShaderBuffers::WritableMask 加宽(ID-104)
2 pg program:逐 link 的反射归档进 SEG_STAGE、set_program_bindings 的三条尾
2 tx2 纹理与 sampler、staged store 的按句柄读
2 fb framebuffer、附件、image、blit
2 ra 传输机制:等待规则、present credit、gPipeInputs 变 server 角色内存
2.5 fix1 设备发现的 monolith 臂回归(ID-107),见下
3 pa program 家族在 draw / dispatch 路径上离开前端
3 mv multi-draw 的前端 VAO 读点;并给五个 multi-draw 档位补上门禁条目
3 gl 门禁机制本身,加两处潜伏崩溃
3 ra2 翻开开关后暴露的竞态,以及间接绑定那次 pull

最重要的一条实现事实:退役 GetProgramForDraw@DrawArrays(每帧每次 draw 都触发的那次拉取) 没有改动任何线上结构。记录早就带着逐 link 的 ProgramArchive;缺的只是仍去问前端对象的读者。 本阶段多数迁移是这个形状——数据已经在线上,读者没跟上。

三类缺陷,以及各自教的东西

这一节是本阶段最可复用的部分。

1. 一整条运行时臂没有被门禁(ID-107 / 109 / 110)

push 构建有两条服务端臂,运行时由 MG_Config::Transport 选;而 integration-split 的每一条条目 都导出 MOBILEGL_TRANSPORT=inproc,split 专属场景又都在 SetUp 里自跳过——手机实际发布的那条臂在 门禁集合里零条目。44f91c74 上 unit 2250/2250、integration-split 113/113 全绿,同一个头 ctest -L integration-gpu 是 1157/1294,137 个 SEGFAULT、横跨 38 个 scenario,设备上表现为 monolith 启动即崩。66621767 修复,gpu 成为常设门禁步。

代码形态的教训:"已经记下句柄"不等于"记录臂会被选中",这是两句不同的话;只因"所有写入点碰巧都被 传输门住"而安全的座,是靠链条安全,要改成自述式的传输测试(ID-110 的 MGB_TEXTURE_RECORD_ARM_SELECTED())。

2. 构建 flavour 也没有被门禁,而且第一次补的门禁是假绿(ID-124)

门禁只配 split flavour。补上 flavours 步后实测:四个 flavour 里只有 split 编得过。

更值得记的是补门禁本身翻了一次车:第一版 pull 臂报 rc=0 / 982 TU,是假的—— CMakeLists.txt:496-500 规定 MOBILEGL_BUILD_DISAGGREGATED=ON 强制 MOBILEGL_PIPE_PUSH=ON, 所以 -DMOBILEGL_PIPE_PUSH=OFF 被静默覆盖,那一臂其实是 split 构建的第二份拷贝;而 CMakeCache 里 那一行仍然写着 OFF。改成从 compile_commands.json 断言实际生效的宏、不符即失败,才拿到真数字 (743 TU,rc=1)。

规则:一条臂的证据必须包含"它确实是那条臂"的证明。 传的 flag 不是证据,cache 不是证据。 同源:ctest -L 是正则,新标签不得以既有标签为前缀,且要数一遍(ID-131)。

3. 一个永远打不响的守卫(ID-132 / 135)

翻开两个常量后 unit / gens / flavours 仍全绿,而 isplit 掉到 112/181、gpu 掉到 1290/1359,且 flaky(-j 4 红 71、-j 1 红 68,单跑一条先过后崩)。全量普查:70 条红、 只涉及四个字段、横跨十一个 verb。

判据:一个字段在一个 verb 上稳定失败 = 漏迁移;四个字段散在十一个 verb 上且 flaky = 竞态。

根因不是当初的猜测。gPipeInputs 的字段值已由 SuppliedFieldMask 分区、不竞态;竞态的是 元数据标量(CurrentVerbSerial、FilledGen[]、m_currentVerb、m_serverStampedVerb)。 而让它长期不可见的,是守卫本身:RefusePipeInputsTouchWhileApplierOwnsIt 的 run-ahead 臂对任何 isBarrieredFill 一律豁免,豁免依据是一句关于未来的断言——"客户端马上就要 park"——而真实顺序是 fill → emit → park。修法是把那句断言变成事实(写块之前强制等待),并让守卫去测事实。

同源的第二个缺陷:MGP_VERB_OP_LIST 把整个 draw 家族并到一行,于是十九个索引 draw verb 都答"设障"、 都去填充、都不 park。

契约 §3.2 早就规定了 per-role 的 stamp 存储,却从未落地——那个缺口正是这次竞态成立的前提。 一份描述"设计意图"而代码另做一套的设计文档,是这类缺陷得以存活的方式;ARCHITECTURE.md 的对应段落 已按落地实现改写。

贯穿三者的方法论结论(ID-132)

strict 车道硬绿是必要条件,而且结构性地不是充分条件。 strict 跑在 lockstep 下,ApplyOne 把每条 记录都盖成 barriered,所以"只在客户端不再等待时才发生"的事,它按构造看不见。它量的是剩余欠债, 不是 run-ahead 的正确性。阶段出口必须有一条独立的、翻开开关之后的车道。

strict 车道成为可被脚本判定的硬绿

GetProgramForDraw@DrawArrays 62 → 0,GetProgramForDispatch@DispatchCompute 7 → 0, GetBoundVertexArray@DrawArrays 9 → 0。采纳规则最终是三个析取项,没有一条是预先设计出来的, 每一条都是被一条真实条目逼出来的:

  1. 该 verb 的线上 op 静态设障(ID-116);
  2. 该字段的退役相不指向本阶段(ID-125);
  3. 记录是按升级而非按静态类设障的(ID-128)——XFB 活跃与客户端数组两种 draw,契约把两者都放在 P5e 之外。

白名单由 scripts/gen_pipe_field_ownership.py 生成而非手抄:ra 原本落在 CI 里的 13 行手抄表 在两个方向上都错(漏了 GetFramebufferBindingSlot@ReadPixels 的 21 条,又多了不该采纳的 GetTextureObject@CopyImageSubData)。被采纳的 pull 响而不致命(ID-117):同样的标记语法,头部 标签换成 Admitted{,按 (field, verb) 去重。车道是两侧棘轮——新出现的对要认领,不再出现的对 要删除,否则会"烂绿"。

棘轮在合并当天就自己点了名:GetProgramForDraw@DrawArrays 不再出现(pa 退役了它), GetBufferBindingSlot@DrawArrays 新出现(mv 的间接档位逼出了它,而 mv 自己在报告里先命名了这行 潜伏项,然后才让它触发;它退役在 P8)。

门

门 阶段初 现在
unit 2250 2256 / 2256
integration-split 116 179 / 179
integration-gpu 1294 1357 / 1357
构建 flavour 只有 split 编得过 pull / push / split 三个全绿
逐条 strict 普查 7 绿 / 109 红 179 绿 / 零 Fatal{,八行 Admitted{

新增的 65 条车道条目,全部覆盖此前一条门禁条目都没有的臂或档位。两条预期红车道各自具名断言: integration-clientarrays-split((Multi)DrawArrays+CLIENT_ARRAYS)、integration-magma-split (GetFramebufferBindingSlot@Clear)。

数字

设备:Redmi 2f7cbe2e,FCL fordebug + Minecraft 26.3-rc-3 世界 test,DirectGLES。

先讲定频,因为第一轮矩阵作废了

第一轮没有定 CPU 频:monolith 自己那一臂的逐帧 CPU 在两次运行之间从 4.43 跳到 7.07 ms, 而慢的那次温度更低(53.5 vs 58.9 °C)——是 walt governor 把 policy0 从 2745 拉到 748 MHz, 不是热降频。未定频的逐线程 CPU ms/帧不能跨臂比较。

tools/device_bench/pin_device.sh 不认识本机 serial 并明确拒绝猜(正确,猜错会静默失败); ~/w7/notes/p5e/pin_redmi.sh 按本机节点写,写序 min→硬件底 → max→目标 → min→目标,并回读 scaling_cur_freq 验证,每轮跑前跑后各 check 一次。

为什么值得做(侧写,7c6f6886,翻开关之前)

symbolized simpleperf cpu-cycles,客户端 GL 线程:SessionProducer::WaitForAppliedOrEventBacklog 31.58% 自身时间,第二名只有 3.72%。计数器同期显示客户端每帧进入 doorbell 等待约 916 次 (对 ~849 次 draw),99.8% 靠自旋解决;MOBILEGL_IPC_SPIN_US=0 让每次都 park,帧率塌到 20.9。 代价是会合本身,不是自旋参数——所以修法是"不再每次 draw 会合",不是"把每次会合做便宜"。 等待之后,客户端线程是一条 1-3% 的长尾:没有第二个杠杆。

渲染距离 12:两条臂都撞面板上限

臂 fps p50 fps max client ms/帧 apply ms/帧
monolith 115.4 / 115.6 117.5 / 117.2 6.25 / 6.45 —
inproc run-ahead 118.0 / 117.6 118.6 / 118.5 6.87 / 6.92 4.35 / 4.43
inproc lockstep(RUN_AHEAD=0) 111.6 113.7 8.16 6.53

monolith 与 run-ahead 都撞到 120 Hz 面板,只有 lockstep 撞不到。所以这一档回答的是"能不能撞到 上限",不是"谁更快"。

渲染距离 32:可比的一轮

renderDistance:32,~3550 draws/帧(VD12 的 4.2 倍),两条臂都远离任何上限(游戏自身 enableVsync:false、maxFps:260,实测 max 只有 37-71)。静置提到 75-150 s——VD32 的区块流式加载 远比 VD12 长。每臂五次,交错。

臂 n fps p50 中位 p50 范围 client ms/帧 中位 apply ms/帧
monolith 5 58.5 56.3 – 67.9(±10%) 16.19 —
inproc run-ahead 5 59.9 56.8 – 61.1(±4%) 15.72 11.1 – 12.1
inproc lockstep 2 35.5 34.5 – 36.5 26.17 19.7 – 20.5

结论:齐平。 run-ahead 在两个中位数上都略微领先,但差值远在 monolith 自身离散度之内,不能读作 "比 monolith 快"。确实超出噪声的是稳定性:monolith 五次 ±10%,run-ahead ±4%。mono1 的 67.9 是 全场唯一离群值(装包后第一次运行)。

相对它取代的 lockstep:p50 +69%、client CPU −40%、apply CPU −43%。 负载越重越值钱,符合预期—— 会合次数随 draw 数走,VD12 每帧约 900 次,VD32 约 3600 次。

两条臂都是 client 线程打满(约 94%),所以 client ms/帧 这一列是各自真正的瓶颈,可直接比。

什么没完成

  • BRIEF-P5E §3 的 item 4 与 item 5:三条阴性对照、逐包 red-once 重跑。item 5 的证据形式在当时 不可执行(每个候选在 revert 之前就已经是红的),ID-121 把它重排到车道变绿之后——之后尚未执行。
  • scripts/ci/split_negative_controls.sh 的 E1 对照自检就失败,且它索要的 Fatal{BarrierViolation} 在 MOBILEGL_IPC_BATCH_WAITS 默认值 1 下根本不可能触发(ID-122)。一个永远打不响的对照比没有对照更坏。
  • G1 仍由 CI 断言;本阶段门禁只编 flavour,不做符号恒等(ID-123)。
  • 契约 §8 修正 8(GetBufferBindingPointCount 的 sticky forward)标为 UNLANDED(ID-120); §3.2 的 per-role stamp 存储同样未落地,正是它让上面第 3 类缺陷成立。
  • VD12 面板上限之上谁更快没有回答:本机定不住更高频率。VD32 那一轮回答了一般性的问题。

下一个机会(不是缺陷)

VD32 下 apply 线程只有 11-12 ms,client 有 15.7 ms——两侧不平衡。把工作从 client 挪到 apply 会直接降低瓶颈。这在 lockstep 下毫无意义(两侧串行相加),是 run-ahead 解锁出来的方向, 留给后续阶段。