Files
MobileGL/docs/Disaggregated/ARCHITECTURE.md
T
swung0x48 5635e33ffe [Docs] (Disaggregated): record the P0.5 landing
- README status, the ROADMAP P0.5 row (measured outcome, the 8-includer correction, the
  DynamicBackendParameters exception) and the ARCHITECTURE note on which header gate A asserts
2026-09-05 23:22:15 -04:00

87 KiB
Raw Blame History

MGPipe 设计与架构

本文描述已决定的设计。每条决定附一行理由;数字凡有实测的取实测(见 MEASUREMENTS.md)。落地状态以 feat/disaggregated@458ccde1 为准:标注"P0 已落地"的是树里的代码,其余是后续阶段要实现的形状(阶段号见 ROADMAP.md)。

1. 边界

1.1 一句话

MG_Backend 已经是一台贴着目标 API 的状态机(Espryt 有逐字节的渲染状态镜像、6 个 twin registry、三条 persistent ringMagma 有 SetupDrawSnapshot、pipeline memo、5 个 Vk*Manager)。它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明:前端在每条 verb 之前把变化过去,后端不再拉 MG_State::pGLContext。server 进程因此只装 MG_Backend + MGPipe 对象表,不链接 MG_StateMG_Impl、glslang。

接口不是从 gallium 自顶向下设计的,而是从两个后端自己维护的关键结构反推出来的:SetupDrawSnapshot 的字段并集 → set_* 组;DrawTextureSyncKeysset_sampler_views+create_sampler_view+set_texture_paramsResolvedDrawBuffers/ResolvedVertexBindings → vertex elements 三件;g_syncedRenderStateParameters → render-state CSOUnpackStagingBlockMGPSubData 的 region 形状;BufferBackendOps7 个 hook,注释自称 pipe_context 类比)→ resource_* 全族。gallium 是目的地(词汇可读、可迁移),不是推导前提;与 gallium 的十条偏离见 §3.5。

1.2 两张函数指针表

MGPipeScreenshare-group 作用域:caps、resource、persistent map、fence)与 MGPipeContext(其余全部:query 命名空间、CSO、set_*、对象操作、verb),由 PipeCalls.def 经 G1 生成(MG_Pipe/generated/PipeTables.inc)。P0 已落地。

  • 函数指针 struct 而非虚基类:边界今天就是函数指针 struct(gBackendFunctionsTable);null 项已经表示"未实现,前端回退",正好就是"这个子系统还没迁移,继续拉取";MG_Test 已用替换整张表的方式 mock 后端。
  • 两张表从第一天分开:事后拆分意味着给记录重新编号。v1 只有一个 screen、一个 context、一条 flowpGLContext 是进程全局,share group 全库无人读取)。
  • 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 函数 两进程出货形态

唯一 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 尚未含它。)

2. 对象模型

2.1 句柄 = {slot, gen}P0 已落地,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 分配器断言回绕。
  • kindBuffer, 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)。
  • GL name 只以 GlNameForDiag 出现在 MGPResourceDesc 里,永不做身份、永不进 memo 键或 content hashGetLifetimeId() 留在 client 作 tracker 自己的身份,client 维护 lifetimeId → slot

2.2 两种世代,严格分开

拥有者 回答 过线
MGPipeHandle::Gen client "还是同一个 GL 对象吗?"
MGGeng_bufferMutationEpochm_textureImageEpochm_cacheStructureEpoch 等 12 个后端纪元) server "我自己是否重铸了驱动对象?" 永不server→client 只以纹理拉取请求出现(§8.4)

规范:任何 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_samplersBackendSamplerObject
SamplerViewCso create/delete + set_sampler_views TextureResource::{perMipViews,…}SyncTextureViewToBackend
ShaderCso create/bind/delete + server 侧惰性特化 ProgramFactory::m_cacheBackendProgramObjectImpl
RenderStateCso create/bind/delete,身份 = pipeline 子集 Espryt 值镜像;Magma ComputePipelineStateHash
Buffer / Texture / Renderbuffer create / respecify / subdata / destroy 各自 twin
Framebuffer / Xfb per-context 身份 + set_* payload BackendFramebufferObjectm_xfbCounterSlotByObject

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 零状态转换。

3. 调用目录(P0 已落地)

3.1 单一真相源

MobileGL/MG_Pipe/PipeCalls.def:一行一个调用 X(Name, PayloadStruct, Class, Flags)线上 opcode 就是行在文件里的位置(1-based),所以目录必须是唯一记录的集合,新调用只能追加到文件末尾、退役的调用保留槽位。MGP_CALL_LIST_DOCUMENTED_COUNT = 71MobileGL/MG_Pipe/PipeCalls.def:69)由 MG_Test/Pipe/PipeCatalogueTest.cpp 钉住。

七个生成器(scripts/gen_pipe.py,产物提交进树,CI pipe-gates 重生成并 git diff --exit-code):

产物 内容
G1 PipeTables.inc 两张函数指针表
G2 PipeThunks.inc monolith 直调 thunk MGP_<Name>()MG_Impl 的约 93 个 gBackendFunctionsTable.GL.* 站点逐名改到它上面
G3 PipeWire.inc wire 记录 + 每种一条尺寸 static_assert + applier 分发前的运行期边界检查 → Fatal{ProtocolCorruption}
G4 PipeVerify.inc MOBILEGL_PIPE_VERIFY 的逐字段比对器(字段表来自 PipeFields.def;浮点按位比较,NaN patch level 不会误报)
G5 PipeFilled.inc PipeInputs 字段 id61 个)与逐 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

3.2 分组与计数

Class 内容
kScreen 11 GetCaps(R)、ResourceCreate/Respecify/DestroyMapPersistent(R,O)/UnmapPersistent(O)、FenceCreate/Status(R)/Wait(R)/Destroy、追加的 FenceWaitServerglWaitSyncGPU 侧等待)
kCtxQuery 8 QueryCreate/Begin/End/Available(R)/Result(R)/Destroy、追加的 QueryTimestamp(R)glGetInteger64v(GL_TIMESTAMP))与 QueryCounterglQueryCounter
kCtxCso 13 create/delete × {render state, vertex elements, sampler, sampler view, shader} + bind × {render state, vertex elements, shader}sampler 与 sampler view 的绑定是下一组的批量调用
kCtxState 17 SetDynamicState(B)、SetFramebufferStateSetVertexBuffers(V)、SetIndexBufferSetIndirectBuffersSetSamplerViews(V)、BindSamplerStates(V)、SetShaderImages(V)、SetShaderBuffers(V,H)、SetStreamOutputTargets(V)、SetGlobalConstants(B)、SetVertexAttribDefaults(V)、SetPixelPackStateSetPatchStateSetDrawProgramSetDispatchProgram、迁移期临时的 SetResidualValueState(B)
kCtxObject 9 按资源寻址:SetTextureParamsResourceSubData(B,V)、BufferSubDataResident(B,O)、ResourceSubDataCompleteResourceFlushRangeResourceReadback(R)、ResourceCopyRegionGenerateMipmapGetTextureImage(R)
kCtxVerb 13 按上下文寻址:BlitClearReadPixels(R)、DrawVbo(H,V)、LaunchGridMemoryBarrierBegin/End/Pause/ResumeStreamOutputFlushPresentSetSwapInterval(O)

