Files
MobileGL/docs/Disaggregated/design/06-transport.md
T
BZLZHH 6e36bf2c1b [Chore, Docs] (...): drop the archived notes and move the skill under tools
- the archive freeze rule goes with the directory
- code comments lose citations to the removed notes
2026-10-31 19:29:06 +08:00

11 KiB
Raw Blame History

传输与数据面(原 ARCHITECTURE §11)

设计细节。设计要点与全部章节的索引见 ../ARCHITECTURE.md。章节编号沿用原 ARCHITECTURE.md,代码注释里的 ARCHITECTURE.md §N 按编号在这里找到。

11. 传输与数据面(MobileGL/MG_Remote/)

11.1 段

段 拥有者 默认 内容
SEG_CMD client(server 只读) 8 MiB RingControl 页 + POD 记录 + 小内联负载
SEG_STAGE client 32 MiB bulk 字节:buffer / 纹理 subdata、UBO、client 数组、persistent-map 脏块
SEG_REPLY server 16 MiB,8 × 2 MiB slot readback 像素、acceptance 答案(ID-47)
SEG_EVENT server 256 KiB SPSC ring 回调事件
SEG_SHADOW / SEG_ADOPT client / server — 零拷贝 shadow(Phase 2)/ ≥16 MiB 采纳(P11);stream 数据面上具名拒绝(MG_Remote/Transport/AdoptTier.cpp,握手期,P11 A1)

段在同机上由 ShmSegment 创建(Android ASharedMemory、Linux memfd、Windows CreateFileMappingW),描述符经 SCM_RIGHTS 传递;stream 数据面上两端各持一份同尺寸的私有段(§11.9)。

11.2 RingControl(Ring.h)

一页 4 KiB,每个争用组一条 cache line,按写者分组(P6 lk):producer 线 cmdHead / submittedSeq;consumer 线两个 tail;四个 consumer 写的水位收成 Progress(appliedSeq / retiredSeq / completedFrameSerial / presentAckSerial);最后是 epoch、generation、park 标志与 eventRingFull / eventDropped。游标是单调字节计数、2 的幂掩码、永不重置;pad 记录处理 wrap;不可能的头 → Fatal{ProtocolCorruption}。

11.2.1 五个水位

水位 由谁推进 含义
submittedSeq producer,发布之后 诊断量(client 领先 server 多少),没有人等它;放在 producer 的 cache line 上,LinkProgress 因此单写者
appliedSeq consumer,每 apply 一条记录 +1 verb barrier 与每个 reply 等待读它,所以不得批量:批量会让等待方卡在已完成的工作上,或在未完成的工作上醒来
retiredSeq consumer,记录引用的 SEG_STAGE 字节用完后 staging 分配器只在它之后回收;不超过 appliedSeq
completedFrameSerial server,present 完成时 回收与老化等它;比 appliedSeq 落后一个 GPU 深度,不得混用
presentAckSerial server,归还 present credit 时 present 节流等它;唯一限制延迟而非字节的背压

规则(SessionRings.h 的 Watermark 是唯一实现):所有等待用 >=,两侧都可能一次前进多步,== 会错过唤醒。水位只许晚发布、不许早发布:晚只是多等,早会让等待方静默使用尚未发生的工作,环上没有校验能发现;除 appliedSeq 外都可惰性发布,回退是 Fatal{ProtocolCorruption}。wrap 填充(kRecPad)不推进 seq:seq 就是 reply slot 的 id,只有一侧计数填充时两侧序号每次 wrap 偏一、静默读到别的调用的答案,所以两侧都在计数前跳过它(RingConsumer::Pop)。

11.3 双向 doorbell(Doorbell.h)

两个方向都是"先自旋、再置 parked、再阻塞",发布方仅当对端 parked 时敲;丢失唤醒由两个 seq_cst fence 关闭。CondVarDoorbell(同进程,带 Kill())与 SocketDoorbell(跨进程,对端关闭即死亡检测)。stream 数据面上门铃是本地条件变量,由读线程敲。

