Files
MobileGL/docs/Disaggregated/notes/recovered/wf2/final/synth-part2a.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

45 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)
    MGPipeHandles.h                    # MGPipeHandle、MGPipeKind、保留 handle、slot 分配契约
    MGPipeHostSpan.h                   # 唯一一个"形状随传输而变"的访问器(§4.5.7)
    MGPipeCallbacks.h                  # 反向通道(事件/回复)的函数表,见 §7
    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 断言
    generated/PipeCoverage.inc         # G6:477 读点 → MGPipe 调用的映射表(CI 重生成 + git diff --exit-code)
MobileGL/MG_Impl/Pipe/
    Tracker.{h,cpp}                    # st_validate_state 类比物
    SlotAllocator.{h,cpp}  CsoCache.{h,cpp}
    HostResolve.cpp                    # 客户端数组界限 / 索引扫描 / restart 重写 / 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}
scripts/gen_pipe.py                    # 跑 G1..G6

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(SetFramebufferState, MGPFramebufferState, kCtxState, kNone)                   \
  X(SetSamplerViews,     MGPSamplerViews,     kCtxState, kVarTail)                \
  /* ---- verb ---- */                                                            \
  X(DrawVbo,             MGPDrawInfo,         kCtxVerb,  kHostSpan|kVarTail)      \
  X(ResourceSubData,     MGPSubData,          kCtxObject,kHasBlob)                \
  X(RenderbufferStorage, MGPRbStorage,        kCtxObject,kNeedsAck)               \
  /* … 共约 72 项,完整目录见 §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.*(89 个 MG_Impl 站点改名即可)
G3 wire 记录结构 + 每种一条 static_assert(sizeof==N) + applier 分发前的运行期边界检查 → Fatal{ProtocolCorruption} 继承并扩展 PLAN.md §6.3 的 Records.def 机制到全部调用
G4 MOBILEGL_PIPE_VERIFY 的逐字段比对器:对每个 kCtxState/kCtxCso 调用生成 Compare(pushed, snapshot, &firstDivergentFieldName) 新增:这是每份候选设计都被判缺失的语义绊线
G5 PipeInputs::m_filledMask 的位定义 + 读未填字段时的 Fatal{UnmigratedPipeInput, "<field>"} 新增:完整性的运行期/构建期绊线,不只是测试绊线
G6 477 行读点清单 → MGPipe 调用的映射,CI 重生成并 git diff --exit-code,0 UNMAPPED 改造自 Feat/CS-Delta-IPC 的 extract_backend_read_inventory.py(删掉制造"0 UNMAPPED"的前缀兜底规则)

G4 与 G5 从同一份 .def 生成,因此不可能与调用目录漂移。 这是把"完整性只有测试绊线、没有构建绊线"这条历史评审结论一次性关掉的机制。

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

4.2 对象模型

4.2.1 Handle

// MG_Pipe/MGPipeHandles.h
enum class MGPipeKind : Uint8 {
    Buffer=1, Texture, Renderbuffer, Framebuffer, Xfb,
    RenderStateCso, VertexElementsCso, SamplerCso, SamplerViewCso, ShaderCso,
    Fence, Query, Context
};
struct MGPipeHandle {          // 8 B,POD,可哈希,按值传寄存器对
    Uint32 slot;               // 按 kind 稠密分配(client 侧 SlotAllocator)
    Uint32 gen;                // slot 复用时 ++;client 侧维护
};
  • slot 稠密、按 kind 分配。这一条是把 server 的对象表从哈希表变成数组的唯一途径,也是删掉 TwinLookupMemo 的斐波那契散列 + owner 相等性检查的原因。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),因为它们没有 GL name。

4.2.2 两种 generation,严格分开

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

MGGen 是 §0.3 推论 3 那 12 个计数器的 wire 名字。接口规范条款:任何 MGPipe 调用都不得要求 client 提供或知晓 MGGen。 这条是"server 拥有自己的状态机"的可执行定义。反过来也是规范:client 侧的版本计数器永远不是新鲜度的唯一证明——每一个回绕的 Uint16(§2.6)在过线时要么被加宽到 32 位、要么与 {slot, gen} 同行。

4.2.3 CSO vs 可变对象

类别 形态 因为 backend 今天就是这么缓存的
VertexElementsCso create/bind/delete VertexInputStateFactory::m_cache,键正是 VertexAttribute 那组字段的 content hash(VertexInputStateFactory.cpp:19-50)
SamplerCso create/bind/delete VkSamplerManager::m_samplers(VkSamplerManager.h:104-117);Espryt 的 BackendSamplerObject(Managers.h:1808-1824)
SamplerViewCso create/delete + 由 set_sampler_views 绑定 TextureResource::{perMipViews, perMipSampledViews, attachmentViews, alternateSampledViews, storageImageViews}(VkTextureManager.h:173-370);Espryt 的 SyncTextureViewToBackend(Managers.cpp:3616-3707)
ShaderCso create/bind/delete + server 侧惰性特化(D-B2) ProgramFactory::m_cache(ProgramFactory.h:614-642);BackendProgramObjectImpl(Managers.h:1473-1725)
RenderStateCso create/bind/delete,payload 是整块 blob(D-B1) Espryt 的值镜像 + 三段 memcmp;Magma 的 ComputePipelineStateHash 单次 bulk fetch
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 的先例):MG_Impl/Pipe/CsoCache 每类一张 ska::flat_hash_map<Uint64 xxHash, MGPipeHandle>,容量上限(blend/DS/rasterizer 合并后的 render-state 64、vertex-elements 1024、sampler 256、sampler-view 4096、shader 不设上限跟随 ProgramObject 生命周期),LRU 淘汰时发 delete_*_state。内容寻址的一个直接收益:两个不同 program 设置了相同的 blend 状态时,server 侧零状态转换。

任何 create_* 都不返回 server 铸造的 handle。 这是对 gallium 的有意偏离(gallium 的 create_*_state 返回 driver 指针),也是这份目录能在零创建 round trip 下远程化的根本原因。今天的 BackendSyncHandle/BackendQueryHandle = void*(BackendObject.h:110, 115)随之变成 MGPipeHandle——fence 与 query 需要的唯一改动。

4.3 MGPipeScreen 与 MGPipeContext

GL 的 share-group 规则本身就是分界,而 MobileGL 的容器归属已经吻合:

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

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

4.4 完整调用目录

完整速查表(含 payload 大小与 flags)在 part4 的附录。这里给出结构与每项的来源/取代关系。

4.4.1 MGPipeScreen(14 项)

调用 payload 取代
get_caps(MGPCaps* out) DynamicBackendParameters(BackendObject.h:302-522,~90 标量,平坦 POD)+ RendererInfo + FormatCapabilityCache(:88-99,14 个能力位 × 全部 internalformat × 12 target 的稠密表 + 每格 SampleCounts)+ 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)泛化;server 自己按 busy 跟踪决定 swap-vs-in-place
resource_destroy(h) handle OnDestroy(:101)+ 两个 WeakPtr GC 扫描(Managers.h:353-390、VkTextureManager.cpp:1694-1720)。server 的 pooling / 延迟释放(Managers.cpp:1271-1300、VkBufferManager::OnResourceDestroyed)原样保留
map_persistent(h) → MGPMapResult / unmap_persistent(h) — AcquirePersistentMap(:112)。改造期不碰(D-B4)
fence_create/status/wait/destroy handle (+timeout) FenceSync…GetSyncStatus(:220-224)。两值契约(:243-249 "false = 还没好,留着 handle")逐字保留
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 拿 BeginOcclusionQuery != nullptr 当能力位)。它同时是 emulation 归属的判据(§5.7):kCapPrimitiveRestart、kCapPrimitiveRestartFixedIndex、kCapMultiDraw、kCapMultiDrawIndirect、kCapMultiDrawIndirectCount、kCapViewportArray、kCapFloat64VertexAttrib、kCapResidentSubData、kCapCpuXfbPrimitiveAccounting、kCapNeedsHostIndexBytes、kCapTimerQuery、kCapOcclusionQuery、kCapXfbPrimitivesQuery。

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_*(14 项)

调用 取代的拉取点
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(stage, start, count, const MGPBoundView*) GetTextureUnitObject ×19、GetActiveTextureUnit ×8、GetTextureBindGeneration ×5。client 侧已解析(§5.5)
bind_sampler_states(stage, start, count, const MGPipeHandle*) TextureUnit.h:394
set_shader_images(start, count, const MGPImageView*) GetImageTextureBinding ×14;这一条退役 ImageUnitFormatsStillMatch(Managers.cpp:6545-6573)
set_shader_buffers(cls, start, count, const MGPBufferRange*, writableMask) GetBufferBindingPoint ×19、GetTouchedBufferBindingPointCount ×2。cls ∈ {Uniform, ShaderStorage, AtomicCounter}
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)
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 输入(RenderState.h:243-249)
set_draw_program(shaderCso) / set_dispatch_program(shaderCso) GetProgramForDraw ×7、GetProgramForDispatch ×3。含 composite(§5.6)

迁移期额外一项(显式临时):set_residual_value_state(MGPBlobRef),见 §6.3。它带一个编译错误式退役绊线。

4.4.4 MGPipeContext — transfer(12 项)

resource_subdata(buffer + texture 同一形状,同时携带 union box 与 rect 列表,§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 项。统一强制的接口会静默改变 Magma 的行为并发布一个 Minecraft chunk 撕裂。

4.4.5 MGPipeContext — 命令(4 + 4 + 2 项)

void draw_vbo (const MGPDrawInfo*, Uint32 drawIdOffset,
               const MGPDrawIndirect*, const MGPDrawRange*, Uint numDraws);
void launch_grid(const MGPGridInfo*);      // + indirect 变体字段
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);                  // GL_SYNC_FLUSH_COMMANDS_BIT 语义
void present(Uint64 frameSerial);  void set_swap_interval(Int interval);   // 后者可 null(Magma)

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

4.4.6 显式删除、不移植的项

  • GetIntegeri_v / GetInteger64i_v / GetProgramiv(BackendObject.h:195-197)。DirectGLES.cpp:7264-7386 的 15 个 case 完全不碰 GL(占 Espryt 124 个读点里的 14 个);Magma 同形(DirectVulkan.cpp:685, 759),只有 GL_COMPUTE_WORK_GROUP_SIZE(:790-795)是真后端答案,进 MGPCaps。
  • ShaderStorageBlockBinding(:207-208)→ 折进 MGPProgramDesc 的反射归档(LinkArtifacts::shaderStorageBlockBinding 按名字索引,ProgramObject.h:1325)。它自己的注释已经说 program interface 由前端回答。
  • 总规则: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   (== PIPE_BIND_*)
    Uint32 internalFormat;    // GL internalformat,已在前端解析为非压缩后备
    Uint32 width, height, depth;
    Uint16 arrayLayers, levels, samples;
    Uint8  fixedSampleLocations, immutable;
    Uint32 usage;             // BufferUsage
    Uint32 storageFlags;      // glBufferStorage flags
    Uint8  hasDefinedContent;  // NULL-data respecify(orphaning 惯用法)之后为 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,实时解析
};

Renderbuffer 保持独立类,不折进纹理。 它有自己的 format-capability target 索引(BackendObject.h:85)、自己的 ComponentSizes 上报(RenderbufferObject.h:37-43)、自己的 twin(Managers.h:1838)。合并零收益且丢掉一张独立能力表。bindMask 让它在绑定语义上与纹理统一,这就够了。

4.5.2 MGPRenderStateDesc / MGPBindRenderState(D-B1)

struct MGPRenderStateDesc {          // create:整块,每个唯一状态一次
    MGPipeHandle cso;
    Uint8  spanMask;                 // bit0=head, bit1=blend[], bit2=tail;未命中时只发变化段
    MGPipeHandle baseCso;            // 增量基(spanMask != 0b111 时有效)
    MGPBlobRef   blob;               // RenderStateParameters 的对应段
};
struct MGPBindRenderState {          // bind:稳态 12 B
    MGPipeHandle cso;  Uint16 version;  Uint16 pipelineVersion;
};

段划分与 DirectGLES.cpp:2038-2047 现有的 head/blend/tail 完全一致,所以两侧对"段"的定义按构造相同。两个版本号都过线(RenderState.h:522 / :529)。

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:glVertexAttribFormat(GL_DOUBLE) 读 double 转 float,glVertexAttribLFormat 保留 64 位,GL_VERTEX_ATTRIB_ARRAY_LONG 上报这个区别)。仅供查询的 LegacyStride/LegacyPointer(:51-52)留在 client,永不过线。

4.5.4 SamplerParameters 与 MGPSamplerView

SamplerParameters(SamplerObject.h:468-492)逐字节原样过线,包括 borderColorForm(:462-466)::455-461 明说没有它 backend 无法在 glSamplerParameterIiv 与 fv 之间、或在 VkBorderColor 家族之间选择,因为三种表示(borderColor/borderColorI/borderColorUI)永远都被数值填满。

struct MGPSamplerView {              // = pipe_sampler_view
    MGPipeHandle cso, texture;
    Uint32 internalFormat;           // 别名格式(glTextureView)
    Uint8  target, depthStencilMode, pad[2];
    Uint16 minLevel, numLevels, minLayer, numLayers;
    Uint8  swizzle[4];
    Uint16 samples;  Uint8 fixedSampleLocations, pad2;
};

glTextureView 因此从"ES name 重铸特殊路径"(Managers.cpp:3616-3707)变成一个普通 view CSO,这对两个 backend 都是净简化。真正需要保留的区别是 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,顺序与 GetLinkedShaderSnapshot() 一致
    MGPBlobRef reflection;           // Visit() 归档的 LinkArtifacts + SpirvArtifacts(全结构体)
    Uint32     globalUboSize;        // GetUBOSize()
    Uint32     reservedNumSamplesOffset;
    Uint8      spirvStatus, nativeFloat64, pointSizeDemoted, enableSpirvValidation;
};

反射归档序列化整个结构体,而不是 backend 今天读的那 ~40 个字段——这样 backend 新增一次 read 永不需要改协议。机制沿用 PLAN.md §6.9 的 Visit() + sizeof 绊线,但用途改变:它不再是"分歧预言机"(没有可分歧的对象),而是schema 完整性绊线:

// 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");

归档必须覆盖:uniformReflection/blockReflection/pipeInputReflection/pipeOutputReflection(ProgramObject.h:1210-1395,各带 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),因为 SPIR-V 装饰的是 block 的实例变量与成员索引,无法装饰单个数组元素。

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 键
};

三点值得单独说:

  1. readSurface 是 client 解析后的表面,不是 glReadBuffer 的索引。 这一条按结构消灭 read-buffer-shared-FBO 缺陷类(同一个 FBO 既做 draw 又做 read 时跳过 read-buffer 同步,导致回读全取 attachment 0)。
  2. internalFormat 内联在每个 MGPSurface 里,所以四个跨对象 mask(g_snormFallbackClampOutputMask、g_unormFallbackClampOutputMask、g_alphaWidenedDrawBufferMask、g_integerColorDrawBufferMask,Managers.cpp:5616-5619)在 set_framebuffer_state 内部零查表推出。配合 D-B3 的固定顺序,DirectGLES.cpp:2712-2732 的 fragColor 重推导 workaround 与 g_broadcastMemo* 一起删除。
  3. contentHash 是全表最有价值的一个字段:它用一次 64 位比较取代 D7 的四元组同步戳与 D15 的 (pointer, lifetimeId, Uint16 version) 三元组,由一个本来就在跟踪全部输入的 client 计算。attachment enum 到 Color31(FramebufferObject.h:65-66)而 MAX_DRAW_BUFFERS 是 8(:106)——surface 数组是 8,其余由 drawBuffers[] 的解析结果承载。ARB_framebuffer_no_attachments 的默认值是独立字段(:134-138)。
struct MGPSubData {
    MGPipeHandle res;
    Uint16 target, level;
    MGPBox box;                       // union box
    Uint32 rectCount;                 // rect 列表在变长尾
    MGPBlobRef blob;
};

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

4.5.7 MGPDrawInfo 与 MGHostSpan

struct MGPDrawInfo {                  // = pipe_draw_info,GL 有对应概念的字段逐一对齐
    Uint32 mode;
    Uint8  indexSize;                 // 0 = arrays,否则 1/2/4
    Uint8  flags;                     // kHasUserIndices | kPrimitiveRestart | kRestartRewritten |
                                      //   kXfbCountUnknown | kIndicesAreClient
    Uint16 pad;
    Uint32 instanceCount, startInstance;
    Uint32 restartIndex;
    Uint32 minIndex, maxIndex;        // client 计算(gallium 有这两个字段);~0 = 未知
    MGPipeHandle indexResource;
    MGHostSpan   userIndices;         // = pipe_draw_info::index 的 union
    Uint64 xfbCpuCapturedVertices;    // GetTransformFeedbackCapturedVertices(),给 scatter 路径定容量
};
struct MGPDrawRange { Uint32 start, count; Int32 indexBias; };   // = pipe_draw_start_count_bias

MGHostSpan 是整份接口里唯一一个"形状随传输而变"的东西,它把 backend 今天在 draw 时刻解引用客户端字节的四个类统一成一个访问器:

struct MGHostSpan {                   // 32 B
    const void* ptr;                  // monolith:指向前端 shadow / 应用内存。split:nullptr
    Uint64      size;
    Uint32      seg;                  // split:SEG_STAGE id
    Uint32      pad;
    Uint64      offset;               // split:段内偏移
};
inline const void* MGPipeHostBytes(const MGHostSpan&);   // 一次分支,每次使用解析一次
消费者 今天的站点 monolith 填法 split 填法
client 顶点数组 Managers.cpp:2500-2592(每 draw 每属性上传 (first+count-1)*stride+elementSize)、VulkanRenderer.cpp:3737 ptr = attrib.Offset tracker 暂存同样的范围进 SEG_STAGE
client 索引数组 DirectGLES.cpp:4425-4442、VulkanRenderer.cpp:3418-3433 ptr = indices 暂存 count*indexSize
indirect / parameter 命令块 DirectGLES.cpp:4666-4695(MultiDrawElementsIndirectCount 从 parameterBuffer->MappedData() 读实际 draw 数)、:4768-4793、VulkanRenderer.cpp:12045 ptr 指向 shadow tracker 解析出计数并暂存解析后的命令块
restart 重写 / multi-draw 展平的索引字节 DirectGLES.cpp:4412-4415、MultiDraw.cpp:498-540、VulkanRenderer.cpp:4159 ptr 指向 shadow 暂存,或 client 已重写(§5.7)

monolith 代价为零(一次指针加载),而且它顺带消灭了"backend 在 draw 中途回头调前端来 reconcile"这一整类:20 处 SyncPersistentMappedRange + 6 处 SyncGpuWrites 全部是"我马上要读你的字节,先把 shadow 对齐",现在改由 tracker 在填 span 之前做同样的 reconcile。这在 monolith 里也是严格改进。

kCapNeedsHostIndexBytes 存在的理由正是这条 monolith/split 的不对称:monolith 里 span 是一个免费指针,split 里是一次拷贝。让一个从不需要宿主索引字节的 backend 不付这笔钱。

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

# gallium MGPipe 理由(证据)
D1 create_*_state 返回 driver 指针 调用方提供 handle,create 返回 void 零创建 round trip;handle 是稠密 slot,让 server 对象表变成数组;退役全部 D 类指针 memo
D2 pipe_screen::get_param(cap)、is_format_supported(...) 逐项查询 一个 MGPCaps POD + 一张稠密 format 表 DynamicBackendParameters(BackendObject.h:302-522)与 FormatCapabilityCache(:88-99)本来就是平坦结构,逐项 RPC 等于每次 glGetInternalformativ 一次 round trip
D3 CSO 切分是 D3D10 时代的:polygon offset、stencil write mask 在 rasterizer/depth-stencil CSO 内部 CSO 边界跟 Vulkan 动态状态走:depth bias、stencil ref/write mask、line width、min sample shading、blend color、viewport、scissor、depth range 是 set_* 而非 CSO 字段 RenderState.h:523-528 记录了共用一个版本号导致 glViewport 冲掉 pipeline memo 和 draw 快路径;m_pipelineStateVersion(:529)恰好是 CSO 相关子集;Magma 的 DynamicStateShadow(VulkanRenderer.cpp:277-340)与 ApplyDynamicDrawStateTail(:5871)已经这么切
D3b 三个独立 CSO:blend / depth_stencil / rasterizer 一个 RenderStateCso,payload 是整块 blob D-B1 的全部理由:is_trivially_copyable_v 断言(DirectGLES.cpp:2035)、三段 memcmp(:2042-2047)、字段顺序承重(RenderState.h:359-368)、两个 backend 都按 span/bulk 消费、拆分需手工维护 ~150 字段划分表且无完整性绊线
D4 transfer_map/transfer_unmap(scoped) resource_subdata 推送 + map_persistent(永久地址空间捐赠) AcquirePersistentMap(BufferObject.h:102-118)把指针交给应用,成为唯一真相源,≥16MiB 自动走到(:226-228)。实测 p99 163→21ms、40→115fps。不是 scoped map
D5 driver 看得见压缩格式与 pixel-unpack 状态 两者在 MGPipe 里都不存在 前端在 glTexImage 时把压缩 internalformat 解析成非压缩后备(GL_Texture.cpp:298-306);ScopedDefaultUnpackState(Managers.cpp:2888-2910)强制 unpack 默认值,因为 PixelStoreProcessor 已经规整过。只有 PACK 方向过线
D6 默认 uniform block = constant_buffer 0 独立入口 set_global_constants(shaderCso, bytes, version) SpirvArtifacts::globalUboScratch(ProgramObject.h:1418)是 link phase B 产出的 CPU 数组,布局由优化后的 SPIR-V 决定(:1400-1408 说明它无法从 glslang 反射提出来,因为 spirv-opt 就地跑且能删掉整个 block)。它没有 GL name、没有 BufferObject、没有 PipeResource。~0u 跳过回绕的规则保留(:792,那是 backend 的"从未上传"哨兵)
D7 pipe_shader_state = tokens → 完成的 handle handle + server 侧惰性特化,variant 键取自已推送状态 D-B2 的 8 个输入。这其实就是 gallium:Mesa 的 st_variant 也按已绑定状态键控(st_get_vp_variant)
D8 pipe_context::flush + fence 是唯一反向通道 MGPipeCallbacks:9 个具名回复/事件(§7) gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇。把它们具名化好过藏在 95 个写回点里
D9 set_viewport_states(start_slot, num) float 数组 + 独立的 writtenMask viewport 是 float 不是 int(RenderState.h:229-237:KHR-GL43.viewport_array.viewport_api 用 == 无容差比较);scissor 必须单独带 ScissorBoxWrittenMask(:363),因为矩形本身回答不了"应用说过话没有"——ScissorBoxes 初值全零而 glScissor(0,0,0,0) 是合法 GL、意思是"拒绝每个片元",把空框反转成"接受一切"被 KHR-GL43.viewport_array.scissor_zero_dimension 抓到(:352-362)

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

4.7 覆盖论证

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

../MobileGL-CS/docs/CS_Refactor/backend_read_inventory.md(Feat/CS-Delta-IPC 生成)把 477 个 backend 读点分成 19 个 delta 类、0 UNMAPPED。它的行已经过时(TwinLookupMemo 族、buffer mutation epoch、DrawTextureSyncKeys、SetupDrawSnapshot/VaoDrawMemo、三条 persistent ring 都晚于它),但它的分类学是现成的覆盖清单。逐类映射:

delta 类 n 满足它的 MGPipe 调用 残余
handle 化(wire 句柄) 167 每个命名对象的调用签名里的 MGPipeHandle —
RenderStateBlob 99 create/bind_render_state —
ObjectBind:Texture / Sampler 33 set_sampler_views + bind_sampler_states —
ObjectBind:Buffer(array/element/indirect/parameter) 29 set_vertex_buffers / set_index_buffer / set_indirect_buffers —
ObjectBind:BufferRange(UBO/SSBO/AC/XFB) 24 set_shader_buffers / set_stream_output_targets —
FboAttach + DrawBuffers + ReadBuffer 19 set_framebuffer_state —
Buffer ops delta 17 resource_create/respecify/destroy、resource_subdata、buffer_subdata_resident、resource_flush_range、resource_readback、map_persistent —
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 create_sampler_view(base/max level、swizzle、dsMode)+ create_sampler_state —
Texture state(dirty level/rect) 7 resource_subdata 归属反转(§7.3)
PixelStoreBlob 6 set_pixel_pack_state unpack 删除
client-resolved(error queue) 6 删除:RecordError → on_gl_error 回调(§7) —
ProgramPublish 3 create_shader_state —
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:谓词只在 glBeginConditionalRender 解析一次
XFB CPU 计数 — 纯 client;MGPDrawInfo::xfbCpuCapturedVertices + MGPCaps 的 kCapCpuXfbPrimitiveAccounting —
backend 重铸纪元 — 无 client 对应物:MGGen,server 私有 —

那 1997 个前端 getter 站点不是第二个面:89 个纯版本读根本不过线(推送即信号),72 个数据字节读全部落在 §5.7 与 MGHostSpan,38 个 GetLifetimeId() 变成 handle,其余是 backend 现在自己持有的 POD 记录的字段读。

4.7.2 覆盖论证不是这张表,是这条门

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

接口纯度门。 在 MOBILEGL_PIPE_PUSH=all 构建里,MG_Backend 被编译时 MG_State/GLState/Core.h 不可用、MG_State::pGLContext 未声明。任何接口没满足的读都是一次指名文件与行号的编译错误。strangler 结束时:grep -c 'pGLContext' MG_Backend/ == 0(注意 grep 的是 pGLContext 不是 pGLContext->,因为还有 58 行非箭头用法,§2.4);MG_Backend 只允许 include 一张共享值头白名单(RenderState.h 的 RenderStateParameters、SamplerObject.h 的 SamplerParameters、RenderState.h 的 PixelStoreParameters、VertexArrayObject.h 的 VertexAttribute、纹理/格式枚举),今天是 50 行 include / 18 个不同头文件;nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang' 为空。

这比生成一张 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) 结构性删除:set_sampler_views 就是信号 ~115 行
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 删除:set_framebuffer_state 是信号 —
D9 ResolvedTextureBindingMemo:9 个键 + 驱动绑定影子的 memcmp(:3218-3291) 任何未枚举的写者扰动某个 unit (shaderCso.slot, viewSetSerial) 两字比较;viewSetSerial 由 server 在 set_sampler_views 内部 ++ 更便宜;自校验性质保留(server 的绑定缓存就是它当年比对的那份影子)
D10 UnitSamplerLookupMemo 的 WeakPtr owner 测试(:3105-3125) 死 sampler 复活 数组下标 删除
D11 VertexInputStateFactory::ComputeHash 混入 GetLifetimeId()(:38-49) 复用 buffer 地址重现整个 content hash(XFB 捕获拿回死 VAO 的顶点数据) CSO handle 就是身份;gen 混进 server 侧每个 content hash 而不只是比较 删除一整类
D12 SetBackendStateMemo(&entry, evictionEpoch):前端 VAO 里存后端堆裸指针(VertexInputStateFactory.cpp:78) table 淘汰 直接删除,不翻译。无 wire 对应物,也不需要;Magma 自己的 VaoDrawMemo 已经演示了替代品 —
D13 VaoDrawMemo 槽(VulkanRenderer.h:1230-1245,注释:"两条声明的防线都失守,因为都归约到 content hash,而 content hash 的 buffer 身份分量本身就是复用的堆地址") ABA CSO handle 2 字
D14 SetupDrawSnapshot 的三组 (ptr, lifetimeId, version) + 有损的 sampledContentSum/sampledParamsSum(.cpp:6249-6250) 一切 三个 handle + 两个 server 纪元 + dirty mask ~14 个探测字段 → 1 次比较;顺带消灭一类哈希碰撞(求和会碰撞)
D15 m_rpFast*(VkRenderPassManager.h:305-320) ABA contentHash + MGGen 1 次比较
D16 VkTextureManager::TextureIdentity{ptr, lifetimeId} + pGLContext->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* 失效(BlitFramebuffer 静默停在 "layout undefined") UNCHANGED。 接口对此零约束;这是 server 内部分配纪律。slot 数组在插入下稳定,实际更好——但这条 postmortem 注释必须逐字带进 review checklist 保留
D19 ProgramFactory::m_cacheStructureEpoch 守 SetupDrawSnapshot 里的 server 内部裸指针 UNCHANGED(MGGen 族) 保留
D20 ConvertedVertexStreamKey + 纯为防地址复用持有的 SharedPtr sourcePin(VulkanRenderer.h:1124-1127) ABA server 拥有资源;changeSerial 过线 pin 删除
D21 m_xfbCounterSlotByObject[GetBoundTransformFeedbackName()](VulkanRenderer.cpp:11136-11146) 什么都没守——这是一个活的潜伏 bug:删了又重建的 XFB 对象会继承前任的计数槽,配上匹配的 m_xfbLastSeenGeneration 会恢复一次本该重启的捕获 XFB 对象 handle 顺带修一个 bug。这一条值得先独立落到 dev

总计:12 条直接删除,8 条重键成比它替代的东西更便宜的比较,1 条(D18)原样不动。 这就是"身份键 memo 不可移植"这条历史反对意见的具体回答——它们不是以指针形式可移植,接口的职责正是给它们一个可移植的键,而树里的证据已经把那个键是什么讲清楚了。