Files
MobileGL/docs/Disaggregated/notes/recovered/wf2/final/part2.md
T
swung0x48 aba329cc53 [Docs] (Disaggregated): P6's plan and contract draft; the working notes move from a local directory into the repository
Two things land together: the plan and contract draft P6 needs before it starts, and the
working notes that until now existed on exactly one machine.

1. P6's plan and contract draft (neither started; not one line of code)

     docs/Disaggregated/P6-SPAWN-PLAN.md      the package plan
     docs/Disaggregated/P6-CONTRACT-DRAFT.md  the contract draft (moves to
                                              MG_Remote/CONTRACT-P6.md when c6 lands)

   The three findings that mattered, all written into the documents:

   a. P6 is SMALLER than its roadmap cell suggests. The transport primitives are already in
      the tree and already tested: SocketDoorbell including hangup detection and the two
      lost-wakeup fences (Doorbell.cpp:211-360, four cases in FdPassingTest), SCM_RIGHTS fd
      passing, ShmSegment::Adopt, and the SurfaceOp / WindowKind schema already defined in
      protocol.fbs. inproc uses a real second shm mapping rather than an alias, and
      SessionRings.h:22-28 says that was done so today is not the first time it runs. P6 is
      assembly plus a control plane plus a process, not "write a socket transport".

   b. The evidence for "P6 is only a transport swap" has EXPIRED. It was bought by P5c, whose
      audit found 59 direct memory accesses in a tree that already looked clean. P5e changed
      the tree again. So the first package, a6, is a read-only audit that writes no code, and
      every contract row that depends on it is marked PENDING a6 rather than decided in
      advance.

   c. P6 does not get a game running on the phone. ANativeWindow* is meaningless in the child,
      and protocol.fbs:186-196's own comment says Android transfers the window out of band -
      that band is P12. P6 lands pbuffer / surfaceless / offscreen, which is what HeadlessGL,
      the split-equivalent lane and trace replay need, and which is exactly what P6's own exit
      gate measures. A real window reaching the server is a named refusal. This gets its own
      section because "P6 is done" would otherwise read as "the game runs now".

   The contract adds rule G: a role may not name a process-local handle. It extends P5c's
   rule E (a role may not name the other role's memory) from memory to identity - EGLDisplay /
   EGLSurface / EGLContext / ANativeWindow* / fd number / pid / mapped address either cross as
   a token this contract defines or they do not cross, and a descriptor crosses only through
   SCM_RIGHTS, where the kernel renumbers it.

   What P5e left for P6 gets its own section. The latch semantics are already designed
   (ARCHITECTURE.md:504) and are copied unchanged. What is new is that after run-ahead the
   client no longer waits while the peer process can DIE: the client holds records that were
   published and will never be applied, and Android's freezer suspends the child without
   closing anything. So slow and dead must be distinguishable WITHOUT a timeout threshold.
   That is P6's only new design; everything else is carriage. It shares a predicate with E1's
   outstanding re-specification (ID-122), and the two should be designed together.

   Two gates carried over from P5e's lessons: G2 requires integration-spawn's test-name set to
   match integration-split's name for name, because a differing set means the lane is quietly
   running less; and the arm-proof gate requires every entry to record the child pid and
   transport=spawn in its OWN private log - ID-124 established that this kind of false green
   really happens, and a spawn lane that actually ran monolith would pass every other gate.
   S2's shape must be settled before it is written, and the document says why: E1 is the
   cautionary case.

2. The working notes move into the repository (docs/Disaggregated/notes/, 415 .md files)

   ROADMAP / CURRENT_STAGE_PROGRESS / MEASUREMENTS cite ruling numbers, audit line numbers and
   the provenance of gate figures from ~/w7/notes/, a directory that exists on one machine and
   is under no version control. A record cited hundreds of times that lives only locally is not
   a record, so it moves in whole, with its directory structure intact.

   Only .md moves. Scripts, run logs, census and gate result JSON, patches, APKs and nm dumps
   stay where they are: they are evidence DATA rather than documents - large, regenerated per
   run, and mostly carrying absolute paths and device serials that mean something only on the
   machine that produced them. The mapping and this split are written in
   docs/Disaggregated/notes/README.md.

   Two exceptions are recorded there too: P6's BRIEF has been promoted to a first-class
   document and keeps no copy under notes/, so the two cannot drift apart; and recovered/ holds
   material salvaged from failed workflow runs, which may duplicate the like-named phase
   directories and is kept because a few of those were the only complete version at the time.

   CI's citation lint takes docs/Disaggregated/*.md only (non-recursive), and notes/ is
   deliberately outside it: those notes' file:line citations were written against their own
   code heads, and resolving them against today's HEAD would fail wholesale - which is not an
   error, it is their correct state as a historical record. To check one, use the revision it
   declares (--rev).

3. Inconsistencies fixed in passing

   - README.md's status line still said P5e was in progress while the stage table already said
     closed.
   - CURRENT_STAGE_PROGRESS's next-step item 1 still listed the exit gate as pending, though
     (a) and (b) were run in the previous commit; rewritten to closed plus the one item that
     actually remains (E1).
   - ROADMAP's P6 row still had the pre-phase plan in three of its cells; it now carries the
     package order, the scope boundary and the two new gates.

Checked with check_doc_citations.py: 125 citations across docs/Disaggregated/*.md, 0 problems.
2026-09-18 23:31:39 -04:00

89 KiB
Raw Blame History

4. 接口设计:MGPipe

4.1 文件布局与单一真相源

MobileGL/MG_Pipe/                      # client 与 server 都 include;不链接 MG_State,不链接 MG_Impl
    PipeCalls.def                      # X-macro:调用目录的唯一真相源,一行一个调用
    MGPipe.h                           # 由 .def 生成的两张函数表 + 手写 payload 声明
    MGPipeTypes.h                      # 全部 payload POD(trivially copyable,逐个 static_assert)
    MGPipeValueTypes.h                 # ★v2 新增:无依赖的共享值类型(见 §4.7.2)
    MGPipeHandles.h                    # MGPipeHandle、MGPipeKind、保留 handle、slot 分配契约
    MGPipeHostSpan.h                   # 唯一一个"形状随传输而变"的访问器(§4.5.7)
    MGPipeCallbacks.h                  # 反向通道(事件/回复)的函数表,见 §7
    MGPipeRenderStateSpans.{h,cpp}     # ★v2 新增:pipeline/dynamic 划分的唯一定义(§4.5.2)
    generated/PipeTables.inc           # G1:两张函数表
    generated/PipeThunks.inc           # G2:monolith 直调 thunk
    generated/PipeWire.inc             # G3:wire 记录 + static_assert + 运行期边界检查 + applier switch
    generated/PipeVerify.inc           # G4:逐字段影子比对器
    generated/PipeFilled.inc           # G5:written-once 位图与 poison 断言(**逐 verb 世代**)
    generated/PipeCoverage.inc         # G6:477 读点 → MGPipe 调用的映射表
    generated/PipeSpanTable.inc        # ★G7:render-state 的 pipeline/dynamic chunk 表 + setter 一致性测试
MobileGL/MG_Impl/Pipe/
    Tracker.{h,cpp}                    # st_validate_state 类比物(含从 backend 搬来的 ~175 行去抖/解析)
    SlotAllocator.{h,cpp}  CsoCache.{h,cpp}
    HostResolve.cpp                    # 客户端数组界限 / 索引扫描 / indirect count 解析
    CompositeResolver.cpp              # program pipeline 合成体的 handle 生命周期
MobileGL/MG_Backend/MGPipe/
    PipeInputs.h                       # backend 私有的"被推送状态"块(迁移载体,§6.2)
    MGPipeImpl_DirectGLES.cpp          # 用 Espryt 的函数填 MGPipeContext
    MGPipeImpl_DirectVulkan.cpp        # 用 Magma 的函数填 MGPipeContext
MobileGL/MG_Remote/                    # 传输,继承 PLAN.md §13(删掉 Server/ReplicaContext.*)
    Server/PipeApplier.cpp  Server/PipeObjectTables.{h,cpp}  Server/IndexHostMirror.{h,cpp}
scripts/gen_pipe.py                    # 跑 G1..G7
scripts/gen_pipe_dirty_surface.py      # ★v2:MG_Impl mutator → 聚合世代 的覆盖生成器(推论 4)
scripts/check_doc_citations.py         # ★v2:docs/**.md 的 file:line 必须解析到存在的行

PipeCalls.def 一行一个调用,七个生成器消费它:

// MG_Pipe/PipeCalls.def   —  X(Name, PayloadStruct, Class, Flags)
//   Class : kScreen | kCtxCso | kCtxState | kCtxObject | kCtxVerb | kCtxQuery
//   Flags : kNone | kNeedsAck | kHasBlob | kVarTail | kHostSpan | kReplySlot | kOptional
#define MGP_CALL_LIST(X)                                                          \
  /* ---- screen ---- */                                                          \
  X(GetCaps,             MGPCaps,             kScreen,   kReplySlot)              \
  X(ResourceCreate,      MGPResourceDesc,     kScreen,   kNone)                   \
  X(ResourceRespecify,   MGPResourceDesc,     kScreen,   kNone)                   \
  X(ResourceDestroy,     MGPHandleOnly,       kScreen,   kNone)                   \
  X(MapPersistent,       MGPHandleOnly,       kScreen,   kReplySlot|kOptional)    \
  /* ---- CSO ---- */                                                             \
  X(CreateRenderState,   MGPRenderStateDesc,  kCtxCso,   kHasBlob)                \
  X(BindRenderState,     MGPBindRenderState,  kCtxCso,   kNone)                   \
  /* ---- state ---- */                                                           \
  X(SetDynamicState,     MGPDynamicState,     kCtxState, kHasBlob)                \
  X(SetFramebufferState, MGPFramebufferState, kCtxState, kNone)                   \
  X(SetSamplerViews,     MGPSamplerViews,     kCtxState, kVarTail)                \
  X(SetTextureParams,    MGPTextureParams,    kCtxObject,kNone)                   \
  X(SetShaderBuffers,    MGPShaderBuffers,    kCtxState, kVarTail|kHostSpan)      \
  /* ---- verb ---- */                                                            \
  X(DrawVbo,             MGPDrawInfo,         kCtxVerb,  kHostSpan|kVarTail)      \
  X(ResourceSubData,     MGPSubData,          kCtxObject,kHasBlob|kVarTail)       \
  X(RenderbufferStorage, MGPRbStorage,        kCtxObject,kNeedsAck)               \
  /* … 共约 74 项,完整目录见 §4.4 与 part4 的速查表 … */
生成器 产物 替代/新增
G1 struct MGPipeScreen { … }; / struct MGPipeContext { void (*DrawVbo)(const MGPDrawInfo*, …); … }; 替代今天手写的 GLFunctionsTable
G2 monolith thunk:inline void MGP_DrawVbo(const MGPDrawInfo* p){ gPipeCtx.DrawVbo(p); } 替代 gBackendFunctionsTable.GL.*(~93 个 MG_Impl 站点改名即可)
G3 wire 记录结构 + 每种一条 static_assert(sizeof==N) + applier 分发前的运行期边界检查 → Fatal{ProtocolCorruption} 继承并扩展 PLAN.md §6.3 的 Records.def 机制到全部调用
G4 MOBILEGL_PIPE_VERIFY 的逐字段比对器 新增:每份候选设计都被判缺失的语义绊线
G5 PipeInputs::m_filledGen[] 的位/世代定义 + 读未填字段时的 Fatal{UnmigratedPipeInput, "<field>"} 新增(v2:由"位图"升级为"逐 verb 世代",见 §6.2.2)
G6 477 行读点清单 → MGPipe 调用的映射,CI 重生成并 git diff --exit-code,0 UNMAPPED 改造自 Feat/CS-Delta-IPC 的 extract_backend_read_inventory.py
G7(v2 新增) RenderStateParameters 的 pipeline/dynamic chunk 表 + 一个遍历每个 RenderState public setter、断言 pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变 的 MG_Test 新增:D-B1 拒绝三 CSO 时点名要求、v1 却没给自己的完整性绊线

G4、G5、G7 与调用目录从同一份 .def/同一张 chunk 表生成,因此不可能漂移。

接口表用函数指针 struct,不用虚基类。 三条本仓库自己的理由:(1) 边界今天就是函数指针 struct,装在 MG_Backend/Init.cpp:44 的唯一 hook 点上;(2) nullptr 项已经表示"未实现,前端回退"(BackendObject.h:212-215、:265-269),DirectVulkan 确实留空 8 项——一个 null set_* 恰好就是"这个子系统还没迁移,继续拉取",纯虚类只能用说谎的 stub override 来模拟;(3) MG_Test 已经会替换这张表做 mock。稀有的 EGL/caps 面继续留在 pActiveBackendObject 的虚函数上。

4.2 对象模型

4.2.1 Handle

enum class MGPipeKind : Uint8 {
    Buffer=1, Texture, Renderbuffer, Framebuffer, Xfb,
    RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso,
    Fence, Query, Context
};
struct MGPipeHandle { Uint32 slot; Uint32 gen; };   // 8 B,POD,按值走寄存器对
  • slot 稠密、按 kind 分配,把 server 的对象表从哈希表变成数组;SlotAllocator 是 free-list + 高水位,与 IndexGenerator 无关(后者的 LIFO 复用正是问题本身)。
  • gen 只在 slot 复用时 ++,不是每次 respecify。{slot, gen} 在同一 slot 被复用 2³² 次之前唯一;文档写明上界,debug 断言它。
  • GL name 只在 resource_create 的 payload 里出现一次,纯诊断,永不做身份、永不进 memo 键或 content hash。
  • GetLifetimeId() 留在 client 侧作为 tracker 自己的身份,不过线;client 维护 lifetimeId → slot。
  • 保留 handle:{0,0} = null;{slot=0, gen=1, kind=Framebuffer} = 默认帧缓冲(退役 DirectGLES.cpp:1917, 2838, 2867, 9675 四处 pDefaultFramebufferInfo->defaultFBO 身份比较);ShaderCso 的高 1/16 slot 段保留给 program pipeline 合成体(§5.6)。

4.2.2 两种 generation,严格分开

拥有者 回答什么 是否过线
身份(MGPipeHandle::gen) client "还是同一个 GL 对象吗?" 是
MGGen(server 纪元) server "我自己是不是重铸了驱动对象 / 冲了自己的缓存?" client→server 永不;server→client 只以纹理拉取请求的形式出现(§7.5)

接口规范条款:任何 MGPipe 调用都不得要求 client 提供或知晓 MGGen。 反过来也是规范:client 侧的版本计数器永远不是新鲜度的唯一证明——每一个回绕的 Uint16(§2.6)在过线时要么加宽到 32 位、要么与 {slot, gen} 同行。

4.2.3 CSO vs 可变对象

类别 形态 因为 backend 今天就是这么缓存的
VertexElementsCso create/bind/delete VertexInputStateFactory::m_cache,键正是那组字段的 content hash(VertexInputStateFactory.cpp:19-50)
SamplerCso create/bind/delete VkSamplerManager::m_samplers;Espryt 的 BackendSamplerObject(Managers.h:1808-1824)
SamplerViewCso create/delete + 由 set_sampler_views 绑定 TextureResource::{perMipViews, …, storageImageViews}(VkTextureManager.h:173-370);Espryt 的 SyncTextureViewToBackend(Managers.cpp:3616-3707)
ShaderCso create/bind/delete + server 侧惰性特化(D-B2) ProgramFactory::m_cache;BackendProgramObjectImpl
RenderStateCso create/bind/delete,身份 = pipeline 子集(D-B1 v2) Espryt 的值镜像 + 单 Uint16 早退 + 三段 memcmp;Magma 的 ComputePipelineStateHash
Buffer / Texture / Renderbuffer create / respecify / subdata / destroy GLESBufferResource、BackendTextureObject、VkBufferResource、TextureResource
Framebuffer / Xfb per-context 身份 + set_* payload BackendFramebufferObject、m_xfbCounterSlotByObject

CSO 在 client 侧内容寻址(Mesa cso_context/cso_cache 先例):每类一张 ska::flat_hash_map<Uint64 xxHash, MGPipeHandle>,容量上限(render-state 64、vertex-elements 1024、sampler 256、sampler-view 4096、shader 跟随 ProgramObject 生命周期),LRU 淘汰时发 delete_*_state。收益:两个不同 program 设置了相同状态时 server 侧零状态转换。

任何 create_* 都不返回 server 铸造的 handle。 这是对 gallium 的有意偏离(D1),也是这份目录能在零创建 round trip 下远程化的根本原因。BackendSyncHandle/BackendQueryHandle = void*(BackendObject.h:110, 115)随之变成 MGPipeHandle。

4.3 MGPipeScreen 与 MGPipeContext

MGPipeScreen(share group) MGPipeContext
caps、format 能力表、renderer 字符串;buffer / texture / renderbuffer / sampler / shader 的对象命名空间;fence 全部 set_*、全部 CSO 绑定、VAO / FBO / XFB 对象 / query 的命名空间、命令流、present

v1 只有一个 screen、一个 context、一条 flow。但两张表从第一天就分开,因为事后拆分意味着给每个记录种类重新编号。两处必须重新归类的事实:GetTextureBindGeneration() 与 GetSamplingResolutionGeneration()(Core.h:130, 136)是绑定(context)事实却住在 share-group 作用域的 TextureState 里;GetTextureContextId()(:143)直接就是 context handle。

4.4 完整调用目录

4.4.1 MGPipeScreen(14 项)

调用 payload 取代
get_caps(MGPCaps* out) DynamicBackendParameters(BackendObject.h:302-522,~90 标量,平坦 POD)+ RendererInfo + FormatCapabilityCache(:88-99)+ callMask 40 个 pActiveBackendObject-> 站点、89 个 caps 读点
resource_create(h, const MGPResourceDesc*) §4.5.1 buffer/texture/renderbuffer 的创建
resource_respecify(h, const MGPResourceDesc*) 同上 BufferBackendOps::Respecify(BufferObject.h:80)泛化
resource_destroy(h) handle OnDestroy(:101)+ 两个 WeakPtr GC 扫描
map_persistent(h) → MGPMapResult / unmap_persistent(h) — AcquirePersistentMap(:112)。改造期不碰(D-B4)
fence_create/status/wait/destroy handle (+timeout) FenceSync…GetSyncStatus(:220-224)。两值契约(:243-249)逐字保留
query_create/begin/end/available/result/destroy handle + kind BackendObject.h:230-256
EGL 生命周期 8 项 BackendObject.h:548-559 原样保留为虚函数(罕见)

callMask 取代"槽位是否为 null"这个隐式能力探测(GL_Query.cpp:471, 545, 768)。v2 修订的能力位集(v1 的五个 emulation 归属位按 D-B7 删除): kCapViewportArray、kCapFloat64VertexAttrib、kCapResidentSubData、kCapCpuXfbPrimitiveAccounting、kCapTimerQuery、kCapOcclusionQuery、kCapXfbPrimitivesQuery、kCapNeedsHostIndexBytes(server 侧的 restart 重写/multi-draw 展平需要索引宿主字节 → split 下开启索引宿主镜像,D-B7)、kCapNeedsHostUboBytes(server 侧要把具名 UBO 打进自己的 ring → 需要 set_shader_buffers 的 host payload,D-B8)。 删除:kCapPrimitiveRestart、kCapPrimitiveRestartFixedIndex、kCapMultiDraw、kCapMultiDrawIndirect、kCapMultiDrawIndirectCount——它们表达的"归属开关"不可表达(D-B7)。

4.4.2 MGPipeContext — CSO(15 项)

create/bind/delete × { render_state, vertex_elements, sampler, sampler_view, shader }。payload 见 §4.5.2-4.5.5。

4.4.3 MGPipeContext — set_*(17 项,v2 从 14 增至 17)

调用 取代的拉取点
set_dynamic_state(MGPBlobRef chunks, Uint16 version) (v2 新增) 渲染状态里 m_pipelineStateVersion 不覆盖的那一半(viewport / scissor / depth range / blend color / line width / polygon offset / stencil ref+write mask / clear values / sample coverage / hints / point-size 族)。这条让 glViewport 不再铸造新 CSO(D-B1)
set_framebuffer_state GetFramebufferBindingSlot ×19、GetAllAttachmentObjects、GetDrawBuffers、GetReadBuffer、4 处 pDefaultFramebufferInfo
set_vertex_buffers(start, count, const MGPVertexBuffer*) VAO binding-point 走查
set_index_buffer(const MGPIndexBuffer*) GetIndexBufferBindingSlot;独立调用——VAO config version 不是它的超集(D5)
set_indirect_buffers(drawIndirect, parameter) GetBufferBindingSlot(DrawIndirect/Parameter)
set_sampler_views(start, count, const MGPBoundView*) (v2:删掉 stage 形参) GetTextureUnitObject ×19、GetActiveTextureUnit ×8、GetTextureBindGeneration ×5。client 侧已解析(§5.5)
bind_sampler_states(start, count, const MGPipeHandle*) (v2:删掉 stage 形参) TextureUnit.h:394
set_texture_params(res, const MGPTextureParams*) (v2 新增) base/max level、swizzle、depth-stencil mode、LOD 钳。必须独立于 sampler view,见下
set_shader_images(start, count, const MGPImageView*) GetImageTextureBinding ×14;退役 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573)
set_shader_buffers(cls, start, count, const MGPBufferRange*, writableMask) (v2:Uniform 类的 range 可带 MGHostSpan payload) GetBufferBindingPoint ×19、GetTouchedBufferBindingPointCount ×2。cls ∈ {Uniform, ShaderStorage, AtomicCounter}。payload 由 kCapNeedsHostUboBytes 门控(D-B8)
set_stream_output_targets(count, const MGPBufferRange*, const Uint32* offsets, Uint64 generation) XFB 绑定走查
set_global_constants(shaderCso, MGPBlobRef, Uint32 version) MapUBO/GetUBOData/GetUBOSize/GetUBOContentVersion(§4.6 D6)。只覆盖默认 uniform block
set_vertex_attrib_defaults(Uint32 mask, const MGPAttribValue*) GetCurrentVertexAttribute ×2;float/int/uint 视图由 ClassifyVertexAttribType(Core.h:51)在 client 侧解析
set_pixel_pack_state(const PixelStoreParameters*) 6 个 PACK 读点。没有 unpack 对应项(§4.6 D5)
set_patch_state(Uint32 vertices, const Float outer[4], const Float inner[2]) GetPatchVertices/…OuterLevel/…InnerLevel ×6。同时是 shader variant 输入
set_draw_program(shaderCso) / set_dispatch_program(shaderCso) GetProgramForDraw ×7、GetProgramForDispatch ×3。含 composite(§5.6)