FlagskNeedsAck(调用方等 server 确认;目录里目前无条目携带,见 §8.3)、kHasBlob(B)、kVarTail(V)、kHostSpan(H)、kReplySlot(R,答进 MGPReplySlot,永不阻塞)、kOptional(O,后端表里可为 null:Magma 故意不注册 BufferSubDataResidentSetSwapInterval)。

  • 今天 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 需要在前端参数版本不动时强制重同步。
  • SetIndexBuffer 独立于 VAO 配置版本(D5):索引 slot 重绑不移动 VAO config version。
  • SetGlobalConstants 只覆盖默认 uniform blockD6):globalUboScratch 是 link phase B 的 CPU 数组,没有 GL name、没有 BufferObject

显式不移植GetIntegeri_v/GetInteger64i_v/GetProgramiv(后两项 P0 已从 GLFunctionsTable 删除,50815a23;唯一属于后端的带下标答案 GL_MAX_COMPUTE_WORK_GROUP_COUNT/SIZEMGPCapse8ee7b1aGL_COMPUTE_WORK_GROUP_SIZE 是前端反射查询)、ShaderStorageBlockBinding(折进反射归档)、set_pixel_unpack_state(不存在:前端已在 glTexImage 时解析压缩格式、强制默认 unpack)、压缩格式概念、pipe_transfer

3.3 能力位(MGPCapBit

kCapViewportArraykCapFloat64VertexAttribkCapResidentSubDatakCapCpuXfbPrimitiveAccountingkCapTimerQuerykCapOcclusionQuerykCapXfbPrimitivesQuerykCapNeedsHostIndexBytesserver 做 restart 重写 / multi-draw 展平,split 下开启索引宿主镜像,§10.3)、kCapNeedsHostUboBytesserver 把具名 UBO 打进自己的 ring,需要 SetShaderBuffers 的 host payload)。CallMask 取代"槽位是否为 null"这个隐式能力探测。

不存在 kCapPrimitiveRestart / kCapMultiDraw* 一类"归属开关"D-B7):ResolveTierForBatch 逐 batch 用 programReadsDrawID(转译后 ESSL 的性质,只存在于 server)选档,两个后端都做 restart 重写,所以这类归属不可用 cap 表达。规则一句话:multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说需要时提供索引字节。

一个待转显式能力位的现有陷阱:GL_Drawing.cppEndTransformFeedback 槽位的非空当作"后端按 GL 顶点序捕获"来跳过 FixupGsStripCaptureOrder。MGPipe 下改为显式 kCapDriverOrderedXfbCapture 一类的位(P8/P9)。

MGPCaps = DynamicBackendParameters(整块包含,~90 个标量含六个 compute 限制)+ CallMask + 两个 blobformat 能力表、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 命名传输段。
  • MGHostSpan32 BMG_Pipe/MGPipeHostSpan.h)是整份接口里唯一形状随传输而变的东西:monolith 下 Ptr 指向 shadow 或应用内存;split 下 Ptr == nullptr、字节在 Seg/Offset 命名的 SEG_STAGE,或 Seg == kMGHostSpanSegFromServerIndexMirrorMobileGL/MG_Pipe/MGPipeHostSpan.h:26)表示"字节已在你那边的索引镜像里"。MGPipeHostBytes() 是一次可预测分支;split 解析器 gMGPipeSegmentResolverMG_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 最多花 total8 字节),超过它的 payload(大 ResourceSubDataCreateShaderState 归档)由发射器切成多条;ring 对更大的记录直接拒绝(nullptr + MGLOG_E)而不是让 producer 等一个永远不够的空闲量。
  • MGPSubData 的 buffer 半边:Target == Buffer 时没有 level 与 box,目的字节范围搭在 UnionBox.Xoffset)与 UnionBox.Wsize)上,MGPipeSetSubDataBufferRange() 是唯一拼写;单条记录上限 offset 2³¹−1 / size 2³²−1,越界由发射器拆分。

4.1 关键 payload

payload 尺寸 要点
MGPResourceDesc 88 buffer / 全部纹理 target / renderbuffer 一个判别式 create/respecify 形状;BindMaskELEMENT_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 同时带解析后的 VertexAttribute[] VertexBufferBindingPoint[],缺一不可(pointer 调用的 stride 0 = element sizebinding 模型的 stride 0 = 每顶点读同一 element);IsLongType == Float64 分开携带;仅供查询的 LegacyStride/LegacyPointer 留在 client
MGPSamplerDesc 32 SamplerParameters 逐字节过线borderColorForm(三种 border color 表示永远都被数值填满,没有它后端无法在 Iiv/fvVkBorderColor 家族间选择)
MGPSamplerView / MGPTextureParams 36 / 32 view 只带视图限制(min/num level、min/num layer、别名格式);纹理参数(base/max level、swizzle、depth-stencil mode、LOD 钳、ForceResync)挂在纹理对象上
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 的发射抑制器
MGPSubData / MGPSubRegion 72 / 40 §6
MGPDrawInfo / MGPDrawRange / MGPDrawIndirect 56 / 12 / 40 Flags 门控 MinIndex/MaxIndex(只在 client-memory 数组路径算)与 XfbCpuCapturedVertices(只在 XFB scatter 路径读)——不是每 draw 都算;NumDrawsMGPDrawRange 在变长尾;用户索引的 MGHostSpan 只在 kDrawHasUserIndices 时进变长尾;indirect 的 DrawCount 由 client 解析,server 永不读 indirect 命令块来数 draw
MGPShaderBuffers / MGPBufferRange 32 / 24 range 不内联 host spankCapNeedsHostUboBytes 下 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 1248 迁移期 Track V 载体,§9.4

每条 kVarTailset_*SetVertexBuffersSetSamplerViewsBindSamplerStatesSetShaderImagesSetShaderBuffersSetStreamOutputTargets)都带 ContentHash——与 MGPFramebufferState 同一模式,hash 未变就不发(§5.4)。

5. 前端 state trackerMG_Impl/Pipe/TrackerP2 起)

5.1 推送发生在 verb 之前的 validate 时刻,不在 GL setter 里

Blaze3D 每个 batch 用 glEnable/glDisable(GL_BLEND) 包住(Espryt 代码自己标它为最热路径),per-setter 推送会把每次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium st_validate_state

八个 validate 入口,由 PipeCalls.defkCtxVerb/kCtxObject 条目生成:ValidateForDraw20 个 draw 入口)、ValidateForDispatchValidateForClearValidateForBlitOrCopyValidateForTextureOpGenerateMipmap / CopyTex* / BindImageTexture)、ValidateForReadbackValidateForXfbSpanValidateForQuery。八个而不是四个,因为 MG_Impl 用到的 70 个表项里只有约 22 个是 draw/dispatch,其余 ~48 个(clear、blit、copy、回读、barrier、XFB 跨度、query/sync)很多自己就读 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_PACKNEW_PATCH_STATEBitwiseEqualNaN 合法)、NEW_VERTEX_ATTRIB_DEFAULTSNEW_VERTEX_ELEMENTSVAO config version 既有计数器
NEW_SHADERNEW_SHADER_BINDINGSNEW_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_VIEWSNEW_SAMPLERSNEW_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 的原因)。

