- Section 0 cites the six commits by their pipe-tree SHAs (b81a8827c569c9160acee60a9328bdc7f0356dbe5d8a2b99), with the branch SHAs alongside; every in-text cite follows. - Section 2.1 keeps "the frame-serial floor is not a proof" on its first reason and drops the second, a stale citation of B3's fix; 6.3 is reworded to match. - New section 9 records the review's must-fix (ID-P7-43): the handle-reuse ABA against UniformManager's memos, the destroy-epoch fix, the deterministic scenario and its red-once on both arms (the dead store's colour read back at base), the finding that the integration binary links the backend statically so a red-once variant has to be built into it as well (round 1's reds re-verified that way, unchanged), the three more knobs the spawn scrub keeps, the review's smaller items, the gates, and what is left for the integrator (the pipe-tree-only lastUseSubmitIndex reset in RespecifyWireBuffer).
31 KiB
Executable File
P7 wave 4 M2:Magma wire 臂的延迟 VkBuffer 集有界(分支 p7/magma-m2)
裁定见
INTEGRATOR-DECISIONS-P7.mdID-P7-27 / ID-P7-32(M1 的归因与 「server 侧回收不能假设一帧有界」);规范见CONTRACT-P7.md§0(规则 I / J)、§7.1 门 3、§9、 §12 的 bsl-esc-menu 行。基线 =ec46a550(build-split/ lavapipe 钉住 / 集成测试 ON)。主机口径:WSL Arch + lavapipe(
/usr/share/vulkan/icd.d/lvp_icd.json),build-split= Release / clang / ccache /DISAGGREGATED=ONINPROC=ONPIPE_PUSH=ON。测量用 M1 的采样器 (~/w7/m1/memrun.sh:0.1 s 采/proc/<pid>/status与maps行数,覆盖 spawn 出的 server)与 M1 的 只读探针(M1WireBuffers/M1DeferredBuffers/M1LiveVkBuffers/MOBILEGL_M1_VMA_DUMP),打在 两棵只用于测量的树上:~/w7/m2-meas=ec46a550+ 探针,~/w7/m2-meas-after= 本分支 + 同一探针 (外加一个数 descriptor set 的探针,§5.3)。探针不进任何提交。基线门读数(
ec46a550):unit2429、integration-split303、integration-magma-split93、-spawn72、-tcp74、integration-magma-full-split533,全绿。
0. 提交
M2 已按下表的 pipe 列挑进集成树(~/w7/pipe);全文引用一律用 pipe 树上的 SHA,分支
p7/magma-m2 上的原提交列在旁边只作对照。第二轮(§9)的提交在分支 6f67fd7f 之上,尚未挑入。
| # | pipe 树 | 分支 | 一句话 |
|---|---|---|---|
| 1 | b81a8827 |
736b16a4 |
[MG_Backend] wire 臂被 glBufferData 孤立的 store 在 defer 路径上一证明无 GPU 命令还能引用就销毁,不再等帧界 |
| 2 | c569c916 |
e47e2eb6 |
[MG_Backend] 仍停放的孤立 store 超过 MOBILEGL_IPC_WIRE_DEFERRED_MB(默认 64)时,server 在帧中取 host-access 同步点并全部回收 |
| 3 | 0acee60a |
09901bae |
[MG_Remote] spawn 启动器把 MOBILEGL_IPC_WIRE_DEFERRED_MB 透传给 server 子进程(环境清洗当时唯一保留的 MOBILEGL_IPC_*;第二轮再留三个,§9.3) |
| 4 | 9328bdc7 |
c4d77917 |
[MG_Backend, MG_Util, MG_Test, MG_IntegrationTest, scripts] MagmaWireReclaimScenario 进 integration-magma-{split,spawn},读 server 的 wbuf[] gauge |
| 5 | f0356dbe |
7db41ff4 |
[MG_Backend, MG_IntegrationTest] 同一同步点在停放超过 1024 个 store 时也触发 |
| 6 | 5d8a2b99 |
6f67fd7f |
[docs] 本文第一轮 |
VulkanRenderer.cpp 一行未动(B3 的文件);WriteWireBuffer / GetCompletedSerial /
NotifyFrameSerialComplete 语义未动(WriteWireBuffer 只把它那条 Fatal 换成共享的
WireBufferSyncFatal("host-write"),同一行字、同一个 abort 站点);monolith 的 OnRespecify 与
transient arena 的帧界节奏未动。
1. 缺陷与机制(file:line 按 ec46a550)
- 每个过 wire 的
glBufferData都孤立旧 store:VkBufferManager::RespecifyWireBuffer(VkBufferManager.cpp:189)在:197无条件DeferRelease(std::move(resource->buffer)),再Create新 store。孤立本身是对的(M1 测过 monolith 式条件孤立:6701 对 6702,无效)。 - 回收只在帧界:
DeferRelease(:1183)压进m_deferredBufferReleases[m_currentFrameIndex], 只有CollectDeferredReleases(:1198)清它,而它只从BeginFrame(:521)与CollectAllDeferredReleases(:530)进;前者在 present 路径(VulkanRenderer.cpp:14231)与TryDrainFrameTransients的每第 8 次 drain(VulkanRenderer.cpp:13691/:13697),后者在TryDrainFrameTransients(:13630/:13675),它要求所有提交完成且当前无录制——长帧里几乎不成立。 - 帧界不来:ID-P7-32 / §12,bsl fixture 1 303 535 次调用里只有 2 次
eglSwapBuffers,harness 在目标 swap 上快照后退出,server 整次回放收到 1 条 present。
主机结果(ec46a550 + 探针,spawn):回放结束时 25 935 个活 VkBuffer,25 903 个在延迟桶里,
25 931 个出自 VkBufferManager.cpp:221(探针树上 RespecifyWireBuffer 的 Create),VMA 25 990 个
分配 / 345.5 MB;monolith 同一回放 10 个活 VkBuffer。
2. 修复
2.1 serial/submit 门控回收(b81a8827,仅 VkBufferManager.{h,cpp},#if MOBILEGL_BUILD_DISAGGREGATED)
wire 臂的孤立 store 不再进按帧槽分桶的 m_deferredBufferReleases,而进一张扁平的
m_deferredWireReleases,每条带三个事实:lastUseSerial(在 RespecifyWireBuffer 把它清零之前读)、
submitIndex(停放时的 GetSyncPointSubmitIndex())、bytes。死亡证明三条,任一成立即销毁:
| 证明 | 何时成立 | 为什么可靠 |
|---|---|---|
lastUseSerial == 0 |
自铸造或自上次 host-access 等待以来没有 GPU 命令引用过这个 store | 每条把 store 交给 GPU 的路径都盖 m_frameSerial(从 1 起);WaitForWireBufferHostAccess 只在等完 sync point 后清零。这种 store 在停放点直接销毁,不进表 |
IsSubmitIndexComplete(submitIndex) |
能引用它的最后一次提交已退休 | 停放时 record 已在 bump 过的 slice epoch 之后,此后录制的命令拿不到它;此前录制的要么已提交(≤ m_submitCounter),要么在下一批(m_submitCounter + 1)。与 CollectWireObjects 对 wire image/view 表用的是同一形状 |
renderer 空闲:IsSubmitIndexComplete(GetSyncPointSubmitIndex())(或无 renderer) |
无录制、所有提交已退休 | 此时表里每一条都死,包括被最小化 Present 丢弃的录制所标记、其 index 可能永远不完成的那条 |
每次停放都扫一遍(DeferWireRelease → SweepDeferredWireReleases),帧界只是多一个扫描点
(CollectDeferredReleases 末尾)。表按停放序、GetSyncPointSubmitIndex() 不减,所以死者是前缀:
第一条未完成的证明其后全未完成,一次扫描至多两三次 fence poll。Shutdown / RecreateTransientArenas
(二者已证明设备空闲)整表销毁。
帧 serial 地板刻意不作证明:它在帧内不动(NotifyFrameSerialComplete 拒绝当前 serial),对单 present
回放一个也放不掉;帧内能动的只有 submit index,而它是 fence 观测不是计数。(第一轮此处还引 B3 把地板判为不可靠
作第二条理由——那是对集成树上已修的地板的过期引用,第二轮删去;决定不变。)可作第四条证明的扩展见 §6.3。
2.2 水位线(c569c916 + f0356dbe)
扫描之后若仍停放 > MOBILEGL_IPC_WIRE_DEFERRED_MB 字节或 > 1024 个 store,停放点调
pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, /*flush*/true)
——WaitForWireBufferHostAccess 的同一个调用:先把当前录制提交,再等它——然后再扫;因为表里没有一条能标在
刚等完的 sync point 之后,这次扫描清空全表。同步点完成不了(WaitForSubmitIndex 返回 false:flush 失败、
设备丢失)时死于既有站点 Fatal{ResourceUnavailable, "buffer-write-sync"}:WriteWireBuffer 那条
MGLOG_F + abort 挪进 WireBufferSyncFatal(site),两处共用,{site=host-write|deferred-watermark} 区分;
fatal_census.py 79 个 abort 站点不变,家族词不变。
为什么还要个数上限(f0356dbe):只有字节水位线时,bsl spawn 的某段无提交区间停放了 12 497 个 store
而只占 39.5 MB(低于 64 MiB),一次同步也没触发(§5.1 中间行)。VkBuffer 按对象计价,小孤立永远碰不到字节
预算。1024 是常量 VkBufferManager::kWireDeferredCountCeiling,不是旋钮。
2.3 spawn 透传(0acee60a)
ServerSpawn.cpp 的 BuildChildEnv 清洗掉所有 MOBILEGL_IPC_*(:83 ShouldScrub),于是新旋钮在 inproc
有意义、在 spawn 静默无意义——实测 spawn server 在 8 MiB 的车道预算下停放 47 MiB、0 次同步,它从一个已经没有
这个变量的环境里解析出了默认值。改为按名保留这一个(不放宽前缀)。它不指名端点、角色或段尺寸,两道防递归
检查(Dial=No、ServerMain 对 TRANSPORT / SERVER_PATH / RING_MB / STAGE_MB 的清洗核验)都不受影响。
tcp 的 server 读 supervisor 自己的环境,与所有旋钮相同。
3. 旋钮语义:MOBILEGL_IPC_WIRE_DEFERRED_MB
- 谁读:只有 server 角色的
VkBufferManager(Magma wire 臂)。经MG_Config::Ipc.WireDeferredMb(Config.hIpcTable,ConfigLoader.cppInitIpc读一次,范围 0..65536,默认 64,写进Config: IPC ... wire-deferred=%uMiB那一行)。inproc:本进程环境;spawn:启动器透传;tcp:supervisor 环境。 - 管什么:已孤立、但还可能被「已录制未退休」的 GPU 工作引用的 wire buffer store 的停放字节。不管活 store (record 持有的),不管 transient arena,不管 monolith 臂。
- 怎么管:停放点扫描后,停放字节
> 值 × 1 MiB或停放个数> 1024→ 取一次同步点(flush + 等)→ 全表回收。 所以 server 的孤立 store 峰值 ≤ 预算 + 一个 store、≤ 1025 个。代价是每一预算量(或每 1024 个)孤立一次帧中提交, 不是每个glBufferData一次;bsl-esc-menu-854 整次回放 23 次。 - 0 = 负对照,不是「无限」:两个触发都关,帧内 respecify-and-draw 回到无界(
MagmaWireReclaimScenario的两条 水位线用例必须红,§4)。lastUseSerial == 0与 submit 门控两条证明在 0 下照常工作。 - 按 CONTRACT-P5/Config.h 的规矩,新 IPC 旋钮应经集成者:本包把它加进了 IpcTable,请集成者追认,并补进
ARCHITECTURE.md的旋钮清单。
4. 场景、车道、red-once(规则 J)
MG_IntegrationTest/Scenarios/MagmaWireReclaimScenario.cpp,三条用例都在一帧之内(第一个到最后一个
respecify 之间没有 swap),读 server 自己发布的四个 PipeStats gauge(PipeStats.h Gauge::WireBuffers
…WireDeferredSyncs,行上是 wbuf[wbufs= wlivepk= wdefpk= wdefsync=],只在 push 构建里有,pull .text 不动)。
峰值是运行最大值:缺陷活在帧内,swap 时刻的采样读的是帧界自己扫完之后的值,什么也证明不了。
| 用例 | 负载 | 断言 |
|---|---|---|
RespecifiesWithNoDrawBetweenKeepTheLiveStoreCountBounded |
1 个 buffer、1024 次 4 KiB glBufferData、中间不画 |
wlivepk ≤ 4 × wbufs;最后一次画落地 |
RespecifyAndDrawEachStoreInOneFrameStaysWithinTheDeferredBudget |
48 轮「1 MiB respecify + 画一条竖带」 | wdefpk ≤ 预算 + 1 MiB、wlivepk ≤ wbufs + 预算/1 MiB + 2;48 条带各是自己的颜色(store 在它的 draw 执行前被销毁会在这里现形) |
ManySmallRespecifyAndDrawRoundsStayUnderTheStoreCountCeiling |
3000 轮「256 B respecify + 画」(750 KB,远低于字节预算) | wlivepk ≤ wbufs + 1024 + 2;最后一次画落地 |
车道:DirectVulkan.{Split,Spawn}.Reclaim.*,标签 integration-magma-split + integration-magma-reclaim /
integration-magma-spawn + integration-magma-spawn-reclaim,环境 = strict + 双块 + MOBILEGL_PIPE_STATS=1
_PERIOD=1 + MOBILEGL_IPC_WIRE_DEFERRED_MB=8 + 私有日志路径。不注册 tcp 臂(ID-P7-14 的形状):tcp 的 server
是整条车道共用的 fixture supervisor 子进程,条目设不了它的环境(给它设 stats + 8 MiB 会改掉其它每条 tcp 条目的
server),也读不到它的日志。spawn_lane_parity.py 的 server 端环境例外表 MAGMA_SERVER_ENV_KNOB_NO_TCP 加入三条 .Reclaim.… tail(集成时并入 B2 的同一机制 compare_arms(no_tcp=),一条 tail 恰对应一个 split 条目;只对 tcp 臂的比较扣除),
PipeStatsTest 钉住四个短名。
red-once(R-16,已执行并还原;数字是 server 的 gauge):
| 场景 / 臂 | 红(修复前) | 绿(修复后) |
|---|---|---|
| 用例 1,split & spawn | 基线 defer 路径(ec46a550 的 DeferRelease 进帧桶,计数照记):wlivepk 1025 vs 4 |
wlivepk=1(wbufs=1) |
| 用例 2,split & spawn | 同上:wdefpk 49 287 168 vs 9 437 184、wlivepk 49 vs 11 |
wdefpk=9 437 184、wlivepk=9、wdefsync=5 |
用例 2,MOBILEGL_IPC_WIRE_DEFERRED_MB=0(旋钮负对照,已提交代码) |
wdefpk 49 287 168 vs 9 437 184、wlivepk 49 vs 11,两臂 |
— |
| 用例 3,split & spawn | 9328bdc7(只有字节水位线):wlivepk 3001 vs 1027 |
f0356dbe:wlivepk=1025、wdefsync=2 |
修 spawn 透传前(c569c916 + 本场景) |
spawn 臂 wdefpk=49 287 168、wdefsync=0(server 没拿到 8 MiB) |
0acee60a 后与 split 臂相同 |
用例 2 的负对照(在 f0356dbe 上重跑)里 48 条带仍全部正确——红的是回收,不是画面。
5. 主机测量(M1 的采样器与探针;lavapipe;pbuffer;DirectVulkan)
5.1 minecraft-1.21.4-fabric-iris-bsl-esc-menu-854(854×480)
「前」= ec46a550 + 探针;「字节线」= 9328bdc7(只有字节水位线);「后」= f0356dbe。三臂的 ssim 与 mismatch
前 / 字节线 / 后完全相同:ssim 0.998402、mismatch 306 892。
| 臂 / 进程 | 峰值 RSS(VmHWM) | maps 峰值 | 结束时活 VkBuffer(其中停放) | VMA 分配 / 字节 | wbuf[] |
|---|---|---|---|---|---|
| monolith,前 | 1058.6 MiB | 28 403 | 10 | 64 / 420.7 MB | — |
| monolith,后 | 1056.9 MiB | 28 402 | 10 † | 64 / 420.7 MB † | — |
| inproc,前 | 1174.6 MiB | 41 021 | 25 935(25 903) | 25 990 / 345.5 MB | — |
| inproc,字节线 | 1140.8 MiB | 41 032 | 32(0) | 87 / 283.6 MB | wlivepk=12497 wdefpk=39536432 wdefsync=0 |
| inproc,后 | 894.2 MiB | 5 847 | 1 053(1 025) | 1 108 / 235.7 MB | wlivepk=1052 wdefpk=3312832 wdefsync=23 |
| spawn server,前 | 825.1 MiB | 41 007 | 25 935(25 903) | 25 990 / 345.5 MB | — |
| spawn server,字节线 | 793.8 MiB | 40 993 | 32(0) | 87 / 283.6 MB | wlivepk=12497 wdefpk=39536432 wdefsync=0 |
| spawn server,后 | 546.3 MiB | 5 832 | 1 053(1 025) | 1 109 / 236.7 MB | wlivepk=1052 wdefpk=3312832 wdefsync=23 |
| spawn client,前 / 后 | 359.8 / 358.8 MiB | 69 / 66 | — | — | — |
† monolith 的 VMA 读数取自 9328bdc7 测量树上的 monolith 一遍(本包不碰 monolith 臂,f0356dbe 只改 wire 路径)。
spawn server 的 RSS / maps 逐 ~10 s 序列(mem.tsv 十分位):
前(ec46a550) t(s) 0 9.6 19.2 29.2 39.9 50.8 61.6 72.5 83.2 93.9 104.7
RSS MiB 9 465 750 763 774 785 796 807 788 800 825
maps 58 7160 27361 30324 32589 35019 37429 39828 29574 32799 28301
后(f0356dbe) t(s) 0 9.6 19.4 29.2 38.8 48.2 57.9 67.6 77.5 87.0 96.4
RSS MiB 19 239 453 455 458 462 465 468 473 477 546
maps 200 2296 5783 5329 5197 5204 5051 4738 5686 5429 4664
前:进入稳态后 +11 MiB / +2 400 maps 每 10 s、锯齿(drain 偶尔成功时回落),与 M1 的 +12.2 MiB / +2600 每 7.2 s 同形。后:稳态斜率 ~+3 MiB / 10 s、maps 平台在 4.6k–5.8k。最后一个采样的 +70 MiB 在前后都有(前 +25),是回放 收尾(快照 / 回读)的一次性抬升,未归因。探针的活 VkBuffer 序列(后,spawn):7 → 1 035 → 1 053 → … → 1 053, 停放恒在 1 025 附近,即个数上限在锯齿地收它;前:7 → 4 526 → 7 215 → 19 687 → 25 935。
5.2 minecraft-1.21.4-fabric-iris-bsl-in-world(轻例)
ssim 0.997324、mismatch 392 471,前后三臂相同。
| 臂 / 进程 | 峰值 RSS 前 → 后 | maps 峰值 前 → 后 | VkBuffer |
|---|---|---|---|
| monolith | 821.3 → 811.9 MiB | 995 → 989 | — |
| inproc | 940.2 → 934.7 MiB | 907 → 941 | 前:4 s 采样 86 活 / 68 停放;后:21 活 / 1 停放 |
| spawn server | 477.0 → 471.0 MiB | 897 → 890 | 后 wbuf[wbufs=26 wlivepk=63 wdefpk=57760 wdefsync=0] |
| spawn client | 459.7 → 460.2 MiB | 66 → 66 | — |
轻例本来就小(M1:1000× 小于 bsl),修复后峰值 63 个 store、57 KB 停放、零次强制同步——只靠 submit 门控。
5.3 M1 的 maps 归因不成立,maps 是 descriptor set
「字节线」那一行是决定性的:活 VkBuffer 从 25 935 降到 32、VMA 分配从 25 990 降到 87,maps 峰值一点没动
(41 007 → 40 993)。maps 普查:增长的全是 4096 B 的 /memfd:allocation fd 映射(前 spawn 峰值快照 34 653 个,
monolith 27 383 个);VMA 的块是另外那几条 32–256 MiB 的映射。加一个只在测量树上的计数探针
(UniformManager::AllocateDescriptorSetsFromActivePool 成功 +1、OnDescriptorSetLayoutDestroyed 减去清掉的)后:
| 采样 | dsets |
maps |
|---|---|---|
| 字节线 spawn | 5 552 / 23 276 / 27 377 | 6 092 / 24 013 / 28 134 |
| monolith | 14 948 / 23 271 / 27 376 | 15 730 / 24 053 / 28 202 |
| 后 spawn | 1 820 / 3 663 / 3 713(平台) | 2 294 / 4 392 / 4 458 |
lavapipe 给每个 descriptor set 一个 4 KiB memfd 映射,maps 就是活 descriptor set 数。 M1 的「每个 VkBuffer 一条
memfd 映射」是相关不是因果(两者都随调用数涨)。「后」的 maps 之所以降到 5.8k,是个数上限的 23 次强制同步顺带让
TryDrainFrameTransients 成功,而 drain 每次都 m_uniformManager->BeginFrame(frameIndex) 回卷 descriptor 游标
(VulkanRenderer.cpp:13630 的 TryDrainFrameTransients,「Descriptor cursors rewind on every drain」)——descriptor set 的有界现在
搭着 M2 的同步节奏,不是它自己的机制(§6.1)。
6. 还没做 / 交给集成者
- wire 臂的 descriptor set 帧内增长(新发现,最该派包的一项)。
UniformManager::AcquireDescriptorSet(UniformManager.cpp:2811)在每帧每 layout 的缓存游标用尽后分配新 set、池满则GrowFrameDescriptorPool(:2745)长新池;游标只在UniformManager::BeginFrame(:628/:653)回卷,而那只在 present 与 drain 上跑—— 一 present 的回放把它饿死,与本包的缺陷同根。monolith 同一回放在 ~27.4k 个 set 处平台(16 s 后不再分配), wire 臂持续增长到 ~40k 后被偶发 drain 锯回。在 Adreno 上 descriptor pool 是驱动的堆内存(经 scudo),这恰是 设备上Scudo ERROR: internal map failure的另一个嫌疑。M2 的强制同步现在碰巧给了它回卷点;正确的修法属于UniformManager.cpp(簇 A)/VulkanRenderer.cpp(B3/B4):用「这些 set 的最后一次提交已退休」(与本包同形的 submit 证明)回收,而不是等帧界。 CollectGarbage()/pruneWire在 wire 臂上几乎不跑(B4)。VulkanRenderer::SetupDraw(VulkanRenderer.cpp:7060)在:7067的m_textureManager->CollectGarbage()之前就返回SetupWireDraw; compute 的两处(:7684/:7737)同样在 wire 分支之后。pruneWire(VkTextureManager.cpp:1779)于是只剩VkTextureManager::BeginFrame每第 64 个帧界的PruneDeadTextures(:729),一 present 的回放上等于不跑: 死的 wire texture / renderbuffer 记录与其图像一直留着(bsl 上量不大:wireTex=48、deferredTex=3)。 按指示未修(VulkanRenderer.cpp是 B3 的)。- 帧 serial 证明:集成树上的地板(B3 之后)可靠,可以把
lastUseSerial <= GetCompletedSerial()加作第四条 证明(放掉「上一帧用过、本帧没碰」的 store 而不必等当前批提交)。本包没加:它在帧内不动,对本包针对的 单 present 回放不增加任何回收(§2.1)。 - tcp 臂:
wbuf[]是 server 进程的 gauge,tcp 的 fixture server 够不着——与MAGMA_INPROC_ONLY的两条同类, 是「跨进程 peek」那条 §12 债的又一个实例。 - 范围外的一处改动:
MG_Remote/Server/ServerSpawn.cpp(§2.3)不在指派的文件里,但没有它旋钮在 spawn 上是空的; 请集成者追认,CONTRACT-P6 §3.1 的 S3(清洗被证伪)不受影响。旋钮本身按Config.h的规矩应经集成者(§3)。 - 性能只记录(ID-P7-9):bsl 一次回放 23 次帧中强制提交;主机上 spawn 回放墙钟 104.8 s(前)/ 96.5 s(后), 共享主机噪声大,不作结论。
- (第二轮,只在 pipe 树上能做)
RespecifyWireBuffer把lastUseSerial清零时也要把 B3 的lastUseSubmitIndex清零(resource->lastUseSubmitIndex = 0;,紧跟resource->lastUseSerial = 0;)。审阅指出 它在 respecify 后残留旧值;这个字段是 B3 在集成树上加的,本分支(6f67fd7f之上)没有它,加了编译不过。 请集成者在挑入第二轮提交时顺手补这一行——它属于本包的RespecifyWireBuffer,不是 B3 的文件。
7. 门(f0356dbe)
- G1:pull 构建 rc 0,
.text= 0xa52203,nm --defined-only对~/w7/p7-before/pull-syms.txt+0 / −0 (~/w7/logs/m2-g1.sh)。 fatal_census.pyrc 0:79 个 abort 站点 / 20 文件,44 家族词,3 拒绝词,0 无标记(不变)。link_ratchet.py --assert-monotone:173,不变。spawn_lane_parity.py build-splitrc 0:gated 三臂 96 / 75 / 72(+2 fixture),inproc-only 21 条与 not-on-tcp 3 条 按名扣除;informational 206×3;full-suite 536×3。- 车道(逐条跑,
-j 8):unit2429/2429、integration-split303/303、integration-magma-split96/96(+3)、-spawn75/75(+3)、-tcp74/74、integration-magma-full-split536/536(+3, 整二进制重放里新用例因无车道标记而 skip)。 - retrace(主机,
run_trace_case.cmake,基线树 vs 本树):OpenRA DirectVulkan monolith / inproc / spawn 三臂 ssim 1.000000、mismatch 0,前后一致;bsl-esc-menu-854 与 iris-bsl-in-world 见 §5(前后逐字相同)。
8. 设备验收(集成者在 Redmi 上跑)
minecraft-1.21.4-fabric-iris-bsl-esc-menu-854,DirectVulkan,--use-pbuffer,同一 reboot-clean 会话:
- monolith 一遍(对照,W4 读数 0.999791667 / 807 MiB)。
- spawn 三遍、inproc 三遍,默认旋钮(64 MiB / 1024)。每遍采 server 与 client 的
VmHWM、/proc/<pid>/maps行数(W4 的采样器没记这一项,M1 标为未答),并开MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=1取 server 日志最后一行的wbuf[...]。 - 判据:不死;门 3 的逐 case 判据(三遍逐位相同、ssim ≥ 阈值且与同会话 monolith 差 ≤ 0.0005);
wlivepk≲ 1 060、wdefsync> 0。 - 若 server 仍在 scudo 里死:先看 maps 行数与 RSS 斜率——主机上剩下的帧内增长是 §6.1 的 descriptor set, 它在设备上是驱动堆而不是 memfd,那是下一个包,不是本包的回退。
- 可选对照:
MOBILEGL_IPC_WIRE_DEFERRED_MB=0一遍 spawn,应复现 W4 的形状(或更早死),证明旋钮在设备上生效。
9. 第二轮:审阅的 must-fix(ID-P7-43)与小项(分支 p7/magma-m2,6f67fd7f 之上)
9.1 缺陷:句柄复用 ABA 打穿 UniformManager 的 memo
M2 让 wire store 在帧中死(serial-0 停放即毁、扫描前缀、水位线),而 UniformManager 的两个 descriptor memo
都按 VkBuffer 句柄记:m_descriptorReuseMemo 的签名混入 VkDescriptorBufferInfo 的字(UniformManager.cpp
签名块),FastRebindMemo 直接比 uboBuffer。它们只在 UniformManager::BeginFrame(present / drain)清空——M2 之前
wire store 也只在那同一个边界死,所以配对成立;M2 打破了它。wire 臂上直接绑定的 UBO 描述符就是 store 自己的句柄
(ResolveWireUniformBufferPayload 的 out.buffer = source.buffer),SSBO 同样。于是:D1 用 store S1(句柄 H)画 →
memo 记下 (H, range) → DS1;glBufferData 停放 S1;一次不等待的 flush 提交;下一次停放的扫描销毁 S1;下一次铸造拿回 H
(lavapipe 的句柄是堆指针,glibc tcache 后进先出);D2 同一程序解析出 (H, range) → memo 命中 → 重绑一个 descriptor
还指着 S1 已释放内存的 set。像素错、无 Fatal。serial-0 路径同理。
9.2 修复:一个 destroy epoch
VkBufferManager 加 m_wireStoreDestroyEpoch(GetWireStoreDestroyEpoch()),每条销毁 wire store 的路径都 ++:
DeferWireRelease 的即时销毁、SweepDeferredWireReleases(每次有销毁的扫描一次)、DestroyAllDeferredWireReleases、
Shutdown 清 record 持有的 store;与 m_sliceEpochCounter 一样永不复位。UniformManager:签名 mix64(epoch),
FastRebindMemo 记录 epoch 并在命中判断里比对——全部 #if MOBILEGL_BUILD_DISAGGREGATED。不用 m_sliceEpochCounter:
它每次 WriteWireBuffer 都动,会让 memo 每次 glBufferSubData 都失效。wire 臂上 SetupWireDraw 不传 sampler hint,
所以 FastRebindMemo 在 wire 臂上从不命中——签名那半是 wire 臂真正走到的,FastRebindMemo 那半是防御性的。
代价:每次销毁事件之后第一笔 draw 多一次 descriptor set 分配 + 写(bsl 整次回放 ~2.6 万次销毁对 130 万次调用),
顺带把 §6.1 的 descriptor set 增长每事件多推一个 set。§9 分区例外(UniformManager.{h,cpp})由集成者授予。
9.3 场景与 red-once(规则 J)
MagmaWireReclaimScenario.ADrawAfterTheEarlyReclaimFollowsTheNewStoreNotTheMemoizedHandle,Split + Spawn 的 Reclaim
条目(与前三条同一 fixture、同一车道环境;spawn_lane_parity.py 按前缀例外,不动)。每一步都是确定的,没有 sleep、
没有 fence 轮询竞态:
- D1 用 UBO store S1(256 B,颜色 A)画——memo 记下 S1 的句柄;
glBufferData(U, 0):S1 孤立(停放,标在 D1 所在的待提交批),不铸新 store;glClientWaitSync(fence, FLUSH_COMMANDS, 0):把那批提交而不等(审阅说的 flush-without-wait);- 对 D1 用过的 vertex store
glBufferSubData4 字节:store 忙 →StagedWireRangeCopy录一条拷贝 → 录制又是 pending, 于是第 5 步的等待不能取会清 memo 的帧界 drain(TryDrainFrameTransients见 pending 就拒绝); glClientWaitSync(fence, 0, 2 s):D1 那批退休,memo 完好;glBufferData(V, 0):vertex store 停放,这次停放的扫描发现 S1 那批已完成 → 销毁 S1;glBufferData(U, 64 KiB, 颜色 B):铸造拿回 S1 的句柄值(tcache LIFO);64 KiB 放不进 S1 那个 256 B 的洞(紧接 S1 铸的 1 MiB pin store 把洞封住),所以 DS1 指的内存还是 S1 的旧字节——同尺寸铸造会被 VMA best-fit 塞回同一段内存, 靠别名意外读到 B,那是证明不了任何东西的绿;- D2 再画:解析出同一 (句柄, 16 B) → 修复前 memo 命中。断言只有一条:像素必须是 B。
| 臂 | 红(修复前:两处 epoch 判断去掉,VkBufferManager 的 epoch 留着不读) |
绿(修复后) |
|---|---|---|
| Split | read rgba(200,60,30,255), wanted rgba(60,200,90,255)——读到的正是死 store 的颜色 A |
过 |
| Spawn | 同上 | 过 |
一个方法学发现:MobileGLIntegrationTest 在桌面上静态链接 MobileGL_s(ldd 无 libMobileGL.so,nm 里
glDrawArrays 在二进制内),inproc 臂跑的是编进测试二进制的 backend,只有 spawn 臂跑 libMobileGL.so。所以只
ninja MobileGL MobileGLServer 的变体到不了 Split 臂——本轮 red-once 第一次跑 Split 就是这样「绿」的。第二轮的
red-once 脚本三个目标都建;并用同样方式把第一轮的 base 变体(DeferWireRelease 回到帧桶)重跑了一遍:两臂三条用例的
红与 §4 表逐字相同(wlivepk 1025 vs 4;wdefpk 49 287 168 vs 9 437 184、wlivepk 49 vs 11;wlivepk 3001 vs 1027),
用例 4 在那个 base 上两臂都绿(没有帧中销毁就没有 ABA)。探针(只在临时变体上打的 MGLOG_I,spawn server 日志)
把机制照了出来:destroy-sweep handle=0x7a0329f75dd0 → mint slot=2 handle=0x7a0329f75dd0 size=65536 → memo-hit buffer=0x7a0329f75dd0。
9.4 审阅的小项
ManySmallRespecifyAndDrawRounds...加ASSERT_GE(livePeak, 0):缺 gauge 读成 −1 会把单边上界当作「没漏」过掉。SweepDeferredWireReleases的注释不再说GetSyncPointSubmitIndex()「不减」:录制被丢弃(RecreateSwapchain、最小化 Present 强清录制标志)时它退一;前缀扫描在那之后停在较高的旧 index 上,直到下一次提交占用该 index 并退休——保守、 不早放,空闲规则照样兜底。VkBufferManager.h的「地板不作证明」只留第一条理由(§2.1 同步改)。ServerSpawn.cpp的保留集加MOBILEGL_IPC_SPIN_US(两侧都读的 doorbell 自旋预算)、MOBILEGL_IPC_SERVER_AFFINITY(apply 线程 CPU 掩码)、MOBILEGL_IPC_AUDIT(退休 stage 字节 0xDD 填充):都是调优 / 审计值,不指名端点、角色、路径 或段尺寸,ServerMain的 catch (b) 仍核验它核验的那四个。前缀下其余的照旧清掉(其中STRICT_ERRORS、RUN_AHEAD、VERB_BARRIER、BATCH_WAITS、PRESENT_CREDIT、PERSISTENT_*、ADOPT_TIER、RESPAWN要么是协议两侧要一致的形状、 要么是 client 读的,超出审阅所列,未动)。RespecifyWireBuffer的lastUseSubmitIndex = 0:字段只在 pipe 树上有,见 §6.7。
9.5 门(第二轮,本分支 HEAD)
ninja -C build-split绿;G1:pull 构建 rc 0,.text= 0xa52203,nm --defined-only对~/w7/p7-before/pull-syms.txt+0 / −0。fatal_census.pyrc 0:79 个 abort 站点 / 20 文件,44 家族词,3 拒绝词,0 无标记(不变)。link_ratchet.py --assert-monotone:173,不变。spawn_lane_parity.py build-splitrc 0:gated 三臂 97 / 76 / 72,not-on-tcp 按名 4 条;informational 206×3;full-suite 537×3。- 车道(逐条,
-j 8):unit2429/2429、integration-split303/303、integration-magma-split97/97(+1)、-spawn76/76(+1)、-tcp74/74(-j 8首跑 73/74:Tcp.Fm.F1WireScenario.CopyTexSubImage2DPixels在会话 建立前就no Welcome from the spawned server within 5000 ms,任何记录都没发;单条重跑 3/3、整车道重跑 74/74—— 配对窗口的瞬时形状,与本轮改动无关:该车道audit=0 spin=50us affinity='auto'全是默认值)、integration-magma-full-split537/537(+1,无车道标记 skip)。 - retrace(主机,
run_trace_case.cmake,本树):OpenRA DirectVulkan monolith / inproc / spawn ssim 1.000000、mismatch 0, 0 条 unsound。