Files
MobileGL/docs/Disaggregated/PLAN.md
T

164 KiB
Raw Blame History

MobileGL 前后端进程拆分实施计划(branch feat/disaggregated

2026-09-05 追加:本文件是方案 Areplica GLContext)。经用户方向修正——backend 应拥有贴近后端 API 的状态机并通过 gallium 式显式接口解耦——推荐路线改为方案 B,见同目录 PLAN-B-MGPipe.md(含逐项对比与 GO/NO-GO 对冲路径)。本文件保留:§6-§13(传输、数据面、同步、present、线程、平台、构建)被方案 B 原样继承并以本文为准;§5、§12 的 replica 特化部分已被取代。 状态:设计定稿 v12026-09-05)。基线 dev@81b17c0b;实施分支 feat/disaggregatedworktree ../MobileGL-disagg)。 产出方式:7 个只读代码调研 → 4 个独立架构方案 → 3 个评审打分 → 综合 → 3 个对抗性审查(38 条发现)→ 修订;评审记录见同目录 REVIEW.md。 上一次尝试 Feat/CS-Delta-IPC2026-08-29/30worktree ../MobileGL-CS)的复用/丢弃结论见 §14。


0. TL;DR 与核心决策

Server 就是 libMobileGL 自己,在自己的进程里跑一个真实的 MG_State::GLState::GLContextreplica,由一个 delta applier 通过普通 MG_State mutator API 驱动。两个 backendDirectGLES 27k / DirectVulkan 40k 行)一行不改。 Client 也是同一个 libMobileGL,在 init 时换掉几个对象:MG_Backend::gBackendFunctionsTable 换成发射表,MG_Backend::pActiveBackendObject 换成 BackendObject_RemoteSetBufferBackendOps 换成发射 ops。一份产物,两个角色,由一个 env var 选择。

这样做的唯一理由是:backend 的 draw-path 失效模型无法表达成 wire 字段。 DirectGLES 有 memo 直接借用 binding slot 的 shared_ptr 地址(DirectGLES.cpp:1463-1477 UnitTextureSyncEntry + PairingsIntact);VertexInputStateFactory.cpp:78后端堆上的裸指针写进前端 VAOIsBufferDrawClean 开头就是裸指针身份比较(Managers.cpp:1435-1436,注释:"Identity first: a respecify path can hand the frontend a NEW resource");三个门控计数器是回绕的 Uint16,只有配合指针身份比较才正确(Managers.h:772-777postmortem 在 DirectGLES.cpp:2823-2831);UniformManager.cpp:1418-1497 构造并驱动真实 TextureObject2DVulkanRenderer.cpp:4211-4356 通过真实 ShaderObject/ProgramObject 编译链接 GLSL。replica 逐字满足 DirectGLES 107/107、DirectVulkan 165/169 次 pGLContext 读取;重写则是把一套被文档记录为"具体设备 bug 疤痕组织"的失效模型重新推导一遍——那正是 Feat/CS-Delta-IPC 走的路,它一帧都没渲出来。

第二个核心决策:每个困难语义先上"慢但可证明正确"的版本,后续阶段用 flag 换成快版本,并把慢版本保留成 oracle。

  • Phase 1-4server 从源码重新 link shader(只需 5 个 schema 字段,而不是 ~40 字段的 reflection schema),并带 reflectionDigest 交叉校验 → Phase 5 换成 ProgramPublishrelink 保留为 A/B 对照与常驻 oracle。
  • Phase 1-6关闭 ≥16MiB persistent-map 采纳(前端在 BufferObject.cpp:174,439-442,470-472 已容忍 nullptr 返回,MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION 已存在)→ Phase 7 攻 external memory 导出,允许结论是"设备 X 上拒绝,已记录,回退成本 N ms"
  • Phase 1-4调用时刻拷贝进 ringWAR 由构造消除)→ Phase 4.5 shadow-in-shm 零拷贝。

第三个核心决策(本轮对抗性评审后新增,是本计划与上一版最大的语义差异):客户端是所有"隐式发布"语义的唯一发起者。 上一版把三件事交给"server 在 replica 上照常做,事件回传给 client",全部被证伪:

  1. persistent-map 的写发布BufferObject::SyncPersistentMappedRange()BufferObject.cpp:238-250)是 shadow-backed persistent 非-FLUSH_EXPLICIT map 的唯一推送点,而 grep -rn SyncPersistentMappedRange MobileGL/ 的全部生产调用点都在 MG_Backend/ 里(DirectGLES.cpp:262/4412/4666/4667/4768/4769、Managers.cpp:1547、MultiDraw.cpp:498、DirectVulkan.cpp:290/481/895、UniformManager.cpp:2022、VkBufferManager.cpp:573/620、VulkanRenderer.cpp:3432/3511/3826/7070/12015/12016)。MG_Impl 与 MG_State 里一个都没有。拆分后这段代码跑在 server 对 replica 上,而 replica 的 m_isMapped 是 false(没有 map delta),第一行就 returnclient 侧则根本没人调。应用通过 coherent persistent map 写下的字节会被静默丢弃。
  2. MarkGpuWritten:同样只有 MG_Backend/ 里的 6 个调用点(DirectGLES.cpp:465,509,1809UniformManager.cpp:1073,1229VulkanRenderer.cpp:11210),而它在 monolith 里是在 draw 调用内同步置位的。拆分后 draw 是 fire-and-forgetglDrawElements(); glMapBufferRange(SSBO, READ); 会在 server 还没 apply 之前就读到陈旧 shadow,零 round trip、零报错。
  3. 纹理 dirty flag:上一版声称"client 从不清 dirty flag",但 MipmapStorage::MarkDirtyRegionMipmapStorage.cpp:196-233)只要 m_isDirty[level] 为真就把 incoming 并进 union box 并追加 rect,只有 MarkDirty(level,false):171-189)会重置。永不清 = union box 只增不减、rect 列表饱和、summedArea*4 >= unionArea*3 一触发就退化成整 level 上传,正好与计划要保留的调优相反。

所以本版的规则是:任何 monolith 里由 backend 代码触发的"前端状态发布/消费",在拆分模式下必须由 client 在发射点自己做一遍,server 侧那份照常跑(它对 replica 操作,幂等或无害)。事件回传只允许作为收窄优化,永远不允许作为语义的建立者

Phase 1 的目标改为:在 Linux 上以 inprocspawn 两种传输跑通垂直切片;真机 OpenRA traceSSIM ≥ 0.99)移到 P2 出口判据。 理由见 §15Android 交付链(server .so 打包、untrusted_app 域 exec、trace app 的 env 透传)本身是独立工作量,把它压进 P1 的 10 天里是上一版最薄弱的排期假设。

核心决策速查

# 决策 理由
D1 Server = replica GLContext + 未改动 backend 293 次 pGLContext 读、13 个身份键 memo、25 个 backend→frontend 写全部原样工作
D2 发射点 = 三个已经是间接的边界(gBackendFunctionsTable / pActiveBackendObject / SetBufferBackendOps),不进 MG_State mutator monolith 侵入面 = MG_Backend/Init.cpp:48-70 里一个 switch 分支;~250 个边界调用点零 #ifdef
D3 版本计数器不上线replica 靠 mutator replay 自然 bump 不需要 Install* setter,不需要在 wire 上维护回绕 Uint16 的单调性
D4 控制面走 SPSC shm ringwatermark 放在一条共享 cache line双向 doorbell GetSyncStatus/IsQueryResultAvailable/ring 回收/present credit 变成一次 acquire load;但所有等待都必须能挂起,不能自旋
D5 FlatBuffers:热路径用 struct(定长、无 vtable、无 verifier walk),罕见/变长用 table 走 socket 满足"用 FlatBuffers 序列化"的要求,同时 DrawArrays 记录 32B 而不是 ~60B
D6 composite pipeline program 由 client 解析并下发 handle Core.cpp:644 在 pipeline cache miss 时 MakeShared<ProgramObject>(0u)linkserver 在 Phase 5 之后没有源码,必须由 client 定
D7 覆盖度由两侧生成的编译期断言保证:backend 的 READ 面 MG_Impl 的 MUTATOR 面 backend 新增一个 read、或 MG_Impl 在 table 调用旁新增一个 mutation 而 applier 没 replay → 编译失败,而不是设备回归
D8 monolith 保留由 nm --defined-only + .text size diff 机械证明,且每个阶段都跑,不只 P0 不靠"测试没变"
D9 client 是隐式发布语义的唯一发起者persistent map 推送、MarkGpuWritten、纹理 dirty 清除、XFB CPU 计数、生成 mip 的存储分配) 见上文三条被证伪的假设
D10 inprocspawn 拆成两个 CMake option:出货构建只开 spawnpGLContext 保持普通全局,GL 热路径上没有 TLS Android dlopen 的 shared library 无法用 initial-exec TLS1494 个 pGLContext-> 上每次 __tls_get_addr 调用不可接受

1. 目标与非目标

目标

  1. 前端(MG_Impl + MG_State + glslang 链接)与后端(MG_Backend + SPIRV-Cross + 驱动)跑在两个进程,通过 IPC 通信。
  2. Client 把前端状态 reconcile 成 delta,序列化(FlatBuffers)后发送;server 更新自身状态并调用 backend API。
  3. 稳态帧零 round tripreadback / 阻塞式 query / sync wait / present credit / 分配类错误 ack 之外)。
  4. 两半尽可能互相异步:client 至多领先 server 1 个 present(默认值,见 §9 的延迟叠加分析)。
  5. 平台特定代码最小化并集中在 MG_Remote/Transport/MG_Remote/Client/Surface*
  6. 单进程 Monolith 保持字节级不变,且可机械验证。
  7. 所有验收门用现有测试ctest -L unit / -L integration-gpu / tools/trace_replay / tools/cts / tools/device_bench

非目标(本分支明确不做)

  • share-group sessioning 重构。 monolith 今天所有 EGL context 共用一个 GLContextGLState/Core.cpp:20,1487eglCreateContext 只存 SharedContextEGLState/Core.cpp:640,全代码库无人读取)。单 context client 与今天等价。c7c9e346/29d721ef 那套(共享 VAO-0 破坏、四个头文件 public: 泄漏、无锁进程全局 current session、MOBILEGL_SESSION_SWAP kill switch)整体丢弃。
  • BFA strict-C-ABI backend 插件 / UtilRuntime C-ABI 化。 server 与 backend 同一 CMake 工程、同一产物发布,ABI 边界永不移动。
  • macOS 拆分。 CAMetalLayer 无公开跨进程表示,MobileGL 在 macOS 是 DYLD_INSERT_LIBRARIES interposer(导出表锁定于 CMakeLists.txt:600-612),无 CI 无设备 → monolith only,写进文档
  • Windows 窗口拆分。 WGL / ANGLE-DXGI 对外进程 HWND 不是受支持配置 → headless(pbuffer) only
  • Phase 9 之前不做任何窗口路径(全部离屏)。

2. 现状:今天的前后端边界(七个面)

(a) GLFunctionsTable — 73 项,MG_Backend/BackendObject.h:117-285

MG_Impl 侧 91 个调用点(GL_Drawing.cpp 37、GL_Query.cpp 22、GL_Framebuffer.cpp 11、GL_Texture.cpp 10、GL_Sync.cpp 6、GL_Getter.cpp 3、GL_Program.cpp 1+ MG_Util/ShaderTranspiler/CompileEnv.cpp:134,138 两处。

  • 20 个 draw、9 个 clear4 个 ClearNamedFramebuffer* 携带 SharedPtr<FramebufferObject>)、5 个 blit/copyCopyImageSubData 携带两个 CopyImageEndpointBackendObject.h:32-39)、GenerateMipmap、3 个 readback、4 个 compute/barrier、BindImageTexture(已经收 GL name)。
  • 两项是死代码GetInteger64i_vBackendObject.h:196MG_Impl 零调用点;GL_Getter.cpp:1307 把 64 位形式委派给 32 位)和 GetProgramiv:197GL_Program.cpp:851 全部从 ProgramObject 回答)。两个 backend 都注册并实现了它们。
  • GetIntegeri_v 只有 GL_MAX_COMPUTE_WORK_GROUP_COUNT/SIZE 真正转发(GL_Getter.cpp:1161-1179)。
  • BeginOcclusionQuery != nullptr 被当作能力探测用(GL_Query.cpp:471,545,768);DirectVulkan 只注册 64/72 项(BackendObject_DirectVulkan.cpp:690-770,不注册 7 个 XFB + PatchParameteri + SetSwapInterval)。

(b) BackendObject 虚函数 — BackendObject.h:541-568

MG_Impl 侧 89 个 pActiveBackendObject->其中 45 个是 GetDynamicParameters(),若干落在 per-API-call 校验路径上(Buffer/Validators.cpp:63VertexArray/Validators.cpp:22GL_VertexArray.cpp:536GL_Texture.cpp:406)。 关键时序:InitCapabilities() 懒执行在第一次成功的 eglMakeCurrent 内部BackendObject.cpp:341-347),DirectGLES 在那里才改写 advertised extension stringBackendObject_DirectGLES.cpp:786-796)。

(c) BufferBackendOps — 7 个 hookBufferState/BufferObject.h:76-121,注册入口 :124

DirectGLES 注册 7/7Managers.cpp:1336-1345),DirectVulkan 注册 6/7(无 ResidentSubDataVkBufferManager.cpp:104-111)。 AcquirePersistentMap:112把 GPU 内存裸指针交给应用TryAdoptLargeStorageBufferObject.cpp:167-176kLargeBufferAdoptBytes = 16MiB)在 store 定义时单方面采纳。理由块 BufferObject.cpp:153-166MC 26.3 的 128MB chunk arena,实测 p99 163→21ms、40→115fps、省 ~400MB。

(d) 状态拉取 — 293 个 pGLContext->DirectGLES 124 / DirectVulkan 169+ ~90 个前端对象 getter

PrepareForDrawDirectGLES.cpp:2916-2976)与 SetupDrawVulkanRenderer.cpp:6371)在这里把整个 GLContext 拉出来。这一面在本设计中不过线。

(e) backend → frontend 写回(25 个语义点 / 14 个类)

MarkGpuWritten ×3、WritebackFromBackend ×7、MarkStorageDirty ×11、SetBackendResource ×2、AllocateStorage ×1、RecordError ×2,加两处 shadow MemcpyDirectGLES.cpp:6861 生成 mip、:7144 CopyImage 镜像);DirectVulkan 另有 SetBackendHashMemo/SetBackendStateMemo存后端堆裸指针/SetBackendAuxMemo/EnsureGpuResidentStorage/InvalidateCompileEnv/SwapchainObject.cpp:276-331 改写 default-FBO 占位纹理。 replica 模型下这 25 处大部分落在 server 自己的 replica 上,但其中三类是"语义建立者",client 必须自己做一遍(§5.6、§5.6a)。

(f) backend 反向进 MG_Impl — 恰好 6 处

DirectGLES.cpp:1917,2838,2867,9675pDefaultFramebufferInfo)、SwapchainObject.cpp:276VulkanRenderer.cpp:10700CopyTextureImageToClientOrPBO_State)。replica 模型下全部正常解析(server 也链接完整 MG_Impl)。 但注意:MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo 全库 22 处引用,client 侧 MG_Impl 也在读(GL_Framebuffer.cpp:495,1827,1837,1897,1905,1913,1927,1936,2549,2590,2598,2608,2611)。它是第二个进程全局inproc 模式下必须与 pGLContext 一起做角色隔离(§12)。

(g) MG_Impl 在 table 调用旁做的 MG_State mutation上一版遗漏的第七个面

GLFunctionsTable 是一个命令边界,不是一个状态边界的两侧对称点:MG_Impl 在调 table 之前/之后还会自己改 MG_State,而这些改动 applier 只 replay table 是拿不到的。已确认的两族:

  1. glGenerateMipmap / glGenerateTextureMipmap / 自动 mipmapGLImpl::GenerateMipmapGL_Texture.cpp:6681-6699)在 GenerateMipmap_Backend 之前EnsureGeneratedMipmapStorageAllocated(*mipmapTexture)GL_Texture.cpp:501-541),后者对 level 1..N 做 AllocateStorageMarkStorageDirty(...,false):528)、TruncateMipmapLevels:533)、BumpContentVersion():538)。:534-537 的注释写明了这个 version bump 存在的理由:没有它,"a cached sampled VkImageView built for the pre-generate level range would otherwise stay stale and clamp LOD>0 sampling to mip 0"。只 replay table 的 applier 会在 replica 上精确复现这个已知 bugGenerateTextureMipmap:6702-6711)和 MaybeAutoGenerateMipmap:1625-1635)同形。
  2. Transform feedback CPU 计数AccountTransformFeedbackPrimitivesGL_Drawing.cpp:172-236)在每个被捕获的 draw 上改 6 个 GLContext 计数器:AddTransformFeedbackPausedPrimitives:177)、AddTransformFeedbackInputPrimitives:184)、AddTransformFeedbackGeometryCaptureDraw:214)、AddTransformFeedbackPrimitives:231)、AddTransformFeedbackCapturedVertices:232)、AddTransformFeedbackAccountedCaptureDraw:237)。DirectGLES 在 DirectGLES.cpp:900GetTransformFeedbackCapturedVertices() 来给 scattered capture 定容量;DirectVulkan 在 DirectVulkan.cpp:1384GetTransformFeedbackPausedPrimitiveCounter() 并在 :1337 把前端 delta 折进 query 结果。这些计数器没有版本号,也不在任何 accessor 的门控里;replica 上它们恒为 0 → scattered XFB 什么都不捕、PRIMITIVES_WRITTEN/PRIMITIVES_GENERATED 错。它们还在 XFB 对象绑定时按对象存取(Core.cpp:1273,1296Core.h:313-357),所以简单"发个标量"的补丁必须跟着对象切换走。