完整性由 scripts/gen_pipe_dirty_surface.py 保证:枚举 MG_Impl/GLImpl 里每个 mutator → 必须 bump 的聚合世代,CI 重生成 + git diff --exit-code,未映射即失败(P1 起成为门)。实测规模926 次 mutator 调用落在 73 个不同 mutator 上,其中 92 次(7 个 mutator,绝大多数 RecordError)位于同函数内也会到达后端的"即时发布点",其余 834 次由紧随其后的 verb 发布——映射表是 73 条目的问题。

三个回绕 Uint16 在 tracker 边界加宽(m_lastPushed[] 是 tracker 自己的字段,不改 MG_State);回绕在 tracker 本地无害(多一次重推,永不漏推),且被集合 hash 抑制器吞掉。

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 自己 static_assert 并做 head/blend/tail 三段 memcmp字段顺序承重ScissorBoxWrittenMaskClipDistanceEnabledMask 故意放在 tail 段);拆成 blend/depth-stencil/rasterizer 三个 CSO 要手工维护 ~150 字段划分表且无绊线。
  • 子集身份的理由:整块内容寻址会让 glViewport/glScissor/glBlendColor/glClearColor 每次铸造新 CSO、冲掉 server 的 pipeline memo——RenderState.h 记录的那次回归。RenderState.cpp 里 viewport/scissor/line-width 族只 ++m_versionSET_CAPABILITY 与 pipeline 相关 setter 才 BumpVersions()
  • 动态子集:viewport、scissor、depth range、blend color、line width、polygon offset、stencil ref/write mask、clear 值、sample coverage、hints、point-size 族。
  • 划分只写在一处:MG_Pipe/MGPipeRenderStateSpans.{h,cpp}P2)的 chunk 表 + MGPipeComputePipelineSubsetHash()从 Magma 的 ComputePipelineStateHash 搬来,client 与两个后端共用;G7 的 MG_Test 遍历每个 RenderState public setter,断言 pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变
  • server 侧:每 context 一份 working RenderStateParameters1168 B),bindset_dynamic_state 各把自己的 chunk 散射进去。Espryt 的 SyncRenderState693 行)拿到的仍是 const RenderStateParameters&,单 Uint16 早退、三段 memcmp 一行不动Magma 的 pipeline memo 键是 cso.slot,动态尾巴仍走 ApplyDynamicDrawStateTail。Espryt 的 head/blend/tail 划分(驱动侧增量)与 pipeline/dynamic 划分(线上与身份)是两回事,并存、各有绊线。
  • client 取值顺序:m_pipelineStateVersion 未变 → 复用上一个 CSO handle,零哈希;变了 → 对 pipeline 子集算 xxHash~25-30 字,Magma 今天就在算)→ CSO map 探测 → 命中发 12 B bind,未命中发变化 chunk 的 create 再 bindm_version 变而子集未变 → 只发 set_dynamic_state~200 B)。
  • FramebufferSrgbDepthClamp 今天没有存储glEnable 被静默吞掉且不报错,六个后端读点恒为 false);chunk 表冻结前要补真存储并把 FramebufferSrgb 划进 pipeline 半边(它改变 attachment/blend 的解释)——待拍板,见 ROADMAP.md

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 的机制是惰性特化,不是调用顺序。

create_shader_state 从编译池的终止 continuation 发出(不是从 draw),SPIR-V 在首个用到它的 draw 之前到达 server——monolith 拿不到的异步收益。

四条合并规则:整块结构优于逐字段;高水位标记(GetTouchedBindPointCountGetMaxTouchedUnit)留在 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 是普通应用代码)。

5.5 sampler view 在 client 侧解析

GL 是每 unit 每 target 各一个绑定;shader 看见哪一个取决于 sampler uniform 类型、mipmap 完备性(IsMipmapCompleteForFilterSamplesAsIncompleteTexture)与 IsUndefinedDefaultTexture。gallium 的"每槽一个 view"就是解析后的形态,解析留在 client 并带自己的 memo(~40 行搬迁)。两处后端特定后处理留在 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 钉住 bufferBufferForTexBuffer,范围实时解析)。
  • 共享组:v1 一个 screen、一个 context、一条 floweglMakeCurrent 是 flow 所有权转移,在既有 EGLOperationMutex 下发射(顺手让 ReleaseThreadSwapInterval 也取该锁)。
  • 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。

5.7 emulation 的归属

规则:驱动表达不了的变换在 tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。 只有三个"读前端字节的纯 CPU 变换"下放到 client。

emulation 归属 过线的是什么
client 顶点数组((first+count-1)*stride+elementSize client 字节(MGHostSpan),永不是指针
最大索引扫描(TryComputeMaxIndexFromHostBytes,唯一无界的应用指针读,只有 client 同时持有两个数组) client MGPDrawInfo::MinIndex/MaxIndexflag 门控,~0 = 未知)
client 索引数组 client 变长尾里的 MGHostSpan
*IndirectCount 计数解析(从 parameter buffer 的 shadow 读实际 draw 数) client 解析后的 MGPDrawRange[](几十字节)
primitive-restart 重写(整 EBO 重写,kMaxRestartRewriteBytes = 64 MiB server 零线上流量:从索引宿主镜像读(§10.3)
multi-draw 五档分档 + 展平(ResolveTierForBatchCPU 展平是回退) server 同上
viewport-array N 遍回放 server 无新增:16 组 viewport/scissor/depth-range 已在渲染状态里
fp64 顶点窄化 server 原始字节;IsLongType 分开过线
image-bindable 存储加宽/拆分 server 正向 ImageBindableHint;反向纹理拉取 + 终止符
生成 mipmap 的前端存储 拆开:client 分配 level 存储,server 生成 MGPMipPlanOnMipLevelsGenerated 只带形状不带字节
CopyImage shadow 镜像 client 只回"拷贝成功",删掉一整条 server→client 字节通道
XFB CPU 图元计数 client XfbCpuCapturedVerticesflag 门控)+ EndStreamOutputMGPXfbAccounting
XFB scatter 的 read-modify-write client §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)。

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 KB16×。
  • MGPSubRegion 显式携带 SrcRowStride/SrcSliceStrideMGPSubData::SourceIsVerbatimLevelShadow 显式携带原来由 uploadData == mipData 指针比较回答的问题:"这批字节是未经转换的 level shadow 吗"。split 下 client 既不发整 level 也不在 server 留整 level 镜像,指针比较不成立;Espryt 的上传路径改为从描述符取步长,UNPACK_ROW_LENGTHSrcRowStride/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 边界各一次。
  • 后端真正在 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-CrossTranspileSpirvToEssl)全在 server,文件级切割。没有 MOBILEGL_IPC_PROGRAM 开关、没有 server 侧 compile pool。
  • 归档机制:Visit() + sizeof 绊线(static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE)),一份字段表服务序列化两个方向。必须覆盖四个 ResourceReflection(各带 TypeFacts)、uniformSamplerOrImageUnitIndexuniformBlockBindingshaderStorageBlockBinding(按名字)、explicitOpaqueUniformBindingsxfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapturecomputeLocalSize、GS/TCS/TES 事实、usesReservedNumSamplesuniformOffsetsXfbVarying 带两套拼写(GL 名字 + block 实例/成员/元素)。
  • P0.5 硬前置:反射类型今天声明在 ProgramObject.h 里,而它 include ShaderObject.h(→ glslang)与 SpvcSession.h(→ spirv_reflect)。P0.5 把 TypeFactsResourceReflectionXfbVaryingLinkArtifactsSpirvArtifacts 抽到 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.hMAX_DRAW_BUFFERSPerBufferBlendStateStencilFaceStatePixelStoreParametersRenderStateParametersSamplerParametersBorderColorFormVertexAttributeVertexBufferBindingPoint),它不 include MG_State/GLState 任何东西;MGPipeTypes.h 今天为此临时 include 了 BackendObject.hRenderState.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 的空 drawGL_LINK_STATUS 永不撤回,同步查询由 client 从 ProgramObject 回答。OnLog 逐字复现——由此要求日志按严重级分级(§8.3)。
  • Magma 的两个内部 shaderblit、depth-mipmap)烘焙成签进树的 SPIR-V + uniform location + UBO 布局,用一个 MG_Test 重跑树内 glslang 逐字节比对守新鲜度(MOBILEGL_BAKED_INTERNAL_SHADERS,P7);顺带把一次 glslang 编译从 monolith 启动路径上删掉。

