- docs/Disaggregated/PLAN.md is now the one plan: the MGPipe design with the transport, control-plane, present, threading, EGL/process, monolith/build chapters inlined as real chapters (7-13) instead of references, sections renumbered 0-17 + appendices, every internal citation updated - the replica-GLContext plan and its review record are removed at the user's request; REVIEW.md is the MGPipe design-competition and adversarial-review record only, with the comparison verdicts dropped and the remaining finding text reworded to the new section numbers - day-43 GO/NO-GO now names its two outcomes (continue / shrink to headless tooling or re-evaluate) without any rollback path
305 KiB
MobileGL 前后端进程拆分实施计划(MGPipe)
状态:设计定稿 v2(2026-09-05,经三视角对抗性评审修订;评审记录见同目录
REVIEW.md)。基线dev@81b17c0b;实施分支feat/disaggregated(worktree../MobileGL-disagg)。 本文是本项目前后端进程拆分的唯一实施计划。它定义一份显式的前后端接口 MGPipe(gallium 式、句柄寻址、只推不拉),让MG_Backend拥有自己的状态机,并在此之上把前后端拆到两个进程。传输、数据面、控制面、同步、present、线程、平台与构建(§7-§13)是本文自带的章节,不依赖任何外部文档。 全部file:line引用针对工作树dev@81b17c0b。工作树有两处未提交的fprintf插桩,使DirectGLES.cpp在 ~660 行之后偏移 +11、Managers.cpp在 872 行之后偏移 +3;MG_State/、MG_Impl/、MG_Backend/DirectVulkan/的行号与 HEAD 一致。 v2 修订说明:v1 里一批继承自调研报告的SamplerObject.h行号(:455-492、:532-537、:551)指向文件末尾之后——该文件共 160 行。实际位置:BorderColorForm在:60-70、SamplerParameters在:72-96、GetLifetimeId()在:141、BumpVersion()在:151、m_version在:155。P0 增加一条 CI lint:本目录下所有.md里的file:line必须在基线提交上解析到存在的行(git show <base>:<path> | wc -l比较),防止同类转抄错误再次进入实施规格。
0. TL;DR、推荐与决策
0.1 一句话
MG_Backend 已经是一台贴着目标 API 的状态机;它缺的不是状态,而是一份"我被告知了什么"的显式声明。MGPipe 就是那份声明。 前端不再让 backend 每 draw 走 293 次 MG_State::pGLContext-> 把整个 GLContext 拉出来,而是在每条命令之前由一个 state tracker 把变化推过去;server 进程因此只需要装 MG_Backend + MGPipe 的对象表,不链接 MG_State、不链接 MG_Impl、不链接 glslang。
0.2 接口不是从 gallium 自顶向下设计的,是从两个 backend 自己维护的关键结构反推出来的
这是本设计与"照抄 gallium"的根本区别,也是完整性论证的来源:
| backend 已有的结构 | 它是什么 | 反推出的接口 |
|---|---|---|
SetupDrawSnapshot(VulkanRenderer.h:948-1042,40+ 字段) |
Magma 一次 draw 必须钉住的全部东西的枚举 | set_* 组的并集 |
DrawTextureSyncKeys + BackendTextureObject::IsDrawSyncClean(Managers.h:1003-1020) |
Espryt 纹理"是否还干净"的全部输入 | set_sampler_views + create_sampler_view + set_texture_params |
ResolvedDrawBuffers(Managers.h:697-717)/ ResolvedVertexBindings(VulkanRenderer.h:1153-1218) |
顶点输入的完整声明 | bind_vertex_elements_state + set_vertex_buffers + set_index_buffer |
g_syncedRenderStateParameters(DirectGLES.cpp:1956) |
渲染状态声明,逐字节 | create/bind_render_state + set_dynamic_state(见 0.4 D-B1) |
UnpackStagingBlock(Managers.cpp:4340-4390,{src, rowBytes, rows, slices, srcRowStride, srcSliceStride, offset}) |
Espryt 纹理上传的带步长的源描述符,已经存在 | MGPSubData 的 region 形状 |
BufferBackendOps(BufferObject.h:76-120,7 个 hook) |
已经是接口,且注释自称 "the pipe_context buffer-op analogue"(:68) |
resource_* 全族 |
把这些结构的输入集合推过去,接口就按构造完整。gallium 是目的地(同名同形的词汇让形状可读、可迁移),不是推导前提。凡 gallium 的词汇与本仓库的证据冲突的地方,本文按证据走,并在 §3.6 逐条记名列出偏离与理由。
0.3 四条结构性推论(决定了后面每一节)
推论 1 — 推送必须发生在 verb 时刻,不是 GL setter 时刻。 Blaze3D 每个 batch 都用 glEnable/glDisable(GL_BLEND) 包住,代码自己把它标成最热的路径(DirectGLES.cpp:2029-2032:mc_state_toggle 干的最热的事)。天真的 per-setter 推送会把每一次冗余开关变成一次接口调用加一次 server 侧 CSO 查表,严格慢于今天。正确形态是 gallium 的 st_validate_state。
v2 修订:v1 把这条写成"只有资源 mutation 在 GL 调用时刻推送——这恰恰是 BufferBackendOps 今天的做法"。这句话对 buffer 成立,对纹理不成立。 实测:glTexSubImage* 根本不调 backend 表——MG_Impl/GLImpl/Texture/GL_Texture.cpp 里只有 3 处 MarkStorageDirtyRegion,全部纹理上传由 Espryt 在 sync 时刻按累积区域做(Managers.cpp:4274-4390),那里才跑 MipmapStorage 的 96-rect 级联合并与 summedArea*4 >= unionArea*3 回退,并在 unpack ring 可用时刻意把 rect 列表塌成一个 union box(:4386-4390:if (BufferImpl::UnpackRingAvailable()) dirtyRectCount = 0;,注释记录 ~100 个精灵 rect 变成 ~100 个 Mali 作业,实测 +6 ms/frame)。若每次 glTexSubImage 发一条 resource_subdata,就精确复现了那个 ~100 作业的形状。规则的正确措辞见 §4.1.1。
推论 2 — handle 就是身份,而且必须是稠密 slot。 每个前端对象已经有一个永不复用的 GetLifetimeId()(BufferObject.h:202-208、VertexArrayObject.h:110-120、FramebufferObject.h:151-158、ProgramObject.h:1620、TextureObject.h:83、SamplerObject.h:141),它们存在的唯一理由是 GL name 会被 IndexGenerator::Generate 从 free list 尾部 LIFO 复用(MG_Util/Miscellany/IndexGenerator.h:30-42)、堆地址会被分配器复用。但单调的 64 位 id 不能索引数组——如果 wire handle 直接用 lifetimeId,server 侧仍然是一张哈希表,那就只是把指针键换成整数键,并没有删掉查表层。所以 wire handle 是 {slot: Uint32, gen: Uint32},slot 由 client 按 kind 稠密分配,gen 在 slot 复用时 ++。lifetimeId 留在 client 侧作为 tracker 自己的身份,不过线。这一条才真正把 6 个 StateBackendObjectRegistry 哈希表和 13 个 Magma 身份键缓存变成数组。
推论 3 — server 拥有 client 看不见、也永远不该被问的 generation。 今天有 12 个纯 backend 侧的单调计数器,它们表达的是"我自己重新铸造了驱动对象",与任何前端版本无关:Espryt 的 g_bufferMutationEpoch(Managers.h:397-441)、g_bufferBackendIdGeneration(:551)、g_attachmentBackendIdGeneration(:1298)、g_backendContextGeneration;Magma 的 m_textureImageEpoch、m_resourceEraseEpoch、m_renderbufferImageEpoch、m_sliceEpochCounter、m_cacheStructureEpoch、m_evictionEpoch、m_recordingGeneration、m_frameSerial。本文把它们统称 MGGen,它们永不上线。"server 拥有自己的状态机"在工程上的确切含义就是这一条:client 绝不是"我的 server 侧状态是否新鲜"的唯一权威。
推论 4(v2 新增)— dirty 位对值类组可以轮询**,对对象类组必须标记。**
v1 同时主张两件互斥的事:§4.2 说"dirty 位全部来自已有计数器,MG_State 零新增记账",§4.1/§13.2 说稳态是"一次 64 位 dirty word 测试"。对值类组(渲染状态、pack、patch、attrib 默认值)两者兼容——一个 Uint16 比较就是全部。对对象类组不兼容:NEW_SAMPLER_VIEWS 在 §4.2 里映射到 GetContentVersion/GetShapeVersion/GetTextureParamsVersion(逐纹理)加 GetTextureBindGeneration()/GetSamplingResolutionGeneration(),没有任何聚合能回答"有没有哪张已绑定纹理的内容动了"。这正是 Magma 不得不用有损的 sampledContentSum/sampledParamsSum(VulkanRenderer.h:975-1000)的原因。轮询版本 = 每次 validate 走查 touched 单元,那不是 O(1),而且是新增的 client 侧工作(backend 的 ResolvedTextureBindingMemo 今天恰好跳过它)。
决定:
- 值类组:沿用既有计数器,O(1) 比较,
MG_State零新增。 - 对象类组:在
MG_State里新增 5 个聚合世代计数器,在既有的 choke point 上 bump,让 tracker 的快门是 O(1):TextureState::m_anyTextureContentGeneration(ITextureObject::MarkStorageDirtyRegion/BumpContentVersion里 ++)TextureState::m_anyTextureParamsGeneration(BumpTextureParamsVersion里 ++)BufferState::m_anyBufferChangeGeneration(BufferObject::BumpChangeSerial里 ++)VertexArrayState::m_anyVaoAttributeGeneration(属性/绑定点 setter 里 ++)FramebufferState::m_anyAttachmentGeneration(attachment setter 里 ++) 合计约 20 行,全部落在既有的 bump 点上,不是枚举 181 个 GL 入口。快门为真时 tracker 才做 touched 前缀走查并重算集合 hash。
- 完整性绊线:新增
scripts/gen_pipe_dirty_surface.py:它枚举MG_Impl/GLImpl/**里每一个会改变某组的 mutator,映射到必须 bump 的聚合世代,CI 上重生成 +git diff --exit-code,未映射的 mutator 直接失败。这是 B-R6 的第四层,也是对"reconciler 完整性只有测试绊线"这条历史结论的第二个答案。 - §4.2 的措辞随之改为"值类零新增记账;对象类新增 5 个聚合世代,换掉 tracker 的逐对象走查"。§13.2 的稳态成本行同步改写(见 §13.2)。
0.4 八个必须先记下来的具体决定(这些是评审里争议最大的点)
D-B1(v2 重写):渲染状态用"整块 blob"过线,但 CSO 的身份只取 pipeline 相关子集,动态状态单独走。
v1 写的是"整块 blob + CSO handle,绝不拆成 blend/depth-stencil/rasterizer 三个 CSO",理由全部成立且保留:RenderStateParameters(RenderState.h:222-370)是平凡可复制 POD,Espryt 在 DirectGLES.cpp:2035 亲自 static_assert(std::is_trivially_copyable_v<...>),紧接着做 head/blend/tail 三段 memcmp(:2038-2047);RenderState.h:359-368 白纸黑字写着 ScissorBoxWrittenMask 与 ClipDistanceEnabledMask 是故意摆在 tail 段里,好让那次 span memcmp 抓到它们;字段顺序是承重的;拆成三个 CSO 要手工维护一张 ~150 字段划分表且没有完整性绊线。
但 v1 同时犯了一个内部矛盾:它一边在 D3 里说"CSO 边界跟 Vulkan 动态状态走:viewport、scissor、depth range、blend color、line width、depth bias、stencil ref/write mask 是 set_* 而非 CSO 字段",一边把 CSO 的内容寻址键定义为整块的三段 xxHash。两者不能同真:整块内容寻址意味着 glViewport/glScissor/glBlendColor/glClearColor/glLineWidth/glStencilMask/glPolygonOffset 每一次都产生不同的 hash、不同的 CSO handle,于是 (a) 64 项 LRU 在 Iris 光影与阴影级联下颠簸,(b) 每次未命中重发 ~1.2KB,(c) 新 handle 冲掉 server 侧按 CSO 缓存的 pipeline hash——正是 RenderState.h:519-528 记录的那次回归("共用一个计数器让 glViewport 把下一个 draw 从 pipeline memo 和 draw 快路径上打下来")。实测确认:RenderState.cpp 里 viewport/scissor/line-width 一族的 setter 只做 ++m_version,SET_CAPABILITY(:312)与 pipeline 相关 setter 才做 BumpVersions()。
最终形态:
create_render_state(cso, MGPBlobRef pipelineSubsetChunks) // 只带 pipeline 子集的字节段
bind_render_state(cso, Uint16 version, Uint16 pipelineVersion) // 稳态 12 B
set_dynamic_state(MGPBlobRef dynamicChunks, Uint16 version) // 只带动态子集的变化段
- server 每 context 持有一份 working
RenderStateParameters(~1.2KB)。bind_render_state把 CSO 的 chunk 散射进去,set_dynamic_state把动态 chunk 散射进去。Espryt 的SyncRenderState拿到的仍然是一个const RenderStateParameters&,693 行函数体与三段 memcmp 一行不动。 - Magma 的 pipeline memo 键是
cso.slot——glViewport不再冲掉它;动态尾巴仍按set_dynamic_state的 version 走ApplyDynamicDrawStateTail今天的两级门。 - 划分只写在一个地方:
MGPipeComputePipelineSubsetHash(const RenderStateParameters&)与它的 chunk 表,从VulkanRenderer.cpp:4826-4906原样搬进MG_Pipe/,client 与两个 backend 共用同一个函数。这样"哪些字段属于 pipeline"不再有第二份定义。 - 完整性绊线(这是 v1 拒绝三 CSO 时点名要求、却没给自己的那一条):G7 生成一个
MG_Test,遍历MG_State::GLState::RenderState的每一个 public setter,用一个不同的值调用它,断言pipelineSubsetHash 变了 ⟺ m_pipelineStateVersion 变了。新加一个 setter 若BumpVersions()却不在 chunk 表里,这个测试立刻红。 - 两个版本计数器都过线(
RenderState.h:522/:529),职责不变。 - 两套 span 划分并存,互不干扰:Espryt 的 head/blend/tail 三段是驱动侧增量的划分(不动);pipeline/dynamic 是线上与 CSO 身份的划分(新增)。两者都有各自的绊线。文档必须写清楚它们不是同一件事。
- 热路径成本(诚实版):
m_pipelineStateVersion未动 → 复用上一个 CSO handle,零哈希;动了 → 哈希 pipeline 子集(~25-30 字,正是 Magma 今天已经在算的那个)+ 一次 map 探测。Blaze3D 的 enable/disable 交替会命中两个交替的 CSO,不重发 blob。对比今天:Espryt 1.2KB×3 段 memcmp + Magma ~30 字哈希。净变便宜,但差距不大,所以 P2 必须带一个专门的 enable/draw/disable/draw 微基准(MC batch 速率)。
D-B2:create_shader_state 不返回一个"做完了的"对象。 backend program 还依赖 8 个额外输入(DirectGLES.cpp:2766-2818:draw FBO 的 snorm/unorm fallback clamp mask、由 draw-buffer 数组推出的 fragColor 广播数、storage-block 绑定签名、atomic counter 绑定集、活的 glBindImageTexture 格式、patch 参数;Magma 另加 FragCoord-Y-flip 的 default-FB 高度和 XFB 布局)。接口明说规则:create_shader_state 发布制品,server 在 verb 时刻从它已经被推送过的状态惰性特化。这正是两个 backend 今天的做法。
D-B3(v2 重写):真正承重的不是"framebuffer 第一",而是"verb 之前状态齐全 + verb 处惰性特化"。
v1 把 §4.3 的编号顺序(1 framebuffer → 2 program → 3 images → 4 render state → 5 vertex)写成契约,并说这是退役 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573,注释明说"不可表达为单调版本")与 fragColor 重推导 workaround(DirectGLES.cpp:2712-2732)的机制。但它自己把 images 排在 program 之后——所以退役这两条的其实是 D-B2 的惰性特化,不是调用顺序。
规范条款改为:
一条 verb 的全部
set_*/bind_*必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,set_*之间没有顺序要求。
§4.3 的编号列表降级为推荐实现顺序(便于 tracker 的代码组织与 dirty 位遍历),不再是正确性契约。收益不变:DirectGLES.cpp:2712-2732 的 workaround 与 g_broadcastMemo* 照删,因为特化发生在 verb 处、那时 FBO 状态一定已在。
D-B4:AcquirePersistentMap 在整个改造期一动不动。 它是永久的地址空间捐赠而不是 gallium 的 scoped transfer_map:返回一个 host-visible coherent 指针,成为该 buffer 的唯一真相源(BufferObject.h:102-118),由 PipeResource::AdoptPersistentMap(PipeResource.h:115)采纳、经 MappedData() 交给应用、≥16MiB 可变 store 由 TryAdoptLargeStorage 自动走到(:226-228)。实测代价是 MC 26.3 的 p99 163→21ms、40→115fps、省 ~400MB。它今天就已经是一个"返回指针的显式调用",因此原样穿过 monolith 改造;只有 IPC 那一步才会打破它。 改造期不碰,IPC 期按 §7.8 的三档 POST 探针决定,spike B 第一周给答案。绝不允许一个平台未知数挡住 267 天的接口工作。
v2 补注:map_persistent 的 round trip 是每次存储定义(respecify)一次,不是"每 store 生命周期一次"——TryAdoptLargeStorage 在存储定义时触发,一个反复扩容的 arena 会付 N 次。StorageBufferRegrowScenario 必须发布 map-persistent-roundtrips 计数。
D-B5(v2 修订):monolith 的字节一致门按构造死亡,这是本方案的成本;但语义门必须活过 P13。
一个"改前改后 nm --defined-only 与剥调试信息后的 .text size 完全相等"的 monolith 门在本方案里不成立——不存在任何配置能让旧字节回来。替换是五部分门(§13.3),其中第 ② 部分(每 draw 逐字段的 pushed-vs-snapshot 影子比对)在语义上严格强于任何符号 diff。
但 v1 的 P13 删掉 SnapshotFromGLContext(),而那正是 verify 的参照物来源——删完之后 verify 无物可比,设计从此没有语义绊线。修正:
SnapshotFromGLContext()与它需要的MG_Stateinclude 在 P13 之后继续存在,但整体包在#if MOBILEGL_PIPE_VERIFY里;verify 构建永不出货。- 纯度门(
grep -c 'pGLContext' MG_Backend/== 0、include 白名单、nm --undefined-only)只跑非 verify 构建,这一点写进门的定义。 - 另外在 P13 交付 §13.4-9 已经勾勒的录制-金标模式:把
MG_Test的 mock backend 变成 MGPipe recorder,在一组 fixture 上录下每 draw 的已推送状态,后续构建对比录像。它不依赖MG_State,所以是长期可用的语义门,也是开放问题 11 的答案。
D-B6:本方案引入一个新的停顿类:server 发起的纹理重铸拉取。 server 不保留纹素字节,所以 RequireImageBindableStorage 的 re-dirty(Managers.cpp:2813)、整格式再生(:3950-4195)、view 源重铸(:3616-3707)都必须回头向 client 要数据。三条缓解同时上,不是三选一,加一个专门的门、一个逐 trace 用例发布的计数器,以及一个显式的"答不出来"终止符(§6.5)——因为存在 client 没有字节可发的 level(纯渲染产生、CanMirrorCopyImageShadow 拒绝的 copy 目标、GPU 生成的 mip),没有终止符 apply 线程会永久 park。上一轮 thin-server 设计正是因为把这条一笔带过而被判死。
D-B7(v2 新增):restart 重写与 multi-draw 分档留在 server**,split 下由一份索引宿主镜像喂养。**
v1 的 §4.8 把这两条按 !kCapPrimitiveRestart / !kCapMultiDraw 下放到 client,而 §3.5.7 的表又写"monolith:ptr 指向 shadow(server 做)"——两处互相矛盾。更根本的是这个划分不可表达:
ResolveTierForBatch(MultiDraw.cpp:282-320)逐 batch在五档里选,输入包含programReadsDrawID——转译出的 ESSL 的性质,只存在于 server——以及perSubDrawBaseVertex、hasIndexBuffer、arbitraryRestart,并在kMaxFlattenedIndices(:72,1<<24)与kMaxComputeFlattenedIndices(:82)上做容量判定。自动阶梯是 Ext → BaseVertex → MultiIndirect → Indirect → DrawElements(:241-243),CPU 展平的DrawElements档是回退,client 无法预判。- restart 重写两个 backend 都做(
DirectGLES.cpp:4283/4377、VulkanRenderer.cpp:3990/4089/4161),所以kCapPrimitiveRestart恒为 false,"cap 门控"没有门可控。
决定:kCapPrimitiveRestart / kCapPrimitiveRestartFixedIndex / kCapMultiDraw / kCapMultiDrawIndirect / kCapMultiDrawIndirectCount 作为归属开关删除。规则改为一句话:multi-draw 分档与 restart 重写永远由 server 拥有;client 在 caps 说 server 可能需要时提供索引字节。 提供方式不是逐 draw 拷贝,而是:
kCapNeedsHostIndexBytes开启时,server 为"曾被绑为GL_ELEMENT_ARRAY_BUFFER的 buffer"维护一份宿主镜像,由它本来就要收的resource_subdata/resource_respecify流增量维护,零额外线上流量、零 round trip。预算MOBILEGL_PIPE_INDEX_MIRROR_MB(默认 64),逐帧计数;超预算时该 buffer 退化为逐 draw 通过MGHostSpan传送并计入index-bytes-shipped计数器。
好处:monolith 行为零变化(不搬代码、不改诊断落在哪个线程 → 开放问题 12 关闭)、split 下 restart/multidraw 零 round trip、kMaxRestartRewriteBytes = 1<<26(64 MiB,DirectGLES.cpp:4218)这种单条记录不再需要塞进 32 MiB 的 SEG_STAGE。代价是那份镜像的内存,已计入 §7.9。
D-B8(v2 新增):per-draw 的具名 uniform block 字节必须有自己的载体。
v1 §6.2 断言 20 处 SyncPersistentMappedRange "作为反向调用彻底消失,因为紧邻它们的 CPU 读全部搬到了 client"。有一处反例:UniformManager::ResolveUniformBufferPayload 在 UniformManager.cpp:2022 调 SyncPersistentMappedRange(),随后在 :2052 读 bufferObject->MappedData() + rangeStart(不足时在 :2053-2057 零填充),把具名 UBO 块打进 Magma 自己的 UBO ring——消费者在 server,搬不走。而 §3.4.3 的 set_shader_buffers 只有 V 标志,没有 kHasBlob/MGHostSpan;set_global_constants(D6)只覆盖默认 uniform block。结果是每个带具名 UBO 的 Iris/MC draw 都有一条没被承载的数据依赖。
决定:set_shader_buffers(cls == Uniform, ...) 的每个 range 增加可选的 MGHostSpan payload(kHostSpan 标志),由 kCapNeedsHostUboBytes 门控(Espryt 不需要——它把具名 UBO 直接绑给驱动)。字节量进 SEG_STAGE 的尺寸表(§7.1)与 P0 计数器(stage-ubo-named)。在 P0 计数器给出逐帧字节量之前,不冻结这个 payload 的形状。 备选(不在本计划内、需独立 dev PR + Iris 性能门):让 Magma 直接描述符绑定常驻 VkBuffer 的 range,不再 ring-pack。
0.5 推荐
按下面这条对冲路径起步,在第 43 天做一次真正的 GO/NO-GO:
先跑 P0(卫生、度量、门与骨架,含两个 spike,尤其是 TracyPlot 逐帧字节与调用计数器——树里今天完全没有 per-frame 字节或调用度量,MG_Util/Metrics 只是格式算术,Tracy 只有 zone 无 plot),然后跑 P0.5 + P1 + P2。
- 第 ~25 天(P1 出口)— 机制里程碑,零产品风险:
MOBILEGL_PIPE_VERIFY影子比对 harness 在全部 40 个 trace 用例与 367 个集成测试上逐 draw 逐字段证明"推送等价于拉取"。这一天不是 GO/NO-GO——它只证明机制,不给性能数字。 - 第 ~42 天(P2 出口)— GO/NO-GO。
v2 修订:GO/NO-GO 的口径必须包含一片 Track H,否则它测的不是它要决定的事。
v1 把 GO/NO-GO 放在"只迁了渲染状态"的时点,而渲染状态恰好是推送收益最小、v1 的 CSO 设计开销最大的那个面:Espryt 已经有逐字节镜像 + 单个 Uint16 早退(DirectGLES.cpp:2016-2018),Magma 已经按 GetPipelineStateVersion() 缓存哈希(:4982-4993)并双门控动态尾巴(:5888-5893)。绿灯不能证明它要担保的事(Track H 的 handle 化在 267 天里划得来),红灯更可能是在指控 CSO 设计而不是推送模型。
因此 P2 的范围扩大为:渲染状态 CSO(双后端)+ 最便宜的两片 Track H——Espryt 的 0b handle 基建(SlotAllocator + 6 个 registry 变 slot 数组 + 删 TwinLookupMemo×3/OwnerEquals)与 Magma 的子系统 4(VertexInputStateFactory/VaoDrawMemo 重键,§5.5 自评"低(纯结构性收益)")。第 43 天你手上会有:
- 逐 draw 逐字段的语义等价证明(P1 交付);
- 两个 backend 上都已推送的渲染状态,
SyncRenderState的 693 行函数体一行未动; - Track H 的实测单位成本(两片,两个 backend 各一);
- 两台设备上 reboot-clean 配对的逐线程 CPU 时间增量,含一个专门的 Blaze3D blend-toggle 微基准;
- 一个负面对照:关掉 CSO 内容寻址(
MOBILEGL_PIPE_PUSH的一个子位)重跑,把"推送更慢"与"CSO 设计更慢"分开。
GO/NO-GO 的两个出口,写死在这里:
- 继续:第 43 天的逐线程 CPU 增量在两台设备的 p50 与 p99 上都不为负、tracker 每 draw 的绝对 ns 落在预设上限内、Track H 的实测单位成本不超出 §5.4/§5.5 估计的 50%。此时按 §14 的两条跑道推进(monolith 跑道 P3a→P4a→P3b/P4b→P7→P13,IPC 跑道 P5→P6→P8→P13)。
- 收缩为 headless 工装用途或重新评估:任何一条判据落空时,不回滚。P0/P0.5/P1/P2 的产物全部是自洽的 monolith 交付物——handle 基建与
{slot, gen}重键、MGPipeValueTypes.h与ProgramArtifacts.h的头文件抽取、逐帧字节与调用计数器、MOBILEGL_PIPE_VERIFY影子比对 harness、渲染状态 CSO——它们就地保留在dev上。MGPipe 本身收缩为 headless 工装用途:MG_Test的 mock backend 变成 MGPipe recorder(§13.4-9),给tools/trace_replay一种比 apitrace 精确得多的、记录已解析状态的录制格式;inproc作为渲染线程实验保留在 CI 形态下。IPC 跑道整体搁置,等一个新的判据(例如 §13.2 的 CPU 数字在别的子系统上转正、或产品侧对崩溃隔离提出硬需求)再重新评估。
沉没成本(诚实版):P0(9-11 天)的卫生、度量与骨架无论后续走哪条路都要花;P0.5 的头文件抽取本身就是 monolith 的净收益(它让制品头不再拖 glslang 与 spirv_reflect)。真正只为 MGPipe 押上的是 P1 + P2 ≈ 28-39 天,而这 28-39 天在 NO-GO 分支下仍然留下上面那份可用产物。v1 说"只损失 16 天"是按一个与它自己的子系统表矛盾的排期算的。
1. 目标与非目标
1.1 目标
- 定义并落地一份显式的前后端接口 MGPipe:句柄寻址、只推不拉、gallium 形状,client 与 server 都只依赖它。
- backend 拥有自己的状态机:
MG_Backend在 MGPipe 构建(非 verify)下不含MG_State::pGLContext,MG_Stateinclude 收缩到一张共享值头文件白名单,server 产物的nm --undefined-only里没有MG_State::GLState::符号、没有 glslang 符号。 - 前后端跑在两个进程,通过 IPC 通信;client 把状态 reconcile 成推送调用、序列化(FlatBuffers)后发送;server 更新自身状态并调 backend API。
- 稳态帧零 round trip(回读 / 阻塞式 query / sync wait / present credit / 分配类错误 ack / 纹理拉取之外,且后者的次数必须实测发布而非声称为零)。
- 两半尽可能互相异步;client 至多领先 server 1 个 present(默认,延迟叠加分析见 §9.1)。
- 平台特定代码最小化并集中在
MG_Remote/Transport/与MG_Remote/Client/Surface*(§11)。 - 单进程 Monolith 保持功能与性能不回归,由五部分门机械验证(§13.3)。注意这不是字节级不变——见 D-B5。
- 所有验收门用现有测试:
ctest -L unit(428 个TEST()/-L integration-gpu(367 个TEST_F,75 个场景文件)/tools/trace_replay(40 个用例,默认 SSIM ≥ 0.99)/tools/cts/tools/device_bench。 - 接口本身是可独立交付的产物:即使 IPC 永不上线,
inproc(同进程第二个 apply 线程)就是 monolith 的渲染线程交付物,且是本项目手上最大的单一 CPU 杠杆。
1.2 非目标
- share-group sessioning 重构。
eglCreateContext的shareCtx只在EGLState/Core.cpp:632被校验、:640被存进EGLContextState::SharedContext,全代码库无人读取;pGLContext是唯一进程全局(GLState/Core.cpp:20, 1487)。v1 = 一条 flow、一个扁平 handle 空间。但接口头文件从第一天就把MGPipeScreen与MGPipeContext分开(§3.3)。c7c9e346/29d721ef那套整体丢弃(理由见 §17 的 DROP 名单)。 - BFA strict-C-ABI backend 插件 / UtilRuntime C-ABI 化(理由见 §17 的 DROP 名单)。
- macOS 拆分(
CAMetalLayer无公开跨进程表示 → monolith only)。 - Windows 窗口拆分(headless/pbuffer only,见 §11.5)。
- 把 emulation 层重写到 client。 只有三个"读前端字节的纯 CPU 变换"下放到 client(v1 说五个,D-B7 收回了两个):client 顶点数组的范围计算、最大索引扫描、
*IndirectCount的计数解析。viewport-array 回放、multi-draw 分档、primitive-restart 重写、fp64 顶点转换、image-bindable 存储加宽等全部留在 server 作为 lowering pass,接口只负责把它们的输入表达清楚(含 D-B7 的索引宿主镜像)。 - 在 P13 之前删除 pull 路径。 旧路径一直编译在里面,任何提交都能用一个 env 位 A/B(但要注意 §5.7 说明的 A/B 口径在 stage C 之后会收窄)。
2. 现状:边界为什么不清楚
2.1 今天的边界有七个面(数字按工作树复核)
(a) GLFunctionsTable — MG_Backend/BackendObject.h:117-278。实测 67 个函数指针 + 1 个 Bool 能力位(PrefersCpuXfbPrimitiveAccounting),GlobalBackendFunctionsTable(:279-285)再加 Present 与 SetSwapInterval → 全体 69 个函数指针。
MG_Impl 侧 ~93 个 gBackendFunctionsTable.GL.* 调用点,覆盖 70 个不同表项。null 项已经表示"未实现,前端回退",写进头注释(:212-215 的 sync 族、:265-269 的 XFB 跨度),且 DirectVulkan 确实留空 8 项而 Espryt 填满。三项是错位的前端查询:GetIntegeri_v/GetInteger64i_v(:195-196,DirectGLES.cpp:7264-7386 有 15 个 case 完全不碰 GL)、GetProgramiv(:197)。
这 70 个表项里只有约 22 个是 draw/dispatch(20 个 draw 族 + DispatchCompute/DispatchComputeIndirect)。其余 ~48 个是 clear(9)、blit(2)、copy(3)、GenerateMipmap、回读(4)、barrier(2)、XFB 跨度(6)、query/sync(~19)、BindImageTexture、PatchParameteri、ShaderStorageBlockBinding 等,而其中很多自己就读 pGLContext(例:UpdateTextureBindingAtTarget 在 DirectGLES.cpp:6051-6052 读 GetActiveTextureUnit() + GetTextureUnitObject(),被 CopyTexImage2D/CopyTexSubImage2D 路径命中;PackStateFromContext 在 :6129 读 GetPixelStoreParameters(false);Clear 在 :4106 读 GetRenderStateParameters().ClearColor、:4165 读 draw FBO;BlitFramebuffer 在 :5988-5989 读两个 FBO slot)。代码自己说明了这一点:DirectGLES.cpp:1501-1502 写着无参 CaptureDrawTextureSyncKeys 包装存在是"for every non-draw call site (Clear, readbacks)"。
这是 v1 的一个实质性缺口:它只在 PrepareForDraw 与 SetupDraw 两处填快照。修正见 §5.2.1 与 §14 P1。
(b) BackendObject 虚函数 — BackendObject.h:543-568,MG_Impl 侧 40 个 pActiveBackendObject->(其中 35 个是 GetDynamicParameters())。InitCapabilities() 懒执行在第一次成功的 eglMakeCurrent 内部(BackendObject.cpp:341-347),且每次 surface 变更重新武装(:301)。
(c) BufferBackendOps — BufferObject.h:76-120,7 个 hook,注册入口 :124。Espryt 注册 7/7(Managers.cpp:1338-1346),Magma 注册 6/7(故意不注册 ResidentSubData,VkBufferManager.cpp:104-111)。这个面已经是 MGPipe 的三分之一,且注释自称 pipe_context 类比。
注意它只覆盖 buffer。 纹理没有对应的 GL 调用时刻分发面(推论 1 的 v2 修订)。
(d) 状态拉取 — MG_State::pGLContext-> 在 MG_Backend 里 293 次出现 / 290 行(DirectGLES 124;DirectVulkan 169),外加 58 行非箭头用法(见 2.4)。此外还有约 1997 个前端对象 getter 调用点、186 个不同 getter(上界统计)。
(e) backend → frontend 写回 — 逐名 grep 实测 95 个调用点 / 17 个方法:SyncPersistentMappedRange 20、MarkStorageDirty 18、AllocateStorage 8、WritebackFromBackend 8、SetInternalFormat 7、SyncGpuWrites 6、MarkGpuWritten 6、RecordError 6、SetBackendResource 4、EnsureGpuResidentStorage 3、SetBackendHashMemo 2、InvalidateCompileEnv 2、SetBackendStateMemo 1、SetBackendAuxMemo 1、UpdateMipmapSubData 1、TruncateMipmapLevels 1、SetSamples 1。
(f) backend 反向进 MG_Impl — 恰好 6 处:DirectGLES.cpp:1917, 2838, 2867, 9675(pDefaultFramebufferInfo 身份比较)、SwapchainObject.cpp:276(写)、VulkanRenderer.cpp:10700(CopyTextureImageToClientOrPBO_State,一处真正的分层倒置)。
(g) MG_Impl 在 table 调用旁做的 MG_State mutation — EnsureGeneratedMipmapStorageAllocated(GL_Texture.cpp:501-544,调用点 :6698, 6708)与 AccountTransformFeedbackPrimitives(GL_Drawing.cpp:172,调用点 :1133, 1141, 1195, 1668)。在 MGPipe 里这个面的 replay 义务不存在(server 没有第二份前端状态可 replay);但标记义务出现(推论 4),由 dirty-surface 生成器覆盖。
(h) 工作树污染 — DirectGLES.cpp:640-663 与 Managers.cpp:875-877 的未提交 per-draw fprintf(stderr)(后者在 pendingMutex 临界区内)。P0 第一件事就是清掉。
2.2 backend 已有的状态机清单(这就是"server 已经是薄服务端"的实证)
DirectGLES(Espryt)
- 6 个 twin registry,全部是
StateBackendObjectRegistry<StateObject*, {BackendPtr, weak_ptr}>(模板Managers.h:270-390;实例:806(VAO):1123(Texture):1216(FBO):1731(Program):1830(Sampler):1858(Renderbuffer)),键是前端裸堆地址,用同址weak_ptr防 ABA,GC 阈值kGCInterval=1024draw /kCreationGCInterval=64次创建。 - 三条 persistent-mapped bump ring(UBO
Managers.h:591-637、纹理 unpack PBO:639-671、buffer upload:673-…),各自 4MiB 起 → 64MiB 上限;buffer pool 预算kMaxPoolBytes = 64MiB、单 buffer 上限 8MiB(Managers.cpp:564-565)。 - 每对象 twin:
GLESBufferResource(Managers.h:443-497)、BackendVertexArrayObject(:675-803)、BackendTextureObject(:944-1119)、BackendFramebufferObject(:1140-1213)、BackendProgramObjectImpl(:1473-1725)、BackendSamplerObject(:1808-1824)、BackendRenderbufferObject(:1838-1855)。 - 完整的渲染状态值镜像
g_syncedRenderStateParameters(DirectGLES.cpp:1956)+ 单个Uint16早退门(:2016-2018)+ 三段 memcmp(:2038-2047)。 - 驱动绑定影子、三个共享 scratch FBO 及其驱动侧 attachment 影子、
PackState。 UnpackStagingBlock(Managers.cpp:4340-4390)——一个已经存在的带步长源描述符,MGPSubData的 region 直接照抄它的形状(§3.5.6)。
DirectVulkan(Magma)
VulkanRenderer:PipelineMemoEntry m_pipelineMemo[8]、SetupDrawSnapshot m_setupDrawSnapshots[4](40+ 字段)、VaoDrawMemo m_vaoDrawMemoTable[2048]、ResolvedVertexBindings、m_convertedVertexStreams、DynamicStateShadow g_dynamicStateShadow、采样集/LOD/BaseVertex 三个 memo、11 个 per-draw scratch vector。- 5 个 manager(
VkBufferManager、VkTextureManager3504 行、VkRenderPassManager、VkSamplerManager、VkClearManager)、3 个 factory、UniformManager、FrameContext、SwapchainObject。
结论:两个 backend 都已经是完整的、贴着各自 API 的状态机。 上面没有一样东西需要删除或重写——需要改的只是它们怎么知道这些事实,以及它们的 memo 用什么做键。
2.3 pull 模型的读点分类:A/B/C/D/E 五类
| 类 | 含义 | DirectGLES | DirectVulkan | 合计 | 占比 |
|---|---|---|---|---|---|
| A | 只为探测变化 | ~21 | ~14 | ~35 | 12% |
| B | 翻译输入,backend 无镜像 | ~88 | ~128 | ~216 | 74% |
| C | 瞬时 draw 参数 | ~2 | ~2 | ~4 | 1% |
| D | 身份 / 缓存键(与 B 重叠计) | ~24 | ~24 | ~48 | — |
| E | 数据字节(经 pGLContext 本身) |
1 | 2 | 3 | 1% |
| 写 | RecordError 6 + InvalidateCompileEnv 2 |
2 | 6 | 8 | 3% |
这张表否定了两种直觉方案:
- "bump 一个版本让 server 自己拉"行不通。 只有 12% 是 A 类。74% 是 B 类:值本身必须过去。
- 两个 backend 想要的推送粒度不同,但可以被同一个接口满足。 Espryt 持有逐字节镜像;Magma 没有任何镜像,它按
GetPipelineStateVersion()缓存一个值哈希(VulkanRenderer.cpp:4982-4993),然后在 payload 构建器里把 ~40 个字段再读一遍(:5155-5200,仅在 pipeline memo 未命中时)。整块 blob 同时满足两者。
另一个角度:1997 个前端 getter 站点里,89 个是纯版本/序号读(A 类)——推送模型里根本不过线;72 个是数据字节读(E 类),全部在 §4.7/§4.8 处理;38 个是 GetLifetimeId() 身份读(D 类),全部变成 handle。
2.3.1 v2 新增:把"每 draw 成本"用动态口径说清楚
v1 的 §13.2 把今天的每 draw 状态获取写成 "Espryt 124 / Magma 169 次 accessor 调用"。124/169 是静态调用点数(§2.1(d) 的定义),不是动态每 draw 调用数。 树里每一处都已经被 memo 门控:
| 路径 | 稳态实际做的事 |
|---|---|
SyncRenderState(DirectGLES.cpp:2003) |
:2007 读一个 Uint16,:2016-2018 相等即 return。三段 memcmp 只在版本移动后跑。 |
SyncNeccessaryTextures(:1520) |
6 值键比较 + PairingsIntact + 每条目一次 IsDrawSyncClean 字比较;单元走查只在未命中时跑 |
CurrentUnitBindingsEpoch(:1418-1436) |
三值快门;owner 走查只在 bind generation 移动后跑 |
TrySetupDrawFastPath(VulkanRenderer.cpp:5994) |
~10 次 accessor + ~20 次字比较 |
GetOrCreatePipeline(:4948) |
:4982-4993 只在 GetPipelineStateVersion() 移动后重算哈希;:5155-5200 的 ~40 次 accessor 走查只在 pipeline memo 未命中时跑 |
ApplyDynamicDrawStateTail(:5871) |
:5888-5893 一次版本比较,然后一次 bulk fetch 建值键 |
所以真实稳态大约是每 backend 每 draw 10-25 次 accessor 调用加几十次字比较,不是 124/169。 推送模型的优势因此比 v1 声称的窄得多,而且它在 §13.2 的对照表必须按动态口径重写(已改)。推论:
- P0 的计数器交付物必须包含动态调用计数器(每 draw 实际执行的 accessor 次数、每个 memo 门的命中/未命中),不只是字节计数器——否则 P2 仍然是在猜。
- 第 43 天的 GO/NO-GO 阈值必须是一个绝对数字(tracker 每 draw 的 ns,两台设备实测),不能只写"落在 monolith-pull 的噪声内"——当真实基线是 20 次调用时,相对噪声阈值会平凡通过。
2.4 pull 模型里 293 之外的 58 行:迁移机制必须显式处理的缺口
| 形态 | 数量 | 例子 | 处理 |
|---|---|---|---|
MOBILEGL_ASSERT(MG_State::pGLContext, ...) 真值判定 |
~34 | DirectVulkan.cpp 密集区、UniformManager.cpp 9 处 |
直接删除(Defines.h:114 在非 debug 下宏为空,所以这批在 RelWithDebInfo 里本来就不生成代码);替换成 §5.2 的 poison mask |
if (MG_State::pGLContext) 空守卫 |
7 | Managers.cpp:3608(守 BackendTextureObject::StampViewSyncKeys 的三次赋值)、:3737, 3808, 4663, 8678、BackendObject_DirectVulkan.cpp:388, 788 |
删除守卫,改读 PipeInputs 字段(永远有效)。这批会改变 .text(见 §14 P1 验收修正) |
MG_State::pGLContext != nullptr ? A : B 三元 |
3 | Managers.cpp:7120, 7128, 7131(patch 参数,在 transpile 路径内) |
由 set_patch_state 覆盖,三元塌成直接读。改变 .text |
MG_State::pGLContext.get() 裸指针捕获 |
1 | DirectGLES.cpp:146 |
sed 完全抓不到,必须手改。相邻的 :142 还有一个 decltype(MG_State::pGLContext->GetFramebufferBindingSlot(...)) 类型别名,同属此类 |
!= nullptr 条件 |
14 | VulkanRenderer.cpp:11150, 12649 等 |
同空守卫 |
| 注释 | 1 | VertexInputStateFactory.h:133 |
改写措辞 |
因此:纯度门 grep 的是 pGLContext,不是 pGLContext->,且 P1 的机械替换步骤必须把这 58 行列成显式清单逐条转换。
2.5 pull 模型为了弥补"没有接口"而付的代价(v2:区分真删除与搬迁)
v1 把下表全部记作"~550 行删除"。其中一部分是搬迁,不是删除,必须分开记账,否则 §13.4 的 monolith 收益被高估。
真删除(结构性,{slot, gen} 与显式 destroy 让它们不可表达)
| 机制 | 位置 | 行数 |
|---|---|---|
TwinLookupMemo ×3(4096+256+64 槽 ≈ 140KiB)+ OwnerEquals |
DirectGLES.cpp:62-131 |
~75 |
g_fbSlotCache + GetFramebufferBindingSlotFast |
DirectGLES.cpp:139-155 |
~17 |
StateBackendObjectRegistry::CollectGarbage ×6 |
Managers.h:353-390 |
~40 |
m_convertedVertexStreams 的 SharedPtr sourcePin |
VulkanRenderer.h:1124-1127 |
~5 |
UniformManager 的 8 类占位 TextureObject 构造 |
UniformManager.cpp:161-181, 1416-1500, 1624-1634 |
~120 |
SetupDrawSnapshot 的 sampledContentSum/sampledParamsSum 与 ~14 个探测字段 |
VulkanRenderer.h:975-1000 |
~30 |
g_broadcastMemo* + fragColor 重推导 workaround |
DirectGLES.cpp:2669-2732 |
~60 |
VkTextureManager::PruneDeadTextures 的 WeakPtr::expired() GC |
VkTextureManager.cpp:1694-1720 |
~25 |
| 小计 | ~372 |
搬迁到 client(不是净删除)
| 机制 | 位置 | 行数 | 为什么搬而不是删 |
|---|---|---|---|
UnitBindingsSnapshot / CaptureUnitBindings / UnitBindingsUnchanged / CurrentUnitBindingsEpoch / UnitTextureSyncEntry / PairingsIntact + 8 个支撑全局 |
DirectGLES.cpp:1372-1489 |
~115 | 它存在的理由是 GetTextureBindGeneration() 在冗余重绑时也 bump(:1414-1420 注释:26.2 在每次纹理单元切换前后重绑同一个 sampler)。而 §4.2 恰好把这个计数器列为 NEW_SAMPLER_VIEWS 的 dirty 输入。若 tracker 直接信它,每一次冗余 glBindSampler 都会重发一次 set_sampler_views——一条 kVarTail 变长记录,每 draw 几百字节,且 server 侧 viewSetSerial 一动就冲掉解析绑定 memo 与 sampler pass memo。 这正是那 115 行要防的 per-batch 回归。去抖必须搬到 client:tracker 对已解析的 view/image/buffer 集合算 hash,hash 未变则不发(MGPFramebufferState::contentHash 已经演示了这个模式,这里把它推广到其余 kVarTail 的 set_*,并且在 client 侧当作发射抑制器用,不只是 server 的 memo 键) |
g_fboTextureSyncList(:1580-1601) |
~20 | 同上,针对 attachment;由 MGPFramebufferState::contentHash 抑制 |
|
ResolvedTextureBindingMemo 的完备性解析(IsMipmapCompleteForFilter / SamplesAsIncompleteTexture / IsUndefinedDefaultTexture) |
DirectGLES.cpp:3218-3291 + TextureObject.h:309/315/329 |
~40 | §4.5 把 view 解析放在 client,所以 client 需要自己的 memo 才不会每 draw 重解析 |
| 小计 | ~175 |
净账:monolith 侧真删除 ~372 行;另有 ~175 行从 backend 搬到 MG_Impl/Pipe/Tracker.cpp。 §13.4 按这个数字改写。
2.6 21 个 D 类身份 memo:它们各自守什么,以及为什么 {slot, gen} 能等价替换
统一事实:每一个进入 memo 键的版本计数器要么是回绕的 Uint16,要么根本不会被它真正害怕的那个 mutation bump。 BindingSlot::m_version(MG_Util/Types.h:197)、FramebufferObject::m_objectVersion(:183)、SamplerObject::m_version(SamplerObject.h:155)、RenderStateParameters 版本(RenderState.h:522)、TextureObjectBase::m_textureParamsVersion(:203)全部回绕。身份比较是堵住回绕洞的那块补丁。 完整的 21 条重键表在 §3.7;这里只点三条最有教育意义的:
- D3
UnitTextureSyncEntry+PairingsIntact(DirectGLES.cpp:1441-1481):注释写明它存在是因为"一次不经过 bind generation 的 slot 交换(DSA by-name 模拟以前就会静默交换一个 slot)会让每个键都匹配,而借来的 slot 指向另一张纹理,replay 于是会用纹理 B 的前端状态驱动纹理 A 的后端 twin——用 B 的形状重新指定 A 的后端存储并毁掉 A 的内容"。这是整份调研里最强的"支持推送接口"的论据:这一整类 bug 只在"client 能改一个绑定而不移动任何计数器"时才存在。审计义务从"哪些读需要守卫"变成"哪些 mutator 必须发消息",由 §13.3 的 verify 模式、poison mask 与推论 4 的 dirty-surface 生成器共同强制。(注意:这条的去抖部分搬到 client,见 §2.5。) - D11
VertexInputStateFactory::ComputeHash(VertexInputStateFactory.cpp:38-49):注释是一份 postmortem——"地址会被分配器复用……一个已销毁 buffer 的 GPU 切片被绑给了它的后继者的 draw,这就是一次 transform feedback 捕获拿回一个死 VAO 的顶点数据(0,0,0,1……)的原因"。所以gen必须被混进 server 侧的每一个 content hash,而不只是被比较。 - D18
VkRenderPassManager::m_renderbufferResources/VkTextureManager::m_textureResources用节点式std::unordered_map而不是本项目开放寻址的UnorderedMap(postmortem 在VkRenderPassManager.h:375-397):因为调用方会跨后续查表缓存RenderbufferResource*/TextureResource*,一次扩表搬迁曾让BlitFramebuffer静默停在"source image layout is undefined"。这一条在重键表里被显式标为 UNCHANGED,并进 review checklist。
2.7 v2 新增:MGPipe 增加的代码(诚实账)
§2.5 数了删除,v1 没有数新增。永久新增的大致规模:
| 组件 | 估计行数 |
|---|---|
MG_Pipe/(PipeCalls.def ~72 行 + MGPipeTypes.h ~14 个 POD + handles + host span + callbacks) |
~1,200 |
7 个生成器 scripts/gen_pipe.py(G1-G7) |
~1,500 |
生成产物(PipeTables.inc/PipeThunks.inc/PipeWire.inc/PipeVerify.inc/PipeFilled.inc/PipeCoverage.inc/PipeSpanTable.inc) |
~4,000(生成,不手写) |
MG_Impl/Pipe/(Tracker、SlotAllocator、CsoCache、HostResolve、CompositeResolver)含从 backend 搬来的 ~175 行 |
~2,200 |
MG_Backend/MGPipe/(PipeInputs.h + 两个 impl) |
~1,500 |
MG_State 的 5 个聚合世代 + ProgramArtifacts.h 抽取 + MGPipeValueTypes.h 抽取 |
~250(净新增很小,多为搬移) |
MG_Remote/(emitter、PipeApplier、PipeObjectTables)——仅 disaggregated 构建 |
~2,500 |
monolith 永久新增(不含 MG_Remote) |
≈ 6,650 手写 + 4,000 生成 |
所以 monolith 的净行数是增加的,不是减少的。 §13.4 里 "~550 行删除" 不再作为主论据;主论据是 §13.3-④ 的逐线程 CPU 数字(每 draw 指令数与 cache line 触达数的减少),而删除清单降级为佐证。B-R2 因此有了一个可证伪的预测而不只是定性主张。
3. 接口设计:MGPipe
3.1 文件布局与单一真相源
MobileGL/MG_Pipe/ # client 与 server 都 include;不链接 MG_State,不链接 MG_Impl
PipeCalls.def # X-macro:调用目录的唯一真相源,一行一个调用
MGPipe.h # 由 .def 生成的两张函数表 + 手写 payload 声明
MGPipeTypes.h # 全部 payload POD(trivially copyable,逐个 static_assert)
MGPipeValueTypes.h # ★v2 新增:无依赖的共享值类型(见 §3.7.2)
MGPipeHandles.h # MGPipeHandle、MGPipeKind、保留 handle、slot 分配契约
MGPipeHostSpan.h # 唯一一个"形状随传输而变"的访问器(§3.5.7)
MGPipeCallbacks.h # 反向通道(事件/回复)的函数表,见 §6
MGPipeRenderStateSpans.{h,cpp} # ★v2 新增:pipeline/dynamic 划分的唯一定义(§3.5.2)
generated/PipeTables.inc # G1:两张函数表
generated/PipeThunks.inc # G2:monolith 直调 thunk
generated/PipeWire.inc # G3:wire 记录 + static_assert + 运行期边界检查 + applier switch
generated/PipeVerify.inc # G4:逐字段影子比对器
generated/PipeFilled.inc # G5:written-once 位图与 poison 断言(**逐 verb 世代**)
generated/PipeCoverage.inc # G6:477 读点 → MGPipe 调用的映射表
generated/PipeSpanTable.inc # ★G7:render-state 的 pipeline/dynamic chunk 表 + setter 一致性测试
MobileGL/MG_Impl/Pipe/
Tracker.{h,cpp} # st_validate_state 类比物(含从 backend 搬来的 ~175 行去抖/解析)
SlotAllocator.{h,cpp} CsoCache.{h,cpp}
HostResolve.cpp # 客户端数组界限 / 索引扫描 / indirect count 解析
CompositeResolver.cpp # program pipeline 合成体的 handle 生命周期
MobileGL/MG_Backend/MGPipe/
PipeInputs.h # backend 私有的"被推送状态"块(迁移载体,§5.2)
MGPipeImpl_DirectGLES.cpp # 用 Espryt 的函数填 MGPipeContext
MGPipeImpl_DirectVulkan.cpp # 用 Magma 的函数填 MGPipeContext
MobileGL/MG_Remote/ # 传输与 server 侧对象表;完整目录与 CMake 接线见 §13.8
Server/PipeApplier.cpp Server/PipeObjectTables.{h,cpp} Server/IndexHostMirror.{h,cpp}
scripts/gen_pipe.py # 跑 G1..G7
scripts/gen_pipe_dirty_surface.py # ★v2:MG_Impl mutator → 聚合世代 的覆盖生成器(推论 4)
scripts/check_doc_citations.py # ★v2:docs/**.md 的 file:line 必须解析到存在的行
PipeCalls.def 一行一个调用,七个生成器消费它:
// MG_Pipe/PipeCalls.def — X(Name, PayloadStruct, Class, Flags)
// Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
// Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
#define MGP_CALL_LIST(X) \
/* ---- screen ---- */ \
X(GetCaps, MGPCaps, kScreen, kReplySlot) \
X(ResourceCreate, MGPResourceDesc, kScreen, kNone) \
X(ResourceRespecify, MGPResourceDesc, kScreen, kNone) \
X(ResourceDestroy, MGPHandleOnly, kScreen, kNone) \
X(MapPersistent, MGPHandleOnly, kScreen, kReplySlot|kOptional) \
/* ---- CSO ---- */ \
X(CreateRenderState, MGPRenderStateDesc, kCtxCso, kHasBlob) \
X(BindRenderState, MGPBindRenderState, kCtxCso, kNone) \
/* ---- state ---- */ \
X(SetDynamicState, MGPDynamicState, kCtxState, kHasBlob) \
X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone) \
X(SetSamplerViews, MGPSamplerViews, kCtxState, kVarTail) \
X(SetTextureParams, MGPTextureParams, kCtxObject,kNone) \
X(SetShaderBuffers, MGPShaderBuffers, kCtxState, kVarTail|kHostSpan) \
/* ---- verb ---- */ \
X(DrawVbo, MGPDrawInfo, kCtxVerb, kHostSpan|kVarTail) \
X(ResourceSubData, MGPSubData, kCtxObject,kHasBlob|kVarTail) \
X(RenderbufferStorage, MGPRbStorage, kCtxObject,kNeedsAck) \
/* … 共约 74 项,完整目录见 §3.4 与附 A 的速查表 … */
| 生成器 | 产物 | 替代/新增 |
|---|---|---|
| G1 | struct MGPipeScreen { … }; / struct MGPipeContext { void (*DrawVbo)(const MGPDrawInfo*, …); … }; |
替代今天手写的 GLFunctionsTable |
| G2 | monolith thunk:inline void MGP_DrawVbo(const MGPDrawInfo* p){ gPipeCtx.DrawVbo(p); } |
替代 gBackendFunctionsTable.GL.*(~93 个 MG_Impl 站点改名即可) |
| G3 | wire 记录结构 + 每种一条 static_assert(sizeof==N) + applier 分发前的运行期边界检查 → Fatal{ProtocolCorruption} |
把 §7.3 的记录格式机制扩展到全部调用 |
| G4 | MOBILEGL_PIPE_VERIFY 的逐字段比对器 |
新增:每份候选设计都被判缺失的语义绊线 |
| G5 | PipeInputs::m_filledGen[] 的位/世代定义 + 读未填字段时的 Fatal{UnmigratedPipeInput, "<field>"} |
新增(v2:由"位图"升级为"逐 verb 世代",见 §5.2.2) |
| G6 | 477 行读点清单 → MGPipe 调用的映射,CI 重生成并 git diff --exit-code,0 UNMAPPED |
改造自 Feat/CS-Delta-IPC 的 extract_backend_read_inventory.py |
| G7(v2 新增) | RenderStateParameters 的 pipeline/dynamic chunk 表 + 一个遍历每个 RenderState public setter、断言 pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变 的 MG_Test |
新增:D-B1 拒绝三 CSO 时点名要求、v1 却没给自己的完整性绊线 |
G4、G5、G7 与调用目录从同一份 .def/同一张 chunk 表生成,因此不可能漂移。
接口表用函数指针 struct,不用虚基类。 三条本仓库自己的理由:(1) 边界今天就是函数指针 struct,装在 MG_Backend/Init.cpp:44 的唯一 hook 点上;(2) nullptr 项已经表示"未实现,前端回退"(BackendObject.h:212-215、:265-269),DirectVulkan 确实留空 8 项——一个 null set_* 恰好就是"这个子系统还没迁移,继续拉取",纯虚类只能用说谎的 stub override 来模拟;(3) MG_Test 已经会替换这张表做 mock。稀有的 EGL/caps 面继续留在 pActiveBackendObject 的虚函数上。
3.2 对象模型
3.2.1 Handle
enum class MGPipeKind : Uint8 {
Buffer=1, Texture, Renderbuffer, Framebuffer, Xfb,
RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso,
Fence, Query, Context
};
struct MGPipeHandle { Uint32 slot; Uint32 gen; }; // 8 B,POD,按值走寄存器对
- slot 稠密、按 kind 分配,把 server 的对象表从哈希表变成数组;
SlotAllocator是 free-list + 高水位,与IndexGenerator无关(后者的 LIFO 复用正是问题本身)。 gen只在 slot 复用时 ++,不是每次 respecify。{slot, gen}在同一 slot 被复用 2³² 次之前唯一;文档写明上界,debug 断言它。- GL name 只在
resource_create的 payload 里出现一次,纯诊断,永不做身份、永不进 memo 键或 content hash。 GetLifetimeId()留在 client 侧作为 tracker 自己的身份,不过线;client 维护lifetimeId → slot。- 保留 handle:
{0,0}= null;{slot=0, gen=1, kind=Framebuffer}= 默认帧缓冲(退役DirectGLES.cpp:1917, 2838, 2867, 9675四处pDefaultFramebufferInfo->defaultFBO身份比较);ShaderCso的高 1/16 slot 段保留给 program pipeline 合成体(§4.6)。
3.2.2 两种 generation,严格分开
| 拥有者 | 回答什么 | 是否过线 | |
|---|---|---|---|
身份(MGPipeHandle::gen) |
client | "还是同一个 GL 对象吗?" | 是 |
MGGen(server 纪元) |
server | "我自己是不是重铸了驱动对象 / 冲了自己的缓存?" | client→server 永不;server→client 只以纹理拉取请求的形式出现(§6.5) |
接口规范条款:任何 MGPipe 调用都不得要求 client 提供或知晓 MGGen。 反过来也是规范:client 侧的版本计数器永远不是新鲜度的唯一证明——每一个回绕的 Uint16(§2.6)在过线时要么加宽到 32 位、要么与 {slot, gen} 同行。
3.2.3 CSO vs 可变对象
| 类别 | 形态 | 因为 backend 今天就是这么缓存的 |
|---|---|---|
VertexElementsCso |
create/bind/delete |
VertexInputStateFactory::m_cache,键正是那组字段的 content hash(VertexInputStateFactory.cpp:19-50) |
SamplerCso |
create/bind/delete |
VkSamplerManager::m_samplers;Espryt 的 BackendSamplerObject(Managers.h:1808-1824) |
SamplerViewCso |
create/delete + 由 set_sampler_views 绑定 |
TextureResource::{perMipViews, …, storageImageViews}(VkTextureManager.h:173-370);Espryt 的 SyncTextureViewToBackend(Managers.cpp:3616-3707) |
ShaderCso |
create/bind/delete + server 侧惰性特化(D-B2) |
ProgramFactory::m_cache;BackendProgramObjectImpl |
RenderStateCso |
create/bind/delete,身份 = pipeline 子集(D-B1 v2) |
Espryt 的值镜像 + 单 Uint16 早退 + 三段 memcmp;Magma 的 ComputePipelineStateHash |
| Buffer / Texture / Renderbuffer | create / respecify / subdata / destroy |
GLESBufferResource、BackendTextureObject、VkBufferResource、TextureResource |
| Framebuffer / Xfb | per-context 身份 + set_* payload |
BackendFramebufferObject、m_xfbCounterSlotByObject |
CSO 在 client 侧内容寻址(Mesa cso_context/cso_cache 先例):每类一张 ska::flat_hash_map<Uint64 xxHash, MGPipeHandle>,容量上限(render-state 64、vertex-elements 1024、sampler 256、sampler-view 4096、shader 跟随 ProgramObject 生命周期),LRU 淘汰时发 delete_*_state。收益:两个不同 program 设置了相同状态时 server 侧零状态转换。
任何 create_* 都不返回 server 铸造的 handle。 这是对 gallium 的有意偏离(D1),也是这份目录能在零创建 round trip 下远程化的根本原因。BackendSyncHandle/BackendQueryHandle = void*(BackendObject.h:110, 115)随之变成 MGPipeHandle。
3.3 MGPipeScreen 与 MGPipeContext
MGPipeScreen(share group) |
MGPipeContext |
|---|---|
| caps、format 能力表、renderer 字符串;buffer / texture / renderbuffer / sampler / shader 的对象命名空间;fence | 全部 set_*、全部 CSO 绑定、VAO / FBO / XFB 对象 / query 的命名空间、命令流、present |
v1 只有一个 screen、一个 context、一条 flow。但两张表从第一天就分开,因为事后拆分意味着给每个记录种类重新编号。两处必须重新归类的事实:GetTextureBindGeneration() 与 GetSamplingResolutionGeneration()(Core.h:130, 136)是绑定(context)事实却住在 share-group 作用域的 TextureState 里;GetTextureContextId()(:143)直接就是 context handle。
3.4 完整调用目录
3.4.1 MGPipeScreen(14 项)
| 调用 | payload | 取代 |
|---|---|---|
get_caps(MGPCaps* out) |
DynamicBackendParameters(BackendObject.h:302-522,~90 标量,平坦 POD)+ RendererInfo + FormatCapabilityCache(:88-99)+ callMask |
40 个 pActiveBackendObject-> 站点、89 个 caps 读点 |
resource_create(h, const MGPResourceDesc*) |
§3.5.1 | buffer/texture/renderbuffer 的创建 |
resource_respecify(h, const MGPResourceDesc*) |
同上 | BufferBackendOps::Respecify(BufferObject.h:80)泛化 |
resource_destroy(h) |
handle | OnDestroy(:101)+ 两个 WeakPtr GC 扫描 |
map_persistent(h) → MGPMapResult / unmap_persistent(h) |
— | AcquirePersistentMap(:112)。改造期不碰(D-B4) |
fence_create/status/wait/destroy |
handle (+timeout) | FenceSync…GetSyncStatus(:220-224)。两值契约(:243-249)逐字保留 |
query_create/begin/end/available/result/destroy |
handle + kind | BackendObject.h:230-256 |
| EGL 生命周期 8 项 | BackendObject.h:548-559 |
原样保留为虚函数(罕见) |
callMask 取代"槽位是否为 null"这个隐式能力探测(GL_Query.cpp:471, 545, 768)。v2 修订的能力位集(v1 的五个 emulation 归属位按 D-B7 删除):
kCapViewportArray、kCapFloat64VertexAttrib、kCapResidentSubData、kCapCpuXfbPrimitiveAccounting、kCapTimerQuery、kCapOcclusionQuery、kCapXfbPrimitivesQuery、kCapNeedsHostIndexBytes(server 侧的 restart 重写/multi-draw 展平需要索引宿主字节 → split 下开启索引宿主镜像,D-B7)、kCapNeedsHostUboBytes(server 侧要把具名 UBO 打进自己的 ring → 需要 set_shader_buffers 的 host payload,D-B8)。
删除:kCapPrimitiveRestart、kCapPrimitiveRestartFixedIndex、kCapMultiDraw、kCapMultiDrawIndirect、kCapMultiDrawIndirectCount——它们表达的"归属开关"不可表达(D-B7)。
3.4.2 MGPipeContext — CSO(15 项)
create/bind/delete × { render_state, vertex_elements, sampler, sampler_view, shader }。payload 见 §3.5.2-3.5.5。
3.4.3 MGPipeContext — set_*(17 项,v2 从 14 增至 17)
| 调用 | 取代的拉取点 |
|---|---|
set_dynamic_state(MGPBlobRef chunks, Uint16 version) (v2 新增) |
渲染状态里 m_pipelineStateVersion 不覆盖的那一半(viewport / scissor / depth range / blend color / line width / polygon offset / stencil ref+write mask / clear values / sample coverage / hints / point-size 族)。这条让 glViewport 不再铸造新 CSO(D-B1) |
set_framebuffer_state |
GetFramebufferBindingSlot ×19、GetAllAttachmentObjects、GetDrawBuffers、GetReadBuffer、4 处 pDefaultFramebufferInfo |
set_vertex_buffers(start, count, const MGPVertexBuffer*) |
VAO binding-point 走查 |
set_index_buffer(const MGPIndexBuffer*) |
GetIndexBufferBindingSlot;独立调用——VAO config version 不是它的超集(D5) |
set_indirect_buffers(drawIndirect, parameter) |
GetBufferBindingSlot(DrawIndirect/Parameter) |
set_sampler_views(start, count, const MGPBoundView*) (v2:删掉 stage 形参) |
GetTextureUnitObject ×19、GetActiveTextureUnit ×8、GetTextureBindGeneration ×5。client 侧已解析(§4.5) |
bind_sampler_states(start, count, const MGPipeHandle*) (v2:删掉 stage 形参) |
TextureUnit.h:394 |
set_texture_params(res, const MGPTextureParams*) (v2 新增) |
base/max level、swizzle、depth-stencil mode、LOD 钳。必须独立于 sampler view,见下 |
set_shader_images(start, count, const MGPImageView*) |
GetImageTextureBinding ×14;退役 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573) |
set_shader_buffers(cls, start, count, const MGPBufferRange*, writableMask) (v2:Uniform 类的 range 可带 MGHostSpan payload) |
GetBufferBindingPoint ×19、GetTouchedBufferBindingPointCount ×2。cls ∈ {Uniform, ShaderStorage, AtomicCounter}。payload 由 kCapNeedsHostUboBytes 门控(D-B8) |
set_stream_output_targets(count, const MGPBufferRange*, const Uint32* offsets, Uint64 generation) |
XFB 绑定走查 |
set_global_constants(shaderCso, MGPBlobRef, Uint32 version) |
MapUBO/GetUBOData/GetUBOSize/GetUBOContentVersion(§3.6 D6)。只覆盖默认 uniform block |
set_vertex_attrib_defaults(Uint32 mask, const MGPAttribValue*) |
GetCurrentVertexAttribute ×2;float/int/uint 视图由 ClassifyVertexAttribType(Core.h:51)在 client 侧解析 |
set_pixel_pack_state(const PixelStoreParameters*) |
6 个 PACK 读点。没有 unpack 对应项(§3.6 D5) |
set_patch_state(Uint32 vertices, const Float outer[4], const Float inner[2]) |
GetPatchVertices/…OuterLevel/…InnerLevel ×6。同时是 shader variant 输入 |
set_draw_program(shaderCso) / set_dispatch_program(shaderCso) |
GetProgramForDraw ×7、GetProgramForDispatch ×3。含 composite(§4.6) |
为什么删掉 stage 形参(v2):MobileGL 的纹理单元空间是合并的,不是分 stage 的——TextureState::m_textureUnits 是 Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> 且 MAX_TEXTURE_IMAGE_UNITS = 192(TextureState.h:41, 128),每 stage 的 32 只是一个广告数字(:46);TextureUnit 本身是 Array<BindingSlot<ITextureObject>, TextureTargetCount> 加一个 sampler(TextureUnit.h:20, 24-25);两个 backend 都按合并单元绑定(g_boundTexturesCache[192][TargetCount])。同一个合并单元可以被两个 stage 采样。加 stage 维度会逼 client 要么按 stage 复制 view、要么发明一个 GL 未定义的 stage 归属,而 server 还得把它塌回去。stage 只在目标 API 真正需要时出现(Magma 的描述符 stage flags),由 server 从反射归档推导。
为什么纹理参数不能只挂在 sampler view 上(v2):Espryt 对每个 touched 单元绑定与每个 draw-FBO attachment 纹理都调 SyncTextureParamsToBackend(DirectGLES.cpp:1548-1560 单元表、:1580-1601 attachment 表),而 RequireImageBindableStorage 会置 m_forceTextureParamsResync,正是因为通道加宽后的载体需要一个前端 params 版本不会移动的 swizzle 覆盖(Managers.cpp:2815-2821)。一张只作 FBO attachment、只作 image 单元绑定、或只作 glCopyImageSubData 端点的纹理没有 sampler view,它的 glTexParameter 状态在 v1 的映射里没有载体。所以:base/max level、swizzle、depth-stencil mode、LOD 钳挂在 set_texture_params(res, …) 上;MGPSamplerView 只带"视图限制"(min/num level、min/num layer、别名格式)。 这同时让 glTextureView 保持它真正的身份——一个有自己参数、自己能当 FBO attachment、自己能当 glTexSubImage 目标的真纹理对象(TextureObjectView.cpp:281, 290)——而不是被降格成"普通 view CSO"。
迁移期额外一项(显式临时):set_residual_value_state(MGPBlobRef),见 §5.3。
3.4.4 MGPipeContext — transfer(12 项)
resource_subdata(buffer + texture 同一形状,带步长的多 region 描述符,§3.5.6)、resource_flush_range(h, Range1D, Flags<BufferMappingAccessBit>)(携带应用真实的 access flags,BufferObject.h:94-96)、resource_readback(h, off, size, MGPReplySlot)、resource_copy_region、blit、clear(一条,判别式合并今天的 Clear + 4 个 ClearBuffer* + 4 个 ClearNamedFramebuffer*)、generate_mipmap(h, target, const MGPMipPlan*)、read_pixels(const MGPReadbackInfo*, MGPReplySlot)、get_texture_image(...)、buffer_subdata_resident(h, off, MGPBlobRef)(可为 null)。
buffer_subdata_resident 的 per-backend 可选性必须被接口允许。 Espryt 注册它、Magma 故意不注册(VkBufferManager.cpp:104-111),差别是 glBufferSubData 在活的 coherent map 上的排序语义(BufferObject.h:84-92 的 Minecraft 撕裂 postmortem)。表现为 kCapResidentSubData 位 + null 项。
3.4.5 MGPipeContext — 命令(10 项)
void draw_vbo (const MGPDrawInfo*, Uint32 drawIdOffset,
const MGPDrawIndirect*, const MGPDrawRange*, Uint numDraws);
void launch_grid(const MGPGridInfo*);
void memory_barrier(GLbitfield bits, Bool byRegion);
void begin_stream_output(GLenum primitiveMode);
void end_stream_output(const MGPXfbAccounting*);
void pause_stream_output(); void resume_stream_output();
void flush(Uint32 flags);
void present(Uint64 frameSerial); void set_swap_interval(Int interval); // 后者可 null(Magma)
今天 20 个 draw 入口塌成 draw_vbo 一条,MGPDrawRange[] 就是 MultiDraw* 族今天的形状(gallium 的 pipe_draw_start_count_bias)。
3.4.6 显式删除、不移植的项
GetIntegeri_v/GetInteger64i_v/GetProgramiv(BackendObject.h:195-197)。只有GL_COMPUTE_WORK_GROUP_SIZE(DirectVulkan.cpp:790-795)是真后端答案,进MGPCaps。ShaderStorageBlockBinding(:207-208)→ 折进MGPProgramDesc的反射归档。- 总规则:server 不回答任何 client 能自己回答的问题;剩下的每个 server 查询都是 async-with-handle,绝不阻塞。
3.5 关键 payload
3.5.1 MGPResourceDesc(判别式,三种 GL 存储类合一)
struct MGPResourceDesc {
Uint8 target; // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
Uint8 storageKind; // Mipmap | Buffer (== TextureStorageType, TextureEnum.h:61-64)
Uint16 bindMask; // VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|
// RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY
Uint32 internalFormat; // 已在前端解析为非压缩后备
Uint32 width, height, depth;
Uint16 arrayLayers, levels, samples;
Uint8 fixedSampleLocations, immutable;
Uint32 usage; // BufferUsage
Uint32 storageFlags; // glBufferStorage flags
Uint8 hasDefinedContent; // NULL-data respecify 之后为 false,BufferObject.h:216
Uint8 imageBindableHint; // client 侧 everImageBound,预防性分配(§6.5(a))
Uint8 glNameForDiag[2]; // 仅诊断
MGPipeHandle viewOf; // 纹理视图的存储属主(GetViewStorageOwner,TextureObject.h:100)
MGPipeHandle bufferForTexBuffer; Uint64 bufOffset, bufSize; // kWholeBuffer = ~0,实时解析
};
bindMask 里的 ELEMENT_ARRAY 位是 D-B7 的开关:server 见到它且 kCapNeedsHostIndexBytes 为真时,把该资源纳入索引宿主镜像。
Renderbuffer 保持独立类:自己的 format-capability target 索引(BackendObject.h:85)、自己的 ComponentSizes 上报(RenderbufferObject.h:37-43)、自己的 twin(Managers.h:1838)。
3.5.2 渲染状态:MGPRenderStateDesc / MGPBindRenderState / MGPDynamicState(D-B1 v2)
// MG_Pipe/MGPipeRenderStateSpans.h —— 划分的唯一定义
struct MGPStateChunk { Uint16 offset, length; };
extern const MGPStateChunk kPipelineChunks[]; // G7 生成,来源 = VulkanRenderer.cpp:4826-4906 的字段表
extern const MGPStateChunk kDynamicChunks[]; // 补集
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters&); // client 与两个 backend 共用
struct MGPRenderStateDesc { // create:只带 pipeline 子集的 chunk 字节
MGPipeHandle cso;
Uint32 chunkMask; // 未命中时可只发变化的 chunk;全新 CSO 为全 1
MGPipeHandle baseCso; // 增量基(chunkMask 非全 1 时有效)
MGPBlobRef blob;
};
struct MGPBindRenderState { // bind:稳态 12 B
MGPipeHandle cso; Uint16 version; Uint16 pipelineVersion;
};
struct MGPDynamicState { // 动态子集,只发变化的 chunk
Uint32 chunkMask;
Uint16 version; Uint16 pad;
MGPBlobRef blob;
};
server 侧模型:每 context 一份 working RenderStateParameters(~1.2KB)。bind_render_state 把 CSO 的 chunk 散射进去;set_dynamic_state 把动态 chunk 散射进去。Espryt 的 SyncRenderState 拿到的仍是 const RenderStateParameters&,693 行函数体、单 Uint16 早退、三段 memcmp、g_syncedColorMaskAlphaWidenMask、dual-source decline 一行不动。 Magma 的 pipeline memo 键是 cso.slot,glViewport 不再冲掉它;动态尾巴仍走 ApplyDynamicDrawStateTail 的两级门。
两套 span 划分并存,互不干扰,各有绊线:
| 划分 | 用途 | 定义在哪 | 绊线 |
|---|---|---|---|
head / blend / tail(DirectGLES.cpp:2038-2047,按 offsetof(BlendStates)、offsetof(LogicOp)) |
Espryt 驱动侧增量 | DirectGLES.cpp 原地,不动 |
已有:static_assert(is_trivially_copyable_v);RenderState.h:359-368 的字段顺序注释 |
| pipeline / dynamic | 线上传输与 CSO 身份 | MGPipeRenderStateSpans.cpp,G7 生成 |
G7 的 setter 一致性测试:遍历每个 RenderState public setter,断言 pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变 |
client 侧的取值顺序(热路径,必须照此实现):
m_pipelineStateVersion未变 → 复用上一个 CSO handle,零哈希;- 变了 → 对 pipeline 子集算 xxHash(~25-30 字,正是 Magma 今天在算的那个)→ CSO map 探测 → 命中发 12 B
bind_render_state,未命中发变化 chunk 的create_render_state再 bind; m_version变而 pipeline 子集未变 → 只发set_dynamic_state的变化 chunk(~200 B)。
性能诚实注记:Blaze3D 的 glEnable/glDisable(GL_BLEND) 走 SET_CAPABILITY(RenderState.cpp:312)→ BumpVersions(),所以每次都进第 2 步。交替的两个状态命中两个交替的 CSO,不重发 blob。对比今天:Espryt 1.2KB×3 段 memcmp + Magma ~30 字哈希。净变便宜但差距不大,因此 P2 必须带一个专门的 enable/draw/disable/draw 微基准(MC batch 速率,两台设备)。
3.5.3 MGPVertexElements
携带两个视图,缺一不可:解析后的 VertexAttribute[32](VertexArrayObject.h:17-53)和 VertexBufferBindingPoint(:58-64,初始 stride 是 16 不是 0,:61-62)。VertexArrayObject.h:22-29 记录了合并它们的代价:pointer 调用的 stride 0 被解析成 element size,而 binding-model 的 stride 0 意味着每个顶点读同一个 element,塌成一个害了 KHR-GL43.vertex_attrib_binding.basic-input-case7/8。IsLong 与 Type == Float64 分开携带(:34-39)。仅供查询的 LegacyStride/LegacyPointer(:51-52)留在 client。
3.5.4 SamplerParameters 与 MGPSamplerView / MGPTextureParams
SamplerParameters(SamplerObject.h:72-96,v1 误引为 :468-492)逐字节原样过线,包括 borderColorForm(:66-70)::60-65 明说没有它 backend 无法在 glSamplerParameterIiv 与 fv 之间、或在 VkBorderColor 家族之间选择,因为三种表示(borderColor/borderColorI/borderColorUI,:93-95)永远都被数值填满。SamplerObject::BumpVersion()(:151,m_version 在 :155)同时bump context 级 sampling-resolution generation,因为 MIN_FILTER 决定是否读 mip 链 → 决定 mipmap 完备性 → 决定 backend 到底绑不绑这张纹理。
struct MGPTextureParams { // ★v2:per-texture-object,与 view 无关
MGPipeHandle res;
Uint16 baseLevel, maxLevel;
Uint8 swizzle[4];
Uint8 depthStencilMode, pad[3];
Float minLod, maxLod, lodBias;
Uint8 forceResync; // 对应 m_forceTextureParamsResync(Managers.cpp:2815-2821)
};
struct MGPSamplerView { // = pipe_sampler_view,**只带视图限制**
MGPipeHandle cso, texture;
Uint32 internalFormat; // 别名格式(glTextureView)
Uint8 target, pad[3];
Uint16 minLevel, numLevels, minLayer, numLayers;
Uint16 samples; Uint8 fixedSampleLocations, pad2;
};
GetViewStorageOwner()(TextureObject.h:96-100,一个 SharedPtr,且它自己永远不是 view)变成 resource_create 的 viewOf + server 侧 keep-alive。
3.5.5 MGPProgramDesc(create_shader_state 的 payload)
struct MGPProgramDesc {
MGPipeHandle cso;
Uint32 stageMask; // == GetLinkedShaderStages()
MGPBlobRef spirv[6]; // GetGeneratedSpirv(),逐 stage
MGPBlobRef reflection; // Visit() 归档的 LinkArtifacts + SpirvArtifacts(全结构体)
Uint32 globalUboSize;
Uint32 reservedNumSamplesOffset;
Uint8 spirvStatus, nativeFloat64, pointSizeDemoted, enableSpirvValidation;
};
v2 前置条件(P0.5):反射类型必须先搬出 ProgramObject.h。 TypeFacts(ProgramObject.h:44)、ResourceReflection(:76)、XfbVarying(:1146)、LinkArtifacts(:1210)、SpirvArtifacts(:1409)今天全部声明在 ProgramObject.h 里,而该文件 :11 include ShaderObject.h(→ ShaderCompileTask.h → glslang;ShaderObject.h:146 返回 SharedPtr<glslang::TShader>)、:14 include SpvcSession.h(→ spirv_reflect.h)。任何链接真 ProgramObject 的 server 就链接了整条编译链,而 server 要反序列化进这些类型就必须 include 被门禁止的头。 P0.5 把它们抽到:
MG_State/GLState/ProgramState/ProgramArtifacts.h # 只 include <Includes.h> 与容器/向量类型
更新 7 个 includer(ProgramFactory.h、UniformManager.cpp、VulkanRenderer.cpp、ProgramInterface.cpp、ProgramLinkTask.h、ProgramObject.h、ProgramTranslationCache.h),并加 CI 断言:ProgramArtifacts.h 的 -H 传递 include 闭包里不得出现 glslang / SPIRV-Cross / spirv_reflect 任何头。没有这一步,P7 的 nm -D | grep glslang 判据不可达。
反射归档序列化整个结构体,机制是 Visit() + sizeof 绊线——一份字段表服务序列化的两个方向,加一条尺寸断言;它在本设计里的用途是 schema 完整性绊线(没有第二份状态模型可分歧,所以它不是"分歧预言机"):
template <class Ar> void Visit(Ar& ar, LinkArtifacts& a) { ar(a.writtenUniformLocationBits, /*…全字段…*/); }
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
"新字段请加进 Visit() 并 bump MGL_LINKARTIFACTS_SIZE");
归档必须覆盖:四个 ResourceReflection(各带 TypeFacts)、uniformSamplerOrImageUnitIndex(:1298)、uniformBlockBinding(:1314)、shaderStorageBlockBinding(按名字,:1325)、explicitOpaqueUniformBindings(:1303)、xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture(:1357-1394)、computeLocalSize、GS/TCS/TES 事实(:1373-1388)、usesReservedNumSamples(:1345)、uniformOffsets(:1416)。
XfbVarying(:1146-1171)必须带两套拼写:GL 名字(Espryt 的 ESSL 驱动侧捕获列表)和 blockInstanceName/blockName/blockMemberIndex/blockMemberElement(:1163-1170)。
"server 从源码重新 link"这条路被显式关闭。 既然链接真 ProgramObject 就链接 glslang,create_shader_state 的 payload 从第一天就是 SPIR-V + 反射归档,没有第二档、没有 MOBILEGL_IPC_PROGRAM 这类开关,也不存在 server 侧 compile pool。glslang 全在 client,SPIRV-Cross 全在 server(§4.7)。
3.5.6 MGPFramebufferState 与 MGPSubData
struct MGPSurface { // = pipe_surface
MGPipeHandle res;
Uint32 internalFormat; // 内联!让四个跨对象 mask 在推送时刻零查表推出
Uint8 kind; // Texture | Renderbuffer | None
Uint8 layered; Uint16 level;
Uint32 layer; Uint16 uploadTarget; Uint16 pad;
};
struct MGPFramebufferState {
MGPipeHandle fbo; // {0,1} = 默认帧缓冲
MGPSurface color[8], depth, stencil;
MGPSurface readSurface; // *** client 侧已解析的读表面,不是索引 ***
Int8 drawBuffers[8]; // attachment 索引,-1 = NONE
Uint16 width, height, layers, samples;
Uint8 fixedSampleLocations, isDefault, complete, pad;
Uint64 contentHash; // client 计算;server 的 render-pass memo 键 + **client 侧发射抑制器**
};
readSurface是 client 解析后的表面,按结构消灭 read-buffer-shared-FBO 缺陷类。internalFormat内联,四个跨对象 mask(Managers.cpp:5616-5619)在set_framebuffer_state内部零查表推出。contentHash有两个用途(v2 强调第二个):server 的 memo 键(取代 D7 四元组与 D15 三元组)以及 client 的发射抑制器——hash 未变就不发这条记录,这是 §2.5 里那 ~175 行去抖搬到 client 后的载体。同一模式必须推广到每一条kVarTail的set_*(set_sampler_views、bind_sampler_states、set_shader_images、set_shader_buffers),否则 26.2 的冗余glBindSampler会让每个 batch 重发一条变长记录。
struct MGPSubRegion { // ★v2:形状照抄已存在的 UnpackStagingBlock(Managers.cpp:4340-4390)
Int32 x, y, z; // 目标 box 原点(level 坐标系)
Uint32 w, h, d;
Uint64 srcOffset; // blob 内偏移
Uint32 srcRowStride; // 源行距(字节);0 = 紧密(= w * bpp)
Uint32 srcSliceStride; // 源片距(字节);0 = 紧密
};
struct MGPSubData {
MGPipeHandle res;
Uint16 target, level;
Uint8 sourceIsVerbatimLevelShadow; // ★ 取代 backend 里的 `uploadData == mipData` 指针比较
Uint8 pad[3];
MGPBox unionBox; // union box(server 可选它)
Uint32 regionCount; // MGPSubRegion[] 在变长尾(server 可选它们)
MGPBlobRef blob;
};
同时携带 union box 与 region 列表,由 server 选上传形状。 这不是冗余:Mali 按作业数给纹理上传计价,实测 ~100 个精灵 rect 对一个 union box 是 +6 ms/frame(Managers.cpp:4386-4390)。client 按 MipmapStorage::GetDirtyRects 的语义产生区域形状(96-rect 级联合并 + summedArea*4 >= unionArea*3 回退,MipmapStorage.cpp:300-305),决策留在付 GPU 代价的那一侧。
v2 关键修正:sub-rect 上传不能再靠指针比较判定。 今天 Managers.cpp:4278-4283 用 uploadData == mipData 判"上传源就是整 level shadow",随后 :4288-4293 与 rectShadowPtr(:4321-4326)用 levelRowBytes/levelSliceBytes 跨步进整 level。在 split 下这个前提不成立:client 若发整 level 就毁掉带宽收益并与零副本主张矛盾;若发紧密区域则 uploadData == mipData 为假,静默退回整 level 上传;若什么都不发就需要 server 侧整 level 镜像——那就是一份重复的 MipmapStorage。
修正:MGPSubRegion 显式携带源步长,sourceIsVerbatimLevelShadow 显式携带原来那个指针比较回答的语义问题("这批字节是未经转换的 level shadow 吗")。Managers.cpp:4274-4326 相应改为从描述符取步长而不是从指针算,UNPACK_ROW_LENGTH 从 srcRowStride/bpp 设。
注意树里已经有这个形状:unpack ring 路径的 UnpackStagingBlock(Managers.cpp:4340-4390)就是 {src, rowBytes, rows, slices, srcRowStride, srcSliceStride, offset},且注释明说 ring 路径把区域紧密重打包、因此完全不发 glPixelStorei。所以 split 的自然形态就是"永远走紧密重打包 + 描述符",与 ring 路径同构。
这项工作从 v1 的"原地不动"移出,计入子系统 5 的天数(§5.4),并加一个 Mali 设备门发布 box-vs-rect 作业数与帧时增量。
3.5.7 MGPDrawInfo 与 MGHostSpan
struct MGPDrawInfo { // = pipe_draw_info
Uint32 mode;
Uint8 indexSize; // 0 = arrays,否则 1/2/4
Uint8 flags; // kHasUserIndices | kPrimitiveRestart | kIndicesAreClient |
// kHasIndexRange | kHasXfbCount
Uint16 pad;
Uint32 instanceCount, startInstance;
Uint32 restartIndex;
MGPipeHandle indexResource;
// 以下三项**由 flags 门控**,只在有消费者时才计算与携带(v2)
Uint32 minIndex, maxIndex; // kHasIndexRange;client 计算,~0 = 未知
Uint64 xfbCpuCapturedVertices; // kHasXfbCount;GetTransformFeedbackCapturedVertices()
MGHostSpan userIndices; // kHasUserIndices;否则不进变长尾
};
struct MGPDrawRange { Uint32 start, count; Int32 indexBias; }; // = pipe_draw_start_count_bias
v2 成本诚实化:今天的 DrawArrays(GLenum, GLint, GLsizei) 是三个寄存器实参(BackendObject.h:117)。替换成一个 ~48 B 的固定头(含 handle)加按需的变长尾。minIndex/maxIndex 今天只在 client-memory 数组路径算(TryComputeMaxIndexFromHostBytes,VulkanRenderer.cpp:3407-3470,用于 :3599),xfbCpuCapturedVertices 今天只在 XFB scatter 路径读(DirectGLES.cpp:900)——所以两者由 flags 门控,不是每 draw 都算。userIndices 的 32 B MGHostSpan 移出固定头进变长尾,让 VBO 路径(MC/Sodium 的全部 draw)不为它付字节。每 draw payload 字节数进 P0 的计数器直方图(cmd-records 是逐帧的,这里要逐 draw 的分布,它才是 SEG_CMD 的定尺依据)。
MGHostSpan 是整份接口里唯一一个"形状随传输而变"的东西:
struct MGHostSpan { // 32 B
const void* ptr; // monolith:指向前端 shadow / 应用内存。split:nullptr
Uint64 size;
Uint32 seg; // split:SEG_STAGE id,或 kFromServerIndexMirror
Uint32 pad;
Uint64 offset;
};
inline const void* MGPipeHostBytes(const MGHostSpan&); // 一次可预测分支
v2 修订的消费者表(与 §4.8 一致,解决 v1 §3.5.7 与 §4.8 互相矛盾的问题):
| 消费者 | 今天的站点 | 归属 | monolith 填法 | split 填法 |
|---|---|---|---|---|
| client 顶点数组 | Managers.cpp:2500-2592、VulkanRenderer.cpp:3737 |
client 供字节 | ptr = attrib.Offset |
tracker 暂存同样范围进 SEG_STAGE |
| client 索引数组 | DirectGLES.cpp:4425-4442、VulkanRenderer.cpp:3418-3433 |
client 供字节 | ptr = indices |
暂存 count*indexSize |
| indirect / parameter 命令块 | DirectGLES.cpp:4655-4695、:4768-4793、VulkanRenderer.cpp:12045 |
client 解析计数 | ptr 指向 shadow |
tracker 解析出计数并发解析后的 MGPDrawRange[](几十字节) |
| restart 重写 / multi-draw 展平的索引字节 | DirectGLES.cpp:4412-4415、MultiDraw.cpp:498-540、VulkanRenderer.cpp:4159 |
server 拥有变换(D-B7) | ptr 指向前端 shadow |
seg = kFromServerIndexMirror:server 从自己的索引宿主镜像取,零线上流量;镜像超预算时退化为 client 逐 draw 暂存并计数 |
monolith 代价:一次可预测分支 + 变长尾里的 32 B(仅 kHasUserIndices 时)。它顺带消灭"backend 在 draw 中途回头调前端 reconcile"的大部分:20 处 SyncPersistentMappedRange + 6 处 SyncGpuWrites 里,凡消费者搬到 client 的那些改由 tracker 在填 span 之前做同一次 reconcile(逐站点对照见 §4.8.1,不是一条笼统规则)。
3.6 与 gallium 的对应与偏离(十条,逐条记名)
| # | gallium | MGPipe | 理由(证据) |
|---|---|---|---|
| D1 | create_*_state 返回 driver 指针 |
调用方提供 handle | 零创建 round trip;handle 是稠密 slot;退役全部 D 类指针 memo |
| D2 | get_param(cap)、is_format_supported(...) 逐项查询 |
一个 MGPCaps POD + 一张稠密 format 表 |
DynamicBackendParameters 与 FormatCapabilityCache 本来就是平坦结构 |
| D3 | CSO 切分是 D3D10 时代的 | CSO 边界跟 Vulkan 动态状态走 | RenderState.h:519-528 记录共用一个版本号让 glViewport 冲掉 pipeline memo 和 draw 快路径;m_pipelineStateVersion(:529)恰好是 CSO 相关子集;Magma 的 DynamicStateShadow 与 ApplyDynamicDrawStateTail 已经这么切 |
| D3b(v2 重写) | 三个独立 CSO:blend / depth_stencil / rasterizer | 一个 RenderStateCso,传输是整块 chunk,身份是 pipeline 子集,动态子集走 set_dynamic_state |
整块的理由:is_trivially_copyable_v 断言(DirectGLES.cpp:2035)、三段 memcmp(:2038-2047)、字段顺序承重(RenderState.h:359-368)、两个 backend 都按 span/bulk 消费。子集身份的理由:整块内容寻址会让 glViewport 铸造新 CSO 并冲掉 pipeline memo——即 D3 要防的那次回归。完整性由 G7 的 setter 一致性测试保证 |
| D4 | transfer_map/transfer_unmap(scoped) |
resource_subdata 推送 + map_persistent(永久地址空间捐赠) |
AcquirePersistentMap(BufferObject.h:102-118)把指针交给应用;≥16MiB 自动走到(:226-228)。实测 p99 163→21ms |
| D5 | driver 看得见压缩格式与 pixel-unpack 状态 | 两者都不存在 | 前端在 glTexImage 时解析压缩 internalformat(GL_Texture.cpp:298-306);ScopedDefaultUnpackState(Managers.cpp:2888-2910)强制 unpack 默认值。只有 PACK 方向过线 |
| D6 | 默认 uniform block = constant_buffer 0 |
独立入口 set_global_constants |
SpirvArtifacts::globalUboScratch(ProgramObject.h:1418)是 link phase B 产出的 CPU 数组,布局由优化后的 SPIR-V 决定(:1400-1408)。它没有 GL name、没有 BufferObject、没有 PipeResource |
| D7 | pipe_shader_state = tokens → 完成的 handle |
handle + server 侧惰性特化,variant 键取自已推送状态 | D-B2 的 8 个输入。这其实就是 gallium(Mesa 的 st_variant 也按已绑定状态键控) |
| D8 | pipe_context::flush + fence 是唯一反向通道 |
MGPipeCallbacks:10 个具名回复/事件(§6) |
gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求/终止、default-FB 几何这些词汇 |
| D9 | set_viewport_states(start_slot, num) |
float 数组 + 独立的 writtenMask |
viewport 是 float(RenderState.h:229-237:KHR-GL43.viewport_array.viewport_api 用 == 无容差);scissor 必须单独带 ScissorBoxWrittenMask(:363),因为 glScissor(0,0,0,0) 是合法 GL、意思是"拒绝每个片元"(:352-362) |
| D10(v2 新增) | 纹理参数(swizzle / base-max level / dsMode)住在 pipe_sampler_view 里 |
set_texture_params(res, …) 独立,MGPSamplerView 只带视图限制 |
一张只作 FBO attachment / image 单元 / CopyImage 端点的纹理没有 sampler view,但 Espryt 对 attachment 也调 SyncTextureParamsToBackend(DirectGLES.cpp:1580-1601),且 RequireImageBindableStorage 要在前端 params 版本不动的情况下强制重同步(Managers.cpp:2815-2821) |
没有 pipe_transfer、没有 set_pixel_unpack_state、没有压缩格式概念、renderbuffer 不折进纹理、set_sampler_views 没有 stage 维度。
3.7 覆盖论证
3.7.1 对 477 读点分类的逐类映射
| delta 类 | n | 满足它的 MGPipe 调用 | 残余 |
|---|---|---|---|
| handle 化(wire 句柄) | 167 | 每个命名对象的调用签名里的 MGPipeHandle |
— |
| RenderStateBlob | 99 | create/bind_render_state + set_dynamic_state |
— |
| ObjectBind:Texture / Sampler | 33 | set_sampler_views + bind_sampler_states |
— |
| ObjectBind:Buffer | 29 | set_vertex_buffers / set_index_buffer / set_indirect_buffers |
— |
| ObjectBind:BufferRange | 24 | set_shader_buffers / set_stream_output_targets |
Uniform 类另带 host payload(D-B8) |
| FboAttach + DrawBuffers + ReadBuffer | 19 | set_framebuffer_state |
— |
| Buffer ops delta | 17 | resource_* 全族 |
— |
| XfbOp | 15 | set_stream_output_targets + *_stream_output |
— |
| ObjectBind:Image | 14 | set_shader_images |
— |
| ObjectBind:VAO | 12 | bind_vertex_elements_state + set_vertex_buffers + set_index_buffer |
— |
| ObjectBind:Program | 10 | set_draw_program / set_dispatch_program |
— |
| TexParam / SamplerParam | 9 | set_texture_params + create_sampler_state + create_sampler_view |
v2 修正归属(D10) |
| Texture state(dirty level/rect) | 7 | resource_subdata(带步长描述符) |
归属反转(§6.3) |
| PixelStoreBlob | 6 | set_pixel_pack_state |
unpack 删除 |
| client-resolved(error queue) | 6 | on_gl_error 回调(§6) |
— |
| ProgramPublish | 3 | create_shader_state |
依赖 P0.5 |
| client-resolved(validation) | 3 | client 自答 | — |
| CurrentAttrib | 2 | set_vertex_attrib_defaults |
— |
| client-resolved(compile env) | 2 | on_caps_invalidated |
— |
| Patch 参数 | — | set_patch_state |
同时是 variant 输入 |
| 条件渲染 | — | client 解析,永不过线 | Core.h:387-391 |
| XFB CPU 计数 | — | 纯 client;MGPDrawInfo::xfbCpuCapturedVertices(flag 门控) |
— |
| backend 重铸纪元 | — | 无 client 对应物:MGGen,server 私有 |
— |
那 1997 个前端 getter 站点不是第二个面:89 个纯版本读根本不过线,72 个数据字节读全部落在 §4.7/§4.8 与 MGHostSpan,38 个 GetLifetimeId() 变成 handle。
3.7.2 覆盖论证不是这张表,是这三道门(v2:从两道增至三道)
上表是声明。证明是机械的:
门 A —— include 图门(v2 新增,取代 v1 单靠 nm 的那半)。
v1 说 MG_Backend 只允许 include "一张共享值头白名单(RenderState.h 的 RenderStateParameters、SamplerObject.h 的 SamplerParameters、…)"。实测这张白名单不是叶子集:RenderState.h:12 include FramebufferState/FramebufferObject.h,后者 :12-13 再 include TextureState/TextureObject.h 与 RenderbufferState/RenderbufferObject.h;依赖是结构性的——RenderStateParameters 用 FramebufferObject::MAX_DRAW_BUFFERS 给两个数组定长(RenderState.h:263, 273)。所以"把 RenderStateParameters 交给纯净的 MG_Backend"会把整张 framebuffer/texture/renderbuffer 类图一起拖进来。而 nm --undefined-only 看不见这个:只 include 而不调用其成员函数的类不产生未定义符号,门可以在 include 图完全耦合的情况下为绿。
修正:P0.5 交付 MG_Pipe/MGPipeValueTypes.h——把 MAX_DRAW_BUFFERS、PerBufferBlendState、StencilFaceState、PixelStoreParameters、RenderStateParameters、SamplerParameters、BorderColorForm、VertexAttribute 与相关枚举搬进去,它不 include MG_State/GLState 的任何东西;RenderState.h/SamplerObject.h/VertexArrayObject.h 反过来 include 它。门变成:
在 disaggregated 配置下编译
MG_Backend时,把MG_State/GLState从 include 搜索路径里移除(或对-H输出断言)。这是唯一一条能因它存在的理由变红的检查。
门 B —— 符号门。 nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang' 为空。保留,作为门 A 的补充(它能抓到通过前置声明+跨 TU 调用绕过 include 图的情况)。
门 C —— 未声明门。 在 MOBILEGL_PIPE_PUSH=all 且非 verify 构建里,MG_State::pGLContext 未声明。任何接口没满足的读是一次指名文件与行号的编译错误。strangler 结束时 grep -c 'pGLContext' MG_Backend/ == 0(grep pGLContext 不是 pGLContext->,因为还有 58 行非箭头用法)。这条门只跑非 verify 构建(D-B5:verify 构建保留 SnapshotFromGLContext())。
这三道门比生成一张 477 行的清单严格得多:它们禁止那次读,而不是给它编目,而且不会过期。 那份 inventory 保留为 tracker 侧覆盖检查表(G6,CI git diff --exit-code,0 UNMAPPED)。
3.7.3 21 条 D 类身份 memo 的重键表
| # | 今天的键 | 守什么 | MGPipe | 净效果 |
|---|---|---|---|---|
| D1 | StateBackendObjectRegistry 用裸 StateObject* + 同址 weak_ptr(Managers.h:282-325)×6 |
分配器地址复用;也是唯一的删除信号 | 按 slot 索引的数组 + gen 比较;显式 resource_destroy |
GC(1024/64 阈值)删除 ×6 |
| D2 | TwinLookupMemo ×3 + OwnerEquals(DirectGLES.cpp:62-131) |
复用堆地址命中 memo 槽 | 删除——数组下标就是查表 | ~75 行 + 140KiB |
| D3 | UnitTextureSyncEntry + PairingsIntact(:1441-1481) |
不移动任何计数器的 slot 交换(DSA by-name) | server 侧删除;去抖搬到 client(§2.5:set_sampler_views 的 client 侧 hash 抑制器,否则冗余 glBindSampler 会 per-batch 重发) |
server −115 行 / client +~60 行 |
| D4 | IsBufferDrawClean 身份优先比较(Managers.cpp:1436) |
respecify 交给前端一个新资源 | server 拥有资源表;gen 比较;GetChangeSerial()(Uint64,不回绕)继续过线 |
简化 |
| D5 | ResolvedDrawBuffers::iboFrontend(Managers.h:711-716) |
索引 slot 重绑而无 epoch/config 移动 | set_index_buffer 是独立调用 |
结构性 |
| D6 | m_syncedIndexBufferObject 陪一个回绕 Uint16(:775-780) |
版本回绕后换了个 buffer | {slot, gen} 比较,不回绕 |
结构性 |
| D7 | StampSyncedFBO 四元组(DirectGLES.cpp:1856-1901);packed_pixels postmortem :2815-2827 |
版本回绕 + backend 侧纹理重铸 | MGPFramebufferState::contentHash + server 私有 attachmentRemintEpoch(MGGen) |
一次 64 位比较 |
| D8 | g_fboTextureSyncList(:1580-1601) |
同 D3,针对 attachment | server 侧删除;由 contentHash 在 client 侧抑制 |
server −20 行 |
| D9 | ResolvedTextureBindingMemo:9 个键 + 驱动绑定影子的 memcmp(:3218-3291) |
任何未枚举的写者扰动某个 unit | (shaderCso.slot, viewSetSerial) 两字比较;viewSetSerial 由 server 在 set_sampler_views 内部 ++。前提是 client 侧的 hash 抑制器已经挡住冗余推送,否则这个 serial 每个 batch 都动 |
更便宜(有前提) |
| D10 | UnitSamplerLookupMemo 的 WeakPtr owner 测试(:3105-3125) |
死 sampler 复活 | 数组下标 | 删除 |
| D11 | VertexInputStateFactory::ComputeHash 混入 GetLifetimeId()(:38-49) |
复用 buffer 地址重现整个 content hash | CSO handle 就是身份;gen 混进 server 侧每个 content hash |
删除一整类 |
| D12 | SetBackendStateMemo(&entry, evictionEpoch):前端 VAO 里存后端堆裸指针(VertexInputStateFactory.cpp:78) |
table 淘汰 | 直接删除,不翻译 | — |
| D13 | VaoDrawMemo 槽(VulkanRenderer.h:1230-1245) |
ABA | CSO handle | 2 字 |
| D14 | SetupDrawSnapshot 的三组 (ptr, lifetimeId, version) + 有损的 sampledContentSum/sampledParamsSum |
一切 | 三个 handle + 两个 server 纪元 + dirty mask | ~14 个探测字段 → 1 次比较;顺带消灭一类哈希碰撞 |
| D15 | m_rpFast*(VkRenderPassManager.h:305-320) |
ABA | contentHash + MGGen |
1 次比较 |
| D16 | VkTextureManager::TextureIdentity + GetTextureObject(name) 存活探测(VkTextureManager.cpp:806-819) |
名字复用 / 删了但仍被 FBO 引用 / 默认纹理 | {slot, gen} + 显式 destroy |
三种失效模式一起消失 |
| D17 | VkClearManager::TextureIdentity(VkClearManager.h:76-83) |
ABA | {slot, gen} |
— |
| D18 | 纹理/renderbuffer 资源用节点式 std::unordered_map(postmortem VkRenderPassManager.h:375-397) |
扩表搬迁使缓存的 Resource* 失效 |
UNCHANGED。 接口零约束;这是 server 内部分配纪律。postmortem 注释必须逐字带进 review checklist | 保留 |
| D19 | ProgramFactory::m_cacheStructureEpoch |
守 server 内部裸指针 | UNCHANGED(MGGen 族) |
保留 |
| D20 | ConvertedVertexStreamKey + 纯为防地址复用持有的 SharedPtr sourcePin |
ABA | server 拥有资源;changeSerial 过线 |
pin 删除 |
| D21 | m_xfbCounterSlotByObject[GetBoundTransformFeedbackName()](VulkanRenderer.cpp:11136-11146) |
什么都没守——活的潜伏 bug | XFB 对象 handle | 顺带修一个 bug,先独立落 dev |
总计:11 条直接删除,2 条(D3/D8)server 删除但去抖搬到 client,7 条重键成更便宜的比较,1 条(D18)原样不动。
4. 前端 state tracker
4.1 推送发生在哪里——本设计里最容易做错的一个决定
不在 GL setter 里。 glEnable(GL_BLEND) 绝不调 bind_render_state。Blaze3D 每个 batch 都用它包住,代码自己标注它是最热的路径(DirectGLES.cpp:2029-2032)。天真的 per-setter 推送把每一次冗余开关变成一次接口调用加一次 server 侧 CSO 查表——严格慢于今天。
在 verb 之前的 validate 时刻。
// MG_Impl/Pipe/Tracker.h
class MGPipeTracker {
public:
// 每一类 verb 一个入口;由 PipeCalls.def 的 kCtxVerb / kCtxObject 条目生成(§5.2.1)
void ValidateForDraw(const MGPValidateHint&); // 20 个 GL draw 入口
void ValidateForDispatch(); // glDispatchCompute*
void ValidateForClear(GLbitfield); // framebuffer + 渲染状态(ClearColor 在其中)
void ValidateForBlitOrCopy(); // framebuffer + pack state
void ValidateForTextureOp(MGPipeHandle res); // GenerateMipmap / CopyTex* / BindImageTexture
void ValidateForReadback(); // ReadPixels / GetTexImage
void ValidateForXfbSpan(); // Begin/End/Pause/Resume TransformFeedback
void ValidateForQuery(); // query begin/end
private:
Uint64 m_dirty;
Uint64 m_lastPushed[kGroupCount];
Uint64 m_lastSetHash[kVarTailGroupCount]; // ★ kVarTail set_* 的发射抑制器(§2.5)
};
这八个入口不是随手列的:MG_Impl 用到 70 个不同表项 / ~93 个调用点,其中只有 ~22 个是 draw/dispatch,其余 ~48 个是纹理操作、回读、blit、clear、XFB 跨度、query——而它们中很多自己就读 pGLContext(§2.1(a) 列了具体行号)。v1 只给 4 个 validate 入口、只在两处填快照,会让第一个 glGenerateMipmap/glReadPixels 撞上 poison Fatal,MOBILEGL_PIPE_VERIFY 的全绿验收因此不可达。
4.1.1 哪些操作在 GL 调用时刻推送(v2 修正推论 1)
规则的正确措辞:
只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送——即
BufferBackendOps的七个 hook(BufferObject.h:70-71自己写着"在 GL 调用时刻分发,就在 shadow 拷贝刚更新之后")。纹理 subdata 不在此列。
理由:glTexSubImage* 根本不调 backend 表(GL_Texture.cpp 只有 3 处 MarkStorageDirtyRegion),全部纹理上传由 Espryt 在 sync 时刻按累积区域做,那里才跑 96-rect 级联合并与 union-box 回退,并在 unpack ring 可用时刻意塌成一个 box(Managers.cpp:4386-4390,实测 +6 ms/frame)。逐 glTexSubImage 发一条 resource_subdata 精确复现那个 ~100 作业的形状。
因此纹理路径的形态是:client 在自己的 MipmapStorage rect 模型里累积(§6.3 的发射游标),在下一个 validate / flush 点把合并后的形状作为一条 resource_subdata(带 union box + region 列表)发出。MOBILEGL_PIPE_STATS 必须把逐帧 resource_subdata 发射次数单列一类,并在 MC 动画图集 fixture 上设上限。
稳态成本:见 §13.2(v2 已按动态口径重写)。
4.2 dirty bits:值类零新增记账,对象类新增 5 个聚合世代(推论 4)
| dirty 位 | 类别 | 快门来源 |
|---|---|---|
NEW_RENDER_STATE / NEW_PIPELINE_STATE |
值 | m_version / m_pipelineStateVersion(RenderState.h:522, 529;bump 点 RenderState.cpp:311-312 等) |
NEW_PIXEL_PACK |
值 | PixelStoreParameters(RenderState.h:190-199) |
NEW_PATCH_STATE |
值 | patch 三字段,用 BitwiseEqual 比较(NaN 合法,DirectGLES.cpp:2807-2814) |
NEW_VERTEX_ATTRIB_DEFAULTS |
值 | GetCurrentVertexAttribute |
NEW_VERTEX_ELEMENTS |
值 | VertexArrayObject::GetConfigVersion()(Uint32,:155) |
NEW_VERTEX_BUFFERS |
对象 | VertexArrayState::m_anyVaoAttributeGeneration(新增)→ 命中后走 32 属性前缀 + 逐属性 VertexAttributeVersion(:66-70) |
NEW_INDEX_BUFFER |
对象 | 索引 slot GetVersion()(回绕 Uint16)+ 绑定对象 {slot,gen} |
NEW_FRAMEBUFFER |
对象 | FramebufferState::m_anyAttachmentGeneration(新增)+ GetObjectVersion() + slot 版本 → 命中后重算 contentHash |
NEW_SAMPLER_VIEWS |
对象 | TextureState::m_anyTextureContentGeneration + m_anyTextureParamsGeneration(新增)+ GetTextureBindGeneration() + GetSamplingResolutionGeneration() → 命中后走 GetMaxTouchedUnit() 前缀、重算集合 hash、hash 未变则不发 |
NEW_SAMPLERS |
对象 | SamplerObject::GetVersion()(回绕 Uint16,SamplerObject.h:155)+ 上面的聚合 |
NEW_SHADER_IMAGES |
对象 | ImageTextureBinding::Version(TextureState.h:24, 34)+ m_anyTextureContentGeneration |
NEW_SHADER |
值 | GetLinkVersion() + GetImageUnitVersion()(ProgramObject.h:844, 906) |
NEW_SHADER_BINDINGS |
值 | GetBackendStateVersion()、GetBlockBindingVersion()、GetUniformWriteSetVersion() |
NEW_GLOBAL_CONSTANTS |
值 | GetUBOContentVersion()(~0u 跳过回绕,:791-794) |
NEW_CONST_BUFFERS / NEW_SHADER_BUFFERS / NEW_SO_TARGETS |
对象 | BufferState::m_anyBufferChangeGeneration(新增)+ slot 版本 → 命中后走 GetTouchedBindPointCount() 前缀 |
五个新增聚合世代(TextureState 两个、BufferState、VertexArrayState、FramebufferState 各一)全部落在既有 bump 点上,合计约 20 行。它们把对象类组的快门从"每 validate 走查 192 个单元 / 84×4 个绑定点 / 32 个属性 / 40 个 attachment"降成一次 Uint64 比较;只有快门为真时才走 touched 前缀并重算集合 hash。
完整性由 gen_pipe_dirty_surface.py 保证(推论 4):它枚举 MG_Impl/GLImpl/** 里每一个会改变某组的 mutator,映射到必须 bump 的聚合世代,CI 重生成 + git diff --exit-code,未映射的 mutator 直接失败。这是 B-R6 的第四层。
三个回绕的 Uint16 在 tracker 边界加宽。 m_lastPushed[] 是 tracker 自己的字段,加宽到 Uint32/Uint64 不需要改 MG_State 一行;同时 handle 与它同行过线。回绕在 tracker 本地是无害的(一次回绕造成一次多余的重推,永不漏推),何况集合 hash 抑制器会把多余重推吞掉。
4.3 每命令 validate 的不变式(v2:从"固定顺序契约"降级)
规范条款(D-B3 v2):
一条 verb 的全部
set_*/bind_*必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,set_*之间没有顺序要求。
推荐实现顺序(便于 tracker 的代码组织与 dirty 位遍历,不是正确性契约):
1 set_framebuffer_state
2 set_draw_program(create_shader_state 在 link 时刻已发)
3 set_texture_params / set_sampler_views / bind_sampler_states / set_shader_images /
set_shader_buffers / set_global_constants
4 bind_render_state(未命中时先 create_render_state)/ set_dynamic_state
5 bind_vertex_elements_state / set_vertex_buffers / set_index_buffer / set_vertex_attrib_defaults
6 set_patch_state / set_stream_output_targets
7 draw_vbo
退役 workaround 的机制是惰性特化,不是调用顺序:DirectGLES.cpp:2712-2732 的 fragColor 重推导与 g_broadcastMemo* 之所以能删,是因为 server 在 verb 处才特化,那时 set_framebuffer_state 一定已到;同理 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573,注释明说"不可表达为单调版本")由 set_shader_images 在 verb 之前告知。v1 把这归因于"framebuffer 严格第一",但它自己把 images 排在 program 之后——那个论证站不住,结论仍然成立。
create_shader_state 从编译池的终止 continuation 发出(JobNode.h:109-123),不是从 draw 发出,这样 SPIR-V 在用到它的第一个 draw 之前就到达 server。这是 monolith 拿不到的异步收益。
4.4 合并:保留代码库已经发现的三条,加上第四条
- 整块结构优于逐字段。 Magma 的
ComputePipelineStateHash(VulkanRenderer.cpp:4818-4826)已经把 ~17 次 accessor 调用换成一次 bulk fetch;Espryt 的三段 memcmp 同理。 - 高水位标记。
BufferState::TouchBindPoint/GetTouchedBindPointCount(BufferState.h:51-62,每 target 84 个绑定点)与TextureState::NoteUnitTouched/GetMaxTouchedUnit(Core.h:124-126,192 个单元)必须留在 tracker 的走查里,它们直接就是set_shader_buffers/set_sampler_views的count实参。 - 只发 program 解析过的集合,用
LinkArtifacts::uniformSamplerOrImageUnitIndex(ProgramObject.h:1298)。两个 backend 今天已经在算(ResolveAndBindUnitTextures,DirectGLES.cpp:2973;UniformManager::CollectSampledTextures)。 - (v2 新增)集合 hash 抑制器。 每一条
kVarTail的set_*在 client 侧算一次已解析集合的 xxHash,与m_lastSetHash[]比较,未变就不发。这是 §2.5 里那 ~175 行去抖搬到 client 后的载体,也是 D9 的前提——没有它,GetTextureBindGeneration()在冗余重绑时的 bump(DirectGLES.cpp:1414-1420,26.2 每次纹理单元切换都重绑同一个 sampler)会让每个 batch 重发一条几百字节的变长记录并冲掉 server 的两个 memo。
索引绑定的范围必须在 validate 时刻实时解析,不是在 bind 时刻快照。 BindingSlotRange1D::GetRange() 对整 buffer 绑定返回 Range1D(0, object->GetSize()),因为 glBindBufferBase 之后再 glBufferData 是普通应用代码。
4.5 sampler view 在 client 侧解析
GL 是每个 unit 每个 target 各一个绑定(TextureUnit.h:20, 24-25;TextureState::m_textureUnits 是 Array<TextureUnit, 192> 按值存放,TextureState.h:128,每 stage 广告上限 32,:46),shader 看见哪一个取决于 sampler uniform 的声明类型、mipmap 完备性(IsMipmapCompleteForFilter,TextureObject.h:309;SamplesAsIncompleteTexture,:315)和 IsUndefinedDefaultTexture(:329-332)。gallium 的"每槽一个 view"就是解析后的形态。
解析留在 client,并且 client 必须为它保留一个自己的 memo(§2.5 的 ~40 行搬迁项),否则每 draw 重跑完备性规则。合并单元空间,无 stage 维度(§3.4.3)。
两处 backend 特定的后处理留在 server,作用在已解析的集合上:Espryt 的 raw-depth-fetch sampler 替换(DirectGLES.cpp:3540-3546)与 Magma 的 feedback-loop 检测(对着 draw FBO,UniformManager.cpp:554)。两者都可从已推送的 set_framebuffer_state + view 集合判定。
4.6 对象生命周期、共享组与 composite pipeline program
4.6.1 生命周期
resource_create 在前端对象构造时发,存储由 resource_respecify 惰性定义。resource_destroy 在前端对象析构时发。三条顺序约束:
- view 先于其存储属主销毁:
GetViewStorageOwner()(TextureObject.h:96-100)→MGPResourceDesc::viewOf+ server 侧 keep-alive。 - FBO attachment 钉住纹理(
FramebufferObject.h:95)→set_framebuffer_state的 surface handle 隐含 server keep-alive。 - buffer texture 钉住 buffer,范围实时解析(
TextureObjectBuffer.h:28, 35-46)→MGPResourceDesc::{bufferForTexBuffer, bufOffset, bufSize}。
4.6.2 共享组
v1:一个 screen、一个 context、一个扁平 handle 空间、一条 flow。eglMakeCurrent 是 flow 所有权转移,在既有 EGLOperationMutex(EGLImpl.cpp:241)下发射——顺手修今天不取该锁的两个入口:ReleaseThread(:341-350)与 SwapInterval(:435-450)。
4.6.3 composite pipeline program:判过死刑的那个反对意见,答案是"什么都不用做"
GLContext::GetProgramForDraw()(Core.cpp:592)今天就已经完全在前端完成合成:join 每个 stage 的 JoinLinkAndSpirv()、按 ComputeDrawProgramSignature()(:630)查 cache、miss 时构造故意不命名的 MakeShared<ProgramObject>(0u)(:644)、挂上每个 stage 被钉住的 linked snapshot、重装捕获 stage 的 XFB varyings、Link(true)、缓存、RefreshCompositeUniforms。
tracker 调它,拿到 SharedPtr<ProgramObject>,推一个 handle。合成体没有 GL name,但有 lifetimeId,slot 从 ShaderCso 的保留高位段分配。生命周期:pipeline cache 淘汰该条目时释放 slot、gen++、发 delete_shader_state——CompositeResolver.cpp 里三行。
合成体从不过线、从不被重新实现,server 侧不需要任何"解析后的 draw program"钩子。 副带收益:阻塞的 JoinLinkAndSpirv() 彻底离开 server 的 draw path。
4.7 program artifacts 与全局 UBO scratch
create_shader_state 的 payload 是 SPIR-V + 全结构体反射归档(§3.5.5),不是源码。依赖 P0.5 的头文件抽取。
SPIRV-Cross 留在 server(TranspileSpirvToEssl,Managers.cpp:6575):它消费 SPIR-V 加设备事实。glslang 留在 client。 这是一次文件级切割。
全局 UBO scratch 走独立入口(D6):set_global_constants(shaderCso, MGPBlobRef bytes, Uint32 version),键 (shaderCso.slot, uboContentVersion),复现 DirectGLES.cpp:3369-3392 的"每 program 每帧至多一次"。它小、每次 glUniform* 变、有版本,字节走 SEG_STAGE。
具名 UBO 字节走 set_shader_buffers 的 host payload(D-B8):UniformManager::ResolveUniformBufferPayload 在 UniformManager.cpp:2022 调 SyncPersistentMappedRange()、:2052 读 MappedData() + rangeStart 打进 Magma 自己的 UBO ring——消费者在 server,搬不走。由 kCapNeedsHostUboBytes 门控(Espryt 直接绑给驱动,不需要)。逐帧字节量进 stage-ubo-named 计数器;在 P0 给出数字之前不冻结这个 payload 的形状。
backend 侧 program link/compile 失败不需要任何同步返回,也不需要新事件种类。 实测:SyncToBackend 在 Managers.cpp:8091 link、:8094 读 GL_LINK_STATUS、:8095 折进 m_backendProgramUsable、:8097-8101 取驱动日志、:8106 发 MGLOG_E;Use() 随后绑 program 0(:8357)并 MGLOG_E_ONCE(:8364-8372)。没有 GL error、没有 ProgramObject 变更、GL_LINK_STATUS 永不撤回(:7098、:7247-7249、:6478、:7827)。同步查询由 client 从 ProgramObject 回答(GL_Program.cpp:851 → ProgramObject.h:913)。所以 on_log 逐字复现它——但由此推出一条对事件通道的强制修正,见 §6.4。
4.8 emulation 所需前端数据的显式传递(v2 按 D-B7 重写)
归属规则:驱动表达不了的变换在 state tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。v1 用 cap 位门控 emulation 归属的做法对 restart 与 multi-draw 不可表达(D-B7),此处收回。
| emulation | 归属 | 门 | 过线的是什么 |
|---|---|---|---|
client 顶点数组(Managers.cpp:2500-2592 把 attrib.Offset 当应用裸指针,每 draw 每属性上传 (first+count-1)*stride+elementSize;VulkanRenderer.cpp:3737 是唯一无界的应用指针读) |
client(它拥有地址空间) | — | 字节,永不是指针(MGHostSpan) |
索引扫描(TryComputeMaxIndexFromHostBytes,VulkanRenderer.cpp:3407-3470,用于 :3599 给上一条定界) |
client(只有它同时持有两个数组) | — | MGPDrawInfo::minIndex/maxIndex(kHasIndexRange 门控),~0 = 未知 |
| client 索引数组 | client | — | MGPDrawInfo::userIndices(kHasUserIndices 门控) |
primitive-restart 重写(DirectGLES.cpp:4368-4470 整 EBO 重写,kMaxRestartRewriteBytes = 1<<26 = 64 MiB,:4218;VulkanRenderer.cpp:4159-4161) |
server(v2 改:v1 曾说 client) | kCapNeedsHostIndexBytes → 索引宿主镜像 |
零线上流量:server 从镜像读。monolith 行为零变化,诊断仍落在原线程(开放问题 12 关闭) |
multi-draw 分档 + 展平(MultiDraw.cpp:282-320 的 ResolveTierForBatch 逐 batch 在五档里选,输入含 programReadsDrawID——转译出的 ESSL 的性质,只存在于 server;容量判定 kMaxFlattenedIndices :72 / kMaxComputeFlattenedIndices :82;自动阶梯 Ext→BaseVertex→MultiIndirect→Indirect→DrawElements :241-243,CPU 展平是回退) |
server,全部五档(v2 改) | kCapNeedsHostIndexBytes |
draw_vbo(info, indirect, MGPDrawRange[], numDraws);索引字节走镜像 |
*IndirectCount CPU 回退(DirectGLES.cpp:4655-4695 从 parameterBuffer->MappedData() 读实际 draw 数) |
client | — | client 从自己的 shadow 解析计数,发解析后的 MGPDrawRange[](几十字节)。注意它今天只调 SyncPersistentMappedRange(),不调 SyncGpuWrites()(§4.8.1) |
viewport-array N 遍回放(DirectGLES.cpp:3742-3846,今天包住 14 个 draw 入口) |
server | kCapViewportArray |
无新增:16 组 viewport/scissor/depth-range 已在渲染状态里 |
fp64 顶点窄化(Managers.cpp:2518-2557) |
server(后端格式决策) | kCapFloat64VertexAttrib(BackendObject.h:487-500 明说它与 SupportsShaderFloat64 独立) |
原始字节;IsLong 与 Type 分开过线 |
image-bindable 存储加宽/拆分(Managers.cpp:2789-2822、:4620-4630) |
server | — | 正向 imageBindableHint;反向 on_texture_pull_request + 终止符(§6.5) |
| 生成 mipmap 的前端存储 | 拆开:client 分配 level 存储,server 生成 | — | MGPMipPlan;on_mip_levels_generated 只带形状不带字节(见 §12.1 的说明);CPU 回退路径的纹素由 on_texture_writeback 回来 |
CopyImage shadow 镜像(DirectGLES.cpp:7065-7140) |
client | — | 只回"拷贝成功"。删掉一整条 server→client 字节通道 |
XFB CPU 图元计数(GL_Drawing.cpp:172,调用点 :1133, 1141, 1195, 1668) |
纯 client | kCapCpuXfbPrimitiveAccounting |
MGPDrawInfo::xfbCpuCapturedVertices(flag 门控)+ end_stream_output 的 MGPXfbAccounting |
XFB scatter 的 read-modify-write(DirectGLES.cpp:893-960) |
client(v2 新增行) | — | 见 §6.2.1 的 on_buffer_writeback 修正 |
| 压缩纹理 / pixel unpack 规整 | 纯 client | — | 无 |
4.8.1 陈旧索引纪律——逐站点表,不是一条笼统规则(v2 修正)
v1 写"上表里每一次 client 侧扫描/重写,在 monolith 里都紧跟在 SyncPersistentMappedRange() + SyncGpuWrites() 之后"。对 *IndirectCount 不成立:DirectGLES.cpp:4666-4667 只调两次 SyncPersistentMappedRange(),然后在 :4690-4694 直接读 MappedData();没有 SyncGpuWrites(),因此今天没有停等。而 SyncGpuWrites 才是触发 ReadbackFromGpu(BufferObject.cpp:265-274)的那一条。照 v1 的笼统规则实施,glMultiDrawElementsIndirectCount 会平白获得一次 publish-and-wait round trip——而 trace 语料里恰好有 minecraft-1.21.1-neoforge-create-indirect-in-world(Create/Flywheel,indirect 与 parameter buffer 每帧被写),于是这会变成一个逐帧逐 batch 的同步 round trip,而 §12.2 第 10 行还把它写成"常见情况代价为零"。
逐站点 reconcile 表(必须逐字复现 monolith 的集合,不多不少):
| client 侧动作 | monolith 对应站点 | 必须做的 reconcile |
|---|---|---|
| client 顶点数组范围计算 + 暂存 | Managers.cpp:2500-2592(无 buffer,源是应用指针) |
无(应用内存,无 GPU 写者) |
| 最大索引扫描(EBO 源) | VulkanRenderer.cpp:3406-3470 前的 :3431 |
SyncPersistentMappedRange() + SyncGpuWrites() |
| 最大索引扫描(client 索引源) | 同上,client 指针分支 | 无 |
*IndirectCount 计数解析 |
DirectGLES.cpp:4666-4667、:4768-4793 |
只 SyncPersistentMappedRange()。不加 SyncGpuWrites() |
| (server 侧)restart 重写 | DirectGLES.cpp:4412-4413 |
server 从镜像读;镜像由 subdata 流维护,GPU 写者的可见性由 on_gpu_written 收窄集驱动——server 侧本地判定,无 round trip |
| (server 侧)multi-draw 展平 | MultiDraw.cpp:498-499 |
同上 |
client 侧需要 reconcile 的那两条的形态:publish → 等 appliedSeq → 排空事件 → 再碰 shadow。跳过它,maxIndex 来自陈旧字节,顶点数组被少拷 → 几何缺失,或越界读应用数组。
门:ClientArrayAfterComputeWriteScenario(新增),必须能因它存在的理由变红。
门:create-indirect fixture 上的 roundtrips-per-frame 计数器必须读零(P8 验收),这是上面那条"不加 SyncGpuWrites()"的绊线。
另注:monolith 在 *IndirectCount 上不调 SyncGpuWrites() 本身可能是一个潜在缺口(compute 写的 indirect buffer)。那是一个独立的 dev 问题,拆分不得借机"顺手修"——那会改变基线并让逐名对比失去意义。列入开放问题。
5. 后端状态机改造
5.1 什么原样不动(先说这个,因为它是"最短可信改造"的依据)
每一个 ring、pool、arena、quirk、lowering pass 原地不动:
Espryt:三条 persistent-mapped ring、PersistentRing 的分配/背压算法、buffer pool、全部 7 条 fallback-repack 路径(Managers.cpp:3209-3527)、m_backendColorSlots draw-buffer 置换表、三个 scratch FBO 及其驱动侧 attachment 影子、PackState、全部驱动绑定影子、Adreno 的"禁用属性无指针 SIGSEGV" workaround(Managers.cpp:2371-2380, 2427-2433)、Mali 的 XFB 捕获丢失 workaround(DirectGLES.cpp:400-410)、ScopedDefaultUnpackState、SPIRV-Cross 会话与 6 次 post-emission ESSL 重写、驱动 POST 自检族、restart 重写与 multi-draw 五档(D-B7)。
Magma:VulkanRenderer 全部 memo 与 scratch、PipelineFactory、ProgramFactory、UniformManager 的 ring 与描述符集、五个 Vk*Manager、FrameContext、SwapchainObject、DynamicStateShadow、VertexInputStateFactory 的 cache 本体、以及 D18 的节点式容器纪律。
v2 从"原样不动"里移出的一项:Managers.cpp:4274-4326 的 sub-rect 上传判定与跨步计算——它今天靠 uploadData == mipData 指针比较与整 level 步长算术,split 下不成立(§3.5.6),必须改成从 MGPSubRegion 描述符取步长。这不是 v1 说的"只把输入从拉取的 shadow 指针换成 MGPBlobRef",是真代码改动,计入子系统 5。
唯一两处必须真改的 MG_State 类型内部用法:
- Magma 的占位纹理(
UniformManager.cpp:161-181, 1416-1500, 1624-1634):构造真的TextureObject2D/TextureObject2DMultisample/TextureObject2DMultisampleArray,走SetInternalFormat(RGBA8)/AllocateStorage({1,1,1},4)/UpdateMipmapSubData/MarkStorageDirty/SetSamples(2)(VUID-RuntimeSpirv-samples-08726)/TruncateMipmapLevels(1),唯一理由是让"未绑定单元"复用SyncTextureAndGetDescriptor(ITextureObject&)这个签名。改成 backend 自己分配VkImage+ view + descriptor:~120 行前端对象木偶戏变成 ~60 行直白的 VMA/Vulkan,34 个MOBILEGL_ASSERT(pGLContext)里的 9 个随之消失。 - Magma 的两个内部 shader(
InitializeBlitResourcesVulkanRenderer.cpp:4210-4283、InitializeDepthMipmapResources:4287-4356):烘焙成 SPIR-V。 方式:把生成的 SPIR-V、uniform location、UBO 布局作为生成头文件签进树,用一个MG_Test重跑树内 glslang 对同一批源码字符串并逐字节比对守新鲜度。不用构建期 host glslang target。uSource的描述符绑定本来就由ProgramFactory自己的 SPIRV-Reflect 走查找到(:4340-4350),原样存活。顺带把一次 glslang 编译从 monolith 启动路径上删掉。
Espryt 有一个小号同类:g_rawDepthFetchSamplerState(DirectGLES.cpp:166-179)→ backend 原生 sampler 记录,~40 行。
5.2 strangler 脚手架:PipeInputs + 逐 verb 填充器 + poison 世代
// MG_Backend/MGPipe/PipeInputs.h
namespace MobileGL::MG_Pipe {
struct PipeInputs {
// 阶段 A:字段类型与 backend 今天读到的**完全一致**
const RenderStateParameters& GetRenderStateParameters() const;
Uint16 GetRenderStateParametersVersion() const;
const MGPVaoRec& GetBoundVertexArray() const;
// … 每个 backend 真正用到的 GLContext 方法一个访问器(Espryt 32 个 / Magma 55 个)
#if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
Uint64 m_filledGen[kFieldCount]; // ★v2:逐字段"上次填充的 verb 序号",不是一位
Uint64 m_currentVerbSerial;
#endif
};
extern PipeInputs gPipeInputs;
}
#if MOBILEGL_PIPE_PUSH
# define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
# define MGB_CTX (::MG_State::pGLContext)
#endif
PipeInputs 按 memo 键组织,不是按读点组织。 这是它只有 ~20KB、且字段集在整个迁移期稳定的原因。
5.2.1 三个阶段,其中阶段 A 可证明是近乎 no-op
| 阶段 | 改什么 | 怎么证明 |
|---|---|---|
| A — 别名 | 机械 sed:MG_State::pGLContext-> → MGB_CTX->(293 处);外加手工转换 58 行非箭头用法(§2.4)。逐 verb 类填充点(见下)填 gPipeInputs。backend 函数体其余部分不变 |
nm --defined-only 不变;.text size 在可逐行归因的范围内(不是完全相等,见下) |
| B — 推送 | tracker 填 gPipeInputs;填充器仍在,按 MOBILEGL_PIPE_PUSH 位图逐字段让位 |
MOBILEGL_PIPE_VERIFY=1(§13.3-②):tracker 再填一份快照版,G4 生成的比对器逐字段每 draw 比一次 |
| C — handle 化 | SharedPtr<FrontendObject> 字段 → MGPipeHandle + POD 描述符;memo 重键;写回变回调 |
全套门(§13.3)。注意 A/B 口径在此收窄,见 §5.7 |
v2 修正 1:填充点必须逐 verb 类,不能只有两处。
v1 只在 PrepareForDraw(DirectGLES.cpp:2916)与 SetupDraw(VulkanRenderer.cpp:6371)顶端填快照。但 MG_Impl 用到的 70 个表项里有 ~48 个不是 draw/dispatch,其中多个自己就读 pGLContext(UpdateTextureBindingAtTarget :6051-6052、PackStateFromContext :6129、Clear :4106/:4165、BlitFramebuffer :5988-5989、GetTexImage :9254-9257、DSA by-name :4038-4043、:7417-7418),而代码自己说明了这一点(:1501-1502:"for every non-draw call site (Clear, readbacks)")。
做法:G5 从 PipeCalls.def 生成"每个 kCtxVerb/kCtxObject 调用可能读哪些 PipeInputs 字段"的表,并在 MG_Impl 的 ~93 个边界站点上生成对应的 validate/fill 调用。这同时把 poison 从"某个 draw 上炸"升级为"在需要它的那个 verb 上炸"。
v2 修正 2:poison 从"位图"升级为"逐 verb 世代"。
一个只被上一个 draw 填过的字段,在紧随其后的 glTexSubImage/glReadPixels 里读到的是陈旧值,位图版的 poison 看不见(位已置)。世代版:每次 verb 递增 m_currentVerbSerial,字段被填时记下当时的序号,读取时断言 m_filledGen[f] == m_currentVerbSerial(对"跨 verb 有效"的字段单独标注为 sticky 并在生成表里显式列出)。这才让"一个字段在某个 verb 上没被推送"必然是一次 Fatal 而不是一次静默陈旧。
5.2.2 poison 世代是完整性的运行期绊线
在 debug 与 disaggregated 构建里,读一个当前 verb 未填的非 sticky 字段是 Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}——响亮、精确、不可能渲染过去。P13 之后(SnapshotFromGLContext() 只在 verify 构建里)完整性变成构建期事实:一个从未被写入的字段就是一个编译器能标出来的字段。
5.3 Track V / Track H 与残余值块
- Track V(值类型):
GetRenderStateParameters、GetPixelStoreParameters、IsCapabilityEnabled(+Indexed)、GetStencilState、GetColorMaskIndexed、GetDepthMask、GetScissorBox、GetPatchVertices、GetCurrentVertexAttribute、Magma 的 ~22 个标量 getter…… 约占 B 类读点的 55%。机械,每组 ~1 天。 - Track H(对象类型):167 个
SharedPtr<MG_State…>点。真活。
Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个显式临时调用 set_residual_value_state(MGPBlobRef):
struct ResidualValueBlock {
RenderStateParameters renderState; // 直到 create/bind_render_state + set_dynamic_state 落地
PixelStoreParameters pack; // 直到 set_pixel_pack_state 落地
Uint64 capabilityBits;
Uint32 patchVertices; Float patchOuter[4], patchInner[2];
// … 每个阶段变小 …
};
三条硬性纪律:
- 退役是一个编译错误。
static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE),常量每阶段下调;P13 到 0 之后static_assert(sizeof(ResidualValueBlock) == 0, ...)一直红到最后一个字段消失。 - 布局必须逐成员断言,不能只断言 sizeof。 异质 POD 并集跨编译器/ABI 最容易出 padding 差异,而 monolith 的 verify harness 看不见它(两侧是同一个 TU)。所以 G3 为每个成员生成
static_assert(offsetof(...) == N),并且在 split 下该块逐字段序列化而不是整块 memcpy。 - 只在 P2..P13 之间存在,
MOBILEGL_PIPE_STATS单独计一类字节。
5.4 DirectGLES(Espryt)逐子系统
PrepareForDraw 的阶段顺序(DirectGLES.cpp:2916-2975):GetBoundVertexArray → ResolveVaoTwin → GetProgramForDraw(join 编译池)→ CaptureDrawTextureSyncKeys → SyncNeccessaryBuffers → SyncCurrentVAO → SyncNeccessaryTextures → SyncImageTextureBindingsForDraw → MarkWritableImageBufferTexturesGpuWritten(改前端)→ SyncCurrentFBO → SyncCurrentProgram → SyncRenderState → BindCurrentFBO → VAO bind → SyncCurrentVertexAttributeValues → BindCurrentTextures → BindCurrentProgramWithResources → StartPendingTransformFeedback。
| # | 子系统 | 消除读点 | memo | 写回 | 轨 | 天 | 风险 |
|---|---|---|---|---|---|---|---|
| 0a | GetIntegeri_v/GetInteger64i_v/GetProgramiv 移回 MG_Impl |
14 | 0 | 0 | — | 1-2 | 极低(严格 no-op) |
| 0b | handle 基建;6 个 registry → slot 数组;删 TwinLookupMemo×3 / OwnerEquals / g_fbSlotCache / 2 个 GC 扫描 |
— | 9 删 | — | — | 5-7 | 低 |
| 1 | 渲染状态(DirectGLES.cpp:1962-2654,693 行) |
4(:2007, 2021, 2050, 2133) |
0 | 0 | V | 3-5 | 低:693 行函数体、单 Uint16 早退、三段 memcmp 全不动 |
| 2 | buffer + 7 个 BufferBackendOps |
19 | 3 | 6(+23 处 re-entry 删除) | H | 10-13 | 高(不碰 AcquirePersistentMap) |
| 3 | VAO / vertex elements | 2(+~10 getter) | 4 | 0(Espryt 不往前端对象写 memo) | H | 7-9 | 中 |
| 4 | framebuffer / renderbuffer | 8 + 4 处 pDefaultFramebufferInfo |
4 | 1 | H | 7-9 | 中高 |
| 5 | 纹理 / sampler / image unit / set_texture_params / subdata 描述符改造 |
18(+~35 getter) | 8(5 删) | 21 | H | 23-30(v1 为 20-26,+3-4 为 §3.5.6 的跨步描述符改造) | 高 |
| 6 | program + constant buffer | 16(+~30 getter) | 5 | 0 | H | 14-18 | 高 |
| 7 | XFB(含 scatter 搬到 client,§6.2.1) | 3 | 1 | 2 | H | 5-7 | 中 |
| 8 | emulation + MGHostSpan + 索引宿主镜像的 server 侧接口 |
~12 | 0 | 3 | — | 8-11 | 中 |
| 9 | 回读 / pack state | ~10 | 1 | 7 | V+H | 5-7 | 中 |
| 10 | 删 pull 路径 + MGB_CTX |
— | — | — | — | 4-6 | 低 |
| 合计 | 124 | ~32 | 28 | 92-124 |
子系统 5 是全表最危险的一处:它同时压着实测 +6ms/frame 的 box-vs-rects 悬崖(Managers.cpp:4386-4390)、7 条 fallback-repack 路径、以及 v2 新增的跨步描述符改造。缓解:resource_subdata 同时携带 box 与 region 列表且 server 选形状;repack 族本体不动;子系统 5 拆成两个可独立落地的半(先 sampler view + sampler + set_texture_params,再 image unit + dirty 归属反转 + 跨步描述符),让回归能二分到其中一半。Mali 设备门必须发布逐帧上传作业数与帧时增量(不是只有 SSIM)。
5.5 DirectVulkan(Magma)逐子系统
| # | 子系统 | 读点 | memo | 写回 | 天 | 风险 |
|---|---|---|---|---|---|---|
| 0a/0b | 同 Espryt;13 个身份缓存重键 | ~10 | 13 | 0 | 5-8 | 低 |
| 1 | pipeline + 动态状态 | ~55 | 1 | 0 | 3-4 | 低——两个 backend 里最便宜的一次转换 |
| 2 | SetupDraw + TrySetupDrawFastPath(:5994,377 行)+ SetupDrawSnapshot[4] |
~48 | 4 | 0 | 10-13 | 高 |
| 3 | VkBufferManager(7 个 op 里的 6 个;ResidentSubData 保持 null) |
~19 | 2 | 4 | 7-9 | 高 |
| 4 | VertexInputStateFactory + VaoDrawMemo(删掉写进前端 VAO 的后端堆裸指针) |
~6 | 2 | 3 | 2-3 | 低(纯结构性收益) |
| 5 | VkTextureManager(3504 行)+ VkSamplerManager + set_texture_params |
~30 | 3 | 7 | 13-16 | 高 |
| 6 | UniformManager 描述符 + 占位纹理原生化 + 具名 UBO host payload(D-B8) |
~35 | 4 | 6,且删 ~120 行 | 12-15 | 高 |
| 7 | VkRenderPassManager / VkClearManager / framebuffer(保留 D18) |
~20 | 2 | 0 | 7-9 | 中高 |
| 8 | ProgramFactory + 内部 shader 烘焙(含 4 天烘焙与回归测试) |
~15 | 1 | 2 | 7-9 | 中(构建 lane) |
| 9 | XFB(顺带修 D21)+ query + 回读 | ~15 | 2 | 5 | 11-14 | 中 |
| 10 | swapchain / default FBO(SwapchainObject.cpp:276-330 的写变 on_surface_changed) |
~4 | 0 | 7 | 4-5 | 中 |
| 11 | 删 pull 路径 | — | — | — | 4-6 | 低 |
| 合计 | 169 | ~34 | 42 | 85-111 |
Espryt 的子系统 1 与 Magma 的子系统 1 作为一个里程碑一起做(合计 6-9 天),这样同一个接口调用在两个 backend 上同时被证明。
5.6 strangler 顺序(风险最小化)
0a getter 移出(AdvertisedLimitsScenario;严格 no-op)
0b 字节/调用计数器落地 ← 含**动态** accessor 计数与 memo 命中率(§2.3.1)
0c 清工作树 per-draw fprintf
0d 值头与制品头抽取(MGPipeValueTypes.h、ProgramArtifacts.h)+ include 图门 ← P0.5
0e handle 基建:slot 分配器 + registry 变数组 + 删 TwinLookupMemo/OwnerEquals/g_fbSlotCache/GC
1 渲染状态(两个 backend 一起)+ Magma 子系统 4 ← 机制证明 + 第一片 Track H
2 buffer + BufferBackendOps ← 泛化已存在的模式;不碰 AcquirePersistentMap
3 VAO / vertex elements
4 framebuffer
5 纹理 / sampler / image unit(拆两半)
6 program + constant buffer
7 XFB + query + 回读 ← 可与 5/6 并行(第二个工程师)
8 emulation + 索引宿主镜像
9 删 pull 路径;三道纯度门转绿
0b 必须在任何迁移之前:所有 ring 尺寸、批处理阈值、wire 粒度决策否则都是猜测。0c 必须在基线之前:那两处 per-draw fprintf 污染每一次测量。0d 必须在 program 与渲染状态之前:否则纯度门与 nm -D | grep glslang 判据不可达。
5.7 A/B:旧路径怎么保留,以及它的口径在哪里收窄
MOBILEGL_PIPE_PUSH = <子系统位图> # 0 = 全 pull;每位一个子系统;含一位关闭 CSO 内容寻址(负面对照)
MOBILEGL_PIPE_VERIFY = 0|1 # 影子比对(~5-10x 慢,永不出货;P13 之后仍保留)
MOBILEGL_PIPE_STATS = 0|1 # 字节/调用/roundtrip/纹理拉取/上传形状计数器
MOBILEGL_PIPE_LEGACY_MEMOS= 0|1 # ★v2:编译期开关,保留 registry / TwinLookupMemo 实现
在 init 时刻锁存,与 MOBILEGL_BACKEND_TYPE 同一套机制(ConfigLoader.cpp:212-225),与树里已有的 ~40 个 MOBILEGL_* 开关并列。
v2 必须写明的口径收窄。 v1 说"任何一次提交都能在同一份二进制上按子系统 A/B,设备回归可以二分到'哪个子系统'"。这在阶段 B(值字段)成立,在阶段 C(handle 化)之后不成立:stage C 把 PipeInputs 的字段类型从 SharedPtr<FrontendObject> 换成 MGPipeHandle + POD 描述符、把 6 个 StateBackendObjectRegistry 哈希表换成 slot 数组、删掉 TwinLookupMemo×3 与 OwnerEquals、把 memo 重键成 {slot, gen}。位清零时,SnapshotFromGLContext() 仍要从 client 的 slot 表合成那个 handle,backend 仍然跑重键后的 memo 代码——两个分支跑的是同一份新代码。一个重键 bug(正是 D1/D2/D3/D11/D13 那一类)在两个分支里都在,位图二分不出来。
对策:MOBILEGL_PIPE_LEGACY_MEMOS(编译期开关)在 P3a 与 P4a 期间保留 registry / TwinLookupMemo 的实现活在同一个 PipeInputs 接口之下,给前两波 handle 化保留一个真正的旧-vs-新臂;随 pull 路径一起在 P13 退役。这条开关的存在期与代价必须写在阶段计划里(P3a/P4a 各 +1 天维护成本)。
P13 删除 pull 路径时:删 SnapshotFromGLContext() 的非 verify 编译分支、MGB_CTX 宏、MOBILEGL_PIPE_PUSH、MOBILEGL_PIPE_LEGACY_MEMOS;MOBILEGL_PIPE_VERIFY 连同它需要的 SnapshotFromGLContext() 与 MG_State include 一起保留(D-B5);static_assert(sizeof(ResidualValueBlock) == 0) 必须编译通过;三道纯度门(§3.7.2)在非 verify 构建上转绿。
6. backend → frontend 反向通道
这是历次评审对任何薄 backend 设计的中心反对意见,所以逐条处理,不做概括。实测:grep -rnoE "(->|\.)(SetBackendResource|SetBackendHashMemo|SetBackendStateMemo|SetBackendAuxMemo|WritebackFromBackend|MarkGpuWritten|MarkStorageDirty|AllocateStorage|SetInternalFormat|UpdateMipmapSubData|EnsureGpuResidentStorage|SyncPersistentMappedRange|SyncGpuWrites|RecordError|InvalidateCompileEnv|TruncateMipmapLevels|SetSamples)\(" MG_Backend/ = 95 个调用点 / 17 个方法,外加 6 处 backend 反向进 MG_Impl。
6.1 MGPipeCallbacks:把反向通道具名化(对 gallium 的偏离 D8)
// MG_Pipe/MGPipeCallbacks.h —— context_create 时安装;monolith 里是直调,split 里是记录
struct MGPipeCallbacks {
void (*on_gl_error) (Uint32 code);
void (*on_gpu_written) (MGPipeHandle res, Uint rangeCount, const MGPRange*);
void (*on_buffer_writeback) (MGPipeHandle res, Uint64 off, MGPBlobRef bytes);
void (*on_texture_writeback) (MGPipeHandle res, const MGPBox*, MGPBlobRef bytes);
void (*on_texture_pull_request) (MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount,
Uint64 pullSerial);
void (*on_mip_levels_generated) (MGPipeHandle res, Uint16 base, Uint16 count); // 只带形状,不带字节
void (*on_surface_changed) (const MGPSurfaceInfo*);
void (*on_caps_invalidated) ();
void (*on_log) (Uint8 level, const char* text);
void (*on_xfb_scatter_ready) (MGPipeHandle scratch, Uint64 packedStride, Uint64 vertices); // ★v2
};
配套的正向终止符(在 MGPipeContext 里,不在 callbacks 里,因为它是 client→server):
// ★v2:拉取请求的显式应答,可以携带零个 region
void (*resource_subdata_complete)(MGPipeHandle res, Uint16 target, Uint16 firstLevel,
Uint16 levelCount, Uint64 pullSerial);
gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求/终止、default-FB 几何这些词汇——因为在 Mesa 里 state tracker 与 driver 共享地址空间。把它们具名化为 10 个回调 + 1 个终止符,好过藏在 95 个 poke 点里。
6.2 95 个写回点的逐族归属
| 族 | n | 变成什么 |
|---|---|---|
SyncPersistentMappedRange |
20 | v2 修正:不是"全部消失",而是逐站点归属。 其中多数紧挨着一次对客户端字节的 CPU 读,而那些读搬到了 client(§4.8),由 tracker 在填 MGHostSpan 之前做同一次 reconcile(逐站点表见 §4.8.1)。但至少一处的消费者搬不走:UniformManager::ResolveUniformBufferPayload(UniformManager.cpp:2022 同步,:2052 读 MappedData()+rangeStart,:2053-2057 零填充)把具名 UBO 打进 Magma 自己的 UBO ring——由 D-B8 的 set_shader_buffers host payload 承载,client 在发射前做 reconcile。P1 的交付物包含这 20 处的逐站点归属表(哪些消失、哪些变 client 发射前 reconcile、哪些需要 host payload),不接受笼统结论 |
MarkStorageDirty |
18 | 16 处是 server 本地记账——零消息(dirty 归属反转,§6.3)。2 处 true(Managers.cpp:2813、DirectGLES.cpp:6852)变 on_texture_pull_request / on_texture_writeback |
AllocateStorage |
8 | 6 处是 backend 凭空造出来的前端对象(Magma 的占位纹理、SwapchainObject 的 default-FBO 占位,SwapchainObject.cpp:284, 305, 329)→ server 原生,永不上线;1 处是生成 mip 的 shadow(DirectGLES.cpp:6261)→ on_mip_levels_generated;1 处是 swapchain 尺寸变更 → on_surface_changed |
WritebackFromBackend |
8 | MGPReplySlot(回读)+ on_buffer_writeback(PBO 回读、XFB 捕获)。必须按操作级批处理:其中两处今天在循环里逐行写回(Utils.cpp:2342、DirectGLES.cpp:7633),绝不能变成"每扫描线一次 IPC" |
SetInternalFormat |
7 | 与 AllocateStorage 同批 |
SyncGpuWrites |
6 | 同 SyncPersistentMappedRange:逐站点,见 §4.8.1 |
MarkGpuWritten |
6 | client 在每个 draw/dispatch 发射点保守自建,镜像 DirectGLES.cpp:459-467, 509, 1809 与 UniformManager.cpp:1073, 1229、VulkanRenderer.cpp:11210 的输入。on_gpu_written{res, ranges[]} 是收窄通道 |
RecordError |
6 | on_gl_error,必须对命令流有序(§6.4) |
SetBackendResource |
4 | 删除。 server 拥有资源表;pooling / 延迟释放原样搬到 server |
EnsureGpuResidentStorage |
3 | server 本地决策 |
SetBackendHashMemo / SetBackendAuxMemo |
3 | 纯值 → server 侧 per-slot 字段 |
InvalidateCompileEnv |
2 | on_caps_invalidated,低频 |
SetBackendStateMemo |
1 | 直接删除,不翻译(D12) |
UpdateMipmapSubData / TruncateMipmapLevels / SetSamples |
3 | 全在 Magma 的占位纹理里 → server 原生 |
6 处 backend 反向进 MG_Impl: 四处 pDefaultFramebufferInfo 身份比较 → 保留 handle {0,1} + MGPFramebufferState::isDefault;SwapchainObject.cpp:276-330(backend 创建 default FBO 的三张 ITextureObject)→ on_surface_changed,client 自己合成对象——顺带删掉 monolith 里的一处分层倒置;VulkanRenderer.cpp:10700(CopyTextureImageToClientOrPBO_State)→ get_texture_image 返回 "该 level 无 GPU 背书,请从你自己的 shadow 回答"(:10691-10704 今天测的正是这个条件)。
6.2.1 v2 新增:XFB scatter 是对 client shadow 的 read-modify-write,必须搬到 client
v1 把 8 处 WritebackFromBackend 全部归给单向的 server→client 通道。ScatterCapturedRecords(DirectGLES.cpp:893-960)不是单向的:它在 :928 做
Memcpy(staged.data(), target.buffer->MappedData() + target.start, rangeBytes);
——从应用已有的字节起步,然后只把捕获到的 varying 补进去,"这样 gl_SkipComponents 要求的空洞保留应用原本放在那里的东西——这正是这个特性的全部意义"(:889-892 的注释;:880-883 点名 KHR-GL46.transform_feedback.capture_special_interleaved_test 是走到这条路径的用例)。server 没有 MappedData(),而 MGPipeCallbacks 里也没有反向的 buffer 读。照 v1 实施,要么空洞被清零(一致性破坏),要么需要一次 §12.2 没有列出的、发生在 glEndTransformFeedback 上的同步反向读。
修正(不新增停顿类):scatter 搬到 client。
- server 把驱动捕获到的紧密打包 scratch 字节通过
on_buffer_writeback(scratchHandle, 0, bytes)推给 client,并用on_xfb_scatter_ready(scratchHandle, packedStride, vertices)告知布局参数; - client 拥有目的 shadow,也从反射归档里拥有
GetTransformFeedbackVaryings()/GetTransformFeedbackStride()/GetTransformFeedbackPackedStride()(ProgramObject.h:1146-1171, 1357-1394),于是原样跑今天:930-939的补丁循环; - client 把补好的范围当作普通
resource_subdata重新发下去(复现今天:946-948的glBufferSubData回灌),并 bump 自己的 change serial(复现:942+BumpBufferMutationEpoch())。
副作用::906-914 的"CPU 模型给出 0 顶点 → 整批捕获丢弃"的诊断落到应用线程上,比落在 server 上更有用。计入 Espryt 子系统 7(§5.4)。
6.3 纹理 dirty 归属反转
client 保留 MipmapStorage 的模型(96-rect 级联合并 + summedArea*4 >= unionArea*3 union-box 回退,MipmapStorage.cpp:300-305),维护一份发射游标,在发射后清自己的标志。server 从不碰 client 的标志。
这是安全的,且已核实:MG_Impl 里没有任何 IsStorageDirty( / GetStorageDirtyRects( / GetStorageDirtyRegion( 调用点(前端从不读自己的 dirty 状态),而它自己在五处主动清(GL_Texture.cpp:528, 701, 5547, 5621, 5691)。这一条让"server 侧逐 level 权威位 + 纹理 ack 协议"整套机制不必存在。
v2 修正 1:发射游标必须按存储属主键控,不能按 (texture, uploadTarget, level)。
TextureObjectView 把 IsStorageDirty / MapMipmapData / MarkStorageDirty / MarkStorageDirtyRegion / GetStorageDirtyRegion 全部转发给存储属主的 mipmap 并做索引重映射(TextureObjectView.cpp:290-322;:281 直接写属主的数据)。一个 view 与它的属主共用同一份 dirty 状态却会各带一个游标:谁先发射谁就清掉了另一个还需要的标志,或者两边都发同一批纹素。
正确键:(storageOwnerHandle, ownerUploadTarget, ownerLevel)——查询与清除前先经 GetViewStorageOwner() 与 view 的 ToOwnerUploadTarget() / ToOwnerLevel() 映射。
门:新增场景,通过 view 上传、经属主采样(以及反向),跨 draw 边界各一次。
v2 修正 2:MOBILEGL_PIPE_VERIFY 需要一个"保留模式",否则它在最危险的子系统上是瞎的。
影子比对(§13.3-②)的参照物是"从头重算一次快照"。但发射后 client 已经把 dirty 标志清了,从头重算无法重建当时的 rect 集合——于是子系统 5(resource_subdata 的 payload)恰恰是 verify 看不见的那一块,而它同时是 §5.4 标注"全表最危险"、押着 +6ms/frame 悬崖与 7 条 repack 路径的那一块。
修正:MOBILEGL_PIPE_VERIFY=1 时 tracker 保留清除前的 dirty 集合到本次 draw 结束,G4 比对发射出去的 (unionBox, regionCount, regions[]) 与快照重算的结果。并且新增 TextureUploadShapeScenario:把逐纹理逐帧的上传形状(box vs N 个 region、作业数)录成金标,与 SSIM 并列比对——+6ms 悬崖由形状相等把关,不是由 SSIM 把关(SSIM 对它完全不敏感)。
上传形状决策留在 server:resource_subdata 同时带 union box 与 region 列表(§3.5.6),Mali 按作业数计价的悬崖在哪一侧付 GPU 代价,决策就留在哪一侧。
6.4 反向通道的有序性是正确性要求,不是优化
on_buffer_writeback 必须与 epoch bump 有序。 今天每一次 WritebackFromBackend 后面都紧跟一次 BumpBufferMutationEpoch()(DirectGLES.cpp:834-837, 942, 7625-7629),否则 server 自己的 draw-clean memo 会在 epoch 背后变陈旧。split 里这变成反向通道上的一条排序规则:一次写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 侧应用。反向通道需要与正向通道相同的有序保证。
on_gl_error 必须对命令流有序,否则 glGetError 答错。glGetError 本身永远本地(GL_Getter.cpp:2811-2817;不变式 Core.cpp:48-49)。
v2 修正:kNeedsAck 只标真正同步的分配点,不是"看起来像分配"的 GL 入口。
v1 把 "glRenderbufferStorage*、可能失败的 glTexImage*/glTexStorage*/glCopyTexImage* 形式、glBufferStorage" 全标成 kNeedsAck,让 OOM 探测惯用法(allocate; if (glGetError()==GL_OUT_OF_MEMORY) 用更小的重试;)成立。实测这批里纹理族根本不调 backend 表:MG_Impl/GLImpl/Texture/GL_Texture.cpp 在 :2515, 2671, 2755 只做 MarkStorageDirty(..., true),Espryt 在 sync 时刻才惰性分配;纹理侧的错误上报 RecordGLError(DirectGLES.cpp:6309-6324)只有一个调用者——glGenerateMipmap(:6916)。连唯一一处真正的同步分配 glRenderbufferStorage* 也是在 BackendRenderbufferObject::SyncToBackend(Managers.cpp:8674-8684)里惰性做的。
修正后的规则:
- 纹理分配的 OOM 在 monolith 里就已经推迟到 sync 时刻,拆分不改变任何可观察行为 —— 这批不标
kNeedsAck,并把这条事实写进文档(避免后人以为是遗漏)。 kNeedsAck只标两项:glBufferStorage(真同步)与glRenderbufferStorage*(若决定把它的分配提前到 GL 调用时刻以支持 OOM 探测;否则它也不标,同样写明)。这个"若"由 P0 回答:查 MC / Iris 语料里有没有真的glRenderbufferStorageOOM 探测惯用法;没有就不标,省掉整条 ack 路径。- 其余错误一律晚到,走有序的
on_gl_error。
对事件通道的强制条款:on_log 必须按严重级分级。 §8.4 的朴素策略把全部日志行设为有损(覆盖最旧 + eventDropped)。但 §4.7 已确认:backend program link/compile 失败只以一行日志加一次 bind-program-0 的空 draw 呈现。统一有损策略下,系统里诊断价值最高的那一行会在日志压力下静默消失。
规则:on_log(level ≤ WARN) 有损;on_log(level ≥ ERROR) 无损,加入触发 eventRingFull + 停止 apply 的语义事件集;再加一个每秒 ERROR 速率限制器,超限时发一条显式的 "N errors suppressed"。MGLOG_E_ONCE 的 latch 变成 per-server。P9 的故障注入门:日志洪泛下注入一次 link 失败,那行 ERROR 必须出现且两侧都恢复。
6.5 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6)
server 不保留纹素字节,三个原因会要求 client 重发已发过的 level:RequireImageBindableStorage 的 re-dirty(Managers.cpp:2813)、整格式再生(:3950-4195)、view 源重铸(:3616-3707)。四条缓解同时上(v1 是三条,v2 补第 (e) 条终止符),加一个专门的门和一个必须发布的计数器:
(a) 预防主因。 client 给纹理打 everImageBound 标记,resource_create/respecify 一直携带 imageBindableHint,于是 image-bindable 存储在前期就分配好。这把 RequireImageBindableStorage 从稳态里彻底移除。
(b) 拉取是异步的。 server 发 on_texture_pull_request{res, target, levels[], pullSerial} 并把那个 twin 标为 not-ready;client 在下一次 publish 时重发。因为 client 跑在前面,常见情况下字节在 server 到达采样该纹理的 draw 之前就到了;即使没到,阻塞的是 mgl-srv-apply 线程,不是应用线程。
(c) 有上限的保留(默认关闭)。 可选的逐纹理保留位,受一个显式的 LRU 字节预算约束(MOBILEGL_PIPE_TEXEL_RETAIN_MB,v2 把默认从 32 改为 0)。理由:MipmapStorage 保有每个 level 的完整 CPU 影子(MipmapStorage.h:117 的 Vector<Vector<Uint8>> m_data),所以一次拉取总是能从 client 已有的字节服务——保留缓存买的是延迟,不是正确性,而它花的是内存,恰好是 §7.11 里被逐项预算的那个指标。只有 (d) 的实测拉取率非平凡才开,并拿真预算。
(d) 门与计数器。 TextureRemintPullScenario:同时强制 RequireImageBindableStorage 与一次帧中格式再生。拉取次数逐 trace 用例发布,与 SSIM 并列。本设计从不声称"零 round trip",它测量并公布。
(e) v2 新增:显式终止符——因为存在"答不出来"的拉取。
RequireImageBindableStorage 的重放会 re-dirty 每个上传目标的每个 level(Managers.cpp:2789-2822),而它自己已经跳过 GetMipmapByteSize(...) == 0 的 level(:2810-2812)。但还有一类 level:内容只来自渲染、来自一次 CanMirrorCopyImageShadow 拒绝的 glCopyTexSubImage(DirectGLES.cpp:7068-7073)、或来自 GPU 侧 mip 生成——client 那里根本没有字节。没有终止符,apply 线程会 park 在一个永远不会 ready 的 twin 上。B-R4 与 TextureRemintPullScenario 只针对拉取的频率,从来没针对无解的拉取。
修正:
- 拉取是 request/response 对,由
resource_subdata_complete(res, target, firstLevel, levelCount, pullSerial)终止,它可以携带零个 region; - 收到零 region 的应答时,server 带着"已分配但为空"的存储继续(这正是 monolith 的行为:
EnsureGenerateMipmapStorageAllocated(DirectGLES.cpp:6270-6271)也是AllocateStorage+MarkStorageDirty(false),不填内容),并记一条MGLOG_W; TextureRemintPullScenario必须包含这个无解用例(一张只被渲染过、随后被 image-bind 的纹理),且它必须在终止符落地之前是红的(表现为 apply 线程挂死或超时)。
若在真实语料(MC 与 Iris fixture)上实测拉取率非平凡,(c) 从可选升级为强制并拿到真预算。
7. 传输与数据面
本章与状态模型无关:它规定字节怎么过去、什么时候可以被覆盖、背压怎么升级。§8 规定控制面与同步,§9-§11 规定帧节奏、线程与平台。
7.1 段(segment)布局
| 段 | 拥有者 | 默认大小 | 内容 |
|---|---|---|---|
SEG_CMD |
client(server 只读) | 8 MiB,2 的幂,64B 对齐 | RingControl(4KiB) + POD 记录 + ≤4KiB 内联负载 |
SEG_STAGE |
client(server 只读) | 32 MiB → 上限由实测定,不是默认 256 MiB | bulk 字节:buffer sub-data、纹理区域、UBO scratch、client 顶点/索引/indirect 数组、persistent-map 脏块 |
SEG_REPLY |
server(client 只读) | 8 MiB,4KiB slot | readback 像素、buffer writeback |
SEG_EVENT |
server(client 只读) | 256 KiB SPSC ring | EvQueryResult/EvGpuWritten/EvGlError/EvLogLine/EvSurfaceChanged… |
SEG_SHADOW[n] |
client(server 只读) | 每对象,P4.5 起,≥256KiB shadow | 零拷贝 buffer/texture shadow |
SEG_ADOPT[n] |
server(client RW) | 每 buffer,P11,≥16MiB adopted store | 应用直写 GPU 内存 |
创建:Android ASharedMemory_create(API 26,android/sharedmem.h:78;libc 的 memfd_create wrapper 是 API 30,sys/mman.h:196);桌面 Linux syscall(SYS_memfd_create, …);macOS shm_open+shm_unlink;Windows CreateFileMappingW(Local\)。
传递:POSIX SCM_RIGHTS,在第一个 transport commit 里实现(asio 无 cmsg API → 在 socket.native_handle() 上裸 sendmsg/recvmsg,约 80 行)。Feat/CS-Delta-IPC 把它推迟到"P6"(LocalSocketTransport.h:16-20,PollOffer 里 out->fd = -1 硬编码于 :296),结果它的数据面在唯一重要的平台上一个字节都过不去。这条是 P0 的第一优先级。
SEG_SHADOW 块的退休规则:§7.4 的 64KiB 块发送水位只解决"覆盖一个活着的 shadow";它没说怎么释放一个 shadow。glDeleteBuffers 或 glBufferData 重定义会释放/重分配 SEG_SHADOW 的 arena 块,而携带 {segId, offset, size} 指向该块的记录可能还没被 apply——server 于是读到另一个对象的字节。规则:释放的块进入 pending 链表,只有当 appliedSeq(对被借入 GPU 时间线的 slot 是 retiredSeq)越过最后一条引用它的记录之后才归还 arena,而不是在对象析构时立即归还。
7.1.1 SEG_STAGE 必须额外容纳的六类字节(v2 清单)
MGPipe 让 SEG_STAGE 承载了它在纯 delta 模型下不承载的字节,定尺时必须算进去:
- client 顶点数组(
(first+count-1)*stride + elementSize/ 属性 / draw); - client 索引数组(
count * indexSize); - multi-draw 参数块(
first[]/count[]/indices[][]/basevertex[],drawcount*4级); - client 解析后的
*IndirectCount命令块(几十字节); - 具名 UBO 的 host payload(D-B8,
kCapNeedsHostUboBytes下逐 draw 逐块,计数器stage-ubo-named); - 纹理 subdata 的紧密重打包区域(§3.5.6;今天走 unpack ring 时也已经紧密重打包,所以字节量同阶,但现在过 ring slot)。
不在此列(D-B7 解决):restart 重写的整 EBO(kMaxRestartRewriteBytes = 1<<26 = 64 MiB,是默认 SEG_STAGE 的两倍)与 multi-draw 展平的索引流(kMaxFlattenedIndices = 1<<24)——它们由 server 侧的索引宿主镜像喂养,不过 SEG_STAGE(§7.10)。
上限由 P0 落地的计数器实测定,不用默认值猜。并且 G3 必须为"单条记录大于段容量"定义明确的分块/降级路径(大 subdata 分块成多条,而不是一条巨记录)。
7.2 RingControl:watermark 是一条共享 cache line,且带双向 doorbell
// MobileGL/MG_Remote/Transport/Ring.h
struct alignas(4096) RingControl {
// ---- SEG_CMD 游标 ----
alignas(64) std::atomic<Uint64> cmdHead; // producer:累计写入字节
alignas(64) std::atomic<Uint64> cmdAppliedTail; // consumer:已解码并拷出的字节
std::atomic<Uint64> cmdRetiredTail; // consumer:被借入 GPU 时间线的 slot 已释放
// ---- SEG_STAGE 游标(独立三元组)----
alignas(64) std::atomic<Uint64> stageHead;
alignas(64) std::atomic<Uint64> stageAppliedTail;
std::atomic<Uint64> stageRetiredTail;
// ---- 序号 / 帧水位 ----
alignas(64) std::atomic<Uint64> appliedSeq; // 已 apply 的记录序号
std::atomic<Uint64> submittedSeq; // 已提交给驱动
std::atomic<Uint64> retiredSeq; // GPU 已完成
std::atomic<Uint64> completedFrameSerial;
std::atomic<Uint64> presentAckSerial;
// ---- doorbell / 代 ----
alignas(64) std::atomic<Uint32> serverEpoch; // context 丢失 / server 重启时 ++
std::atomic<Uint32> ringGeneration; // 硬 drain 后 ++,作废缓存 offset
std::atomic<Uint32> consumerParked; // server 睡了,producer 要敲门
std::atomic<Uint32> producerParked; // client 睡了,server 要敲门
std::atomic<Uint32> eventRingFull; // SEG_EVENT 满,server 已停止 apply
std::atomic<Uint32> eventDropped; // 被丢弃的有损日志行计数
};
三个 seq 水位严格区分(混为一谈是经典错误):appliedSeq 释放 cmdAppliedTail/stageAppliedTail;submittedSeq 释放 staging;retiredSeq/completedFrameSerial 释放 *RetiredTail 与 SEG_ADOPT 复用。
两个 tail 是必须的:Ops_ResidentSubData 把字节拷进 pendingResidentWrites(Managers.cpp:1158-1166),P11 之后 server 会借用 ring slot 而不是再拷一次——那种 slot 只能在 completedFrameSerial 之后回收。单 tail 会在那一天变成保守回收。
SEG_STAGE 必须有自己的游标三元组:§8.2 把"SEG_STAGE 余量 < 1/4"列为 Publish 触发器,而第二个 ring 的占用率无法从第一个 ring 的游标算出;且 stage slot 的退休条件(retiredSeq)与 cmd 记录(appliedSeq)不同。
7.2a 双向 doorbell
- client → server:consumer 自旋 ~200µs → 置
consumerParked=1→ 在控制 socket 上阻塞读 1 字节;producer 在 release-storecmdHead之后,仅当consumerParked时写 1 字节(字节码0x01 = 'ring advanced')。 - server → client:client 在任何等待里(present credit、
kNeedsAck阻塞请求、ring/stage 满的升级等待)先自旋MOBILEGL_IPC_SPIN_US(默认 50µs),再置producerParked=1,然后在同一个 socket 的反向流上阻塞读;server 在 release-store 任何 watermark 之后,仅当producerParked时写 1 字节(字节码0x02 = 'watermark advanced')。
没有这一条,每一处 client 等待都退化成跨进程自旋一条共享 cache line:present-credit 等待最长一整帧(60Hz 下 16.6ms),在手机上就是一颗大核满频空转,与 GPU 和游戏 JVM 抢核;§7.5 的"有界 50ms 等待"就是 50ms 自旋。而 MobileGL 全库没有任何亲和性控制(grep -rn 'sched_setaffinity\|cpu_set_t' MobileGL/ 零命中),无法把它赶到小核上。
spawn 模式用 socketpair 的两个方向做 doorbell;inproc 模式用一对 std::condition_variable(同一套 producerParked/consumerParked 语义)。零 futex/eventfd/named-event 平台代码(asio 已 vendored,3rdparty/asio/include 已在主 target 的 include path 上,CMakeLists.txt:483)。
7.3 记录格式
// MobileGL/MG_Remote/Protocol/RecordKinds.h
struct RecHeader { Uint16 kind; Uint16 flags; Uint32 size; }; // 8 B,size 含 header,8 字节倍数
enum RecFlags : Uint16 { kNone=0, kNeedsAck=1<<0, kHasBlob=1<<1, kPad=1<<2, kBorrowSlot=1<<3, kVarTail=1<<4 };
struct BlobRef { Uint32 seg; Uint32 pad; Uint64 offset; Uint64 size; }; // 24 B
没有 per-record 序号字段:seq 就是记录序数(producer m_emitSeq++,consumer m_applySeq++),省 8B/记录并消除一整类失步。
单一真相源是 PipeCalls.def,生成器是 G3(§3.1)。它对每一个 MGPipe 调用生成三样东西:
// 1) 一条尺寸断言(每种记录一条,不是只对 union 首成员)
static_assert(sizeof(MobileGL::Wire::RecDrawVbo) == 56, "DrawVbo record size drift");
// 2) applier 分发前的运行期边界检查
case RecKind::DrawVbo:
if (h.size < 56 || h.size > remainingRingBytes || (h.size & 7u))
return Fatal(FatalCode::ProtocolCorruption, "DrawVbo");
break;
// 3) applier switch 的一个分支:解码 → 更新对象表 → 调 backend 函数指针
每种一条 static_assert ——修掉正是 Feat/CS-Delta-IPC 中过一次的 bug 类(b50f3348:"旧的 off-by-one 让 applier 误读 TexImage 之后的每一条 state delta"),而它那条只断言 union 首成员的 assert(ServerCore.cpp:31-33)永远抓不到中间插入。
运行期边界纪律:SEG_CMD 是对端并发写入的区域,编译期 static_assert 管不到运行期损坏。kVarTail 记录额外校验 定长前缀 + 尾巴自描述长度 == h.size;kHasBlob 记录额外校验 BlobRef 落在它声明的段内。违反一律 Fatal{ProtocolCorruption},绝不进入未定义行为。
变长记录(set_sampler_views 的 view 数组、resource_subdata 的 rect 列表、draw_vbo 的 MGPDrawRange[] 与 MGHostSpan、set_shader_buffers 的 range 数组):kVarTail + 定长前缀 + 自描述长度的内联尾巴。
7.4 WAR 危害与字节稳定性
Phase 1 规则(P5-P8):GL 调用时刻把字节拷进 ring slot。 slot 从写入到 stageAppliedTail 越过它为止不可变,client 拿不回它 → 危害按构造消除。代价是一次 memcpy,而 Ops_ResidentSubData(Managers.cpp:1165)和 StageBlocksIntoUnpackRing 在 monolith 里已经在付同样的钱。
Phase 2 规则(shadow-in-shm,零拷贝): ≥256KiB 的 shadow 分配在 client 拥有的 SEG_SHADOW 里——PipeResource 的 MapAlignedAllocator(PipeResource.h:33-60,无状态、25 行、64B 对齐)增加一个 shm arena(保留 MIN_MAP_BUFFER_ALIGNMENT=64 契约,PipeResource.h:28),MipmapStorage 的 level vector 同理。resource_subdata 于是只带 {segId, offset, size},client 侧零拷贝。
WAR 用 per-shadow 64KiB 块发送水位:若应用写入某块而该块最后一次发送尚未 appliedSeq 覆盖,这次写走 SEG_STAGE。有界、局部、压力下自动退化成 Phase-1 行为。这套块水位同时是 §7.8.1 精确版 persistent-map 推送的脏位来源。
该改动必须整段 #if MOBILEGL_BUILD_DISAGGREGATED 包裹:PipeResource 与 MipmapStorage 住在 MG_State,不在 MG_Remote,而改一个容器的 allocator 就改了类型;不包裹的话 §13.5 的编译期折叠保证不成立。写法是"分配器特化:option OFF 时逐字折叠成今天的 MapAlignedAllocator"。
拷贝账(MC pan 一帧约 9MB section mesh + ~1MB UBO scratch)
- monolith 的
glBufferSubData→ shadow store 是 2 次:(1) app→shadow(BufferObject::UploadSubData的Memcpy),(2) shadow→目的地(FlushPendingRangesNow:Memcpy(dst, bufferObject.MappedData()+start, size)进 invalidating map,Managers.cpp:914;或Memcpy(g_uploadRing.store.mappedPtr+ringOffset, ..., size)进 upload ring,Managers.cpp:922)。 - split Phase 1 是 3 次:app→client shadow (1)、client shadow→
SEG_STAGE(2)、server 的FlushPendingRangesNow⇒SEG_STAGE→upload ring (3)。 - Phase 2(shadow-in-shm)去掉 (2),剩 2 次——与 monolith 持平。
这是 MGPipe 的一个结构性收益:server 没有第二份 BufferObject/PipeResource,所以不存在"staging → server 侧 shadow"这次中间拷贝,也不需要为它设计一种只读采纳模式或 copy-on-write 升级。
| 路径 | monolith | split 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 推送(§7.8.1 保守版) | 0 | 1/发射点 | 1/发射点(精确块) |
glTexSubImage |
2 | 2 | 2 |
| 全局 UBO / draw | 1 | 2 | 1 |
| adopted ≥16MiB(P11 T1/T0) | 0 | 0 | 0 |
TracyPlot 字节计数器必须装在 wire 两侧(client 的 emit 字节 + server 的 apply 字节 + server 的 ring/staging 字节),验收看总量,不是只看 client 一侧的数字。
7.5 Ring 分配与背压
逐字移植 PersistentRing(Managers.cpp:657-727、RingAllocateSlow :1891-1970、RingOnPresent :1975-2016):单调 head/tail、2 的幂掩码、frame mark。分配失败升级:扩容(翻倍) → 对最老未 retire 批次有界等待(默认 50ms,走 §7.2a 的 producerParked doorbell,不是自旋) → 硬 Drain 请求 + ringGeneration bump。generation bump 上线,防止后续记录引用被回收的 offset。
硬 drain 之后的恢复很便宜,因为 MGPipe 的正向流是自洽的推送流:client 的 tracker 把全部 dirty 位置为"必须重推",下一个 verb 就会重新发出完整的 set_* 集合;纹理侧由 §6.3 的发射游标负责(游标未被清的 rect 仍在 client 手上)。没有"重发未 apply 的对象状态"这类特殊协议。
SEG_CMD 与 SEG_STAGE 各自独立跑这套升级(各有自己的游标三元组)。
7.6 纹理
- Unpack PBO 完全在 client 解析(
GL_Texture.cpp:1719,1765,1887,1976,2457,2604,2722,4458,6176读pixelUnpackBufferObject->MappedData() + (SizeT)pixels,再由ProcessTexturePixelsDataUnpack紧密重排)。没有任何纹理像素以 PBO 引用形式过线,server 永远不需要GL_PIXEL_UNPACK_BUFFER状态。set_pixel_pack_state只用于 PACK 方向(§3.6 D5)。 - 压缩纹理永不到达任何 backend(前端在
glTexImage时把压缩 internalformat 解析成非压缩后备,GL_Texture.cpp:298-306;grep -i compress MG_Backend/DirectGLES/*.cpp只命中一条注释)。逐字节m_compressedDatablob 仅供glGetCompressedTexImage,纯 client 侧,不过线。 glCopyTexSubImage*与glClearTexImage整体留在 client。 这两个入口今天就是纯前端操作:CopyTexSubImage{1,2,3}D_State(GL_Texture.cpp:3955,3979)调CopyReadFramebufferIntoMipmapRegion(:1044-1097),它借一次 backendReadPixels进 CPU scratch(:1079)、逐行 memcpy 进 mipmap shadow(:1089-1094)、MarkStorageDirty(...,true)(:1095)。拆分后它恰好是一次阻塞 ReadPixels round trip,产生的脏区按普通resource_subdata下发——正确,且不需要任何新命令。glClearTexImage(GL_Texture.cpp:985-1006)同形。- 逐 level "server 权威" 位不存在。 dirty 归属反转(§6.3)让 client 始终是纹素的权威;backend 真正在 shadow 里写字节的两处(CPU 生成 mip 路径
DirectGLES.cpp:6811-6861、glCopyImageSubData的目的地镜像:7144)分别由on_texture_writeback与"CopyImage 镜像搬到 client"处理,server 需要重读纹素时走on_texture_pull_request+resource_subdata_complete(§6.5)。
7.7 回读
| 路径 | monolith | 拆分后 |
|---|---|---|
glReadPixels → 客户内存 |
阻塞 | 一次 round trip,像素放 SEG_REPLY slot;逐行写回循环留在 server 内,按操作级批成一段 |
glReadPixels → pack PBO |
也阻塞(DirectGLES.cpp:9189-9205 把整个 PBO map 回来写 shadow) |
fire-and-forget + client 侧对该 PBO 置 MarkGpuWritten,代价推迟到之后的 map/read。严格优于 monolith |
glGetTexImage/glGetTextureImage |
DirectGLES 从 client shadow 回答 | DirectGLES 零 round trip(GPU 生成的 level 也是——monolith 那里同样是"已分配但未填充",§12.1);DirectVulkan 一次(get_texture_image 对"无 GPU 背书"的 level 回答"请用你自己的 shadow",VulkanRenderer.cpp:10691-10704) |
glGetBufferSubData / glMapBuffer(READ) on gpuWritePending |
阻塞(glFinish(),Managers.cpp:1246) |
一次,由 client 侧保守 pending 集合触发,被 on_gpu_written{ranges} 收窄 |
| XFB capture writeback | glEndTransformFeedback 里无条件无限 ClientWaitSync(GL_Drawing.cpp:1326-1337) |
不等,client 对 capture target 置 MarkGpuWritten,首次读时付;scatter 由 §6.2.1 的 client 侧路径完成 |
glCopyTexSubImage* |
内含一次同步 ReadPixels | 一次 round trip(保持前端实现不变,§7.6) |
7.8 persistent map 与 ≥16MiB 采纳
三档,由运行时 POST 探针选择(遵循本项目"后端限制一律探针判定、绝不硬编码驱动名"的既定规则):
- T2 — 拒绝(IPC 期默认,永久正确回退):
AcquirePersistentMap返回nullptr,前端已在三处容忍(BufferObject.cpp:174, 439-442, 470-472)。此档下 §7.8.1 的 client 侧推送是强制的,否则应用的 coherent persistent 写会丢。 - T1 — server 导出自己的映射(P11 主攻):server 照常铸造 coherent map(
Managers.cpp:988-1058/VkBufferManager.cpp:515-563),经VK_KHR_external_memory_fd/AHardwareBuffer_sendHandleToUnixSocket(API 26,hardware_buffer.h:521)/VK_KHR_external_memory_win32/GL_EXT_memory_object_fd导出,clientmmap后调PipeResource::AdoptPersistentMap(base)。每次存储定义(respecify)一次 round trip(v2 修正 v1 的"每 store 生命周期一次"——TryAdoptLargeStorage在存储定义时触发,一个反复扩容的 arena 付 N 次)。StorageBufferRegrowScenario必须发布map-persistent-roundtrips。采纳成功后 §7.8.1 的推送对该 buffer 自动停止(SyncPersistentMappedRange的IsGpuResident()早退),与 monolith 一致。 - T0 — server 导入 client 分配:client 分配
AHardwareBuffer/dma-buf,server 以GL_EXT_external_buffer+glBufferStorageExternalEXT或VK_EXT_external_memory_host导入。理想但可用性未知。
决策路径:P0 的 spike B 在第一周给方向(导出 HOST_VISIBLE|HOST_COHERENT VkBuffer 的 fd,client mmap 后回读,在两台设备上各跑一次)。若两台都否,P11 从 8 天缩为 2 天的文档与负面对照。绝不允许一个平台未知数挡住 267 天的接口工作(D-B4)。
7.8.1 client 侧的 persistent map 推送
问题(已在仓库确认):BufferObject::SyncPersistentMappedRange()(BufferObject.cpp:238-250)依次早退于 GPU-resident、非 Persistent、非 Write、FlushExplicit、空 range,剩下的情况(persistent + write + coherent + shadow-backed)走 NotifySubData(整个 mapped range)。它的全部生产调用点都在 MG_Backend/ 里(20 处)。T2 档下 AcquireMemoryRange(BufferObject.cpp:459-475)回退到 shadow 并把 m_resource.Bytes() + range.start 交给应用——应用之后不再调任何 GL 函数就直接写。拆分后没人推,字节丢失。
另外 IsBufferDrawClean 里 if (frontend->IsMapped()) return false;(Managers.cpp:1447,注释:"A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push")也依赖 map 位。
解法三件套(第 1 条按 MGPipe 收缩,第 2、3 条逐字保留):
- 不需要把 map/unmap 做成一对上线的命令。 server 没有第二份
BufferObject,它唯一需要知道的是"这个资源现在有没有活的宿主写入者"——因为那正是IsBufferDrawClean那一行要表达的东西。所以resource_respecify/resource_subdata的 payload 里带一个推送的hasLiveHostWrites位(由 client 在 map/unmap 时更新),server 的 draw-clean 判定读它。零新增记录种类。 - client 侧脏块推送。 tracker 维护
m_livePersistentMaps(只装 persistent+write+非-FlushExplicit+非-GpuResident 的 buffer,进出由 map/unmap 入口维护)。在每个 validate 点,对本次操作可达的每个这类 buffer(VAO attribute buffer、index buffer、indirect/parameter buffer、UBO/SSBO/atomic binding point、XFB capture target——即 backend 那 20 个SyncPersistentMappedRange调用点的并集)做块粒度发送:把 mapped span 切成 64KiB 块,只发自上次发送以来被改过的块。 "被改过"的判定:Phase 1 用保守版(每个发射点把该 buffer 的整个 mapped span 当脏,但按块拆成多条resource_subdata,让 §7.5 的 range 合并与 ring 复用机制生效);Phase 2 shadow-in-shm 落地后升级为精确版(shadow 住在 client 拥有的SEG_SHADOW里,用与 WAR 水位同一套 64KiB 块脏位跟踪;块脏位由memcmp或 mprotect 写屏障提供——先做memcmp,它对 1MB 块是 ~50µs 量级,且只在真正 mapped 的 buffer 上跑)。 保守版在持久映射的 chunk arena 上代价可观(每个可达发射点重传整个 mapped span)。所以MOBILEGL_IPC_PERSISTENT_BLOCK_KB(默认 64)可调,且 P5 验收必须记录这条路径的字节量(Tracy 计数器persistent-map-push)。若保守版在 Create/Flywheel fixture 上不可接受,把精确版提前——这是计划里唯一一个允许因测量结果而改变阶段顺序的地方。 - 门从第一天就有:
PersistentCoherentMapScenario(map PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验),列为 P5 验收项。今天计划里没有任何其它门能抓到这个 bug。
与 MOBILEGL_COHERENT_AS_FLUSH 的关系:该开关(GL_Buffer.cpp:297-305,默认 false,Config.h:174 / ConfigLoader.cpp:185)把应用请求的 persistent+FLUSH_EXPLICIT 改写成 coherent,从而制造上面这个情形。有了三件套,"我们自己改写出来的 coherent map"与"应用自己请求的 coherent map"走同一条正确路径,所以该开关在拆分模式下照常生效——这样 tools/trace_replay/trace_cases.json 里那两个带 coherent_as_flush: true 的用例(minecraft-1.21.1-neoforge-create-indirect-in-world、minecraft-1.21.1-neoforge-create-instancing-in-world)在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义。若实测保守推送在这两个 fixture 上代价过高,改为"这两个用例在 split 模式下同时关掉该开关,并在报告里标注",而不是让两侧走不同路径还宣称对比通过。
7.9 应用指针(四类,范围全部可算)
| 类 | 范围 | 站点 |
|---|---|---|
| client 顶点数组(仅 DrawArrays 族) | (first+count-1)*stride + elementSize |
Managers.cpp:2560、VulkanRenderer.cpp:3737 |
| client 索引数组 | count * indexSize |
DirectGLES.cpp:4436、VulkanRenderer.cpp:4081 |
| client indirect / parameter 块 | stride*(drawcount-1)+cmdSize |
DirectGLES.cpp:276、DirectVulkan.cpp:303 |
MultiDraw* 参数数组、ClearBuffer* value |
drawcount*4、16B |
DirectVulkan.cpp:963-1057 |
唯一无界的是索引 draw 下的 client 顶点数组:索引扫描(TryComputeMaxIndexFromHostBytes,VulkanRenderer.cpp:3406-3470)必须在 client 侧跑,只有 client 同时持有两个数组。
这四类的归属、门控与陈旧索引纪律全部由 §4.8 与 §4.8.1 规定(MGHostSpan 的四行消费者表在 §3.5.7):字节永远走 SEG_STAGE,指针永不过线;minIndex/maxIndex 是 flag 门控的 MGPDrawInfo 字段;reconcile 是逐站点表而不是一条笼统规则(*IndirectCount 明确不加 SyncGpuWrites())。实现落在 MG_Impl/Pipe/HostResolve.cpp,两个 backend 共用。
draw_vbo 的 kIndicesAreClient 标志由"是否绑定了 element array buffer"决定(DirectGLES.cpp:4423 vs :4425-4442),在 binding 所在的一侧判定。
7.10 server 侧索引宿主镜像(D-B7)
MG_Remote/Server/IndexHostMirror.{h,cpp}:
- 覆盖范围:
MGPResourceDesc::bindMask & ELEMENT_ARRAY的资源,且仅当kCapNeedsHostIndexBytes为真(即 split 且 server 侧确实需要索引字节做 restart 重写 / multi-draw 展平)。 - 维护方式:由 server 本来就要收的
resource_create/resource_respecify/resource_subdata流增量维护。零额外线上流量、零 round trip。 - 可见性:GPU 写者对镜像的影响由
on_gpu_written的收窄集在 server 侧本地判定(server 知道自己提交了什么),不需要问 client。 - 预算:
MOBILEGL_PIPE_INDEX_MIRROR_MB(默认 64),逐帧发布index-mirror-bytes。超预算时该 buffer 退化为逐 draw 通过MGHostSpan传送(seg指向SEG_STAGE而不是kFromServerIndexMirror),并计入index-bytes-shipped。 - 为什么必须是它:
kMaxRestartRewriteBytes = 1<<26(64 MiB,DirectGLES.cpp:4218)是默认SEG_STAGE的两倍,kMaxFlattenedIndices = 1<<24(MultiDraw.cpp:72)同量级;把这些字节逐 draw 塞进 32 MiB 的段既不可行也无必要。
7.11 内存预算
| 项 | 字节 | 说明 |
|---|---|---|
| 传输段 | 48.25 MiB | SEG_CMD 8 + SEG_STAGE 32 + SEG_REPLY 8 + SEG_EVENT 0.25 |
SEG_STAGE 额外余量 |
+0~32 MiB | §7.1.1 的六类新字节实测后定;上限由 P0 计数器给 |
server 侧索引宿主镜像(仅 split,仅 kCapNeedsHostIndexBytes) |
0~64 MiB(默认上限) | §7.10;只镜像曾被绑为 ELEMENT_ARRAY 的 buffer,由 subdata 流增量维护,零额外线上流量 |
| 纹素保留 LRU | 默认 0 | MOBILEGL_PIPE_TEXEL_RETAIN_MB 默认 0;只有实测拉取率非平凡才开(§6.5c) |
| POD slot 记录 + CSO 缓存 | ~1-2 MiB | server 侧对象表是数组,不是对象图 |
| 典型(不开索引镜像) | ≈ +50-60 MiB | |
| 最坏(镜像满 + stage 余量满) | ≈ +145 MiB |
诚实注记:索引宿主镜像是本设计里唯一的"数据副本",它是把 restart 重写与 multi-draw 分档留在 server(D-B7)所付的价钱。它只覆盖索引缓冲、有显式预算与计数器、且超预算时有回退路径(逐 draw 通过 MGHostSpan 发送,代价记账)。server 不持有任何 buffer 的完整副本、不持有任何纹素、不持有前端对象图——这是"server 拥有自己的状态机"在内存上的直接后果。P5 验收要求记录两个角色的峰值 RSS,作为这张表的实测基线。
8. 控制面与同步
8.1 FlatBuffers 用法
一份 schema MobileGL/MG_Remote/Protocol/protocol.fbs,两种用法:
- 热路径 → FlatBuffers
struct(flatc 保证定长布局、无 vtable、无偏移间接、无需 verifier walk,只需边界检查),直接放进 ring:[RecHeader | struct | 可选变长尾]。draw_vbo的固定头是 8+48 = 56B(对比 table-per-command 的 ~90B 与一次 vtable 遍历)。这正是Feat/CS-Delta-IPC自己的 plan 第 55 行要求而实现没做的事。 - 罕见/变长/需演进 → FlatBuffers
table,走 CTRL socket。
namespace MobileGL.Wire;
// ---------- 热路径 struct(进 ring;与 MGPipeTypes.h 的 POD 一一对应)----------
struct PipeHandle { slot:uint; gen:uint; }
struct BlobRef { seg:uint; pad:uint; offset:ulong; size:ulong; }
struct HostSpan { ptr:ulong; size:ulong; seg:uint; pad:uint; offset:ulong; }
struct RecBindRenderState { cso:PipeHandle; version:ushort; pipelineVersion:ushort; }
struct RecSetDynamicState { chunkMask:uint; version:ushort; pad:ushort; blob:BlobRef; }
struct RecSetIndexBuffer { res:PipeHandle; offset:ulong; indexSize:uint; restartIndex:uint; }
struct RecResourceSubData { res:PipeHandle; target:ushort; level:ushort; flags:uint;
box:[uint:6]; regionCount:uint; pad:uint; blob:BlobRef; } // regions 在变长尾
struct RecDrawVbo { mode:uint; indexSize:ubyte; flags:ubyte; pad:ushort;
instanceCount:uint; startInstance:uint; restartIndex:uint;
indexResource:PipeHandle; minIndex:uint; maxIndex:uint;
xfbCaptured:ulong; } // ranges/HostSpan 在变长尾
struct RecPresent { frameSerial:ulong; swapInterval:int; pad:uint; }
struct RecRenderbufferStorage { res:PipeHandle; internalFormat:uint; width:int; height:int;
samples:int; pad:uint; }
// … 共约 74 项,与 PipeCalls.def 逐条对应 …
// ---------- 控制面 table(走 socket)----------
table SegmentRef { id:uint; kind:ubyte; sizeBytes:ulong; name:string; }
table Hello { abiMajor:uint; abiMinor:uint; buildFingerprint:string; backendType:uint;
pid:uint; configBlob:[ubyte]; }
table Welcome { abiMajor:uint; abiMinor:uint; serverPid:uint;
cmdRing:SegmentRef; stageRing:SegmentRef; replyPool:SegmentRef; eventRing:SegmentRef; }
table CapsSnapshot { dynamicParameters:[ubyte]; // DynamicBackendParameters 逐字节
rendererInfo:[ubyte]; formatCaps:[ubyte]; extensions:[string];
apiVersion:string;
maxComputeWorkGroupCount:[int:3]; maxComputeWorkGroupSize:[int:3];
callMask:ulong; // 远端实际填了 MGPipe 的哪些槽
capBits:ulong; } // kCapNeedsHostIndexBytes 等
table SurfaceInfo { width:int; height:int; colorFormat:uint; depthFormat:uint; stencilFormat:uint; }
table SurfaceOp { seq:ulong; kind:ubyte; display:ulong; surface:ulong; windowKind:ubyte;
nativeToken:ulong; width:int; height:int; swapInterval:int; }
table SurfaceReply { seq:ulong; ok:bool; eglMajor:int; eglMinor:int; info:SurfaceInfo; }
table ResyncRequest { serverEpoch:uint; } table ResyncDone {}
table AuxRequest { seq:ulong; kind:ubyte; payload:[ubyte]; } // 外来线程 sync/query
table Fatal { code:uint; message:string; }
table LogLine { level:ubyte; text:string; }
union CtrlMsg { Hello, Welcome, CapsSnapshot, SurfaceOp, SurfaceReply,
ResyncRequest, ResyncDone, AuxRequest, Fatal, LogLine }
table CtrlEnvelope { msg:CtrlMsg; }
root_type CtrlEnvelope;
两份定义不可能漂移:G3 为每条记录生成 static_assert(sizeof(MobileGL::Wire::Rec*) == sizeof(MGP*)) 与逐成员 offsetof 断言,把 fbs struct 与 MGPipeTypes.h 的 POD 钉在一起(§7.3)。
protocol_generated.h 提交进仓库,由 scripts/gen_protocol.py 重新生成(镜像 tools/trace_replay/CMakeLists.txt:52-69 驱动 glproc.py 的做法);CI 加 flatc-check 步骤重新生成并 git diff --exit-code。
codegen 绝不进默认构建图:Feat/CS-Delta-IPC:MobileGL/Protocol/CMakeLists.txt:22-38 在 MOBILEGL_FLATC_EXECUTABLE 未设时 add_subdirectory(3rdparty/flatbuffers) 并开 FLATBUFFERS_BUILD_FLATC ON——这正是它自称要修的 NDK 陷阱(交叉编译造出 arm64 flatc 然后在 host 上执行)。本计划不复用这一段:gen_protocol.py 是纯开发者/CI 目标,默认构建图里没有 flatc,MOBILEGL_FLATC_EXECUTABLE 只服务 CI 的 flatc-check。FlatBuffers 运行时是 header-only,只需要 3rdparty/flatbuffers/include 在 include path 上(用 nm 复核 libMobileGL.so 链接行没有新增库,不靠断言)。
8.2 帧封装与 publish 策略
CTRL socket 封帧:[u32 'MGLF'][u32 len][payload],64MiB 上限,读时校验(Feat/CS-Delta-IPC 的 Feed() 永远返回 OK,坏 magic 变成静默永久挂起,Framing.h:41-45;StartRead 直接按 wire 长度分配无上限检查,LocalSocketTransport.cpp:232-236)。接收缓冲不足时返回所需大小并保留消息(那份 transport 会失败且不弹出消息,把流永久卡死)。
Publish 触发器
不设"records ≥ 64KiB"这类阈值。 按 §7.3 的记录尺寸,64KiB ≈ 1200-2700 条记录,即一整帧(MC 帧是 1000-4000 draw)。那意味着 server 在 client 发完整帧之前无法开始工作——这不是异步,是一个整帧的流水线气泡,且在 present credit 之上再加一整帧延迟;它还会在 inproc 跑之前就先把 inproc 的假设否掉(inproc 的全部意义就是让 apply 与 GL 线程重叠,帧粒度 publish 保证零重叠)。而 SEG_CMD 是 SPSC ring,"publish" 只是一次 cmdHead 的 release store,唯一值得摊销的是门铃写。
规则:
- 每条记录(或每 8-16 条,用来摊销 store)release-store
cmdHead;仅当consumerParked时敲门铃。 - 显式门铃点:
present、任何kNeedsAck阻塞请求、eglMakeCurrent、glFlush(刷出 outbox,不等待)。 SEG_STAGE余量 < 1/4 时敲门铃(用stageHead - stageAppliedTail)。- 轮询类入口点也是门铃点(修 livelock):
glClientWaitSync(任意 timeout)、glGetSynciv(GL_SYNC_STATUS)、glGetQueryObject*(GL_QUERY_RESULT_AVAILABLE | GL_QUERY_RESULT_NO_WAIT)。 理由:GL 的标准惯用法是glFenceSync(); while (glClientWaitSync(s, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {}与while (!avail) glGetQueryObjectuiv(id, GL_QUERY_RESULT_AVAILABLE, &avail);。循环里没有别的 GL 调用,若这些入口不 publish,fence_create就永远躺在 ring 里,server 看不到,watermark 不动,循环永久自旋——这是挂死,不是变慢。仓库自己在意这件事:DirectVulkan.cpp:1158-1160写明 "GL_SYNC_FLUSH_COMMANDS_BIT: flush regardless of timeout, so a zero-timeout poll loop makes progress across calls",而 MG_Impl 无条件把 flags 透传给 backend(GL_Sync.cpp:96)。 携带GL_SYNC_FLUSH_COMMANDS_BIT的调用无条件 publish(spec 要求 flush)。 - 饥饿升级:同一个 handle 连续 N 次(默认 64,
MOBILEGL_IPC_POLL_ESCALATE)本地回答TIMEOUT_EXPIRED/ "未就绪" 而 watermark 毫无移动时,升级成一次阻塞 round trip,这样一个已经卡住的 server 不会把 client 自旋成死循环。
glFinish/glFlush 保持纯 no-op(Definitions.cpp:111-112)——应用唯一的强制停顿手段在 monolith 里免费,拆分后也必须免费。
8.3 序号与 credit
seq = 记录序数。两个互相独立的窗口,绝不是 per-batch 锁步(Feat/CS-Delta-IPC 在 apply 循环里同步发 ack,ServerCore.cpp:421-429,是最差的节奏;而且它的 credit 算成 baseSeq + items.size(),只有 baseSeq==0 时才对):
- 字节 credit:
SEG_CMD与SEG_STAGE各自的占用,升级路径见 §7.5。 - Present credit:
eglSwapBuffers在presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT(默认 1,见 §9.1)时阻塞。
server 端不发 credit 消息:它对 RingControl 做 release store,consumer 每 64 条记录更新一次 appliedSeq,并在 producerParked 时敲反向门铃。
8.4 事件回传通道
SEG_EVENT 是 server→client 的 SPSC POD ring,承载 §6.1 的十个回调加回读完成通知:EvQueryResult{handle, available, value}、EvFenceSignaled{handle}、EvGpuWritten{handle, rangeCount, ranges[]}、EvBufferWriteback{handle, offset, BlobRef}、EvTextureWriteback{handle, box, BlobRef}、EvTexturePullRequest{handle, target, firstLevel, levelCount, pullSerial}、EvMipLevelsGenerated{handle, base, count}、EvXfbScatterReady{handle, packedStride, vertices}、EvReadbackDone{seq, BlobRef}、EvGlError{code}、EvSurfaceChanged、EvCapsInvalidated、EvLogLine{level,len,text}。
排空点
client 在下列位置排空:glGetError、glGetQueryObject*、glClientWaitSync、glGetSynciv、eglSwapBuffers、glMapBuffer / glMapBufferRange / glGetBufferSubData / glGetNamedBufferSubData / glCopyBufferSubData,以及每一次等待循环的每一轮(present credit、kNeedsAck、ring/stage 满)。最后一条是必须的,见下。
溢出策略(修一个双向死锁)
具体死锁:client 卡在 eglSwapBuffers 等 present credit;server 的 apply 线程一边 apply 一边产 EvLogLine 与 EvGpuWritten;SEG_EVENT 满;apply 线程阻塞在生产上;presentAckSerial 永不前进;client 永不离开 eglSwapBuffers,因而永不排空。两边都死。
策略:
- client 必须在每个等待循环内排空
SEG_EVENT,不只是在入口点边界。 EvLogLine按严重级分级(§6.4 的强制条款):level ≤ WARN是有损的——覆盖最旧,并累加RingControl.eventDropped(client 在排空时把丢失条数打进日志);丢一条 INFO/WARN 绝不允许卡住渲染。- 语义承载事件无损:
EvGpuWritten、EvReadbackDone、EvFenceSignaled、EvBufferWriteback、EvTextureWriteback、EvTexturePullRequest、EvMipLevelsGenerated、EvXfbScatterReady、EvGlError、EvSurfaceChanged、EvCapsInvalidated,以及EvLogLine{level ≥ ERROR}(因为 backend program link 失败只以一行 ERROR 日志呈现,§4.7)。ring 装不下时 server 置RingControl.eventRingFull=1并停止 apply(停在一条记录的边界上,不是记录中间),敲反向门铃;client 排空后清标志并敲正向门铃。状态因此永远可恢复。 - ERROR 速率限制器:每秒上限,超限时发一条显式的 "N errors suppressed",避免无损化把 ring 变成死锁源(B-R13)。
MGLOG_E_ONCE的 latch 变成 per-server。 - 故障注入测试:在 client 被 credit 阻塞时灌满
SEG_EVENT;以及日志洪泛下注入一次 backend link 失败,那行 ERROR 必须出现且两侧都恢复。
server 侧的 MGLOG 与延迟诊断按流顺序 replay 进 client 日志流——复用已存在的 DeferredLogLine/ApplyDeferredDiagnostics 机制(JobNode.h:26-58,149-158)。
8.5 fence 完成度必须来自真的逐 fence 退休,不是 present 水位
一个诱人的简化是让 retiredSeq/completedFrameSerial 兜底 fence 语义。不行。 在 DirectGLES 上这两个水位只在 Present() 里前进(DirectGLES.cpp:10626-10643 在 eglSwapBuffers 之后轮询 4 深 fence ring),或在 WaitForFrameSerialCompleted(:10583-10607)里。帧中创建的 fence 于是要等到下一次 present 退休才报 signalled,即 fence 完成度退化成帧计数推断。DirectVulkan.cpp:1120-1128 恰恰写明这是被修掉的 bug:完成度必须"track the GPU itself rather than the frame-count inference; MC 1.21.5's fence-paced ring buffers depend on this to recycle their space instead of growing without bound",而项目记忆 magma-mc1215-fence-oom 记录了它曾导致 native-heap OOM kill。
规则:fence_create 在 server 侧转成一次真实的 backend FenceSync();server 用自己已有的逐 fence 轮询(DirectGLES 有 WaitForFrameSerialCompleted 的 fence 选择逻辑 :10586-10600 可复用;DirectVulkan 有 IsSubmitIndexComplete)在非 present 时刻也推进,并发 EvFenceSignaled{handle}。client 的本地快路径读的是"由真实逐 fence 退休导出的 handle 水位",不是 present 水位。
8.6 三个应先独立落到 dev 的 monolith 修复(可二分、monolith 自身受益)
glEndTransformFeedback的无条件无限ClientWaitSync(GL_Drawing.cpp:1326-1337)→ 用既有MarkGpuWritten/SyncGpuWrites推迟到首次读。glDispatchCompute的三次GetIntegeri_v校验查询(GL_Drawing.cpp:719)→ 改读CompileEnv::maxComputeWorkGroupCount(CompileEnv.h:52-54)。- 删除
GetInteger64i_v/GetProgramiv两个死表项及两个 backend 的实现。
(另有两项在 §13.4-5 列出:D21 的 XFB 计数槽重键与 RenderbufferObject::GetLifetimeId(),同样先独立落 dev。)
9. Present 与帧节奏
eglSwapBuffers → EGLImpl::SwapBuffers(EGLImpl.cpp:162-183)→ BackendObject::SwapEGLBuffers(BackendObject.cpp:369-398,其线程归属校验全部对 client 镜像的 EGL 状态求值,不需要回复)→ 发 present{frameSerial}(swap interval 搭在同一条记录上)→ publish + 敲门铃 → 返回,除非 presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT。
present 与应用的 eglSwapBuffers 严格 1:1,绝不批量。 Magma 侧四次 OnFrameBoundary() 缓存老化、TryDrainFrameTransients 和全部四次 BeginFrame 只在 Present 内发生(VulkanRenderer.cpp:12765-12904);Espryt 侧三个 ring 与 TrimBufferPool 在那里 retire(DirectGLES.cpp:10646-10649)。批量会饿死这些排空。
9.1 延迟是叠加的:credit 默认为 1
一个"credit=2 镜像系统已有预算、因此不引入新的停顿类别"的论证是错的:停顿类别确实不新,但延迟会叠加:
- server 自己的
Present在返回之前就已经等了 2-3 帧:VulkanRenderer::Present末尾调FrameContext::WaitAndAcquireNextImage,其第一条语句是vkWaitForFences(device, 1, &frame.imageInFlightFence, VK_TRUE, timeout)(FrameContext.cpp:288-290)。presentAckSerial因此只能在那次等待完成后才前进。 - 一个被允许领先 2 个 present 的 client,叠在一个自身已领先 GPU 2-3 帧的 server 上 = 端到端 4-5 帧,60Hz 下 66-83ms,对第一人称游戏不可接受。
- 现有的验收门都看不见它:SSIM 是帧内容比较,
bench.sh量的是 FPS,都不是 input-to-photon。
规则:MOBILEGL_IPC_PRESENT_CREDIT 默认 1(可配 1-4)。文档里写明叠加公式:端到端 ≈ client credit + server FIF + 驱动深度。P10 与 P12 的验收增加输入延迟测量:用已有的 GetGpuTimestampNs 与 trace-replay --benchmark 的逐帧 JSON 构建 "记录发射时刻 → present 完成时刻" 直方图;只有当实测吞吐收益能抵掉实测延迟代价时才调高 credit。
参考基线:MagmaFramesInFlight = 3 钳到 [2, maxImageCount](VulkanRendererConfig.h:14-19、VulkanRenderer.cpp:3051-3058),Espryt 深度 4 的 fence ring 刻意高于驱动的 2-3(DirectGLES.cpp:10071-10074)。
9.2 swap interval 与 Magma
Swap interval 搭 present 记录过去。注意 Magma 从不注册 SetSwapInterval(BackendObject_DirectVulkan.cpp:698 只注册 Present,所以 set_swap_interval 在 Magma 上是 null 项)且偏好 MAILBOX/IMMEDIATE(SwapchainObject.h:74-79),因此 IPC credit 成为 Magma 唯一的显式限帧器 —— 记录在案,P10/P12 在设备上测量输入延迟与帧节奏;若 Magma 需要,把"注册 SetSwapInterval 并映射到 FIFO"作为独立的 dev 变更,不让两套机制同时管节奏。
9.3 无 present 循环下的水位饥饿
retiredTail 的回收依赖 server 发布准确的 completedFrameSerial。DirectVulkan 有 TryDrainFrameTransients/RefreshCompletedSubmits 可以在非 present 时刻推进,DirectGLES 没有对应物:g_completedFrameSerial 只在 Present() 里(DirectGLES.cpp:10626-10643)和 WaitForFrameSerialCompleted(:10583-10607,且要求存在覆盖目标 serial 的活 fence,slot 被回收时返回 false)前进。在无 present 的负载里——tools/cts 的 run_cts_local.py、回读循环、从不 swap 的 MG_IntegrationTest 场景——一个 fence 都不会被插入,retiredTail 永不前进,SEG_STAGE 填满,§7.5 的升级路径在每个用例上都跑到硬 drain。那会把一次 CTS run 变成一连串 50ms 等待加整体 drain,并可能被误读成一致性回归。
规则:给 DirectGLES 的 server 加非 present fence tick——距上次 Present 超过阈值(默认 8ms)或每 N 条已 apply 记录(默认 4096)时,插入一个 glFenceSync 并轮询 fence ring,复用 g_frameFenceRing 机制。同时把 ring 占用率与升级次数打进 Tracy 计数器(P0 交付),让"水位饿死"表现为一个指标而不是一次无法解释的停顿。P8 增加一个无 present 的 split 用例。
10. 线程模型
Client
- v1 不加线程。 编码在调用方 GL 线程上直接写进 ring。前端本来就是 per-context 单线程契约(
GLContext无 mutex;EGLState::MakeCurrent强制一个 owner 线程,EGLState/Core.cpp:1215-1220,测试在MG_Test/EGLState/EGLStateTest.cpp:39-92)。 - flow = per context,不是 per thread。 今天恰好一个 flow。
eglMakeCurrent是 flow 所有权转移,在既有EGLOperationMutex(EGLImpl.cpp:241)下发射。顺手修既有漏洞:EGLImpl::ReleaseThread(:341-350)与SwapInterval(:435-450)今天不取该锁而另外三个(MakeCurrent/SwapBuffers/DestroySurface)取。 - 外来线程的 sync/query:读全部从
RingControl无锁 acquire load 回答(比取 registry mutex 更好);少数必须发射的(fence_create、query_begin,以及 §8.2 要求的轮询 publish)取ctrlMutex并走 CTRL socket 的 out-of-bandAuxRequest帧(SPSC ring 不允许第二个 producer)。 - 等待必须能挂起:所有 client 侧等待(present credit、
kNeedsAck、ring/stage 满、轮询升级)走 §7.2a 的producerParked+ 反向门铃,自旋窗口MOBILEGL_IPC_SPIN_US(默认 50µs)。 - ShaderCompilePool 原样保留在 client(
ShaderCompilePool.h:77-82,≤4 worker,为 RSS 上限)。glslang 全在 client,create_shader_state从编译池的终止 continuation 发出(§4.3)。 - 可选
mgl-client-tx双缓冲发送线程:凭测量决定。在 Tracy 数据出来之前不要预先加线程(会引入拷贝或锁)。
Server
| 线程 | 职责 |
|---|---|
mgl-srv-io |
asio io_context::run:封帧读写、SCM_RIGHTS、双向 doorbell、CTRL RPC |
mgl-srv-apply |
终身持有原生 EGL/Vulkan context:消费 ring → 解码 → 更新 MGPipe 对象表与 PipeInputs → 调 backend 函数表 |
mgl-srv-dec(可选) |
边界校验/解码前置,凭测量决定 |
因为 context 永不迁移:g_backendContextOwnerThread(DirectGLES.cpp:10052)只写一次;DirectGLES::MakeCurrent 的 8 缓存失效风暴(:10123-10140)变成启动期一次性成本;IsBackendContextCurrentOnThisThread 的每帧 EGL 复核(:10195-10228,动机是 eglGetCurrentContext 实测占渲染线程 16%)恒真。DirectGLES 的 off-thread 降级(FenceSync 返回 null 等)消失——保真度提升。延迟 replay 机制(Managers.h:458-473 的 pendingRespecify/pendingRanges/pendingResidentWrites)保留但永不触发。
核心放置(是性能主张的前提)
§13.2 说明推送模型把可达性遍历搬走而不是翻倍:client 的 tracker 做 O(1) 快门加未命中时的 touched 前缀走查,server 做解码加 backend 调用。但那仍然是 CPU 工作,只是换了线程,而且 client 侧新增了 payload 构造与集合 hash。所以拆分的全部性能主张都押在"两半落在两个都快的核上"。
而 MobileGL 全库从不设置亲和性(grep -rn 'sched_setaffinity\|cpu_set_t\|affinity' MobileGL/ 零命中),server 是 fork/exec 出来的独立进程、不继承 launcher 的亲和性,项目记忆 pojav-bigcore-affinity-trap 又记录过 pojavBigCore=true 把整个游戏 JVM 加 MobileGL worker 钉死单核、让一整批历史测量作废。若 mgl-srv-apply 落到 1.55GHz 小核,它做的工作严格多于 monolith 在 1.96GHz 大核上做的,拆分按构造就是回归,而"帧时在 monolith 10% 内"会以一个没人会正确归因的理由失败。
规则:
- 计划里必须写出总 CPU 工作量差(client tracker + encode + decode + server apply vs monolith 的
PrepareForDraw),不只是单侧成本。 - 复用
ShaderCompilePool已有的大核探测(ShaderCompilePool.cpp:73-96的ReadCpuMaxFrequencyKHz/DetectBigCoreCount)把mgl-srv-apply绑到大核,开关MOBILEGL_IPC_SERVER_AFFINITY(默认 auto),并把解析出的 mask 打进日志。 - 每个阶段都必须报逐线程 CPU 时间,不只是墙钟帧时,这样"没有收益"的结论能被归因到放置 vs 编码成本。
拆机顺序(三条约束)
publish + server 排空并 ack → 停 apply 线程 → 关 transport →(client)排空 compile pool(必须先于 glslang::FinalizeProcess() 与 pGLContext 析构,ShaderCompilePool.h:106-110、Init.cpp:56-62)→ MobileGL::Destroy()(EGLImpl.cpp:335-338)→ 释放 sync/query handle(GL_Sync.cpp:223-226)。
11. EGL/窗口与进程生命周期
11.1 启动与握手
client 定位 server 的顺序:
MOBILEGL_IPC_SERVER_PATH(主要机制)。dladdr(&MobileGL::Initialize)→ dirname →libMobileGLServer.so(兜底)。
把 dladdr 当主要机制会让两个桌面验收门都找不到 server:MG_IntegrationTest/CMakeLists.txt:28-35 在非 Android 上把 MGL_ITEST_MOBILEGL_TARGET 设成 MobileGL_s(静态链接),dladdr 解析到测试可执行文件自身的路径而不是库目录;trace replay 则由 tools/trace_replay/CMakeLists.txt:285-290 显式传 -DMOBILEGL_LIBRARY=$<TARGET_FILE:MobileGL>,其目录是 MobileGL 的构建输出目录,而 CMake 默认把 add_executable 放在定义它的目录的 binary dir。
配套:把 MobileGLServer 的 RUNTIME_OUTPUT_DIRECTORY 设成 $<TARGET_FILE_DIR:MobileGL>,并把 "MOBILEGL_IPC_SERVER_PATH=$<TARGET_FILE:MobileGLServer>" 加进每一条新的 ctest ENVIRONMENT(经 mgl_itest_join_environment 与 ${MGL_ITEST_COMMON_ENV} 合并)以及 add_trace_replay_test 的 SPLIT 分支。并复核绝对路径能否活过 CI 的 artifact 搬运:.github/workflows/test.yml:174-185 只重写 CTestTestfile.cmake 里的 cmake 路径,不重写 ENVIRONMENT 值——若不行,改为在测试启动时由 harness 相对 argv[0] 解析。
启动方式:socketpair(AF_UNIX, SOCK_STREAM) + fork/execve,fd 3 = socket(Windows 见 §11.5)。无文件系统 socket 路径、无 abstract namespace、Android 上无 SELinux 争议。
子进程必须被强制成 monolith(修无界 fork 链):MG_Config::Transport 由 ConfigLoader 从环境变量读(与 features.CoherentAsFlush = QueryEnvFlag(...)(ConfigLoader.cpp:185)同形),而 fork/execve 的子进程会继承 MOBILEGL_TRANSPORT=spawn。server stub 里 dlopen(libMobileGL.so) + dlsym("mobilegl_server_main") 之后必然要起一个真 backend,即走 MG_Backend::Init()(Init.cpp:48-70)——变量还在,于是它再构造一个 BackendObject_Remote 并再 spawn 一次,首次 GL 调用时形成无界 fork 链。
规则:(a) spawn 时构造显式 envp,剔除 MOBILEGL_TRANSPORT 与所有 MOBILEGL_IPC_*(只保留 server 真正需要的少数几个,如 MOBILEGL_BACKEND_TYPE、日志路径);(b) mobilegl_server_main 在能到达 MG_Backend::Init() 之前把 MG_Config::Transport 硬置为 Monolith。两条都做,任一条单独失效时另一条兜住。P0 增加一个 MG_Test/Wire 测试:spawn 一个 server 并断言进程树只多出恰好一个子进程。
Hello{abiVersion, backendType, buildFingerprint, configBlob} → Welcome。configBlob 转发 client 解析好的 MG_Config::Features,两半不可能对某个 quirk 开关有分歧。buildFingerprint(git hash + PipeCalls.def 的 hash)不匹配 → 握手期 Fatal。
11.2 mobilegl_server_main 的可见性
CMakeLists.txt:497-510 在非 Debug 构建上给共享目标设 C_VISIBILITY_PRESET hidden / CXX_VISIBILITY_PRESET hidden / VISIBILITY_INLINES_HIDDEN ON——而 plugin 与 FCL 出货的正是 RelWithDebInfo(MobileGL/build.gradle 的 fordebug 类型强制 -DCMAKE_BUILD_TYPE=RelWithDebInfo)。所以 dlsym("mobilegl_server_main") 在 Debug 下能用、在设备上静默失败。
规则:入口点声明为
extern "C" __attribute__((visibility("default"))) int mobilegl_server_main(int argc, char** argv);
并在 P0 验收里加 nm -D libMobileGL.so | grep mobilegl_server_main 断言(与既有的 nm --defined-only 门并列)。若哪天 macOS/Windows 也要托管 server,还需同步 MG_Impl/DyldInterpose/ExportedSymbols.txt 与 wgl.def。
11.3 Android
minSdk 26 没有任何公开 NDK API 能扁平化 ANativeWindow(NDK r27.3 的 android/native_window.h 无 parcel 符号;libbinder_ndk 是 API 29,binder_ibinder.h:191;ASurfaceControl 是 API 29,surface_control.h:67)。Feat/CS-Delta-IPC 的 nativeBlob "binder-flattened ANativeWindow"(protocol.fbs:377-379)不可实现。
- P5-P11 验证路径:无窗口。 两个 PIE ELF。实测:从解压出的 nativeLibraryDir exec 在 API 36 上可行(
run-as … libtrace_replay_runner.so→ exit 132 = SIGILL,即 ELF 已被加载进入,而非EACCES;文件 0755 /u:object_r:apk_data_file:s0且无 MLS category,跨 package 也可)。useLegacyPackaging = true在 FCL(../FCL/build.gradle.kts:76-82)与 plugin(android-plugin/app/build.gradle.kts:198-203)都已开。surface 用 pbuffer 或AImageReader支持的ANativeWindow(HeadlessGL.cpp:86-131,268-274),trace replay 默认 pbuffer(apitrace_glws_egl.cpp:614-618)。 注意实测的域:上述 SIGILL 证据是经run-as取得的,即runas_app域,而不是 trace Activity 所在的untrusted_app域。P0 的 Android spike 必须从应用自身进程posix_spawn一次(见 §14 P0)。 - P12 生产路径:Java
Surface(Parcelable)→ Messenger/AIDL →MobileGLServerService(android:process=":mgl")→ JNIANativeWindow_fromSurface(env, surface),就是 FCLauncher 今天在egl_bridge.c:81做的那一次调用。仓内先例:android-plugin的BenchService已在android:process=":bench"里跑 MobileGL(BenchService.java:19-77)。代价:server 进程多一个 ART(~15-25MB)。 - 纠正一条过期笔记:FCL 把游戏 JVM 跑在主进程,不是
:jvm(../FCL/src/main/AndroidManifest.xml:112-121,JVMActivity没有android:process;:jvm是下载 Service)。第二个进程必须新建。 - HeadlessGL 的 fork 预检与孤儿 server:
MG_IntegrationTest/Harness/HeadlessGL.cpp:344-368会 fork 一个子进程跑完整 EGL bring-up 然后_exit(step),注释(:364-366)明说这是刻意的——"every atexit handler and static destructor in this address space belongs to the parent's copy of the world"。拆分模式下那个子进程的 bring-up 会走到MG_Backend::Init()并 spawn 一个 server;_exit不跑任何拆机,那个 server 成为孤儿,活到它发现 EOF 或撞上MOBILEGL_IPC_IDLE_EXIT_S(默认 30s)。父进程随即对同一设备起自己的 server。HeadlessGL.cpp:585-589已经把这种失败模式命名为"a leaked exclusive device, an environment the child did not have"。 规则:server 的 EOF 检测必须即时且无条件退出(亚秒级,不靠 30s 看门狗);client spawn 时把 socket fd 设成_exit会确定性关闭的形态(不设FD_CLOEXEC以外的保活);再加一次有界重试的就绪握手,这样残留的预检 server 不会把父进程弄 flaky。这个交互本身列为 P6 验收步骤的一部分,先于任何广度工作。
11.4 Linux / X11
Window 是 XID,nativeToken:u64 直接送。backend 自己 XOpenDisplay(getenv("DISPLAY")) 并构造 VkXlibSurfaceCreateInfoKHR(VulkanRenderer.cpp:14486-14521),只要同 DISPLAY/XAUTHORITY 就免费。Wayland 今天不支持(BackendObject.h:529 TODO),维持。
WSL/CI:永不开窗 —— EGL_PLATFORM=surfaceless + EnsureHeadlessPlatform()(HeadlessGL.cpp:160-196,它存在正是因为一台带 WSLg DISPLAY 的工作站曾把这条 lane 弄挂)。
11.5 Windows
HWND 进 nativeToken。Vulkan 可行(hinstance 是历史遗留,VulkanRenderer.cpp:14456-14463);WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only。
transport:默认 named pipe(asio windows::stream_handle)。"继承句柄就免掉 accept/connect"这句在 asio 上不能直接照搬:windows::stream_handle 的 IOCP 服务要求句柄是 overlapped 的,而 CreatePipe 造的匿名管道不是。所以句柄对必须这样造:用一个 GUID 唯一命名的 CreateNamedPipeW(..., FILE_FLAG_OVERLAPPED) 做 server 端,配一次 CreateFileW(..., FILE_FLAG_OVERLAPPED) 做 client 端,然后把 server 端句柄设为可继承并 CreateProcess 传下去。
asio 1.38.2 在 Win32 上确实定义了 ASIO_HAS_LOCAL_SOCKETS(3rdparty/asio/asio/include/asio/detail/config.hpp:1085-1092,只排除 ASIO_WINDOWS_RUNTIME,且自带 sockaddr_un_type 于 socket_types.hpp:220),但其 IOCP async_accept 走 AcceptEx,AF_UNIX 从不支持它——AF_UNIX-everywhere 是一个可选简化,需真编真跑验证,named pipe 是已知可用的默认。
11.6 崩溃
- server 死:client 读到 EOF/EPIPE → device-lost 闩锁:后续 GL 调用变 no-op、
eglSwapBuffers返回EGL_FALSE+EGL_CONTEXT_LOST、glGetGraphicsResetStatus(若 robustness 分支落地)返回GL_UNKNOWN_CONTEXT_RESET。MOBILEGL_IPC_RESPAWN=1时重启并让 tracker 把全部 dirty 位置为"必须重推"、对每个活的 handle 重发resource_create/respecify与全部 CSO(默认关,静默重启会掩盖 bug;且与MOBILEGL_IPC_ADOPT_TIER != 2互斥,因为被采纳的 store 是 server 拥有的内存,见 §7.8)。 - client 死:server 读到 EOF → 立即销毁原生 context 并退出(不等看门狗);
MOBILEGL_IPC_IDLE_EXIT_S(默认 30)只作为 EOF 都收不到时的最后保险。
12. Roundtrip 清单与稳态零 roundtrip 论证
12.1 稳态零 roundtrip 的项
| 类 | roundtrip | 依据 |
|---|---|---|
全部 draw、clear、blit、copy、dispatch、barrier、XFB 跨度标记、全部 bind、全部 CSO create/bind、全部 set_*、全部 buffer/texture 上传、present |
0 | 单向记录;present 只查 credit |
| 全部 89 个 caps 站点 | 0 | 首次 MakeEGLCurrent 的一次 MGPCaps 快照(BackendObject.cpp:341-347,每次 surface 变更重新武装 :301);callMask 精确复现 DirectVulkan 少注册的槽位 |
glGetError / glFinish / glFlush |
0 | 前者永远本地(GL_Getter.cpp:2811-2817;不变式 Core.cpp:48-49),后两者是彻底的 no-op(Definitions.cpp:111-112)且必须继续免费 |
| fence 与 query 的创建,以及每一次非阻塞轮询 | 0 | handle 由 client 铸造;未命中合法地答 GL_UNSIGNALED/"未就绪"(BackendObject.h:210-214、:236-241;前端已遵守,GL_Query.cpp:302-311) |
glGetTexImage / glGetTextureImage(DirectGLES),包括 GPU 生成的 mip level |
0 | client shadow 回答(CopyTextureImageToClientOrPBO_State,GL_Texture.cpp:5368-5420,取用点 :6460)。v2 显式决定:on_mip_levels_generated 只带形状不带字节,因为 monolith 也是如此——EnsureGenerateMipmapStorageAllocated(DirectGLES.cpp:6243-6274)对每个新 level 做 AllocateStorage(...) + MarkStorageDirty(..., false),内容留空。split 因此与 monolith 行为一致:GPU 生成的 level 在两种模式下都返回已分配但未填充的影子。只有 CPU 回退生成路径(RGB16F/RGB32F,:6811-6861)产生真纹素,由 on_texture_writeback 回来 |
glReadPixels → pack PBO |
0 | fire-and-forget + client 侧 MarkGpuWritten。严格优于 monolith(DirectGLES.cpp:9189-9205 无条件停等) |
glEndTransformFeedback |
0 | 取消无限 fence 等待(GL_Drawing.cpp:1326-1337),改为对 capture target 置 MarkGpuWritten;scatter 由 §6.2.1 的 client 侧路径完成 |
eglSwapBuffers |
0 次阻塞 round trip,一次非阻塞 credit 检查 | 只有 presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT(默认 1)时才阻塞 |
glMultiDrawElementsIndirectCount / glMultiDrawArraysIndirectCount |
0 | client 从自己的 shadow 解析计数,只做 SyncPersistentMappedRange()——与 monolith 完全相同的 reconcile 集合(§4.8.1)。P8 验收要求 create-indirect fixture 上该计数器读零 |
| primitive-restart 重写 / multi-draw 展平 | 0 | server 从索引宿主镜像读(D-B7、§7.10) |
12.2 不可避免的阻塞点(全部罕见,逐条给理由与缓解)
| # | 站点 | 为什么不可避免 | 缓解 |
|---|---|---|---|
| 1 | 握手 Hello/Welcome + 段 fd 传递 |
— | 一次 |
| 2 | InitializeEGLDisplay、Create/Resize EGL*Surface、首次 MakeEGLCurrent + InitCapabilities |
出参 / 返回 Bool;caps 只在那一刻存在 |
每 surface 至多一次;surface 回复顺带 SurfaceInfo。SwapEGLBuffers 不需要回复(BackendObject.cpp:365-393 对 client 镜像的 EGL 状态求值) |
| 3 | glReadPixels → 客户内存 |
GL 要求返回时字节已就位 | 像素进 SEG_REPLY slot;逐行写回循环留在 server 内,按操作级批成一段 |
| 4 | glGetTexImage/glGetTextureImage(DirectVulkan) |
Magma 对只存在于 GPU 的 level 没有 client 可答的 shadow | get_texture_image 对"无 GPU 背书"的 level 返回"请从你的 shadow 回答"(VulkanRenderer.cpp:10691-10704) |
| 5 | GPU-write pending 的 buffer 首次 CPU 读 | shader 在前端背后写了 store | monolith 里本来就阻塞(Managers.cpp:1246 的 glFinish();VkBufferManager.cpp:80-85 → VulkanRenderer.cpp:9807-9817)。client 保守 pending 集触发,由 writableMask 与 on_gpu_written{ranges} 两侧收窄 |
| 6 | glClientWaitSync(timeout>0)、glGetQueryObject*(GL_QUERY_RESULT) 未完成、glBeginConditionalRender |
GL 定义即阻塞;glBeginConditionalRender 连 _NO_WAIT 模式也阻塞(GL_Query.cpp:705-706) |
非阻塞兄弟是 0 round trip。条件渲染谓词只解析一次(Core.h:387-391),之后每个条件 draw 在 client 侧丢弃,server 永远不需要那个 query 对象 |
| 7 | 分配类入口的 ack | OOM 探测惯用法 | v2 收窄:只有 glBufferStorage(真同步)与——若 P0 证实语料里确有 glRenderbufferStorage OOM 探测——glRenderbufferStorage*。纹理族在 monolith 里就已经推迟到 sync 时刻,不标 kNeedsAck(§6.4) |
| 8 | map_persistent(仅 T1 档) |
应用必须拿到一个不再经过任何 API 调用就能写的地址 | 每次存储定义一次(v2 修正),不是每 store 生命周期一次;StorageBufferRegrowScenario 发布计数 |
| 9 | server 发起的纹理重铸拉取 | server 不保留纹素 | 四条缓解 + 终止符 + 专门的门 + 逐用例发布的计数器(§6.5)。异步形态下阻塞的是 mgl-srv-apply 而非应用线程;零 region 的应答让 server 带着空存储继续,永不永久 park |
| 10 | client 侧索引扫描,当源 EBO 在 pending 集里 | monolith 在同一位置调 SyncGpuWrites()(VulkanRenderer.cpp:3431) |
§4.8.1 的逐站点表;*IndirectCount 不在此列(它今天不调 SyncGpuWrites()) |
| 11 | ring/stage 耗尽、present credit | 节奏,非语义 | PersistentRing 的升级路径 + producerParked doorbell(§7.5、§7.2a) |
12.3 论证的形式:测量,不是声称
验收门措辞:在全部 40 个 trace 用例上发布逐用例的 roundtrip 计数器、纹理拉取计数器、索引镜像字节数与 index-bytes-shipped。不做笼统的"零 round trip"声明。 条件渲染与阻塞 query 的次数按用例列出。
轮询挂死的防护(§8.2 的轮询门铃点与饥饿升级)必须有它自己的门:glFenceSync(); while (glClientWaitSync(s, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {} 必须在有界时间内退出。
13. Monolith 保留、模式选择与构建布局
13.1 接口在进程内就是直调
monolith 模式下 MGPipeContext 用 backend 自己的函数填充,MGPipeCallbacks 用对 MG_State 的直调填充,MGHostSpan.ptr 指向 client 自己的 shadow(零新增拷贝),MGPipeHandle 按值走一对寄存器。split 模式下同一张表换成发射器,applier 反序列化后调同一批 backend 函数。全世界只有一份 backend 实现。
13.2 热路径的间接成本,动态口径的诚实版(v2 重写)
v1 这张表把今天的每 draw 状态获取写成 "Espryt 124 / Magma 169 次 accessor 调用"。那是静态调用点数(§2.1(d) 的定义),不是动态每 draw 调用数——树里每一处都已被 memo 门控(§2.3.1 逐条列了早退位置)。按动态口径重写:
| 今天(动态稳态) | 之后(动态稳态) | |
|---|---|---|
| 每 verb 的分发 | 1 次间接调用 + 3 个寄存器实参(DrawArrays) |
1 次间接调用 + ~48 B 固定头(MGPDrawInfo)+ 按 flag 的变长尾。这是一项新增成本,不是持平 |
| 每 draw 的状态获取(值类) | Espryt:1 次 Uint16 比较(DirectGLES.cpp:2016-2018)早退;未命中时 1.2KB×3 段 memcmp。Magma:1 次版本比较(:4982)+ 1 次版本比较(:5888);pipeline memo 未命中时 ~40 次 accessor 走查(:5155-5200) |
1 次 Uint16 比较;pipeline 版本动了才算 ~25-30 字的子集哈希 + 1 次 map 探测(D-B1);动态子集动了才发 ~200 B |
| 每 draw 的状态获取(对象类) | Espryt:SyncNeccessaryTextures 6 值键 + PairingsIntact + 每条目 IsDrawSyncClean;CurrentUnitBindingsEpoch 三值快门。Magma:TrySetupDrawFastPath ~10 次 accessor + ~20 次字比较 + 两次有损版本求和(:6249-6250) |
5 个聚合世代各 1 次 Uint64 比较(推论 4);命中才走 touched 前缀 + 集合 hash;hash 未变不发(§4.4-4) |
| memo 查表 | 对指针位做斐波那契散列的直接映射探测 + owner 相等性(3 次/draw) | 按 slot 的数组下标 |
| 真删除的机制 | — | ~372 行 per-draw 失效发现(§2.5) |
| 搬到 client 的机制 | — | ~175 行(去抖 + 完备性解析,§2.5) |
结论(诚实版):推送在稳态应当是净减少——省掉三次散列探测、一次 1.2KB 三段 memcmp(换成 ~30 字哈希)、两次有损求和、CurrentUnitBindingsEpoch 的 owner 走查;付出 MGPDrawInfo 的 payload 构造与集合 hash。但差距远小于 v1 声称的量级,而且 §2.7 表明 monolith 的净行数是增加的。所以本设计的 monolith 论据是 §13.3-④ 的逐线程 CPU 数字,不是删除行数。
两个诚实的告诫:
- 可达性遍历是搬走了,不是消失了,头号指标必须是逐线程 CPU 时间。
- Magma 的
SetupDrawSnapshot快路径命中率在两种模式下会合法地不同,A/B 比的是渲染输出与计数器,永远不是 memo 轨迹。
两个 backend 编进同一个共享库(CMakeLists.txt:356-383、:485),backend 在 init 时锁存一次(ConfigLoader.cpp:212-225),所以去虚化在两种形态下都不可得,也都不需要。函数指针 struct 而非虚基类的理由见 §3.1。
13.3 替代字节一致门的五部分验证门
先把成本写在明面上:一个"改前改后 nm --defined-only 与剥调试信息后的 .text size 完全相等"的 monolith 门(§13.5 的第四层)在本方案里按构造死亡。这是本方案的代价,必须写进设计文档而不是藏起来。
①(v2 扩为三道)接口纯度门。
- 门 A(include 图):disaggregated 配置编译
MG_Backend时把MG_State/GLState从 include 搜索路径移除(或断言-H输出)。这是唯一能因它存在的理由变红的检查——nm --undefined-only对"只 include 不调用"是瞎的,而RenderState.h:12 → FramebufferObject.h:12-13 → TextureObject.h / RenderbufferObject.h正是这种耦合,RenderStateParameters用FramebufferObject::MAX_DRAW_BUFFERS定长(:263, 273)。依赖 P0.5 的MGPipeValueTypes.h。 - 门 B(符号):
nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'为空。 - 门 C(未声明):
grep -c 'pGLContext' MG_Backend/== 0(greppGLContext不是pGLContext->)。三道门都只跑非 verify 构建(D-B5)。 - 外加一条 debug 断言"每个 backend memo 键都是
{slot, gen}对,永不是裸前端指针",由HandleRecycleScenario支撑——这个场景在 0e 重键之前必须在至少一个 backend 上是红的。
② 语义影子比对(MOBILEGL_PIPE_VERIFY=1)——决定性的那一条。
阶段 B 期间两套状态模型活在同一个地址空间:tracker 再用 SnapshotFromGLContext() 填一份 PipeInputs,G4 生成的比对器逐字段、每 draw 与推送版本比对,打印第一个分歧字段名与 draw 序号。抓三种事:(a) tracker 忘了推的字段;(b) dirty 位触发得太少——危险的那个方向;(c) 两条路径上被变换得不一样的值。第三种 CI 模式,跑全部 40 个 trace 与 367 个集成测试;~5-10× 慢,永不出货。
必须逐字段比而不是 memcmp:DirectGLES.cpp:2029-2033 明确记录 RenderStateParameters 的 memcmp 会因 padding false-DIFFER(无害)但永不 false-match——比对器要零误报。
v2 修正 A:verify 需要"保留模式"。 消费即清的组(纹理 dirty rect)在发射后无法从头重算,所以 verify 在纹理 subdata 上是瞎的——而那正是最危险的子系统。MOBILEGL_PIPE_VERIFY=1 时 tracker 保留清除前的集合,G4 比对发射出去的 (unionBox, regionCount, regions[])(§6.3)。
v2 修正 B:verify 活过 P13。 SnapshotFromGLContext() 与它的 MG_State include 整体包在 #if MOBILEGL_PIPE_VERIFY 里保留;纯度门只跑非 verify 构建(D-B5)。P13 另交付录制-金标模式(MGPipe recorder,§13.4-9)作为不依赖 MG_State 的长期语义门。
③ 行为 A/B。
全部 ~40 个 trace 用例(tools/trace_replay/trace_cases.json,默认 SSIM 阈值 0.99)在 {monolith-pull, monolith-push, split} 三种下同一判定、SSIM ≥ 0.99;ctest -L integration-gpu 在 DirectGLES. 与 DirectGLES.Pipe./DirectGLES.Split.(以及 DirectVulkan 对)之间产生逐名相同的通过/失败集;428 个单元测试全绿;CTS 逐后端 conformance 在 0.5 个百分点内,按本项目的逐后端表格式上报(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母)。
两个 Create fixture 带 coherent_as_flush: true,必须在两种模式下都开着该开关跑(§7.8.1)。
v2 补充:TextureUploadShapeScenario——上传形状(box vs N region、作业数)录金标比对,因为 SSIM 对 +6ms 悬崖完全不敏感(§6.3)。
v2 补充:参考构建的定义。 P2 之后 monolith 本身已经变了,所以逐名基线必须明确为**"P1 出口的重构后 monolith",而 P1 出口本身要先用 verify 证明重构等价于 81b17c0b。81b17c0b 的 monolith 只作为 §13.3-④ 性能对照的锚点,不作为逐名功能基线。**
④ monolith 性能不回归。
两台设备(35d0befa Adreno 830、3B159D009VZ00000 Mali),reboot-clean、同热窗口、配对 A/B,用 tools/bench.sh + trace replay 的 --benchmark --benchmark-tail-frames --benchmark-result 逐帧 JSON。指标是逐线程 CPU 时间,monolith-push 在 p50 与 p99 上都要落在 monolith-pull 的噪声内。CPU 定频按本项目协议。
v2 补充三条:(a) 绝对阈值——tracker 每 draw 的 ns 必须公布并设上限,因为真实拉取基线只有 10-25 次 accessor(§2.3.1),相对噪声阈值会平凡通过;(b) Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)单列,它是 D-B1 的判据;(c) 负面对照——关掉 CSO 内容寻址(MOBILEGL_PIPE_PUSH 的一位)重跑,把"推送更慢"与"CSO 设计更慢"分开。
⑤ 覆盖 + poison + handle 纪律。
gen_pipe.py 重生成 477 行 inventory 的 MGPipe 映射列,0 UNMAPPED,git diff --exit-code;gen_pipe_dirty_surface.py 重生成 mutator→聚合世代 映射,0 未映射(推论 4);PipeInputs::m_filledGen 的逐 verb世代 poison(§5.2.2);G7 的 render-state setter 一致性测试;P13 的 static_assert(sizeof(ResidualValueBlock) == 0);ResidualValueBlock 的逐成员 offsetof 断言。
两条字节级等式仍然幸存:MOBILEGL_BUILD_DISAGGREGATED=OFF 时 nm --defined-only libMobileGL.so | grep MG_Remote 为空且链接行不增加任何库;nm -D libMobileGL.so | grep mobilegl_server_main 在 RelWithDebInfo 里命中。
符号与 .text 漂移每阶段作为信息性指标发布——一次无法解释的跳变仍然是一个 smell,只是不再是一条断言。
13.4 monolith 侧净收益清单(即使 IPC 永不上线也成立)
- ~372 行 per-draw 失效发现机制真删除(§2.5),另有 ~175 行搬到 client。注意 §2.7:monolith 的净代码量是增加的(约 +6,650 手写 + 4,000 生成),所以这一条是佐证,不是主论据。
- 复用地址 ABA 一整类不可表达:D1/D2/D3/D10/D11/D13/D14/D16/D17/D20 全部由
{slot, gen}关闭。 - FBO → program 排序 hazard 消失:
DirectGLES.cpp:2712-2732的 fragColor 重推导 workaround 与g_broadcastMemo*删除(机制是惰性特化,D-B3 v2)。 - 一处分层倒置消失:
SwapchainObject.cpp:276-330不再往MG_Impl的pDefaultFramebufferInfo里写。 - 两个潜伏 bug 顺带修掉:D21(
m_xfbCounterSlotByObject用裸 GL name 做键,VulkanRenderer.cpp:11136-11146)与RenderbufferObject缺GetLifetimeId()。两条都先独立落dev。 - 一个死能力被暴露:
CapabilityInput::FramebufferSrgb与DepthClamp(RenderState.h:165, 168)没有任何存储——SetCapability落到default: // not supported currently(RenderState.cpp:380),IsCapabilityEnabled返回false(:428-429)。六个 backend 读点今天恒为 false。 必须在渲染状态 chunk 表冻结之前回答(它决定 pipeline/dynamic 划分里要不要这个字段)。 - 一次 glslang 编译离开 monolith 启动路径(Magma 的内部 shader 烘焙)。
inproc= monolith 的渲染线程,且只需隔离两个进程全局(§13.6)——本项目手上最大的单一 CPU 杠杆。MG_Test的 mock backend 顺理成章变成 MGPipe recorder:tools/trace_replay获得一种比 apitrace 精确得多的 MGPipe 级录制格式(记录的是已解析的状态),而且它是 P13 之后不依赖MG_State的长期语义门(D-B5、开放问题 11 的答案)。
13.5 三层编译期保证与唯一 hook 点
从强到弱:
- 编译期折叠。
MOBILEGL_BUILD_DISAGGREGATED(默认 OFF)关闭时MobileGL/MG_Remote/**不进SOURCE_FILES,MG_Config::Transport是constexpr Monolith,MG_Backend/Init.cpp里的分支在编译期消失。注意MG_Pipe/不在这个 option 之后——它是 monolith 的架构,永远进构建(§13.8)。 - 唯一 hook 点。 整个拆分入口是
MG_Backend/Init.cpp:48-70里的一个分支:
void Init() {
MGLOG_D("Initializing MobileGL Backend...");
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Config::Transport != TransportKind::Monolith) {
pActiveBackendObject = MakeUnique<MG_Remote::BackendObject_Remote>();
} else
#endif
switch (MG_Config::ActiveBackendType) { /* 原样不动 */ }
if (!InitSpecificBackendLibs()) { /* 原样 */ }
LogBackendInfo();
}
BackendObject_Remote::GetPipeTables() 返回发射版的 MGPipeScreen/MGPipeContext,Initialize() 负责 spawn/connect。下游的 MG_Impl 边界调用点零 #ifdef。
3. shadow-in-shm 的 allocator 改动必须同样包裹。 PipeResource::MapAlignedAllocator 与 MipmapStorage 的 level vector 住在 MG_State,改它们的 allocator 就改了类型;写成"分配器特化,option OFF 时逐字折叠回今天的 MapAlignedAllocator"(§7.4)。
第四层——nm --defined-only 与 .text size 逐阶段完全相等——在本方案里不成立(D-B5),由 §13.3 的五部分门取代,只保留两条字节级等式作断言、符号/尺寸漂移作信息性指标。
13.6 两个 CMake option 与 inproc 的角色隔离
每一条部署路径都要求出货构建是 ON:FCL 用户可编辑 env、plugin APK 的 V2 开关表、ctest ENVIRONMENT 变体、/data/local/tmp CTS 路径。所以 option 必须拆成两个:
MOBILEGL_BUILD_DISAGGREGATED(出货形态):只含spawn/unix:/pipe:。每进程只有一个GLContext、一份gPipeCtx、一个pActiveBackendObject→ 它们全部保持普通全局,GL 热路径上没有任何 TLS 与间接。侵入面就是MG_Backend/Init.cpp里那一个可预测的分支。MOBILEGL_BUILD_DISAGGREGATED_INPROC(CI/调试形态,隐含开启前者):额外加角色隔离 shim。
inproc 需要隔离的是两个进程全局,不是四个。 在拉取模型下,同进程同时扮演两个角色需要给 pGLContext、gBackendFunctionsTable、pActiveBackendObject、pDefaultFramebufferInfo 四个全局都做角色分身,其中 pGLContext 的 shim 坐在全库最热的路径上(grep -rho 'pGLContext->' MobileGL/MG_Impl | wc -l = 1494,加 backend 侧 293),而 Android 上 dlopen 的共享库无法可靠使用 initial-exec TLS,每次访问会退化成一次 __tls_get_addr 调用。
MGPipe 把这个数字降到 2:
| 全局 | 还需要角色隔离吗 | 为什么 |
|---|---|---|
MG_State::pGLContext(GLState/Core.h:564 / Core.cpp:1487) |
不需要 | server 角色不再读它(三道纯度门就是这个断言)。它只属于 client 角色 |
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo(GL_Framebuffer.cpp:3344) |
不需要 | backend 侧的 4 处身份比较改用保留 handle {0,1} + MGPFramebufferState::isDefault;SwapchainObject.cpp:276-330 的写改成 on_surface_changed。server 角色不再触碰它 |
MG_Backend 的 pipe 表(今天的 gBackendFunctionsTable,MGPipe 下是 gPipeCtx/gPipeScreen) |
需要 | client 角色要看见发射表,server 角色要看见真 backend 表 |
MG_Backend::pActiveBackendObject(Init.cpp:53-61) |
需要 | 同上:EGL/caps 虚函数面 |
两个全局的 shim 只需要 operator-> / operator bool / get() / 赋值,而且都不在 GL 热路径的每次访问上(pipe 表在每个 MGPipe 调用处取一次,pActiveBackendObject 只在 EGL/caps 面)。这条是 MGPipe 让 inproc 从"成本可疑的实验"变成"可交付形态"的直接原因。
13.7 inproc 作为产品交付物与运行时选择
inproc 不只是测试脚手架:同进程第二个 apply 线程 = monolith 的渲染线程。今天 PrepareForDraw(状态调和、VAO/FBO/纹理/program/render-state sync、UBO ring memcpy)加驱动调用全部同步跑在 glDrawElements 里;把它们搬到 apply 线程,对 GL 线程 CPU-bound 的应用(本项目的 profiling 史说 Minecraft 就是)是手上最大的单一杠杆,且不需要任何 IPC/shm/平台工作。
InProcessTransport 必须走与 spawn 完全相同的 G3 编解码路径,只在门铃/拷贝机制上不同(§14 P5 的规范条款)。否则 inproc 里程碑证明不了 wire 完整性。
MOBILEGL_TRANSPORT = monolith(默认) | inproc | spawn | unix:<path> | pipe:<name>,在 ConfigLoader.cpp 与既有开关并列解析。这一个选择免费换来:ctest ENVIRONMENT 变体、trace-replay 的 setenv 块(trace_replay_core.cpp:134-207)、FCL 的用户可编辑 env 偏好(FCLauncher.java:417-430)、plugin APK 的 V2 开关表(android-plugin/app/build.gradle.kts:77-103,由 .github/scripts/validate-plugin-apks.sh 校验)、/data/local/tmp CTS 路径。零新增管线。
保留全部既有负面对照开关(MOBILEGL_ESPRYT_DISABLE_{UBO,UNPACK,UPLOAD}_RING、_INVALIDATE_FLUSH、MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION、MOBILEGL_COHERENT_AS_FLUSH);新增开关见附 B。
13.8 构建布局与测试接线
MobileGL/MG_Pipe/ # 见 §3.1;**不在任何 option 之后**,永远进构建
MobileGL/MG_Impl/Pipe/ # tracker、slot 分配器、CSO 缓存、HostResolve、CompositeResolver
MobileGL/MG_Backend/MGPipe/ # PipeInputs 与两个 backend 的表填充
MobileGL/MG_Remote/ # 仅 MOBILEGL_BUILD_DISAGGREGATED
Protocol/ protocol.fbs protocol_generated.h(提交) RecordKinds.h
Transport/ ITransport.h InProcessTransport.{h,cpp} SocketTransport.{h,cpp}
Framing.h Ring.{h,cpp} ShmSegment.{h,cpp} ShmSegmentPosix.cpp ShmSegmentWin32.cpp
FdPassing.{h,cpp} Doorbell.{h,cpp}
Client/ PipeEmitter.{h,cpp} EmitTables.cpp
BackendObject_Remote.{h,cpp} CapsMirror.{h,cpp}
ShadowArena.{h,cpp} PersistentMapTracker.{h,cpp} GpuWritePending.{h,cpp}
Surface/{X11,Win32,Android,Headless}.cpp
Server/ PipeApplier.cpp PipeObjectTables.{h,cpp} IndexHostMirror.{h,cpp}
ServerLoop.{h,cpp} ReplyPool.{h,cpp} EventRing.{h,cpp} ServerMain.cpp
ServerJni.cpp # Android,与 DriverPostJni.cpp 并列
scripts/ gen_pipe.py gen_pipe_dirty_surface.py gen_protocol.py check_doc_citations.py
MobileGL/MG_Test/Wire/CMakeLists.txt # 复制自 MG_Test/Buffer/(27 行)
CMake:
MG_Pipe/**与MG_Impl/Pipe/**与MG_Backend/MGPipe/**无条件进SOURCE_FILES。 只有MG_Remote/**在MOBILEGL_BUILD_DISAGGREGATED之后追加(CMakeLists.txt:226-419),因此MobileGL(:485)与MobileGL_s(:552)都拿到。MobileGLServer:桌面add_executable链接MobileGL_s,RUNTIME_OUTPUT_DIRECTORY设为$<TARGET_FILE_DIR:MobileGL>(§11.1);Androidadd_executable+set_target_properties(MobileGLServer PROPERTIES PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer")并链接共享的MobileGL,由 AGP 打进jniLibs。server 主体是 ~30 行 stub:dlopen(libMobileGL.so)→dlsym("mobilegl_server_main")(可见性见 §11.2)。一份共享库、两个角色,版本必然匹配(对比Feat/CS-Delta-IPC的四件必须互相匹配的产物)。 AGP 能否打包一个被改名成lib*.so的add_executable,是 P0 spike A 的验证项之一(MobileGL/build.gradle没有设targets列表)。 注意:Android 上那份共享库仍然包含 glslang/SPIRV-Cross/SPIRV-Tools(~43MB),因为它同时服务 client 角色;nm --undefined-only的 glslang 门(§13.3-①B)检的是 server 侧代码有没有引用它们,不是产物里有没有这些符号。- FlatBuffers:submodule
3rdparty/flatbuffers置于既有的if (EXISTS .../flatbuffers/CMakeLists.txt)保护下,去掉if (NOT ANDROID)一刀切。因为protocol_generated.h已提交,默认构建图里没有flatc,也不add_subdirectory(3rdparty/flatbuffers)(§8.1)。运行时是 header-only,只需要3rdparty/flatbuffers/include在 include path 上。 第二重 guard:若MOBILEGL_BUILD_DISAGGREGATED=ON而3rdparty/flatbuffers/include不存在,强制把该 option 设回 OFF 并message(WARNING ...)——否则MG_Remote/**已经进了SOURCE_FILES而头文件找不到,构建以一个莫名其妙的错误失败(现有的EXISTS保护只包住 Protocol 子目录)。MOBILEGL_FLATC_EXECUTABLE只服务 CI 的flatc-check,经MobileGL/build.gradle:17-21已在用的externalNativeBuild { cmake { arguments } }槽传入。 - 测试接线(三个已被文档记录的陷阱要遵守):
MG_Test/Wire/(labelunit)→ 现有 CItestjob 自动收,无需改 workflow。MG_IntegrationTest/CMakeLists.txt每 backend 增加两条gtest_discover_tests(TEST_PREFIX "DirectGLES.Pipe."用于 monolith-push、"DirectGLES.Split."用于拆分,DirectVulkan 同),必须用mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV})构造,并带上MOBILEGL_IPC_SERVER_PATH。陷阱:ctestENVIRONMENT是替换而非追加(:339-343)、;必须转义(:322-332)、property 覆盖 job env(test.yml:253-262)。- trace replay 的
SPLIT接线:add_trace_replay_test今天把测试命名为MobileGLTraceReplay.${CASE_NAME}.${BACKEND}(tools/trace_replay/CMakeLists.txt:330-332),加一个SPLIT参数会与同 case+backend 的现有测试重名。改成MobileGLTraceReplay.${CASE_NAME}.${BACKEND}${SPLIT_SUFFIX}。另外该测试的命令是cmake -P run_trace_case.cmake加约 18 个-DTRACE_*变量,所以还要加-DTRACE_TRANSPORT=并在run_trace_case.cmake里消费它——这两个文件都要列进 P5 的交付物。
- CI 新增步骤:
pipe-gen-check:重跑gen_pipe.py(G1-G7)+git diff --exit-code;dirty-surface-check:重跑gen_pipe_dirty_surface.py+git diff --exit-code,0 未映射 mutator;flatc-check:重生成protocol_generated.h+git diff --exit-code;include-graph-check:MGPipeValueTypes.h与ProgramArtifacts.h的-H闭包断言(§3.7.2 门 A、§14 P0.5);doc-citation-lint:check_doc_citations.py,docs/**里每个file:line必须在基线提交上解析到存在的行;- 一条 grep 门:禁止
MG_Backend/与MG_State/下出现fprintf(stderr/printf(; monolith-symbol-report:OFF 构建与 ON+monolith 构建的nm --defined-only/.textsize 对基线,信息性发布 + 两条幸存等式作断言(§13.3)。
14. 分阶段实施计划
通用纪律(每个 commit 都适用):默认 ALL target 必须能完整构建;禁止提交热路径插桩;每个门必须能因它存在的理由变红;Windows 机器不是正确性门(其 Vulkan 缺
vkCreateHeadlessSurfaceEXT,占该机 567 个基线集成失败中的 423 个);设备对比走 reboot-clean + 同热窗口配对 A/B,CPU 定频按项目协议(大核 1.96 / 小核 1.55GHz,GPU 拉满,40°C 门槛);每个阶段的出口都跑一次 §13.3 的五部分门;每个阶段的性能判据都是逐线程 CPU 时间,不是墙钟帧时。 两条跑道:P0-P4a、P3b/P4b、P7、P8、P13 是 monolith 跑道,每一段都可独立交付、可随时中止且 monolith 严格好于起点;P5、P6、P9-P12 是 IPC 跑道。 v2 排期修订说明:v1 的阶段天数与它自己的 §5.4/§5.5 逐子系统表互相矛盾(例如 P3a 给 12 天,而它包含的三行合计 22-29 天,等于"再基线检查点"按构造必然触发;P7 报 48 天下界而同口径是 85-111)。本节的每个天数都是它所含 §5.4/§5.5 行的求和,算术在 §14.5 公布。
P0 — 卫生、度量、门与骨架(9-11 天)
交付物
- 清工作树 per-draw
fprintf:DirectGLES.cpp:640-663、Managers.cpp:875-877(后者在pendingMutex临界区内)。CI 加 grep 门禁止MG_Backend/与MG_State/下出现fprintf(stderr/printf(。 TracyPlot逐帧计数器,装在边界两侧,字节类:cmd-records、cmd-bytes-per-draw(直方图,SEG_CMD的定尺依据)、stage-buffer、stage-texture、stage-vertex-client、stage-index-client、stage-ubo-global、stage-ubo-named、persistent-map-push、server-ring、server-staging、residual-value-block、index-mirror-bytes、index-bytes-shipped、texture-pull;调用类(v2 新增):每 draw 实际执行的 accessor 次数、每个 memo 门(SyncRenderState早退、SyncNeccessaryTextures键比较、CurrentUnitBindingsEpoch快门、TrySetupDrawFastPath、pipeline memo、ApplyDynamicDrawStateTail)的命中/未命中、resource_subdata发射次数与上传作业数。没有调用类计数器,P2 的判据仍然是猜(§2.3.1)。两台设备取基线。MG_Pipe/PipeCalls.def+MGPipeTypes.h+MGPipeHandles.h+MGPipeCallbacks.h:完整调用目录,即使暂未实现的条目也占位(记录编号绝不 churn)。scripts/gen_pipe.py与七个生成器 G1-G7 的骨架 + CIpipe-gen-check(重生成 +git diff --exit-code)。scripts/gen_pipe_dirty_surface.py骨架(推论 4)与 CI 接线。scripts/check_doc_citations.py(v2 新增):docs/**里每个file:line必须在基线提交上解析到存在的行。v1 有一批SamplerObject.h引用指向 160 行文件的 468-551 行;本文件已修正,lint 防止再犯。MOBILEGL_PIPE_PUSH/_VERIFY/_STATS/_LEGACY_MEMOS/_TEXEL_RETAIN_MB/_INDEX_MIRROR_MB在ConfigLoader.cpp与既有开关并列解析;两个 CMake option(§13.6)与MOBILEGL_TRANSPORT解析;§13.8 的 flatbuffers include-dir guard。- 三个严格 no-op 的免费收益:
GetIntegeri_v/GetInteger64i_v/GetProgramiv的纯前端 case 移回MG_Impl(Espryt 14 / Magma ~10 个读点);RenderbufferObject::GetLifetimeId()(不加GetVersion()——推送模型里glRenderbufferStorage*本身就是一次 pipe 调用);D21 重键——这一条是潜伏 bug 修复,先独立落dev。 - 回答两个阻塞问题:
FramebufferSrgb/DepthClamp无存储是潜伏 bug 还是有意为之(§13.4-6,必须在渲染状态 chunk 表冻结之前);语料里是否存在glRenderbufferStorage的 OOM 探测惯用法(决定kNeedsAck要不要标它,§6.4)。 MG_Remote/{Protocol,Transport}骨架:ITransport、InProcessTransport、校验型Framing、Ring+RingControl(双 tail、双游标三元组、双向 doorbell)、Doorbell、ShmSegment(memfd/ASharedMemory/shm_open/CreateFileMappingW)、SCM_RIGHTSfd 传递(第一优先);protocol.fbs+ 提交的protocol_generated.h+gen_protocol.py+ CIflatc-check;MG_Test/Wire/目录。mobilegl_server_main的extern "C" __attribute__((visibility("default")))声明(§11.2)。- spike A(Android 交付链,半天):从根 CMakeLists 造一个平凡的
libMobileGLServer.so(add_executable+PREFIX "lib"/SUFFIX ".so"),确认 AGP 把它打进lib/arm64-v8a/;让TraceReplayActivity从getApplicationInfo().nativeLibraryDirposix_spawn它并打一行日志——在**应用自身进程(untrusted_app域)**验证 exec,而不是靠run-as。同时把一个通用 env 透传(--es mobilegl_env "K=V;K=V")接进 trace 路径的五个文件(trace-replay-ci.sh、TraceReplayActivity.java、JNI Request marshalling、trace_replay_core.cpp、run_android_retrace_local.py),取代逐 knob 加--es/--ez。 - spike B(external memory 可行性,半天):最小程序,导出一个
HOST_VISIBLE|HOST_COHERENTVkBuffer 的 fd,mmap后回读校验,在35d0befa(Adreno 830)与3B159D009VZ00000(Mali)各跑一次。与SCM_RIGHTS测试同批。目的是让 P11 的结论在第一周就有方向:若两台都不行,P11 缩为"记录并回退",省 6 天。
验收:AdvertisedLimitsScenario(6 个测试)绿;367 集成 × 2 backend + 428 单元逐名不变;40 个 trace 全绿;两台设备的基线字节、调用、逐线程 CPU 数字记录在案;MG_Test/Wire 的 fd 传递测试把一个 memfd 从 fork 出的子进程传回父进程并读到相同字节;spawn 测试断言进程树只多出恰好一个子进程;nm --defined-only 与去符号 .text size 与改动前的 libMobileGL.so 一致(OFF 构建),nm -D | grep mobilegl_server_main 在 RelWithDebInfo 下命中;spike A/B 出结论(spike B 直接决定 P11 规模);citation lint 全绿。
P0.5 — 值头与制品头抽取(6-9 天)★v2 新增,P1 与 P7 的硬前置
交付物
MG_Pipe/MGPipeValueTypes.h:把MAX_DRAW_BUFFERS、PerBufferBlendState、StencilFaceState、PixelStoreParameters、RenderStateParameters、SamplerParameters、BorderColorForm、VertexAttribute、VertexBufferBindingPoint与相关枚举搬进来,它不 includeMG_State/GLState的任何东西;RenderState.h/SamplerObject.h/VertexArrayObject.h反过来 include 它。 必须做的理由:RenderState.h:12includeFramebufferState/FramebufferObject.h,后者:12-13再 includeTextureObject.h与RenderbufferObject.h;RenderStateParameters用FramebufferObject::MAX_DRAW_BUFFERS给两个数组定长(:263, 273)。所以 v1 的"共享值头白名单"不是叶子集,把它交给"纯净的MG_Backend"会拖进整张类图,而nm --undefined-only看不见(只 include 不调用不产生未定义符号)。MG_State/GLState/ProgramState/ProgramArtifacts.h:把TypeFacts(ProgramObject.h:44)、ResourceReflection(:76)、XfbVarying(:1146)、LinkArtifacts(:1210)、SpirvArtifacts(:1409)抽出来,不 includeShaderObject.h、不 includeSpvcSession.h;更新 7 个 includer(ProgramFactory.h、UniformManager.cpp、VulkanRenderer.cpp、ProgramInterface.cpp、ProgramLinkTask.h、ProgramObject.h、ProgramTranslationCache.h)。 必须做的理由:server 要反序列化进这五个类型就必须有它们的定义,而它们今天住在会拖进 glslang(ShaderObject.h:12→ShaderCompileTask.h;:146返回SharedPtr<glslang::TShader>)与 spirv_reflect(ProgramObject.h:14→SpvcSession.h)的头里。没有这一步,P7 的nm -D | grep glslang判据不可达。- CI include 闭包断言:
MGPipeValueTypes.h的-H闭包里没有MG_State/GLState/;ProgramArtifacts.h的闭包里没有 glslang / SPIRV-Cross / spirv_reflect 任何头。 ProgramArtifacts.h的Visit()归档 +sizeof绊线(§3.5.5)。
验收:全套现有测试逐名不变(这是一次纯搬移);两条 include 闭包断言绿,且人为把一个 MG_State include 加回 MGPipeValueTypes.h 能让它变红;nm --defined-only 与 .text 变化可逐符号归因(搬移会改变某些内联决策,允许,但要解释)。
P1 — PipeInputs 替换与 verify harness(10-13 天)
交付物
MG_Backend/MGPipe/PipeInputs.h:每个 backend 真正用到的GLContext方法一个访问器(Espryt 32 / Magma 55),字段类型与今天读到的完全一致,按 memo 键组织。- 机械
sed:MG_State::pGLContext->→MGB_CTX->(293 处);外加逐条手工转换 58 行非箭头用法(§2.4:~34 处MOBILEGL_ASSERT真值判定删除、7 处空守卫改直读、3 处 patch 三元、DirectGLES.cpp:146的.get()裸指针捕获与:142的decltype别名、14 处!= nullptr、1 处注释)。这份 58 行清单是本阶段的显式交付物。 - 逐 verb 类填充点(v2 修正,§5.2.1):G5 从
PipeCalls.def生成"每个kCtxVerb/kCtxObject调用可能读哪些PipeInputs字段"的表,并在MG_Impl的 ~93 个边界站点上生成对应的 validate/fill 调用。不是只在PrepareForDraw/SetupDraw两处——MG_Impl用到的 70 个表项里 ~48 个不是 draw/dispatch,其中多个自己就读pGLContext(UpdateTextureBindingAtTarget:6051-6052、PackStateFromContext:6129、Clear:4106/:4165、BlitFramebuffer:5988-5989、GetTexImage:9254-9257、DSA by-name:4038-4043、:7417-7418),而:1501-1502的注释已经点明"for every non-draw call site (Clear, readbacks)"。 - G5 的逐 verb 世代 poison:
m_filledGen[f] == m_currentVerbSerial(非 sticky 字段);debug 与 disaggregated 构建里读陈旧/未填字段 =Fatal{UnmigratedPipeInput, "<field>@<verb>"}。 - G4 的
MOBILEGL_PIPE_VERIFY=1逐字段影子比对器 + 第三种 CI 模式接线。 - 20 处
SyncPersistentMappedRange+ 6 处SyncGpuWrites的逐站点归属表(§6.2、§4.8.1),作为文档交付物。
验收(v2 修正)
nm --defined-only在 pull 构建里不变;.textsize 变化必须能逐行归因。 v1 要求"完全一致",但本阶段自己的交付物里就有 ~24 处会生成代码的转换(7 处if (pGLContext)空守卫、14 处!= nullptr、3 处三元)——只有 ~34 处MOBILEGL_ASSERT是真免费(Defines.h:114在非 debug 下宏为空)。此外SnapshotFromGLContext与 G4/G5 机制必须包在#if MOBILEGL_PIPE_PUSH/_VERIFY/DEBUG里,pull 构建才不多出调用。把空守卫与三元的重写推迟到 P2(那时字段确实永远有效),本阶段只做 assert 删除与sed,则.text差异可压到零附近。- 全部 40 个 trace 与 367 个集成测试在
MOBILEGL_PIPE_VERIFY=1下零分歧; - 故意损坏一个快照字段能让 verify 门变红;
- 故意在某个非 draw verb(
glGenerateMipmap)的填充表里漏一个字段,能在那条 verb 上触发 poison Fatal——不是在某个后续 draw 上。
**★ 第 25 天(低端估计)— 最早可见里程碑:**零产品风险地证明"推送等价于拉取",逐 draw 逐字段。这不是 GO/NO-GO(它没有性能数字,也没有 Track H 单位成本)。
P2 — 值推送:渲染状态 CSO(双后端)+ 第一片 Track H + 残余值块(18-26 天)
交付物
MG_Impl/Pipe/Tracker.{h,cpp}:dirty 位(§4.2,值类用既有计数器、对象类新增 5 个聚合世代)+ §4.3 的不变式 + §4.4-4 的集合 hash 抑制器骨架。MG_State的 5 个聚合世代(TextureState两个、BufferState、VertexArrayState、FramebufferState各一,合计约 20 行)+gen_pipe_dirty_surface.py的首轮映射与 CI 接线。MG_Pipe/MGPipeRenderStateSpans.{h,cpp}+ G7:pipeline/dynamic chunk 表(从VulkanRenderer.cpp:4826-4906原样搬来)+ 遍历每个RenderStatepublic setter 断言pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变的测试。MG_Impl/Pipe/CsoCache:64 项 LRU,键是 pipeline 子集的 xxHash(不是整块,D-B1 v2)。create_render_state/bind_render_state/set_dynamic_state:Espryt 侧RenderStateImpl的 693 行函数体、单Uint16早退、三段 memcmp、g_syncedColorMaskAlphaWidenMask、dual-source decline 一行不动(消除 4 个读点);Magma 侧ComputePipelineStateHash/GetOrCreatePipeline/ApplyDynamicDrawStateTail改从 CSO 与动态 payload 取(消除 ~55 个读点)。两个版本号都过线。set_pixel_pack_state(PACK only)、set_patch_state、set_vertex_attrib_defaults;P1 推迟的空守卫/三元重写。set_residual_value_state+ResidualValueBlock(§5.3):static_assert(sizeof == MGL_RESIDUAL_BLOCK_SIZE)(逐阶段下调)+ 逐成员offsetof断言 + split 下逐字段序列化。- 第一片 Track H(v2 新增,让 GO/NO-GO 测的是它要决定的事):Espryt 子系统 0b(
SlotAllocator+ 6 个 registry → slot 数组 + 删TwinLookupMemo×3 /OwnerEquals/g_fbSlotCache/ 2 个 GC 扫描)与 Magma 子系统 4(VertexInputStateFactory/VaoDrawMemo重键,删掉写进前端 VAO 的后端堆裸指针)。 MOBILEGL_PIPE_LEGACY_MEMOS编译期开关(§5.7):让前两波 handle 化保留一个真正的旧-vs-新臂。
验收
- 367 集成 × 2 backend × 2 模式(pull / push)逐名相同;40 个 trace 在 monolith-push 下 SSIM ≥ 0.99,双后端;
ClipDistance、SampleMaskScope、SampleVariables、DualSourceBlend、ViewportArray、PrimitiveRestart场景绿;verify 模式零分歧; HandleRecycleScenario绿,且它在 0b 重键之前必须是红的;- G7 的 setter 一致性测试绿,且人为把一个字段从 pipeline chunk 表里拿掉能让它变红;
- 两台设备 reboot-clean 配对:monolith-push 在 p50 与 p99 逐线程 CPU 上落在 monolith-pull 噪声内或更好,并且 tracker 每 draw 的绝对 ns 落在预设上限内(相对阈值不够,§13.3-④a);
- Blaze3D blend-toggle 微基准(enable/draw/disable/draw,MC batch 速率)单列发布;
- 负面对照:关掉 CSO 内容寻址重跑,把"推送更慢"与"CSO 设计更慢"分开。
★ 第 43 天(低端估计)— GO/NO-GO 决策点。 此刻手上有:verify harness、双后端已推送的渲染状态、真实 CPU 增量与绝对 ns、Blaze3D 微基准、CSO 负面对照、Track H 在两个 backend 的最便宜子系统上的实测单位成本。两个出口(继续 / 收缩为 headless 工装用途或重新评估)与沉没成本口径写在 §0.5。
P3a — handle wave 1(Espryt):buffer、VAO(18-23 天)
handle 基建(0b)已在 P2 交付。
交付物:7 个 BufferBackendOps → resource_create/respecify/destroy、resource_subdata、buffer_subdata_resident(可 null,保住 Magma 的差异)、resource_flush_range(带应用真实 access flags)、resource_readback、map_persistent(不碰实现);pool 与延迟释放机制原样搬;create/bind/delete_vertex_elements_state(两个视图都带;IsLong 与 Type 分开);set_vertex_buffers(baseInstance 是显式字段,不再是调用方武装的 ScopedFetchBaseInstance 作用域);set_index_buffer(带 restart index 与模式);Adreno 禁用属性 SIGSEGV workaround 原样保留;MOBILEGL_PIPE_LEGACY_MEMOS 分支维护。
验收:全套门(monolith-push,DirectGLES);LargeArenaAdoption、ResidentIndex、StorageBufferRegrow(发布 map-persistent-roundtrips)、AtomicCounter、BufferTexture、CrossFrameBuffer、SsboArrayLength、SsboArrayDynamicIndex、VertexArrayEnableDisable、VertexAttribBinding、DoublePrecision、DrawParameters、MultiDraw、PrimitiveRestart 场景;create-indirect、create-instancing、rd12-odinlite、improved-transparency-26.3、fabric-sodium trace SSIM ≥ 0.99;MC 26.3 在 Adreno 上 p99 不变(16MiB 采纳结果不得回归)。
⚠ 再基线检查点 1:若 P3a 超过 27 天(上界 +50%),"窄 handle 化"的前提就是错的,必须在 P4a 开始之前重定基线。
P4a — handle wave 2(Espryt):FBO / 纹理 / sampler / program 的身份与描述符(26-34 天)
刻意推迟到首帧之后的部分:memo 重键、dirty 归属反转、跨步描述符改造、program 陈旧性重构(→ P3b/P4b)。
交付物:set_framebuffer_state(8 个 MGPSurface + client 解析后的 readSurface + 内联 internalFormat + contentHash + isDefault 保留 handle,退役 4 处 pDefaultFramebufferInfo 读);四个跨对象 mask 在推送时刻推出;create/bind/delete_sampler_state(SamplerParameters 逐字节含 borderColorForm,SamplerObject.h:66-96);create/delete_sampler_view(只带视图限制)+ set_texture_params(D10:base/max level、swizzle、dsMode、LOD 钳、forceResync);set_sampler_views(client 侧解析,无 stage 维度)+ bind_sampler_states;set_shader_images;create/bind/delete_shader_state(逐 stage SPIR-V + ProgramArtifacts.h 的 Visit() 全结构体归档);set_draw_program / set_dispatch_program;set_global_constants;CompositeResolver.cpp;纹理与 renderbuffer 的 resource_create/respecify/subdata。emulation 路径在 split 模式下显式 Fatal 直到 P8。
验收:全套门;CrossFrameBuffer、LayeredAttachmentShape/Barrier、SnormAttachment、RenderbufferBlendFormat、FragmentOutputArrayIndex、Orientation、ClearThenReadPixels、FragCoordOrigin、TextureView、ProgramPipeline、PostLinkAttach、RelinkStageSet、SpirvShaderBinary、AsyncCompile(6 个)场景;新增"只作 FBO attachment / 只作 image 单元 / 只作 CopyImage 端点的纹理其 glTexParameter 生效"场景(D10 的门,必须在 set_texture_params 落地前是红的);KHR-GL46.direct_state_access.framebuffers* 与整个 packed_pixels 块在两台设备上绿(~3300 个 framebuffer/用例,handle 复用的压力测试)。
⚠ 再基线检查点 1b:若 P4a 超过 39 天,同上处理。
P5 — 传输 + inproc applier + 发射表(12 天)
交付物:MG_Remote/Client 的发射表实现 MGPipeScreen/MGPipeContext;Server/PipeApplier.cpp;ServerLoop(mgl-srv-io + mgl-srv-apply,后者终身持有原生 context);单一 hook 点 MG_Backend/Init.cpp:48-70 装 BackendObject_Remote;MGPCaps 快照;一条阻塞 read_pixels;client 侧保守 MarkGpuWritten 与 emitSeq;client 侧块粒度 persistent-map 推送(T2 档下强制,§7.8.1);InProcessTransport;trace-replay 的 SPLIT 后缀与 -DTRACE_TRANSPORT= 接线(§13.8)。
v2 规范条款:InProcessTransport 必须走与 spawn 完全相同的 G3 编解码路径,只在门铃/拷贝机制上不同。否则第 99 天的里程碑证明不了 wire 完整性,而 P6(第 104 天)才在关键路径上发现缺口。PipeApplier 里加一条 debug 断言:任何传输下都不得有 SharedPtr 或裸前端指针跨过 applier 边界。
验收:ctest -R 'DirectGLES\.Split\..*(ClearThenReadPixels|Triangle)' 在 MOBILEGL_TRANSPORT=inproc 下绿;OpenRA trace 在 split 模式下 SSIM ≥ 0.99;PersistentCoherentMapScenario 绿;两个角色的峰值 RSS 记录在案,作为 §7.11 内存预算的实测基线;persistent-map-push 字节量出数;任何未迁移的 PipeInputs 字段读产生 Fatal{UnmigratedPipeInput}。
★ 第 99 天 — 首个 IPC 帧(inproc)。诚实标注:这是缩减路径——client 数组、indirect-count 解析、索引宿主镜像在 split 下仍是 Fatal,全功能要等 P8。
P6 — spawn transport(5 天)
交付物:SocketTransport(socketpair + fork/execve,显式 envp 剔除 + mobilegl_server_main 内强制 Monolith 的双保险);ServerMain;MOBILEGL_IPC_SERVER_PATH 为主 + dladdr 兜底;就绪握手有界重试;client EOF 即时退出;server 死亡的 device-lost latch。
验收:P5 全部测试在 MOBILEGL_TRANSPORT=spawn 下绿;fork 链测试断言进程树只多一个子进程;HeadlessGL 的 fork 预检交互测试无孤儿 server(§11.3);run_android_retrace_local.py --case OpenRA --backend DirectGLES 在 35d0befa 上 SSIM ≥ 0.99。
★ 第 104 天 — 首个跨进程帧(缩减路径)。
P3b / P4b — 深化(Espryt):memo 重键、dirty 反转、跨步描述符、XFB scatter、回读(29-38 天)
交付物:重键 ResolvedDrawBuffers、PendingAttribValueMask、ConvertedFloat64Stream、SyncCurrentFBO 四元组戳、ResolvedTextureBindingMemo、SamplerPassMemo、image sweep、program registry 到 {slot, gen};server 侧删 g_unitTextureSyncList、g_fboTextureSyncList、g_unitSamplerLookupMemos、g_imageSweep*、DirectGLES.cpp:1372-1489 的 ~115 行 unit-bindings epoch 推导,同时在 MG_Impl/Pipe/Tracker.cpp 落地对应的集合 hash 抑制器(§2.5、§4.4-4);dirty 归属反转(§6.3,client 保 rect 模型与按存储属主键控的发射游标、发射后自清);MGPSubRegion 跨步描述符改造(§3.5.6:Managers.cpp:4274-4326 从描述符取步长,替代 uploadData == mipData 指针比较与整 level 步长算术);XFB scatter 搬到 client(§6.2.1);删 fragColor 重推导 workaround 与 g_broadcastMemo*;用推送状态退役 9 条陈旧性判定里的第 4-6、8-9 条;Espryt 的 raw-depth-fetch SamplerObject 原生化;回读 / pack state。
验收:~25 个纹理场景(TextureView、LayeredTextureReadback、ImageSizeAfterRespec、FormatlessImageBake、NonCoreImageFormat、ImageFormatQualifier、ImageTargetKind、ImageLoadStoreSso、UnboundImageDescriptor、SwizzleAccessRoutine、IntegerBorderColor、PixelStoreSweep、SampledSetStaleness、ThreeChannelAttachment、BufferTexture、CopyImage*×3、ClearTexImageUndefinedLevelZero、DepthStencilReadback×3、PackedWordReadback);21 个 program 场景 + 整个 MG_Test/ShaderTranspiler 目录;两台设备上完整 KHR-GL46.texture_* / internalformat.texture2d.* / shader_image_* / packed_pixels 块,conformance 在 pull 基线 0.5pp 内;每一个 Iris trace;
v2 新增三个门:
TextureUploadShapeScenario:逐纹理逐帧的上传形状(box vs N region、作业数)录金标比对——+6ms 悬崖由形状相等把关,SSIM 对它不敏感;Mali 上帧时增量必须发布;- view/owner 发射游标别名场景:通过 view 上传、经属主采样(以及反向),跨 draw 边界各一次(§6.3 修正 1);
- verify 保留模式:
MOBILEGL_PIPE_VERIFY=1下resource_subdata的(unionBox, regionCount, regions[])与快照重算逐项相等(§6.3 修正 2); XfbAfterClipDistance/XfbCaptureBufferReuse/XfbRepeatedCapture/TessellationXfbCapture与KHR-GL46.transform_feedback.capture_special_interleaved_test(scatter 的gl_SkipComponents空洞保留,§6.2.1)。
P7 — DirectVulkan(Magma)全量迁移(80-104 天,可与 P5/P6/P8 并行)
子系统 1(pipeline+动态状态)与子系统 4(VertexInput/VaoDrawMemo)已在 P2 交付,所以是 §5.5 的 85-111 减去 5-7。
交付物:§5.5 的其余 10 个子系统,重点四项:SetupDrawSnapshot 的 ~14 个探测字段(含两个有损的版本求和)塌成 dirty mask 比较;UniformManager 的 8 类占位 TextureObject 换成原生 VkImage+view+descriptor(~120 行删除,34 个 MOBILEGL_ASSERT(pGLContext) 里的 9 个消失);具名 UBO 的 host payload(D-B8:ResolveUniformBufferPayload UniformManager.cpp:2022/2052 改从 set_shader_buffers 的 MGHostSpan 取,kCapNeedsHostUboBytes 门控);blit / depth-mipmap 内部 shader 烘焙成签进树的 SPIR-V + uniform location + UBO 布局,由一个 MG_Test 重跑树内 glslang 逐字节比对的用例守新鲜度;VertexInputStateFactory 的后端堆裸指针写回直接删除;VkRenderPassManager / VkTextureManager 的节点式容器纪律原样保留(D18,postmortem 注释逐字带进 review checklist)。
验收:367 集成 + 40 trace 在 DirectVulkan 的 monolith-push 与 split 下全绿;verify 零分歧;nm -D libMobileGLServer.so | grep glslang 为空——这是整个论点的强制执行点(依赖 P0.5);UnboundImageDescriptor、SampleMaskScope、ImageLoadStoreSso、AtomicCounter、SsboArrayDynamicIndex、NonCoreImageFormat、Orientation、DepthStencilReadback* 场景;Iris trace 上 stage-ubo-named 逐帧字节量发布(D-B8 的定尺依据);两台设备 CTS 在 0.5pp 内。
⚠ 再基线检查点 2:P7 中点(第 40-52 个工作日)若已完成子系统 < 40%,立即重定基线——P3a 的检查点发现不了 Magma 特有的超期,而 P7 在单跑道下位于关键路径。
P8 — emulation 下放 + 索引宿主镜像 + 协议广度(12-16 天)
交付物:MG_Impl/Pipe/HostResolve.cpp——client 数组范围计算、最大索引扫描(TryComputeMaxIndexFromHostBytes 移到 client,唯一的无界应用指针读)、*IndirectCount 计数解析,每一条前面都有 §4.8.1 逐站点表规定的 reconcile(不是笼统的 publish/wait/drain:*IndirectCount 只做 SyncPersistentMappedRange(),因为 monolith 也只做这一个,DirectGLES.cpp:4666-4667);MGHostSpan 的 split 填法;Server/IndexHostMirror(D-B7、§7.10);CopyImage shadow 镜像搬到 client;draw_vbo(info, indirect, ranges[], numDraws) 收编 multi-draw 族(分档仍在 server);viewport-array 回放验证在一次 pipe 调用驱动下各遍之间观察到的状态与今天一致(EndViewportRoutingPasses 会调 InvalidateSyncedRenderState,DirectGLES.cpp:3841);generate_mipmap 返回 level 计划(形状,不带字节)与 CPU 回退的纹素;G3 的"单条记录大于段容量"分块/降级路径(§7.1.1);§9.3 的无 present fence tick 与一个无 present 的 split 用例。
验收:ctest -L integration-gpu -R '^DirectGLES\.Split\.' 与 '^DirectGLES\.' 逐名相同,DirectVulkan 同;40 个 trace 在 split 下双后端 SSIM ≥ 0.99,含两个 coherent_as_flush: true 的 Create fixture(两种模式都开着该开关跑);新增 ClientArrayAfterComputeWriteScenario 绿,且去掉那次等待必须能看到几何缺失;create-indirect fixture 上 roundtrips-per-frame 读零(§4.8.1 的绊线:证明没有给 *IndirectCount 平白加一次 publish-and-wait);index-mirror-bytes 与 index-bytes-shipped 逐用例发布;MultiDraw、PrimitiveRestart、ViewportArray、DrawParameters、CopyImage*×3、GuiBatch 场景。
★ 第 145 天 — 全功能 split。
P9 — 反向通道(10 天)
交付物:SEG_REPLY 4KiB slot 池;阻塞 read_pixels;PBO 回读 fire-and-forget;on_gpu_written{res, ranges} 收窄(配 writableMask);on_buffer_writeback 按操作级批处理(今天两处逐行循环:Utils.cpp:2342、DirectGLES.cpp:7633)配 epoch bump 的排序规则(§6.4);on_xfb_scatter_ready + client 侧 scatter(§6.2.1);on_texture_writeback(一个生产者);on_mip_levels_generated(只带形状);on_texture_pull_request 四条缓解全上 + resource_subdata_complete 终止符(§6.5);on_gl_error 有序 + 收窄后的 kNeedsAck(§6.4);on_caps_invalidated;on_surface_changed;on_log 按严重级分级(≤WARN 有损 / ≥ERROR 无损 + 每秒速率限制器 + "N errors suppressed");SEG_EVENT 溢出策略 + 等待循环内排空(§8.4)。
验收:DepthStencilReadback×3、PackedWordReadback、LayeredTextureReadback、ClearThenReadPixels、XfbAfterClipDistance、XfbCaptureBufferReuse、XfbRepeatedCapture、TessellationXfbCapture、KHR-GL46.transform_feedback.capture_special_interleaved_test 在 split 下绿;TextureRemintPullScenario 绿,且它必须包含一个"答不出来"的用例(一张只被渲染过、随后被 image-bind 的纹理)并在终止符落地前表现为 apply 线程挂死/超时;拉取计数逐 trace 用例发布;故障注入:client 被 credit 阻塞时灌满 SEG_EVENT,两侧都必须恢复;日志洪泛下注入一次 backend link 失败,那行 ERROR 必须出现。
P10 — sync / query / present 节奏(6 天)
交付物:client 铸造 sync 与 query handle;轮询入口成为门铃点 + MOBILEGL_IPC_POLL_ESCALATE 饥饿升级(§8.2);fence 完成度来自真的逐 fence 退休(§8.5,不是 present 水位——那正是 MC 1.21.5 native-heap OOM 的成因);DirectGLES 的非 present fence tick;present 严格 1:1;MOBILEGL_IPC_PRESENT_CREDIT 默认 1 + 叠加公式;逐帧 roundtrip 计数器与输入延迟直方图;§8.6 的三个独立 dev monolith 修复。
验收:XfbPrimitiveQuery、PrimitivesGeneratedNoXfb、AsyncCompile 在 split 下绿;40 个用例上 draw/state/upload 路径的 roundtrip 计数器读零,条件渲染与阻塞 query 次数逐用例发布;零 timeout 轮询循环测试在有界时间退出;bench.sh 在 35d0befa 上配对 A/B:两侧都关采纳时 split 帧时在 monolith 10% 内,输入延迟直方图 p50/p99 记录在案。
P11 — persistent map 与 ≥16MiB 采纳(8 天;spike B 全否则缩为 2 天)
交付物:由 P0 spike B 驱动的 POST 探针档位选择(T2 / T1 / T0,§7.8);SEG_ADOPT 生命周期绑 completedFrameSerial;MOBILEGL_IPC_ADOPT_TIER 覆盖开关做负面对照。
验收:LargeArenaAdoptionScenario 在所选档位下绿;improved-transparency-minecraft-26.3 与两个 Create fixture SSIM ≥ 0.99;StorageBufferRegrowScenario 发布 map-persistent-roundtrips(T1 档下每次存储定义一次,不是每 store 一次);35d0befa 上配对 reboot-clean 的 p99 帧时与峰值 RSS 对 monolith 采纳基线(p99 163→21ms、40→115fps、~400MB)——split 在所选档位下 p99 不得回归超过 10%;若 T2 成为永久答案,其实测代价必须写进文档。
P12 — Android 生产窗口路径(10 天)
交付物:android:process=":mgl" 的 Service 收 Java Surface(Binder)后 ANativeWindow_fromSurface(minSdk 26 无公开 ANativeWindow 扁平化;树内先例是 android:process=":bench" 的 BenchService,§11.3);server 生命周期绑 Activity;FCL 用户 env 与 plugin APK V2 开关表接线(零新增管线)。
验收:Minecraft 通过 FCL 在 spawn 模式下在 35d0befa 上双后端入世界;配对 reboot-clean bench + 输入延迟直方图;杀 server 产生干净的 device-lost latch;SIGKILL 故障注入。
P13 — 退役 pull 路径(8-12 天)
交付物:删 SnapshotFromGLContext() 的非 verify 编译分支、MGB_CTX 宏、MOBILEGL_PIPE_PUSH、MOBILEGL_PIPE_LEGACY_MEMOS;保留 MOBILEGL_PIPE_VERIFY 及其 SnapshotFromGLContext() 与 MG_State include(D-B5);交付 MGPipe recorder 金标模式(MG_Test mock backend → 录制器,§13.4-9),作为不依赖 MG_State 的长期语义门与开放问题 11 的答案;删 set_residual_value_state 与 ResidualValueBlock;MG_Backend 的 MG_State include 收缩到 MGPipeValueTypes.h;在计数器活着的情况下重调所有幸存缓存的容量(Magma 的 2048 槽 VaoDrawMemo、4 个 SetupDrawSnapshot、8 个 pipeline memo、8 个 syncedTextureMemo)并把它们变成带 env 覆盖的调优参数;最终符号/尺寸/CPU 报告。
验收:static_assert(sizeof(ResidualValueBlock) == 0) 编译通过;三道纯度门在非 verify 构建上转绿(include 图门 A、符号门 B、未声明门 C,§13.3-①);verify 构建仍能跑且零分歧;MGPipe recorder 金标在 40 个 trace 上建立并可回归;全套门(367 × 2 backend × {monolith, split}、428 单元、40 trace SSIM ≥ 0.99、两台设备 CTS 在 81b17c0b 基线 0.5pp 内);monolith 逐线程 CPU 在两台设备的 p50 与 p99 上不差于 P0 基线——本设计的性能主张在这里成立或倒下。
14.5 总估时、里程碑与 CTS 周转
逐阶段求和(低端 / 高端,单跑道累计)
| 阶段 | 天 | 累计(低端) | 构成(§5.4/§5.5 的行) |
|---|---|---|---|
| P0 | 9-11 | 9 | Espryt 0a(1-2) + Magma 0a(~1) + 共享基建 |
| P0.5 | 6-9 | 15 | 头文件抽取(新增) |
| P1 | 10-13 | 25 | PipeInputs + 逐 verb 填充 + verify(共享基建) |
| P2 | 18-26 | 43 | Espryt 1(3-5) + Magma 1(3-4) + Espryt 0b(5-7) + Magma 4(2-3) + tracker/CSO/G7(4-6) + 聚合世代(1) |
| P3a | 18-23 | 61 | Espryt 2(10-13) + 3(7-9) + LEGACY 维护(1) |
| P4a | 26-34 | 87 | Espryt 4(7-9) + 5 前半(11-15) + 6 身份半(7-9) + LEGACY(1) |
| P5 | 12 | 99 | IPC 跑道 |
| P6 | 5 | 104 | IPC 跑道 |
| P3b/P4b | 29-38 | 133 | Espryt 5 后半(12-15) + 6 后半(7-9) + 7(5-7) + 9(5-7) |
| P8 | 12-16 | 145 | Espryt 8(8-11) + Magma 份额(4-5) |
| P9 | 10 | 155 | IPC 跑道 |
| P10 | 6 | 161 | IPC 跑道 |
| P11 | 8 | 169 | IPC 跑道(spike B 全否则 2) |
| P12 | 10 | 179 | IPC 跑道 |
| P13 | 8-12 | 187 | Espryt 10(4-6) + Magma 11(4-6) |
| P7(Magma) | 80-104 | 267 | §5.5 的 85-111 减去已在 P2 交付的子系统 1 与 4 |
报作 267-337 人天(不含 CTS 周转)。两个工程师、P7 与 P5/P6/P8 并行 → 约 7-9 个月,真正的约束是两台设备的争用而不是人头。
与独立成本分析的一致性:一次独立的改造成本调研给出 backend 工作单独 202-266 天(Espryt 95-125 + Magma 85-111 + 共享 22-30)。本节的 267-337 = 那个区间 + IPC 跑道 51 天 + P0.5 的 6-9 天,方向一致。v1 报的 200-260(含 IPC)落在其乐观端之外,已作废。
里程碑(低端估计):第 25 天 verify harness 全绿(零产品风险,不是 GO/NO-GO);第 43 天 GO/NO-GO(含一片真 Track H,出口见 §0.5);第 99 天首个 inproc IPC 帧(缩减路径);第 104 天首个跨进程帧(缩减路径);第 145 天全功能 split;第 187 / 267 天三道纯度门转绿。
再基线检查点:P3a > 27 天;P4a > 39 天;P7 中点(第 40-52 个工作日)完成子系统 < 40%。任一触发,先跑 inproc 的证伪数字再决定是否继续。
CTS 周转必须单独计价,不折进阶段估时。 gl44to46 caselist 约 56,271 例。分层门控:逐阶段只跑该阶段改动可能影响的具名 CTS 块(P4a 的 packed_pixels、P3b/P4b 的 texture_*/shader_image_*、P9 的 transform_feedback*),完整 caselist 只在五个架构边界跑(P0.5 头文件抽取、P3a handle、P4a framebuffer/纹理身份、P3b/P4b 纹理、P13 纯度)以及每次合并 dev 之前,且放在 CI 而不是关键路径上。设备锁协议照旧。若实测周转仍主导排期,诚实做法是加宽估时而不是削弱门。
15. 风险与对策
| # | 风险 | 对策 |
|---|---|---|
| B-R1 | 总成本 267-337 人天,首个跨进程帧在第 104 天、全功能在第 145 天。 排期驱动的评审可以只凭这一条否掉本方案 | 把价值排在承诺之前:P0-P2(43 天,其中 28-39 天是 MGPipe 独有)交付 handle 化 twin 与内容寻址的渲染状态 CSO——零 IPC 风险的可测量 monolith 工作——并产出字节/调用计数器与第一个逐线程 CPU 数字与 Track H 单位成本。第 43 天显式 GO/NO-GO,两个出口写在 §0.5。 P13 是一个完全自洽、不含任何 IPC 的 monolith 交付物;P5 的 inproc 只要 12 天 |
| B-R2 | 中心性能主张未经测量,且它的基线被 v1 高估了一个数量级。 可达性遍历是搬走而不是消失;真实稳态拉取只有每 backend 每 draw 10-25 次 accessor(§2.3.1),不是 124/169 | 字节与调用计数器是 P0 交付物。每阶段验收用逐线程 CPU 时间,两台设备、reboot-clean、配对,并设绝对 ns 上限(相对噪声阈值在真实基线下会平凡通过)。P2 除渲染状态外必须含一片 Track H,否则测的不是要决定的事。加 Blaze3D blend-toggle 微基准与 CSO 内容寻址的负面对照。先清工作树 per-draw fprintf |
| B-R3 | monolith 字节一致门按构造死亡,逐名集成基线也随之移动 | 五部分替代门,全部在 P0/P0.5/P1 落地(§13.3),其中 ② 逐 draw 逐字段影子比对在语义上严格强于任何符号 diff。两条字节等式仍作断言保留。逐名功能基线明确定义为"P1 出口的重构后 monolith",而 P1 出口自己先用 verify 证明等价于 81b17c0b;81b17c0b 只作性能锚点 |
| B-R4 | server 发起的纹理拉取是新停顿类,触发路径之一(整格式再生 Managers.cpp:3950-4195)在普通 glTexImage 格式变更上就会触发、无法被 hint 预防;而且存在 client 根本答不出来的 level(纯渲染产生 / CanMirrorCopyImageShadow 拒绝的 copy 目标 / GPU 生成的 mip),会让 apply 线程永久 park |
四条缓解同时上:imageBindableHint 预防主因;异步 park-and-re-emit 让停顿落在 mgl-srv-apply;resource_subdata_complete 终止符可携带零 region,server 带着"已分配但为空"的存储继续(正是 monolith 的行为,DirectGLES.cpp:6270-6271);保留 LRU 默认关闭(MipmapStorage 保有完整 CPU 影子,所以拉取总能被服务,缓存买的是延迟不是正确性)。TextureRemintPullScenario 必须包含无解用例并在终止符前是红的,拉取计数逐 trace 用例发布 |
| B-R5 | P3b/P4b(29-38 天)与 P7 中的 VkTextureManager 是最大最险的段,压在实测 +6ms/frame 悬崖(rect 列表 vs union box)与 7 条 fallback-repack 路径上,而后者的可行性判定 uploadData == mipData(Managers.cpp:4278-4283)在 split 下不成立——它要求上传源就是整 level shadow 并按整 level 步长跨步 |
resource_subdata 同时带 box 与 region 列表、server 选形状;MGPSubRegion 显式携带 srcRowStride/srcSliceStride 与 sourceIsVerbatimLevelShadow,Managers.cpp:4274-4326 改为从描述符取步长(形状照抄已存在的 UnpackStagingBlock,:4340-4390,ring 路径本来就紧密重打包)。这项工作计入子系统 5 的天数(+3-4 天),不再列为"原地不动"。TextureUploadShapeScenario 录金标比对上传形状与作业数,因为 SSIM 对这个悬崖完全不敏感。P3b/P4b 拆成两个可独立落地的半 |
| B-R6 | tracker 完整性:推送之后 server 不能再重读活状态校验快路径。任何 tracker 忘记发的 mutator 会静默漂移。历史上最危险的正是这个形状(DirectGLES.cpp:1441-1465) |
四层:(1) 构建期 G5 的逐 verb 世代表 + G7 的 render-state setter 一致性测试;(2) 运行期 poison 在需要该字段的那个 verb 上 Fatal(不是某个后续 draw);(3) 语义 MOBILEGL_PIPE_VERIFY 逐 draw 逐字段比对(含纹理 subdata 的保留模式,否则最危险的子系统是瞎区);(4) 枚举 gen_pipe_dirty_surface.py 枚举 MG_Impl 里每个 mutator → 必须 bump 的聚合世代,CI 上未映射即失败。迁移粒度是一个 accessor。 477 行 inventory 保留为覆盖检查表 |
| B-R7 | AcquirePersistentMap 跨进程无解会葬送 MC 26.3 的结果,而没有任何目标平台的支持被验证过 |
显式隔离:改造期完全不碰,只有 IPC 那一步会打破它。决策交给三档 POST 探针与 P0 第一周的 spike B(§7.8)。T2 前端已在三处容忍并让 client 侧块推送成为强制(P5 交付)。若两台设备都否,P11 从 8 天缩为 2 天。注意 T1 是每次存储定义一次 round trip,不是每 store 一次(StorageBufferRegrowScenario 发布计数)。不让一个平台未知数挡住 267 天的接口工作 |
| B-R8 | D18 的节点式容器纪律在重构中丢失:m_renderbufferResources / m_textureResources 是故意用 std::unordered_map,一次扩表搬迁曾让 BlitFramebuffer 静默停在 "layout undefined"(VkRenderPassManager.h:375-397) |
D18 是重键表里唯一标为 UNCHANGED 的身份行;postmortem 注释必须逐字带进 P7 的 review checklist。slot 数组在插入下稳定,实际改善了处境——但仍然点名 |
| B-R9 | 逐 backend 的行为不对称被统一接口抹平(Magma 故意不注册 ResidentSubData,VkBufferManager.cpp:104-111;PrefersCpuXfbPrimitiveAccounting;DirectVulkan 留空的 8 个槽) |
可选性是接口的一等属性:null 项在本代码库里已经表示"未实现,前端回退"(BackendObject.h:212-215, 265-269),MGPCaps 携带显式 callMask。但 v2 收回了用 cap 位表达 emulation 归属的做法(D-B7):ResolveTierForBatch 逐 batch 用 programReadsDrawID(server 独有事实)选档,且两个 backend 都做 restart 重写,所以那五个 cap 位没有门可控。归属规则改成一句话 + 一个 kCapNeedsHostIndexBytes |
| B-R10 | 接口在未测量的形状上过早冻结;若干 server 侧缓存的容量是按拉取模式调的 | payload 结构从第一天走 structSize-first 版本纪律,可增长。字节与调用计数器在 P0 落地。stage-ubo-named 出数之前不冻结 set_shader_buffers 的 host payload 形状(D-B8)。P13 在计数器活着的情况下重调所有幸存缓存的容量,并把它们当作带 env 覆盖的调优参数。screen/context 划分在 P0 定进头文件但按 context 计数 == 1 实现 |
| B-R11 | 58 行非箭头 pGLContext 用法的迁移缺口;DirectGLES.cpp:146 的 .get() 与 :142 的 decltype 别名 sed 完全抓不到 |
§2.4 已逐形态分类。P1 的交付物包含这份 58 行清单的逐条转换。纯度门 grep 的是 pGLContext 而不是 pGLContext-> |
| B-R12 | 残余值块是迁移期边界上的一个洞:poison 抓不到"两侧布局不同",而 monolith 的 verify harness 看不见它(两侧是同一个 TU) | 逐成员 offsetof 断言 加上 split 模式下逐字段序列化(走 G3 编解码器)。块的字节量单独计一类。static_assert(sizeof == 0) 让退役是编译错误 |
| B-R13 | SEG_EVENT 的 ERROR 无损化重新引入死锁 |
每秒 ERROR 速率限制器 + "N errors suppressed";MGLOG_E_ONCE 的 latch 变 per-server;P9 的故障注入门要求"日志洪泛下注入一次 link 失败,那行 ERROR 必须出现"且"两侧都恢复"(§8.4) |
| B-R14 | 排期估计:v1 的阶段天数与它自己的子系统表矛盾,且低于同口径的独立分析 | §14.5 的每个天数都是它所含 §5.4/§5.5 行的求和,算术公布。总数改报 267-337(不含 CTS)。三个再基线检查点按求和后的上界 +50% 设定。CTS 周转单独计价 |
| B-R15 | 在 GL setter 时刻推送会让整件事变慢,且这是最容易被后续实现者做错的一处 | 写成规范条款并给出证据(DirectGLES.cpp:2029-2032 的 Blaze3D per-batch blend toggle);P2 的设备门直接暴露它。v2 补一条同等重要的:glTexSubImage 不是 GL 调用时刻推送的对象(它根本不调 backend 表,GL_Texture.cpp 只有 3 处 MarkStorageDirtyRegion),逐调用发 resource_subdata 会精确复现 Mali 的 ~100 作业形状(+6ms/frame)。规则的正确措辞在 §4.1.1;resource_subdata 逐帧发射次数进计数器并在 MC 动画图集 fixture 上设上限 |
| B-R16(v2 新增) | stage C 之后 MOBILEGL_PIPE_PUSH 不再是对"旧 backend"的 A/B:位清零时 SnapshotFromGLContext 仍要合成 handle,backend 仍跑重键后的 memo 代码,两个分支跑同一份新代码;一个重键 bug(D1/D2/D3/D11/D13 那一类)在两臂都在,位图二分不出来 |
在 §5.7 写明这条口径收窄。为 P3a 与 P4a 加编译期 MOBILEGL_PIPE_LEGACY_MEMOS,让前两波 handle 化保留一个真正的旧-vs-新臂;随 pull 路径在 P13 退役。维护成本各阶段 +1 天,已计入 |
| B-R17(v2 新增) | MOBILEGL_PIPE_VERIFY 是唯一的语义门,而 v1 的 P13 删掉了它的参照物(SnapshotFromGLContext),删完之后设计没有语义绊线 |
SnapshotFromGLContext() 与它的 MG_State include 整体包在 #if MOBILEGL_PIPE_VERIFY 里保留过 P13;三道纯度门只跑非 verify 构建;P13 另交付 MGPipe recorder 金标模式作为不依赖 MG_State 的长期语义门(同时是开放问题 11 的答案) |
| B-R18(v2 新增) | monolith 的净代码量是增加的(§2.7:约 +6,650 手写 + 4,000 生成,对 ~372 行真删除),所以"~550 行删除"不能当主论据 | 把 §13.3-④ 的逐线程 CPU 数字作为 monolith 论据的主体,删除清单降级为佐证。§2.7 公布净 LOC 估计,让 B-R2 有一个可证伪的预测。若 P2 与 P13 的 CPU 数字持平而非改善,monolith 论据只剩架构性收益(ABA 不可表达、排序 hazard 消失、inproc 杠杆),必须据此重新评估是否值得 |
16. 开放问题
- client 侧 dirty 走查的真实每 draw CPU 代价是多少? 中心性能主张是"遍历搬走而不是翻倍",而真实基线只有每 backend 每 draw 10-25 次 accessor(§2.3.1)。P2 的头号数字,按逐线程 CPU + 绝对 ns、两台设备报。
- 真实语料上纹理重铸拉取的实际发生率?
imageBindableHint能预防主因,但整格式再生(Managers.cpp:3950-4195)在普通glTexImage格式变更上就触发。若 MC 或 Iris fixture 上实测率非平凡,保留 LRU 从"默认 0"升为强制并需要真预算。 AcquirePersistentMap跨进程能不能成? P0 spike B 第一周回答。未验证:VK_KHR_external_memory_fd的 host-visible-coherent 支持在四条 lane 上的可用性;GLES 侧能否用GL_EXT_memory_object_fd+glBufferStorageMemEXT走同一条路。- 渲染状态的 wire 粒度:pipeline 子集的 chunk 划分定下来之后,CSO LRU 的容量(暂定 64)与
set_dynamic_state的 chunk 粒度仍需 P0 计数器定。 MG_Util的切割缝在哪里? server 需要 SPIRV-Cross pass 流水线、ESSL 转译缓存、像素/纹理格式处理器、POST 探针、loader;client 需要 glslang phase A/B 与反射层。P0.5 解决了ProgramObject.h这一处,但MG_Util内部是否存在一条干净的 Transpile-vs-Reflect 缝仍未审计。- 一份反射归档能服务三个消费者吗? Espryt 读前端表,Magma 跑 SPIRV-Reflect,而
DirectVulkan.cpp:161为glGetProgramResource*又反射了第二遍。 - viewport-array 回放能塞进一次
draw_vbo吗? 今天它从 14 个 draw 入口经ForEachViewportRoutingPass重发应用的 draw N 次,而EndViewportRoutingPasses会调InvalidateSyncedRenderState(DirectGLES.cpp:3841)。未验证各遍之间观察到的状态是否与今天一致。 ResidentSubData的不对称该怎么收口? null 项保住今天的行为,但拆分工作可能正是给 Magma 补一个真实现的时机——那是行为变更而不是重构,应作为独立devPR。SEG_STAGE的上限定多少? 六类新字节(§7.1.1)需要 P8 之后用 MC in-world 与 Create 两类 fixture 的stage-*计数器给 p99 占用。并且 G3 的"单条记录大于段容量"分块路径需要设计与测试。FramebufferSrgb/DepthClamp无存储是潜伏 bug 还是有意为之? 六个 backend 消费者今天读到恒定 false(RenderState.cpp:380, 428-429)。必须在渲染状态 chunk 表冻结之前回答。- P13 之后还有 server 侧"第二意见"吗? v2 部分回答:保留 verify 构建(D-B5)+ P13 的 MGPipe recorder 金标。但 split-only 的渲染 bug(而非状态推送 bug)仍然没有 server 侧第二意见——recorder 只覆盖推送内容,不覆盖 backend 对它的解释。
client 侧 restart 重写与 indirect-count 解析会不会改变可观察行为?v2 已关闭:D-B7 把 restart 重写与 multi-draw 分档留在 server,monolith 行为零变化,诊断仍落在原线程。只有*IndirectCount的计数解析搬到 client,它的 decline 路径(DirectGLES.cpp:4682-4688)随之落到应用线程——这是改善而非退化,但需要在 P8 的验收里核对日志文本与顺序。- Magma 的两个内部 shader 烘焙后,uniform location 与 UBO 布局能否在没有活
ProgramObject的情况下表达?(VulkanRenderer.cpp:4238-4241, 4319-4324, 8450-8452)未做原型。 - 推送模型会改变哪些按拉取模式调过的缓存命中率? Magma 的 2048 槽
VaoDrawMemo、4 个SetupDrawSnapshot、8 个 pipeline memo、8 个syncedTextureMemo;Espryt 的 4096/256/64 槽TwinLookupMemo(后者会消失)。幸存者的容量在 P13 重调。 - (v2 新增)monolith 的
*IndirectCount不调SyncGpuWrites()是不是一个潜在缺口?DirectGLES.cpp:4666-4667只做SyncPersistentMappedRange(),而 compute 写的 indirect buffer 理论上需要前者。这是一个独立的dev问题,拆分不得借机"顺手修"——那会改变基线并让逐名对比失去意义。 - (v2 新增)索引宿主镜像的实际内存占用? D-B7 的预算是 64 MiB 默认上限,但 MC/Sodium/Iris 语料里 element-array buffer 的总量未测。若显著超预算,退化路径(逐 draw 通过
MGHostSpan传送)的频率与代价必须实测,因为它会把 §7.11 的内存预算和 §12.1 的零 round trip 主张同时削弱。
17. 对 Feat/CS-Delta-IPC 的复用清单
分支 worktree
../MobileGL-CS。判定分三类:REUSE(原样取)、CHANGE(取走并改造)、DROP(不取,逐条给理由)。
REUSE(原样取)
| 路径 | commit | 备注 |
|---|---|---|
MobileGL/Protocol/mg_protocol_base.h |
546895aa |
干净无依赖的词汇(MobileGLResult、span、ShmRegion、id typedef、structSize-first 版本纪律)。后者直接是 B-R10 的对策 |
docs/CS_Refactor/HandleSessionGeneration.md |
546895aa |
分支上最好的产物。三处修改:handle 清单补 RenderbufferObject::GetLifetimeId()——只补它,不补 GetVersion()(GetVersion() 只是 delta 触发器;推送模型里 glRenderbufferStorage* 本身就是一次 pipe 调用);把第 2 节的 server 侧 share-group 要求降为 v2(§1.2);把"lifetimeId 不符 → 销毁重建"改成 Fatal |
docs/CS_Refactor/HANDOFF.md 第 6 节"已知坑清单" |
d5c00b9d/5964628d |
逐字留作事后复盘:路径转换、versionCode 降级、双设备 ANDROID_SERIAL、flatbuffers camelCase accessor、union vector 产生指针、Release 下 MGLOG_D 被编译掉、嵌套 submodule 配方、assembleTraceDebug 改名 |
MobileGL/Protocol/tests/ProtocolSmoke.cpp |
546895aa |
schema 往返门(默认改 ON) |
根 CMakeLists.txt 的 EXISTS 保护 + .gitmodules 条目 |
546895aa |
去掉 NOT ANDROID,另加 §13.8 的 include-dir guard |
CHANGE(取走并改造)
| 路径 | commit | 改造 |
|---|---|---|
MobileGL/Protocol/protocol.fbs |
546895aa |
保留它的 delta 目录构想、RenderStateBlob 整块思想、BufferShmAdopt、命令清单、事件分类学。改:热路径转 struct + ring(§8.1);记录种类改为由 PipeCalls.def 生成,与 MGPipeTypes.h 逐条 static_assert 对齐;删掉冗余的 inlineBytes/data 双胞胎(:111-112、:125-126,两半代码对哪个字段是真的意见不一:ServerCore.cpp:184-208 只读 data,StateEmitter.h:60,111 只写 inlineBytes);加 AuxRequest;kind 枚举生成 + 每 kind static_assert + 运行期边界检查 |
MobileGL/ServerCore/ServerCore.{h,cpp} |
65717b4c+c2260dd8 |
保留握手→解码→apply→credit 的形状与 plugin manifest loader 思路,改造成 Server/PipeApplier.cpp + Server/ServerLoop。修:单次校验 + 零拷贝解码(今天校验两次外加一次整体拷贝,:492-498 与 :218-221);io/apply 分线程(:404-406 自承 worker 从未落地);完整事件集(SendEvent 只实现 BATCH_APPLIED,:373-382);credit 用最后一条实际 seq(:427 的 baseSeq + items.size() 只有 baseSeq==0 时才对);接收缓冲不能是对着 64MiB 帧上限的固定 4MiB(:478);真正的段生命周期(m_segments 只增不减,blobOwners 只 push 不释放) |
MobileGL/Remote/InProcessTransport.h |
65717b4c |
重表述在 C++ ITransport 上;单侧 shutdown(今天 :89-92 连对端 inbox 一起关);真段生命周期(Unmap/Close 今天是 no-op);补 §7.2a 的双向 doorbell(condvar 版)。并且必须走与 spawn 相同的 G3 编解码路径(§14 P5 规范条款) |
MobileGL/Remote/Framing.h |
65717b4c |
保留帧格式;m_pendingSize/m_haveHeader 改 mutable(今天 const_cast,:81,85);Feed() 真校验 magic 与长度(今天永远返回 OK,坏 magic = 静默永久挂起);缓冲不足返回所需大小且保留消息;真正在 socket transport 里使用它(今天是死代码) |
MobileGL/RemoteClient/StateEmitter.h:39-307(仅 emit 半边) |
b50f3348+d96be9f3 |
各域的字段遍历是真知识,而且更直接可用:那些字段集就是 MGPipe 的状态对象 payload,抬进 MG_Impl/Pipe/Tracker.cpp。必须修的缺陷:GL name 换 lifetimeId/handle(今天 :48-49, 85, 166-168, 203, 230 全把 GL name 塞进 handle)、O(n²) 线性扫描换 slot 数组(:175-181, 244-249, 253-258, 293-298)、固定 6 attachment(:232-236)换 MaxColorAttachments、补上被跳过的 texture view(:70-74)。applier 半边(:312-501)不取 |
scripts/extract_backend_read_inventory.py |
546895aa |
改造成 G6:删掉制造"0 UNMAPPED"的前缀兜底规则(:234-241),未知 accessor 一律 UNMAPPED 并编译失败;把真 pull point 与 signature handle 化分开统计。用途改变:它是 tracker 侧的覆盖检查表,真正的门是 §3.7.2 的三道纯度门。(gen_pipe_dirty_surface.py 在原分支没有任何对应物,是全新的。) |
DROP
| 路径 | 理由 |
|---|---|
MobileGL/Protocol/bfa.h(480 行) |
"strict C ABI"不是 C ABI:ServerCore.cpp:177-179 把 FlatBuffers 生成表的指针交给插件,插件必须是 C++ 且链接 FlatBuffers(StateEmitter.h:330,351,362,372 就是这么用的)。手抄的 60 字段 MobileGLDynamicParameters(:63-129)自承尾部不全、同步脚本从未写过——正是已在本项目造成 481 例 CTS 失败簇的那类数据的长期静默漂移炸弹。而 MGPipe 根本不需要 delta-apply vtable:接口是两张生成的函数指针表 |
MobileGL/Protocol/mgruntime_api.h + MobileGL/UtilRuntime/* |
360 行契约对 ~50 行实现(8 域实现 2 域);唯一消费者传 nullptr(ServerCore.cpp:61);缓存每次命中整份拷贝(:79)、按 clear() 淘汰(:91-93);smoke 断言 api->metrics == nullptr(RuntimeApiSmoke.cpp:66)。它的唯一理由随 BFA 消失;且本设计里翻译全在 server(它无论如何要链 SPIRV-Cross),glslang 全在 client(§4.7) |
MobileGL/Remote/LocalSocketTransport.{h,cpp}、ShmFactory.{h,cpp} 的实现 |
从未被任何测试执行(LoopbackSmoke 用的是 InProcessTransport,唯一另一个消费者 ServerHost 编译不过);每次 send 都 use-after-free(:199,asio::buffer(next) 指向局部 vector 而 lambda 捕获的是另一份拷贝);按 wire 长度无上限分配(:232-236);Start 里阻塞 accept/connect(:116、:139-144);无 strand 且 framesSent++ 非原子(:177-178);且完全没有 POSIX fd 传递(:296 硬编码 fd=-1),Linux/Android 数据面一字节过不去。只保留 ShmFactory.h:4-12 作平台矩阵规格 |
MobileGL/ServerHost/main.cpp |
编译不过(:31,39,44,53-54 对指针用 .,c2260dd8 改返回类型后成为死码)。MobileGLServer 在默认 ALL target 里,分支 tip 无法完成一次完整构建 |
MobileGL/RemoteClient/tests/StateEquivalenceTest.cpp |
把 delta apply 进第二个 MG_State::GLContext——它验证的正是本设计明确不存在的那条数据路径(server 侧没有第二份前端状态);与生产 apply 路径零共享代码;只测全量 resync;d96be9f3 声称五域逐字段而文件只比了纹理、buffer、render-state blob、buffer binding slot(没有 VAO 属性/FBO attachment/RBO 格式比较)。替代物是 §13.3-② 的逐 draw 逐字段影子比对,它比的是同一份状态的推送版与拉取版 |
c7c9e346 + 29d721ef 全部(share-group sessioning) |
非 v1 前提(monolith 只有一个 GLContext:GLState/Core.cpp:20,1487);且非可合并质量:VertexArrayState.cpp:+20-26 往已共享的表里再压一个 default VAO 并重复 Insert(0);四个头文件 public: 未复位泄漏私有成员;current session 是无锁进程全局,连它自己的 per-thread current 都没兑现;在状态权威里塞 MOBILEGL_SESSION_SWAP env kill switch 与 s_defaultAdopted 偷 context 的 hack。日后作为独立 PR 带多 context 测试落 dev(本设计的 MGPipeScreen/MGPipeContext 划分已经为它留好形状,§3.3) |
b50f3348 的 RenderState::InstallParameters + 裸 public: |
本设计不需要 Install setter:server 侧的 working RenderStateParameters 由 bind_render_state / set_dynamic_state 的 chunk 散射填充(D-B1)。若日后需要整块安装,用正确作用域的方法或单条 friend,绝不靠裸 public: |
d96be9f3 的 TRIAGE 指令(DirectGLES.cpp:+2583-2590) |
per-draw fprintf(stderr)。分支上每一次测量都跑在它上面。 同规则适用于当前工作树的 [IBOTX]/[BUFTX](P0 清除),并由 CI grep 门永久禁止(§13.8) |
附 A:接口调用目录速查表
Flags:
A=kNeedsAck、B=kHasBlob、V=kVarTail、H=kHostSpan、R=kReplySlot、O=kOptional。
MGPipeScreen(14)
| 调用 | payload | flags | 取代 |
|---|---|---|---|
get_caps |
MGPCaps |
R | 40 pActiveBackendObject-> + 89 caps 读点 |
resource_create |
MGPResourceDesc |
— | buffer/texture/renderbuffer 创建 |
resource_respecify |
MGPResourceDesc |
— | BufferBackendOps::Respecify 泛化 |
resource_destroy |
handle | — | OnDestroy + 两个 WeakPtr GC 扫描 |
map_persistent / unmap_persistent |
handle | R, O | AcquirePersistentMap(改造期不碰) |
fence_create / _status / _wait / _destroy |
handle (+timeout) | — / — / R / — | FenceSync…GetSyncStatus(两值契约保留) |
query_create / _begin / _end / _available / _result / _destroy |
handle + kind | — | BackendObject.h:230-256 |
MGPipeContext — CSO(15)
create/bind/delete × render_state / vertex_elements / sampler / sampler_view / shader。
create_render_state 带 B(只带 pipeline 子集的 chunk);create_shader_state 带 B(SPIR-V + ProgramArtifacts 归档)。
MGPipeContext — set_*(17 + 1 临时)
set_dynamic_state(B) · set_framebuffer_state · set_vertex_buffers · set_index_buffer · set_indirect_buffers · set_sampler_views(V) · bind_sampler_states(V) · set_texture_params · set_shader_images(V) · set_shader_buffers(V,H) · set_stream_output_targets(V) · set_global_constants(B) · set_vertex_attrib_defaults · set_pixel_pack_state · set_patch_state · set_draw_program / set_dispatch_program
临时(P2..P13):set_residual_value_state(B),带 static_assert(sizeof(ResidualValueBlock)==0) 退役绊线。
MGPipeContext — transfer(12)
resource_subdata(B,V) · buffer_subdata_resident(B,O) · resource_flush_range · resource_readback(R) · resource_copy_region · blit · clear · generate_mipmap · read_pixels(R) · get_texture_image(R) · resource_subdata_complete(拉取终止符,可零 region)
MGPipeContext — 命令(10)
draw_vbo(H,V) · launch_grid · memory_barrier · begin/end/pause/resume_stream_output · flush · present · set_swap_interval(O)
反向:MGPipeCallbacks(10)
on_gl_error · on_gpu_written · on_buffer_writeback · on_texture_writeback · on_texture_pull_request · on_mip_levels_generated(只带形状)· on_surface_changed · on_caps_invalidated · on_log(≤WARN 有损 / ≥ERROR 无损 + 速率限制)· on_xfb_scatter_ready
显式删除
GetIntegeri_v · GetInteger64i_v · GetProgramiv · ShaderStorageBlockBinding(折进 MGPProgramDesc)· set_pixel_unpack_state(不存在)· 压缩格式概念(不存在)· pipe_transfer(不存在)· set_sampler_views 的 stage 维度(不存在)· kCapPrimitiveRestart / kCapPrimitiveRestartFixedIndex / kCapMultiDraw / kCapMultiDrawIndirect / kCapMultiDrawIndirectCount(归属不可表达,D-B7)
附 B:环境变量与 CMake 选项
CMake
| 选项 | 默认 | 说明 |
|---|---|---|
MOBILEGL_BUILD_DISAGGREGATED |
OFF | 出货形态。开启后 MG_Remote/** 进 SOURCE_FILES,支持 spawn/unix:/pipe:。两个进程全局保持普通全局,GL 热路径无 TLS(§13.6) |
MOBILEGL_BUILD_DISAGGREGATED_INPROC |
OFF | CI/调试形态,隐含开启上者,额外加角色隔离 shim(只需隔离 gPipeCtx 与 pActiveBackendObject) |
MOBILEGL_PIPE_VERIFY |
OFF | 构建期开关(不只是运行期):编译进 SnapshotFromGLContext() 与 G4 比对器。P13 之后仍保留;三道纯度门只跑此项为 OFF 的构建 |
MOBILEGL_PIPE_LEGACY_MEMOS |
ON(P2..P13) | 保留 registry / TwinLookupMemo 实现,给前两波 handle 化一个真正的旧-vs-新臂(B-R16) |
MOBILEGL_FLATC_EXECUTABLE |
空 | 只服务 CI 的 flatc-check;默认构建图里没有 flatc |
MOBILEGL_BAKED_INTERNAL_SHADERS |
ON(P7+) | DirectVulkan 的 blit/depth-mipmap shader 烘焙成签进树的 SPIR-V,由 MG_Test 重跑树内 glslang 逐字节比对守新鲜度。monolith 也受益 |
注:
MG_Pipe/**、MG_Impl/Pipe/**、MG_Backend/MGPipe/**不在任何 option 之后——它们是 monolith 的架构,永远进构建(§13.8)。
运行时(MGPipe 新增)
| 变量 | 默认 | 说明 |
|---|---|---|
MOBILEGL_PIPE_PUSH |
迁移期按阶段推进;P13 后删除 | 子系统位图(0 = 全 pull),含一位关闭 CSO 内容寻址(P2 的负面对照)。注意 stage C 之后 A/B 口径收窄(§5.7、B-R16) |
MOBILEGL_PIPE_VERIFY |
0 | 逐 draw 逐字段影子比对(~5-10× 慢,含纹理 dirty 集合的保留模式,永不出货) |
MOBILEGL_PIPE_STATS |
0 | 字节 / 调用 / roundtrip / 纹理拉取 / 上传形状 / 残余块 / 索引镜像计数器转储 |
MOBILEGL_PIPE_TEXEL_RETAIN_MB |
0(v2 从 32 改) | 纹理重铸拉取的保留 LRU 预算。默认关闭:MipmapStorage 保有完整 CPU 影子,缓存买的是延迟不是正确性(§6.5c) |
MOBILEGL_PIPE_INDEX_MIRROR_MB |
64 | server 侧索引宿主镜像预算(D-B7、§7.10)。超预算退化为逐 draw 传送并计入 index-bytes-shipped |
运行时(传输与 IPC)
| 变量 | 默认 | 说明 |
|---|---|---|
MOBILEGL_TRANSPORT |
monolith |
monolith / inproc / spawn / unix:<path> / pipe:<name> |
MOBILEGL_IPC_SERVER_PATH |
空 | server 可执行文件路径(主要发现机制,dladdr 兜底,§11.1) |
MOBILEGL_IPC_RING_MB |
8 | SEG_CMD 大小 |
MOBILEGL_IPC_STAGE_MB |
32 | SEG_STAGE 初始大小;上限由实测定(§7.1.1、开放问题 9) |
MOBILEGL_IPC_PRESENT_CREDIT |
1 | client 允许领先的 present 数(1-4);延迟叠加见 §9.1 |
MOBILEGL_IPC_SPIN_US |
50 | 挂起前的自旋窗口(两侧 doorbell 共用,§7.2a) |
MOBILEGL_IPC_POLL_ESCALATE |
64 | 同一 handle 连续无进展轮询多少次后升级为阻塞 round trip(§8.2) |
MOBILEGL_IPC_PERSISTENT_BLOCK_KB |
64 | persistent-map 推送的块粒度(§7.8.1) |
MOBILEGL_IPC_ADOPT_TIER |
auto |
auto/0(T0)/1(T1)/2(T2 拒绝);与 MOBILEGL_IPC_RESPAWN 互斥(§11.6) |
MOBILEGL_IPC_SHADOW_SHM |
1(Phase 2 起) | shadow-in-shm 零拷贝(§7.4) |
MOBILEGL_IPC_INLINE_PAYLOADS |
0 | 负面对照:一律内联,不用 SEG_STAGE |
MOBILEGL_IPC_SERVER_AFFINITY |
auto |
mgl-srv-apply 的核绑定;auto 用 ShaderCompilePool 的大核探测(§10) |
MOBILEGL_IPC_STRICT_ERRORS |
0 | 诊断开关:让所有 backend 错误同步 ack |
MOBILEGL_IPC_AUDIT |
0 | 记录级审计日志 |
MOBILEGL_IPC_TRACE |
0 | 逐记录 trace(仅调试构建) |
MOBILEGL_IPC_ATTACH |
空 | 附着到已运行的 server(调试) |
MOBILEGL_IPC_RESPAWN |
0 | server 死亡后重启 + 全量重推(§11.6) |
MOBILEGL_IPC_IDLE_EXIT_S |
30 | server 的最后保险看门狗(EOF 应当即时退出) |
显式不设立:MOBILEGL_IPC_PROGRAM(没有 relink 档——链接真 ProgramObject 就链接 glslang,§3.5.5)· MOBILEGL_IPC_VALIDATE_SERVER(server 没有 MG_Impl 校验器——替代手段是保留的 verify 构建 + P13 的 MGPipe recorder 金标,见开放问题 11)。
保留的既有负面对照开关:MOBILEGL_ESPRYT_DISABLE_UBO_RING · _UNPACK_RING · _UPLOAD_RING · _INVALIDATE_FLUSH · MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION · MOBILEGL_COHERENT_AS_FLUSH(在拆分模式下照常生效,§7.8.1,这样两个 coherent_as_flush: true 的 Create fixture 在 split 与 monolith 下走同一条 buffer 路径,逐名对比才有意义)