[Docs] (docs/Disaggregated, MG_Impl/Pipe, MG_Remote/Client): shorten long comments and move their design arguments into docs

This commit is contained in:
2026-10-15 21:18:49 -04:00
parent 7ec1448f03
commit 37ecba200b
26 changed files with 704 additions and 2401 deletions
@@ -49,3 +49,10 @@ client 的回绕 `Uint16` 版本计数器永远不是新鲜度的唯一证明:
- CSO(render state、vertex elements、sampler、sampler view、shader)在 **client 侧内容寻址**(Mesa `cso_cache` 先例),LRU 淘汰时发 `delete_*`;render-state 容量 64(P13 重调)、sampler state 256。
- Buffer / Texture / Renderbuffer 走 create / respecify / subdata / destroy;Framebuffer / Xfb 是 per-context 身份 + `set_*` payload。
- 两条偏离:**D-G1** Espryt 的 vertex-elements 按 VAO 身份寻址(twin 持驱动 VAO 名,不能共享);**D-F2** Espryt 的 sampler view 按纹理身份寻址,sampler state 内容寻址且带引用计数(被 `BuiltinSampler` 指着的项不许被 LRU 挤掉)。
#### 2.3.1 sampler state 缓存:引用计数与值生命周期
sampler state 在 client 侧按值寻址(`MGPipeSamplerCsoCache`,容量 256):哈希、探测、再 memcmp 确认;只凭 64 位哈希相等会把两种状态并进一个 CSO,造成没有门看得见的错误过滤。
- 活句柄躲开 LRU 靠引用计数而不是容量:已发布的 `MGPTextureParams` 在纹理参数不动时一直指着它的 `BuiltinSampler`,applier 不解析这个句柄,而淘汰不算参数变化,既不会重发也没人拒绝。所以每次 `Acquire` 取一个引用、`Release` 还一个;LRU 只考虑引用为 0 的项;全被钉住时超容量铸造并计入 `OverCapacityMints`(多一个槽费内存,句柄被抽走费正确性)。
- 持有引用的是纹理的内建 sampler(纹理死亡或参数变化重取时释放)和 `bind_sampler_states` 当前绑定的每个 CSO;凡在返回后还会指名该句柄的调用方都必须持有。容量大于 `kMGPipeMaxTextureUnits` 只是尺寸界:装不下一整轮发射的缓存会每轮都超容量铸造。
- 槽不带 lifetime id:CSO 属于值而不属于前端对象(别的 `SamplerObject` 可能持有同值),`~SamplerObject` 不释放任何槽,唯一死亡路径是 client 侧的 LRU 淘汰。因此来自 `~SamplerObject` 的 `NotifyStateObjectDestroyed(SamplerCso, lifetimeId)` 背后永远没有句柄;后端不得按 `SamplerObject` 的 lifetime id 键控 sampler twin,twin 的生命只是 `create_sampler_state` 到 LRU 发出的 `delete_sampler_state`。
+30
View File
@@ -25,6 +25,22 @@ per-setter 推送会把 Blaze3D 每个 batch 的冗余 `glEnable/glDisable` 变
位答案由脚本推导:RenderState 族与 `RenderState.cpp` 双向比对(`BumpVersions()` 动两个计数器,裸 `++m_version` 只动 `NEW_RENDER_STATE`);其余位把快门读的成员 / 字段与 mutator 传递写入的成员 / 字段求交,只能查出"该位的快门在任何路径上都不动"这一种错答,证明不了每条路径都动。写入根定位不了的函数被污染,依赖它的答案为 UNDECIDED,须列入 `MGP_DIRTY_SURFACE_UNDECIDED_LIST`,列了却已可判定同样报红。散文答案不经推导(`kPulledPartialShutter` 所附的位除外);扫描器按文本匹配函数体,lambda 里的 mutator 记到外层函数上,经 helper 发布的修改读作延迟发布,语义证明仍是 `MOBILEGL_PIPE_VERIFY`。
#### 5.2.2 记录字段 → setter → 快门对照表
由 `Tracker.h` 的 dirty 位触发的每条记录,其每个字段只能经由会推动该位快门输入的 setter 改变,否则必须在 setter 处无条件发射(`resource_*` 族、`set_texture_params`)。`DirtySurface.def` 只把 mutator 映射到位,看不出派生字段依赖另一族的 mutator,所以下表手工维护;记录新增字段时在此加一行。
| 位 | 记录与字段 | 改变字段的 setter | 快门输入 |
|---|---|---|---|
| 6 | `create/bind_shader_state`、`set_draw/dispatch_program`:Cso、StageMask、GlobalUboSize、artefact blob ref、两个已绑定句柄 | `glUseProgram`、`glLinkProgram`、`glBindProgramPipeline`、`glUseProgramStages` | lifetime id × link version;pipeline 下为 `stageLinks`(pipeline 名 × 各 stage 的 lifetime id 与 link version) |
| 7 | program 绑定:image unit、block binding、uniform 写集合 | opaque uniform 上的 `glUniform1i`、`glUniformBlockBinding`、`glShaderStorageBlockBinding`、任意 `glUniform*` | 四个 per-program 计数器;pipeline 下再混入 `stageLinks` |
| 8 | `set_global_constants`:ShaderCso、Version、默认 uniform 块镜像 | 默认块上的 `glUniform*`、`glUseProgram` | lifetime id × UBO content version;pipeline 下为 `stageState` |
| 11 | `set_framebuffer_state`:Fbo、Color[8]/Depth/Stencil/ReadSurface(Res、Kind、InternalFormat、TextureTarget、Layered、Level、Layer、UploadTarget)、DrawBuffers[8]、Width/Height/Layers/Samples/FixedSampleLocations、IsDefault、Complete、Target | `glFramebufferTexture*`、`glFramebufferRenderbuffer`、`glDrawBuffer(s)`、`glReadBuffer`、`glFramebufferParameteri`、`glBindFramebuffer`;已挂接纹理或 renderbuffer 的存储重定义(格式、纹理目标、尺寸、采样数、完备性在发射时内联,所以 `PipePublishDescriptor` bump attachment 聚合) | attachment 聚合 × draw / read 绑定槽版本 |
| 12 | `set_sampler_views`:每单元 {View, Texture} | `glBindTexture`、`glActiveTexture`、纹理或 sampler 参数、定义 level 的上传、默认纹理出现;在用 program(`glUseProgram`、relink、sampler uniform 的 `glUniform1i`) | content × params × bind generation × `opaqueUnits` |
| 13 | `bind_sampler_states`:每单元 sampler CSO 句柄 | `glBindSampler`、`glDeleteSamplers`、`glSamplerParameter*`、`glTexParameter*` | params × sampling resolution × bind generation |
| 14 | `set_shader_images`:program image-unit 窗口内每单元 {Res, InternalFormat, Layer, Level, Layered, Access} | `glBindImageTexture`、纹理 content / params、image uniform 的 `glUniform1i`;在用 program(`glUseProgram`、relink) | content × params × bind generation × `programImages` |
位 11–14 还在 share group 的孪生聚合时钟变化时触发(setter 经兄弟 context 调用);最大触及纹理单元变化时 12、13 也触发。
### 5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集(D-B1)
```
@@ -50,6 +66,14 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与
- `resource_create` 在前端对象构造时发,存储由 `resource_respecify` 惰性定义,`resource_destroy` 在析构时发;死亡转发给每个 emitter(只释放 slot 曾导致 UAF)。view 先于存储属主销毁、FBO attachment 钉住纹理、buffer texture 钉住 buffer。
- program pipeline 合成体(`CompositeResolver`)完全在前端合成、推一个句柄;释放恰好一次(resolver 换掉或 `~ProgramObject`,D-H7);记忆按 `(ContextId, 管线 GL 名)` 键控。
#### 5.6.1 program pipeline 合成体的句柄、释放与记忆键
- 合成体由 `GLContext::GetProgramForDraw()` 在前端合成、缓存在各自的 `ProgramPipelineObject` 里;program 发射器按合成体自己的 lifetime id 从 `ShaderCso` 保留高段(§2.1)给它一个句柄,照普通 program 发 `create_shader_state`,server 不知道它是合成体。`MGPipeSlotAllocator::Allocate` 拒绝该段,`AllocateComposite` 是唯一入口;用保留段而不用句柄 flag,是因为 flag 要被两侧每个 `ShaderCso` 消费者携带和屏蔽,而保留段让"普通 program 拿不到合成体 slot"由分配器保证。
- slot 有两条释放路径:签名变化时 `MGPipeCompositeResolver::Observe` 释放旧合成体,以及合成体自己的 `~ProgramObject`。两条都走 `MGPipeEmitShaderCsoDestroyAndFree`,后到的一条是空操作(`MGPipeSlotAllocator::Free` 拒绝不在该世代 live 的 slot,也不推进世代)。只靠析构不够:前端一旦多持一份引用,签名变化时析构就不会先到。
- 签名取 `ComputeDrawProgramSignature()`(每个图形 stage 的 lifetime id 与 link version),不含 `GetBackendStateVersion()`:后者在 sampler uniform 的 `glUniform1i` 时也会变,会让合成体每次 draw 都重新编译。`Tracker.h` 的位 6 快门读同一组值。
- 记忆键是 `(GLContext::GetTextureContextId(), 管线 GL 名)`:resolver 是进程单例,而 pipeline 名按 context 分配,只按名键控会在 make-current 后命中另一 context 的同名项、释放一个仍活着的合成体,每次切换都重建 program;context 地址也不行,销毁重建的 context 会落在旧地址上,而这个 id 永不复用。
- context 销毁时其合成体经 `~ProgramObject` 释放,残留项再也查不到;make-current 时的 `Reset()` 丢弃它们而不释放,表长因此只受合成体 slot 仍 live 的项限制。
### 5.7 emulation 的归属
规则:**驱动表达不了的变换在 tracker 里 lowering,硬件 / 驱动强加的变换在 driver 里 lowering。**
@@ -77,6 +101,12 @@ shader 看见哪个纹理取决于 sampler uniform 类型、mipmap 完备性与
- 只换元数据(`RespecifyRedefinesNoStorage`:sticky 的 `BindMask` / `ImageBindableHint` 变化时重发的描述符)不丢任何项。
- buffer 没有待上传项,也永远不算只换元数据。
#### 6.1.1 region 列表是精确脏足迹
纹理 `resource_subdata` 的 region 列表是该 level 自上次变干净以来的**精确脏足迹**:互不相交的盒子,覆盖每个写过的纹素,不多一个(`MGPipeTextureEmitter::EmitOneLevel`)。原因:server 端图像一旦可能含 GPU 写入,超出应用写入范围的上传就会盖掉它们。Magma 总把列表当作要上传的纹素;Espryt 在驱动纹理可能含 GPU 写入时同样如此,否则仍自己在 box 与 rect 之间选上传形状(Mali 作业数照旧由 server 决定)。
- `RegionCount == 0` 只表示整 level 写;其余写入一律带盒子(只有一个也带),所以 applier 的累积不会因为某一侧「只有 box」而把精确贡献放宽成并集盒。
- 例外:足迹超过一条记录能带的 `kMGPipeMaxPendingUploadRegions` 个盒子时,退回存储的 rect 列表(只保证覆盖,日志记一次);该列表又被扣下(rect 填满并集盒的 3/4)时,0 是一个**不精确**的部分盒。把「只有 box」读作「client 拥有盒内每个纹素」的读者(`AdoptDriverLevelIntoShadow` 的覆盖盒规则)只在这种情况下不精确。
## 7. Shader state = SPIR-V + 反射归档
- `CreateShaderState` 过线的是逐 stage SPIR-V + 反射归档,**不是源码**:glslang 全在 client、SPIRV-Cross 全在 server,文件级切割;server 侧无编译池。归档序列化器 `ProgramArtifactsCodec`,monolith 只在 verify 构建里调它(D-H3)。
@@ -22,6 +22,15 @@ client 的残余填充在自己的 verb 边界给全部字段盖戳,server 读
- 非边界记录(如两次 draw 之间的 `set_dynamic_state`)不盖戳,沿用前一边界的序号、掩码与 verb 名,但盖戳标志已在上一条记录后清除,读检查按盖戳之外处理;20 个 draw 入口都盖成 `DrawArrays`,掩码正确,Fatal 里的名字是泛称。
- 读检查只在 server 盖戳的 verb 内按归属类(§3.1 的 G8)分流:`RECORD_SUPPLIED` / `APPLIER_DERIVED` 读到旧值是 `Fatal{RoleViolation}`,`FATAL` 直接中止,`BARRIER_PULLED`(仅供生成器对照,没有生产行)在该 verb 类内计入 `rsp`,由 `CountBarrierPull` 决定何时改为中止(记录未 barrier、两角色用不同块、strict 下未准入)。盖戳之外一律是普通 poison `Fatal{UnmigratedPipeInput}`。
- 盖戳标志由 `PipeApplier::LeaveApplier` 在每条记录之后清除;server 进程里没有 client 半边替它清,漏清会让之后的读都按上一个 verb 的掩码判定。
#### 9.2.2 run-ahead 下 client 对 `PipeInputs` 的写
run-ahead 下,unbarriered 记录 apply 期间 `PipeInputs` 是 server 角色内存:对这类 verb,client 不填充、不盖戳、不撤 server 的戳,只跑 tracker 与 emitter(`MGPipeValidateForVerb` 的 `fillOwed`)。GL 线程唯一合法的写是 barriered verb 的填充,而且写的那一刻就得成立:
- 验证点的顺序是填充 → 发射 → park,填充时 apply 线程可能还在执行先前发布的 unbarriered 记录,所以每次写之前 `QuiesceApplierBeforeFill` 先等 applier 追上最后发布的序号(`ClientSession::WaitForApplyToCatchUp`,与 `glFinish` 同一种强制等待)。
- 块的写分两段、夹着记录发布:发射前写 verb 序号、撤 server 戳与 verb 名,发射后做残余字段遍历;发出的记录 apply 时要读这块,一次等待盖不住两段。
- `ClientSession::RefusePipeInputsTouchWhileApplierOwnsIt` 查的是事实(apply 线程不在 applier 内),不采信调用方的声明;漏掉等待即 `Fatal{RoleViolation, "gPipeInputs"}`。
- lockstep 下 `appliedSeq` 已在最后发布的序号上,等待立即返回。client 与 server 用不同块时(`MGPipeBlocksAreDistinct`:分角色演练或 spawn),client 只写自己的块、不必等待,server 块的戳只由 `PipeApplier::LeaveApplier` 清除。
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
# define MGB_CTX (::MG_State::pGLContext)
@@ -29,6 +29,22 @@ T2 下 client 侧推送三件套:不做 map/unmap 命令对(payload 带 `has
- `resource_respecify`(`MGPipeResourceRespecifyWantsItsReply`):wire 臂上 buffer 那半不等;纹理那半等到对象的 create 被接受(`MGPipeHandleIsPublished`),之后 acceptance 已是 client 知道的事实,首次使用时付一次阻塞的 create;create 被拒则闩不置位,后续照常等真答案,代价是吞吐而不是静默。
- monolith 下答案在调用线程上直接返回、没有代价,谓词保持原答案(纹理那半照收)。
#### 13.1.2 client 侧保守 GPU 写集合
`BufferObject::SyncGpuWrites` 在应用线程上、于 `glMapBuffer` / `glGetBufferSubData` 时回答"GPU 是否可能写过这块 buffer"。后端的标记点在 server 侧,逐次去问是稳态禁止的 roundtrip,所以 client 在每条 draw / dispatch 记录发出之前,从自己持有的绑定构建这个集合(`MG_Impl/Pipe/Verb/GpuWriteSet.h`)。
- 宁多勿少:多标一次的代价是一次回读,漏标则静默读到陈旧 shadow。每个生产者有计数器(`ProducerMarkCount`),单元用例断言它确实触发。
- 前四个生产者对应后端标记点:SSBO 绑定点、atomic counter 绑定点(标全部 touched 点,比后端宽)、`Access != GL_READ_ONLY` 的 image buffer texture、XFB 捕获目标。后端故意窄的两处照搬:只读 image 绑定不标;image 遍历在 draw 准备时做,不在 `glBindImageTexture` 时做。
- 后端没有的两个生产者:读进 pack PBO 的 `glReadPixels` 发出即返回、只做标记,拷贝推迟到第一次真要读时;传输下 `glEndTransformFeedback` 不再无界等 fence,只标记捕获目标(monolith 仍等)。
- 收窄尚未实现:`MGPipeClientOnGpuWritten` 只接受一个整 buffer 范围;零个范围留给将来的"已完全收窄",不能当作整块已写。
- 不覆盖后端自己的 scratch buffer(Espryt 的转换顶点流与 primitive-restart 替换 buffer、Magma 的 UBO ring):后端那边同样没有 `MarkGpuWritten`,是两边共有的洞。
#### 13.1.3 glReadPixels:线上只有矩形,pack 状态留在 client
`read_pixels` 记录带的是矩形而不是布局:`MGPReadbackInfo::DstSize` 是紧凑的 `w*h*bytesPerPixel`,server 用中性 pack 状态读进这段紧凑行(就是 reply payload),client——唯一持有应用 pack 状态的一侧——再按应用的 `GL_PACK_*` 把各行散射到应用指针,行间空隙不写。若 `DstSize` 取打包后的尺寸,server 按它分配、后端又按实时 pack 状态写,就会越界(4x3 RGBA8、`PACK_ROW_LENGTH=8`、`SKIP_ROWS=1`、`SKIP_PIXELS=2`:分配 80 B,驱动写到第 120 B)。规则只关乎布局,深度与深度模板读同样适用。
- 紧凑答案大于一个 reply slot(`LinkTerms.maxReplyBytes`)时切的是读,不是 reply:一行放得下就切成 `floor(maxReplyBytes / rowBytes)` 行的整宽行带,否则把每行切成 `floor(maxReplyBytes / bpp)` 像素的列段。每段是一条普通 `read_pixels` 记录,带自己的框与紧凑 `DstSize`,各自按 pack 状态散射;段按行主序恰好覆盖矩形一次,且在紧凑布局里连续,中性 pack 的快路径因此仍可直接读进应用指针(`PlanReadbackBands` / `ForEachReadbackBand`)。
- 只有连一个像素都放不进 reply(含没有 reply 池的链路)时,client 才在发射前具名拒绝为 `Fatal{ReplyTooLarge}`:像素不能拆得比自身更小。
### 13.2 五部分验证门(取代 monolith 的字节一致门)
1. **接口纯度**:A 门 include 图(`scripts/check_include_closure.py`)、B 门 server 镜像不引用 `MG_State::GLState` 与 glslang(P6 的 a6 实测 server 仍链 `MG_Impl`,由 `scripts/link_ratchet.py` 的下行棘轮追踪;P13 把 172 条降到 27 条具名债务、`# P13` 注解清零并由脚本拒绝回潮,W7 的 `MobileGL_server_linkcheck` 把它变成真链接)、C 门 `MG_Backend` 里零 `pGLContext`;外加"每个后端 memo 键都是 `{slot, gen}`"(`HandleRecycleScenario`)。