§5.9 的覆盖生成器抓不到这一类:它扫 MG_Backend/** 的 READ 面,所以 backend 读 GetTransformFeedbackCapturedVertices 会被正常分类并通过,而 MG_Impl 那半个生产者从来没被审计过。所以 §5.9 必须有第二个生成器(见 §5.9b)。

(h) 工作树污染(Phase 0 必须先清)

DirectGLES.cpp:640-663Managers.cpp:875-877未提交的 per-draw fprintf(stderr)(格式串里还有字面量 ' + NL + ',且位于 pendingMutex 临界区内的 buffer flush 路径上)。Feat/CS-Delta-IPCd96be9f3 提交过同类东西(DirectGLES.cpp:+2583-2590),导致该分支上每一次测量144-failure Windows run、OpenRA ssim=0.000036 设备 run)都跑在每 draw 一次 stderr 写的构建上。


3. 目标架构总览

┌───────────────────────── CLIENT 进程 (libMobileGL.so) ─────────────────────────┐
│  App / LWJGL                                                                   │
│      │ gl*                                                                     │
│      ▼                                                                         │
│  MG_Impl  (validate → RecordError → 调 MG_State mutator)   ← glGetError 本地   │
│      │                                                                         │
│      ▼                                                                         │
│  MG_State::pGLContext  (权威状态 + ShaderCompilePool + glslang)                │
│      │                                                                         │
│      ├─ gBackendFunctionsTable = EmitTable      ─┐                             │
│      ├─ pActiveBackendObject   = BackendObject_Remote (+ CapsMirror)           │
│      └─ SetBufferBackendOps(&g_emitBufferOps)   ─┤                             │
│                                                  ▼                             │
│                                     MG_Remote::WireMirror                      │
│                              (① PublishImplicitStatepersistent-map 推送、    │
│                                  MarkGpuWritten 保守置位、XFB 计数、mip 分配   │
│                               ② 读版本计数器 → 决定发什么                      │
│                               ③ 清 dirty flag / 记录 shipped 水位)             │
│                                                  │                             │
└──────────────────────────────────────────────────┼─────────────────────────────┘
        SEG_CMD (SPSC ring, POD 记录)  ────────────┤  写
        SEG_STAGE (bulk 字节 ring, 独立游标)  ─────┤  写
        SEG_SHADOW[n] (P4.5+, client 拥有)────────┤  RW
        RingControl (一条 cache line 的 atomics)  ◄─┤  读 watermarkacquire load
                                                   ├─ producerParked ──► server 敲门铃
        SEG_REPLY / SEG_EVENT (server 拥有)       ◄─┘  读(在每个等待循环里排空)
        CTRL socket (socketpair / 继承 overlapped pipe): FlatBuffers table
                                                        + SCM_RIGHTS + 双向 doorbell
┌──────────────────────────────────────────────────┼─────────────────────────────┐
│  MobileGLServer (dlopen libMobileGL.so → mobilegl_server_main)                 │
│   thread mgl-srv-io   : asioframingfd 传递,doorbell,控制面 RPC            │
│   thread mgl-srv-apply: 终身持有 EGL/Vulkan context(可绑大核)                 │
│        │                                                                       │
│        ▼  Applier::Apply(RecHeader)  →  MG_State mutator / 共享 helper /       │
│                                          GLFunctionsTable                      │
│   MG_State::pGLContext  (replica)                                              │
│        ▲                                                                       │
│        │ 293 次 pGLContext-> + ~90 getter**零改动**                          │
│   MG_Backend (DirectGLES / DirectVulkan) + MG_Util(SPIRV-Cross, 转译缓存)      │
│        │                                                                       │
│        ▼  真实 GLES / Vulkan 驱动                                              │
└────────────────────────────────────────────────────────────────────────────────┘

三种运行模式(MOBILEGL_TRANSPORT):monolith(默认,编译期折叠)、inproc(同进程第二个 GLContext + apply 线程,需要 MOBILEGL_BUILD_DISAGGREGATED_INPROC)、spawn / unix:<path> / pipe:<name>(真跨进程,出货形态)。


4. 边界定义(每个面变成什么)

变成
(a) GLFunctionsTable MG_Remote::Client::MakeEmitTable() 返回的发射表。61 项 = 先跑 PublishImplicitState、再追加一条定长记录、返回;5 项 request/reply4 项分配类 kNeedsAck(§5.6c);GetIntegeri_v 由 CapsMirror 本地回答;GetInteger64i_v/GetProgramiv 从 wire 与 table 中删除(并提议在 dev 上删掉两个 backend 的实现)。7 个携带 SharedPtr 的项转成 WireHandleDirectVulkan 未注册的 8 个槽由 CapsSnapshot.tableSlotMask 精确复现GL_Query.cpp:471,545,768 拿槽位空否当能力探测)。
(b) BackendObject 虚函数 BackendObject_Remote。8 个 EGL 生命周期虚函数 → SurfaceOp RPC(前 4 个阻塞,因为返回 Bool)。GetDynamicParameters(45)/GetRendererInfo(8)/GetFormatCapabilities(4)/GetBackendType(3)/GetBackendAPIVersionStringCapsMirror 本地,零 round tripDynamicBackendParametersBackendObject.h:299-521)是 flat POD,逐字节传。先例:CompileEnvCompileEnv.h:28-45)就是为同一个原因做的同一件事。GetBackendType() 返回远端类型,所以 GL_Texture.cpp:6453-6457CompileEnv.cpp:122GL_Framebuffer.cpp:38 全部照旧。
(c) BufferBackendOps g_emitBufferOpsRespecify/SubData/FlushMappedRange/OnDestroy → 记录;ResidentSubData P7 之前不注册(只有 adopted store 才可达);AcquirePersistentMap → P1-6 返回 nullptrP7 返回 AdoptSeg 映射基址;ReadbackFromGpu → 阻塞请求(monolith 里本来就是 glFinish()Managers.cpp:1246)。
(d) 状态拉取 不过线。 backend 读 server 自己的 replica。
(e) backend→frontend 写 大部分落在 replica 上;三类"语义建立者"由 client 自己做(MarkGpuWritten、纹理 dirty 清除、persistent-map 推送);六类需要事件回传(§5.6)。per-row WritebackFromBackend 循环(Utils.cpp:2342DirectGLES.cpp:7633)在 server 内部执行,永远不会变成"每扫描线一次 IPC"。
(f) MG_Impl 反向调用 server 链接完整 MG_Impl6 处照常解析。default-FBO 描述通过 EvDefaultFramebufferInfo 事件回传给 clientSwapchainObject.cpp:276-331 的写在 server 侧发生)。
(g) MG_Impl 在 table 旁的 mutation 抽成 client/server 共享 helperMG_Remote::Shared::),applier 在 replay 对应记录时调同一个 helper;或作为显式记录下发。由 §5.9b 的生成器强制全覆盖。

5. 状态 delta 模型

5.0 决定:replica GLContext vs 重写 backend

选 replica。 三条不可协商的证据:

  1. 身份键 memo 无 delta 对应物。 UnitTextureSyncEntry 借用 binding slot 的 shared_ptr 地址DirectGLES.cpp:1463-1467),PairingsIntact 再校验 entry.slot->get() != entry.texture:1472-1477)——注释明说没有它"replay 会拿纹理 B 的前端状态驱动纹理 A 的后端 twin"。IsBufferDrawCleanManagers.cpp:1435-1436)第一句就是资源裸指针身份比较。DirectVulkan 13 个缓存同类。replica 里这些逐字工作,因为对象仍由 SharedPtr 持有、unit 数组仍按值存放。
  2. 回绕计数器。 FramebufferBindingSlot::GetVersion()FramebufferObject::GetObjectVersion()VAO::GetIndexBufferBindingSlot().GetVersion() 都是 Uint16 回绕,只有配合指针身份才正确。replay 让两侧跑同一段回绕逻辑。
  3. backend 自有 generation 表达的是"驱动对象被重新铸造"g_bufferBackendIdGenerationg_attachmentBackendIdGeneration),任何 client delta 都无法承载——它们本来就该纯 server 侧,replica 天然满足。

代价:server 进程要链接 MG_State + MG_Impl + MG_UtilSPIRV-Cross、转译缓存、格式处理器、POST 探针)。这本来就无法避免——BackendProgramObjectImpl::TranspileSpirvToEsslManagers.cpp:6575-7110)在 draw 线程跑 SPIRV-CrossUniformManager.cpp:1418-1497 构造真实 TextureObjectVulkanRenderer.cpp:4211-4356ShaderObject::Compile()/ProgramObject::Link(false)。"thin server"在这个代码库里是伪命题。

replica 模型的边界必须明确写出来(R1 的真正内容)replica 只保证"对 backend 可见的状态"与 client 一致。凡是 client 的入口点MG_Impl)在调 table 之外还做过的 MG_State 改动,applier 必须显式复刻——这不是理论风险,是 §2(g) 已经确认的两族实例。§5.9b 把它变成编译期门。

5.1 reconcile 在哪里发生

MobileGL/MG_Remote/Client/WireMirror.{h,cpp},在发射点运行:每个 GLFunctionsTable 命令、Present、任何阻塞请求。

每个发射点分三步,顺序不可换:

步骤 ①:PublishImplicitState(scope) —— 复刻 backend 在 monolith 里会做的隐式发布,必须在读任何版本计数器之前跑,因为它自己会 bump 版本:

  • 对 scope 内每个 live persistent-mapped buffer 调 client 侧的推送(§5.10)。
  • 对 draw/dispatch scope,保守置 MarkGpuWritten():镜像 MarkShaderStorageBuffersGpuWrittenDirectGLES.cpp:459-467,走 GetTouchedBufferBindingPointCount(ShaderStorage) + GetBufferBindingPoint)、SyncAtomicCounterBuffers:509、以及可写 image-buffer 纹理的 :1809。XFB active 时对每个 capture target 同样置位(镜像 VulkanRenderer.cpp:11210)。
  • 对 draw scope,若 XFB active,跑共享的 AccountTransformFeedbackPrimitives helper(§2(g)-2monolith 里这一步本来就在 MG_Impl 里,拆分后它继续在 client 跑,同时把结果作为 RecXfbAccounting 下发给 replica)。
  • GenerateMipmap scopeEnsureGeneratedMipmapStorageAllocated 本来就在 client 的 MG_Impl 里跑过了;WireMirror 只需把它产生的 level 分配 + TruncateMipmapLevels + BumpContentVersion 作为 RecGenerateMipmapLevels 下发(§5.6a)。

步骤 ②:可达性遍历。 这就是 DirectGLES::PrepareForDrawDirectGLES.cpp:2916-2976)的遍历,把 sync 换成 emit——不是比喻,是同一集合、同一顺序、同一门控:

  1. GetBoundVertexArray()GetConfigVersion();其 enabled attribute 的 BufferObjectindex buffer slot版本 + 裸指针身份)。
  2. GetProgramForDraw()(在 client 侧 join compile pool,与今天一致)→ link/UBO-content/block-binding/SSBO-override 版本。composite pipeline 见 §5.7。
  3. texture unit [0, GetMaxTouchedTextureUnit()],门控 GetTextureBindGeneration();每纹理 GetContentVersion()/GetTextureParamsVersion();每 unit GetSamplerObject()->GetVersion()
  4. image unit [0, imageHighWater]GetImageTextureBinding(unit)
  5. 每 target 的 buffer binding point,上界 GetTouchedBufferBindingPointCount(target)
  6. draw/read FBO,门控 slot version + GetObjectVersion() + GetAllFramebufferAttachmentVersions(),再逐 attachmentattachment 若是 renderbuffer,另查 RenderbufferObject::GetVersion()P0 新增,见 §5.4)。
  7. GetRenderStateParameters(),门控 render-state 版本。
  8. pack pixel-storebackend 从不读 unpack;六个读点全部传 falseDirectGLES.cpp:6129,7614,9101,9480Utils.cpp:2301VulkanRenderer.cpp:10622ScopedDefaultUnpackState 强制默认值,Managers.cpp:2888-2910)。

步骤 ③:清消费型状态。 对本次发射的每个纹理 level 调 MarkStorageDirty(uploadTarget, level, false)(§5.6a)。

因为 backend 自身这套门控已被证明有界且便宜,reconciler 的每 draw 成本形状是已知的,不是估计。

存储:

// MobileGL/MG_Remote/Client/WireMirror.h
struct ShipRecord {              // 40 B
    Uint64 shippedA, shippedB, shippedC;  // 打包版本元组,按 kind 解释
    Uint32 flags;                         // Created | Published | Deleted | ServerAuthoritative
    Uint32 pad;
};
class WireMirror {
    ska::flat_hash_map<Uint64 /*lifetimeId*/, ShipRecord> m_ship;
    struct DrawKeys { Uint64 contextId, samplingGen, bindGen; Int maxUnit; } m_lastDrawKeys;
    // ① 的输入:只遍历真正 mapped / 真正可能被 GPU 写的对象,不是全表
    ska::flat_hash_set<MG_State::GLState::BufferObject*> m_livePersistentMaps;
    ska::flat_hash_map<Uint64 /*lifetimeId*/, Uint64 /*emitSeq*/> m_gpuWritePendingSeq;
public:
    void PublishImplicitState(EmitScope, RingProducer&);
    void ReconcileForDraw(RingProducer&);      // 上面 1-8
    void ReconcileForDispatch(RingProducer&);
    void ReconcileForClear(RingProducer&);
    void OnObjectCreated(ObjKind, Uint32 name, Uint64 lifetimeId);
    void OnObjectDestroyed(ObjKind, Uint64 lifetimeId);
    void OnBufferMapped(BufferObject&, Range1D, BufferMappingAccess);
    void OnBufferUnmapped(BufferObject&);
};

外加与 backend DrawTextureSyncKeysDirectGLES.cpp:1496-1518)同键的 per-draw memo:状态未变的重复 draw 只花 ~10 次整数比较就追加一条 32 字节记录。

5.2 版本计数器不上线

applier 不设置版本,它 replay mutation,所以 replica 的计数器恰在 applier 改动了东西时 bump——恰是 backend 必须重新 sync 的时刻。

  • 不需要给 RenderState/ProgramObjectInstall* setterFeat/CS-Delta-IPCb50f3348 加了,代价是把 RenderState.h 的私有成员漏成 public)。
  • 不需要在 wire 上维护回绕 Uint16 的单调性。
  • 唯一残留风险是过度失效replica bump 了而 client 没 bump。只要 applier 只做 client 明确下发的 mutation,就不会发生;RenderStateBlob 整块下发是唯一例外(它整块 bump m_version,与 client 自己的 bump 等价)。

5.3 触发器 → delta 对照表

Client 触发器(accessor / 事件) Delta 记录
BufferObject::GetChangeSerial() + emit-ops 里排队的 range RecBufferRespecify / RecBufferSubData / RecBufferFlushRange
glMapBuffer* / glUnmapBuffer新增 RecBufferMap{handle, range, accessFlags} / RecBufferUnmap{handle}
persistent-map 脏块(新增,§5.10 RecBufferSubData(块粒度)
MipmapStorage::IsStorageDirty(target,level), GetContentVersion() RecTexAllocLevel / RecTexSubImageunion box 或 ≤96 rects,变长)
glGenerateMipmap 前的 level 分配(新增 RecGenerateMipmapLevels{handle, target, requiredLevelCount, bytesPerTexel}
ITextureObject::GetTextureParamsVersion() RecTexParam
ITextureObject::GetViewStorageOwner() + view 字段 RecTexView必须先于 owner 的任何 re-mint 顺序到达
SamplerObject::GetVersion() RecSamplerParam
RenderbufferObject::GetVersion()P0 新增 RecRenderbufferStorage{handle, internalFormat, w, h, samples}
VertexArrayObject::GetConfigVersion() + 每 attrib Switch/Format/Buffer 版本 RecVaoConfig(变长,整份配置;P6 再做逐属性 diff)
index buffer slot version + 指针身份 RecVaoIndexBuffer
FramebufferObject::GetObjectVersion() + attachment 版本 RecFboAttach / RecFboDrawBuffers / RecFboReadBuffer
RenderState::m_version / m_pipelineStateVersion RecRenderStateBlob(整个 trivially-copyable RenderStateParametersRenderState.h:517-535
ProgramObject::GetLinkVersion() RecProgramLinkOp(P1-4) → RecProgramPublish(P5+)
ProgramObject::GetUBOContentVersion() RecProgramUboContent
block-binding / SSBO-override 版本 RecProgramBlockBinding / RecProgramSsboBinding
GetProgramForDraw() 解析出 composite RecSetResolvedDrawProgram(§5.7
GetTextureBindGeneration() + unit slot 遍历 RecBindTexture / RecBindSampler / RecActiveTexture
GetTouchedBufferBindingPointCount() 遍历 RecBindBuffer / RecBindBufferRange
GetImageTextureBinding(unit) RecBindImageTexture
pack PixelStoreParameters RecPixelStorePack
XFB active 时的 draw新增 RecXfbAccounting{pausedPrims, inputPrims, prims, capturedVerts, geomDraws, accountedDraws} 增量
GLFunctionsTable 命令 RecDraw* / RecClear* / RecBlit* / RecCopy* / RecDispatch* / RecXfb* / RecPresent

5.4 对象身份、创建/删除顺序

wire handle = WireHandle { kind:u8, glName:u32, lifetimeId:u64 }GetLifetimeId() 永不复用(BufferObject.h:208FramebufferObject.h:158ProgramObject.h:1620VertexArrayObject.h:120SamplerObject.h:141TextureObject.h:83,161)。

RenderbufferObject 既没有 GetLifetimeId() 也没有 GetVersion()(已在 MG_State/GLState/RenderbufferState/RenderbufferObject.h 上确认为零命中)—— Phase 0 两个都补上GetVersion()SamplerObject::GetVersion 的同款形状(Uint16,每次 RenderbufferStorage* bump),并在 §5.1 步骤②-6 的 per-attachment 遍历里读它。理由:BackendRenderbufferObject::SyncToBackendManagers.cpp:~8620-8700)缓存 {internalFormat,width,height,samples},而对一个已 attach 的 renderbuffer 重新 glRenderbufferStorageMultisample 不必然 bump GetAllFramebufferAttachmentVersions(),没有 GetVersion() 就没有触发器。

replica 使用与 client 相同的 GL nameapplier 直接 ctx.CreateBufferObject(name),绕过 server 自己的 IndexGenerator。server 侧维护 ska::flat_hash_map<(kind,name), {SharedPtr<T>, clientLifetimeId}>

若某次 create 的 lifetimeId 与记录不符 → Fatal{IdentityDivergence},不做"先销毁再创建"的修复。 上一版的"先销毁"是错的:replica 上那个对象可能仍被 FBO attachment、binding slot、texture viewGetViewStorageOwner)或 XFB capture target 通过 SharedPtr 合法持有,GL 保证它活到最后一个引用消失;强行销毁要么留下悬挂引用要么静默 detach,把一个协议 bug 变成一个会被归咎于 backend 的渲染 bug。协议正确时这个分支不可达,所以响亮地停下来严格优于静默的破坏性修复(MOBILEGL_IPC_RESPAWN=1 时改为强制 ResyncSnapshot)。

这就是 packed_pixels 的教训(身份 + 计数器,绝不单靠计数器)在协议层的应用,也是本设计对 name 空间漂移的结构性预防(而非事后 checksum 检测)。

创建/删除在 glGen*/glDelete* 时刻立即发射,顺序即 ring 顺序。client 的 ~BufferObject 触发 emit-ops 的 OnDestroy 追加 RecObjDeleteserver 的 replica ~BufferObject 触发真实Ops_OnDestroy,完成 pooling / 延迟 glDeleteBuffersManagers.cpp:1271-1300)——一行不改。

5.5 合并规则

  1. 版本门控本身就是合并器。 两个发射点之间的 N 次 mutation 折叠成一条 delta;改了又改回去的状态永不上线。
  2. Buffer range 在 per-buffer VecRange1D 里累积(复用 MG_Util/Math/VectorTypes.h:264 已调优的 7% span gap 合并),在该 buffer 的下一个发射点 flush。绝不 union 成整个 buffer——Managers.cpp:860-864 的 postmortem 记录了那样会每帧重拷近乎整个 chunk-mesh arena。
  3. 纹理区域逐字沿用 MipmapStorage::GetDirtyRects,含 summedArea*4 >= unionArea*3 回退(MipmapStorage.cpp:305)。注意代价轴是反的buffer 按字节计价,texture sub-image 按 job 数计价(Managers.cpp:4311-4319~100 rects vs 一个 box 实测 +6ms/frame)。client 下发区域形状union box 或 rect 列表,按 client 自己的 GetDirtyRects 判定),server 的 backend 从自己 replica 的 dirty 状态重新推导上传形状,让已调优的启发式留在付 GPU 代价的那一侧。
  4. RecRenderStateBlob、各类 bindlast-writer-winsreconciler 只发当前值
  5. 命令永不合并、永不重排。

5.6 backend→frontend 写:三种归属