8. 反向通道

8.1 MGPipeCallbacksP0 已落地,MobileGL/MG_Pipe/MGPipeCallbacks.h:27-51

十个具名回调 + 一个正向终止符(ResourceSubDataComplete),取代今天 95 个调用点 / 17 个方法直接 poke 前端对象。gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇(Mesa 里两者共享地址空间),具名化是有意偏离(D8)。monolith 下直调,split 下是 SEG_EVENT 上的记录。

回调 取代
OnGlError(code) 6 处 RecordError必须对命令流有序,否则 glGetError 答错(glGetError 本身永远本地)
OnGpuWritten(res, ranges[]) 6 处 MarkGpuWrittenclient 在每个 draw/dispatch 发射点保守自建 pending 集,这是收窄通道
OnBufferWriteback(res, offset, bytes) PBO 回读、XFB 捕获;按操作级批处理(今天两处逐行循环绝不能变成每扫描线一次 IPC);必须与 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 行为一致
OnSurfaceChanged(info) SwapchainObjectpDefaultFramebufferInfo 的分层倒置;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 拥有资源表);SetBackendStateMemo(前端 VAO 里存后端堆裸指针)直接删除;SetBackendHashMemo/AuxMemo → server 侧 per-slot 字段。20 处 SyncPersistentMappedRange + 6 处 SyncGpuWrites 按 §5.7 逐站点归属,其中至少一处消费者搬不走:Magma 的 ResolveUniformBufferPayload 把具名 UBO 打进自己的 UBO ring → SetShaderBuffers 的 host payloadD-B8)。

8.2 有序性是正确性要求

每一次 WritebackFromBackend 后面都紧跟 BumpBufferMutationEpoch(),否则 server 的 draw-clean memo 会在 epoch 背后变陈旧——split 里这变成反向通道上的排序规则:写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 应用。反向通道需要与正向通道相同的有序保证。

8.3 错误、ack 与日志

  • 纹理分配的 OOM 在 monolith 里就已推迟到 sync 时刻(glTexImage*/glTexStorage*MarkStorageDirtyEspryt 惰性分配;连 glRenderbufferStorage* 也在 SyncToBackend 里惰性做),拆分不改变可观察行为,这批不同步 ack。
  • 唯一允许同步 ack 的入口是 glBufferStorage(真同步分配)glRenderbufferStorage* 不 ack41 个 trace fixture 里 OOM 探测惯用法出现 0 次(9 次调用散在 5 个 fixture,无一在 3 个调用内跟 glGetError;语料里的成功性检查是 glCheckFramebufferStatus,client 本地作答)。目录里目前没有条目携带 kNeedsAckResourceRespecifykNone),标记随 P3a 的 buffer 路径落地。
  • 其余错误一律晚到,走有序的 OnGlError
  • OnLog 分级:≤WARN 有损(覆盖最旧 + eventDropped 计数);≥ERROR 无损,加入触发 eventRingFull + 停止 apply 的语义事件集;每秒 ERROR 速率限制器,超限发一条 "N errors suppressed"MGLOG_E_ONCE 的 latch 变 per-server。理由:后端 link 失败只以一行 ERROR 呈现,统一有损会让最有诊断价值的那一行在日志压力下消失。

8.4 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6)

server 不保留纹素,三个原因会要求重发已发过的 level:RequireImageBindableStorage 的 re-dirty、整格式再生、view 源重铸。四条缓解同时上:

  1. 预防主因client 给纹理打 everImageBoundResourceCreate/Respecify 一直携带 ImageBindableHintimage-bindable 存储前期分配好。
  2. 拉取异步server 发 OnTexturePullRequest 并把 twin 标 not-readyclient 下次 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 生成的 levelclient 根本没有字节;收到零 region 时 server 带着"已分配但为空"的存储继续(正是 monolith 的行为)并记 MGLOG_W。没有终止符 apply 线程会永久 park。

门:TextureRemintPullScenario(含无解用例,且在终止符落地前必须是红的);拉取次数逐 trace 用例发布。本设计从不声称"零 round trip",它测量并公布。

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。不新增停顿类。

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 五档。 MagmaVulkanRenderer 全部 memo 与 scratch、PipelineFactoryProgramFactoryUniformManager 的 ring 与描述符集、五个 Vk*ManagerFrameContextSwapchainObjectDynamicStateShadowVertexInputStateFactory 的 cache 本体、D18 的节点式容器纪律m_renderbufferResources/m_textureResources 故意用 std::unordered_map,调用方跨查表缓存 Resource*postmortem 注释逐字进 review checklist)。

从"不动"里移出的一项:Espryt 的 sub-rect 上传判定与跨步计算(§6,从描述符取步长)。

唯一两处必须真改的 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

// MG_Backend/MGPipe/PipeInputs.h —— 按 memo 键组织,不按读点组织(~20 KB,字段集全迁移期稳定)
struct PipeInputs {
    const RenderStateParameters& GetRenderStateParameters() const;   // 阶段 A:类型与后端今天读到的完全一致
    // … 每个后端真正用到的 GLContext 方法一个访问器(Espryt 32 / Magma 55
#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 别名 机械 sedMG_State::pGLContext->MGB_CTX->(293 处)+ 手工转换 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
  • 填充点逐 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->

9.3 Track V / Track H

  • Track V(值类型:GetRenderStateParametersGetPixelStoreParameters、capability 位、stencil/colormask/depthmask/scissor/patch/attrib 默认值、Magma ~22 个标量 getter……约 B 类读点的 55%):机械。
  • Track H(对象类型:167 个 SharedPtr<MG_State…> 点):真活。
  • 读点分类实测(静态):A 探测变化 ~35(12%)、B 翻译输入 ~21674%)、C 瞬时参数 ~4、D 身份/缓存键 ~48(与 B 重叠)、E 数据字节 3、写 8。74% 是 B 类——"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:535P13 变成 static_assert(sizeof == 0));布局逐成员 offsetof 断言且 split 下逐字段序列化(异质 POD 并集的 padding 差异 monolith verify 看不见);只在 P2..P13 存在,MOBILEGL_PIPE_STATS 单独计一类字节(ResidualValueBlockP0 已占位)。

