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.
45 KiB
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 键
};
三点值得单独说:
readSurface是 client 解析后的表面,不是glReadBuffer的索引。 这一条按结构消灭 read-buffer-shared-FBO 缺陷类(同一个 FBO 既做 draw 又做 read 时跳过 read-buffer 同步,导致回读全取 attachment 0)。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*一起删除。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 不可移植"这条历史反对意见的具体回答——它们不是以指针形式可移植,接口的职责正是给它们一个可移植的键,而树里的证据已经把那个键是什么讲清楚了。