mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-15 22:13:24 +09:00
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.
250 lines
37 KiB
Markdown
250 lines
37 KiB
Markdown
## 7. backend → frontend 反向通道
|
||
|
||
这是历次评审对任何薄 backend 设计的中心反对意见,所以逐条处理,**不做概括**。实测:`grep -rnoE "(->|\.)(SetBackendResource|SetBackendHashMemo|SetBackendStateMemo|SetBackendAuxMemo|WritebackFromBackend|MarkGpuWritten|MarkStorageDirty|AllocateStorage|SetInternalFormat|UpdateMipmapSubData|EnsureGpuResidentStorage|SyncPersistentMappedRange|SyncGpuWrites|RecordError|InvalidateCompileEnv|TruncateMipmapLevels|SetSamples)\(" MG_Backend/` = **95 个调用点 / 17 个方法**,外加 6 处 backend 反向进 `MG_Impl`。
|
||
|
||
### 7.1 `MGPipeCallbacks`:把反向通道具名化(对 gallium 的偏离 D8)
|
||
|
||
```cpp
|
||
// MG_Pipe/MGPipeCallbacks.h —— context_create 时安装;monolith 里是直调,split 里是记录
|
||
struct MGPipeCallbacks {
|
||
void (*on_gl_error) (Uint32 code);
|
||
void (*on_gpu_written) (MGPipeHandle res, Uint rangeCount, const MGPRange*);
|
||
void (*on_buffer_writeback) (MGPipeHandle res, Uint64 off, MGPBlobRef bytes);
|
||
void (*on_texture_writeback) (MGPipeHandle res, const MGPBox*, MGPBlobRef bytes);
|
||
void (*on_texture_pull_request) (MGPipeHandle res, Uint16 target, Uint16 firstLevel, Uint16 levelCount);
|
||
void (*on_mip_levels_generated) (MGPipeHandle res, Uint16 base, Uint16 count);
|
||
void (*on_surface_changed) (const MGPSurfaceInfo*);
|
||
void (*on_caps_invalidated) ();
|
||
void (*on_log) (Uint8 level, const char* text);
|
||
};
|
||
```
|
||
|
||
gallium 没有 shadow writeback、GPU-write 通知、纹理重发请求、default-FB 几何这些词汇——因为在 Mesa 里 state tracker 与 driver 共享地址空间。**把它们具名化为 9 个回调,好过藏在 95 个 poke 点里。**
|
||
|
||
### 7.2 95 个写回点的逐族归属
|
||
|
||
| 族 | n | 变成什么 |
|
||
|---|---|---|
|
||
| `SyncPersistentMappedRange` | **20** | **作为反向调用彻底消失。** 每一处都紧挨着一次对客户端字节的 CPU 读,而那些读全部搬到了 client(§5.8);**tracker 在填 `MGHostSpan` 之前**做同一次 reconcile。**这是整份设计里最大的一处结构性简化**,而且它在 monolith 里也是净改进——今天这 20 处是 backend 在 draw 中途回头调前端 |
|
||
| `MarkStorageDirty` | **18** | 16 处是 server 本地记账("我上传了这个 level")——**零消息**,因为 dirty 归属反转(§7.3)。2 处 `true`(`Managers.cpp:2813`、`DirectGLES.cpp:6852`)变 `on_texture_pull_request` / `on_texture_writeback` |
|
||
| `AllocateStorage` | **8** | 6 处是 **backend 凭空造出来的前端对象**(Magma 的占位纹理、`SwapchainObject` 的 default-FBO 颜色/深度/模板占位,`SwapchainObject.cpp:284, 305, 329`)→ **server 原生,永不上线**;1 处是生成 mip 的 shadow(`DirectGLES.cpp:6261`)→ `on_mip_levels_generated`;1 处是 swapchain 尺寸变更 → `on_surface_changed` |
|
||
| `WritebackFromBackend` | **8** | `MGPReplySlot`(回读)+ `on_buffer_writeback`(XFB 捕获、PBO 回读)。**必须按操作级批处理**:其中两处今天在循环里**逐行**写回(`Utils.cpp:2342`、`DirectGLES.cpp:7633`),绝不能变成"每扫描线一次 IPC" |
|
||
| `SetInternalFormat` | **7** | 与 `AllocateStorage` 同批:server 原生或 `on_surface_changed` |
|
||
| `SyncGpuWrites` | **6** | 同 `SyncPersistentMappedRange`:变成 tracker 发射前的 reconcile |
|
||
| `MarkGpuWritten` | **6** | client 在每个 draw/dispatch 发射点**保守自建**,镜像 `DirectGLES.cpp:459-467, 509, 1809` 与 `UniformManager.cpp:1073, 1229`、`VulkanRenderer.cpp:11210` 的输入(client 全都有)。`on_gpu_written{res, ranges[]}` 是**收窄**通道——因为 server 拥有描述符解析,它能比今天"任何绑了 SSBO 的 draw 一律置位"**更精确** |
|
||
| `RecordError` | **6** | `on_gl_error`,**必须对命令流有序**(§7.4) |
|
||
| `SetBackendResource` | **4** | **删除。** server 拥有资源表;client 只留 `{slot, gen}`。`resource_destroy` 取代由 `~BufferObject` 驱动的 `OnDestroy`;pooling / 延迟释放(`Managers.cpp:1271-1300`、`VkBufferManager::OnResourceDestroyed`)原样搬到 server 侧 |
|
||
| `EnsureGpuResidentStorage` | **3** | server 本地决策(`map_persistent` 的档位在 IPC 期决定,§8.3) |
|
||
| `SetBackendHashMemo` / `SetBackendAuxMemo` | **3** | 纯值 → server 侧 per-slot 字段(Magma 的 `VaoDrawMemo` 已经在镜像其中两个) |
|
||
| `InvalidateCompileEnv` | **2** | `on_caps_invalidated`,context 重建时,低频 |
|
||
| `SetBackendStateMemo` | **1** | **直接删除,不翻译**(D12:前端对象里的后端堆裸指针,无 wire 对应物) |
|
||
| `UpdateMipmapSubData` / `TruncateMipmapLevels` / `SetSamples` | **3** | 全在 Magma 的占位纹理里 → server 原生 |
|
||
|
||
**6 处 backend 反向进 `MG_Impl`:** 四处 `pDefaultFramebufferInfo` 身份比较(`DirectGLES.cpp:1917, 2838, 2867, 9675`)→ 保留 handle `{0,1}` + `MGPFramebufferState::isDefault`;`SwapchainObject.cpp:276-330`(backend **创建** default FBO 的三张 `ITextureObject`)→ `on_surface_changed{extent, colorFormat, depthFormat, stencilFormat}`,client 自己合成对象——**顺带删掉 monolith 里的一处分层倒置**;`VulkanRenderer.cpp:10700`(`CopyTextureImageToClientOrPBO_State`,唯一一处真正无法 handle 化的调用)→ `get_texture_image` 返回 **"该 level 无 GPU 背书,请从你自己的 shadow 回答"**(`:10691-10704` 今天测的正是这个条件),client 跑它本来就拥有的 pack/convert 路径。
|
||
|
||
### 7.3 纹理 dirty 归属反转
|
||
|
||
**client** 保留 `MipmapStorage` 的模型(96-rect 级联合并 + `summedArea*4 >= unionArea*3` union-box 回退,`MipmapStorage.cpp:300-305`),维护一份**逐 `(texture, uploadTarget, level)` 的发射游标**,在发射后清自己的标志。**server 从不碰 client 的标志。**
|
||
|
||
这是安全的,且已核实:**`MG_Impl` 里没有任何 `IsStorageDirty(` / `GetStorageDirtyRects(` / `GetStorageDirtyRegion(` 调用点**(前端从不读自己的 dirty 状态),而它自己在五处主动清(`GL_Texture.cpp:528, 701, 5547, 5621, 5691`)。
|
||
|
||
**这一条删掉 `PLAN.md` §5.6a 的整个 ack 协议与风险 R6。**
|
||
|
||
**但上传形状决策留在 server**:`resource_subdata` 同时带 union box 与 rect 列表(§4.5.6),Mali 按作业数计价的 +6ms/frame 悬崖(`Managers.cpp:4311-4319`)在哪一侧付 GPU 代价,决策就留在哪一侧。
|
||
|
||
### 7.4 反向通道的有序性是正确性要求,不是优化
|
||
|
||
**`on_buffer_writeback` 必须与 epoch bump 有序。** 今天每一次 `WritebackFromBackend` 后面都紧跟一次 `BumpBufferMutationEpoch()`(`DirectGLES.cpp:834-837, 942, 7625-7629`),因为写回会 bump client 的 change serial 而没有对应的 op,否则 server 自己的 draw-clean memo 会在 epoch 背后变陈旧。split 里这变成**反向通道上的一条排序规则**:一次写回的 epoch bump 必须在任何后续读该 handle 的命令之前被 server 侧应用。**反向通道需要与正向通道相同的有序保证。**
|
||
|
||
**`on_gl_error` 必须对命令流有序**,否则 `glGetError` 答错。分配类入口(`glRenderbufferStorage*`、可能失败的 `glTexImage*`/`glTexStorage*`/`glCopyTexImage*` 形式、`glBufferStorage`)带 `kNeedsAck`,让 OOM 探测惯用法(`allocate; if (glGetError()==GL_OUT_OF_MEMORY) 用更小的重试;`)成立;其余晚到。`glGetError` 本身永远本地(`GL_Getter.cpp:2811-2817`;不变式 `Core.cpp:48-49`)。
|
||
|
||
**对 `PLAN.md` §7.4 的强制修正:`on_log` 必须按严重级分级。** `PLAN.md` 把**全部** `EvLogLine` 设为有损(覆盖最旧 + `eventDropped`)。但 §5.7 已经确认:**backend program link/compile 失败只以一行日志加一次 bind-program-0 的空 draw 呈现**,没有 GL error、没有前端变更、`GL_LINK_STATUS` 永不撤回。在统一有损策略下,系统里诊断价值最高的那一行会在日志压力下静默消失。
|
||
|
||
**规则**:`on_log(level ≤ WARN)` 有损(`eventDropped` 计数);**`on_log(level ≥ ERROR)` 无损**,加入触发 `eventRingFull` + 停止 apply 的语义事件集。为了不让这条重新引入 §7.4 本来要防的死锁,再加一个**每秒 ERROR 速率限制器**,超限时发一条显式的 "N errors suppressed"。`MGLOG_E_ONCE` 的 latch 变成 per-server,这正是想要的行为。P9 的故障注入门:日志洪泛下注入一次 link 失败,那行 ERROR 必须出现。
|
||
|
||
### 7.5 唯一的新停顿类:server 发起的纹理重铸拉取(D-B6)
|
||
|
||
server 不保留纹素字节,所以三个原因会要求 client 重发已发过的 level:`RequireImageBindableStorage` 的 re-dirty(`Managers.cpp:2813`)、整格式再生(`:3950-4195`)、view 源重铸(`:3616-3707`)。**上一轮薄 server 设计正是因为把这条一笔带过而被判死。三条缓解同时上,加一个专门的门和一个必须发布的计数器:**
|
||
|
||
**(a) 预防主因。** client 给纹理打 `everImageBound` 标记,`resource_create`/`respecify` 一直携带 `imageBindableHint`(`MGPResourceDesc`),于是 image-bindable 存储在前期就分配好。这把 `RequireImageBindableStorage` 从稳态里彻底移除。
|
||
|
||
**(b) 拉取是异步的。** server 发 `on_texture_pull_request{res, target, levels[]}` 并把那个 twin **标为 not-ready**;client 在下一次 publish 时重发。因为 client 跑在前面,常见情况下字节在 server 到达采样该纹理的 draw 之前就到了;即使没到,**阻塞的是 `mgl-srv-apply` 线程,不是应用线程**——这是一个应用没有发起的停顿应该待的地方,也严格好过 monolith 里阻塞应用线程的行为。
|
||
|
||
**(c) 有上限的保留。** 可选的逐纹理保留位,受一个**显式的 LRU 字节预算**约束(默认 32MiB,`MOBILEGL_PIPE_TEXEL_RETAIN_MB`),使一张被拉过一次的纹理永不第二次停顿。
|
||
|
||
**(d) 门与计数器。** 新增 `TextureRemintPullScenario`:同时强制 `RequireImageBindableStorage` 与一次帧中格式再生。**拉取次数逐 trace 用例发布**,与 SSIM 并列。**本设计从不声称"零 round trip",它测量并公布。**
|
||
|
||
若在真实语料(MC 与 Iris fixture)上实测拉取率非平凡,(c) 从可选升级为强制并拿到真预算。
|
||
|
||
---
|
||
|
||
## 8. 传输、数据面、同步、present、线程、平台、构建
|
||
|
||
### 8.1 原样继承方案 A 的部分
|
||
|
||
以下全部**逐条继承 `PLAN.md`,本文不复述**。它们是传输/平台/节奏/构建机制,与状态模型无关,且已经过对抗性评审:
|
||
|
||
| `PLAN.md` 章节 | 内容 |
|
||
|---|---|
|
||
| **§6.1** | 段布局(`SEG_CMD` 8MiB / `SEG_STAGE` 32MiB↑ / `SEG_REPLY` 8MiB / `SEG_EVENT` 256KiB / `SEG_SHADOW[n]` / `SEG_ADOPT[n]`);shm 创建矩阵(`ASharedMemory_create` API 26 / `SYS_memfd_create` / `shm_open` / `CreateFileMappingW`);**`SCM_RIGHTS` 必须在第一个 transport commit 里实现**(asio 无 cmsg API → 在 `socket.native_handle()` 上裸 `sendmsg`/`recvmsg`,~80 行;`Feat/CS-Delta-IPC` 把 `out->fd = -1` 硬编码在 `LocalSocketTransport.cpp:296`,它的数据面在唯一重要的平台上一个字节都没过去);`SEG_SHADOW` 块的 pending free-list 退休规则 |
|
||
| **§6.2 / §6.2a** | `RingControl`:两组独立游标三元组、三个不同的 seq 水位(`appliedSeq`/`submittedSeq`/`retiredSeq` + `completedFrameSerial`/`presentAckSerial`)、`serverEpoch`、`ringGeneration`、`consumerParked`/`producerParked`、`eventRingFull`/`eventDropped`;**双向 doorbell**,`MOBILEGL_IPC_SPIN_US` 默认 50µs,`inproc` 用 condvar。没有这条,每一次 client 等待都退化成跨进程自旋一条共享 cache line,在手机上就是一颗大核满频空转,而本库全无亲和性控制 |
|
||
| **§6.3** | 记录格式:8B `RecHeader`、24B `BlobRef`、**无 per-record 序号**、X-macro 每种一条 `static_assert` **加**生成的运行期边界检查 → `Fatal{ProtocolCorruption}`、`kVarTail` 自描述长度自洽校验。方案 B 把这套机制扩展到**全部** MGPipe 调用(G3) |
|
||
| **§6.4** | WAR 纪律:调用时刻拷进 ring slot(P1-4);P4.5 的 `SEG_SHADOW` 零拷贝 + 逐 shadow 64KiB 块发送水位 |
|
||
| **§6.5** | ring 分配与背压:逐字移植 `PersistentRing`(`Managers.cpp:641-727`、`RingAllocateSlow` `:1891-1970`、`RingOnPresent` `:1975-2016`),升级路径 扩容 → 走 doorbell 的有界等待 → 硬 drain + `ringGeneration` bump |
|
||
| **§6.6 前三条** | unpack PBO 完全在 client 解析;压缩 internalformat 永不到达 backend;`glCopyTexSubImage*` 与 `glClearTexImage` 整体留在 client |
|
||
| **§6.7 第 2、5 行** | PBO 回读改 fire-and-forget(**严格优于 monolith**,后者无条件停等,`DirectGLES.cpp:9189-9205`);`glEndTransformFeedback` 的无限 `ClientWaitSync`(`GL_Drawing.cpp:1326-1337`)推迟到首次读 |
|
||
| **§6.8** | persistent map 与 ≥16MiB 采纳的三档,**由运行时 POST 探针选择,绝不硬编码驱动名** |
|
||
| **§7.1** | FlatBuffers 纪律:热路径 `struct` 进 ring、罕见/变长 `table` 走 socket;`protocol_generated.h` 提交;`gen_protocol.py` + CI `flatc-check`;**默认构建图里没有 `flatc`**(`Feat/CS-Delta-IPC` 的 NDK 陷阱本体) |
|
||
| **§7.2** | 帧封装(`MGLF`、64MiB 上限、读时校验、缓冲不足返回所需大小**且保留消息**);publish 触发器(每记录 release-store `cmdHead`、显式门铃点、`SEG_STAGE` 余量 < 1/4、**轮询入口也是门铃点**、`GL_SYNC_FLUSH_COMMANDS_BIT` 无条件 publish、`MOBILEGL_IPC_POLL_ESCALATE` 饥饿升级);**`glFinish`/`glFlush` 保持免费**(`Definitions.cpp:111-112`) |
|
||
| **§7.3** | 两个互相独立的窗口(字节 credit、present credit);server 不发 credit 消息,只做 release store + 反向门铃 |
|
||
| **§7.4** | 事件 ring + 排空点 + 溢出策略(等待循环内必须排空;语义事件无损,满时 `eventRingFull` + 停在记录边界停止 apply)。**加上 §7.4 的分级修正** |
|
||
| **§8 末尾** | fence 完成度必须来自**真的逐 fence 退休**,不是 present 水位(`DirectVulkan.cpp:1120-1128` 记录这是被修掉的 bug;`magma-mc1215-fence-oom` 记录它曾导致 native-heap OOM kill);三个应先独立落 `dev` 的 monolith 修复 |
|
||
| **§9 / §9.1 / §9.2 / §9.3** | `Present` 与 `eglSwapBuffers` 严格 1:1、绝不批量(Magma 的四次 `OnFrameBoundary`、`TryDrainFrameTransients` 与全部四次 `BeginFrame` 只在 `Present` 内发生,`VulkanRenderer.cpp:12765-12904`;Espryt 在那里 retire 三个 ring 与 `TrimBufferPool`,`DirectGLES.cpp:10646-10649`);`MOBILEGL_IPC_PRESENT_CREDIT` **默认 1** 与延迟叠加公式;Magma 从不注册 `SetSwapInterval`(`BackendObject_DirectVulkan.cpp:698`)所以 IPC credit 是它唯一的显式限帧器;DirectGLES 的非 present fence tick |
|
||
| **§10** | 线程模型:v1 不加 client 线程、flow = per context、外来线程的 sync/query 从 `RingControl` acquire load 回答、`ShaderCompilePool` 留 client、修 `EGLOperationMutex` 的两个漏洞;server 的 `mgl-srv-io` + `mgl-srv-apply`(终身持有原生 context);核心放置与 `MOBILEGL_IPC_SERVER_AFFINITY`、**报逐线程 CPU 时间**;拆机顺序 |
|
||
| **§11.1-§11.6** | 启动与握手(`MOBILEGL_IPC_SERVER_PATH` 为主 + `dladdr` 兜底;socketpair + fork/execve;**显式 envp 剔除 + `mobilegl_server_main` 内强制 Monolith** 的双保险);`extern "C" visibility("default")` 与 `nm -D` 门;Android 的 `android:process=":mgl"` Service 路径(minSdk 26 无公开 `ANativeWindow` 扁平化);X11 XID;`EGL_PLATFORM=surfaceless`;Windows overlapped named pipe;崩溃时的 device-lost latch |
|
||
| **§12 第 1-3 层 / §12.4** | 编译期折叠(`MOBILEGL_BUILD_DISAGGREGATED` 默认 OFF);**唯一 hook 点** `MG_Backend/Init.cpp:48-70`,下游 ~152 个边界调用点零 `#ifdef`;P4.5 的 allocator 改动整段包裹;`MOBILEGL_TRANSPORT` 复用全部既有开关通道(ctest `ENVIRONMENT`、trace-replay 的 `setenv` 块、FCL 用户 env、plugin APK V2 开关表、`/data/local/tmp` CTS) |
|
||
| **§13** | 目录形状;`MobileGLServer` 作为改名成 `lib*.so` 的 `add_executable` 链接**共享**的 `MobileGL`(一份库两个角色,版本必然匹配);FlatBuffers submodule 的 `EXISTS` 保护 **加** include-dir guard;ctest/trace-replay 的三个陷阱(`ENVIRONMENT` 是替换而非追加、`;` 转义、property 覆盖 job env);`SPLIT` 后缀与 `-DTRACE_TRANSPORT=`;CI 的 `flatc-check` 与 `fprintf` grep 门 |
|
||
| **§14** | 对 `Feat/CS-Delta-IPC` 的 REUSE / CHANGE / DROP 判定(本文 §14 只列差异) |
|
||
| **§15 P0** | 卫生清单与两个 spike,尤其是**两侧的 `TracyPlot` 字节计数器** |
|
||
|
||
### 8.2 与方案 A 的差异
|
||
|
||
**删除:**
|
||
`Server/ReplicaContext.{h,cpp}`(换成 `Server/PipeObjectTables.{h,cpp}`:POD 记录的 slot 数组,不是状态模型);§5.0 的"replica vs 重写"决策;§5.1 的三步发射协议;§5.2;§5.4 的 replica 对象表规则与 `Fatal{IdentityDivergence}`;§5.6a 的纹理 ack 协议;§5.7 的 Phase 1-4 composite 分支;§5.9b 与整个 mutation 覆盖生成器(`gen_impl_mutation_surface.py`、`MutationCoverage.def`、`ImplMutationSurface.inc`、`MG_Remote::Shared::` helper 族);§6.9 的 relink 档与 `MOBILEGL_IPC_PROGRAM=publish|relink`;§6.4 的拷贝第 (3) 行;§12.2 的 `pGLContext` shim;阶段 **P5**(6 天回收);风险 **R1** 与 **R6**;开放问题 **§17-5**。
|
||
|
||
**改变:**
|
||
|
||
| `PLAN.md` § | 差异 |
|
||
|---|---|
|
||
| §5.9a | READ 面的**编目**生成器变成**禁止**门(§4.7.2):不是"每个读都映射到某个 delta 类",而是"这次读根本编译不过"。原 477 行 inventory 保留为 tracker 侧覆盖检查表(G6) |
|
||
| §6.4 拷贝账 | 第 (3) 行 `SEG_STAGE → replica shadow` 不存在:**P1-4 = 3 次,P4.5 = 2 次**。`PLAN.md` 自己的"方案 B"目标(第 549 行,被它标为"激进、需额外设计"并推到 P6)**按结构达成**,开放问题 §17-5 自动关闭 |
|
||
| §6.6 第 4 条 | 逐 level `serverAuthoritative` 位被 dirty 归属反转(§7.3)+ `on_texture_writeback`(一个生产者:CPU 生成 mip 路径;CopyImage 镜像已搬到 client 而消失)+ `on_texture_pull_request` 取代 |
|
||
| §6.9 | `RecProgramLinkOp` **不可能**(§5.7)。`ProgramPublish` 第一天;`reflectionDigest` 从"分歧预言机"换成"schema 完整性绊线";P5 消失,其副产品(内部 shader 烘焙)升级为 Magma 阶段的前置条件,`nm -D | grep glslang` 为空成为验收判据 |
|
||
| §5.10 | 第 2、3 条**逐字继承**(client 侧块粒度 persistent 推送、`PersistentCoherentMapScenario` 作为门,**R2 仍是最高优先级正确性项**)。第 1 条从"完整 map 状态复制"缩成**一个推送的 `hasLiveHostWrites` 位**——因为 replica 的 `SyncPersistentMappedRange` 与 `IsBufferDrawClean` 的 `IsMapped()` 门(`Managers.cpp:1447`)在 server 侧都不存在 |
|
||
| §6.10 | 四类应用指针由 §5.8 的 cap 门控 lowering 处理;`ClientArrayBounds` 变成 `MGPDrawInfo::minIndex/maxIndex`。**陈旧索引纪律(§5.8.1)一字不改地保留并加门** |
|
||
| §7.4 | `on_log` **按严重级分级**(§7.4),加每秒 ERROR 速率限制器 |
|
||
| §12.2 | 需要角色隔离的进程全局从 **4 个降到 2 个**:`MG_State::pGLContext` 整个退出(server 不托管 `GLContext`,因此跨越 `MG_Impl` 1494 个站点的 `operator->` shim 与 Android dlopen TLS 论证一起消失);`pDefaultFramebufferInfo` 变成接口输出(§7.2)后也退出。只剩 `gBackendFunctionsTable`(在方案 B 里是 `gPipeCtx`)与 `pActiveBackendObject`。**这实质性降低 R11 的风险,并让 `inproc`——最早的证伪门、且完全不需要跨进程工作——便宜到可以提前跑** |
|
||
| §13 | `MG_Pipe/` 是**默认构建里的非可选目录**(它是 monolith 的架构);只有 `MG_Remote/` 在 `MOBILEGL_BUILD_DISAGGREGATED` 之后 |
|
||
| §15 | **在 P1a 之前新增一整段 backend 推送改造**。`PLAN.md` 把这块工作定价为"~0 逻辑改动";在方案 B 里它是工作量主体,并有它自己的 monolith A/B(§10.3) |
|
||
|
||
**新增:**
|
||
|
||
- **`SEG_STAGE` 尺寸必须额外容纳四类它以前不承载的字节**:client 顶点数组、client 索引数组、multi-draw 参数块、client 解析后的 indirect 命令块。**字节体量不变**(它们今天就在每 draw 被重新上传,`glBufferData(..., GL_STREAM_DRAW)`,`Managers.cpp:~2578`),但现在这些字节要过一个 ring slot。上限由 P0 落地的计数器实测定,不用默认值猜。
|
||
- **新事件种类**:`on_texture_writeback`(CopyImage 镜像搬走后只剩一个生产者:CPU 生成 mip 路径 `DirectGLES.cpp:6811-6861`)、`on_texture_pull_request`、`on_mip_levels_generated`。`on_buffer_writeback` 从"优化"升级为**承载语义**。
|
||
- **新环境变量**:`MOBILEGL_PIPE_PUSH`、`MOBILEGL_PIPE_VERIFY`、`MOBILEGL_PIPE_STATS`、`MOBILEGL_PIPE_TEXEL_RETAIN_MB`(见附录)。
|
||
- **`RenderbufferObject::GetLifetimeId()`**(今天没有)。**但不需要 `GetVersion()`**——`PLAN.md` P0 要求两个,`GetVersion()` 只是 replica 的 delta 触发器;在推送模型里 `glRenderbufferStorage*` **本身就是**一次 pipe 调用,不需要触发器。
|
||
|
||
### 8.3 persistent map:唯一被显式隔离的传输相关决策
|
||
|
||
`AcquirePersistentMap`(`BufferObject.h:112`)是**永久的地址空间捐赠**(D4/D-B4)。**它原样穿过 monolith 改造(P0..P13 一动不动),只有 IPC 那一步才打破它。** 决策路径:
|
||
|
||
- **P0 的 spike B 在第一周给方向**:导出 `HOST_VISIBLE|HOST_COHERENT` VkBuffer 的 fd,client `mmap` 后回读,在 `35d0befa`(Adreno 830)与 `3B159D009VZ00000`(Mali)两台上跑。
|
||
- **T2(拒绝,永久正确的回退)**:返回 `nullptr`,前端已在三处容忍(`BufferObject.cpp:174, 439-442, 470-472`)。**此档下 `PLAN.md` §5.10 的 client 侧块粒度推送是强制的**,由 `PersistentCoherentMapScenario` 把门。
|
||
- **T1(server 导出自己的映射)**:每 store 生命周期一次 round trip;采纳成功后推送自动停止(`IsGpuResident()` 早退),与 monolith 一致。
|
||
- **T0(server 导入 client 分配)**:理想但可用性未知。
|
||
|
||
若两台设备都否,IPC 期的该阶段从 8 天缩为 2 天的文档与负面对照,代价记录在案。**绝不允许一个平台未知数挡住 200 天的接口工作。**
|
||
|
||
---
|
||
|
||
## 9. Roundtrip 清单与稳态零 roundtrip 论证
|
||
|
||
### 9.1 稳态零 roundtrip 的项
|
||
|
||
| 类 | roundtrip | 依据 |
|
||
|---|---|---|
|
||
| 全部 draw、clear、blit、copy、dispatch、barrier、XFB 跨度标记、全部 bind、全部 CSO create/bind、全部 `set_*`、全部 buffer/texture 上传、`present` | **0** | 单向记录;present 只查 credit |
|
||
| **全部 89 个 caps 站点**(40 `pActiveBackendObject->` + 全部 `glGetIntegeri_v` + `IsTimerQuerySupported` + `PrefersCpuXfbPrimitiveAccounting` + `BeginOcclusionQuery != nullptr` 能力探测) | **0** | 首次 `MakeEGLCurrent` 的一次 `MGPCaps` 快照。caps 只在那一刻才存在(`BackendObject.cpp:341-347`,每次 surface 变更重新武装 `:301`);`callMask` 精确复现 DirectVulkan 少注册的槽位 |
|
||
| `glGetError` / `glFinish` / `glFlush` | **0** | 前者永远本地(`GL_Getter.cpp:2811-2817`;不变式 `Core.cpp:48-49`),后两者是彻底的 no-op(`Definitions.cpp:111-112`)**且必须继续免费** |
|
||
| fence 与 query 的**创建**,以及每一次**非阻塞轮询**(`GetSyncStatus`、`ClientWaitSync(0)`、`IsQueryResultAvailable`、`GetQueryResult64(wait=false)`) | **0** | handle 由 client 铸造(`GL_Sync.cpp:61` 今天就把前端堆对象 reinterpret 成 `GLsync`;`GL_Query.cpp:50-54` 铸造 id);未命中合法地答 `GL_UNSIGNALED`/"未就绪",两处契约明说(`BackendObject.h:210-214`、`:236-241`),而前端已经遵守(`GL_Query.cpp:302-311`:读 0、**不缓存**、保留 handle) |
|
||
| `glGetTexImage` / `glGetTextureImage`(**DirectGLES**) | **0** | client shadow 回答(`CopyTextureImageToClientOrPBO_State`,`GL_Texture.cpp:5368-5420`,取用点 `:6460`) |
|
||
| `glReadPixels` → pack PBO | **0** | fire-and-forget + client 侧 `MarkGpuWritten`。**严格优于 monolith**,后者无条件停等(`DirectGLES.cpp:9189-9205`) |
|
||
| `glEndTransformFeedback` | **0** | 取消无限 fence 等待(`GL_Drawing.cpp:1326-1337`),改为对 capture target 置 `MarkGpuWritten` |
|
||
| `eglSwapBuffers` | **0 次阻塞 round trip**,一次非阻塞 credit 检查 | 只有 `presentsSent - presentAckSerial >= MOBILEGL_IPC_PRESENT_CREDIT`(默认 1)时才阻塞 |
|
||
|
||
### 9.2 不可避免的阻塞点(全部罕见,逐条给理由与缓解)
|
||
|
||
| # | 站点 | 为什么不可避免 | 缓解 |
|
||
|---|---|---|---|
|
||
| 1 | 握手 `Hello`/`Welcome` + 段 fd 传递 | — | 一次 |
|
||
| 2 | `InitializeEGLDisplay`(写 `major`/`minor`)、`Create/Resize EGL*Surface`、首次 `MakeEGLCurrent` + `InitCapabilities` | 出参 / 返回 `Bool`;caps 只在那一刻存在 | 每 surface 至多一次;surface 回复顺带 `MGPSurfaceInfo`(**同时删掉 `SwapchainObject.cpp:276-330` 往前端对象里的写**)。`SwapEGLBuffers` 不需要回复:它的线程归属校验对 client 镜像的 EGL 状态求值(`BackendObject.cpp:365-393`) |
|
||
| 3 | `glReadPixels` → 客户内存 | GL 要求返回时字节已就位 | 像素进 `SEG_REPLY` slot;**逐行写回循环留在 server 内,按操作级批成一段** |
|
||
| 4 | `glGetTexImage`/`glGetTextureImage`(**DirectVulkan**) | Magma 对只存在于 GPU 的 level 没有 client 可答的 shadow | `get_texture_image` 对"无 GPU 背书"的 level 返回"请从你的 shadow 回答"(`VulkanRenderer.cpp:10691-10704` 今天就在测这个条件),所以只有真正 GPU-only 的 level 付钱 |
|
||
| 5 | GPU-write pending 的 buffer 首次 CPU 读(`glMapBuffer*`、`glGetBufferSubData`、`glGetNamedBufferSubData`、`glCopyBufferSubData` 源、`FillSubData`) | shader 在前端背后写了 store | monolith 里**本来就阻塞**(`glFinish()`,`Managers.cpp:1246`;Vulkan `VkBufferManager.cpp:80-85` → `VulkanRenderer.cpp:9807-9817`)。client 保守 pending 集触发,由 `set_shader_buffers` 的 `writableMask` 与 `on_gpu_written{ranges}` 两侧收窄——**比今天"任何绑了 SSBO 的 draw 一律置位"更精确** |
|
||
| 6 | `glClientWaitSync(timeout>0)`、`glGetQueryObject*(GL_QUERY_RESULT)` 未完成、`glBeginConditionalRender` | GL 定义即阻塞;`glBeginConditionalRender` 连 `_NO_WAIT` 模式也阻塞(`GL_Query.cpp:705-706`) | 全部非阻塞兄弟是 0 round trip(§9.1)。条件渲染的谓词**只解析一次**(`Core.h:387-391`),之后每个条件 draw 在 client 侧丢弃(`ConditionalRenderDiscardsCommand`,用于 `GL_Drawing.cpp:711, 740, 795`),**server 永远不需要那个 query 对象** |
|
||
| 7 | 分配类入口的错误 ack | OOM 探测惯用法 | `kNeedsAck` 只标这一族;罕见且本来就贵 |
|
||
| 8 | `map_persistent`(仅 T1 档) | 应用必须拿到一个不再经过任何 API 调用就能写的地址 | **每 store 生命周期一次**,不是每次使用;档位由 POST 探针选(§8.3) |
|
||
| 9 | **server 发起的纹理重铸拉取** | server 不保留纹素 | **三条缓解 + 专门的门 + 逐用例发布的计数器**(§7.5)。异步形态下阻塞的是 `mgl-srv-apply` 而非应用线程 |
|
||
| 10 | client 侧索引扫描 / restart 重写 / indirect-count 解析,当源 buffer 在 pending 集里 | monolith 在**同一位置**调 `SyncGpuWrites()` | §5.8.1 的 publish/wait/drain 纪律;常见情况不 pending,代价为零 |
|
||
| 11 | ring/stage 耗尽、present credit | **节奏,非语义** | `PersistentRing` 的升级路径 + `producerParked` doorbell(不是自旋) |
|
||
|
||
### 9.3 论证的形式:测量,不是声称
|
||
|
||
**验收门措辞(`PLAN.md` §15 P3-2 的措辞加一行)**:在**全部 40 个 trace 用例**上发布**逐用例的 roundtrip 计数器与纹理拉取计数器**。**不做笼统的"零 round trip"声明。** 条件渲染与阻塞 query 的次数按用例列出而不是宣称为零。
|
||
|
||
轮询挂死的防护(继承 `PLAN.md` §7.2/R4)必须有它自己的门:`glFenceSync(); while (glClientWaitSync(s, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {}` 必须在有界时间内退出——不加 publish 规则它会永远挂住。
|
||
|
||
---
|
||
|
||
## 10. Monolith 保留
|
||
|
||
### 10.1 接口在进程内就是直调
|
||
|
||
monolith 模式下 `MGPipeContext` 用 backend 自己的函数填充,`MGPipeCallbacks` 用对 `MG_State` 的直调填充,`MGHostSpan.ptr` 指向 client 自己的 shadow(**零新增拷贝**),`MGPipeHandle` 按值走一对寄存器。split 模式下同一张表换成发射器,applier 反序列化后调**同一批 backend 函数**。**全世界只有一份 backend 实现。** 这比方案 A 的安排更强——后者两侧都跑 `MG_Impl`。
|
||
|
||
### 10.2 热路径的间接成本,诚实版
|
||
|
||
| | 今天 | 之后 |
|
||
|---|---|---|
|
||
| 每 verb 的分发 | 1 次间接调用(**已经在付**:`gBackendFunctionsTable.GL.DrawArrays(...)`,89 个 `MG_Impl` 站点) | 1 次间接调用 |
|
||
| 每 draw 的状态获取 | Espryt 124 / Magma 169 次 accessor 调用 + 版本比较 + 一次 ~1.2KB 三段 memcmp + `CurrentUnitBindingsEpoch` 的逐 unit owner 走查 + Magma 的两次有损版本求和 + ~40 次 payload accessor 走查 | 1 次 dirty word 测试 + N 次 `set_*` 间接调用(稳态 N=1-3;CSO 内容寻址下重复状态 N=0) |
|
||
| memo 查表 | 对指针位做斐波那契散列的直接映射探测 + owner 相等性 | 按 slot 的数组下标 |
|
||
| 删除的机制 | — | **~550 行 per-draw 失效发现**(§2.5) |
|
||
|
||
**推送在稳态应当是严格的减少——但只因为 tracker 在 draw 时刻 validate。** 如果有人把它实现在 GL setter 时刻,它会更慢,而 trace 语料会立刻说出来(§5.1)。
|
||
|
||
两个诚实的告诫:
|
||
1. **可达性遍历是搬走了,不是消失了**,所以头号指标必须是**逐线程 CPU 时间**,不是墙钟帧时。
|
||
2. **Magma 的 `SetupDrawSnapshot` 快路径命中率在两种模式下会合法地不同**,所以 A/B 比的是**渲染输出与计数器**,永远不是 memo 轨迹。
|
||
|
||
两个 backend 编进同一个共享库(`CMakeLists.txt:356-383` 的源文件列表,`add_library(... SHARED ...)` 在 `:485`),backend 在 init 时锁存一次(`ConfigLoader.cpp:212-225`),所以去虚化在两种形态下都不可得,也都不需要——代价是一次已经在付的间接调用。**函数指针 struct 而非虚基类**的理由见 §4.1。
|
||
|
||
### 10.3 替代字节一致门的五部分验证门
|
||
|
||
**先把成本写在明面上**:`PLAN.md` §12 第 4 层(`nm --defined-only` + 剥调试信息后 `.text` size **相等**,且每阶段都跑)在方案 B 里**按构造死亡**——backend 停止读 `pGLContext`、`PrepareForDraw` 的遍历搬到 client、~66 个 memo 族换键、`MG_Impl` 多出 validate 调用、Magma 的两处 `MG_State` 类型内部用法被重写。**不存在任何配置能让旧字节回来。** 这是方案 B 的代价,必须写进设计文档而不是藏起来。
|
||
|
||
替代品是**五部分,全部在 P0/P1 落地**,这样每一条都能因它存在的理由变红:
|
||
|
||
**① 接口纯度门(机械,取代读点编目生成器)。**
|
||
`grep -c 'pGLContext' MG_Backend/` == **0**(grep 的是 `pGLContext` 不是 `pGLContext->`,因为还有 58 行非箭头用法,§2.4);`MG_Backend` 只允许 include 一张共享**值**头白名单(今天是 50 行 include / 18 个不同 `MG_State` 头);`nm --undefined-only libMobileGLServer.so | grep -E 'MG_State::GLState::|glslang'` 为空;**外加**一条 debug 断言"每个 backend memo 键都是 `{slot, gen}` 对,永不是裸前端指针",由 `HandleRecycleScenario`(删除并重建对象、跨 draw 边界复用同一个 GL name)支撑——**这个场景在 0d 重键之前必须在至少一个 backend 上是红的**,否则它没在测任何东西。
|
||
|
||
**② 语义影子比对(`MOBILEGL_PIPE_VERIFY=1`)——决定性的那一条。**
|
||
阶段 B 期间两套状态模型活在同一个地址空间:tracker 再用 `SnapshotFromGLContext()` 填一份 `PipeInputs`,G4 生成的比对器**逐字段**、**每 draw** 与推送版本比对,打印第一个分歧字段名与 draw 序号。它抓三种事:(a) tracker 忘了推的字段;(b) **dirty 位触发得太少**——危险的那个方向;(c) 两条路径上被变换得不一样的值。作为第三种 CI 模式跑全部 40 个 trace 与 367 个集成测试;~5-10× 慢,永不出货。
|
||
**必须逐字段比而不是 `memcmp`**:`DirectGLES.cpp:2029-2033` 明确记录 `RenderStateParameters` 的 memcmp 会因 padding 字节 false-DIFFER(无害)但永不 false-match——比对器要的是零误报。
|
||
**这是历次评审在每一个候选设计上指出的那个缺失,而它只有在接口先落 monolith 时才存在。**
|
||
|
||
**③ 行为 A/B。**
|
||
全部 ~40 个 trace 用例(`tools/trace_replay/trace_cases.json`,默认 SSIM 阈值 0.99;OpenRA、MC 1.17/1.21.4/1.21.11 族、15+ Iris shader pack、两个 Create fixture、`improved-transparency-minecraft-26.3`)在 `{monolith-pull, monolith-push, split}` 三种下同一判定、SSIM ≥ 0.99;`ctest -L integration-gpu` 在 `DirectGLES.` 与 `DirectGLES.Pipe.`/`DirectGLES.Split.`(以及 DirectVulkan 对)之间产生**逐名相同**的通过/失败集(367 个 `TEST_F`);428 个单元测试全绿;CTS 逐后端 conformance 在 0.5 个百分点内,按本项目的逐后端表格式上报(行 = GL 版本/扩展,列 = 状态计数,rate = Pass/(Pass+Fail),NS 不进分母)。
|
||
**两个 Create fixture 带 `coherent_as_flush: true`**,必须在两种模式下都开着该开关跑,否则逐名对比没有意义。
|
||
|
||
**④ monolith 性能不回归。**
|
||
两台设备(`35d0befa` Adreno 830、`3B159D009VZ00000` Mali),reboot-clean、同热窗口、配对 A/B,用 `tools/bench.sh` + trace replay 的 `--benchmark --benchmark-tail-frames --benchmark-result` 逐帧 JSON。**指标是逐线程 CPU 时间**(因为遍历是搬走不是消失),monolith-push 在 **p50 与 p99** 上都要落在 monolith-pull 的噪声内。CPU 定频按本项目协议(大核 1.96 / 小核 1.55GHz,GPU 拉满,40°C 门槛)。
|
||
|
||
**⑤ 覆盖 + poison + handle 纪律。**
|
||
`gen_pipe.py` 重生成 477 行 inventory 的 MGPipe 映射列,0 UNMAPPED,`git diff --exit-code`;`PipeInputs::m_filledMask` poison(§6.2.2);P13 的 `static_assert(sizeof(ResidualValueBlock) == 0)`;`ResidualValueBlock` 的逐成员 `offsetof` 断言(§6.3-2)。
|
||
|
||
**两条字节级等式仍然幸存,仍然作为断言保留**:`MOBILEGL_BUILD_DISAGGREGATED=OFF` 时 `nm --defined-only libMobileGL.so | grep MG_Remote` 为空且链接行不增加任何库;`nm -D libMobileGL.so | grep mobilegl_server_main` 在 RelWithDebInfo 里命中(`CMakeLists.txt:497-510` 在非 Debug 下设 hidden visibility)。
|
||
**符号与 `.text` 漂移每阶段作为信息性指标发布**——一次无法解释的跳变仍然是一个 smell,只是不再是一条断言。
|
||
|
||
### 10.4 monolith 侧净收益清单(即使 IPC 永不上线也成立)
|
||
|
||
1. **~550 行 per-draw 失效发现机制删除**(§2.5),连同运行它的每帧代价。
|
||
2. **复用地址 ABA 一整类不可表达**:D1/D2/D3/D10/D11/D13/D14/D16/D17/D20 全部由 `{slot, gen}` 关闭。
|
||
3. **FBO → program 排序 hazard 消失**:`DirectGLES.cpp:2712-2732` 的 fragColor 重推导 workaround 与 `g_broadcastMemo*` 删除(D-B3)。
|
||
4. **一处分层倒置消失**:`SwapchainObject.cpp:276-330` 不再往 `MG_Impl` 的 `pDefaultFramebufferInfo` 里写。
|
||
5. **两个潜伏 bug 顺带修掉**:D21(`m_xfbCounterSlotByObject` 用裸 GL name 做键,`VulkanRenderer.cpp:11136-11146`,删了又建的 XFB 对象会**恢复**一次本该重启的捕获)与 `RenderbufferObject` 缺 `GetLifetimeId()`。**这两条都值得先独立落 `dev`。**
|
||
6. **一个死能力被暴露**:`CapabilityInput::FramebufferSrgb` 与 `DepthClamp`(`RenderState.h:165, 168`)**没有任何存储**——`SetCapability` 落到 `default: // not supported currently`(`RenderState.cpp:380`),`IsCapabilityEnabled` 返回 `false`(`:428-429`)。**六个 backend 读点(`DirectGLES.cpp:2133`;`VkTextureManager.cpp:951`;`VkRenderPassManager.cpp:613, 965, 1111`;`VkClearManager.cpp:57`)今天恒为 false。** 把接口写下来这件事**发现了**它。这是潜伏 bug 还是有意为之,**必须在渲染状态 blob 冻结之前回答**。
|
||
7. **一次 glslang 编译离开 monolith 启动路径**(Magma 的内部 shader 烘焙,§6.1-2)。
|
||
8. **`inproc` = monolith 的渲染线程**,且在方案 B 下只需隔离两个进程全局而不是四个——这是本项目手上最大的单一 CPU 杠杆,且完全不需要 shm 或平台工作。
|
||
9. **`MG_Test` 的 mock backend 顺理成章变成 MGPipe recorder**,`tools/trace_replay` 可获得一种比 apitrace 精确得多的 MGPipe 级录制格式(它记录的是**已解析**的状态,重放确定性更强)。
|