9.5 21 条身份 memo 的重键

统一事实:每个进入 memo 键的版本计数器要么是回绕 Uint16,要么根本不会被它害怕的那个 mutation bump;身份比较是堵回绕洞的补丁。{slot, gen} + 显式 destroy 让 11 条直接删除registry 的同址 weak_ptr + GC ×6、TwinLookupMemo ×3 + OwnerEqualsUnitSamplerLookupMemoWeakPtr 测试、SetBackendStateMemoVkTextureManager::TextureIdentity 存活探测、ConvertedVertexStreamKeysourcePin……),2 条 server 删除但去抖搬到 client(§5.4),7 条重键成更便宜的比较(StampSyncedFBO 四元组 → ContentHash + server 私有 attachmentRemintEpochResolvedTextureBindingMemo 9 键 → (shaderCso.slot, viewSetSerial)SetupDrawSnapshot 的 ~14 探测字段与两个有损求和 → 三个 handle + 两个 server 纪元 + dirty maskVertexInputStateFactory::ComputeHash 里的 lifetimeId → gen 混进 server 侧每个 content hash),1 条(D18)原样不动。两个顺带修掉的潜伏 bug 已先独立落地:m_xfbCounterSlotByObject 用裸 GL name 做键(bd2b4158)、RenderbufferObjectGetLifetimeId()9c7339b2)。

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 天维护)。

P13:删 SnapshotFromGLContext() 的非 verify 分支、MGB_CTXMOBILEGL_PIPE_PUSHMOBILEGL_PIPE_LEGACY_MEMOS保留 MOBILEGL_PIPE_VERIFY 连同它需要的 SnapshotFromGLContext()MG_State includeD-B5,verify 构建永不出货);三道纯度门在非 verify 构建上转绿。

10. server 侧

10.1 对象表与 applier

  • MG_Remote/Server/PipeObjectTables:按 kind 的 slot 数组,不是对象图;server 不持有任何 buffer 的完整副本、不持有纹素、不持有前端对象图。
  • PipeApplier:解码 → 更新对象表与 PipeInputs → 调后端函数指针。debug 断言:任何传输下都不得有 SharedPtr 或裸前端指针跨过 applier 边界。InProcessTransport 走与 spawn 完全相同的 G3 编解码路径,只在门铃/拷贝机制上不同。
  • 每 context 一份 working RenderStateParameters(§5.3)。

10.2 monolith 侧的净收益

即使 IPC 永不上线:复用地址 ABA 一整类不可表达;FBO → program 排序 hazard 消失;SwapchainObjectMG_Impl 的分层倒置消失;两个潜伏 bug 已修;一次 glslang 编译离开启动路径;inproc = 渲染线程;MG_Test 的 mock 后端变成 MGPipe recorder(§13.3)。monolith 净代码量是增加的(约 +6,650 手写 + 4,000 生成,对 ~372 行真删除),所以 monolith 论据是逐线程 CPU 数字(§13.2-④),不是删除行数。

10.3 索引宿主镜像(MG_Remote/Server/IndexHostMirrorP8

  • 覆盖:BindMask & ELEMENT_ARRAY 的资源,且仅当 kCapNeedsHostIndexBytessplit 且 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 的段既不可行也无必要。它是本设计里唯一的"数据副本"。

11. 传输与数据面(骨架 P0 已落地,MobileGL/MG_Remote/

11.1 段

拥有者 默认 内容
SEG_CMD clientserver 只读) 8 MiB2 的幂 RingControl4 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 serverclient 只读) 8 MiB4 KiB slot readback 像素、buffer writeback
SEG_EVENT server 256 KiB SPSC ring 十个回调的事件 + EvQueryResult/EvFenceSignaled/EvReadbackDone
SEG_SHADOW[n] client 每对象,≥256 KiB shadowPhase 2 零拷贝 buffer/texture shadow
SEG_ADOPT[n] serverclient RW 每 buffer,≥16 MiB adopted storeP11 应用直写 GPU 内存

创建(ShmSegment):Android ASharedMemory_createAPI 26libc 的 memfd_create wrapper 是 API 30);桌面 Linux syscall(SYS_memfd_create);其他 POSIX shm_open+shm_unlinkWindows CreateFileMappingW(Local\)。传递:POSIX SCM_RIGHTSFdPassing,专用 AF_UNIX SOCK_DGRAM socketpair——消息边界保住 ancillary data 与 payload 不被拆开,sideband ≤256 B);Windows 段名走 SegmentRef。fd 传递在第一个 transport commit 里实现——没有它数据面在唯一重要的平台上一字节过不去。

不进 SEG_STAGE 的:restart 重写的整 EBO 与 multi-draw 展平的索引流(走索引镜像)。SEG_SHADOW 块的退休规则:释放的块进 pending 链表,appliedSeq(借入 GPU 时间线的 slot 用 retiredSeq)越过最后一条引用它的记录后才归还 arena。

11.2 RingControlRing.h

一页 4 KiB,每个争用组各占一条 cache line:SEG_CMD 游标三元组 cmdHead / cmdAppliedTail / cmdRetiredTailSEG_STAGE 独立三元组(stageHead / stageAppliedTail / stageRetiredTail——"SEG_STAGE 余量 < 1/4"是 publish 触发器,占用率不能从另一个 ring 算出,且 stage slot 的退休条件不同);三个严格区分的水位 appliedSeq(释放 *AppliedTail/ submittedSeq(释放 staging/ retiredSeq + completedFrameSerial(释放 *RetiredTailSEG_ADOPT+ presentAckSerialserverEpochcontext 丢失 / server 重启 ++)、ringGeneration(硬 drain 后 ++,作废缓存 offset)、consumerParked/producerParkedeventRingFulleventDropped。两个 tail 是必须的:P11 之后 server 会借用 ring slot 而不是再拷一次,那种 slot 只能在 completedFrameSerial 之后回收。游标是单调字节计数、2 的幂掩码、永不重置。

记录头 RingRecordHeader{kind, flags, size}kind 0 保留给 wrap 填充;RingProducer::Reserve 在记录会跨 wrap 边界时自动发 pad 记录,保证每条记录连续;MaxRecordBytes() == Capacity()/2RingConsumer::Pop 拒绝不可能的头(非 8 对齐、小于头、大于已发布)并置 corrupt → Fatal{ProtocolCorruption}HardDrainRing 只在两侧静默且 ring 全空时 bump generation。

11.3 双向 doorbellDoorbell.h

  • client → serverconsumer 自旋 → 置 consumerParked=1 → 阻塞;producer release-store cmdHead 之后仅当 consumerParked 时敲(字节码 0x01)。
  • server → clientclient 在任何等待(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 平台代码:CondVarDoorbellinproc,带 Kill() 死亡态让 Shutdown 能 join 一个 parked 的等待者)与 SocketDoorbellspawn,一字节;SOCK_STREAM 端在对端关闭时报 POLLIN|POLLHUP + recv()==0,这是死亡检测)。
  • 丢失唤醒窗口由两个 seq_cst fence 关闭(等待者置标志 → fence → 再测条件;通知者发布 watermark → fence → 读标志),标志本身的访问是 relaxed。NotifyIfParked 的前置条件:watermark 已发布。死亡的 doorbell 让 Wait 停止重新 park。

11.4 控制面(protocol.fbsFraming.hITransport.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-228CtrlMsgHelloWelcome(四个段的 SegmentRef)、CapsSnapshotSurfaceOp/SurfaceReplyResyncRequest/DoneAuxRequest(外来线程的 fence wait / query result / scalar get)、FatalProtocolCorruption/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)。
  • ITransportSendFrame / 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,改动 = majormajor 不符是结构化失败)。