为什么删掉 stage 形参(v2):MobileGL 的纹理单元空间是合并的,不是分 stage 的——TextureState::m_textureUnits 是 Array<TextureUnit, MAX_TEXTURE_IMAGE_UNITS> 且 MAX_TEXTURE_IMAGE_UNITS = 192(TextureState.h:41, 128),每 stage 的 32 只是一个广告数字(:46);TextureUnit 本身是 Array<BindingSlot<ITextureObject>, TextureTargetCount> 加一个 sampler(TextureUnit.h:20, 24-25);两个 backend 都按合并单元绑定(g_boundTexturesCache[192][TargetCount])。同一个合并单元可以被两个 stage 采样。加 stage 维度会逼 client 要么按 stage 复制 view、要么发明一个 GL 未定义的 stage 归属,而 server 还得把它塌回去。stage 只在目标 API 真正需要时出现(Magma 的描述符 stage flags),由 server 从反射归档推导。

为什么纹理参数不能只挂在 sampler view 上(v2):Espryt 对每个 touched 单元绑定与每个 draw-FBO attachment 纹理都调 SyncTextureParamsToBackend(DirectGLES.cpp:1548-1560 单元表、:1580-1601 attachment 表),而 RequireImageBindableStorage 会置 m_forceTextureParamsResync,正是因为通道加宽后的载体需要一个前端 params 版本不会移动的 swizzle 覆盖(Managers.cpp:2815-2821)。一张只作 FBO attachment、只作 image 单元绑定、或只作 glCopyImageSubData 端点的纹理没有 sampler view,它的 glTexParameter 状态在 v1 的映射里没有载体。所以:base/max level、swizzle、depth-stencil mode、LOD 钳挂在 set_texture_params(res, …) 上;MGPSamplerView 只带"视图限制"(min/num level、min/num layer、别名格式)。 这同时让 glTextureView 保持它真正的身份——一个有自己参数、自己能当 FBO attachment、自己能当 glTexSubImage 目标的真纹理对象(TextureObjectView.cpp:281, 290)——而不是被降格成"普通 view CSO"。

迁移期额外一项(显式临时):set_residual_value_state(MGPBlobRef),见 §6.3。

4.4.4 MGPipeContext — transfer(12 项)

