[docs] (P6.5): the design for the first wave - all-TCP transport, WSL client to adb-device server

ROADMAP gains the section "the immediate next item": a design, not an
implementation, for running the frontend on an x86_64 WSL process and the
backend on the Redmi over TCP, with the integration lane and the CI retrace
subset as the acceptance. The P6.5 row now points at it.

What the design rests on, verified in the tree: the control plane already
treats the rendezvous as a NAME with two ordered connections (SocketTransport),
ClientSession::StartOverSocket already separates "how the socket came" from the
handshake, MGPBlobRef addresses kSegStage only through one ResolveOrFatal, the
reply header and the event heads are already wire-shaped, LogLine exists in
CtrlMsg with no producer, goldens are shared between desktop and device, and
the trace APK can already exec libMobileGLServer.so from nativeLibraryDir.

What it does not rest on, stated rather than assumed: ILink is declared and
unused beyond LinkProgress, ShmLink does not exist, both sessions hold
SessionSegments directly - the lk seam body from the end-state review was never
built and is this wave's first package (lk2). The manifest has no INTERNET
permission.

The mechanism: StreamLink as mirrored private segments replicated by messages -
same sizes both ends, same offsets, so the codec, the watermark rules, the apply
thread and the encoder do not change; RingControl becomes Progress frames sent
on flush-on-idle (the recorded amendment to "appliedSeq is never lazy": late by
at most one flush, never early); replies become seq-stamped messages; the event
ring is mirrored with the P5e blocking rule intact. Cross-architecture makes
the PipeFields.def layout digest mandatory (wf). Server logs on another machine
make LogLine forwarding mandatory (lf) so the 34 server-log readers stay
unchanged. A --serve supervisor forks one child per session on the device,
started by a foreground Service in the trace APK.