11.5 WAR 危害、拷贝账与背压

  • Phase 1(P5P8):GL 调用时刻把字节拷进 ring slotslot 到 stageAppliedTail 越过它为止不可变,危害按构造消除;代价一次 memcpy,Ops_ResidentSubDataStageBlocksIntoUnpackRing 在 monolith 里已经在付。
  • Phase 2shadow-in-shm,零拷贝):≥256 KiB 的 shadow 分配在 SEG_SHADOWPipeResource::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)。
路径 monolith Phase 1 Phase 2
glBufferSubData → shadow store 2 3 2
glBufferSubData → adopted storeP11 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 MiBP11 T1/T0 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_CMDSEG_STAGE 各自独立跑这套升级。

11.6 publish、序号与 credit

  • 不设"records ≥ 64 KiB"一类阈值(那是一整帧的流水线气泡,且否掉 inproc 的全部意义)。规则:每条记录(或每 8–16 条摊销)release-store cmdHead,仅当 consumerParked 时敲门铃。
  • 显式门铃点:present、任何 kNeedsAck 请求、eglMakeCurrentglFlush(刷出不等待)、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 creditpresentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDITeglSwapBuffers 阻塞)。server 不发 credit 消息:对 RingControl release storeconsumer 每 64 条记录更新一次 appliedSeqproducerParked 时敲反向门铃。

11.7 事件回传与溢出

SEG_EVENT 承载十个回调加回读完成通知。client 排空点:glGetErrorglGetQueryObject*glClientWaitSyncglGetSynciveglSwapBuffersglMapBuffer*/glGetBufferSubData/glCopyBufferSubData,以及每一次等待循环的每一轮。溢出策略(修一个双向死锁:client 卡在 present credit、server apply 线程卡在生产事件):EvLogLine ≤WARN 有损;语义承载事件(EvGpuWrittenEvReadbackDoneEvFenceSignaled、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 机制)。

11.8 fence 与无 present 负载

  • fence 完成度必须来自真的逐 fence 退休,不是 present 水位:DirectGLES 的 g_completedFrameSerial 只在 Present()WaitForFrameSerialCompleted 里前进,帧中 fence 会退化成帧计数推断——DirectVulkan.cpp 写明这是被修掉的 bugMC 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 用例。

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 那一步会打破它。

三档,由运行时 POST 探针选择(本项目"后端限制一律探针判定、不硬编码驱动名"的既定规则),spike B 已在两台设备上给出答案MEASUREMENTS.md §2):

形态 实测
T0 — server 导入 client 分配P11 主攻) client 分配 AHardwareBuffer BLOBsocket 交接;server 以 VK_ANDROID_external_memory_android_hardware_bufferMagma)或 EGL_ANDROID_get_native_client_buffer + glBufferStorageExternalEXTEspryt)导入,两侧 persistent+coherent 映射 Adreno 830 与 Mali 都是完整读写往返,含 GPU 访问与两侧字节校验——唯一在两台设备、两个后端上都成立的档
T1 — server 导出自己的映射 VK_KHR_external_memory_fd opaque fdclient mmap + 导入 只有 Adreno 的 Vulkan 路径可用;Adreno 的 GLES 导入 glMapBufferRange 全部 GL_INVALID_OPERATIONMali 不可导出。每次存储定义一次 round trip(不是每 store 一次),StorageBufferRegrowScenario 发布 map-persistent-roundtrips
T3 — host pointer 导入(VK_EXT_external_memory_host Adreno 无扩展;Mali 只读(GPU 写对宿主映射不可见)
T2 — 拒绝(永久正确回退) AcquirePersistentMap 返回 nullptr,前端已在三处容忍 此档下 client 侧推送强制

MOBILEGL_IPC_ADOPT_TIERauto/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_livePersistentMapspersistent+write+非 FlushExplicit+非 GpuResident),在每个 validate 点对本次操作可达的每个这类 bufferVAO/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. 门从第一天就有:PersistentCoherentMapScenariomap PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)。

MOBILEGL_COHERENT_AS_FLUSH 在拆分模式下照常生效:两个带 coherent_as_flush: true 的 Create fixture 在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义。

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,未命中合法地答"未就绪");glGetTexImageDirectGLES,含 GPU 生成的 mip);glReadPixels → pack PBOfire-and-forget + client 侧 MarkGpuWritten,严格优于 monolith 的无条件停等);glEndTransformFeedback(取消无限 fence 等待,对 capture target 置 MarkGpuWritten);eglSwapBuffers(只查 credit);*IndirectCountrestart/multi-draw。

不可避免(全部罕见):握手一次;surface 生命周期与首次 MakeCurrent+InitCapabilities 每 surface 至多一次;glReadPixels → 客户内存(像素进 SEG_REPLY,逐行写回循环留在 server 内按操作级批成一段);glGetTexImageDirectVulkan,对"无 GPU 背书"的 level 回答"请用你自己的 shadow");GPU-write pending 的 buffer 首次 CPU 读(monolith 本来就 glFinish();由 writableMaskOnGpuWritten 收窄);glClientWaitSync(timeout>0)GL_QUERY_RESULT 未完成、glBeginConditionalRender(谓词只解析一次,之后每个条件 draw 在 client 丢弃,server 永远不需要那个 query);glBufferStorage 的 ackMapPersistent(仅 T1,每次存储定义一次);纹理拉取(§8.4);client 侧索引扫描当源 EBO 在 pending 集里;ring/stage 耗尽与 present credit(节奏,非语义)。

验收措辞:在全部 40 个 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数与 index-bytes-shipped;零 timeout 轮询循环必须在有界时间内退出。

13.2 五部分验证门(取代 monolith 的字节一致门)