resource_subdata(buffer + texture 同一形状,带步长的多 region 描述符,§4.5.6)、resource_flush_range(h, Range1D, Flags<BufferMappingAccessBit>)(携带应用真实的 access flags,BufferObject.h:94-96)、resource_readback(h, off, size, MGPReplySlot)、resource_copy_region、blit、clear(一条,判别式合并今天的 Clear + 4 个 ClearBuffer* + 4 个 ClearNamedFramebuffer*)、generate_mipmap(h, target, const MGPMipPlan*)、read_pixels(const MGPReadbackInfo*, MGPReplySlot)、get_texture_image(...)、buffer_subdata_resident(h, off, MGPBlobRef)(可为 null)。

buffer_subdata_resident 的 per-backend 可选性必须被接口允许。 Espryt 注册它、Magma 故意不注册(VkBufferManager.cpp:104-111),差别是 glBufferSubData 在活的 coherent map 上的排序语义(BufferObject.h:84-92 的 Minecraft 撕裂 postmortem)。表现为 kCapResidentSubData 位 + null 项。

4.4.5 MGPipeContext — 命令(10 项)

void draw_vbo (const MGPDrawInfo*, Uint32 drawIdOffset,
               const MGPDrawIndirect*, const MGPDrawRange*, Uint numDraws);
void launch_grid(const MGPGridInfo*);
void memory_barrier(GLbitfield bits, Bool byRegion);
void begin_stream_output(GLenum primitiveMode);
void end_stream_output(const MGPXfbAccounting*);
void pause_stream_output();  void resume_stream_output();
void flush(Uint32 flags);
void present(Uint64 frameSerial);  void set_swap_interval(Int interval);   // 后者可 null(Magma)

今天 20 个 draw 入口塌成 draw_vbo 一条,MGPDrawRange[] 就是 MultiDraw* 族今天的形状(gallium 的 pipe_draw_start_count_bias)。

4.4.6 显式删除、不移植的项

  • GetIntegeri_v / GetInteger64i_v / GetProgramiv(BackendObject.h:195-197)。只有 GL_COMPUTE_WORK_GROUP_SIZE(DirectVulkan.cpp:790-795)是真后端答案,进 MGPCaps。
  • ShaderStorageBlockBinding(:207-208)→ 折进 MGPProgramDesc 的反射归档。
  • 总规则:server 不回答任何 client 能自己回答的问题;剩下的每个 server 查询都是 async-with-handle,绝不阻塞。

4.5 关键 payload

4.5.1 MGPResourceDesc(判别式,三种 GL 存储类合一)

struct MGPResourceDesc {
    Uint8  target;            // Buffer | Tex1D..TexCubeArray | Tex2DMS.. | Renderbuffer | TexBuffer
    Uint8  storageKind;       // Mipmap | Buffer   (== TextureStorageType, TextureEnum.h:61-64)
    Uint16 bindMask;          // VERTEX|INDEX|CONSTANT|SHADER_BUFFER|INDIRECT|SAMPLER|SHADER_IMAGE|
                              //   RENDER_TARGET|DEPTH_STENCIL|STREAM_OUTPUT|ATOMIC|ELEMENT_ARRAY
    Uint32 internalFormat;    // 已在前端解析为非压缩后备
    Uint32 width, height, depth;
    Uint16 arrayLayers, levels, samples;
    Uint8  fixedSampleLocations, immutable;
    Uint32 usage;             // BufferUsage
    Uint32 storageFlags;      // glBufferStorage flags
    Uint8  hasDefinedContent;  // NULL-data respecify 之后为 false,BufferObject.h:216
    Uint8  imageBindableHint;  // client 侧 everImageBound,预防性分配(§7.5(a))
    Uint8  glNameForDiag[2];   // 仅诊断
    MGPipeHandle viewOf;       // 纹理视图的存储属主(GetViewStorageOwner,TextureObject.h:100)
    MGPipeHandle bufferForTexBuffer;  Uint64 bufOffset, bufSize;   // kWholeBuffer = ~0,实时解析
};

bindMask 里的 ELEMENT_ARRAY 位是 D-B7 的开关:server 见到它且 kCapNeedsHostIndexBytes 为真时,把该资源纳入索引宿主镜像。

Renderbuffer 保持独立类:自己的 format-capability target 索引(BackendObject.h:85)、自己的 ComponentSizes 上报(RenderbufferObject.h:37-43)、自己的 twin(Managers.h:1838)。

4.5.2 渲染状态:MGPRenderStateDesc / MGPBindRenderState / MGPDynamicState(D-B1 v2)

// MG_Pipe/MGPipeRenderStateSpans.h —— 划分的唯一定义
struct MGPStateChunk { Uint16 offset, length; };
extern const MGPStateChunk kPipelineChunks[];   // G7 生成,来源 = VulkanRenderer.cpp:4826-4906 的字段表
extern const MGPStateChunk kDynamicChunks[];    // 补集
Uint64 MGPipeComputePipelineSubsetHash(const RenderStateParameters&);   // client 与两个 backend 共用

struct MGPRenderStateDesc {          // create:只带 pipeline 子集的 chunk 字节
    MGPipeHandle cso;
    Uint32       chunkMask;          // 未命中时可只发变化的 chunk;全新 CSO 为全 1
    MGPipeHandle baseCso;            // 增量基(chunkMask 非全 1 时有效)
    MGPBlobRef   blob;
};
struct MGPBindRenderState {          // bind:稳态 12 B
    MGPipeHandle cso;  Uint16 version;  Uint16 pipelineVersion;
};
struct MGPDynamicState {             // 动态子集,只发变化的 chunk
    Uint32     chunkMask;
    Uint16     version;  Uint16 pad;
    MGPBlobRef blob;
};

server 侧模型:每 context 一份 working RenderStateParameters(~1.2KB)。bind_render_state 把 CSO 的 chunk 散射进去;set_dynamic_state 把动态 chunk 散射进去。Espryt 的 SyncRenderState 拿到的仍是 const RenderStateParameters&,693 行函数体、单 Uint16 早退、三段 memcmp、g_syncedColorMaskAlphaWidenMask、dual-source decline 一行不动。 Magma 的 pipeline memo 键是 cso.slot,glViewport 不再冲掉它;动态尾巴仍走 ApplyDynamicDrawStateTail 的两级门。

两套 span 划分并存,互不干扰,各有绊线:

划分 用途 定义在哪 绊线
head / blend / tail(DirectGLES.cpp:2038-2047,按 offsetof(BlendStates)、offsetof(LogicOp)) Espryt 驱动侧增量 DirectGLES.cpp 原地,不动 已有:static_assert(is_trivially_copyable_v);RenderState.h:359-368 的字段顺序注释
pipeline / dynamic 线上传输与 CSO 身份 MGPipeRenderStateSpans.cpp,G7 生成 G7 的 setter 一致性测试:遍历每个 RenderState public setter,断言 pipelineSubsetHash 变 ⟺ m_pipelineStateVersion 变

client 侧的取值顺序(热路径,必须照此实现):

  1. m_pipelineStateVersion 未变 → 复用上一个 CSO handle,零哈希;
  2. 变了 → 对 pipeline 子集算 xxHash(~25-30 字,正是 Magma 今天在算的那个)→ CSO map 探测 → 命中发 12 B bind_render_state,未命中发变化 chunk 的 create_render_state 再 bind;
  3. m_version 变而 pipeline 子集未变 → 只发 set_dynamic_state 的变化 chunk(~200 B)。

性能诚实注记:Blaze3D 的 glEnable/glDisable(GL_BLEND) 走 SET_CAPABILITY(RenderState.cpp:312)→ BumpVersions(),所以每次都进第 2 步。交替的两个状态命中两个交替的 CSO,不重发 blob。对比今天:Espryt 1.2KB×3 段 memcmp + Magma ~30 字哈希。净变便宜但差距不大,因此 P2 必须带一个专门的 enable/draw/disable/draw 微基准(MC batch 速率,两台设备)。

4.5.3 MGPVertexElements

携带两个视图,缺一不可:解析后的 VertexAttribute[32](VertexArrayObject.h:17-53)和 VertexBufferBindingPoint(:58-64,初始 stride 是 16 不是 0,:61-62)。VertexArrayObject.h:22-29 记录了合并它们的代价:pointer 调用的 stride 0 被解析成 element size,而 binding-model 的 stride 0 意味着每个顶点读同一个 element,塌成一个害了 KHR-GL43.vertex_attrib_binding.basic-input-case7/8。IsLong 与 Type == Float64 分开携带(:34-39)。仅供查询的 LegacyStride/LegacyPointer(:51-52)留在 client。

4.5.4 SamplerParameters 与 MGPSamplerView / MGPTextureParams

SamplerParameters(SamplerObject.h:72-96,v1 误引为 :468-492)逐字节原样过线,包括 borderColorForm(:66-70)::60-65 明说没有它 backend 无法在 glSamplerParameterIiv 与 fv 之间、或在 VkBorderColor 家族之间选择,因为三种表示(borderColor/borderColorI/borderColorUI,:93-95)永远都被数值填满。SamplerObject::BumpVersion()(:151,m_version 在 :155)同时bump context 级 sampling-resolution generation,因为 MIN_FILTER 决定是否读 mip 链 → 决定 mipmap 完备性 → 决定 backend 到底绑不绑这张纹理。

struct MGPTextureParams {            // ★v2:per-texture-object,与 view 无关
    MGPipeHandle res;
    Uint16 baseLevel, maxLevel;
    Uint8  swizzle[4];
    Uint8  depthStencilMode, pad[3];
    Float  minLod, maxLod, lodBias;
    Uint8  forceResync;              // 对应 m_forceTextureParamsResync(Managers.cpp:2815-2821)
};
struct MGPSamplerView {              // = pipe_sampler_view,**只带视图限制**
    MGPipeHandle cso, texture;
    Uint32 internalFormat;           // 别名格式(glTextureView)
    Uint8  target, pad[3];
    Uint16 minLevel, numLevels, minLayer, numLayers;
    Uint16 samples;  Uint8 fixedSampleLocations, pad2;
};

GetViewStorageOwner()(TextureObject.h:96-100,一个 SharedPtr,且它自己永远不是 view)变成 resource_create 的 viewOf + server 侧 keep-alive。

4.5.5 MGPProgramDesc(create_shader_state 的 payload)

struct MGPProgramDesc {
    MGPipeHandle cso;
    Uint32     stageMask;            // == GetLinkedShaderStages()
    MGPBlobRef spirv[6];             // GetGeneratedSpirv(),逐 stage
    MGPBlobRef reflection;           // Visit() 归档的 LinkArtifacts + SpirvArtifacts(全结构体)
    Uint32     globalUboSize;
    Uint32     reservedNumSamplesOffset;
    Uint8      spirvStatus, nativeFloat64, pointSizeDemoted, enableSpirvValidation;
};

v2 前置条件(P0.5):反射类型必须先搬出 ProgramObject.h。 TypeFacts(ProgramObject.h:44)、ResourceReflection(:76)、XfbVarying(:1146)、LinkArtifacts(:1210)、SpirvArtifacts(:1409)今天全部声明在 ProgramObject.h 里,而该文件 :11 include ShaderObject.h(→ ShaderCompileTask.h → glslang;ShaderObject.h:146 返回 SharedPtr<glslang::TShader>)、:14 include SpvcSession.h(→ spirv_reflect.h)。server 要反序列化进这些类型就必须 include 被门禁止的头。 P0.5 把它们抽到:

MG_State/GLState/ProgramState/ProgramArtifacts.h    # 只 include <Includes.h> 与容器/向量类型

更新 7 个 includer(ProgramFactory.h、UniformManager.cpp、VulkanRenderer.cpp、ProgramInterface.cpp、ProgramLinkTask.h、ProgramObject.h、ProgramTranslationCache.h),并加 CI 断言:ProgramArtifacts.h 的 -H 传递 include 闭包里不得出现 glslang / SPIRV-Cross / spirv_reflect 任何头。没有这一步,P7 的 nm -D | grep glslang 判据不可达。

反射归档序列化整个结构体,机制沿用 PLAN.md §6.9 的 Visit() + sizeof 绊线,但用途改变:不再是"分歧预言机"(没有可分歧的对象),而是schema 完整性绊线:

template <class Ar> void Visit(Ar& ar, LinkArtifacts& a) { ar(a.writtenUniformLocationBits, /*…全字段…*/); }
static_assert(sizeof(LinkArtifacts) == MGL_LINKARTIFACTS_SIZE,
              "新字段请加进 Visit() 并 bump MGL_LINKARTIFACTS_SIZE");

归档必须覆盖:四个 ResourceReflection(各带 TypeFacts)、uniformSamplerOrImageUnitIndex(:1298)、uniformBlockBinding(:1314)、shaderStorageBlockBinding(按名字,:1325)、explicitOpaqueUniformBindings(:1303)、xfbVaryings/xfbStrides/xfbPackedStride/xfbNeedsScatteredCapture(:1357-1394)、computeLocalSize、GS/TCS/TES 事实(:1373-1388)、usesReservedNumSamples(:1345)、uniformOffsets(:1416)。

