Files
MobileGL/docs/Disaggregated/MEASUREMENTS.md
T

529 lines
76 KiB
Markdown
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.
# 实测记录(P0、P1、P2、P3a
> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74CAdreno 830Android 16`3B159D009VZ00000` = Oppo PLG110MaliAndroid 16ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。
## 1. Spike A — 从应用自身进程 exec 第二个原生可执行文件
问题:Android 上能否把 server 以 `lib*.so` 打进 APK,并从应用自己的 `untrusted_app``fork`+`execve` 它(`adb run-as` 跑在别的域,证明不了)。
| 设备 | 结果 |
|---|---|
| Adreno 830 | **OK**。父进程 `u:r:untrusted_app:s0:c173,c257,c512,c768` `fork`+`execve` `<nativeLibraryDir>/libMobileGLServer.so` → 子进程 pid 31348exit 0;子进程 SELinux `u:r:untrusted_app:s0:c173,c257,c512,c768`(同域同 category);marker 文件、stdout 捕获、报告全在;`execErrno=0`;窗口内**零 avc denial** |
| Mali | **OK**,同形:子进程 pid 28433exit 0`execErrno=0``u:r:untrusted_app:s0:c94,c257,c512,c768`,零 avc denial |
主机侧已证的三条(随 `8a239177`):AGP 会把改名成 `lib*.so``add_executable` 打进 `lib/arm64-v8a/`,前提是把 `RUNTIME_OUTPUT_DIRECTORY` 重定向到 `CMAKE_LIBRARY_OUTPUT_DIRECTORY``posix_spawn` 在 minSdk 26 不可用(bionic API 28 起),出货臂是 `fork`+`execve`;应用进程 stdout/stderr 是 `/dev/null`,子进程用 marker 文件证明自己活过。
- 代码:`tools/spikes/server_stub/main.cpp`stub:打印并写 marker 自己的 pid/uid/SELinux 上下文)、`android-plugin/app/src/trace/cpp/spawn_spike.cpp``RunSpawnSpike`)、`CMakeLists.txt:784-808``MOBILEGL_BUILD_SERVER_SPIKE`)。
- APK`p0-spike-a-android/trace-debug-spike-on.apk`(在 `30d7595b` 构建,与 `7ef7c7e5` 源码相同)。
- ColorOS 陷阱:首次 `adb install` 一个未安装的包会卡在 `com.oplus.appdetail InstallGuideActivity` 确认页,直到点"继续安装"1272×2772 面板上 `input tap 353 2349`);同签名重装静默通过。另一台设备上一个外来签名的 trace APKversionCode 26080769)会让 `install -r``INSTALL_FAILED_UPDATE_INCOMPATIBLE`,需先卸载。
- 42-device.sh 的 env 透传 A/B 腿在该 ROM 上跑不了(`run-as sh -c 'cat > files/…'` 被拒);透传由下面的 stats 基线端到端证明(`--env MOBILEGL_PIPE_STATS=1` 必须在 `mobilegl.log` 里产生 `MGPipe stats` 行)。
## 2. Spike B — 跨进程外部内存分档
问题:`AcquirePersistentMap` 背后的内存能否共享给另一个进程并在那里映射,两个后端各走哪条路。探针 `tools/spikes/extmem_probe/``39f982e6` 源码,arm64`adb shell` = `u:r:shell:s0` 域),4 MiB payload,64 KiB 同判决。每一行都取一次真 GPU 访问(`vkCmdCopyBuffer` + `vkCmdFillBuffer` + host-read barrier)并两侧字节校验才算 OK。
| 路线 | Adreno 830 | Mali |
|---|---|---|
| T1-opaque-fdserver 导出 `VkDeviceMemory` fdclient 裸 `mmap` + 导入) | **OK** 完整往返含 GPU 访问(`/dmabuf:system`dedicatedOnly=1 | UNSUPPORTED`vkCreateBuffer(external)=VK_ERROR_INVALID_EXTERNAL_HANDLE`advertisedExportable=0 |
| T1-dma-buf | UNSUPPORTED`VK_EXT_external_memory_dma_buf` 缺) | UNSUPPORTED |
| T1-gles-memobj-fd`GL_EXT_memory_object_fd` 导入导出的 fd | **FAIL**:导入 + `glBufferStorageMemEXT` 接受(`GL_NO_ERROR`)但每次 `glMapBufferRange``GL_INVALID_OPERATION`persistent 与 plain 都是);`GL_DEVICE_UUID` 不可读 | UNSUPPORTED(扩展字符串缺,入口点可解析) |
| **T0-ahb-blob-transfer**client 分配 `AHardwareBuffer` BLOB → socket 交接 → server Vulkan 导入 + GL 导入) | **OK** 全链:cpu-lock、vk-import+map、GPU copy/fill、GL map persistent+coherent、写回 client 全部字节校验 | **OK** 全链,判决相同(glPersistentCoherent=1gpuRan=1 |
| T3-external-memory-host`VK_EXT_external_memory_host` | UNSUPPORTED(扩展缺) | PARTIAL:导入 + map 往返,但 **GPU 写对宿主映射不可见**(只读档) |
| T3-memfd-cross-process / client-memfd-server-import | UNSUPPORTED | OK / PARTIAL(同样的 GPU 只读 caveat |
**P11 的分档决定**:唯一在两台设备、两个后端上都是完整读写的档是 **T0**——client 分配 `AHardwareBuffer` BLOBserver 以 `VK_ANDROID_external_memory_android_hardware_buffer`Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`Espryt)导入,两侧 persistent+coherent 映射。Adreno 另有 T1Vulkan 路径);Mali 无任何 server 导出路线,host-pointer 导入只读。Caveat:运行域是 `shell` 不是 `untrusted_app`AHB 的 socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,域风险在 memfd/opaque-fd 腿上。
复现:
```sh
ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 tools/spikes/extmem_probe/build_android.sh /tmp/extmem-build
S=<serial>; adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/extmem_probe \
&& adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe; echo EXIT=\$?" | tee out-$S.txt
```
判决语义(OK / PARTIAL / FAIL / UNSUPPORTED)与逐腿 trace 格式见 `tools/spikes/extmem_probe/README.md`。主机构建(lavapipe)用来证明探针本身报得对:T1/T3 在 lavapipe 上全 OKT1-gles 在 llvmpipe 上 `GL_OUT_OF_MEMORY` 是 Mesa interop 缺口,不是探针缺陷。
## 3. 边界计数器基线(双设备、双后端、四条 trace)
`MOBILEGL_PIPE_STATS=1` 经 retrace 通道的 `--env` 透传;trace APK 从 `7ef7c7e5` 构建,spike OFF。取每次运行的**最后一个完整 120 帧窗口**。accessor/draw 与 memo 门数字是软件确定的(同一 trace 在两台设备上完全相同:它们数的是代码路径不是硬件),只有墙钟/CPU 时间随设备变。
| trace(窗口内帧数) | 后端 | draws/f | **acc/draw** | buf B/f | tex B/f(发射 box/rect | ubo-global B/f | **ubo-named B/f** | memo 门(hit/miss |
|---|---|---|---|---|---|---|---|---|
| `minecraft-1.21.4-in-world`360 | Espryt | 91.6 | **9.28** | 13.5 K | **635 K**185 box / 0 rect | 16.7 K | 0 | ers 9257/2577etl 10538/1296eub 10720/1114 |
| `minecraft-1.21.4-in-world`360 | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K97 box / 89 rect | 16.7 K | 0 | mfp 0/10994mpm 9240/1754mdt 9120/1874 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world`120memo 冷) | Espryt | 23.2 | 21.04 | 32.6 K | 8.8 K | 1.8 K | 0 | ers 1958/1843etl 722/3079 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world`120memo 冷) | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0vtxc 1.7 K | mpm 1890/895mdt 1573/1212 |
| `improved-transparency-minecraft-26.3`1200 | Espryt | 1320 | **8.44** | 333 K | 0 | 0 | 0 | ers 156925/2791etl 148606/11110eub 148246/11470 |
| `improved-transparency-minecraft-26.3`1200 | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036mpm 134421/2615mdt 156611/1785 |
| `minecraft-1.21.1-neoforge-create-indirect-in-world` | 两者 | — | — | — | — | — | — | 两台设备都失败(§5),且不足 120 帧 |
门缩写:ers = `EsprytRenderState`etl = `EsprytTextureSyncList`eub = `EsprytUnitBindingsEpoch`mfp = `MagmaDrawFastPath`mpm = `MagmaPipelineMemo`mdt = `MagmaDynamicTail``MobileGL/MG_Util/Metrics/PipeStats.h:46-122`)。`accessor-calls` 是约 10 个热入口的静态计数,是每 draw accessor 数的**下界**(站点清单 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`)。
对设计的读法:
- **真机稳态动态 accessor 成本是每 draw 6.59.3 次**(预测区间 1025 的下沿;llvmpipe 的 15.5/20.7 是 memo 冷的)。推送要打败的是 ~8 次 accessor + memo 探测,不是 124/169 的静态调用点数。GO/NO-GO 的 tracker 绝对 ns 上限从这里定。
- **`stage-ubo-named`D-B8**Magma 在 26.3 世界每帧重打包 **331 KB** 具名 UBO 字节,Espryt 直接绑定为 0——host payload 决定的第一个真数字。
- **union box vs region list**vanilla 世界同样 185 次发射,Espryt 的整 box 路径移动 **635 K** 纹素字节/帧,Magma 的 rect 路径 **40 K**16×——"server 选上传形状"这一条的量化依据(Mali 侧的 +6 ms/frame 作业数悬崖在另一个方向)。
复现(一台设备一次;两台必须**串行**,见 §4):
```sh
ANDROID_SERIAL=<serial> MSYS_NO_PATHCONV=1 \
python3 tools/trace_replay/run_android_retrace_local.py \
--case minecraft-1.21.4-in-world --backend DirectGLES \
--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120
# 数字在结果目录的 mobilegl.log 里,grep 'MGPipe stats:',取最后一个完整窗口
```
## 4. 桌面数据点与语料事实
- **llvmpipe / lavapipe 动态 accessor**`GuiBatchScenario`14 帧 / 26 drawmemo 冷):Espryt 20.65 / Magma 15.54 次/draw——落在预测区间内,且因场景太短偏高;真机稳态数字见 §3。
- **dirty-surface 面**`python3 scripts/gen_pipe_dirty_surface.py --summary`,本树):`MG_Impl/GLImpl` 41 个文件,926 次 mutator 调用,73 个不同 mutator`RecordError` 一项就占 836 次);92 次(36 个即时发布点、7 个 mutator,绝大多数是 `RecordError`)位于同函数内也到达后端的入口,其余 834 次由紧随的 verb 发布。映射表是 73 条目的问题。
- **读点覆盖**`python3 scripts/gen_pipe.py`):71 条调用(11 screen / 60 context)、63 个 verify payload、61 个 `PipeInputs` 字段;477 行后端读点清单 → 299 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED**。
- **OOM 探测惯用法**41 个 trace fixture 中 0 例——全部语料只有 9 次 `glRenderbufferStorage` 调用散在 5 个 fixture,无一在其后 3 个调用内跟 `glGetError`;语料里真实的成功性检查是 `glCheckFramebufferStatus`。→ `glRenderbufferStorage*` 不 ack。
- **`FramebufferSrgb` / `DepthClamp`**`FramebufferSrgb` 的六个后端读点全部消费一个编译期常量 `false``DepthClamp` 零读点;两者的 `glEnable` 落到 `RenderState.cpp``default:` 分支既不存储也不报 `GL_INVALID_ENUM`41 个 fixture 无一开启任一项(补真存储不会改动任何既有 fixture 的输出)。
- **`GetIntegeri_v` 族**Espryt 实现里是 `GetIntegeri_v` 的 9 个分支 + `GetInteger64i_v` 的 2 个(不是"15 个 case");`GL_COMPUTE_WORK_GROUP_SIZE``GL_Program.cpp``ProgramObject::GetComputeLocalSize` 纯前端回答。
- **payload 尺寸**`MG_Pipe/MGPipeTypes.h``static_assert`arm64 与 x86-64 一致):`MGPDrawInfo` **56**`MGHostSpan` 32、`MGPBindRenderState` **12**`MGPResourceDesc` 88、`MGPFramebufferState` 304、`MGPProgramDesc` 192、`MGPSubData` 72、`MGPPixelPackState` 28、`ResidualValueBlock` **1248**(其中 `RenderStateParameters` 1168)。`SEG_CMD` 按 56 B 固定头定尺:MC 帧 10004000 draw 时每帧 56224 KiB 头字节。
- **persistent map 采纳的既有基线**`dev`MC 26.3Adreno):≥16 MiB 可变 store 定义时采纳为 coherent persistent map 后 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB。P11 的回归上限对着它。
- **Mali 上传作业数悬崖**(Espryt 代码注释记录的既有实测):~100 个精灵 rect 对一个 union box 是 +6 ms/frame。
## 5. Harness 事实与陷阱
1. trace app 从不到达 `MobileGL::DestroyImpl`,所以 `MOBILEGL_PIPE_STATS_FILE` 的 JSON 转储在设备上永远不会写——只有 `mobilegl.log` 里的周期汇总行;短于一个周期的 trace 什么都不产出。`MOBILEGL_PIPE_STATS_PERIOD``458ccde1`)为此而加:需要数字的运行把它设到足够小。
2. `run_android_retrace_local.py` 每棵树共用一个 `.trace-work/android-retrace-result` 根并在每次调用时 `rmtree`,所以两台设备必须从一棵树**串行**跑。
3. `--env` 值里嵌入的 `/data/...` 会被 runner 的 bash.exe 做 MSYS 路径转换(`MSYS2_ARG_CONV_EXCL="/data/*"` 只覆盖开头匹配)——用 `MSYS_NO_PATHCONV=1` 跑。
4. `coherent_as_flush` 管线完好:`--ez coherent_as_flush true``trace_replay_core.cpp``setenv`,独立于 `--env` 透传。
5. **`minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败**Adreno 830Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`MaliSSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。
---
# P1 实测(`feat/disaggregated`lavapipe / llvmpipe
## 6. 规模:站点、访问器、填充点
| 量 | 值 | 出处 |
|---|---|---|
| 后端 `pGLContext->` 箭头站点 | 277Espryt 113、Magma 164 | 计划写 293,出自过期的 vendored 清单 |
| 非箭头行 | 58Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT` | 与计划一致 |
| `PipeInputs` 字段 | 63 | 计划写 61`GetBoundTransformFeedbackLifetimeId``HasOpenTransformFeedbackSpan` 是 D21 之后新增的读点 |
| 后端调用的不同访问器 | 62Espryt 32、Magma 56 | `GetBoundTransformFeedbackName` 已无人读,留作已标注的死行 |
| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 | `MG_Pipe/FillPoints.def` |
| `SyncPersistentMappedRange` / `SyncGpuWrites` | 20 / 6 | 与计划一致 |
## 7. verify 通道发现的两类真问题
**缺填充行(9 处)。** 逐 verb poison 在 79 例 retrace 与 818 条 integration-verify 上抓出三组:`kReadback``IsTransformFeedbackActive`/`IsTransformFeedbackPaused`(深度/模板回读仿真的 `ScopedEmulationDrawState` 会暂停在飞的捕获)、`kTextureOp``kDispatch``IsCapabilityEnabled``kBlitOrCopy`/`kTextureOp` 缺着色器 blit 用到的 viewport 与顶点/缓冲绑定。其中 8 行是静态过近似(代码路径可达但通道未跑到),过近似会让那对 (类, 字段) 的 poison 永久失效,**P2 收紧填充表时先复查这 8 行**。
**verb 内后端改前端(3 个字段)。** Magma 在自己的 draw 里写前端对象(为未绑定采样器合成回退纹理、材质化排队清除、覆写采样器 filter),把边界已经拷走的值挪了位,8 条 DirectVulkan 用例与 2 条 trace 因此报 `Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@Draw*", where=read}`。**选定的解法是 push-on-mutation**:前端计数器移动时用 `MGP_NOTE_MUTATION(Field)``MG_Pipe/PipeMutation.h`)刷新推送块里的那一个字段(只刷值不刷戳记),"推送块在每次读取时都等于活上下文"这条不变式因此字面成立,也正是 P2 tracker 需要的形状。
三个被这样处理的字段与它们的钩子:
| 字段 | 钩子 |
|---|---|
| `GetSamplingResolutionGeneration` | `TextureState::BumpSamplingResolutionGeneration()` |
| `GetTextureBindGeneration` | `TextureState::BumpTextureBindGeneration()``NoteUnitTouched()``bindingChanged` 分支 |
| `GetMaxTouchedTextureUnit` | `NoteUnitTouched()` 的高水位分支 |
钩子挂在**计数器**上而不是四十个写入点上:后端写前端的全部路径(`SamplerObject` 的 8 个 setter、`ITextureObject` 的 6 个、纹理单元绑定路径)都经由这三个计数器,`BufferObject`/`ProgramObject`/`VertexArrayObject` 的后端写入不移动任何推送字段(它们是句柄类读点)。
## 8. P1 验收(合入后在 `~/w7/pipe` 实测)
| 门 | 结果 |
|---|---|
| pull 构建符号与 `.text` | 0 增 / 0 删 / 0 改尺寸 / 0 重命名,`.text` 字节不变 |
| `MG_Backend` 里的 `pGLContext` | 0(唯一保留处是开关头文件的 pull 分支) |
| 单元(pull / push / verify | 1485 全绿 × 3 |
| `integration-gpu`pull | 878 全绿 |
| `integration-verify` | 818 全绿,零 `Fatal{` |
| 79 例 retrace`MOBILEGL_PIPE_VERIFY=1` | 79/79 通过,79/79 带 armed 摘要,零 `Fatal{` |
| 两个阴性对照 | 4 条专用条目全绿(篡改字段变红、抽掉一个填充戳记在那条 verb 上变红) |
| 测试名 | 0 删除,+29 |
**verify 构建的代价**`integration-verify` 818 条在 4 路并行下约与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。
---
# P2 实测(`feat/disaggregated@738b289d`
> **口径变更(用户 2026-09-08**push 比 pull 多约 10% 逐线程 CPU 可接受;**性能自此对着 pull 臂记录,不作阻塞门**——pull 臂的数字就是此后的基准线,第 43 天的 tracker 绝对 ns 上限降为记录项;路线图先推完(下一步 P3a),专门的优化阶段排在其后(或首个 IPC 帧之后)。所以本部分把"门"与"记录"分开写:§9 是门,§10–§14 是记录与遗留判定。
## 9. P2 验收门(合并点实测;一个在飞的修复落地后复核)
| 门 | 结果 |
|---|---|
| G1 pull 构建符号 | 0 增 / 0 删 / 0 重命名;**4 处 resize**,全部事先认定:`RenderState::{RenderState, SetCapability, IsCapabilityEnabled}``_GLOBAL__sub_I_DirectGLES.cpp``g_syncedRenderStateParameters` 的静态初始化器);`.text` **+160 B** |
| G5 `SyncRenderState` 一行未动 | `DirectGLES.cpp` 里整个 `namespace RenderStateImpl` 的 sha 与 P2 起点**逐字节相同** |
| G2 pull 与 push 逐名相同 | `ctest -N` 名集合**差 0** |
| G14 测试名只增不删 | **0 删除,+119**+115 的 P2 包 + ABA 世代覆盖的 4 条单元) |
| 单元 | **1566** 全绿 × {pull, push, verify}1562 + ABA 世代覆盖的 4 条 `MagmaPipeIdentity` 单元) |
| `integration-gpu` | **916/916** pull、**916/916** push、**916/916** `MOBILEGL_PIPE_PUSH=0`(全 pull 臂) |
| 渲染状态敏感子集 | **72/72** |
| `integration-verify` | **828** 条,零 `Fatal{` |
| 79 例 retrace | push 下 **79/79**`MOBILEGL_PIPE_VERIFY=1`**79/79 全部 armed,零分歧** |
| G7 setter 一致性的负面对照 | 按设计变红并**点名 `SetColorMask`**(把一个成员从 pipeline 半边降到 dynamic 半边,划分仍完整、仍能编译) |
| CSO 内容寻址对照 | `CsoContentAddressing` **6/6** |
| verify 构建的三组对照 | `PoisonOmitted``VerifyCorrupted``HandleRecycle` 合计 **44/44** |
## 10. 设备配对 A/B:逐线程 CPUD.4.2
> **勘误(2026-09-08P3a 收尾时发现)**:本节两机 64 次运行所用的两臂 APK 是**未优化构建**——其 `libMobileGL.so` 43.2 MB、`.text` 21.8 MB,而同一 clang 18.0.4 的 Release 库是 15.5 MB / `.text` 9.4 MB(两者都无 `.debug_info`),同一 `improved-transparency-minecraft-26.3` 用例在 Oppo 上 Espryt pull 的 CPU p50 为 42.1 ms 对 Release 的 9.7 ms。因此本节的**绝对值与 +8–18% 的差值都是 -O0 读数**(tracker 这类内联密集的代码在 -O0 下付出最多),不能作为性能基准线。**基准线以 §20 的 P3a 两机 A/B3e298c9a 的 Release APK)为准**:其 pull 臂与 P2 的 pull 路径符号一致(P3a 全程 G1 0/0/0/0,与 44c2b5cf 只差 d7655247 的 5 行),其 `MOBILEGL_PIPE_PUSH=0x7f` 臂就是 P2 边界在 Release 下的代价。本节其余内容(协议、计数器、harness 误杀)仍然有效。构建脚本 `~/w7/notes/tools/wsl_build_trace_apks.sh` 从此打印库尺寸(Release `.text` ≈ 9.4 MB)。
协议:reboot-clean、同热窗口、按项目协议定频(大核 1.96 / 小核 1.55 GHz、GPU 拉满、40 °C 门),两臂背靠背同一会话,一次一台设备(`run_android_retrace_local.py` 每棵树共用一个结果根,见 §5.2),`--benchmark-no-finish` 为主臂(P2 问的是 CPU)。数字取 `benchmark.json``frameCpuTimesMs[]` 尾 200 帧,主机侧算 p50/p99。`minecraft-1.21.1-neoforge-create-indirect-in-world` 不在设备 A/B 里(§5.5,基线就坏)。
每格取 runner 自己的 "best of 3"(三次重复里平均墙钟帧时间最低的那次;`run_android_retrace_local.py` 的约定),p50 按设备的中位数规则、p99 为 nearest-rank,都在同一尾窗口上算;`tools/device_bench/pin_device.sh check` 在每次运行前后各跑一次,判定记在最后一列(P = 三个节点都在钉住的频率上;D = 大核被热管理钳在 1689600 kHz,脚本钉的是 1958400——用例前后两臂同一状态才可比)。APK 两臂出自 `55d2af9b`(P2 集成后、ABA 对照补丁前;差异只在负面对照臂)。
小米 24129PN74C`35d0befa`Adreno 830,会话 reboot-clean 一次、整段钉频),单位 ms/帧:
| trace | 后端 | finish | pull p50 | push p50 | Δ p50 | pull p99 | push p99 | Δ p99 | 钉频 pull / push |
|---|---|---|---|---|---|---|---|---|---|
| `minecraft-1.21.4-in-world` | DirectGLES | 关 | 7.392 | 8.187 | **+10.8%** | 11.086 | 13.192 | +19.0% | P/P |
| `minecraft-1.21.4-in-world` | DirectGLES | 开 | 7.433 | 8.383 | **+12.8%** | 10.969 | 11.789 | +7.5% | P/P |
| `minecraft-1.21.4-in-world` | DirectVulkan | 关 | 5.230 | 5.849 | **+11.8%** | 6.772 | 7.396 | +9.2% | P/P |
| `minecraft-1.21.4-in-world` | DirectVulkan | 开 | 5.237 | 5.842 | **+11.6%** | 6.767 | 7.380 | +9.1% | P/P |
| `improved-transparency-minecraft-26.3` | DirectGLES | 关 | 37.263 | 42.436 | **+13.9%** | 54.965 | 60.409 | +9.9% | P/P |
| `improved-transparency-minecraft-26.3` | DirectGLES | 开 | 37.222 | 42.422 | **+14.0%** | 54.879 | 60.401 | +10.1% | P/P |
| `improved-transparency-minecraft-26.3` | DirectVulkan | 关 | 61.549 | 68.118 | **+10.7%** | 79.403 | 88.111 | +11.0% | D/D(热钳,两臂同) |
| `improved-transparency-minecraft-26.3` | DirectVulkan | 开 | 61.446 | 68.252 | **+11.1%** | 79.244 | 88.357 | +11.5% | D/D |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectGLES | 关 | 4.911 | 5.353 | **+9.0%** | 2314.8 | 2315.7 | 编译主导 | P/P |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectGLES | 开 | 4.972 | 5.370 | **+8.0%** | 2312.5 | 2322.2 | 编译主导 | P/P |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectVulkan | 关 | 4.158 | 4.525 | **+8.8%** | 1604.5 | 1624.2 | 编译主导 | P/P |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectVulkan | 开 | 4.158 | 4.583 | **+10.2%** | 1614.0 | 1646.9 | 编译主导 | P/P |
| `minecraft-1.21.4-startup` | DirectGLES | 关 | 0.941 | 1.068 | +13.5% | 1180.4 | 1291.2 | 加载主导 | D/D |
| `minecraft-1.21.4-startup` | DirectGLES | 开 | 0.822 | 0.841 | +2.3% | 1271.6 | 1284.3 | 加载主导 | D/D |
| `minecraft-1.21.4-startup` | DirectVulkan | 关 | 0.382 | 0.415 | +8.6% | 1463.0 | 1487.1 | 加载主导 | D/D |
| `minecraft-1.21.4-startup` | DirectVulkan | 开 | 0.368 | 0.422 | +14.7% | 1485.4 | 1459.7 | 加载主导 | D/D |
读法:**推送没有在拉取基线之下净减少**(那是开放问题 1 原本的期望),而是在四个 trace、两个后端上稳定多花 **814% 逐线程 CPU**p50),p99 同向;finish 开与关两臂几乎一致,说明多出来的是客户端 CPU 而不是 GPU 时间。三点读数纪律:bsl 用例的 p99 两臂都由着色器编译主导(约 1.6–2.3 s),startup 用例只有 59 帧尾窗且 p99 是加载,两者的 p99 都不承载这个问题;26.3 的 DirectVulkan 两臂都在热钳下跑(大核 1689600 kHz),绝对值偏高但两臂同状态,相对差有效;`acc/draw` 两臂相同(accessor 计数还是 tracker 填充时的调用,P2 没改它的定义)。
Oppo PLG110`3B159D009VZ00000`Mali,会话 reboot-clean 一次、整段钉频,32 次运行前后判定全部 PINNED),单位 ms/帧:
| trace | 后端 | finish | pull p50 | push p50 | Δ p50 | pull p99 | push p99 | Δ p99 | 备注 |
|---|---|---|---|---|---|---|---|---|---|
| `minecraft-1.21.4-in-world` | DirectGLES | 关 | 8.297 | 9.803 | **+18.2%** | 10.446 | 11.817 | +13.1% | |
| `minecraft-1.21.4-in-world` | DirectGLES | 开 | 8.366 | 9.700 | **+15.9%** | 10.774 | 11.935 | +10.8% | |
| `minecraft-1.21.4-in-world` | DirectVulkan | 关 | 5.341 | 5.941 | **+11.2%** | 6.785 | 7.287 | +7.4% | |
| `minecraft-1.21.4-in-world` | DirectVulkan | 开 | 5.349 | 5.931 | **+10.9%** | 6.765 | 7.376 | +9.0% | |
| `improved-transparency-minecraft-26.3` | DirectGLES | 关 | 41.252 | 45.022 | **+9.1%** | 79.054 | 91.729 | +16.0% | |
| `improved-transparency-minecraft-26.3` | DirectGLES | 开 | 41.062 | 44.227 | **+7.7%** | 82.357 | 85.087 | +3.3% | push 臂第 1 次重复被 harness 误杀(见下),best of 2 |
| `improved-transparency-minecraft-26.3` | DirectVulkan | 关 | 54.478 | 60.108 | **+10.3%** | 91.733 | 111.789 | +21.9% | |
| `improved-transparency-minecraft-26.3` | DirectVulkan | 开 | 50.657 | 59.829 | +18.1% | 68.068 | 100.402 | +47.5% | pull 臂这次 best-of-3 明显快于同臂 nofinish50.7 对 54.5),差值被放大;以 nofinish 行为准 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectGLES | 关 | 6.615 | 7.185 | **+8.6%** | 2491.9 | 2268.9 | 编译主导 | |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectGLES | 开 | 6.543 | 7.065 | **+8.0%** | 2170.5 | 3387.9 | 编译主导 | |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectVulkan | 关 | 4.087 | 4.452 | **+8.9%** | 1264.6 | 1295.4 | 编译主导 | |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | DirectVulkan | 开 | 4.058 | 4.464 | **+10.0%** | 1271.9 | 1295.3 | 编译主导 | |
| `minecraft-1.21.4-startup` | DirectGLES | 关 | 1.471 | 1.740 | +18.3% | 966.9 | 956.3 | 加载主导 | |
| `minecraft-1.21.4-startup` | DirectGLES | 开 | 1.473 | 1.641 | +11.4% | 959.0 | 946.2 | 加载主导 | |
| `minecraft-1.21.4-startup` | DirectVulkan | 关 | 0.416 | 0.460 | +10.6% | 1129.1 | 1124.7 | 加载主导 | |
| `minecraft-1.21.4-startup` | DirectVulkan | 开 | 0.419 | 0.471 | +12.4% | 1131.2 | 1124.0 | 加载主导 | |
两机读法:Mali 上 Espryt 的相对代价比 Adreno 高一档(in-world +1618% 对 +1113%26.3 +89% 对 +14%),Magma 两机一致(+1011%);p99 在 26.3 上同向放大(Mali Magma +22%),这是 P2 tracker 在 draw 最密的用例上的尾部代价,随 P3a–P4a 的句柄化收缩,按口径记录。计数器(`acc/draw`、六个 memo 门、`resid=``csom`/`csob`)两机逐字相同——它们数的是代码路径不是硬件(§3 的结论再次成立)。
**harness 误杀(记录,dev 侧跟进)**Oppo `improved-transparency-minecraft-26.3` DirectGLES push/finish 的第 1 次重复在第 960 帧被 `android-plugin/trace-replay-ci.sh` 强停(events 日志 `am_kill … due to from pid <adb shell>`oom adj 0`logcat -b crash` 无崩溃):那个脚本的等待循环对 `pidof` 单次采样失败即判定"应用已退出"并 force-stop,一次 adb 抖动就丢一次重复。数字用剩下两次重复的 best-of;修法是连续 N 次采样失败才判退出。
## 11. tracker 的绝对 ns:上限与 T1/T2D.4.3
**上限在跑之前钉死**`ARCHITECTURE.md` §13.2 第 4 条要绝对阈值,因为相对噪声阈值会平凡通过)。推导:拉取基线是每 draw **6.59.3 次 accessor 调用加 memo 探测**(§3 的表:MC 1.21.4 in-world Espryt 9.28 / Magma 8.56 @ 91.6 draws/帧;improved-transparency 26.3 Espryt 8.44 / Magma 6.53 @ 1320 draws/帧)。按"一次未内联的 accessor 调用加一次 load"计价——610 周期,1.96 GHz 上约 4 ns——再加六次 memo 探测各约 2 ns,得到推送必须不差于的 **约 44 ns/draw**。因此:
> **T1 ≤ 45 ns/drawAdreno 830 `35d0befa`)、≤ 60 ns/drawMali `3B159D009VZ00000`**。若开跑前的校准(pull 库的一次 DriverBench`mc_vanilla_draw` 的 `ns_per_op` 减去 `native` 对照)显示这两台设备上的单次 accessor 价格与估计不同,则用**实测**价格按同一算术重推上限——但仍在测 push 臂之前钉死,不在之后。
两个差值都要公布,分母是 `mc_vanilla_draw` 的 5495 draws/帧:**T1** = `ns_per_op(push, 默认位图) ns_per_op(pull)`,即整个边界的每 draw 代价;**T2** = `ns_per_op(push, MOBILEGL_PIPE_PUSH=0) ns_per_op(pull)`,即 P1 的残余填充本身,于是 **T1 T2** 恰好隔离出 P2 加了什么、删了什么。`DriverBench` 只在 Linux 桌面构建,且它不链接 MobileGL——`dlopen` 一个 provider,所以同一个二进制同时量原生驱动与两个后端;这对 P2 够用,因为绝对 ns 是客户端 CPU 问题,而"两台设备"的要求落在 §10 的逐线程 CPU 上。
## 12. Blaze3D blend-toggle 与 CSO 内容寻址负面对照(D.4.4 / D.4.5
**微基准用例已在树里并有存活门**`mc_state_toggle` 就是 `glEnable(GL_BLEND); glBlendFuncSeparate; glDrawElements; glDisable(GL_BLEND); glDrawElements` × 46,按 vanilla 帧的真实速率(每帧 46 对开关、28 次 `glBlendFuncSeparate`)。`DriverBenchStateToggle` 这条 ctest 用 `PASS_REGULAR_EXPRESSION` 钉住该用例自己的 CSV 行、并把每帧 ops 列钉在 **46**,所以用例被改名、被删、改了每帧 ops 或干脆没打印,它都会红;`DriverBench` 遇到不认识的用例名现在返回 rc 2 并列出现有用例,而不是打个表头就 rc 0。
**负面对照的开关是行为位 63**`kMGPipeBehaviourNoCsoContentAddressing``MobileGL/MG_Pipe/MGPipe.h:83`):它关掉 map 探测与句柄复用,**不关 CSO 记录**——否则量的是另一回事。开关不会烂掉,因为 `CsoContentAddressingScenario` 是常开的 ctest,钉住的数值契约是:一帧 8 对开关 = 16 draw;内容寻址臂 `csom ≤ 4` **且** `csom < csob`;位 63 臂 `csom == csob`;两臂都 `csob ≥ 16`;两帧连续开关的回读全绿且逐字节相同(要 `MOBILEGL_PIPE_STATS_PERIOD=1` 才读得到)。
对照的意义是把"**推送更慢**"与"**CSO 设计更慢**"分开:若 T1 越界而对照不越界,代价在 tracker;两个都越界,代价在线上形状。
**实测(桌面 llvmpipe / lavapipe`~/w7/notes/tools/wsl_p2_bench.sh``DriverBench` Release 构建,240 帧,每臂 5 次重复取中位数;pull 库 = `build-linux`push 库 = `build-push` 默认位图 `0x7f`**`ns_per_op`
| 臂 | `mc_vanilla_draw`ns/draw | `mc_state_toggle`ns/开关对) | `mc_pass_switch`ns/pass |
|---|---|---|---|
| native(裸驱动对照) | 4771 | 22968 | 437759 |
| Espryt pull | 5121 | 23753 | 443300 |
| Espryt push | 5443 | 24869 | 446017 |
| Espryt push`MOBILEGL_PIPE_PUSH=0` | 5671 | 24584 | 443807 |
| Espryt push,位 63(无 CSO 内容寻址) | 5374 | 24630 | 435454 |
| Magma pull | 16900 | 32705 | 438406 |
| Magma push | 17245 | 33857 | 446067 |
| Magma push`MOBILEGL_PIPE_PUSH=0` | 17599 | 34082 | 445006 |
| Magma push,位 63 | 17444 | 34780 | 444758 |
分解(`mc_vanilla_draw`ns/draw):
| | Espryt | Magma |
|---|---|---|
| **T1** = push pull(整个边界的每 draw 代价) | **+322**pull 的 +6.3% | **+345**+2.0% |
| **T2** = push(`PIPE_PUSH=0`) − pullP1 的残余填充本身) | +550 | +699 |
| **T1 T2**P2 自己加的减的) | **228** | **354** |
| 位 63 对照 − push(CSO 内容寻址的净值) | −69(在 5 次重复的离散内,≈ 0) | +200(内容寻址每 draw 省 200 |
| blend-toggle`mc_state_toggle`ns/开关对) | +1116+4.7% | +1153+3.5% |
| pass switch`mc_pass_switch` | +2716+0.6% | +7661+1.7% |
读法:**P2 的净效果是负的**——tracker + CSO 比它替掉的 P1 残余填充便宜 228 / 354 ns/draw,两个后端一致;剩下的 T1(+322 / +345)是还没迁成句柄的那部分 `PipeInputs` 填充与 dirty 走查,随 P3a–P4a 逐子系统收缩。位 63 对照把"推送更慢"与"CSO 设计更慢"分开:Espryt 上 CSO 内容寻址不花钱也不省钱(`SyncRenderState` 本来就是 memo 化的),Magma 上省 200 ns/drawpipeline 键从 CSO 句柄取,少一次哈希)。§11 钉的上限(T1 ≤ 45 ns/draw @ Adreno 830)是**设备**口径,`DriverBench` 只在桌面栈上跑(同一 draw 在 llvmpipe 上花 5.1 / 16.9 µs),两者不能直接比;设备上对应的读数是 §10 的 +814% p50——按 2026-09-08 的口径记录在案,不判门。
## 13. 计数器读数
**`resid=` 字节类(G10)已非零**,且棘轮已经压到底:`MGL_RESIDUAL_BLOCK_SIZE`**1248 降到 8**`MobileGL/MG_Pipe/MGPipeTypes.h:546`,只降不升是 `static_assert`),块里只剩 `Uint64 CapabilityBits`。桌面 push retrace`minecraft-1.21.4-fabric-iris-bsl-in-world``MOBILEGL_PIPE_STATS_PERIOD=60`
```
MGPipe stats: frames=120 window=60 draws=2293 draws/f=38.22 … resid=197.07 … cso[csom=8 csob=1415]
```
- **残余块发射率**197.07 B/帧 ÷ 8 B × 60 帧 = **每 60 帧 1478 块,合每 draw 0.64 块**。(`CrossFrameBufferScenario` 那种"一个窗口 `resid=8.00`、其后全 `0.00`"是抑制器在起作用的最小形态,不是语料上的速率,别拿它当代表数。)
- **`csom` / `csob`(新的两个调用类)**:同一窗口 **8 次铸造对 1415 次绑定**——CSO 内容寻址在真实语料上的复用比,也是位 63 对照要打掉的那件事。
**六个 memo 门的 hit/miss、设备上的 `resid=` 与 `csom`/`csob`**:拉取侧的基线在 §3 的表里(`ers`/`etl`/`eub`/`mfp`/`mpm`/`mdt`)。推送臂的读数出自 §10 那批运行(`--env MOBILEGL_PIPE_STATS=1 --env MOBILEGL_PIPE_STATS_PERIOD=120`,取最后一个完整的 120 帧窗口;设备上只有 `mobilegl.log` 的周期行,`MOBILEGL_PIPE_STATS_FILE` 永远不会写,见 §5.1;finish 开/关两臂逐字相同,软件确定性),小米 `35d0befa`push 臂:
| trace | Espryt `ers` / `etl` / `eub`hit/miss | Magma `mfp` / `mpm` / `mdt`hit/miss | `resid=` B/帧 | `csom` / `csob`(每 120 帧) |
|---|---|---|---|---|
| `minecraft-1.21.4-in-world` | 6020/1850 · 6884/986 · 6962/908 | 0/7278 · 6030/1248 · 5928/1350 | 178.31 | 2 / 1719 |
| `improved-transparency-minecraft-26.3` | 78960/1367 · 75378/4949 · 75199/5128 | 10740/68928 · 67660/1268 · 78800/868 | 185.36 | 0 / 1157 |
| `minecraft-1.21.4-fabric-iris-bsl-in-world` | 113/133 · 85/161 · 82/164 | 0/177 · 86/91 · 81/96 | 370.67 | 1 / 122 |
| `minecraft-1.21.4-startup` | 174/185 · 305/54 · 305/54 | 0/118 · 0/118 · 0/118 | 25.08 | 9 / 181 |
读法:pull 臂的 `resid=` 恒为 0.00(残余块只在 push 下发射),push 臂每帧 25–371 B,即 8 字节块每帧 346 次,与桌面的 0.64 块/draw 同量级;CSO 内容寻址在稳态窗口里几乎不再铸造(in-world 2 次对 1719 次绑定,26.3 零铸造),只有 startup 在建状态时铸 9 次;六个 memo 门的形状与 §3 的拉取基线一致(Espryt 三门以 hit 为主;Magma 的 `mfp``MagmaDrawFastPath`)在拉取基线上就是 miss 为主——in-world 0/10994、26.3 21360/137036——P2 没有碰这个门)。
**逐 dirty 位的触发率:未测。** 计数本身已实现(`MGPipeTracker``FireCount`/`WalkCount`,挂在 `PipeStats::Enabled()` 后面),但汇总行的格式里没有它们,所以没有任何东西把 18 个计数打出来。补法是给 `FormatWindowLine` 加一行,或从一个场景里经访问器读——两者都是 P3a 的顺带项。
**每 draw payload 直方图:未测。** 24 桶已实现,但只在 teardown 的 JSON 里输出(`MOBILEGL_PIPE_STATS_FILE`),而 trace app 从不到达那次 teardown(§5.1),所以设备上取不到;桌面也没有记录过一次。
## 14. 遗留判定
**8 条静态过近似的填充行:全部保留,逐组给了理由**,写在 `MobileGL/MG_Pipe/FillPoints.def` 的表头注释里(就是 §7 那 9 处缺填充行里静态过近似的那 8 行)。理由三组同一条:这些行不是猜的,每一条都点名一条具体的后端路径,而能退役它们的证据只能是**动态**的——语料没走到某条路径,什么也证明不了,据此删行等于把一条罕见路径变成出货构建里的 `Fatal{UnmigratedPipeInput}`。三组分别是:`kReadback` + `IsTransformFeedback{Active,Paused}`(深度/模板回读仿真自己会画一个 draw 并暂停在飞的捕获,只在仿真被驱动条件触发时才走到);`kTextureOp` / `kDispatch` + `IsCapabilityEnabled`Magma 的 `GenerateMipmap``PrepareStorageImageTextures` 都经 `VkClearManager``GL_FRAMEBUFFER_SRGB`——**P2 给这个 capability 补了真存储之后,这一行从读编译期常量变成了读真状态,比以前更承重**);`kBlitOrCopy` / `kTextureOp` + 着色器 blit 的 viewport 与顶点/缓冲绑定(`TryBlitToDefaultFramebufferWithShader` 是后端自有 program 的真 draw,同样是驱动条件决定的)。真正能退役一行的是 `MOBILEGL_PIPE_POISON_OMIT` 跑遍两台设备上完整的 `gl44to46` caselist——记为 P3a 的活,不在桌面语料这种撑不住的证据上做。
**`integration` 的第二遍过滤(`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1`186 条)不进比对器**`.github/workflows/test.yml:534`)。比对器的代价现在是已知的 5–10×,而这 186 条是 buffer/回读方向的过滤,P2 在那里什么也没改;**P3b 再复核**,这条决定记在这里而不是把 CI 里那句注释一直吊着。
**Track H 单位成本的日历口径:未记录。** 产出侧在案(`ARCHITECTURE.md` §9.5 那份 21 条身份 memo 普查里的 11 条直接删除与 2 条重键全部落地,两片 Track H 零回归,pre-handle 臂在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下并存到 P13),但两个包各自的实际工作日没有记,所以"不超出估计的 50%"这条判据这轮是按产出而不是按日历结算的。
**句柄 ABA 对照的"重键前红"证据**D.2)。修复前,`HandleRecycle` 的 28 条里有 1 条红——`AbaControl` 那条 VAO 用例看到的是替换对象的绿,而这个臂**期望**看到死对象的红:
```
$ ctest --test-dir build-push -R 'HandleRecycle' --no-tests=error -j 4 --output-on-failure
96% tests passed, 1 tests failed out of 28
DirectVulkan.HandleRecycle.AbaControl.…AVertexArrayAtARecycledAddressDoesNotInheritItsPredecessorsVertexInput (Failed)
… [AbaControl expects the STALE object's pixels …]: 11625 of 11625 pixels (100%) are not red;
first offender at (2,2) is green
```
修好之后 32/32(多出来的四条是新增的 `AbaControlHandles` 臂,让对照能够到 P2 出货的 `{slot, gen}` 臂而不只是 pre-handle 臂),并且逐臂把判决打出来:`arm=Handles expected=FRESH observed=FRESH``arm=AbaControl expected=STALE observed=STALE`,两个臂都如此。**对照确实承重**:把 `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` 关掉,两个臂都变成 `observed=FRESH` 并**失败**——污染由被打掉的身份产生,别无他因,而退役的旧守卫与出货的 `{slot, gen}` 都能拦住它。
---
# P3a 实测(`feat/disaggregated@fde5fda3`
> 基线:`5cb826b0`P2 的 `44c2b5cf` 加上 `dev@9eae9858` 的合并);两个版本号提交之后 G1 的 pull 基线在 `b9eaa474` 重取。四个包 = contract/wire、client、espryt、gates,集成顺序 contract → wire → client → espryt → gates。
>
> **口径不变**:性能对着 pull 臂**记录**、不作阻塞门;正确性门照旧是硬门。所以下面 §16 是门,§19–§21 是记录与遗留判定。
## 15. 用户的口径规则,逐字
2026-09-08 的用户规则,`docs/Disaggregated/` 里此前没有一处写下它,所以逐字抄在这里:
> (a) advance the roadmap as fast as possible without a large performance regression; performance is RECORDED against the pull baseline, never a blocking gate in P3a; correctness gates stay.
落到 P3a 的后果:五部分门的**第 4 部分**(`ARCHITECTURE.md:514` 的 monolith 性能)与 `ROADMAP.md:19` 里那句"MC 26.3 在 Adreno 上 p99 不变"都变成**测了就发布**的义务,不是通过/不通过的判据;第 1、2、3、5 部分仍是硬门。一次回归要写下数字与怀疑的成因并带进 P4a 的排期——**唯一不可谈判的是数字必须真的采到**:没测到的回归不算记录在案。
## 16. P3a 五部分门(本地 `~/w7/pipe`HEAD `3e298c9a`,基线 `44c2b5cf`
**第 1 部分 —— 接口纯度**
| 门 | 结果 |
|---|---|
| 三个构建(pull / push / verify | rc 0 / 0 / 0。fail-fast 规则原样保留:任一非零就打印 `BUILD FAILED - gate aborted (no stale-binary verdicts)` 并退出,绝不拿陈旧二进制下判决 |
| A 门 include 闭包 | 4 个探针,0 skip**0 problem** |
| C 门 `MG_Backend` 里的 `pGLContext` | 空 |
| G13 `PipeApply.h` 的 ops 块里的 `MG_State` | 空 |
| **G1** pull 构建符号(P3a 的认定 resize 集**为空** | `.text` 10806323 → **10806323+0+0.000%**27811 → 27811 个已定义符号,**0 增 / 0 删 / 0 resize / 0 重命名** |
| **G5** 十个函数逐字节相同 | rc 0,对 `44c2b5cf` **与** `5cb826b0` 都成立(九个 pool / 延迟释放 / ring,加 ID-11 追加的 `FlushPendingRangesNow` |
| `RenderStateImpl` 段 shaP2 的不变量) | `d8fd1c48716056c53675`,逐字相同 |
| **G8** `HandleRecycle`verify 构建) | **60/60**,含新增的 buffer 用例三臂 |
**第 2 部分 —— 语义影子比对(决定性的一条)**
| 门 | 结果 |
|---|---|
| `integration-verify` | **842/842**`Fatal{`**0** |
| 79 例 retrace`MOBILEGL_PIPE_VERIFY=1` | **79/79 通过****79/79 全部 armed**(每份日志都带 `MGPipe verify:` 行),带 `Fatal{` 的日志 **0**——即零分歧、零未迁移读 |
**第 3 部分 —— 行为 A/B**
| 门 | 结果 |
|---|---|
| **G2** pull 与 push 的 ctest 名集合 | 差 **0** 行 |
| **G14** 测试名 | **0 删除 / +58** |
| 单元 | **1619** 全绿 × {pull, push, verify} |
| `integration-gpu` pull / push | **958/958** / **958/958** |
| `MOBILEGL_PIPE_PUSH=0`(全 pull 臂) | **958/958** |
| `MOBILEGL_PIPE_PUSH=0x7f`P3a 两个子系统关闭,**G12** | **958/958** |
| `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1`(具名的 kill-switch 臂,见 §21 | **958/958** |
| buffer/VAO 族(`LargeArenaAdoption``StorageBufferRegrow``VertexAttribBinding``MultiDraw``PrimitiveRestart``CrossFrameBuffer``ResidentIndex``BufferTexture``AtomicCounter``XfbCaptureBufferReuse``PackedWordReadback``DoublePrecision``VertexArrayEnableDisable``DrawParameters` | **202/202** |
| P2 的 G7 setter 一致性阴性对照 | rc 0,按设计变红后树又恢复绿 |
| **G7** vertex-input 阴性对照 | rc 0 —— **变红并点名 `IsBgra`**,随后恢复、重建、再变绿 |
| `CsoContentAddressing` + `ResourceSubsystemControl`(**G12**:开关真的改变行为,不是死代码) | **10/10** |
| verify 构建的对照组(`PoisonOmitted``VerifyCorrupted``HandleRecycle``AbaControl*``ResourceSubsystem*``MapPersistentRoundtrips*` | **64/64** |
| 79 例 retracepush | **79/79 通过**`failed: []`),每条都在自己的 SSIM 阈值之上 |
| **G3b** 具名五条(`create-indirect``create-instancing``rd12-odinlite``improved-transparency-minecraft-26.3``fabric-sodium` | **桌面侧全绿,但要看清是怎么绿的。** 五条**都在上面那次 79 例 push 扫描里通过**——它们全都在 `tools/trace_replay/trace_cases.json` 的语料内,而那次扫描是 79/79。**单独跑的那次具名扫描自己选中了 0 例**(`passed 0 / 0`):`retrace_gate.py``--only` 收的是**正则**,门脚本传的却是逗号分隔的清单,于是过滤器一条都没匹配上。这是门脚本的缺陷、不是本阶段的失败,重跑要写成 `--only 'create-indirect\|create-instancing\|rd12-odinlite\|improved-transparency-minecraft-26\.3\|fabric-sodium'`。**设备侧**按 `ROADMAP.md` 开放问题 17 把 `create-indirect` 排除在 A/B 之外,所以 **G3b 整体记为"桌面全绿、设备侧部分(受阻于 `dev` 的开放问题 17"**,不当作完整通过 |
**第 4 部分 —— 性能:记录,不设门**(规则 (a))。见 §20。
**第 5 部分 —— 覆盖、poison、句柄纪律**
| 门 | 结果 |
|---|---|
| `gen_pipe.py --check` / `--self-test` | 生成物是最新的;self-test **7 个阴性对照全部触发**,正对照 OK |
| `gen_pipe_dirty_surface.py --check`**G9**,扫描根已扩到 `MG_State/GLState` | **75 个 mutator 全映射、无陈旧行**45 条 render-state 答案由 `RenderState.cpp` 推导并吻合,另 10 条 (mutator, bit) 答案由各自在 `Tracker.h` 里的快门推导,**0 COARSE / 0 UNDECIDED** |
| `gen_pipe_dirty_surface.py --self-test` | **21 个阴性对照全部触发**,正对照 OK |
| `RenderStateSpans` / `Residual` / **`VertexInputEmit`G6** / **`ResourceEmit`** | **48/48** |
| `p3a_untouched_regions.sh --self-test`(**G5** 自己的阴性对照) | rc 0 —— 3 个正对照 + **2 个阴性对照**(扰动 `ClearBufferPool``FlushPendingRangesNow` 各被点名) |
**G15(完整 `gl44to46` caselist,约 56,271 例,两台设备)**P3a 是五个架构边界之一,按 `ROADMAP.md:34` 排在 `dev` 合并时、**关键路径之外**`MOBILEGL_PIPE_POISON_OMIT` 的清扫(§14 那 8 条静态过近似填充行)搭同一次 caselist 运行。
## 17. D.2 的"重键前红"证据:buffer 类
`ROADMAP.md:7` 要求每个门必须能因它存在的理由变红。P2 已经为 VAO 类留下了证据(§14 末尾);P3a 为 **buffer 类**留下这一份,取自 `p3a/contract` 树上、package C 还没注册 `MGPipeResourceOps` 的时刻,`ctest --test-dir build-verify -R HandleRecycle`
```
16/60 Test #2488: DirectGLES.HandleRecycle.Handles.HandleRecycleScenario
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ......***Skipped
22/60 Test #2494: DirectVulkan.HandleRecycle.Handles.HandleRecycleScenario
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ......***Skipped
28/60 Test #2500: DirectGLES.HandleRecycle.Legacy.HandleRecycleScenario
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ...... Passed
40/60 Test #2512: DirectVulkan.HandleRecycle.AbaControl.HandleRecycleScenario
.ABufferAtARecycledAddressDoesNotInheritItsPredecessorsContents ...... Passed
100% tests passed, 0 tests failed out of 60
```
读法与预期逐条吻合:**`.AbaControl` 通过**——那个臂**断言的就是被污染的内容**(把句柄身份打掉之后,复用地址上的新 buffer 读到前一个的字节),这正是"重键前红"的正面证据;**`.Legacy` 通过**pre-handle 守卫拦得住);**`.Handles` 可见地 skip** 并写明缺什么,而不是一条消失的测试。package C 注册 `MGPipeResourceOps` 之后这两条 skip 转为断言,最终树上 `HandleRecycle`**60/60**(§16 第 1 部分)。完整日志见 `~/w7/notes/p3a/p3a-results/handlerecycle-before.log`(另有 `-verbose.log`,含 22 行逐臂判决)。
## 18. 验证轮在真实流量下找到的三个缝隙缺陷
句柄路径第一次对着**真的发射器与真的 applier 本体**跑起来,是 espryt 的验证轮。默认位图那条 integration 通道当时留下 13 条红,其中 11 条是真的,掩码二分把它们干净地劈开(`0x7f`0 条;`0xff`(只开位 7):5 条;`0x1ff`:11 条)。三个缺陷都在本包的文件清单内,都有实测的前后对比。**这一轮是承重的**——三个都是别的门看不见的形状:
| # | 缺陷 | 位置 | 表现 |
|---|---|---|---|
| **F1** | vertex buffer 条目按属性的 **GL 绑定点**解析,而不是按**属性下标** | `MobileGL/MG_Backend/DirectGLES/Managers.cpp` 的查找助手(`VertexBufferForBindingIndex``VertexBufferForAttributeIndex`)与它的调用点 | `MGPVertexBuffer::BindingIndex``MGPVertexAttribWire::BindingIndex` 是两个不同的数:Espryt 消费的是**已解析**的属性,所以 client 把 `set_vertex_buffers` 发成"逐属性槽一条"`BindingIndex == 属性下标`;另一个是 `glVertexAttribBinding` 绑上去的 GL 绑定点,而那个视图这条臂从不读。两者在所有 `glVertexAttribPointer` 配出来的属性上恰好相等——这就是别的场景全绿的原因;它们只在 `KHR-GL43.vertex_attrib_binding` 的题材上分叉,于是 `VertexAttribBindingScenario` 的 6 条读到禁用/零默认值。单这一个修完 6 → 3 |
| **F2** | ensure 路径去问一个**没有任何内容调用会刷新**的描述符,来判断影子里有没有字节 | `Managers.cpp``EnsureBufferResourceForHandle` | `MGPResourceDesc::HasDefinedContent` 陈述的是**上一次 `resource_respecify` 当时**的事实,此后 `NotifySubData` / `NotifyFlushMappedRange` / `NotifyContentWrite` / `MarkGpuWritten` / `LandBytesIntoResidentStore` 都只改前端的 `m_hasDefinedContent`、不重发描述符(逐 `glBufferSubData` 重发是新线上流量,设计上禁止)。于是在语料里最常见的惯用法 `glBufferData(size, NULL)` + `glBufferSubData(data)` 之后,句柄臂用**应用的字节**配上**applier 的描述符**,重定义出一个"已声明为当前"的空 store,应用的字节被丢掉且没有任何诊断——**静默的错像素**,不是崩溃。改为在持有前端对象时读 `HasDefinedContent()`(无对象的纯句柄排水仍读描述符)。关掉了全部 4 条 `XfbCaptureBufferReuse``LargeArenaAdoption.RespecifiedIndexArenaKeepsVaoBinding`,即所有 `0xff` 红 |
| **F3** | 惰性生成的 twin 从不发布影子基址,于是 fp64 收窄拒绝每一个 draw | `Managers.cpp``EnsureBufferResourceForHandle` | `GLESBufferResource::hostBytes` 是那些**不持有**前端对象的读者读的(回读队列排空、flush-range 的 kill-switch map 臂、`SyncFloat64AttributeAsFloat32ByHandle`)。`ResourceCreate` 是 no-optwin 因此是**惰性**建的,第一次 `resource_respecify` 找不到 twin,把基址丢在地上;对"`glGenBuffers` + `glBufferData(size, data)` 之后再无内容调用"的普通静态顶点数组,`hostBytes` 就此终身为 null。整份重传没事(它走另一个取基址的路径),fp64 收窄却因 `sourceBase == nullptr` 直接失败 → Adreno workaround 禁用该属性 → 着色器读到 `(0, 0, 0, 1)`。修法是在已经算出基址、且每个用到该 store 的 draw 之前都会跑到的那一处补发布,且**只对非采纳资源**(采纳臂更早返回,所以 adopted store 的 `hostBytes` 仍为 null、字节仍走 `persistentPtr`)。关掉最后 3 条 `VertexAttribBinding` |
三个修完之后那条通道在 espryt 自己的树上是 920/920,最终集成树上是 §16 的 958/958。诊断用的五行临时 `MGLOG_E("TRACE …")` 在任何提交之前就已删除。
同一轮里的两条方法学记录。**包的 worktree 里 `tools/trace_replay/fixtures` 是 LFS 指针**131/133 字节的 stub 对 `~/w7/pipe` 的 803 MB),对着指针文件跑 retrace 会报 `passed 2 / 79`、每条 Minecraft 用例 `ssim=None`,读起来跟"整体回归"一模一样——那是**假红**,建树脚本不物化 fixture。**base-instance 对照在 llvmpipe 上要先关掉原生 base-instance 才可证伪**llvmpipe 暴露 `GL_EXT_base_instance`,原生门因此为真、`fetchBaseInstance` 被钳成 0,而属性偏移仿真才是 `MGPipeApplierState::VertexFetchBaseInstance` 在整个后端里**唯一**的消费者。把两个原生门都强制为假之后,对照两个方向都成立:三个调用点在时 10/10 绿,抠掉调用点时 8/10。**这条门是 `VertexAttribBindingScenario``BaseInstanceMovesTheInstancedArraysStartElement``BaseInstanceLeavesPerVertexArraysWhereTheyWere` 两条,不是 `DrawParametersScenario` 的八条**——后者观察的是 `gl_BaseInstance` / `gl_DrawID`,来自 `SetCurrentBaseInstance`,与取数偏移正交,两个方向都绿是对的。
## 19. Track H 单位成本普查(`ROADMAP.md:49` 的判据)
**日历口径:P3a 全部四个包 = 1 天**2026-09-08)。contract/wire、client、espryt、gates 四个包各一轮实现加一轮对抗性评审加至多两轮返工,连同集成与本地五部分门,全部落在同一个日历日内。对着 `ROADMAP.md:66`**27 天绊线**是 **1 / 27**,估时是 18–23 天,**没有超出估计的 50%**,不触发重定基线,也不需要 `ROADMAP.md:70``inproc` 证伪数字。§14 那条"Track H 单位成本的日历口径:未记录"到此补上一半——P3a 自己的口径有了,P2 两个包的仍然没有。
**diff 规模**`git diff --stat 5cb826b0 3e298c9a`,区间内 44 个提交):
```
37 files changed, 9273 insertions(+), 230 deletions(-)
```
**memo 账**`ARCHITECTURE.md:363` 那份 21 条普查;P2 付了 11 条直接删除里的全部 11 条与 7 条重键里的 2 条):
| 类别 | P3a 付了什么 |
|---|---|
| 直接删除(普查 11 条里的最后一条) | `ConvertedVertexStreamKey``sourcePin``ConvertedFloat64Stream::sourceLifetimeId` |
| 句柄臂上另外退役的 twin 成员 | `m_hasSyncedConfigVersion``m_syncedConfigVersion``m_syncedAttributeVersions``Array<VertexAttributeVersion, 32>`,本阶段最大的一处)、`m_syncedIndexBufferVersion``m_syncedIndexBufferObject`(裸前端指针,堵回绕洞的那个身份补丁) |
| 重键 | VAO twin 的索引槽 memo(回绕 `Uint16` + 裸 `BufferObject*` → 一个 server `Serial`);`ResolvedDrawBuffers``configVersion``{elementsHandle, elementsSerial, buffersSerial}``Entry``iboFrontend` 各加一个 `MGPipeHandle`);twin 的同步门(config version + 32 组逐属性版本 → `{elementsHandle, elementsSerial}` + `VertexBuffersSerial`);`ConvertedFloat64Stream`(前端 lifetime id + change serial → buffer `{slot, gen}` + applier `Serial`);`GLESBufferResource::syncedChangeSerial` 从镜像 `BufferObject::GetChangeSerial()` 改为镜像 applier 的 `Serial` |
| **保留**`MOBILEGL_PIPE_LEGACY_MEMOS` | 上面每一条"退役"都只是**句柄臂上**的:成员仍在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下编译(`MobileGL/MG_Backend/DirectGLES/Managers.h:1109-1130``:1067-1075`),pull 构建强制该开关 ON,所以 `sizeof` 一处不动——**G1 的 0/0/0/0 就是这条的度量** |
| **保留,且计划里的"grep 为空"不可达** | `g_pendingFetchBaseInstance` / `SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance` 与它的三个 scope`MobileGL/MG_Backend/DirectGLES/Managers.h:1189-1200``MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp:5293`)。句柄臂改由 `MGPipeApplierState::VertexFetchBaseInstance` 供给,但**真删会从 pull 构建移走两个符号**,是直接的 G1 破坏;所以计划里那条"`grep` 结果为空"在 P3a **不可达**,每一处残留命中都在一个 `MOBILEGL_PIPE_LEGACY_MEMOS` 臂里。这是记录在案的偏差,随 pull 路径在 P13 退役 |
| **保留(D-K** | `PipeResource::m_backend``SetBackendResource``ReleaseBackend``BackendBufferResource`:push 下不再被写,删除会移动 pull 构建里的 `sizeof(BufferObject)`,同样随 P13 退役(`ARCHITECTURE.md:286` |
## 20. 设备配对 A/B、MC 26.3 的 p99、DriverBench(记录项)
协议与 §10 完全相同:reboot-clean、同热窗口、按 `tools/device_bench/pin_device.sh` 定频(大核 1.96 / 小核 1.55 GHz、GPU 拉满、40 °C 门,每次运行前后各 `check` 一次,DRIFT 的那次作废重跑)、两臂背靠背、一次一台设备、`benchmark.json``frameCpuTimesMs[]` 尾 200 帧在主机侧算 p50/p99p99 用 nearest rankp50 按设备自己的中位数规则)。臂 = {pull APK, push APK} × {`--benchmark-no-finish`(主臂,P3a 问的是 CPU, finishGPU 时间没动的 sanity)} × 4 用例 × 2 后端。用例:`improved-transparency-minecraft-26.3``minecraft-1.21.4-rd12-odinlite-in-world``minecraft-1.21.4-fabric-sodium-in-world``minecraft-1.21.1-neoforge-create-instancing-in-world`(一个 `coherent_as_flush` fixture)。**`minecraft-1.21.1-neoforge-create-indirect-in-world` 排除在外**,理由与 P2 相同:它在 `dev@81b17c0b` 的基线 APK 上就在两台设备上失败(§5.5、`ROADMAP.md` 开放问题 17),不是本分支的回归;它留在桌面 SSIM 语料里。同一批运行里读出六个 memo 门的 hit/miss、`stage-*` 字节类与 **`mpr`**。
**读数纪律,必须写在数字上面而不是下面**`CallClass::AccessorCalls` 是约 10 个热入口上的**静态计数**(`MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份站点清单本身就是契约),所以一个只把读**挪了位置**的改动会拿到更低的 `acc/draw` 而并没有减少工作量。带头看的应当是**六个 memo 门的 hit/miss** 与 **CPU 时间序列**;引用 `acc/draw` 必须同时复核那些 tally 常量。
**两机配对 A/BD.4.2),三臂**APK 都是 `3e298c9a` 的 Release trace 构建(`wsl_build_trace_apks.sh``.text` 9.4 MBdebug 签名);pull 臂 = pull 库;`P2` 臂 = 同一个 push APK 跑 `--env MOBILEGL_PIPE_PUSH=0x7f`(只开 P2 的七个子系统,P3a 关);`P3a` 臂 = push APK 默认掩码 `0x1ff`。协议同 P2reboot-clean、`pin_device.sh` 三次校验、每次运行前后 check、`--benchmark-repeats 3 --benchmark-tail-frames 200`、runner 的 best-of-3、`MOBILEGL_PIPE_STATS_PERIOD=120`);表取 `--benchmark-no-finish` 臂,单位 ms/帧,p50 括号内为相对 pull 的增量。
| 设备 | trace | 后端 | pull p50 | P2 臂 p50 | P3a 臂 p50 | pull p99 | P3a p99 | 备注 |
|---|---|---|---|---|---|---|---|---|
| 小米 Adreno 830 | `improved-transparency-minecraft-26.3` | Espryt | 10.810 | 11.485+6.2% | 11.697**+8.2%** | 25.457 | 26.322 | finish 开:10.789 → 11.705+8.5% |
| 小米 | 同上 | Magma | 10.675 | 11.473+7.5% | 11.459**+7.3%** | 25.014 | 26.035 | finish 开:10.710 → 11.486 |
| 小米 | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 8.220 | 9.117+10.9% | 10.650**+29.6%** | 21.895 | 24.487 | finish 开:8.241 → 10.656 |
| 小米 | 同上 | Magma | — | — | — | — | — | 三臂都在启动数秒后 SIGABRTscudo map error,见下),既有问题 |
| 小米 | `minecraft-1.21.4-fabric-sodium-in-world` | Espryt | 1.292 | 1.349+4.4% | 1.382**+7.0%** | 2.418 | 2.443 | |
| 小米 | 同上 | Magma | 0.485 | 0.504 | 1.010 | 1.469 | 2.187 | 亚毫秒帧,pull 自己两模式相差 2×(0.485 / 0.984),噪声,不读 |
| 小米 | `minecraft-1.21.1-neoforge-create-instancing-in-world` | Espryt | 944.4 | 945.2 | 949.9+0.6% | — | — | fixture 只有两帧(p50 = mean),是正确性 fixture 不是基准 |
| 小米 | 同上 | Magma | 1003.9 | 1019.5 | 1015.6+1.2% | — | — | 同上 |
| Oppo Mali | `improved-transparency-minecraft-26.3` | Espryt | 9.741 | 10.570+8.5% | 10.542**+8.2%** | 26.312 | 27.022 | finish 开:9.625 → 10.698+11.1% |
| Oppo | 同上 | Magma | 8.162 | 8.920+9.3% | 8.973**+9.9%** | 22.851 | 23.638 | finish 开:8.120 → 9.064 |
| Oppo | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 9.938 | 11.009+10.8% | 12.963**+30.4%** | 27.137 | 29.765 | finish 开:9.944 → 12.852 |
| Oppo | 同上 | Magma | 8.001 | 8.933+11.6% | 10.161**+27.0%** | 24.723 | 27.355 | finish 开:7.718 → 10.226 |
| Oppo | `minecraft-1.21.4-fabric-sodium-in-world` | Espryt | 1.896 | 1.770 | 1.841(−2.9% | 2.993 | 3.079 | 亚 2 ms 帧,噪声内 |
| Oppo | 同上 | Magma | 0.399 | 0.431 | 0.437+9.5% | 1.206 | 1.409 | 亚毫秒帧 |
| Oppo | `minecraft-1.21.1-neoforge-create-instancing-in-world` | Espryt | 1075.1 | 1045.1 | 1060.1(−1.4% | — | — | 两帧 fixture |
| Oppo | 同上 | Magma | 758.3 | 731.0 | 760.0+0.2% | — | — | 两帧 fixture |
所有入表运行前后 `pin_device.sh check` 都是 PINNED(小米 rd12/Magma 崩溃后 GPU pwrlevel 被重置,其后的 sodium/create-instancing 行两臂同状态)。
**读法。** (1) **P2 的边界在 Release 下的真实代价是 +612%**`0x7f` 臂),两机两后端一致,比 -O0 表的 +8–18% 小但同量级;(2) **P3a 在 26.3 与 sodium 上几乎不再加价**P3a 臂与 P2 臂在 26.3 上相差 −0.1 +2 个百分点),**但在 rd12 上把差距从 +11% 推到 +2730%**——rd12Odin Lite 世界)每帧的 VAO/buffer 绑定切换远多于 26.326.3 的 1350 draw/帧大多复用同一 VAO),每次切换都走一遍 `set_vertex_buffers` 构造 + `ContentHash` + applier 记录 + Espryt 侧逐属性走查;Magma 上没有句柄消费者也多 15 个百分点,说明 client 侧发射本身就是大头;(3) **MC 26.3 在 Adreno 上的 p99**`ROADMAP.md:19` 点名的那个数):pull 25.46 ms → P3a 26.32 ms+3.4%),finish 开 25.48 → 26.29;对着 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms)仍在 21–26 ms 档,没有回到采纳前的形态——按口径记录,不判门;(4) `mpr`map-persistent-roundtrips,按窗口累加):26.3 两机都是 8(首窗 4,之后两次 2——都是 ≥16 MiB store 定义时的采纳),sodium 1rd12 与 create-instancing 0P2 臂上恒 0(子系统关)——G10 在设备上成立;(5) `CreateVertexElements` 每帧字节数:统计行没有这一类(`vtxc` 是 client 数组),**未测**,留给 P4a 给汇总行加类;(6) 计数器(`acc/draw`、六个 memo 门、`resid=``csom/csob`)在 pull/P2/P3a 三臂间逐字相同——它们数的是代码路径,P3a 没有改它们的定义。
**小米 rd12 + Magma 的崩溃**:三臂(含 pull)都在启动后数秒 `SIGABRT``scudo::reportMapError``remapImpl``scudo_calloc``libMobileGL.so`0x818b14 / 0x7e2c44,已剥符号),当时 MemAvailable 6.1 GB——一次巨大或负尺寸的 calloc,在 Adreno 830 + Magma + 这条 fixture 上;pull 库与 P3a 前的 Magma 路径符号一致,所以是**既有 bug**,不入 P3a 账,已开独立任务(先符号化再修)。Oppo/Magma 与小米/Espryt 上同一 fixture 正常。
**harness 与设备陷阱(本轮新增)**Oppo ColorOS 的安装确认页是 `topResumedActivity=…InstallGuideActivity``mCurrentFocus` 显示 systemui 窗口,按它 grep 永远匹配不到),首次安装不点会等到 harness 超时;在 streamed install 进行中点它会让设备从 adb 掉线几秒(那一轮的三次重复全失败,需要重跑);小米 32 次运行后立即 reboot+pin 必因热钳失败(大核钳 1689600 或 GPU 读 1050 MHz),要等 cpuss-0-0 <42 °C;一次崩溃会把 GPU pwrlevel 范围重置成 2..5。
对照的既有基线是本文件 `:87`——`dev` 上 MC 26.3 在 Adreno 上把 ≥16 MiB 可变 store 采纳为 coherent persistent map 之后的 p99 163→21 ms、稳态 40→115 fps、省 ~400 MB。P3a 的 `AcquirePersistentMap` 一个语句都没动(`ARCHITECTURE.md:474` 的 D-B4),`FlushPendingRangesNow` 的三档排水在 G5 的逐字节集合里,所以这条 p99 是"没动过的东西是否真的没动"最直接的读数。
**DriverBench(桌面 llvmpipe / lavapipe`~/w7/notes/tools/wsl_p3a_bench.sh`Release240 帧,每臂 5 次重复取中位数;pull 库 = `build-linux`push 库 = `build-push`,都是 `c20e2f2b`**`ns_per_op`
| 臂 | `mc_vanilla_draw`ns/draw | `mc_state_toggle`ns/开关对) | `mc_pass_switch`ns/pass |
|---|---|---|---|
| native | 4759 | 22715 | 438175 |
| Espryt pull | 5115 | 23287 | 435815 |
| Espryt push,默认 `0x1ff` | 6162 | 24496 | 435975 |
| Espryt push`MOBILEGL_PIPE_PUSH=0x7f`(只开 P2 子系统) | 5463 | 24576 | 436031 |
| Espryt push`MOBILEGL_PIPE_PUSH=0` | 5693 | 24465 | 436598 |
| Espryt push,位 63(无 CSO 内容寻址) | 6156 | 24856 | 435276 |
| Magma pull | 17099 | 32612 | 436748 |
| Magma push,默认 `0x1ff` | 17709 | 33537 | 437314 |
| Magma push`0x7f` | 17287 | 33550 | 436458 |
| Magma push`0` | 17739 | 33800 | 440446 |
| Magma push,位 63 | 17729 | 34222 | 436465 |
分解(`mc_vanilla_draw`,ns/draw;T2 在本阶段定义为 `0x7f` 臂 = P2 的边界):
| | Espryt | Magma |
|---|---|---|
| **T1** = push(`0x1ff`) pull(整个边界) | **+1048**pull 的 +20.5% | **+611**+3.6% |
| **T2** = push(`0x7f`) pullP2 的边界) | +349 | +189 |
| **T1 T2 = P3a 自己加的** | **+699** | **+422** |
| push(`0`) − pull(P1 残余填充,全部拉取) | +578 | +640 |
| 位 63 push(CSO 内容寻址净值) | 6(≈ 0) | +20(≈ 0 |
| blend-togglens/开关对) | +1209+5.2% | +925+2.8% |
| pass switch | +160(≈ 0 | +566(≈ 0 |
读法(记录项,不设门):P3a 的 buffer/VAO 句柄路径在**每个 draw** 上多花 Espryt 699 / Magma 422 ns——这是 P3a 到目前为止最大的一笔边界成本,来源是每 draw 的 vertex-input 发射(`set_vertex_buffers``MGPVertexBuffer[]` 构造与 `ContentHash``bind_vertex_elements`)、applier 的记录写入,以及 Espryt 侧 `SyncToBackendFromApplier` 对记录的逐属性走查;Magma 没有句柄化的 VAO 消费者,它多出的 422 ns 全是 client 侧发射 + applier 记录(P7 之前的纯开销)。与 P2 相比,T2 本身从 P2 实测的 +322/+345 变为 +349/+189(同量级;Magma 的差别是 P2 数字来自同批次内的相对比较)。这个数字进优化阶段的清单:候选是 vertex-input 发射的 per-draw 抑制(同一 VAO/同一 buffer 集合的连续 draw 不重发;现在 `ContentHash` 命中时仍走一遍构造)与 applier 记录的就地更新。
`DriverBench` 的 T1/T2 口径与 §11–§12 相同,只是分档换了:**T1** = `ns_per_op(push, 默认位图 0x1ff) ns_per_op(pull)`(整个边界的每 draw 代价),**T2** = `ns_per_op(push, MOBILEGL_PIPE_PUSH=0x7f) ns_per_op(pull)`P2 的边界本身),于是 **T1 − T2 恰好隔离出 P3a 加了什么、删了什么**`mc_state_toggle` 作为非 P3a 的对照并列发布(一次 blend 开关既不碰 buffer 也不碰 VAO,它不该动)。`DriverBench` 走独立的 `build-bench`(门的构建配了 `-DMOBILEGL_BUILD_BENCHMARK=OFF`),并且必须经 `DRIVERBENCH_EGL_LIB` 显式 `dlopen` 一个 provider、**不得靠 `LD_LIBRARY_PATH` 遮挡**——glvnd 系统上一个裸 `libEGL.so.1` 会解析到 Mesa/llvmpipe,即在基准底下把 GPU 悄悄换成软件光栅器。**不钉上限、也不强制上限**(规则 (a));数字发布出来,让 P4a 自己决定要不要钉。
## 21. 决定与遗留判定
**整体 diff 终审(`5cb826b0..3e298c9a`)与其返工(`3e298c9a..c20e2f2b`12 个提交)**:终审在四个包各自过审之后又抓出两个跨包接缝——(1) client 为每个 VAO 铸的 `VertexElementsCso` 槽只有 Espryt 的死亡通知会释放,Magma 不装 `StateObjectDeathOps`,默认掩码下每个 VAO 泄漏一个槽和约 1.3 KB 的 applier 记录,过 65536 个槽后 `create_vertex_elements` 永久 `Fatal{ProtocolCorruption}`;修法是 `~VertexArrayObject` 走后端无关的死亡路径(`MGPipeEmitVertexElementsDestroyAndFree`:先删记录、再发通知、最后释放槽,Espryt 的通知退化为可重入的第二条路径),泄漏测试在 DirectVulkan 上修前 48 轮 live 2→50、修后不增长;(2) `FlushPendingRangesNow` 位于只在 pull 构建里编译的 `#else`,G5 的文本哈希覆盖不到 push 构建真正跑的 `FlushPendingRangesFrom`——裁定如下。
> **ID-15(取代 ID-13 里"定义在任何 `#if` 之外"那句)。** `Managers.cpp` 把三层 flush 排水梯各带一份、每个预处理臂一份,任一构建只编译其中之一:`#if MOBILEGL_PIPE_PUSH` 下的 `FlushPendingRangesFrom` 是 **push** 构建跑的(legacy 调用点与 `Ops_H_Readback` 都到它),`#else` 里与 `5cb826b0` 逐字节相同的 `FlushPendingRangesNow` 是 **pull** 构建跑的;push 构建根本不编译 `FlushPendingRangesNow`。转发函数不可行:G5 的提取器不认预处理,一个转发的 `FlushPendingRangesNow` 会让同名出现两个定义、门直接退出 2(门跑不了)而不是比对。因此**两个名字都是 G5 行**:十一个函数,pull 梯对着 P3a 基线比、push 梯对着钉在 `3e298c9a` 的 sha 比;两条梯都不许漂,也不许彼此漂而门不响。
> **M-3 裁定:fp64 顶点数组收窄在 handle 臂上少做 D-N 十一处 `SyncGpuWrites` 里的一处,这是 P3a 的裁定而非疏漏。** D-N 的措辞是"不把这些站点*搬*离前端";这一臂不是搬走了它,而是**根本做不了**——`SyncFloat64AttributeAsFloat32ByHandle` 手里只有句柄,那次调用需要前端 `BufferObject`,而 server 没有回到前端对象的反向映射正是设计本身(`ARCHITECTURE.md` §4.2),不是设计的缺口。保住这个站点的两条路各会破坏 D-N 或 D-J 保护的东西:句柄→对象的映射正是拆分要去掉的东西,而 client 在每个 draw 上急切拉回字节是新行为和新成本。**影响面,写明以便日后核对**:默认掩码下,一个 64 位顶点数组、其*源* buffer 被 shader 写过且尚未回读,handle 臂收窄到的是旧字节、legacy 臂是新字节。就这一种:fp64 顶点数组、由 shader 写的 buffer 喂、中间没有显式回读。其它属性类型不走这条路,持久映射的源另行排除(memo 从不信任它)。**P8 把拉取搬到对象所在的 client 侧即关闭**;在那之前这是默认掩码下两臂唯一的行为差异。(同一段文字在 `MobileGL/MG_Backend/DirectGLES/Managers.cpp` 的站点上;带 adopted 源的 `DoublePrecisionScenario` 用例未加——它需要一条新工作负载而非既有用例的参数,留给 D 包。)
新的"看穿 GL API"接缝:`MG_IntegrationTest/Harness/PipeSlotPeek.{h,cpp}` 直接读 client 的槽分配器(泄漏测试的依据),与 `BackendCapsPeek` 并列。
**CI 独有的 teardown 崩溃(`DirectVulkan.Verify.CrossFrameBufferScenario.{Vertex,Index}CopyBufferSubData``double free or corruption`,本地 30 次不复现)**ASan 定位为 `exit()` 时的静态析构顺序 UAF——命名空间作用域的 `gPipeInputs` 持有 `SharedPtr<VertexArrayObject>`,其析构链 `~VertexArrayObject → ~BufferObject → MGPipeEmitResourceDestroyAndFree → MGPipeSlotAllocator::FindByLifetimeId` 读的是已被 `~MGPipeSlotAllocator` 释放的哈希表(`MGPipeSlots()` 是首个 buffer 时才构造的 Meyers 单例,先于 `gPipeInputs` 析构)。全部 13 个用例 × 两个后端都走这条链,只有释放后的表恰好还能解析出句柄时 `Free()` 才写坏堆——CI 的分配器布局中招、本地没有;原生复现钥匙是 `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`(c20e2f2b 上恰好那两例失败,修后 0)。修法:`MGPipeSlots()``MGPipeResourceTrackerInstance()``g_applier` 改为永不析构的单例(堆上构造、退出时有意泄漏),一并覆盖 C-1 返工新增的第二条到达路径(`~VertexArrayObject``MGPipeEmitVertexElementsDestroyAndFree`)。 落地为三个提交:`d54ec57a`(三个单例永不析构)、`6515c8e6`**从源头收口**`gPipeInputs``g_snapshot``g_readScratch``probe` 这四个持有前端 `SharedPtr` 的静态 `PipeInputs` 改为退出时泄漏的存储——树里只有它们持有前端对象,于是没有任何前端析构函数会从 exit handler 里跑;这是 `Init.cpp`/`GlobalObjects.cpp` 已有的规则)、`fde5fda3`(其余四个 MGPipe 单例永不析构,作为纵深防御——C-1 返工把 `MGPipeVertexInputEmitterInstance()` 放上了 `~VertexArrayObject` 的死亡路径,ASan 在 6/6 代表用例上抓到 `VertexInputEmit.h` 的 heap-use-after-free)。证据:开着钥匙 `integration-verify` 844/844、push 臂 `integration-gpu` 966/966ASan 10/10 干净;整条 verify 通道在 ASan 下只剩 `IntegerBorderColorScenario` 一个与本阶段无关的既有测试 bug(2×2 上传的 4 字节源配 `UNPACK_ALIGNMENT=4` 的栈越界读),另开任务。
**`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` 那 186 条过滤:P3a 跑,跑一次,在 push 下跑,不进比对器。** §14 里 P2 把这条推给 P3b,理由是"P2 在 buffer/回读方向什么也没改";**P3a 改的恰恰是这个过滤的题材**——它是 `FlushPendingRangesNow` 第一档的 kill switch`MobileGL/MG_Backend/DirectGLES/Managers.cpp:1071``:1331`),而 P3a 重写了那个函数的调用方。所以门里加了具名的一步 `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1 ctest --test-dir build-push -L integration-gpu`,结果 **958/958**(§16 第 3 部分)。**放进 5–10× 的比对器里跑仍然推迟到 P3b**;这次是把拆开的两半都写下来,而不是让 CI 里那句注释再吊一轮。
**`g_uploadRing` 不被重置的不对称:原样保留,记为 `dev` 侧跟进。** `OnBackendContextDestroyed``MobileGL/MG_Backend/DirectGLES/Managers.cpp:2481`)对 `g_uboRing``g_unpackRing``ResetRingForNewContext``:2492-2493`),**不对 `g_uploadRing` 调**`RingAvailable``:3186`)在首次使用时按 `contextGeneration` 自愈,所以它是良性的。P3a **故意不在飞地顺手修**`ROADMAP.md:98` 那条纪律:拆分不得借机修不相关的 `dev` 问题),把它作为 `dev` 侧跟进项留在这里。
**`FlushPendingRangesNow` 定义一次,句柄臂另有一条自己的档梯。** G5 的第十项与 G1 的空 resize 集之间有一处真冲突:就地重构那几个 helper 会 resize 五个 pull 符号(`FlushPendingRangesNow +14` 在内),G1 不允许。落地形状是:**`FlushPendingRangesNow` 只定义一次、对 `5cb826b0` 逐字节相同**(在 `#if MOBILEGL_PIPE_PUSH``#else` 臂里,`Managers.cpp:1316`,调用点 `:1723``:2906`),句柄臂另有一个 `FlushPendingRangesFrom(twin, hostBase, size)``:1051`,调用点 `:1721``:2044``:2766``:2904`)。**在 P3a 接受档梯在 push 构建里被复制一份**(与 respecify 核心已经用过的形状相同),代价是两条梯子会漂移;对冲是 `CrossFrameBufferScenario` 的十三条加 `StreamedArenaScenario` 的两条 recycle 用例,以及 §20 的 MC 26.3 p99。**它随 pull 臂在 P13 退役**`ARCHITECTURE.md:367`)。共享模板加访问器接口的方案被否决:它同样 resize pull 符号(G1)。
**`MG_Test/Buffer/BufferTest.cpp` 的 fixture 只 scope 了一半的表。** push 构建下后端在 bring-up 同时装 `BufferBackendOps``MGPipeResourceOps`,而 `ScopedBackendOps` 只 scope 前者,于是 86 条 `BufferBackendOps` 分发用例里有 **26 条**被路由进了 pipe(症状是 `EnsureGpuResidentStorage()` 返回 `false`、mock 从没被调用过)。这是**合并缝**的典型形态——两个分支各自绿、合起来红:espryt 那边没有东西经 pipe 发射,client 那边没有东西注册表。集成者落了单 scope 的修法(fixture 现在像 `MG_Test/Pipe/ResourceEmitTest.cpp` 的 `ApplierGuard` scope applier 那样,保存 / 置空 / 恢复 pipe 表):**修完 86/86,整套单元 1619/1619**。它不削弱任何东西——那 86 条是 `BufferBackendOps` 的分发测试,pipe 侧的分发有 `ResourceEmitTest` 自己的覆盖。**跟进(不属于本阶段)**:给这个 fixture 一个 pipe 形的 mock,让同样的 86 条断言在句柄路径上再跑一遍。
**逐 dirty 位的触发率与每 draw payload 直方图:仍然未测。** §13 记的那两条在 P3a 也没有补上——没有任何一份 P3a 的结果文件报过位 5 / 9 / 10 收窄前后的 `FireCount` / `WalkCount`,也没有报过 24 桶的直方图(它仍然只在 teardown 的 JSON 里输出,而 trace app 从不到达那次 teardown,§5.1)。计划把这两项列为"虽然没人要也要发布"的项,**P3a 没有做到,如实记在这里**;补法与 §13 写的一样(给 `FormatWindowLine` 加一行,或从一个场景里经访问器读)。
**峰值 RSSpush vs pull79 例 retrace):工具不报,所以没有数。** `~/w7/retrace_gate.py` 只有五个参数(`--tree --lib --out -j --only`),代码里没有任何 `rss` / `maxrss` / `getrusage` 引用。这个数原本是用来盯第七张 slot 表泄漏的——一个没人销毁的 buffer 会永远漏掉它的 twin,而这对每一个正确性门都不可见;**这一轮拿不到它**,要拿必须先给那个工具加测量。对冲仍在:`ResourceDestroy` 是从 `~BufferObject` **无条件**发射的、不是靠清扫,顺序(先发射、后 `MGPipeSlots().Free`)由 `HandleRecycleScenario` 的三个臂把关(§16、§17)。