[Docs] (Disaggregated): record P4a as landed - the fourteen family calls and six re-keyed kinds, the seven contract corrections and the defect each one closed, the three-arm A/B on a pinned Adreno 830 with active cooling, the upload shape that did not move, and the seam taxonomy the next brief has to carry

This commit is contained in:
rereview
2026-09-11 11:37:08 -04:00
parent 8c458cd594
commit a29807cc48
4 changed files with 130 additions and 17 deletions
+14 -7
View File
@@ -62,6 +62,8 @@
CSO 在 client 侧内容寻址(Mesa `cso_cache` 先例):每类一张 `ska::flat_hash_map<xxHash, MGPipeHandle>`,容量上限 render-state 64 / vertex-elements 1024 / sampler 256 / sampler-view 4096 / shader 跟随 `ProgramObject` 生命周期,LRU 淘汰时发 `delete_*`。两个不同 program 设置了相同状态时 server 零状态转换。
> **[deviation] D-F2P4a 落地)**:上面这行的 **sampler-view 4096 项内容寻址是 P7Magma)的形状**Espryt 这一波把 sampler **view** 做成**按纹理对象身份寻址**——一纹理一视图,按纹理自己的 lifetime id 铸造,视图限制变了就在**同一个句柄**上重发。理由是 Espryt 的视图没有可共享的驱动侧对象,内容寻址只会多一张表和一次哈希。sampler **state** 的 256 项内容寻址照做,并且 P4a 给它加了**引用计数**`MGPTextureParams::BuiltinSampler` 指着的项不允许被 LRU 挤掉(全被引用时超容铸造,计数在 `OverCapacityMints`),否则一次驱逐就会让一条标准记录指向一个已经换代的句柄。
**[deviation] D-G1P3a 落地):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 已落地)
@@ -97,7 +99,7 @@ Flags`kNeedsAck`(调用方等 server 确认;目录里目前无条目携
- 今天 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` 需要在前端参数版本不动时强制重同步。
- `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 blockD6):`globalUboScratch` 是 link phase B 的 CPU 数组,没有 GL name、没有 `BufferObject`
@@ -131,9 +133,9 @@ Flags`kNeedsAck`(调用方等 server 确认;目录里目前无条目携
| `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 / 32 | view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、`ForceResync`)挂在纹理对象上 |
| `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 推送时零查表);`ContentHash` 既是 server 的 render-pass memo 键也是 client 的发射抑制器 |
| `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 |
| `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 数组下标对齐 |
@@ -209,7 +211,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
- `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 合成体:`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handleslot 从 `ShaderCso` 保留高位段分配,pipeline cache 淘汰时释放 slot、`gen++`、发 `delete_shader_state`。合成体从不过线,server 不需要任何"解析后的 draw program"钩子;副带收益是阻塞的 `JoinLinkAndSpirv()` 离开 server 的 draw path。
- program pipeline 合成体**P4a 落地:`MobileGL/MG_Impl/Pipe/CompositeResolver.h`**`GLContext::GetProgramForDraw()` 今天就完全在前端合成(join、签名查 cache、`Link(true)`)。tracker 拿到 `SharedPtr<ProgramObject>` 推**一个** handleslot 从 `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。
### 5.7 emulation 的归属
@@ -252,13 +254,15 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
- `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 回退生成 mipRGB16F/RGB32F)与 `glCopyImageSubData` 目的地镜像——分别由 `OnTextureWriteback` 与"CopyImage 镜像搬到 client"处理。
- Unpack PBO 完全在 client 解析;压缩纹理永不到达后端;`glCopyTexSubImage*``glClearTexImage` 整体留在 client(今天就是纯前端操作:借一次 `ReadPixels` 进 CPU scratch 再写 shadow),拆分后恰好是一次阻塞 ReadPixels round trip,脏区按普通 subdata 下发。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 的 payload 是逐 stage SPIR-V + 反射归档(`LinkArtifacts` + `SpirvArtifacts` 全结构体),**不是源码**。"server 从源码重新 link"这条路显式关闭:链接真 `ProgramObject` 就链接 glslang。glslang 全在 clientSPIRV-Cross`TranspileSpirvToEssl`)全在 server,文件级切割。没有 `MOBILEGL_IPC_PROGRAM` 开关、没有 server 侧 compile pool。
- 归档机制:`Visit()` + `sizeof` 绊线(`static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)`),一份字段表服务序列化两个方向。必须覆盖四个 `ResourceReflection`(各带 `TypeFacts`)、`uniformSamplerOrImageUnitIndex``uniformBlockBinding``shaderStorageBlockBinding`(按名字)、`explicitOpaqueUniformBindings``xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture``computeLocalSize`、GS/TCS/TES 事实、`usesReservedNumSamples``uniformOffsets``XfbVarying` 带两套拼写(GL 名字 + block 实例/成员/元素)。
- 归档机制:`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 `<Includes.h>` 与容器),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)。
@@ -292,7 +296,7 @@ GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 samp
### 8.3 错误、ack 与日志
- 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(`glTexImage*`/`glTexStorage*``MarkStorageDirty`Espryt 惰性分配;连 `glRenderbufferStorage*` 也在 `SyncToBackend` 里惰性做),拆分不改变可观察行为,这批不同步 ack。
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack41 个 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 把门铃接到这个谓词上。
- **唯一允许同步 ack 的入口是 `glBufferStorage`(真同步分配)**。`glRenderbufferStorage*` 不 ack41 个 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 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。
@@ -320,6 +324,8 @@ Magma`VulkanRenderer` 全部 memo 与 scratch、`PipelineFactory`、`ProgramF
从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。
**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+descriptor34 个 `MOBILEGL_ASSERT(pGLContext)` 里的 9 个随之消失);两个内部 shader 烘焙(§7)。Espryt 的小号同类:`g_rawDepthFetchSamplerState` → 后端原生 sampler。
### 9.2 strangler 脚手架:`PipeInputs` + 逐 verb 填充 + poison 世代(P1
@@ -372,6 +378,7 @@ Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一
- **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-B5verify 构建永不出货);三道纯度门在非 verify 构建上转绿。
@@ -595,7 +602,7 @@ CMake
| 变量 | 默认 | 说明 |
|---|---|---|
| `MOBILEGL_PIPE_PUSH` | pull 构建 `0`**push 构建 `0x1ff`**`kMGPipeSubsystemsMigratedAtP3a``MobileGL/MG_Pipe/MGPipe.h:95`,读入点 `MobileGL/ConfigLoader.cpp:257`P2 的默认是 `0x7f` = `kMGPipeSubsystemsMigratedAtP2``MGPipe.h:94`,保留作分阶段对照 | 子系统位图,十进制或 `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 slotsTrack H)、`0x40` Magma vertex inputTrack H)、**`0x80` 位 7 resourcesP3a`resource_*` 家族,`kMGPipeSubsystemResources``MGPipe.h:84`**、**`0x100` 位 8 vertex inputP3avertex 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_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 要位 7buffer 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 slotsTrack H)、`0x40` Magma vertex inputTrack H)、**`0x80` 位 7 resourcesP3a`resource_*` 家族,`kMGPipeSubsystemResources``MGPipe.h:84`**、**`0x100` 位 8 vertex inputP3avertex 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) |