XfbVarying(:1146-1171)必须带两套拼写:GL 名字(Espryt 的 ESSL 驱动侧捕获列表)和 blockInstanceName/blockName/blockMemberIndex/blockMemberElement(:1163-1170)。

4.5.6 MGPFramebufferState 与 MGPSubData

struct MGPSurface {                  // = pipe_surface
    MGPipeHandle res;
    Uint32 internalFormat;           // 内联!让四个跨对象 mask 在推送时刻零查表推出
    Uint8  kind;                     // Texture | Renderbuffer | None
    Uint8  layered;  Uint16 level;
    Uint32 layer;    Uint16 uploadTarget;  Uint16 pad;
};
struct MGPFramebufferState {
    MGPipeHandle fbo;                // {0,1} = 默认帧缓冲
    MGPSurface   color[8], depth, stencil;
    MGPSurface   readSurface;        // *** client 侧已解析的读表面,不是索引 ***
    Int8   drawBuffers[8];           // attachment 索引,-1 = NONE
    Uint16 width, height, layers, samples;
    Uint8  fixedSampleLocations, isDefault, complete, pad;
    Uint64 contentHash;              // client 计算;server 的 render-pass memo 键 + **client 侧发射抑制器**
};
  1. readSurface 是 client 解析后的表面,按结构消灭 read-buffer-shared-FBO 缺陷类。
  2. internalFormat 内联,四个跨对象 mask(Managers.cpp:5616-5619)在 set_framebuffer_state 内部零查表推出。
  3. contentHash 有两个用途(v2 强调第二个):server 的 memo 键(取代 D7 四元组与 D15 三元组)以及 client 的发射抑制器——hash 未变就不发这条记录,这是 §2.5 里那 ~175 行去抖搬到 client 后的载体。同一模式必须推广到每一条 kVarTail 的 set_*(set_sampler_views、bind_sampler_states、set_shader_images、set_shader_buffers),否则 26.2 的冗余 glBindSampler 会让每个 batch 重发一条变长记录。
struct MGPSubRegion {                // ★v2:形状照抄已存在的 UnpackStagingBlock(Managers.cpp:4340-4390)
    Int32  x, y, z;                  // 目标 box 原点(level 坐标系)
    Uint32 w, h, d;
    Uint64 srcOffset;                // blob 内偏移
    Uint32 srcRowStride;             // 源行距(字节);0 = 紧密(= w * bpp)
    Uint32 srcSliceStride;           // 源片距(字节);0 = 紧密
};
struct MGPSubData {
    MGPipeHandle res;
    Uint16 target, level;
    Uint8  sourceIsVerbatimLevelShadow;  // ★ 取代 backend 里的 `uploadData == mipData` 指针比较
    Uint8  pad[3];
    MGPBox unionBox;                 // union box(server 可选它)
    Uint32 regionCount;              // MGPSubRegion[] 在变长尾(server 可选它们)
    MGPBlobRef blob;
};

同时携带 union box 与 region 列表,由 server 选上传形状。 这不是冗余:Mali 按作业数给纹理上传计价,实测 ~100 个精灵 rect 对一个 union box 是 +6 ms/frame(Managers.cpp:4386-4390)。client 按 MipmapStorage::GetDirtyRects 的语义产生区域形状(96-rect 级联合并 + summedArea*4 >= unionArea*3 回退,MipmapStorage.cpp:300-305),决策留在付 GPU 代价的那一侧。

v2 关键修正:sub-rect 上传不能再靠指针比较判定。 今天 Managers.cpp:4278-4283 用 uploadData == mipData 判"上传源就是整 level shadow",随后 :4288-4293 与 rectShadowPtr(:4321-4326)用 levelRowBytes/levelSliceBytes 跨步进整 level。在 split 下这个前提不成立:client 若发整 level 就毁掉带宽收益并与 §0.4 的零副本主张矛盾;若发紧密区域则 uploadData == mipData 为假,静默退回整 level 上传;若什么都不发就需要 server 侧整 level 镜像——那就是 replica 的 MipmapStorage。 修正:MGPSubRegion 显式携带源步长,sourceIsVerbatimLevelShadow 显式携带原来那个指针比较回答的语义问题("这批字节是未经转换的 level shadow 吗")。Managers.cpp:4274-4326 相应改为从描述符取步长而不是从指针算,UNPACK_ROW_LENGTH 从 srcRowStride/bpp 设。 注意树里已经有这个形状:unpack ring 路径的 UnpackStagingBlock(Managers.cpp:4340-4390)就是 {src, rowBytes, rows, slices, srcRowStride, srcSliceStride, offset},且注释明说 ring 路径把区域紧密重打包、因此完全不发 glPixelStorei。所以 split 的自然形态就是"永远走紧密重打包 + 描述符",与 ring 路径同构。 这项工作从 v1 的"原地不动"移出,计入子系统 5 的天数(§6.4),并加一个 Mali 设备门发布 box-vs-rect 作业数与帧时增量。

4.5.7 MGPDrawInfo 与 MGHostSpan

struct MGPDrawInfo {                  // = pipe_draw_info
    Uint32 mode;
    Uint8  indexSize;                 // 0 = arrays,否则 1/2/4
    Uint8  flags;                     // kHasUserIndices | kPrimitiveRestart | kIndicesAreClient |
                                      //   kHasIndexRange | kHasXfbCount
    Uint16 pad;
    Uint32 instanceCount, startInstance;
    Uint32 restartIndex;
    MGPipeHandle indexResource;
    // 以下三项**由 flags 门控**,只在有消费者时才计算与携带(v2)
    Uint32 minIndex, maxIndex;        // kHasIndexRange;client 计算,~0 = 未知
    Uint64 xfbCpuCapturedVertices;    // kHasXfbCount;GetTransformFeedbackCapturedVertices()
    MGHostSpan userIndices;           // kHasUserIndices;否则不进变长尾
};
struct MGPDrawRange { Uint32 start, count; Int32 indexBias; };   // = pipe_draw_start_count_bias

v2 成本诚实化:今天的 DrawArrays(GLenum, GLint, GLsizei) 是三个寄存器实参(BackendObject.h:117)。替换成一个 ~48 B 的固定头(含 handle)加按需的变长尾。minIndex/maxIndex 今天只在 client-memory 数组路径算(TryComputeMaxIndexFromHostBytes,VulkanRenderer.cpp:3407-3470,用于 :3599),xfbCpuCapturedVertices 今天只在 XFB scatter 路径读(DirectGLES.cpp:900)——所以两者由 flags 门控,不是每 draw 都算。userIndices 的 32 B MGHostSpan 移出固定头进变长尾,让 VBO 路径(MC/Sodium 的全部 draw)不为它付字节。每 draw payload 字节数进 P0 的计数器直方图(cmd-records 是逐帧的,这里要逐 draw 的分布,它才是 SEG_CMD 的定尺依据)。

MGHostSpan 是整份接口里唯一一个"形状随传输而变"的东西:

struct MGHostSpan {                   // 32 B
    const void* ptr;                  // monolith:指向前端 shadow / 应用内存。split:nullptr
    Uint64      size;
    Uint32      seg;                  // split:SEG_STAGE id,或 kFromServerIndexMirror
    Uint32      pad;
    Uint64      offset;
};
inline const void* MGPipeHostBytes(const MGHostSpan&);   // 一次可预测分支

v2 修订的消费者表(与 §5.8 一致,解决 v1 §4.5.7 与 §5.8 互相矛盾的问题):

消费者 今天的站点 归属 monolith 填法 split 填法
client 顶点数组 Managers.cpp:2500-2592、VulkanRenderer.cpp:3737 client 供字节 ptr = attrib.Offset tracker 暂存同样范围进 SEG_STAGE
client 索引数组 DirectGLES.cpp:4425-4442、VulkanRenderer.cpp:3418-3433 client 供字节 ptr = indices 暂存 count*indexSize
indirect / parameter 命令块 DirectGLES.cpp:4655-4695、:4768-4793、VulkanRenderer.cpp:12045 client 解析计数 ptr 指向 shadow tracker 解析出计数并发解析后的 MGPDrawRange[](几十字节)
restart 重写 / multi-draw 展平的索引字节 DirectGLES.cpp:4412-4415、MultiDraw.cpp:498-540、VulkanRenderer.cpp:4159 server 拥有变换(D-B7) ptr 指向前端 shadow seg = kFromServerIndexMirror:server 从自己的索引宿主镜像取,零线上流量;镜像超预算时退化为 client 逐 draw 暂存并计数

monolith 代价:一次可预测分支 + 变长尾里的 32 B(仅 kHasUserIndices 时)。它顺带消灭"backend 在 draw 中途回头调前端 reconcile"的大部分:20 处 SyncPersistentMappedRange + 6 处 SyncGpuWrites 里,凡消费者搬到 client 的那些改由 tracker 在填 span 之前做同一次 reconcile(逐站点对照见 §5.8.1,不是一条笼统规则)。

4.6 与 gallium 的对应与偏离(十条,逐条记名)

# gallium MGPipe 理由(证据)
D1 create_*_state 返回 driver 指针 调用方提供 handle 零创建 round trip;handle 是稠密 slot;退役全部 D 类指针 memo
D2 get_param(cap)、is_format_supported(...) 逐项查询 一个 MGPCaps POD + 一张稠密 format 表 DynamicBackendParameters 与 FormatCapabilityCache 本来就是平坦结构
D3 CSO 切分是 D3D10 时代的 CSO 边界跟 Vulkan 动态状态走 RenderState.h:519-528 记录共用一个版本号让 glViewport 冲掉 pipeline memo 和 draw 快路径;m_pipelineStateVersion(:529)恰好是 CSO 相关子集;Magma 的 DynamicStateShadow 与 ApplyDynamicDrawStateTail 已经这么切
D3b(v2 重写) 三个独立 CSO:blend / depth_stencil / rasterizer 一个 RenderStateCso,传输是整块 chunk,身份是 pipeline 子集,动态子集走 set_dynamic_state 整块的理由:is_trivially_copyable_v 断言(DirectGLES.cpp:2035)、三段 memcmp(:2038-2047)、字段顺序承重(RenderState.h:359-368)、两个 backend 都按 span/bulk 消费。子集身份的理由:整块内容寻址会让 glViewport 铸造新 CSO 并冲掉 pipeline memo——即 D3 要防的那次回归。完整性由 G7 的 setter 一致性测试保证
D4 transfer_map/transfer_unmap(scoped) resource_subdata 推送 + map_persistent(永久地址空间捐赠) AcquirePersistentMap(BufferObject.h:102-118)把指针交给应用;≥16MiB 自动走到(:226-228)。实测 p99 163→21ms
D5 driver 看得见压缩格式与 pixel-unpack 状态 两者都不存在 前端在 glTexImage 时解析压缩 internalformat(GL_Texture.cpp:298-306);ScopedDefaultUnpackState(Managers.cpp:2888-2910)强制 unpack 默认值。只有 PACK 方向过线
D6 默认 uniform block = constant_buffer 0 独立入口 set_global_constants SpirvArtifacts::globalUboScratch(ProgramObject.h:1418)是 link phase B 产出的 CPU 数组,布局由优化后的 SPIR-V 决定(:1400-1408)。它没有 GL name、没有 BufferObject、没有 PipeResource
D7 pipe_shader_state = tokens → 完成的 handle handle + server 侧惰性特化,variant 键取自已推送状态 D-B2 的 8 个输入。这其实就是 gallium(Mesa 的 st_variant 也按已绑定状态键控)
D8 pipe_context::flush + fence 是唯一反向通道 MGPipeCallbacks:10 个具名回复/事件(§7) gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求/终止、default-FB 几何这些词汇
D9 set_viewport_states(start_slot, num) float 数组 + 独立的 writtenMask viewport 是 float(RenderState.h:229-237:KHR-GL43.viewport_array.viewport_api 用 == 无容差);scissor 必须单独带 ScissorBoxWrittenMask(:363),因为 glScissor(0,0,0,0) 是合法 GL、意思是"拒绝每个片元"(:352-362)
D10(v2 新增) 纹理参数(swizzle / base-max level / dsMode)住在 pipe_sampler_view 里 set_texture_params(res, …) 独立,MGPSamplerView 只带视图限制 一张只作 FBO attachment / image 单元 / CopyImage 端点的纹理没有 sampler view,但 Espryt 对 attachment 也调 SyncTextureParamsToBackend(DirectGLES.cpp:1580-1601),且 RequireImageBindableStorage 要在前端 params 版本不动的情况下强制重同步(Managers.cpp:2815-2821)

