79 KiB
MGPipe 设计与架构
本文只写已决定的设计,每条决定附一行理由。落地状态: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:逐字节渲染状态镜像、twin registry、三条 persistent ring;Magma:SetupDrawSnapshot、pipeline memo、五个 Vk*Manager)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化推过去,后端不再拉 MG_State::pGLContext;server 进程因此只装 MG_Backend + MGPipe 对象表,不链接 MG_State、MG_Impl、glslang。
接口是从两个后端自己维护的关键结构反推出来的(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(其余全部),由 PipeCalls.def 经 G1 生成(MG_Pipe/generated/PipeTables.inc)。
- 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(
gBackendFunctionsTable),null 项已经表示"未实现,前端回退",MG_Test已用替换整张表的方式 mock 后端。 - 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flow。
- EGL 生命周期 8 项与 caps 面留在
pActiveBackendObject的虚函数上(罕见路径)。
1.3 三种形态,一份后端
| 形态 | 表里装的是什么 | 用途 |
|---|---|---|
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() 里一个 #if MOBILEGL_BUILD_DISAGGREGATED 分支:MG_Config::Transport != Monolith 时装 MG_Remote::BackendObject_Remote,否则走今天的 switch。MG_Impl 的边界调用点零 #ifdef。
2. 对象模型
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³² 次才回绕,debug 分配器断言回绕。- kind:
Buffer, Texture, Renderbuffer, Framebuffer, Xfb, RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso, Fence, Query, Context。 - 保留句柄:
{0,0}= null;{0,1}ofFramebuffer= 默认帧缓冲(退役 Espryt 的defaultFBO身份比较);ShaderCsoslot 空间的高段保留给 program pipeline 合成体(kMGPipeShaderCsoCompositeSlotBase)。 - GL name 只以
GlNameForDiag出现在MGPResourceDesc里,永不做身份、永不进 memo 键或 content hash;GetLifetimeId()留在 client 作 tracker 自己的身份,client 维护lifetimeId → slot。
2.2 两种世代,严格分开
| 拥有者 | 回答 | 过线 | |
|---|---|---|---|
MGPipeHandle::Gen |
client | "还是同一个 GL 对象吗?" | 是 |
MGGen(g_bufferMutationEpoch、m_textureImageEpoch 等后端纪元) |
server | "我自己是否重铸了驱动对象?" | 永不;server→client 只以纹理拉取请求出现(§8.4) |
规范:任何 MGPipe 调用不得要求 client 提供或知晓 MGGen;client 的回绕 Uint16 版本计数器永远不是新鲜度的唯一证明——过线时要么加宽、要么与 {slot, gen} 同行。
2.3 CSO 与可变对象
| 类别 | 形态 | 对应后端已有缓存 |
|---|---|---|
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 先例):每类一张 flat_hash_map<xxHash, MGPipeHandle>,LRU 淘汰时发 delete_*;render-state 容量 64(kMGPipeCsoCacheCapacity,P13 重调)、sampler state 256。两条已落地的偏离:
- 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),否则一条标准记录会指向已换代的句柄。
3. 调用目录
3.1 单一真相源
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 钉住。
生成器(产物提交进树,CI pipe-gates 重生成并 git diff --exit-code):
| 产物 | 内容 | |
|---|---|---|
| G1 | PipeTables.inc |
两张函数指针表 |
| G2 | PipeThunks.inc |
monolith 直调 thunk MGP_<Name>(),MG_Impl 的边界站点逐名改到它上面 |
| G3 | PipeWire.inc |
wire 记录 + 每种一条尺寸 static_assert + applier 分发前的运行期边界检查 → Fatal{ProtocolCorruption} |
| 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 不允许符号位移) |
另有 PipeFillPoints.inc(FillPoints.def,83 条 MGP_FILL 覆盖 69 个 verb)与 DirtySurface.def(gen_pipe_dirty_surface.py,§5.2)。
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 |
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 |
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、kHasBlob(B,payload 拥有一个 MGPBlobRef 成员,不弱于此)、kVarTail(V)、kHostSpan(H)、kReplySlot(R,答进 MGPReplySlot)、kOptional(O,后端表里可为 null:Magma 不注册 BufferSubDataResident 与 SetSwapInterval)。
- 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(后两项已从 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)、kCapBackendOwnsXfbCapture(P5b)。CallMask 取代"槽位是否为 null"这个隐式能力探测。
不存在 kCapPrimitiveRestart / kCapMultiDraw* 一类"归属开关"(D-B7):multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。 MGPCaps = DynamicBackendParameters 整块 + CallMask + 两个 blob(format 能力表、renderer 字符串),握手后一次快照,取代 40 个 pActiveBackendObject-> 站点与 89 个 caps 读点。
4. 记录与 payload 约定(MG_Pipe/MGPipeTypes.h)
- 每个 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 就是记录序数(producerm_emitSeq++/ consumerm_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 是纹理视图的存储属主;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;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_* 都带 ContentHash——hash 未变就不发(§5.4)。
5. 前端 state tracker(MG_Impl/Pipe/Tracker)
5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里
Blaze3D 每个 batch 用 glEnable/glDisable(GL_BLEND) 包住,per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium st_validate_state。
九个 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)。
5.2 dirty 位:值类零新增记账,对象类新增 5 个聚合世代
| 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 |
值 | 既有计数器 |
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() 前缀 |
五个聚合世代落在既有 bump 点上,把对象类组的快门降成一次 Uint64 比较;对象类不能靠轮询逐对象版本(没有聚合能回答"有没有哪张已绑定纹理动了")。完整性由 scripts/gen_pipe_dirty_surface.py 保证:枚举每个 mutator → 必须 bump 的聚合世代,未映射或映射行点名一个已不存在的 mutator 即失败(--check + --self-test 都是 CI 门)。快门看不见自己的主体是 P4a 踩过三次的坑(glBindSampler、SSO 下 GetCurrentProgram()、glBindImageTexture 只换 level)——新增字段必须带"记录字段 → setter → 快门"一行,且优先混入已有世代(新增计数器会撑大 pull 对象,G1 不允许)。
三个回绕 Uint16 在 tracker 边界加宽(m_lastPushed[] 是 tracker 自己的字段);回绕在 tracker 本地无害(多一次重推,永不漏推)。
5.3 渲染状态:整块 blob 过线,身份只取 pipeline 子集,动态状态单独走(D-B1)
create_render_state(cso, MGPBlobRef pipelineSubsetChunks) // 只带 pipeline 子集
bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B
set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化 chunk
- 整块:
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_* 之间没有顺序要求。 退役 Espryt 的 fragColor 重推导 workaround、g_broadcastMemo* 与 ImageUnitFormatsStillMatch 的机制是惰性特化,不是调用顺序。
create_shader_state 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server。
合并规则:整块结构优于逐字段;高水位标记留在 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 完备性与 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 钉住纹理、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 的归属
规则:驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。 只有三个"读前端字节的纯 CPU 变换"下放到 client。
| emulation | 归属 | 过线的是什么 |
|---|---|---|
| client 顶点数组 | client | 字节(MGHostSpan),永不是指针 |
最大索引扫描(TryComputeMaxIndexFromHostBytes) |
client | MGPDrawInfo::MinIndex/MaxIndex(flag 门控) |
| client 索引数组 | client | 变长尾里的 MGHostSpan |
*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 | 只回"拷贝成功" |
| XFB CPU 图元计数 / scatter 的 read-modify-write | client | XfbCpuCapturedVertices、MGPXfbAccounting;§8.5 |
| 压缩纹理 / pixel unpack 规整 | client | 无 |
陈旧索引纪律是逐站点表,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 时刻按累积区域做(96-rect 级联合并、union-box 回退、unpack ring 可用时塌成一个 box)——Mali 按作业数给上传计价,~100 个精灵 rect 对一个 union box 实测 +6 ms/frame。逐glTexSubImage发一条记录会精确复现那个形状。- 因此 client 在自己的
MipmapStoragerect 模型里累积,在下一个 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全结构体),不是源码。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(不 includeMG_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(MG_Pipe/MGPipeCallbacks.h)
十个具名回调 + 一个正向终止符(ResourceSubDataComplete),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 SEG_EVENT 上的记录。
| 回调 | 取代 |
|---|---|
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 行为一致 |
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 原生;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 应用。反向通道需要与正向通道相同的有序保证。
8.3 错误、ack 与日志
- 纹理分配的 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) 预防主因: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(gl_SkipComponents 的空洞保留应用原本的内容)。server 没有 MappedData(),所以 server 把紧密打包的 scratch 经 OnBufferWriteback 推给 client,用 OnXfbScatterReady 告知布局;client 拥有目的 shadow 与反射归档里的 varying/stride,原样跑补丁循环,补好的范围作为普通 ResourceSubData 重发。不新增停顿类。
9. 后端状态机改造
9.1 原样不动的东西
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)。
字节一致门 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。
必须真改的 MG_State 类型内部用法(都在 Magma,P7):占位纹理(~120 行木偶戏 → 原生 VkImage)、两个内部 shader 烘焙(§7)。
9.2 strangler 脚手架:PipeInputs + 逐 verb 填充 + poison 世代(P1)
// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织
struct PipeInputs {
const RenderStateParameters& GetRenderStateParameters() const; // 类型与后端今天读到的完全一致
// … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 56)
#if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
Uint64 m_filledGen[kFieldCount]; // 逐字段"上次填充的 verb 序号"
Uint64 m_currentVerbSerial;
#endif
};
#if MOBILEGL_PIPE_PUSH
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
# define MGB_CTX (::MG_State::pGLContext)
#endif
| 阶段 | 改什么 | 证明 |
|---|---|---|
| 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) |
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(值类型:render state、pixel store、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma 标量 getter,约 B 类读点的 55%):机械。Track H(对象类型:167 个 SharedPtr<MG_State…> 点):真活。读点分类(静态):探测变化 ~12%、翻译输入 ~74%、瞬时参数 / 身份键 / 数据字节其余——74% 是翻译输入,"bump 一个版本让 server 自己拉"行不通,值本身必须过去。
9.4 残余值块
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、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 子系统位图在阶段 B 是真正的旧-vs-新 A/B;阶段 C 之后不是——位清零时后端仍跑重键后的 memo 代码。对策:编译期 MOBILEGL_PIPE_LEGACY_MEMOS(默认 ON)保留 pre-handle 臂活在同一个 PipeInputs 接口之下,随 pull 路径在 P13 退役。
- 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 就是这条的度量。
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/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 的分层倒置消失;一次 glslang 编译离开启动路径;inproc = 渲染线程;MG_Test 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是增加的,所以 monolith 论据是逐线程 CPU 数字,不是删除行数。
10.3 索引宿主镜像(Server/IndexHostMirror,P8)
覆盖 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. 传输与数据面(MobileGL/MG_Remote/)
11.1 段
| 段 | 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 内存 |
SEG_STAGE 默认 32 MiB 装不下 P5b 目标负载的单次 128 MiB 上传;默认不改,普查与设备用显式 MOBILEGL_IPC_STAGE_MB=256 profile,分块 / 专用 carrier 是 P8 的设计(ROADMAP.md 开放问题 11)。
创建(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 独立三元组;三个水位 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 记录;RingConsumer::Pop 拒绝不可能的头并置 corrupt → Fatal{ProtocolCorruption};HardDrainRing 只在两侧静默且 ring 全空时 bump generation。
11.3 双向 doorbell(Doorbell.h)
- client → server:consumer 自旋 → 置
consumerParked=1→ 阻塞;producer release-storecmdHead之后仅当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_cstfence 关闭。
11.4 控制面(protocol.fbs、Framing.h、ITransport.h)
- 一份 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),CIflatc-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。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 → 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) | 0 | 0 | 0 |
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"一类阈值。规则:每条记录(或每 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 事件回传与溢出(P9)
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 负载(P10)
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 指针;≥16 MiB 可变 store 由 TryAdoptLargeStorage 自动走到,实测 MC 26.3 p99 163→21 ms、40→115 fps、省 ~400 MB)。整个 monolith 改造期一动不动(D-B4)。
三档,由运行时 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)导入 |
唯一在两台设备、两个后端上都成立的完整读写档 |
| 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)数的是每一次 MapPersistent 发射,铸成还是拒绝都算——按"真正发生的往返"定义在 monolith 下按构造恒为 0,永远红不了;按获取尝试计数两种模式下数字相同、在 monolith 下就可断言(门:StorageBufferRegrowScenario、LargeArenaAdoptionScenario)。
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;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);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。
验收措辞:在全部 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数;零 timeout 轮询循环必须在有界时间内退出。
13.2 五部分验证门(取代 monolith 的字节一致门)
- 接口纯度三道门: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支撑(重键前必须在至少一个后端上是红的)。 - 语义影子比对
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。 - 行为 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 对它完全不敏感。 - 性能:Redmi
2f7cbe2ereboot-clean、同热窗口、配对 A/B(devices/pin-verification-2026-09-07.md),trace replay--benchmark逐帧 JSON(frameTimesMs[]+frameCpuTimesMs[]);指标是逐线程 CPU 时间,p50 与 p99;tracker 每 draw 的绝对 ns(DriverBenchT1/T2)与 Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。口径(用户 2026-09-08 起):对着 pull 臂记录,不作阻塞门;专门的优化阶段排在路线图之后。 - 覆盖 + poison + 句柄纪律:G6 重生成 0 UNMAPPED;
gen_pipe_dirty_surface.py0 未映射 mutator;G8 字段归属完备;逐 verb 世代 poison;G7 setter 一致性;ResidualValueBlock棘轮。
两条幸存的字节级等式: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 在发布配置里被编译掉,必须逐用例读自己的日志文件)。
13.3 长期语义门:MGPipe recorder(P13)
MG_Test 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 MG_State,也给 tools/trace_replay 一种记录已解析状态的录制格式。它只覆盖推送内容,不覆盖后端对它的解释。
14. Present、线程与帧节奏
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 与平台(P6 / P12)
15.1 启动与握手
- 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.defhash)不匹配 →Fatal{AbiMismatch};segment 尺寸尚未混入 fingerprint(ID-47 债)。mobilegl_server_main声明为extern "C" __attribute__((visibility("default")))(非 Debug 构建设了 hidden visibility)。
15.2 Android(spike A 已证,MEASUREMENTS.md §1.1)
- 交付链:APK 唯一可 exec 的位置是
lib/<abi>/,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 生产路径:JavaSurface→ Messenger/AIDL →MobileGLServerService(android:process=":mgl")→ JNIANativeWindow_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直接送;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 的架构)
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
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:
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:<path> | pipe:<name>(ConfigLoader.cpp解析;P5 只接受前两个,其余具名拒绝并回落 monolith),免费换来 ctestENVIRONMENT变体、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 / 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 默认开启。理由不是吞吐,而是 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}。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},未知 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
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(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,只向 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 对 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 加一。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。
17.4 lane 隔离与阶段边界
每条 DirectGLES.Split.* entry 有独立 MOBILEGL_LOG_FILE_PATH;E1/E3 控制从被选 entry 的私有文件取 Fatal,且把"选中条目被跳过"判为失败(ID-53/62)。integration-split 是缩减路径的硬门;broad inproc integration-gpu 车道只记录普查、不设门(ID-65)。
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:开关
CMake:
| 选项 | 默认 | 状态 |
|---|---|---|
MOBILEGL_BUILD_DISAGGREGATED |
OFF | 已落地 |
MOBILEGL_BUILD_DISAGGREGATED_INPROC |
OFF | 已落地(P5,隐含前者) |
MOBILEGL_BUILD_SERVER_SPIKE |
OFF(仅 Android) | 已落地(spike A,非出货) |
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、ConfigLoader.cpp):
| 变量 | 默认 | 说明 |
|---|---|---|
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) | 索引宿主镜像预算(P8) |
运行时,传输与 IPC:
| 变量 | 默认 | 说明 |
|---|---|---|
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 消费 |
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 直方图已实现但从未接进汇总行(未测)。