11.3.1 丢失唤醒为什么靠两道 fence

等待方:置 park 标志 → std::atomic_thread_fence(seq_cst) → 重测条件(Doorbell::Wait);通知方:发布水位 → 同样的 fence → 读 park 标志(NotifyIfParked)。两道 fence 都在 seq_cst 全序里、必有先后,按 [atomics.order] 至少一侧看得见另一侧的写,所以标志自身的读写可以是 relaxed。

只把标志的 store / load 改成 seq_cst 不够:Dekker 论证要求四个访问都在全序里,而水位发布是 release store(RingProducer::Publish)、条件重测是 acquire load。x86 上 release store 是普通 MOV,还停在 store buffer 里时,对标志的普通 MOV 读就可能读到 0,通知方于是不敲门铃,等待方在旧水位上永久 park;ARMv8 上 STLR→LDAR 是 RCsc,只是碰巧没事。所以两道 fence 都不可删,且 NotifyIfParked 必须在水位发布之后调用:fence 只约束它之前的访问。

11.4 控制面(protocol.fbs、Framing.h、ITransport.h)

  • 一份 FlatBuffers schema:热路径是 struct 直接进 ring(与 POD 逐条 static_assert 对齐);罕见 / 变长 / 需演进的走控制连接的 CtrlMsg(Hello、Welcome、Refuse、CapsSnapshot、SurfaceOp/SurfaceReply、SessionFault、LogLine …),union tag 只追加,控制修订号钉在 sha256(protocol.fbs) 上。
  • 生成物提交进树,CI flatc-check 重生成并 diff;codegen 不进默认构建图。
  • 封帧 [u32 'MGLF'][u32 len][payload],64 MiB 上限,读时校验,坏帧立即闩住失败。

11.5 WAR 危害、拷贝账与背压

Phase 1(今天):GL 调用时刻把字节拷进 stage slot,slot 到 apply 越过它为止不可变,危害按构造消除,代价一次 memcpy;Phase 2(SEG_SHADOW 零拷贝)未做。server 没有第二份 BufferObject,不存在中间拷贝。分配失败升级:扩容 → 对最老未退休批次有界等待 → 硬 drain + generation bump(之后 tracker 全部重推)。

11.6 publish、序号与 credit

  • 每条记录 release-store cmdHead,仅当 consumer parked 时敲门铃;显式门铃点:present、kNeedsAck、eglMakeCurrent、glFlush、stage 余量不足、轮询类入口。
  • seq = 记录序数;字节 credit 与 present credit 两个独立窗口。
  • 等待规则由 WaitClass 列决定(P5e):kWaitReply / kWaitPresent / kWaitApplied / kWaitNone;run-ahead 武装时 kWaitNone 发布即返回。barriered 谓词(MGPipeBarriered)client 与 server 用同一个函数、同一份数据算。
  • 强制等待点:MapBuffer(READ) / GetBufferSubData 源走 SyncGpuWrites;glFinish = 等 appliedSeq 追上再排空反向通道;每个 Server* EGL forwarder 之前等一次。glGetError 最多晚一个 present credit。

11.6.1 barriered 谓词的条款

MGPipeBarriered(op, payload, applierState)(MG_Pipe/PipeApply.h)由 server 的 ApplyOne 计算,决定这条记录能否读 client 内存;client 不调用它,而是在发布前对同样的条款求值(ClientSession 的等待规则读 WaitClass 列,PipeFill.cpp 的 ClientVerbIsBarriered 另判 XFB)。两侧答案一旦不同,失败是静默的(client 继续跑,server 读到已经变了的值),所以条款只读线上可见的数据:静态 WaitClass 列、记录自身的 payload、ring 上更早的记录写进 applier 的状态。条款只有三条:

  1. WaitClass != kWaitNone;
  2. 打开的 XFB span 内的 kCtxVerb(XFB 保持 lockstep);
  3. 带 kDrawClientArrays 的 draw_vbo:run-ahead 下 client 在发布前就拒绝它,此条只为 lockstep 臂而在,使谓词完备。