没有 pipe_transfer、没有 set_pixel_unpack_state、没有压缩格式概念、renderbuffer 不折进纹理、set_sampler_views 没有 stage 维度。

4.7 覆盖论证

4.7.1 对 477 读点分类的逐类映射

delta 类 n 满足它的 MGPipe 调用 残余
handle 化(wire 句柄) 167 每个命名对象的调用签名里的 MGPipeHandle —
RenderStateBlob 99 create/bind_render_state + set_dynamic_state —
ObjectBind:Texture / Sampler 33 set_sampler_views + bind_sampler_states —
ObjectBind:Buffer 29 set_vertex_buffers / set_index_buffer / set_indirect_buffers —
ObjectBind:BufferRange 24 set_shader_buffers / set_stream_output_targets Uniform 类另带 host payload(D-B8)
FboAttach + DrawBuffers + ReadBuffer 19 set_framebuffer_state —
Buffer ops delta 17 resource_* 全族 —
XfbOp 15 set_stream_output_targets + *_stream_output —
ObjectBind:Image 14 set_shader_images —
ObjectBind:VAO 12 bind_vertex_elements_state + set_vertex_buffers + set_index_buffer —
ObjectBind:Program 10 set_draw_program / set_dispatch_program —
TexParam / SamplerParam 9 set_texture_params + create_sampler_state + create_sampler_view v2 修正归属(D10)
Texture state(dirty level/rect) 7 resource_subdata(带步长描述符) 归属反转(§7.3)
PixelStoreBlob 6 set_pixel_pack_state unpack 删除
client-resolved(error queue) 6 on_gl_error 回调(§7) —
ProgramPublish 3 create_shader_state 依赖 P0.5
client-resolved(validation) 3 client 自答 —
CurrentAttrib 2 set_vertex_attrib_defaults —
client-resolved(compile env) 2 on_caps_invalidated —
Patch 参数 — set_patch_state 同时是 variant 输入
条件渲染 — client 解析,永不过线 Core.h:387-391
XFB CPU 计数 — 纯 client;MGPDrawInfo::xfbCpuCapturedVertices(flag 门控) —
backend 重铸纪元 — 无 client 对应物:MGGen,server 私有 —

那 1997 个前端 getter 站点不是第二个面:89 个纯版本读根本不过线,72 个数据字节读全部落在 §5.7/§5.8 与 MGHostSpan,38 个 GetLifetimeId() 变成 handle。

4.7.2 覆盖论证不是这张表,是这三道门(v2:从两道增至三道)

上表是声明。证明是机械的:

门 A —— include 图门(v2 新增,取代 v1 单靠 nm 的那半)。 v1 说 MG_Backend 只允许 include "一张共享值头白名单(RenderState.h 的 RenderStateParameters、SamplerObject.h 的 SamplerParameters、…)"。实测这张白名单不是叶子集:RenderState.h:12 include FramebufferState/FramebufferObject.h,后者 :12-13 再 include TextureState/TextureObject.h 与 RenderbufferState/RenderbufferObject.h;依赖是结构性的——RenderStateParameters 用 FramebufferObject::MAX_DRAW_BUFFERS 给两个数组定长(RenderState.h:263, 273)。所以"把 RenderStateParameters 交给纯净的 MG_Backend"会把整张 framebuffer/texture/renderbuffer 类图一起拖进来。而 nm --undefined-only 看不见这个:只 include 而不调用其成员函数的类不产生未定义符号,门可以在 include 图完全耦合的情况下为绿。 修正:P0.5 交付 MG_Pipe/MGPipeValueTypes.h——把 MAX_DRAW_BUFFERS、PerBufferBlendState、StencilFaceState、PixelStoreParameters、RenderStateParameters、SamplerParameters、BorderColorForm、VertexAttribute 与相关枚举搬进去,它不 include MG_State/GLState 的任何东西;RenderState.h/SamplerObject.h/VertexArrayObject.h 反过来 include 它。门变成:

在 disaggregated 配置下编译 MG_Backend 时,把 MG_State/GLState 从 include 搜索路径里移除(或对 -H 输出断言)。这是唯一一条能因它存在的理由变红的检查。

门 B —— 符号门。 nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang' 为空。保留,作为门 A 的补充(它能抓到通过前置声明+跨 TU 调用绕过 include 图的情况)。

门 C —— 未声明门。 在 MOBILEGL_PIPE_PUSH=all 且非 verify 构建里,MG_State::pGLContext 未声明。任何接口没满足的读是一次指名文件与行号的编译错误。strangler 结束时 grep -c 'pGLContext' MG_Backend/ == 0(grep pGLContext 不是 pGLContext->,因为还有 58 行非箭头用法)。这条门只跑非 verify 构建(D-B5:verify 构建保留 SnapshotFromGLContext())。

这三道门比生成一张 477 行的清单严格得多:它们禁止那次读,而不是给它编目,而且不会过期。 那份 inventory 保留为 tracker 侧覆盖检查表(G6,CI git diff --exit-code,0 UNMAPPED)。

4.7.3 21 条 D 类身份 memo 的重键表

# 今天的键 守什么 MGPipe 净效果
D1 StateBackendObjectRegistry 用裸 StateObject* + 同址 weak_ptr(Managers.h:282-325)×6 分配器地址复用;也是唯一的删除信号 按 slot 索引的数组 + gen 比较;显式 resource_destroy GC(1024/64 阈值)删除 ×6
D2 TwinLookupMemo ×3 + OwnerEquals(DirectGLES.cpp:62-131) 复用堆地址命中 memo 槽 删除——数组下标就是查表 ~75 行 + 140KiB
D3 UnitTextureSyncEntry + PairingsIntact(:1441-1481) 不移动任何计数器的 slot 交换(DSA by-name) server 侧删除;去抖搬到 client(§2.5:set_sampler_views 的 client 侧 hash 抑制器,否则冗余 glBindSampler 会 per-batch 重发) server −115 行 / client +~60 行
D4 IsBufferDrawClean 身份优先比较(Managers.cpp:1436) respecify 交给前端一个新资源 server 拥有资源表;gen 比较;GetChangeSerial()(Uint64,不回绕)继续过线 简化
D5 ResolvedDrawBuffers::iboFrontend(Managers.h:711-716) 索引 slot 重绑而无 epoch/config 移动 set_index_buffer 是独立调用 结构性
D6 m_syncedIndexBufferObject 陪一个回绕 Uint16(:775-780) 版本回绕后换了个 buffer {slot, gen} 比较,不回绕 结构性
D7 StampSyncedFBO 四元组(DirectGLES.cpp:1856-1901);packed_pixels postmortem :2815-2827 版本回绕 + backend 侧纹理重铸 MGPFramebufferState::contentHash + server 私有 attachmentRemintEpoch(MGGen) 一次 64 位比较
D8 g_fboTextureSyncList(:1580-1601) 同 D3,针对 attachment server 侧删除;由 contentHash 在 client 侧抑制 server −20 行
D9 ResolvedTextureBindingMemo:9 个键 + 驱动绑定影子的 memcmp(:3218-3291) 任何未枚举的写者扰动某个 unit (shaderCso.slot, viewSetSerial) 两字比较;viewSetSerial 由 server 在 set_sampler_views 内部 ++。前提是 client 侧的 hash 抑制器已经挡住冗余推送,否则这个 serial 每个 batch 都动 更便宜(有前提)
D10 UnitSamplerLookupMemo 的 WeakPtr owner 测试(:3105-3125) 死 sampler 复活 数组下标 删除
D11 VertexInputStateFactory::ComputeHash 混入 GetLifetimeId()(:38-49) 复用 buffer 地址重现整个 content hash CSO handle 就是身份;gen 混进 server 侧每个 content hash 删除一整类
D12 SetBackendStateMemo(&entry, evictionEpoch):前端 VAO 里存后端堆裸指针(VertexInputStateFactory.cpp:78) table 淘汰 直接删除,不翻译 —
D13 VaoDrawMemo 槽(VulkanRenderer.h:1230-1245) ABA CSO handle 2 字
D14 SetupDrawSnapshot 的三组 (ptr, lifetimeId, version) + 有损的 sampledContentSum/sampledParamsSum 一切 三个 handle + 两个 server 纪元 + dirty mask ~14 个探测字段 → 1 次比较;顺带消灭一类哈希碰撞
D15 m_rpFast*(VkRenderPassManager.h:305-320) ABA contentHash + MGGen 1 次比较
D16 VkTextureManager::TextureIdentity + GetTextureObject(name) 存活探测(VkTextureManager.cpp:806-819) 名字复用 / 删了但仍被 FBO 引用 / 默认纹理 {slot, gen} + 显式 destroy 三种失效模式一起消失
D17 VkClearManager::TextureIdentity(VkClearManager.h:76-83) ABA {slot, gen} —
D18 纹理/renderbuffer 资源用节点式 std::unordered_map(postmortem VkRenderPassManager.h:375-397) 扩表搬迁使缓存的 Resource* 失效 UNCHANGED。 接口零约束;这是 server 内部分配纪律。postmortem 注释必须逐字带进 review checklist 保留
D19 ProgramFactory::m_cacheStructureEpoch 守 server 内部裸指针 UNCHANGED(MGGen 族) 保留
D20 ConvertedVertexStreamKey + 纯为防地址复用持有的 SharedPtr sourcePin ABA server 拥有资源;changeSerial 过线 pin 删除
D21 m_xfbCounterSlotByObject[GetBoundTransformFeedbackName()](VulkanRenderer.cpp:11136-11146) 什么都没守——活的潜伏 bug XFB 对象 handle 顺带修一个 bug,先独立落 dev

总计:11 条直接删除,2 条(D3/D8)server 删除但去抖搬到 client,7 条重键成更便宜的比较,1 条(D18)原样不动。


5. 前端 state tracker

5.1 推送发生在哪里——本设计里最容易做错的一个决定

不在 GL setter 里。 glEnable(GL_BLEND) 绝不调 bind_render_state。Blaze3D 每个 batch 都用它包住,代码自己标注它是最热的路径(DirectGLES.cpp:2029-2032)。天真的 per-setter 推送把每一次冗余开关变成一次接口调用加一次 server 侧 CSO 查表——严格慢于今天。

在 verb 之前的 validate 时刻。

// MG_Impl/Pipe/Tracker.h
class MGPipeTracker {
public:
    // 每一类 verb 一个入口;由 PipeCalls.def 的 kCtxVerb / kCtxObject 条目生成(§6.2.1)
    void ValidateForDraw(const MGPValidateHint&);   // 20 个 GL draw 入口
    void ValidateForDispatch();                     // glDispatchCompute*
    void ValidateForClear(GLbitfield);              // framebuffer + 渲染状态(ClearColor 在其中)
    void ValidateForBlitOrCopy();                   // framebuffer + pack state
    void ValidateForTextureOp(MGPipeHandle res);    // GenerateMipmap / CopyTex* / BindImageTexture
    void ValidateForReadback();                     // ReadPixels / GetTexImage
    void ValidateForXfbSpan();                      // Begin/End/Pause/Resume TransformFeedback
    void ValidateForQuery();                        // query begin/end
private:
    Uint64 m_dirty;
    Uint64 m_lastPushed[kGroupCount];
    Uint64 m_lastSetHash[kVarTailGroupCount];       // ★ kVarTail set_* 的发射抑制器(§2.5)
};

这八个入口不是随手列的:MG_Impl 用到 70 个不同表项 / ~93 个调用点,其中只有 ~22 个是 draw/dispatch,其余 ~48 个是纹理操作、回读、blit、clear、XFB 跨度、query——而它们中很多自己就读 pGLContext(§2.1(a) 列了具体行号)。v1 只给 4 个 validate 入口、只在两处填快照,会让第一个 glGenerateMipmap/glReadPixels 撞上 poison Fatal,MOBILEGL_PIPE_VERIFY 的全绿验收因此不可达。

5.1.1 哪些操作在 GL 调用时刻推送(v2 修正推论 1)

规则的正确措辞:

只有今天就在 GL 调用时刻分发的资源 op 在 GL 调用时刻推送——即 BufferBackendOps 的七个 hook(BufferObject.h:70-71 自己写着"在 GL 调用时刻分发,就在 shadow 拷贝刚更新之后")。纹理 subdata 不在此列。

理由:glTexSubImage* 根本不调 backend 表(GL_Texture.cpp 只有 3 处 MarkStorageDirtyRegion),全部纹理上传由 Espryt 在 sync 时刻按累积区域做,那里才跑 96-rect 级联合并与 union-box 回退,并在 unpack ring 可用时刻意塌成一个 box(Managers.cpp:4386-4390,实测 +6 ms/frame)。逐 glTexSubImage 发一条 resource_subdata 精确复现那个 ~100 作业的形状。

