From a79a0af69de8c6c53ef985dd23be7eab8cd17641 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Wed, 16 Sep 2026 23:08:39 -0400 Subject: [PATCH] [Docs] (Disaggregated): consolidate the five documents at the P5b close - drop superseded narrative, the -O0 device tables and process history, renumber MEASUREMENTS by phase, correct the catalogue and emit-table counts to the head, and record the Redmi pin profile --- .../PersistentCoherentMapScenario.cpp | 2 +- docs/Disaggregated/ARCHITECTURE.md | 631 ++++++------ docs/Disaggregated/CURRENT_STAGE_PROGRESS.md | 113 +- docs/Disaggregated/MEASUREMENTS.md | 964 +++++------------- docs/Disaggregated/README.md | 69 +- docs/Disaggregated/ROADMAP.md | 186 ++-- .../devices/pin-verification-2026-09-07.md | 427 +------- 7 files changed, 713 insertions(+), 1679 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp index 7ee9be12..b0f79fb0 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/PersistentCoherentMapScenario.cpp @@ -8,7 +8,7 @@ // // Scenario - A PERSISTENT|WRITE|COHERENT MAPPING, WRITTEN THROUGH WITH NO GL CALL ANNOUNCING IT. // -// Target C of P5's reduced path, spelled out in full at docs/Disaggregated/ARCHITECTURE.md:500: +// Target C of P5's reduced path, spelled out in full at docs/Disaggregated/ARCHITECTURE.md section 12: // map PERSISTENT|WRITE|COHERENT, write through the pointer, MAKE NO OTHER GL CALL, draw, read // back and check. // diff --git a/docs/Disaggregated/ARCHITECTURE.md b/docs/Disaggregated/ARCHITECTURE.md index 0bf56d13..abdebc41 100644 --- a/docs/Disaggregated/ARCHITECTURE.md +++ b/docs/Disaggregated/ARCHITECTURE.md @@ -1,42 +1,42 @@ # MGPipe 设计与架构 -> 本文描述**已决定**的设计。每条决定附一行理由;数字凡有实测的取实测(见 `MEASUREMENTS.md`)。落地状态以 `feat/disaggregated@eec0e836` 为准:P0–P5 已落地;明确标成 P6+ 的仍是后续形状(阶段号见 `ROADMAP.md`)。 +> 本文只写**已决定**的设计,每条决定附一行理由。落地状态:P0–P5b 已落地(代码头 `82683d4a`);标 **P6+** 的是后续形状,阶段号见 `ROADMAP.md`;实测数字见 `MEASUREMENTS.md`;P5 / P5b 的 wire 契约原文在 `MobileGL/MG_Remote/CONTRACT-P5.md`、`CONTRACT-P5B.md`。 ## 1. 边界 ### 1.1 一句话 -`MG_Backend` 已经是一台贴着目标 API 的状态机(Espryt 有逐字节的渲染状态镜像、6 个 twin registry、三条 persistent ring;Magma 有 `SetupDrawSnapshot`、pipeline memo、5 个 `Vk*Manager`)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化**推**过去,后端不再拉 `MG_State::pGLContext`。server 进程因此只装 `MG_Backend` + MGPipe 对象表,不链接 `MG_State`、`MG_Impl`、glslang。 +`MG_Backend` 已经是一台贴着目标 API 的状态机(Espryt:逐字节渲染状态镜像、twin registry、三条 persistent ring;Magma:`SetupDrawSnapshot`、pipeline memo、五个 `Vk*Manager`)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化**推**过去,后端不再拉 `MG_State::pGLContext`;server 进程因此只装 `MG_Backend` + MGPipe 对象表,不链接 `MG_State`、`MG_Impl`、glslang。 -接口不是从 gallium 自顶向下设计的,而是从两个后端自己维护的关键结构反推出来的:`SetupDrawSnapshot` 的字段并集 → `set_*` 组;`DrawTextureSyncKeys` → `set_sampler_views`+`create_sampler_view`+`set_texture_params`;`ResolvedDrawBuffers`/`ResolvedVertexBindings` → vertex elements 三件;`g_syncedRenderStateParameters` → render-state CSO;`UnpackStagingBlock` → `MGPSubData` 的 region 形状;`BufferBackendOps`(7 个 hook,注释自称 `pipe_context` 类比)→ `resource_*` 全族。gallium 是目的地(词汇可读、可迁移),不是推导前提;与 gallium 的十条偏离见 §3.5。 +接口是从两个后端自己维护的关键结构反推出来的(`SetupDrawSnapshot` 字段并集 → `set_*`;`DrawTextureSyncKeys` → sampler view 三件;`ResolvedDrawBuffers` / `ResolvedVertexBindings` → vertex elements 三件;`g_syncedRenderStateParameters` → render-state CSO;`UnpackStagingBlock` → `MGPSubData` 的 region 形状;`BufferBackendOps` 七个 hook → `resource_*` 全族)。gallium 是目的地(词汇可读、可迁移),不是推导前提。 ### 1.2 两张函数指针表 -`MGPipeScreen`(share-group 作用域:caps、resource、persistent map、fence)与 `MGPipeContext`(其余全部:query 命名空间、CSO、`set_*`、对象操作、verb),由 `PipeCalls.def` 经 G1 生成(`MG_Pipe/generated/PipeTables.inc`)。**P0 已落地。** +`MGPipeScreen`(share-group 作用域:caps、resource、persistent map、fence)与 `MGPipeContext`(其余全部),由 `PipeCalls.def` 经 G1 生成(`MG_Pipe/generated/PipeTables.inc`)。 -- 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(`gBackendFunctionsTable`);**null 项已经表示"未实现,前端回退"**,正好就是"这个子系统还没迁移,继续拉取";`MG_Test` 已用替换整张表的方式 mock 后端。 -- 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flow(`pGLContext` 是进程全局,share group 全库无人读取)。 +- 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(`gBackendFunctionsTable`),**null 项已经表示"未实现,前端回退"**,`MG_Test` 已用替换整张表的方式 mock 后端。 +- 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flow。 - EGL 生命周期 8 项与 caps 面留在 `pActiveBackendObject` 的虚函数上(罕见路径)。 ### 1.3 三种形态,一份后端 | 形态 | 表里装的是什么 | 用途 | |---|---|---| -| `monolith`(默认) | backend 自己的函数;`MGPipeCallbacks` 是对 `MG_State` 的直调;`MGHostSpan.Ptr` 指向 client shadow(零新增拷贝) | 出货 | -| `inproc` | 发射器 → 同进程第二个线程上的 applier | CI 形态;同时就是 monolith 的**渲染线程**(把 `PrepareForDraw` 与驱动调用搬离 GL 线程,是本项目手上最大的单一 CPU 杠杆) | -| `spawn` | 发射器 → SPSC shm ring → 另一个进程的 applier → 同一批 backend 函数 | 两进程出货形态 | +| `monolith`(默认) | backend 自己的函数;回调直调 `MG_State`;`MGHostSpan.Ptr` 指向 client shadow | 出货 | +| `inproc` | 发射器 → 同进程第二个线程上的 applier | CI 形态;同时就是 monolith 的**渲染线程**(把 `PrepareForDraw` 与驱动调用搬离 GL 线程) | +| `spawn`(P6) | 发射器 → SPSC shm ring → 另一个进程的 applier → 同一批 backend 函数 | 两进程出货形态 | -唯一 hook 点是 `MG_Backend::Init()`(`MG_Backend/Init.cpp`)里一个 `#if MOBILEGL_BUILD_DISAGGREGATED` 分支:`MG_Config::Transport != Monolith` 时装 `MG_Remote::BackendObject_Remote`,否则走今天的 `switch`。下游 `MG_Impl` 的边界调用点零 `#ifdef`。(分支在 P5 落地;P0 的 `Init.cpp` 尚未含它。) +唯一 hook 点是 `MG_Backend::Init()` 里一个 `#if MOBILEGL_BUILD_DISAGGREGATED` 分支:`MG_Config::Transport != Monolith` 时装 `MG_Remote::BackendObject_Remote`,否则走今天的 `switch`。`MG_Impl` 的边界调用点零 `#ifdef`。 ## 2. 对象模型 -### 2.1 句柄 = `{slot, gen}`(P0 已落地,`MG_Pipe/MGPipeHandles.h`) +### 2.1 句柄 = `{slot, gen}`(`MG_Pipe/MGPipeHandles.h`) - 8 字节 POD,按值走寄存器对;**client 铸造,server 永不返回句柄** → 整份目录零创建 round trip(对 gallium 的偏离 D1)。 -- slot 稠密、**按 kind 分配**(free list + 高水位),server 对象表是数组而非哈希表。与 `IndexGenerator` 无关——后者的 LIFO 名字复用正是句柄要关掉的问题。 -- `gen` 只在 slot 复用时 ++,不在 respecify 时 ++;同一 slot 复用 2³² 次才回绕(1000 fps 逐帧复用约 50 天),debug 分配器断言回绕。 +- slot 稠密、按 kind 分配(free list + 高水位),server 对象表是数组不是哈希表;`IndexGenerator` 的 LIFO 名字复用正是句柄要关掉的问题。 +- `gen` 只在 slot 复用时 ++,不在 respecify 时 ++;同一 slot 复用 2³² 次才回绕,debug 分配器断言回绕。 - kind:`Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context`。 -- 保留句柄:`{0,0}` = null;`{0,1}` of `Framebuffer` = 默认帧缓冲(退役 Espryt 四处 `pDefaultFramebufferInfo->defaultFBO` 身份比较);`ShaderCso` slot 空间的高 1/16 保留给 program pipeline 合成体(`MobileGL/MG_Pipe/MGPipeHandles.h:88-90`)。 +- 保留句柄:`{0,0}` = null;`{0,1}` of `Framebuffer` = 默认帧缓冲(退役 Espryt 的 `defaultFBO` 身份比较);`ShaderCso` slot 空间的高段保留给 program pipeline 合成体(`kMGPipeShaderCsoCompositeSlotBase`)。 - GL name 只以 `GlNameForDiag` 出现在 `MGPResourceDesc` 里,永不做身份、永不进 memo 键或 content hash;`GetLifetimeId()` 留在 client 作 tracker 自己的身份,client 维护 `lifetimeId → slot`。 ### 2.2 两种世代,严格分开 @@ -44,128 +44,112 @@ | | 拥有者 | 回答 | 过线 | |---|---|---|---| | `MGPipeHandle::Gen` | client | "还是同一个 GL 对象吗?" | 是 | -| `MGGen`(`g_bufferMutationEpoch`、`m_textureImageEpoch`、`m_cacheStructureEpoch` 等 12 个后端纪元) | server | "我自己是否重铸了驱动对象?" | **永不**;server→client 只以纹理拉取请求出现(§8.4) | +| `MGGen`(`g_bufferMutationEpoch`、`m_textureImageEpoch` 等后端纪元) | server | "我自己是否重铸了驱动对象?" | **永不**;server→client 只以纹理拉取请求出现(§8.4) | -规范:任何 MGPipe 调用不得要求 client 提供或知晓 `MGGen`;反过来,client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明——过线时要么加宽、要么与 `{slot, gen}` 同行。 +规范:任何 MGPipe 调用不得要求 client 提供或知晓 `MGGen`;client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明——过线时要么加宽、要么与 `{slot, gen}` 同行。 ### 2.3 CSO 与可变对象 | 类别 | 形态 | 对应后端已有缓存 | |---|---|---| -| `VertexElementsCso` | create/bind/delete | `VertexInputStateFactory::m_cache` | -| `SamplerCso` | create/delete + `bind_sampler_states` | `VkSamplerManager::m_samplers`、`BackendSamplerObject` | -| `SamplerViewCso` | create/delete + `set_sampler_views` | `TextureResource::{perMipViews,…}`、`SyncTextureViewToBackend` | +| `VertexElementsCso` | create/bind/delete | `VertexInputStateFactory::m_cache`(Magma);Espryt 逐 VAO twin | +| `SamplerCso` | create/delete + `bind_sampler_states` | `VkSamplerManager`、`BackendSamplerObject` | +| `SamplerViewCso` | create/delete + `set_sampler_views` | `TextureResource::perMipViews`、`SyncTextureViewToBackend` | | `ShaderCso` | create/bind/delete + server 侧惰性特化 | `ProgramFactory::m_cache`、`BackendProgramObjectImpl` | | `RenderStateCso` | create/bind/delete,身份 = pipeline 子集 | Espryt 值镜像;Magma `ComputePipelineStateHash` | | Buffer / Texture / Renderbuffer | create / respecify / subdata / destroy | 各自 twin | | Framebuffer / Xfb | per-context 身份 + `set_*` payload | `BackendFramebufferObject`、`m_xfbCounterSlotByObject` | -CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。 +CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `flat_hash_map`,LRU 淘汰时发 `delete_*`;render-state 容量 64(`kMGPipeCsoCacheCapacity`,P13 重调)、sampler state 256。两条已落地的偏离: -> **[deviation] D-F2(P4a 落地)**:上面这行的 **sampler-view 4096 项内容寻址是 P7(Magma)的形状**;Espryt 这一波把 sampler **view** 做成**按纹理对象身份寻址**——一纹理一视图,按纹理自己的 lifetime id 铸造,视图限制变了就在**同一个句柄**上重发。理由是 Espryt 的视图没有可共享的驱动侧对象,内容寻址只会多一张表和一次哈希。sampler **state** 的 256 项内容寻址照做,并且 P4a 给它加了**引用计数**:`MGPTextureParams::BuiltinSampler` 指着的项不允许被 LRU 挤掉(全被引用时超容铸造,计数在 `OverCapacityMints`),否则一次驱逐就会让一条标准记录指向一个已经换代的句柄。 +- **D-G1(P3a)**:Espryt 的 vertex-elements CSO **按 VAO 身份寻址**(一 VAO 一句柄,配置变化在同一句柄上重发)——Espryt 的 twin 持有驱动 VAO 名与 scratch buffer,两个格式相同的 VAO 不能共享;1024 项的内容寻址是 P7(Magma)在同一组调用之上加的。 +- **D-F2(P4a)**:Espryt 的 sampler **view** 按纹理对象身份寻址(Espryt 没有可共享的驱动侧视图对象);sampler **state** 内容寻址并带**引用计数**——`MGPTextureParams::BuiltinSampler` 指着的项不允许被 LRU 挤掉(超容铸造计入 `OverCapacityMints`),否则一条标准记录会指向已换代的句柄。 -**[deviation] D-G1(P3a 落地):vertex-elements CSO 在 P3a 是身份寻址,不是内容寻址。** Espryt 根本没有 vertex-elements CSO,它有的是**逐 VAO 的 twin**(`BackendVertexArrayObject`,`MobileGL/MG_Backend/DirectGLES/Managers.h:957-1163`),twin 持有一个驱动 VAO 名(`:1096`)、32 个 client-array scratch buffer id(`:1097`)与 32 个 fp64 scratch id(`:1101`);两个格式相同的前端 VAO 不能共享它,因为驱动 VAO 同时持有 element-array 绑定与逐属性缓冲绑定,共享 CSO 会把它们变成每次 `BindVertexElements` 都要重发——严格比今天更慢。所以 P3a 逐前端 `VertexArrayObject` 铸一个 `VertexElementsCso` 句柄(配置变化时**在同一句柄上重发** `CreateVertexElements`,`MGPipeHandle::Gen` 只在槽位复用时递增),**这是一个命中率恒为 1 的合法内容寻址缓存**。上面那张 1024 项的内容寻址表是 **P7** 的活——Magma 的 `VertexInputStateFactory` 接管 CSO 时,`VkPipelineVertexInputStateCreateInfo` 要的正是内容寻址;它加在 P3a 同一组 `CreateVertexElements`/`BindVertexElements`/`DeleteVertexElements` 与同一个 slot 分配器**之上**,P3a 的线上形状与 applier 记录都不妨碍它。 - -## 3. 调用目录(P0 已落地) +## 3. 调用目录 ### 3.1 单一真相源 -`MobileGL/MG_Pipe/PipeCalls.def`:一行一个调用 `X(Name, PayloadStruct, Class, Flags)`。**线上 opcode 就是行在文件里的位置**(1-based),所以目录必须是唯一记录的集合,新调用只能**追加**到文件末尾、退役的调用保留槽位。`MGP_CALL_LIST_DOCUMENTED_COUNT = 71`(`MobileGL/MG_Pipe/PipeCalls.def:69`)由 `MG_Test/Pipe/PipeCatalogueTest.cpp` 钉住。 +`MobileGL/MG_Pipe/PipeCalls.def`:一行一个调用 `X(Name, PayloadStruct, Class, Flags)`。**线上 opcode 就是行的位置**(1-based),新调用只能**追加**、退役的调用保留槽位。`MGP_CALL_LIST_DOCUMENTED_COUNT = 76`(P5b 契约追加了 5 条 verb),由 `MG_Test/Pipe/PipeCatalogueTest.cpp` 钉住。 -七个生成器(`scripts/gen_pipe.py`,产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`): +生成器(产物提交进树,CI `pipe-gates` 重生成并 `git diff --exit-code`): | | 产物 | 内容 | |---|---|---| | G1 | `PipeTables.inc` | 两张函数指针表 | -| G2 | `PipeThunks.inc` | monolith 直调 thunk `MGP_()`,`MG_Impl` 的约 93 个 `gBackendFunctionsTable.GL.*` 站点逐名改到它上面 | +| G2 | `PipeThunks.inc` | monolith 直调 thunk `MGP_()`,`MG_Impl` 的边界站点逐名改到它上面 | | G3 | `PipeWire.inc` | wire 记录 + 每种一条尺寸 `static_assert` + applier 分发前的运行期边界检查 → `Fatal{ProtocolCorruption}` | -| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表来自 `PipeFields.def`;浮点按位比较,NaN patch level 不会误报) | -| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id(**63 个**,`static_assert(kMGPipeInputFieldCount == 63)`)与逐 verb 世代 poison | -| G6 | `PipeCoverage.inc` | 477 行后端读点清单 → MGPipe 调用的映射(`Coverage.def` 手工维护一半):299 → 调用、5 client 自答、6 反向通道、167 结构性句柄、**0 UNMAPPED** | -| G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表(24 个,取自 `ComputePipelineStateHash` 今天哈希的字段,`scripts/gen_pipe.py:67-92`);带 `offsetof` 的 chunk 表与 setter 一致性测试在 P2 | +| G4 | `PipeVerify.inc` | `MOBILEGL_PIPE_VERIFY` 的逐字段比对器(字段表 `PipeFields.def`;浮点按位比较) | +| G5 | `PipeFilled.inc` | `PipeInputs` 字段 id(63 个,`static_assert`)与逐 verb 世代 poison | +| G6 | `PipeCoverage.inc` | 后端读点清单 → MGPipe 调用的映射(`Coverage.def`),**0 UNMAPPED** 是门 | +| G7 | `PipeSpanTable.inc` | render-state pipeline 子集的成员名表;chunk 表与 setter 一致性测试在 `RenderStateSpansTest` | +| G8 | `PipeFieldOwnership.inc`(`gen_pipe_field_ownership.py`) | 每个 `PipeInputs` 字段恰好属于 `RECORD-SUPPLIED / APPLIER-DERIVED / BARRIER-PULLED / FATAL` 之一,不在任何一类 = 构建失败;目前 33 / 2 / 36 / 4。从 `MG_Backend/MGPipe/PipeInputs.h` include,**不从 `MG_Pipe/MGPipe.h`**(后者在 pull 构建的 include 闭包里,G1 不允许符号位移) | -**G8(P5 新增,另一个生成器)** `scripts/gen_pipe_field_ownership.py` → `generated/PipeFieldOwnership.inc`: -`CONTRACT-P5.md` §3 的表 2,63 个字段 + 7 个 sticky forward = **70 行**,每行恰好属于 -`RECORD-SUPPLIED / APPLIER-DERIVED / BARRIER-PULLED / FATAL` 之一,**不在任何一类里 = 构建失败**(R-7.1)。 -`RECORD-SUPPLIED` 是**推导**出来的(`Coverage.def` 的 emitted 列表减去 `PipeFill.cpp` 的 -`EmittedCallSuppliesTheWholeField` 拒绝项),手工那一半在 `MG_Pipe/FieldOwnership.def`。 -它从 `MG_Backend/MGPipe/PipeInputs.h` include,**不从 `MG_Pipe/MGPipe.h`**——后者在 pull 构建的 -include 闭包里,而 G1 不允许那里有任何符号位移。`--check` 与 `--self-test`(11 条阴性对照)与 -`gen_pipe.py` 的同样在 CI 里跑。 +另有 `PipeFillPoints.inc`(`FillPoints.def`,83 条 `MGP_FILL` 覆盖 69 个 verb)与 `DirtySurface.def`(`gen_pipe_dirty_surface.py`,§5.2)。 -**G1 的 P5 codegen 陷阱(实测,`c1-v2.md` §9)。** capability gate 若替换的是指针表达式,就必须保留它的**表达式形状**,不能只保留真假值。两个 `if (const auto f = TABLE.GL.Slot)` 被 Bool-valued macro 改写成 block 后,pull `.text` **−144 B**,`GLImpl::GetInteger64v` −150、`GLImpl::GetIntegerv` +2;语义没变,G1 仍正确地判红。落地形状分成 `MGL_BACKEND_SLOT_CAP` 与 pointer-valued `MGL_BACKEND_SLOT_PTR_CAP` / `_PTR_LOCAL`,后者在 pull 展开回原 slot expression。规则不是“宏结果相等”,而是“byte-identical gate 必须保住 init-statement / short-circuit / pointer 的原表达式形状”。 +**G1 的 codegen 规则**:capability gate 若替换的是指针表达式,必须保留其**表达式形状**(init-statement / short-circuit / pointer),不能只保留真假值——`MGL_BACKEND_SLOT_CAP` 与 pointer-valued `MGL_BACKEND_SLOT_PTR_CAP` / `_PTR_LOCAL` 分开,后者在 pull 展开回原 slot expression,否则 pull `.text` 会漂移。 ### 3.2 分组与计数 | Class | 条 | 内容 | |---|---|---| -| `kScreen` | 11 | `GetCaps`(R)、`ResourceCreate/Respecify/Destroy`、`MapPersistent`(R,O)/`UnmapPersistent`(O)、`FenceCreate/Status(R)/Wait(R)/Destroy`、追加的 `FenceWaitServer`(`glWaitSync`,GPU 侧等待) | -| `kCtxQuery` | 8 | `QueryCreate/Begin/End/Available(R)/Result(R)/Destroy`、追加的 `QueryTimestamp`(R)(`glGetInteger64v(GL_TIMESTAMP)`)与 `QueryCounter`(`glQueryCounter`) | -| `kCtxCso` | 13 | create/delete × {render state, vertex elements, sampler, sampler view, shader} + bind × {render state, vertex elements, shader};sampler 与 sampler view 的绑定是下一组的批量调用 | +| `kScreen` | 11 | `GetCaps`(R)、`ResourceCreate/Respecify/Destroy`、`MapPersistent`(R,O)/`UnmapPersistent`(O)、`FenceCreate/Status(R)/Wait(R)/Destroy`、`FenceWaitServer` | +| `kCtxQuery` | 8 | `QueryCreate/Begin/End/Available(R)/Result(R)/Destroy`、`QueryTimestamp`(R)、`QueryCounter` | +| `kCtxCso` | 13 | create/delete × {render state, vertex elements, sampler, sampler view, shader} + bind × {render state, vertex elements, shader} | | `kCtxState` | 17 | `SetDynamicState`(B)、`SetFramebufferState`、`SetVertexBuffers`(V)、`SetIndexBuffer`、`SetIndirectBuffers`、`SetSamplerViews`(V)、`BindSamplerStates`(V)、`SetShaderImages`(V)、`SetShaderBuffers`(V,H)、`SetStreamOutputTargets`(V)、`SetGlobalConstants`(B)、`SetVertexAttribDefaults`(V)、`SetPixelPackState`、`SetPatchState`、`SetDrawProgram`、`SetDispatchProgram`、迁移期临时的 `SetResidualValueState`(B) | -| `kCtxObject` | 9 | 按资源寻址:`SetTextureParams`、`ResourceSubData`(B,V)、`BufferSubDataResident`(B,O)、`ResourceSubDataComplete`、`ResourceFlushRange`、`ResourceReadback`(R)、`ResourceCopyRegion`、`GenerateMipmap`、`GetTextureImage`(R) | -| `kCtxVerb` | 13 | 按上下文寻址:`Blit`、`Clear`、`ReadPixels`(R)、`DrawVbo`(H,V)、`LaunchGrid`、`MemoryBarrier`、`Begin/End/Pause/ResumeStreamOutput`、`Flush`、`Present`、`SetSwapInterval`(O) | +| `kCtxObject` | 9 | `SetTextureParams`、`ResourceSubData`(B,V)、`BufferSubDataResident`(B,O)、`ResourceSubDataComplete`、`ResourceFlushRange`、`ResourceReadback`(R)、`ResourceCopyRegion`、`GenerateMipmap`、`GetTextureImage`(R) | +| `kCtxVerb` | 18 | `Blit`、`Clear`、`ReadPixels`(R)、`DrawVbo`(H,V)、`LaunchGrid`、`MemoryBarrier`、`Begin/End/Pause/ResumeStreamOutput`、`Flush`、`Present`、`SetSwapInterval`(O);P5b 追加 `BindShaderImage`、`PatchParameter`、`BindStreamOutput`、`SetStorageBlockBinding`、`CopyFramebufferToTexture` | -Flags:`kNeedsAck`(调用方等 server 确认;目录里目前无条目携带,见 §8.3)、`kHasBlob`(B)、`kVarTail`(V)、`kHostSpan`(H)、`kReplySlot`(R,答进 `MGPReplySlot`,永不阻塞)、`kOptional`(O,后端表里可为 null:Magma 故意不注册 `BufferSubDataResident` 与 `SetSwapInterval`)。 +Flags:`kNeedsAck`、`kHasBlob`(B,payload 拥有一个 `MGPBlobRef` 成员,不弱于此)、`kVarTail`(V)、`kHostSpan`(H)、`kReplySlot`(R,答进 `MGPReplySlot`)、`kOptional`(O,后端表里可为 null:Magma 不注册 `BufferSubDataResident` 与 `SetSwapInterval`)。 -- 今天 20 个 draw 入口塌成 `DrawVbo` 一条,`MGPDrawRange[]` 就是 `MultiDraw*` 族今天的形状;`Clear` 一条判别式合并 `glClear` + 4 个 `glClearBuffer*` + 4 个 `glClearNamedFramebuffer*`。 -- `SetSamplerViews` / `BindSamplerStates` **没有 stage 维度**:MobileGL 的纹理单元空间是合并的(`TextureState::m_textureUnits` 是 192 个单元的一个数组,每 stage 32 只是广告数字),同一单元可被两个 stage 采样;stage 只在目标 API 需要时由 server 从反射归档推导。 -- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 FBO attachment / image 单元 / `glCopyImageSubData` 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数,且 `RequireImageBindableStorage` 需要在前端参数版本不动时强制重同步。**P4a 有意把这个缺口关掉了**(Espryt 从 `SyncNeccessaryTextures` 侧补上 READ-only attachment 的参数同步),并且**证明它此前确实是坏的**:证据不是公共 GL 的图——回读模拟自己会写 `GL_DEPTH_STENCIL_TEXTURE_MODE`、`IsDrawSyncClean` 又在首次采样时把参数推下去,所以任何纯 GL 序列都看不见它——而是 `MG_IntegrationTest/Harness/PipeApplyPeek.{h,cpp}` 的白盒断言(读 applier 的参数记录与 Espryt 已应用状态),变异注入下 4/4 变红。 -- `SetIndexBuffer` 独立于 VAO 配置版本(D5):索引 slot 重绑不移动 VAO config version。 -- `SetGlobalConstants` 只覆盖默认 uniform block(D6):`globalUboScratch` 是 link phase B 的 CPU 数组,没有 GL name、没有 `BufferObject`。 +- 20 个 draw 入口塌成 `DrawVbo` 一条,`MGPDrawRange[]` 是 `MultiDraw*` 族的形状;`Clear` 一条判别式合并 `glClear` + `glClearBuffer*` + `glClearNamedFramebuffer*`。 +- `SetSamplerViews` / `BindSamplerStates` **没有 stage 维度**:MobileGL 的纹理单元空间是合并的(192 个单元一个数组),stage 只在目标 API 需要时由 server 从反射归档推导。 +- `SetTextureParams` 按资源寻址、与 sampler view 分开(D10):只作 attachment / image 单元 / copy 端点的纹理没有 sampler view,但 Espryt 对 attachment 也同步纹理参数。P4a 用白盒断言(`MG_IntegrationTest/Harness/PipeApplyPeek`)证明这个缺口此前确实是坏的。 +- `SetIndexBuffer` 独立于 VAO 配置版本(D5);`SetGlobalConstants` 只覆盖默认 uniform block(D6)。 -**显式不移植**:`GetIntegeri_v`/`GetInteger64i_v`/`GetProgramiv`(后两项 P0 已从 `GLFunctionsTable` 删除,`50815a23`;唯一属于后端的带下标答案 `GL_MAX_COMPUTE_WORK_GROUP_COUNT/SIZE` 进 `MGPCaps`,`e8ee7b1a`;`GL_COMPUTE_WORK_GROUP_SIZE` 是前端反射查询)、`ShaderStorageBlockBinding`(折进反射归档)、`set_pixel_unpack_state`(不存在:前端已在 `glTexImage` 时解析压缩格式、强制默认 unpack)、压缩格式概念、`pipe_transfer`。 +**显式不移植**:`GetIntegeri_v`/`GetInteger64i_v`/`GetProgramiv`(后两项已从 `GLFunctionsTable` 删除;`GL_MAX_COMPUTE_WORK_GROUP_*` 进 `MGPCaps`)、`ShaderStorageBlockBinding` 的反射折叠、`set_pixel_unpack_state`(前端已在 `glTexImage` 时解析压缩格式、强制默认 unpack)、压缩格式概念、`pipe_transfer`。 ### 3.3 能力位(`MGPCapBit`) -`kCapViewportArray`、`kCapFloat64VertexAttrib`、`kCapResidentSubData`、`kCapCpuXfbPrimitiveAccounting`、`kCapTimerQuery`、`kCapOcclusionQuery`、`kCapXfbPrimitivesQuery`、`kCapNeedsHostIndexBytes`(server 做 restart 重写 / multi-draw 展平,split 下开启索引宿主镜像,§10.3)、`kCapNeedsHostUboBytes`(server 把具名 UBO 打进自己的 ring,需要 `SetShaderBuffers` 的 host payload)。`CallMask` 取代"槽位是否为 null"这个隐式能力探测。 +`kCapViewportArray`、`kCapFloat64VertexAttrib`、`kCapResidentSubData`、`kCapCpuXfbPrimitiveAccounting`、`kCapTimerQuery`、`kCapOcclusionQuery`、`kCapXfbPrimitivesQuery`、`kCapNeedsHostIndexBytes`(server 做 restart 重写 / multi-draw 展平,split 下开启索引宿主镜像,§10.3)、`kCapNeedsHostUboBytes`(server 把具名 UBO 打进自己的 ring)、`kCapBackendOwnsXfbCapture`(P5b)。`CallMask` 取代"槽位是否为 null"这个隐式能力探测。 -不存在 `kCapPrimitiveRestart` / `kCapMultiDraw*` 一类"归属开关"(D-B7):`ResolveTierForBatch` 逐 batch 用 `programReadsDrawID`(转译后 ESSL 的性质,只存在于 server)选档,两个后端都做 restart 重写,所以这类归属不可用 cap 表达。规则一句话:**multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。** +不存在 `kCapPrimitiveRestart` / `kCapMultiDraw*` 一类"归属开关"(D-B7):**multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。** `MGPCaps` = `DynamicBackendParameters` 整块 + `CallMask` + 两个 blob(format 能力表、renderer 字符串),握手后一次快照,取代 40 个 `pActiveBackendObject->` 站点与 89 个 caps 读点。 -一个待转显式能力位的现有陷阱:`GL_Drawing.cpp` 把 `EndTransformFeedback` 槽位的非空当作"后端按 GL 顶点序捕获"来跳过 `FixupGsStripCaptureOrder`。MGPipe 下改为显式 `kCapDriverOrderedXfbCapture` 一类的位(P8/P9)。 +## 4. 记录与 payload 约定(`MG_Pipe/MGPipeTypes.h`) -`MGPCaps` = `DynamicBackendParameters`(整块包含,~90 个标量含六个 compute 限制)+ `CallMask` + 两个 blob(format 能力表、renderer 字符串),握手后一次快照,取代 40 个 `pActiveBackendObject->` 站点与 89 个 caps 读点。 - -## 4. 记录与 payload 约定(P0 已落地,`MG_Pipe/MGPipeTypes.h`) - -- 每个 payload 是平坦 POD、显式 padding、`static_assert` 平凡可复制与**精确尺寸**;**永不含指针**。 -- `MGPBlobRef{Offset, Size, Seg}`(24 B)指向 blob 区:monolith 下 `Seg == kMGHostSpanSegNone`、Offset 是调用方 staging arena 内地址;split 下 Seg 命名传输段。 -- `MGHostSpan`(32 B,`MG_Pipe/MGPipeHostSpan.h`)是整份接口里**唯一形状随传输而变**的东西:monolith 下 `Ptr` 指向 shadow 或应用内存;split 下 `Ptr == nullptr`、字节在 `Seg/Offset` 命名的 `SEG_STAGE`,或 `Seg == kMGHostSpanSegFromServerIndexMirror`(`MobileGL/MG_Pipe/MGPipeHostSpan.h:26`)表示"字节已在你那边的索引镜像里"。`MGPipeHostBytes()` 是一次可预测分支;split 解析器 `gMGPipeSegmentResolver` 由 `MG_Remote` 安装。它只进变长尾(`DrawVbo` 的用户索引、`SetShaderBuffers` 的具名 UBO 字节),永不内联进定长 payload——VBO 路径(MC/Sodium 的全部 draw)不为它付字节。 -- 变长记录(`kVarTail`)= 定长前缀 + 自描述长度的内联尾巴;`kHasBlob` 记录额外校验 `BlobRef` 落在其声明的段内。运行期边界纪律:`SEG_CMD` 是对端并发写入的区域,`static_assert` 管不到运行期损坏,违反一律 `Fatal{ProtocolCorruption}`。 -- wire 记录头 `MGPWireRecHeader{Op:u16, Flags:u16, Size:u32}`(8 B),Size 含头、8 字节倍数;**没有逐记录序号字段**——seq 就是记录序数(producer `m_emitSeq++` / consumer `m_applySeq++`)。 -- **分块上界 = ring 容量的一半**(`RingProducer::MaxRecordBytes()`):这是每个 head 偏移都能放下的最大记录(wrap pad 最多花 total−8 字节),超过它的 payload(大 `ResourceSubData`、`CreateShaderState` 归档)由发射器切成多条;ring 对更大的记录直接拒绝(nullptr + `MGLOG_E`)而不是让 producer 等一个永远不够的空闲量。 -- `MGPSubData` 的 buffer 半边:`Target == Buffer` 时没有 level 与 box,目的字节范围搭在 `UnionBox.X`(offset)与 `UnionBox.W`(size)上,`MGPipeSetSubDataBufferRange()` 是唯一拼写;单条记录上限 offset 2³¹−1 / size 2³²−1,越界由发射器拆分。 +- 每个 payload 是平坦 POD、显式 padding、`static_assert` 平凡可复制与精确尺寸;**永不含指针**。 +- `MGPBlobRef{Offset, Size, Seg}`(24 B)指向 blob 区:monolith 下 `Seg == kMGHostSpanSegNone`;split 下 Seg 命名传输段。 +- `MGHostSpan`(32 B)是唯一形状随传输而变的东西:monolith 下 `Ptr` 指向 shadow 或应用内存;split 下 `Ptr == nullptr`、字节在 `SEG_STAGE`,或 `Seg == kMGHostSpanSegFromServerIndexMirror`(P8)。它只进变长尾(`DrawVbo` 的用户索引、`SetShaderBuffers` 的具名 UBO 字节),永不内联进定长 payload——VBO 路径不为它付字节。 +- 变长记录(`kVarTail`)= 定长前缀 + 自描述长度的内联尾巴;`kHasBlob` 记录额外校验 `BlobRef` 落在其声明的段内。`SEG_CMD` 是对端并发写入的区域,运行期违反一律 `Fatal{ProtocolCorruption}`。 +- wire 记录头 `MGPWireRecHeader{Op:u16, Flags:u16, Size:u32}`(8 B),Size 含头、8 字节倍数;**没有逐记录序号字段**——seq 就是记录序数(producer `m_emitSeq++` / consumer `m_applySeq++`)。`Flags` 是 ring 封帧,永不是 call flags(R-17)。 +- 单条记录上界 = ring 容量的一半(`RingProducer::MaxRecordBytes()`),超过的记录被具名拒绝(`Fatal{RingOverrun}`),不分块(R-10;实测 `maxrec` 见 `MEASUREMENTS.md` §6.3)。 +- `MGPSubData` 的 buffer 半边:`Target == Buffer` 时目的字节范围搭在 `UnionBox.X/W` 上,`MGPipeSetSubDataBufferRange()` 是唯一拼写;单条记录上限 offset 2³¹−1 / size 2³²−1。 ### 4.1 关键 payload | payload | 尺寸 | 要点 | |---|---|---| -| `MGPResourceDesc` | 88 | buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;`BindMask` 的 `ELEMENT_ARRAY` 位是索引镜像的开关;`ImageBindableHint` 预防性分配 image-bindable 存储;`ViewOf` 是纹理视图的存储属主(server 侧 keep-alive);`BufferForTexBuffer/BufOffset/BufSize` 实时解析(`kMGPipeWholeBuffer = ~0`)。Renderbuffer 保持独立类(自己的 format-capability target、`ComponentSizes`、twin) | -| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / **12** / 32 | §5.3 | -| `MGPVertexElements` | 40 | blob 同时带解析后的 `MGPVertexAttribWire[]` **和** `MGPVertexBindingPointWire[]`(P3a 落地的两个 POD 线上形,24 B / 16 B,`MobileGL/MG_Pipe/MGPipeValueTypes.h:568-600`)。**两个视图都过线的理由是记录自洽,不是 stride 消歧**:前端已经把 pointer 调用的 stride 0 解析成 element size,一个活到 `VertexAttribute::Stride` 的 0 只可能来自 binding 模型(`MobileGL/MG_Pipe/MGPipeValueTypes.h:496-503`),后端从来不读 binding point(`MG_Backend` 里 `VertexBufferBindingPoint` / `GetAttributeBindingIndex` / `GetAttributeRelativeOffset` 零命中);真正承重的是 `MGPVertexElements` **声明**了 `BindingPointCount`,一条不描述自己 blob 的记录会让 applier 的边界门永远无法收口。代价按配置变化付一次、不按 draw 付(blob 只搭 `CreateVertexElements`),裁掉第二个视图是 P13 的重调项。`IsLong` 与 `Type == Float64` 分开携带;`Divisor` 不在属性视图里(走 `MGPVertexBuffer::Divisor`);仅供查询的 `LegacyStride/LegacyPointer` 留在 client | -| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`**(三种 border color 表示永远都被数值填满,没有它后端无法在 `Iiv`/`fv` 或 `VkBorderColor` 家族间选择) | -| `MGPSamplerView` / `MGPTextureParams` | 36 / **40**(P4a) | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上。**[deviation] D-E1**:`MGPTextureParams` 从 32 涨到 **40**,多出 `BuiltinSampler`(该纹理自带 `SamplerObject` 所对应的 sampler CSO 句柄,**不允许是空句柄**——每个 `ITextureObject` 都有一个 sampler 对象,空句柄是协议损坏而不是"没有 sampler")与第二个 resync 位 `SamplerResync`;参数本身一项没变。句柄从 client 的**内容寻址 cache** 取(`MGPipeSamplerCsoCacheInstance().Acquire`),不是按纹理身份铸的——这条缝在 P4a 里被踩了两次:客户端一度按对象身份铸、Espryt 的 twin 一度按身份查,两边都拿不到那条内容寻址的记录 | -| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset` + 四个状态字节,§7 | -| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**(按结构消灭 read-buffer-shared-FBO 缺陷类);`MGPSurface::InternalFormat` 内联(四个跨对象 mask 推送时零查表——P4a 发现只有格式不够,`MGPSurface` 的 `Pad0` 因此变成 `TextureTarget`,否则 `ShouldUseCaveatTextureFormat` 这类 mask 仍得回前端问纹理目标);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器。**P4a:多一个 `Target` 字节,记录按绑定目标发**(`Draw` / `Read` / `Both`,`Complete` 是后端无关的 `CheckCompleteness()` 答案,D-C3),**并且 applier 把记录按 framebuffer 句柄存成每对象一张表**外加两个"当前绑定"句柄——第四个取值 `Named = 3` 就是"这条记录描述它点名的那个 framebuffer,不动任何绑定",`BlitNamedFramebuffer` 与四个 `ClearNamedFramebuffer*` 之前正是因为只有两条绑定记录而打进了一个从未收到附件的 FBO | +| `MGPResourceDesc` | 88 | buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;`BindMask` 的 `ELEMENT_ARRAY` 位是索引镜像的开关;`ImageBindableHint` 预防性分配 image-bindable 存储;`ViewOf` 是纹理视图的存储属主;`BufferForTexBuffer/BufOffset/BufSize` 实时解析;`HasDefinedContent` 陈述的是上一次 respecify 当时的事实 | +| `MGPRenderStateDesc` / `MGPBindRenderState` / `MGPDynamicState` | 48 / 12 / 32 | §5.3 | +| `MGPVertexElements` | 40 | blob 带解析后的 `MGPVertexAttribWire[]` **和** `MGPVertexBindingPointWire[]`(记录自洽:一条不描述自己 blob 的记录让边界门无法收口;代价按配置变化付一次;裁掉第二个视图是 P13 的重调项);`IsLong` 与 `Type == Float64` 分开携带 | +| `MGPSamplerDesc` | 32 | `SamplerParameters` 逐字节过线**含 `borderColorForm`** | +| `MGPSamplerView` / `MGPTextureParams` | 36 / 40 | view 只带视图限制;纹理参数挂在纹理对象上。**D-E1**:`MGPTextureParams::BuiltinSampler` 是该纹理自带 `SamplerObject` 对应的 sampler CSO 句柄(从内容寻址 cache 取,**不允许为空**——空句柄是协议损坏)+ `SamplerResync` 位 | +| `MGPProgramDesc` | 192 | 逐 stage SPIR-V blob ×6 + 反射归档 blob + `StageMask`/`GlobalUboSize`/`ReservedNumSamplesOffset`,§7 | +| `MGPFramebufferState` | 304 | 8 color + depth + stencil + **client 解析后的 `ReadSurface`**;`MGPSurface::InternalFormat` 与 `TextureTarget` 内联;`ContentHash` 既是 render-pass memo 键也是发射抑制器;`Target` 字节按绑定目标发(`Draw` / `Read` / `Both` / **`Named = 3`**),applier 按 framebuffer 句柄存成每对象一张表外加两个"当前绑定"句柄——否则 `BlitNamedFramebuffer` / `ClearNamedFramebuffer*` 会打进一个从未收到附件的 FBO | | `MGPSubData` / `MGPSubRegion` | 72 / 40 | §6 | -| `MGPDrawInfo` / `MGPDrawRange` / `MGPDrawIndirect` | **56** / 12 / 40 | `Flags` 门控 `MinIndex/MaxIndex`(只在 client-memory 数组路径算)与 `XfbCpuCapturedVertices`(只在 XFB scatter 路径读)——不是每 draw 都算;`NumDraws` 个 `MGPDrawRange` 在变长尾;用户索引的 `MGHostSpan` 只在 `kDrawHasUserIndices` 时进变长尾;indirect 的 `DrawCount` 由 client 解析,server 永不读 indirect 命令块来数 draw | -| `MGPShaderBuffers` / `MGPBufferRange` | 32 / 24 | range 不内联 host span;`kCapNeedsHostUboBytes` 下 Uniform 类带第二个变长尾 `MGHostSpan[HostSpanCount]`,与 range 数组下标对齐 | -| `MGPPixelPackState` | 28 | 只有 PACK 方向(D5) | -| `MGPPatchState` | 40 | 同时是 shader variant 输入 | -| `MGPClear` | 48 | Whole / Color / Depth / Stencil / DepthStencil 判别式 | -| `MGPGlobalConstants` | 40 | `(ShaderCso, Version)` 键控,每 program 每帧至多一次 | -| `MGPSubDataComplete` | 24 | 纹理拉取的正向终止符,可携带零个 region | -| `ResidualValueBlock` | **8**(P2 前 1248) | 迁移期 Track V 载体,§9.4 | +| `MGPDrawInfo` / `MGPDrawRange` / `MGPDrawIndirect` | 56 / 12 / 40 | `Flags` 门控 `MinIndex/MaxIndex`(只在 client-memory 数组路径算)与 `XfbCpuCapturedVertices`;`NumDraws` 个 `MGPDrawRange` 在变长尾;用户索引的 `MGHostSpan` 只在 `kDrawHasUserIndices` 时进变长尾;indirect 的 `DrawCount` 由 client 解析 | +| `MGPShaderBuffers` / `MGPBufferRange` | 32 / 24 | `kCapNeedsHostUboBytes` 下 Uniform 类带第二个变长尾 `MGHostSpan[HostSpanCount]` | +| `MGPPixelPackState` / `MGPPatchState` / `MGPClear` / `MGPGlobalConstants` / `MGPSubDataComplete` | 28 / 40 / 48 / 40 / 24 | 只有 PACK 方向(D5);patch 同时是 shader variant 输入;clear 五种判别式;`(ShaderCso, Version)` 键控;纹理拉取的正向终止符,可携带零个 region | +| `ResidualValueBlock` | 8 | 迁移期 Track V 载体,§9.4 | -每条 `kVarTail` 的 `set_*`(`SetVertexBuffers`、`SetSamplerViews`、`BindSamplerStates`、`SetShaderImages`、`SetShaderBuffers`、`SetStreamOutputTargets`)都带 `ContentHash`——与 `MGPFramebufferState` 同一模式,hash 未变就不发(§5.4)。 +每条 `kVarTail` 的 `set_*` 都带 `ContentHash`——hash 未变就不发(§5.4)。 -## 5. 前端 state tracker(`MG_Impl/Pipe/Tracker`,P2 起) +## 5. 前端 state tracker(`MG_Impl/Pipe/Tracker`) ### 5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里 -Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住(Espryt 代码自己标它为最热路径),per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium `st_validate_state`。 +Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住,per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium `st_validate_state`。 -**九个** validate 类(P2 落地,`MGP_FILL_CLASS_LIST`,`MobileGL/MG_Pipe/FillPoints.def:146-147`):`kDraw`(20 个 draw 入口)、`kDispatch`、`kClear`、`kBlitOrCopy`、`kTextureOp`(GenerateMipmap / CopyTex* / BindImageTexture)、`kReadback`、`kXfbSpan`、`kProgramOp`、`kQuery`。落地形态**不是**九个 `ValidateForX` 函数:`MGP_FILL(Verb)` 展开成唯一的 `MGPipeValidateForVerb(MGPipeVerb)`(`MobileGL/MG_Impl/Pipe/PipeFill.h:35`,宏在 `:84`),它已经恰好是每次经函数指针表调用之前的一条语句、位于每个提前返回之后(83 条语句覆盖 69 个 verb),verb 到类的映射由生成表回答。九个而不是四个,因为 `MG_Impl` 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、program op、query/sync)很多自己就读 `pGLContext`。 +**九个** validate 类(`FillPoints.def`):`kDraw`、`kDispatch`、`kClear`、`kBlitOrCopy`、`kTextureOp`、`kReadback`、`kXfbSpan`、`kProgramOp`、`kQuery`。落地形态是唯一的 `MGPipeValidateForVerb(MGPipeVerb)`(`MG_Impl/Pipe/PipeFill.h`),恰好是每次经函数指针表调用之前的一条语句、位于每个提前返回之后,verb 到类的映射由生成表回答。九个而不是四个,因为约 48 个非 draw 表项自己就读 `pGLContext`。 **只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送**——即 `BufferBackendOps` 的七个 hook。纹理 subdata 不在此列(§6)。 @@ -174,19 +158,17 @@ Blaze3D 每个 batch 用 `glEnable/glDisable(GL_BLEND)` 包住(Espryt 代码 | dirty 位 | 类 | 快门来源 | |---|---|---| | `NEW_RENDER_STATE` / `NEW_PIPELINE_STATE` | 值 | `m_version` / `m_pipelineStateVersion` | -| `NEW_PIXEL_PACK`、`NEW_PATCH_STATE`(`BitwiseEqual`,NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS`、`NEW_VERTEX_ELEMENTS`(VAO config version) | 值 | 既有计数器 | -| `NEW_SHADER`、`NEW_SHADER_BINDINGS`、`NEW_GLOBAL_CONSTANTS` | 值 | link/image-unit/backend-state/block-binding/uniform-write-set/UBO-content 版本 | -| `NEW_VERTEX_BUFFERS` | 对象 | **`VertexArrayState::m_anyVaoAttributeGeneration`**(新增)→ 命中后走 32 属性前缀 | +| `NEW_PIXEL_PACK`、`NEW_PATCH_STATE`(`BitwiseEqual`,NaN 合法)、`NEW_VERTEX_ATTRIB_DEFAULTS`、`NEW_VERTEX_ELEMENTS` | 值 | 既有计数器 | +| `NEW_SHADER`、`NEW_SHADER_BINDINGS`、`NEW_GLOBAL_CONSTANTS` | 值 | link / image-unit / backend-state / block-binding / uniform-write-set / UBO-content 版本 | +| `NEW_VERTEX_BUFFERS` | 对象 | `VertexArrayState::m_anyVaoAttributeGeneration` → 命中后走 32 属性前缀 | | `NEW_INDEX_BUFFER` | 对象 | 索引 slot 版本 + 绑定对象 `{slot,gen}` | -| `NEW_FRAMEBUFFER` | 对象 | **`FramebufferState::m_anyAttachmentGeneration`**(新增)+ 对象/slot 版本 → 重算 `ContentHash` | -| `NEW_SAMPLER_VIEWS`、`NEW_SAMPLERS`、`NEW_SHADER_IMAGES` | 对象 | **`TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration`**(新增)+ bind/sampling-resolution generation → 走 `GetMaxTouchedUnit()` 前缀、重算集合 hash | -| `NEW_CONST_BUFFERS` / `NEW_SHADER_BUFFERS` / `NEW_SO_TARGETS` | 对象 | **`BufferState::m_anyBufferChangeGeneration`**(新增)→ 走 `GetTouchedBindPointCount()` 前缀 | +| `NEW_FRAMEBUFFER` | 对象 | `FramebufferState::m_anyAttachmentGeneration` + 对象/slot 版本 → 重算 `ContentHash` | +| `NEW_SAMPLER_VIEWS`、`NEW_SAMPLERS`、`NEW_SHADER_IMAGES` | 对象 | `TextureState::m_anyTextureContentGeneration` + `m_anyTextureParamsGeneration` + bind / sampling-resolution generation → `GetMaxTouchedUnit()` 前缀、重算集合 hash | +| `NEW_CONST_BUFFERS` / `NEW_SHADER_BUFFERS` / `NEW_SO_TARGETS` | 对象 | `BufferState::m_anyBufferChangeGeneration` → `GetTouchedBindPointCount()` 前缀 | -五个聚合世代全部落在既有 bump 点上(约 20 行),把对象类组的快门从"每 validate 走查 192 单元 / 84×4 绑定点 / 32 属性 / 40 attachment"降成一次 `Uint64` 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了",这正是 Magma 不得不用有损 `sampledContentSum` 的原因)。 +五个聚合世代落在既有 bump 点上,把对象类组的快门降成一次 `Uint64` 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了")。完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举每个 mutator → 必须 bump 的聚合世代,未映射或映射行点名一个已不存在的 mutator 即失败(`--check` + `--self-test` 都是 CI 门)。**快门看不见自己的主体**是 P4a 踩过三次的坑(`glBindSampler`、SSO 下 `GetCurrentProgram()`、`glBindImageTexture` 只换 level)——新增字段必须带"记录字段 → setter → 快门"一行,且优先混入已有世代(新增计数器会撑大 pull 对象,G1 不允许)。 -完整性由 `scripts/gen_pipe_dirty_surface.py` 保证:枚举 `MG_Impl/GLImpl` 里每个 mutator → 必须 bump 的聚合世代,未映射、或映射行点名一个已不存在的 mutator 即失败(**P2 起成为门**:`pipe-gates` 跑 `--check` 与 `--self-test` 两半,`.github/workflows/test.yml:1601-1604`;`--self-test` 的 21 个阴性对照是让 `--check` 保持诚实的那一半,信息性的 `--summary` 步骤随之退役)。**实测规模**:926 次 mutator 调用落在 73 个不同 mutator 上,其中 92 次(7 个 mutator,绝大多数 `RecordError`)位于同函数内也会到达后端的"即时发布点",其余 834 次由紧随其后的 verb 发布——映射表是 73 条目的问题。 - -三个回绕 `Uint16` 在 tracker 边界加宽(`m_lastPushed[]` 是 tracker 自己的字段,不改 `MG_State`);回绕在 tracker 本地无害(多一次重推,永不漏推),且被集合 hash 抑制器吞掉。 +三个回绕 `Uint16` 在 tracker 边界加宽(`m_lastPushed[]` 是 tracker 自己的字段);回绕在 tracker 本地无害(多一次重推,永不漏推)。 ### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集,动态状态单独走(D-B1) @@ -196,33 +178,30 @@ bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化 chunk ``` -- 整块的理由:`RenderStateParameters` 是平凡可复制 POD,Espryt 自己 `static_assert` 并做 head/blend/tail 三段 memcmp,**字段顺序承重**(`ScissorBoxWrittenMask`、`ClipDistanceEnabledMask` 故意放在 tail 段);拆成 blend/depth-stencil/rasterizer 三个 CSO 要手工维护 ~150 字段划分表且无绊线。 -- 子集身份的理由:整块内容寻址会让 `glViewport`/`glScissor`/`glBlendColor`/`glClearColor` 每次铸造新 CSO、冲掉 server 的 pipeline memo——`RenderState.h` 记录的那次回归。`RenderState.cpp` 里 viewport/scissor/line-width 族只 `++m_version`,`SET_CAPABILITY` 与 pipeline 相关 setter 才 `BumpVersions()`。 -- 动态子集:viewport、scissor、depth range、blend color、line width、polygon offset、stencil ref/write mask、clear 值、sample coverage、hints、point-size 族。 -- 划分只写在一处,**P2 已落地**:`MG_Pipe/MGPipeRenderStateSpans.{h,cpp}` 的 chunk 表 + `MGPipeComputePipelineSubsetHash()`,从 Magma 的 `ComputePipelineStateHash` 搬来,client 与两个后端共用。表的形态是**头文件里的 16 个 `constexpr` 边界**(不是 15 个区间,因为两半完美交替:chunk 0 dynamic、chunk 1 pipeline……),每个边界都是一个 `offsetof` 或 `sizeof`;**7 个 pipeline chunk 共 396 B + 8 个 dynamic chunk 共 772 B = 1168**,总数、划分完整性与边界严格递增都是 `static_assert`(`MobileGL/MG_Pipe/MGPipeRenderStateSpans.h:190-191`)——一个缺口是编译错误,不是测试失败。G7 的 `MG_Test/Pipe/RenderStateSpansTest.cpp` 遍历每个 `RenderState` public setter,断言 `pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变`;把一个成员从 pipeline 半边挪到 dynamic 半边(划分仍完整,仍能编译)会让它点名那个 setter 变红(`scripts/g7_negative_control.sh`)。 -- server 侧:每 context 一份 working `RenderStateParameters`(1168 B),`bind` 与 `set_dynamic_state` 各把自己的 chunk 散射进去。**Espryt 的 `SyncRenderState`(693 行)拿到的仍是 `const RenderStateParameters&`,单 `Uint16` 早退、三段 memcmp 一行不动**;Magma 的 pipeline memo 键是 `cso.slot`,动态尾巴仍走 `ApplyDynamicDrawStateTail`。Espryt 的 head/blend/tail 划分(驱动侧增量)与 pipeline/dynamic 划分(线上与身份)是两回事,并存、各有绊线。 -- client 取值顺序:`m_pipelineStateVersion` 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 pipeline 子集算 xxHash(**396 B = 49.5 个 64-bit 字**,7 个 chunk 拼起来;Magma 今天就在算)→ CSO map 探测 → 命中发 12 B bind,未命中发变化 chunk 的 create 再 bind;`m_version` 变而子集未变 → 只发 `set_dynamic_state`(~200 B)。 -- **P2 已补真存储的三个 capability**:`FramebufferSrgb`、`DepthClamp` 与 `TextureCubeMapSeamless`——`RenderState::SetCapability` 的 `default:` 分支把这三个都静默吞掉(不存储也不报 `GL_INVALID_ENUM`,`FramebufferSrgb` 的六个后端读点恒为编译期 false,另两个零读点)。三个 `Bool` 落进 `ColorMasks` 与 `ClearColor` 之间那 3 字节的空洞(偏移 581/582/583,`MobileGL/MG_Pipe/MGPipeValueTypes.h:303-305`),所以 `sizeof(RenderStateParameters)` 仍是 1168 且**既有成员一个都没挪位**——Espryt 的 `kBlendSpanBegin`/`kBlendSpanEnd` 与整张手推偏移表因此仍然有效。三个都在 pipeline 半边(setter 都走 `BumpVersions()`)。 +- 整块:`RenderStateParameters` 是平凡可复制 POD,Espryt 做 head/blend/tail 三段 memcmp,**字段顺序承重**;拆成三个 CSO 要手工维护 ~150 字段划分表且无绊线。 +- 子集身份:整块内容寻址会让 `glViewport`/`glScissor`/`glClearColor` 每次铸造新 CSO、冲掉 server 的 pipeline memo(`RenderState.h` 记录的那次回归)。 +- 划分只写在一处:`MG_Pipe/MGPipeRenderStateSpans.{h,cpp}` 的 chunk 表(16 个 `constexpr` 边界,两半交替:**7 个 pipeline chunk 共 396 B + 8 个 dynamic chunk 共 772 B = 1168**,总数与划分完整性是 `static_assert`)+ `MGPipeComputePipelineSubsetHash()`,client 与两个后端共用。G7 的 `RenderStateSpansTest` 遍历每个 setter,断言 `pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变`。 +- server 侧:每 context 一份 working `RenderStateParameters`,`bind` 与 `set_dynamic_state` 各把自己的 chunk 散射进去;**Espryt 的 `SyncRenderState` 一行不动**;Magma 的 pipeline memo 键是 `cso.slot`。 +- client 取值顺序:`m_pipelineStateVersion` 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 396 B 子集算 xxHash → map 探测 → 命中发 12 B bind,未命中发 create 再 bind;`m_version` 变而子集未变 → 只发 `set_dynamic_state`。 +- P2 补了三个此前无存储的 capability:`FramebufferSrgb`、`DepthClamp`、`TextureCubeMapSeamless`,落在 `ColorMasks` 与 `ClearColor` 之间的 3 字节空洞,`sizeof` 仍 1168、既有成员一个没挪位。 ### 5.4 验证不变式、合并与抑制器 -规范(D-B3):**一条 verb 的全部 `set_*`/`bind_*` 必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态惰性特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,`set_*` 之间没有顺序要求。** 推荐实现顺序(framebuffer → program → 纹理/sampler/image/buffer/global constants → render state/dynamic → vertex elements/buffers/index/attrib defaults → patch/XFB → verb)只是代码组织,不是契约。退役 Espryt 的 fragColor 重推导 workaround、`g_broadcastMemo*` 与 `ImageUnitFormatsStillMatch` 的机制是惰性特化,不是调用顺序。 +规范(D-B3):**一条 verb 的全部 `set_*`/`bind_*` 必须在该 verb 之前完成;server 在 verb 处从它此刻持有的全部已推送状态惰性特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,`set_*` 之间没有顺序要求。** 退役 Espryt 的 fragColor 重推导 workaround、`g_broadcastMemo*` 与 `ImageUnitFormatsStillMatch` 的机制是惰性特化,不是调用顺序。 -`create_shader_state` 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server——monolith 拿不到的异步收益。 +`create_shader_state` 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server。 -四条合并规则:整块结构优于逐字段;高水位标记(`GetTouchedBindPointCount`、`GetMaxTouchedUnit`)留在 tracker 走查里,直接就是 `count` 实参;只发 program 解析过的集合(`uniformSamplerOrImageUnitIndex`);**集合 hash 抑制器**——每条 `kVarTail` `set_*` 在 client 算已解析集合的 xxHash,未变不发。最后一条是从后端搬到 client 的 ~175 行去抖(`UnitBindingsSnapshot`/`PairingsIntact`/`g_fboTextureSyncList` 族)的载体:`GetTextureBindGeneration()` 在冗余重绑时也 bump(MC 26.2 每次纹理单元切换都重绑同一个 sampler),没有抑制器每个 batch 都会重发一条几百字节的变长记录并冲掉 server 的两个 memo。 - -索引绑定范围在 validate 时刻实时解析(`glBindBufferBase` 之后再 `glBufferData` 是普通应用代码)。 +合并规则:整块结构优于逐字段;高水位标记留在 tracker 走查里、直接是 `count` 实参;只发 program 解析过的集合;**集合 hash 抑制器**——每条 `kVarTail` `set_*` 在 client 算已解析集合的 xxHash,未变不发(从后端搬到 client 的 ~175 行去抖;MC 26.2 每次纹理单元切换都重绑同一个 sampler)。索引绑定范围在 validate 时刻实时解析。 ### 5.5 sampler view 在 client 侧解析 -GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 sampler uniform 类型、mipmap 完备性(`IsMipmapCompleteForFilter`、`SamplesAsIncompleteTexture`)与 `IsUndefinedDefaultTexture`。gallium 的"每槽一个 view"就是解析后的形态,解析留在 client 并带自己的 memo(~40 行搬迁)。两处后端特定后处理留在 server、作用于已解析集合:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。 +GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 sampler uniform 类型、mipmap 完备性与 `IsUndefinedDefaultTexture`。gallium 的"每槽一个 view"就是解析后的形态,解析留在 client 并带自己的 memo。两处后端特定后处理留在 server:Espryt 的 raw-depth-fetch sampler 替换、Magma 的 feedback-loop 检测。 ### 5.6 生命周期、共享组、composite program -- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义;`resource_destroy` 在析构时发。三条顺序约束由 payload 表达:view 先于存储属主销毁(`ViewOf` + server keep-alive)、FBO attachment 钉住纹理(surface handle 隐含 keep-alive)、buffer texture 钉住 buffer(`BufferForTexBuffer`,范围实时解析)。 -- 共享组:v1 一个 screen、一个 context、一条 flow;`eglMakeCurrent` 是 flow 所有权转移,在既有 `EGLOperationMutex` 下发射(顺手让 `ReleaseThread` 与 `SwapInterval` 也取该锁)。 -- program pipeline 合成体(**P4a 落地:`MobileGL/MG_Impl/Pipe/CompositeResolver.h`**):`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr` 推**一个** handle,slot 从 `ShaderCso` 保留高位段分配(段基 983040,独立稠密表——按 slot 索引的向量在这个段上要 ~236 MB),pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。**释放有两条路,都必须恰好一次**:合成体自己被换掉时由 resolver 释放,程序对象死亡时由 `~ProgramObject` → `MGPipeEmitShaderCsoDestroyAndFree` 释放(D-H7)。**记忆按 `(ContextId, 管线 GL 名)` 键控**——resolver 是进程级单例而 GL 名是每上下文的,只按名字记会在两个上下文用同一个管线名时把对方还活着的合成体释放掉(复审在真实 make-current 序列上打出来的);`Reset()` 只清"新鲜度",不清"该释放"这件事,否则一次 make-current 之后释放路径就永久失灵。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。 +- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义;`resource_destroy` 在析构时发。顺序约束由 payload 表达:view 先于存储属主销毁(`ViewOf` + server keep-alive)、FBO attachment 钉住纹理、buffer texture 钉住 buffer。死亡在 wire delete 与 slot free 之间转发给每个 emitter(六个死亡 helper 只释放 slot 曾导致 UAF)。 +- 共享组:v1 一个 screen、一个 context、一条 flow;`eglMakeCurrent` 是 flow 所有权转移。 +- program pipeline 合成体(`MG_Impl/Pipe/CompositeResolver.h`):`GLContext::GetProgramForDraw()` 完全在前端合成,tracker 推**一个** handle,slot 从 `ShaderCso` 保留段分配,pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。释放有两条路且必须恰好一次:合成体被换掉时由 resolver 释放,程序对象死亡时由 `~ProgramObject` 释放(D-H7)。**记忆按 `(ContextId, 管线 GL 名)` 键控**——resolver 是进程级单例而 GL 名是每上下文的。 ### 5.7 emulation 的归属 @@ -230,122 +209,96 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp | emulation | 归属 | 过线的是什么 | |---|---|---| -| client 顶点数组(`(first+count-1)*stride+elementSize`) | client | 字节(`MGHostSpan`),永不是指针 | -| 最大索引扫描(`TryComputeMaxIndexFromHostBytes`,唯一无界的应用指针读,只有 client 同时持有两个数组) | client | `MGPDrawInfo::MinIndex/MaxIndex`(flag 门控,`~0` = 未知) | +| client 顶点数组 | client | 字节(`MGHostSpan`),永不是指针 | +| 最大索引扫描(`TryComputeMaxIndexFromHostBytes`) | client | `MGPDrawInfo::MinIndex/MaxIndex`(flag 门控) | | client 索引数组 | client | 变长尾里的 `MGHostSpan` | -| `*IndirectCount` 计数解析(从 parameter buffer 的 shadow 读实际 draw 数) | client | 解析后的 `MGPDrawRange[]`(几十字节) | -| primitive-restart 重写(整 EBO 重写,`kMaxRestartRewriteBytes` = 64 MiB) | **server** | 零线上流量:从索引宿主镜像读(§10.3) | -| multi-draw 五档分档 + 展平(`ResolveTierForBatch`,CPU 展平是回退) | **server** | 同上 | -| viewport-array N 遍回放 | server | 无新增:16 组 viewport/scissor/depth-range 已在渲染状态里 | +| `*IndirectCount` 计数解析 | client | 解析后的 `MGPDrawRange[]` | +| primitive-restart 重写(整 EBO) | **server** | 零线上流量:从索引宿主镜像读(§10.3) | +| multi-draw 五档分档 + 展平 | **server** | 同上 | +| viewport-array N 遍回放 | server | 无新增(16 组 viewport/scissor/depth-range 已在渲染状态里) | | fp64 顶点窄化 | server | 原始字节;`IsLong` 与 `Type` 分开过线 | | image-bindable 存储加宽/拆分 | server | 正向 `ImageBindableHint`;反向纹理拉取 + 终止符 | -| 生成 mipmap 的前端存储 | 拆开:client 分配 level 存储,server 生成 | `MGPMipPlan`;`OnMipLevelsGenerated` 只带形状不带字节 | -| CopyImage shadow 镜像 | client | 只回"拷贝成功",删掉一整条 server→client 字节通道 | -| XFB CPU 图元计数 | client | `XfbCpuCapturedVertices`(flag 门控)+ `EndStreamOutput` 的 `MGPXfbAccounting` | -| XFB scatter 的 read-modify-write | client | §8.5 | +| 生成 mipmap 的前端存储 | 拆开:client 分配 level 存储,server 生成 | `MGPMipPlan`;`OnMipLevelsGenerated` 只带形状 | +| CopyImage shadow 镜像 | client | 只回"拷贝成功" | +| XFB CPU 图元计数 / scatter 的 read-modify-write | client | `XfbCpuCapturedVertices`、`MGPXfbAccounting`;§8.5 | | 压缩纹理 / pixel unpack 规整 | client | 无 | -**陈旧索引纪律是逐站点表,不是一条笼统规则**(client 侧扫描/解析之前要做的 reconcile 必须逐字复现 monolith 的集合): - -| client 侧动作 | 必须做的 reconcile | -|---|---| -| client 顶点数组范围计算 + 暂存 | 无(应用内存,无 GPU 写者) | -| 最大索引扫描(EBO 源) | `SyncPersistentMappedRange()` **+** `SyncGpuWrites()` | -| 最大索引扫描(client 指针源) | 无 | -| `*IndirectCount` 计数解析 | **只** `SyncPersistentMappedRange()`,不加 `SyncGpuWrites()`——monolith 今天就只做这一个,加了会给 Create/Flywheel 的每 batch 平白加一次 publish-and-wait | -| server 侧 restart 重写 / multi-draw 展平 | server 从镜像读;GPU 写者可见性由 `OnGpuWritten` 收窄集在 server 本地判定 | - -前两条 client reconcile 的形态:publish → 等 `appliedSeq` → 排空事件 → 再碰 shadow。门:`ClientArrayAfterComputeWriteScenario`(去掉等待必须看到几何缺失);`create-indirect` fixture 上 `roundtrips-per-frame` 必须读零(P8)。 +**陈旧索引纪律是逐站点表**,client 侧扫描/解析之前的 reconcile 必须逐字复现 monolith 的集合:最大索引扫描(EBO 源)= `SyncPersistentMappedRange()` **+** `SyncGpuWrites()`;`*IndirectCount` 计数解析 = **只** `SyncPersistentMappedRange()`(monolith 今天就只做这一个);client 顶点数组范围计算与 client 指针源扫描 = 无;server 侧重写 / 展平从镜像读,GPU 写者可见性由 `OnGpuWritten` 收窄集在 server 本地判定。门:`ClientArrayAfterComputeWriteScenario`(去掉等待必须看到几何缺失);`create-indirect` fixture 上 `roundtrips-per-frame` 必须读零(P8)。 ## 6. 纹理 subdata 与 dirty 归属 -- `glTexSubImage*` 根本不调后端表:全部纹理上传由 Espryt 在 sync 时刻按**累积**区域做,那里跑 `MipmapStorage` 的 96-rect 级联合并与 `summedArea*4 >= unionArea*3` 的 union-box 回退,并在 unpack ring 可用时刻意塌成一个 box——Mali 按**作业数**给上传计价,~100 个精灵 rect 对一个 union box 实测 +6 ms/frame。逐 `glTexSubImage` 发一条记录会精确复现那个形状。 -- 因此:client 在自己的 `MipmapStorage` rect 模型里累积,在**下一个 validate / flush 点**把合并后的形状作为**一条** `ResourceSubData` 发出。`MOBILEGL_PIPE_STATS` 单列逐帧发射次数与上传作业数(`TextureUploadEmissions/Box/Rect/Jobs`)。 -- **同时携带 union box 与 region 列表,由 server 选上传形状**:决策留在付 GPU 代价的那一侧。实测(`MEASUREMENTS.md`):vanilla 世界同样 185 次发射,Espryt 的整 box 路径每帧 635 KB 纹素、Magma 的 rect 路径 40 KB,16×。 -- `MGPSubRegion` 显式携带 `SrcRowStride/SrcSliceStride`,`MGPSubData::SourceIsVerbatimLevelShadow` 显式携带原来由 `uploadData == mipData` 指针比较回答的问题:"这批字节是未经转换的 level shadow 吗"。split 下 client 既不发整 level 也不在 server 留整 level 镜像,指针比较不成立;Espryt 的上传路径改为从描述符取步长,`UNPACK_ROW_LENGTH` 从 `SrcRowStride/bpp` 设。形状照抄已存在的 `UnpackStagingBlock`(ring 路径本来就紧密重打包、不发 `glPixelStorei`)。 -- **dirty 归属反转**:client 保留 rect 模型、维护一份发射游标、发射后清自己的标志,server 从不碰 client 的标志。安全,因为 `MG_Impl` 里没有任何 `IsStorageDirty/GetStorageDirtyRects/GetStorageDirtyRegion` 调用点(前端从不读自己的 dirty 状态)。逐 level "server 权威位"与纹理 ack 协议因此不必存在。 -- 发射游标按**存储属主**键控 `(storageOwnerHandle, ownerUploadTarget, ownerLevel)`:`TextureObjectView` 把 dirty 查询/清除全部转发给属主并做索引重映射,view 与属主共用同一份 dirty 状态。门:通过 view 上传、经属主采样(及反向),跨 draw 边界各一次。 - -> **P4a 落到哪一步**:**排水列表(drain list)是这一波的**——client 在 validate 点把每个脏的 `(存储属主, 上传目标, level)` 发成**一条** `resource_subdata`,逐区带 `SrcOffset`/`SrcRowStride`/`SrcSliceStride`,`RegionCount == 0` 合法且表示"并集框就是全部"。**上面那条按存储属主键控的发射游标与索引重映射仍然是 P3b/P4b 的**,P4a 没有做。两条 P4a 学到的规矩写在这里:(1) **清 dirty 要等 applier 的 acceptance**,不是发射即清——`MGPipeApplyResourceSubData` 返回 `Bool`,被拒的记录必须让 level 留在脏表里(否则拉取路径再也不会补它);(2) **服务端自己的重新变脏是服务端的事**——`RequireImageBindableStorage` 会让一个已经上传过的 level 重新需要上传,client 的标志早就清了,所以 twin 直接重新武装 applier 记录里的待上传项。`TextureUploadShapeScenario`(形状金标)**已经建好但只记录不设门**,要等 Mali 侧的帧时增量才升级成门。 -- 后端真正在 shadow 里写字节的两处——CPU 回退生成 mip(RGB16F/RGB32F)与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。 -- Unpack PBO 完全在 client 解析;压缩纹理永不到达后端;`glCopyTexSubImage*` 与 `glClearTexImage` 整体留在 client(今天就是纯前端操作:借一次 `ReadPixels` 进 CPU scratch 再写 shadow),拆分后恰好是一次阻塞 ReadPixels round trip,脏区按普通 subdata 下发。 +- `glTexSubImage*` 根本不调后端表:全部纹理上传由 Espryt 在 sync 时刻按**累积**区域做(96-rect 级联合并、union-box 回退、unpack ring 可用时塌成一个 box)——Mali 按**作业数**给上传计价,~100 个精灵 rect 对一个 union box 实测 +6 ms/frame。逐 `glTexSubImage` 发一条记录会精确复现那个形状。 +- 因此 client 在自己的 `MipmapStorage` rect 模型里累积,在**下一个 validate / flush 点**把合并后的形状作为**一条** `ResourceSubData` 发出,**同时携带 union box 与 region 列表,由 server 选上传形状**(决策留在付 GPU 代价的那一侧;`MEASUREMENTS.md` §1.3:vanilla 世界同样 185 次发射,Espryt 整 box 635 KB/帧、Magma rect 40 KB)。 +- `MGPSubRegion` 显式携带 `SrcRowStride/SrcSliceStride`,`MGPSubData::SourceIsVerbatimLevelShadow` 显式回答原来由指针比较回答的问题;Espryt 的上传路径改为从描述符取步长。 +- **dirty 归属反转**:client 保留 rect 模型、维护发射游标、发射后清自己的标志,server 从不碰 client 的标志(`MG_Impl` 里没有任何 `IsStorageDirty` 调用点)。**P4a 落地的是排水列表**——client 在 validate 点把每个脏的 `(存储属主, 上传目标, level)` 发成一条 `resource_subdata`;按存储属主键控的发射游标与 view 索引重映射是 P3b/P4b 的。两条规矩:**清 dirty 要等 applier 的 acceptance**(被拒的记录必须让 level 留在脏表里);**服务端自己的重新变脏是服务端的事**(twin 直接重新武装 applier 记录里的待上传项)。 +- 后端在 shadow 里写字节的两处——CPU 回退生成 mip 与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。Unpack PBO 完全在 client 解析;`glCopyTexSubImage*` 与 `glClearTexImage` 整体留在 client。 +- `TextureUploadShapeScenario`(形状金标)已建好但只记录不设门,等 Mali 侧的帧时增量才升级成门。 ## 7. Shader state = SPIR-V + 反射归档 -- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 client,SPIRV-Cross(`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。 -- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。**P4a 落地:序列化器在树里了**(`MobileGL/MG_Pipe/ProgramArtifactsCodec.{h,cpp}`),但 **monolith 只在 verify 构建里调它**——句柄臂上 server 读的仍是前端自己的归档(D-H3:制品不在 monolith 过线,编解码因此不在热路径上),真正逐帧走 codec 是 P5 之后的事。**未完的一件配套**:`ProgramArtifacts.h` 的 libc++/NDK 尺寸钉仍是惰性的(`MGL_ARTIFACT_SIZES_LIBCXX_PINNED` 没定义),也就是说 Android 构建上这个 58 成员结构还没有绊线——记在 P3b/P4b 的清单里。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex`、`uniformBlockBinding`、`shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings`、`xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture`、`computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples`、`uniformOffsets`。`XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。 -- **P0.5 硬前置**:反射类型今天声明在 `ProgramObject.h` 里,而它 include `ShaderObject.h`(→ glslang)与 `SpvcSession.h`(→ spirv_reflect)。P0.5 把 `TypeFacts`、`ResourceReflection`、`XfbVarying`、`LinkArtifacts`、`SpirvArtifacts` 抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`(只 include `` 与容器),8 个 includer 靠类内 `using` 别名零改动,加 CI `-H` 闭包断言(已落地,见 `ROADMAP.md` P0.5 行;`DynamicBackendParameters` 留在 `BackendObject.h`,所以闭包门 A 断言的是 `MGPipeValueTypes.h` 而不是 `MGPipeTypes.h`)。同批抽取 `MG_Pipe/MGPipeValueTypes.h`(`MAX_DRAW_BUFFERS`、`PerBufferBlendState`、`StencilFaceState`、`PixelStoreParameters`、`RenderStateParameters`、`SamplerParameters`、`BorderColorForm`、`VertexAttribute`、`VertexBufferBindingPoint`),它不 include `MG_State/GLState` 任何东西;`MGPipeTypes.h` 今天为此临时 include 了 `BackendObject.h` 与 `RenderState.h`(文件头注明为 P0.5 债务)。没有这一步,P7 的 `nm -D | grep glslang` 判据不可达。 -- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 snorm/unorm clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度与 XFB 布局),`create_shader_state` 发布**制品**,server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法,也是 gallium `st_variant` 的做法。 -- 后端 link/compile 失败不需要同步返回:今天只是一行 `MGLOG_E` 加 bind program 0 的空 draw,`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答。`OnLog` 逐字复现——由此要求日志按严重级分级(§8.3)。 -- Magma 的两个内部 shader(blit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 `MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。 +- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。glslang 全在 client,SPIRV-Cross 全在 server,文件级切割;没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。 +- 归档机制:`Visit()` + `sizeof` 绊线。序列化器在树里(`MG_Pipe/ProgramArtifactsCodec.{h,cpp}`),**monolith 只在 verify 构建里调它**(D-H3:制品不在 monolith 过线,编解码不在热路径上)。未完:`ProgramArtifacts.h` 的 libc++/NDK 尺寸钉仍是惰性的(`MGL_ARTIFACT_SIZES_LIBCXX_PINNED` 未定义),记在 P3b/P4b。 +- P0.5 把反射类型抽到 `MG_State/GLState/ProgramState/ProgramArtifacts.h`、值类型抽到 `MG_Pipe/MGPipeValueTypes.h`(不 include `MG_State/GLState`),CI `-H` 闭包断言把关;没有这一步 P7 的 `nm -D | grep glslang` 判据不可达。 +- server 侧惰性特化(D-B2):后端 program 还依赖 8 个额外输入(draw FBO 的 clamp mask、fragColor 广播数、storage-block 绑定签名、atomic counter 集、活的 image 格式、patch 参数;Magma 另加 Y-flip 高度与 XFB 布局),`create_shader_state` 发布**制品**,server 在 verb 时刻从已推送状态特化——正是两个后端今天的做法。 +- 后端 link/compile 失败不需要同步返回:`GL_LINK_STATUS` 永不撤回,同步查询由 client 从 `ProgramObject` 回答,失败以 `OnLog` ≥ERROR 无损呈现。 +- Magma 的两个内部 shader(blit、depth-mipmap)烘焙成签进树的 SPIR-V,`MG_Test` 重跑树内 glslang 逐字节比对守新鲜度(`MOBILEGL_BAKED_INTERNAL_SHADERS`,P7)。 ## 8. 反向通道 -### 8.1 `MGPipeCallbacks`(P0 已落地,`MobileGL/MG_Pipe/MGPipeCallbacks.h:27-51`) +### 8.1 `MGPipeCallbacks`(`MG_Pipe/MGPipeCallbacks.h`) -十个具名回调 + 一个正向终止符(`ResourceSubDataComplete`),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 `SEG_EVENT` 上的记录。 +十个具名回调 + 一个正向终止符(`ResourceSubDataComplete`),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 `SEG_EVENT` 上的记录。 | 回调 | 取代 | |---|---| -| `OnGlError(code)` | 6 处 `RecordError`;**必须对命令流有序**,否则 `glGetError` 答错(`glGetError` 本身永远本地) | -| `OnGpuWritten(res, ranges[])` | 6 处 `MarkGpuWritten`:client 在每个 draw/dispatch 发射点**保守自建** pending 集,这是**收窄**通道 | -| `OnBufferWriteback(res, offset, bytes)` | PBO 回读、XFB 捕获;**按操作级批处理**(今天两处逐行循环绝不能变成每扫描线一次 IPC);必须与 epoch bump 有序 | +| `OnGlError(code)` | 6 处 `RecordError`;**必须对命令流有序**(`glGetError` 本身永远本地) | +| `OnGpuWritten(res, ranges[])` | 6 处 `MarkGpuWritten`:client 在每个 draw/dispatch 发射点保守自建 pending 集,这是**收窄**通道 | +| `OnBufferWriteback(res, offset, bytes)` | PBO 回读、XFB 捕获;按操作级批处理;必须与 epoch bump 有序 | | `OnTextureWriteback(res, box, bytes)` | CPU 回退生成 mip 的纹素(唯一生产者) | | `OnTexturePullRequest(res, target, firstLevel, levelCount, pullSerial)` | §8.4 | -| `OnMipLevelsGenerated(res, base, count)` | 只带形状:monolith 的 `EnsureGenerateMipmapStorageAllocated` 也只 `AllocateStorage` + `MarkStorageDirty(false)` 不填内容,split 行为一致 | +| `OnMipLevelsGenerated(res, base, count)` | 只带形状,与 monolith 的 `EnsureGenerateMipmapStorageAllocated` 行为一致 | | `OnSurfaceChanged(info)` | `SwapchainObject` 写 `pDefaultFramebufferInfo` 的分层倒置;client 自己合成 default-FB 对象 | | `OnCapsInvalidated()` | 2 处 `InvalidateCompileEnv` | | `OnLog(level, text)` | ≤WARN 有损,≥ERROR 无损 + 速率限制 | | `OnXfbScatterReady(scratch, packedStride, vertices)` | §8.5 | -95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账(零消息);后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendResource` 删除(server 拥有资源表)——**[deviation] D-K(P3a):P3a 并没有删它**。`PipeResource::m_backend`、`SetBackendResource`、`ReleaseBackend`、`BackendBufferResource` 在 push 下只是**不再被写**(buffer twin 已经搬进第七张 slot 表,§9.5),真删掉会移动 pull 构建里 `sizeof(BufferObject)`,那是直接的 G1 破坏;这组删除随 pull 路径在 **P13** 退役。`SetBackendStateMemo`(前端 VAO 里存后端堆裸指针)直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 `ResolveUniformBufferPayload` 把具名 UBO 打进自己的 UBO ring → `SetShaderBuffers` 的 host payload(D-B8)。 +95 个写回点的其余归属:`MarkStorageDirty` 大多是 server 本地记账;后端凭空造的前端对象(Magma 占位纹理、swapchain default-FB 占位)→ server 原生;`SetBackendStateMemo` 直接删除;`SetBackendHashMemo/AuxMemo` → server 侧 per-slot 字段。**D-K(P3a)**:`PipeResource::m_backend` / `SetBackendResource` / `ReleaseBackend` / `BackendBufferResource` 在 push 下只是不再被写,真删会移动 pull 构建里 `sizeof(BufferObject)`(G1 破坏),随 pull 路径在 P13 退役。 ### 8.2 有序性是正确性要求 -每一次 `WritebackFromBackend` 后面都紧跟 `BumpBufferMutationEpoch()`,否则 server 的 draw-clean memo 会在 epoch 背后变陈旧——split 里这变成反向通道上的排序规则:写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。** +每一次 `WritebackFromBackend` 后面都紧跟 `BumpBufferMutationEpoch()`,否则 server 的 draw-clean memo 会在 epoch 背后变陈旧——split 里这是反向通道上的排序规则:写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。**反向通道需要与正向通道相同的有序保证。** ### 8.3 错误、ack 与日志 -- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*` 只 `MarkStorageDirty`,Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。 -- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack:41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 `glGetError`;语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。**P3a 起 `ResourceRespecify` 携带 `kNeedsAck`,并且带一个逐记录谓词**:flag 是**逐调用**的静态属性,而 `ResourceRespecify` 同时服务 `glBufferData` 与 `glBufferStorage`,裸 flag 会把 Minecraft 的整块 chunk 上传变成每 store 一次往返。所以真正拍板的是 `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)`(`MobileGL/MG_Pipe/MGPipeTypes.h:680`),`PipeCalls.def:18-24` 的图例把这条规则写在目录里(调用行 `:82`),`MG_Test/Pipe/PipeCatalogueTest.cpp:552-585` 的 `ResourceRespecifyAcksOnlyImmutableStorage` 对两种惯用法各钉一次。monolith 下 ack 是 `((void)0)`(applier 只隔一次函数调用),P5 把门铃接到这个谓词上。**P4a 把这个谓词收窄到 buffer 目标(D-A2)**:纹理与 renderbuffer 从这一波起也走 `resource_respecify`,而 `glTexStorage*` 的 `Immutable` 同样是 1——不收窄的话每一次不可变纹理分配都会变成一次同步往返。收窄之后 `ResourceRespecify` 还多了两条 P4a 的语义:一是**逐 level 作用域**(带 `MGPRespecifiedLevel` 尾,说明这次重定义的是哪一级;空指针才是"整个资源"),二是**元数据更新**(存储定义字段与已存描述符逐项相同时,只换 `BindMask`/`ImageBindableHint`,不 ack、不清待上传)——前者是终审抓出的 critical:不带 level 时 `glTexImage2D(level 1)` 会把同一纹理上已经被 applier 接受、客户端也已清了 dirty 标记的第 0 级待上传一起丢掉,画面读回全黑。 -- 其余错误一律晚到,走有序的 `OnGlError`。 -- `OnLog` 分级:≤WARN 有损(覆盖最旧 + `eventDropped` 计数);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed";`MGLOG_E_ONCE` 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。 +- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻,拆分不改变可观察行为,不同步 ack。**唯一允许同步 ack 的入口是 `glBufferStorage`**;`glRenderbufferStorage*` 不 ack(41 个 trace fixture 里 OOM 探测惯用法出现 0 次,语料里的成功性检查是 `glCheckFramebufferStatus`,client 本地作答)。 +- `ResourceRespecify` 携带 `kNeedsAck` 并带逐记录谓词 `MGPipeResourceRespecifyNeedsAck(desc)`(`Immutable != 0` **且是 buffer 目标**,D-A2——`glTexStorage*` 的 `Immutable` 同样是 1,不收窄每次不可变纹理分配都会变成同步往返);另有**逐 level 作用域**(`MGPRespecifiedLevel` 尾;不带 level 时 `glTexImage2D(level 1)` 会把已接受的第 0 级待上传一起丢掉)与**元数据更新**(描述符逐项相同时只换 `BindMask`/`ImageBindableHint`,不 ack、不清待上传)。 +- 其余错误一律晚到,走有序的 `OnGlError`。`OnLog` ≤WARN 有损(覆盖最旧 + `eventDropped`);≥ERROR 无损,加入触发 `eventRingFull` + 停止 apply 的语义事件集,每秒速率限制器。 ### 8.4 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6) -server 不保留纹素,三个原因会要求重发已发过的 level:`RequireImageBindableStorage` 的 re-dirty、整格式再生、view 源重铸。四条缓解同时上: - -1. **预防主因**:client 给纹理打 `everImageBound`,`ResourceCreate/Respecify` 一直携带 `ImageBindableHint`,image-bindable 存储前期分配好。 -2. **拉取异步**:server 发 `OnTexturePullRequest` 并把 twin 标 not-ready,client 下次 publish 时重发;阻塞的是 `mgl-srv-apply` 线程不是应用线程。 -3. **有上限的保留,默认关**:`MOBILEGL_PIPE_TEXEL_RETAIN_MB` 默认 0——`MipmapStorage` 保有每 level 完整 CPU 影子,拉取总能被服务,缓存买的是延迟不是正确性。只有实测拉取率非平凡才开。 -4. **显式终止符**:拉取是 request/response 对,由 `ResourceSubDataComplete(res, target, firstLevel, levelCount, pullSerial)` 终止,**可携带零个 region**——内容只来自渲染、被 `CanMirrorCopyImageShadow` 拒绝的 copy、或 GPU 侧 mip 生成的 level,client 根本没有字节;收到零 region 时 server 带着"已分配但为空"的存储继续(正是 monolith 的行为)并记 `MGLOG_W`。没有终止符 apply 线程会永久 park。 - -门:`TextureRemintPullScenario`(含无解用例,且在终止符落地前必须是红的);拉取次数逐 trace 用例发布。本设计从不声称"零 round trip",它测量并公布。 +server 不保留纹素,三个原因会要求重发已发过的 level:`RequireImageBindableStorage` 的 re-dirty、整格式再生、view 源重铸。四条缓解:(1) **预防主因**:`ResourceCreate/Respecify` 一直携带 `ImageBindableHint`;(2) **拉取异步**:server 发 `OnTexturePullRequest` 并把 twin 标 not-ready,阻塞的是 apply 线程不是应用线程;(3) 有上限的保留默认关(`MOBILEGL_PIPE_TEXEL_RETAIN_MB=0`,`MipmapStorage` 保有每 level 完整 CPU 影子,缓存买的是延迟不是正确性);(4) **显式终止符** `ResourceSubDataComplete`,可携带零个 region(内容只来自渲染 / GPU mip 的 level,client 没有字节;server 带着"已分配但为空"的存储继续)。真实语料上重铸拉取率见 `ROADMAP.md` 开放问题 2(780 个窗口 2 次)。P5b 下这条路径是 `Fatal{UnmigratedEmulation,"texture-remint-pull"}`(三条 trace 的首阻塞),P9 落地。 ### 8.5 XFB scatter 搬到 client -Espryt 的 `ScatterCapturedRecords` 是对 client shadow 的 read-modify-write:从应用已有的字节起步,只把捕获到的 varying 补进去(`gl_SkipComponents` 的空洞保留应用原本的内容,`KHR-GL46.transform_feedback.capture_special_interleaved_test` 走到它)。server 没有 `MappedData()`,所以:server 把紧密打包的 scratch 通过 `OnBufferWriteback` 推给 client,用 `OnXfbScatterReady` 告知布局;client 拥有目的 shadow 与反射归档里的 varying/stride,原样跑补丁循环;补好的范围作为普通 `ResourceSubData` 重发并 bump change serial。不新增停顿类。 +Espryt 的 `ScatterCapturedRecords` 是对 client shadow 的 read-modify-write(`gl_SkipComponents` 的空洞保留应用原本的内容)。server 没有 `MappedData()`,所以 server 把紧密打包的 scratch 经 `OnBufferWriteback` 推给 client,用 `OnXfbScatterReady` 告知布局;client 拥有目的 shadow 与反射归档里的 varying/stride,原样跑补丁循环,补好的范围作为普通 `ResourceSubData` 重发。不新增停顿类。 ## 9. 后端状态机改造 ### 9.1 原样不动的东西 -Espryt:三条 persistent-mapped ring 与 `PersistentRing` 算法、buffer pool、7 条 fallback-repack 路径、`m_backendColorSlots` 置换表、三个 scratch FBO 及驱动侧影子、`PackState`、全部驱动绑定影子、Adreno 禁用属性 SIGSEGV workaround、Mali XFB 捕获丢失 workaround、`ScopedDefaultUnpackState`、SPIRV-Cross 会话与 post-emission ESSL 重写、驱动 POST 自检族、restart 重写与 multi-draw 五档。 -Magma:`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory`、`ProgramFactory`、`UniformManager` 的 ring 与描述符集、五个 `Vk*Manager`、`FrameContext`、`SwapchainObject`、`DynamicStateShadow`、`VertexInputStateFactory` 的 cache 本体、**D18 的节点式容器纪律**(`m_renderbufferResources`/`m_textureResources` 故意用 `std::unordered_map`,调用方跨查表缓存 `Resource*`;postmortem 注释逐字进 review checklist)。 +Espryt:三条 persistent-mapped ring 与 `PersistentRing` 算法、buffer pool、fallback-repack 路径、`m_backendColorSlots` 置换表、scratch FBO 及驱动侧影子、全部驱动绑定影子、Adreno / Mali workaround、SPIRV-Cross 会话与 ESSL 重写、驱动 POST 自检、restart 重写与 multi-draw 五档。Magma:`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory`、`ProgramFactory`、`UniformManager`、五个 `Vk*Manager`、`FrameContext`、`SwapchainObject`、`DynamicStateShadow`、`VertexInputStateFactory` 的 cache 本体、**D18 的节点式容器纪律**。从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6)。 -从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。 +**字节一致门 G5**(`scripts/p3a_untouched_regions.sh`:pool / 延迟释放 / ring / flush 梯十一函数;`scripts/p4a_untouched_regions.sh`:depth-stencil 采样模拟与格式 caveat 两族 17 区 / 3 文件):这些是"被推送的记录改变了输入、但算法本身不许动"的那一类,字节一致是唯一能证明这点的门;两份脚本都对固定 pin 比较,re-pin 必须同时改两份。仍在 `MG_Backend` 里的前端类型:`g_rawDepthFetchSamplerState`(Espryt 自己为原始深度取样铸的 `SamplerObject`,client 从没见过它),原生化归 P3b/P4b。 -**P4a 的字节一致行**(`scripts/p4a_untouched_regions.sh`,17 个区 / 3 个文件,8 个阴性对照按名变红;与 P3a 的十一函数并列):Espryt 的 depth-stencil 采样模拟与格式 caveat 两族——`ShouldUseCaveatTextureFormat`、`BackendTextureFormatAddsAlpha`、`StageBlocksIntoUnpackRing`、`UnpackRingAvailable`、`UnpackRingAllocate`、`RecomputeBackendColorSlots` 等——它们是"被推送的记录改变了输入、但算法本身不许动"的那一类,字节一致是唯一能证明这点的门。**仍在 `MG_Backend` 里的前端类型**:`g_rawDepthFetchSamplerState` 是一个前端 `SamplerObject`(Espryt 自己为原始深度取样铸的,client 从没见过它,因此永远不会有记录——句柄臂上"对象即权威"的那条分支就是为它留的),它的原生化归 P3b/P4b,是句柄化之后 Espryt 侧最大的一处残留。 - -唯一两处必须真改的 `MG_State` 类型内部用法(都在 Magma):占位纹理(构造真的 `TextureObject2D*` 只为复用 `SyncTextureAndGetDescriptor(ITextureObject&)` 签名,~120 行木偶戏 → ~60 行原生 `VkImage`+view+descriptor,34 个 `MOBILEGL_ASSERT(pGLContext)` 里的 9 个随之消失);两个内部 shader 烘焙(§7)。Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。 +必须真改的 `MG_State` 类型内部用法(都在 Magma,P7):占位纹理(~120 行木偶戏 → 原生 `VkImage`)、两个内部 shader 烘焙(§7)。 ### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1) ```cpp -// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织(~20 KB,字段集全迁移期稳定) +// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织 struct PipeInputs { - const RenderStateParameters& GetRenderStateParameters() const; // 阶段 A:类型与后端今天读到的完全一致 - // … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 55) + const RenderStateParameters& GetRenderStateParameters() const; // 类型与后端今天读到的完全一致 + // … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 56) #if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED Uint64 m_filledGen[kFieldCount]; // 逐字段"上次填充的 verb 序号" Uint64 m_currentVerbSerial; @@ -360,272 +313,255 @@ struct PipeInputs { | 阶段 | 改什么 | 证明 | |---|---|---| -| A 别名 | 机械 `sed`:`MG_State::pGLContext->` → `MGB_CTX->`(**实测 277 处**,293 出自过期的 vendored 清单)+ 手工转换 58 行非箭头用法(~34 处 `MOBILEGL_ASSERT` 删除、7 处空守卫、3 处三元、`.get()` 裸指针捕获与 `decltype` 别名、14 处 `!= nullptr`、1 处注释);逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因(空守卫/三元的重写推迟到 P2) | -| B 推送 | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 | -| C 句柄化 | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) | +| A 别名(P1) | 机械替换 `MG_State::pGLContext->` → `MGB_CTX->`(277 处)+ 58 行非箭头用法;逐 verb 类填充点填 `gPipeInputs` | `nm --defined-only` 不变;`.text` 差异可逐行归因 | +| B 推送(P2) | tracker 填 `gPipeInputs`,填充器按 `MOBILEGL_PIPE_PUSH` 位图逐字段让位 | `MOBILEGL_PIPE_VERIFY=1`:tracker 再填一份快照版,G4 比对器逐字段每 draw 比一次 | +| C 句柄化(P3a–P4a、P7) | `SharedPtr<前端对象>` 字段 → `MGPipeHandle` + POD 描述符;memo 重键;写回变回调 | 全套门(§13) | -- 填充点逐 verb 类,不只 `PrepareForDraw`/`SetupDraw` 两处:G5 从 `PipeCalls.def` 生成"每个 `kCtxVerb`/`kCtxObject` 调用可能读哪些字段"的表,在 `MG_Impl` 的 ~93 个边界站点生成 validate/fill 调用。 -- poison 是**逐 verb 世代**不是位图:每次 verb 递增 `m_currentVerbSerial`,字段被填时记下序号,读取时断言相等(跨 verb 有效的字段显式标 sticky)。位图看不见"上一个 draw 填过、紧随的 `glTexSubImage` 读到陈旧值"。debug 与 disaggregated 构建里读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}`。纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`。 +poison 是**逐 verb 世代**不是位图:每次 verb 递增 `m_currentVerbSerial`,字段被填时记下序号,读取时断言相等(跨 verb 有效的字段显式标 sticky)。读一个当前 verb 未填的字段是 `Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}`。纯度门 grep 的是 `pGLContext` 不是 `pGLContext->`。`FillPoints.def` 里 8 条静态过近似的填充行有意保留(能退役它们的证据只能是动态的:`MOBILEGL_PIPE_POISON_OMIT` 跑遍完整 CTS caselist,P3a 之后未做)。 ### 9.3 Track V / Track H -- Track V(值类型:`GetRenderStateParameters`、`GetPixelStoreParameters`、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。 -- Track H(对象类型:167 个 `SharedPtr` 点):真活。 -- 读点分类实测(静态):A 探测变化 ~35(12%)、B 翻译输入 ~216(74%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"bump 一个版本让 server 自己拉"行不通,值本身必须过去。 +Track V(值类型:render state、pixel store、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma 标量 getter,约 B 类读点的 55%):机械。Track H(对象类型:167 个 `SharedPtr` 点):真活。读点分类(静态):探测变化 ~12%、翻译输入 ~74%、瞬时参数 / 身份键 / 数据字节其余——74% 是翻译输入,"bump 一个版本让 server 自己拉"行不通,值本身必须过去。 ### 9.4 残余值块 -Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)`,payload `ResidualValueBlock{RenderStateParameters, PixelStoreParameters, CapabilityBits, patch 三字段}`。三条纪律:退役是编译错误(`MGL_RESIDUAL_BLOCK_SIZE` 只降不升,`MobileGL/MG_Pipe/MGPipeTypes.h:546`,P13 变成 `static_assert(sizeof == 0)`)——P2 已把它从 **1248 棘轮到 8**:`create/bind_render_state` + `set_dynamic_state`、`set_pixel_pack_state`、`set_patch_state` 落地之后,块里只剩一个 `Uint64 CapabilityBits`;布局逐成员 `offsetof` 断言且 split 下逐字段序列化(异质 POD 并集的 padding 差异 monolith verify 看不见);只在 P2..P13 存在,`MOBILEGL_PIPE_STATS` 单独计一类字节(`ResidualValueBlock`,P0 已占位)。 +P2 发一个**显式临时**调用 `SetResidualValueState(MGPBlobRef)`,payload `ResidualValueBlock`。三条纪律:退役是编译错误(`MGL_RESIDUAL_BLOCK_SIZE` 只降不升,P2 已从 1248 棘轮到 **8**——只剩 `Uint64 CapabilityBits`,P13 变成 `static_assert(sizeof == 0)`);布局逐成员 `offsetof` 断言且 split 下逐字段序列化;`MOBILEGL_PIPE_STATS` 单独计一类字节。 ### 9.5 21 条身份 memo 的重键 -统一事实:每个进入 memo 键的版本计数器要么是回绕 `Uint16`,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。`{slot, gen}` + 显式 destroy 让 **11 条直接删除**(registry 的同址 `weak_ptr` + GC ×6、`TwinLookupMemo` ×3 + `OwnerEquals`、`UnitSamplerLookupMemo` 的 `WeakPtr` 测试、`SetBackendStateMemo`、`VkTextureManager::TextureIdentity` 存活探测、`ConvertedVertexStreamKey` 的 `sourcePin`……),**2 条** server 删除但去抖搬到 client(§5.4),**7 条重键**成更便宜的比较(`StampSyncedFBO` 四元组 → `ContentHash` + server 私有 `attachmentRemintEpoch`;`ResolvedTextureBindingMemo` 9 键 → `(shaderCso.slot, viewSetSerial)`;`SetupDrawSnapshot` 的 ~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty mask;`VertexInputStateFactory::ComputeHash` 里的 lifetimeId → `gen` **混进** server 侧每个 content hash),**1 条**(D18)原样不动。两个顺带修掉的潜伏 bug 已先独立落地:`m_xfbCounterSlotByObject` 用裸 GL name 做键(`bd2b4158`)、`RenderbufferObject` 缺 `GetLifetimeId()`(`9c7339b2`)。 +统一事实:每个进入 memo 键的版本计数器要么是回绕 `Uint16`,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。`{slot, gen}` + 显式 destroy 让 **11 条直接删除**(registry 的同址 `weak_ptr` + GC ×6、`TwinLookupMemo` ×3 + `OwnerEquals`、`SetBackendStateMemo`、`VkTextureManager::TextureIdentity` 存活探测、`ConvertedVertexStreamKey` 的 `sourcePin`……),**2 条** server 删除但去抖搬到 client(§5.4),**7 条重键**成更便宜的比较(`StampSyncedFBO` 四元组 → `ContentHash` + server 私有 `attachmentRemintEpoch`;`ResolvedTextureBindingMemo` 9 键 → `(shaderCso.slot, viewSetSerial)`;`SetupDrawSnapshot` 的 ~14 探测字段 → 三个 handle + 两个 server 纪元 + dirty mask;`VertexInputStateFactory::ComputeHash` 里的 lifetimeId → `gen` 混进 server 侧每个 content hash),**1 条**(D18)原样不动。P2 付了 11 条删除与 2 条重键,P3a 付了 buffer/VAO 的重键(`MEASUREMENTS.md` §4.3)。 ### 9.6 A/B 与口径收窄 -`MOBILEGL_PIPE_PUSH` 子系统位图(含一位关闭 CSO 内容寻址,负面对照)在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时 `SnapshotFromGLContext()` 仍要合成句柄,后端仍跑重键后的 memo 代码,一个重键 bug 两臂都在。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)在 P3a/P4a 期间保留 registry / `TwinLookupMemo` 实现活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役(各阶段 +1 天维护)。 +`MOBILEGL_PIPE_PUSH` 子系统位图在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时后端仍跑重键后的 memo 代码。对策:**编译期** `MOBILEGL_PIPE_LEGACY_MEMOS`(默认 ON)保留 pre-handle 臂活在同一个 `PipeInputs` 接口之下,随 pull 路径在 P13 退役。 -**P3a 的臂,逐项点名**(两个子系统的 arm 判定在 `MobileGL/MG_Backend/DirectGLES/Managers.cpp:2292`(resources,位 7)与 `:2312`(vertex input,位 8),逐进程各打一行 `MGLOG_D`): +- buffer 家族的 pre-handle 臂是 `Ops_*` 表加 `g_glesBufferBackendOps`,无条件编译;VAO twin 与 P4a 六种 twin 的前端读取臂在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下,位清零 + `LEGACY_MEMOS=0` 是没有任何臂的配置,arm resolver **响亮地**报 `Fatal{PipeLegacyMemosDisabled}` 而不是静默。 +- 位依赖两侧都拒:服务端在各族 arm resolver 里拒绝并跑旧臂,客户端在 `PipeFill.cpp` 的族门里**根本不发射**——只在服务端拒会出现"客户端已按 acceptance 清了 dirty、服务端却走旧臂"的丢上传。同一族门上挂着**消费者条件**:没有任何后端注册 `MGPipeResourceOps` 时 P4a 四族一条不发(Magma 就是这种情形,P7 之前)。 +- 退役的 twin 成员(VAO twin 的五个同步 memo、`g_pendingFetchBaseInstance` 与 `ScopedFetchBaseInstance`、D-K 那组)都**仍在 pull 构建里编译**——真删会移走 pull 符号或改 `sizeof`,G1 的 0/0/0/0 就是这条的度量。 -- **buffer 家族的 pre-handle 臂是 `Ops_*` 表加 `g_glesBufferBackendOps`,它们无条件编译**,不在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下——所以位 7 单独清零永远有一条真臂可跑,`NoArm` 对这个家族不可达(`Managers.cpp:2294-2298` 把这句写在代码里)。前端的十一处分发点(`MobileGL/MG_State/GLState/BufferState/BufferObject.cpp:45`、`:71`、`:87`、`:103`、`:237`、`:375`、`:438`、`:450`、`:493`、`:602`、`:656`)按 `MGPipeResourceSubsystemEnabled()` 二选一。 -- **VAO twin 的前端读取臂是有条件的**:pre-handle 的 `SyncToBackend` 本体与它读的那组 memo(`m_syncedIndexBufferVersion` / `m_syncedIndexBufferObject` / `m_hasSyncedConfigVersion` / `m_syncedConfigVersion` / `m_syncedAttributeVersions`)都在 `MOBILEGL_PIPE_LEGACY_MEMOS` 里(`MobileGL/MG_Backend/DirectGLES/Managers.h:1109-1130`)。位 8 清零 + `LEGACY_MEMOS=0` 是**没有任何 vertex-input 臂**的配置,`Managers.cpp:2343-2351` 明确报 `PipeLegacyMemosDisabled` 而不是静默。位 8 还要求位 7(属性的缓冲 id 经资源 slot 表解析),`0x17f` 会点名拒绝并回落到 legacy 臂。 -- **P4a 的六种 twin 同样有条件臂**(纹理、renderbuffer、framebuffer、sampler、sampler view、program):pre-handle 的本体与它读的那批身份 memo 都在 `MOBILEGL_PIPE_LEGACY_MEMOS` 之下,`#if !MOBILEGL_PIPE_LEGACY_MEMOS` 的分支留着但不可达(arm resolver 在第一次查找就停进程),保持**响亮**。这条开关给这一波多加了约一天:pull 构建被 `CMakeLists.txt` 强制打开它,所以每个"删掉旧 memo"的动作都只是让新臂不再读它、`sizeof` 一点不动——G1 的 0/0/0/0 正是这条的度量,真正的删除跟着 pull 路径在 P13 发生。 -- **base-instance 的 ambient 作用域同在其中**:`SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance` 与 `DirectGLES.cpp:5293` 的三个 scope(`Managers.h:1189-1200`)。句柄臂改由 `MGPipeApplierState::VertexFetchBaseInstance` 供给,但这三个声明**不能删**:删掉会从 pull 构建移走两个符号(G1)。 - -P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;**保留 `MOBILEGL_PIPE_VERIFY` 连同它需要的 `SnapshotFromGLContext()` 与 `MG_State` include**(D-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。 +P13:删 `SnapshotFromGLContext()` 的非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;**保留 `MOBILEGL_PIPE_VERIFY`**(D-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。 ## 10. server 侧 ### 10.1 对象表与 applier -- `MG_Remote/Server/PipeObjectTables`:按 kind 的 slot 数组,不是对象图;server 不持有任何 buffer 的完整副本、不持有纹素、不持有前端对象图。 -- `PipeApplier`:解码 → 更新对象表与 `PipeInputs` → 调后端函数指针。debug 断言:任何传输下都不得有 `SharedPtr` 或裸前端指针跨过 applier 边界。`InProcessTransport` 走与 spawn **完全相同**的 G3 编解码路径,只在门铃/拷贝机制上不同。 +- `MG_Remote/Server/PipeApplier`:解码 → 更新对象表与 `PipeInputs` → 调后端函数指针。server 不持有任何 buffer 的完整副本、不持有纹素、不持有前端对象图;debug 断言任何传输下都不得有 `SharedPtr` 或裸前端指针跨过 applier 边界。`InProcessTransport` 走与 spawn **完全相同**的 G3 编解码路径,只在门铃/拷贝机制上不同。 - 每 context 一份 working `RenderStateParameters`(§5.3)。 ### 10.2 monolith 侧的净收益 -即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;`SwapchainObject` 写 `MG_Impl` 的分层倒置消失;两个潜伏 bug 已修;一次 glslang 编译离开启动路径;`inproc` = 渲染线程;`MG_Test` 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是**增加**的(约 +6,650 手写 + 4,000 生成,对 ~372 行真删除),所以 monolith 论据是逐线程 CPU 数字(§13.2-④),不是删除行数。 +即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;`SwapchainObject` 写 `MG_Impl` 的分层倒置消失;一次 glslang 编译离开启动路径;`inproc` = 渲染线程;`MG_Test` 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是**增加**的,所以 monolith 论据是逐线程 CPU 数字,不是删除行数。 -### 10.3 索引宿主镜像(`MG_Remote/Server/IndexHostMirror`,P8) +### 10.3 索引宿主镜像(`Server/IndexHostMirror`,P8) -- 覆盖:`BindMask & ELEMENT_ARRAY` 的资源,且仅当 `kCapNeedsHostIndexBytes`(split 且 server 需要索引字节做 restart 重写 / multi-draw 展平)。 -- 由 server 本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护:零额外线上流量、零 round trip。GPU 写者对镜像的影响由 `OnGpuWritten` 收窄集在 server 本地判定。 -- 预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),逐帧发布 `index-mirror-bytes`;超预算时该 buffer 退化为逐 draw 经 `MGHostSpan` 传送(`Seg` 指向 `SEG_STAGE`),计入 `index-bytes-shipped`。 -- 必须是它:`kMaxRestartRewriteBytes` = 64 MiB 是默认 `SEG_STAGE` 的两倍,`kMaxFlattenedIndices` = 1<<24 同量级,逐 draw 塞进 32 MiB 的段既不可行也无必要。它是本设计里唯一的"数据副本"。 +覆盖 `BindMask & ELEMENT_ARRAY` 的资源,且仅当 `kCapNeedsHostIndexBytes`。由 server 本来就要收的 `ResourceCreate/Respecify/SubData` 流增量维护:零额外线上流量、零 round trip;GPU 写者对镜像的影响由 `OnGpuWritten` 收窄集在 server 本地判定。预算 `MOBILEGL_PIPE_INDEX_MIRROR_MB`(默认 64),超预算时该 buffer 退化为逐 draw 经 `MGHostSpan` 传送(计入 `index-bytes-shipped`)。必须是它:`kMaxRestartRewriteBytes` = 64 MiB 是默认 `SEG_STAGE` 的两倍。它是本设计里唯一的"数据副本"。 -## 11. 传输与数据面(骨架 P0 已落地,`MobileGL/MG_Remote/`) +## 11. 传输与数据面(`MobileGL/MG_Remote/`) ### 11.1 段 -| 段 | 拥有者 | 默认 | 内容 | -|---|---|---|---| -| `SEG_CMD` | client(server 只读) | 8 MiB,2 的幂 | `RingControl`(4 KiB 页)+ POD 记录 + ≤4 KiB 内联负载 | -| `SEG_STAGE` | client | 32 MiB,上限实测定 | bulk 字节:buffer sub-data、纹理紧密重打包区域、UBO scratch、client 顶点/索引/indirect 数组、multi-draw 参数块、具名 UBO host payload、persistent-map 脏块 | -| `SEG_REPLY` | server(client 只读) | 16 MiB,8 个 2 MiB slot(ID-47:按最大场景读回 640×480 RGBA8 定尺,超过即 client 侧具名拒绝) | readback 像素、buffer writeback | -| `SEG_EVENT` | server | 256 KiB SPSC ring | 十个回调的事件 + `EvQueryResult/EvFenceSignaled/EvReadbackDone` | -| `SEG_SHADOW[n]` | client | 每对象,≥256 KiB shadow(Phase 2) | 零拷贝 buffer/texture shadow | -| `SEG_ADOPT[n]` | server(client RW) | 每 buffer,≥16 MiB adopted store(P11) | 应用直写 GPU 内存 | +| 段 | id | 拥有者 | 默认 | 内容 | +|---|---|---|---|---| +| `SEG_CMD` | 1 | client(server 只读) | 8 MiB,2 的幂 | `RingControl`(4 KiB 页)+ POD 记录 + ≤4 KiB 内联负载 | +| `SEG_STAGE` | 2 | client | 32 MiB | bulk 字节:buffer sub-data、纹理紧密重打包区域、UBO scratch、client 顶点/索引/indirect 数组、multi-draw 参数块、具名 UBO host payload、persistent-map 脏块 | +| `SEG_REPLY` | 3 | server(client 只读) | 16 MiB,8 个 2 MiB slot(ID-47) | readback 像素、buffer writeback、acceptance 答案 | +| `SEG_EVENT` | 4 | server | 256 KiB SPSC ring | 十个回调的事件 + `EvQueryResult/EvFenceSignaled/EvReadbackDone` | +| `SEG_SHADOW[n]` | 5 | client | 每对象 ≥256 KiB(Phase 2) | 零拷贝 buffer/texture shadow | +| `SEG_ADOPT[n]` | 6 | server(client RW) | 每 buffer ≥16 MiB(P11) | 应用直写 GPU 内存 | -创建(`ShmSegment`):Android `ASharedMemory_create`(API 26;libc 的 `memfd_create` wrapper 是 API 30);桌面 Linux `syscall(SYS_memfd_create)`;其他 POSIX `shm_open`+`shm_unlink`;Windows `CreateFileMappingW`(`Local\`)。传递:POSIX `SCM_RIGHTS`(`FdPassing`,专用 `AF_UNIX SOCK_DGRAM` socketpair——消息边界保住 ancillary data 与 payload 不被拆开,sideband ≤256 B);Windows 段名走 `SegmentRef`。fd 传递在第一个 transport commit 里实现——没有它数据面在唯一重要的平台上一字节过不去。 +`SEG_STAGE` 默认 32 MiB 装不下 P5b 目标负载的单次 128 MiB 上传;**默认不改**,普查与设备用显式 `MOBILEGL_IPC_STAGE_MB=256` profile,分块 / 专用 carrier 是 P8 的设计(`ROADMAP.md` 开放问题 11)。 -不进 `SEG_STAGE` 的:restart 重写的整 EBO 与 multi-draw 展平的索引流(走索引镜像)。`SEG_SHADOW` 块的退休规则:释放的块进 pending 链表,`appliedSeq`(借入 GPU 时间线的 slot 用 `retiredSeq`)越过最后一条引用它的记录后才归还 arena。 +创建(`ShmSegment`):Android `ASharedMemory_create`(API 26);桌面 Linux `memfd_create`;其他 POSIX `shm_open`;Windows `CreateFileMappingW`。传递:POSIX `SCM_RIGHTS`(`FdPassing`,专用 `AF_UNIX SOCK_DGRAM` socketpair);Windows 段名走 `SegmentRef`。不进 `SEG_STAGE` 的:restart 重写的整 EBO 与 multi-draw 展平的索引流(走索引镜像)。`SEG_SHADOW` 块的退休:释放的块进 pending 链表,`appliedSeq`(借入 GPU 时间线的 slot 用 `retiredSeq`)越过最后一条引用它的记录后才归还。 ### 11.2 `RingControl`(`Ring.h`) -一页 4 KiB,每个争用组各占一条 cache line:`SEG_CMD` 游标三元组 `cmdHead / cmdAppliedTail / cmdRetiredTail`;`SEG_STAGE` 独立三元组(`stageHead / stageAppliedTail / stageRetiredTail`——"`SEG_STAGE` 余量 < 1/4"是 publish 触发器,占用率不能从另一个 ring 算出,且 stage slot 的退休条件不同);三个严格区分的水位 `appliedSeq`(释放 `*AppliedTail`)/ `submittedSeq`(释放 staging)/ `retiredSeq` + `completedFrameSerial`(释放 `*RetiredTail` 与 `SEG_ADOPT`)+ `presentAckSerial`;`serverEpoch`(context 丢失 / server 重启 ++)、`ringGeneration`(硬 drain 后 ++,作废缓存 offset)、`consumerParked`/`producerParked`、`eventRingFull`、`eventDropped`。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次,那种 slot 只能在 `completedFrameSerial` 之后回收。游标是单调字节计数、2 的幂掩码、永不重置。 +一页 4 KiB,每个争用组各占一条 cache line:`SEG_CMD` 游标三元组 `cmdHead / cmdAppliedTail / cmdRetiredTail`;`SEG_STAGE` 独立三元组;三个水位 `appliedSeq`(释放 `*AppliedTail`)/ `submittedSeq`(释放 staging)/ `retiredSeq` + `completedFrameSerial`(释放 `*RetiredTail` 与 `SEG_ADOPT`)+ `presentAckSerial`;`serverEpoch`、`ringGeneration`、`consumerParked`/`producerParked`、`eventRingFull`、`eventDropped`。两个 tail 是必须的:P11 之后 server 会**借用** ring slot 而不是再拷一次。游标是单调字节计数、2 的幂掩码、永不重置。 -记录头 `RingRecordHeader{kind, flags, size}`,kind 0 保留给 wrap 填充;`RingProducer::Reserve` 在记录会跨 wrap 边界时自动发 pad 记录,保证每条记录连续;`MaxRecordBytes() == Capacity()/2`;`RingConsumer::Pop` 拒绝不可能的头(非 8 对齐、小于头、大于已发布)并置 corrupt → `Fatal{ProtocolCorruption}`;`HardDrainRing` 只在两侧静默且 ring 全空时 bump generation。 +记录头 `RingRecordHeader{kind, flags, size}`,kind 0 保留给 wrap 填充;`RingProducer::Reserve` 在记录会跨 wrap 边界时自动发 pad 记录;`RingConsumer::Pop` 拒绝不可能的头并置 corrupt → `Fatal{ProtocolCorruption}`;`HardDrainRing` 只在两侧静默且 ring 全空时 bump generation。 ### 11.3 双向 doorbell(`Doorbell.h`) -- client → server:consumer 自旋 → 置 `consumerParked=1` → 阻塞;producer release-store `cmdHead` 之后仅当 `consumerParked` 时敲(字节码 `0x01`)。 -- server → client:client 在**任何**等待(present credit、`kNeedsAck`、ring/stage 满)先自旋 `MOBILEGL_IPC_SPIN_US`(默认 50 µs)→ 置 `producerParked=1` → 阻塞;server 在 release-store 任何 watermark 之后仅当 `producerParked` 时敲(`0x02`)。没有第二个方向,每处 client 等待都退化成跨进程自旋一条 cache line——手机上一颗大核满频空转一整帧,而全库没有亲和性控制。 -- 两个实现,零 futex/eventfd/named-event 平台代码:`CondVarDoorbell`(`inproc`,带 `Kill()` 死亡态让 `Shutdown` 能 join 一个 parked 的等待者)与 `SocketDoorbell`(`spawn`,一字节;`SOCK_STREAM` 端在对端关闭时报 `POLLIN|POLLHUP` + `recv()==0`,这是死亡检测)。 -- 丢失唤醒窗口由**两个 `seq_cst` fence** 关闭(等待者置标志 → fence → 再测条件;通知者发布 watermark → fence → 读标志),标志本身的访问是 relaxed。`NotifyIfParked` 的前置条件:watermark 已发布。死亡的 doorbell 让 `Wait` 停止重新 park。 +- client → server:consumer 自旋 → 置 `consumerParked=1` → 阻塞;producer release-store `cmdHead` 之后仅当 `consumerParked` 时敲。 +- server → client:client 在**任何**等待先自旋 `MOBILEGL_IPC_SPIN_US`(默认 50 µs)→ 置 `producerParked=1` → 阻塞;server 在 release-store 任何 watermark 之后仅当 `producerParked` 时敲。没有第二个方向,每处 client 等待都退化成跨进程自旋一条 cache line。 +- 两个实现:`CondVarDoorbell`(`inproc`,带 `Kill()` 死亡态让 `Shutdown` 能 join 一个 parked 的等待者)与 `SocketDoorbell`(`spawn`,一字节;对端关闭时 `POLLIN|POLLHUP` + `recv()==0` 是死亡检测)。丢失唤醒窗口由两个 `seq_cst` fence 关闭。 ### 11.4 控制面(`protocol.fbs`、`Framing.h`、`ITransport.h`) -- 一份 schema,两种用法:热路径 → FlatBuffers `struct`(定长、无 vtable、只需边界检查)直接进 ring——即 G3 生成的记录,与 `MGPipeTypes.h` 的 POD 逐条 `static_assert` 尺寸/`offsetof` 对齐;罕见/变长/需演进 → `table` 走 CTRL socket。今天 `protocol.fbs` 只含控制面(`MobileGL/MG_Remote/Protocol/protocol.fbs:218-228` 的 `CtrlMsg`:`Hello`、`Welcome`(四个段的 `SegmentRef`)、`CapsSnapshot`、`SurfaceOp/SurfaceReply`、`ResyncRequest/Done`、`AuxRequest`(外来线程的 fence wait / query result / scalar get)、`Fatal`(`ProtocolCorruption/RingOverrun/SegmentMismatch/DeviceLost/ServerCrashed/AbiMismatch`)、`LogLine`),`file_identifier "MGLC"`;union tag 是 wire 值,只追加。 -- `protocol_generated.h` 提交进树,`scripts/gen_protocol.py` 再生成(只用 `MOBILEGL_FLATC_EXECUTABLE` 或从 pinned submodule 在仓库外构建一次的 flatc,不用 PATH 上的),CI `flatc-check`(`.github/workflows/test.yml:304`)重生成并 diff。**codegen 绝不进默认构建图**;运行时 header-only。 -- 封帧 `[u32 'MGLF'][u32 len][payload]`,64 MiB 上限,**读时校验**:坏 magic / 超长长度立即 latch 失败并报 `MOBILEGL_ERR_PROTOCOL_MISMATCH`(不是静默永久挂起);接收缓冲不足**返回所需大小并保留消息**(`MOBILEGL_ERR_BUFFER_TOO_SMALL`)。 -- `ITransport`:`SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown / Role`;热路径完全绕过它。`Shutdown` 拆掉整个连接(两端都不能再发,等待者全部解锁,已排队消息仍可读完)。`WireLog.h` 是唯一的日志入口,让 `Transport/` 的头不 include 前端 umbrella(纯度门 A 断言 `-H` 输出)。 -- `mg_protocol_base.h`:纯 C、无依赖的结果码 / span / `ShmRegion` / id 词汇,structSize-first 版本纪律(追加 = minor,改动 = major,major 不符是结构化失败)。 +- 一份 schema,两种用法:热路径 → FlatBuffers `struct` 直接进 ring(即 G3 生成的记录,与 `MGPipeTypes.h` 的 POD 逐条 `static_assert` 对齐);罕见/变长/需演进 → `table` 走 CTRL socket(`CtrlMsg`:`Hello`、`Welcome`、`CapsSnapshot`、`SurfaceOp/SurfaceReply`、`ResyncRequest/Done`、`AuxRequest`、`Fatal`、`LogLine`),`file_identifier "MGLC"`,union tag 只追加。 +- `protocol_generated.h` 提交进树,`scripts/gen_protocol.py` 再生成(只用 `MOBILEGL_FLATC_EXECUTABLE` 或 pinned submodule 构建的 flatc),CI `flatc-check` 重生成并 diff;**codegen 绝不进默认构建图**。 +- 封帧 `[u32 'MGLF'][u32 len][payload]`,64 MiB 上限,**读时校验**:坏 magic / 超长长度立即 latch 失败并报 `MOBILEGL_ERR_PROTOCOL_MISMATCH`;接收缓冲不足返回所需大小并保留消息。 +- `ITransport`:`SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown / Role`;热路径完全绕过它。`WireLog.h` 是 `Transport/` 唯一的日志入口(纯度门 A)。`mg_protocol_base.h`:纯 C 的结果码 / span / `ShmRegion` 词汇,structSize-first 版本纪律。 ### 11.5 WAR 危害、拷贝账与背压 -- Phase 1(P5–P8):GL 调用时刻把字节拷进 ring slot,slot 到 `stageAppliedTail` 越过它为止不可变,危害按构造消除;代价一次 memcpy,`Ops_ResidentSubData` 与 `StageBlocksIntoUnpackRing` 在 monolith 里已经在付。 -- Phase 2(shadow-in-shm,零拷贝):≥256 KiB 的 shadow 分配在 `SEG_SHADOW`(`PipeResource::MapAlignedAllocator` 增加 shm arena,保留 64 B 对齐契约;`MipmapStorage` 的 level vector 同理),`ResourceSubData` 只带 `{seg, offset, size}`。WAR 用 per-shadow 64 KiB 块发送水位:应用写某块而该块上次发送尚未被 `appliedSeq` 覆盖 → 这次写走 `SEG_STAGE`。必须整段 `#if MOBILEGL_BUILD_DISAGGREGATED` 包裹(改容器 allocator 就改了类型,option OFF 时逐字折叠回今天的 allocator)。 +Phase 1(P5–P8):GL 调用时刻把字节拷进 ring slot,slot 到 `stageAppliedTail` 越过它为止不可变,危害按构造消除;代价一次 memcpy。Phase 2(shadow-in-shm,零拷贝):≥256 KiB 的 shadow 分配在 `SEG_SHADOW`,`ResourceSubData` 只带 `{seg, offset, size}`,WAR 用 per-shadow 64 KiB 块发送水位;必须整段 `#if MOBILEGL_BUILD_DISAGGREGATED` 包裹。 | 路径 | monolith | Phase 1 | Phase 2 | |---|---|---|---| -| `glBufferSubData` → shadow store | 2 | 3 | **2** | +| `glBufferSubData` → shadow store | 2 | 3 | 2 | | `glBufferSubData` → adopted store(P11) | 2 | 2 | 2 | | `glMapBufferRange(WRITE)`+unmap | 3 | 4 | 3 | | persistent coherent map 推送(§12) | 0 | 1/发射点 | 1/发射点(精确块) | | `glTexSubImage` | 2 | 2 | 2 | | 全局 UBO / draw | 1 | 2 | 1 | -| adopted ≥16 MiB(P11 T1/T0) | 0 | 0 | 0 | +| adopted ≥16 MiB(P11) | 0 | 0 | 0 | -server 没有第二份 `BufferObject`,所以不存在"staging → server 侧 shadow"这次中间拷贝。字节计数器装在 wire 两侧,验收看总量。 - -- 分配与背压:逐字移植 `PersistentRing`(单调 head/tail、2 的幂掩码、frame mark)。分配失败升级:扩容(翻倍)→ 对最老未 retire 批次有界等待(默认 50 ms,走 `producerParked` doorbell)→ 硬 `Drain` + `ringGeneration` bump。硬 drain 后恢复便宜:正向流是自洽的推送流,tracker 把全部 dirty 位置为"必须重推",下一个 verb 重发完整 `set_*` 集合,纹理侧由发射游标负责,没有"重发未 apply 对象状态"的特殊协议。`SEG_CMD` 与 `SEG_STAGE` 各自独立跑这套升级。 +server 没有第二份 `BufferObject`,不存在"staging → server 侧 shadow"这次中间拷贝。分配与背压:逐字移植 `PersistentRing`;分配失败升级:扩容 → 对最老未 retire 批次有界等待(走 `producerParked` doorbell)→ 硬 `Drain` + `ringGeneration` bump;硬 drain 后恢复便宜:tracker 把全部 dirty 位置为"必须重推"。P5 落地形状:staging 与 command ring 的等待都以未退休发布记录的真实退休为条件(`ringwaits` 只计真正阻塞的分配),超大单记录仍拒绝。 ### 11.6 publish、序号与 credit -- 不设"records ≥ 64 KiB"一类阈值(那是一整帧的流水线气泡,且否掉 `inproc` 的全部意义)。规则:每条记录(或每 8–16 条摊销)release-store `cmdHead`,仅当 `consumerParked` 时敲门铃。 -- 显式门铃点:`present`、任何 `kNeedsAck` 请求、`eglMakeCurrent`、`glFlush`(刷出不等待)、`SEG_STAGE` 余量 < 1/4、**轮询类入口**(`glClientWaitSync` 任意 timeout、`glGetSynciv(GL_SYNC_STATUS)`、`glGetQueryObject*(AVAILABLE|NO_WAIT)`——否则 `while (glClientWaitSync(s, FLUSH_COMMANDS_BIT, 0) == TIMEOUT_EXPIRED) {}` 永久自旋);带 `GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish。 -- 饥饿升级:同一 handle 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)本地回答"未就绪"而 watermark 毫无移动 → 升级为一次阻塞 round trip。 -- `glFinish`/`glFlush` 保持纯 no-op。 -- seq = 记录序数;两个互相独立的窗口:字节 credit(两个 ring 各自占用)与 present credit(`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT` 时 `eglSwapBuffers` 阻塞)。server 不发 credit 消息:对 `RingControl` release store,consumer 每 64 条记录更新一次 `appliedSeq`,`producerParked` 时敲反向门铃。 +- 不设"records ≥ 64 KiB"一类阈值。规则:每条记录(或每 8–16 条摊销)release-store `cmdHead`,仅当 `consumerParked` 时敲门铃。 +- 显式门铃点:`present`、任何 `kNeedsAck` 请求、`eglMakeCurrent`、`glFlush`、`SEG_STAGE` 余量 < 1/4、**轮询类入口**(`glClientWaitSync` 任意 timeout、`glGetSynciv(GL_SYNC_STATUS)`、`glGetQueryObject*(AVAILABLE|NO_WAIT)`);带 `GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish。饥饿升级:同一 handle 连续 N 次(`MOBILEGL_IPC_POLL_ESCALATE`,默认 64)本地回答"未就绪"而 watermark 毫无移动 → 升级为一次阻塞 round trip(P10)。 +- `glFinish`/`glFlush` 保持纯 no-op。seq = 记录序数;两个互相独立的窗口:字节 credit 与 present credit(`presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT` 时 `eglSwapBuffers` 阻塞)。server 不发 credit 消息,consumer 每 64 条记录更新一次 `appliedSeq`。 -### 11.7 事件回传与溢出 +### 11.7 事件回传与溢出(P9) -`SEG_EVENT` 承载十个回调加回读完成通知。client 排空点:`glGetError`、`glGetQueryObject*`、`glClientWaitSync`、`glGetSynciv`、`eglSwapBuffers`、`glMapBuffer*`/`glGetBufferSubData`/`glCopyBufferSubData`,以及**每一次等待循环的每一轮**。溢出策略(修一个双向死锁:client 卡在 present credit、server apply 线程卡在生产事件):`EvLogLine` ≤WARN 有损;语义承载事件(`EvGpuWritten`、`EvReadbackDone`、`EvFenceSignaled`、writeback、pull request、mip、scatter、`EvGlError`、surface、caps、`EvLogLine ≥ERROR`)无损——ring 满时 server 置 `eventRingFull=1`、**在记录边界停止 apply**、敲反向门铃,client 排空后清标志并敲正向门铃;ERROR 速率限制器。故障注入:client 被 credit 阻塞时灌满 `SEG_EVENT`;日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧恢复。server 侧 `MGLOG` 按流顺序 replay 进 client 日志流(复用 `DeferredLogLine` 机制)。 +`SEG_EVENT` 承载十个回调加回读完成通知。client 排空点:`glGetError`、`glGetQueryObject*`、`glClientWaitSync`、`glGetSynciv`、`eglSwapBuffers`、`glMapBuffer*`/`glGetBufferSubData`/`glCopyBufferSubData`,以及每一次等待循环的每一轮。溢出策略(修一个双向死锁):`EvLogLine` ≤WARN 有损;语义承载事件无损——ring 满时 server 置 `eventRingFull=1`、**在记录边界停止 apply**、敲反向门铃,client 排空后清标志并敲正向门铃。故障注入:client 被 credit 阻塞时灌满 `SEG_EVENT`;日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧恢复。 -### 11.8 fence 与无 present 负载 +### 11.8 fence 与无 present 负载(P10) -- fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位:DirectGLES 的 `g_completedFrameSerial` 只在 `Present()` 与 `WaitForFrameSerialCompleted` 里前进,帧中 fence 会退化成帧计数推断——`DirectVulkan.cpp` 写明这是被修掉的 bug(MC 1.21.5 的 fence-paced ring 曾因此 native-heap OOM)。规则:`FenceCreate` 转成真实的后端 `FenceSync()`,server 用自己已有的逐 fence 轮询在非 present 时刻也推进并发 `EvFenceSignaled`。 -- 无 present 循环(CTS、回读循环、从不 swap 的集成场景)下 `retiredTail` 会饿死、`SEG_STAGE` 填满、每个用例都跑到硬 drain。规则:DirectGLES 的 server 加**非 present fence tick**——距上次 `Present` 超过 8 ms 或每 4096 条已 apply 记录插一个 `glFenceSync` 并轮询 fence ring;ring 占用率与升级次数进计数器;P8 加一个无 present 的 split 用例。 +fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位(DirectGLES 的 `g_completedFrameSerial` 只在 `Present()` 里前进,帧中 fence 会退化成帧计数推断——`DirectVulkan.cpp` 写明这是被修掉的 bug)。无 present 循环下 `retiredTail` 会饿死:DirectGLES 的 server 加**非 present fence tick**(距上次 `Present` 超过 8 ms 或每 4096 条已 apply 记录插一个 `glFenceSync`);P8 加一个无 present 的 split 用例。P5b 已把五条 sync 槽搬上 apply 线程(§17.5)。 ## 12. persistent map 与 ≥16 MiB 采纳 -`AcquirePersistentMap` 是永久的地址空间捐赠(返回 host-visible coherent 指针,成为该 buffer 的唯一真相源;≥16 MiB 可变 store 由 `TryAdoptLargeStorage` 自动走到,实测 MC 26.3 p99 163→21 ms、40→115 fps、省 ~400 MB)。**整个 monolith 改造期一动不动**(D-B4),只有 IPC 那一步会打破它。 +`AcquirePersistentMap` 是永久的地址空间捐赠(返回 host-visible coherent 指针;≥16 MiB 可变 store 由 `TryAdoptLargeStorage` 自动走到,实测 MC 26.3 p99 163→21 ms、40→115 fps、省 ~400 MB)。**整个 monolith 改造期一动不动**(D-B4)。 -三档,由运行时 POST 探针选择(本项目"后端限制一律探针判定、不硬编码驱动名"的既定规则),**spike B 已在两台设备上给出答案**(`MEASUREMENTS.md` §2): +三档,由运行时 POST 探针选择,spike B 已在两台设备上给出答案(`MEASUREMENTS.md` §1.2): | 档 | 形态 | 实测 | |---|---|---| -| **T0 — server 导入 client 分配**(P11 主攻) | client 分配 `AHardwareBuffer` BLOB,socket 交接;server 以 `VK_ANDROID_external_memory_android_hardware_buffer`(Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`(Espryt)导入,两侧 persistent+coherent 映射 | **Adreno 830 与 Mali 都是完整读写往返**,含 GPU 访问与两侧字节校验——唯一在两台设备、两个后端上都成立的档 | -| T1 — server 导出自己的映射 | `VK_KHR_external_memory_fd` opaque fd,client `mmap` + 导入 | 只有 Adreno 的 Vulkan 路径可用;Adreno 的 GLES 导入 `glMapBufferRange` 全部 `GL_INVALID_OPERATION`;Mali 不可导出。**每次存储定义一次 round trip**(不是每 store 一次),`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips` | -| T3 — host pointer 导入(`VK_EXT_external_memory_host`) | | Adreno 无扩展;Mali 只读(GPU 写对宿主映射不可见) | -| T2 — 拒绝(永久正确回退) | `AcquirePersistentMap` 返回 `nullptr`,前端已在三处容忍 | 此档下 client 侧推送强制 | +| **T0 — server 导入 client 分配**(P11 主攻) | client 分配 `AHardwareBuffer` BLOB,socket 交接;server 以 `VK_ANDROID_external_memory_android_hardware_buffer`(Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`(Espryt)导入 | 唯一在两台设备、两个后端上都成立的完整读写档 | +| T1 — server 导出自己的映射 | `VK_KHR_external_memory_fd` opaque fd | 只有 Adreno 的 Vulkan 路径可用;每次存储定义一次 round trip | +| T3 — host pointer 导入 | `VK_EXT_external_memory_host` | Adreno 无扩展;Mali 只读 | +| T2 — 拒绝(永久正确回退) | `AcquirePersistentMap` 返回 `nullptr` | 此档下 client 侧推送强制;**P5 split 使用的档**(`MOBILEGL_IPC_ADOPT_TIER=2`) | -**`map-persistent-roundtrips`(`mpr`)的定义,P3a 拍板并落地**:它数的是**每一次 `MapPersistent` 发射,铸成还是拒绝都算**(`MobileGL/MG_Impl/Pipe/PipeFill.cpp:736`,client 侧发射器,`CallClass::MapPersistentRoundtrips`,`MobileGL/MG_Util/Metrics/PipeStats.h:126`,摘要行印 `mpr=`)。理由是 `ROADMAP.md:7` 的"每个门必须能因它存在的理由变红":定义成"真正发生的往返次数"在 monolith 下按构造恒为 0,永远红不了。按"每次获取尝试"计数则两种模式下**数字相同**,恰好等于上表 T1 那句"每次存储定义一次 round trip",在 monolith 下就非零、就可断言,而一个改成逐 draw 获取的回归立刻现形。门是 `StorageBufferRegrowScenario.NStorageDefinitionsCostNMapPersistentRoundtripsNotOnePerDraw`(`MobileGL/MG_IntegrationTest/Scenarios/StorageBufferRegrowScenario.cpp:255`)与 `LargeArenaAdoptionScenario.AnAdoptionCostsExactlyOneMapPersistentRoundtrip`(`MobileGL/MG_IntegrationTest/Scenarios/LargeArenaAdoptionScenario.cpp:450`)。 +`map-persistent-roundtrips`(`mpr`)数的是**每一次 `MapPersistent` 发射,铸成还是拒绝都算**——按"真正发生的往返"定义在 monolith 下按构造恒为 0,永远红不了;按获取尝试计数两种模式下数字相同、在 monolith 下就可断言(门:`StorageBufferRegrowScenario`、`LargeArenaAdoptionScenario`)。 -`MOBILEGL_IPC_ADOPT_TIER`(`auto`/0/1/2)做负面对照;与 `MOBILEGL_IPC_RESPAWN` 互斥(被采纳的 store 是 server 拥有的内存)。 - -**client 侧 persistent map 推送三件套**(T2 档强制,P5): - -1. 不做 map/unmap 命令对:server 唯一需要知道的是"这个资源现在有没有活的宿主写入者"(`IsBufferDrawClean` 那一行要表达的东西),所以 `ResourceRespecify/SubData` 的 payload 带一个 `hasLiveHostWrites` 位,零新增记录种类。 -2. 块粒度脏块推送:tracker 维护 `m_livePersistentMaps`(persistent+write+非 FlushExplicit+非 GpuResident),在每个 validate 点对本次操作可达的每个这类 buffer(VAO/index/indirect/UBO/SSBO/atomic/XFB target——即后端 20 个 `SyncPersistentMappedRange` 站点的并集)按 `MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(默认 64)切块发送。Phase 1 保守版(整个 mapped span 当脏,按块拆);Phase 2 精确版(shadow-in-shm 的 64 KiB 块脏位,`memcmp` 先行)。P5 验收记录 `persistent-map-push` 字节量;若保守版在 Create/Flywheel fixture 上不可接受,精确版提前——计划里唯一允许因测量改变阶段顺序的地方。 -3. 门从第一天就有:`PersistentCoherentMapScenario`(map PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)。 - -`MOBILEGL_COHERENT_AS_FLUSH` 在拆分模式下照常生效:两个带 `coherent_as_flush: true` 的 Create fixture 在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义。 +**client 侧 persistent map 推送三件套**(T2 档强制,P5 落地):(1) 不做 map/unmap 命令对,`ResourceRespecify/SubData` 的 payload 带 `hasLiveHostWrites` 位;(2) 块粒度脏块推送:tracker 维护 `m_livePersistentMaps`,在每个 validate 点对本次操作可达的每个这类 buffer 按 `MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(默认 64)切块发送——Phase 1 保守版(整个 mapped span 当脏),Phase 2 精确版(shadow-in-shm 的块脏位);(3) 门 `PersistentCoherentMapScenario`(map PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)。R-1(§17.1)的 apply-role producer guard 保证 server 不会重入 client 的 persistent-map producer。`MOBILEGL_COHERENT_AS_FLUSH` 在拆分模式下照常生效。 ## 13. 回读、roundtrip 清单与验证 ### 13.1 稳态零 roundtrip 与不可避免的阻塞点 -零 round trip:全部 draw/clear/blit/copy/dispatch/barrier/XFB 跨度/bind/CSO/`set_*`/上传/`present`(单向记录);全部 caps 站点(握手快照);`glGetError`/`glFinish`/`glFlush`(本地 / no-op);fence 与 query 的创建及非阻塞轮询(client 铸造 handle,未命中合法地答"未就绪");`glGetTexImage`(DirectGLES,含 GPU 生成的 mip);`glReadPixels` → pack PBO(fire-and-forget + client 侧 `MarkGpuWritten`,严格优于 monolith 的无条件停等);`glEndTransformFeedback`(取消无限 fence 等待,对 capture target 置 `MarkGpuWritten`);`eglSwapBuffers`(只查 credit);`*IndirectCount`;restart/multi-draw。 +零 round trip:全部 draw/clear/blit/copy/dispatch/barrier/XFB 跨度/bind/CSO/`set_*`/上传/`present`;全部 caps 站点;`glGetError`/`glFinish`/`glFlush`;fence 与 query 的创建及非阻塞轮询;`glGetTexImage`(DirectGLES);`glReadPixels` → pack PBO(fire-and-forget + client 侧 `MarkGpuWritten`,P6+);`glEndTransformFeedback`;`eglSwapBuffers`;`*IndirectCount`;restart/multi-draw。 -不可避免(全部罕见):握手一次;surface 生命周期与首次 `MakeCurrent`+`InitCapabilities` 每 surface 至多一次;`glReadPixels` → 客户内存(像素进 `SEG_REPLY`,逐行写回循环留在 server 内按操作级批成一段);`glGetTexImage`(DirectVulkan,对"无 GPU 背书"的 level 回答"请用你自己的 shadow");GPU-write pending 的 buffer 首次 CPU 读(monolith 本来就 `glFinish()`;由 `writableMask` 与 `OnGpuWritten` 收窄);`glClientWaitSync(timeout>0)`、`GL_QUERY_RESULT` 未完成、`glBeginConditionalRender`(谓词只解析一次,之后每个条件 draw 在 client 丢弃,server 永远不需要那个 query);`glBufferStorage` 的 ack;`MapPersistent`(仅 T1,每次存储定义一次);纹理拉取(§8.4);client 侧索引扫描当源 EBO 在 pending 集里;ring/stage 耗尽与 present credit(节奏,非语义)。 +不可避免(全部罕见):握手一次;surface 生命周期与首次 `MakeCurrent`+`InitCapabilities` 每 surface 至多一次;`glReadPixels` → 客户内存(像素进 `SEG_REPLY`);`glGetTexImage`(DirectVulkan);GPU-write pending 的 buffer 首次 CPU 读;`glClientWaitSync(timeout>0)`、`GL_QUERY_RESULT` 未完成、`glBeginConditionalRender`(谓词只解析一次);`glBufferStorage` 的 ack;`MapPersistent`(仅 T1);纹理拉取(§8.4);client 侧索引扫描当源 EBO 在 pending 集里;ring/stage 耗尽与 present credit。 -验收措辞:在全部 40 个 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数与 `index-bytes-shipped`;零 timeout 轮询循环必须在有界时间内退出。 +验收措辞:在全部 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数;零 timeout 轮询循环必须在有界时间内退出。 ### 13.2 五部分验证门(取代 monolith 的字节一致门) -"改前改后 `nm --defined-only` 与 `.text` size 完全相等"的门在本方案里按构造死亡(不存在能让旧字节回来的配置);替换是: +1. **接口纯度三道门**:**A 门 include 图**(disaggregated 配置编译 `MG_Backend` 时把 `MG_State/GLState` 从 include 搜索路径移除,`scripts/check_include_closure.py`);**B 门符号** `nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**C 门** `grep -c 'pGLContext' MG_Backend/` == 0。外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。 +2. **语义影子比对 `MOBILEGL_PIPE_VERIFY=1`**——决定性的一条:两套状态模型活在同一地址空间,tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 比对器逐字段每 draw 比对,打印第一个分歧字段与 draw 序号。第三种 CI 模式(`build-linux-verify` / `integration-verify` / `retrace-verify`),~5–10× 慢,永不出货。**保留模式**:消费即清的组(纹理 dirty rect)verify 时保留清除前的集合并比对发射出去的 `(UnionBox, RegionCount, Regions[])`。活过 P13。 +3. **行为 A/B**:trace 语料在 `{monolith-pull, monolith-push, split}` 下 SSIM ≥ 0.99;`ctest -L integration-gpu` 在 `DirectGLES.` 与 `DirectGLES.Split.`(DirectVulkan 同)之间逐名相同(G2);测试名只增不删(G14);单元全绿;CTS 逐后端 conformance 在 0.5 pp 内(只在五个架构边界与合并 `dev` 之前跑完整 caselist)。`TextureUploadShapeScenario` 把逐纹理逐帧的上传形状录金标比对——+6 ms 悬崖由形状相等把关,SSIM 对它完全不敏感。 +4. **性能**:Redmi `2f7cbe2e` reboot-clean、同热窗口、配对 A/B(`devices/pin-verification-2026-09-07.md`),trace replay `--benchmark` 逐帧 JSON(`frameTimesMs[]` + `frameCpuTimesMs[]`);**指标是逐线程 CPU 时间**,p50 与 p99;tracker 每 draw 的绝对 ns(`DriverBench` T1/T2)与 Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。**口径(用户 2026-09-08 起):对着 pull 臂记录,不作阻塞门**;专门的优化阶段排在路线图之后。 +5. **覆盖 + poison + 句柄纪律**:G6 重生成 0 UNMAPPED;`gen_pipe_dirty_surface.py` 0 未映射 mutator;G8 字段归属完备;逐 verb 世代 poison;G7 setter 一致性;`ResidualValueBlock` 棘轮。 -1. **接口纯度三道门**(只跑非 verify 构建):**A 门 include 图**——disaggregated 配置编译 `MG_Backend` 时把 `MG_State/GLState` 从 include 搜索路径移除(`nm --undefined-only` 对"只 include 不调用"是瞎的,而 `RenderState.h → FramebufferObject.h → TextureObject.h` 正是这种耦合),依赖 P0.5;**B 门符号**——`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**C 门未声明**——`grep -c 'pGLContext' MG_Backend/` == 0。外加 debug 断言"每个后端 memo 键都是 `{slot, gen}`,永不是裸前端指针",由 `HandleRecycleScenario` 支撑(重键前必须在至少一个后端上是红的)。 -2. **语义影子比对 `MOBILEGL_PIPE_VERIFY=1`**——决定性的一条:两套状态模型活在同一地址空间,tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 比对器逐字段、每 draw 比对,打印第一个分歧字段与 draw 序号。抓 tracker 忘推的字段、**dirty 位触发得太少**(危险方向)、两条路径变换不一致的值。第三种 CI 模式,40 个 trace + 全部集成测试,~5–10× 慢,永不出货。逐字段而非 `memcmp`(padding 会 false-DIFFER)。**保留模式**:消费即清的组(纹理 dirty rect)发射后无法重算,verify 时 tracker 保留清除前的集合并比对发射出去的 `(UnionBox, RegionCount, Regions[])`。**活过 P13**。 -3. **行为 A/B**:40 个 trace 在 `{monolith-pull, monolith-push, split}` 下 SSIM ≥ 0.99(默认阈值);`ctest -L integration-gpu` 在 `DirectGLES.` 与 `DirectGLES.Pipe.`/`DirectGLES.Split.`(DirectVulkan 同)之间逐名相同;单元测试全绿;CTS 逐后端 conformance 在 0.5 pp 内(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母)。`TextureUploadShapeScenario` 把逐纹理逐帧的上传形状(box vs N region、作业数)录金标比对——+6 ms 悬崖由形状相等把关,SSIM 对它完全不敏感。逐名功能基线是"P1 出口的重构后 monolith"(P1 出口先用 verify 证明等价于 `81b17c0b`);`81b17c0b` 只作性能锚点。 -4. **monolith 性能**:两台设备 reboot-clean、同热窗口、配对 A/B,`tools/device_bench/bench.sh` + trace replay `--benchmark` 逐帧 JSON(P2 起 `benchmark.json` 除 `frameTimesMs[]` 外还带 `frameCpuTimesMs[]`,任意分位数在主机侧算);定频与设备档案在同一处:`tools/device_bench/pin_device.sh` 与 `tools/device_bench/devices/*.env`(`odinlite`、`xiaomi-adreno830`、`oppo-mali`;未经核验的档案带 `PROFILE_VERIFIED=0`,脚本默认拒跑,两台战役设备的核验记录在 `docs/Disaggregated/devices/pin-verification-2026-09-07.md`)。**指标是逐线程 CPU 时间**,p50 与 p99;tracker 每 draw 的**绝对 ns** 公布(真实拉取基线只有每 draw 6.5–9.3 次 accessor,相对噪声阈值会平凡通过);Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。**口径(2026-09-08 起)**:这一条对着 pull 臂**记录**而不阻塞——push 比 pull 多约 10% 逐线程 CPU 已被接受,绝对 ns 上限降为记录项,专门的优化阶段排在路线图之后。 -5. **覆盖 + poison + 句柄纪律**:G6 重生成 0 UNMAPPED;`gen_pipe_dirty_surface.py` 重生成 0 未映射 mutator;逐 verb 世代 poison;G7 setter 一致性测试;`ResidualValueBlock` 的 `offsetof` 断言与 P13 的 `sizeof == 0`。 +两条幸存的字节级等式:`MOBILEGL_BUILD_DISAGGREGATED=OFF` 时 `nm --defined-only libMobileGL.so | grep MG_Remote` 为空;**G1**:pull 构建的符号集与 `.text` 字节对 P3a 起的基线恒等(`scripts/symbol_report.py --threshold 0`,每阶段 0 增 / 0 删 / 0 resize / 0 重命名)。**每个门必须能因它存在的理由变红**:每个门都带阴性对照并真跑过一次红(R-16);公共 GL 看不见的改白盒断言(`PipeApplyPeek`、`PipeSlotPeek`、`BackendCapsPeek`);`ctest -V` 做拒绝普查是假零(console sink 在发布配置里被编译掉,必须逐用例读自己的日志文件)。 -两条幸存的字节级等式:`MOBILEGL_BUILD_DISAGGREGATED=OFF` 时 `nm --defined-only libMobileGL.so | grep MG_Remote` 为空且链接行不增加库;`nm -D libMobileGL.so | grep mobilegl_server_main` 在 RelWithDebInfo 里命中。符号与 `.text` 漂移每阶段作为信息性指标发布。 +### 13.3 长期语义门:MGPipe recorder(P13) -### 13.3 长期语义门:MGPipe recorder - -P13 把 `MG_Test` 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 `MG_State`,是 P13 之后不靠 verify 构建的语义门,也给 `tools/trace_replay` 一种记录**已解析**状态的、比 apitrace 精确得多的录制格式。它只覆盖推送内容,不覆盖后端对它的解释(split-only 的渲染 bug 仍无 server 侧第二意见)。 +`MG_Test` 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 `MG_State`,也给 `tools/trace_replay` 一种记录**已解析**状态的录制格式。它只覆盖推送内容,不覆盖后端对它的解释。 ## 14. Present、线程与帧节奏 -- `eglSwapBuffers` → `present{frameSerial}`(swap interval 搭在同一条记录上)→ publish + 敲门铃 → 返回,除非超出 credit。**`present` 与 `eglSwapBuffers` 严格 1:1**:两个后端的帧边界排空(Magma 四次 `OnFrameBoundary` 老化、`TryDrainFrameTransients`、`BeginFrame`;Espryt 三个 ring 与 `TrimBufferPool` 的 retire)只在 `Present` 内发生,批量会饿死它们。 -- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(可配 1–4):延迟叠加,`端到端 ≈ client credit + server 帧数 + 驱动深度`;server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧(60 Hz 下 66–83 ms)。P10/P12 用 `GetGpuTimestampNs` 与 `--benchmark` 逐帧 JSON 构建输入延迟直方图,只有实测吞吐收益能抵掉延迟代价才调高。 -- Magma 从不注册 `SetSwapInterval` 且偏好 `MAILBOX`/`IMMEDIATE`,IPC credit 是它唯一的显式限帧器;若需要 FIFO 作为独立 `dev` 变更。 -- 线程——client:**v1 不加线程**,编码在 GL 线程上直接写 ring(前端本就是 per-context 单线程契约);外来线程的 sync/query 读全部从 `RingControl` 无锁回答,必须发射的少数取 `ctrlMutex` 走 CTRL socket 的 `AuxRequest`(SPSC ring 不允许第二个 producer);`ShaderCompilePool` 原样在 client;可选 `mgl-client-tx` 凭测量决定。server:`mgl-srv-io`(asio、封帧、`SCM_RIGHTS`、doorbell、CTRL RPC)、`mgl-srv-apply`(**终身持有原生 context**:`g_backendContextOwnerThread` 只写一次,`MakeCurrent` 的缓存失效风暴变启动期一次性,每帧 EGL 复核恒真,off-thread 降级消失)、可选 `mgl-srv-dec`。 -- **核心放置**:拆分的全部性能主张押在两半落在两个都快的核上。全库无亲和性控制,server 是独立进程不继承 launcher 的亲和性。规则:报总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith `PrepareForDraw`);复用 `ShaderCompilePool` 的大核探测把 `mgl-srv-apply` 绑到大核(`MOBILEGL_IPC_SERVER_AFFINITY`,默认 auto,解析出的 mask 打进日志);每阶段报逐线程 CPU 时间。 -- 拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool(先于 `glslang::FinalizeProcess()` 与 `pGLContext` 析构)→ `MobileGL::Destroy()` → 释放 sync/query handle。 +- `eglSwapBuffers` → `present{frameSerial}` → publish + 敲门铃 → 返回,除非超出 credit。**`present` 与 `eglSwapBuffers` 严格 1:1**:两个后端的帧边界排空只在 `Present` 内发生,批量会饿死它们。 +- **`MOBILEGL_IPC_PRESENT_CREDIT` 默认 1**(P10):延迟叠加,server 的 `Present` 末尾已在 `vkWaitForFences` 上等 2–3 帧,credit 2 就是端到端 4–5 帧;只有实测吞吐收益能抵掉延迟代价才调高。Magma 从不注册 `SetSwapInterval`,IPC credit 是它唯一的显式限帧器。 +- 线程——client:**v1 不加线程**,编码在 GL 线程上直接写 ring;外来线程的 sync/query 读从 `RingControl` 无锁回答,必须发射的少数取 `ctrlMutex` 走 `AuxRequest`(SPSC ring 不允许第二个 producer)。server:`mgl-srv-io`(封帧、`SCM_RIGHTS`、doorbell、CTRL RPC)、`mgl-srv-apply`(**终身持有原生 context**,`MakeCurrent` 的缓存失效风暴变启动期一次性)。 +- **核心放置**:全库无亲和性控制。规则:报总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith `PrepareForDraw`);复用 `ShaderCompilePool` 的大核探测把 `mgl-srv-apply` 绑到大核(`MOBILEGL_IPC_SERVER_AFFINITY`,默认 auto)。 +- 拆机顺序:publish + server 排空并 ack → 停 apply 线程 → 关 transport → client 排空 compile pool → `MobileGL::Destroy()` → 释放 sync/query handle(P5 落地形状见 §17.3)。 -## 15. 进程、EGL 与平台 +## 15. 进程、EGL 与平台(P6 / P12) ### 15.1 启动与握手 -- server 定位:`MOBILEGL_IPC_SERVER_PATH`(主要)→ `dladdr(&MobileGL::Initialize)` 同目录的 `libMobileGLServer.so`(兜底;不能当主要机制,因为集成测试静态链接 `MobileGL_s`、trace replay 的可执行文件不在库目录)。配套:`MobileGLServer` 的 `RUNTIME_OUTPUT_DIRECTORY` 设为 `$`,每条新 ctest `ENVIRONMENT` 与 `add_trace_replay_test` 的 `SPLIT` 分支带 `MOBILEGL_IPC_SERVER_PATH`。 -- 启动:`socketpair(AF_UNIX, SOCK_STREAM)` + `fork`/`execve`,fd 3 = socket。无文件系统 socket 路径、无 abstract namespace、Android 上无 SELinux 争议。 -- **子进程强制 monolith**(修无界 fork 链——server stub `dlopen(libMobileGL.so)` 后必然走 `MG_Backend::Init()`,继承的 `MOBILEGL_TRANSPORT=spawn` 会再 spawn):spawn 时构造显式 envp 剔除 `MOBILEGL_TRANSPORT` 与全部 `MOBILEGL_IPC_*`;`mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`。两条都做。`MG_Test/Wire` 测试:spawn 一个 server,进程树只多出恰好一个子进程。 -- `Hello{abi, backendType, buildFingerprint, configBlob}` → `Welcome{四个段}`。`configBlob` 转发 client 解析好的 `MG_Config::Features`,两半不可能对 quirk 开关有分歧;`buildFingerprint`(git hash + `PipeCalls.def` hash)不匹配 → 握手期 `Fatal{AbiMismatch}`。 -- `mobilegl_server_main` 声明为 `extern "C" __attribute__((visibility("default")))`:非 Debug 构建设了 hidden visibility,而 FCL/plugin 出货的是 RelWithDebInfo,否则 `dlsym` 在设备上静默失败。 +- server 定位:`MOBILEGL_IPC_SERVER_PATH`(主要)→ `dladdr(&MobileGL::Initialize)` 同目录的 `libMobileGLServer.so`(兜底;集成测试静态链接 `MobileGL_s`、trace replay 的可执行文件不在库目录)。 +- 启动:`socketpair(AF_UNIX, SOCK_STREAM)` + `fork`/`execve`,fd 3 = socket。无文件系统 socket 路径、无 abstract namespace。 +- **子进程强制 monolith**(修无界 fork 链):spawn 时构造显式 envp 剔除 `MOBILEGL_TRANSPORT` 与全部 `MOBILEGL_IPC_*`;`mobilegl_server_main` 在到达 `Init()` 之前把 `MG_Config::Transport` 硬置为 `Monolith`。两条都做。`MG_Test/Wire` 测试:进程树只多出恰好一个子进程。 +- `Hello{abi, backendType, buildFingerprint, configBlob}` → `Welcome{四个段}`。`configBlob` 转发 client 解析好的 `MG_Config::Features`;`buildFingerprint`(git hash + `PipeCalls.def` hash)不匹配 → `Fatal{AbiMismatch}`;segment 尺寸尚未混入 fingerprint(ID-47 债)。 +- `mobilegl_server_main` 声明为 `extern "C" __attribute__((visibility("default")))`(非 Debug 构建设了 hidden visibility)。 -### 15.2 Android(spike A 已证) +### 15.2 Android(spike A 已证,`MEASUREMENTS.md` §1.1) -- 交付链:APK 唯一可 exec 的位置是 `lib//`,打包器只收 `lib*.so`,所以 server 以 `add_executable` + `PREFIX "lib"/SUFFIX ".so"` 构建(真 PIE),并把 `RUNTIME_OUTPUT_DIRECTORY` 指到 AGP 收集原生产物的 `CMAKE_LIBRARY_OUTPUT_DIRECTORY`(`CMakeLists.txt:784-808`,`MOBILEGL_BUILD_SERVER_SPIKE`)。**两台设备上都已证明**:从 `TraceReplayActivity` 自身的 `untrusted_app` 进程 `fork`+`execve` `/libMobileGLServer.so`,子进程落在同一域、同一 MLS category,exit 0,零 avc denial(`MEASUREMENTS.md` §1)。 -- `fork`+`execve` 而非 `posix_spawn`:bionic 从 API 28 才声明后者,minSdk 26(`android-plugin/app/src/trace/cpp/spawn_spike.cpp:63-68`)。fork 与 execve 之间只做 async-signal-safe 的 open/dup2/execve/write/_exit(父进程是多线程 JVM)。 -- 应用进程的 stdout/stderr 是 `/dev/null`:子进程用 **marker 文件** 证明自己活过,exec 被拒的 errno 经 close-on-exec pipe 回传(EACCES 与 ENOEXEC 是完全不同的判决)。 -- 生产 server 主体是 ~30 行 stub:`dlopen(libMobileGL.so)` → `dlsym("mobilegl_server_main")`。一份共享库、两个角色、版本必然匹配(Android 上那份库仍含 glslang/SPIRV-Cross,因为它同时服务 client;B 门检的是 server 侧代码有没有引用它们)。 -- minSdk 26 没有公开 NDK API 能扁平化 `ANativeWindow`(`libbinder_ndk`、`ASurfaceControl` 都是 API 29)。**P5–P11 验证路径无窗口**:pbuffer 或 `AImageReader` 的 `ANativeWindow`,trace replay 默认 pbuffer。**P12 生产路径**:Java `Surface`(Parcelable)→ Messenger/AIDL → `MobileGLServerService`(`android:process=":mgl"`)→ JNI `ANativeWindow_fromSurface`(FCLauncher 今天在 `egl_bridge.c` 做的那一次调用);仓内先例是 `android:process=":bench"` 的 `BenchService`。代价:server 进程多一个 ART(~15–25 MB)。FCL 把游戏 JVM 跑在主进程,第二个进程必须新建。 -- `HeadlessGL` 的 fork 预检会 fork 一个子进程跑完整 EGL bring-up 然后 `_exit`——拆分模式下那个子进程会 spawn 一个孤儿 server。规则:server 的 EOF 检测**即时且无条件退出**(亚秒级);client 的 socket fd 设成 `_exit` 会确定性关闭的形态;就绪握手有界重试。列为 P6 验收。 -- 通用 env 透传 `--env K=V`(`run_android_retrace_local.py` → intent extra `mobilegl_env` → `trace_replay_core.cpp` 在加载 `libMobileGL.so` 前 `setenv`)已接进 retrace 通道,取代逐 knob 加 `--es/--ez`。 +- 交付链:APK 唯一可 exec 的位置是 `lib//`,server 以 `add_executable` + `PREFIX "lib"/SUFFIX ".so"` 构建(真 PIE),`RUNTIME_OUTPUT_DIRECTORY` 指到 AGP 收集原生产物的目录(`MOBILEGL_BUILD_SERVER_SPIKE`)。**两台设备上都已证明**:从 `untrusted_app` 进程 `fork`+`execve` 子进程落在同一域、exit 0、零 avc denial。 +- `fork`+`execve` 而非 `posix_spawn`:bionic 从 API 28 才声明后者,minSdk 26。fork 与 execve 之间只做 async-signal-safe 调用。应用进程的 stdout/stderr 是 `/dev/null`:子进程用 marker 文件证明自己活过,exec 被拒的 errno 经 close-on-exec pipe 回传。 +- 生产 server 主体是 ~30 行 stub:`dlopen(libMobileGL.so)` → `dlsym("mobilegl_server_main")`。一份共享库、两个角色。 +- minSdk 26 没有公开 NDK API 能扁平化 `ANativeWindow`。**P5–P11 验证路径无窗口**(pbuffer / `AImageReader`)。**P12 生产路径**:Java `Surface` → Messenger/AIDL → `MobileGLServerService`(`android:process=":mgl"`)→ JNI `ANativeWindow_fromSurface`;代价 server 进程多一个 ART(~15–25 MB)。 +- `HeadlessGL` 的 fork 预检会 fork 一个子进程跑完整 EGL bring-up 然后 `_exit`——拆分模式下那个子进程会 spawn 一个孤儿 server。规则:server 的 EOF 检测**即时且无条件退出**;client 的 socket fd 设成 `_exit` 会确定性关闭的形态;握手有界重试。P6 验收。 +- 通用 env 透传 `--env K=V`(`run_android_retrace_local.py` → intent extra → `trace_replay_core.cpp` 在加载库前 `setenv`)已接进 retrace 通道。 ### 15.3 Linux / Windows / 崩溃 -- Linux/X11:`Window` 是 XID,`nativeToken:u64` 直接送,backend 自己 `XOpenDisplay(getenv("DISPLAY"))`;Wayland 维持不支持。WSL/CI 永不开窗:`EGL_PLATFORM=surfaceless` + `EnsureHeadlessPlatform()`。 -- Windows:`HWND` 进 `nativeToken`,Vulkan 可行,WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only。transport 默认 named pipe:asio `windows::stream_handle` 要求 overlapped 句柄,所以用 GUID 命名的 `CreateNamedPipeW(FILE_FLAG_OVERLAPPED)` + `CreateFileW(FILE_FLAG_OVERLAPPED)` 造句柄对再继承给 `CreateProcess`;AF_UNIX-everywhere 是可选简化。Windows 机器不是正确性门。macOS 不拆分(`CAMetalLayer` 无跨进程表示)。 -- server 死:client 读到 EOF/EPIPE → device-lost 闩锁(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_FALSE`+`EGL_CONTEXT_LOST`、`glGetGraphicsResetStatus` 返回 `GL_UNKNOWN_CONTEXT_RESET`);`MOBILEGL_IPC_RESPAWN=1` 时重启并全量重推(默认关,静默重启会掩盖 bug)。client 死:server 读到 EOF → 立即销毁原生 context 并退出;`MOBILEGL_IPC_IDLE_EXIT_S`(默认 30)只作最后保险。 +- Linux/X11:`Window` 是 XID,`nativeToken:u64` 直接送;Wayland 维持不支持;WSL/CI 永不开窗(`EGL_PLATFORM=surfaceless`)。Windows:`HWND` 进 `nativeToken`,Vulkan 可行,WGL/ANGLE 对外进程 HWND 不受支持 → headless only;transport 默认 named pipe。Windows 机器不是正确性门。macOS 不拆分。 +- server 死:client 读到 EOF/EPIPE → device-lost 闩锁(GL 调用 no-op、`eglSwapBuffers` 返回 `EGL_FALSE`+`EGL_CONTEXT_LOST`、`glGetGraphicsResetStatus` 返回 `GL_UNKNOWN_CONTEXT_RESET`);`MOBILEGL_IPC_RESPAWN=1` 时重启并全量重推(默认关)。client 死:server 读到 EOF → 立即销毁原生 context 并退出;`MOBILEGL_IPC_IDLE_EXIT_S`(默认 30)只作最后保险。 ## 16. 构建布局 ``` -MobileGL/MG_Pipe/ 永远进构建(monolith 的架构,不在任何 option 之后) [P0] -MobileGL/MG_Impl/Pipe/ Tracker、SlotAllocator、CsoCache、HostResolve、CompositeResolver [P2+] -MobileGL/MG_Backend/MGPipe/ PipeInputs.h + MGPipeImpl_DirectGLES/DirectVulkan.cpp [P1+] +MobileGL/MG_Pipe/ 永远进构建(monolith 的架构) + PipeCalls.def PipeFields.def Coverage.def FieldOwnership.def FillPoints.def DirtySurface.def + MGPipe.h MGPipeTypes.h MGPipeValueTypes.h MGPipeHandles.h MGPipeHostSpan.h MGPipeCallbacks.h + MGPipeRenderStateSpans.{h,cpp} PipeApply.{h,cpp} PipeRoute.{h,cpp} PipeMutation.h PipeInputsSwitch.h + generated/ PipeTables PipeThunks PipeWire PipeVerify PipeFilled PipeCoverage PipeSpanTable PipeFieldOwnership PipeFillPoints (.inc) +MobileGL/MG_Impl/Pipe/ Tracker.h PipeFill.{h,cpp} SlotAllocator CsoCache CompositeResolver ResourceTracker + SetHashSuppressor {VertexInput,Framebuffer,Texture,Sampler,Image,Program}Emit.h +MobileGL/MG_Backend/MGPipe/ PipeInputs.{h,cpp} MobileGL/MG_Remote/ 仅 MOBILEGL_BUILD_DISAGGREGATED - Protocol/ protocol.fbs generated/protocol_generated.h mg_protocol_base.h [P0] - Transport/ ITransport InProcessTransport Framing Ring ShmSegment(+Posix/Win32) FdPassing Doorbell WireLog [P0] - SocketTransport [P6] - Client/ PipeEmitter EmitTables BackendObject_Remote CapsMirror ShadowArena PersistentMapTracker GpuWritePending Surface/{X11,Win32,Android,Headless} [P5+] - Server/ PipeApplier PipeObjectTables IndexHostMirror ServerLoop ReplyPool EventRing ServerMain [P5+] - ServerJni.cpp [P12] + CONTRACT-P5.md CONTRACT-P5B.md CapsCodec.{h,cpp} + Protocol/ protocol.fbs generated/protocol_generated.h mg_protocol_base.h + Transport/ ITransport InProcessTransport Framing Ring SessionRings ReplySlot EventRing RoleMemory + ShmSegment(+Posix/Win32) FdPassing Doorbell WireLog SocketTransport [P6] + Wire/ PipeWireCodec.{h,cpp}(记录编解码、WireVerbSink) + Client/ BackendObject_Remote ClientSession EmitTables WireTables SlotCaps CapsMirror + PersistentMapTracker GpuWritePending + Server/ PipeApplier(ServerVerbSink)ServerLoop ServerSession StagedShadow IndexHostMirror ServerMain [P6/P8] ``` -- CMake option(`CMakeLists.txt:23`):`MOBILEGL_BUILD_DISAGGREGATED`(默认 OFF)追加 `MG_Remote/**` 进 `SOURCE_FILES`(`CMakeLists.txt:454-469`)并定义 `-DMOBILEGL_BUILD_DISAGGREGATED=1`;OFF 时 `MG_Config::Transport` 是 `constexpr Monolith`,`Init.cpp` 的分支编译期消失。`3rdparty/flatbuffers/include` 缺失时把 option 强制回 OFF 并 `message(WARNING)`(`CMakeLists.txt:440-451`)。`MobileGL` 与 `MobileGL_s` 都拿到同一份源。`MG_Test/Wire` 只在该 option 下注册(`MobileGL/MG_Test/CMakeLists.txt:93-95`)。 -- `MOBILEGL_BUILD_DISAGGREGATED_INPROC`(P5 已落地):CI/调试形态,隐含开启前者并加角色隔离。MGPipe 让需要角色分身的进程全局从四个(`pGLContext`、`gBackendFunctionsTable`、`pActiveBackendObject`、`pDefaultFramebufferInfo`)降到**两个**(pipe 表与 `pActiveBackendObject`):server 角色不读 `pGLContext`,`pDefaultFramebufferInfo` 由保留句柄 `{0,1}` + surface forwarder 取代。两个角色通过 apply thread 与控制 mailbox 分开,而不是给 1494 个 `pGLContext->` 读点加 TLS。 -- `MobileGLServer`:桌面 `add_executable` 链接 `MobileGL_s`;Android `add_executable` 改名 `lib*.so` 链接共享 `MobileGL`,由 AGP 打进 `jniLibs`。 -- `MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix: | pipe:`(P5 起在 `ConfigLoader.cpp` 解析),免费换来 ctest `ENVIRONMENT` 变体、trace-replay 的 `setenv` 块、FCL 用户可编辑 env、plugin APK 的 V2 开关表、`/data/local/tmp` CTS 路径。 -- 测试接线陷阱:ctest `ENVIRONMENT` 是替换而非追加、`;` 必须转义、property 覆盖 job env,必须用 `mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})` 构造;`add_trace_replay_test` 加 `SPLIT` 后缀(否则与同 case+backend 重名)并加 `-DTRACE_TRANSPORT=` 给 `run_trace_case.cmake` 消费。 -- CI(`.github/workflows/test.yml:1538` `pipe-gates`,P0 已落地):`gen_pipe.py` 重生成 + diff;`MG_Backend`/`MG_State` 下禁止 stdio 插桩的 grep 门;`gen_pipe_dirty_surface.py --check` + `--self-test`(**P2 起成为门**,`.github/workflows/test.yml:1601-1604`,取代原来信息性的 `--summary` 步骤);`check_doc_citations.py`(警告级,文档定稿后 `--strict`)。独立 job `flatc-check`。已落地:`include-graph-check`(P0.5)、`monolith-symbol-report`、`build-linux-verify` / `integration-verify` / `retrace-verify`(P1)。 +- CMake option:`MOBILEGL_BUILD_DISAGGREGATED`(默认 OFF)追加 `MG_Remote/**` 并定义 `-DMOBILEGL_BUILD_DISAGGREGATED=1`;OFF 时 `MG_Config::Transport` 是 `constexpr Monolith`。`MOBILEGL_BUILD_DISAGGREGATED_INPROC` 隐含前者并加角色隔离:MGPipe 让需要角色分身的进程全局从四个降到两个(pipe 表与 `pActiveBackendObject`),两个角色通过 apply thread 与控制 mailbox 分开,而不是给 1494 个 `pGLContext->` 读点加 TLS。`3rdparty/flatbuffers/include` 缺失时把 option 强制回 OFF。 +- `MobileGLServer`(P6):桌面 `add_executable` 链接 `MobileGL_s`;Android 改名 `lib*.so` 链接共享 `MobileGL`。 +- `MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix: | pipe:`(`ConfigLoader.cpp` 解析;P5 只接受前两个,其余具名拒绝并回落 monolith),免费换来 ctest `ENVIRONMENT` 变体、trace-replay 的 `setenv` 块、FCL 用户 env、plugin APK 的开关表。**split build 不设 `MOBILEGL_TRANSPORT=inproc` 时是 monolith control arm**。 +- 测试接线陷阱:ctest `ENVIRONMENT` 是替换而非追加、`;` 必须转义,必须用 `mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})` 构造;`add_trace_replay_test` 加 `SPLIT` 后缀并 `-DTRACE_TRANSPORT=`;每条 `DirectGLES.Split.*` 条目带独立 `MOBILEGL_LOG_FILE_PATH`。 +- CI(`.github/workflows/test.yml`):`pipe-gates`(G1–G8 重生成 + diff、生成器 self-test、符号报告门、`MG_Backend`/`MG_State` 禁止 stdio 插桩、dirty-surface、字段归属、R-16 阴性对照 smoke、G5 两族、文档引用 lint);`flatc-check`;`include-graph-check`;`monolith-symbol-report`;`build-linux` / `build-linux-verify` / `build-linux-split`;`integration` / `integration-verify` / `integration-split`(含 `scripts/ci/split_negative_controls.sh` 的 E1/E3(a)/E2 硬门,broad inproc 车道只记录普查);`retrace` / `retrace-verify` / `retrace-split`。`apk.yml` 构建 pull / push / split 三份 APK 并在 AVD 上 retrace。两份 workflow 里的 `feat/disaggregated` 触发器是 **TEMPORARY**,合入 dev 前移除。 -## 17. P5 落地形状:lockstep `inproc`、reply 与 apply-thread server +## 17. P5 / P5b 落地形状:lockstep `inproc`、reply、apply-thread server 与 verb 迁移 ### 17.1 verb barrier 与诚实的同地址空间传输 -P5 的 `inproc` 是真第二线程,但还是 **lockstep**:每条 class-B verb 发射后,client 的 `EmitAndWait` 等到 `appliedSeq == emitSeq`;`MOBILEGL_IPC_VERB_BARRIER=1` 默认开启。理由不是吞吐,而是 27 个 BARRIER-PULLED 字段尚无记录载体;在这些字段退役前让两线程同时跑,会让 server 读到 client 的“未来值”(R-1,ID-4)。barrier 是逐族可退役对象,不是 P6 transport 的要求。 +P5 的 `inproc` 是真第二线程,但还是 **lockstep**:每条 class-B verb 发射后 client 的 `EmitAndWait` 等到 `appliedSeq == emitSeq`;`MOBILEGL_IPC_VERB_BARRIER=1` 默认开启。理由不是吞吐,而是 BARRIER-PULLED 字段(G8 表里目前 36 行)尚无记录载体;在这些字段退役前让两线程同时跑,server 会读到 client 的"未来值"(R-1)。barrier 是逐族可退役对象,不是 P6 transport 的要求;它的设备代价(barrier tax)见 `MEASUREMENTS.md` §7.4。 -同一地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写成 `nullptr`,内容 blob 必须带真实 `SEG_STAGE` / 段内 offset / 非零 size;decoder 对四种形状分别 `Fatal{ProtocolCorruption}`——非空 `Ptr`、内容记录 size 为 0、非零 size 却无 segment、`offset+size` 越界。`MapPersistent` 在 split 恒 decline;`MOBILEGL_IPC_AUDIT=1` 在 retire 后把 staging 填 `0xDD`,让跨 applier 返回持针的实现下一次读取时可见地失败。四个 Fatal 与 audit 使 `inproc` 和未来 spawn 表达同一份所有权。 +同一地址空间不得成为旁路(R-2):encoder 把 `MGHostSpan::Ptr` 恒写成 `nullptr`,内容 blob 必须带真实 `SEG_STAGE` offset / 非零 size;decoder 对四种形状分别 `Fatal{ProtocolCorruption}`。`MapPersistent` 在 split 恒 decline;`MOBILEGL_IPC_AUDIT=1` 在 retire 后把 staging 填 `0xDD`,让跨 applier 返回持针的实现下一次读取时可见地失败(R-11:任何 widened read 先过 `RequireStagedCoverageForPendingRanges`,否则 `Fatal{StageSnapshotTooNarrow}`)。apply 角色不得进入 client 的 persistent-map producer(r1)。 -reply mailbox 以**记录序号作为 slot id**(R-3):slot header 是 `{Seq, Status, Size}`,acceptance 的四个 Bool 答案与 `MapPersistent` decline 都在既有 verb wait 内读取,不增加第二次等待。`Status=ERROR` 一律 `Fatal{ReplyError}`,不能退化成 false;读到 reply 之前先由 `appliedSeq` 证明该记录已离开 applier(R-5)。P9 才把这套同步 mailbox 推广成异步池。 +reply mailbox 以**记录序号作为 slot id**(R-3):slot header 是 `{Seq, Status, Size}`,acceptance 的 Bool 答案与 `MapPersistent` decline 都在既有 verb wait 内读取;`Status=ERROR` 一律 `Fatal{ReplyError}`,未知 status 是 `Fatal{ReplyStatusInvalid}`;读到 reply 之前先由 `appliedSeq` 证明该记录已离开 applier(R-5)。P9 才把这套同步 mailbox 推广成异步池。等待预算:普通 verb / 容量等待 30 s(`Fatal{BarrierTimeout}`);`ClientWaitSync` 的 applied/reply 预算 = ceil(timeout ns / 1e6) + 30 000 ms,有限 chunk 避开溢出。 -### 17.2 71 槽的路由、caps 与 tight readback +### 17.2 71 槽的三类、caps 与 tight readback -client 表只有三类(`CONTRACT-P5.md` §7):2 个 getter 从 caps mirror 本地回答;5 个 P5 verb 发射;其余 64 个 `Fatal{UnmigratedVerb}`。实现不是手写 switch:**33 条**走 generated route tables,**4 条 escape** 保留专用 reply/资源语义,`PipeCatalogueTest` 同时钉住 **37**。R-17 的第一轮只替换 `MGPipeApply(` 调用表达式,漏掉了五个 `&MGPipeApply` 地址获取点,四条 delete/resource row 于是仍在 GL thread 同步执行;落地门因此既扫调用也扫 address-of。教训:生成路由的完备性必须覆盖值调用与函数地址,两者不是同一张 grep 表(ID-58/61)。 +client 表(`Client/EmitTables.cpp`)把后端函数表的 71 个槽分成三类,`static_assert` 钉住 **A = 2**(getter 从 caps mirror 本地回答)/ **B = 54**(发射:P5 的 `Clear`、`DrawArrays`、`ReadPixels`、`Blit`、`Present` + P5b 的 d1 19、i1 7、t2 6、f1 11、`BlitNamedFramebuffer`、sync 5)/ **C = 15**(`Fatal{UnmigratedVerb}` 具名拒绝,永不回落到 monolith applier——R-4)。applier 侧 33 条走生成路由表 + 4 条 escape,`PipeCatalogueTest` 钉 37;生成路由的完备性覆盖值调用与 `&MGPipeApply*` 地址取用(R-17)。 -能力存活只读 `CapsMirror` 中的 `MGPCaps::CallMask`;server 的 consumer mask 由 backend 类型显式设置并以实际 op table 校验,**永不**从进程级 `MGPipeGetResourceOps()` 推导。41 个旧 slot-null probe 也改读对应 cap,否则 non-null emit table 会把所有 fallback 错判成“支持”(R-8)。成功的 `MakeEGLCurrent` / `InitCapabilities` 重新发布 snapshot;client 按 generation 采纳(R-12)。 +能力存活只读 `CapsMirror` 中的 `MGPCaps::CallMask`(R-8);server 的 consumer mask 由 backend 类型显式设置并以实际 op table 校验,**永不**从进程级 `MGPipeGetResourceOps()` 推导。成功的 `MakeEGLCurrent` / `InitCapabilities` 重新发布 snapshot,client 按 generation 采纳(R-12);相同 (dpy, draw, read, ctx) tuple 的重复 make-current 不重发(ID-67)。 -`ReadPixels` 在线上恒为 **tight**:server 临时设 neutral pack(row length / skips = 0,alignment = 1),只向 reply 写 `width*height*bytesPerPixel`;client 用自己持有的 pack state scatter,row gap 保持原字节(ID-49)。绑定 PACK PBO 时 P5 在 client 侧、发射和解引用之前 `Fatal{UnmigratedVerb, "ReadPixels+PACK_BUFFER"}`;server 直接写 buffer resource 并 `MarkGpuWritten` 是 P6+ 的真实形状(ID-57)。reply 单槽 payload 上限是 `2 MiB - 16`,更大 readback 的 carrier 同样留给 P6+。 +`ReadPixels` 在线上恒为 **tight**:server 临时设 neutral pack,只向 reply 写 `width*height*bytesPerPixel`;client 用自己持有的 pack state scatter(含 `PACK_SWAP_BYTES` 的 component / packed-word swap,r1)。绑定 PACK PBO 时在 client 侧 `Fatal{UnmigratedVerb, "ReadPixels+PACK_BUFFER"}`;reply 单槽 payload 上限 `2 MiB - 16`,更大 readback 的 carrier 留给 P6+(ID-47/57)。 ### 17.3 server 角色、shadow 与退出 -`ServerLoop` 的 `mgl-srv-apply` 是 native context 的终身 owner。`ServerMakeEGLCurrent` 对 `(dpy, draw, read, ctx)` tuple **只 bind 一次**;相同 tuple 是 no-op,client release 只记账、绝不让 apply thread native-unbind,context 直到 destroy / context loss 才离开该线程(ID-54)。罕见 EGL 操作经 caller-serialised one-slot control mailbox 进 apply thread;十二个 `Server*` forwarder 是唯一缝,其中 make-current 与 init-caps 成功时执行 R-12 republish。mailbox 在 stop 判定与 publish 上共用 mutex;有 backend 却无 thread 时 `Fatal{ApplyThreadNotRunning}`,不允许回落到 app thread。 +`ServerLoop` 的 `mgl-srv-apply` 是 native context 的终身 owner。`ServerMakeEGLCurrent` 对 tuple **只 bind 一次**,client release 只记账、绝不让 apply thread native-unbind(ID-54)。罕见 EGL 操作经 caller-serialised one-slot control mailbox 进 apply thread;十二个 `Server*` forwarder 是唯一缝。有 backend 却无 thread 时 `Fatal{ApplyThreadNotRunning}`,不允许回落到 app thread。framebuffer death 的驱动调用也经 mailbox 上 apply thread(r1)。 -`PipeApplier::ApplyOne` 依次 stamp verb、decode/apply、清 stamp;只有 `SessionConsumer::ApplyOne` 每条记录把 `appliedSeq` 加一,pad 不计数。五条没有 `MGPipeApply*` 的 class-B verb 由 `ServerVerbSink` 消费。`StagedShadowStore` 按 resource twin 复制并合并**精确覆盖范围**;`Ops_H_SubData` / flush / respecify 只把 server-owned copy 交给后端,任何 widened read 先过 `RequireStagedCoverageForPendingRanges`,否则 `Fatal{StageSnapshotTooNarrow}`(R-11)。 +`PipeApplier::ApplyOne` 依次 stamp verb、decode/apply、清 stamp;只有 `SessionConsumer::ApplyOne` 每条记录把 `appliedSeq` 加一。`StagedShadowStore` 按 resource twin 复制并合并**精确覆盖范围**;`Ops_H_SubData` / flush / respecify 只把 server-owned copy 交给后端;有传输时 `liveHostBase()` 不得回落到 client 的 `MappedData()`(ID-50/52)。 -退出顺序承重:`ShutdownSplitRoles` 先让 client publish 并 bounded-drain,再 shutdown transport / 唤醒 wait,随后 bounded-join apply thread;线程在仍持 context 时 detach decoder、销毁 private backend,最后才销毁 emitter 与 transport。这样 stats dump、`pActiveBackendObject.reset()` 与 ring unmap 都发生在 thread 退出之后。 +退出顺序:`ShutdownSplitRoles` 先让 client publish 并 bounded-drain,再 shutdown transport / 唤醒 wait,随后 bounded-join apply thread;线程在仍持 context 时 detach decoder、销毁 private backend,最后才销毁 emitter 与 transport。 -### 17.4 lane 隔离、G5 与阶段边界 +### 17.4 lane 隔离与阶段边界 -每条 `DirectGLES.Split.*` entry 有独立 `MOBILEGL_LOG_FILE_PATH`;E1/E3 控制从被选 entry 的私有文件取 Fatal,不能从并行 lane 的公共日志“借红”(ID-53)。G5 的第十一行 `FlushPendingRangesFrom` 因 P5 的 range 参数化被承认,但两份 untouched-region script 都**始终**对固定 pin `172b0222…` 比较;re-pin 必须同时改两份脚本,不能让 ref-a/ref-b 漂移掩掉改变(ID-41)。 +每条 `DirectGLES.Split.*` entry 有独立 `MOBILEGL_LOG_FILE_PATH`;E1/E3 控制从被选 entry 的私有文件取 Fatal,且把"选中条目被跳过"判为失败(ID-53/62)。`integration-split` 是缩减路径的硬门;broad inproc `integration-gpu` 车道只记录普查、不设门(ID-65)。 -P5 到此只声称 reduced path:private lane、barrier、reply、shadow 与 context ownership 已落地;class-C verb、27 个 readback/query wrong-answer、`rsp` residual inputs、PACK-PBO 真 carrier、>2 MiB readback、GetCaps 的两个 server→client blob carrier 与 ABI fingerprint 的 segment-size 混入都属于 P6+ / P7 / P3b-P4b,逐项见 `ROADMAP.md`,不由 `inproc` 同地址空间替它们背书。 +### 17.5 P5b:class-C verb 迁移 + +- **顺序由动态普查决定**(ID-68):79 个 trace × 集成车道在 inproc 下的首阻塞统计(`~/w7/notes/p6/census-classC.md`),所有 Minecraft trace 首阻塞 = `DrawElements` → Minecraft 优先,四包并行(d1 draws、i1 image/compute、t2 XFB/tess、f1 clear/copy/mip)+ 尾包(sync、具名 blit、GLES mip)。 +- **一次迁移 = 一个发射器 + 一个 sink 体**:c0b 先给 25 个已测量槽铺 wire 记录(`Wire/PipeWireCodec` 的行)、`WireVerbSink` 分派与 `ServerVerbSink` 具名 Fatal stub;包只在 `EmitTables.cpp` 把槽从 C 挪到 B、在 `PipeApplier.cpp` 填 sink 体(apply 线程、只从记录与 server 状态取输入)。这些槽到达的是 sink,不是 `MGPipeApply*`。 +- **规则 D**:记录把 GL 调用逐字带在句柄旁边(draw 的 mode/count/type/offset/instance/base 字段、clear 的值类与 drawbuffer 下标、named-framebuffer 的句柄形式),后端保留它的 barrier-pulled 读;未测量的形式**按名拒绝**(`+RENDERBUFFER`、`+UNBOUND` 等)而不是猜。 +- **draw 家族**:19 个索引 / 实例 / multi-draw / indirect 槽全部下沉到 `draw_vbo`;用户索引 span 的 shape/extent 门在 encoder / decoder / sink 三端共用(单 range、宽度 1/2/4、`Uint64(Count) × IndexSize ≤ Size`)。 +- **sync**:`FenceSync` / `ClientWaitSync` / `GetSyncStatus` / `WaitSync` / `DeleteSync` 五条现有 opcode 接线,client 铸造 Fence 句柄,wire 只过 `{slot,gen}`,native fence 归 apply 线程;顺带修掉 `MGL_BACKEND_SLOT_PTR_LOCAL` 对 split 恒返回 nullptr。 +- **具名 blit**:`BlitNamedFramebuffer` 经作用域化 client-shadow read/draw 绑定发布,降为现有 bound backend 调用,退出恢复公开绑定。**GLES mip storage**:前端先定义并发布层级,server 只验证 applier descriptor 的 Levels/extent;registry 身份解析只 Find 不 mint。 +- **P5b 留下的 inproc 依赖,P6 必须替换**:具名 blit 的 scoped client binding + barrier;mip descriptor 的 barrier-held registry 查询;FBO death 的 inproc mailbox。三者跨地址空间都不成立。 ## 附 A:开关 @@ -634,29 +570,44 @@ CMake: | 选项 | 默认 | 状态 | |---|---|---| | `MOBILEGL_BUILD_DISAGGREGATED` | OFF | 已落地 | +| `MOBILEGL_BUILD_DISAGGREGATED_INPROC` | OFF | 已落地(P5,隐含前者) | | `MOBILEGL_BUILD_SERVER_SPIKE` | OFF(仅 Android) | 已落地(spike A,非出货) | -| `MOBILEGL_BUILD_DISAGGREGATED_INPROC` | OFF | 已落地(P5) | -| `MOBILEGL_PIPE_VERIFY` | OFF | 计划(P1;构建期开关,编译进 `SnapshotFromGLContext()` 与 G4 比对器,P13 后保留) | -| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON(P2..P13) | 已落地(P2;`CMakeLists.txt:36`,OFF 时不编译 pre-handle 臂;`MOBILEGL_PIPE_PUSH=OFF` 会把它强制回 ON 并 `message(STATUS)`,`CMakeLists.txt:476-479`,因为 pull 构建里 pre-handle 臂就是唯一的实现) | -| `MOBILEGL_FLATC_EXECUTABLE` | 空 | 已落地(只服务 `flatc-check`) | -| `MOBILEGL_BAKED_INTERNAL_SHADERS` | ON(P7+) | 计划 | +| `MOBILEGL_PIPE_PUSH` | OFF | 已落地(P1;push / verify / split flavour 都打开它) | +| `MOBILEGL_PIPE_VERIFY` | OFF | 已落地(P1;隐含 `PIPE_PUSH`,编译 `SnapshotFromGLContext()` 与 G4 比对器,永不出货,P13 后保留) | +| `MOBILEGL_PIPE_LEGACY_MEMOS` | ON | 已落地(P2;OFF 时不编译 pre-handle 臂;`PIPE_PUSH=OFF` 强制回 ON) | +| `MOBILEGL_FLATC_EXECUTABLE` | 空 | 只服务 `flatc-check` | +| `MOBILEGL_BAKED_INTERNAL_SHADERS` | ON | 计划(P7) | -运行时,MGPipe(`MobileGL/Config.h:319-398`,`MobileGL/ConfigLoader.cpp:247-278`;P0 已落地,P2 扩了 push 位图的默认值与两个对照旋钮): +运行时,MGPipe(`MobileGL/Config.h`、`ConfigLoader.cpp`): | 变量 | 默认 | 说明 | |---|---|---| -| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`;**push 构建 `0x1fff`**(`kMGPipeSubsystemsMigratedAtP4a`;`0x1ff` = `kMGPipeSubsystemsMigratedAtP3a`、`0x7f` = `...AtP2` 都保留作分阶段对照,`MobileGL/MG_Pipe/MGPipe.h`,读入点 `MobileGL/ConfigLoader.cpp`)。**P4a 的四位与它们的三条依赖拒绝**:位 9 framebuffer、位 10 纹理资源、位 11 sampler、位 12 program;位 11 要位 10(每个 `MGPBoundView::Texture` / `MGPImageView::Res` 都是纹理句柄)、位 9 要位 10(`MGPSurface::Res` 同理)、位 10 要位 7(buffer texture 的 `BufferForTexBuffer`)**外加 P4a 加的第四条:位 10 要位 11**(`MGPTextureParams::BuiltinSampler` 是 sampler CSO 句柄,只有位 11 铸它,空句柄是 `Fatal{ProtocolCorruption}`)。**两侧都要拒**:服务端在各族的 arm resolver 里拒绝并跑旧臂,客户端在 `PipeFill.cpp` 的族门里**根本不发射**——只在服务端拒会出现"客户端已按 acceptance 清了 dirty、服务端却走旧臂"的丢上传(`0x7ff` 一度 438/491)。同一个族门上还挂着**消费者条件**:没有任何后端注册 `MGPipeResourceOps` 时四族一条不发(Magma 就是这种情形) | 子系统位图,十进制或 `0x`;位按 ROADMAP 顺序分配、永不复用(`MobileGL/MG_Pipe/MGPipe.h:72-95`):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch state、`0x08` vertex attrib defaults、`0x10` residual values、`0x20` Espryt slots(Track H)、`0x40` Magma vertex input(Track H)、**`0x80` 位 7 resources(P3a:`resource_*` 家族,`kMGPipeSubsystemResources`,`MGPipe.h:84`)**、**`0x100` 位 8 vertex input(P3a:vertex elements / vertex buffers / index buffer,`kMGPipeSubsystemVertexInput`,`MGPipe.h:85`)**;位 9..62 留给后续阶段。**位 8 依赖位 7**:属性的缓冲 id 经资源 slot 表解析,只有位 7 填那张表,所以 `0x17f` 会打一行 ERROR 点名拒绝位 8 并回落到 legacy vertex-input 臂(`MobileGL/MG_Backend/DirectGLES/Managers.cpp:2312`)。**位 63 不是子系统而是行为**:`kMGPipeBehaviourNoCsoContentAddressing`(`MGPipe.h:90`)关掉 client 侧 CSO 内容寻址(每次 pipeline 版本变化都铸新 CSO、永不探测 map),即 P2 的负面对照。`0` = 全 pull,但 P2 之后只有在 `MOBILEGL_PIPE_LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 | -| `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` | 0 | 负面对照 C(push 构建才有,`MobileGL/Config.h:360-371`):故意打掉句柄身份,让 `HandleRecycle` 的 ABA 臂重现旧的 A-B-A 污染。它变绿即为控制失效 | -| `MOBILEGL_PIPE_VERIFY` | 0 | 逐 draw 逐字段影子比对 | -| `MOBILEGL_PIPE_STATS` | 0 | 边界计数器(§附 B) | +| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`;push 构建 `0x1fff` | 子系统位图(`MG_Pipe/MGPipe.h`,位永不复用):`0x01` 渲染状态、`0x02` pixel pack、`0x04` patch、`0x08` attrib defaults、`0x10` residual、`0x20` Espryt slots、`0x40` Magma vertex input(以上 P2 = `0x7f`)、`0x80` resources、`0x100` vertex input(P3a = `0x1ff`)、`0x200` framebuffer、`0x400` 纹理资源、`0x800` sampler、`0x1000` program(P4a = `0x1fff`)。依赖(两侧都拒,客户端不发射):位 8 要 7;位 11 要 10;位 9 要 10;位 10 要 7 和 11。位 63 `kMGPipeBehaviourNoCsoContentAddressing` 是行为对照不是子系统。`0` = 全 pull,只在 `LEGACY_MEMOS` 编进了 pre-handle 臂时才是有效对照 | +| `MOBILEGL_PIPE_VERIFY` / `_VERIFY_FATAL` / `_VERIFY_CORRUPT` / `_POISON_OMIT` | 0 / 1 / 空 / 空 | 逐 draw 逐字段影子比对;首个分歧是否 abort;篡改字段 / 抽掉填充戳记两个阴性对照(verify 构建才有) | +| `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` | 0 | 阴性对照 C:故意打掉句柄身份让 `HandleRecycle` 的 ABA 臂重现旧污染 | +| `MOBILEGL_PIPE_STATS` / `_STATS_PERIOD` / `_STATS_FILE` | 0 / 120 / 空 | 边界计数器(附 B);每多少帧一条汇总行;teardown JSON(trace app 从不到达 teardown,设备上只有周期行) | | `MOBILEGL_PIPE_LEGACY_MEMOS` | ON | 三态读取,只有显式 falsy 才关 | | `MOBILEGL_PIPE_TEXEL_RETAIN_MB` | 0(0–4096) | 纹理拉取保留 LRU | -| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64(0–4096) | 索引宿主镜像预算 | -| `MOBILEGL_PIPE_STATS_PERIOD` | 120(1–10⁶) | 每多少帧一条汇总行 | -| `MOBILEGL_PIPE_STATS_FILE` | 空 | teardown 时的 JSON 转储路径 | +| `MOBILEGL_PIPE_INDEX_MIRROR_MB` | 64(0–4096) | 索引宿主镜像预算(P8) | -运行时,传输与 IPC(P5 已落地的核心项):`MOBILEGL_TRANSPORT`(monolith)、`MOBILEGL_IPC_VERB_BARRIER`(1)、`MOBILEGL_IPC_RING_MB`(8)、`MOBILEGL_IPC_STAGE_MB`(32)、`MOBILEGL_IPC_PERSISTENT_BLOCK_KB`(64)、`MOBILEGL_IPC_ADOPT_TIER`(2)、`MOBILEGL_IPC_STRICT_ERRORS`(0)、`MOBILEGL_IPC_AUDIT`(0)、`MOBILEGL_IPC_SERVER_AFFINITY`(auto)。`MOBILEGL_IPC_SERVER_PATH`、present credit、poll escalation、shadow shm、attach/respawn/idle-exit 等随 P6+ 生效。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`(server 没有 `MG_Impl` 校验器)。既有负面对照开关全部保留。 +运行时,传输与 IPC: -## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h:46-122`,P0 已落地) +| 变量 | 默认 | 说明 | +|---|---|---| +| `MOBILEGL_TRANSPORT` | `monolith` | `inproc` 已落地;`spawn` / `unix:` / `pipe:` 是 P6,当前具名拒绝并回落 monolith | +| `MOBILEGL_IPC_RING_MB` | 8 | `SEG_CMD`;单条记录至多一半 | +| `MOBILEGL_IPC_STAGE_MB` | 32 | `SEG_STAGE`;目标负载 profile 显式 256 | +| `MOBILEGL_IPC_SPIN_US` | 50 | park 前自旋 | +| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | 64 | persistent-map 推送块粒度;`0` 是 E3(a) 阴性对照 | +| `MOBILEGL_IPC_ADOPT_TIER` | 2 | `auto/0/1/2`;P5 split 用 emulated(T2) | +| `MOBILEGL_IPC_VERB_BARRIER` | 1 | 每 verb 等 `appliedSeq == emitSeq`;`0` 只作 E1 阴性对照 | +| `MOBILEGL_IPC_STRICT_ERRORS` | 0 | BARRIER-PULLED residual input 提升为具名 Fatal | +| `MOBILEGL_IPC_AUDIT` | 0 | retire 后 `0xDD` 填退休 staging | +| `MOBILEGL_IPC_SERVER_AFFINITY` | `auto` | apply 线程亲和性 | +| `MOBILEGL_IPC_SERVER_PATH` | 空 | P6 消费 | -关闭时每站点一次全局 load + 一条永不命中的分支。字节类:`stage-buffer`、`stage-texture`、`stage-ubo-global`、`stage-ubo-named`(只有 Magma 贡献,D-B8 的不对称)、`stage-vertex-client`、`stage-index-client`、`stage-indirect-cmd`(Espryt 独有)、`persistent-map-push`(P0 未接线:monolith 期不存在推送)、`residual-value-block`(占位)。调用类:`draws`、`accessor-calls`(实际执行的 GLContext accessor 次数,在约 10 个热入口做静态计数,是**下界**)、`texture-upload-emissions/box/rect/jobs`。六个 memo 门(`SyncRenderState` 早退、`SyncNeccessaryTextures` 键比较、`CurrentUnitBindingsEpoch` 快门、`TrySetupDrawFastPath`、pipeline memo、`ApplyDynamicDrawStateTail`)各计 hit/miss。每 draw payload 直方图(24 桶)已实现,等第一个发射器接入。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行(`MGLOG_I`),`TRACY_ENABLE` 下逐帧 `TracyPlot`,teardown 时可选 JSON。站点清单——哪些路径**没有**接线——写在 `MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100`,那份清单是契约。 +P6+ 生效:`MOBILEGL_IPC_PRESENT_CREDIT`、`MOBILEGL_IPC_POLL_ESCALATE`、shadow shm、`MOBILEGL_IPC_RESPAWN`、`MOBILEGL_IPC_IDLE_EXIT_S`。显式不设立:`MOBILEGL_IPC_PROGRAM`(没有 relink 档)、`MOBILEGL_IPC_VALIDATE_SERVER`(server 没有 `MG_Impl` 校验器)。 + +## 附 B:边界计数器(`MobileGL/MG_Util/Metrics/PipeStats.h`) + +关闭时每站点一次全局 load + 一条永不命中的分支。字节类:`stage-buffer`、`stage-texture`、`stage-ubo-global`、`stage-ubo-named`(只有 Magma 贡献,D-B8)、`stage-vertex-client`、`stage-index-client`、`stage-indirect-cmd`、`persistent-map-push`(`pmap`)、`residual-value-block`(`resid`)、`cso-blob-bytes`(`csob-blob`)。调用类:`draws`、`accessor-calls`(约 10 个热入口的**静态**计数,是下界——把读点搬走与把工作去掉在它上面长得一样,判性能只看 CPU 时间序列与 memo 门)、`texture-upload-emissions/box/rect/jobs`、`csom`/`csob`(CSO 铸造 / 绑定)、`mpr`、`trp`(纹理重铸拉取)、`rsp`(residual reads,只是有 stamp 读点的下界)、`maxrec`/`ringwraps`/`ringpads`/`ringwaits`(run totals)。六个 memo 门(`ers`/`etl`/`eub`、`mfp`/`mpm`/`mdt`)各计 hit/miss。每 `MOBILEGL_PIPE_STATS_PERIOD` 帧一条 `MGPipe stats:` 汇总行;站点清单——哪些路径**没有**接线——写在 `PipeStats.cpp` 头部,那份清单是契约。逐 dirty 位触发率与每 draw payload 直方图已实现但从未接进汇总行(未测)。 diff --git a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md index 2b5112ac..0da10e2e 100644 --- a/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md +++ b/docs/Disaggregated/CURRENT_STAGE_PROGRESS.md @@ -1,15 +1,14 @@ # 当前阶段进度 -分支 `feat/disaggregated`,头 `c77831e0`(2026-09-16,已推 GitHub)。本文随每次落地更新。 -逐条裁定的长文原文在 git 历史里(`git log -p -- docs/Disaggregated/CURRENT_STAGE_PROGRESS.md`,`ef35ea0c` 之前的版本 = ID-1..75 全文)。 +分支 `feat/disaggregated`;代码头 `82683d4a`(2026-09-16),其后只有文档提交。本文随每次落地更新。ID-1..75 的逐条裁定长文在 git 历史(`ef35ea0c` 之前版本的本文件)。 ## 1. 阶段状态 -| 阶段 | 状态 | 范围 | +| 阶段 | 状态 | 范围 / 证据 | |---|---|---| -| P0 / P0.5 / P1 / P2 / P3a / P4a | 已落地 | 见 `ROADMAP.md`、`MEASUREMENTS.md` §1–§25 | -| **P5** 首个 IPC 帧(reduced path) | **已收官** | `ff2994d9..37fc4fdb`,八包 + j0/x2/ap/docs | -| **P5b** inproc 下的 verb 迁移 | **已收官(2026-09-16)** | `37fc4fdb..c77831e0`;出口证据见 `MEASUREMENTS.md` §33–§35 | +| P0 / P0.5 / P1 / P2 / P3a / P4a(monolith 跑道) | 已落地 | `ROADMAP.md` 阶段表;`MEASUREMENTS.md` §1–§5 | +| **P5** 首个 IPC 帧(reduced path,lockstep inproc) | 已收官 | `ff2994d9..37fc4fdb`;`MEASUREMENTS.md` §6 | +| **P5b** inproc 下的 verb 迁移(Minecraft 优先) | **已收官(2026-09-16)** | `37fc4fdb..82683d4a`;`MEASUREMENTS.md` §7 | | P6 spawn transport | 未开始 | 设备出口已完成,可以开始 | ## 2. 当前头实测 @@ -18,16 +17,16 @@ |---|---| | 构建 | pull / push / verify / split 四个 flavour 全部通过 | | G1(pull 符号恒等) | `.text +0`,27814 符号 0 增 / 0 删 / 0 resize / 0 重命名 | -| unit | pull / push / verify 各 1817/1817;split 2138/2138(`348d22a4` 主机门为 2132;含 `7cb29d46` 后 UserIndexSpan 3/3 复证) | +| 发射表分区(`EmitTables.cpp` 的 `static_assert`) | A(本地回答)2 / B(发射)54 / C(具名拒绝)15,共 71 槽 | +| unit | pull / push / verify 各 1817;split 2138(含 `7cb29d46` 后 `UserIndexSpan` 3/3) | | `integration-split`(inproc) | **107/107**(P5 收官时 22) | -| `integration-gpu`(push) | 1148/1148 | -| `integration-gpu`(split,单体传输) | 1267/1267 | +| `integration-gpu` | push 1148/1148;split 单体传输 1267/1267 | | G5(p3a / p4a 保护区) | 双绿 | -| 主机收尾全门(`348d22a4`) | **complete=true, exit 0**:31 步原始 rc=0 + retrace-verify 续跑合并 **79/79**,retrace-push 79/79,OpenRA 2/2;对账 `reconciliation-20260916.md` | -| 集成普查(inproc 车道,`348d22a4`) | 1267 selected = 811 passed / 203 skipped / 62 aborted / 191 failed,对 c0b **零回退** | -| 79 trace 普查(inproc,stage 256 MiB,`348d22a4`) | **72 passed / 6 aborted / 1 failed**(三轮续跑补齐;首阻塞清单见 `MEASUREMENTS.md` §33) | -| Redmi 正确性(`82683d4a`,p5bcodex2,stage 256 MiB) | **8/8**,四条 A/B trace 双后端首次在设备上 inproc 渲染 | -| Redmi 四臂 A/B(同上) | 24/32 组 200 帧尾验证全绿,barrier tax 首次实测(`MEASUREMENTS.md` §35);iris-bsl 8 组为 fixture 123 帧上限,补充表另记 | +| 主机收尾全门(`348d22a4`) | complete=true,exit 0:31 步 rc=0;retrace-push 79/79、retrace-verify 79/79(钉住库续跑合并);OpenRA 2/2 | +| 集成普查(inproc 车道,`348d22a4`) | 1267 selected = 811 passed / 203 skipped / 62 aborted / 191 failed;对 c0b 基线零回退 | +| 79 trace 普查(inproc,stage 256 MiB,`348d22a4`) | **72 passed / 6 aborted / 1 failed** | +| Redmi 正确性(`82683d4a`,APK `p5bcodex2`,stage 256 MiB) | **8/8**:四条 A/B trace × 双后端首次在设备上 inproc 渲染 | +| Redmi 四臂 A/B | 24/32 组 200 帧尾验证全绿;barrier tax(split−push 逐线程 CPU p50)+5.9% – +18.2%;iris-bsl 8 组为 fixture 123 帧上限(pull 同失败),补充表另记 | ## 3. P5b 落地内容 @@ -45,31 +44,30 @@ ## 4. 真实负载(inproc,独立 apply 线程) -- `DrawElements` 不再是阻塞。d1 普查时 77 个 Minecraft 后端用例中 28 个渲染通过(SSIM ≥ 0.99995)。 -- blit + mip 之后:`improved-transparency-minecraft-26.3` DirectVulkan SSIM 0.999914;`minecraft-1.21.4-fabric-iris-bsl-in-world` DirectVulkan 0.997324、DirectGLES 0.997496。 -- OpenRA 双后端 SSIM 1.0。 -- **新头完整 trace 普查已跑(2026-09-16,`348d22a4`,stage 256 MiB):79 = 72 passed / 6 aborted / 1 failed**。首阻塞:`rd12` GLES `InitialBytesNotCarried/resource_respecify`、VK `BarrierTimeout/Present`;`iris-photon` / `iris-derivative` / `create-indirect` GLES 均 `UnmigratedEmulation/texture-remint-pull`;`iris-bsl-esc-menu-854` GLES `InitialBytesNotCarried/resource_respecify`;`create-indirect` VK 内存膨胀被守护杀死(llvmpipe 上 >60 GiB RSS,两次复现)。 -- **Redmi 出口达成(2026-09-16)**:四条 A/B trace × 双后端在设备上以 inproc split 渲染,正确性 8/8;四臂 A/B 首次测得 barrier tax(逐线程 CPU p50 split−push:+5.9% – +18.2%,六组;`MEASUREMENTS.md` §35)。 +- `DrawElements` 不再是阻塞:d1 普查时 77 个 Minecraft 后端用例中 28 个渲染通过(SSIM ≥ 0.99995)。 +- blit + mip 之后:`improved-transparency-minecraft-26.3` DirectGLES SSIM 1.0 / DirectVulkan 0.999914;`minecraft-1.21.4-fabric-iris-bsl-in-world` DirectVulkan 0.997324、DirectGLES 0.997496;OpenRA 双后端 1.0。 +- 79 trace 普查 7 条未过项的首阻塞:`rd12` GLES `Fatal{InitialBytesNotCarried,"resource_respecify"}`、VK `Fatal{BarrierTimeout,"Present"}`;`iris-photon` / `iris-derivative` / `create-indirect` GLES 均 `Fatal{UnmigratedEmulation,"texture-remint-pull"}`;`iris-bsl-esc-menu-854` GLES 同 rd12 GLES;`create-indirect` VK 在 llvmpipe 上内存膨胀(>60 GiB RSS,两次复现,守护杀死)。 +- Redmi 出口(2026-09-16):正确性 8/8;barrier tax 首测,逐例见 `MEASUREMENTS.md` §7.4。 ## 5. 开放项(按优先级) -| 项 | 证据 | +| 项 | 证据 / 去向 | |---|---| -| `SEG_STAGE` 默认 32 MiB 装不下 128 MiB 上传:**决定 = 默认不改**,普查/Redmi 显式 256 MiB profile;更大 blob 的分块/专用 carrier 留 P8 设计(ROADMAP 开放问题 11) | `p5b-results/blit-codex-v1.md`;`MEASUREMENTS.md` §33 | +| `SEG_STAGE` 默认 32 MiB 装不下目标负载的单次 128 MiB 上传;**决定 = 默认不改**,普查与 Redmi 显式 `MOBILEGL_IPC_STAGE_MB=256`;分块 / 专用 carrier 留 P8(`ROADMAP.md` 开放问题 11) | `p5b-results/blit-codex-v1.md`;`MEASUREMENTS.md` §7.2 | | Magma(DirectVulkan)split compute/image 路径:89 个错答中 82 个 | `p5b-results/i1-v1.md` | -| rd12 DirectGLES `InitialBytesNotCarried/resource_respecify`;rd12 DirectVulkan `BarrierTimeout/Present`;`iris-bsl-esc-menu-854` GLES 同 rd12 GLES;`create-indirect` VK 主机内存膨胀(守护杀死,两次复现) | 79 trace 普查 `counts.json` / `trace-transitions.json` | -| RGB 三通道 CPU mip 回退 | `p5b-results/mip-codex-v1.md` | -| ~~设备 barrier tax 未测~~ **已测(2026-09-16)**:split−push 逐线程 CPU p50 +5.9% – +18.2%,逐例见 `MEASUREMENTS.md` §35 | `ab-tables.md` | -| 27 个 P5 inproc 错答:纹理读回走 client-shadow 回退、P7 query 计数、一个检查 harness、一个 FBO/RBO 删除后生命期 | P4b / P7 / P6 债 | -| `rsp` 残余输入;`SEG_REPLY` 2 MiB 上限;GetCaps 两个 blobref 的载体;PACK-PBO 读回真实形式 | P3b/P4b、P6+ | -| 39 个未被任何负载命中的 class-C 槽(query / sync / indirect / 具名 FBO clear 等) | P5b wave 3 / P9 / P10 | -| `test.yml`/`apk.yml` 里的 `feat/disaggregated` 触发器是临时的,合入 dev 前必须移除 | — | -| 设备钉频口径:Redmi `2f7cbe2e` 厂商 GPU 上限已消失,2026-09-16 起 pin 为 1100 MHz(原 1050),跨口径活动不可比钟频 | `pin_device.sh` 更新记录;`MEASUREMENTS.md` §35 | +| rd12 GLES `InitialBytesNotCarried/resource_respecify`、rd12 VK `BarrierTimeout/Present`、`iris-bsl-esc-menu-854` GLES、三条 `texture-remint-pull` 仿真槽、`create-indirect` VK 内存膨胀 | 79 trace 普查 `counts.json` / `trace-transitions.json` | +| RGB 三通道 CPU mip 回退仍是具名 Fatal | `p5b-results/mip-codex-v1.md` | +| P5b 的 inproc 依赖:具名 blit 的 scoped client binding + barrier、mip descriptor 的 barrier-held registry 查询、FBO death 的 inproc mailbox | P6 必须替换(`ROADMAP.md` 债务表) | +| 27 个 P5 inproc 错答:22 纹理读回走 client-shadow 回退、3 query、1 inspection、1 FBO/RBO 删除后生命期 | P4b / P7 / P6 债 | +| `rsp` 残余输入;`SEG_REPLY` 2 MiB 单槽上限;GetCaps 两个 blobref 的载体;PACK-PBO 读回真实形式 | P3b/P4b、P6+ | +| 15 个 class-C 槽(query / sync 尾 / `GetTexImage` / `SetSwapInterval` 等)无负载命中,仍具名拒绝 | P9 / P10 | +| Redmi 定频行只在树外 `~/w7/notes/p2/devices/pin_device.sh`;钉频口径 2026-09-16 起 1100 MHz(原 1050) | `devices/pin-verification-2026-09-07.md` | +| `test.yml` / `apk.yml` 的 `feat/disaggregated` 触发器是临时的,合入 dev 前必须移除 | — | ## 6. 下一步 -1. **P6 spawn transport**:替换 P5b 的 inproc 依赖(具名 blit scoped binding + barrier、mip descriptor 的 barrier-held registry 查询、FBO death 的 inproc mailbox——ROADMAP 债务表),`SocketTransport` + `ServerMain` + 握手/退出语义。 -2. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、texture-remint-pull 仿真槽)。 +1. **P6 spawn transport**:`SocketTransport` + `ServerMain` + 握手 / 退出语义;替换 P5b 的三处 inproc 依赖(上表)。 +2. 剩余首阻塞一轮(Magma compute/image、rd12、RGB mip、`texture-remint-pull` 仿真槽)。 3. P6 出口门:P5b 的完整渲染路径在 `spawn` 下绿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99。 ## 7. 记录位置 @@ -81,48 +79,29 @@ | P5b brief / 契约 / 报告 | `~/w7/notes/p5b/BRIEF-P5B.md`、`MobileGL/MG_Remote/CONTRACT-P5B.md`、`~/w7/notes/p5b/p5b-results/` | | P5b 收官主机门 | `~/w7/p5b-final-host-348d22a4/`(`reconciliation-20260916.md`、`summary-counts.{json,md}`) | | P5b 合并普查 | `~/w7/p5b-final-census-348d22a4/`(+ `-resume1/2/3`)、`p5b-results/joint-codex-v1.md` census 块 | -| P5b 设备证据 | `~/w7/notes/p5b/apk/p5bcodex2/`(APK + proof);`MobileGL/.trace-work/p5b-redmi/p5bcodex2/2f7cbe2e/`(correctness 8/8、benchmark `ab-tables.md` 与 bsl 补充表) | +| P5b 设备证据 | `~/w7/notes/p5b/apk/p5bcodex2/`(APK + proof);`MobileGL/.trace-work/p5b-redmi/p5bcodex2/2f7cbe2e/`(correctness 8/8、`ab-tables.md` 与 bsl 补充表) | | class-C 普查 | `~/w7/notes/p6/census-classC.md`;基线 `~/w7/p5b-c0b-census-logs/results.json` | | 门日志 | `~/w7/p5-joint-gate.log`、`~/w7/p5b-quickgate.log`、`~/w7/p5-joint-evidence/` | -| 脚本 | `~/w7/notes/tools/`(`wsl_p5_gate.sh`、`p5_ab_redmi.sh`、`wsl_build_p5_apks.sh`、`p6_census_*.{sh,py}`、`p5b-c0b-census.sh`) | +| 脚本 | `~/w7/notes/tools/`(`wsl_p5_gate.sh`、`p5_ab_redmi.sh`、`p5b_codex_redmi.sh`、`wsl_build_p5_apks.sh`、`p6_census_*.{sh,py}`、`p5b-c0b-census.sh`) | -## 8. 裁定索引(ID-40..77;ID-1..39 见 git 历史) +## 8. 仍在生效的裁定(ID 索引;过程性裁定与 ID-1..39 见 git 历史) | ID | 裁定 | |---|---| -| 40 | wave-1 谱系重写:`~/w7/pipe` 曾残留 `rereview` 身份,58 个提交改回用户身份并重挂到 GitHub 谱系 | -| 41 | G5 第十一行 `FlushPendingRangesFrom` 始终对 pin 比较,pin 重钉在 `3dadd4c1` | -| 42 | persistent-map 成员性期望改为 `live == (arm == emulated)`;emulated 方向红检在联调头 VERIFIED | -| 43 | `RingTest` 双旗标空间用例改述实树:带 CALL 旗标的头会被弹出,不再被当 wrap filler 吞掉 | -| 44 | dev 合并;G1 基线按 dev 重取(`4595163c`) | -| 45 | wave-2 从配额中断处重启 | -| 46 | codex 对 wave-1 的跨族审查:10 个 major,逐条执行验证全部确认 | -| 47 | `SEG_REPLY` 改 16 MiB / 8 个 2 MiB slot;超限在 client 侧具名拒绝 | -| 48 | wave 1.5 分派(w1/s1/t1/p1) | +| 41 | G5 第十一行 `FlushPendingRangesFrom` 始终对固定 pin 比较;re-pin 必须同时改两份脚本 | +| 42 | persistent-map 成员性期望 `live == (arm == emulated)` | +| 43 | 带 CALL 旗标的 ring 头会被弹出,不当 wrap filler | +| 47 | `SEG_REPLY` 16 MiB / 8 × 2 MiB slot;超限在 client 侧具名拒绝;ABI fingerprint 尚未混入段尺寸 | | 49 | ReadPixels 以紧凑形式过线:server 用中性 pack 读进回复,client 按应用 pack 散布 | -| 50/52 | v1 两份审查合并;`liveHostBase()` 在有传输时不得回落到 client 的 `MappedData()` | -| 51 | CI 上内存台账测试红 = 内核 hiwater 滞后,台账自持峰值 | -| 53 | 每个 `DirectGLES.Split.*` 条目一个私有日志;E1 对照从日志读 `Fatal{BarrierViolation}` | +| 50/52 | 有传输时 `liveHostBase()` 不得回落到 client 的 `MappedData()` | +| 53 | 每个 `DirectGLES.Split.*` 条目一个私有日志;对照只从被选条目的私有日志取 Fatal | | 54 | forwarder 每 (dpy, draw, read, ctx) tuple 只 bind 一次;client 的 release 不解绑 apply 线程的上下文 | -| 55 | c1 的 R-17:33 行走生成表 + 4 行 escapes + 回复邮箱;`PipeCatalogueTest` 钉 37 | -| 56/58 | c1 两份审查(codex 12 项 / Claude 3 blocker 8 major 6 minor);B1 = 五处取地址的 `&MGPipeApply*` 逃过转换 | -| 57 | split 下绑定 PACK PBO 的 ReadPixels 具名拒绝,P5 不实现 | -| 59 | v1 round 2 落地;联调 `integration-split` 21/21 | -| 60 | 实现包可派给 GPT 族;实现方与审查方错开模型族 | -| 61 | c1 round 3 + j0 review + 联调包 | -| 62 | v1 round-2 审查:`MOBILEGL_IPC_VERB_BARRIER=0` 杀掉 pre-flight 导致 21 个条目全 skip、ctest 返 0 —— 对照必须把"选中条目被跳过"判为失败 | -| 63 | 联调头五部分全门实测 | -| 64 | c1 round-3 判定 MERGEABLE;余项转 c1f 门包 | -| 65 | 联调处置:27 个 inproc 错答归 P4b/P7 债;E2 clear-drop 对照未变红 → x2 | -| 66 | 过程规则改变:不过度验证、轮次间不审查、阶段收官由 codex 审一次、只读/非核心任务派给 codex | +| 55 | R-17:33 行生成路由 + 4 escape,`PipeCatalogueTest` 钉 37;`&MGPipeApply*` 地址取用点同样在扫描内 | +| 57 | split 下绑定 PACK PBO 的 ReadPixels 具名拒绝;真实形式(server 写 buffer resource + client `MarkGpuWritten`)留 P6+ | +| 62 | 对照必须把"选中条目被跳过"判为失败 | +| 65 | 27 个 inproc 错答归 P4b/P7 债;普查只记录不设门 | +| 66 | 过程规则:不过度验证、轮次间不审查、每阶段收官由 Codex 审一次、只读 / 非核心任务派给 Codex | | 67 | 相同 tuple 的重复 make-current 不重发 caps;不同 tuple 重发且 client 立即采纳 | -| 68 | class-C 普查:所有 Minecraft trace 首阻塞 = `DrawElements`;P5b 计划(Minecraft 优先) | -| 69 | v1 round 3;红米 A/B 记录(split 臂当时不可测) | -| 70 | c0b 落地;四包启动;APK lane 根因 = setup-android 默认 cmdline-tools 20.0 已无 `tools` 包 | -| 71 | x2 经 x2m 合入,P5 代码齐全 | -| 72 | f1 落地:34 个 clear/copy/mip 中止归零 | -| 73 | P5 收官审查:1 blocker / 10 major / 2 minor → r1(核心)/ r2(CI) | -| 74 | 配额中断期间由用户的 Codex root task 推进 P5b 第二波;不重启同名 Claude 包 | -| 75 | P5b 整波落地推送 `7cb29d46`;快速门全绿;修掉一个测试日志捕获缺陷(库按进程截断日志,`RunInChild` 的增量读把第 2、3 个子进程的诊断读空) | -| 76 | P5b 收官:主机门(`348d22a4`)retrace-verify 以钉住库续跑合并 79/79、complete=true;79 trace 普查三轮续跑补齐(72/6/1);审查补遗追加钉定结果;**P5b 出口 = Redmi 正确性 8/8 + 四臂 A/B(barrier tax 首测)** | -| 77 | Redmi `2f7cbe2e` 钉频期望 1050→1100 MHz:2026-09-11 实测的厂商 thermal_pwrlevel 上限已消失,`pin_device.sh` 按 2026-09-16 实测更新;`iris-bsl` benchmark 8 组判为 fixture 123 帧上限(pull 同失败,非回归),123 帧序列折算补充表不进 200 帧尾门表 | +| 68 | class-C 普查决定迁移顺序(所有 Minecraft trace 首阻塞 = `DrawElements` → Minecraft 优先) | +| 76 | P5b 出口 = 主机门 `348d22a4` complete + 79 trace 普查 + Redmi 正确性 8/8 + 四臂 A/B | +| 77 | Redmi 钉频期望 1050 → 1100 MHz(2026-09-16);iris-bsl 8 组判为 fixture 123 帧上限,补充表不进 200 帧尾门表 | diff --git a/docs/Disaggregated/MEASUREMENTS.md b/docs/Disaggregated/MEASUREMENTS.md index 71639e9e..846658b0 100644 --- a/docs/Disaggregated/MEASUREMENTS.md +++ b/docs/Disaggregated/MEASUREMENTS.md @@ -1,591 +1,291 @@ -# 实测记录(P0、P1、P2、P3a) +# 实测记录 -> 每张表都写明设备、提交与命令,以便复现。设备:`35d0befa` = Xiaomi 24129PN74C,Adreno 830,Android 16;`3B159D009VZ00000` = Oppo PLG110,Mali,Android 16(ColorOS)。设备运行日期 2026-09-05。设备锁协议照旧。 +> 每张表写明设备、提交与命令。设备:`35d0befa` = Xiaomi 24129PN74C(SM8750 / Adreno 830v2)与 `3B159D009VZ00000` = Oppo PLG110(MT6993 / Mali)用于 P0–P3a;`2f7cbe2e` = Redmi M332BF(SM8750 / Adreno 830v2,与小米同 SoC 同定频点)是 P4a 起唯一的 A/B 设备。定频、风扇与热协议见 `devices/pin-verification-2026-09-07.md`。 +> +> 口径(用户 2026-09-08 起):性能对着 pull 臂**记录**、不作阻塞门;正确性门是硬门。两条读数纪律:`acc/draw` 是约 10 个热入口的**静态**计数,把读点搬走与把工作去掉在它上面长得一样,判性能只看 CPU 时间序列与六个 memo 门;`ctest -V` 做拒绝普查是假零(console sink 在发布配置里被编译掉),必须逐用例读私有日志。 -## 1. Spike A — 从应用自身进程 exec 第二个原生可执行文件 +## 1. P0(2026-09-05,trace APK `7ef7c7e5`,spike `8a239177`) -问题:Android 上能否把 server 以 `lib*.so` 打进 APK,并从应用自己的 `untrusted_app` 域 `fork`+`execve` 它(`adb run-as` 跑在别的域,证明不了)。 +### 1.1 Spike A — 从应用自身进程 exec 第二个原生可执行文件 | 设备 | 结果 | |---|---| -| Adreno 830 | **OK**。父进程 `u:r:untrusted_app:s0:c173,c257,c512,c768` `fork`+`execve` `/libMobileGLServer.so` → 子进程 pid 31348,exit 0;子进程 SELinux `u:r:untrusted_app:s0:c173,c257,c512,c768`(同域同 category);marker 文件、stdout 捕获、报告全在;`execErrno=0`;窗口内**零 avc denial** | -| Mali | **OK**,同形:子进程 pid 28433,exit 0,`execErrno=0`,`u:r:untrusted_app:s0:c94,c257,c512,c768`,零 avc denial | +| Adreno 830 | **OK**:`untrusted_app` 父进程 `fork`+`execve` `/libMobileGLServer.so`,子进程同域同 category、exit 0、`execErrno=0`、零 avc denial | +| Mali | **OK**,同形 | -主机侧已证的三条(随 `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 文件证明自己活过。 +主机侧已证:AGP 会把改名成 `lib*.so` 的 `add_executable` 打进 `lib/arm64-v8a/`(`RUNTIME_OUTPUT_DIRECTORY` 重定向到 `CMAKE_LIBRARY_OUTPUT_DIRECTORY`);`posix_spawn` 在 minSdk 26 不可用;应用进程 stdout/stderr 是 `/dev/null`,子进程用 marker 文件证明自己活过。代码:`tools/spikes/server_stub/`、`android-plugin/app/src/trace/cpp/spawn_spike.cpp`、`MOBILEGL_BUILD_SERVER_SPIKE`。 -- 代码:`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 APK(versionCode 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。 +### 1.2 Spike B — 跨进程外部内存分档(`tools/spikes/extmem_probe/`,`shell` 域,4 MiB payload,每行一次真 GPU 访问 + 两侧字节校验) | 路线 | Adreno 830 | Mali | |---|---|---| -| T1-opaque-fd(server 导出 `VkDeviceMemory` fd,client 裸 `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=1,gpuRan=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) | +| T1-opaque-fd(server 导出 `VkDeviceMemory` fd,client `mmap` + 导入) | **OK** 完整往返 | UNSUPPORTED(`VK_ERROR_INVALID_EXTERNAL_HANDLE`) | +| T1-dma-buf | UNSUPPORTED | UNSUPPORTED | +| T1-gles-memobj-fd(`GL_EXT_memory_object_fd`) | **FAIL**:导入接受,但每次 `glMapBufferRange` → `GL_INVALID_OPERATION` | UNSUPPORTED | +| **T0-ahb-blob-transfer**(client 分配 `AHardwareBuffer` BLOB → socket 交接 → server Vulkan + GL 导入) | **OK** 全链 | **OK** 全链 | +| T3-external-memory-host | UNSUPPORTED | PARTIAL:GPU 写对宿主映射不可见(只读档) | -**P11 的分档决定**:唯一在两台设备、两个后端上都是完整读写的档是 **T0**——client 分配 `AHardwareBuffer` BLOB,server 以 `VK_ANDROID_external_memory_android_hardware_buffer`(Magma)或 `EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT`(Espryt)导入,两侧 persistent+coherent 映射。Adreno 另有 T1(Vulkan 路径);Mali 无任何 server 导出路线,host-pointer 导入只读。Caveat:运行域是 `shell` 不是 `untrusted_app`;AHB 的 socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,域风险在 memfd/opaque-fd 腿上。 - -复现: +决定:唯一在两台设备、两个后端上都是完整读写的档是 **T0**;Adreno 另有 T1(Vulkan 路径);Mali 无任何 server 导出路线。Caveat:运行域是 `shell` 不是 `untrusted_app`(`ROADMAP.md` 开放问题 3)。 ```sh ANDROID_NDK=$HOME/android-sdk/ndk/27.3.13750724 tools/spikes/extmem_probe/build_android.sh /tmp/extmem-build -S=; 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 +adb -s $S push /tmp/extmem-build/extmem_probe /data/local/tmp/ && adb -s $S shell "chmod 755 /data/local/tmp/extmem_probe && /data/local/tmp/extmem_probe" ``` -判决语义(OK / PARTIAL / FAIL / UNSUPPORTED)与逐腿 trace 格式见 `tools/spikes/extmem_probe/README.md`。主机构建(lavapipe)用来证明探针本身报得对:T1/T3 在 lavapipe 上全 OK;T1-gles 在 llvmpipe 上 `GL_OUT_OF_MEMORY` 是 Mesa interop 缺口,不是探针缺陷。 +### 1.3 边界计数器基线(`MOBILEGL_PIPE_STATS=1`,最后一个完整 120 帧窗口;accessor / memo 数字是软件确定的,两台设备相同) -## 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) | +| 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/2577,etl 10538/1296,eub 10720/1114 | -| `minecraft-1.21.4-in-world`(360) | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K(97 box / 89 rect) | 16.7 K | 0 | mfp 0/10994,mpm 9240/1754,mdt 9120/1874 | +| 同上 | Magma | 91.6 | **8.56** | 13.5 K | 39.9 K(97 box / 89 rect) | 16.7 K | 0 | mfp 0/10994,mpm 9240/1754,mdt 9120/1874 | | `minecraft-1.21.4-fabric-iris-bsl-in-world`(120,memo 冷) | Espryt | 23.2 | 21.04 | 32.6 K | 8.8 K | 1.8 K | 0 | ers 1958/1843,etl 722/3079 | -| `minecraft-1.21.4-fabric-iris-bsl-in-world`(120,memo 冷) | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0(vtxc 1.7 K) | mpm 1890/895,mdt 1573/1212 | +| 同上 | Magma | 23.2 | 11.26 | 313 K | 256 K | 1.8 K | 0(vtxc 1.7 K) | mpm 1890/895,mdt 1573/1212 | | `improved-transparency-minecraft-26.3`(1200) | Espryt | 1320 | **8.44** | 333 K | 0 | 0 | 0 | ers 156925/2791,etl 148606/11110,eub 148246/11470 | -| `improved-transparency-minecraft-26.3`(1200) | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036,mpm 134421/2615,mdt 156611/1785 | -| `minecraft-1.21.1-neoforge-create-indirect-in-world` | 两者 | — | — | — | — | — | — | 两台设备都失败(§5),且不足 120 帧 | +| 同上 | Magma | 1320 | **6.53** | 173 K | 0 | 0 | **331 K** | mfp 21360/137036,mpm 134421/2615,mdt 156611/1785 | -门缩写: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.5–9.3 次**(预测区间 10–25 的下沿;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): +门缩写:ers = `EsprytRenderState`,etl = `EsprytTextureSyncList`,eub = `EsprytUnitBindingsEpoch`,mfp = `MagmaDrawFastPath`,mpm = `MagmaPipelineMemo`,mdt = `MagmaDynamicTail`。读法:真机稳态动态 accessor 成本是每 draw 6.5–9.3 次(预测区间 10–25 的下沿),推送要打败的是 ~8 次 accessor + memo 探测;Magma 在 26.3 每帧重打包 **331 KB** 具名 UBO 字节(D-B8);同样 185 次发射,Espryt 整 box 路径 635 K 纹素字节 / 帧、Magma rect 路径 40 K,16×("server 选上传形状"的依据)。 ```sh -ANDROID_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:',取最后一个完整窗口 +ANDROID_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. 桌面数据点与语料事实 +### 1.4 桌面数据点与语料事实 -- **llvmpipe / lavapipe 动态 accessor**(`GuiBatchScenario`,14 帧 / 26 draw,memo 冷):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、**63** 个 `PipeInputs` 字段(旧数 61 是 P1 之前的,`gen_pipe.py` 自己打印 63);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 帧 1000–4000 draw 时每帧 56–224 KiB 头字节。 -- **persistent map 采纳的既有基线**(`dev`,MC 26.3,Adreno):≥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。 +- llvmpipe / lavapipe 动态 accessor(`GuiBatchScenario`,memo 冷):Espryt 20.65 / Magma 15.54 次 / draw。 +- dirty-surface 面:`MG_Impl/GLImpl` 41 个文件、926 次 mutator 调用、73 个不同 mutator(`RecordError` 占 836 次);92 次位于同函数内也到达后端的即时发布点,其余由紧随的 verb 发布。 +- 读点覆盖:63 个 `PipeInputs` 字段;后端读点清单 → 调用映射 **0 UNMAPPED**。 +- OOM 探测惯用法:41 个 trace fixture 中 0 例(9 次 `glRenderbufferStorage` 无一在 3 个调用内跟 `glGetError`)→ `glRenderbufferStorage*` 不 ack。 +- `FramebufferSrgb` / `DepthClamp`:六个后端读点消费编译期常量 `false` / 零读点;`glEnable` 落到 `default:` 分支既不存储也不报错;41 个 fixture 无一开启。 +- payload 尺寸(`MGPipeTypes.h` 的 `static_assert`,arm64 与 x86-64 一致):`MGPDrawInfo` 56、`MGHostSpan` 32、`MGPBindRenderState` 12、`MGPResourceDesc` 88、`MGPFramebufferState` 304、`MGPProgramDesc` 192、`MGPSubData` 72;`SEG_CMD` 按 56 B 头定尺,MC 帧 1000–4000 draw 时每帧 56–224 KiB 头字节。 +- persistent map 采纳的既有基线(`dev`,MC 26.3,Adreno):p99 163→21 ms、稳态 40→115 fps、省 ~400 MB;P11 的回归上限对着它。Mali 上传作业数悬崖:~100 个精灵 rect 对一个 union box 是 +6 ms/frame。 -## 5. Harness 事实与陷阱 +### 1.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 830:Espryt ~4.5 分钟后黑帧,Magma 纹理上传提交时 `VK_ERROR_DEVICE_LOST`;Mali:SSIM 0.85 / 0.45)。Adreno 830 上用 `dev@81b17c0b` 基线 APK 复现,**是基线就有的问题,不是本分支造成**;它是 P3a/P8 验收清单里的用例,需先在 `dev` 修。 +- trace app 从不到达 `MobileGL::DestroyImpl`,`MOBILEGL_PIPE_STATS_FILE` 在设备上永远不会写;只有 `mobilegl.log` 的周期汇总行,`MOBILEGL_PIPE_STATS_PERIOD` 为此而加。 +- `run_android_retrace_local.py` 每棵树共用一个 `.trace-work/android-retrace-result` 根并 `rmtree`,两台设备必须从一棵树**串行**跑。 +- `--env` 值里的 `/data/...` 会被 MSYS 路径转换,用 `MSYS_NO_PATHCONV=1`。 +- ColorOS 首次 `adb install` 卡在 `InstallGuideActivity` 确认页;streamed install 进行中点它会让设备掉线几秒。 +- `minecraft-1.21.1-neoforge-create-indirect-in-world` 在两台设备上都失败,`dev@81b17c0b` 基线 APK 复现——`dev` 侧问题(`ROADMAP.md` 开放问题 17)。 ---- +## 2. P1(lavapipe / llvmpipe) -# P1 实测(`feat/disaggregated`,lavapipe / llvmpipe) +### 2.1 规模 -## 6. 规模:站点、访问器、填充点 - -| 量 | 值 | 出处 | -|---|---|---| -| 后端 `pGLContext->` 箭头站点 | 277(Espryt 113、Magma 164) | 计划写 293,出自过期的 vendored 清单 | -| 非箭头行 | 58(Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT`) | 与计划一致 | -| `PipeInputs` 字段 | 63 | 计划写 61;`GetBoundTransformFeedbackLifetimeId`、`HasOpenTransformFeedbackSpan` 是 D21 之后新增的读点 | -| 后端调用的不同访问器 | 62(Espryt 32、Magma 56) | `GetBoundTransformFeedbackName` 已无人读,留作已标注的死行 | -| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 | `MG_Pipe/FillPoints.def` | -| `SyncPersistentMappedRange` / `SyncGpuWrites` | **21** / 6 | P5 b1 复核:21 = Espryt 9 + Magma 12,不是 20。原来的 20 与 `Managers.cpp:5047-5048` 的"十一处 Espryt"都恰好少一行,少的是 `DirectGLES.cpp:361`(`ResolveIndirectCommandBytes`)——它是共享 helper 而不是 draw-path 站点,多半因此被排除,但一个能读到持久映射区间的 helper 与 draw 站点一样会读到陈旧字节。`ARCHITECTURE.md:290` 引的那张 §5.7 逐站点归属表**在树里不存在**。 | - -## 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()` 的高水位分支 | +| 后端 `pGLContext->` 箭头站点 | 277(Espryt 113、Magma 164) | +| 非箭头行 | 58(Espryt 9、Magma 49,其中 43 条是 Magma 的逐 verb `MOBILEGL_ASSERT`) | +| `PipeInputs` 字段 / 不同访问器 | 63 / 62(Espryt 32、Magma 56) | +| 填充点 | 83 条 `MGP_FILL`,覆盖 69 个 verb、9 个类 | +| `SyncPersistentMappedRange` / `SyncGpuWrites` | **21**(Espryt 9 + Magma 12;P5 b1 复核,原记 20 漏了共享 helper `ResolveIndirectCommandBytes`)/ 6 | -钩子挂在**计数器**上而不是四十个写入点上:后端写前端的全部路径(`SamplerObject` 的 8 个 setter、`ITextureObject` 的 6 个、纹理单元绑定路径)都经由这三个计数器,`BufferObject`/`ProgramObject`/`VertexArrayObject` 的后端写入不移动任何推送字段(它们是句柄类读点)。 +### 2.2 verify 通道发现的两类真问题 -## 8. P1 验收(合入后在 `~/w7/pipe` 实测) +**缺填充行(9 处)**:`kReadback` 缺 `IsTransformFeedback{Active,Paused}`、`kTextureOp` 与 `kDispatch` 缺 `IsCapabilityEnabled`、`kBlitOrCopy`/`kTextureOp` 缺着色器 blit 用到的 viewport 与顶点 / 缓冲绑定。其中 8 行是静态过近似(代码路径可达但通道未跑到),有意保留——能退役它们的证据只能是动态的(完整 CTS caselist + `MOBILEGL_PIPE_POISON_OMIT`,未做)。 + +**verb 内后端改前端(3 个字段)**:Magma 在 draw 里写前端对象(合成回退纹理、材质化排队清除、覆写 sampler filter),8 条 DirectVulkan 用例与 2 条 trace 报 `Fatal{PipeVerifyDiffer, "GetSamplingResolutionGeneration@Draw*"}`。解法 **push-on-mutation**:前端计数器移动时用 `MGP_NOTE_MUTATION(Field)` 刷新推送块里那一个字段——钩子挂在三个计数器上(`BumpSamplingResolutionGeneration`、`BumpTextureBindGeneration` / `NoteUnitTouched`、高水位分支),不挂在四十个写入点上。 + +### 2.3 验收 | 门 | 结果 | |---|---| -| 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 上变红) | +| pull 构建符号与 `.text` | 0 增 / 0 删 / 0 改尺寸 / 0 重命名,`.text` 不变 | +| `MG_Backend` 里的 `pGLContext` | 0 | +| 单元(pull / push / verify) | 1485 × 3 | +| `integration-gpu`(pull) / `integration-verify` | 878 / 818 零 `Fatal{` | +| 79 例 retrace(`MOBILEGL_PIPE_VERIFY=1`) | 79/79,79/79 armed,零 `Fatal{` | +| 两个阴性对照 | 篡改字段变红、抽掉一个填充戳记在那条 verb 上变红 | | 测试名 | 0 删除,+29 | -**verify 构建的代价**:`integration-verify` 818 条在 4 路并行下约与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。 +verify 构建的代价:`integration-verify` 与 `integration-gpu` 同量级;79 例 retrace 在 4 路下约 20 分钟。 ---- +## 3. P2(`738b289d`) -# P2 实测(`feat/disaggregated@738b289d`) - -> **口径变更(用户 2026-09-08)**:push 比 pull 多约 10% 逐线程 CPU 可接受;**性能自此对着 pull 臂记录,不作阻塞门**——pull 臂的数字就是此后的基准线,第 43 天的 tracker 绝对 ns 上限降为记录项;路线图先推完(下一步 P3a),专门的优化阶段排在其后(或首个 IPC 帧之后)。所以本部分把"门"与"记录"分开写:§9 是门,§10–§14 是记录与遗留判定。 - -## 9. P2 验收门(合并点实测;一个在飞的修复落地后复核) +### 3.1 门 | 门 | 结果 | |---|---| -| 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** | +| G1 pull 构建符号 | 0 增 / 0 删 / 0 重命名;**4 处认定 resize**(`RenderState::{RenderState, SetCapability, IsCapabilityEnabled}`、`_GLOBAL__sub_I_DirectGLES.cpp`);`.text` +160 B | +| G5 `SyncRenderState` | `RenderStateImpl` 段 sha 逐字节相同 | +| G2 / G14 | 名差 0;0 删除 / +119 | +| 单元 | 1566 × {pull, push, verify} | +| `integration-gpu` | 916/916 × {pull, push, `MOBILEGL_PIPE_PUSH=0`};渲染状态敏感子集 72/72 | +| `integration-verify` | 828 零 `Fatal{` | +| 79 例 retrace | push 79/79;verify 79/79 armed 零分歧 | +| G7 setter 一致性阴性对照 | 变红并点名 `SetColorMask` | +| `CsoContentAddressing` / verify 三组对照 | 6/6 / 44/44(`PoisonOmitted`、`VerifyCorrupted`、`HandleRecycle`) | -## 10. 设备配对 A/B:逐线程 CPU(D.4.2) +### 3.2 设备配对 A/B(-O0 勘误) -> **勘误(2026-09-08,P3a 收尾时发现)**:本节两机 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/B(3e298c9a 的 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)。 +两机 64 次运行(小米 + Oppo,四 trace × 两后端 × finish 开/关,reboot-clean、钉频、best-of-3、尾 200 帧)所用的两臂 APK 是**未优化构建**(`libMobileGL.so` 43.2 MB / `.text` 21.8 MB,对 Release 15.5 / 9.4 MB),绝对值与差值都是 -O0 读数,不作基准线;Release 基准线以 §4.4 为准。-O0 读数:小米 p50 **+8–14%**(四 trace 两后端),Oppo Espryt **+16–18%** / Magma **+10–11%**,p99 同向;finish 开关两臂几乎一致(多出的是客户端 CPU);计数器两机逐字相同。原始 32 行表在 git 历史(`ea34bccb` 版本的本文件 §10)。harness 误杀记录:`trace-replay-ci.sh` 对 `pidof` 单次采样失败即 force-stop,`dev` 侧跟进。 -协议: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、两个后端上稳定多花 **8–14% 逐线程 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 明显快于同臂 nofinish(50.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 +16–18% 对 +11–13%;26.3 +8–9% 对 +14%),Magma 两机一致(+10–11%);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 `,oom adj 0,`logcat -b crash` 无崩溃):那个脚本的等待循环对 `pidof` 单次采样失败即判定"应用已退出"并 force-stop,一次 adb 抖动就丢一次重复。数字用剩下两次重复的 best-of;修法是连续 N 次采样失败才判退出。 - -## 11. tracker 的绝对 ns:上限与 T1/T2(D.4.3) - -**上限在跑之前钉死**(`ARCHITECTURE.md` §13.2 第 4 条要绝对阈值,因为相对噪声阈值会平凡通过)。推导:拉取基线是每 draw **6.5–9.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"计价——6–10 周期,1.96 GHz 上约 4 ns——再加六次 memo 探测各约 2 ns,得到推送必须不差于的 **约 44 ns/draw**。因此: - -> **T1 ≤ 45 ns/draw(Adreno 830 `35d0befa`)、≤ 60 ns/draw(Mali `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`: +### 3.3 DriverBench:T1 / T2(桌面 llvmpipe / lavapipe,`wsl_p2_bench.sh`,Release,240 帧 × 5 次取中位数,`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 | +| native | 4771 | 22968 | 437759 | +| Espryt pull / push / push `PIPE_PUSH=0` / push 位 63 | 5121 / 5443 / 5671 / 5374 | 23753 / 24869 / 24584 / 24630 | 443300 / 446017 / 443807 / 435454 | +| Magma pull / push / push `PIPE_PUSH=0` / push 位 63 | 16900 / 17245 / 17599 / 17444 | 32705 / 33857 / 34082 / 34780 | 438406 / 446067 / 445006 / 444758 | -分解(`mc_vanilla_draw`,ns/draw): - -| | Espryt | Magma | +| `mc_vanilla_draw`,ns/draw | Espryt | Magma | |---|---|---| -| **T1** = push − pull(整个边界的每 draw 代价) | **+322**(pull 的 +6.3%) | **+345**(+2.0%) | -| **T2** = push(`PIPE_PUSH=0`) − pull(P1 的残余填充本身) | +550 | +699 | +| **T1** = push − pull(整个边界) | **+322**(+6.3%) | **+345**(+2.0%) | +| **T2** = push(`PIPE_PUSH=0`) − pull(P1 残余填充) | +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%) | +| 位 63 − push(CSO 内容寻址净值) | −69(≈ 0) | +200(内容寻址每 draw 省 200) | +| blend-toggle / pass switch | +4.7% / +0.6% | +3.5% / +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/draw(pipeline 键从 CSO 句柄取,少一次哈希)。§11 钉的上限(T1 ≤ 45 ns/draw @ Adreno 830)是**设备**口径,`DriverBench` 只在桌面栈上跑(同一 draw 在 llvmpipe 上花 5.1 / 16.9 µs),两者不能直接比;设备上对应的读数是 §10 的 +8–14% p50——按 2026-09-08 的口径记录在案,不判门。 +读法:P2 的 tracker + CSO 比它替掉的 P1 残余填充便宜;剩下的 T1 是尚未句柄化的填充与 dirty 走查。`mc_state_toggle` 由 `DriverBenchStateToggle` 钉住每帧 46 对开关;位 63 对照的开关 `CsoContentAddressingScenario` 常开(内容寻址臂 `csom ≤ 4` 且 `csom < csob`,位 63 臂 `csom == csob`)。 -## 13. 计数器读数 +### 3.4 计数器读数 -**`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`: +`resid=` 已非零且棘轮压到底(1248 → 8):桌面 push retrace `iris-bsl`,`resid=197.07` B/帧 = 每 draw 0.64 块;`csom/csob` = 8 / 1415(CSO 复用比)。小米 push 臂(-O0,计数器与优化无关): -``` -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 帧) | +| trace | Espryt `ers` / `etl` / `eub` | Magma `mfp` / `mpm` / `mdt` | `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 字节块每帧 3–46 次,与桌面的 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 没有碰这个门)。 +CSO 内容寻址在稳态窗口里几乎不再铸造;memo 门形状与 §1.3 的拉取基线一致。**未测**:逐 dirty 位触发率(`FireCount`/`WalkCount` 已实现但不在汇总行)、每 draw payload 直方图(只在 teardown JSON 里)。 -**逐 dirty 位的触发率:未测。** 计数本身已实现(`MGPipeTracker` 的 `FireCount`/`WalkCount`,挂在 `PipeStats::Enabled()` 后面),但汇总行的格式里没有它们,所以没有任何东西把 18 个计数打出来。补法是给 `FormatWindowLine` 加一行,或从一个场景里经访问器读——两者都是 P3a 的顺带项。 +### 3.5 遗留判定 -**每 draw payload 直方图:未测。** 24 桶已实现,但只在 teardown 的 JSON 里输出(`MOBILEGL_PIPE_STATS_FILE`),而 trace app 从不到达那次 teardown(§5.1),所以设备上取不到;桌面也没有记录过一次。 +- `MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1` 的 186 条过滤不进比对器(P3a 在 push 下跑了一次 958/958;进比对器推迟到 P3b)。 +- Track H 单位成本的日历口径未记录(产出侧在案:11 条 memo 删除 + 2 条重键零回归)。 +- 句柄 ABA 对照"重键前红":修复前 `HandleRecycle` 28 条中 1 条红(`AbaControl` 期望死对象的红却看到替换对象的绿);修后 32/32,且关掉 `MOBILEGL_PIPE_HANDLE_ABA_CONTROL` 两臂都 `observed=FRESH` 并失败——污染只由被打掉的身份产生。 -## 14. 遗留判定 +## 4. P3a(`fde5fda3`,基线 `5cb826b0`,44 个提交,37 文件 / +9273 / −230) -**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 部分 —— 接口纯度** +### 4.1 五部分门(本地 `~/w7/pipe`) | 门 | 结果 | |---|---| -| 三个构建(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` 段 sha(P2 的不变量) | `d8fd1c48716056c53675`,逐字相同 | -| **G8** `HandleRecycle`(verify 构建) | **60/60**,含新增的 buffer 用例三臂 | +| 构建 pull / push / verify | rc 0 / 0 / 0(fail-fast,绝不拿陈旧二进制下判决) | +| A 门 include 闭包 / C 门 `pGLContext` / G13 | 4 probes 0 problem / 空 / 空 | +| **G1** | `.text` 10806323 → 10806323(+0);27811 符号 **0 / 0 / 0 / 0** | +| **G5** 十一函数(pool / 延迟释放 / ring / `FlushPendingRangesNow` 与 `FlushPendingRangesFrom`) | 对 `44c2b5cf` 与 `5cb826b0` 都逐字节相同;self-test 两个阴性对照按名变红 | +| `integration-verify` / 79 retrace verify | 842/842 零 `Fatal{` / 79/79 armed 零分歧 | +| G2 / G14 | 差 0 / 0 删除 +58 | +| 单元 | 1619 × 3 | +| `integration-gpu` | 958/958 × {pull, push, `PIPE_PUSH=0`, `0x7f`(G12 子系统关闭臂), `ESPRYT_DISABLE_INVALIDATE_FLUSH=1`} | +| buffer/VAO 族 / `RenderStateSpans`+`Residual`+`VertexInputEmit`+`ResourceEmit` | 202/202 / 48/48 | +| `HandleRecycle`(verify) / 子系统对照 / verify 对照组 | 60/60 / 10/10 / 64/64 | +| G7 vertex-input 阴性对照 | 变红并点名 `IsBgra` | +| 79 retrace push | 79/79 | +| G3b 具名五条 | 桌面全绿(都在 79 例扫描内),但单独的具名扫描因门脚本把正则写成逗号清单而选中 0 例;设备侧按开放问题 17 排除 `create-indirect` → 记为"桌面全绿、设备侧部分" | +| `gen_pipe` / `gen_pipe_dirty_surface`(扫描根扩到 `MG_State/GLState`) | self-test 7 / 21 个阴性对照全部触发;75 个 mutator 全映射,0 COARSE / 0 UNDECIDED | -**第 2 部分 —— 语义影子比对(决定性的一条)** +### 4.2 "重键前红"与验证轮找到的三个缝隙缺陷 -| 门 | 结果 | -|---|---| -| `integration-verify` | **842/842**,`Fatal{` 行 **0** | -| 79 例 retrace,`MOBILEGL_PIPE_VERIFY=1` | **79/79 通过**,**79/79 全部 armed**(每份日志都带 `MGPipe verify:` 行),带 `Fatal{` 的日志 **0**——即零分歧、零未迁移读 | +buffer 类的红前证据(package C 尚未注册 `MGPipeResourceOps` 时):`HandleRecycle` 的 `.AbaControl` 臂通过(它断言的就是被污染的内容)、`.Legacy` 通过、`.Handles` 可见地 skip;C 注册后两条 skip 转为断言,最终 60/60。 -**第 3 部分 —— 行为 A/B** +| # | 缺陷 | 表现与修法 | +|---|---|---| +| F1 | vertex buffer 条目按属性的 GL 绑定点解析,而不是按属性下标 | 两者只在 `KHR-GL43.vertex_attrib_binding` 题材上分叉;`VertexAttribBindingScenario` 6 → 3 | +| F2 | ensure 路径读 `MGPResourceDesc::HasDefinedContent`(上一次 respecify 时的事实)判断影子有无字节 | `glBufferData(size, NULL)` + `glBufferSubData` 后应用字节被静默丢掉;改为持有前端对象时读 `HasDefinedContent()` | +| F3 | 惰性生成的 twin 从不发布影子基址,fp64 收窄拒绝每个 draw | `hostBytes` 终身 null → Adreno workaround 禁用属性;在算出基址处补发布,只对非采纳资源 | -| 门 | 结果 | -|---|---| -| **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 例 retrace(push) | **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)"**,不当作完整通过 | +方法学:包 worktree 里 `tools/trace_replay/fixtures` 是 LFS 指针,对着指针跑 retrace 报 `passed 2 / 79`、`ssim=None`——**假红**;base-instance 对照在 llvmpipe 上要先关掉原生 `GL_EXT_base_instance` 才可证伪。 -**第 4 部分 —— 性能:记录,不设门**(规则 (a))。见 §20。 +### 4.3 Track H 单位成本 -**第 5 部分 —— 覆盖、poison、句柄纪律** +日历:四个包(contract/wire、client、espryt、gates)各一轮实现 + 对抗性评审 + 至多两轮返工,全部在 2026-09-08 一天内,对 27 天绊线 1/27。memo 账:直接删除普查 11 条里的最后一条(`ConvertedVertexStreamKey::sourcePin`);句柄臂上退役 VAO twin 五个同步 memo 成员;重键 VAO twin 索引槽 memo、`ResolvedDrawBuffers`、twin 同步门、`ConvertedFloat64Stream`;**保留**(`MOBILEGL_PIPE_LEGACY_MEMOS` 下仍编译):以上全部成员、`g_pendingFetchBaseInstance` 族、D-K 那组——真删会移走 pull 符号或改 `sizeof`,G1 的 0/0/0/0 就是这条的度量,随 P13 退役。 -| 门 | 结果 | -|---|---| -| `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` 各被点名) | +### 4.4 两机 Release 配对 A/B、MC 26.3 的 p99、DriverBench(记录项) -**G15(完整 `gl44to46` caselist,约 56,271 例,两台设备)**:P3a 是五个架构边界之一,按 `ROADMAP.md:34` 排在 `dev` 合并时、**关键路径之外**;`MOBILEGL_PIPE_POISON_OMIT` 的清扫(§14 那 8 条静态过近似填充行)搭同一次 caselist 运行。 +协议:reboot-clean、`pin_device.sh` 钉频并前后 `check`、两臂背靠背、`--benchmark-repeats 3 --benchmark-tail-frames 200`、best-of-3、`--benchmark-no-finish` 主臂;APK 都是 `3e298c9a` 的 Release trace 构建(`.text` 9.4 MB)。臂:pull = pull 库;P2 = push APK `MOBILEGL_PIPE_PUSH=0x7f`;P3a = push APK 默认 `0x1ff`。`create-indirect` 排除(开放问题 17)。单位 ms/帧。 -## 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-op,twin 因此是**惰性**建的,第一次 `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`,本阶段最大的一处)、`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/p99(p99 用 nearest rank,p50 按设备自己的中位数规则)。臂 = {pull APK, push APK} × {`--benchmark-no-finish`(主臂,P3a 问的是 CPU), finish(GPU 时间没动的 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/B(D.4.2),三臂**:APK 都是 `3e298c9a` 的 Release trace 构建(`wsl_build_trace_apks.sh`,`.text` 9.4 MB,debug 签名);pull 臂 = pull 库;`P2` 臂 = 同一个 push APK 跑 `--env MOBILEGL_PIPE_PUSH=0x7f`(只开 P2 的七个子系统,P3a 关);`P3a` 臂 = push APK 默认掩码 `0x1ff`。协议同 P2(reboot-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 | 备注 | +| 设备 | 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 | — | — | — | — | — | 三臂都在启动数秒后 SIGABRT(scudo map error,见下),既有问题 | +| 小米 | `improved-transparency-minecraft-26.3` | Espryt | 10.810 | 11.485(+6.2%) | 11.697(**+8.2%**) | 25.457 | 26.322 | | +| 小米 | 同上 | Magma | 10.675 | 11.473(+7.5%) | 11.459(**+7.3%**) | 25.014 | 26.035 | | +| 小米 | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 8.220 | 9.117(+10.9%) | 10.650(**+29.6%**) | 21.895 | 24.487 | | +| 小米 | 同上 | Magma | — | — | — | — | — | 三臂都 SIGABRT(`scudo::reportMapError`,`dev` 侧) | | 小米 | `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 | +| 小米 | 同上 | Magma | 0.485 | 0.504 | 1.010 | 1.469 | 2.187 | 亚毫秒帧,噪声 | +| 小米 | `create-instancing` | Espryt / Magma | 944.4 / 1003.9 | 945.2 / 1019.5 | 949.9 / 1015.6 | — | — | 两帧 fixture | +| Oppo | `improved-transparency-minecraft-26.3` | Espryt | 9.741 | 10.570(+8.5%) | 10.542(**+8.2%**) | 26.312 | 27.022 | | +| Oppo | 同上 | Magma | 8.162 | 8.920(+9.3%) | 8.973(**+9.9%**) | 22.851 | 23.638 | | +| Oppo | `minecraft-1.21.4-rd12-odinlite-in-world` | Espryt | 9.938 | 11.009(+10.8%) | 12.963(**+30.4%**) | 27.137 | 29.765 | | +| Oppo | 同上 | Magma | 8.001 | 8.933(+11.6%) | 10.161(**+27.0%**) | 24.723 | 27.355 | | +| Oppo | `minecraft-1.21.4-fabric-sodium-in-world` | Espryt / Magma | 1.896 / 0.399 | 1.770 / 0.431 | 1.841 / 0.437 | 2.993 / 1.206 | 3.079 / 1.409 | 噪声内 | +| Oppo | `create-instancing` | Espryt / Magma | 1075.1 / 758.3 | 1045.1 / 731.0 | 1060.1 / 760.0 | — | — | 两帧 fixture | -所有入表运行前后 `pin_device.sh check` 都是 PINNED(小米 rd12/Magma 崩溃后 GPU pwrlevel 被重置,其后的 sodium/create-instancing 行两臂同状态)。 +读法:(1) **P2 边界在 Release 下的真实代价是 +6–12%**,两机两后端一致;(2) P3a 在 26.3 与 sodium 上几乎不再加价,**但在 rd12 上把差距从 +11% 推到 +27–30%**——rd12 每帧 VAO/buffer 绑定切换远多于 26.3,每次切换走一遍 `set_vertex_buffers` 构造 + `ContentHash` + applier 记录 + 逐属性走查(P4a 补的 `csob-blob` 计数器给了字节口径:rd12 1.06 MB/帧,其余用例中位数 2.9 KB/帧;DirectVulkan 恒 0);(3) **MC 26.3 在 Adreno 上的 p99**:25.46 → 26.32 ms(+3.4%),仍在采纳基线的 21–26 ms 档;(4) `mpr` 在设备上成立(26.3 两机都是 8,P2 臂恒 0);(5) 计数器三臂逐字相同。 -**读法。** (1) **P2 的边界在 Release 下的真实代价是 +6–12%**(`0x7f` 臂),两机两后端一致,比 -O0 表的 +8–18% 小但同量级;(2) **P3a 在 26.3 与 sodium 上几乎不再加价**(P3a 臂与 P2 臂在 26.3 上相差 −0.1 ~ +2 个百分点),**但在 rd12 上把差距从 +11% 推到 +27–30%**——rd12(Odin Lite 世界)每帧的 VAO/buffer 绑定切换远多于 26.3(26.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 1,rd12 与 create-instancing 0;P2 臂上恒 0(子系统关)——G10 在设备上成立;(5) `CreateVertexElements` 每帧字节数:统计行原本没有这一类(`vtxc` 是 client 数组),**P4a 已补上并测了**——汇总行的 `bytes/f[...]` 多了一个 **`csob-blob`**(`cso-blob-bytes`:所有 CSO create 调用的 blob 字节,含 vertex-elements、sampler、shader)。79 例 retrace、`MOBILEGL_PIPE_STATS=1 MOBILEGL_PIPE_STATS_PERIOD=60`、push 构建(`8c458cd5`):**DirectGLES 27 个用例逐用例窗口均值的中位数 2928 B/帧、均值 42.8 KB/帧、无一为零**,最大 `rd12-odinlite` **1.06 MB/帧**(它每帧换 VAO 的次数远多于别人,正是 §20 读法 (2) 里 rd12 多花 17–19 个百分点的同一根因,这下有了字节口径);其后依次 `minecraft-1.21.4-in-world` 11.4 KB、`common-mods-inventory` 8.3 KB、`common-mods-in-world` 7.8 KB、`rei-inventory` 7.2 KB。**DirectVulkan 侧恒 0**——Magma 没有注册 `MGPipeResourceOps`,P4a 的消费者门因此让四族一条不发(见 `ARCHITECTURE.md` 的 `MOBILEGL_PIPE_PUSH` 行),这也是这个计数器第一次把那条门量化出来;(6) 计数器(`acc/draw`、六个 memo 门、`resid=`、`csom/csob`)在 pull/P2/P3a 三臂间逐字相同——它们数的是代码路径,P3a 没有改它们的定义。 +小米 rd12 + Magma 三臂(含 pull)都 `SIGABRT`:`scudo::reportMapError` ← `scudo_calloc`,pull 库与 P3a 前的 Magma 路径符号一致——既有 bug,`dev` 侧任务。 -**小米 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`:Release,240 帧,每臂 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 的边界): +DriverBench(`wsl_p3a_bench.sh`,`c20e2f2b`,`mc_vanilla_draw` ns/draw;T2 = `0x7f` 臂): | | Espryt | Magma | |---|---|---| -| **T1** = push(`0x1ff`) − pull(整个边界) | **+1048**(pull 的 +20.5%) | **+611**(+3.6%) | -| **T2** = push(`0x7f`) − pull(P2 的边界) | +349 | +189 | +| **T1** = push(`0x1ff`) − pull | **+1048**(+20.5%) | **+611**(+3.6%) | +| **T2** = push(`0x7f`) − pull | +349 | +189 | | **T1 − T2 = P3a 自己加的** | **+699** | **+422** | -| push(`0`) − pull(P1 残余填充,全部拉取) | +578 | +640 | -| 位 63 − push(CSO 内容寻址净值) | −6(≈ 0) | +20(≈ 0) | -| blend-toggle(ns/开关对) | +1209(+5.2%) | +925(+2.8%) | -| pass switch | +160(≈ 0) | +566(≈ 0) | +| 位 63 − push | −6 | +20 | +| blend-toggle / pass switch | +5.2% / ≈ 0 | +2.8% / ≈ 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 记录的就地更新。 +读法:P3a 的 buffer/VAO 句柄路径每 draw 多花 Espryt 699 / Magma 422 ns(Magma 没有句柄化的 VAO 消费者,全是 client 侧发射 + applier 记录);进优化清单:vertex-input 发射的 per-draw 抑制与 applier 记录就地更新。`DriverBench` 必须经 `DRIVERBENCH_EGL_LIB` 显式 `dlopen` provider,不得靠 `LD_LIBRARY_PATH`(glvnd 会把 GPU 悄悄换成 llvmpipe)。 -`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 自己决定要不要钉。 +### 4.5 决定与遗留 -## 21. 决定与遗留判定 +- **ID-15**:`Managers.cpp` 的三层 flush 排水梯每个预处理臂各一份——push 构建跑 `FlushPendingRangesFrom`,pull 构建跑逐字节不变的 `FlushPendingRangesNow`;两个名字都是 G5 行,各对自己的 pin 比。转发函数不可行(G5 提取器不认预处理)。 +- **M-3**:fp64 顶点数组收窄在句柄臂上少做一处 `SyncGpuWrites`(`SyncFloat64AttributeAsFloat32ByHandle` 手里只有句柄)——默认掩码下两臂唯一的行为差异(fp64 数组 + shader 写过的源 buffer + 无显式回读),P8 把拉取搬到 client 侧即关闭。 +- 整体 diff 终审抓出两个跨包接缝:client 为每个 VAO 铸的 `VertexElementsCso` 槽在 Magma 下泄漏(修为 `~VertexArrayObject` 走后端无关的死亡路径);`FlushPendingRangesFrom` 在 G5 覆盖之外(→ ID-15)。 +- CI 独有的 teardown 崩溃(`exit()` 时静态析构顺序 UAF):`MGPipeSlots()` 等单例与四个持有前端 `SharedPtr` 的静态 `PipeInputs` 改为退出时泄漏(`d54ec57a`、`6515c8e6`、`fde5fda3`),ASan 10/10 干净;复现钥匙 `GLIBC_TUNABLES=glibc.malloc.tcache_count=0`。 +- `dev` 侧跟进(拆分不顺手修):`g_uploadRing` 不被 `OnBackendContextDestroyed` 重置(良性,`RingAvailable` 自愈);`ScopedDefaultUnpackState::s_synced` 没有失效路径。 +- `MG_Test/Buffer/BufferTest.cpp` 的 fixture 只 scope 了 `BufferBackendOps` 不 scope `MGPipeResourceOps`,26 条被路由进 pipe——修后 86/86;给它一个 pipe 形 mock 是跟进项。 +- 峰值 RSS(push vs pull,79 例):`retrace_gate.py` 不报,没有数。 -**整体 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`——裁定如下。 +## 5. P4a(`8c458cd5`,基线 `37da3c3a`,101 个提交,86 文件 / +30066 / −384) -> **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 比;两条梯都不许漂,也不许彼此漂而门不响。 +### 5.1 全门(`6035c9d7` 全量 + `8c458cd5` 复跑) -> **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 → ~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/966,ASan 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` 侧跟进项留在这里。 - -**P4a 在它旁边加第二条同类项:`ScopedDefaultUnpackState::s_synced` 没有失效路径(D-O)。** `Managers.cpp` 的 `ScopedDefaultUnpackState` 用一个**进程级**影子记住"默认 unpack 状态已经同步过",那个影子被写、被读,**却没有任何地方让它失效**——换上下文、别的代码路径自己调 `glPixelStorei`,它都不知道。P4a 既不修它也没让它更糟(句柄臂的 ring 路径一条 `glPixelStorei` 都不发,唯一的外部写点仍被 `!ringStaged` 挡着),按同一条纪律记为 `dev` 侧跟进。**两条并列的理由是同一个**:它们都是"缓存了一个事实、却没有让这个事实失效的路径",而 P4a 自己在 `Tracker.h` 上被同一类问题咬了三次(`glBindSampler` 不动位 13 的快门、SSO 下 `GetCurrentProgram()` 恒 null、`glBindImageTexture` 只换 level 时三个计数器都不动)——所以下一阶段的 brief 必须带一张"记录字段 → 写它的 setter → emitter 读的快门"的完备表,而不是让每个包各自去发现。 - -**`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` 加一行,或从一个场景里经访问器读)。 - -**峰值 RSS(push vs pull,79 例 retrace):工具不报,所以没有数。** `~/w7/retrace_gate.py` 只有五个参数(`--tree --lib --out -j --only`),代码里没有任何 `rss` / `maxrss` / `getrusage` 引用。这个数原本是用来盯第七张 slot 表泄漏的——一个没人销毁的 buffer 会永远漏掉它的 twin,而这对每一个正确性门都不可见;**这一轮拿不到它**,要拿必须先给那个工具加测量。对冲仍在:`ResourceDestroy` 是从 `~BufferObject` **无条件**发射的、不是靠清扫,顺序(先发射、后 `MGPipeSlots().Free`)由 `HandleRecycleScenario` 的三个臂把关(§16、§17)。 - - ---- - -## 22. P4a 五部分门(`6035c9d7` 全量 + `8c458cd5` 复跑,基线 `37da3c3a`) - -P4a 的代码头是 **`8c458cd5`**;下面的"全量"一列跑在 `6035c9d7`(终审修复轮之前的那个头,`wsl_p4a_gate.sh` 完整五部分含三次 retrace),"复跑"一列是修复轮落地后在 `8c458cd5` 上重跑的同一组。两次之间只差终审那五个提交,门的口径没变。 - -| 门 | `6035c9d7`(全量) | `8c458cd5`(复跑) | +| 门 | `6035c9d7` | `8c458cd5` | |---|---|---| -| **G1** pull 符号(认定 resize 集为空) | 0 增 / 0 删 / 0 重命名 / 0 resize,`.text` 字节不变 | 同上 | -| **G5** 字节一致区 | P3a 十一函数 rc 0;P4a 自己 **17 区 / 3 文件** rc 0,self-test **8 个阴性对照全部按名变红** | 同上 | -| **G2** pull vs push 测试名 | 差 0 | 差 0(**2902** 条) | -| **G14** 测试名增删 | 0 删除 / +275 | 0 删除 / **+314** | -| 单元 | 1772 × 3(linux / push / verify) | **1785 × 3** | -| `integration-gpu` | **1091/1091 × 七臂**(默认 `0x1fff`、`0x1ff`、`0`、`0x7f`、pull、`ESPRYT_DISABLE_INVALIDATE_FLUSH=1`、族正则 497/497) | **1117/1117 × 七臂**(多了 `0x9ff`、`0x5ff` 两个依赖拒绝臂;DirectVulkan **559/559**) | -| `integration-verify` | **896/896,零 `Fatal{`** | **920/920,零 `Fatal{`** | -| retrace(79 例) | verify 臂 **79/79 全 armed、零分歧、零 Fatal**;push 臂 **79/79**;G3b 具名 **12/12** | push 臂 **79/79** | -| 三个阴性对照脚本 | 全部 rc 0:`g7_negative_control.sh`、`p3a_vertex_input_negative_control.sh` 点名 `IsBgra`、**`p4a_descriptor_negative_control.sh` 点名 `Layered` 与 `borderColorForm`** | 同上 | -| 子系统对照套件 | `CsoContentAddressing` + `ResourceSubsystemControl` + `ObjectSubsystemControl` **24/24**,`0x9ff` 依赖拒绝臂 **14/14**,`HandleRecycle`(verify)**180/180** | 同上 + `184/184` controls | -| 八族拒绝普查 | — | **0**(16 条 needle × retrace 日志、13 行 × itest 日志) | +| **G1**(认定 resize 集为空) | 0 / 0 / 0 / 0,`.text` 不变 | 同上 | +| **G5** | P3a 十一函数 rc 0;P4a **17 区 / 3 文件** rc 0,self-test 8 个阴性对照按名变红 | 同上 | +| G2 / G14 | 差 0 / +275 | 差 0(2902 条)/ +314 | +| 单元 | 1772 × 3 | **1785 × 3** | +| `integration-gpu` | 1091/1091 × 七臂 | **1117/1117 × 七臂**(默认 `0x1fff`、`0x1ff`、`0`、`0x9ff`、`0x5ff`、pull、`ESPRYT_DISABLE_INVALIDATE_FLUSH=1`;DirectVulkan 559/559) | +| `integration-verify` | 896/896 零 `Fatal{` | **920/920** | +| retrace(79 例) | verify 79/79 armed 零分歧;push 79/79;G3b 12/12 | push 79/79 | +| 阴性对照脚本 | `g7_negative_control.sh`、`p3a_vertex_input_negative_control.sh`(`IsBgra`)、**`p4a_descriptor_negative_control.sh`(`Layered` 与 `borderColorForm`)** rc 0 | 同上 | +| 子系统对照套件 | `CsoContentAddressing` + `ResourceSubsystemControl` + `ObjectSubsystemControl` 24/24;`0x9ff` 依赖拒绝臂 14/14;`HandleRecycle`(verify)180/180 | 同上 + 184/184 controls | +| 八族拒绝普查 | — | **0**(逐用例读私有日志;`ctest -V` 是假零) | -**两处口径,都是这一波踩出来的**: +G9 的"落地前必须红"没能通过公共 GL 达成(回读模拟自己会设 `GL_DEPTH_STENCIL_TEXTURE_MODE`):改成 `PipeApplyPeek` 白盒断言,变异下 4/4 变红。真正红过再绿的两条来自终审:per-level respecify 丢上传(`0x1fff` 下读回全黑)与 delete-then-draw(`pure virtual method called`)。CTS `direct_state_access.framebuffers*` / `packed_pixels`(G15)未跑,随 P3b/P4b 补。 -1. **`ctest -V` 做拒绝普查是假零。** console sink 在发布配置里被编译掉,`ctest -V` 抓不到任何 `MGLOG_E`;必须逐用例单跑并读它自己的日志文件(工具 `~/w7/notes/tools/wsl_p4a_refusal_census.sh`)。一份"零拒绝"的普查如果是用 `-V` 取的,它证明的只是 sink 被关了。 -2. **普查的短语表必须覆盖全部八族**,而且要小心跨字符串字面量换行的句子——最初那版漏了 sampler 与 renderbuffer 两族,正好是后来真出问题的那两族。 +### 5.2 缝的分类(契约七次修正 `c0b`…`c0g` + 一轮缝类审计 + 一轮终审修复) -## 23. 这一波真正的产出:缝的分类 - -P4a 的契约改了七次(`c0b`…`c0g`),外加一轮缝类审计与一轮终审修复。把它们按**类**记下来,比按包记有用得多——每一类都在多个包里重复出现过,而下一阶段的 brief 应当在开工前就把这几张表写死: - -| 类 | 这一波的实例 | 症状 | 预防 | +| 类 | 实例 | 症状 | 预防 | |---|---|---|---| -| **编码没定死** | `MGPSubData::Target`(低字节资源目标 + 高字节上传目标 vs 裸枚举)、`DepthStencilMode`、`MGPSurface::Kind`、缺 `TextureTarget` | 两侧各自发明一套;`TextureUploadTarget::Texture1D == 0` 与 `kMGPipeResourceTargetBuffer == 0` 撞上,applier 的"这是 buffer 吗"判定被静默污染 | **编码表**:每个字段一行,写清位布局与零值含义,放进契约而不是包头 | -| **身份 vs 内容** | 内置 sampler:client 按对象身份铸、cache 按内容铸;Espryt twin 按身份查那条内容寻址的记录 | 查找永远落空 → 整族拒绝(本波两次,其中一次让 17 条 Iris 光影 trace 全部用驱动默认采样器) | **每 kind 两侧 handle 规则表**:谁铸、按什么键、谁查、按什么键 | -| **记录键错了维度** | framebuffer 记录按"当前绑定"存,DSA 的 `BlitNamedFramebuffer` / `ClearNamedFramebuffer*` 按名字来 | 打进一个从没收到附件的 FBO;SSIM 看得见但没有任何拒绝 | 记录按**对象**存,绑定另存句柄;第四个 target 值 `Named` | -| **进程级单例 vs 每上下文命名** | `CompositeResolver` 按管线 GL 名记忆,GL 名是每上下文的 | 一次 make-current 就释放掉另一个上下文还活着的合成体 | 单例的键必须含上下文身份 | -| **破坏性客户端动作缺前置条件** | 按 acceptance 清 dirty,但 (a) Magma 根本没有消费者,(b) D-K2 依赖位只在服务端拒 | 上传丢失:66 条 DirectVulkan 用例、`0x7ff` 下 438/491 | **消费者门 + 依赖门都要在客户端侧**:没消费者/依赖不满足时**一条不发**,而不是发了再在服务端拒 | -| **快门看不见自己的主体** | `glBindSampler` 只动位 12 的世代;SSO 下 `GetCurrentProgram()` 恒 null;`glBindImageTexture` 只换 level 时三个计数器都不动 | 记录停在上一次的值,第一个真读该字段的消费者画错(`create-indirect` ssim 0.887) | **"记录字段 → setter → 快门"完备表**;新增计数器会撑大 pull 对象、G1 不允许,所以优先混入已有世代 | -| **清得太宽** | `resource_respecify` 清掉整张待上传表 | 已被接受、客户端标志已清的那一级永久丢失(读回全黑) | 作用域随调用走:一级 / 截断链 / 整资源 | -| **死亡没通知发射方** | 六个死亡 helper 只释放 slot | 已删但未复用的句柄仍解析到已释放的前端对象 → 下一个 validate 点对已释放内存调虚函数 | 死亡在 wire delete 与 free 之间转发给每个 emitter;查找按"活着"判定而不只按世代 | -| **门不能变红** | `G7` 脚本因 scoped enum 写 0 而永远编译不过、`HighWater(ShaderCso)` 取的是段顶、G9 的红前态公共 GL 不可见 | 绿得毫无意义 | 每个门都要有阴性对照并**真跑过一次红**;公共 GL 看不见的,改白盒断言(`PipeApplyPeek`) | +| 编码没定死 | `MGPSubData::Target`、`DepthStencilMode`、`MGPSurface::Kind`、缺 `TextureTarget` | 两侧各自发明一套;`Texture1D == 0` 与 `kMGPipeResourceTargetBuffer == 0` 撞上 | 编码表放进契约 | +| 身份 vs 内容 | 内置 sampler:client 按身份铸、cache 按内容铸 | 查找永远落空 → 17 条 Iris trace 用驱动默认采样器 | 每 kind 两侧 handle 规则表 | +| 记录键错了维度 | framebuffer 记录按"当前绑定"存,DSA 按名字来 | 打进从没收到附件的 FBO | 记录按对象存;`Named = 3` | +| 进程级单例 vs 每上下文命名 | `CompositeResolver` 按管线 GL 名记忆 | 释放另一个上下文还活着的合成体 | 单例的键含上下文身份 | +| 破坏性客户端动作缺前置条件 | 按 acceptance 清 dirty,但 Magma 无消费者、依赖位只在服务端拒 | 上传丢失:66 条 DirectVulkan 用例、`0x7ff` 下 438/491 | 消费者门 + 依赖门都在客户端:一条不发 | +| 快门看不见自己的主体 | `glBindSampler`、SSO 下 `GetCurrentProgram()`、`glBindImageTexture` 只换 level | 记录停在上一次的值(`create-indirect` SSIM 0.887) | "记录字段 → setter → 快门"完备表;优先混入已有世代 | +| 清得太宽 | `resource_respecify` 清掉整张待上传表 | 已接受的那一级永久丢失 | 作用域随调用走 | +| 死亡没通知发射方 | 六个死亡 helper 只释放 slot | 已删句柄解析到已释放对象 → UAF | 死亡转发给每个 emitter | +| 门不能变红 | G7 脚本永远编译不过、`HighWater(ShaderCso)` 取段顶、G9 红前态公共 GL 不可见 | 绿得毫无意义 | 每个门带阴性对照并真跑过一次红 | -**一个方法论上的结论**:本波六个包的 v1 全部通过了自己的门,**六份对抗性复审全部判 REWORK**,而其中最贵的两个缺陷(丢上传、delete 后 UAF)是**整体 diff 终审**才抓到的——因为它们跨包:发射方、applier、twin 各自都自洽。所以"每包一审 + 集成后整体终审"这条流程里,**终审不是形式**,它是唯一能看见跨包契约的那一轮。 +方法论结论:六个包的 v1 全部通过自己的门、六份对抗性复审全部判 REWORK,最贵的两个缺陷(丢上传、delete 后 UAF)是**整体 diff 终审**才抓到的——它们跨包,各方自洽。终审是唯一能看见跨包契约的那一轮。 -## 24. P4a 设备配对 A/B(三臂)、MC 26.3 的 p99、上传形状(记录项) +### 5.3 Redmi 三臂 A/B、MC 26.3 的 p99、上传形状(记录项) -**先说口径,再看数(`MEASUREMENTS.md:440` 那条告诫在 P4a 上再次成立)**:`acc/draw` 数的是**约十个热入口上的静态计数点**,所以"把读点搬走"和"把工作去掉"在它上面长得一模一样。P4a 恰好是**搬**的一波:79 例语料上 DirectGLES 的 `acc/draw` 普遍下降(26.3 `10.49 → 8.34`、`rei-inventory` `13.33 → 11.25`、`rd12` `8.09 → 6.09`),而同一批运行的逐线程 CPU 是**上升**的。**这不是矛盾,是这个计数器的定义**:后端不再每 draw 去 `pGLContext` 上取,改成读被推送的记录,站点自然少计——工作搬到了客户端的发射侧。要判性能只看 CPU 时间序列与门的命中/未命中对,`acc/draw` 只能与站点常量表一起读。 - -**设备与协议(与 §20 的两台机不同,这里换了机器)**:红米 M332BF(`2f7cbe2e`,SM8750 / **Adreno 830v2**,与 §20 的小米同 SoC 同定频点,数值可比)。reboot-clean → 大核 `policy6` 钉 1958400、小核 `policy0` 钉 1555200、GPU `pwrlevel 0`;**这台的 GPU 有效定频是 1050 MHz 不是 1100**——厂商把 `kgsl-3d0/thermal_pwrlevel` 常驻 1,root 写 0 无效(33 °C + 风扇全速下验证),`pin_device.sh` 按 1050 判定。全程**主动风扇恒定 level 2(~14.5k rpm)**:它对两臂是同一个常量,作用是把每用例之间的降温从 20–30 分钟压到 1 分钟以内,**40 个样本 40 个 `pin check` 全是 PINNED**(§20 那轮有 22 个样本因热漂移作废重跑)。APK 是 `8c458cd5` 的 Release trace 双臂(pull 8504077 B / push 8626957 B)。 - -**三臂表**(`--benchmark-no-finish`,尾 200 帧、best-of-3、逐线程 CPU p50 ms;`0x1ff` = P2+P3a 边界,`0x1fff` = P4a 默认): +设备换为 Redmi `2f7cbe2e`(与小米同 SoC 同定频点,数值可比;GPU 当时钉 1050 MHz;主动风扇 level 2,40 个样本 40 个 PINNED)。APK `8c458cd5` Release 双臂;`--benchmark-no-finish`,尾 200 帧、best-of-3、逐线程 CPU p50 ms;`0x1ff` = P2+P3a 边界,`0x1fff` = P4a 默认。 | 用例 | 后端 | pull | `0x1ff` | `0x1fff` | Δ P2+P3a | Δ 合计 | **P4a 自己** | |---|---|---|---|---|---|---|---| | improved-transparency-26.3 | Espryt | 10.716 | 11.754 | 12.124 | +9.7% | +13.1% | **+3.4 pt / +0.37 ms** | | improved-transparency-26.3 | Magma | 10.603 | 11.543 | 11.585 | +8.9% | +9.3% | +0.4 pt(噪声) | | rd12-odinlite | Espryt | 8.210 | 10.798 | 11.182 | +31.5% | +36.2% | **+4.7 pt / +0.38 ms** | -| rd12-odinlite | Magma | — | — | — | — | — | 三臂全 `rc=1`,见下 | +| rd12-odinlite | Magma | — | — | — | — | — | 三臂全 `rc=1`(`scudo`,`dev` 侧,换机仍复现) | | fabric-sodium | Espryt | 1.312 | 1.406 | 1.454 | +7.2% | +10.8% | +3.6 pt / +0.05 ms | | fabric-sodium | Magma | 0.474 | 0.502 | 0.507 | +5.9% | +7.0% | +1.1 pt(噪声) | | 1.21.4-in-world | Espryt | 2.369 | 2.732 | 2.867 | +15.3% | +21.0% | **+5.7 pt / +0.14 ms** | @@ -593,292 +293,136 @@ P4a 的契约改了七次(`c0b`…`c0g`),外加一轮缝类审计与一轮 | fabric-iris-bsl | Espryt | 1.727 | 1.740 | 1.811 | +0.8% | +4.9% | +4.1 pt / +0.08 ms | | fabric-iris-bsl | Magma | 0.742 | 0.788 | 0.786 | +6.2% | +5.9% | −0.3 pt(噪声) | -**读法。** (1) **P4a 自己在 Espryt 上是 +3.4 ~ +5.7 个百分点**(绝对值 0.05–0.38 ms/帧),大头仍然是 P2+P3a 那条边界——rd12 上 36.2% 里有 31.5% 是它。(2) **Magma 的 `0x1ff` 与 `0x1fff` 两臂在四个用例上逐个落在噪声内**(+1.1 / +0.4 / −0.2 / −0.3 pt),这是 c0f 那道"没有后端注册 `MGPipeResourceOps` 就一条不发"的门在设备上的读数——Magma 仍然要付 P2+P3a 的客户端发射(它消费那些族),但 P4a 的四族对它完全免费。(3) **`vanilla`(1.21.4-in-world)是 Espryt 上 P4a 占比最高的用例**(+5.7 pt),它 draw 少、状态切换密,正是句柄化最不划算的形状;`sodium`/`iris-bsl` 这种把状态压平的语料几乎不受影响。 +读法:P4a 自己在 Espryt 上是 +3.4 – +5.7 pt(0.05–0.38 ms/帧),大头仍是 P2+P3a 的边界;Magma 两臂在四个用例上落在噪声内——消费者门在设备上的读数(Magma 一条 P4a 记录都不发);`vanilla` 是 P4a 占比最高的用例(draw 少、状态切换密)。**MC 26.3 在 Adreno 上的 p99**:pull 25.297 → P4a 26.841 ms(+6.1%),仍在 21–26 ms 档。**上传形状 pull 与 push 逐项相同**(79 例两臂 `tex[emit/box/rect/jobs]`:18451/16060/2391/39926 对 18453/16062/2391/39928,唯一差异是 2 次 `trp` 带来的重放上传)。线索:设备上 26.3 Espryt 的 `sve` ≈ draw 数(9143 / 9138),桌面 ~0.07/draw——每 draw 重发一次 sampler-view 集合,进 P3b/P4b 优化清单。`acc/draw` 在这一波普遍下降而 CPU 上升,不是矛盾而是该计数器的定义。 -**头条一:MC 26.3 在 Adreno 上的 p99。** pull **25.297** → P4a **26.841 ms(+6.1%)**,`0x1ff` 臂 26.387;Magma 侧 24.979 → 26.021。对照 §20 的 P3a 读数(25.457 → 26.322,+3.4%)与 `MEASUREMENTS.md:87` 的采纳基线(p99 163 → 21 ms),**仍在 21–26 ms 档内、没有回到采纳前的形态**——按口径记录,不判门。 +### 5.4 DriverBench(`wsl_p4a_bench.sh`,`8c458cd5`,`mc_vanilla_draw` ns/draw) -**头条二:GUI/atlas 的纹理上传形状,pull 与 push 逐项相同。** 79 例语料两臂各跑一遍带 `MOBILEGL_PIPE_STATS=1` 的 retrace(`8c458cd5`,客户端计数器 `tex[emit/box/rect/jobs]`): +Espryt T1(`0x1fff` − pull)= +1076.1,T2(`0x1ff` − pull)= +1064.7,T1 − T2 = +11.4;Magma T1 = +629.0、T2 = +452.6、T1 − T2 = +176.4。两臂绝对值在两轮之间各漂 ~200 ns 而差只有 10–130 ns,**桌面 bench 分辨不出 P4a 这一档**;可引用的是 T1 ≈ +1.1 µs/draw 的总边界(Espryt)与设备侧三臂表。Magma 的 +176 ns 不是"Magma 在跑 P4a"(消费者门让它一条不发),是 tracker 多算的快门加噪声。 -| | emit | box | rect | jobs | -|---|---|---|---|---| -| pull | 18451 | 16060 | 2391 | 39926 | -| push | 18453 | 16062 | 2391 | 39928 | -| 差 | **+2** | **+2** | **0** | **+2** | +## 6. P5(joint `e61d0012` → 落地 `eec0e836` → 收尾头 `37fc4fdb`) -**整份语料上唯一的形状差异是 2 次**,而且正是那 2 次 `trp`(纹理重铸拉取,见 `ROADMAP.md` 开放问题 2)带来的重放上传——即"盒 vs 矩形"的分解一格没动。这是 SSIM 看不见、Mali 那道 ~+6 ms/帧的悬崖就藏在里面的那个数(`ARCHITECTURE.md` §6),P4a 在这里是**中性**的。逐用例看也一致:`rei-inventory` Espryt 两臂都是 16/16/0/16,Magma 两臂都是 47/46/1/75。 - -**一条留给优化阶段的线索(不是缺陷,是读数)**:设备上 26.3 Espryt 的 `sve`(真正发出去的 sampler-view 集合数)**≈ draw 数**(9143 次 / 9138 draw,每窗口 120 帧),而桌面同一 fixture 只有 ~0.07/draw。`PipeStats.h` 给这四个集合计数器写的用途正是这个——"抑制器不再抑制时,它的计数会跟着 draw 数走而不是跟着状态变化走"。桌面与设备的差异说明这跟负载形状有关而不是无条件失效,但 **26.3 在设备上每 draw 重发一次 sampler-view 集合**是 Espryt 侧 P4a 那 +0.37 ms 最值得先查的去处,列进 P3b/P4b 的优化清单。 - -**`rd12` + Magma 在这台机上照样崩**:三臂(含 pull)全部 `rc=1`,与 §20 在小米上的记录一致(`scudo::reportMapError` ← `remapImpl` ← `scudo_calloc` ← `libMobileGL.so`)。**换了一台同 SoC 的机器仍然复现,进一步确认它是 `dev` 侧的问题而不是设备个例**;按 `ROADMAP.md:7` 的纪律不在本分支顺手修,处置沿用 §20:排除在 A/B 之外、留在桌面语料里(桌面两臂均通过)。 - -## 25. P4a DriverBench:T1 / T2 / T3(桌面,lavapipe + llvmpipe,记录项) - -`wsl_p4a_bench.sh` 在 `8c458cd5` 上重跑(原始表 `~/w7/notes/p4a/bench/driverbench.{csv,md}`,repeats=5 / frames=240,空闲机)。臂:`pull`、`push`(`0x1fff`)、**`push7f` 一列在 P4a 里装的是 `0x1ff`**(P2+P3a 边界 = 设备侧那个 T2 的桌面对应物)、`push0`(`PIPE_PUSH=0`)、`nocso`(关 CSO 内容寻址的负面对照)。 - -| 臂 | `mc_vanilla_draw` | `mc_state_toggle` | `mc_pass_switch` | -|---|---|---|---| -| native | 4398.9 | 21387.3 | 412657.8 | -| espryt-pull | 4684.4 | 22010.4 | 410936.2 | -| espryt-push(`0x1fff`) | 5760.5 | 22974.9 | 419703.1 | -| espryt-`0x1ff` | 5749.1 | 23768.2 | 419288.0 | -| espryt-push0 | 5355.2 | 23618.8 | 420362.2 | -| espryt-nocso | 5929.0 | 23685.0 | 418677.0 | -| magma-pull | 15394.9 | 31678.5 | 420197.6 | -| magma-push(`0x1fff`) | 16023.9 | 32417.4 | 415641.9 | -| magma-`0x1ff` | 15847.5 | 32100.3 | 416670.4 | -| magma-push0 | 15946.6 | 31610.2 | 417410.5 | -| magma-nocso | 15846.2 | 32495.9 | 411306.4 | - -`mc_vanilla_draw` 上:**espryt T1(`0x1fff` − pull)= +1076.1 ns/draw,T2(`0x1ff` − pull)= +1064.7,T1 − T2 = +11.4**;magma T1 = +629.0、T2 = +452.6、T1 − T2 = +176.4。blend toggle:espryt +964.5 / magma +738.9 ns per toggle pair;pass switch:espryt +8766.9、magma −4555.7(后者符号为负,属该项的噪声量级)。 - -**桌面这台机上,"P4a 自己"落在本 bench 的噪声底以下,所以不要单独引用它。** 同一份脚本在 `6035c9d7`(终审修复前)上跑出的是 espryt T1 +1269.7 / T2 +1135.8 / **T1 − T2 = +133.9**,本轮是 +1076.1 / +1064.7 / **+11.4**——**两臂的绝对值在两轮之间各自漂了 ~200 ns,而它们的差只有 10–130 ns**,也就是说这个 bench 分辨不出 P4a 这一档的增量。真正可引用的是:(1) **T1 ≈ +1.1 µs/draw 的总边界**(对 pull 基线,Espryt;这条在两轮之间是稳的);(2) **设备侧的三臂表(§24)**——那里 P4a 自己是 +3.4 ~ +5.7 个百分点、0.05–0.38 ms/帧,样本全部在验证过的定频窗口里。**Magma 的 T1 − T2 = +176.4 ns 不是"Magma 在跑 P4a"**:c0f 的消费者门让它一条 P4a 记录都不发(设备侧 §24 的 Magma 两臂差也在噪声内),这 176 ns 是 tracker 多算的那几个快门加噪声。 - ---- - -## 26. P5 五部分门(joint `e61d0012`)与 landed quick gate(`eec0e836`) - -P5 的历史 joint 记录来自 `~/w7/notes/p5/p5-results/joint-v1.md` §2 与 ID-63(其中设备部分未运行);脚本按 **1 → 5 → 3 → 2 → 4** 的顺序跑。`e61d0012` 是 joint scratch head,随后同一组 landed 代码进入 `feat/disaggregated@eec0e836`;landing 后按 ID-66 只跑 quick gate,不把 quick gate 写成第二次 full gate。 +### 6.1 五部分门(joint)与落地快门 | 部分 | joint 实测 | |---|---| -| **1 接口纯度** | include closure **4 probes / 0 problems**;pull `MG_Remote` symbol **0**、split **610**;G1 `.text` **10806611 → 10806611**、defined symbols **27814 → 27814**、**0 added / 0 removed / 0 resized / 0 renamed**;P3a/P4a G5 对 `ff2994d9` byte-identical | -| **5 coverage / generator** | `gen_pipe` up-to-date、self-test **9/9**;dirty-surface **27/27**(`UseProgram` 一项明确为 UNDECIDED,不伪装成 absence proof);field ownership up-to-date、**15/15**;emitter/CSO **185/185**;verify controls **4/4** | -| **3 行为 A/B** | G2 name diff **0**;G14 **0 removed / 42 added**;unit **1816/1816 ×3**、split **2038/2038**;Wire **58/58**;integration-gpu pull **1128/1128**、push **1128/1128**、split-monolith **1149/1149**;split-inproc **426 pass / 185 skip / 511 abort / 27 fail / 0 segfault**;`integration-split` **21/21(19 run、2 design-skip)**;persistent arm **2/2**,split `pmap=2160.00, mpr=1`,push `pmap=0.00, mpr=1` | -| **2 语义 / retrace** | integration-verify **930/930**、`Fatal{` **0**;OpenRA inproc **2/2**、两后端 SSIM **1.0**、`Fatal{` **0**;push retrace **79/79**;verify retrace **79/79**、armed **79/79**、`Fatal{` **0** | -| **4 设备** | **未跑**;joint package 明确禁止 adb。设备 A/B 留在 §30,不用 desktop 数代填 | +| 1 接口纯度 | include closure 4 probes / 0 problems;pull `MG_Remote` 符号 0、split 610;G1 `.text` 10806611 → 10806611、27814 符号 0/0/0/0;P3a/P4a G5 对 `ff2994d9` byte-identical | +| 5 覆盖 / 生成器 | `gen_pipe` self-test 9/9;dirty-surface 27/27(`UseProgram` 一项明确 UNDECIDED);field ownership 15/15;emitter/CSO 185/185;verify controls 4/4 | +| 3 行为 A/B | G2 差 0;G14 0 / +42;unit 1816×3、split 2038;Wire 58/58;`integration-gpu` pull 1128、push 1128、split-monolith 1149;split-inproc 普查 426 pass / 185 skip / 511 abort / 27 fail;`integration-split` 21/21(19 run、2 design-skip);persistent arm 2/2,split `pmap=2160.00, mpr=1`,push `pmap=0.00, mpr=1` | +| 2 语义 / retrace | `integration-verify` 930/930 零 `Fatal{`;OpenRA inproc 2/2 双后端 SSIM 1.0;push retrace 79/79;verify retrace 79/79 armed | +| 4 设备 | 未跑(joint 禁止 adb);见 §6.4 | -landed quick gate(ID-66):split unit **2038/2038**;`integration-split` inproc **22/22**(比 joint 多 `SplitLogPaths`);push integration-gpu **1128/1128**;G1 **0/0/0/0、`.text +0`**。这组数证明 landing 没丢 joint 的 reduced path,不声称重跑了 Part 2/4。 +落地快门(`eec0e836`,ID-66):split unit 2038;`integration-split` 22/22;push `integration-gpu` 1128;G1 0/0/0/0 `.text +0`。 -## 27. P5 退出门 E1–E6:历史读数与后续修正 +### 6.2 退出门 E1–E6 与 27 个 wrong-answer -本表保留 joint 的历史基线,并明确写出 x2、v1-r3 和 r2 的后续证据。收尾头 `37fc4fdb` 的实跑边界另见 §31;表内不同头的结果不能合并成一次全门。 - -| 门 | 实测与“为什么会红” | +| 门 | 实测与"为什么会红" | |---|---| -| **E1 barrier** | default reduced path 三次均 **19 pass / 2 skip / 0 fail**;`MOBILEGL_IPC_VERB_BARRIER=0` 选中的 **14/14** 全 abort,且每个 private lane file 都有自己的 `Fatal{BarrierViolation, ""}`(ID-53/65,`joint-v1.md` §3) | -| **E2 OpenRA** | joint baseline **2/2、SSIM 1.0**。x2 在 DirectGLES 实测丢 **29 clears** 仍 SSIM 1.0:目标帧被后续地形 draw 全面覆盖;换为丢 **758 DrawVbo** 后 SSIM **0.000036**、mismatchPixels **295296**,并要求正的丢弃计数。r2 又验证真实 CTest 顺序:baseline → pull-library control → 恢复原 split 库 → draw-drop;先放入 pull 库会在 replay 前被 `MG_Remote=0` 拒绝(§31) | -| **E3 persistent map** | joint (a) **6 selected / 4 pixel red / 2 design-skip**;x2 补每进程一次 disabled-push 私有诊断,报告中的“6 red”更正为上述口径。j0 的逐选项 skip 拒绝使 `37fc4fdb` 全门停止;r2 精选默认/SmallRing 下两条 pixel 场景,共 **4 own pixel + private-log reds**。(b) ID-42 emulated membership 经 green → 3 red → restored green;(c) 历史 pmap=2160.00/mpr=1,push 0.00/1;(d) split apply decline;(e) x2 已测 wrap,r2 再证明真实 retirement wait,数字见 §28。pmap 或 block-zero 红都不能单独证明 server 不会重入 client producer(r1 #1) | -| **E4 field ownership** | generator **15/15** own-message controls;strict lane **19 abort / 2 skip / 0 pass**,首条具名 `Fatal{UnmigratedPipeInput, "GetTextureContextId@Clear"}`。这是 BARRIER-PULLED 债务的响亮读法,不是 default regression(ID-65) | -| **E5 honest inproc** | joint audit **19 pass / 2 skip / 0 fail**。v1-r3 的 `StagedShadowProductionTest.TheEnsurePathUploadsTheServerShadowNotTheClientObjectsBytes` 用 client A/server B 两份数据,真实 ensure 后 driver READ map 得 B;删 transport-only shadow 保护则因读到 client A 而红。它覆盖 **unmapped** BufferObject,不能扩写为 coherent-map 的 wire-only 证明;收官审查确认该绕过,归 r1 #1 | -| **E6 phase gate** | joint G1 **0/0/0/0**、G2 name diff **0**、G14 **0 removed / 42 added**;verify **930/930**;push/verify retrace **79/79**。历史 census **426/185/511/27/0** 在 v1-r3 修掉六条过宽拒绝后变为 **432/185/505/27/0**;这些是分头证据,`37fc4fdb` 最终门并未全部完成(§31) | +| E1 barrier | reduced path 19 pass / 2 skip / 0 fail;`MOBILEGL_IPC_VERB_BARRIER=0` 选中的 14/14 全 abort,每个私有日志有自己的 `Fatal{BarrierViolation, ""}` | +| E2 OpenRA | 2/2 SSIM 1.0;x2:丢 29 clears 仍 SSIM 1.0(被后续地形 draw 覆盖),改为丢 758 `DrawVbo` 后 SSIM 0.000036;r2 验证真实 CTest 顺序 baseline → pull-library control → 恢复原库 → draw-drop,并断言库身份 | +| E3 persistent map | (a) 收尾头 6 selected 中 4 pixel red、2 条 `TheMapLandsInTheArmItsLaneDeclares` 设计性 skip 被 j0 判红 → r2 精选默认 / SmallRing 下 4 条 pixel case 各带自己的 assertion + 私有诊断;(b) ID-42 emulated membership green → 3 red → green;(e) x2 证明 wrap,r2 证明真实 retirement wait(1 MiB 臂 `ringwraps=1 ringwaits=1`,8 MiB 对照同时红) | +| E4 field ownership | 生成器 15/15;strict lane 19 abort / 2 skip,首条 `Fatal{UnmigratedPipeInput, "GetTextureContextId@Clear"}`——BARRIER-PULLED 债务的响亮读法 | +| E5 honest inproc | 19 pass / 2 skip;v1-r3 的 `StagedShadowProductionTest` 证明 ensure 上传 server shadow(删保护会读 client A 变红),覆盖 unmapped 对象;coherent-map 的 server 侧绕过由 r1 #1 关闭 | +| E6 phase gate | 普查在 v1-r3 修掉六条过宽 whole-store 拒绝后 **432 / 185 / 505 UnmigratedVerb abort / 27 failed / 0 segfault**(1149 项,`~/w7/p5b-c0b-census-logs/results.json`) | -E6 的 27 个普通失败逐条复跑均 `rc=1`、private log **0 Fatal**;8 个争议项又各跑三次,无 flake。归属不是从测试名猜的,而是 `joint-v1.md` §4 的 probe: - -| family | 条 | 记录去向 | +| 27 个 wrong-answer 家族 | 条 | 去向 | |---|---:|---| | `LayeredAttachmentShapeScenario` | 14 | P4b/P7 layered texture readback | | packed depth/stencil `GetTexImage` | 3 | P4b/P7 texture-shadow readback | -| framebuffer `HandleRecycleScenario` | 5 | P4b/P7 readback;不是已证明的 handle recycle bug | -| `PrimitivesGeneratedNoXfbScenario` | 3 | P7 query / primitive accounting | -| `TextureParamsWithoutASamplerView` | 1 | P6 shared-backend inspection forwarder | -| `P4aFinalFixScenario` FBO/RBO delete | 1 | 原记 P6 lifetime;实际读回用 ReadPixels,完整像素因果未隔离。收官确认 framebuffer death 的 client-thread 驱动调用另归 r1 #2,不断言此条已证明与 server 改动无关 | +| framebuffer `HandleRecycleScenario` | 5 | P4b/P7 readback(不是已证明的 handle recycle bug) | +| `PrimitivesGeneratedNoXfbScenario` | 3 | P7 query | +| `TextureParamsWithoutASamplerView` | 1 | P6 inspection forwarder | +| `P4aFinalFixScenario` FBO/RBO delete | 1 | 读回用 ReadPixels,完整像素因果未隔离;client-thread framebuffer death 归 r1 #2 | -joint 当时只计数 511 个 abort,抽样了 `DrawElements` / `BeginTransformFeedback`,并未证明“511 全是 class C”。v1-r3 的逐项复查随后辨明:**505 UnmigratedVerb + 6 StageSnapshotTooNarrow**;后六条是 P5 的 whole-store coverage 过宽拒绝,修复后六条由 abort→pass,其余逐名状态不变。c0b 与 r2 的新鲜私有日志再次给出 **432 passed / 185 skipped / 505 UnmigratedVerb abort / 27 failed**(1149 项)。证据:`v1-v3.md` §2 item 6、`~/w7/p5b-c0b-census-logs/results.json`、`~/w7/p5b-r2-census-logs/results.json`。 +### 6.3 R-10、逐帧 ledger 与内存 -27 条 wrong-answer 中只有 **22** 是上述 texture-shadow/readback 家族,另有 3 query、1 inspection、1 FBO/RBO lifetime。阶段接纳没有消除这些错误;“全部 27 条都是纹理读回”或“均非 v1 的问题”均不受证据支持。 +规范 ledger(`ProtocolSmokeTest` 钉住):**SEG_CMD 8 MiB / SEG_STAGE 32 MiB / SEG_REPLY 16 MiB / SEG_EVENT 256 KiB**。`MOBILEGL_PIPE_STATS_PERIOD=1` 下 persistent-map 场景 `pmap=600.00 B/帧, mpr=1, rsp=35`(`rsp` 只是有 stamp 读点的下界)。inproc 进程峰值 RSS:server accept 时 11.5 MB、client teardown 141 MB(两角色共享一个进程,不是两个独立峰值)。 -## 28. R-10、逐帧 ledger 与内存口径 +max record bytes(x2,`37fc4fdb`):Triangle / persistent map / OpenRA 25 帧 / SmallRing 都是 **`maxrec=784 B` / `SetVertexAttribDefaults`**,默认 cap 4 MiB(占 0.019%)——这些负载不需要 chunking;oversized `DrawVbo` 尾的 unit 具名拒绝 `Fatal{RingOverrun, "DrawVbo"}`。 -`joint-v1.md` §5 在 `MOBILEGL_PIPE_STATS_PERIOD=1` 下只取真实非零窗口: +### 6.4 Redmi 四臂 A/B(split 未测) -| entry | frame/window | `pmap` bytes/frame | `mpr` | `rsp` | -|---|---|---:|---:|---:| -| Triangle redraw | 1/1 | 0.00 | 0 | 35 | -| Triangle redraw | 2/1 | 0.00 | 0 | 35 | -| Persistent map write-after-frame | 1/1 | 600.00 | 1 | 35 | -| Persistent map write-after-frame | 2/1 | 600.00 | 0 | 35 | +会话 2026-09-16,runner `eec0e836`,三份 APK 源码头 joint `e61d0012`;臂 = pull APK / push APK / split APK + `MOBILEGL_TRANSPORT=inproc` / splitctl(split APK 不设 transport)。40 组前后 pin check 全 P/P,37.2–39.9 °C。**27 组完成 / 13 组失败:全部 10 组 split 在 benchmark 前中止**(improved-transparency 双后端 `DrawElementsInstancedBaseVertex`,其余 `DrawElements`),另 3 组是 rd12/DirectVulkan 的既有 `scudo` 崩溃。帧 p50 / p99(ms): -| entry | server accept peak RSS | client handshake peak RSS | client teardown peak RSS | +| case / backend | pull | push | splitctl | |---|---:|---:|---:| -| Triangle | 11,526,144 B | 11,706,368 B | 141,451,264 B | -| Persistent map | 11,464,704 B | 11,649,024 B | 141,422,592 B | +| improved-transparency / DirectGLES | 10.535 / 25.415 | 12.106 / 26.973 | 12.235 / 27.131 | +| improved-transparency / DirectVulkan | 10.742 / 25.031 | 11.678 / 26.134 | 11.837 / 26.476 | +| fabric-sodium / DirectGLES | 8.314 / 9.573 | 8.311 / 9.701 | 8.303 / 9.486 | +| vanilla 1.21.4 / DirectVulkan | 1.043 / 2.293 | 1.159 / 2.404 | 1.179 / 2.426 | +| rd12 / DirectGLES | 7.999 / 22.089 | 11.105 / 24.865 | 11.261 / 24.771 | -`inproc` 两角色共享一个进程,所以上表是**进程**峰值,不是两个独立 physical peak;server 只在 accept 时取样,不能声称 full-run server peak,也没有 N-frame RSS slope。日志另报 `roleMapped=58,990,592 B`、`allRolesMapped=117,981,184 B`。规范 ledger 由执行过的 `ProtocolSmokeTest` 钉住:**SEG_CMD 8 MiB / SEG_STAGE 32 MiB / SEG_REPLY 16 MiB / SEG_EVENT 256 KiB**(ID-47/65)。 +splitctl 相对 push 的帧 p50 −0.1% – +1.6%(split build 的 monolith 臂与 push 同源等价)。barrier tax 在 P5 未测得,归 P5b(§7.4)。 -**x2 后续实测 max record bytes**(`p5/x2@579118a1`,经 x2m 合到 `37fc4fdb`;`x2-v1.md` §2、`x2m-v1.md`): +### 6.5 收尾头停止点、r1 / r2 -| workload | maxrec / row | cap | cmd bytes / records | -|---|---|---:|---| -| Triangle redraw | **784 B / SetVertexAttribDefaults** | 4,194,304 B | 4,040 B / 48 | -| Persistent map write-after-frame | **784 B / SetVertexAttribDefaults** | 4,194,304 B | 4,904 B / 59 | -| OpenRA DirectGLES,25 帧 | **784 B / SetVertexAttribDefaults** | 4,194,304 B | 未发布 run 总量 | -| SmallRing triangle,1 MiB SEG_CMD | **784 B / SetVertexAttribDefaults** | 524,288 B | 1,314,880 B / 24,323 | +`~/w7/p5-final-gate.log`(`37fc4fdb`):构建 4 × rc 0;Part 1 G1 27,814 符号 0/0/0/0、`.text +0`、两条 G5 byte-identical;Part 5 完成;Part 3 unit 1817×3 / split 2086、Wire 58、pull/push GPU 1128、split-monolith 1149 零失败、reduced 22 零失败、broad lane 1149 selected(532 non-success,含 abort);E1 14 red;**E3(a) 因 2 条设计性 skip 被 j0-v3 的逐选项拒绝判失败,Part 2 / 4 未到达**。j0 的 skip 拒绝是对的(不能放宽),选择器才是错的。 -默认 cap 下占 **0.019%**,这些负载不需要 R-10 chunking;更大的后续 draw tail 不能由这个结果保证。stats 上的 maxrec/maxcap/wrap/wait 是 **run totals**,不是窗口内计数。oversized DrawVbo 尾的 unit 具名拒绝 `Fatal{RingOverrun, "DrawVbo"}`,证明 half-ring 上限能红。 +收官审查(`p5-close-codex-review.md`,只读):1 blocker / 10 major / 2 minor(ID-73)。r1(核心四项):apply-role 不得进入 persistent-map producer;framebuffer death 转 apply mailbox;`PACK_SWAP_BYTES` 的 component / packed-word swap;command-ring retirement 等待与重试。r2(`37fc4fdb..d50183cb`,CI / 控制 / 跑器九项):broad debt lane 记录后继续硬门、全 skip 则 baseline 失败;E2 库身份 SHA256 恢复;E3(a) 精选 4 pixel case;`ringwaits` 只计真正阻塞的分配;split unit 强制 `MOBILEGL_ITEST_REQUIRE_GPU=1`;普查跑器零执行拒绝、launch 前清私有日志;G5 pin 自测驱动生产选择路径;c1f 改用 ID-67 用例。r2 包门:split unit 2087、reduced 22、push 1128 零失败;E1 14 own reds、E3(a) 4 own reds;smoke 16 core + 12 private;census 432/185/505/27。r2 没有新跑 pull G1(不影响 pull production code)。 -x2 SmallRing 实测 `ringwraps=1, ringpads=0, ringwaits=1`,其中 **wrap 已自证,旧 wait 数只说明发生 reclaim**,不能称阻塞等待。r2 改为立即 reclaim 仍不够、确实被未退休发布记录阻塞的 allocation 才计一次;普通 GL 768 KiB uploads 配合只延迟退休的 hook,1 MiB 臂得到 `maxrec=784 cap=524288 cmdbytes=1315256 ringwraps=1 ringpads=0 ringwaits=1 emitseq=24328`。把 command/staging 都增至 8 MiB,同一门分别因 `NEVER WENT ROUND` / `producer NEVER WAITED` 变红(`r2-v1.md` #8、`p5b-r2-ring-{1,8}.{out,log}`)。这是 r2 包头的 staging-wait 证据;r1 的完整 command-ring/shutdown 处置另计。 +过程记录(不计入实现产量):wave-1 跨族复审十项全部经独立 perturbation 确认(ring 空 wrap 算术、reply 几何差 16 B、blob 校验接受任意段、pad-bit 控制没发送该 bit、三个 CI 对照接受任意非零退出……);后续 v1 / c1 各轮的 review 计数在 ID-52/58/62/64;本地 `rereview` 身份污染的 81 + 58 个提交经 parent/env rewrite 接回 GitHub `ff2994d9`(ID-40),此后每阶段首个 commit 前先跑 `git var GIT_COMMITTER_IDENT`。这些数字是流程在 P5 尾声改变(ID-66)的原因。 -`rsp=35` 等历史值仅是当前有 stamp 的计数下界:未盖 verb stamp 的 sticky/non-verb frontend reads 仍有未计数/未 strict 拒绝的路径,见收官审查与 r2 债务表;不能据此称全部 residual reads 已纳入 ledger。 +## 7. P5b(主机门 `348d22a4`,设备源头 `82683d4a`) -## 29. 复审作为过程测量:跨族发现、轮次与身份修复 +### 7.1 迁移与包证据 -wave-1 跨模型族复审的十项全部经独立 perturbation **CONFIRMED(10/10,0 refuted,0 partial)**;它量到的不是“代码行多”,而是十个本轮同族门没有捕获的具体缝(ID-46/48,`wave1-codex-review.md` + `wave1-codex-verify.md`): +`348d22a4` 含 d1 / i1 / t2 / f1、r1 / r2、五槽 sync、具名 blit 与 GLES mip storage;发射表 A=2 / B=54 / C=15(目录 76 条,槽数与 opcode 数不是同一统计量)。 -| # | confirmed finding | -|---:|---| -| 1 | empty `SEG_STAGE` wrap arithmetic 拒绝本可容纳的 blob | -| 2 | 旧 8-slot reply geometry 连 512×512 RGBA8 都差 16 B | -| 3 | blob validation 接受任意 mapped segment,audit poison 覆盖不全 | -| 4 | wire `MapPersistent` decline 绕过 adoption-tier refusal | -| 5 | pad-bit control 经 `Reserve` 后根本没有发送该 bit | -| 6 | ABI sensitivity gate 驱动的是 production 已不调用的 helper | -| 7 | null-union control 没到达 handshake guard | -| 8 | 三个 CI negative control 接受任意非零退出 | -| 9 | `gen_pipe.py expect_trip` 接受任意 `SystemExit` | -| 10 | 两个 death control 使用空 diagnostic regex | - -后续 v1/c1 review 的数量本身也入账,作为门质量的过程数据: - -| 决策 | review 读数 | 处置 | -|---|---|---| -| ID-52(v1 两审合并) | cross-family **2 blockers + 8 majors + 1 minor**;same-family 另给 M-1..M-7、m-1..m-7,五个 codex-only 项先执行验证 | confirmed set 进入 v1 round 2 | -| ID-58(c1 round 2) | **3 blockers / 8 majors / 6 minors** | round 3;若仍有 blocker 才拆包 | -| ID-62(v1 round-2 review) | **7 closed / 7 partial / 1 not closed**,新增 **5 majors** | round 3;E1 skip-false-green 转 j0 | -| ID-64(c1 round-3 review) | **8 closed / 6 partial / 1 not closed**,新增 **0 blockers / 5 majors / 4 minors** | 判 MERGEABLE;剩余 gate debt 转 c1f | -| ID-73(P5 收官 Codex) | **1 blocker / 10 major / 2 minor**,只读 source-confirmed,确认扰动未在审查中执行 | #1–4 转 r1,#5–13 转 r2;包内执行结果与剩余债务见 §31 | - -这些数字解释了流程为何在 P5 尾声改变:用户要求不再在轮次中做过度验证;以后 package 自门 + quick gate 落地,每阶段末只做一次 Codex adversarial review,发现进入下一阶段首轮(ID-66)。 - -另一个过程事实是 identity rewrite(ID-40):Sep 8 以后 WSL repo-local `rereview ` 污染了 **81** 个被 Windows 重写的提交与 **58** 个 wave-1 提交;本地谱系经 parent/env rewrite 接到 GitHub `ff2994d9`,验证 **59** 个重写提交全部是 `Swung0x48 `,tree/patch 等价。自此每阶段首个 commit 前必须先跑 `git var GIT_COMMITTER_IDENT`。这是过程修复,不计成 P5 实现产量。 - -## 30. P5 Redmi 四臂 A/B(已记录,split 性能仍不可测) - -证据:`~/w7/notes/p5/p5-results/ab-v1.md`、`ab-v1-tables.md`,构建身份见 `ap-v1.md`。设备 Redmi `2f7cbe2e`,会话 **2026-09-16 10:31:20–11:10:05 UTC−04**;runner head **eec0e836**,三份 APK 的源码头是 **joint e61d0012**,不能当成 `37fc4fdb` 的 APK。三份 APK 有独立 application ID;split 与 splitctl 共用同一 split APK。 - -| arm | APK / env | 回答的问题 | -|---|---|---| -| pull | pull APK | 既有 pull 基线 | -| push | push APK | monolith push 边界 | -| split | split APK + `MOBILEGL_TRANSPORT=inproc` | 第二 apply 线程的 barrier / codec / copy 总成本 | -| splitctl | split APK,不设 transport env | split build 的 monolith 对照 | - -reboot-clean、风扇 level 2、同热窗口与定频、四臂交错。每臂三次,取最低平均墙钟时间那次的尾 200 帧;所有 **40 组** 前后 pin check 都为 **P/P**,采样温度 **37.2–39.9 °C**。五个 case × 两后端 × 四臂,**27 组完成 / 13 组失败**,每次失败只重试一次;第五个 iris-BSL 用例一并保留在报告中。 - -所有 **10 组 split** 都在 benchmark 前中止:improved-transparency 双后端为 `DrawElementsInstancedBaseVertex`,rd12 / fabric-sodium / vanilla 1.21.4 / iris-BSL 双后端为 `DrawElements`。其余三次失败是 rd12/DirectVulkan 的 pull、push、splitctl 均复现既有 `scudo::reportMapError`(dev 侧)。因此 **每个 case 的 barrier tax 都未测得**,不能从 splitctl 或启动后的 RSS 推算。 - -关键帧时摘要(单位 ms,p50 / p99;完整逐线程 CPU、repeat、计数器与私有日志见上述报告): - -| case / backend | pull | push | splitctl | split | -|---|---:|---:|---:|---| -| improved-transparency / DirectGLES | 10.535 / 25.415 | 12.106 / 26.973 | 12.235 / 27.131 | 索引 draw 中止 | -| improved-transparency / DirectVulkan | 10.742 / 25.031 | 11.678 / 26.134 | 11.837 / 26.476 | 索引 draw 中止 | -| fabric-sodium / DirectGLES | 8.314 / 9.573 | 8.311 / 9.701 | 8.303 / 9.486 | 索引 draw 中止 | -| vanilla 1.21.4 / DirectVulkan | 1.043 / 2.293 | 1.159 / 2.404 | 1.179 / 2.426 | 索引 draw 中止 | -| rd12 / DirectGLES | 7.999 / 22.089 | 11.105 / 24.865 | 11.261 / 24.771 | 索引 draw 中止 | - -在 push 与 splitctl 都完成的组合里,splitctl 相对 push 的帧 p50 为 **−0.1%…+1.6%**,p99 **−3.0%…+1.4%**。这只描述 split build 的 monolith 臂。举例 improved-transparency/DirectGLES 的逐线程 CPU p50/p99 为 pull **10.447/25.213**、push **12.011/26.775**、splitctl **12.135/26.936 ms**;它不提供 apply 线程成本。P5b 索引 draw 迁移后用新 APK 重跑,直到四个目标 trace 在设备上渲染;性能仍只记录、不阻塞 landing。 - -## 31. P5 收尾实跑的停止点、j0/v1-r3 与 r2 修正 - -`~/w7/p5-final-gate.log` 明确钉在 **37fc4fdb**,不是 §26 的 joint head。首次启动曾被 WSL 会话杀死;重启后的保留日志给出了以下结果,最后一行为 E3(a) 的 skip 拒绝,没有全门成功终止标记: - -| 已执行部分 | 该次实跑结果 | +| 包 | 已落地行为与证据边界 | |---|---| -| 配置/构建 | pull / push / verify / split 都 rc=0 | -| Part 1 纯度与 G1/G5 | include closure 4 probes/0 problems;27,814 symbols,0/0/0/0;.text 10,806,611 B (+0);两条 G5 byte-identical | -| Part 5 | emitter/CSO 185/185,verify controls 4/4;生成器检查完成 | -| Part 3 单元与 monolith | unit 1817×3 / split 2086;Wire 58;pull/push GPU 1128,各零失败;split-monolith 1149 零失败 | -| Part 3 inproc | broad lane 1149 selected,CTest 报 532 non-success(含 abort,不等于 532 wrong-answer);reduced 22 selected 零失败,含两条设计性 skip | -| E1 | 14 selected 全部按 BarrierViolation 变红;逐项私有日志控制通过 | -| E3(a) | 6 selected 中 4 条 pixel assertion 红、2 条 `TheMapLandsInTheArmItsLaneDeclares` 设计性 skip;控制以 `the knob killed the pre-flight, not the entry - 2 selected entries skipped` **失败** | -| Part 2 / Part 4 | **该次执行未到达**;§26 joint retrace 与 §30 旧 APK A/B 是独立证据 | +| d1 | 19 个索引 / 实例 / multi-draw / indirect 槽经 `draw_vbo` 过线;独立包普查 77 个 Minecraft 后端用例 28 passed、49 first blockers,全部越过原 draw 首阻塞(默认 stage 32 MiB) | +| i1 | 七个 image / compute / barrier / copy-image / storage-block 槽;原首阻塞 239 归零;CopyImage 的 client-shadow 镜像被跳过,`GetTexImage` 前端回退仍可能读旧 shadow | +| t2 | 六个 XFB / 曲面细分槽;原首阻塞实测 140(`BeginTransformFeedback` 95 + `PatchParameteri` 43 + `BindTransformFeedback` 2);包内基线 490 / 191 / 390 / 78,原 432 passes 全保留 | +| f1 | 11 个 clear / copy / mip 槽;原 34 个首阻塞归零;bound named-clear 接线,unbound 仍具名拒绝 | +| r1 / r2 | P5 收官 #1–13;r1 定向 10 passed / 0 skipped;client-only push suppression 两个像素控制变红 | +| 具名 blit | `BlitNamedFramebuffer` 经 scoped read/draw binding 发布,退出恢复公开绑定;两后端四个 split 像素 entry 通过 + 15 unit | +| GLES mip storage | server 只验证 applier descriptor 的 Levels/extent;三个 mip 像素控制 3/3;iris-BSL GLES 单例 SSIM 0.997496(默认 stage 32 MiB);registry 只 Find 不 mint(barrier 债);RGB 三通道 CPU fallback 保持 Fatal | +| sync | 五条现有 opcode 接线,wire 只传句柄,native fence 归 apply 线程;原 LOCAL guard 使 trace 的 `FenceSync` 没真正执行,所以 d1 旧普查里没有可扣除的 FenceSync 首阻塞数 | -j0-v3 的 `cc20de37` 加了 fresh JUnit 逐选项检查:skip、未运行、缺失/重复条目都拒绝,E1 每条必须有自己的 Fatal。它正确暴露了 E3(a) 选择器把设计性 body skip 纳入像素控制的问题;不能通过放宽 j0 的 skip 检查修复。x2/x2m 的包内 smoke、OpenRA drop-draw 与 SmallRing 读数也不等于此最终门成功。日志里 broad census 的旧 name/status diff 含分类问题,不作为逐条归因依据;采用 §27 新鲜私有日志的 census。 +包报告:`~/w7/notes/p5b/p5b-results/{d1-codex-v1,i1-v1,t2-codex-v1,f1-v1,r1-codex-v1,r2-v1,blit-codex-v1,mip-codex-v1,sync-codex-v1}.md`。 -v1-r3 已完成并需保留的事实:native EGL bind 在 reduced lane 实测每进程 **1/1/1**、release **0**;真实 EGL 四套件 **33/33、0 skip**,36 个定向扰动因自己的诊断变红;`HasDefinedContent`/coverage 修复使六个过宽 whole-store 拒绝归零。各读数来自 `v1-v3.md` 所列包头/共同测试头,不能替代 `37fc4fdb` 的门。unmapped shadow 控制的边界见 §27 E5。 +### 7.2 主机收尾门与合并普查(`348d22a4`,`~/w7/p5b-final-host-348d22a4/`) -收官审查 `p5-close-codex-review.md` 在 `37fc4fdb` 只读确认 **1 blocker / 10 major / 2 minor**(ID-73),不宣称当场执行了 proposed perturbations。r1 负责 coherent-map server re-entry、framebuffer death 驱动线程、PACK_SWAP_BYTES、RingOverrun 等待;r2 的 `37fc4fdb..d50183cb`(`6fa6a925` + `d50183cb`)完成 #5–13 并执行各自的具名 red-once: - -| 修正 | r2 包内证据与限制 | -|---|---| -| broad debt lane 与 runtime baseline | 普查 exit code/JUnit 记录后继续硬门;全部 runtime skip 则 baseline 失败,metadata 不能使其变绿。仅删掉 exit 捕获会再次阻断控制 | -| E2 库身份/恢复 | frozen split path + SHA256 恢复;真实 CTest 连续运行 baseline/pull/drop,baseline SSIM 1.0、drop 0.000036,758 records;故意换成 pull 库在 replay 前被拒绝 | -| E3(a) 选择 | 精选默认/SmallRing 的两条 pixel case,共 4 条自己的 assertion+disabled-push 私有诊断;恢复旧 broad selection 重现收尾错误,pre-flight skip 仍拒绝 | -| ringwaits | 只有真正等未退休发布记录才计数;lazy reclaim 单元为 0,GL SmallRing 的 1/8 MiB 对照同时证明 wrap 与 wait;完整 transport policy 与 r1 对齐 | -| REQUIRE_GPU / zero-run / stale evidence | split unit 强制 GPU,invalid EGL 从可选 skip 变 required failure;两份 census runner 零执行拒绝为 harness-error,launch 前清空私有日志,旧 Fatal 不可冒充新证据 | -| G5 pin / c1f mutation | 删 production pin-selection 后各自具名红;c1f 改用 ID-67 的 different-tuple case,15/15 RED 后 restored GREEN,旧 case 名证据不沿用 | - -r2 最终包门:split unit **2087 selected**、reduced **22 selected**、push **1128 selected** 均零失败(selected 包含已注明 skip);E1 **14 own reds**、E3(a) **4 own reds**;smoke **16 core + 12 private**;census **432/185/505/27**,零 segfault/harness-error。r2 **没有新跑 pull G1 binary comparison**,只说明修改不影响 pull production code;不能把继承的 0/0/0/0 当它的新测量。日志 `~/w7/p5b-r2-final-gate.log` 的 `UNIT_OK SPLIT_OK CONTROLS_OK SMOKE_OK CENSUS_OK PUSH_OK` 只认证 r2 包头,不认证后续合并头。 - -r2 还更新树外 `~/w7/p5b-c0b-census.{sh,py}`、`~/w7/notes/tools/p6_census_lane.py`、`~/w7/notes/tools/wsl_p5_gate.sh`;git merge 不会把它们自动带到别处。在 r2 包头时仍未关闭的债务包括 r1 四项、无 stamp 的 residual reads、ReadPixels 正确大小但非法 status=3 的接受路径,以及 §27 的 27 条 wrong-answer。完整归属/退休门见 `ROADMAP.md`;实现与证据索引为 `~/w7/notes/p5b/p5b-results/r2-v1.md`。本节只汇总现有证据,没有追加轮次审查或重复全门。 - -## 32. P5b 真实负载迁移与包证据 - -集成基准头 **`348d22a4b9c15cc571af840dedc4b4edc8c53dbb`**(2026-09-16)。此头包含 d1/i1/t2/f1、P5 收官 r1/r2、五槽 sync、具名 blit 与 GLES mip storage 修复。发射表 **A=2 / B=54 / C=15**;B=54 中包含 P5 的五槽,以及本轮 19+7+6+11+1+5 槽。目录仍是 76 条;槽数与 wire opcode 数不是同一个统计量。 - -| 包 / 后续首阻塞 | 已落地行为与证据边界 | -|---|---| -| d1 | 19 个索引/实例/multi-draw/indirect 槽经 draw_vbo 过线。独立包动态 Minecraft 普查实际执行 77 backend cases,28 passed、49 first blockers;全部越过原 draw 首阻塞。该次默认 stage=32 MiB,不能用它直接推算后续合并头或 256 MiB profile 的进步幅度。 | -| i1 | 七个 image/compute/barrier/copy-image/storage-block 槽迁移;包内五类原首阻塞合计 239(138+49+18+30+4)归零。CopyImage 的 client-shadow 镜像被跳过,GetTexImage 前端本地回退仍可能读旧 shadow,不能宣称始终有 Fatal 挡住它。 | -| t2 | 六个 XFB/曲面细分槽迁移;原首阻塞实测 **140**(BeginTransformFeedback 95 + PatchParameteri 43 + BindTransformFeedback 2),不是早期计划的 138。包内同名基线 490 passed / 191 skipped / 390 aborted / 78 failed,原 432 passes 全保留;新增暴露的 Vulkan capture/query 失败详见包表。 | -| f1 | 11 个 clear/copy/mip 槽迁移;原 34 个首阻塞归零。bound named-clear 已接线,unbound named-clear 仍具名拒绝;深度 copy/readback 和 Vulkan named publication 留待最终普查与后续阶段。 | -| r1 / r2 | P5 收官 #1–13 修复已集成。r1 阻止 apply-role 进入 persistent-map producer,FBO death 转 apply mailbox,补 PACK_SWAP_BYTES,保留 r2 staging 等待并补 command retirement 重试。r1 最终定向 **10 passed / 0 skipped**;client-only push suppression 两个自己的像素失败,另一个 metadata skip 不算 red。r2 的库身份/恢复、skip/zero-run/陈旧日志、CI 控制顺序等证据见 §31。 | -| 具名 blit | `BlitNamedFramebuffer` 经 scoped read/draw binding 发布降为现有 bound backend 调用,退出恢复公开绑定,下次普通 verb 重新发布。两后端四个 split runtime 像素 entry 通过;附带 15 unit passes,另四个 monolith 注册项设计性 skip,故包内选择 23 不等于 23 passes。 | -| GLES mip storage | 前端先定义并发布层级,server 只验证 applier descriptor 的 Levels/extent,跳过重复 client-shadow grow。原 RGBA8、新 R11F、新 depth 三个 mip 像素控制 **3 passed / 0 skipped**;iris-BSL GLES 单例 SSIM **0.997496**、无 Fatal、默认 stage 32 MiB。registry 身份解析只 Find,不 mint;仍是 P5b barrier 债。RGB 三通道 CPU fallback 保持 Fatal。 | -| sync | FenceSync / ClientWaitSync / GetSyncStatus / WaitSync / DeleteSync 的五条现有 opcode 接线,wire 只传句柄,native fence 归 apply 线程。原 LOCAL guard 使 trace 的 FenceSync 没真正执行,所以 d1 旧普查不存在可直接扣除的 FenceSync first-blocker 数。真实 wait 返回、64-bit timeout、flush flag、世代与 orphan cleanup 已有包内控制;长 wait 的运输 watchdog 另见 §34。 | - -包报告位于 `~/w7/notes/p5b/p5b-results/{d1-codex-v1,i1-v1,t2-codex-v1,f1-v1,r1-codex-v1,r2-v1,blit-codex-v1,mip-codex-v1,sync-codex-v1}.md`。这些是各包固定二进制上的证据;合并头以以下一次阶段门与普查为准。 - -## 33. P5b 一次主机收尾门与合并普查 - -主机门固定 **`348d22a4`**,证据目录 `~/w7/p5b-final-host-348d22a4/`;原始 `head.txt`、`libraries.sha256`、逐步 `status.tsv`、JUnit 及 `summary-counts.{json,md}` 保留。下表严格区分 selected、passed 和 skipped;当前已结束的表项均无 failed/error,完整回放尚待终止标记,不能据此把全门写成完成。 - -| 车道 | Selected | Passed | Skipped | Failed/error | +| 车道 | Selected | Passed | Skipped | Failed | |---|---:|---:|---:|---:| -| unit-linux | 1817 | 1500 | 317 | 0 | -| unit-push | 1817 | 1782 | 35 | 0 | -| unit-verify | 1817 | 1795 | 22 | 0 | +| unit-linux / unit-push / unit-verify | 1817 × 3 | 1500 / 1782 / 1795 | 317 / 35 / 22 | 0 | | unit-split | 2132 | 2122 | 10 | 0 | -| gpu-linux-monolith | 1148 | 895 | 253 | 0 | -| gpu-push-monolith | 1148 | 953 | 195 | 0 | -| gpu-split-monolith | 1178 | 953 | 225 | 0 | -| split | 107 | 105 | 2 | 0 | -| verify | 950 | 804 | 146 | 0 | -| audit | 10 | 9 | 1 | 0 | +| gpu-linux-monolith / gpu-push-monolith / gpu-split-monolith | 1148 / 1148 / 1178 | 895 / 953 / 953 | 253 / 195 / 225 | 0 | +| split(`integration-split`,inproc) | 107 | 105 | 2 | 0 | +| verify / audit | 950 / 10 | 804 / 9 | 146 / 1 | 0 | -G1:27,814 defined symbols,新增/删除/resize/rename 全为 0;`.text` **10,806,611 B → 10,806,611 B(+0)**。G5 的 P3a 11 函数 / P4a 17 区域及各自 production pin-selection 自测完成,生成器/纯度检查完成;测试注册名比较与 G14 步骤 rc=0。 +G1 27,814 符号 0/0/0/0、`.text` 10,806,611 → 10,806,611;G5 两族 + pin 自测;生成器 / 纯度检查;G2 / G14 rc=0;E1 / E3(a)(精选 4 case)rc=0;E2 OpenRA 2/2、draw-drop SSIM 0.000036 / 758 records、pull-library 拒绝与库 SHA 恢复一致;smoke 16 core + 12 private。retrace-push **79/79**;retrace-verify 在 WSL 重启前完成 4/79,其余 75 条以 SHA-256 钉住的同一 verify 库串行续跑,合并 **79/79**;对账无缺步、无非零退出(`reconciliation-20260916.md`),`complete=true`、exit 0。一次 shell 异常(追加 runner 行触发 `-test-dir: command not found`)保留在原始输出,不冒充测试失败。 -E1 与 E3(a) 的控制步骤 rc=0,各自 selected entry 必须自己的诊断与私有日志;E3(a) 精选四个像素 case,不含旧两条 metadata skip。E2 OpenRA **2 selected / 2 passed**;draw-drop 的单例 SSIM **0.000036**、758 records,pull-library 拒绝与恢复后的库 SHA 保持一致。控制跑器 smoke **16 core + 12 private** 按各自预期通过。 +合并 inproc 普查(`~/w7/p5b-final-census-348d22a4/`):integration lane 显式 32 MiB,**1267 selected = 811 passed / 203 skipped / 62 aborted / 191 failed**;旧 1149 名全部保留、新增 118;旧 432 passes 全保留、零回退;旧 505 abort → 265 passed / 18 skipped / 58 aborted / 164 failed;旧 27 failed → 1 passed / 26 failed。完整 trace 显式 256 MiB(容纳单次 128 MiB 上传,不是默认容量修复):**79 = 72 passed / 6 aborted / 1 failed**(主跑止于 73/79,三轮续跑补齐;`create-indirect` DirectVulkan 在 llvmpipe 上内存膨胀 >60 GiB RSS 被守护杀死,两次复现,记 failed)。7 条未过项首阻塞:`rd12` DirectGLES `Fatal{InitialBytesNotCarried,"resource_respecify"}`、DirectVulkan `Fatal{BarrierTimeout,"Present"}`;`iris-photon`、`iris-derivative`、`create-indirect` 三个 DirectGLES `Fatal{UnmigratedEmulation,"texture-remint-pull"}`;`iris-bsl-esc-menu-854` DirectGLES `InitialBytesNotCarried`;`create-indirect` DirectVulkan 内存守护。通过项含 `improved-transparency-minecraft-26.3` DirectGLES SSIM 1.0 / DirectVulkan 0.999914、`iris-iterationrp` DirectVulkan 0.995833、`iris-bsl-esc-menu-854` DirectVulkan 0.998402。逐 trace 见 `joint-codex-v1.md` 的 census 块与 `identity.json` / `counts.json` / `trace-transitions.json`。 -**执行异常独立记账:**为归档 E1/E3 私有日志而在运行中追加 runner 行,Bash 的读取偏移触发过一次 `-test-dir: command not found`;外层未设置 set-e。该 shell 异常保留在原始会话输出,不冒充测试失败,也不能省略。verify / audit 自己的 JUnit 均完整且步骤 rc=0,后续 E2 正常;`negative-private-logs` 已提前手动归档。最终须用全部必需步骤状态、JUnit 数量及回放计数确认没有缺步,而不是仅凭最后一个 shell exit 宣称通过。 +### 7.3 唯一收官审查与定向修复 -主机 push / verify 的各 79 replay 与最终 complete/exit **已收口(2026-09-16)**:retrace-push **79/79**(原始会话内完成,rc=0);retrace-verify 在 WSL 重启前完成 4/79,其余 75 条以 `verify-recovery-r1/` 里 SHA-256 钉住的同一 verify 库串行续跑(`p5b_codex_resume_verify.py --run`,exit 0),合并 **79/79**(`retrace-verify-combined/summary.json`)。逐步对账无缺失步骤、无非零退出(`reconciliation-20260916.md`),`exit-code.txt=0`、`complete=true`,`summary-counts.{json,md}` 已按最终状态重算。 +审查 `p5b-close-codex-review.md`(`348d22a4`,diff base `37fc4fdb`,只读):**0 blocker / 2 major / 1 minor**。 -合并 inproc 普查由独立 frozen runner 在 `~/w7/p5b-final-census-348d22a4/` 记录:integration lane 显式 **32 MiB**,完整 trace 显式 **256 MiB**。后者容纳目标负载的单次 128 MiB 上传;这不是默认容量修复。integration lane 已执行 **1267 selected = 811 passed + 203 skipped + 62 aborted + 191 failed**。旧 1149 名全部保留,新增 118、删除 0;旧 **432 passes 全保留、零回退**。旧 505 abort → 265 passed / 18 skipped / 58 aborted / 164 failed;旧 27 failed → 1 passed / 26 failed。后续错误被执行到不等于这些路径已正确,新增测试也不计入旧名回归判断。完整 trace 的 79 项**已收口(2026-09-16)**:**72 passed / 6 aborted / 1 failed**。主跑在 WSL 重启时止于 73/79,续跑三轮补齐——resume1 命中 `create-indirect` DirectVulkan 的内存膨胀(进程组 >60 GiB RSS,守护 SIGKILL);resume2 重试同例再次被内存守护杀死(本条记为 failed,证据在案),其余 5 条因内存水位未恢复被守护推迟;resume3 在干净实例上跑完(4 passed / 1 aborted)。7 条未过项及其首阻塞:`minecraft-1.21.4-rd12-odinlite-in-world` DirectGLES `Fatal{InitialBytesNotCarried,"resource_respecify"}` 与 DirectVulkan `Fatal{BarrierTimeout,"Present"}`;`iris-photon-v1.3b`、`iris-derivative-main-d24.4.14`、`minecraft-1.21.1-neoforge-create-indirect` 三个 DirectGLES 均为 `Fatal{UnmigratedEmulation,"texture-remint-pull"}`;`minecraft-1.21.4-fabric-iris-bsl-esc-menu-854` DirectGLES `Fatal{InitialBytesNotCarried,"resource_respecify"}`;`create-indirect` DirectVulkan 为内存守护杀死(llvmpipe/lvp 上的真实膨胀,两次复现)。通过项含 `improved-transparency-minecraft-26.3` DirectGLES SSIM 1.0 / DirectVulkan 0.999914、`iris-iterationrp` DirectVulkan 0.995833、`iris-bsl-esc-menu-854` DirectVulkan 0.998402。逐 trace first blocker/SSIM 以 `joint-codex-v1.md` 的 census 块及 `identity.json`、`counts.json`、`trace-transitions.json` 为准。 - -## 34. P5b 唯一收官审查与定向修复 - -阶段审查 `~/w7/notes/p5b/p5b-results/p5b-close-codex-review.md` 固定 `348d22a4`、diff base `37fc4fdb`,只读结论 **0 blocker / 2 major / 1 minor**。没有宣称现场重现 driver 越界、长时间 GPU wait 或设备结果;P5 的 #1–13 修复在此头已存在,没有追加发现。 - -| 项 | 问题 | 当前处置 | +| 项 | 问题 | 处置 | |---|---|---| -| Major 1 | user-index span 虽在 segment 范围内,其 Size 仍可能短于 Count × IndexSize,sink 会按独立 count 消费 | 集成 `a021e3cc`(包 `0b67568e`):encoder/decoder/sink 共用 shape/extent gate,单 range、index width 1/2/4、Start=0,Uint64(Count) × IndexSize ≤ Size;三端短 span 均拒绝、segment 末尾 exact-fit 通过,**9 selected / 9 passed / 0 skipped** | -| Major 2 | ClientWaitSync 保留 64-bit 原 timeout,但 applied/reply 等待一律 30 秒,合法长 wait 会被 BarrierTimeout 打断 | 集成 `82683d4a`(包 `6388035f`):applied/reply 预算为 ceil(timeout ns / 1,000,000) + 30,000 ms,以有限 chunk 避开溢出和 forever sentinel;普通容量等待/FenceWaitServer 仍为 30 秒,shutdown 可唤醒 | -| Minor 3 | ReadPixels exact-size reply 的未知 status 仍可穿过 production helper | 集成 `82683d4a`(包 `6388035f`):tight 与 bounce 实际 return path 拒绝 status=3,报 ReplyStatusInvalid;既有 ERROR/DECLINED 诊断保留 | +| Major 1 | user-index span 在段内但 Size 可能短于 Count × IndexSize | `a021e3cc`:encoder / decoder / sink 共用 shape/extent gate(单 range、宽度 1/2/4、`Uint64(Count) × IndexSize ≤ Size`),三端短 span 均拒绝、段末 exact-fit 通过;定向 9/9 | +| Major 2 | `ClientWaitSync` 保留 64-bit timeout 但 applied/reply 等待一律 30 s | `82683d4a`:预算 = ceil(timeout ns / 1e6) + 30,000 ms,有限 chunk;普通容量等待 / `FenceWaitServer` 仍 30 s,shutdown 可唤醒 | +| Minor 3 | ReadPixels exact-size reply 的未知 status 穿过 production helper | `82683d4a`:tight 与 bounce 路径拒绝 status=3,`Fatal{ReplyStatusInvalid}` | -`6388035f` 的定向 RemoteClientTest **7 selected / 7 passed / 0 skipped / 0 failed**(80 ms):真实 EmitAndWait 的 0 ns、1 ns、60 s、UINT64_MAX 预算,FenceWaitServer、shutdown,以及 tight/bounce 的未知状态控制。报告 `~/w7/notes/p5b/p5b-results/wait-codex-v1.md`;该结果属于后续修复包;index span 的报告为 `indexspan-codex-v1.md`。最终代码源头 **`82683d4a83b5c8f4f7c375a7a92ebee9a1854646`** 包含三项修复。**合并后定向确认已汇总(2026-09-16)**:`7cb29d46` 之前最后一次 quickgate 里 split 单元车道仅有的 2 个失败(`PipeWireCodecTest.UserIndexSpan*`)是测试日志捕获缺陷(库按进程截断日志、fork 子进程增量读读空),由 `7cb29d46` 修复;在 `c77831e0` 的 build-split 上 `PipeWireCodecTest.UserIndexSpan*` **3/3 通过**,三项修复在最终头上均有绿色定向证据。 +定向 `RemoteClientTest` 7/7(0 ns、1 ns、60 s、UINT64_MAX 预算,`FenceWaitServer`、shutdown、未知状态)。合并后 quickgate 里 split 单元车道仅有的 2 个失败(`PipeWireCodecTest.UserIndexSpan*`)是测试日志捕获缺陷(库按进程截断日志,fork 子进程增量读读空),`7cb29d46` 修复后 3/3。本阶段不再做第二轮全门或审查;原始全门源头仍为 `348d22a4`,三项修复只以定向证据补齐。 -本阶段不再做第二轮全门或收官审查。原始全门的源头仍为 `348d22a4`,后续三项只以修复提交上的定向证据补齐;最终 APK/设备源头另行明确,不能倒改历史 gate 身份。 +### 7.4 Redmi 出口与四臂 A/B(`82683d4a`,APK `p5bcodex2`) -## 35. P5b Redmi 出口与四臂 A/B +Redmi `2f7cbe2e`,证据根 `MobileGL/.trace-work/p5b-redmi/p5bcodex2/2f7cbe2e/`;全部组合显式 `MOBILEGL_IPC_STAGE_MB=256`。**钉频口径变更**:2026-09-11 的厂商 GPU 上限(1050 MHz)已消失,本次 deterministic pin 为 **1100 MHz**;与 1050 MHz 时代的活动不可比钟频,只有同场四臂配对可比。 -最终代码源头 **`82683d4a83b5c8f4f7c375a7a92ebee9a1854646`**,APK 相位 `p5bcodex2`(pull / push / split 三个签名 APK,provenance 与 proof 在 `~/w7/notes/p5b/apk/p5bcodex2/`),split APK 分别运行 monolith control(splitctl)与 inproc(split)。设备 Redmi M332BF(SM8750 / Adreno 830v2,串号 `2f7cbe2e`),证据根 `MobileGL/.trace-work/p5b-redmi/p5bcodex2/2f7cbe2e/`。全部组合显式 **`MOBILEGL_IPC_STAGE_MB=256`**,默认 32 MiB 保持不变(容量决定见 §33 与 ROADMAP 开放问题 11)。**设备钉频口径变更**:2026-09-11 记录的厂商 GPU 频率上限(thermal_pwrlevel 锁 1、1050 MHz)在本机已消失,2026-09-16 实测 deterministic pin 为 **1100 MHz**(`pin_device.sh` 已按实测更新);本节数字与 1050 MHz 时代钉频的活动不可直接比钟频,仅同场四臂配对可比。 +**正确性 8/8**:`improved-transparency-minecraft-26.3`、`minecraft-1.21.4-in-world`(GLES SSIM 0.999995)、`minecraft-1.21.4-fabric-sodium-in-world`、`minecraft-1.21.4-fabric-iris-bsl-in-world` × 双后端,inproc split 臂,逐组钉频取证 + PNG / SSIM 阈值 + inproc + apply 线程 + 正 wire 记录三证,skip 即失败。**四条 A/B trace 首次在 Redmi 上以独立 apply 线程渲染——P5b 出口判据达成。** -**正确性矩阵 8/8**(4 条目标 trace × 双后端,inproc split 臂,逐组钉频前后取证、PNG/target_call/SSIM 阈值、inproc + apply 线程 + 正 wire 记录三证,skip 即失败):`improved-transparency-minecraft-26.3`、`minecraft-1.21.4-in-world`(GLES SSIM 0.999995)、`minecraft-1.21.4-fabric-sodium-in-world`、`minecraft-1.21.4-fabric-iris-bsl-in-world` 双后端全部通过。**这是四条 A/B trace 首次在 Redmi 上以 inproc 独立 apply 线程渲染——P5b 的出口判据(BRIEF-P5B §5)达成。** +**四臂 A/B(barrier tax 首测)**:32 组中 24 组带 200 帧尾验证全绿;三次重复取 wall-time 均值最低者、尾 200 帧(`ab-tables.md`)。逐线程 CPU p50: -**四臂性能 A/B(barrier tax 首次实测)**:32 组中 **24 组带 200 帧尾验证全绿**(improved-transparency / in-world / sodium 三例的全部组);配对口径为三次重复取 wall-time 均值最低者、尾 200 帧(`ab-tables.md`)。逐线程 CPU p50 的 push−pull 与 **barrier tax(split−push)**: - -| trace | 后端 | push−pull CPU p50 | barrier tax CPU p50 | barrier tax 帧 p50 | +| trace | 后端 | push−pull | **barrier tax(split−push)** | barrier tax 帧 p50 | |---|---|---:|---:|---:| | improved-transparency-26.3 | DirectGLES | +15.6% | **+10.3%** | +0.1% | | improved-transparency-26.3 | DirectVulkan | +8.8% | **+13.1%** | +13.3% | | minecraft-1.21.4-in-world | DirectGLES | +21.4% | **+18.2%** | +0.2% | | minecraft-1.21.4-in-world | DirectVulkan | +11.8% | **+17.9%** | +19.7% | -| minecraft-1.21.4-fabric-sodium | DirectGLES | +10.9% | **+5.9%** | -0.1% | +| minecraft-1.21.4-fabric-sodium | DirectGLES | +10.9% | **+5.9%** | −0.1% | | minecraft-1.21.4-fabric-sodium | DirectVulkan | +7.6% | **+8.3%** | +4.3% | -splitctl−push 全部在 ±1.6% 内(monolith control 与 push 同源等价),证明上述增量来自 inproc 传输与 barrier 而非 APK/构建差。p99 方向同向放大(sodium DirectVulkan 帧 p99 +107.7% 为离群尾帧,原始序列在 `ab-tables.md`)。inproc 臂 peak RSS 389 MiB–2.5 GiB、全角色映射 560.5 MiB(256 MiB × 角色视图是虚拟映射容量,不是 RSS 增量)。 +splitctl−push 全部在 ±1.6% 内(增量来自 inproc 传输与 barrier,不是 APK / 构建差);p99 同向放大(sodium DirectVulkan 帧 p99 +107.7% 为离群尾帧)。inproc 臂 peak RSS 389 MiB – 2.5 GiB,全角色映射 560.5 MiB(256 MiB × 角色视图是虚拟映射容量,不是 RSS 增量)。 -**fixture 受限的 8 组**:`iris-bsl-in-world` 全部四臂 × 双后端验证失败,原因是 fixture 本身只有 **123 个 benchmark 帧**,低于跑器的 200 帧尾规则——pull 臂同样失败,是 fixture 上限不是回归。保留的 123 帧完整序列按同法(三次取均值最低)单独折算成带标注的补充表 `ab-tables-bsl-123frame-supplement.md`(全程 123 帧、无尾裁剪;p99 在各臂均被着色器编译主导,与 P2 基线记录一致):barrier tax CPU p50 DirectGLES **+7.1%**、DirectVulkan **+9.1%**,不混入上表。 - -性能仍按 2026-09-08 口径只记录、不作 landing 阻塞门。设备出口完成,**P5b 收官**;P6 spawn transport 自此开始(其 inproc 依赖替换清单见 ROADMAP 的 P5/P5b 债务表)。 +**fixture 受限的 8 组**:`iris-bsl-in-world` 只有 123 个 benchmark 帧,低于 200 帧尾规则,四臂 × 双后端验证全部失败(pull 同失败,是 fixture 上限不是回归);123 帧完整序列按同法折算成补充表 `ab-tables-bsl-123frame-supplement.md`:barrier tax CPU p50 DirectGLES **+7.1%**、DirectVulkan **+9.1%**,不混入上表。性能仍只记录、不作阻塞门。 diff --git a/docs/Disaggregated/README.md b/docs/Disaggregated/README.md index dbd967fc..687157bc 100644 --- a/docs/Disaggregated/README.md +++ b/docs/Disaggregated/README.md @@ -1,14 +1,14 @@ # MGPipe:MobileGL 前后端拆分 -> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5 已落地,P5b 收尾中**。P5b 已合入索引/实例/multi-draw、image/compute/barrier、XFB/曲面细分、clear/copy/mip、具名 framebuffer blit 和五个 sync 槽;`348d22a4` 的发射表为 A=2 / B=54 / C=15。主机阶段门、合并普查与 Redmi 出口分别记账:主机已完成的 split lane 为 **107 selected = 105 passed + 2 skipped**,最终回放和设备结果尚待汇总;不能据此宣布收官。出口仍是四个 A/B trace 在 Redmi 上 `inproc` 渲染,之后进入 P6 spawn。当前实现保留具名 barrier-pulled 身份/状态依赖,尚不等于跨进程可运行。实测与剩余债务见 `MEASUREMENTS.md` §32–35、`ROADMAP.md`。 +> 状态:**P0–P5b 已收官**(2026-09-16,代码头 `82683d4a`)。目标负载(四条 Minecraft A/B trace)已在 Redmi Adreno 830 上以 `inproc`(独立 apply 线程)双后端渲染,barrier tax 首次实测。**下一个是 P6 spawn transport**。当前头、逐门数字与开放项见 [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)。 > -> 性能纪律(2026-09-08 起):逐线程 CPU 与 tracker 绝对 ns **对着 pull 臂基线记录**,不再作阻塞门(push 比 pull 多约 10% 逐线程 CPU 已被接受;该读数出自 -O0 APK,Release 基准线见 `MEASUREMENTS.md` §20),专门的优化阶段排在路线图推完之后。 +> 性能纪律(2026-09-08 起):逐线程 CPU 与 tracker 绝对 ns **对着 pull 臂基线记录**,不作阻塞门;专门的优化阶段排在路线图推完之后。 ## 是什么 -MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`:Espryt = DirectGLES、Magma = DirectVulkan)之间的一份**显式接口**:gallium 形状、句柄寻址、只推不拉。它取代今天后端每 draw 直接读 `MG_State::pGLContext` 的做法,让后端拥有自己的状态机,并在此之上把前后端拆到**两个进程**。 +MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend`:Espryt = DirectGLES、Magma = DirectVulkan)之间的一份**显式接口**:gallium 形状、句柄寻址、只推不拉。它取代后端每 draw 直接读 `MG_State::pGLContext` 的做法,让后端拥有自己的状态机,并在此之上把前后端拆到**两个线程**(`inproc`,已达成)与**两个进程**(`spawn`,P6)。 -接口本身是可独立交付的产物:即使 IPC 永不上线,`inproc`(同进程第二个 apply 线程)就是 monolith 的渲染线程。 +接口本身是可独立交付的产物:即使 IPC 永不上线,`inproc` 就是 monolith 的渲染线程。 ## 架构(一段) @@ -16,68 +16,71 @@ MGPipe 是 MobileGL 前端(`MG_State` + `MG_Impl`)与后端(`MG_Backend` 应用 GL 调用 → MG_Impl(GL 语义、错误、shadow) → MG_Impl/Pipe/Tracker:在每条 verb 之前 validate,把变化推成 MGPipe 调用 - → MGPipeScreen / MGPipeContext(两张函数指针表,76 条调用(P5b 契约头),单一真相源 PipeCalls.def) - monolith:直调 backend 函数 split:发射器写 SEG_CMD ring → server applier + → MGPipeScreen / MGPipeContext(两张函数指针表,76 条调用,单一真相源 PipeCalls.def) + monolith:直调 backend 函数 split:发射器写 SEG_CMD ring → server applier(apply 线程) → server 对象表(按 {slot, gen} 句柄索引的数组)+ PipeInputs(后端被推送的状态块) → MG_Backend(Espryt / Magma),两个后端的 ring / pool / memo / lowering pass 原样不动 ← MGPipeCallbacks(10 个具名反向回调 + 1 个正向终止符) ``` -三种构建/运行形态共用**同一份 backend 实现**:`monolith`(默认,接口在进程内直调)、`inproc`(同进程两个线程,CI 形态与渲染线程交付物)、`spawn`(`fork`+`execve` 出 server 进程,SPSC 共享内存 ring + FlatBuffers 控制面)。 +三种形态共用**同一份 backend 实现**:`monolith`(默认,进程内直调)、`inproc`(同进程两个线程,CI 形态与渲染线程交付物;P5 起 lockstep verb barrier,P5b 起 71 个后端槽里 54 个发射、15 个具名拒绝)、`spawn`(P6:`fork`+`execve` 出 server 进程,SPSC 共享内存 ring + FlatBuffers 控制面)。 -## 运行 split lane +## 构建与运行 -本地门使用四个 flavour:`build-linux`(pull)、`build-push`(monolith push)、`build-verify`(影子比对)与 `build-split`(disaggregated + inproc)。split 运行时显式设置: +四个 flavour:`build-linux`(pull,默认)、`build-push`(`-DMOBILEGL_PIPE_PUSH=ON`)、`build-verify`(`+MOBILEGL_PIPE_VERIFY` 影子比对,`MOBILEGL_ITEST_REQUIRE_GPU=1`)、`build-split`(`+MOBILEGL_BUILD_DISAGGREGATED +MOBILEGL_BUILD_DISAGGREGATED_INPROC`)。split 运行时必须显式设置传输: ```text -MOBILEGL_TRANSPORT=inproc ctest --test-dir build-split -L integration-split --output-on-failure -MOBILEGL_TRANSPORT=inproc ctest --test-dir build-split -L integration-gpu --output-on-failure +MOBILEGL_TRANSPORT=inproc MOBILEGL_ITEST_REQUIRE_GPU=1 ctest --test-dir build-split -L integration-split --output-on-failure +MOBILEGL_TRANSPORT=inproc ctest --test-dir build-split -L integration-gpu --output-on-failure # 普查车道,只记录 ``` -`integration-gpu` 在 inproc 下仍是带已知 abort/wrong-answer 的普查车道;它的失败不会免除缩减路径和阴性控制的硬门。split 场景的具名入口包含 `DirectGLES.Split.*` 与 `DirectVulkan.Split.*`;每条 entry 都带独立的 `MOBILEGL_LOG_FILE_PATH`,日志不共享,阴性控制也只读被选 entry 的私有文件。常用运行时旋钮: +`integration-split`(`DirectGLES.Split.*` / `DirectVulkan.Split.*`)是硬门;`integration-gpu` 在 inproc 下是带已知 abort / wrong-answer 的普查车道。每条 split entry 带独立 `MOBILEGL_LOG_FILE_PATH`,阴性控制(`scripts/ci/split_negative_controls.sh`)只读被选 entry 的私有日志。常用旋钮(全表见 `ARCHITECTURE.md` 附 A): | 变量 | 默认 | 用途 | |---|---:|---| -| `MOBILEGL_IPC_STAGE_MB` | `32` | 单次 staging 容量;P5b 真实负载普查与 Redmi 四臂显式使用 `256`,默认值不变 | +| `MOBILEGL_IPC_STAGE_MB` | `32` | `SEG_STAGE` 容量;目标负载 profile 显式 `256`(单次 128 MiB 上传),默认不改 | | `MOBILEGL_IPC_VERB_BARRIER` | `1` | 每个 verb 等 `appliedSeq == emitSeq`;`0` 只作 E1 阴性控制 | | `MOBILEGL_IPC_AUDIT` | `0` | apply 返回后以 `0xDD` 填退休 staging,查跨返回持针 | -| `MOBILEGL_IPC_STRICT_ERRORS` | `0` | 把 BARRIER-PULLED residual input 提升为具名 Fatal | -| `MOBILEGL_IPC_ADOPT_TIER` | `2` | P5 split 使用 emulated persistent-map 路径;接受 `auto/0/1/2` | -| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | `64` | persistent-map 保守块推送粒度;`0` 是 E3(a) 阴性控制 | +| `MOBILEGL_IPC_STRICT_ERRORS` | `0` | BARRIER-PULLED residual input 提升为具名 Fatal | +| `MOBILEGL_IPC_ADOPT_TIER` | `2` | split 使用 emulated persistent-map 路径;`auto/0/1/2` | +| `MOBILEGL_IPC_PERSISTENT_BLOCK_KB` | `64` | persistent-map 块推送粒度;`0` 是 E3(a) 阴性控制 | -P5b 的真实负载 profile 显式设置 `MOBILEGL_IPC_STAGE_MB=256`,用于容纳目标 trace 的单次 128 MiB 上传。它只增加容量,不提供分块传输;默认 32 MiB 对超大单次 blob 的拒绝仍是已知边界。设备 pull、push、splitctl、split 四臂使用同一 profile,profile 与源头、APK/library 身份一起记录。 - -Android 有三份 APK flavour:pull、push 与 split-inproc;构建映射分别由 Gradle properties `mobilegl.pipePush`、`mobilegl.buildDisaggregated`、`mobilegl.buildDisaggregatedInproc` 驱动。split APK 仍需运行环境 `MOBILEGL_TRANSPORT=inproc`;不设置时是 split build 的 monolith control arm。 +Android 三份 APK flavour:pull、push、split(Gradle 属性 `mobilegl.pipePush`、`mobilegl.buildDisaggregated`、`mobilegl.buildDisaggregatedInproc`;`~/w7/notes/tools/wsl_build_p5_apks.sh`)。split APK 需运行环境 `MOBILEGL_TRANSPORT=inproc`,不设置时是 split build 的 monolith control arm(splitctl)。设备 A/B 只用 Redmi `2f7cbe2e`,定频协议见 `devices/pin-verification-2026-09-07.md`。 ## 文件地图 | 文件 | 内容 | |---|---| -| `ARCHITECTURE.md` | 已定稿的设计与架构:句柄与世代、调用目录、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程/EGL/平台、构建与纯度门、验证策略 | -| `ROADMAP.md` | P0…P13 阶段表、两条跑道、GO/NO-GO 清单、再基线检查点、仍然开放的问题 | -| `MEASUREMENTS.md` | 逐阶段实测:P0(spike A/B、双设备边界计数器基线、桌面数据点、语料事实)、P1(verify harness 门)、P2(五部分门、两机配对 A/B、DriverBench T1/T2、计数器)、P3a(门、接缝缺陷、Track H 普查、两机 A/B)、P4a(门、契约七次修正与两轮终审修复、缝类审计、三臂设备 A/B、DriverBench T1/T2/T3)、P5(分头门证据、R-10、红米四臂 A/B、收官失败与 r2 修正)、P5b(迁移、一次主机门、普查、收官审查与设备出口)及复现命令 | +| `CURRENT_STAGE_PROGRESS.md` | 当前头实测、P5b 落地内容、开放项、下一步、证据位置、仍在生效的裁定;随每次落地更新 | +| `ARCHITECTURE.md` | 已定稿的设计:句柄与世代、调用目录与生成器、记录约定、tracker、纹理路径、shader 制品、反向通道、后端改造、传输、persistent map 分档、进程 / EGL / 平台、构建与门、P5 / P5b 落地形状、开关表 | +| `ROADMAP.md` | 纪律、两条跑道、P0…P13 阶段表、里程碑、债务表、开放问题 | +| `MEASUREMENTS.md` | 逐阶段实测:P0 spike 与基线、P1 verify、P2 门与 DriverBench、P3a / P4a 门与设备 A/B、P5 全门 / E1–E6 / Redmi 四臂、P5b 主机门 / 普查 / 审查 / Redmi 出口 | +| `devices/pin-verification-2026-09-07.md` | 设备定频档案与核验(Redmi 当前口径;小米 / Oppo 历史核验) | -代码地图(P0 已落地的部分): +代码地图: | 路径 | 作用 | |---|---| -| `MobileGL/MG_Pipe/` | `PipeCalls.def`(目录)、`PipeFields.def`(比对器字段表)、`Coverage.def`(读点覆盖)、`MGPipeTypes.h`(payload POD)、`MGPipeHandles.h`、`MGPipeHostSpan.h`、`MGPipeCallbacks.h`、`MGPipe.h`、`generated/*.inc`(G1–G7 产物,提交进树) | -| `scripts/gen_pipe.py` | 七个生成器 G1–G7;`gen_pipe_dirty_surface.py` 前端 mutator 面扫描;`gen_protocol.py` FlatBuffers 头再生成;`check_doc_citations.py` 本目录 `file:line` lint | -| `MobileGL/MG_Remote/` | `Protocol/protocol.fbs`(控制面 schema)、`Transport/`(`Ring`、`Doorbell`、`ShmSegment`、`FdPassing`、`Framing`、`InProcessTransport`、`ITransport`);仅 `MOBILEGL_BUILD_DISAGGREGATED=ON` 编译 | -| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | 边界计数器(字节 / 动态 accessor 调用 / 六个 memo 门 / 上传形状),`MOBILEGL_PIPE_STATS=1` 开启 | -| `MobileGL/Config.h`、`MobileGL/ConfigLoader.cpp` | `MOBILEGL_PIPE_*` 八个开关 | -| `tools/spikes/server_stub`、`android-plugin/app/src/trace/cpp/spawn_spike.cpp` | spike A:Android 上以 `lib*.so` 打包并从应用进程 exec 第二个原生可执行文件 | -| `tools/spikes/extmem_probe/` | spike B:跨进程外部内存分档探针 | -| `MobileGL/MG_Test/Pipe/`、`MG_Test/Wire/`、`MG_Test/Util/PipeStatsTest.cpp` | 目录算术、wire 层五个套件、计数器测试 | +| `MobileGL/MG_Pipe/` | `PipeCalls.def`(目录)、`PipeFields.def`、`Coverage.def`、`FieldOwnership.def`、`FillPoints.def`、`DirtySurface.def`;`MGPipeTypes.h`(payload POD)、`MGPipeValueTypes.h`、`MGPipeHandles.h`、`MGPipeHostSpan.h`、`MGPipeCallbacks.h`、`MGPipeRenderStateSpans`、`PipeApply`、`PipeRoute`;`generated/*.inc`(G1–G8 产物,提交进树) | +| `MobileGL/MG_Impl/Pipe/` | `Tracker.h`、`PipeFill`、`SlotAllocator`、`CsoCache`、`CompositeResolver`、`ResourceTracker`、`SetHashSuppressor`、各族 `*Emit.h` | +| `MobileGL/MG_Backend/MGPipe/` | `PipeInputs.{h,cpp}`(后端被推送的状态块,G5 / G8 产物的 include 点) | +| `MobileGL/MG_Remote/` | 仅 `MOBILEGL_BUILD_DISAGGREGATED=ON`:`Protocol/`(`protocol.fbs`)、`Transport/`(`Ring`、`Doorbell`、`ShmSegment`、`FdPassing`、`Framing`、`InProcessTransport`、`ReplySlot`、`EventRing`)、`Wire/PipeWireCodec`(记录编解码、`WireVerbSink`)、`Client/`(`BackendObject_Remote`、`ClientSession`、`EmitTables`、`CapsMirror`、`PersistentMapTracker`)、`Server/`(`PipeApplier` + `ServerVerbSink`、`ServerLoop`、`ServerSession`、`StagedShadow`);`CONTRACT-P5.md`、`CONTRACT-P5B.md` | +| `scripts/` | `gen_pipe.py`(G1–G7)、`gen_pipe_field_ownership.py`(G8)、`gen_pipe_dirty_surface.py`、`gen_protocol.py`、`symbol_report.py`(G1)、`check_include_closure.py`、`check_doc_citations.py`(本目录 `file:line` lint)、`p3a_/p4a_untouched_regions.sh`(G5)、三个阴性对照脚本、`ci/`(split 阴性控制、普查 JUnit、smoke) | +| `MobileGL/MG_Util/Metrics/PipeStats.{h,cpp}` | 边界计数器,`MOBILEGL_PIPE_STATS=1` 开启 | +| `MobileGL/Config.h`、`MobileGL/ConfigLoader.cpp` | `MOBILEGL_PIPE_*`、`MOBILEGL_TRANSPORT`、`MOBILEGL_IPC_*` | +| `MobileGL/MG_Test/Pipe/`、`MG_Test/Wire/`、`MG_IntegrationTest/Harness/Pipe*Peek` | 目录 / tracker / emitter 单元、wire 层与 remote client/server 单元、白盒断言 | +| `tools/spikes/`、`android-plugin/app/src/trace/cpp/spawn_spike.cpp` | spike A(Android exec 第二个原生可执行文件)、spike B(跨进程外部内存分档) | ## 术语 -- **client / server**:前端进程 / 后端进程;monolith 下是同一进程的两个角色。 +- **client / server**:前端进程 / 后端进程;monolith 下是同一进程的两个角色,inproc 下是两个线程。 - **verb**:会让 server 做事的命令(draw、dispatch、clear、blit、readback、XFB 跨度、query、纹理操作)。推送只发生在 verb 之前的 validate 时刻。 +- **class A / B / C**:后端槽的三类——A 从 caps mirror 本地回答,B 发射记录,C 具名拒绝(`Fatal{UnmigratedVerb}`),永不回落到 monolith applier。 - **CSO**:常量状态对象(render state、vertex elements、sampler、sampler view、shader),client 侧内容寻址,server 侧按句柄缓存。 - **Track V / Track H**:值类读点的迁移(整块 POD 过线)/ 对象类读点的迁移(`SharedPtr<前端对象>` → 句柄)。 - **`MGGen`**:server 私有的"我重铸了驱动对象"纪元,永不过线;与句柄里的 client 世代严格分开。 +- **G1 / G2 / G5 / G14**:pull 符号恒等 / pull==push 测试名 / 保护区字节一致 / 测试名只增不删。**barrier tax**:split − push 的逐线程 CPU 增量(inproc 传输 + verb barrier 的代价)。 ## 历史 -本目录此前是一份 328 KB 的实施计划(`PLAN.md`)加 135 KB 的设计竞赛与三视角对抗性评审记录(`REVIEW.md`)。设计已定稿,本次改写只保留设计与架构本身;评审记录、早期草案与修订史留在 git 历史里(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`;`git show 87ee17c6:docs/Disaggregated/REVIEW.md` 可取回评审记录全文)。更早的一条已放弃分支 `Feat/CS-Delta-IPC` 的逐文件可复用判定见 `8349babe` 版 `PLAN.md` §17。 +本目录此前是一份 328 KB 的实施计划(`PLAN.md`)加 135 KB 的设计竞赛与评审记录(`REVIEW.md`);设计定稿后只保留设计与架构本身,评审记录与早期草案留在 git 历史(`8b31de2f`、`1794ac94`、`8349babe`、`87ee17c6`)。P5b 收官后(`ea34bccb` 及之前)各文档的完整历史叙述、-O0 设备表与逐轮审查记录同样以 git 历史为准;集成者的逐条裁定长文(ID-1..75)在 `ef35ea0c` 之前版本的 `CURRENT_STAGE_PROGRESS.md`。 diff --git a/docs/Disaggregated/ROADMAP.md b/docs/Disaggregated/ROADMAP.md index 316820e0..f4b11408 100644 --- a/docs/Disaggregated/ROADMAP.md +++ b/docs/Disaggregated/ROADMAP.md @@ -1,143 +1,89 @@ # MGPipe 路线图 -> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b 已收官**(2026-09-16,头 `c77831e0`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**(随每次落地更新)。P5b 出口:主机全门 `348d22a4` complete=true、合并普查车道零回退 + 79 trace 72/6/1、Redmi 正确性 8/8 + 四臂 A/B barrier tax 首测(`MEASUREMENTS.md` §33–§35)。**下一个是 P6 spawn transport**。设计见 `ARCHITECTURE.md`,分头证据见 `MEASUREMENTS.md` §26–35。 +> 状态:**P0、P0.5、P1、P2、P3a、P4a、P5、P5b 已收官**(2026-09-16,代码头 `82683d4a`)。当前头、逐门数字、开放项与下一步见 **[`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)**;设计见 `ARCHITECTURE.md`;逐阶段实测见 `MEASUREMENTS.md`。**下一个是 P6 spawn transport**。 ## 通用纪律(每个 commit) -默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门);**每个门必须能因它存在的理由变红**;Windows 机器不是正确性门;设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议;每阶段出口跑一次五部分门;每阶段性能判据是**逐线程 CPU 时间**;**拆分不借机顺手修 `dev` 的 bug**——句柄化过程中发现的 `dev` 侧缺陷记录成独立条目、独立 PR,不混进本分支(下文课题 15、17 各是一例)。 +- 默认 ALL target 必须完整构建;禁止提交热路径插桩(CI grep 门)。 +- **每个门必须能因它存在的理由变红**:每个门带阴性对照并真跑过一次红(R-16);公共 GL 看不见的改白盒断言。 +- 正确性门是硬门;**性能对着 pull 臂只记录、不作阻塞门**(用户 2026-09-08:push 比 pull 多约 10% 逐线程 CPU 已被接受;专门的优化阶段排在路线图推完之后)。唯一不可谈判的是数字必须真的采到。 +- 每阶段出口跑一次五部分门(`ARCHITECTURE.md` §13.2);性能判据是**逐线程 CPU 时间**;设备对比走 reboot-clean + 同热窗口配对 A/B,只用 Redmi `2f7cbe2e`(`devices/pin-verification-2026-09-07.md`);Windows 机器不是正确性门。 +- G1:pull 构建的符号集与 `.text` 字节对基线恒等(每阶段 0/0/0/0);G2:pull 与 push 的 ctest 名集合相同;G14:测试名只增不删;G5:保护区字节一致。 +- **拆分不借机顺手修 `dev` 的 bug**:句柄化过程中发现的 `dev` 侧缺陷记成独立条目、独立 PR(开放问题 15、17)。 +- 过程(用户 2026-09-16,ID-66):不过度验证;轮次间不做对抗性审查,每个 P 阶段收官由 Codex 审一次,发现进入下一阶段首轮;只读 / 非核心任务派给 Codex;实现方与审查方错开模型族。 -两条跑道仍然分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b `inproc` verb migration → P6 → P9 → P10 → P11 → P12。这里只调整 IPC 跑道内部的先后,不把 spawn 混进 monolith 深化。 +两条跑道分开:**monolith 跑道** P0 → P0.5 → P1 → P2 → P3a → P4a → P3b/P4b → P7 → P8 → P13,每段可独立交付、可随时中止;**IPC 跑道** P5 → P5b → P6 → P9 → P10 → P11 → P12。目标顺序:先完整的 separate-thread rendering(P5b,已达成),再 separate-process transport(P6)。 ## 阶段 -| 阶段 | 天 | 落地什么 | 验收门 | 依赖 | -|---|---|---|---|---| -| **P0** 卫生、度量、门、骨架 | 9–11 | ✅ 边界计数器(字节 / 动态 accessor / 六个 memo 门 / 上传形状);`PipeCalls.def` 完整目录 + payload POD + 七个生成器 + CI `pipe-gates`;`gen_pipe_dirty_surface.py`;`check_doc_citations.py`;八个 `MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架(`SCM_RIGHTS` 第一优先、双 tail 双三元组的 `RingControl`、双向 doorbell、校验型 `Framing`、`ShmSegment`、`InProcessTransport`)+ `protocol.fbs` + `flatc-check` + `MG_Test/Wire` 五个套件;三个严格 no-op 收益(`GetInteger64i_v`/`GetProgramiv` 退役、`RenderbufferObject::GetLifetimeId()`、D21 XFB 计数槽重键);compute 限制进 `DynamicBackendParameters`;spike A、spike B;retrace 通道 `--env` 透传 | ✅ 单元/集成/40 trace 逐名不变;wire 层测试(fd 传递、doorbell、ring、封帧、inproc)绿;两台设备的字节/调用基线在案;spike A/B 出结论;citation lint 绿 | — | -| **P0.5** 值头与制品头抽取 | 6–9 | ✅(`5d99ee43`)`MG_Pipe/MGPipeValueTypes.h`(`RenderStateParameters`、`SamplerParameters`、`PixelStoreParameters`、`VertexAttribute`… 不 include `MG_State/GLState`);`MG_State/GLState/ProgramState/ProgramArtifacts.h`(五个反射类型,不 include `ShaderObject.h`/`SpvcSession.h`,8 个 includer 零改动——类内 `using` 别名保住每一种既有拼写);`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言(`scripts/check_include_closure.py`,text + clang 两模式、自带阴性对照、`--require-all` 棘轮;`scripts/symbol_report.py` 做逐符号归因)。实测落地:测试名零删除、`MG_Backend` 零 diff、`.text` 字节不变、符号 0 增 / 0 删 / 42 重命名;`DynamicBackendParameters` 未搬(含 `SizeT` 与 `TextureTarget` 成员,搬动不是纯移动),`MGPipeTypes.h` 仍 include `BackendObject.h`,闭包门 A 因此断言 `MGPipeValueTypes.h` | 全套测试逐名不变(纯搬移);两条闭包断言绿且人为加回一个 `MG_State` include 能变红;`nm`/`.text` 变化可逐符号归因 | P0;**P1 与 P7 的硬前置** | -| **P1** `PipeInputs` 替换与 verify harness | 10–13 | ✅ `MG_Backend/MGPipe/PipeInputs.h`(63 字段;Espryt 32 / Magma 56 访问器);**实测 277 处箭头 + 58 行非箭头**(Espryt 113+9、Magma 164+49)逐条转换;逐 verb 类填充点(G5 表,~93 个边界站点);逐 verb 世代 poison;G4 影子比对器 + 第三种 CI 模式;20 处 `SyncPersistentMappedRange` + 6 处 `SyncGpuWrites` 的逐站点归属表 | pull 构建 `nm --defined-only` 不变、`.text` 差异逐行归因(空守卫/三元重写推迟到 P2);40 trace + 全部集成测试在 `MOBILEGL_PIPE_VERIFY=1` 下零分歧;故意损坏一个快照字段能让 verify 变红;故意在 `glGenerateMipmap` 的填充表漏一个字段能在**那条 verb** 上触发 poison Fatal | P0.5 | -| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | 18–26 | ✅(`738b289d`)`MG_Impl/Pipe/Tracker`(dirty 位、5 个聚合世代、抑制器骨架);`gen_pipe_dirty_surface.py` 首轮映射成门(73 个 mutator 全映射,45 条 render-state 答案 + 8 条 (mutator, bit) 答案逐字段导出,0 COARSE / 0 UNDECIDED,`.github/workflows/test.yml:1601-1604`);`MGPipeRenderStateSpans` chunk 表 **7 个 pipeline chunk / 396 B + 8 个 dynamic chunk / 772 B = 1168**(`MobileGL/MG_Pipe/MGPipeRenderStateSpans.h:190-191`)+ G7 setter 一致性测试;`CsoCache`(**64 项**,键 = pipeline 子集,`MobileGL/MG_Impl/Pipe/CsoCache.h:53`);`create/bind_render_state` + `set_dynamic_state`(Espryt `SyncRenderState` 一行不动;Magma `ComputePipelineStateHash`/`GetOrCreatePipeline`/`ApplyDynamicDrawStateTail` 改从 CSO 与动态 payload 取);`set_pixel_pack_state`、`set_patch_state`、`set_vertex_attrib_defaults`;`set_residual_value_state` + `ResidualValueBlock` 绊线(**棘轮 1248 → 8**,`MobileGL/MG_Pipe/MGPipeTypes.h:546`);**第一片 Track H**:Espryt 0b(`SlotAllocator` + 6 个 registry → slot 数组 + 删 `TwinLookupMemo`×3/`OwnerEquals`/`g_fbSlotCache`/GC)与 Magma 子系统 4(`VertexInputStateFactory`/`VaoDrawMemo` 重键,删前端 VAO 里的后端裸指针)——共 **11 条身份 memo 直接删除 + 2 条重键**(`ARCHITECTURE.md` §9.5 的普查),pre-handle 臂在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下并存到 P13;`MOBILEGL_PIPE_LEGACY_MEMOS`(`CMakeLists.txt:36`);补**三个** capability 存储 `FramebufferSrgb`/`DepthClamp`/`TextureCubeMapSeamless`(`MobileGL/MG_Pipe/MGPipeValueTypes.h:303-305`) | ✅ 集成 × 2 后端 × {pull, push} 逐名相同(名差 0);pull 构建符号 0 增 / 0 删 / 0 重命名、四个已认定 resize、`.text` +160 B;`RenderStateImpl` sha 不变;单元 1566 × {pull, push, verify};`integration-gpu` 916/916(pull、push、`MOBILEGL_PIPE_PUSH=0` 三臂);79 例 retrace push 下 79/79、verify 下 79/79 零分歧;`integration-verify` 828 条零 `Fatal{`;verify 构建的三组对照(`PoisonOmitted`、`VerifyCorrupted`、`HandleRecycle`)44/44,且 ABA 对照关掉句柄身份即红;G7 负面对照按设计变红并点名 `SetColorMask`;`CsoContentAddressing` 6/6;测试名 0 删除 / +119。设备(小米 Adreno 830,配对、定频、尾 200 帧 p50):push 比 pull 多约 10% 逐线程 CPU——**用户 2026-09-08 决定接受**(**勘误**:这批 APK 是 -O0 构建,见 `MEASUREMENTS.md` §10 勘误;Release 基准线以 §20 的 P3a 表为准),性能自此对 pull 基线只记录不设门;两机各 16 行在案(小米 p50 +8–14%,Oppo Espryt +8–18% / Magma +10–11%);桌面 `DriverBench` T1 = +322 / +345 ns/draw(Espryt / Magma),T1 − T2 = −228 / −354(P2 比它替掉的 P1 残余填充便宜),blend-toggle +4.7% / +3.5%,见 `MEASUREMENTS.md` §9–§14 | P1 | -| **P3a** handle wave 1(Espryt):buffer、VAO | 18–23 | ✅(`fde5fda3`,44 个提交,`git diff --stat 5cb826b0 3e298c9a` = 37 文件 / +9273 / −230)**九条 `resource_*` 调用**接线:`ResourceCreate`、`ResourceRespecify`、`ResourceSubData`、`BufferSubDataResident`(`kOptional`,Magma 仍不注册)、`ResourceFlushRange`、`ResourceReadback`、`ResourceDestroy`、`MapPersistent`、`UnmapPersistent`——全部在 GL 调用时刻从 `BufferObject` 的十一处分发点发射(`ARCHITECTURE.md:157` 的唯一例外),落到新的句柄形后端表 `MGPipeResourceOps`(`MobileGL/MG_Pipe/PipeApply.h`,签名里没有任何 `MG_State` 类型);Espryt 的 `Ops_*` 行为逐条不变,只换了读输入的地方。**五条 vertex-input 调用**:`CreateVertexElements`/`BindVertexElements`/`DeleteVertexElements`(blob 两个视图都带,`MGPVertexAttribWire[]` + `MGPVertexBindingPointWire[]`,`ARCHITECTURE.md:132`)、`SetVertexBuffers`(`BaseInstance` 显式字段,取代 `g_pendingFetchBaseInstance` 那个 ambient 全局)、`SetIndexBuffer`。**第七张 Espryt slot 表**:`GLESBufferResource` 搬出 `PipeResource::m_backend`,进 `BackendSlotTable`(`MobileGL/MG_Backend/DirectGLES/Managers.h:691-692`)——第一张按 client 铸造、随调用过线的句柄索引的表。`kNeedsAck` 首次落到目录里(`ResourceRespecify`,逐记录谓词 `MGPipeResourceRespecifyNeedsAck(desc) == (desc.Immutable != 0)`,`MobileGL/MG_Pipe/MGPipeTypes.h:680`)。pool、延迟释放与三条 ring 原样搬(G5 十个函数逐字节相同);Adreno 禁用属性 SIGSEGV workaround 保留。**memo 普查(`ARCHITECTURE.md` §9.5)**:句柄臂上**退役 6 条身份 memo 成员**——VAO twin 的 `m_hasSyncedConfigVersion` / `m_syncedConfigVersion` / `m_syncedAttributeVersions`(`Array`,本阶段最大的一处)/ `m_syncedIndexBufferVersion` / `m_syncedIndexBufferObject`(裸前端指针),加 `ConvertedFloat64Stream::sourceLifetimeId`(即普查里 11 条直接删除的最后一条,`ConvertedVertexStreamKey` 的 `sourcePin`)。六条都**仍在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下编译**(`Managers.h:1109-1130`、`:1067-1075`),pull 构建强制该开关 ON,所以 `sizeof` 一处不动——G1 的 0/0/0/0 就是这条的度量,真正的删除随 pull 路径在 P13 发生;**4 条重键** —— twin 的同步门(config version + 32 组逐属性版本 → `{elementsHandle, elementsSerial}` + `VertexBuffersSerial`)、twin 的索引槽 memo(回绕 `Uint16` + 裸 `BufferObject*` → 一个 `Uint64 m_syncedIndexSerial`)、`ResolvedDrawBuffers`(`configVersion` → `{elementsHandle, elementsSerial, buffersSerial}`,`Entry` 与 `iboFrontend` 各加 `MGPipeHandle`)、`ConvertedFloat64Stream`(前端 lifetime id + change serial → buffer `{slot, gen}` + applier `Serial`),另 `GLESBufferResource::syncedChangeSerial` 从镜像 `BufferObject::GetChangeSerial()` 改为镜像 applier 的 `Serial`;**保留不删** —— `g_pendingFetchBaseInstance` / `SetPendingFetchBaseInstance` / `GetPendingFetchBaseInstance` / `ScopedFetchBaseInstance` 与它的三个 scope 仍在 `MOBILEGL_PIPE_LEGACY_MEMOS` 下编译(`Managers.h:1189-1200`、`DirectGLES.cpp:5293`),因为真删会从 pull 构建移走两个符号 = 直接的 G1 破坏,计划里"grep 为空"那条在 P3a **不可达**;`PipeResource::m_backend` / `SetBackendResource` / `ReleaseBackend` / `BackendBufferResource` 同理(D-K,`ARCHITECTURE.md:286`),push 下只是不再被写,随 pull 路径在 P13 退役。**`map-persistent-roundtrips`(`mpr`)**:定义为**每一次 `MapPersistent` 发射,铸成或拒绝都算**(`ARCHITECTURE.md:485`),所以 monolith 下非零、可断言。子系统位 7(resources)与位 8(vertex input),push 默认 `0x7f` → **`0x1ff`**(`kMGPipeSubsystemsMigratedAtP3a`) | ✅ 五部分门(本地 `~/w7/pipe`):**G1** pull 符号 0 增 / 0 删 / 0 重命名 / **0 resize**、`.text` 10806323 字节不变(P3a 的认定 resize 集为空);**G5** 十个 pool / 延迟释放 / ring / flush-drain 函数对 `44c2b5cf` **与** `5cb826b0` 都逐字节相同,self-test 两个阴性对照(`ClearBufferPool`、`FlushPendingRangesNow`)都按名变红;`RenderStateImpl` sha 仍不变;**G2** pull 与 push 逐名相同(名差 0);**G14** 测试名 0 删除 / **+58**;单元 **1619** × {pull, push, verify};`integration-gpu` **958/958** × {pull、push、`MOBILEGL_PIPE_PUSH=0`、`0x7f`(G12 的子系统关闭臂)、`MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH=1`};buffer/VAO 族 **202/202**;`RenderStateSpans`/`Residual`/`VertexInputEmit`/`ResourceEmit` **48/48**;`HandleRecycle`(verify)**60/60**,含新的 buffer 用例三臂;`CsoContentAddressing` + `ResourceSubsystemControl` **10/10**,controls **64/64**;**G7** 两个阴性对照都按设计变红,vertex-input 那个**点名 `IsBgra`**;`integration-verify` **842/842**,零 `Fatal{`;`gen_pipe` 与 `gen_pipe_dirty_surface`(scan root 已扩到 `MG_State/GLState`,75 个 mutator 全映射、0 COARSE / 0 UNDECIDED)`--check` + `--self-test` 全绿。79 例 retrace:push 下 **79/79**,`MOBILEGL_PIPE_VERIFY=1` 下 **79/79 全部 armed、零分歧、零 `Fatal{`**;G3b 的具名五条都在这两次扫描里通过,**但单独那次具名扫描因门脚本把正则写成了逗号清单而选中 0 例**,且设备侧仍按开放问题 17 排除 `create-indirect`,所以 G3b 记为"桌面全绿、设备侧部分"——详见 `MEASUREMENTS.md` §16。**性能只记录不设门(用户 2026-09-08 规则 (a))**:MC 26.3 在 Adreno 上的 p99 对与两机 p50/p99 表见 `MEASUREMENTS.md` §20。**再基线检查点 1:实际日历 1 天(2026-09-08,四个包),远低于 27 天,不触发重定基线** | P2 | -| **P4a** handle wave 2(Espryt):FBO / 纹理 / sampler / program 身份与描述符 | 26–34 | ✅(`8c458cd5`,101 个提交,`git diff --stat 37da3c3a 8c458cd5` = 86 文件 / +30066 / −384)**十四条族调用接线**:`set_framebuffer_state`、`create/delete_sampler_state`、`create/delete_sampler_view`、`set_texture_params`、`set_sampler_views`、`bind_sampler_states`、`set_shader_images`、`create/delete_shader_state`、`set_draw_program`/`set_dispatch_program`、`set_global_constants`;纹理与 renderbuffer 的 create/respecify/subdata/destroy **复用 P3a 的四条 `resource_*` 行**(D-D1),所以目录仍是 **71 条,P4a 一行未加**。**六种 kind 按 `{slot, gen}` 重键**:Texture、Renderbuffer、Framebuffer、SamplerCso、SamplerViewCso、ShaderCso——其中 sampler **state** 内容寻址(容量 256、引用计数防 LRU 驱逐掉仍被 `MGPTextureParams::BuiltinSampler` 指着的项),sampler **view** 按纹理对象身份寻址(D-F2),framebuffer 只有句柄没有 wire 生命期(D-I2)。子系统位 **9(framebuffer)/ 10(纹理资源)/ 11(sampler)/ 12(program)**,`MOBILEGL_PIPE_PUSH` 默认 `0x7f`→`0x1ff`→**`0x1fff`**。`CompositeResolver` 落地(program pipeline 解析成保留段里的一条 shader CSO,按 `(ContextId, 管线名)` 记忆——进程级单例配每上下文 GL 名,是复审在真实 make-current 序列上打出来的)。**契约改了七次才对**(`c0b`…`c0g`):birth 半边 hook 与发布 latch、`wants()` 看 wired 常量、四个缝的编码一次性写死(`MGPSubData::Target` 低字节资源目标 + 高字节上传目标、`DepthStencilMode` 0/1、`MGPSurface::Kind`、`Pad0`→`TextureTarget`)、Tracker 两个看不见自己主体的快门(`glBindSampler` 只动位 12 的世代、SSO 下 `GetCurrentProgram()` 恒 null)、`MGPipeFramebufferTarget::Named = 3`(**记录改按 framebuffer 句柄存**:原设计只存两个"当前绑定"记录,于是 `BlitNamedFramebuffer` / `ClearNamedFramebuffer*` 在句柄臂上打进一个从没拿到附件的 FBO)、**消费者门**(四族必须"某后端注册了 `MGPipeResourceOps`"才发射——Magma 没有 P4a twin,客户端却按 acceptance 清了 dirty,直接让 66 条 DirectVulkan 用例丢上传)、**客户端侧 D-K2 依赖表**(位 10 要位 7 和位 11,位 9 要位 10;只在服务端拒绝时客户端已经把标记清了)。emulation 在 split 下显式 Fatal 直到 P8(`MGPipeUnmigratedEmulation` 在 `Managers.cpp` 2 处、`DirectGLES.cpp` 4 处) | ✅ 全门(`6035c9d7` 全量 + `8c458cd5` 上复跑):**G1** pull 符号 0 增 / 0 删 / 0 重命名 / 0 resize、`.text` 字节不变(P4a 认定 resize 集为空;Espryt 侧靠预处理器宏而非 lambda/局部量改写——插一个 lambda 就会把同函数里九个既有 `$_N` 全部重编号);**G5** P3a 的十一函数 + P4a 自己的 **17 个区**(`scripts/p4a_untouched_regions.sh`,8 个阴性对照全部按名变红);**G2** pull 与 push 逐名相同 **2902**;**G14** 0 删除 / **+314**;单元 **1785 × 3**;`integration-gpu` **1117/1117 × 七臂**(默认 `0x1fff`、`0x1ff`、`0`、`0x9ff`、`0x5ff`、pull、`ESPRYT_DISABLE_INVALIDATE_FLUSH=1`),其中 DirectVulkan **559/559**;`integration-verify` **920/920 零 `Fatal{`**;79 例 retrace 在 verify 臂 **79/79 全 armed 零分歧**、push 臂 **79/79**、G3b 具名 **12/12**;**八族拒绝普查 = 0**(按族逐条 grep 服务端拒绝句,工具 `wsl_p4a_refusal_census.sh`;`ctest -V` 的普查是假零——console sink 被编译掉了,必须逐用例取自己的日志)。**G9 的"落地前必须红"没能通过公共 GL 达成**:回读模拟自己会设 `GL_DEPTH_STENCIL_TEXTURE_MODE`,`IsDrawSyncClean` 又在首次采样时推参数,所以该场景改成**白盒断言**(新增 `MG_IntegrationTest/Harness/PipeApplyPeek.{h,cpp}` 直接读 applier 的参数记录 + Espryt 已应用状态,变异下 4/4 变红),D 的 `SanityTest` 另加一条覆盖"推迟到首次取视图"的那半。真正红过再绿的是终审抓出的两条:per-level respecify 丢上传(`L0; draw(other); L1; draw(T)` 在 `0x1fff` 下读回全黑、`0x1ff` 下正常)与 delete-then-draw(`glTexImage2D; glDeleteTextures; draw` 在句柄臂 SIGABRT `pure virtual method called`)。**CTS `direct_state_access.framebuffers*` / `packed_pixels`(G15)未跑**,判在关键路径外,随 P3b/P4b 补。**再基线检查点 1b:实际 1 天(2026-09-08 起,六包并行 + 契约七修 + 两轮终审修复),远低于 39 天,未触发** | P3a | -| **P5** 传输 + inproc applier + 发射表 | 12 | ✅(`ff2994d9..37fc4fdb`)同一 codec 上的 `InProcessTransport`;发射表、caps/reply;apply-thread context;lockstep barrier;tight ReadPixels;persistent-map 块推送;八包及 j0/x2/v1-r3 收尾 | 历史 joint:OpenRA 双后端 2/2、SSIM 1.0。收尾头:G1 0/0/0/0、.text +0,unit 1817×3 / 2086,push 1128、split-monolith 1149、reduced 22(含 2 skip)均零失败;**最终全门在 E3(a) 停止,Part 2/4 未到达**。收官审查 1 blocker / 10 major / 2 minor 转 r1/r2;证据见 `MEASUREMENTS.md` §31 | P4a | -| **P5b** `inproc` verb migration | 按首阻塞迭代 | ✅ d1 19、i1 7、t2 6、f1 11 槽已迁移;新增具名 blit lowering、GLES mip descriptor 路径、五槽 sync;P5 收官 r1/r2 已合入。合并头 `348d22a4`:A=2 / B=54 / C=15;收官审查修复 `a021e3cc`/`82683d4a` | ✅ 主机 split 107 selected = 105 passed + 2 skipped;主机全门 `348d22a4` **complete=true**(retrace-push/verify 各 79/79);合并普查车道 1267 = 811/203/62/191 对 c0b 零回退、79 trace **72/6/1**;**Redmi 正确性 8/8(四条 A/B trace 双后端 inproc 渲染)+ 四臂 A/B 24/32 组 200 帧尾全绿、barrier tax 首测**(`MEASUREMENTS.md` §33–35);iris-bsl 8 组为 fixture 123 帧上限,补充表另记。256 MiB stage 为显式 profile,默认仍为 32 MiB | P5 | -| **P6** spawn transport(后移) | 5 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 | P5b | -| **P3b / P4b** 深化(Espryt) | 29–38 | **P4a 已经付掉的,别再排进来**:`ResolvedDrawBuffers` 重键(P3a 就做了)、Tracker 的集合 hash 抑制器(三个 unit set 各自带内容 hash,clientsp)、`g_unitTextureSyncList` 那条 unit-bindings epoch(E 的验证轮换成 sampler set serial,顺带重键四条下游 memo)、`MGPSubRegion` 的跨步描述符(P4a 的 `resource_subdata` 已经逐区携带 `SrcOffset`/`SrcRowStride`/`SrcSliceStride`)。**仍然欠着**:按存储属主键控的发射游标与 index 重映射(dirty 归属反转,P4a 只落了 drain list);`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 的重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;**raw-depth-fetch sampler 原生化**(`g_rawDepthFetchSamplerState` 至今是 `MG_Backend` 里的一个前端 `SamplerObject`,是句柄臂上唯一一处有意保留的前端读——P4a 终审的 R-6);回读 / pack state;外加 P4a 记下的 R-3(三处只活在包头里的编码收进契约)、R-4(`UseProgram`/`BindVertexArray` 两行的 G9 推导)、R-5(D-K2 依赖表两侧各一份,要合成一处)、R-7(三通道 CPU mipmap 重武装缺口)、R-10(六种新 kind 的 ABA 对照目前是惰性的,缺一个消费者 `if`)、R-11(`TextureUploadShapeScenario` 仍是"只记录不设门") | ~25 个纹理场景、21 个 program 场景 + `MG_Test/ShaderTranspiler`;两台设备 `KHR-GL46.texture_*`/`internalformat.texture2d.*`/`shader_image_*`/`packed_pixels` 在 pull 基线 0.5 pp 内;每一个 Iris trace;**`TextureUploadShapeScenario`**(形状金标,Mali 帧时增量必须发布);view/owner 发射游标别名场景;verify 保留模式下 subdata 形状逐项相等;XFB 场景 + `capture_special_interleaved_test` | P4a | -| **P7** DirectVulkan(Magma)全量迁移 | 80–104 | §5.5 其余 10 个子系统(子系统 1、4 已在 P2):`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化(~120 行删除);具名 UBO host payload(D-B8,`kCapNeedsHostUboBytes`);blit/depth-mipmap 内部 shader 烘焙 + 新鲜度测试;`VertexInputStateFactory` 裸指针写回删除;D18 容器纪律原样保留 | 集成 + 40 trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;**`nm -D libMobileGLServer.so | grep glslang` 为空**;Iris trace 上 `stage-ubo-named` 逐帧字节发布;两台设备 CTS 0.5 pp 内。**再基线检查点 2:中点(第 40–52 工作日)完成子系统 < 40% 立即重定基线** | P0.5、P2;可与 P5/P6/P8 并行 | -| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 12–16 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,各带逐站点 reconcile);`MGHostSpan` split 填法;`Server/IndexHostMirror`;CopyImage 镜像搬到 client;`draw_vbo` 收编 multi-draw 族(分档仍在 server);viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;G3 分块路径;无 present fence tick + 无 present split 用例;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同(DirectVulkan 同);40 trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` Create fixture;`ClientArrayAfterComputeWriteScenario` 绿(去掉等待必须见几何缺失);`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes`/`index-bytes-shipped` 逐用例发布。**第 145 天:全功能 split** | P6、P3b/P4b | -| **P9** 反向通道 | 10 | `SEG_REPLY` slot 池;阻塞 `read_pixels`;PBO 回读 fire-and-forget;`OnGpuWritten` 收窄;`OnBufferWriteback` 按操作级批处理 + epoch 排序;`OnXfbScatterReady` + client scatter;`OnTextureWriteback`;`OnMipLevelsGenerated`;纹理拉取四条缓解 + 终止符;`OnGlError` 有序 + `glBufferStorage` 的 ack;`OnCapsInvalidated`;`OnSurfaceChanged`;`OnLog` 分级 + 速率限制;`SEG_EVENT` 溢出策略 | 回读/XFB 场景在 split 下绿;`TextureRemintPullScenario` 绿且含无解用例(终止符前表现为 apply 线程挂死/超时);拉取计数逐 trace 发布;故障注入:credit 阻塞时灌满 `SEG_EVENT`、日志洪泛下注入 link 失败 | P8 | -| **P10** sync / query / present 节奏 | 6 | client 铸造 sync/query handle;轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`;fence 完成度来自真的逐 fence 退休;DirectGLES 非 present fence tick;`present` 1:1;credit 默认 1 + 叠加公式;roundtrip 计数器与输入延迟直方图;三个独立 `dev` monolith 修复(`glEndTransformFeedback` 无限 `ClientWaitSync` → 推迟到首次读;`glDispatchCompute` 三次 `GetIntegeri_v` 校验 → 读 `CompileEnv`;D21 已落地) | query/XFB/`AsyncCompile` 场景在 split 下绿;40 个用例上 draw/state/upload 路径 roundtrip 读零,条件渲染与阻塞 query 次数逐用例发布;零 timeout 轮询在有界时间退出;`bench.sh` 配对 A/B:两侧都关采纳时 split 帧时在 monolith 10% 内,输入延迟 p50/p99 在案 | P9 | -| **P11** persistent map 与 ≥16 MiB 采纳 | 8 | POST 探针档位选择(T0 主攻,Adreno 可选 T1,T2 回退);`SEG_ADOPT` 生命周期绑 `completedFrameSerial`;`MOBILEGL_IPC_ADOPT_TIER` 负面对照 | `LargeArenaAdoptionScenario` 在所选档下绿;26.3 与两个 Create fixture SSIM ≥ 0.99;`StorageBufferRegrowScenario` 发布 `map-persistent-roundtrips`;Adreno 830 上 p99 帧时与峰值 RSS 对 monolith 采纳基线(163→21 ms / 40→115 fps / ~400 MB)**回归不超过 10%**;若 T2 成为某设备的永久答案,其实测代价写进文档 | P10、spike B(已答) | -| **P12** Android 生产窗口路径 | 10 | `android:process=":mgl"` Service 收 Java `Surface` → `ANativeWindow_fromSurface`;server 生命周期绑 Activity;FCL 用户 env 与 plugin APK V2 开关表接线 | Minecraft 经 FCL 在 spawn 模式下于 Adreno 830 双后端入世界;配对 reboot-clean bench + 输入延迟直方图;杀 server 产生干净 device-lost latch;SIGKILL 故障注入 | P11 | -| **P13** 退役 pull 路径 | 8–12 | 删 `SnapshotFromGLContext()` 非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;保留 `MOBILEGL_PIPE_VERIFY`;MGPipe recorder 金标模式;删 `set_residual_value_state`;`MG_Backend` 的 `MG_State` include 收缩到 `MGPipeValueTypes.h`;在计数器活着的情况下重调幸存缓存容量(`VaoDrawMemo` 2048、`SetupDrawSnapshot` 4、pipeline memo 8、`syncedTextureMemo` 8)并变成带 env 覆盖的调优参数;最终符号/尺寸/CPU 报告 | `static_assert(sizeof(ResidualValueBlock) == 0)` 编译通过;三道纯度门在非 verify 构建上转绿;verify 构建仍零分歧;recorder 金标在 40 trace 上建立;全套门(集成 × 2 后端 × {monolith, split}、单元、40 trace、两台设备 CTS 在 `81b17c0b` 基线 0.5 pp 内);**monolith 逐线程 CPU 在两台设备 p50/p99 上不差于 P0 基线** | P7、P8、P12 | +| 阶段 | 状态 | 落地什么 | 验收门 / 证据 | +|---|---|---|---| +| **P0** 卫生、度量、门、骨架 | ✅ | 边界计数器;`PipeCalls.def` 完整目录 + payload POD + 生成器 G1–G7 + CI `pipe-gates`;`gen_pipe_dirty_surface.py`;`check_doc_citations.py`;`MOBILEGL_PIPE_*` 开关;`MG_Remote/{Protocol,Transport}` 骨架 + `protocol.fbs` + `flatc-check` + `MG_Test/Wire`;三个严格 no-op 收益;spike A / B;retrace `--env` 透传 | 单元 / 集成 / trace 逐名不变;wire 层测试绿;两台设备字节 / 调用基线在案;spike 结论。`MEASUREMENTS.md` §1 | +| **P0.5** 值头与制品头抽取 | ✅ `5d99ee43` | `MG_Pipe/MGPipeValueTypes.h`;`ProgramArtifacts.h`;`Visit()` 归档 + `sizeof` 绊线;CI `-H` include 闭包断言(`scripts/check_include_closure.py`)+ `scripts/symbol_report.py` | 测试名零删除、`.text` 不变、符号 0 增 / 0 删 / 42 重命名 | +| **P1** `PipeInputs` 替换与 verify harness | ✅ | `MG_Backend/MGPipe/PipeInputs.h`(63 字段);277 处箭头 + 58 行非箭头逐条转换;逐 verb 类填充点;逐 verb 世代 poison;G4 影子比对器 + verify CI 模式;push-on-mutation 三字段 | pull `nm` 不变;单元 1485×3;`integration-gpu` 878;`integration-verify` 818 零 Fatal;79 retrace verify 79/79 armed 零分歧。§2 | +| **P2** 渲染状态 CSO + 第一片 Track H + 残余值块 | ✅ `738b289d` | Tracker(dirty 位、5 个聚合世代、抑制器);dirty-surface 成门;`MGPipeRenderStateSpans` chunk 表 + G7;`CsoCache`(64);`create/bind_render_state` + `set_dynamic_state`(`SyncRenderState` 一行不动);pixel pack / patch / attrib defaults;`ResidualValueBlock` 棘轮 1248 → 8;Espryt slot 表 + Magma vertex input 重键(11 条 memo 删除 + 2 条重键);`MOBILEGL_PIPE_LEGACY_MEMOS`;三个 capability 真存储 | G1 4 处认定 resize、`.text` +160 B;G2 名差 0;单元 1566×3;`integration-gpu` 916 三臂;retrace 79/79 push + verify;对照 44/44。**GO/NO-GO(2026-09-08):继续**。§3 | +| **P3a** handle wave 1(Espryt):buffer、VAO | ✅ `fde5fda3` | 九条 `resource_*` + 五条 vertex-input 调用接线到句柄形 `MGPipeResourceOps`;第七张 Espryt slot 表;`ResourceRespecify` 的 `kNeedsAck` 谓词;VAO twin 六条身份 memo 在句柄臂退役、四条重键;`mpr` 定义;位 7/8,push 默认 `0x1ff` | 五部分门全绿:G1 0/0/0/0 `.text +0`;G5 十一函数;单元 1619×3;`integration-gpu` 958 五臂;verify 842;retrace 79/79 ×2。实际 1 天。§4 | +| **P4a** handle wave 2(Espryt):FBO / 纹理 / sampler / program | ✅ `8c458cd5` | 十四条族调用接线(framebuffer state、sampler state / view、texture params、shader images、shader state、draw / dispatch program、global constants);纹理 / renderbuffer 复用 `resource_*`(目录不加行);六种 kind 按 `{slot, gen}` 重键;`CompositeResolver`;`Named = 3` framebuffer 记录;消费者门 + 客户端依赖表;位 9–12,push 默认 `0x1fff`;emulation 在 split 下具名 Fatal | 全门:G1 0/0/0/0;G5 17 区;G2 2902;单元 1785×3;`integration-gpu` 1117 七臂;verify 920;retrace 79/79;八族拒绝普查 0;`PipeApplyPeek` 白盒对照。实际 1 天。§5 | +| **P5** 传输 + inproc applier + 发射表 | ✅ `ff2994d9..37fc4fdb` | 同一 codec 上的 `InProcessTransport`;四种 build flavour;发射表三类 / caps mirror / reply mailbox;apply-thread context 终身持有;lockstep verb barrier;tight ReadPixels;persistent-map 块推送;G8 字段归属生成器;split 测试 / CI / APK 车道;八包(c0 契约、w1 codec、s1 session、p1 归属、b1 persistent map、t1 车道、c1 路由、v1 server)+ j0 / x2 / v1-r3 收尾 | joint:OpenRA inproc 2/2 SSIM 1.0;`integration-split` 21/21;E1 14/14 红;G1 0/0/0/0。收尾头全门在 E3(a) 停止(设计性 skip 被判红),Part 2/4 未到达;收官审查 1 blocker / 10 major / 2 minor → P5b r1/r2。§6 | +| **P5b** `inproc` verb migration | ✅ `37fc4fdb..82683d4a` | class-C 普查决定顺序;d1 19 / i1 7 / t2 6 / f1 11 槽迁移;sync 五槽;具名 blit;GLES mip descriptor;r1 / r2;收官审查三项修复。发射表 A=2 / B=54 / C=15 | 主机全门 `348d22a4` complete;`integration-split` 107/107;合并普查零回退、79 trace 72/6/1;**Redmi 正确性 8/8 + 四臂 A/B、barrier tax 首测**。§7 | +| **P6** spawn transport | 下一个 | `SocketTransport`(socketpair + fork/execve,envp 剔除 + 强制 monolith 双保险);`ServerMain`;`MOBILEGL_IPC_SERVER_PATH` + `dladdr` 兜底;有界重试握手;EOF 即时退出;device-lost latch;替换 P5b 的三处 inproc 依赖 | P5b 的完整渲染路径在 `spawn` 下绿;进程树只多一个子进程;`HeadlessGL` fork 预检无孤儿;OpenRA 在 Adreno 830 上 split SSIM ≥ 0.99 | +| **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 原生化;回读 / pack state;P4a 记下的 R-3 / R-4 / R-5 / R-7 / R-10 / R-11;`ProgramArtifacts.h` 的 NDK 尺寸钉 | 纹理 / program 场景;CTS `texture_*` / `shader_image_*` / `packed_pixels` 在 0.5 pp 内;每一个 Iris trace;`TextureUploadShapeScenario` 升级成门 | +| **P7** DirectVulkan(Magma)全量迁移 | 待排 | 其余 10 个子系统:`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化;具名 UBO host payload(D-B8);内部 shader 烘焙;`VertexInputStateFactory` 内容寻址 CSO;D18 容器纪律保留 | 集成 + trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;`nm -D libMobileGLServer.so \| grep glslang` 为空;CTS 0.5 pp 内。**再基线检查点:中点完成子系统 < 40% 立即重定基线** | +| **P8** emulation 下放 + 索引宿主镜像 + 协议广度 | 待排 | `MG_Impl/Pipe/HostResolve.cpp`(client 数组范围、最大索引扫描、`*IndirectCount` 解析,逐站点 reconcile);`Server/IndexHostMirror`;CopyImage 镜像搬到 client;viewport-array 回放验证;`generate_mipmap` 计划 + CPU 回退纹素;大 blob carrier(开放问题 11);无 present fence tick;`kCapDriverOrderedXfbCapture` | `'^DirectGLES\.Split\.'` 与 `'^DirectGLES\.'` 逐名相同;trace split 双后端 SSIM ≥ 0.99 含两个 `coherent_as_flush` fixture;`ClientArrayAfterComputeWriteScenario`;`create-indirect` 上 `roundtrips-per-frame` 读零;`index-mirror-bytes` 逐用例发布 | +| **P9** 反向通道 | 待排 | `SEG_REPLY` 异步 slot 池;PBO 回读 fire-and-forget;`OnGpuWritten` 收窄;`OnBufferWriteback` 批处理 + epoch 排序;`OnXfbScatterReady`;`OnTextureWriteback`;`OnMipLevelsGenerated`;纹理拉取四条缓解 + 终止符;`OnGlError` 有序;`OnSurfaceChanged`;`OnLog` 分级;`SEG_EVENT` 溢出策略;class-C 的 readback 尾 | 回读 / XFB 场景在 split 下绿;`TextureRemintPullScenario`(含无解用例);拉取计数逐 trace 发布;两条故障注入 | +| **P10** sync / query / present 节奏 | 待排 | client 铸造 query handle;轮询入口成门铃点 + `MOBILEGL_IPC_POLL_ESCALATE`;fence 完成度来自逐 fence 退休;非 present fence tick;`present` 1:1;credit 默认 1;roundtrip 计数器与输入延迟直方图;class-C 的 query / swap-interval 尾 | query / XFB / `AsyncCompile` 场景在 split 下绿;trace 上 draw / state / upload 路径 roundtrip 读零;零 timeout 轮询有界退出;配对 A/B 记录 | +| **P11** persistent map 与 ≥16 MiB 采纳 | 待排 | POST 探针档位选择(T0 主攻,Adreno 可选 T1,T2 回退);`SEG_ADOPT` 生命周期绑 `completedFrameSerial` | `LargeArenaAdoptionScenario` 在所选档下绿;26.3 与两个 Create fixture SSIM ≥ 0.99;Adreno 830 上 p99 帧时与峰值 RSS 对采纳基线(163→21 ms / 40→115 fps / ~400 MB)回归不超过 10% | +| **P12** Android 生产窗口路径 | 待排 | `android:process=":mgl"` Service 收 Java `Surface`;server 生命周期绑 Activity;FCL env 与 plugin APK 开关表接线 | Minecraft 经 FCL 在 spawn 模式下于 Adreno 830 双后端入世界;杀 server 产生干净 device-lost latch | +| **P13** 退役 pull 路径 | 待排 | 删 `SnapshotFromGLContext()` 非 verify 分支、`MGB_CTX`、`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_LEGACY_MEMOS`;保留 `MOBILEGL_PIPE_VERIFY`;MGPipe recorder;删 `set_residual_value_state`;在计数器活着的情况下重调幸存缓存容量 | `static_assert(sizeof(ResidualValueBlock) == 0)`;三道纯度门在非 verify 构建上转绿;recorder 金标建立;monolith 逐线程 CPU 不差于 P0 基线 | -P5 的八个实现包(报告在 `~/w7/notes/p5/p5-results/`): - -| 包 | 落地物 | -|---|---| -| c0 | wire/segment 契约、四种 build、71 槽三分类、acceptance reply-slot 与 respecify scope carrier | -| w1 | 记录 codec、blob/tail 边界检查、call/ring flag 翻译、R-10 记录上限 | -| s1 | session/ring/handshake、caps snapshot、reply pool(16 MiB / 8×2 MiB)与 ABI fingerprint | -| p1 | 63 字段 + 7 sticky forward 的四类归属、poison/strict、`rsp` stamp 与生成器门 | -| b1 | persistent-map tracker、保守块推送、GPU-write 标记与 G5 第十一行重钉 | -| t1 | split 测试/CI/retrace/APK 车道、五部分 gate 与阴性控制脚手架 | -| c1 | 33 条生成路由 + 4 escape、37-row catalogue、caps 本地回答、tight readback client scatter 与 PACK-PBO 具名拒绝 | -| v1 | `ServerLoop` / `PipeApplier`、apply-thread EGL 生命周期、server shadow、12 条 forwarder 与 caps republish | - -累计(低端):P0 9 → P0.5 15 → P1 25 → P2 43 → P3a 61 → P4a 87 → P5 99 → P6 104 → P3b/P4b 133 → P8 145 → P9 155 → P10 161 → P11 169 → P12 179 → P13 187;P7 另 80–104,单跑道累计 267。 - -**CTS 周转单独计价**:`gl44to46` 约 56,271 例。逐阶段只跑该阶段可能影响的具名块(P4a `packed_pixels`、P3b/P4b `texture_*`/`shader_image_*`、P9 `transform_feedback*`);完整 caselist 只在五个架构边界(P0.5、P3a、P4a、P3b/P4b、P13)与每次合并 `dev` 之前跑,放 CI 不放关键路径。若周转仍主导排期,加宽估时而不是削弱门。 +CTS 周转单独计价(`gl44to46` 约 56,271 例):逐阶段只跑该阶段可能影响的具名块;完整 caselist 只在架构边界与合并 `dev` 之前跑,放 CI 不放关键路径。 ## 里程碑 -- **第 25 天(P1 出口)**:verify harness 逐 draw 逐字段证明"推送等价于拉取"。零产品风险,**不是** GO/NO-GO。 -- **第 43 天(P2 出口):GO/NO-GO —— 判定继续**。P2 的五部分门全绿(逐名相同、零分歧、负面对照能红),逐线程 CPU 代价约 +10% 被接受,下一步立即开 P3a。 -- **P3a 出口(2026-09-08)**:五部分门全绿,pull 构建仍 0/0/0/0,`resource_*` 与 vertex-input 两个家族在 Espryt 上换到句柄寻址。**实际日历口径:1 天**——contract / wire / client / espryt / gates 四个包(五轮)全部在 2026-09-08 内完成、集成并跑完门,对着"P3a > 27 天"的绊线是 **1 / 27**,不触发重定基线,也不需要 `inproc` 的证伪数字(`:70`)。Track H 单位成本的完整普查见 `MEASUREMENTS.md` §19。 -- **P5 出口(2026-09-16)**:缩减路径首个 IPC 帧已经观察到;joint、落地快门、收尾头实跑分别记在 `MEASUREMENTS.md` §26/31。收尾头 `37fc4fdb` 的 E3(a) 控制因两条设计性 skip 失败;不能以早期 joint 或各包自门替代未完成的最终全门。红米 P5 四臂 A/B 的 split 均停在索引 draw,barrier tax 未测(`MEASUREMENTS.md` §30),迁移与重跑归 P5b。 -- 原“第 104 天跨进程帧”后移到 `inproc` verb migration 之后;目标先是完整 separate-thread rendering,再是 separate-process transport(ID-66)。第 145 天全功能 split与第 187 / 267 天纯度门仍是方向,不据此伪造新日历。 +- **P2 出口(2026-09-08):GO/NO-GO 判定继续**——五部分门全绿,逐线程 CPU 代价约 +10% 被接受;tracker 绝对 ns 上限降为记录项。 +- **P3a / P4a 出口(2026-09-08)**:各 1 天,远低于 27 / 39 天的再基线绊线,未触发重定基线。 +- **P5 出口(2026-09-16)**:缩减路径首个 IPC 帧;最终全门在 E3(a) 停止;红米四臂的 split 均停在索引 draw,barrier tax 未测(归 P5b)。 +- **P5b 出口(2026-09-16,已达成)**:主机全门 `348d22a4` complete;79 trace 72/6/1;设备源头 `82683d4a`(APK `p5bcodex2`)Redmi 正确性 8/8——四条 A/B trace 双后端首次在设备上 inproc 渲染;barrier tax 首测 split−push 逐线程 CPU p50 +5.9% – +18.2%。 +- 仍是方向、不据此伪造新日历:全功能 split(P8 之后)、纯度门在非 verify 构建上转绿(P13)。 -- **P5b 出口(2026-09-16,已达成)**:唯一收官审查(0 blocker / 2 major / 1 minor)的定向修复以 `a021e3cc`/`82683d4a` 的靶向证据补齐,未触发第二轮全门/审查。主机全门固定 `348d22a4`:31 步原始 rc=0 + retrace-verify 以钉住库续跑合并 **79/79**,complete=true;合并普查车道对 c0b 零回退、79 trace **72 passed / 6 aborted / 1 failed**。设备源头 `82683d4a`(APK `p5bcodex2`):四臂统一 256 MiB stage profile,**Redmi 正确性 8/8**——四条 A/B trace 双后端首次在设备上 inproc 渲染,出口判据(BRIEF-P5B §5)达成;四臂 A/B 24/32 组 200 帧尾全绿,**barrier tax 首测**:split−push 逐线程 CPU p50 +5.9% – +18.2%(`MEASUREMENTS.md` §35)。iris-bsl 8 组为 fixture 123 帧上限(pull 同失败,非回归),补充表另记。 +再基线检查点仅剩一条:**P7 中点完成子系统 < 40% 立即重定基线**(P3a 的检查点发现不了 Magma 特有的超期)。 -## 第 43 天 GO/NO-GO 清单 - -手上必须有(**结算:继续**,2026-09-08): - -- [x] P1 交付的逐 draw 逐字段语义等价证明(40 trace + 全部集成测试零分歧) -- [x] 两个后端上都已推送的渲染状态,`SyncRenderState` 693 行一行未动(`RenderStateImpl` 段 sha 与 P2 起点相同) -- [~] 两片 Track H 的实测单位成本(Espryt 0b、Magma 子系统 4)—— 产出侧在案(`ARCHITECTURE.md` §9.5 那份 21 条身份 memo 普查里,11 条直接删除 + 2 条重键全部落地,两片都零回归),日历口径(实际工作日 vs 估计的 5–7 / 2–3 天)**未记录** -- [x] 两台设备(Adreno 830 `35d0befa`、Mali `3B159D009VZ00000`)reboot-clean 配对的逐线程 CPU 时间增量,p50 与 p99 —— 两机各 16 行全部在案(`MEASUREMENTS.md` §10):小米 p50 +8–14%,Oppo Espryt +8–18% / Magma +10–11%,p99 同向 -- [x] tracker 每 draw 的**绝对 ns**(T1/T2)—— 桌面 `DriverBench`:Espryt T1 +322 / T2 +550、Magma T1 +345 / T2 +699 ns/draw,T1 − T2 = −228 / −354(`MEASUREMENTS.md` §12);设备侧的等价读数是配对 A/B 的 +8–14%;按下面的口径为**记录项**,不再是门 -- [x] Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)—— `mc_state_toggle`(每帧 46 对):Espryt 23753 → 24869 ns/开关对(+4.7%)、Magma 32705 → 33857(+3.5%);pass switch +0.6% / +1.7% -- [x] 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开(`kMGPipeBehaviourNoCsoContentAddressing`,`CsoContentAddressing` 6/6 证明开关真的改变 mint/bind 计数) - -判据与出口: - -- **口径变更(用户 2026-09-08)**:push 比 pull 多约 10% 逐线程 CPU 可接受(该读数来自 -O0 APK,`MEASUREMENTS.md` §10 勘误;Release 数字在 §20);性能自此**对着 pull 臂基线记录**、不作阻塞门,第 43 天的绝对 ns 上限降为记录项;路线图先推完,专门的优化阶段排在其后(或首个 IPC 帧之后)。 -- **继续**(本次结算):五部分门全绿,两片 Track H 按计划的产出全部落地且零回归,按两条跑道推进,下一步 P3a。原判据(两台设备 p50 与 p99 逐线程 CPU 增量都不为负;tracker 绝对 ns 在上限内)保留为后续阶段的记录口径。 -- **收缩为 headless 工装用途或重新评估**:任一判据落空。**不回滚**:P0/P0.5/P1/P2 的产物(句柄基建与重键、两个头文件抽取、计数器、verify harness、渲染状态 CSO)全是自洽的 monolith 交付物,留在 `dev`;MGPipe 收缩为 `MG_Test` mock 后端 → MGPipe recorder(给 trace_replay 一种记录已解析状态的录制格式)+ `inproc` 渲染线程实验;IPC 跑道搁置到出现新判据。 -- 沉没成本:P0 与 P0.5 无论走哪条路都要花(后者本身是 monolith 净收益);真正只为 MGPipe 押上的是 P1 + P2 ≈ 28–39 天,NO-GO 分支下仍留下上述产物。 - -## 再基线检查点 - -| 触发 | 动作 | -|---|---| -| P3a > 27 天 | "窄句柄化"的前提错了,P4a 开始前重定基线。**未触发**:P3a 实际 **1 天**(2026-09-08,四个包全部落地),`MEASUREMENTS.md` §19 记了逐包口径 | -| P4a > 39 天 | 同上。**未触发**:P4a 实际 **1 天**(2026-09-08 起;六个包并行、契约七次修正 `c0b`…`c0g`、缝类审计一轮、终审修复一轮)。逐包口径与每一次修正的因由见 `MEASUREMENTS.md` §22 与 `~/w7/notes/p4a/INTEGRATOR-DECISIONS.md`(ID-1…56) | -| P5 > 计划窗口 | **未触发重定基线**:实际日历 **2026-09-11 至 2026-09-16**。Sep 11 的模型配额 outage 中断 wave 2;Sep 16 又把本地 `rereview@local` 谱系重写到用户身份并重新接到 GitHub `ff2994d9`,两项都是过程时间,不是实现规模(ID-40/45/66) | -| P7 中点(第 40–52 工作日)完成子系统 < 40% | 立即重定基线(P3a 的检查点发现不了 Magma 特有的超期) | - -任一触发,先跑 `inproc` 的证伪数字再决定是否继续。 - -## P5/P5b 出口记录的债务 - -ID-65 的原始处置、ID-73 的 r1/r2 修复与 P5b 残余项如下。历史 27 条 wrong-answer 不是当前合并头计数,最终普查以 `MEASUREMENTS.md` §33 的同名比对为准。 +## P5 / P5b 出口记录的债务 | 债务 | 去向 / 当前口径 | |---|---| -| P5 基线 27 个普通 inproc wrong-answer(历史) | **22**(14 layered + 3 packed depth/stencil + 5 framebuffer recycle)→ P4b/P7 texture readback;3 → P7 query;1 inspection → P6。FBO/RBO delete 那 1 条使用 ReadPixels,完整像素因果未隔离;client-thread framebuffer death 已由 **r1 #2** 转至 apply mailbox,原 FBO/RBO delete 像素用例在 r1 定向验证中通过;合并普查决定最终计数,不能从单例外推 | -| `rsp` residual inputs | STRICT_ERRORS reduced lane 19 abort/2 skip;既有 rsp=35 只是有 stamp 读点的下界。无 verb stamp 的 sticky/non-verb forward 仍可绕过计数/strict;P7/P8 补逐字段归类和自证门(r2 提议),其余按字段的 P3b/P4b/P7 phase 退役 | -| `SEG_REPLY` 2 MiB payload cap | 当前 `MaxReplyBytes = 2 MiB - 16`;更大 readback 需要 P6+ chunking 或专用 carrier(ID-47) | -| `GetCaps` 的两个 blobref | 目前不骑 record;一旦运输,必须有 server→client carrier rule,不能套 `SEG_STAGE`(ID-48) | -| PACK-PBO readback | P5 具名拒绝;真实形状是 server 写 buffer resource、client `MarkGpuWritten`,列入 P6(ID-57) | -| ABI fingerprint | 目前不混 segment sizes;改变 8/32/16 MiB + 256 KiB ledger 时仍欠这项(ID-47) | -| max record bytes telemetry | **x2 已实测** reduced/OpenRA maxrec=784 B(SetVertexAttribDefaults),默认 cap=4 MiB;只代表所测负载,后续索引/indirect 尾仍须记录 | -| E2 wire 内容控制 | x2 draw-drop:758 draws,SSIM 0.000036;clear-drop 被全屏 overdraw 掩盖,不能作 OpenRA 控制。**r2 已在包头验证 pull-library → 恢复原库 → draw-drop 顺序及 split 库身份** | -| E3 small-ring wait telemetry | x2 已证明 wrap,旧 ringwaits=1 只证明 reclaim。**r2** 改为等待 outstanding retirement 后才计数,1 MiB 时 wrap=1/wait=1,8 MiB 对照同时红;**r1 #4 已合入** command-ring retirement 等待,重试保留两条尾与 ordinal;超大单记录仍拒绝 | -| E3(a) block-zero control | x2 补私有 disabled-push 诊断;j0 逐选项拒绝 skip。旧 6 selected 实为 4 red/2 skip,收尾全门因此停下;**r2** 精选 4 pixel case,每条必须自己的 assertion+私有诊断,且仍拒绝 pre-flight skip | -| E5 staged shadow 与 coherent map | v1-r3 的 unmapped 双 shadow 控制证明 ensure 上传 server pattern B,删保护会读 client A 并变红;**不能证明 coherent-map wire-only**。**r1 #1 已合入** apply-role producer guard;仅取消 client pre-draw push 的两个像素控制均变红,恢复后变绿,见 `MEASUREMENTS.md` §32 | -| tight ReadPixels PACK_SWAP_BYTES | **r1 #3 已修** tight/scatter 的 component/packed-word swap;非对称 UNSIGNED_SHORT 像素在 monolith/inproc 均通过,混合 depth/stencil 按两个 32-bit word 处理 | -| P5b 收官审查三项 | **已在 `82683d4a` 修复**:user-index span 长度、长 ClientWaitSync 预算、ReadPixels 非 OK 状态;包内定向 9 passed + 7 passed,均零 skip;原始审查为 0 blocker / 2 major / 1 minor,证据见 `MEASUREMENTS.md` §34 | -| CI / 控制 / 跑器 | **r2 包内已修** broad debt lane 吞掉后续控制、all-skip 绿、REQUIRE_GPU、E2 库恢复、E3 选择、陈旧/零用例普查、G5 pin 自测、c1f 旧名。外部 notes 脚本也有修改,git merge 不会自动传播;验收详见 `MEASUREMENTS.md` §31 | -| 默认 32 MiB staging 容量 | 128 MiB 单次上传不能装入默认 stage;P5b 普查/Redmi 显式 profile=256 MiB,默认不改。更大 blob 的分块/专用 carrier 仍属后续设计 | -| P5b 的 inproc 依赖 | 具名 blit 使用 scoped client binding + barrier;mip descriptor 的 registry 身份查询仍依赖 barrier-held object;FBO death 使用 inproc mailbox。P6 需替换这些跨地址空间不成立的路径 | -| 未迁移与仿真路径 | `348d22a4` 尚有 class-C 15 槽;client vertex arrays、multi-draw client indices、RGB 三通道 CPU mip/writeback、renderbuffer copy endpoint、未绑定 named clear 等保留具名拒绝或后续阶段债务;query/texture readback 的错答仍按普查记录 | -| 临时 CI trigger | 合入 dev 前删除 `feat/disaggregated` 的 TEMPORARY trigger;不能随阶段遗留 | +| P5 的 27 个普通 inproc wrong-answer(历史计数) | 22(14 layered + 3 packed depth/stencil + 5 framebuffer recycle)→ P4b/P7 texture readback;3 → P7 query;1 inspection → P6;1 FBO/RBO delete 用例在 r1 定向验证中通过,完整像素因果未隔离。最终计数以 `MEASUREMENTS.md` §7.2 的同名比对为准 | +| `rsp` residual inputs | STRICT_ERRORS reduced lane 19 abort / 2 skip;`rsp=35` 只是有 stamp 读点的下界,无 verb stamp 的 sticky / non-verb forward 仍可绕过计数;P7/P8 补逐字段归类,其余按字段的 P3b/P4b/P7 退役 | +| `SEG_REPLY` 2 MiB 单槽 payload cap | 更大 readback 需要 P6+ chunking 或专用 carrier(ID-47) | +| `GetCaps` 的两个 blobref | 目前不骑 record;一旦运输,必须有 server→client carrier rule,不能套 `SEG_STAGE` | +| PACK-PBO readback | P5 具名拒绝;真实形状是 server 写 buffer resource、client `MarkGpuWritten`,P6(ID-57) | +| ABI fingerprint | 不混 segment sizes;改变 8/32/16 MiB + 256 KiB ledger 时仍欠这项 | +| 默认 32 MiB staging | 目标负载单次 128 MiB 上传装不进默认 stage;普查 / Redmi 显式 256 MiB profile,默认不改;分块 / 专用 carrier 归 P8(开放问题 11) | +| P5b 的 inproc 依赖 | 具名 blit 的 scoped client binding + barrier;mip descriptor 的 registry 身份查询依赖 barrier-held object;FBO death 的 inproc mailbox。P6 必须替换 | +| 未迁移与仿真路径 | 15 个 class-C 槽(query / sync 尾 / `GetTexImage` / `SetSwapInterval`)→ P9 / P10;client vertex arrays、multi-draw client indices、RGB 三通道 CPU mip、renderbuffer copy endpoint、未绑定 named clear、`texture-remint-pull` 仿真保留具名拒绝 → P8 / P9;Magma split compute / image 路径 82 个错答 → P7 | +| E2 wire 内容控制 | draw-drop 是 OpenRA 的有效控制(758 draws,SSIM 0.000036);clear-drop 被全屏 overdraw 掩盖,不能作控制 | +| max record bytes | 实测 reduced / OpenRA `maxrec=784 B`,默认 cap 4 MiB;只代表所测负载,后续索引 / indirect 尾仍须记录 | +| 树外脚本 | 普查跑器、`wsl_p5_gate.sh`、Redmi 定频行都在 `~/w7/notes/`,git merge 不会传播 | +| 临时 CI trigger | 合入 dev 前删除 `test.yml` / `apk.yml` 的 `feat/disaggregated` TEMPORARY trigger | -## 仍然开放的问题 +## 开放问题 P0 已回答的不再列出(spike A 的域、spike B 的分档、`posix_spawn` 不可用、OOM 探测惯用法、`GetInteger64i_v`/`GetProgramiv` 退役、D21 与 `RenderbufferObject` lifetime id、动态 accessor 基线)。 -1. **client 侧 dirty 走查的真实每 draw CPU 代价。** **已答(P2,数字仍在补齐)**:推送**没有**在拉取基线(每 draw 6.5–9.3 次 accessor + memo 探测)之下净减少,而是多花约 10% 逐线程 CPU。小米 Adreno 830(`35d0befa`,reboot-clean、定频、配对、尾 200 帧 p50,单位 ms/帧):`minecraft-1.21.4-in-world` DirectGLES 7.39 → 8.19(+10.8%)、DirectVulkan 5.23 → 5.85(+11.9%);`minecraft-1.21.4-fabric-iris-bsl-in-world` DirectGLES 4.91 → 5.35(+9.0%)、DirectVulkan 4.158 → 4.525(`--benchmark-no-finish`,+8.8%)/ 4.583(finish,+10.2%)。bsl 用例的 p99 两臂都由着色器编译主导(~1.6 s),不承载这个问题。 - - 小米全部 16 行(4 trace × 2 后端 × finish 开/关,p50 与 p99、钉频判定)在 `MEASUREMENTS.md` §10:p50 增量在四个 trace、两个后端上都落在 **+8–14%**(`improved-transparency-minecraft-26.3` DirectGLES 37.26 → 42.44、DirectVulkan 61.55 → 68.12;`minecraft-1.21.4-startup` 只有加载帧,不承载)。 - - Oppo Mali(`3B159D009VZ00000`)全部 16 行同在 §10:Espryt in-world **+16–18%**、26.3 +8–9%,Magma 两机一致 **+10–11%**;26.3 上 p99 同向放大到 +16–22%。两机 32+32 次运行的 pull 臂即此后的性能基准线。 - - 绝对 ns 的分解(T1 = push 默认位图 − pull,T2 = `MOBILEGL_PIPE_PUSH=0` − pull,`T1 − T2` 正是 P2 自己的增删;桌面 `DriverBench`,`mc_vanilla_draw`,ns/draw,全表在 `MEASUREMENTS.md` §11–§12):Espryt **T1 +322**(pull 5121 的 +6.3%)、T2 +550、**T1 − T2 = −228**;Magma **T1 +345**(+2.0%)、T2 +699、**T1 − T2 = −354**——P2 的 tracker + CSO 比它替掉的 P1 残余填充便宜,剩下的 T1 是尚未句柄化的填充与 dirty 走查。无 CSO 内容寻址对照:Espryt −69(噪声内)、Magma +200(内容寻址每 draw 省 200);blend-toggle +4.7% / +3.5%,pass switch +0.6% / +1.7%。 - - **口径**:用户 2026-09-08 接受这一代价,性能自此对着 pull 臂只记录不设门(见上文"判据与出口")。 -2. **真实语料上纹理重铸拉取的发生率。** `ImageBindableHint` 预防主因,但整格式再生在普通 `glTexImage` 格式变更上就触发。若 MC/Iris fixture 上非平凡,保留 LRU 从默认 0 升为强制并拿真预算。**P4a 已答:可以忽略,保留 LRU 维持默认 0。** 计数器 `tex-remint-pulls`(汇总行 `emit[... trp=]`,`PipeStats` 的 `CallClass::TextureRemintPulls`,随终审修复轮落地)数的是"Espryt 重铸一份它已经持有的存储";在 79 例 retrace × 两后端、`MOBILEGL_PIPE_STATS_PERIOD=60`、push 构建(`8c458cd5`)下:**780 个统计窗口里总共 2 次**,分布在 **2 个用例**(`iris-photon-v1.3b-in-world`、`iris-derivative-main-d24.4.14-in-world`,都只在 DirectGLES 侧,各 1 次),其余 77 例恒 0。`ImageBindableHint` 这一波也第一次有了真实的生产者(`glBindImageTexture` 与 sampler view 解析各标一位,此前两位无人写、hint 恒 0),所以这个数是"预防已经生效之后"的残量。 -3. **spike B 的 `untrusted_app` 域复核。** 两台设备的分档在 `shell` 域测得;T0 的 AHB socket 交接是每个与 SurfaceFlinger 共享 buffer 的应用都在走的路径,风险在 memfd/opaque-fd 腿上。从应用进程再跑一次 `extmem_probe`(spike A 的 exec 钩子已可用)。 -4. **渲染状态的 wire 粒度。** **已答(P2)**:chunk 表冻结为 **16 个边界 / 15 个 chunk**,两半完美交替(chunk 0 dynamic、chunk 1 pipeline……),**7 个 pipeline chunk 共 396 B + 8 个 dynamic chunk 共 772 B = 1168**,每个边界都是一个 `offsetof` 或 `sizeof`,划分与总数由 `static_assert` 把关(`MobileGL/MG_Pipe/MGPipeRenderStateSpans.h:190-191`)。`set_dynamic_state` 的粒度就是这 8 个 dynamic chunk(只发变化的那些)。CSO LRU 容量取 **64**(`MobileGL/MG_Impl/Pipe/CsoCache.h:53`)——这是暂定值,**在 P13 连同其余幸存缓存一起、在计数器活着的情况下重调**(P13 行的"重调幸存缓存容量")。 -5. **`FramebufferSrgb` / `DepthClamp` / `TextureCubeMapSeamless` 的拍板。** **已答(P2)**:无存储的是**三个**能力不是两个——`RenderState::SetCapability` 的 `default:` 分支同样吞掉 `TextureCubeMapSeamless`(`glEnable` 可达、零读点)。三个都在 P2 拿到真存储,落在 `ColorMasks` 与 `ClearColor` 之间那 3 字节的空洞(偏移 581/582/583,`MobileGL/MG_Pipe/MGPipeValueTypes.h:303-305`),所以 `sizeof(RenderStateParameters)` 仍是 1168 且既有成员一个都没挪位。三个都划进 **pipeline 半边**(它们的 setter 都走 `BumpVersions()`;`DepthClamp` 是 `VkPipelineRasterizationStateCreateInfo::depthClampEnable`),chunk 表随之冻结。 -6. **具名 UBO host payload 的形状(D-B8)。** 第一个数字已有:Magma 在 26.3 世界每帧重打包 331 KB 具名 UBO 字节,Espryt 为 0。要么冻结现在的第二变长尾形状,要么走备选(Magma 直接描述符绑定常驻 `VkBuffer` range,独立 `dev` PR + Iris 性能门)。 -7. **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross pass 流水线、ESSL 转译缓存、格式处理器、POST 探针;client 需要 glslang phase A/B 与反射层。P0.5 解决了 `ProgramObject.h` 一处,`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。 -8. **一份反射归档能否服务三个消费者**(Espryt 读前端表、Magma 跑 SPIRV-Reflect、`DirectVulkan.cpp` 为 `glGetProgramResource*` 又反射一遍)。**P4a 答了 Espryt 那一半:能,而且不需要复制**——`create_shader_state` 过线的是逐 stage SPIR-V 与归档 blob 加上身份与序号,monolith 下 Espryt 仍直接读前端那份归档(D-H3),`ProgramArtifactsCodec.{h,cpp}` 只在 verify 构建里被调用,所以句柄化没有给热路径添一次编解码;换句话说"一份归档服务 Espryt"在 monolith 上已经是事实,跨进程时它就是那条 blob。**Magma 的两个消费者仍未答**(P7):`DirectVulkan.cpp` 为 `glGetProgramResource*` 重跑一遍反射这件事,P4a 没有碰。 -9. **viewport-array 回放能否塞进一次 `draw_vbo`**:`EndViewportRoutingPasses` 会 `InvalidateSyncedRenderState`,各遍之间观察到的状态是否与今天一致未验证。 -10. **`ResidentSubData` 的不对称怎么收口。** null 项保住今天的行为;给 Magma 补真实现是行为变更,独立 `dev` PR。**P3a 的处置(仍开放)**:不对称原样保留。`MGPipeResourceOps::SubDataResident` 是 `kOptional`、允许为 null,前端检查它就像今天检查 `g_bufferBackendOps->ResidentSubData` 一样;**P3a 没有给 Magma 补实现**(Magma 的 buffer 路径是 P7),`kCapResidentSubData` 也没有接线。所以这条问题原样留给 P7 / 独立 `dev` PR。 -11. **`SEG_STAGE` 的上限。** P5b 目标负载已实测单次 128 MiB 上传,超过默认 32 MiB;完整 trace 普查及 Redmi 四臂统一显式设置 256 MiB。默认不变,分块路径尚未实现;P8 仍需 MC in-world / Create 的占用分布与更大 blob 的 carrier 设计。 +1. **client 侧 dirty 走查的真实每 draw CPU 代价。** 已答(P2–P4a):推送没有在拉取基线之下净减少;Release 下 P2 边界 +6–12%,P3a 在 VAO 切换密的 rd12 上再加 +17–19 pt,P4a 在 Espryt 上 +3.4–5.7 pt(`MEASUREMENTS.md` §3–§5)。用户 2026-09-08 接受,性能自此只记录。 +2. **真实语料上纹理重铸拉取的发生率。** 已答(P4a):可忽略,保留 LRU 维持默认 0——79 例 × 两后端 780 个统计窗口里共 2 次(`iris-photon`、`iris-derivative` 各 1,只在 DirectGLES)。但 P5b 下这条路径是具名 Fatal,正是这两条 trace 加 `create-indirect` GLES 的首阻塞(P9)。 +3. **spike B 的 `untrusted_app` 域复核。** 两台设备的分档在 `shell` 域测得;从应用进程再跑一次 `extmem_probe`(spike A 的 exec 钩子已可用)。P11 前做。 +4. **渲染状态的 wire 粒度。** 已答(P2):16 个边界 / 15 个 chunk,7 pipeline 396 B + 8 dynamic 772 B;CSO LRU 64 暂定,P13 重调。 +5. **无存储的 capability。** 已答(P2):`FramebufferSrgb`、`DepthClamp`、`TextureCubeMapSeamless` 三个都补了真存储,`sizeof(RenderStateParameters)` 仍 1168。 +6. **具名 UBO host payload 的形状(D-B8)。** Magma 在 26.3 世界每帧重打包 331 KB 具名 UBO 字节,Espryt 为 0。要么冻结第二变长尾形状,要么走备选(Magma 直接描述符绑定常驻 `VkBuffer` range,独立 `dev` PR)。P7。 +7. **`MG_Util` 的切割缝。** server 需要 SPIRV-Cross / ESSL 转译缓存 / 格式处理器 / POST 探针,client 需要 glslang 与反射层;`MG_Util` 内部是否有干净的 Transpile-vs-Reflect 缝未审计。P7。 +8. **一份反射归档能否服务三个消费者。** Espryt 那一半已答(P4a):能且不需要复制。Magma 的两个消费者(`DirectVulkan.cpp` 为 `glGetProgramResource*` 重跑反射)未答。P7。 +9. **viewport-array 回放能否塞进一次 `draw_vbo`**:各遍之间观察到的状态是否与今天一致未验证。P8。 +10. **`ResidentSubData` 的不对称。** P3a 原样保留:`SubDataResident` 是 `kOptional`,Magma 未补实现、`kCapResidentSubData` 未接线。P7 / 独立 `dev` PR。 +11. **`SEG_STAGE` 的上限。** 目标负载已实测单次 128 MiB 上传,超过默认 32 MiB;普查与 Redmi 统一显式 256 MiB,默认不变,分块路径未实现。P8 需要 MC in-world / Create 的占用分布与更大 blob 的 carrier 设计。 12. **P13 之后 split-only 渲染 bug 的 server 侧第二意见。** verify 构建 + recorder 只覆盖推送内容,不覆盖后端对它的解释。 -13. **烘焙后的内部 shader 能否在没有活 `ProgramObject` 的情况下表达 uniform location 与 UBO 布局。** 未做原型。 -14. **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。 -15. **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口**(compute 写的 indirect buffer)。独立 `dev` 问题,拆分不得借机顺手修。 -16. **索引宿主镜像的实际内存占用。** MC/Sodium/Iris 语料里 element-array buffer 总量未测;若显著超 64 MiB,退化路径的频率与代价必须实测。 -17. **create-indirect fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;它是 P3a/P8 验收清单里的用例,需要先在 `dev` 上修。**P3a 出口的状态:仍然开放,仍是 `dev` 侧的活。** P3a 没有碰它,也不该碰(`ROADMAP.md:7` 的纪律:拆分不借机顺手修 `dev` 的 bug)。**P4a 出口的状态:设备侧照旧开放(本分支没碰,也没有新证据),但桌面侧这条 fixture 在本波中间坏过一次又被修好,值得记下来**——Espryt 的 per-draw 包把 `MGPImageView::Level` 改成从推送记录里读之后,`create-indirect` 的桌面 SSIM 从 0.999961 掉到 0.887403:根因不在那个包,而在 `Tracker.h` 的 `NewShaderImages` 快门只混了三个前端计数器,`glBindImageTexture` 用同一张纹理换一个 level 重新绑定时**一个都不动**,于是 applier 留着上一次绑定的 `Level`,第一个真去读这个字段的消费者就画错 mip(Flywheel/Create 恰好这么用,79 例里只有这一条能发现)。修法是把 `ctx.GetTextureBindGeneration()` 混进那个快门(`glBindImageTexture` 本来就会动它,不必新增计数器——新增会撑大 pull 构建的对象、G1 不允许)。这也是 P4a 之后写进 P4b brief 的那张"记录字段 → setter → 快门"表的由来。处置沿用 P2:`minecraft-1.21.1-neoforge-create-indirect-in-world` **留在桌面 79 例 SSIM 语料里**(G3/G3b 的具名扫描包含它,桌面栈上它是通过的),**排除在两台设备的 A/B 之外**(D.4.2);因此 G3b 记为"桌面全绿,设备侧因开放问题 17 而部分",不当作设备侧的通过。P8 要在这条 fixture 上断言 `roundtrips-per-frame == 0`,所以 `dev` 的修复在别人的关键路径上,不在 P3a 的。 +13. **烘焙后的内部 shader 能否在没有活 `ProgramObject` 的情况下表达 uniform location 与 UBO 布局。** 未做原型。P7。 +14. **推送模型改变哪些按拉取模式调过的缓存命中率。** 幸存者容量在 P13 重调。一条线索:设备上 26.3 Espryt 的 `sve` ≈ draw 数(每 draw 重发一次 sampler-view 集合),桌面只有 ~0.07/draw;列入 P3b/P4b 优化清单。 +15. **monolith 的 `*IndirectCount` 不调 `SyncGpuWrites()` 是不是潜在缺口。** 独立 `dev` 问题,拆分不得借机顺手修。 +16. **索引宿主镜像的实际内存占用。** MC / Sodium / Iris 语料里 element-array buffer 总量未测;若显著超 64 MiB,退化路径的频率与代价必须实测。P8。 +17. **`create-indirect` fixture 在 Adreno 830 上的失败**是 `dev@81b17c0b` 就有的(基线 APK 复现),不是本分支造成;`rd12` + Magma 在两台 Adreno 830 上的 `scudo::reportMapError` 崩溃同样是 `dev` 侧。两者排除在设备 A/B 之外、留在桌面语料里;P8 要在 `create-indirect` 上断言 `roundtrips-per-frame == 0`,所以 `dev` 的修复在别人的关键路径上。 diff --git a/docs/Disaggregated/devices/pin-verification-2026-09-07.md b/docs/Disaggregated/devices/pin-verification-2026-09-07.md index 5cb91ab8..71d18ce9 100644 --- a/docs/Disaggregated/devices/pin-verification-2026-09-07.md +++ b/docs/Disaggregated/devices/pin-verification-2026-09-07.md @@ -1,417 +1,28 @@ -# P2 frequency-pin profiles: what was read, what was written, what was proven +# 设备定频档案与核验 -Date: 2026-09-07. Devices: `35d0befa` (Xiaomi 24129PN74C, Snapdragon 8 Elite / SM8750, Adreno -830v2) and `3B159D009VZ00000` (Oppo PLG110 / ColorOS, MediaTek MT6993 "Dimensity 9500", Mali). -Both rooted via Magisk (`su` returns uid 0 in context `u:r:magisk:s0`). Host: Windows, Git Bash, -`MSYS_NO_PATHCONV=1` on every adb invocation. +本分支性能 A/B 所用设备的定频方式与核验结果。规则:每次运行前后各跑一次 `pin_device.sh check`,DRIFT 的运行作废重跑;stock 值硬编码、不从已钉设备采样(否则一次 restore 会把钉频变成永久);温度门 40 °C;reboot-clean、同热窗口、两臂背靠背。 -Outputs, all in this directory: +## 当前战役设备(2026-09-11 起,P4a 之后唯一用于 A/B 的设备) -| file | what it is | -|---|---| -| `xiaomi-adreno830.env` | completed profile, `PROFILE_VERIFIED=1` | -| `oppo-mali.env` | completed profile, `PROFILE_VERIFIED=1` | -| `pin_device.sh` | standalone ` pin\|unpin\|check` helper, pure adb + su | -| `REPORT.md` | this file | - -Both devices were left **unpinned**, confirmed by `pin_device.sh check` at the end -(transcripts at the bottom). Every node written was restored. - ---- - -## Headline: bench.sh cannot pin either of these devices - -This is the finding that matters most, because it inverts an assumption written into the -existing profiles. - -The `pin_freqs()` / `unpin_freqs()` pair in bench.sh writes -`/proc/ppm/policy/hard_userlimit_{min,max}_cpu_freq` and `/proc/gpufreq/gpufreq_opp_freq`. -On these devices: - -| path | `35d0befa` (Qualcomm) | `3B159D009VZ00000` (MediaTek) | -|---|---|---| -| `/proc/ppm/policy/` | absent | **absent** | -| `/proc/gpufreq/` | absent | **absent** (superseded by `/proc/gpufreqv2/`) | - -The Qualcomm case was already anticipated - `xiaomi-adreno830.env` said so. The MediaTek case -was **not**: the old `oppo-mali.env` reasoned "this part is a MediaTek SoC, so unlike the Adreno -profile the harness's existing /proc/ppm + /proc/gpufreq pin path is probably the right one" and -set `PIN_STYLE=ppm`. It is not right. MT6993 is new enough to have dropped both legacy -interfaces. So the exact failure mode the `PROFILE_VERIFIED` guard was written to prevent - a -root `echo` redirect into a missing /proc path exiting 0, and the run then reporting itself as -pinned - was waiting on the device the guard's own comment guessed was safe. - -Consequence for P2: run bench.sh / session.sh with **`--no-pin`**, drive the pin with -`pin_device.sh`, and use `pin_device.sh check` in place of the `big_cur`/`little_cur`/`gpu_cur_khz` -integrity fields. `PROFILE_VERIFIED=1` in the two profiles certifies *the nodes and pins in those -files*, not that bench.sh can drive them; both files say so in a header block. Teaching bench.sh -a `PIN_STYLE` switch is the real fix and is not done here. - ---- - -## Device 1 - `35d0befa`, Xiaomi 24129PN74C / SM8750 / Adreno 830v2 - -### CPU: which policy is "big", which is "little" - -`ro.board.platform=sun`, `ro.soc.model=SM8750`. Two policies, and **no true little cluster** - -SM8750 is a 2+6 part: - -``` -policy0 cpus 0 1 2 3 4 5 gov=walt cpuinfo 384000..3532800 (6x performance) - avail: 384000 556800 748800 960000 1152000 1363200 1555200 1785600 1996800 2227200 - 2400000 2745600 2918400 3072000 3321600 3532800 -policy6 cpus 6 7 gov=walt cpuinfo 1017600..4320000 (2x prime) - avail: 1017600 1209600 1401600 1689600 1958400 2246400 2438400 2649600 2841600 3072000 - 3283200 3513600 3801600 4089600 4204800 4320000 -``` - -So **big = policy6, little = policy0**, and both protocol targets are *exact* members of -`scaling_available_frequencies` - no rounding: - -| role | policy | target | chosen | rounding | -|---|---|---|---|---| -| big | policy6 | 1.96 GHz | **1958400** | exact | -| little | policy0 | 1.55 GHz | **1555200** | exact | - -### CPU: how to pin, and does it hold - -Method: `scaling_min_freq = scaling_max_freq = target`, **stock `walt` governor left alone**. No -performance or userspace governor is needed - once min == max the governor has no room, and not -switching it means nothing extra to restore. (`migov walt conservative powersave performance -schedutil` are all offered.) - -Writes returned exit 0 and took effect immediately. Held for the whole window under load -(8 concurrent `yes > /dev/null`), sampled every 5 s: - -``` -t=1 p0_cur=1555200 p0_min=1555200 p0_max=1555200 p6_cur=1958400 p6_min=1958400 p6_max=1958400 gpuclk=1100000000 cpuss-0-0=47100 -t=2 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=49400 -t=3 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=49800 -t=4 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=50200 -t=5 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=51300 -t=6 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=52100 -t=7 p0_cur=1555200 (min/max same) p6_cur=1958400 (min/max same) gpuclk=1100000000 cpuss-0-0=52100 -``` - -7 samples over 30 s, **zero drift** on either cluster or the GPU, while the SoC heated 47 -> 52 C. - -Restore sequence (verified back to the stock snapshot): - -``` -policy6: scaling_min_freq=1017600 scaling_max_freq=2841600 -policy0: scaling_min_freq=556800 scaling_max_freq=2745600 -``` - -`pin_device.sh` writes min -> `cpuinfo_min_freq` first, then max, then min, so the order works -regardless of where stock sits relative to the target (writing min above the current max is -clamped by cpufreq). - -### GPU: kgsl pwrlevels - -``` -gpu_model=Adreno830v2 num_pwrlevels=14 default_pwrlevel=12 -gpu_available_frequencies: 1100000000 1050000000 967000000 900000000 832000000 734000000 - 660000000 607000000 525000000 443000000 389000000 342000000 - 222000000 160000000 -stock: min_pwrlevel=12 max_pwrlevel=0 gpuclk=222000000 - devfreq/governor=msm-adreno-tz devfreq/min_freq=160000000 devfreq/max_freq=1050000000 -``` - -Pin: `echo 0 > /sys/class/kgsl/kgsl-3d0/min_pwrlevel` and the same to `max_pwrlevel`, collapsing -the range onto level 0. `gpuclk` read `1100000000` immediately and for the whole 30 s window. - -Worth noting: **the devfreq route cannot reach the top step.** `devfreq/max_freq` is 1050000000, -one OPP below level 0, so a `devfreq/min_freq` + `max_freq` pin tops out at 1050 MHz. The -pwrlevel pin unlocks 1100 MHz. Restore: `min_pwrlevel=12`, `max_pwrlevel=0`. - -**Unit trap.** bench.sh reports `gpu_cur_khz` from `$GPU_CURFREQ_NODE` via `awk` taking the last -field. The kgsl `gpuclk` node is in **Hz**; the MediaTek `current_freqency` node is in **kHz**. -So on this device the field bench.sh calls `gpu_cur_khz` is actually Hz. The profile carries -`GPU_PIN_HZ=1100000000` and `GPU_CURFREQ_UNIT=hz` alongside a schema-compatible -`GPU_PIN_KHZ=1100000`; compare against the former. - -**GPU util node corrected.** The draft profile had `GPU_UTIL_NODE=/sys/class/kgsl/kgsl-3d0/gpubusy`. -Reading `gpubusy` prints `263821 1008410` - busy and total *cycles* - and bench.sh takes the -first field, so it would have logged raw cycle counts as a busy percentage. Changed to -`gpu_busy_percentage`, which prints `26 %` and whose first field is what bench.sh means. - -### Thermal - -84 zones; type strings are unique, so the match-first-zone-by-type loop in bench.sh is -unambiguous. Chosen: **`cpuss-0-0`** (`thermal_zone13`), the CPU-subsystem sensor for the 6-core -cluster that carries most of the load, and the hottest of the candidates under the load test: - -| zone | idle | end of 30 s load | -|---|---|---| -| `cpuss-0-0` (13) | 34700 | 52100 | -| `cpuss-1-0` (20) | - | 49000 | -| `quiet_therm` (71) | 29835 | 32668 (skin sensor, not SoC) | -| `gpuss-0` (23) | 31200 | - | - -The 40 C gate is reachable: it idles at 34.7 C, and was observed between 36.6 and 40.1 C across -the session. - ---- - -## Device 2 - `3B159D009VZ00000`, Oppo PLG110 / MT6993 / Mali - -### CPU: three policies, and the one that would have silently ruined the pin - -`ro.soc.model=MT6993`. MT6993 is a **4+3+1** part: - -``` -policy0 cpus 0 1 2 3 gov=sugov_ext cpuinfo 300000..2700000 stock max 2100000 -policy4 cpus 4 5 6 gov=sugov_ext cpuinfo 300000..3500000 stock max 3500000 -policy7 cpu 7 gov=sugov_ext cpuinfo 300000..4210000 stock max 3200000 -``` - -Decision: **big = policy4, little = policy0**, *and policy7 is pinned to the big target as well*. -policy4 rather than policy7 is the structural analogue of the 2-core policy6 on the Xiaomi - a -multi-core cluster the render and worker threads can share, rather than a single core they would -contend for. But policy7 cannot be left alone: an unpinned 4.21 GHz core defeats the entire pin -the moment the scheduler lands a hot thread on it, and nothing in the result JSON would show it. -Pinned, the device reads as 4 little + 4 big. - -Neither target is an exact OPP here - both are the **nearest available step**: - -| role | policy | target | candidates | chosen | offset | +| 设备 | SoC / GPU | 大核 | 小核 | GPU | 备注 | |---|---|---|---|---|---| -| big | policy4 | 1958400 | 1900000 (-58400), 2000000 (+41600) | **2000000** | +2.1% | -| little | policy0 | 1555200 | 1500000 (-55200), 1600000 (+44800) | **1600000** | +2.9% | -| extra | policy7 | 1958400 | 2000000 is an exact member | **2000000** | +2.1% | +| Redmi M332BF `2f7cbe2e` | SM8750 / Adreno 830v2 | policy6 → 1958400(stock 1017600–3072000) | policy0 → 1555200(stock 556800–2745600) | kgsl `min/max_pwrlevel = 0`;2026-09-11 实测 1050 MHz(厂商 `thermal_pwrlevel` 锁 1),2026-09-16 起实测 1100 MHz | 主动风扇 `/sys/class/xm_power/hw_monitor/pwm_fan` 恒 level 2,用例间降温 <1 min;温度门 `cpuss-0-0` | -Carry that 2-3% offset when comparing absolute per-thread CPU cost against the Xiaomi. +与小米 `35d0befa` 同 SoC、同 OPP 表,两台的钉频数值可比;跨钉频口径(1050 vs 1100 MHz)的活动不可比钟频,只有同场配对可比。Redmi 的行目前只在 `~/w7/notes/p2/devices/pin_device.sh`(树外,`~/w7/notes/tools/p5b_codex_redmi.sh` 引用它);树内 `tools/device_bench/pin_device.sh` 只含下面两台,把 Redmi 行并回树内是待办。 -### CPU: how to pin, and does it hold +## 2026-09-07 核验:小米 `35d0befa` 与 Oppo `3B159D009VZ00000`(P2 / P3a 使用) -Same method (min = max = target, stock `sugov_ext` left alone). All six writes returned 0. -Held under the same 8-way load, sampled every 5 s, in `cur/min-max` form: +结论:`bench.sh` 的 `/proc/ppm` + `/proc/gpufreq` 钉法在两台上都不存在(MT6993 已改 `/proc/gpufreqv2/`),所以用 `bench.sh --no-pin` + `tools/device_bench/pin_device.sh pin|unpin|check`(纯 adb + su;exit 0 PINNED / 1 DRIFT / 2 UNPINNED,UNPINNED 非零使 `check && measure` 不可能量到未钉设备)。`devices/*.env` 的 `PROFILE_VERIFIED=1` 只认证文件里的节点与钉值,不认证 `bench.sh` 能驱动它们;给 `bench.sh` 加 `PIN_STYLE` 仍未做。 -``` -t=1 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=52833 -t=2 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=56859 -t=3 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=58063 -t=4 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=59661 -t=5 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=60353 -t=6 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=61354 -t=7 p0=1600000/1600000-1600000 p4=2000000/2000000-2000000 p7=2000000/2000000-2000000 gpu="0 1716000" soc_max=62025 -``` +| | 小米 24129PN74C(SM8750 / Adreno 830v2) | Oppo PLG110(MT6993 / Mali,gpufreqv2) | +|---|---|---| +| 大核 | policy6 → 1958400(精确 OPP;stock 1017600–2841600) | policy4 → 2000000(最近 OPP,+2.1%;stock 300000–3500000,厂商守护随后改回 3200000) | +| 小核 | policy0 → 1555200(精确;stock 556800–2745600) | policy0 → 1600000(+2.9%;stock 300000–2100000) | +| 额外 | — | policy7(单核 4.21 GHz)同钉 2000000,否则一个热线程就打破钉频 | +| 方法 | `scaling_min_freq = scaling_max_freq = 目标`;governor(walt / sugov_ext)不动;写序 min→`cpuinfo_min_freq`、max→目标、min→目标(min 高于当前 max 会被 cpufreq 钳住) | 同左 | +| GPU | `kgsl-3d0/{min,max}_pwrlevel = 0` → 1100 MHz(devfreq 路径封顶 1050);`gpuclk` 单位是 Hz | `/proc/gpufreqv2/fix_target_opp_index = 0` → 1716000 kHz,恢复写 −1;不用 ged `custom_*`(取 OPP 下标、且被 urcc-service 争写) | +| 温度门 | `cpuss-0-0`(zone13),idle 34.7 °C | `soc_max`(zone14),idle 36–37 °C,门只勉强可达 | +| 30 s 八路负载核验 | 7 个样本零漂移,47 → 52 °C | 7 个样本零漂移,53 → 62 °C | +| 陷阱 | `gpubusy` 打的是周期数,busy 百分比读 `gpu_busy_percentage` | `scaling_governor/min/max` 是 0660 system,必须经 su 读,否则空值看起来像在工作 | -7 samples over 30 s, **zero drift** on all three policies, SoC 52.8 -> 62.0 C. The ColorOS -performance daemons did not contend for the cpufreq nodes during the window. - -**Read permissions.** `scaling_governor`, `scaling_min_freq` and `scaling_max_freq` are `0660 -system:system` here. A plain `adb shell cat .../scaling_governor` answers `Permission denied`, -which a careless parser reads as an empty governor rather than a failure. Every read goes through -su. Note `scaling_cur_freq` and `scaling_available_frequencies` *are* world-readable, which makes -the trap worse - a script can look like it is working. - -### GPU: gpufreqv2, not legacy gpufreq - -57 working OPPs, index 0 fastest: - -``` -/proc/gpufreqv2/gpu_working_opp_table - [00] freq: 1716000 [01] 1690000 [02] 1664000 ... [56] 390000 (kHz) -``` - -Pin: `echo 0 > /proc/gpufreqv2/fix_target_opp_index`; restore: `echo -1`. Evidence: - -``` -before : [GPUFREQ-DEBUG] fix GPU/STACK OPP index is disabled -after 0 : [GPUFREQ-DEBUG] fix GPU/STACK OPP index: 0/0 - /sys/kernel/ged/hal/current_freqency = "0 1716000" (index 0, 1716000 kHz) -after -1: [GPUFREQ-DEBUG] fix GPU/STACK OPP index is disabled -``` - -**Deliberately not the ged `custom_boost_gpu_freq` / `custom_upbound_gpu_freq` pair**, the usual -MTK route, for two reasons both visible in the nodes: - -1. On this kernel they take an **OPP index, not kHz**. Stock reads `boost=56`, `upbound=0` - a - floor at the slowest OPP and a ceiling at the fastest. Porting the kHz-valued - `GPU_PIN_KHZ=902000` from odinlite.env into them would write garbage. -2. Reading them prints a request log showing - `/odm/bin/hw/vendor.oplus.hardware.urcc-service` (pid 1534) writing the same nodes. A pin - there is one voter among several. `fix_target_opp_index` has no such contention. - -### Thermal - -87 zones, unique types. Chosen: **`soc_max`** (`thermal_zone14`) - the SoC-wide max aggregate, -which is what "SoC temperature" means for a gate, and the strictest candidate. - -Cooldown series after the load test (screen awake, idle), 15 s apart: - -``` -soc_max 39094 37177 36806 35745 36954 36312 -soc-top0 36361 35082 34641 34226 34079 34031 -ap_ntc 36414 35034 34342 33965 33746 33566 -``` - -It settles to **36-37 C**, so `THERMAL_START_MAX_MC=40000` is reachable - but only just, and it -was still reading 42.4 C several minutes after ordinary use. Expect this gate to actually wait, -unlike the one on the Xiaomi. Under load it went 52.8 -> 62.0 C in 30 s. - ---- - -## Every node read or written - -Read-only (both devices): `/sys/devices/system/cpu/cpufreq/policy*/{affected_cpus, -scaling_governor,scaling_available_governors,scaling_available_frequencies,cpuinfo_min_freq, -cpuinfo_max_freq,scaling_cur_freq}`, `/sys/class/thermal/thermal_zone*/{type,temp}`, -`getprop`, `id`. - -Read-only, Xiaomi: `/sys/class/kgsl/kgsl-3d0/{gpu_model,num_pwrlevels,default_pwrlevel, -thermal_pwrlevel,max_gpuclk,gpuclk,clock_mhz,max_clock_mhz,min_clock_mhz, -gpu_available_frequencies,freq_table_mhz,gpubusy,gpu_busy_percentage,throttling,temp}`, -`/sys/class/kgsl/kgsl-3d0/devfreq/{name,governor,available_governors,available_frequencies, -cur_freq,min_freq,max_freq,target_freq,gpu_load,mod_percent}`, `/sys/class/devfreq/`. - -Read-only, Oppo: `/proc/gpufreqv2/{gpufreq_status,gpu_working_opp_table,fix_target_opp_index}`, -`/sys/kernel/ged/hal/{current_freqency,custom_boost_gpu_freq,custom_upbound_gpu_freq, -gpu_utilization,total_gpu_freq_level_count}`. - -**Written** (all restored): - -| device | node | stock | pinned to | restored to | -|---|---|---|---|---| -| Xiaomi | `policy6/scaling_min_freq` | 1017600 | 1958400 | 1017600 | -| Xiaomi | `policy6/scaling_max_freq` | 2841600 | 1958400 | 2841600 | -| Xiaomi | `policy0/scaling_min_freq` | 556800 | 1555200 | 556800 | -| Xiaomi | `policy0/scaling_max_freq` | 2745600 | 1555200 | 2745600 | -| Xiaomi | `kgsl-3d0/min_pwrlevel` | 12 | 0 | 12 | -| Xiaomi | `kgsl-3d0/max_pwrlevel` | 0 | 0 | 0 | -| Oppo | `policy0/scaling_min_freq` | 300000 | 1600000 | 300000 | -| Oppo | `policy0/scaling_max_freq` | 2100000 | 1600000 | 2100000 | -| Oppo | `policy4/scaling_min_freq` | 300000 | 2000000 | 300000 | -| Oppo | `policy4/scaling_max_freq` | 3500000 | 2000000 | 3500000, then 3200000 by the vendor daemon - see below | -| Oppo | `policy7/scaling_min_freq` | 300000 | 2000000 | 300000 | -| Oppo | `policy7/scaling_max_freq` | 3200000 | 2000000 | 3200000 | -| Oppo | `/proc/gpufreqv2/fix_target_opp_index` | disabled | 0 | -1 (disabled) | - -No governor was ever written on either device; both stayed on their vendor governor (`walt`, -`sugov_ext`) throughout. Nothing else was written - no `settings put`, no thermal-engine -tampering, no app installs. - -### The one restore that does not settle at its stock value - -On the Oppo, `policy4/scaling_max_freq` was restored to its snapshotted 3500000 and a ColorOS -daemon lowered it to **3200000** by itself within seconds. This is correct behaviour - the range -is handed back and the vendor takes over - but it means **stock maxima on this device are not -constants**. - -It caught a real bug in the first version of `pin_device.sh check`, which classified each node as -at-pin / at-stock / neither and reported DRIFT for anything else: it declared DRIFT on a device -that had been released perfectly. The classifier now asserts only against *our* pins (pinned -means `min == max == pin`; anything else is not pinned, with a `min == max` at some other value -flagged as an external clamp, which is equally fatal to comparability). The hardcoded stock -values still drive the restore path, where handing the range back is all they have to do. - ---- - -## `pin_device.sh` - -Usage: `pin_device.sh pin|unpin|check`. Pure adb + su; no profile sourcing, no bench.sh. -The device table (policies, pins, stock values, GPU style, thermal zone type) is embedded so it -has no inputs to get wrong. All three actions print the live big/little/GPU frequencies, -governors and gate temperature, so the output of `pin` and of `unpin` is itself the before/after -evidence. - -Exit codes: **0** PINNED (every pinned node at its pin and every live frequency equal to it), -**1** DRIFT (partly pinned, externally clamped, or a live frequency has left its pin - discard any -overlapping run), **2** UNPINNED (no node is at a pin this script set). UNPINNED is non-zero on -purpose, so that `pin_device.sh X check && measure` cannot silently measure an unpinned device. - -Three things it does that are load-bearing: - -- **Write order** is min -> `cpuinfo_min_freq`, then max -> target, then min -> target. Setting - min above the current max is clamped by cpufreq, so a naive two-write pin half-applies - depending on where stock sits relative to the target. -- **Stock values are hardcoded, not sampled at pin time.** A restore that read "stock" off an - already-pinned device would make the pin permanent. -- **The device-side read emits raw node contents and parses locally.** The read is delivered as a - single-quoted `su -c` argument, so a single quote inside it (an awk program, a sed expression) - closes that quoting and the whole read returns nothing. That bug was hit during this work: the - Oppo `check` printed `` for every field and reported DRIFT on a correctly pinned - device. A zero-key read is now a hard error (exit 66), not a verdict. - -### Final `check` on both devices, unpinned state - -``` -$ pin_device.sh 35d0befa check -== check == - device : Xiaomi 24129PN74C / SM8750 / Adreno 830v2 (35d0befa) - big policy6 gov=walt cur=2841600 min=1017600 max=2841600 (pin 1958400 / stock 1017600-2841600) - little policy0 gov=walt cur=2400000 min=556800 max=2745600 (pin 1555200 / stock 556800-2745600) - gpu kgsl-3d0 pwrlevel=0..12 freq=222000000 Hz busy=7% (pin lvl 0 = 1100000000 Hz / stock lvl 0..12) - thermal cpuss-0-0 38200 mC = 38.2 C (/sys/class/thermal/thermal_zone13) - - VERDICT: UNPINNED - none of the 3 nodes is at a pin this script set; the vendor - governors have their range back and nothing this script writes is in effect. -rc=2 -``` - -``` -$ pin_device.sh 3B159D009VZ00000 check -== check == - device : Oppo PLG110 / MT6993 / Mali (gpufreqv2) (3B159D009VZ00000) - big policy4 gov=sugov_ext cur=300000 min=300000 max=3200000 (pin 2000000 / stock 300000-3500000) - little policy0 gov=sugov_ext cur=2100000 min=300000 max=2100000 (pin 1600000 / stock 300000-2100000) - extra policy7 gov=sugov_ext cur=2750000 min=300000 max=3200000 (pin 2000000 / stock 300000-3200000) - gpu gpufreqv2 fix_opp=off freq=1508000 kHz busy=0% (pin idx 0 = 1716000 kHz / stock off) - thermal soc_max 45292 mC = 45.3 C (/sys/class/thermal/thermal_zone14) - - VERDICT: UNPINNED - none of the 4 nodes is at a pin this script set; the vendor - governors have their range back and nothing this script writes is in effect. - note: big(policy4) unpinned, range 300000-3200000 (recorded stock 300000-3500000, which vendor daemons move) -rc=2 -``` - -Both report their stock governors (`walt`, `sugov_ext`), the GPU back on DVFS -(pwrlevel 0..12 / `fix_opp=off`), and cpufreq ranges wide open. Nothing written is still in -effect. - -For the record, the pinned half of the same cycle on each device: - -``` -$ pin_device.sh 35d0befa pin (after pin) - big policy6 gov=walt cur=1958400 min=1958400 max=1958400 - little policy0 gov=walt cur=1555200 min=1555200 max=1555200 - gpu kgsl-3d0 pwrlevel=0..0 freq=1100000000 Hz - VERDICT: PINNED - all 3 pinned nodes at their pins, live frequencies match. rc=0 - -$ pin_device.sh 3B159D009VZ00000 pin (after pin) - big policy4 gov=sugov_ext cur=2000000 min=2000000 max=2000000 - little policy0 gov=sugov_ext cur=1600000 min=1600000 max=1600000 - extra policy7 gov=sugov_ext cur=2000000 min=2000000 max=2000000 - gpu gpufreqv2 fix_opp=0 freq=1716000 kHz - VERDICT: PINNED - all 4 pinned nodes at their pins, live frequencies match. rc=0 -``` - ---- - -## What is NOT verified - -1. **bench.sh cannot drive these pins.** Not fixed here. The profiles are verified; the harness - integration is not. Use `--no-pin` plus `pin_device.sh` until a `PIN_STYLE` switch exists. -2. **The Oppo GPU pin was confirmed at the DVFS-request level, not under graphics load.** The - pinned window was a CPU load, so the GPU was power-collapsed throughout (`gpufreq_status` - showed `PowerCount: 0`). `current_freqency` reported OPP index 0 / 1716000 kHz for the whole - window - which is exactly the field bench.sh samples as `gpu_cur_khz`, so the check bench.sh - would perform does pass - but nobody has watched this GPU actually clock 1716 MHz while - rendering. Related readout quirk: while powered down, `/proc/gpufreqv2/gpufreq_status` keeps - printing `Freq: 26000` for the fixed OPP entry. That is a parked rail, not the pin failing; - `current_freqency` is the node to believe. -3. **Pin durability over a full bench window is untested.** The verification window is 30 s of - synthetic CPU load, which is what the README defines. Neither device was held pinned through a - 180 s warmup plus a 30-sample measurement with the game running, where a vendor game-boost - daemon has far more reason to intervene than it did here. Running `pin_device.sh check` after - each run is what closes that gap; treat `rc=1` as "discard the run". -4. **Thermal gate behaviour under the real protocol is untested.** 40 C is reachable on both, but - on the Oppo it idles only 3-4 C below the gate, so back-to-back paired runs will spend real - time waiting. Nobody has measured how long. -5. **No reboot test.** All pins were applied and released within one session. Nothing here - persists across a reboot by construction (every node written is volatile sysfs or procfs), but - that was not demonstrated. -6. **The `walt` governor and the Xiaomi thermal daemon were never provoked.** 30 s at 52 C is - well short of the thermal-limit regime where `thermal_pwrlevel` (read 0 throughout) would - start capping the GPU. +两台核验后均已解钉并复核 UNPINNED,写过的节点全部恢复。未核验:Oppo GPU 钉在图形负载下的实际时钟(核验窗口 GPU 处于 power-collapse)、整场 bench 窗口内的钉频持久性、重启后状态、厂商热管理介入。