"改前改后 nm --defined-only.text size 完全相等"的门在本方案里按构造死亡(不存在能让旧字节回来的配置);替换是:

  1. 接口纯度三道门(只跑非 verify 构建):A 门 include 图——disaggregated 配置编译 MG_Backend 时把 MG_State/GLState 从 include 搜索路径移除(nm --undefined-only 对"只 include 不调用"是瞎的,而 RenderState.h → FramebufferObject.h → TextureObject.h 正是这种耦合),依赖 P0.5B 门符号——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× 慢,永不出货。逐字段而非 memcmppadding 会 false-DIFFER)。保留模式:消费即清的组(纹理 dirty rect)发射后无法重算,verify 时 tracker 保留清除前的集合并比对发射出去的 (UnionBox, RegionCount, Regions[])活过 P13
  3. 行为 A/B40 个 trace 在 {monolith-pull, monolith-push, split} 下 SSIM ≥ 0.99(默认阈值);ctest -L integration-gpuDirectGLES.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/Btools/bench.sh + trace replay --benchmark 逐帧 JSON指标是逐线程 CPU 时间p50 与 p99绝对阈值——tracker 每 draw 的 ns 公布并设上限(真实拉取基线只有每 draw 6.5–9.3 次 accessor,相对噪声阈值会平凡通过);Blaze3D blend-toggle 微基准单列;关掉 CSO 内容寻址的负面对照。
  5. 覆盖 + poison + 句柄纪律G6 重生成 0 UNMAPPEDgen_pipe_dirty_surface.py 重生成 0 未映射 mutator;逐 verb 世代 poisonG7 setter 一致性测试;ResidualValueBlockoffsetof 断言与 P13 的 sizeof == 0

两条幸存的字节级等式:MOBILEGL_BUILD_DISAGGREGATED=OFFnm --defined-only libMobileGL.so | grep MG_Remote 为空且链接行不增加库;nm -D libMobileGL.so | grep mobilegl_server_main 在 RelWithDebInfo 里命中。符号与 .text 漂移每阶段作为信息性指标发布。

13.3 长期语义门:MGPipe recorder

P13 把 MG_Test 的 mock 后端变成 MGPipe recorder:在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖 MG_State,是 P13 之后不靠 verify 构建的语义门,也给 tools/trace_replay 一种记录已解析状态的、比 apitrace 精确得多的录制格式。它只覆盖推送内容,不覆盖后端对它的解释(split-only 的渲染 bug 仍无 server 侧第二意见)。

14. Present、线程与帧节奏

  • eglSwapBufferspresent{frameSerial}swap interval 搭在同一条记录上)→ publish + 敲门铃 → 返回,除非超出 credit。presenteglSwapBuffers 严格 1:1:两个后端的帧边界排空(Magma 四次 OnFrameBoundary 老化、TryDrainFrameTransientsBeginFrameEspryt 三个 ring 与 TrimBufferPool 的 retire)只在 Present 内发生,批量会饿死它们。
  • MOBILEGL_IPC_PRESENT_CREDIT 默认 1(可配 14):延迟叠加,端到端 ≈ client credit + server 帧数 + 驱动深度server 的 Present 末尾已在 vkWaitForFences 上等 2–3 帧,credit 2 就是端到端 45 帧(60 Hz 下 6683 ms)。P10/P12 用 GetGpuTimestampNs--benchmark 逐帧 JSON 构建输入延迟直方图,只有实测吞吐收益能抵掉延迟代价才调高。
  • Magma 从不注册 SetSwapInterval 且偏好 MAILBOX/IMMEDIATE,IPC credit 是它唯一的显式限帧器;若需要 FIFO 作为独立 dev 变更。
  • 线程——clientv1 不加线程,编码在 GL 线程上直接写 ring(前端本就是 per-context 单线程契约);外来线程的 sync/query 读全部从 RingControl 无锁回答,必须发射的少数取 ctrlMutex 走 CTRL socket 的 AuxRequestSPSC ring 不允许第二个 producer);ShaderCompilePool 原样在 client;可选 mgl-client-tx 凭测量决定。servermgl-srv-ioasio、封帧、SCM_RIGHTS、doorbell、CTRL RPC)、mgl-srv-apply终身持有原生 contextg_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。

15. 进程、EGL 与平台

15.1 启动与握手

  • server 定位:MOBILEGL_IPC_SERVER_PATH(主要)→ dladdr(&MobileGL::Initialize) 同目录的 libMobileGLServer.so(兜底;不能当主要机制,因为集成测试静态链接 MobileGL_s、trace replay 的可执行文件不在库目录)。配套:MobileGLServerRUNTIME_OUTPUT_DIRECTORY 设为 $<TARGET_FILE_DIR:MobileGL>,每条新 ctest ENVIRONMENTadd_trace_replay_testSPLIT 分支带 MOBILEGL_IPC_SERVER_PATH
  • 启动:socketpair(AF_UNIX, SOCK_STREAM) + fork/execvefd 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 开关有分歧;buildFingerprintgit hash + PipeCalls.def hash)不匹配 → 握手期 Fatal{AbiMismatch}
  • mobilegl_server_main 声明为 extern "C" __attribute__((visibility("default"))):非 Debug 构建设了 hidden visibility,而 FCL/plugin 出货的是 RelWithDebInfo,否则 dlsym 在设备上静默失败。

15.2 Androidspike A 已证)

  • 交付链:APK 唯一可 exec 的位置是 lib/<abi>/,打包器只收 lib*.so,所以 server 以 add_executable + PREFIX "lib"/SUFFIX ".so" 构建(真 PIE),并把 RUNTIME_OUTPUT_DIRECTORY 指到 AGP 收集原生产物的 CMAKE_LIBRARY_OUTPUT_DIRECTORYCMakeLists.txt:784-808MOBILEGL_BUILD_SERVER_SPIKE)。两台设备上都已证明:从 TraceReplayActivity 自身的 untrusted_app 进程 fork+execve <nativeLibraryDir>/libMobileGLServer.so,子进程落在同一域、同一 MLS categoryexit 0,零 avc denialMEASUREMENTS.md §1)。
  • fork+execve 而非 posix_spawnbionic 从 API 28 才声明后者,minSdk 26android-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 行 stubdlopen(libMobileGL.so)dlsym("mobilegl_server_main")。一份共享库、两个角色、版本必然匹配(Android 上那份库仍含 glslang/SPIRV-Cross,因为它同时服务 client;B 门检的是 server 侧代码有没有引用它们)。
  • minSdk 26 没有公开 NDK API 能扁平化 ANativeWindowlibbinder_ndkASurfaceControl 都是 API 29)。P5P11 验证路径无窗口pbuffer 或 AImageReaderANativeWindowtrace replay 默认 pbuffer。P12 生产路径Java SurfaceParcelable)→ Messenger/AIDL → MobileGLServerServiceandroid:process=":mgl")→ JNI ANativeWindow_fromSurfaceFCLauncher 今天在 egl_bridge.c 做的那一次调用);仓内先例是 android:process=":bench"BenchService。代价:server 进程多一个 ART~1525 MB)。FCL 把游戏 JVM 跑在主进程,第二个进程必须新建。
  • HeadlessGL 的 fork 预检会 fork 一个子进程跑完整 EGL bring-up 然后 _exit——拆分模式下那个子进程会 spawn 一个孤儿 server。规则:server 的 EOF 检测即时且无条件退出(亚秒级);client 的 socket fd 设成 _exit 会确定性关闭的形态;就绪握手有界重试。列为 P6 验收。
  • 通用 env 透传 --env K=Vrun_android_retrace_local.py → intent extra mobilegl_envtrace_replay_core.cpp 在加载 libMobileGL.sosetenv)已接进 retrace 通道,取代逐 knob 加 --es/--ez