因此纹理路径的形态是:client 在自己的 MipmapStorage rect 模型里累积(§7.3 的发射游标),在下一个 validate / flush 点把合并后的形状作为一条 resource_subdata(带 union box + region 列表)发出。MOBILEGL_PIPE_STATS 必须把逐帧 resource_subdata 发射次数单列一类,并在 MC 动画图集 fixture 上设上限。

稳态成本:见 §10.2(v2 已按动态口径重写)。

5.2 dirty bits:值类零新增记账,对象类新增 5 个聚合世代(推论 4)

dirty 位 类别 快门来源
NEW_RENDER_STATE / NEW_PIPELINE_STATE 值 m_version / m_pipelineStateVersion(RenderState.h:522, 529;bump 点 RenderState.cpp:311-312 等)
NEW_PIXEL_PACK 值 PixelStoreParameters(RenderState.h:190-199)
NEW_PATCH_STATE 值 patch 三字段,用 BitwiseEqual 比较(NaN 合法,DirectGLES.cpp:2807-2814)
NEW_VERTEX_ATTRIB_DEFAULTS 值 GetCurrentVertexAttribute
NEW_VERTEX_ELEMENTS 值 VertexArrayObject::GetConfigVersion()(Uint32,:155)
NEW_VERTEX_BUFFERS 对象 VertexArrayState::m_anyVaoAttributeGeneration(新增)→ 命中后走 32 属性前缀 + 逐属性 VertexAttributeVersion(:66-70)
NEW_INDEX_BUFFER 对象 索引 slot GetVersion()(回绕 Uint16)+ 绑定对象 {slot,gen}
NEW_FRAMEBUFFER 对象 FramebufferState::m_anyAttachmentGeneration(新增)+ GetObjectVersion() + slot 版本 → 命中后重算 contentHash
NEW_SAMPLER_VIEWS 对象 TextureState::m_anyTextureContentGeneration + m_anyTextureParamsGeneration(新增)+ GetTextureBindGeneration() + GetSamplingResolutionGeneration() → 命中后走 GetMaxTouchedUnit() 前缀、重算集合 hash、hash 未变则不发
NEW_SAMPLERS 对象 SamplerObject::GetVersion()(回绕 Uint16,SamplerObject.h:155)+ 上面的聚合
NEW_SHADER_IMAGES 对象 ImageTextureBinding::Version(TextureState.h:24, 34)+ m_anyTextureContentGeneration
NEW_SHADER 值 GetLinkVersion() + GetImageUnitVersion()(ProgramObject.h:844, 906)
NEW_SHADER_BINDINGS 值 GetBackendStateVersion()、GetBlockBindingVersion()、GetUniformWriteSetVersion()
NEW_GLOBAL_CONSTANTS 值 GetUBOContentVersion()(~0u 跳过回绕,:791-794)
NEW_CONST_BUFFERS / NEW_SHADER_BUFFERS / NEW_SO_TARGETS 对象 BufferState::m_anyBufferChangeGeneration(新增)+ slot 版本 → 命中后走 GetTouchedBindPointCount() 前缀

五个新增聚合世代(TextureState 两个、BufferState、VertexArrayState、FramebufferState 各一)全部落在既有 bump 点上,合计约 20 行。它们把对象类组的快门从"每 validate 走查 192 个单元 / 84×4 个绑定点 / 32 个属性 / 40 个 attachment"降成一次 Uint64 比较;只有快门为真时才走 touched 前缀并重算集合 hash。

完整性由 gen_pipe_dirty_surface.py 保证(推论 4):它枚举 MG_Impl/GLImpl/** 里每一个会改变某组的 mutator,映射到必须 bump 的聚合世代,CI 重生成 + git diff --exit-code,未映射的 mutator 直接失败。这是 PLAN.md 的 gen_impl_mutation_surface.py 的改造版(replay 义务消失、标记义务出现),也是 B-R6 的第四层。

三个回绕的 Uint16 在 tracker 边界加宽。 m_lastPushed[] 是 tracker 自己的字段,加宽到 Uint32/Uint64 不需要改 MG_State 一行;同时 handle 与它同行过线。回绕在 tracker 本地是无害的(一次回绕造成一次多余的重推,永不漏推),何况集合 hash 抑制器会把多余重推吞掉。

5.3 每命令 validate 的不变式(v2:从"固定顺序契约"降级)

规范条款(D-B3 v2):

一条 verb 的全部 set_*/bind_* 必须在该 verb 之前完成;server 在 verb 处、从它此刻持有的全部已推送状态特化 shader 与 pipeline。除"资源 create 先于对它的 bind"外,set_* 之间没有顺序要求。

推荐实现顺序(便于 tracker 的代码组织与 dirty 位遍历,不是正确性契约):

1  set_framebuffer_state
2  set_draw_program(create_shader_state 在 link 时刻已发)
3  set_texture_params / set_sampler_views / bind_sampler_states / set_shader_images /
   set_shader_buffers / set_global_constants
4  bind_render_state(未命中时先 create_render_state)/ set_dynamic_state
5  bind_vertex_elements_state / set_vertex_buffers / set_index_buffer / set_vertex_attrib_defaults
6  set_patch_state / set_stream_output_targets
7  draw_vbo

退役 workaround 的机制是惰性特化,不是调用顺序:DirectGLES.cpp:2712-2732 的 fragColor 重推导与 g_broadcastMemo* 之所以能删,是因为 server 在 verb 处才特化,那时 set_framebuffer_state 一定已到;同理 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573,注释明说"不可表达为单调版本")由 set_shader_images 在 verb 之前告知。v1 把这归因于"framebuffer 严格第一",但它自己把 images 排在 program 之后——那个论证站不住,结论仍然成立。

create_shader_state 从编译池的终止 continuation 发出(JobNode.h:109-123),不是从 draw 发出,这样 SPIR-V 在用到它的第一个 draw 之前就到达 server。这是 monolith 拿不到的异步收益。

5.4 合并:保留代码库已经发现的三条,加上第四条

  1. 整块结构优于逐字段。 Magma 的 ComputePipelineStateHash(VulkanRenderer.cpp:4818-4826)已经把 ~17 次 accessor 调用换成一次 bulk fetch;Espryt 的三段 memcmp 同理。
  2. 高水位标记。 BufferState::TouchBindPoint / GetTouchedBindPointCount(BufferState.h:51-62,每 target 84 个绑定点)与 TextureState::NoteUnitTouched / GetMaxTouchedUnit(Core.h:124-126,192 个单元)必须留在 tracker 的走查里,它们直接就是 set_shader_buffers / set_sampler_views 的 count 实参。
  3. 只发 program 解析过的集合,用 LinkArtifacts::uniformSamplerOrImageUnitIndex(ProgramObject.h:1298)。两个 backend 今天已经在算(ResolveAndBindUnitTextures,DirectGLES.cpp:2973;UniformManager::CollectSampledTextures)。
  4. (v2 新增)集合 hash 抑制器。 每一条 kVarTail 的 set_* 在 client 侧算一次已解析集合的 xxHash,与 m_lastSetHash[] 比较,未变就不发。这是 §2.5 里那 ~175 行去抖搬到 client 后的载体,也是 D9 的前提——没有它,GetTextureBindGeneration() 在冗余重绑时的 bump(DirectGLES.cpp:1414-1420,26.2 每次纹理单元切换都重绑同一个 sampler)会让每个 batch 重发一条几百字节的变长记录并冲掉 server 的两个 memo。

索引绑定的范围必须在 validate 时刻实时解析,不是在 bind 时刻快照。 BindingSlotRange1D::GetRange() 对整 buffer 绑定返回 Range1D(0, object->GetSize()),因为 glBindBufferBase 之后再 glBufferData 是普通应用代码。

5.5 sampler view 在 client 侧解析

GL 是每个 unit 每个 target 各一个绑定(TextureUnit.h:20, 24-25;TextureState::m_textureUnits 是 Array<TextureUnit, 192> 按值存放,TextureState.h:128,每 stage 广告上限 32,:46),shader 看见哪一个取决于 sampler uniform 的声明类型、mipmap 完备性(IsMipmapCompleteForFilter,TextureObject.h:309;SamplesAsIncompleteTexture,:315)和 IsUndefinedDefaultTexture(:329-332)。gallium 的"每槽一个 view"就是解析后的形态。

解析留在 client,并且 client 必须为它保留一个自己的 memo(§2.5 的 ~40 行搬迁项),否则每 draw 重跑完备性规则。合并单元空间,无 stage 维度(§4.4.3)。

两处 backend 特定的后处理留在 server,作用在已解析的集合上:Espryt 的 raw-depth-fetch sampler 替换(DirectGLES.cpp:3540-3546)与 Magma 的 feedback-loop 检测(对着 draw FBO,UniformManager.cpp:554)。两者都可从已推送的 set_framebuffer_state + view 集合判定。

5.6 对象生命周期、共享组与 composite pipeline program

5.6.1 生命周期

resource_create 在前端对象构造时发,存储由 resource_respecify 惰性定义。resource_destroy 在前端对象析构时发。三条顺序约束:

  • view 先于其存储属主销毁:GetViewStorageOwner()(TextureObject.h:96-100)→ MGPResourceDesc::viewOf + server 侧 keep-alive。
  • FBO attachment 钉住纹理(FramebufferObject.h:95)→ set_framebuffer_state 的 surface handle 隐含 server keep-alive。
  • buffer texture 钉住 buffer,范围实时解析(TextureObjectBuffer.h:28, 35-46)→ MGPResourceDesc::{bufferForTexBuffer, bufOffset, bufSize}。

5.6.2 共享组

v1:一个 screen、一个 context、一个扁平 handle 空间、一条 flow。eglMakeCurrent 是 flow 所有权转移,在既有 EGLOperationMutex(EGLImpl.cpp:241)下发射——顺手修今天不取该锁的两个入口:ReleaseThread(:341-350)与 SwapInterval(:435-450)。

5.6.3 composite pipeline program:判过死刑的那个反对意见,答案是"什么都不用做"

GLContext::GetProgramForDraw()(Core.cpp:592)今天就已经完全在前端完成合成:join 每个 stage 的 JoinLinkAndSpirv()、按 ComputeDrawProgramSignature()(:630)查 cache、miss 时构造故意不命名的 MakeShared<ProgramObject>(0u)(:644)、挂上每个 stage 被钉住的 linked snapshot、重装捕获 stage 的 XFB varyings、Link(true)、缓存、RefreshCompositeUniforms。

tracker 调它,拿到 SharedPtr<ProgramObject>,推一个 handle。合成体没有 GL name,但有 lifetimeId,slot 从 ShaderCso 的保留高位段分配。生命周期:pipeline cache 淘汰该条目时释放 slot、gen++、发 delete_shader_state——CompositeResolver.cpp 里三行。

合成体从不过线、从不被重新实现,PLAN.md 提议的 SetReplicaResolvedDrawProgram 钩子完全不需要。 副带收益:阻塞的 JoinLinkAndSpirv() 彻底离开 server 的 draw path。

5.7 program artifacts 与全局 UBO scratch

create_shader_state 的 payload 是 SPIR-V + 全结构体反射归档(§4.5.5),不是源码。依赖 P0.5 的头文件抽取。

SPIRV-Cross 留在 server(TranspileSpirvToEssl,Managers.cpp:6575):它消费 SPIR-V 加设备事实。glslang 留在 client。 这是一次文件级切割。

全局 UBO scratch 走独立入口(D6):set_global_constants(shaderCso, MGPBlobRef bytes, Uint32 version),键 (shaderCso.slot, uboContentVersion),复现 DirectGLES.cpp:3369-3392 的"每 program 每帧至多一次"。

具名 UBO 字节走 set_shader_buffers 的 host payload(D-B8):UniformManager::ResolveUniformBufferPayload 在 UniformManager.cpp:2022 调 SyncPersistentMappedRange()、:2052 读 MappedData() + rangeStart 打进 Magma 自己的 UBO ring——消费者在 server,搬不走。由 kCapNeedsHostUboBytes 门控(Espryt 直接绑给驱动,不需要)。逐帧字节量进 stage-ubo-named 计数器;在 P0 给出数字之前不冻结这个 payload 的形状。

backend 侧 program link/compile 失败不需要任何同步返回,也不需要新事件种类。 实测:SyncToBackend 在 Managers.cpp:8091 link、:8094 读 GL_LINK_STATUS、:8095 折进 m_backendProgramUsable、:8097-8101 取驱动日志、:8106 发 MGLOG_E;Use() 随后绑 program 0(:8357)并 MGLOG_E_ONCE(:8364-8372)。没有 GL error、没有 ProgramObject 变更、GL_LINK_STATUS 永不撤回(:7098、:7247-7249、:6478、:7827)。同步查询由 client 从 ProgramObject 回答(GL_Program.cpp:851 → ProgramObject.h:913)。所以 on_log 逐字复现它——但由此推出一条对 PLAN.md §7.4 的强制修正,见 §7.4。

