Files
MobileGL/docs/Disaggregated/notes/p7/magma-m2.md
T
swung0x48 5ba34fcf42 [docs] (P7): magma-m2.md round 2 - the memo epoch fix and its red-once, the pipe tree's SHAs, the floor decision on its first reason alone, and the round's gates
- Section 0 cites the six commits by their pipe-tree SHAs (b81a8827 c569c916 0acee60a 9328bdc7 f0356dbe 5d8a2b99), 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).
2026-09-22 22:51:20 -04:00

357 lines
31 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P7 wave 4 M2:Magma wire 臂的延迟 VkBuffer 集有界(分支 `p7/magma-m2`)
> 裁定见 [`INTEGRATOR-DECISIONS-P7.md`](INTEGRATOR-DECISIONS-P7.md) ID-P7-27 / ID-P7-32(M1 的归因与
> 「server 侧回收不能假设一帧有界」);规范见
> [`CONTRACT-P7.md`](../../../../MobileGL/MG_Remote/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=ON` `INPROC=ON` `PIPE_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`):`unit` 2429、`integration-split` 303、`integration-magma-split` 93、
> `-spawn` 72、`-tcp` 74、`integration-magma-full-split` 533,全绿。
## 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`)
1. **每个过 wire 的 `glBufferData` 都孤立旧 store**:`VkBufferManager::RespecifyWireBuffer`
(`VkBufferManager.cpp:189`)在 `:197` 无条件 `DeferRelease(std::move(resource->buffer))`,再 `Create`
新 store。孤立本身是对的(M1 测过 monolith 式条件孤立:6701 对 6702,无效)。
2. **回收只在帧界**:`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`),它要求所有提交完成**且**当前无录制——长帧里几乎不成立。
3. **帧界不来**: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.h` IpcTable,`ConfigLoader.cpp` `InitIpc` 读一次,范围 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. 还没做 / 交给集成者
1. **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 证明)回收,而不是等帧界。
2. **`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 的)。
3. **帧 serial 证明**:集成树上的地板(B3 之后)可靠,可以把 `lastUseSerial <= GetCompletedSerial()` 加作第四条
证明(放掉「上一帧用过、本帧没碰」的 store 而不必等当前批提交)。本包没加:它在帧内不动,对本包针对的
单 present 回放不增加任何回收(§2.1)。
4. **tcp 臂**:`wbuf[]` 是 server 进程的 gauge,tcp 的 fixture server 够不着——与 `MAGMA_INPROC_ONLY` 的两条同类,
是「跨进程 peek」那条 §12 债的又一个实例。
5. **范围外的一处改动**:`MG_Remote/Server/ServerSpawn.cpp`(§2.3)不在指派的文件里,但没有它旋钮在 spawn 上是空的;
请集成者追认,CONTRACT-P6 §3.1 的 S3(清洗被证伪)不受影响。旋钮本身按 `Config.h` 的规矩应经集成者(§3)。
6. **性能只记录**(ID-P7-9):bsl 一次回放 23 次帧中强制提交;主机上 spawn 回放墙钟 104.8 s(前)/ 96.5 s(后),
共享主机噪声大,不作结论。
7. **(第二轮,只在 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.py` rc 0:**79** 个 abort 站点 / 20 文件,44 家族词,3 拒绝词,0 无标记(不变)。
- `link_ratchet.py --assert-monotone`:**173**,不变。
- `spawn_lane_parity.py build-split` rc 0:gated 三臂 96 / 75 / 72(+2 fixture),inproc-only 21 条与 not-on-tcp 3 条
按名扣除;informational 206×3;full-suite 536×3。
- 车道(逐条跑,`-j 8`):`unit` **2429/2429**、`integration-split` **303/303**、`integration-magma-split`
**96/96**(+3)、`-spawn` **75/75**(+3)、`-tcp` **74/74**、`integration-magma-full-split` **536/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 会话:
1. monolith 一遍(对照,W4 读数 0.999791667 / 807 MiB)。
2. **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. 判据:不死;门 3 的逐 case 判据(三遍逐位相同、ssim ≥ 阈值且与同会话 monolith 差 ≤ 0.0005);`wlivepk` ≲ 1 060、
`wdefsync` > 0。
4. **若 server 仍在 scudo 里死**:先看 maps 行数与 RSS 斜率——主机上剩下的帧内增长是 §6.1 的 descriptor set,
它在设备上是驱动堆而不是 memfd,那是下一个包,不是本包的回退。
5. 可选对照:`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 轮询竞态**:
1. D1 用 UBO store S1(256 B,颜色 A)画——memo 记下 S1 的句柄;
2. `glBufferData(U, 0)`:S1 孤立(停放,标在 D1 所在的待提交批),**不铸新 store**;
3. `glClientWaitSync(fence, FLUSH_COMMANDS, 0)`:把那批**提交而不等**(审阅说的 flush-without-wait);
4. 对 D1 用过的 vertex store `glBufferSubData` 4 字节:store 忙 → `StagedWireRangeCopy` 录一条拷贝 → 录制又是 pending,
于是第 5 步的等待**不能**取会清 memo 的帧界 drain(`TryDrainFrameTransients` 见 pending 就拒绝);
5. `glClientWaitSync(fence, 0, 2 s)`:D1 那批退休,memo 完好;
6. `glBufferData(V, 0)`:vertex store 停放,**这次停放的扫描**发现 S1 那批已完成 → 销毁 S1;
7. `glBufferData(U, 64 KiB, 颜色 B)`:铸造拿回 S1 的句柄值(tcache LIFO);64 KiB 放不进 S1 那个 256 B 的洞(紧接 S1
铸的 1 MiB pin store 把洞封住),所以 DS1 指的内存还是 S1 的旧字节——同尺寸铸造会被 VMA best-fit 塞回同一段内存,
靠别名意外读到 B,那是证明不了任何东西的绿;
8. 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.py` rc 0:**79** 个 abort 站点 / 20 文件,44 家族词,3 拒绝词,0 无标记(不变)。
- `link_ratchet.py --assert-monotone`:**173**,不变。
- `spawn_lane_parity.py build-split` rc 0:gated 三臂 97 / 76 / 72,not-on-tcp 按名 4 条;informational 206×3;full-suite 537×3。
- 车道(逐条,`-j 8`):`unit` **2429/2429**、`integration-split` **303/303**、`integration-magma-split` **97/97**(+1)、
`-spawn` **76/76**(+1)、`-tcp` **74/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-split` **537/537**(+1,无车道标记 skip)。
- retrace(主机,`run_trace_case.cmake`,本树):OpenRA DirectVulkan monolith / inproc / spawn **ssim 1.000000、mismatch 0**,
0 条 unsound。