- the archive freeze rule goes with the directory - code comments lose citations to the removed notes
16 KiB
帧节奏、进程、EGL 与平台(原 ARCHITECTURE §14–§15)
设计细节。设计要点与全部章节的索引见
../ARCHITECTURE.md。章节编号沿用原ARCHITECTURE.md,代码注释里的ARCHITECTURE.md §N按编号在这里找到。
14. Present、线程与帧节奏
eglSwapBuffers→present{frameSerial}→ publish + 敲门铃 → 返回,除非超出 credit;present与eglSwapBuffers严格 1:1(后端的帧边界排空只在Present内发生)。MOBILEGL_IPC_PRESENT_CREDIT默认 1(range 1..8):credit 是 run-ahead 唯一的稳态背压,FrameSerial由 client 铸造、1 起算,server 在Present()返回后归还 credit;等待发生在编码之前。credit > 1 从未量过(P10)。- 预算(~850 draw/帧):
SEG_CMD~70 KB/帧,SEG_STAGE~1 MB/帧;跨机时这 ~1 MB/帧对着的是 Wi-Fi。 - 线程:client v1 不加线程,编码在 GL 线程直接写 ring;server 有 io 线程与终身持有原生 context 的
mgl-srv-apply。全库无有效亲和性控制(Redmi 内核忽略 app 线程的sched_setaffinity)。 - 拆机顺序:publish + server 排空 → 停 apply 线程 → 关传输 → client 排空编译池 →
MobileGL::Destroy()(落地形状见 §17)。
14.1 apply 线程的亲和与自旋(auto)
MOBILEGL_IPC_SERVER_AFFINITY=auto(缺省)按会话对端(Hello 的 DialMode)与数据面选择 apply 线程的亲和与 park 前自旋(MG_Remote/Server/ApplyThreadPolicy.h,纯函数):同进程、fork 出的、拨入但走 stream 的对端 → 全部大核与 MOBILEGL_IPC_SPIN_US;拨入且走共享段的对端 → 一个核(ReservedApplyCore:多个 prime 核时取最低的;只有一个 prime 核时取另一个大核;否则不钉,绝不给唯一的 prime 核)与 kConnectedPeerSpinUs(除非显式设了 MOBILEGL_IPC_SPIN_US)。显式掩码或 off 对任何对端优先。
- 拨入的共享段 client 直接写 apply 线程自旋的那条环,两边又落在同两颗 prime 核上:Redmi 上共享段 23–26 fps、TCP 45–54,手工分开后 118。stream 数据面由 io 线程喂 apply 线程,apply 不在 client 的写上自旋(apply CPU 7.9 ms/帧,共享段 26.8),所以维持全部大核。
- 取最低而非最高的 prime 核:调度器把前台应用的热 GL 线程放在最高的 prime 核(44 次采样 43 次在 cpu7);apply 线程钉在那里 6.5 fps,钉在最低的 120 fps。
- 短自旋保留:立即 park(0)在所有量过的格子里都更慢(helper 路径 120 → 74 fps,shell 27 → 17)。
- 经
ExternalClientHelper启动的 client 也被挡在保留核之外:supervisor 启动时公布该核(ReservedApplyCoreForConfig),broker 交给 helper;操作员给了掩码或off时不保留任何核。
15. 进程、EGL 与平台(P6 / P6.5 / P12)
15.1 启动与握手
- 端点:
fork(socketpair+fork/execve,fd 3 = socket;server 由MOBILEGL_IPC_SERVER_PATH或dladdr同目录的libMobileGLServer.so定位)、unix:<path>、tcp://host:port。设备侧 supervisormobilegl_server_main <endpoint> --serve:listen → 认证 → 每会话 fork 一个子进程(或进程内线程,P12),同时只服务一个会话,第二个连接Refuse{Busy}。 - server 进程里
Transport读作Spawn,不强制 monolith(两百多处Transport != Monolith判断靠它选 server 臂;CONTRACT-P6.md§3.1 修订了原"子进程强制 monolith"的规则)。防无界 fork 链靠不继承的Dial == No(MOBILEGL_IPC_DIAL=no),spawn 时构造的 envp 剔除MOBILEGL_TRANSPORT与MOBILEGL_IPC_*作第二道独立保险;server 主程序自设MOBILEGL_IPC_ROLE=server。 Hello(abi、后端类型、指纹、配置、令牌、LinkTerms请求)→Welcome(server 陈述的尺寸、段 / aux 名、dataNonce、真实 pid)或Refuse{code, detail, 对端值};握手路径零 abort。非 loopback 监听必须有令牌(≥16 字节、常量时间比较、数据面按dataNonce绑定到已认证的控制连接、fork 前认证;Ph,OQ-21 裁定不做 TLS)。- TCP keepalive / 心跳失败由传输层报告为挂断,进 device-lost 闩;"闩绝不取自 apply 超时"不因此松动。冷启动期 server 发
SurfaceProgress,client 预算这段沉默(控制修订 2)。
15.1.1 TCP supervisor 的预认证
TCP --serve 的 supervisor(ServerMain.cpp 的 TcpSupervisor,判定部分在 PreAuthGate.h)先认证、再把会话交出去(fork 子进程,或进程内线程);未认证的对端最多占一个有界的 pending 槽:
- 新连接在首帧完整之前是 pending。supervisor 在一个 poll 循环里同时从所有 pending 连接非阻塞地读,任何未认证对端都拖不住别人;只读到首帧末尾,之后的字节属于会话。
- 每个 pending 连接有
MOBILEGL_IPC_PREAUTH_MS(默认 2000)读完首帧,同时最多MOBILEGL_IPC_PREAUTH_MAX(默认 8)个。队列按地址共享:满时新连接挤掉占槽最多的地址最老的那个;它自己的地址已占同样多时,被拒Busy的是它。 - Hello 在 supervisor 里认证、令牌先于指纹:未认证的对端只收到
Refuse{Authentication},得不到本构建的 wire / build 指纹。只为已认证的 Hello 交出会话,并把这一帧交给会话。 - 失败(令牌错、首帧畸形、超时、静默占槽超过
kPreAuthProbeGraceMs后关闭或被挤掉)记在对端地址上;满MOBILEGL_IPC_AUTH_BACKOFF_AFTER(默认 5,0 = 关)次后该地址在 accept 时、读任何字节之前即被拒,窗口自MOBILEGL_IPC_AUTH_BACKOFF_MS(默认 1000)起逐次翻倍、上限一分钟;该地址认证成功一次即清零。宽限期内无字节就关闭的端口探测、指向无活会话的 DataBind、退避本身发出的拒绝都不算失败。 - DataBind 经 hand-off 连同 nonce 转给活会话(会话比较 nonce);没有活会话时由 supervisor 具名拒绝。已认证的第二个 Hello 收到
Busy。 - 判定部分不含 I/O(
MG_Remote/Server/PreAuthGate.h),由PreAuthGateTest覆盖;早于kAuthBackoffForgetMs的失败不再计入退避。
15.2 Android
- 交付链(spike A 已证):APK 唯一可 exec 的位置是
lib/<abi>/,server 以add_executable+lib*.so命名构建;从untrusted_app进程fork+execve同域、零 avc denial;posix_spawn在 minSdk 26 不可用。一份共享库两个角色(stubdlopen+dlsym("mobilegl_server_main"))。 - 离屏 server:前台
MobileGLServerService(:mglsrv),从nativeLibraryDirexec,fork-per-session,EGL 是 pbuffer / surfaceless。 - 上屏 server(P12 子集):窗口归 server 自己(client 的窗口不跨进程)。
MobileGLDisplayActivity(:mglwin)持全屏 SurfaceView,在自己的进程里用一个线程跑 TCP server(会话串行、会话间重置闩、后端按进程钉住);ServerDisplay以 acquire / release / 几何三个钩子持有窗口,apply 线程以租约使用它。client 设MOBILEGL_IPC_SURFACE=server后无头:eglCreateWindowSurface(NULL)发一帧WindowKind::ServerOwned(控制修订 3),不发SetWindowHandle,尺寸由 reply 带回,resize 变成对 server 窗口的几何请求;client 没给EGL_WIDTH/HEIGHT(SDL)时取 native window 的大小作请求,server 把窗口缓冲固定成它并等比留黑边(不然横屏游戏在竖屏窗口里被裁)。FCL 只能接外部 server(自带 spawn 会把窗口指针发过来,被 Rule H 拒),且要开版本设置「游戏退到后台时不暂停」,否则 FCL 一退后台游戏就暂停(见 git 历史)。一台设备同一时刻一个 server;一个会话一种表面模式(SurfaceModeMismatch);无显示的 server 对 ServerOwned 具名拒绝(NoServerDisplay)。 - 失窗:
surfaceDestroyed有界(3 s)阻塞,直到 apply 线程(批内每条记录前也检查)放掉后端表面并以Fatal{ServerWindowLost}闩住会话;server 继续监听。进程内会话结束时手动做进程退出本来白给的清理(Espryt twin 与单元影子、swap interval、Magma renderer)。 HeadlessGL的 fork 预检会产生孤儿 server:server 的 EOF 检测即时且无条件退出。
15.3 Linux / Windows / 崩溃
- Linux / WSL / CI 永不开窗(
EGL_PLATFORM=surfaceless);Wayland 不支持。Windows / 桌面 client 在 TCP 终局下是真实形态(Windows 是第二批,开放问题 22);macOS 不拆分;Windows 机器不是正确性门。 - server 死:client 读到挂断 → device-lost 闩(GL 调用 no-op、
eglSwapBuffers返回EGL_CONTEXT_LOST、reset statusGL_UNKNOWN_CONTEXT_RESET);Session::Fail漏斗向对端发SessionFault帧命名家族(FatalFamilies.def)。闩之前创建的 context 永久丢失(eglMakeCurrent/eglSwapBuffers返回EGL_CONTEXT_LOST,glGetError报一次GL_CONTEXT_LOST,销毁时不往新 session 发任何记录)。进程可恢复(MOBILEGL_IPC_RECOVER,默认 1):应用下一次eglCreateContext/eglCreate*Surface/eglInitialize拆掉丢失的 session、重新拨号得到新 session,client 侧描述旧 server 的缓存全部清空(发布闩、CSO/sampler 缓存、emitter 闩、create window、applier-reset 序号、persistent map),wire epoch 前进使旧 GLContext 一律视为丢失;surface 保留并在下次 make-current 时于新 session 重建(EGL 对 power-management 事件的语义),Wayland 窗口的 shared image 在新 session 重新分配。in-process server 与fd:对不可重拨,仍是永久丢失。MOBILEGL_IPC_RESPAWN(server 重启后全量重推)仍是具名拒绝。client 死:server 读到 EOF 立即销毁 context 并退出(supervisor 继续服务下一连接)。
15.3.1 Fatal 家族与 wire FatalCode
内部 Fatal 家族(MG_Pipe/FatalFamilies.def)远多于 wire 上 FatalCode 的 7 个值,所以家族是内部枚举:对端只收到粗粒度的码,细粒度的词留在本地日志与 SessionFault 帧的 family 字符串里。码只回答「这是哪一类结束」:
ProtocolCorruption:无法正确执行的东西——畸形记录、未实现的 verb / surface、两个角色必须守住的不变式、不合契约的回复。对端读到的是「双方对协议的理解不一致」。RingOverrun:共享环或事件通道溢出,是传输容量问题。SegmentMismatch:共享段的尺寸放不下某个角色必须放进去的东西(尺寸错,不是一时满)。AbiMismatch:两端的布局或协议版本不一致。ServerCrashed:等待方归咎于对端的活性结束:排空未完成、apply 线程不在、server 拿不到必需的原生资源(ResourceUnavailable)、server 窗口被销毁(ServerWindowLost)、GPU 设备丢失(BackendDeviceLost,只闩住该 session)。拿不到资源不报SegmentMismatch,那会把错归到共享段尺寸上;wire 上也没有「拿不到资源」的值,新增会改wireFingerprint。DeviceLost:只由 device-lost 闩设置,没有家族投影到它。
15.4 共享图像(shared_image)
server 分配并以 dma-buf 描述符导出的颜色缓冲,让一个 client 的帧不离开 GPU 内存就进入另一个 client 的纹理。一行记录(MGPSharedImageOp),每个操作都经 reply slot 以 MGPSharedImageReply 回答;server 交给 client 的描述符走 aux socket,先于 reply 到达;ImageId 由 server 铸造,server 生命期内不复用。Damage 是 GL 窗口坐标、左下原点的矩形,0 个 = 整个表面。
| 操作 | 语义 |
|---|---|
Allocate / Release |
新建 Width×Height 的 Format 图像,reply 给出 id,描述符经 aux socket 送达 / 会话放掉对 ImageId 的引用 |
Import |
认领 client 在 aux socket 上排队的描述符(MGPSharedImageFdOffer 带本记录的 seq);须是本 server 导出的图像,尺寸与格式一致 |
Present |
当前 context 的默认帧缓冲按顶行在先拷进 ImageId(窗口帧成为 wl_buffer 的内容),只拷 Damage:client 按图像记录它自上次写入后缺的区域;GPU 拷完才回 reply |
Attach |
纹理 Texture 的存储变成 ImageId(glEGLImageTargetTexture2DOES),直到它下一次定义存储的 respecify |
QueryBufferAge |
在流中回答当前 draw surface 的 EGL_BUFFER_AGE_EXT(0 = 未知);应用先声明绘制区域(EGL_KHR_partial_update)时 Format = kMGPBufferAgeDamageRegionFollows |
SetDamageRegion |
eglSetDamageRegionKHR:本帧将画的区域 |
Flush |
往图像里渲染的会话(X server 的 glamor)的 glFlush / glFinish:自上个边界用过的图像按 flush 的 fence 发布为已写、已读(隐式同步),发布后回答 |
NativeFence |
EGL_ANDROID_native_fence_sync:会话已提交工作完成时 signal 的 sync_file(写图像的会话同时做 Flush 的发布),经 aux socket 先于 reply 送达,Format = kMGPNativeFenceFdFollows;Format = 0 表示 server 给不出描述符,reply 发出时工作已完成 |
15.5 EGL 归属:apply 线程终身持有原生 context
split 下应用线程从不调用驱动的 eglMakeCurrent:BackendObject_Remote 的 EGL 虚函数调用 ServerLoop.h 的 Server* 转发函数,每个都经 RunSurfaceControlFrame 成为阻塞控制请求,由 mgl-srv-apply 对 server 私有的后端对象执行。
- 为什么是这条线程:DirectGLES 的
IsBackendContextCurrentOnThisThread()与Managers.cpp的CanTouchGLNow()守卫答「否」时静默降级——fence 恒为已 signal、query 返回空句柄、Present不建帧 fence(buffer pool 的回收水位不再前进)、persistent map 拒绝采纳。apply 线程终身持有 context,这些守卫在 server 上都答「是」。 - 原生
eglMakeCurrent只在 apply 线程当前绑定的 tuple 真正改变时运行,去重分两层:apply 线程上的ServerLoop::ApplyMakeCurrent用ClassifyEglMakeCurrent与上次绑定的 tuple 比较,相同即 no-op、不重发 caps,不同则转发并重发 caps 快照,client 的 release-current 只记录(ClientReleaseCount)不转发;BackendObject_DirectGLES::MakeEGLCurrent在有传输时,若目标 draw surface 已是本线程原生当前、且虚 tuple 正是那次绑定所为(或那次绑定是 surface 自己的创建),跳过原生调用。同一 surface 上换虚 context 仍原生重绑,因为MakeCurrent的失效针对的是变了的前端 context。只记一个 tuple:线程上同时只有一个 current context,A → B → A 是三次原生绑定。 - 在原生绑定可能已改变、或 tuple 指向的 context 可能已不存在的每个事件之后遗忘 tuple(
ServerLoop::ForgetCurrentTuple):ReleaseEGLResources、释放它指向的 surface、surface(重)建(创建时原生绑定新 surface)、后端销毁。EGL 句柄值会复用;记住死 tuple 的代价是静默地没有 current context,遗忘的代价只是一次多余的转发。 ReleaseEGLResources与~BackendObject_DirectGLES必须阻塞:前者本身是控制请求,后者在 apply 线程退出前于该线程执行,ServerLoop::Stop()等它结束;否则MobileGL::Destroy()会在 server 仍持有 context 时继续往下拆。- 对照
ServerLoopEglTest.MakeCurrentBindsNativelyOncePerContextAndNeverForwardsAClientRelease在 EGL 函数表处计数:surface 创建 + 两次相同 make-current + 一次 client release + release 后再绑定 = 一次原生eglMakeCurrent、零次原生 release。