5.8 emulation 所需前端数据的显式传递(v2 按 D-B7 重写)

归属规则:驱动表达不了的变换在 state tracker 里 lowering,硬件/驱动强加的变换在 driver 里 lowering。v1 用 cap 位门控 emulation 归属的做法对 restart 与 multi-draw 不可表达(D-B7),此处收回。

emulation 归属 门 过线的是什么
client 顶点数组(Managers.cpp:2500-2592 把 attrib.Offset 当应用裸指针,每 draw 每属性上传 (first+count-1)*stride+elementSize;VulkanRenderer.cpp:3737 是唯一无界的应用指针读) client(它拥有地址空间) — 字节,永不是指针(MGHostSpan)
索引扫描(TryComputeMaxIndexFromHostBytes,VulkanRenderer.cpp:3407-3470,用于 :3599 给上一条定界) client(只有它同时持有两个数组) — MGPDrawInfo::minIndex/maxIndex(kHasIndexRange 门控),~0 = 未知
client 索引数组 client — MGPDrawInfo::userIndices(kHasUserIndices 门控)
primitive-restart 重写(DirectGLES.cpp:4368-4470 整 EBO 重写,kMaxRestartRewriteBytes = 1<<26 = 64 MiB,:4218;VulkanRenderer.cpp:4159-4161) server(v2 改:v1 曾说 client) kCapNeedsHostIndexBytes → 索引宿主镜像 零线上流量:server 从镜像读。monolith 行为零变化,诊断仍落在原线程(开放问题 12 关闭)
multi-draw 分档 + 展平(MultiDraw.cpp:282-320 的 ResolveTierForBatch 逐 batch 在五档里选,输入含 programReadsDrawID——转译出的 ESSL 的性质,只存在于 server;容量判定 kMaxFlattenedIndices :72 / kMaxComputeFlattenedIndices :82;自动阶梯 Ext→BaseVertex→MultiIndirect→Indirect→DrawElements :241-243,CPU 展平是回退) server,全部五档(v2 改) kCapNeedsHostIndexBytes draw_vbo(info, indirect, MGPDrawRange[], numDraws);索引字节走镜像
*IndirectCount CPU 回退(DirectGLES.cpp:4655-4695 从 parameterBuffer->MappedData() 读实际 draw 数) client — client 从自己的 shadow 解析计数,发解析后的 MGPDrawRange[](几十字节)。注意它今天只调 SyncPersistentMappedRange(),不调 SyncGpuWrites()(§5.8.1)
viewport-array N 遍回放(DirectGLES.cpp:3742-3846,今天包住 14 个 draw 入口) server kCapViewportArray 无新增:16 组 viewport/scissor/depth-range 已在渲染状态里
fp64 顶点窄化(Managers.cpp:2518-2557) server(后端格式决策) kCapFloat64VertexAttrib(BackendObject.h:487-500 明说它与 SupportsShaderFloat64 独立) 原始字节;IsLong 与 Type 分开过线
image-bindable 存储加宽/拆分(Managers.cpp:2789-2822、:4620-4630) server — 正向 imageBindableHint;反向 on_texture_pull_request + 终止符(§7.5)
生成 mipmap 的前端存储 拆开:client 分配 level 存储,server 生成 — MGPMipPlan;on_mip_levels_generated 只带形状不带字节(见 §9.1 的说明);CPU 回退路径的纹素由 on_texture_writeback 回来
CopyImage shadow 镜像(DirectGLES.cpp:7065-7140) client — 只回"拷贝成功"。删掉一整条 server→client 字节通道
XFB CPU 图元计数(GL_Drawing.cpp:172,调用点 :1133, 1141, 1195, 1668) 纯 client kCapCpuXfbPrimitiveAccounting MGPDrawInfo::xfbCpuCapturedVertices(flag 门控)+ end_stream_output 的 MGPXfbAccounting
XFB scatter 的 read-modify-write(DirectGLES.cpp:893-960) client(v2 新增行) — 见 §7.2 的 on_buffer_writeback 修正
压缩纹理 / pixel unpack 规整 纯 client — 无

5.8.1 陈旧索引纪律——逐站点表,不是一条笼统规则(v2 修正)

v1 写"上表里每一次 client 侧扫描/重写,在 monolith 里都紧跟在 SyncPersistentMappedRange() + SyncGpuWrites() 之后"。对 *IndirectCount 不成立:DirectGLES.cpp:4666-4667 只调两次 SyncPersistentMappedRange(),然后在 :4690-4694 直接读 MappedData();没有 SyncGpuWrites(),因此今天没有停等。而 SyncGpuWrites 才是触发 ReadbackFromGpu(BufferObject.cpp:265-274)的那一条。照 v1 的笼统规则实施,glMultiDrawElementsIndirectCount 会平白获得一次 publish-and-wait round trip——而 trace 语料里恰好有 minecraft-1.21.1-neoforge-create-indirect-in-world(Create/Flywheel,indirect 与 parameter buffer 每帧被写),于是这会变成一个逐帧逐 batch 的同步 round trip,而 §9.2 第 10 行还把它写成"常见情况代价为零"。

逐站点 reconcile 表(必须逐字复现 monolith 的集合,不多不少):

client 侧动作 monolith 对应站点 必须做的 reconcile
client 顶点数组范围计算 + 暂存 Managers.cpp:2500-2592(无 buffer,源是应用指针) 无(应用内存,无 GPU 写者)
最大索引扫描(EBO 源) VulkanRenderer.cpp:3406-3470 前的 :3431 SyncPersistentMappedRange() + SyncGpuWrites()
最大索引扫描(client 索引源) 同上,client 指针分支 无
*IndirectCount 计数解析 DirectGLES.cpp:4666-4667、:4768-4793 只 SyncPersistentMappedRange()。不加 SyncGpuWrites()
(server 侧)restart 重写 DirectGLES.cpp:4412-4413 server 从镜像读;镜像由 subdata 流维护,GPU 写者的可见性由 on_gpu_written 收窄集驱动——server 侧本地判定,无 round trip
(server 侧)multi-draw 展平 MultiDraw.cpp:498-499 同上

client 侧需要 reconcile 的那两条的形态:publish → 等 appliedSeq → 排空事件 → 再碰 shadow。跳过它,maxIndex 来自陈旧字节,顶点数组被少拷 → 几何缺失,或越界读应用数组。

门:ClientArrayAfterComputeWriteScenario(新增),必须能因它存在的理由变红。 门:create-indirect fixture 上的 roundtrips-per-frame 计数器必须读零(P8 验收),这是上面那条"不加 SyncGpuWrites()"的绊线。

另注:monolith 在 *IndirectCount 上不调 SyncGpuWrites() 本身可能是一个潜在缺口(compute 写的 indirect buffer)。那是一个独立的 dev 问题,拆分不得借机"顺手修"——那会改变基线并让逐名对比失去意义。列入开放问题。


6. 后端状态机改造

6.1 什么原样不动(先说这个,因为它是"最短可信改造"的依据)

每一个 ring、pool、arena、quirk、lowering pass 原地不动:

Espryt:三条 persistent-mapped ring、PersistentRing 的分配/背压算法、buffer pool、全部 7 条 fallback-repack 路径(Managers.cpp:3209-3527)、m_backendColorSlots draw-buffer 置换表、三个 scratch FBO 及其驱动侧 attachment 影子、PackState、全部驱动绑定影子、Adreno 的"禁用属性无指针 SIGSEGV" workaround(Managers.cpp:2371-2380, 2427-2433)、Mali 的 XFB 捕获丢失 workaround(DirectGLES.cpp:400-410)、ScopedDefaultUnpackState、SPIRV-Cross 会话与 6 次 post-emission ESSL 重写、驱动 POST 自检族、restart 重写与 multi-draw 五档(D-B7)。

Magma:VulkanRenderer 全部 memo 与 scratch、PipelineFactory、ProgramFactory、UniformManager 的 ring 与描述符集、五个 Vk*Manager、FrameContext、SwapchainObject、DynamicStateShadow、VertexInputStateFactory 的 cache 本体、以及 D18 的节点式容器纪律。

v2 从"原样不动"里移出的一项:Managers.cpp:4274-4326 的 sub-rect 上传判定与跨步计算——它今天靠 uploadData == mipData 指针比较与整 level 步长算术,split 下不成立(§4.5.6),必须改成从 MGPSubRegion 描述符取步长。这不是 v1 说的"只把输入从拉取的 shadow 指针换成 MGPBlobRef",是真代码改动,计入子系统 5。

唯一两处必须真改的 MG_State 类型内部用法:

  1. Magma 的占位纹理(UniformManager.cpp:161-181, 1416-1500, 1624-1634):构造真的 TextureObject2D / TextureObject2DMultisample / TextureObject2DMultisampleArray,走 SetInternalFormat(RGBA8) / AllocateStorage({1,1,1},4) / UpdateMipmapSubData / MarkStorageDirty / SetSamples(2)(VUID-RuntimeSpirv-samples-08726)/ TruncateMipmapLevels(1),唯一理由是让"未绑定单元"复用 SyncTextureAndGetDescriptor(ITextureObject&) 这个签名。改成 backend 自己分配 VkImage + view + descriptor:~120 行前端对象木偶戏变成 ~60 行直白的 VMA/Vulkan,34 个 MOBILEGL_ASSERT(pGLContext) 里的 9 个随之消失。
  2. Magma 的两个内部 shader(InitializeBlitResources VulkanRenderer.cpp:4210-4283、InitializeDepthMipmapResources :4287-4356):烘焙成 SPIR-V。 方式:把生成的 SPIR-V、uniform location、UBO 布局作为生成头文件签进树,用一个 MG_Test 重跑树内 glslang 对同一批源码字符串并逐字节比对守新鲜度。不用构建期 host glslang target。uSource 的描述符绑定本来就由 ProgramFactory 自己的 SPIRV-Reflect 走查找到(:4340-4350),原样存活。顺带把一次 glslang 编译从 monolith 启动路径上删掉。

Espryt 有一个小号同类:g_rawDepthFetchSamplerState(DirectGLES.cpp:166-179)→ backend 原生 sampler 记录,~40 行。

6.2 strangler 脚手架:PipeInputs + 逐 verb 填充器 + poison 世代

// MG_Backend/MGPipe/PipeInputs.h
namespace MobileGL::MG_Pipe {
struct PipeInputs {
    // 阶段 A:字段类型与 backend 今天读到的**完全一致**
    const RenderStateParameters& GetRenderStateParameters() const;
    Uint16 GetRenderStateParametersVersion() const;
    const MGPVaoRec&             GetBoundVertexArray() const;
    // … 每个 backend 真正用到的 GLContext 方法一个访问器(Espryt 32 个 / Magma 55 个)
#if MOBILEGL_DEBUG || MOBILEGL_BUILD_DISAGGREGATED
    Uint64 m_filledGen[kFieldCount];   // ★v2:逐字段"上次填充的 verb 序号",不是一位
    Uint64 m_currentVerbSerial;
#endif
};
extern PipeInputs gPipeInputs;
}
#if MOBILEGL_PIPE_PUSH
#  define MGB_CTX (&::MobileGL::MG_Pipe::gPipeInputs)
#else
#  define MGB_CTX (::MG_State::pGLContext)
#endif

PipeInputs 按 memo 键组织,不是按读点组织。 这是它只有 ~20KB、且字段集在整个迁移期稳定的原因。

6.2.1 三个阶段,其中阶段 A 可证明是近乎 no-op

阶段 改什么 怎么证明
A — 别名 机械 sed:MG_State::pGLContext-> → MGB_CTX->(293 处);外加手工转换 58 行非箭头用法(§2.4)。逐 verb 类填充点(见下)填 gPipeInputs。backend 函数体其余部分不变 nm --defined-only 不变;.text size 在可逐行归因的范围内(不是完全相等,见下)
B — 推送 tracker 填 gPipeInputs;填充器仍在,按 MOBILEGL_PIPE_PUSH 位图逐字段让位 MOBILEGL_PIPE_VERIFY=1(§10.3-②):tracker 再填一份快照版,G4 生成的比对器逐字段每 draw 比一次
C — handle 化 SharedPtr<FrontendObject> 字段 → MGPipeHandle + POD 描述符;memo 重键;写回变回调 全套门(§10.3)。注意 A/B 口径在此收窄,见 §6.7