Packages lk2 -> wf -> ct -> sl -> lf -> sv -> t65, each with red-once; the gate
list, the mandatory numbers (kWaitReply per frame x RTT, SEG_STAGE bytes/frame
against the link, PRESENT_CREDIT 1..3), the two-sided configuration runbook and
the risks (RTT per reply row, single large uploads, AP isolation) are in the
section. check_doc_citations: 74 citations, 0 problems.
This commit is contained in:
2026-09-22 03:04:50 -04:00
parent 872370ba9d
commit 4e7bf0fe65
+110 -1
View File
@@ -35,7 +35,7 @@
| **P5e** 退役 Espryt draw path 的 lockstep | ✅ **已收官(2026-09-19)**,附一条具名未决:E1 对照需按 ID-114 重新定义 | 契约 `MG_Remote/CONTRACT-P5E.md`(规则 F:**未设障**记录的 apply 不得读任何 client 内存;ID-84 限定它只约束未设障记录);裁定 **ID-80..136**(`~/w7/notes/p5e/INTEGRATOR-DECISIONS-P5E.md`)。**十二个包,三波**:wave 1 **c0e**(契约 + 线上行:`PipeCalls.def` 第五列 `WaitClass`、`set_program_bindings` opcode 80、`kCapRunAheadApply` 位 10 仅 DirectGLES 臂、`kDrawClientArrays`、子系统位 13、两个旋钮)→ **id**(registry 按 `{slot, gen}` 重键、by-handle resolver、分配器守卫);wave 2 **vi / sb / pg / tx2 / fb** 五个 per-draw 家族 ∥ **ra**(等待规则、present credit、`gPipeInputs` 变 server 角色内存),加 **fix1**(设备发现的 monolith 臂回归,ID-107);wave 3 **pa**(program 家族离开前端)、**mv**(multi-draw + 五个档位的门禁条目)、**gl**(门禁机制本身 + 两处潜伏崩溃)、**ra2**(翻开关后暴露的竞态)。**`kMGPipeP5eRunAheadReady` 与 `kMGPipeP5eClientWaitRuleLanded` 均已翻**。最大的那次迁移(逐 draw 的 `GetProgramForDraw`)**没有改动任何线上结构**:记录早就带着逐 link 的 `ProgramArchive`,缺的是仍去问前端对象的读者。报告 [`P5E-RUNAHEAD.md`](P5E-RUNAHEAD.md) | 出口门已逐条跑过(BRIEF-P5E §3,2026-09-19 在集成树上,run-ahead 已武装):**item 1** unit 在 strict 两臂均 **2256/2256**;**item 2/3** `integration-split` **179/179**、逐条普查零 `Fatal{`、`Admitted{` 恰为八行;**item 4(a)** `RUN_AHEAD=0` 作为配对对照 **179/179 绿**;**item 4(b)** `VERB_BARRIER=0` **变红**(车道 62/179);**item 4(c)** Magma 逐字记下 `run-ahead requested, server does not publish kCapRunAheadApply - running lockstep`,而 Espryt 对照记下 `run-ahead ARMED`;**item 5** 在集成树上重跑 red-once(ID-121 重排后的形式):倒掉 pa 的臂得 `GetProgramForDraw@DrawArrays`、倒掉 mv 的臂得 `GetBoundVertexArray@DrawArrays`,**恰好各一个具名对**,还原后各自转绿;**item 7** `MOBILEGL_IPC_AUDIT=1` **179/179**。**唯一未过的是 E1 对照脚本本身**(ID-122):已修掉它两个遮蔽性缺陷(选中了一个在本车道永远跑不起来的阳性对照用例;ctest 与日志助手对“谁被选中”意见不一致),但它索要的 `Fatal{BarrierViolation}` 在整次运行里 **出现 0 次**:`BATCH_WAITS` 默认值 1 下那个重叠不发生,而 run-ahead 武装后 `VERB_BARRIER=0` 还会顺手关掉 run-ahead(ID-114),它产生的那条臂**既不是 lockstep 也不是 run-ahead**。E1 需要重新定义它要证伪什么,不是再调阈值;在想清楚之前不动它的计数规则,否则只是换一种伪绿。G1 仍由 CI 断言(ID-123) | P5d |
| **P5f** 一切状态上 wire | ✅ **已收官(2026-09-20)**;验收代码 Windows `c42577a4` / WSL `cfca93c7885f` | f0 普查、f1 双块、fc 控制帧、fe Espryt 字段、fm Magma record consumers、fs 静态世代/角色及 liveness、fr registry、fv 反向回调均已合并。字段归属 **41 RECORD_SUPPLIED / 6 APPLIER_DERIVED / 0 BARRIER_PULLED / 16 FATAL**;strict / dualblock 预期 marker 集合均空。控制帧与静态量工作已完成,不再列为 P6 待实现项 | unit **2310 = 2300 PASS + 10 skip**;GPU **1377** 零失败;ordinary / strict split **180 = 176 PASS + 4 skip**;双块 **212 = 206 PASS + 6 精确具名 skip**;双后端 RSP 门 **2 PASS / 0 skip**、逐帧 rsp=0。G1 `.text` 10806051、symbols 27815 恒等且 0/0/0/0;G2 3016 同名;G14 3646→3744 无删名。逐包与收官修复均有 red-once;Claude 异族终审 P1/P2 已修;Redmi 六 clean-boot 臂 **60 PASS / 6 skip / 0 failed**、36 个窗口 rsp=0。详见 [`close-report`](notes/p5f/close-report.md) |
| **P6** spawn transport | ⏳ **全部包已落地(2026-09-21,头 `16bfab10`);出口门 1–6 达成,7/8 待采**(真机配对 A/B + 三个性能数)。收官前另欠(2026-09-22 核出):server 进程侧 `PipeStats` 从未初始化(`srv=0 srvpark=0` 是结构性零,门 8 无 server 半边;修复进行中)、184 符号棘轮 CI 门(契约 §12.1);契约 §2 分给 hs 的 `LinkTerms` / `Refuse` / 指纹拆分**未落地**,移交 P6.5(负控 S6 随之) | 十包全落地:**a6** 进程装配/链接边界审计 → **c6** 规范契约 + 数据面缝惰声明 → **lk** `RingControl` 按写者重组 → **so** `SocketTransport`(AF_UNIX SOCK_STREAM + SCM_RIGHTS,12 条契约用例)→ **sm** 两个独立启动的进程(非 fork,name 汇合、共享匿名段,四守卫接线)→ **cp** EGL 控制帧跨进程并应答(`InitCapabilities` wire kind 11)→ **hs** build stamp 显式 present 入指纹 + `abiMajor/Minor` 补检 + 真实 pid → **dl** device-lost 闩(取自描述符挂断绝不取自超时)+ `Session::Fail` 漏斗(`FatalFamilies.def` 词表 + 90 站点归一 + `SessionFault` 帧命名家族)+ Fatal 普查门 → **st** fb-slot memo 具名 Fatal → **t6** `integration-spawn` 车道(102/102)+ 名集合结构性一致门 + per-role 日志(`<base>.client.log` / `<base>.server.log`)。Android CI 新增 `spawn-acceptance` 臂,App 从 `nativeLibraryDir` 解析 server 路径。真机 Redmi `2f7cbe2e` 上 `OpenRA.DirectGLES` spawn ssim=1.0 连跑三次 + 6 case 绿;`DirectVulkan` 真机分离路径不确定分歧(inproc 与 spawn 同表现→ Magma × 真实 GPU,非 P6,桌面 lavapipe 为 1.0)。边界仍为 pbuffer / surfaceless / 离屏;真窗口归 P12。契约 [`MG_Remote/CONTRACT-P6.md`](../../MobileGL/MG_Remote/CONTRACT-P6.md);a6 产出 [`a6-audit-v1.md`](notes/p6/a6-audit-v1.md)、[`a6-link-experiment.md`](notes/p6/a6-link-experiment.md)(**server 不能不链 `MG_Impl`**:184 符号,P7 101 / P3b-P4b 14 / 共有 60 / P6 自己 6);终局重审 [`P6-ENDSTATE-REVIEW.md`](P6-ENDSTATE-REVIEW.md) | 出口门(契约 §9)1–6 ✅:G1 pull 0/0/0/0、`.text` 恒 `0xa52203`;`integration-spawn` 与 split 名集合一致(可比 102==102)且同数绿;进程树运行时恰多一子进程、结束为零;每条 spawn 条目记子进程 pid 与 `transport=spawn`(`--require-spawn` 门);每包 red-once。**7/8 待采**:真机 reboot-clean 同热窗口配对 A/B、三个强制性能数(socket 门铃 vs condvar、`SEG_STAGE` 字节/帧、chunking 后记录/帧)尚未正式记录 |
| **P6.5** 传输栈两轴化 + 跨机 | 待排,**IPC 跑道关键路径**(2026-09-22 定) | 包:**wf** wire 定宽与布局摘要——`wireFingerprint` = `PipeFields.def` 派生的逐成员 name / `offsetof` / `sizeof` 摘要 + 有序目录摘要 + 记录头布局 + 两个 blob codec 版本 + `kOpCount` + 字节序 / 指针宽度,**保留** `sizeof(MGPCaps)` / `sizeof(DynamicBackendParameters)`(wire 事实)、删 `sizeof(GLFunctionsTable)`;`buildFingerprint` 只在 `Dial == Fork` 下比较;`DynamicBackendParameters` / `MGPCaps` / `RenderStateParameters` 整块 blob 的定宽化(两端是不同编译器产的不同二进制,`long` / padding / 位域差异都是静默读错状态);`Refuse{code, detail, 对端值}` 控制消息,握手路径零 abort → **ct** 控制面端点种类(`fork` 继承 fd / `unix:<path>` / `tcp://host:port`;`DialMode{No, Fork, Connect}` 与 `MGPipeShouldDial()` 按契约 §3 补齐;TCP keepalive 失败**作为传输层报告的挂断**进 dl 的闩,不是 apply 超时)→ **nd** 数据面协商(`LinkTerms{dataPlane, delivery, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` 入 Hello / Welcome,四个尺寸由 **server** 陈述;`dataPlane ∈ {SharedSegments, Stream}`;`auto` 的判据是**段实际交付成功**而不是地址判断,回落必须具名入日志与 stats;链路工厂在会话建立时选定一对,之后不变——缝是 setup-time 的,契约 §8.2)→ **sd** 共享段交付与控制面解耦(`ShmLink` 自带 AF_UNIX aux 汇合名、在 `Welcome` 里公布,`SCM_RIGHTS` 只走它;于是**控制面 TCP + 数据面共享段**在同机是合法配对;`ITransport` 收窄为纯控制帧,描述符传递归数据面)→ **sl** `StreamLink`(分块封帧;watermark 变消息且**规则逐字保留**——late-never-early、`>=` 不是 `==`、倒退是 Fatal;`appliedSeq` 修正案 + flush-on-idle;reply 变自带 seq 的消息,本链路上删 slot 池 / 取模 / stamp 自检;`SEG_STAGE` 变发送窗口,三个 cursor 要么完整复活要么不用、不留中间态;program archive 与 `draw_vbo` range 尾分片;`GetCaps` 两个 blobref 变消息;每方向每进程一条专用读线程)→ **t65** 车道。上层纯度:`SessionProducer` / `SessionConsumer` / `ServerLoop` / `PipeWireEncoder`、`ClientSession.cpp` 的 `kSegEvent` 挂载与 `ResourceTracker.h` 的 writeback 解析都只经 `ILink`。设计 `ARCHITECTURE.md` §11.9 | 门:inproc / spawn 车道结果数与逐线程 CPU 逐字不变(`ILink::ReserveRecord` 虚调用代价**实测**,`RingProducer::Reserve` 失去内联是具体风险);纯度 grep——`ShmLink` / `StreamLink` / `RingControl` / `ReplySlotPool` / `EventRingConsumer` / `m_shm.` 只出现在 `Transport/` 与其测试,上层零链路种类分支;**2×2 矩阵车道**(控制面 {unix, tcp} × 数据面 {shm, stream},loopback)名集合与 spawn 一致且同数绿,每条目记 `control=… data=…` 的 arm 证明(ID-124 的形状);跨机臂一条真 trace 逐像素;wf 三条 red-once(临时树里同宽成员互换 → 摘要变而 stamp 不变;`wireMajor = 99` → 两端具名拒绝、server exit 0、client 不回落 monolith;仅 stamp 不同的对端在 `Dial != Fork` 下连接成功——正控制);**必测数**:逐帧 `kWaitReply` 次数(`ResourceCreate` / `SetTextureParams` / 纹理半边 `ResourceSubData` 都在此列,TCP 上每次即一个 RTT)、`SEG_STAGE` 字节/帧对链路带宽、`PRESENT_CREDIT` 1..3 配对、混搭(tcp + shm)对(unix + shm)的逐线程 CPU 差 |
| **P6.5** 传输栈两轴化 + 跨机 | **立即的下一项**(第一波:全 TCP,WSL client ↔ adb 设备 server,设计见本表下方「立即的下一项」节;2026-09-22 定) | 包:**wf** wire 定宽与布局摘要——`wireFingerprint` = `PipeFields.def` 派生的逐成员 name / `offsetof` / `sizeof` 摘要 + 有序目录摘要 + 记录头布局 + 两个 blob codec 版本 + `kOpCount` + 字节序 / 指针宽度,**保留** `sizeof(MGPCaps)` / `sizeof(DynamicBackendParameters)`(wire 事实)、删 `sizeof(GLFunctionsTable)`;`buildFingerprint` 只在 `Dial == Fork` 下比较;`DynamicBackendParameters` / `MGPCaps` / `RenderStateParameters` 整块 blob 的定宽化(两端是不同编译器产的不同二进制,`long` / padding / 位域差异都是静默读错状态);`Refuse{code, detail, 对端值}` 控制消息,握手路径零 abort → **ct** 控制面端点种类(`fork` 继承 fd / `unix:<path>` / `tcp://host:port`;`DialMode{No, Fork, Connect}` 与 `MGPipeShouldDial()` 按契约 §3 补齐;TCP keepalive 失败**作为传输层报告的挂断**进 dl 的闩,不是 apply 超时)→ **nd** 数据面协商(`LinkTerms{dataPlane, delivery, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` 入 Hello / Welcome,四个尺寸由 **server** 陈述;`dataPlane ∈ {SharedSegments, Stream}`;`auto` 的判据是**段实际交付成功**而不是地址判断,回落必须具名入日志与 stats;链路工厂在会话建立时选定一对,之后不变——缝是 setup-time 的,契约 §8.2)→ **sd** 共享段交付与控制面解耦(`ShmLink` 自带 AF_UNIX aux 汇合名、在 `Welcome` 里公布,`SCM_RIGHTS` 只走它;于是**控制面 TCP + 数据面共享段**在同机是合法配对;`ITransport` 收窄为纯控制帧,描述符传递归数据面)→ **sl** `StreamLink`(分块封帧;watermark 变消息且**规则逐字保留**——late-never-early、`>=` 不是 `==`、倒退是 Fatal;`appliedSeq` 修正案 + flush-on-idle;reply 变自带 seq 的消息,本链路上删 slot 池 / 取模 / stamp 自检;`SEG_STAGE` 变发送窗口,三个 cursor 要么完整复活要么不用、不留中间态;program archive 与 `draw_vbo` range 尾分片;`GetCaps` 两个 blobref 变消息;每方向每进程一条专用读线程)→ **t65** 车道。上层纯度:`SessionProducer` / `SessionConsumer` / `ServerLoop` / `PipeWireEncoder`、`ClientSession.cpp` 的 `kSegEvent` 挂载与 `ResourceTracker.h` 的 writeback 解析都只经 `ILink`。设计 `ARCHITECTURE.md` §11.9 | 门:inproc / spawn 车道结果数与逐线程 CPU 逐字不变(`ILink::ReserveRecord` 虚调用代价**实测**,`RingProducer::Reserve` 失去内联是具体风险);纯度 grep——`ShmLink` / `StreamLink` / `RingControl` / `ReplySlotPool` / `EventRingConsumer` / `m_shm.` 只出现在 `Transport/` 与其测试,上层零链路种类分支;**2×2 矩阵车道**(控制面 {unix, tcp} × 数据面 {shm, stream},loopback)名集合与 spawn 一致且同数绿,每条目记 `control=… data=…` 的 arm 证明(ID-124 的形状);跨机臂一条真 trace 逐像素;wf 三条 red-once(临时树里同宽成员互换 → 摘要变而 stamp 不变;`wireMajor = 99` → 两端具名拒绝、server exit 0、client 不回落 monolith;仅 stamp 不同的对端在 `Dial != Fork` 下连接成功——正控制);**必测数**:逐帧 `kWaitReply` 次数(`ResourceCreate` / `SetTextureParams` / 纹理半边 `ResourceSubData` 都在此列,TCP 上每次即一个 RTT)、`SEG_STAGE` 字节/帧对链路带宽、`PRESENT_CREDIT` 1..3 配对、混搭(tcp + shm)对(unix + shm)的逐线程 CPU 差 |
| **Ph** 不可信对端加固 + 配对 | 待排,**P12 非 loopback 监听之前** | `Session::Fail` 的策略翻成每会话闩(每个依赖 `[[noreturn]]` 的站点造真实返回路径,不是一个函数翻一下;P6 dl 已把站点收口到一处);handle slot 预算(`BackendSlotTable` 的两条拒绝从 `MOBILEGL_ASSERT` 变 release 下的具名拒绝,杜绝静默 null twin);readback 尺寸门挪 server 侧且在 resize **之前**设绝对上限;`StagedTextureStore` 按该 level 宣告范围设界;`ArchiveVector` 按 `count * sizeof(element)` 设上限;`SEG_EVENT` 溢出不再能让一个仅仅不排空的对端 60 秒杀掉宿主渲染器;**配对 / 认证**(`Hello` 带配对令牌,无令牌只允许 loopback;令牌够不够、要不要 TLS 见开放问题 21);段 / 窗口尺寸由 server 钳制(P6.5 nd 已陈述) | 非 loopback 监听在无令牌时具名拒绝;畸形帧 / 超界计数 / 不排空对端的模糊臂下 server **不 abort、会话闩住、下一连接照常服务**;`Session::Fail` 站点普查门继续零新增;契约 §12.2 五处无界分配各一条 red-once |
| **P3b / P4b** 深化(Espryt) | 待排 | 按存储属主键控的发射游标与 view 索引重映射;`g_fboTextureSyncList`;`ResolvedTextureBindingMemo` / `SamplerPassMemo` / image sweep / program registry 重键;XFB scatter 搬到 client;删 fragColor 重推导 workaround 与 `g_broadcastMemo*`;raw-depth-fetch sampler 的 monolith 清理(transport 原生臂已由 P5f fs 落地);回读 / pack state;P4a 记下的 R-3 / R-4 / R-5 / R-7 / R-10 / R-11;`ProgramArtifacts.h` 的 NDK 尺寸钉;链接实验分给它的 **14 个符号**(D12) | 纹理 / program 场景;CTS `texture_*` / `shader_image_*` / `packed_pixels` 在 0.5 pp 内;每一个 Iris trace;`TextureUploadShapeScenario` 升级成门 |
| **P7** DirectVulkan(Magma)全量迁移 | 部分已提前落地,阶段未收官 | `38919d45` 已补实际游戏的 server buffer consumers 和 Android 旋转 blit([报告](notes/p5f/magma-inproc-fix.md));`194382c9` 又完成 Magma run-ahead、GPU 延迟退休与 VAO 身份碰撞修复([报告](notes/p5f/magma-runahead.md));其余工作:其余子系统:`SetupDrawSnapshot` 探测字段塌成 dirty mask;占位纹理原生化;具名 UBO host payload(D-B8);内部 shader 烘焙;`VertexInputStateFactory` 内容寻址 CSO;D18 容器纪律保留。**P6 真机发现(2026-09-22 归此)**:`DirectVulkan` 分离路径在 Redmi 上分歧且不确定(0.976–0.989,inproc 与 spawn 同表现,pbuffer monolith 1.0)——Magma × 真实 GPU;树上 `@P7` 具名拒绝清单(multisample-blit、depth-stencil-mipmap、vertex-format-conversion、texel-buffer-native-format、unaligned-*-range、uniform-buffer-byte-tail / dynamic-offset、copy-image-in-place、Magma 无 `StateObjectDeathOps`);链接实验分给 P7 的 **101 个符号**(D12)。`DynamicBackendParameters` 定宽重写**不再是 P7 的**——它是跨平台 wire 的前提,归 P6.5 wf | 集成 + trace 在 Magma 的 push 与 split 下全绿;verify 零分歧;真机 DirectVulkan split ssim 回到 1.0;184 符号棘轮(契约 §12.1)下降 P7 的 101 个——原 "`nm -D libMobileGLServer.so \| grep glslang` 为空" 不可达,D12 裁定 server 镜像就是整个 `libMobileGL.so`;CTS 0.5 pp 内。**再基线检查点:中点完成子系统 < 40% 立即重定基线** |
@@ -48,6 +48,115 @@
CTS 周转单独计价(`gl44to46` 约 56,271 例):逐阶段只跑该阶段可能影响的具名块;完整 caselist 只在架构边界与合并 `dev` 之前跑,放 CI 不放关键路径。
## 立即的下一项:P6.5 第一波 —— 全 TCP 传输(client 在 WSL,server 在 adb 设备)
> 状态:**设计已定(2026-09-22),未实现。** 目标形态:x86_64 的 WSL 进程作为 client,Redmi `2f7cbe2e`(aarch64,Adreno)上的 server 进程作为 server,两者在同一局域网内经 **TCP** 连接;`integration` 全部用例与 CI retrace 的 split 子集在这条链路上跑通并对上 golden。本节是 P6.5 行的第一波,只做「控制面 TCP + 数据面 stream」这一对;同机的「TCP 控制 + 共享段数据」混搭(nd `auto` / sd)是第二波,本节不做。
### 1 树上已有的(不重写)与必须先补的(不能绕)
| 已有 | 出处 | 对本波的意义 |
|---|---|---|
| 控制面 `SocketTransport`:`Listen` / `AcceptPair` / `ConnectTo`,**汇合点是一个名字**,两条连接按序接受(第一条控制、第二条 aux);`auxFd = -1` 已是「这条链不传 fd」的诚实答案 | `SocketTransport.h:70-91` | TCP 只是第三种名字(`tcp://host:port`);两条连接的规矩原样用:第一条控制,**第二条从 aux 变成数据流** |
| 帧:`[u32 'MGLF'][u32 len][payload]`,`FrameReader` 是真正的字节流重组器 | `Framing.h` | 数据连接复用同一重组器,只换 magic |
| server 主程序:endpoint 来自 `argv[1]` / `MOBILEGL_IPC_ENDPOINT`;`Listen` → `AcceptPair` → 建 backend → `Accept`(Welcome + 七个描述符)→ 控制泵,**对端 EOF 即退出**;`MOBILEGL_IPC_DIAL=no` 反递归 | `ServerMain.cpp`(正在被门 8 的工作修改,不引行号) | 单会话进程形状保留为 **child**;设备侧要加一个 **supervisor** 循环 |
| client:`StartOverSocket(transport)` 已把「socket 怎么来的」与握手分开;spawn 路径 = 造 `@mgl-<pid>-<this>` 名 → `LaunchServer` → `ConnectTo` | `ClientSession.cpp:693-724` | `Dial = Connect` 只是**跳过 `LaunchServer`**、把名字换成 `tcp://` |
| 握手后 client 收 **7 个 fd**(4 段 + 3 个门铃)再 `AdoptFromDescriptors` | `ClientSession.cpp:832-897` | stream 上这一段整体不存在:段是本地私有的,门铃是本地条件变量 |
| **数据面没有缝**:`ILink` 只有声明,树里用到它的只有 `LinkProgress`(`Ring.h:97`);`ShmLink` 不存在;两端 session 直接持有 `SessionSegments`(`SessionRings.h:130`);`ServerSession.h:93` 自己写着「`lk` 把门铃挪到 `ILink` 后面并删掉这些访问器」——**没做** | `Ring.h:108`(`RingControl` 重组已做)、`ILink.h`、`StreamLink.h` | 重审 §5.1 那「约一周」的 lk 主体仍然欠着,是本波**第一个包**,没有它 `StreamLink` 无处可插 |
| wire 寻址:`MGPBlobRef{Offset, Size, Seg}` 只允许 `kSegStage`,解析集中在 `ResolveOrFatal` | `PipeWireCodec.cpp:1421`(拒绝其它段:`:496`、`:1401`) | **两端各建一个同尺寸的本地 stage 窗口、偏移一致,codec 一行不改** |
| reply:`ReplySlotHeader{Seq, Status, Size}` 16 字节、`seq % slotCount`(8 槽) | `ReplySlot.h` | 头已经是消息形状;stream 上删掉取模与 stamp 自检 |
| event:四种 kind、定宽头 | `EventRing.h` | 已是 wire 形状,原样装帧 |
| 指纹:`CapsAbiFingerprint()` = 三个 `sizeof` + 协议版本 + git stamp | `protocol.fbs` `table Hello` 的注释 | x86_64 ↔ aarch64 两份二进制:**必须换成布局摘要**,否则要么永远拒绝(stamp 不同)要么静默读错 |
| 日志按角色分文件;server 角色由启动方设的 `MOBILEGL_IPC_ROLE=server` 决定;`LogLine` 已在 `CtrlMsg` 里、**零生产者** | `ServerSpawn.cpp:116`、`ClientSession.cpp:1948` | 34 个读 `<base>.server.log` 的消费方在跨机时读不到 → 日志前送(§3.5) |
| 车道:`mgl_itest_register_split_arms` 一个宏两条臂 {Split, Spawn},环境差只有 `MOBILEGL_TRANSPORT=spawn` + `MOBILEGL_IPC_SERVER_PATH` | `MG_IntegrationTest/CMakeLists.txt:1985-2010` | 加第三条臂 `Tcp`;名集合门(`scripts/ci/spawn_lane_parity.py`)与 arm 证明(`scripts/ci/junit_tally.py:27`)各加一臂 |
| retrace:桌面经 `MOBILEGL_TRANSPORT` / `MOBILEGL_IPC_SERVER_PATH` 环境(`run_trace_case.cmake:47-52`);设备经 `--env K=V;…` intent extra,server 路径从 `nativeLibraryDir` 解析(`TraceReplayActivity.java:374-386`);**桌面与设备共用同一套 golden**(`trace_cases.json`),驱动差异由 SSIM 阈值吸收 | — | 跨机 retrace 的 golden 就是现有的,不另造 |
| 设备已能从 app 进程 exec `libMobileGLServer.so`(spike A、`spawn-acceptance` 臂) | `apk.yml:477-487` | supervisor 就是这个可执行文件加一个 `--serve` |
| manifest **没有 `INTERNET` 权限** | `android-plugin/app/src/main/AndroidManifest.xml` | server 要监听 TCP,trace flavour 必须加 |
### 2 拓扑:谁监听、谁拨号、为什么
server 在手机上**监听**,client 从 WSL **拨出**。理由不是偏好:WSL2 默认 NAT,出站到局域网直通、入站要 portproxy;手机侧无防火墙。`Role × Dial` 两轴(契约 §3)因此在本波是 `Role=Client, Dial=Connect` / `Role=Server, Dial=No`。备用形态:`adb forward tcp:40613 tcp:40613` 把 server 绑在手机 loopback、client 连 WSL 内 adb 的 loopback——**要求 adb 跑在 WSL 里**(adb-over-Wi-Fi:`adb connect <phone>:5555`),因为 Windows 侧 adb 绑的 127.0.0.1 WSL2 够不着;路由器开了 AP 隔离时用它。
### 3 机制
**3.1 端点与握手(包 ct)**。`SocketTransport::Listen/ConnectTo` 识别 `tcp://host:port`(其余仍是 AF_UNIX 路径);两条 TCP 连接按序:控制、数据。socket 选项:`TCP_NODELAY`(批处理由我们的 publish 点决定,不交给 Nagle)、`SO_KEEPALIVE` + `TCP_KEEPIDLE=2s / KEEPINTVL=1s / KEEPCNT=3` + `TCP_USER_TIMEOUT=5000ms`——Wi-Fi 掉线不产生 FIN,这是唯一能把「半开」变成挂断的机制;它以 `POLLHUP`/错误进入 `Doorbell::Dead()`,走 dl 的闩,**是传输层报告的挂断,不是 apply 超时**(开放问题 23)。`Hello` 追加 `linkTerms{dataPlane=Stream, wireForm, maxReplyBytes, cmdWindowBytes, stageWindowBytes}` 与 `token`;`Welcome` 回填 server 陈述的四个尺寸;新增 `Refuse{code, detail, 对端值}`(`CtrlMsg` 只追加)。`buildFingerprint`(git stamp)比较策略:`Dial=Fork` 不匹配 → Refuse(抓陈旧 `MOBILEGL_IPC_SERVER_PATH`,不变);`Dial=Connect` 不匹配 → **警告**,`MOBILEGL_IPC_REQUIRE_SAME_BUILD=1` 时 → Refuse(车道设它:APK 与 WSL 构建必须同一 commit)。令牌:`MOBILEGL_IPC_TOKEN` 两端一致;server 无令牌时**只允许绑 loopback**,错令牌 → Refuse 并记日志。
**3.2 wire 定宽与布局摘要(包 wf)**。`wireFingerprint` = `gen_pipe.py` 从 `PipeFields.def` 生成的逐成员 `{name, offsetof, sizeof}` 表的摘要 + 有序目录摘要(opcode / payload 名 / flags / WaitClass)+ 记录头布局 + `MGPipeRenderStateSpans` 的 offsetof 表 + 两个 blob codec 版本 + `kOpCount` + `MOBILEGL_ABI_VERSION` + 字节序 + 指针宽度 + `sizeof(MGPCaps)` / `sizeof(DynamicBackendParameters)`;**删** `sizeof(GLFunctionsTable)`。x86_64 ↔ aarch64 都是 LP64、小端、Itanium ABI,布局大概率一致,但**由摘要断言、不由这句话断言**;已知的真实差异是 **`char` 的符号性**(aarch64 默认无符号)——wf 普查 payload 与 blob 里裸 `char` 当数用的站点。`DynamicBackendParameters` 定宽重写(从 P7 挪来)与 `RenderStateParameters` 1168 字节整块的成员表在此包。三条 red-once 见 P6.5 行。
**3.3 缝的主体(包 lk2,零行为变化)**。`SessionSegments` + `RingProducer/Consumer` + `ReplySlotPool` + `EventRingProducer/Consumer` + 两个门铃搬到 `ShmLink : ILink` 后面;`SessionProducer` / `SessionConsumer` / `ServerLoop` / `PipeWireEncoder` / `ClientSession.cpp` 的 `kSegEvent` 挂载 / `ResourceTracker.h` 的 writeback 解析都改收 `ILink*`;`ServerSession.h:93` 说要删的访问器删掉。门:inproc / spawn 车道结果数与逐线程 CPU **逐字不变**(`ILink::ReserveRecord` 的虚调用在 ~850–3550 次/帧上实测;若不在噪声里,答案是把 reserve/commit 做成 link 自有 arena 上的非虚函数);纯度 grep 门上线。
**3.4 `StreamLink`(包 sl)——被消息复制的镜像段**。设计原则一句话:**两端各自持有一份同尺寸的私有段,链路只负责把字节搬到对面同一偏移;codec、watermark 规则、apply 线程、编码器全部不变。**
- 段:握手协商后两端各 `malloc` 四块:cmd 环(`cmdWindowBytes`)、stage 窗口(`stageWindowBytes`)、event 环、reply 邮箱。尺寸相同 ⇒ 偏移相同 ⇒ `MGPBlobRef` 与 `ResolveOrFatal` 原样工作。
- 数据连接帧:`[u32 'MGLD'][u32 len][u8 kind][u8×3][u64 a][u64 b][payload]`。C→S:`Cmd{ringOffset, bytes}`(记录字节逐字、写到对面环的同一偏移)、`Stage{offset, bytes}`、`ClientProgress{cmdHead, submittedSeq, eventTail}`。S→C:`Progress{appliedSeq, retiredSeq, completedFrameSerial, presentAckSerial, cmdAppliedTail, eventRingFull}`、`Reply{seq, status, bytes}`(头即 `ReplySlotHeader`)、`Event{ringOffset, bytes}`。
- client 侧:编码器照旧写本地 cmd 环与 stage 窗口;`Flush()` 把 `[上次已送, cmdHead)` 作为一帧 `Cmd` 发出(环回绕用两段 iov 一次 `sendmsg`),**之前先发本次新 stage 的 `Stage` 帧**——顺序由编码器「先 stage 后 reserve」保证;`LinkCapabilities.PublishIsDelivery=false`,所以每个等待点先 `Flush`(这个位就是为此设的)。C→S 背压:`WaitForCmdSpace` 不变(读 `Progress` 里的 `cmdAppliedTail`),`send()` 阻塞是第二层,没有无界缓冲。
- server 侧:`mgl-srv-io` 读线程把 `Cmd`/`Stage` 拷到镜像段同偏移,再推进本地 `cmdHead`/`submittedSeq` 并敲 apply 的本地门铃;**apply 线程一行不改**。
- `Progress` S→C 的发出时机(对 `Ring.h` 「`appliedSeq` 不得懒发布」的**修正案**,规则改成「最多晚一次 flush,绝不早」):(a) apply 线程**即将 park 前**(flush-on-idle,解决「帧内最后一条 `kWaitReply` 记录两端互等」);(b) 每次 `PostReply` 之后;(c) present ack / `completedFrameSerial` 变化时;(d) 每 64 条已 apply 记录或 1 ms 取先到者。接收侧对每个 watermark 做 `AdvanceMonotonic`,**倒退是 `Fatal{ProtocolCorruption}`**。
- reply:`kWaitReply` 下同一时刻最多一条未决,client 侧是「单槽 + 条件变量 + seq 核对」;本链路上 slot 池、取模、stamp 自检**删除**(`ShmLink` 保留)。
- event:server 的 `EventRingProducer` 写本地环,io 线程把新增字节装 `Event` 帧;client 读线程写进 client 本地环同偏移,`EventRingConsumer` 不变;`Drained()` 发 `ClientProgress{eventTail}`;server producer 在 `head − 对端 tail > capacity` 时**阻塞**(P5e 的规则原样,`eventRingFull` 经 `Progress` 回传)。
- 门铃:`ConsumerBell()` / `ProducerBell()` 是本地 `CondVarDoorbell`,由各自的读线程敲;读线程见 EOF / 错误置 `Dead()`。每进程数据连接**一条**专用读线程(写在调用线程上),控制连接的读法不变。
- 帧上限:`Cmd`/`Stage` 单帧 ≤ `kMaxFramePayloadSize`(64 MiB);一条记录本来 ≤ 4 MiB。未分片的 program archive / `draw_vbo` range 尾在 stream 上和今天一样受 `stageWindowBytes` 约束,超出仍是 `Fatal{RingOverrun}`——**不是本波阻塞项**,记在开放问题 11。
- 不做的:`kSegShadow` / `kSegAdopt`(P8 / P11 的段)在 stream 上具名拒绝;`GetCaps` 的 blobref 若仍走 stage 之外 → 具名拒绝,本波不发明第三条载体。
**3.5 日志前送(包 lf)**。server 进程的日志 sink 在 `MOBILEGL_IPC_ROLE=server` 且有控制传输且 `MOBILEGL_IPC_LOG_FORWARD=1`(`Dial=Connect` 默认开)时,每行**同时**写本地 `<base>.server.log` 并作为一帧 `LogLine`(已在 `CtrlMsg` 里)经控制连接发出;client 收到后写入**自己的** `<base>.server.log`。于是 34 个消费方(`PipeStatsWindow` 的计数器窗口、四个 arming 场景、拒绝普查)**一字不改**。顺序:读者在 `MarkLaneLog` / `ReadLaneLogSince` 之前调一个导出的 `MGPipeSyncPeerLog()`(一次控制往返:`LogFlush` → server 冲刷并 ack),远端不存在时它是 no-op;导出符号一个,`-fvisibility=hidden` 下显式 `default`。若 lf 滑期,退路是这 34 处在 tcp 臂上**按名 SKIP**(清单文件,像 `dualblock-expected-fatals.txt`),不许静默通过。stats 行一帧一条,前送代价可忽略。
**3.6 设备侧 supervisor 与启动(包 sv)**。`mobilegl_server_main tcp://0.0.0.0:40613 --serve [--max-sessions 1]`:listen → 循环 { `AcceptPair`(控制 + 数据)→ `fork()` → child 关掉 listen fd、用 `SocketTransport(streamFd, dataFd)` 走今天的单会话路径到 `_exit`;parent 关掉已接受的 fd、非阻塞 `waitpid`、继续 accept }。每会话一进程 ⇒ `eglTerminate` 的进程级拆机被隔离(D9 原样);同时只服务 1 个会话,第二个连接 → `Refuse{Busy}`。启动:trace flavour 新增前台 `MobileGLServerService`(`android:process=":mglsrv"`),`am start-foreground-service` 带 `--es listen`、`--es token`、`--es env "K=V;…"`(复用 `--env` 的 `K=V;` 语法),它从 `nativeLibraryDir` exec `libMobileGLServer.so`(spike A 的路径)、给 child 设 `MOBILEGL_IPC_ROLE=server`、持 partial wake lock;`am force-stop` 整包停掉。后端由 client 的 `Hello.backendType` 请求、supervisor 的 env 可钉死,不一致 → Refuse。EGL 在 child 里是 pbuffer / surfaceless(P6 边界;真窗口归 P12)。manifest 加 `INTERNET`。
**3.7 谓词与测试的拓扑感知**。client 进程:`Transport=Spawn`(拓扑:两进程)、`Dial=Connect`、不 fork;`MGPipeBackendIsLocal()=false`。`CountOwnChildren()` 一类断言改为按 Dial 分支:Connect 下断言**本地零子进程** + `Welcome::serverPid` 非零 + 日志 `control=tcp`。S2(kill server)在跨机臂上经 `MGITEST_PEER_KILL_CMD` 模板(`adb -s <serial> shell run-as top.mobilegl.plugin.trace kill -9 {pid}`)真杀——**不**允许用协作式「请你退出」代替,那测的不是挂断。
### 4 包与顺序
`lk2`(缝主体,零行为)→ `wf`(定宽 + 摘要 + Refuse)→ `ct`(tcp 端点 + keepalive + 令牌 + Connect 拨号)→ `sl`(`StreamLink`,先在 **WSL loopback** 上对着 spawn 车道打平)→ `lf`(日志前送)→ `sv`(supervisor + Service + adb 脚本)→ `t65`(tcp 臂、跨机臂、门、数)。每包 red-once(R-16)。第二波(不在本节):`nd` 的 `auto` 与 `sd` 的 aux 交付(同机 TCP 控制 + 共享段数据)、P11 的 stream 具名拒绝、Windows client。
### 5 出口门(本波)
1. G1 pull 0/0/0/0、`.text` 不变(全部新代码在 `MOBILEGL_BUILD_DISAGGREGATED` 下)。
2. lk2 后 inproc / spawn 车道结果数与逐线程 CPU 逐字不变(实测,不论证)。
3. **`integration-tcp`(WSL loopback,server 以 `--serve tcp://127.0.0.1:40613` 本地起)**:名集合 == spawn(parity 脚本加臂)、同数绿;每条目 arm 证明 `control=tcp data=stream server=<host:port> pid=N`(`junit_tally --require-tcp-ran`)。
4. **`integration-tcp-device`(server 在 Redmi)**:同一名集合;首轮**记录**、不设阻塞门,每一条与 loopback 臂的差异必须具名(GPU 能力 / P7 的 Magma 分歧 / 时限);第二轮起转硬门。
5. **retrace**:CI split 子集(`trace_cases.json` 里未 `"split": false` 的)从 WSL 对着手机跑,DirectGLES 的 SSIM ≥ 现有阈值、OpenRA 为 1.0(E2 的锚);DirectVulkan 记录不设门(真机分歧是 P7 的)。
6. red-once:wf 三条(P6.5 行);ct——错令牌 → Refuse、`wireMajor` 不匹配 → 两端具名拒绝、`adb shell svc wifi disable` → 10 s 内闩住(而不是 120 s `BarrierTimeout`);sl——`Progress` 倒退 → `Fatal{ProtocolCorruption}`、越窗的 `Stage` 帧 → 拒绝、去掉 flush-on-idle 后「帧内最后一条 `kWaitReply`」用例必须挂住并被超时抓到;lf——关掉前送后四个 arming 场景必须**按名红**;sv——第二个并发连接 → `Refuse{Busy}`。
7. **必测数(记录)**:逐帧 `kWaitReply` 次数与 RTT 分布(OpenRA、MC in-world 各一);`SEG_STAGE` 字节/帧对链路实测吞吐(车道自己量一次 64 MiB `Stage` 突发);`PRESENT_CREDIT` 1 / 2 / 3 的帧率;loopback tcp+stream 对 spawn(unix+shm)的逐线程 CPU 差。
### 6 两边怎么配(落地后的操作手册;今天还跑不了)
**手机(server)**
```bash
# 1. 同一 commit 构建 trace APK(分离构建)并安装
./gradlew :app:assembleTraceDebug -Pmobilegl.buildDisaggregated=ON && adb install -r app/build/outputs/apk/trace/debug/*.apk
# 2. 手机 IP
adb shell ip -f inet addr show wlan0
# 3. 起 server(前台 Service → supervisor 监听)
adb shell am start-foreground-service -n top.mobilegl.plugin.trace/.MobileGLServerService \
--es listen tcp://0.0.0.0:40613 --es token devtoken \
--es env "MOBILEGL_BACKEND_TYPE=DirectGLES;MOBILEGL_PIPE_STATS=1;MOBILEGL_PIPE_STATS_PERIOD=1;MOBILEGL_LOG_FILE_PATH=/data/data/top.mobilegl.plugin.trace/files/mgl.log"
adb logcat -s MobileGL | grep -m1 'listening on tcp://'
# 停:adb shell am force-stop top.mobilegl.plugin.trace
```
**WSL(client)**,在分离构建目录(今天是 `~/lk-split`):
```bash
export MOBILEGL_TRANSPORT=spawn # 拓扑:两进程;不 fork,因为下面给了远端
export MOBILEGL_IPC_CONTROL=tcp://<phone-ip>:40613
export MOBILEGL_IPC_DATA=stream
export MOBILEGL_IPC_TOKEN=devtoken
export MOBILEGL_IPC_REQUIRE_SAME_BUILD=1 # APK 与本地构建必须同 commit
ctest -L integration-tcp-device -j 1 --output-on-failure # -j 1:设备同时只服务一个会话
# retrace(同一环境):
(cd build-retrace/tools/trace_replay && ctest -V -R '^MobileGLTraceReplay\.OpenRA\.DirectGLES$')
```
日志:client 侧 `<base>.client.log` 与前送来的 `<base>.server.log` 都在 WSL;设备本地副本用 `adb shell run-as top.mobilegl.plugin.trace cat files/mgl.server.log`。
**备用(AP 隔离 / 没有局域网直通)**:adb 跑在 WSL 里——Windows 上一次 `adb tcpip 5555`,WSL 里 `adb connect <phone-ip>:5555 && adb forward tcp:40613 tcp:40613`;手机侧 `--es listen tcp://127.0.0.1:40613`(loopback,不需令牌),WSL 侧 `MOBILEGL_IPC_CONTROL=tcp://127.0.0.1:40613`。
### 7 风险与未决(本波)
1. **`kWaitReply` × RTT**:`ResourceCreate` / `SetTextureParams` / 纹理 `ResourceSubData` 每次一个 RTT(Wi-Fi 2–5 ms);MC 加载区块的创建突发会可感知地慢。本波**只量**(门 7),减少这一列是 P9 / P10 的活。
2. **带宽**:`SEG_STAGE` 字节/帧对 Wi-Fi;单次 128 MiB 上传 = 秒级停顿(开放问题 19、20)。
3. **Wi-Fi AP 隔离**:用 §6 的 adb forward 备用。
4. **DirectVulkan 真机分歧**(P7)会在门 5 出现:记录,不当本波的红。
5. **`MOBILEGL_IPC_ROLE`**:supervisor 必须替 child 设它,否则 child 的日志会当 client 写(`Log.cpp` 从这个 env 解析角色)。
6. **ctest 并行 vs 一设备一会话**:`-j 1` 或 supervisor `--max-sessions N`(每会话一进程,N 个 EGL 上下文同时存在,Adreno 上未验证)。
7. **同 commit**:两端二进制不同是常态,指纹只保证 wire 布局;`REQUIRE_SAME_BUILD` 是车道纪律,不是协议要求。
## 历史:P5c 计划与审计(`inproc` 共享内存读点;审计头 `a79a0af6`,2026-09-17)
**为什么插在 P6 之前**:P5 / P5b 的 `inproc` 只把 verb 记录、`SEG_STAGE` blob、reply 与 caps 快照做成了 wire。对 `a79a0af6` 的只读静态审计(`~/w7/notes/p5c/p5c-audit-v1.md`,59 行清单,逐条在该头上重新解析)证明两个角色之间仍有一批**不经任何载体的直接内存读写**,靠 verb barrier 与同一地址空间才正确。它们不是传输问题:`spawn` 换掉的只是 ring 的传递方式,而这些读写在另一个进程里根本没有对应内存。P5c 的目标是让 `inproc` 诚实——两线程之间除 `SEG_CMD` / `SEG_STAGE` / `SEG_REPLY` / `SEG_EVENT` / caps 快照 / 控制帧之外零直接访问——从而 P6 只是传输替换。**本节保留 P5c 启动时的计划与审计语境;P5c 已收官,后续对象类、控制帧、静态量与反向通道的收口结果以 P5f 报告为准。**