新条款必须在同一次改动里同时加进这个函数与 client 侧的对应判断,不能只在一侧加。评判候选条款要问哪条臂为它付代价,而不是它让哪条车道变绿:曾为让 Espryt 间接 multi-draw 档读 GL_DRAW_INDIRECT_BUFFER 绑定合法化,把所有普通 multi-draw 升级为 barriered,于是默认档的每次 glMultiDraw* 都多一次会合,而这次读只发生在两个可选档里;最终改由做绑定的一侧按句柄回答(MultiDraw.cpp 的 BoundDrawIndirectBufferId),撤回了该条款。

11.7 事件回传与溢出

client 在 glGetError、query / sync 查询、eglSwapBuffers、buffer 回读与每轮等待循环中排空 SEG_EVENT。日志有损;语义事件无损:环满时 server 置 eventRingFull、在记录边界停 apply、敲 client,client 排空后清标志并敲回(两半都在,才不死锁)。run-ahead 下 producer 阻塞等排空,整笔预算 MOBILEGL_IPC_EVENT_WAIT_MS;client 不排空 / 已走 / 会话停止 → ReverseChannelForfeit{…} 弃投并闩住(Ph PH-6),而不是 Fatal 打掉 server。不发布 kCapRunAheadApply 的 server(lockstep 臂)保留 P5c 的 Fatal。

11.8 fence 与无 present 负载(P10)

fence 完成度必须来自真的逐 fence 退休,不是 present 水位(DirectGLES 的 g_completedFrameSerial 今天只在 Present 前进);无 present 循环需要 server 插非 present fence tick。Magma 那半已随 run-ahead 落地。

11.9 传输栈的两根轴(P6.5)

终局(2026-09-22):client 与 server 可在不同机器、不同 OS / 架构上经 TCP 连接,同机 spawn 保留。传输栈拆成两根独立可选、握手协商、可混搭的轴,上层只见两个接口:

轴 接口 实现 选项(MOBILEGL_IPC_CONTROL / MOBILEGL_IPC_DATA)
控制面 ITransport(只承载控制帧,不承载描述符) SocketTransport fork(继承 fd)、unix:<path>、tcp://host:port
数据面 ILink(记录预留 / 提交、水位、reply、event、span 解析) ShmLink(共享段 + RingControl + 门铃)、StreamLink(分块封帧,水位与 reply 变消息,发送窗口) shm、stream、auto
  • 配对在会话建立时选定一次;LinkTerms(数据面、窗口尺寸、maxReplyBytes …)由 server 陈述,client 请求可被钳制。
  • 共享段交付与控制面解耦:ShmLink 自带 AF_UNIX aux 汇合名,在 Welcome 里公布,所以同机"TCP 控制 + 共享段数据"是合法配对。auto 的判据是段实际交付成功,回落到 stream 必须具名进日志。
  • 上层零链路种类分支:ShmLink / StreamLink / RingControl 等只出现在 Transport/ 与其测试(纯度 grep 门);角色谓词回答"谁在哪个进程",不回答"用什么链路"。
  • 两端是不同二进制:wireFingerprint(PipeFields.def 派生的逐成员布局摘要 + 目录摘要 + 字节序 / 指针宽度)永远比较;buildFingerprint 只在 Dial == Fork(或 MOBILEGL_IPC_REQUIRE_SAME_BUILD=1)下比较;不一致是 Refuse,不是 abort。
  • TCP 上每条 kWaitReply 就是一个 RTT、SEG_STAGE 字节对的是 30–100 MB/s 的链路:这两列从"记录项"变成可行性数(开放问题 19、20)。

第一波(全 TCP 控制 + stream 数据)已落地;同机混搭(nd auto / sd)是第二波。细节与契约见 git 历史。