v2 修正 1:填充点必须逐 verb 类,不能只有两处。 v1 只在 PrepareForDraw(DirectGLES.cpp:2916)与 SetupDraw(VulkanRenderer.cpp:6371)顶端填快照。但 MG_Impl 用到的 70 个表项里有 ~48 个不是 draw/dispatch,其中多个自己就读 pGLContext(UpdateTextureBindingAtTarget :6051-6052、PackStateFromContext :6129、Clear :4106/:4165、BlitFramebuffer :5988-5989、GetTexImage :9254-9257、DSA by-name :4038-4043、:7417-7418),而代码自己说明了这一点(:1501-1502:"for every non-draw call site (Clear, readbacks)")。 做法:G5 从 PipeCalls.def 生成"每个 kCtxVerb/kCtxObject 调用可能读哪些 PipeInputs 字段"的表,并在 MG_Impl 的 ~93 个边界站点上生成对应的 validate/fill 调用。这同时把 poison 从"某个 draw 上炸"升级为"在需要它的那个 verb 上炸"。

v2 修正 2:poison 从"位图"升级为"逐 verb 世代"。 一个只被上一个 draw 填过的字段,在紧随其后的 glTexSubImage/glReadPixels 里读到的是陈旧值,位图版的 poison 看不见(位已置)。世代版:每次 verb 递增 m_currentVerbSerial,字段被填时记下当时的序号,读取时断言 m_filledGen[f] == m_currentVerbSerial(对"跨 verb 有效"的字段单独标注为 sticky 并在生成表里显式列出)。这才让"一个字段在某个 verb 上没被推送"必然是一次 Fatal 而不是一次静默陈旧。

6.2.2 poison 世代是完整性的运行期绊线

在 debug 与 disaggregated 构建里,读一个当前 verb 未填的非 sticky 字段是 Fatal{UnmigratedPipeInput, "GetStencilState@DrawVbo"}——响亮、精确、不可能渲染过去。P13 之后(SnapshotFromGLContext() 只在 verify 构建里)完整性变成构建期事实:一个从未被写入的字段就是一个编译器能标出来的字段。

6.3 Track V / Track H 与残余值块

  • Track V(值类型):GetRenderStateParameters、GetPixelStoreParameters、IsCapabilityEnabled(+Indexed)、GetStencilState、GetColorMaskIndexed、GetDepthMask、GetScissorBox、GetPatchVertices、GetCurrentVertexAttribute、Magma 的 ~22 个标量 getter…… 约占 B 类读点的 55%。机械,每组 ~1 天。
  • Track H(对象类型):167 个 SharedPtr<MG_State…> 点。真活。

Track V 的 55% 不需要逐字段接口条目就能跑起来,所以 P2 发一个显式临时调用 set_residual_value_state(MGPBlobRef):

struct ResidualValueBlock {
    RenderStateParameters renderState;   // 直到 create/bind_render_state + set_dynamic_state 落地
    PixelStoreParameters  pack;          // 直到 set_pixel_pack_state 落地
    Uint64 capabilityBits;
    Uint32 patchVertices; Float patchOuter[4], patchInner[2];
    // … 每个阶段变小 …
};

三条硬性纪律:

  1. 退役是一个编译错误。 static_assert(sizeof(ResidualValueBlock) == MGL_RESIDUAL_BLOCK_SIZE),常量每阶段下调;P13 到 0 之后 static_assert(sizeof(ResidualValueBlock) == 0, ...) 一直红到最后一个字段消失。
  2. 布局必须逐成员断言,不能只断言 sizeof。 异质 POD 并集跨编译器/ABI 最容易出 padding 差异,而 monolith 的 verify harness 看不见它(两侧是同一个 TU)。所以 G3 为每个成员生成 static_assert(offsetof(...) == N),并且在 split 下该块逐字段序列化而不是整块 memcpy。
  3. 只在 P2..P13 之间存在,MOBILEGL_PIPE_STATS 单独计一类字节。

6.4 DirectGLES(Espryt)逐子系统

PrepareForDraw 的阶段顺序(DirectGLES.cpp:2916-2975):GetBoundVertexArray → ResolveVaoTwin → GetProgramForDraw(join 编译池)→ CaptureDrawTextureSyncKeys → SyncNeccessaryBuffers → SyncCurrentVAO → SyncNeccessaryTextures → SyncImageTextureBindingsForDraw → MarkWritableImageBufferTexturesGpuWritten(改前端)→ SyncCurrentFBO → SyncCurrentProgram → SyncRenderState → BindCurrentFBO → VAO bind → SyncCurrentVertexAttributeValues → BindCurrentTextures → BindCurrentProgramWithResources → StartPendingTransformFeedback。

# 子系统 消除读点 memo 写回 轨 天 风险
0a GetIntegeri_v/GetInteger64i_v/GetProgramiv 移回 MG_Impl 14 0 0 — 1-2 极低(严格 no-op)
0b handle 基建;6 个 registry → slot 数组;删 TwinLookupMemo×3 / OwnerEquals / g_fbSlotCache / 2 个 GC 扫描 — 9 删 — — 5-7 低
1 渲染状态(DirectGLES.cpp:1962-2654,693 行) 4(:2007, 2021, 2050, 2133) 0 0 V 3-5 低:693 行函数体、单 Uint16 早退、三段 memcmp 全不动
2 buffer + 7 个 BufferBackendOps 19 3 6(+23 处 re-entry 删除) H 10-13 高(不碰 AcquirePersistentMap)
3 VAO / vertex elements 2(+~10 getter) 4 0(Espryt 不往前端对象写 memo) H 7-9 中
4 framebuffer / renderbuffer 8 + 4 处 pDefaultFramebufferInfo 4 1 H 7-9 中高
5 纹理 / sampler / image unit / set_texture_params / subdata 描述符改造 18(+~35 getter) 8(5 删) 21 H 23-30(v1 为 20-26,+3-4 为 §4.5.6 的跨步描述符改造) 高
6 program + constant buffer 16(+~30 getter) 5 0 H 14-18 高
7 XFB(含 scatter 搬到 client,§7.2) 3 1 2 H 5-7 中
8 emulation + MGHostSpan + 索引宿主镜像的 server 侧接口 ~12 0 3 — 8-11 中
9 回读 / pack state ~10 1 7 V+H 5-7 中
10 删 pull 路径 + MGB_CTX — — — — 4-6 低
合计 124 ~32 28 92-124

子系统 5 是全表最危险的一处:它同时压着实测 +6ms/frame 的 box-vs-rects 悬崖(Managers.cpp:4386-4390)、7 条 fallback-repack 路径、以及 v2 新增的跨步描述符改造。缓解:resource_subdata 同时携带 box 与 region 列表且 server 选形状;repack 族本体不动;子系统 5 拆成两个可独立落地的半(先 sampler view + sampler + set_texture_params,再 image unit + dirty 归属反转 + 跨步描述符),让回归能二分到其中一半。Mali 设备门必须发布逐帧上传作业数与帧时增量(不是只有 SSIM)。

6.5 DirectVulkan(Magma)逐子系统

# 子系统 读点 memo 写回 天 风险
0a/0b 同 Espryt;13 个身份缓存重键 ~10 13 0 5-8 低
1 pipeline + 动态状态 ~55 1 0 3-4 低——两个 backend 里最便宜的一次转换
2 SetupDraw + TrySetupDrawFastPath(:5994,377 行)+ SetupDrawSnapshot[4] ~48 4 0 10-13 高
3 VkBufferManager(7 个 op 里的 6 个;ResidentSubData 保持 null) ~19 2 4 7-9 高
4 VertexInputStateFactory + VaoDrawMemo(删掉写进前端 VAO 的后端堆裸指针) ~6 2 3 2-3 低(纯结构性收益)
5 VkTextureManager(3504 行)+ VkSamplerManager + set_texture_params ~30 3 7 13-16 高
6 UniformManager 描述符 + 占位纹理原生化 + 具名 UBO host payload(D-B8) ~35 4 6,且删 ~120 行 12-15 高
7 VkRenderPassManager / VkClearManager / framebuffer(保留 D18) ~20 2 0 7-9 中高
8 ProgramFactory + 内部 shader 烘焙(含 4 天烘焙与回归测试) ~15 1 2 7-9 中(构建 lane)
9 XFB(顺带修 D21)+ query + 回读 ~15 2 5 11-14 中
10 swapchain / default FBO(SwapchainObject.cpp:276-330 的写变 on_surface_changed) ~4 0 7 4-5 中
11 删 pull 路径 — — — 4-6 低
合计 169 ~34 42 85-111

Espryt 的子系统 1 与 Magma 的子系统 1 作为一个里程碑一起做(合计 6-9 天),这样同一个接口调用在两个 backend 上同时被证明。

6.6 strangler 顺序(风险最小化)

0a  getter 移出(AdvertisedLimitsScenario;严格 no-op)
0b  字节/调用计数器落地  ← 含**动态** accessor 计数与 memo 命中率(§2.3.1)
0c  清工作树 per-draw fprintf
0d  值头与制品头抽取(MGPipeValueTypes.h、ProgramArtifacts.h)+ include 图门  ← P0.5
0e  handle 基建:slot 分配器 + registry 变数组 + 删 TwinLookupMemo/OwnerEquals/g_fbSlotCache/GC
1   渲染状态(两个 backend 一起)+ Magma 子系统 4   ← 机制证明 + 第一片 Track H
2   buffer + BufferBackendOps          ← 泛化已存在的模式;不碰 AcquirePersistentMap
3   VAO / vertex elements
4   framebuffer
5   纹理 / sampler / image unit(拆两半)
6   program + constant buffer
7   XFB + query + 回读                 ← 可与 5/6 并行(第二个工程师)
8   emulation + 索引宿主镜像
9   删 pull 路径;三道纯度门转绿

0b 必须在任何迁移之前:所有 ring 尺寸、批处理阈值、wire 粒度决策否则都是猜测。0c 必须在基线之前:那两处 per-draw fprintf 污染每一次测量。0d 必须在 program 与渲染状态之前:否则纯度门与 nm -D | grep glslang 判据不可达。

6.7 A/B:旧路径怎么保留,以及它的口径在哪里收窄

MOBILEGL_PIPE_PUSH        = <子系统位图>   # 0 = 全 pull;每位一个子系统;含一位关闭 CSO 内容寻址(负面对照)
MOBILEGL_PIPE_VERIFY      = 0|1            # 影子比对(~5-10x 慢,永不出货;P13 之后仍保留)
MOBILEGL_PIPE_STATS       = 0|1            # 字节/调用/roundtrip/纹理拉取/上传形状计数器
MOBILEGL_PIPE_LEGACY_MEMOS= 0|1            # ★v2:编译期开关,保留 registry / TwinLookupMemo 实现

在 init 时刻锁存,与 MOBILEGL_BACKEND_TYPE 同一套机制(ConfigLoader.cpp:212-225),与树里已有的 ~40 个 MOBILEGL_* 开关并列。

v2 必须写明的口径收窄。 v1 说"任何一次提交都能在同一份二进制上按子系统 A/B,设备回归可以二分到'哪个子系统'"。这在阶段 B(值字段)成立,在阶段 C(handle 化)之后不成立:stage C 把 PipeInputs 的字段类型从 SharedPtr<FrontendObject> 换成 MGPipeHandle + POD 描述符、把 6 个 StateBackendObjectRegistry 哈希表换成 slot 数组、删掉 TwinLookupMemo×3 与 OwnerEquals、把 memo 重键成 {slot, gen}。位清零时,SnapshotFromGLContext() 仍要从 client 的 slot 表合成那个 handle,backend 仍然跑重键后的 memo 代码——两个分支跑的是同一份新代码。一个重键 bug(正是 D1/D2/D3/D11/D13 那一类)在两个分支里都在,位图二分不出来。

对策:MOBILEGL_PIPE_LEGACY_MEMOS(编译期开关)在 P3a 与 P4a 期间保留 registry / TwinLookupMemo 的实现活在同一个 PipeInputs 接口之下,给前两波 handle 化保留一个真正的旧-vs-新臂;随 pull 路径一起在 P13 退役。这条开关的存在期与代价必须写在阶段计划里(P3a/P4a 各 +1 天维护成本)。

P13 删除 pull 路径时:删 SnapshotFromGLContext() 的非 verify 编译分支、MGB_CTX 宏、MOBILEGL_PIPE_PUSH、MOBILEGL_PIPE_LEGACY_MEMOS;MOBILEGL_PIPE_VERIFY 连同它需要的 SnapshotFromGLContext() 与 MG_State include 一起保留(D-B5);static_assert(sizeof(ResidualValueBlock) == 0) 必须编译通过;三道纯度门(§4.7.2)在非 verify 构建上转绿。