15.3 Linux / Windows / 崩溃

  • Linux/X11Window 是 XIDnativeToken:u64 直接送,backend 自己 XOpenDisplay(getenv("DISPLAY"))Wayland 维持不支持。WSL/CI 永不开窗:EGL_PLATFORM=surfaceless + EnsureHeadlessPlatform()
  • WindowsHWNDnativeTokenVulkan 可行,WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only。transport 默认 named pipeasio windows::stream_handle 要求 overlapped 句柄,所以用 GUID 命名的 CreateNamedPipeW(FILE_FLAG_OVERLAPPED) + CreateFileW(FILE_FLAG_OVERLAPPED) 造句柄对再继承给 CreateProcessAF_UNIX-everywhere 是可选简化。Windows 机器不是正确性门。macOS 不拆分(CAMetalLayer 无跨进程表示)。
  • server 死:client 读到 EOF/EPIPE → device-lost 闩锁(GL 调用 no-op、eglSwapBuffers 返回 EGL_FALSE+EGL_CONTEXT_LOSTglGetGraphicsResetStatus 返回 GL_UNKNOWN_CONTEXT_RESET);MOBILEGL_IPC_RESPAWN=1 时重启并全量重推(默认关,静默重启会掩盖 bug)。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_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]
  • CMake optionCMakeLists.txt:23):MOBILEGL_BUILD_DISAGGREGATED(默认 OFF)追加 MG_Remote/**SOURCE_FILESCMakeLists.txt:454-469)并定义 -DMOBILEGL_BUILD_DISAGGREGATED=1OFF 时 MG_Config::Transportconstexpr MonolithInit.cpp 的分支编译期消失。3rdparty/flatbuffers/include 缺失时把 option 强制回 OFF 并 message(WARNING)CMakeLists.txt:440-451)。MobileGLMobileGL_s 都拿到同一份源。MG_Test/Wire 只在该 option 下注册(MobileGL/MG_Test/CMakeLists.txt:93-95)。
  • MOBILEGL_BUILD_DISAGGREGATED_INPROC(尚不存在):CI/调试形态,隐含开启前者,额外加角色隔离 shim。MGPipe 让需要角色分身的进程全局从四个(pGLContextgBackendFunctionsTablepActiveBackendObjectpDefaultFramebufferInfo)降到两个pipe 表与 pActiveBackendObject):server 角色不再读 pGLContext(三道纯度门就是这个断言),pDefaultFramebufferInfo 由保留句柄 {0,1} + OnSurfaceChanged 取代。两个 shim 都不在 GL 热路径的每次访问上——这是 inproc 从"成本可疑的实验"变成"可交付形态"的直接原因(Android 上 dlopen 的库无法可靠用 initial-exec TLSpGLContext->MG_Impl 有 1494 处)。
  • MobileGLServer:桌面 add_executable 链接 MobileGL_sAndroid add_executable 改名 lib*.so 链接共享 MobileGL,由 AGP 打进 jniLibs
  • MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>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_testSPLIT 后缀(否则与同 case+backend 重名)并加 -DTRACE_TRANSPORT=run_trace_case.cmake 消费。
  • CI.github/workflows/test.yml:809 pipe-gatesP0 已落地):gen_pipe.py 重生成 + diffMG_Backend/MG_State 下禁止 stdio 插桩的 grep 门;gen_pipe_dirty_surface.py --summary(信息性,P1 成门);check_doc_citations.py(警告级,文档定稿后 --strict)。独立 job flatc-check。后续:include-graph-checkP0.5)、monolith-symbol-report

附 A:开关

CMake

选项 默认 状态
MOBILEGL_BUILD_DISAGGREGATED OFF 已落地
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 ONP2..P13 计划(编译期臂)
MOBILEGL_FLATC_EXECUTABLE 已落地(只服务 flatc-check
MOBILEGL_BAKED_INTERNAL_SHADERS ONP7+ 计划

运行时,MGPipeMobileGL/Config.h:319-358MobileGL/ConfigLoader.cpp:245-256P0 已落地):

变量 默认 说明
MOBILEGL_PIPE_PUSH 0 子系统位图(0 = 全 pull),含一位关闭 CSO 内容寻址;十进制或 0x
MOBILEGL_PIPE_VERIFY 0 逐 draw 逐字段影子比对
MOBILEGL_PIPE_STATS 0 边界计数器(§附 B
MOBILEGL_PIPE_LEGACY_MEMOS ON 三态读取,只有显式 falsy 才关
MOBILEGL_PIPE_TEXEL_RETAIN_MB 004096 纹理拉取保留 LRU
MOBILEGL_PIPE_INDEX_MIRROR_MB 6404096 索引宿主镜像预算
MOBILEGL_PIPE_STATS_PERIOD 120110⁶) 每多少帧一条汇总行
MOBILEGL_PIPE_STATS_FILE teardown 时的 JSON 转储路径

运行时,传输与 IPC(计划,P5+):MOBILEGL_TRANSPORT(monolith)、MOBILEGL_IPC_SERVER_PATHMOBILEGL_IPC_RING_MB(8)、MOBILEGL_IPC_STAGE_MB(32)、MOBILEGL_IPC_PRESENT_CREDIT(1)、MOBILEGL_IPC_SPIN_US(50)、MOBILEGL_IPC_POLL_ESCALATE(64)、MOBILEGL_IPC_PERSISTENT_BLOCK_KB(64)、MOBILEGL_IPC_ADOPT_TIER(auto)、MOBILEGL_IPC_SHADOW_SHM(1Phase 2 起)、MOBILEGL_IPC_INLINE_PAYLOADS(0,负面对照)、MOBILEGL_IPC_SERVER_AFFINITY(auto)、MOBILEGL_IPC_STRICT_ERRORS(0)、MOBILEGL_IPC_AUDIT(0)、MOBILEGL_IPC_TRACE(0)、MOBILEGL_IPC_ATTACHMOBILEGL_IPC_RESPAWN(0)、MOBILEGL_IPC_IDLE_EXIT_S(30)。显式不设立:MOBILEGL_IPC_PROGRAM(没有 relink 档)、MOBILEGL_IPC_VALIDATE_SERVERserver 没有 MG_Impl 校验器)。既有负面对照开关(MOBILEGL_ESPRYT_DISABLE_{UBO,UNPACK,UPLOAD}_RING_INVALIDATE_FLUSHMOBILEGL_DISABLE_LARGE_BUFFER_ADOPTIONMOBILEGL_COHERENT_AS_FLUSH)全部保留。

附 B:边界计数器(MobileGL/MG_Util/Metrics/PipeStats.h:46-122P0 已落地)

关闭时每站点一次全局 load + 一条永不命中的分支。字节类:stage-bufferstage-texturestage-ubo-globalstage-ubo-named(只有 Magma 贡献,D-B8 的不对称)、stage-vertex-clientstage-index-clientstage-indirect-cmdEspryt 独有)、persistent-map-pushP0 未接线:monolith 期不存在推送)、residual-value-block(占位)。调用类:drawsaccessor-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 下逐帧 TracyPlotteardown 时可选 JSON。站点清单——哪些路径没有接线——写在 MobileGL/MG_Util/Metrics/PipeStats.cpp:16-100,那份清单是契约。