归属
SetBackendResourceSetBackendHashMemo/StateMemo/AuxMemo 纯 server 本地,零 wire 流量
MarkGpuWritten ×3 + EnsureGpuResidentStorage client 保守自建(§5.6b)。server 侧照常在 replica 上置位;EvGpuWritten{handle, ranges[]} 仅作为收窄提示
MarkStorageDirty(…,false) ×11 client 在发射后自己清(§5.6a)。server 侧照常在 replica 上清
MarkStorageDirty(…,true)Managers.cpp:2813 RequireImageBindableStorage 的 re-dirty 纯 server 本地:它是 server 的 re-mint 导致的,client 无从预测,也无需知道——重传由 server 自己在 replica 上完成
WritebackFromBackendPBO/XFB server 侧写 replica shadow;合并后的 range 变成 EvBufferWriteback 回传 client
RecordError ×2DirectGLES.cpp:6319Managers.cpp:8679+ DirectVulkan 4 处 分两类(§5.6c):分配类同步 ack,其余走 EvGlError 晚一批可见
AllocateStorage 生成 mipDirectGLES.cpp:6270-6271,6861)、MirrorCopyImageIntoDestinationShadow:7144 per-level serverAuthoritative(§6.6
InvalidateCompileEnvSwapchainObject 改写 default-FBO 占位纹理 事件 EvCompileEnvInvalidate / EvDefaultFramebufferInfo

5.6a 纹理 dirty flagclient 必须清(推翻上一版)

上一版写"client 的 dirty flag 从不被清,已发送状态存在 WireMirror 里"。这是错的:

  • MipmapStorage::MarkDirtyRegionMipmapStorage.cpp:196-233)只要 m_isDirty[level] 为真,就把 incoming union 进 m_dirtyRegions[level]InsertDirtyRect;只有 MarkDirty(level,false):171-189)重置两者。永不清 ⇒ box 单调增长、rect 列表撑满 kMaxDirtyRectsGetDirtyRects 一旦跨过 3/4 阈值就返回 0"用 box"),于是每次动画图集 tick 都传整个 level。
  • ShipRecord 只有三个 Uint64 版本字,无法从中重建区域。
  • MarkDirtyRegion 的 rect 播种分支(:214-221if (!m_isDirty[level]) rects.clear(); else if (rects.empty() && !region.Empty()) rects.push_back(region);)本身就是为"有人会清"写的。

好消息是清是安全的:MG_Impl 里没有任何 IsStorageDirty( / GetStorageDirtyRects( / GetStorageDirtyRegion( 调用点(已 grep 确认为零),前端从不读自己的 dirty 状态;它自己也在五处主动清(GL_Texture.cpp:528,701,5547,5621,5691)。

规则WireMirror 在追加纹理记录之后,立刻对该 (target, level) 调 MarkStorageDirty(..., false)。ack 问题按两条收口:

  1. ResyncSnapshot 永远从完好的 shadow 传整 levelshadow 从不被丢弃,除非 buffer 被 adopt——纹理没有 adopt 路径),所以"清早了导致重传丢数据"在 resync 场景不成立。
  2. 硬 drain(§6.5)会 bump ringGenerationdrain 后 client 对所有已发射但未 appliedSeq 覆盖的纹理记录做一次重发(WireMirror 保留最近一批记录的 (handle, target, level) 列表 + emitSeqdrain 时把 seq > appliedSeq 的重新标脏并重发)。这是有界的,因为 ring 里最多只有 ring 容量那么多未 apply 的记录。

5.6b MarkGpuWrittenclient 保守自建(推翻上一版)

monolith 里这个 flag 是在 draw 调用内部同步置位的:MarkShaderStorageBuffersGpuWrittenDirectGLES.cpp:459-467)走 GetTouchedBufferBindingPointCount(ShaderStorage) 并对每个绑定对象 MarkGpuWritten(),从 draw 路径的 SyncNeccessaryBuffers 调用(DirectGLES.cpp:687,697);atomic counter 在 :509;可写 image-buffer 纹理在 :1809DirectVulkan 在 UniformManager.cpp:1073,1229VulkanRenderer.cpp:11210

拆分后 draw 是 fire-and-forget,所以 glDispatchCompute(); glMapBufferRange(SSBO,...,GL_MAP_READ_BIT); 会在 server 还没 apply 前就走完 AcquireMemoryRangeSyncGpuWrites()BufferObject.cpp:454)→ m_gpuWritePending 为 false → 立即 returnBufferObject.cpp:266)→ 应用拿到陈旧 shadow,零 round trip、零报错。这会以"看起来像 flaky"的形式打掉 P4 计划里的 SsboArrayLengthScenarioAtomicCounterScenarioStorageBufferRegrowScenario 一整族。

规则PublishImplicitState(§5.1 步骤①)在每个 draw/dispatch 发射点保守置位,输入与 DirectGLES.cpp:459-467/509/1809 完全一致(client 全都有)。同时把 emitSeq 记进 m_gpuWritePendingSeq。在任一读入口(glMapBuffer*glMapBufferRangeglGetBufferSubDataglGetNamedBufferSubDataglCopyBufferSubData 的源、FillSubData):若该 buffer 在 pending 集合里 → Publish() → 等 appliedSeq >= recordedSeq → 排空 SEG_EVENT → 再读。EvGpuWritten{handle, ranges[]} 只用于取消该 pending 项或收窄 readback 范围,晚到无害。

同时,§7.4 的事件排空点必须补上 glMapBuffer / glMapBufferRange / glGetBufferSubData / glGetNamedBufferSubData——上一版的排空点列表(glGetErrorglGetQueryObject*glClientWaitSynceglSwapBuffers)不含它们。

5.6c GL 错误:分配类同步 ack,其余晚到

上一版把所有 backend RecordError 一律走"晚一批"事件,只给 CTS lane 留 MOBILEGL_IPC_STRICT_ERRORS。这在分配探测这个通用惯用法上是错的:那两个站点(Managers.cpp:8679 renderbuffer 存储、DirectGLES.cpp:6319 纹理操作)报的是 GL_OUT_OF_MEMORY,而应用的标准写法是 glRenderbufferStorage(...); if (glGetError() == GL_OUT_OF_MEMORY) { 用更小的目标重试; }。晚到 ⇒ 应用走成功分支 ⇒ 往一块 server 从未分配的存储上渲染。

规则:只把分配类入口点标 kNeedsAck——glRenderbufferStorage / glRenderbufferStorageMultisample / glNamedRenderbufferStorage*glTexImage* / glTexStorage* / glCopyTexImage* 中 backend 可能失败的形式、glBufferStorage。它们本来就罕见且昂贵,ack 几乎免费,换来 OOM 探测精确。其余全部保持晚到。有了这个划分,MOBILEGL_IPC_STRICT_ERRORS 从"CTS 专用"降级为纯诊断开关(默认 0,出问题时用来判断某个失败是不是错误时序引起的)。

glGetError 本身永远本地(GL_Getter.cpp:2811-2817Core.cpp:48-49 的 "GL error state is GL-thread-owned" 不变式)。

5.7 composite pipeline program

GLContext::GetProgramForDraw()Core.cpp:612-660)在 program-pipeline 路径下:join 每个 stage → ComputeDrawProgramSignature() → cache miss 时 MakeShared<ProgramObject>(0u) 并 link 一个匿名 compositeCore.cpp:644;注释明说"故意不是命名 program……不得占用应用可能拿到的 name"),随后 RefreshCompositeUniforms/MirrorUniformValues 每 draw 改它。

  • Phase 1-4server relink:下发 pipeline 状态(UseProgramStages 等)+ 各 stage program 的 RecProgramLinkOpserver 的 replica 自己走同一路径构建自己的 composite。加一条 RecResolvedProgramDigest{signature, reflectionDigest} 让分歧当场暴露。
  • Phase 5+ProgramPublishserver 没有源码,不得 link。client 解析 composite,把它作为保留高位 handle 的合成 program 发布(RecProgramPublish + RecSetResolvedDrawProgram{handle})。MG_State 加:
// MobileGL/MG_State/GLState/Core.h  (整段 #if MOBILEGL_BUILD_DISAGGREGATED 包裹,保证 monolith 字节不变)
void SetReplicaResolvedDrawProgram(SharedPtr<ProgramObject>);
void SetReplicaResolvedDispatchProgram(SharedPtr<ProgramObject>);
// GetProgramForDraw()/GetProgramForDispatch() 首行先查该槽位

server 因此永不 link、永不 join compile poolPrepareForDraw 首条语句照常工作。

5.8 全量快照 / resync

稳态没有初始状态transport 在 MG_Backend::Init() 内建立,早于任何 GL 对象存在。

ResyncSnapshot 只服务三件事:server 重启backend context 丢失EGL surface 变更销毁整个原生 context 并 bump g_backendContextGeneration/g_syncContextGenerationDirectGLES.cpp:10664-10676)、硬 drain 后的纹理重发(§5.6a)。实现 = 同一个 reconcile 遍历,关闭"已发送版本"门控。

关键纪律:一个 applier、两个 producer——快照发同样的记录种类,因而被同一套测试覆盖。Feat/CS-Delta-IPC 的结构性错误正是有一个与生产路径零共享代码的平行 applier(StateEmitter.h:312-501 vs ServerCore.cpp:389-401)。

P7 之后的限制adopted store 的字节住在 serverclient 无法重建它们。因此 MOBILEGL_IPC_RESPAWN=1MOBILEGL_IPC_ADOPT_TIER != 2 互斥:要么关采纳换可 resync,要么开采纳并接受 server 死亡 = context lost(不重启)。这条互斥必须在 ConfigLoader 里显式检查并 MGLOG_W

5.9 覆盖度的编译期保证

5.9a READ 面(backend 读了什么)

  1. scripts/gen_backend_state_surface.py 扫描 MG_Backend/**,抽出 pGLContext->X 与前端对象 getter,生成 MG_Remote/Protocol/generated/BackendStateSurface.inc已提交)。相对 Feat/CS-Delta-IPCextract_backend_read_inventory.py删掉 GetBuffer*/GetTexture*/GetProgram*/GetVertex* 前缀兜底规则:234-241,它把"0 UNMAPPED"制造出来),未知 accessor 一律 UNMAPPED。同时把"真 pull point"与"signature handle 化"分开统计(那 167 个 "handle-ify" 里含 BackendObject.h:158-186声明DirectGLES.cpp:55 的静态全局)。
  2. 手维护 MG_Remote/Protocol/Coverage.defaccessor → 记录种类 | MGL_COVER_LOCAL | MGL_COVER_NA(理由字符串)
  3. MG_Remote/Client/CoverageAssert.cpp 同时 include 两者,未映射 accessor → #error

5.9b MUTATOR 面(MG_Impl 在 table 调用旁改了什么)—— 本轮新增,是 §2(g) 的门

  1. scripts/gen_impl_mutation_surface.py 扫描 MG_Impl/**:找出同时包含 gBackendFunctionsTable.GL.*pActiveBackendObject-> 调用 pGLContext-> mutator 调用(写方法:Add*/Set*/Mark*/Bump*/Allocate*/Truncate*/Record*/Notify*/Begin*/End*)的函数,把每个 mutator 站点写进 MG_Remote/Protocol/generated/ImplMutationSurface.inc已提交)。为避免误报,脚本对每个函数做一次简单的调用图一层展开(EnsureGeneratedMipmapStorageAllocated 这种 helper 会被计入调用它的 GenerateMipmap)。
  2. 手维护 MG_Remote/Protocol/MutationCoverage.def函数::mutator → MGL_MUT_REPLAYED_BY(记录种类) | MGL_MUT_SHARED_HELPER(helper 名) | MGL_MUT_CLIENT_ONLY(理由) | MGL_MUT_NA(理由)
  3. 同一个 CoverageAssert.cpp 展开两张表,未映射站点 → #error

已知必须在第一轮映射的条目(不是穷举,是脚本首次运行时保证不为空的锚点):

  • GenerateMipmap / GenerateTextureMipmap / MaybeAutoGenerateMipmapEnsureGeneratedMipmapStorageAllocatedAllocateStorage / MarkStorageDirty(false) / TruncateMipmapLevels / BumpContentVersionMGL_MUT_REPLAYED_BY(RecGenerateMipmapLevels)。applier 收到该记录后调同一个共享 helper(把 EnsureGeneratedMipmapStorageAllocated 抽到 MG_Remote::Shared:: 或让 applier 直接调 MG_Impl::GLImpl::TextureImpl:: 里那个已存在的函数——server 链接完整 MG_Impl,这是可行且最省的做法)。
  • DrawArrays/DrawElements/… 的 AccountTransformFeedbackPrimitives 六个计数器 ⇒ MGL_MUT_REPLAYED_BY(RecXfbAccounting)applier 把六个增量加到 replica 的对应计数器上;必须跟着 RecBindTransformFeedback 的对象切换走,因为它们按 XFB 对象存取,Core.cpp:1273,1296)。
  • glCopyTexSubImage*CopyReadFramebufferIntoMipmapRegionMarkStorageDirty(...,true)GL_Texture.cpp:1095)⇒ MGL_MUT_CLIENT_ONLY(该函数整体留在 client,见 §6.6)。
  • glClearTexImageMarkStorageDirty(...,true)GL_Texture.cpp:1005)⇒ MGL_MUT_CLIENT_ONLY(同上)。
  • GL_Query.cpp 的 conditional-render 布尔与查询结果缓存 ⇒ MGL_MUT_CLIENT_ONLY

CI:两个生成器都重新生成 + git diff --exit-code

backend 长出一个 reconciler 走不到的 read,或 MG_Impl 长出一个 applier 没 replay 的 mutation → 编译失败,而不是设备回归。

5.10 persistent mapclient 侧的推送(本轮新增的独立小节)

问题(已在仓库确认):BufferObject::SyncPersistentMappedRange()BufferObject.cpp:238-250)依次早退于 GPU-resident、非 Persistent、非 Write、FlushExplicit、空 range,剩下的情况(persistent + write + coherent + shadow-backed)走 NotifySubData(整个 mapped range)。它的全部生产调用点都在 MG_Backend/ 里(19 处,见 §0)。P1-P6 默认关采纳(§6.8 T2),AcquireMemoryRangeBufferObject.cpp:459-475)于是回退到 shadow 并把 m_resource.Bytes() + range.start 交给应用——应用之后不再调任何 GL 函数就直接写。拆分后:client 没人推,server 的 replica m_isMapped==false 第一行就 return。字节丢失。

另外,IsBufferDrawCleanif (frontend->IsMapped()) return false;Managers.cpp:1447,注释:"A live non-zero-copy map may owe a per-draw SyncPersistentMappedRange push")也依赖 map 位,replica 上恒 false 会把这个 buffer 判成 clean 而跳过整个同步。

解法三件套

  1. map/unmap 上线RecBufferMap{handle, rangeStart, rangeEnd, accessFlags}RecBufferUnmap{handle},从 glMapBuffer/glMapBufferRange/glUnmapBuffer/glFlushMappedBufferRange 的 MG_Impl 入口发射(emit-ops 的 FlushMappedRange 已覆盖最后一个)。replica 的 m_isMapped/m_mappedRange/m_mappingAccess 于是与 client 一致,IsMapped() 门和 server 侧的 SyncPersistentMappedRange 都恢复 monolith 行为。

  2. client 侧脏块推送WireMirror 维护 m_livePersistentMaps(只装 persistent+write+非-FlushExplicit+非-GpuResident 的 buffer,进出由 OnBufferMapped/OnBufferUnmapped 维护)。PublishImplicitState本次操作可达的每个这类 bufferVAO attribute buffer、index buffer、indirect/parameter buffer、UBO/SSBO/atomic binding point、XFB capture target——即 backend 那 19 个调用点的并集)做块粒度发送:把 mapped span 切成 64KiB 块,只发自上次发送以来被改过的块。

    "被改过"的判定:P1-4 用保守版(每个发射点把该 buffer 的整个 mapped span 当脏,但按块拆成多条 RecBufferSubData,让 §6.5 的 range 合并与 ring 复用机制生效);P4.5 shadow-in-shm 落地后升级为精确版shadow 住在 client 拥有的 SEG_SHADOW 里,用与 WAR 水位同一套 64KiB 块脏位跟踪;块脏位由 SyncPersistentMappedRange 的调用点触发一次 memcmp 或由 mprotect 写屏障提供——先做 memcmp,它对 1MB 块是 ~50µs 量级,且只在真正 mapped 的 buffer 上跑)。

    这是 §6.4 拷贝表里上一版完全没有的一行,且在 P1-4 的保守版下代价可观(一个持久映射的 chunk arena 会在每个可达发射点重传整个 mapped span)。所以:MOBILEGL_IPC_PERSISTENT_BLOCK_KB(默认 64)可调,且P1 验收必须记录这条路径的字节量Tracy 计数器分类为 persistent-map-push)。若 P1-4 的保守版在 Create/Flywheel fixture 上不可接受,把 P4.5 的精确版提前到 P2(这是计划里唯一一个允许因测量结果而改变阶段顺序的地方)。

  3. P1 就要有门:新增 PersistentCoherentMapScenariomap PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验),列为 P1 验收项。今天计划里没有任何门能抓到这个 bug。

MOBILEGL_COHERENT_AS_FLUSH 的关系:该开关(GL_Buffer.cpp:297-305,默认 falseConfig.h:174 / ConfigLoader.cpp:185)把应用请求的 persistent+FLUSH_EXPLICIT 改写成 coherent,从而制造上面这个情形。上一版禁止它在拆分模式下生效——但那只处理了"我们自己改写出来的 coherent map",没处理"应用自己就请求 coherent"。有了上面的三件套,两种来源都被覆盖,所以禁令改为可选MOBILEGL_COHERENT_AS_FLUSH 在拆分模式下照常生效,这样 tools/trace_replay/trace_cases.json 里那两个带 coherent_as_flush: true 的用例(minecraft-1.21.1-neoforge-create-indirect-in-worldminecraft-1.21.1-neoforge-create-instancing-in-world)在 split 与 monolith 下走同一条 buffer 路径,P2 的逐名对比才有意义。若 P2 测出保守推送在这两个 fixture 上代价过高,改为"这两个用例在 split 模式下同时关掉该开关,并在报告里标注",而不是让两侧走不同路径还宣称对比通过。


6. 数据面

6.1 段(segment)布局

拥有者 默认大小 内容
SEG_CMD clientserver 只读) 8 MiB2 的幂,64B 对齐 RingControl(4KiB) + POD 记录 + ≤4KiB 内联负载
SEG_STAGE clientserver 只读) 32 MiB → 上限由实测定,不是默认 256 MiB bulk 字节:buffer sub-data、纹理区域、UBO scratch、client 顶点/索引/indirect 数组、persistent-map 脏块
SEG_REPLY serverclient 只读) 8 MiB4KiB slot readback 像素、buffer writeback
SEG_EVENT serverclient 只读) 256 KiB SPSC ring EvQueryResult/EvGpuWritten/EvGlError/EvLogLine/EvDefaultFramebufferInfo
SEG_SHADOW[n] clientserver 只读) 每对象,P4.5+,≥256KiB shadow 零拷贝 buffer/texture shadow
SEG_ADOPT[n] serverclient RW 每 bufferP7,≥16MiB adopted store 应用直写 GPU 内存

创建:Android ASharedMemory_createAPI 26android/sharedmem.h:78libc 的 memfd_create wrapper 是 API 30sys/mman.h:196);桌面 Linux syscall(SYS_memfd_create, …)macOS shm_open+shm_unlinkWindows CreateFileMappingW(Local\)。

传递:POSIX SCM_RIGHTS,在第一个 transport commit 里实现asio 无 cmsg API → 在 socket.native_handle() 上裸 sendmsg/recvmsg,约 80 行)。Feat/CS-Delta-IPC 把它推迟到"P6"LocalSocketTransport.h:16-20PollOfferout->fd = -1 硬编码于 :296),结果它的数据面在唯一重要的平台上一个字节都过不去

SEG_SHADOW 块的退休规则(本轮新增):§6.4 的 64KiB 块发送水位只解决"覆盖一个活着的 shadow";它没说怎么释放一个 shadow。glDeleteBuffersglBufferData 重定义会释放/重分配 SEG_SHADOW 的 arena 块,而携带 {segId, offset, size} 指向该块的记录可能还没被 apply——server 于是读到另一个对象的字节。规则:释放的块进入 pending 链表,只有当 appliedSeq(对被借入 GPU 时间线的 slot 是 retiredSeq)越过最后一条引用它的记录之后才归还 arena,而不是在对象析构时立即归还。

6.2 RingControlwatermark 是一条共享 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;         // 被丢弃的 EvLogLine 计数
};

三个 seq 水位严格区分(混为一谈是经典错误):appliedSeq 释放 cmdAppliedTail/stageAppliedTailsubmittedSeq 释放 stagingretiredSeq/completedFrameSerial 释放 *RetiredTailSEG_ADOPT 复用。

两个 tail 是必须的Ops_ResidentSubData 把字节拷进 pendingResidentWritesManagers.cpp:1158-1166),P7 之后 server 会借用 ring slot 而不是再拷一次——那种 slot 只能在 completedFrameSerial 之后回收。单 tail 会在 P7 落地当天变成保守回收。

SEG_STAGE 必须有自己的游标三元组:§7.2 把"SEG_STAGE 余量 < 1/4"列为 Publish 触发器,而第二个 ring 的占用率无法从第一个 ring 的游标算出;且 stage slot 的退休条件(retiredSeq)与 cmd 记录(appliedSeq)不同。

6.2a 双向 doorbell(本轮新增,修 "client 只能自旋" 的缺陷)

  • client → serverconsumer 自旋 ~200µs → 置 consumerParked=1 → 在控制 socket 上阻塞读 1 字节;producer 在 release-store cmdHead 之后,仅当 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 linepresent-credit 等待最长一整帧(60Hz 下 16.6ms),在手机上就是一颗大核满频空转,与 GPU 和游戏 JVM 抢核;§6.5 的"有界 50ms 等待"就是 50ms 自旋。而 MobileGL 全库没有任何亲和性控制(grep -rn 'sched_setaffinity\|cpu_set_t' MobileGL/ 零命中),无法把它赶到小核上。

spawn 模式用 socketpair 的两个方向做 doorbellinproc 模式用一对 std::condition_variable(同一套 producerParked/consumerParked 语义)。零 futex/eventfd/named-event 平台代码asio 已 vendored3rdparty/asio/include 已在主 target 的 include path 上,CMakeLists.txt:483)。

6.3 记录格式

// MobileGL/MG_Remote/Protocol/RecordKinds.h
struct RecHeader { Uint16 kind; Uint16 flags; Uint32 size; };   // 8 Bsize 含 header8 字节倍数
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/记录并消除一整类失步。

X-macro 单一真相源:

// MobileGL/MG_Remote/Protocol/Records.def
#define MGL_REC_LIST(X)                                       \
    X(BindBuffer,      RecBindBuffer,       24)               \
    X(DrawArrays,      RecDrawArrays,       32)               \
    X(DrawElements,    RecDrawElements,     56)               \
    X(BufferSubData,   RecBufferSubData,    64)               \
    X(BufferMap,       RecBufferMap,        40)               \
    X(BufferUnmap,     RecBufferUnmap,      24)               \
    X(RenderStateBlob, RecRenderStateBlob,  40)               \
    X(XfbAccounting,   RecXfbAccounting,    56)               \
    X(GenerateMipmapLevels, RecGenerateMipmapLevels, 32)      \
    X(RenderbufferStorage,  RecRenderbufferStorage,  40)      \
    /* … ~95 项 … */
#define MGL_REC_SIZE_CHECK(name, T, sz) \
    static_assert(sizeof(MobileGL::Wire::T) == (sz), #name " record size drift");
MGL_REC_LIST(MGL_REC_SIZE_CHECK)

每种一条 static_assert ——修掉正是 Feat/CS-Delta-IPC 中过一次的 bug 类(b50f3348"旧的 off-by-one 让 applier 误读 TexImage 之后的每一条 state delta"),而它那条只断言 union 首成员的 assertServerCore.cpp:31-33)永远抓不到中间插入。

运行期边界纪律(本轮新增)SEG_CMD 是对端并发写入的区域,编译期 static_assert 管不到运行期损坏。同一个 X-macro 额外生成 applier 分发前的前置条件:

#define MGL_REC_BOUNDS_CHECK(name, T, sz)                                     \
    case RecKind::name:                                                       \
        if (h.size < (sz) || h.size > remainingRingBytes || (h.size & 7u))    \
            return Fatal(FatalCode::ProtocolCorruption, #name);               \
        break;

kVarTail 记录额外校验 定长前缀 + 尾巴自描述长度 == h.size。违反一律 Fatal{ProtocolCorruption},绝不进入未定义行为。

变长记录(RecVaoConfigRecTexSubImage 的 rect 列表、RecProgramLinkOpRecMultiDrawArgs):kVarTail + 定长前缀 + 自描述长度的内联尾巴。

6.4 WAR 危害与字节稳定性

Phase 1-4 规则:GL 调用时刻把字节拷进 ring slot。 slot 从写入到 stageAppliedTail 越过它为止不可变,client 拿不回它 → 危害按构造消除。代价是一次 memcpy,而 Ops_ResidentSubDataManagers.cpp:1165)和 StageBlocksIntoUnpackRing 在 monolith 里已经在付同样的钱。

Phase 4.5 规则(shadow-in-shm,零拷贝): ≥256KiB 的 shadow 分配在 client 拥有的 SEG_SHADOW 里——PipeResourceMapAlignedAllocatorPipeResource.h:33-60,无状态、25 行、64B 对齐)增加一个 shm arena(保留 MIN_MAP_BUFFER_ALIGNMENT=64 契约,PipeResource.h:28),MipmapStorage 的 level vector 同理。RecBufferSubData 于是只带 {segId, offset, size}client 侧零拷贝。 WAR 用 per-shadow 64KiB 块发送水位:若应用写入某块而该块最后一次发送尚未 appliedSeq 覆盖,这次写走 SEG_STAGE。有界、局部、压力下自动退化成 Phase-1 行为。这套块水位同时是 §5.10 精确版 persistent-map 推送的脏位来源。

该改动必须整段 #if MOBILEGL_BUILD_DISAGGREGATED 包裹PipeResourceMipmapStorage 住在 MG_State,不在 MG_Remote,而改一个容器的 allocator 就改了类型;不包裹的话 §12/D8 的 nm/.text 门会在 P4.5 变红。写法是"分配器特化:option OFF 时逐字折叠成今天的 MapAlignedAllocator"。

拷贝账(更正版,MC pan 一帧约 9MB section mesh + ~1MB UBO scratch

上一版这张表把 monolith 和 split 两侧都数少了。逐条核对:

  • monolith 的 glBufferSubData → shadow store 是 2 次(1) app→shadowBufferObject::UploadSubDataMemcpy),(2) shadow→目的地(FlushPendingRangesNowMemcpy(dst, bufferObject.MappedData()+start, size) 进 invalidating mapManagers.cpp:914;或 Memcpy(g_uploadRing.store.mappedPtr+ringOffset, ..., size) 进 upload ringManagers.cpp:922)。
  • split P1-4 是 4 次app→client shadow (1)、client shadow→SEG_STAGE (2)、applier replay mutator ⇒ SEG_STAGEreplica shadow (3)、server 的 FlushPendingRangesNow ⇒ replica shadow→upload ring (4)。
  • P4.5 只去掉 (2),剩 3 次。它去不掉 (3),因为 SEG_SHADOW 是 client 拥有 / server 只读,而 replica 的 BufferObject 拥有自己的 PipeResource 分配。
路径 monolith P1-4 P4.5 P4.5+replica-adopt(可选,见下)
glBufferSubData → shadow store 2 4 3 2
glBufferSubData → adopted storeP7 2 2
glMapBufferRange(WRITE)+unmap 3 5 4 3
persistent coherent map 推送(§5.10 保守版) 0 2/发射点 1/发射点(精确块) 1/发射点
glTexSubImage 2 3 2 2
全局 UBO / draw 1 2 2 1
adopted ≥16MiBP7 T1/T0 0 0

目标选择(必须在 P4.5 之前拍板)

  • 方案 A(默认,保守):接受 3 次,写进文档。P4.5 的价值是消掉 client 侧那次拷贝与那份重复内存。
  • 方案 B(激进,需额外设计):给 replica 的 PipeResource 增加第三种模式 AdoptedClientShadow——Bytes() 返回 server 映射的 client SEG_SHADOW(只读),applier 的 UploadSubData 退化成一次 range 记账 + change-serial bump,只剩 server 的 ring 拷贝。这保持了 mutator replay 的全部副作用(包括 IsBufferDrawClean 比较的 change serial),只是不搬字节。风险:replica 的 shadow 变成只读会让任何 server 侧写(WritebackFromBackend、生成 mip、CopyImage 镜像)需要就地 copy-on-write 升级回普通 shadow。先按方案 A 实现并测量,方案 B 作为 P6 的候选优化项,由 Tracy 计数器决定是否值得。

无论选哪个,TracyPlot 字节计数器必须装在 wire 两侧client 的 emit 字节 + server 的 apply 字节 + server 的 ring/staging 字节),P4.5 的验收看总量,不是只看 client 一侧的数字。

6.5 Ring 分配与背压

逐字移植 PersistentRingManagers.cpp:657-727RingAllocateSlow :1891-1970RingOnPresent :1975-2016):单调 head/tail、2 的幂掩码、frame mark。分配失败升级:扩容(翻倍) → 对最老未 retire 批次有界等待(默认 50ms,走 §6.2a 的 producerParked doorbell,不是自旋) → 硬 Drain 请求 + ringGeneration bump。generation bump 上线,防止后续记录引用被回收的 offset;硬 drain 之后按 §5.6a 重发未 apply 的纹理记录。

SEG_CMDSEG_STAGE 各自独立跑这套升级(各有自己的游标三元组)。

6.6 纹理

  • Unpack PBO 完全在 client 解析GL_Texture.cpp:1719,1765,1887,1976,2457,2604,2722,4458,6176pixelUnpackBufferObject->MappedData() + (SizeT)pixels,再由 ProcessTexturePixelsDataUnpack 紧密重排)。没有任何纹理像素以 PBO 引用形式过线,server 永远不需要 GL_PIXEL_UNPACK_BUFFER 状态。PixelStoreBlob 只用于 PACK 方向。
  • 压缩纹理永不到达任何 backend(前端在 glTexImage 时把压缩 internalformat 解析成非压缩后备,GL_Texture.cpp:298-306grep -i compress MG_Backend/DirectGLES/*.cpp 只命中一条注释)。逐字节 m_compressedData blob 仅供 glGetCompressedTexImage,纯 client 侧,不过线。
  • glCopyTexSubImage*glClearTexImage 整体留在 client(推翻上一版的 P4 项)。 已确认这两个入口今天就是纯前端操作CopyTexSubImage{1,2,3}D_StateGL_Texture.cpp:3955,3979)调 CopyReadFramebufferIntoMipmapRegion:1044-1097),它借一次 backend ReadPixels 进 CPU scratch:1079)、逐行 memcpy 进 mipmap shadow:1089-1094)、MarkStorageDirty(...,true):1095)。拆分后它恰好是一次阻塞 ReadPixels round trip,产生的脏区按普通纹理 delta 下发——正确,且不需要任何新命令。上一版提议"整体移到 server + EvTexWriteback"是错的:那个事件在 §7.4 的列表里根本不存在(只有 EvBufferWriteback),它仍然要付一次 round tripclient shadow 必须为 glGetTexImage 保持最新),还多出一个 GLFunctionsTable 里没有对应项的命令。glClearTexImageGL_Texture.cpp:985-1006)同形。
  • per-level serverAuthoritative只保留给两处字节确实在 backend 里写进 shadow 的场景:生成 mip 的 CPU 路径(DirectGLES.cpp:6270-6271,6861AllocateStorage + 直写 MapMipmapData)与 MirrorCopyImageIntoDestinationShadow:7144glCopyImageSubData 的目的地镜像)。client 在发射对应命令时对受影响 level 置位。CopyTextureImageToClientOrPBO_State 查它:清 → 本地 shadow 回答,零 round trip(应用自己上传的 level 全走这条);置 → 一次 round trip

6.7 回读

路径 monolith 拆分后
glReadPixels → 客户内存 阻塞 一次 round trip,像素放 SEG_REPLY slotper-row 循环留在 server 内
glReadPixels → pack PBO 也阻塞DirectGLES.cpp:9189-9205 把整个 PBO map 回来写 shadow fire-and-forget + client 侧对该 PBO 置 MarkGpuWritten(§5.6b),代价推迟到之后的 map/read。严格优于 monolith
glGetTexImage/glGetTextureImage DirectGLES 从 client shadow 回答 DirectGLES 零 round trip(除 serverAuthoritative level);DirectVulkan 一次
glGetBufferSubData / glMapBuffer(READ) on gpuWritePending 阻塞(glFinish()Managers.cpp:1246 一次,由 client 侧 pending 集合触发(§5.6b),被 EvGpuWritten{ranges} 收窄
XFB capture writeback glEndTransformFeedback 里无条件无限 ClientWaitSyncGL_Drawing.cpp:1326-1337 不等client 对 capture target 置 MarkGpuWritten,首次读时付;FixupGsStripCaptureOrder 移到 server
glCopyTexSubImage* 内含一次同步 ReadPixels 一次 round trip(保持前端实现不变)

6.8 persistent map 与 ≥16MiB 采纳

三档,由运行时 POST 探针选择(遵循本项目"后端限制一律探针判定、绝不硬编码驱动名"的既定规则):

  • T2 — 拒绝(P1-6 默认,永久正确回退)AcquirePersistentMap 返回 nullptr此档下 §5.10 的 client 侧推送是强制的,否则应用的 coherent persistent 写会丢。
  • T1 — server 导出自己的映射(P7 主攻)server 照常铸造 coherent mapManagers.cpp:988-1058 / VkBufferManager.cpp:515-563),经 VK_KHR_external_memory_fd / AHardwareBuffer_sendHandleToUnixSocketAPI 26hardware_buffer.h:521/ VK_KHR_external_memory_win32 / GL_EXT_memory_object_fd 导出,client mmap 后调 PipeResource::AdoptPersistentMap(base)每 store 生命周期一次 round trip。 采纳成功后 §5.10 的推送对该 buffer 自动停止(SyncPersistentMappedRangeIsGpuResident() 早退),与 monolith 一致。
  • T0 — server 导入 client 分配client 分配 AHardwareBuffer/dma-bufserver 以 GL_EXT_external_buffer+glBufferStorageExternalEXTVK_EXT_external_memory_host 导入。理想但可用性未知。

MOBILEGL_COHERENT_AS_FLUSH 在拆分模式下照常生效(推翻上一版的禁令,理由见 §5.10 结尾):有了 client 侧推送,被改写出来的 coherent map 与应用原生请求的 coherent map 走同一条正确路径,两个 Create/Flywheel fixture 才能在 split 与 monolith 下做同路径对比。

6.9 program artifacts

  • P1-4RecProgramLinkOp{handle, shaderSources[], bindAttribLocations[], bindFragDataLocations[], xfbVaryings[], xfbMode, separable, reflectionDigest} — server 重新 link。只需 5 个 schema 字段,且分歧不可能静默(两半跑同一二进制里的同一段代码)。源码可得:ProgramObject::GetLinkedShaderSnapshot()ProgramObject.h:157)刻意持有 linked shader 的 SharedPtr(注释在 :1716),所以 glDeleteShader 之后源码仍在。
  • reflectionDigest 必须覆盖 backend 实际读的全集xxHash over (uniformName, location, type, typeFacts, samplerOrImageUnitIndex) 全表 + maxUniformLocation + (blockName, blockBinding, blockSize) 全表 + shaderStorageBlockBindingOverrides + PointSizeDemoted + GetLinkedShaderStages + xfbVaryings/xfbStrides/xfbPackedStride/xfbBufferMode + GetGeneratedSpirv() 各 module 的 xxHash。不匹配 → Fatal{ReflectionDivergence}。 (理由:本项目自己的二分历史记录过"glslang 反射/生成顺序是真载重,桌面字节一致是语料受限的假绿"。)
  • P5RecProgramPublish{handle, stages[], spirvBlobs[], reflectionBlobRef}reflection 用 Visit() 式归档
// MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsArchive.h
template <class Ar> void Visit(Ar& ar, LinkArtifacts& a) { ar(a.writtenUniformLocationBits, /*…全字段…*/); }
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
              "新字段请加进 Visit() 并 bump MGL_LINKARTIFACTS_SIZE");

一份字段表服务两个方向 + sizeof 绊线。序列化整个结构体(而非 backend 当前读的 ~40 字段),这样 backend 新增一次 read 永不需要改协议。 安装入口:ProgramObject::InstallPublishedLink(LinkArtifacts&&, SpirvArtifacts&&, linkVersion, imageUnitVersion, backendStateVersion),绕过 m_pendingLink/m_pendingSpirvserver 因此不需要 compile pool

  • relink 路径保留为常驻 oracle 与 A/B 对照(MOBILEGL_IPC_PROGRAM=publish|relink)。
  • 全局 UBO scratch 相反:小、每次 glUniform* 变、有版本 → 走 SEG_STAGE,键 (programHandle, uboContentVersion),复现 monolith 的"每 program 每帧至多一次"DirectGLES.cpp:3369-3392)。

6.10 应用指针(四类,范围全部可算)

范围 站点
client 顶点数组(仅 DrawArrays 族) (first+count-1)*stride + elementSize Managers.cpp:2560VulkanRenderer.cpp:3737
client 索引数组 count * indexSize DirectGLES.cpp:4436VulkanRenderer.cpp:4081
client indirect / parameter 块 stride*(drawcount-1)+cmdSize DirectGLES.cpp:276DirectVulkan.cpp:303
MultiDraw* 参数数组、ClearBuffer* value drawcount*4、16B DirectVulkan.cpp:963-1057

唯一无界的是索引 draw 下的 client 顶点数组:索引扫描(TryComputeMaxIndexFromHostBytesVulkanRenderer.cpp:3406-3470)必须在 client 侧跑,只有 client 同时持有两个数组。实现于 MG_Remote/Client/ClientArrayBounds.cpp,两个 backend 共用。

陈旧索引危害(本轮新增):monolith 在每次这类扫描之前都调 indexBuffer->SyncGpuWrites()DirectGLES.cpp:4413MultiDraw.cpp:499VulkanRenderer.cpp:3431,4159),因为 EBO 可能刚被 compute shader 或 XFB 写过。client 侧扫的是 client shadow,若不做同样的强制回读,算出的 maxIndex 来自陈旧字节,顶点数组会被少拷 → 几何缺失/花屏,或越界读应用数组。同样的暴露面还有 primitive-restart 重写(DirectGLES.cpp:4412-4414)与 *IndirectCount 的 parameter buffer 读(DirectGLES.cpp:4666-4693,4768-4793)。

规则ClientArrayBounds、restart 重写、indirect-count 读者在触碰 shadow 之前,必须走 §5.6b 的 pending 检查(Publish + 等 appliedSeq + 排空事件),即 monolith 里 SyncGpuWrites() 所在的同一个位置。P2 增加 ClientArrayAfterComputeWriteScenario 作为门。

draw 记录里 indicesAreClient 由"是否绑定了 element array buffer"决定(DirectGLES.cpp:4423 vs :4425-4442),在 binding 所在的一侧判定。


7. 控制面

7.1 FlatBuffers 用法

一份 schema MobileGL/MG_Remote/Protocol/protocol.fbs,两种用法:

  • 热路径 → FlatBuffers struct(flatc 保证定长布局、无 vtable、无偏移间接、无需 verifier walk,只需边界检查),直接放进 ring:[RecHeader | struct | 可选变长尾]DrawArrays = 8+24 = 32B(对比 table-per-command 的 ~60B 与一次 vtable 遍历)。这正是 Feat/CS-Delta-IPC 自己的 plan 第 55 行要求而实现没做的事。
  • 罕见/变长/需演进 → FlatBuffers table,走 CTRL socket。
namespace MobileGL.Wire;

// ---------- 热路径 struct(进 ring----------
struct WireHandle  { kind:ubyte; p0:ubyte; p1:ubyte; p2:ubyte; glName:uint; lifetimeId:ulong; }
struct BlobRef     { seg:uint; pad:uint; offset:ulong; size:ulong; }
struct RecBindBuffer     { target:uint; index:uint; h:WireHandle; }
struct RecDrawArrays     { mode:uint; first:int; count:int; instances:int; baseInstance:uint; pad:uint; }
struct RecDrawElements   { mode:uint; count:int; type:uint; flags:uint; indices:ulong; blob:BlobRef; }
struct RecBufferSubData  { h:WireHandle; offset:ulong; size:ulong; blob:BlobRef; }
struct RecBufferMap      { h:WireHandle; rangeStart:ulong; rangeEnd:ulong; access:uint; pad:uint; }
struct RecBufferUnmap    { h:WireHandle; }
struct RecTexSubImage    { h:WireHandle; target:uint; level:uint; box:[uint:6]; rectCount:uint;
                           pad:uint; blob:BlobRef; }        // rects 在变长尾
struct RecGenerateMipmapLevels { h:WireHandle; target:uint; requiredLevelCount:uint;
                                 bytesPerTexel:uint; shrinkingAxes:uint; }
struct RecRenderbufferStorage  { h:WireHandle; internalFormat:uint; width:int; height:int;
                                 samples:int; pad:uint; }
struct RecXfbAccounting  { pausedPrims:ulong; inputPrims:ulong; prims:ulong;
                           capturedVerts:ulong; geomDraws:uint; accountedDraws:uint; }
struct RecRenderStateBlob{ version:ushort; pipelineVersion:ushort; pad:uint; blob:BlobRef; }
struct RecPresent        { frameSerial:ulong; swapInterval:int; pad:uint; }
struct RecSetResolvedDrawProgram { h:WireHandle; }
// … 共约 95 个

// ---------- 控制面 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];
                     tableSlotMask:ulong;              // 远端实际注册了哪些 GLFunctionsTable 槽
                     prefersCpuXfbPrimitiveAccounting:bool; }
table DefaultFramebufferInfo { 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; defaultFb:DefaultFramebufferInfo; }
table ProgramReflection { /* Visit() 归档的结构化镜像,P5 */ }
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,
                ProgramReflection, ResyncRequest, ResyncDone, AuxRequest, Fatal, LogLine }
table CtrlEnvelope { msg:CtrlMsg; }
root_type CtrlEnvelope;

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-38MOBILEGL_FLATC_EXECUTABLE 未设时 add_subdirectory(3rdparty/flatbuffers) 并开 FLATBUFFERS_BUILD_FLATC ON——这正是它自称要修的 NDK 陷阱(交叉编译造出 arm64 flatc 然后在 host 上执行)。本计划不复用这一段gen_protocol.py 是纯开发者/CI 目标,默认构建图里没有 flatcMOBILEGL_FLATC_EXECUTABLE 只服务 CI 的 flatc-check。FlatBuffers 运行时是 header-only,只需要 3rdparty/flatbuffers/include 在 include path 上(P4 用 nm 复核 libMobileGL.so 链接行没有新增库,不靠断言)。

7.2 帧封装与 flush 策略

CTRL socket 封帧:[u32 'MGLF'][u32 len][payload]64MiB 上限,读时校验Feat/CS-Delta-IPCFeed() 永远返回 OK,坏 magic 变成静默永久挂起,Framing.h:41-45StartRead 直接按 wire 长度分配无上限检查,LocalSocketTransport.cpp:232-236)。接收缓冲不足时返回所需大小并保留消息(上一版的 transport 会失败且不弹出消息,把流永久卡死)。

Publish 触发器(重写,删掉 64KiB 阈值)

上一版设 "records ≥ 64KiB" 为主触发器。按 §6.3 的记录尺寸,64KiB ≈ 1200-2700 条记录,即一整帧(计划自己把 MC 帧估为 1000-4000 draw)。那意味着 server 在 client 发完整帧之前无法开始工作——这不是异步,是一个整帧的流水线气泡,且在 present credit 之上再加一整帧延迟。它还在 P2.5 跑之前就先把 P2.5 的假设否掉了(inproc 的全部意义就是让 PrepareForDraw 与 GL 线程重叠,帧粒度 publish 保证零重叠)。而 SEG_CMD 是 SPSC ring"publish" 只是一次 cmdHead 的 release store,唯一值得摊销的是门铃写。

新规则

  • 每条记录(或每 8-16 条,用来摊销 storerelease-store cmdHead;仅当 consumerParked 时敲门铃。
  • 显式门铃点:Present、任何 kNeedsAck 阻塞请求、eglMakeCurrentglFlush刷出 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,RecFenceSync 就永远躺在 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 透传给 backendGL_Sync.cpp:96)。 携带 GL_SYNC_FLUSH_COMMANDS_BIT 的调用无条件 publishspec 要求 flush)。
  • 饥饿升级:同一个 handle 连续 N 次(默认 64MOBILEGL_IPC_POLL_ESCALATE)本地回答 TIMEOUT_EXPIRED / "未就绪" 而 watermark 毫无移动时,升级成一次阻塞 round trip,这样一个已经卡住的 server 不会把 client 自旋成死循环。

glFinish 保持纯 no-opDefinitions.cpp:111-112)——应用唯一的强制停顿手段在 monolith 里免费,拆分后也必须免费。

7.3 序号与 credit

seq = 记录序数。两个互相独立的窗口,绝不是 per-batch 锁步Feat/CS-Delta-IPC 在 apply 循环里同步发 ack,ServerCore.cpp:421-429,是最差的节奏;而且它的 credit 算成 baseSeq + items.size(),只有 baseSeq==0 时才对):

  • 字节 creditSEG_CMDSEG_STAGE 各自的占用,升级路径见 §6.5。
  • Present crediteglSwapBufferspresentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT默认 1,见 §9)时阻塞。

server 端不发 credit 消息:它对 RingControl 做 release storeconsumer 每 64 条记录更新一次 appliedSeq,并在 producerParked 时敲反向门铃。

7.4 事件回传通道

SEG_EVENT 是 server→client 的 SPSC POD ringEvQueryResult{handle, available, value}EvFenceSignaled{handle}EvGpuWritten{handle, rangeCount, ranges[]}EvBufferWriteback{handle, offset, BlobRef}EvReadbackDone{seq, BlobRef}EvGlError{code}EvDefaultFramebufferInfoEvCompileEnvInvalidateEvLogLine{level,len,text}

排空点(补齐)

client 在下列位置排空:glGetErrorglGetQueryObject*glClientWaitSyncglGetSynciveglSwapBuffersglMapBuffer / glMapBufferRange / glGetBufferSubData / glGetNamedBufferSubData / glCopyBufferSubData(§5.6b 要求),以及每一次等待循环的每一轮present credit、kNeedsAck、ring/stage 满)。最后一条是必须的,见下。

溢出策略(本轮新增,修一个双向死锁)

上一版没说 SEG_EVENT 满了怎么办,也没要求 client 在等待中排空。具体死锁:client 卡在 eglSwapBuffers 等 present creditserver 的 apply 线程一边 apply 一边产 EvLogLineEvGpuWrittenSEG_EVENT 满;apply 线程阻塞在生产上;presentAckSerial 永不前进;client 永不离开 eglSwapBuffers,因而永不排空。两边都死。

策略

  1. client 必须在每个等待循环内排空 SEG_EVENT,不只是在入口点边界。
  2. EvLogLine有损的:覆盖最旧,并累加 RingControl.eventDropped(client 在排空时把丢失条数打进日志)。丢一条日志绝不允许卡住渲染。
  3. 语义承载事件(EvGpuWrittenEvReadbackDoneEvFenceSignaledEvBufferWritebackEvGlErrorEvDefaultFramebufferInfoEvCompileEnvInvalidate无损ring 装不下时 server 置 RingControl.eventRingFull=1停止 apply(停在一条记录的边界上,不是记录中间),敲反向门铃;client 排空后清标志并敲正向门铃。状态因此永远可恢复。
  4. 故障注入测试:在 client 被 credit 阻塞时灌满 SEG_EVENT,与 P8 的 SIGKILL 测试并列。

server 侧的 MGLOG 与延迟诊断按流顺序 replay 进 client 日志流——复用已存在的 DeferredLogLine/ApplyDeferredDiagnostics 机制(JobNode.h:26-58,149-158)。


8. Roundtrip 清单

不可避免的阻塞点

# 站点 频率 为什么
1 握手 Hello/Welcome + 段 fd 传递 一次
2 InitializeEGLDisplay(写 major/minor 一次 出参
3 CreateEGL{Window,Pbuffer}Surface / Resize 罕见 返回 Bool;回复顺带 DefaultFramebufferInfo
4 首次 MakeEGLCurrent + InitCapabilitiesCapsSnapshot 每 surface 一次 caps 只在那一刻才存在(BackendObject.cpp:341-347
5 glReadPixels → 客户内存 罕见(CTS 热) GL 要求返回时字节已就位
6 glCopyTexSubImage* / glClearTexImage(内含 ReadPixels 罕见 前端实现本来就借一次 ReadPixels
7 glGetTexImage/glGetTextureImageDirectVulkanDirectGLES 仅 serverAuthoritative level 罕见
8 glGetBufferSubData / glMapBuffer(READ) on client-pending 罕见 monolith 里本来就阻塞;client pending 集合触发
9 client 顶点数组的索引扫描 / restart 重写 / indirect-count 读(当 EBO 在 pending 集合里) 罕见 monolith 在同一位置调 SyncGpuWrites()
10 glClientWaitSync(timeout>0) 超出 watermark 每帧级 应用请求的等待
11 glGetQueryObject*(GL_QUERY_RESULT) 未完成;glBeginConditionalRender 罕见 GL_Query.cpp:300:705-706(后者注释明说"by WAITING even for the _NO_WAIT modes"
12 轮询饥饿升级(连续 N 次无进展) 极罕见 防死锁保险
13 分配类入口点的错误 ackglRenderbufferStorage*、部分 glTexImage*/glTexStorage*/glCopyTexImage*glBufferStorage 罕见 OOM 探测惯用法(§5.6c
14 AcquirePersistentMap(仅 P7 T1 每 store 一次 返回映射
15 ring/stage 耗尽、present credit 节奏 非语义

变成异步或本地的

  • 全部 20 个 draw、9 个 clear、blit/copy、GenerateMipmap、dispatch、barrier、image bind、7 个 XFB 跨度标记、PatchParameteriShaderStorageBlockBinding(权威状态已在 clientGL_Program.cpp:3391)、所有 buffer/texture/program/VAO/FBO delta、Present
  • glGetError 永远本地GL_Getter.cpp:2811-2817Core.cpp:48-49 的不变式)。
  • glFinish/glFlush 保持免费
  • 89 个 caps 站点全部本地45 GetDynamicParameters + 8 GetRendererInfo + 4 GetFormatCapabilities + 3 GetBackendType + IsTimerQuerySupported + PrefersCpuXfbPrimitiveAccounting + BeginOcclusionQuery!=nullptr)。
  • glGetIntegeri_v 全部本地glDispatchCompute 的三次 per-dispatch 校验查询(GL_Drawing.cpp:719)改读 CompileEnv::maxComputeWorkGroupCountCompileEnv.h:52-54)。
  • GetInteger64i_vGetProgramiv 删除
  • FenceSyncBegin{TimeElapsed,Occlusion,XfbPrimitives}QueryQueryCounterTimestamp → client 铸造 handlefire-and-forget(前端本来就铸造应用可见的名字:GL_Sync.cpp:61GL_Query.cpp:54)。
  • GetSyncStatusClientWaitSync(0)IsQueryResultAvailableGetQueryResult64(wait=false) → 先 publish(§7.2),再从水位一次 acquire load 回答。miss 返回 GL_UNSIGNALED / "未就绪",两处契约明确允许(BackendObject.h:210-214:236-241GL_Query.cpp:302-311 已遵守:读 0、不缓存、保留 backend handle)。
  • glReadPixels 进 PBO → fire-and-forget(配 client 侧 MarkGpuWritten),比 monolith 更好。
  • glEndTransformFeedback 的无限 fence 等待取消(配 client 侧对 capture target 置 MarkGpuWritten)。

稳态帧:零 round trip(对不使用 conditional render / 阻塞式 query / 分配类调用的帧而言;见 §15 P3 的门措辞修正)。

fence 完成度必须来自真 fence,不是 present 水位(本轮新增)

上一版让 retiredSeq/completedFrameSerial 兜底 fence 语义。但在 DirectGLES 上这两个水位只在 Present() 里前进DirectGLES.cpp:10626-10643eglSwapBuffers 之后轮询 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。

规则RecFenceSync 在 server 侧转成一次真实的 backend FenceSync()server 用自己已有的逐 fence 轮询(DirectGLES 有 WaitForFrameSerialCompleted 的 fence 选择逻辑 :10586-10600 可复用;DirectVulkan 有 IsSubmitIndexComplete)在非 present 时刻也推进,并发 EvFenceSignaled{handle}。client 的本地快路径读的是"由真实逐 fence 退休导出的 handle 水位",不是 present 水位。

三个应先独立落到 dev 的 monolith 修复(可二分、monolith 自身受益)

  1. glEndTransformFeedback 的无条件无限 ClientWaitSyncGL_Drawing.cpp:1326-1337)→ 用既有 MarkGpuWritten/SyncGpuWrites 推迟到首次读。
  2. glDispatchCompute 三次 GetIntegeri_vCompileEnv
  3. 删除 GetInteger64i_v/GetProgramiv 两个死表项及两个 backend 的实现。

9. Present 与帧节奏

eglSwapBuffersEGLImpl::SwapBuffersEGLImpl.cpp:162-183)→ BackendObject::SwapEGLBuffersBackendObject.cpp:369-398,其线程归属校验全部对 client 镜像的 EGL 状态求值,不需要回复)→ 发 RecPresent{frameSerial, swapInterval} → publish + 敲门铃 → 返回,除非 presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT

Present 与应用的 eglSwapBuffers 严格 1:1,绝不批量。 Magma 侧四次 OnFrameBoundary() 缓存老化、TryDrainFrameTransients 和全部四次 BeginFrame 只在 Present 内发生(VulkanRenderer.cpp:12765-12904);Espryt 侧三个 ring 与 TrimBufferPool 在那里 retireDirectGLES.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 + 驱动深度。P3 与 P9 的验收增加输入延迟测量:用已有的 GetGpuTimestampNs 与 trace-replay --benchmark 的逐帧 JSON 构建 "记录发射时刻 → present 完成时刻" 直方图;只有当实测吞吐收益能抵掉实测延迟代价时才调高 credit。

参考基线:MagmaFramesInFlight = 3 钳到 [2, maxImageCount]VulkanRendererConfig.h:14-19VulkanRenderer.cpp:3051-3058),Espryt 深度 4 的 fence ring 刻意高于驱动的 2-3DirectGLES.cpp:10071-10074)。

9.2 swap interval 与 Magma

Swap interval 搭 RecPresent 过去。注意 Magma 从不注册 SetSwapIntervalBackendObject_DirectVulkan.cpp:698 只注册 Present)且偏好 MAILBOX/IMMEDIATESwapchainObject.h:74-79),因此 IPC credit 成为 Magma 唯一的显式限帧器 —— 记录在案,P6/P9 在设备上测量输入延迟与帧节奏;若 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 的活 fenceslot 被回收时返回 false)前进。在无 present 的负载里——tools/ctsrun_cts_local.py、回读循环、从不 swap 的 MG_IntegrationTest 场景——一个 fence 都不会被插入,retiredTail 永不前进,SEG_STAGE 填满,§6.5 的升级路径在每个用例上都跑到硬 drain。那会把一次 CTS run 变成一连串 50ms 等待加整体 drain,并可能被误读成一致性回归。

规则:给 DirectGLES 的 server 加非 present fence tick——距上次 Present 超过阈值(默认 8ms)或每 N 条已 apply 记录(默认 4096)时,插入一个 glFenceSync 并轮询 fence ring,复用 g_frameFenceRing 机制。同时把 ring 占用率与升级次数打进 Tracy 计数器(P0 交付),让"水位饿死"表现为一个指标而不是一次无法解释的停顿。P2 增加一个无 present 的 split 用例。


10. 线程模型

Client

  • v1 不加线程。 编码在调用方 GL 线程上直接写进 ring。前端本来就是 per-context 单线程契约(GLContext 无 mutexEGLState::MakeCurrent 强制一个 owner 线程,EGLState/Core.cpp:1215-1220,测试在 MG_Test/EGLState/EGLStateTest.cpp:39-92)。
  • flow = per context,不是 per thread。 今天恰好一个 flow。eglMakeCurrent 是 flow 所有权转移,在既有 EGLOperationMutexEGLImpl.cpp:241)下发射。顺手修既有漏洞EGLImpl::ReleaseThread:341-350)与 SwapInterval:435-450)今天不取该锁而另外三个(MakeCurrent/SwapBuffers/DestroySurface)取。
  • 外来线程的 sync/query:读全部从 RingControl 无锁 acquire load 回答(比取 registry mutex 更好);少数必须发射的(FenceSyncBegin*Query,以及 §7.2 要求的轮询 publish)取 ctrlMutex 并走 CTRL socket 的 out-of-band AuxRequest 帧(SPSC ring 不允许第二个 producer)。
  • 等待必须能挂起:所有 client 侧等待(present credit、kNeedsAck、ring/stage 满、轮询升级)走 §6.2a 的 producerParked + 反向门铃,自旋窗口 MOBILEGL_IPC_SPIN_US(默认 50µs)。
  • ShaderCompilePool 原样保留在 clientShaderCompilePool.h:77-82,≤4 worker,为 RSS 上限)。
  • 可选 mgl-client-tx 双缓冲发送线程:P6 项,凭测量决定。在 P6 的 Tracy 数据出来之前不要预先加线程(会引入拷贝或锁)。

Server

线程 职责
mgl-srv-io asio io_context::run:封帧读写、SCM_RIGHTS、双向 doorbell、CTRL RPC
mgl-srv-apply 终身持有原生 EGL/Vulkan context:消费 ring → 解码 → apply 进 replica → 调 backend 表
mgl-srv-dec(可选,P6 FlatBuffers/边界校验前置,凭测量决定

因为 context 永不迁移:g_backendContextOwnerThreadDirectGLES.cpp:10052)只写一次;DirectGLES::MakeCurrent 的 8 缓存失效风暴(:10123-10140)变成启动期一次性成本;IsBackendContextCurrentOnThisThread 的每帧 EGL 复核(:10195-10228,动机是 eglGetCurrentContext 实测占渲染线程 16%)恒真。DirectGLES 的 off-thread 降级(FenceSync 返回 null 等)消失——保真度提升。延迟 replay 机制(Managers.h:458-473pendingRespecify/pendingRanges/pendingResidentWrites)保留但永不触发。

核心放置(本轮新增,是性能主张的前提)

§5.1 明说 reconciler "就是 PrepareForDraw 的可达性遍历"。这意味着这套遍历每 draw 跑两次client 的 WireMirror 一次,server 未改动的 PrepareForDrawDirectGLES.cpp:2916-2975)一次,外加编码与解码。其中有些并不便宜:CurrentUnitBindingsEpochDirectGLES.cpp:1421-1438)在 GetTextureBindGeneration() 变动时会退化成对每个 touched texture unit 做 owner-equality 全走查,而代码自己注明这在冗余重绑时就会发生("26.2 re-binds the unit's own sampler around every texture-unit switch")。

所以拆分的全部性能主张都押在"两半落在两个都快的核上"。而 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 大核上做的,拆分按构造就是回归,而 §15 P3 的"帧时在 monolith 10% 内"会以一个没人会正确归因的理由失败。

规则

  1. 计划里必须写出总 CPU 工作量差client reconcile + encode + decode + server PrepareForDraw vs monolith 的 PrepareForDraw),不只是单侧成本。
  2. 复用 ShaderCompilePool 已有的大核探测(ShaderCompilePool.cpp:73-96ReadCpuMaxFrequencyKHz / DetectBigCoreCount)把 mgl-srv-apply 绑到大核,开关 MOBILEGL_IPC_SERVER_AFFINITY(默认 auto),并把解析出的 mask 打进日志。
  3. P2.5 与 P3 必须报逐线程 CPU 时间,不只是墙钟帧时,这样"没有收益"的结论能被归因到放置 vs 编码成本。

拆机顺序(三条约束)

Publish() + server 排空并 ack → 停 apply 线程 → 关 transport →(client)排空 compile pool(必须先于 glslang::FinalizeProcess()pGLContext 析构,ShaderCompilePool.h:106-110Init.cpp:56-62)→ MobileGL::Destroy()EGLImpl.cpp:335-338)→ 释放 sync/query handleGL_Sync.cpp:223-226)。


11. EGL/窗口与进程生命周期

11.1 启动与握手

client 定位 server 的顺序(本轮修正):

  1. MOBILEGL_IPC_SERVER_PATH主要机制)。
  2. 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。

配套:把 MobileGLServerRUNTIME_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_testSPLIT 分支。并复核绝对路径能否活过 CI 的 artifact 搬运.github/workflows/test.yml:174-185 只重写 CTestTestfile.cmake 里的 cmake 路径,不重写 ENVIRONMENT 值——若不行,改为在测试启动时由 harness 相对 argv[0] 解析。

启动方式:socketpair(AF_UNIX, SOCK_STREAM) + fork/execvefd 3 = socketWindows 见 §11.5)。无文件系统 socket 路径、无 abstract namespace、Android 上无 SELinux 争议。

子进程必须被强制成 monolith(本轮新增,修无界 fork 链)MG_Config::TransportConfigLoader 从环境变量读(与 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}WelcomeconfigBlob 转发 client 解析好的 MG_Config::Features,两半不可能对某个 quirk 开关有分歧。buildFingerprintgit hash + Records.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 出货的正是 RelWithDebInfoMobileGL/build.gradlefordebug 类型强制 -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.txtwgl.def

11.3 Android

minSdk 26 没有任何公开 NDK API 能扁平化 ANativeWindowNDK r27.3 的 android/native_window.h 无 parcel 符号;libbinder_ndk 是 API 29binder_ibinder.h:191ASurfaceControl 是 API 29surface_control.h:67)。Feat/CS-Delta-IPCnativeBlob"binder-flattened ANativeWindow"protocol.fbs:377-379)不可实现。

  • P1-P8 验证路径:无窗口。 两个 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)与 pluginandroid-plugin/app/build.gradle.kts:198-203)都已开。surface 用 pbuffer 或 AImageReader 支持的 ANativeWindowHeadlessGL.cpp:86-131,268-274),trace replay 默认 pbufferapitrace_glws_egl.cpp:614-618)。 注意实测的域:上述 SIGILL 证据是经 run-as 取得的,即 runas_app 域,而不是 trace Activity 所在的 untrusted_app 域。P0 的 Android spike 必须从应用自身进程 posix_spawn 一次(见 §15 P0)。
  • P9 生产路径Java SurfaceParcelable)→ Messenger/AIDL → MobileGLServerServiceandroid:process=":mgl")→ JNI ANativeWindow_fromSurface(env, surface),就是 FCLauncher 今天在 egl_bridge.c:81 做的那一次调用。仓内先例android-pluginBenchService 已在 android:process=":bench" 里跑 MobileGLBenchService.java:19-77)。代价:server 进程多一个 ART~15-25MB)。
  • 纠正一条过期笔记FCL 把游戏 JVM 跑在主进程,不是 :jvm../FCL/src/main/AndroidManifest.xml:112-121JVMActivity 没有 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。这个交互本身列为 P1 验收步骤 1 的一部分,先于任何广度工作。

11.4 Linux / X11

Window 是 XIDnativeToken:u64 直接送。backend 自己 XOpenDisplay(getenv("DISPLAY")) 并构造 VkXlibSurfaceCreateInfoKHRVulkanRenderer.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

HWNDnativeToken。Vulkan 可行(hinstance 是历史遗留,VulkanRenderer.cpp:14456-14463);WGL/ANGLE-DXGI 对外进程 HWND 不受支持 → headless only

transport:默认 named pipeasio 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 传下去。§11 必须把这套构造写清楚。

asio 1.38.2 在 Win32 上确实定义了 ASIO_HAS_LOCAL_SOCKETS3rdparty/asio/asio/include/asio/detail/config.hpp:1085-1092,只排除 ASIO_WINDOWS_RUNTIME,且自带 sockaddr_un_typesocket_types.hpp:220),但其 IOCP async_acceptAcceptExAF_UNIX 从不支持它——AF_UNIX-everywhere 是 P6 的可选简化,需真编真跑验证,named pipe 是已知可用的默认。

11.6 崩溃

  • server 死client 读到 EOF/EPIPE → device-lost 闩锁:后续 GL 调用变 no-op、eglSwapBuffers 返回 EGL_FALSE+EGL_CONTEXT_LOSTglGetGraphicsResetStatus(若 robustness 分支落地)返回 GL_UNKNOWN_CONTEXT_RESETMOBILEGL_IPC_RESPAWN=1 时重启 + ResyncSnapshot(默认关,静默重启会掩盖 bug;且与 MOBILEGL_IPC_ADOPT_TIER != 2 互斥,见 §5.8)。
  • client 死server 读到 EOF → 立即销毁原生 context 并退出(不等看门狗);MOBILEGL_IPC_IDLE_EXIT_S(默认 30)只作为 EOF 都收不到时的最后保险。

12. Monolith 保留与模式选择

四层保证,从强到弱:

  1. 编译期折叠。 MOBILEGL_BUILD_DISAGGREGATED(默认 OFF)关闭时 MobileGL/MG_Remote/** 不进 SOURCE_FILESMG_Config::Transportconstexpr MonolithMG_Backend/Init.cpp 里的分支在编译期消失。默认构建与今天字节一致。
  2. 唯一 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::GetBackendFunctions() 返回发射表,Initialize() 负责 spawn/connect。下游 ~250 个边界调用点#ifdef。 3. P4.5 的 allocator 改动必须同样包裹。 PipeResource::MapAlignedAllocatorMipmapStorage 的 level vector 住在 MG_State,改它们的 allocator 就改了类型;写成"分配器特化,option OFF 时逐字折叠回今天的 MapAlignedAllocator",否则第 4 层会在 P4.5 变红。 4. 机械证明:对 libMobileGL.sonm --defined-only 与去调试信息后的 .text size diff,改前改后必须一致。这是每个阶段的出口判据(P0…P9),不只是 P0(上一版只在 P0 跑)。

12.1 两个 option,不是一个(本轮重大修正)

上一版说"OFF 时字节一致",但每一条部署路径都要求出货构建是 ONFCL 用户可编辑 env、plugin APK 的 V2 开关表、ctest ENVIRONMENT 变体、/data/local/tmp CTS 路径。而上一版又说 ON 构建里 inproc 会把 pGLContext 变成 thread-local 加 operator-> shim。那个 shim 坐在全库最热的路径上:grep -rho 'pGLContext->' MobileGL/MG_Impl | wc -l = 1494,加 DirectGLES 124、DirectVulkan 169。Android 上 dlopen 的共享库无法可靠使用 initial-exec TLS,每次访问会退化成一次 __tls_get_addr 调用,而今天那里只是一次对全局引用的加载(Core.h:564 extern UniquePtr<GLState::GLContext>& pGLContext)。

规则:拆成两个 option。

  • MOBILEGL_BUILD_DISAGGREGATED(出货形态):只含 spawn/unix:/pipe:。每进程只有一个 GLContext、一份 gBackendFunctionsTable、一个 pActiveBackendObject、一份 pDefaultFramebufferInfo → 这四个全部保持普通全局,GL 热路径上没有任何 TLS 与间接。侵入面就是 MG_Backend/Init.cpp 里那一个可预测的分支。
  • MOBILEGL_BUILD_DISAGGREGATED_INPROC(CI/调试形态,隐含开启前者):额外加角色隔离 shim。

12.2 inproc 需要隔离的是四个进程全局,不是一个(本轮修正)

上一版只谈了 pGLContext。实际上 inproc 下同一进程要同时扮演两个角色,以下四个全局都必须按角色分身:

全局 定义处 谁读
MG_State::pGLContext GLState/Core.h:564 声明,Core.cpp:1487 定义,Core.cpp:20 构造,Init.cpp:63 reset 全部
MG_Backend::gBackendFunctionsTable MG_Backend/Init.cpp:44 赋值 client 侧 MG_Impl91 处)与 server 侧 MG_ImplGL_Texture.cpp:1621 GenerateMipmap_Backend:6713-6725 GetTexImage 回退链、FixupGsStripCaptureOrderCopyReadFramebufferIntoMipmapRegionReadPixels
MG_Backend::pActiveBackendObject MG_Backend/Init.cpp:53-61 赋值 MG_Impl 89 处 + backend 内部
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo GL_Framebuffer.cpp:3344 定义,全库 22 处引用 client 侧 MG_Impl 13 处(GL_Framebuffer.cpp:495,1827,1837,1897,1905,1913,1927,1936,2549,2590,2598,2608,2611+ server 侧 backend 5 处(DirectGLES.cpp:1917,2838,2867,9675SwapchainObject.cpp:276,其中 SwapchainObject

一旦 client 装上发射表,inproc 里 applier 与 server 侧 MG_Impl 就没有任何路径能拿到真正的 DirectGLES/DirectVulkan 表;而一个进程也不可能同时持有 client 的 default-FBO 描述与 server 的(SwapchainObject 直接往里写 server 的视角)。

shim 的完整需求(上一版只提了 operator->):operator->operator boolget()== nullptr 相等比较、从 MakeUnique 赋值、reset()。非箭头用法的实际数量是 133grep -rn pGLContext MobileGL/ --include=*.cpp --include=*.h | grep -v 'pGLContext->' | wc -l = 133,上一版写的"约 65 处"少了一倍),其中 MG_Impl 只有 2 处(GL_Debug.cpp:99.get()GL_Program.cpp:1630== nullptr),绝大多数在 MG_Backend——尤其 DirectVulkan 里约 90 处 MOBILEGL_ASSERT(MG_State::pGLContext, ...) 的真值判断,另有 DirectGLES.cpp:146.get()Managers.cpp 里十来处 if (MG_State::pGLContext) 守卫。因为 backend 侧那一簇恰恰是必须看到 replica 的,shim 的原型应当先拿 MG_Backend/DirectVulkan/DirectVulkan.cpp 的 assert 密集区开刀。

如果这层隔离的成本被判定过高,退路是把 inproc 降级为纯测试模式:applier 通过显式传入的表指针工作,server 侧不跑 MG_Impl(于是 GenerateMipmap_Backend 那类回退不可用,需要在 inproc 下走另一条路径)。但那样 P2.5 就不再测量它本该测量的"monolith 渲染线程"交付物——这个取舍必须在 P0 结束前拍板并写进文档,不能悬着

12.3 inproc 作为产品交付物

在隔离成本可接受的前提下,inproc 不只是测试脚手架:同进程第二个 GLContext + mgl-srv-apply 线程 = monolith 的渲染线程。今天 PrepareForDraw(状态调和、VAO/FBO/纹理/program/render-state sync、UBO ring memcpy)加驱动调用全部同步跑在 glDrawElements 里;把它们搬到 apply 线程,对 GL 线程 CPU-bound 的应用(本项目的 profiling 史说 Minecraft 就是)是手上最大的单一杠杆,且不需要任何 IPC/shm/平台工作。§15 的 P2.5 就是证伪它的门。

12.4 运行时选择与开关

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_FLUSHMOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION),新增:MOBILEGL_IPC_SHADOW_SHMMOBILEGL_IPC_ADOPT_TIERMOBILEGL_IPC_PROGRAMMOBILEGL_IPC_INLINE_PAYLOADSMOBILEGL_IPC_PRESENT_CREDITMOBILEGL_IPC_SPIN_USMOBILEGL_IPC_POLL_ESCALATEMOBILEGL_IPC_PERSISTENT_BLOCK_KBMOBILEGL_IPC_SERVER_AFFINITY


13. 构建布局

MobileGL/MG_Remote/
  Protocol/  protocol.fbs  protocol_generated.h(提交)  Records.def  RecordKinds.h
             Handles.h  Coverage.def  MutationCoverage.def
             generated/BackendStateSurface.inc(提交)  generated/ImplMutationSurface.inc(提交)
  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}
  Shared/    XfbAccounting.{h,cpp}   # client 与 applier 共用的 MG_Impl-side mutation helper
             MipmapLevelPlan.{h,cpp}
  Client/    WireMirror.{h,cpp}  EmitTable.cpp  EmitBufferOps.cpp
             BackendObject_Remote.{h,cpp}  CapsMirror.{h,cpp}
             ClientArrayBounds.cpp  CompositeResolver.cpp  ShadowArena.{h,cpp}
             PersistentMapTracker.{h,cpp}  GpuWritePending.{h,cpp}
             CoverageAssert.cpp  Surface/{X11,Win32,Android,Headless}.cpp
  Server/    ReplicaContext.{h,cpp}  Applier.cpp  ServerLoop.{h,cpp}
             ReplyPool.{h,cpp}  EventRing.{h,cpp}  ServerMain.cpp
  ServerJni.cpp                                  # Android,与 DriverPostJni.cpp 并列
scripts/     gen_protocol.py  gen_backend_state_surface.py  gen_impl_mutation_surface.py
MobileGL/MG_Test/Wire/CMakeLists.txt             # 复制自 MG_Test/Buffer/27 行)+ MobileGL_Protocol

CMake

  • MG_Remote/** 仅在 MOBILEGL_BUILD_DISAGGREGATED 下追加进 SOURCE_FILESCMakeLists.txt:226-419),因此 MobileGL:485)与 MobileGL_s:552)都拿到。
  • MobileGLServer:桌面 add_executable 链接 MobileGL_sRUNTIME_OUTPUT_DIRECTORY 设为 $<TARGET_FILE_DIR:MobileGL>(§11.1);Android add_executable + set_target_properties(MobileGLServer PROPERTIES PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer") 并链接共享MobileGL(一份 ~43MB 的 glslang/SPIRV-Cross/SPIRV-Tools),由 AGP 打进 jniLibs。server 主体是 ~30 行 stubdlopen(libMobileGL.so)dlsym("mobilegl_server_main")(可见性见 §11.2)。一份共享库、两个角色,版本必然匹配(对比 Feat/CS-Delta-IPC 的四件必须互相匹配的产物)。 AGP 能否打包一个被改名成 lib*.soadd_executable,是 P0 spike 的验证项之一MobileGL/build.gradle 没有设 targets 列表,上一版把这条当成已知事实)。
  • FlatBufferssubmodule 3rdparty/flatbuffers 置于既有的 if (EXISTS .../flatbuffers/CMakeLists.txt) 保护下,去掉 if (NOT ANDROID) 一刀切。因为 protocol_generated.h 已提交,默认构建图里没有 flatc,也不 add_subdirectory(3rdparty/flatbuffers)(§7.1)。运行时是 header-only,只需要 3rdparty/flatbuffers/include 在 include path 上。 guard(本轮新增):若 MOBILEGL_BUILD_DISAGGREGATED=ON3rdparty/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/label unit)→ 现有 CI test job 自动收,无需改 workflow
    • MG_IntegrationTest/CMakeLists.txt 每 backend 增加一条 gtest_discover_testsTEST_PREFIX "DirectGLES.Split." / "DirectVulkan.Split."),必须用 mgl_itest_join_environment(... ${MGL_ITEST_COMMON_ENV}) 构造,并带上 MOBILEGL_IPC_SERVER_PATH。三个已被文档记录的陷阱要遵守:ctest ENVIRONMENT替换而非追加:339-343)、; 必须转义(:322-332)、property 覆盖 job envtest.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 里消费它——这两个文件都要列进 P2 的交付物
  • CI 新增三个 stepflatc-check(重生成 protocol_generated.h + git diff --exit-code)、coverage-check(重生成两个 .inc + git diff --exit-code)、monolith-abi-checkOFF 构建与 ON+monolith 构建的 nm --defined-only / .text size 对基线)。
  • CI 新增一条 grep 门:禁止 MG_Backend/MG_State/ 下出现 fprintf(stderr / printf(

14. 对 Feat/CS-Delta-IPC 的复用清单

REUSE(原样取)

路径 commit 备注
docs/CS_Refactor/HandleSessionGeneration.md 546895aa 分支上最好的产物。三处修改:handle 清单补 RenderbufferObject::GetLifetimeId() GetVersion();把第 2 节的 server 侧 share-group 要求降为 v2;把"lifetimeId 不符 → 销毁重建"改成 Fatal(§5.4
MobileGL/Protocol/mg_protocol_base.h 546895aa 干净无依赖的词汇(MobileGLResult、span、ShmRegion、id typedef、structSize-first 版本纪律)
MobileGL/Protocol/tests/ProtocolSmoke.cpp 546895aa schema 往返门(默认改 ON
CMakeLists.txtEXISTS 保护 + .gitmodules 条目 546895aa 去掉 NOT ANDROID,另加 §13 的 include-dir guard
docs/CS_Refactor/HANDOFF.md 第 6 节"已知坑清单" d5c00b9d/5964628d 逐字留作事后复盘:路径转换、versionCode 降级、双设备 ANDROID_SERIAL、flatbuffers camelCase accessor、union vector 产生指针、Release 下 MGLOG_D 被编译掉、嵌套 submodule 配方、assembleTraceDebug 改名

CHANGE(取走并改造)

路径 commit 改造
MobileGL/Protocol/protocol.fbs 546895aa 保留 delta 目录、RenderStateBlob 整块思想、BufferShmAdopt、命令清单、事件分类学。改:热路径转 struct + ring;删掉冗余的 inlineBytes/data 双胞胎(:111-112:125-126,两半代码对哪个字段是真的意见不一:ServerCore.cpp:184-208 只读 dataStateEmitter.h:60,111 只写 inlineBytes);加 ResyncSnapshotAuxRequest;给 ProgramPublish.reflectionObjectCreate.params 真 schemakind 枚举生成 + 每 kind static_assert + 运行期边界检查
MobileGL/Protocol/CMakeLists.txt 的 flatc 解析 546895aa/65717b4c 不再照搬add_subdirectory(3rdparty/flatbuffers) 从默认路径整段删除(它就是那个 NDK 陷阱本体);只保留 MOBILEGL_FLATC_EXECUTABLE 供 CIenable_testing() 移到根
MobileGL/ServerCore/ServerCore.{h,cpp} 65717b4c+c2260dd8 保留握手→解码→apply→credit 形状与 plugin manifest loader 思路。修:单次校验 + 零拷贝解码(今天校验两次外加一次整体拷贝,:492-498:218-221);io/apply 分线程(:404-406 自承 worker 从未落地);完整事件集(SendEvent 只实现 BATCH_APPLIED:373-382);credit 用最后一条实际 seq(:427baseSeq + items.size());接收缓冲不能是对着 64MiB 帧上限的固定 4MiB(:478);真正的段生命周期(m_segments 只增不减,blobOwners 只 push 不释放)
ServerCore/tests/LoopbackSmoke.cpp + Backends/Dummy/ 65717b4c 分支上最便宜的端到端门,第一个重建,重定向到真 applier
MobileGL/Remote/InProcessTransport.h 65717b4c 重表述在 C++ ITransport 上;单侧 shutdown(今天 :89-92 连对端 inbox 一起关);真段生命周期(Unmap/Close 今天是 no-op);补 §6.2a 的双向 doorbellcondvar 版)
MobileGL/Remote/Framing.h 65717b4c 保留帧格式;m_pendingSize/m_haveHeadermutable(今天 const_cast:81,85);Feed() 真校验 magic 与长度(今天永远返回 OK,坏 magic = 静默永久挂起);缓冲不足返回所需大小且保留消息;真正在 socket transport 里使用它(今天是死代码)
MobileGL/RemoteClient/StateEmitter.h:39-307仅 emit 半边 b50f3348+d96be9f3 各域字段遍历是真知识,抬进 WireMirror/ResyncSnapshot。GL name 换 lifetimeId(今天 :48-49,85,166-168,203,230 全把 GL name 塞进 handle);:175-181,:244-249,:253-258,:293-298 的 O(n²) 线性扫描换 handle map;固定 6 attachment:232-236)换 MaxColorAttachments;补上被跳过的 texture view:70-74)。不取 applier 半边(:312-501
scripts/extract_backend_read_inventory.py 546895aa 改造成 gen_backend_state_surface.py:删掉前缀兜底(:234-241),未知 accessor 一律 UNMAPPED 并编译失败;把真 pull point 与 signature handle 化分开统计。另写一个全新的 gen_impl_mutation_surface.py(§5.9b),它在原分支没有对应物

DROP

路径 理由
MobileGL/Protocol/bfa.h480 行) "strict C ABI"不是 C ABIServerCore.cpp:177-179 把 FlatBuffers 生成表的指针交给插件,插件必须是 C++ 且链接 FlatBuffersStateEmitter.h:330,351,362,372 就是这么用的)。手抄的 60 字段 MobileGLDynamicParameters:63-129)自承尾部不全、同步脚本从未写过——正是已在本项目造成 481 例 CTS 失败簇的那类数据的长期静默漂移炸弹。而本设计根本不需要 delta-apply vtable
MobileGL/Protocol/mgruntime_api.h + MobileGL/UtilRuntime/* 360 行契约对 ~50 行实现(8 域实现 2 域);唯一消费者传 nullptrServerCore.cpp:61);缓存每次命中整份拷贝(:79)、按 clear() 淘汰(:91-93);smoke 断言 api->metrics == nullptrRuntimeApiSmoke.cpp:66)。它的唯一理由随 BFA 消失;且本设计里翻译全在 server(它无论如何要链 SPIRV-Cross),glslang 全在 client
MobileGL/Remote/LocalSocketTransport.{h,cpp}ShmFactory.{h,cpp} 实现 从未被任何测试执行(LoopbackSmoke 用的是 InProcessTransport,唯一另一个消费者 ServerHost 编译不过);每次 send 都 use-after-free:199asio::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——验证的是它自己的 thin-server 前提说不该存在的数据路径;与生产 apply 路径零共享代码;只测全量 resync;d96be9f3 声称五域逐字段而文件只比了纹理、buffer、render-state blob、buffer binding slot(没有 VAO 属性/FBO attachment/RBO 格式比较)
c7c9e346 + 29d721ef 全部(share-group sessioning 非 v1 前提(monolith 只有一个 GLContextGLState/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
b50f3348RenderState::InstallParameters + public: 本设计不需要 Install setter(D3);若日后需要整块安装,用正确作用域的方法或单条 friend,绝不靠裸 public:
d96be9f3 的 TRIAGE 指令(DirectGLES.cpp:+2583-2590 per-draw fprintf(stderr)分支上每一次测量都跑在它上面。 同规则适用于当前工作树的 [IBOTX]/[BUFTX]P0 清除)

15. 分阶段实施计划

通用纪律(每个 commit 都适用):默认 ALL target 必须能完整构建;禁止提交热路径插桩;每个门必须能因它存在的理由变红Windows 机器不是正确性门(其 Vulkan 缺 vkCreateHeadlessSurfaceEXT,占该机 567 个基线集成失败中的 423 个);设备对比走 reboot-clean + 同窗口配对 A/B每个阶段的出口都跑一次 §12 第 4 层的 nm/.text monolith 门(不只是 P0)。

P0 — 卫生、骨架与两个 spike(5 天)

交付物

  • 清除工作树 [IBOTX]/[BUFTX] fprintfDirectGLES.cpp:640-663Managers.cpp:875-877,后者在 pendingMutex 临界区内)。
  • RenderbufferObject::GetLifetimeId() GetVersion()(§5.4)。
  • 两个 CMake optionMOBILEGL_BUILD_DISAGGREGATED(OFF) 与 MOBILEGL_BUILD_DISAGGREGATED_INPROC(OFF)MOBILEGL_TRANSPORT 解析;§13 的 flatbuffers include-dir guard。
  • MG_Remote/{Protocol,Transport} 骨架:ITransportInProcessTransport、校验型 FramingRing + RingControl双 tail、双游标三元组、双向 doorbell)、DoorbellShmSegmentmemfd/ASharedMemory/shm_open/CreateFileMappingW)、SCM_RIGHTS fd 传递(第一优先)
  • protocol.fbs + 提交的 protocol_generated.h + gen_protocol.py + CI flatc-checkRecords.defstatic_assert运行期边界检查生成。
  • gen_backend_state_surface.py + Coverage.def gen_impl_mutation_surface.py + MutationCoverage.def + CoverageAssert.cpp + CI coverage-check
  • MG_Test/Wire/ 目录(复制 MG_Test/Buffer/CMakeLists.txt)。
  • TracyPlot 字节计数器,装在 wire 两侧,按类别分:cmd-recordsstage-bufferstage-texturestage-ubopersistent-map-pushserver-ringserver-staging(树里今天完全没有 per-frame 字节度量:MG_Util/Metrics 只是格式算术,Tracy 只有 zone 无 plotMC 26.3 战役的 PANDIAG 已不在树里)。
  • mobilegl_server_mainextern "C" __attribute__((visibility("default"))) 声明(§11.2)。
  • spike AAndroid 交付链,半天):从根 CMakeLists 造一个平凡的 libMobileGLServer.soadd_executable + PREFIX "lib"/SUFFIX ".so"),确认 AGP 把它打进 lib/arm64-v8a/;让 TraceReplayActivitygetApplicationInfo().nativeLibraryDir posix_spawn 它并打一行日志——在**应用自身进程(untrusted_app 域)**验证 exec,而不是靠 run-as。同时把一个通用 env 透传(--es mobilegl_env "K=V;K=V")接进 trace 路径的五个文件(trace-replay-ci.shTraceReplayActivity.java、JNI Request marshalling、trace_replay_core.cpprun_android_retrace_local.py),取代逐 knob 加 --es/--ez
  • spike Bexternal memory 可行性,半天):最小程序,导出一个 HOST_VISIBLE|HOST_COHERENT VkBuffer 的 fdmmap 后回读校验,在 35d0befaAdreno 830)与 3B159D009VZ00000Mali)各跑一次。与 SCM_RIGHTS 测试同批。目的是让 P7 的结论在第一周就有方向:若两台都不行,P7 缩为"记录并回退",省 6 天。

验收

  • Linux 与 Android/NDK 上 cmake --build . 默认 target 成功。
  • ctest -L unit-L integration-gpu81b17c0b 同一通过集。
  • MG_Test/Wire 的 fd 传递测试把一个 memfd 从 fork 出的子进程传回父进程并读到相同字节。
  • nm --defined-only 与去符号 .text size 与改动前的 libMobileGL.so 一致OFF 构建);nm -D | grep mobilegl_server_main 在 RelWithDebInfo 下命中。
  • spike A:设备上打出那行日志。
  • spike B:结论写进 §17 的开放问题并驱动 P7 的排期。
  • §12.2 的取舍拍板inproc 走"四全局角色隔离"还是"降级为纯测试模式",写进文档。

P1a — 垂直切片(client + inproc applier),Linux 门(6 天)

范围刻意收窄到 OpenRA 需要的东西:仅 DirectGLESbuffer(仅 shadow,采纳强制关,含 §5.10 的 persistent-map 推送);2D 纹理的整 level 与 union-box 上传(含 §5.6a 的 clear-on-emit);VAOFBOrender statebinds;索引与非索引 drawclearpresentserver 从源码 relink(带全字段 reflectionDigest);一条阻塞 ReadPixelsclient 侧 MarkGpuWritten 保守置位(§5.6b。不含 sync/query/XFB/compute/dirty-rects/MultiDraw。

交付物WireMirror(含 PublishImplicitStatePersistentMapTrackerGpuWritePending)、EmitTableEmitBufferOpsBackendObject_RemoteCapsMirrorClientArrayBoundsCompositeResolverP1-4 走"server 自建 composite + digest 校验",见 §5.7);ReplicaContextApplier(含 MG_Remote::Shared:: 的 XFB/mipmap helper 接线,即使这一阶段还用不到 XFB)、ServerLoop(io+apply)InProcessTransport 上跑通。

验收

  1. ctest -R "DirectGLES\.Split\..*(ClearThenReadPixels|Triangle)" 在 Linux + MOBILEGL_TRANSPORT=inproc 绿。
  2. 新增 PersistentCoherentMapScenariomap PERSISTENT|WRITE|COHERENT、写、不做任何其它 GL 调用、draw、readback 校验)在 split 下绿。这是本计划里唯一一个专为一个 fatal 缺陷设的门,必须在 P1a 就绿。
  3. 记录两个进程/两个角色的峰值 RSS(不只是 server 的)作为 P5 与 §16-R14 的基线。
  4. Tracy 计数器给出 persistent-map-push 的字节量(§5.10 保守版的代价)。

明确非目标:性能。P1-4 双份 glslangMC 级负载不在此测

P1b — spawn transportLinux 门(4 天)

交付物SocketTransportsocketpair + fork/execve + 显式 envp 剔除 MOBILEGL_TRANSPORT/MOBILEGL_IPC_*)、ServerMainMOBILEGL_IPC_SERVER_PATH 发现链、就绪握手与有界重试、EOF 即时退出。

验收

  1. P1a 的全部测试在 MOBILEGL_TRANSPORT=spawn 下绿(两个真进程、真 socket、真 SCM_RIGHTS 段)。
  2. fork 链测试spawn 一个 server 并断言进程树只多出恰好一个子进程(§11.1)。
  3. HeadlessGL 预检交互测试:在开着 fork 预检的 Linux 上跑整套 split 集成用例,断言没有孤儿 server(用 pgrep 计数 + 预检结束后 100ms 内归零)。

P2 — 广度:集成套件、trace 语料对齐、设备首跑(9 天)

交付物

  • 其余记录种类(MultiDraw/indirect 族含 client 数组范围计算与索引扫描、纹理 dirty rects、texture view、buffer texture、image unit、sampler、renderbuffer storage、program pipeline、CopyImageSubDataBlitNamedFramebufferPixelStorePackCurrentAttrib)。
  • 完整 caps mirror 与 tableSlotMask
  • DirectVulkan applier 支持(SwapchainObject 的 default-FBO 占位写变成 EvDefaultFramebufferInfo)。
  • add_trace_replay_testSPLIT 参数:测试名加后缀、-DTRACE_TRANSPORT=run_trace_case.cmake 的消费、MOBILEGL_IPC_SERVER_PATH 注入(§13)。
  • 一个无 present 的 split 集成用例(§9.3)。
  • ClientArrayAfterComputeWriteScenario(§6.10)。

验收

  1. ctest -L integration-gpu -R '^DirectGLES\.Split\.''^DirectGLES\.' 逐名同一通过/失败集DirectVulkan 同。
  2. CI 全部 trace caseOpenRA、minecraft-1.21.4-startup-main-menu1.21.111.17、两个 Create)在 Linux split 模式 SSIM ≥ 0.99。两个带 coherent_as_flush: true 的 Create 用例在 split 与 monolith 下都开着该开关跑(§5.10 已让两侧走同一路径),若 Tracy 显示保守推送在这两个 fixture 上代价不可接受,则把 §5.10 的精确版(P4.5 的块脏位)提前到本阶段——这是全计划唯一允许因测量改变阶段顺序的地方。
  3. python tools/trace_replay/run_android_retrace_local.py --case OpenRA --backend DirectGLES35d0befa 上 SSIM ≥ 0.99split 模式) —— 本阶段的出口判据(从 P1 移来),每轮约 1 分钟。

P2.5 — inproc 渲染线程:单机收益证伪门(3 天)

交付物add_trace_replay_testINPROC 变体;应用线程与 apply 线程的逐线程 CPU 时间插桩(不只是墙钟);MOBILEGL_IPC_SERVER_AFFINITY 的大核绑定(复用 ShaderCompilePool.cpp:73-96);用现有 --benchmark --benchmark-tail-frames --benchmark-result 在全部 fixture 上跑。

验收inprocmonolith 的应用线程帧时差 + 两侧 CPU 时间在 Create/Flywheel 与 MC fixture 上被测量并记录,且带亲和性开/关两组。若不利,整个计划的价值主张在第 6 周(而不是第 15 周)被重新审视。这是本计划最早的证伪点,也是 §16-R15 排期风险的退火器。

P3 — sync / query / present 节奏(5 天)

交付物

  • client 铸造的 sync/query handle;轮询入口的 publish + 饥饿升级(§7.2)。
  • fence 完成度来自真实逐 fence 退休(§8 末尾):server 侧真 FenceSync + 非 present 轮询 + EvFenceSignaled
  • DirectGLES 的非 present fence tick(§9.3)。
  • EvQueryResultpresent credit 默认 1 + 三个 seq 水位;swap interval 搭 RecPresent
  • §8 的三个 dev 独立修复。
  • per-frame round-trip 计数器;输入延迟直方图(记录发射 → present 完成,§9.1)。

验收

  1. XfbPrimitiveQueryScenarioPrimitivesGeneratedNoXfbScenarioAsyncCompileScenario 在 split 下绿。
  2. round-trip 计数器:在全部 trace case 的稳态帧上,draw/state/upload 路径的 round trip 读 0conditional render 与阻塞式 query 的次数按用例列表公布(不是笼统宣称"零 round trip")。
  3. 零 timeout 轮询循环测试:一个只有 glFenceSync + while(glClientWaitSync(...,0)==GL_TIMEOUT_EXPIRED){} 的用例必须在有界时间内退出(若无 §7.2 的 publish 规则它会永久挂起)。
  4. bench.sh35d0befa 配对 A/B(两侧均关采纳)显示 split 帧时在 monolith 的 10% 内,且输入延迟直方图的 p50/p99 被记录。

P4 — 回读与 GPU-written5 天)

交付物SEG_REPLY;阻塞 ReadPixels → 客户内存;PBO readback 变 fire-and-forget + client 侧 MarkGpuWrittenEvGpuWritten 作为收窄提示;EvBufferWritebackglGetTexImage/GetTextureImage 路由 + per-level serverAuthoritative(只覆盖生成 mip 与 CopyImage 镜像两处,§6.6);EvGlError + 分配类入口的 kNeedsAck(§5.6c);SEG_EVENT 溢出策略与等待中排空(§7.4)。 glCopyTexSubImage* / glClearTexImage 保持前端实现不变(推翻上一版的"移到 server + EvTexWriteback")。

验收

  1. split 下 DepthStencilReadbackScenarioDepthStencilReadbackMatrixScenarioDepthStencilReadbackAttachmentShapeScenarioPackedWordReadbackScenarioLayeredTextureReadbackScenarioClearThenReadPixelsScenarioPixelStoreSweepScenarioCopyImage*(4)、SsboArrayLengthScenarioAtomicCounterScenarioStorageBufferRegrowScenario 双 backend 全绿。
  2. OOM 探测用例:请求一个必然失败的巨大 renderbuffer,断言紧接着的 glGetError() 返回 GL_OUT_OF_MEMORY
  3. 事件 ring 溢出故障注入client 被 present credit 阻塞时灌满 SEG_EVENT,双方都不死锁,eventDropped 只统计到 EvLogLine

P4.5 — 零拷贝 shadow-in-shm 前移(4 天)

(原计划推到 P6;MC pan 每帧 ~9MB 的额外拷贝不该背六个阶段)

交付物ShadowArenaMapAlignedAllocatorMipmapStorage level vector 的 shm arena(≥256KiB 才走,整段 #if MOBILEGL_BUILD_DISAGGREGATED 包裹,§12 第 3 层);per-shadow 64KiB 块发送水位 WAR 规则;shadow 块退休规则(§6.1);§5.10 精确版 persistent-map 推送复用同一套块脏位;MOBILEGL_IPC_SHADOW_SHM 开关。

验收

  1. P2/P4 门在开关两态下均不回归。
  2. 两侧 TracyPlot 显示 buffer 上传路径的总拷贝次数从 4 降到 3(或选方案 B 则到 2,§6.4);staged-copy 回退率被记录成数字。
  3. nm/.text monolith 门仍绿(这一条是本阶段最容易破的)。
  4. 对象删除/重定义与未 apply 记录并发的压力测试不读到别的对象的字节。

P5 — ProgramPublish,退役 server relink6 天)

交付物ProgramArtifactsArchive.hVisit() + sizeof 绊线);ProgramObject::InstallPublishedLinkGLContext::SetReplicaResolvedDrawProgram#if MOBILEGL_BUILD_DISAGGREGATED 包裹)+ client 侧 composite 解析;MOBILEGL_IPC_PROGRAM=publish|relinkpublish 下移除 server compile pool顺带把 DirectVulkan 的 blit / depth-mipmap 四段固定 shader 在构建期烘成 SPIR-VVulkanRenderer.cpp:4211-4356,同时也从 monolith 启动里去掉一次 glslang 编译链接;逃生口 MOBILEGL_BAKED_INTERNAL_SHADERS=0)。

验收

  1. P2 全门在 publish 下重跑不变。
  2. relinkreflectionDigest 在每个 trace case 绿(即它是活门不是死门)。
  3. 35d0befa 上用 minecraft-1.21.4-startup trace 做首帧 link 延迟 A/Bpublish ≤ relink
  4. nm 复核 libMobileGLServer.sopublish 下不再引用 glslang 库符号(注意 ProgramObject.h 传递包含 ShaderObject.hShaderCompileTask.h,所以这条必须nm 验证而不是断言)。
  5. server 峰值 RSS 相对 P1a 基线下降;两个进程的 RSS 合计与 §16-R14 的预算对表。

P6 — 数据面性能(6 天)

交付物PendingResidentWrite 借用 ring slot(用 *RetiredTail 门控);全局 UBO ring 进 shm;解码移到 mgl-srv-io;可选 mgl-client-txbind 合并(凭数据决定);mirror-map 两次映射消除 ring wrapMOBILEGL_IPC_INLINE_PAYLOADS 负面对照;§6.4 方案 Breplica adopt client shadow)的可行性评估与实现(若 Tracy 数据支持);Windows AF_UNIX 评估(§11.5);MOBILEGL_IPC_SPIN_USMOBILEGL_IPC_PRESENT_CREDIT 的设备调优。

验收:两台设备上 minecraft-1.21.4-fabric-sodium-in-world 的配对 A/B,每项优化用自己的开关单独可 A/B;P2/P4 门在任意开关组合下不回归;输入延迟直方图不因任何优化恶化。

P7 — persistent map 与 ≥16MiB 采纳(8 天,若 P0 spike B 全否则缩为 2 天)

交付物SEG_ADOPT(server 分配)+ 三档探针(T2/T1/T0)+ 自动回退到 P4.5 路径;阻塞 AcquirePersistentMapclient 侧注册 ResidentSubDataMOBILEGL_IPC_RESPAWNMOBILEGL_IPC_ADOPT_TIER 的互斥检查(§5.8)。

验收LargeArenaAdoptionScenarioResidentIndexScenario 在采纳开启下绿;bench.sh 在 Mali 设备 3B159D009VZ00000 上用 minecraft-1.21.4-in-world 报出 {monolith, split+采纳, split+回退} 的 p99 帧时,以 MC 26.3 的 163→21ms 为标尺。 "设备 X 上拒绝,已记录,回退成本 N ms" 是本阶段的可接受结论——因为回退路径在 P1a/P4.5 已交付并测量。

P8 — XFB / compute / 健壮性 / 多线程(6 天)

交付物XFB capture writeback 与 scatter(全部在 server 对 replica 执行,只有合并后的 range 过线);RecXfbAccounting 与共享 helper 的完整接线(§2(g)-2;注意它必须跟着 RecBindTransformFeedback 的对象切换走,Core.cpp:1273,1296);GS strip 顺序修正移到 servercompute dispatch/indirect/barrier/image load-storeEvGlErrorglGetError 的顺序 + MOBILEGL_IPC_STRICT_ERRORS 诊断开关;server 死亡的 device-lost 闩锁与 client 死亡的 server 拆机;外来线程 sync/query 的 AuxRequest;修 EGLOperationMutex 既有漏洞(ReleaseThreadSwapInterval)。

验收

  1. split 下 Xfb*(5)、Tessellation*(2)、SsboArrayDynamicIndexScenarioImageLoadStoreSsoScenario 双 backend 绿。
  2. tools/cts/scripts/run_cts_local.py --backend {DirectGLES,DirectVulkan} --env MOBILEGL_TRANSPORT=spawn 在 GL33 caselist 上 conformance rate 与 monolith 相差 ≤ 0.5 个百分点(按项目既定的逐 backend 表格式报告:行=GL 版本/扩展,列=状态计数,conformance rate = Pass/(Pass+Fail),分母不含 NS)。
  3. 故障注入测试在帧中 SIGKILL serverclient 干净地以 EGL_CONTEXT_LOST 退出而不崩溃。

XFB 场景的 RecXfbAccounting 骨架其实在 P1a 就要落地(helper + 记录 + applier 分支),只是这里才被真正测到。§5.9b 的生成器会在 P0 就把它标成未映射并让编译失败,从而强制这个顺序。

P9 — Android 生产窗口路径(10 天)

交付物MobileGLServerServiceandroid:process=":mgl");Messenger/AIDL 的 Surface 交接;surface 生命周期(surfaceDestroyed、1×1 pbuffer 交换舞、resize)作为协议消息;ResyncSnapshotAPK 打包与 validate-plugin-apks.sh 更新;MOBILEGL_TRANSPORT 进 plugin V2 metadata 与 FCL 用户 env 偏好。

验收FCL 在 35d0befa 上以 split 模式把 Minecraft 1.21.4 拉到主菜单并进入世界;bench.sh 在同一个热窗口内报出 split vs monolith 的游戏内 FPS 与输入延迟plugin APK 通过 .github/scripts/validate-plugin-apks.sh;旋屏/后台切换的 surface 销毁重建无泄漏无挂起;两个进程的合计 RSS 落在预算内。

合计 ≈ 77 人日 ≈ 16 周5+6+4+9+3+5+5+4+6+6+8+6+10)。里程碑:第 3 周末 Linux 上跨进程渲染出第一帧P1b),第 5 周末真机 OpenRA 绿P2),第 6 周有 monolith 侧的独立收益数字P2.5)。


16. 风险与对策

# 风险 对策
R1 replica applier 在某个长尾副作用上与 client 的 MG_State 语义分歧——具体形态是 MG_Impl 在 table 调用旁做的 mutation(§2(g) 已确认两族:EnsureGeneratedMipmapStorageAllocatedAccountTransformFeedbackPrimitives)。症状是错误像素或错误查询结果,不是崩溃 §5.9b 的第二个生成器把这一面变成编译期门:MG_Impl 里任何与 table 调用同函数的 mutator 未映射即 #error。两族已知实例在 P1a 就用共享 helper 接线。P2 的门是全部集成场景 + trace 语料的逐名通过集对齐,远比 Feat/CS-Delta-IPC 的两 GLContext 逐字段比较(且只查了 5 域中的 2 域)严苛。外加 MOBILEGL_IPC_VALIDATE_SERVERserver 侧保留 MG_Impl 校验器,分歧变成 server 侧 GL error 而非错误像素;CI 常开,出货构建用 kPrevalidated 短路)
R2 应用通过 coherent persistent map 写下的字节丢失SyncPersistentMappedRange 无 client 侧调用者) §5.10 三件套:map/unmap 上线、client 侧块粒度推送、PersistentCoherentMapScenario 作为 P1a 门。这是本轮新增的最高优先级修复
R3 read-after-GPU-write 静默读到陈旧 shadowMarkGpuWritten 无 client 侧建立者) §5.6bclient 在每个 draw/dispatch 发射点保守置位并记 emitSeq;读入口强制 publish+等待+排空;EvGpuWritten 降级为收窄提示。§7.4 的排空点补上四个 buffer 读入口
R4 零 timeout 轮询循环挂死(轮询入口不是 publish 触发器) §7.2glClientWaitSync/glGetSynciv/glGetQueryObject*(AVAILABLE|NO_WAIT) 全部成为门铃点,GL_SYNC_FLUSH_COMMANDS_BIT 无条件 publish;连续 N 次无进展升级为阻塞 round trip。P3 有专门的门
R5 fence 完成度退化成帧计数推断DirectGLES 的 completedFrameSerial 只在 Present 前进),重蹈 MC 1.21.5 的 native-heap OOM §8 末尾:server 侧真 fence + 非 present 轮询 + EvFenceSignaled;§9.3 的非 present fence tick 同时解决无 present 循环下的 ring 饥饿
R6 纹理每次更新都传整 level(永不清 dirty flag ⇒ union box 单调增长) §5.6aclient 在发射后立刻 MarkStorageDirty(...,false)ack 问题由"resync 从完好 shadow 传整 level"+"硬 drain 后重发未 apply 记录"两条收口。已确认 MG_Impl 从不读自己的 dirty 状态,所以清是安全的
R7 每 draw 编解码成本超过它替换掉的东西MC 级帧(1000-4000 draw)反而更慢;且总 CPU 工作量本来就变大(遍历跑两次) 记录是 FlatBuffers struct8B header + 定长),无 verifier walkpublish 是每记录一次 release store 而不是 64KiB 攒批(§7.2)。TracyPlot 两侧计数器在 P0 就落地P2.5 在第 6 周给出 inproc 的证伪数字并带逐线程 CPU 时间mgl-srv-apply 绑大核(§10),mask 打日志;P3 门要求 split 帧时在 monolith 10% 内授权后续优化
R8 client 侧等待全是跨进程自旋(无 producer 侧门铃),手机上一颗大核满频空转 §6.2a 的双向 doorbellproducerParked + 反向 1 字节;自旋窗口 MOBILEGL_IPC_SPIN_US 可调可测。inproc 用 condvar
R9 SEG_EVENT 满 + client 被 credit 阻塞 = 双向死锁 §7.4:等待循环内必须排空;EvLogLine 有损(覆盖最旧 + eventDropped 计数);语义事件无损,满时 server 置 eventRingFull 并停在记录边界上停止 apply。P4 有故障注入门
R10 端到端延迟叠加client credit + server FIF + 驱动深度 = 4-5 帧) §9.1:credit 默认 1;文档写出叠加公式;P3/P9 增加输入延迟直方图门,只有实测吞吐收益抵得过实测延迟才调高
R11 inproc 因为四个进程全局而不可行,从而 P2.5 这个最早的证伪门消失 §12.1/§12.2:拆成两个 CMake option(出货只开 spawn,热路径无 TLS);四个全局都要角色隔离,shim 需求列全,非箭头用法实测 133 处;P0 结束前必须拍板是做隔离还是把 inproc 降级为纯测试模式,并写清后者对 P2.5 的含义
R12 分配类 GL 错误晚到,OOM 探测惯用法失效 §5.6c:只把分配类入口标 kNeedsAck(罕见且本来就贵),其余保持晚到;glGetError 永远本地。P4 有 OOM 探测门
R13 server 分配的 host-visible coherent 内存无法导出重映射,丢掉 ≥16MiB 采纳(值 p99 163→21ms、~400MB RSS 排在最后P7),且 P0 的 spike B 在第一周就给出方向。此时 P1a/P4.5 的 shadow 路径已交付并测量。阶段明确允许"拒绝,已记录"的结论。前端已容忍 nullptr(三处),kill switch 已存在,无需回滚任何代码
R14 内存翻倍无预算client 段(SEG_CMD 8MiB + SEG_STAGE 32MiB↑)+ 完整 replica context(每 buffer 一份 PipeResource、每 texture level 一份 MipmapStorage+ server 自己的三个 ringUBO/unpack/upload 各 4→64MiBManagers.cpp:82-96+ 64MiB buffer poolManagers.cpp:566)。合计可达 ~450MiB 新增,而本项目把"省 400MB"当作采纳修复的头条成果,且有 blanket-immutable 导致 LMK 屠杀的记忆 计划里与 round-trip 预算并列写出稳态内存预算P1a 验收记录两个进程的 RSS(不只是 server);SEG_STAGE 上限由实测定而不是默认 256MiB;优先推进 §6.4 方案 Breplica 采纳 client shadow),因为它同时消掉重复 shadow 而不只是一次拷贝
R15 排期乐观P0 5 天含两个 spike + 四平台 shm + SCM_RIGHTS + 两个代码生成器;P1a+P1b 10 天做完整 client 与 server)。校准点:Feat/CS-Delta-IPC 10 个 commit / 6668 行、从未渲出一帧,并自承四天耗在一个不可复现的回归上 P1 已拆成 P1a/P1b,设备 retrace 移到 P2 出口;P2.5 是排期风险的退火器——第 6 周就能拿到"这条路值不值得走"的数字,且它本身不依赖任何跨进程工作。若 P0/P1 超期 50%,先跑 P2.5 的 inproc 部分再决定是否继续
R16 socket transport 是新实现,而上一版有每次 send 的 UAF、无上限分配、无 fd 传递 从设计草图重写而非修补:读时按 64MiB 上限校验 magic/长度;接收缓冲不足时返回所需大小且保留消息async_writeshared_ptr payload 自持缓冲;socketpair + 继承 fd 完全去掉 accept/connectWindows 用 overlapped named pipe 对,§11.5)。SCM_RIGHTS 是 P0 交付物并带独立测试
R17 Android 交付链server .so 打包、untrusted_app 域 exec、trace app env 透传)比想象的重,或被 AGP/SELinux 挡住 P0 的 spike A 在第一周就验证;P1-P8 全部离屏且不依赖它(Linux 门优先);两条回退:裸 exec PIE server 配 AHardwareBuffer_sendHandleToUnixSocket blit-back;或把 split 作为 headless/工装专用配置发布
R18 spawn 出来的 server 继承 MOBILEGL_TRANSPORT 而无限 fork §11.1 双保险:显式 envp 剔除 + mobilegl_server_main 强制 MonolithP1b 有进程树计数门
R19 HeadlessGL 的 fork 预检留下持有 GPU 的孤儿 server §11.3:EOF 即时退出(亚秒);就绪握手有界重试;P1b 有 pgrep 计数门
R20 MobileGLServer 在两个桌面门里都找不到dladdr 对静态链接的 itest 与显式 -DMOBILEGL_LIBRARY 的 retrace 都失效) §11.1MOBILEGL_IPC_SERVER_PATH 为主、dladdr 兜底;RUNTIME_OUTPUT_DIRECTORY 对齐;每条新 ctest ENVIRONMENT 都注入;并复核 CI artifact 搬运后绝对路径是否还成立
R21 mobilegl_server_main 在出货构建里 dlsym 不到(非 Debug 的 hidden visibility preset §11.2:显式 visibility("default")P0 加 nm -D 断言
R22 Magma 的 present 节奏被 IPC credit 改变(它从不注册 SetSwapInterval 且偏好 MAILBOX/IMMEDIATE MOBILEGL_IPC_PRESENT_CREDIT 可配;P6/P9 在设备上测量输入延迟与帧节奏;若 Magma 需要,把"注册 SetSwapInterval 并映射到 FIFO"作为独立的 dev 变更,不让两套机制同时管节奏
R23 两件 Android 产物版本漂移 一份共享库两个角色:server 是 ~30 行 stubdlopen(libMobileGL.so) + dlsym(mobilegl_server_main)Hello/Welcome 里的 build fingerprintgit hash + Records.def hash)不匹配 → 明确报错而非静默协议故障
R24 SEG_CMD 的记录被并发写坏导致 applier 游标走飞 §6.3 的运行期边界检查(size >= sizeof(T) && size <= remainingRingBytes && (size%8)==0kVarTail 另查尾长自洽),违反即 Fatal{ProtocolCorruption},绝不进入 UB

17. 开放问题

  1. T1/T0 采纳在 Adreno 830 与 Mali-G925 上到底能不能用?P0 的 spike B 在第一周回答(导出 HOST_VISIBLE|HOST_COHERENT VkBuffer 的 fdclient mmap 后回读),与 SCM_RIGHTS 测试同批。若两台设备都不行,P7 缩为"记录并回退",节省 6 天;若可行,还要回答 GLES 侧能否用 GL_EXT_memory_object_fd + glBufferStorageMemEXT 走同一条路(DirectGLES 的采纳今天走的是 glBufferStorageEXT + glMapBufferRange(PERSISTENT|COHERENT),不是外部内存)。
  2. glGetError 的严格性 CTS 到底要求到什么程度? §5.6c 已把分配类改成同步 ack,剩下的晚到错误里,哪些 CTS case 可能观察到?P8 需要列出清单。若清单为空,MOBILEGL_IPC_STRICT_ERRORS 可以永久保持默认关。
  3. P2.5 的 inproc 数字若为负怎么办? 需要事先约定:若 inproc 相对 monolith 无收益甚至更慢(含亲和性绑定之后),是继续(因为拆分本身还有内存隔离、崩溃隔离、工装价值)还是收缩到 headless 工装用途?建议在 P2.5 前由协调者拍板判据,并同时约定"绑大核后仍无收益"与"未绑核无收益"是两个不同的结论。
  4. §12.2 的隔离取舍inproc 做四全局角色隔离(含 133 处非箭头用法的 shim)值不值?若判定不值而把 inproc 降级为纯测试模式,P2.5 测的就不再是 monolith 渲染线程交付物——那时 monolith 侧的收益要靠什么证明?P0 结束前必须有答案。
  5. §6.4 的拷贝目标选方案 A 还是 B? 方案 Breplica 的 PipeResource 采纳 client 的 SEG_SHADOW 只读映射)能把 buffer 上传路径从 3 次降到 2 次并消掉重复 shadow(对 R14 的内存预算意义更大),但要处理 server 侧写(WritebackFromBackend、生成 mip、CopyImage 镜像)的 copy-on-write 升级。P4.5 先做 A 并测量,P6 由数据决定是否做 B。
  6. SEG_SHADOW 在 Android 上应该用 ASharedMemory 还是 memfd 前者是平台正道且有 setProt 只读降权(正好匹配"client 拥有、server 只读"),后者有 sealing。大 buffer 频繁重映射的场景需要一次实测。
  7. client 侧是否需要 mgl-client-tx 发送线程? 只有 P6 的 TracyPlot 数据能回答;在此之前不要预先加线程(会引入拷贝或锁)。
  8. ResyncSnapshot 与采纳的互斥能否放松? §5.8 目前规定 MOBILEGL_IPC_RESPAWN=1MOBILEGL_IPC_ADOPT_TIER != 2 互斥,因为 adopted store 的字节在 server。是否值得为 adopted buffer 单独做一条"server 死亡时其内容视为丢失、按 hasDefinedContent=false 重建"的降级路径?取决于 MC 的 chunk arena 在 respawn 后能否被应用自己重填。
  9. Windows AF_UNIX-everywhere 是否值得? asio 的 IOCP async_acceptAcceptEx(AF_UNIX 从不支持);我们用继承 overlapped 句柄绕开 accept,理论上可行但需真编真跑。P6 评估,named pipe 是已知可用的默认。
  10. P9 的 ART 启动成本具体是多少? 若不可接受,是否接受"游戏内走 monolith,工装/CTS 走 split"的长期二元形态?
  11. tools/trace_replay 的 Android 应用内路径是否从非主线程驱动 GL、是否每重放帧调 Present 桌面重放器传 --singlethreadtrace_replay_core.cpp:430),Android 应用内路径本次未完整追踪,它决定该工装能否验证节奏模型(尤其是 §9.1 的输入延迟直方图)。
  12. MOBILEGL_IPC_PERSISTENT_BLOCK_KB 的默认值与脏块判定方式P1-4 的保守版(整 mapped span 按块重传)在 Create/Flywheel fixture 上的实测代价是多少?精确版用 memcmp 还是 mprotect 写屏障?前者对 1MB 块是 ~50µs 量级且只在真正 mapped 的 buffer 上跑,看起来够用,但需要 P2 的数据确认。
  13. SEG_STAGE 的上限该定多少? R14 要求由实测定而不是默认 256MiB。需要 P2 之后用 MC in-world 与 Create 两类 fixture 的 stage-* Tracy 计数器给出 p99 占用。

附:环境变量与 CMake 选项汇总

CMake

选项 默认 说明
MOBILEGL_BUILD_DISAGGREGATED OFF 出货形态。开启后 MG_Remote/**SOURCE_FILES,支持 spawn/unix:/pipe:。四个进程全局保持普通全局,GL 热路径无 TLS
MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF CI/调试形态,隐含开启上者,额外加四全局角色隔离 shim
MOBILEGL_FLATC_EXECUTABLE 只服务 CI 的 flatc-check;默认构建图里没有 flatc
MOBILEGL_BAKED_INTERNAL_SHADERS ON (P5+) DirectVulkan 的 blit/depth-mipmap shader 构建期烘 SPIR-Vmonolith 也受益

运行时

变量 默认 说明
MOBILEGL_TRANSPORT monolith monolith / inproc / spawn / unix:<path> / pipe:<name>
MOBILEGL_IPC_SERVER_PATH server 可执行文件路径(主要发现机制dladdr 兜底)
MOBILEGL_IPC_RING_MB 8 SEG_CMD 大小
MOBILEGL_IPC_STAGE_MB 32 SEG_STAGE 初始大小;上限由实测定(§17-13)
MOBILEGL_IPC_PRESENT_CREDIT 1 client 允许领先的 present 数(1-4);延迟叠加见 §9.1
MOBILEGL_IPC_SPIN_US 50 挂起前的自旋窗口(两侧 doorbell 共用)
MOBILEGL_IPC_POLL_ESCALATE 64 同一 handle 连续无进展轮询多少次后升级为阻塞 round trip
MOBILEGL_IPC_PERSISTENT_BLOCK_KB 64 persistent-map 推送的块粒度
MOBILEGL_IPC_PROGRAM relink (P1-4) → publish (P5+) program artifact 传输方式;relink 保留为常驻 oracle
MOBILEGL_IPC_ADOPT_TIER auto auto/0(T0)/1(T1)/2(T2 拒绝);与 MOBILEGL_IPC_RESPAWN 互斥(§5.8
MOBILEGL_IPC_SHADOW_SHM 1 (P4.5+) shadow-in-shm 零拷贝
MOBILEGL_IPC_INLINE_PAYLOADS 0 负面对照:一律内联,不用 SEG_STAGE
MOBILEGL_IPC_SERVER_AFFINITY auto mgl-srv-apply 的核绑定;autoShaderCompilePool 的大核探测
MOBILEGL_IPC_VALIDATE_SERVER CI=1,出货=0 server 侧保留 MG_Impl 校验器,分歧变成 server GL error
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 死亡后重启 + ResyncSnapshot
MOBILEGL_IPC_IDLE_EXIT_S 30 server 的最后保险看门狗(EOF 应当即时退出)

保留的既有负面对照开关MOBILEGL_ESPRYT_DISABLE_UBO_RING_UNPACK_RING_UPLOAD_RING_INVALIDATE_FLUSHMOBILEGL_DISABLE_LARGE_BUFFER_ADOPTIONMOBILEGL_COHERENT_AS_FLUSH在拆分模式下照常生效,§5.10/§6.8)。