[Docs, Chore] (docs, tools, MobileGL, scripts, CONTRIBUTING.md, .gitignore): translate the documentation and use US spelling

This commit is contained in:
BZLZHH
2026-10-31 19:58:19 +08:00
parent 47892afddc
commit 4f50bedde0
512 changed files with 2955 additions and 2945 deletions
+2 -2
View File
@@ -126,7 +126,7 @@ timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built.
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labeled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's color hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built.
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
@@ -158,7 +158,7 @@ Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49,
- **pkill -f self-match**: a `ct.sh`/`droidspaces run bash -lc "<script>"` process carries the script in its argv; `pkill -f chrome` kills the runner. Use `pkill -x`.
- **Root-owned /tmp files**: a log created by a root-run client cannot be reopened by swung0x48 later (no log appears). `rm -f` it as root first.
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "正在使用的软件渲染器"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "software renderer in use"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
- **Lock screen**: autolock is off (`kscreenlockerrc [Daemon] Autolock=false`); a real lock can only be left by typing the password (logind lock/unlock is not supported for this session type - `device/unlock.sh` is unverified). Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and kill it.
- **CRLF**: `core.autocrlf=true` here; anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Strip CR before anything runs on Linux/Android (`tr -d '\r'`; `ct.sh`, `push-tools.sh`, `kwin-stage.sh` do). This skill has a `.gitattributes` forcing LF.
- **Display daemon**: the app starts it; the session's `wait` step keeps KWin from starting without it (a KWin that cannot reach it logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits in ~3 s). Anything started from the app's `su` must leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`) or a force-stop kills it.
@@ -1,11 +1,12 @@
# Reference material
Frozen copies of MobileGL planning documents this skill depends on. They lived in the MobileGL
repository under `docs/archive/notes/anland/` and were removed from the tree during the
Frozen MobileGL planning documents this skill depends on, translated to English. They lived in the
MobileGL repository under `docs/archive/notes/anland/` and were removed from the tree during the
`feat/disaggregated` cleanup, so the skill carries them here.
Source: MobileGL `feat/disaggregated`, commit `b5d64175` (2026-10-19). These are background
reading, not operational instructions; do not edit them here.
Source: MobileGL `feat/disaggregated`, commit `b5d64175` (2026-10-19). The text was translated from
the originals; technical names and paths are preserved. These are background reading, not
operational instructions; do not edit them here.
| File | Original path |
|---|---|
@@ -63,7 +63,7 @@ Details: `docs/Performance/glmark2-handoff.md`.
- Bench scripts: `tools/device_bench/anland_glmark2/`.
### 2. GitHub Actions CI (stopped mid-way, no handoff from its agent)
- **Background:** CI had been red since about 2026-09-26 (Test workflow) and since 2026-10-02 (APK workflow retraces). Most runs show `cancelled` only because each newer push cancels them.
- **Background:** CI had been red since about 2026-09-26 (Test workflow) and since 2026-10-02 (APK workflow retraces). Most runs show `canceled` only because each newer push cancels them.
- **Landed:**
| Commit | What |
@@ -79,7 +79,7 @@ Details: `docs/Performance/glmark2-handoff.md`.
- the APK workflow's **retrace failures** (Retrace and validate), on DirectGLES for minecraft-1.21.11-main-menu, 1.21.4 startup/in-world/main-menu, 1.17-main-menu-854 and OpenRA, and on DirectVulkan for 1.17-main-menu-854;
- they first appeared around `5accb098` / `8da082f8` (2026-10-02); last green APK run `2f64ede8`;
- not yet bisected. Candidates:
- the clear-colour default (0,0,0,0), which is GL-correct, so judge any golden against the spec;
- the clear-color default (0,0,0,0), which is GL-correct, so judge any golden against the spec;
- anonymous-struct sampler handling in glslang;
- sampler CSO publishing;
- the scissor-shadow fix;
@@ -134,7 +134,7 @@ Details: `docs/Performance/glmark2-handoff.md`.
- `5a7b23f6` / `258b9460` / `16b07cf4` suspend, resume and resize.
- **Shared images:** `38842985`, `3d17e8c1`, `350fc699`, `67226176`, `b5cab441`, `7520e53b`.
- **Misc:**
- `8da082f8` clear colour starts at (0,0,0,0);
- `8da082f8` clear color starts at (0,0,0,0);
- `51c37bcb` sampler param conversion;
- `15f9b27a` sampler CSO per share group;
- `239fc228` gl_FragCoord access chains;
@@ -1,95 +1,95 @@
# 计划:AHB 当 dma-buf,server 当分配器(窗口零拷贝)(2026-10-02)
# Plan: AHB as a dma-buf, the server as allocator (zero-copy windows) (2026-10-02)
> 接 [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md)。调研 + 设备探针,未动代码。硬约束:**厂商中立**——只依赖 Android 公开设施(AHardwareBuffer、EGL_ANDROID_*、VK_ANDROID_external_memory_android_hardware_buffer、sync_file)和通用 Linux 设施(dma-buf、SCM_RIGHTS、libgbm 后端 ABI、linux-dmabuf 协议),不碰 kgsl / gralloc 私有布局 / 厂商修饰符。
> Follows [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md). Research and device probes, no code changed. Hard constraint: **vendor-neutral** — depend only on public Android facilities (AHardwareBuffer, EGL_ANDROID_*, VK_ANDROID_external_memory_android_hardware_buffer, sync_file) and generic Linux facilities (dma-buf, SCM_RIGHTS, the libgbm backend ABI, the linux-dmabuf protocol); never touch kgsl, gralloc private layouts or vendor modifiers.
## 现状:每个窗口每帧 GPU→CPU→GPU
## Today: GPU→CPU→GPU per window per frame
KWin 自己(`MOBILEGL_IPC_SURFACE=server`)直接画在 Android Surface 上,没问题。其余所有 Wayland 客户端:
KWin itself (`MOBILEGL_IPC_SURFACE=server`) draws straight onto the Android Surface, which is fine. Every other Wayland client:
1. **生产端**(`EGLImpl.cpp:210` → `WaylandWindow.cpp:398`):server 渲染进 pbuffer;`eglSwapBuffers` 时客户端 `ReadPixels` 整帧,按 ~2 MiB 回复槽切带,**每带一次阻塞往返**;server `glReadPixels`/`vkCmdCopyImageToBuffer`+等待+重排 → 回复槽 memcpy → 客户端 memcpy → BGRA 换序+翻转写进 `wl_shm`。CPU 整帧遍历:Espryt 4 次,Magma ~6 次。
2. **消费端**(KWin 也是 MobileGL 客户端):`glTexSubImage2D` 上传 wl_shm → 解包 malloc+拷贝 → 层影子拷贝 → **整层**(不是脏区)进 SEG_STAGE → server `AdoptRun` 再拷贝 → 驱动上传。又 4 次。
3. linux-dmabuf 根本没开:KWin 6.7 的 `EglBackend::initWayland` 要求 `drmDevice()`,anland 后端在 MobileGL 模式下给空设备,`kwin.patch` 让它直接退回 wl_shm(还连带了 screencast 的空指针崩溃补丁)。
1. **Producer side** (`EGLImpl.cpp:210` → `WaylandWindow.cpp:398`): the server renders into a pbuffer; on `eglSwapBuffers` the client `ReadPixels` the whole frame and cuts it into ~2 MiB reply slots, **one blocking round trip per slice**; the server `glReadPixels`/`vkCmdCopyImageToBuffer` + wait + reorder → copy into the reply slot → client memcpy → BGRA swizzle and flip into `wl_shm`. Full-frame CPU passes: 4 on Espryt, ~6 on Magma.
2. **Consumer side** (KWin is also a MobileGL client): `glTexSubImage2D` uploads wl_shm → unpack malloc and copy → layer shadow copy → the **whole layer** (not the dirty region) into SEG_STAGE → the server's `AdoptRun` copies again → driver upload. 4 more passes.
3. linux-dmabuf is not enabled at all: KWin 6.7's `EglBackend::initWayland` requires `drmDevice()`, the anland backend provides an empty device in MobileGL mode, and `kwin.patch` makes it fall back to wl_shm (together with a null-pointer crash fix for screencast).
2560×1412 的窗口一帧 14.5 MB,上面合计 ~8–10 次整帧 CPU 遍历 + 若干阻塞往返。
A 2560×1412 window is 14.5 MB per frame; the above adds up to ~8–10 full-frame CPU passes plus several blocking round trips.
## 目标形状
## Target shape
```
client(GL) ──渲染──▶ [AHB] ◀──采样── KWin(GL) 全在 server 进程里:同一块 AHB,两个会话各自导入
│ ▲ dma-buf fd CPU 拷贝 0 次;fd 每块缓冲只传一次
└─ wl_buffer(linux-dmabuf, fd) ─▶ KWin ─ eglCreateImage(DMA_BUF, fd) ─▶ server 按身份认回 AHB
client(GL) ──render──▶ [AHB] ◀──sample── KWin(GL) all in the server process: one AHB, imported by two sessions
│ ▲ dma-buf fd zero CPU copies; the fd crosses once per buffer
└─ wl_buffer(linux-dmabuf, fd) ─▶ KWin ─ eglCreateImage(DMA_BUF, fd) ─▶ server recognizes the AHB by identity
```
关键点:**生产者和消费者都是同一个 server 的会话**。所以"导入 dma-buf"不需要驱动支持 `EGL_EXT_image_dma_buf_import`(Adreno 的 Android EGL 就没有)——server 只需认出"这是我发出去的那块 AHB",再走 Android 标准导入:Espryt 用 `eglGetNativeClientBufferANDROID` + `EGL_NATIVE_BUFFER_ANDROID`,Magma 用 `VK_ANDROID_external_memory_android_hardware_buffer`。两者都是 Android 规范要求的,厂商中立。
Key point: **producer and consumer are sessions of the same server**. So importing the dma-buf needs no driver support for `EGL_EXT_image_dma_buf_import` (Adreno's Android EGL does not have it) — the server only has to recognize "this is the AHB I handed out" and then use the standard Android import: Espryt uses `eglGetNativeClientBufferANDROID` + `EGL_NATIVE_BUFFER_ANDROID`, Magma uses `VK_ANDROID_external_memory_android_hardware_buffer`. Both are required by the Android specification and vendor-neutral.
## 设备探针(HA27Q3LQ,内核 6.1,Android 15)
## Device probe (HA27Q3LQ, kernel 6.1, Android 15)
- AHB(RGBA8,GPU 用途,有/无 CPU 用途)的 native_handle = 2 个 fd,都是 `/dmabuf:` 文件(图像 + 32 KB 元数据)。
- 从 Android 侧(`shell` 域)经 SCM_RIGHTS 发给容器进程(`droidspacesd` 域):收到、`mmap`、像素逐点正确;两边 `st_ino` 相同 → **按 inode 认身份可行**。
- 容器里有 `/dev/dri/renderD128`(显示驱动 msm_drm,组 `droidspaces-gpu`),Mesa 26.3 的 libgbm 会按 `GBM_BACKEND` 或驱动名加载 `/usr/lib/gbm/<name>_gbm.so`(ABI v1,`gbm_backend_abi.h` 已装)。**但这个节点是厂商的,别的机器未必有**——见下面"薄节点"。
- KWin 6.7:`RenderDevice::open(path)` = `DrmDevice`(open+fstat+`gbm_create_device`) → `eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm)`;`initWayland` 的 tranche 设备号取 `renderDevNode()` 否则 `drmDevice->deviceId()`;dma-buf 导入走 `eglCreateImage(EGL_LINUX_DMA_BUF_EXT)`,格式表走 `eglQueryDmaBufFormats/ModifiersEXT`。
- An AHB (RGBA8, GPU usage, with and without CPU usage) has a native_handle of 2 fds, both `/dmabuf:` files (the image and 32 KB of metadata).
- Sent from the Android side (`shell` domain) to the container process (`droidspacesd` domain) over SCM_RIGHTS: received, `mmap`ed, pixels correct point by point; both sides report the same `st_ino` → **identity by inode works**.
- The container has `/dev/dri/renderD128` (display driver msm_drm, group `droidspaces-gpu`); Mesa 26.3's libgbm loads `/usr/lib/gbm/<name>_gbm.so` from `GBM_BACKEND` or the driver name (ABI v1, `gbm_backend_abi.h` installed). **But this node is vendor-specific and other machines may not have it** — see "thin node" below.
- KWin 6.7: `RenderDevice::open(path)` = `DrmDevice`(open + fstat + `gbm_create_device`) → `eglGetPlatformDisplayEXT(EGL_PLATFORM_GBM_KHR, gbm)`; `initWayland` takes the tranche device id from `renderDevNode()`, otherwise `drmDevice->deviceId()`; dma-buf import goes through `eglCreateImage(EGL_LINUX_DMA_BUF_EXT)`, and the format table through `eglQueryDmaBufFormats/ModifiersEXT`.
## 厂商中立的几条规则
## Vendor-neutral rules
1. **只分配 AHB**,只用公开 NDK 拿 fd:`AHardwareBuffer_sendHandleToUnixSocket` 发到 server 自己的 socketpair,`recvmsg` 取 SCM_RIGHTS 里的 fd(不依赖 LL-NDK 的 `getNativeHandle`,也不解析 handle 里的 int)。
2. **不假设哪个 fd 是图像**:发 fd[0] 作 plane 0,身份登记 handle 里的所有 fd。
3. **不假设布局**:默认通告 `DRM_FORMAT_MOD_INVALID`("驱动定义布局",正好就是 AHB 的语义)。`LINEAR` 只在启动自检通过时才通告:分配带 CPU 用途的 AHB,`lock` 写图案,`mmap` fd[0] 按 `describe().stride` 读回比对。自检失败就不通告,CPU 映射走 server(`AHardwareBuffer_lock` + 拷贝)。
4. **身份**:首选 `st_ino`(启动时分配两块确认 inode 互异——老内核所有 dma-buf 共用一个 anon inode);不唯一时退到 `DMA_BUF_SET_NAME` 打 `mgl:<id>` 标签、读 fdinfo 的 `name:`。非本 server 分配的 dma-buf → `EGL_BAD_MATCH`(KWin 会回退 shm)。
5. **同步只用 sync_file**:`EGL_ANDROID_native_fence_sync`、`VK_KHR_external_semaphore_fd`(SYNC_FD)、必要时 `DMA_BUF_IOCTL_EXPORT/IMPORT_SYNC_FILE`(通用 Linux ≥5.20/6.0)。不用 DRM syncobj(要真 DRM 驱动)。
6. **薄节点**:GBM 后端用 `GBM_BACKEND=mobilegl` 选中(`/usr/lib/gbm/mobilegl_gbm.so`),不装成 `msm_drm_gbm.so`。它对设备 fd 不发任何 ioctl,只当"打开了某个设备"的令牌;真正的通道是到 server 的连接。节点路径由 anland 配置(`MOBILEGL_GBM_NODE`):有可访问的 DRM 节点就借来当身份(只 open/fstat),没有就用占位节点。MobileGL 现有的假 EGL device 里写死的 `/dev/dri/renderD128` 默认值同样改成跟这个配置走。
1. **Allocate only AHBs**, and obtain fds only through the public NDK: `AHardwareBuffer_sendHandleToUnixSocket` to the server's own socketpair, `recvmsg` to take the fd from SCM_RIGHTS (no LL-NDK `getNativeHandle`, no parsing the ints inside the handle).
2. **Do not assume which fd is the image**: send fd[0] as plane 0 and register every fd in the handle for identity.
3. **Do not assume the layout**: advertise `DRM_FORMAT_MOD_INVALID` by default ("driver-defined layout", exactly AHB's semantics). Advertise `LINEAR` only when the startup self-test passes: allocate an AHB with CPU usage, `lock` and write a pattern, `mmap` fd[0] and read it back using `describe().stride` for comparison. If the self-test fails, do not advertise it, and CPU mappings go through the server (`AHardwareBuffer_lock` + copy).
4. **Identity**: prefer `st_ino` (allocate two at startup to confirm the inodes differ — old kernels share one anon inode across all dma-bufs); when not unique, fall back to tagging with `DMA_BUF_SET_NAME` as `mgl:<id>` and reading `name:` from fdinfo. A dma-buf not allocated by this server → `EGL_BAD_MATCH` (KWin falls back to shm).
5. **Synchronize only through sync_file**: `EGL_ANDROID_native_fence_sync`, `VK_KHR_external_semaphore_fd` (SYNC_FD), and when necessary `DMA_BUF_IOCTL_EXPORT/IMPORT_SYNC_FILE` (generic Linux ≥5.20/6.0). No DRM syncobj (that requires a real DRM driver).
6. **Thin node**: the GBM backend is selected with `GBM_BACKEND=mobilegl` (`/usr/lib/gbm/mobilegl_gbm.so`), not installed as `msm_drm_gbm.so`. It issues no ioctl on the device fd; the fd is only a token meaning "some device is open", and the real channel is the connection to the server. The node path comes from anland configuration (`MOBILEGL_GBM_NODE`): if an accessible DRM node exists, borrow it as identity (open/fstat only); otherwise use a placeholder node. MobileGL's existing fake EGL device likewise stops hardcoding `/dev/dri/renderD128` and follows this configuration.
## 归属:这事主要是 MobileGL 的
## Ownership: mostly MobileGL's job
| 部分 | 在哪 | 内容 |
| Part | Where | Content |
|---|---|---|
| 分配器 + 身份登记 | **MobileGL server**(Android 侧) | AHB 分配、fd 导出、inode 登记、会话生命周期/引用计数、隐式同步(server 看得见两端,就当"内核") |
| 后端导入 | **MobileGL Espryt / Magma** | AHB → EGLImage → 纹理/渲染缓冲;AHB → VkImage(每会话各自 VkDevice,各导一次) |
| 生产端 WSI | **MobileGL 客户端 EGL**(glibc) | `wl_egl_window` 改为 AHB 交换链 + `zwp_linux_dmabuf_v1`,没有该 global 时退回现在的 wl_shm |
| 消费端 EGL | **MobileGL 客户端 EGL** | `EGL_EXT_image_dma_buf_import(_modifiers)`、`eglQueryDmaBuf*`、真 `eglCreateImage`(现在是空壳)、`glEGLImageTargetTexture2DOES`/`RenderbufferStorageOES` |
| GBM 后端 | **MobileGL 新组件** `mobilegl_gbm.so`(glibc) | gbm ABI v1:`bo_create*` → server 分配,`bo_import`,`bo_map`,格式查询;外加 EGL 的真 GBM 平台(现在 GBM 窗口被拒) |
| 合成器 | anland | 后端打开薄节点建 `RenderDevice`;撤掉 `initWayland` 跳过 dma-buf 与 screencast 两处补丁;启动脚本设 `GBM_BACKEND`/`MOBILEGL_GBM_NODE` |
| Allocator + identity registry | **MobileGL server** (Android side) | AHB allocation, fd export, inode registry, session lifetime / refcounts, implicit synchronization (the server sees both ends and acts as the "kernel") |
| Backend import | **MobileGL Espryt / Magma** | AHB → EGLImage → texture / renderbuffer; AHB → VkImage (one VkDevice per session, one import each) |
| Producer-side WSI | **MobileGL client EGL** (glibc) | `wl_egl_window` becomes an AHB swapchain + `zwp_linux_dmabuf_v1`, falling back to the current wl_shm when that global is absent |
| Consumer-side EGL | **MobileGL client EGL** | `EGL_EXT_image_dma_buf_import(_modifiers)`, `eglQueryDmaBuf*`, a real `eglCreateImage` (currently a stub), `glEGLImageTargetTexture2DOES`/`RenderbufferStorageOES` |
| GBM backend | **New MobileGL component** `mobilegl_gbm.so` (glibc) | gbm ABI v1: `bo_create*` → server allocation, `bo_import`, `bo_map`, format queries; plus a real GBM platform for EGL (GBM windows are currently rejected) |
| Compositor | anland | The backend opens the thin node and creates a `RenderDevice`; drop the two patches that skip dma-buf in `initWayland` and fix screencast; the startup script sets `GBM_BACKEND`/`MOBILEGL_GBM_NODE` |
ANativeWindow 只剩 KWin 输出那一块,不变。
ANativeWindow remains only for KWin's output; unchanged.
## 分阶段
## Phases
**P0 去风险(小)**
- server 真实所在域 `untrusted_app` → 容器 `droidspacesd` 传 dma-buf fd 过 SELinux(ashmem 段已经能过,dma-buf 类别要实测;看 `avc: denied`)。
- 确认 libgbm 在 `GBM_BACKEND` 指定时不对 fd 发 ioctl(读 Mesa 26.3 `backend.c` 或拿 `/dev/null` 实测)。
- 确认 KWin 拿非 DRM 的设备号(占位节点)时 tranche/feedback 不出事。
- Magma 跨 VkDevice 同一 AHB 两次导入可用;Espryt 同一 AHB 两个 context 的 EGLImage。
**P0 de-risk (small)**
- Pass a dma-buf fd from the server's real domain `untrusted_app` to the container's `droidspacesd` across SELinux (ashmem segments already pass; the dma-buf class needs measuring; watch `avc: denied`).
- Confirm libgbm does not issue ioctls on the fd when `GBM_BACKEND` is specified (read Mesa 26.3 `backend.c` or measure with `/dev/null`).
- Confirm KWin's tranche/feedback does not break when it gets a non-DRM device id (placeholder node).
- Magma imports the same AHB twice across VkDevices; Espryt creates EGLImages for the same AHB in two contexts.
**P1 server 分配器 + 登记表**
- `SharedImageRegistry`:`Allocate(w,h,format,usage) → {id, fds, stride, modifier}`;按 inode(或 name 标签)查找;归属会话 + 引用计数;会话死掉释放(键用单调 id,不用客户端句柄——P14 的跨会话句柄冲突教训)。
- 线协议新动词:分配 / 释放 / 按 fd 导入(fd 走现成的 `ITransport::ShareFd`)。
- 单元测试:注册、身份、跨会话释放只放自己的。
**P1 server allocator + registry**
- `SharedImageRegistry`: `Allocate(w,h,format,usage) → {id, fds, stride, modifier}`; look up by inode (or name tag); owning session + refcount; release when the session dies (key on a monotonic id, not a client handle — the P14 cross-session handle collision lesson).
- New wire verbs: allocate / release / import by fd (the fd uses the existing `ITransport::ShareFd`).
- Unit tests: registration, identity, and that a cross-session release only frees its own.
**P2 生产端:wl_egl_window 走 AHB 交换链**
- 先做最稳的一步:server 照旧渲染进 pbuffer/目标,`eglSwapBuffers` 时**GPU blit**(带 Y 翻转)到当前空闲 AHB——CPU 零拷贝,只多一次 GPU 拷贝。之后再让默认帧缓冲直接落在 AHB 上(Magma 已有默认 FB 变换机制;Espryt 用 FBO 冒充默认 FB)。
- 客户端:绑定 `zwp_linux_dmabuf_v1`,每块 AHB 建一次 `wl_buffer`(`create_immed`,`ABGR8888`/`XBGR8888` = GL RGBA8 内存序,不再换序);按 `wl_buffer.release` 轮转 3 块;resize 重建。没 global → 现路径。
- 同步第一版:swap 返回前 server CPU 等自己的渲染完成(现在 readback 本来就全等,不退步);server 记录每块的"最后消费者 fence",生产者重用前等它。
**P2 producer side: wl_egl_window over an AHB swapchain**
- Do the safest step first: the server still renders into the pbuffer/target, and on `eglSwapBuffers` **GPU-blits** (with a Y flip) into the currently free AHB — zero CPU copies, one extra GPU copy. Later let the default framebuffer land directly on the AHB (Magma already has a default-FB transform mechanism; Espryt emulates the default FB with an FBO).
- Client: bind `zwp_linux_dmabuf_v1`, create one `wl_buffer` per AHB (`create_immed`, `ABGR8888`/`XBGR8888` = the GL RGBA8 memory order, no more swizzling); rotate three buffers on `wl_buffer.release`; rebuild on resize. Without the global → current path.
- Synchronization, first version: before swap returns, the server CPU-waits for its own rendering (readback already waits fully today, so no regression); the server records each buffer's "last consumer fence" and the producer waits for it before reuse.
**P3 消费端:KWin 导入**
- 客户端 EGL 通告上述扩展;`eglCreateImage(EGL_LINUX_DMA_BUF_EXT)` 把 fd 发给 server → 认身份 → 后端导入;`glEGLImageTargetTexture2DOES` 绑到 KWin 的纹理。
- anland:后端建真 `DrmDevice`(薄节点 + `mobilegl_gbm.so`),撤补丁,linux-dmabuf 起来。这一步和 P2 合起来就是第一个可见成果:plasmashell/Qt/GTK/glmark2-wayland 零拷贝。
**P3 consumer side: KWin import**
- Client EGL advertises the extensions above; `eglCreateImage(EGL_LINUX_DMA_BUF_EXT)` sends the fd to the server → identity → backend import; `glEGLImageTargetTexture2DOES` binds it to KWin's texture.
- anland: the backend creates a real `DrmDevice` (thin node + `mobilegl_gbm.so`), drops the patches, and linux-dmabuf comes up. Together with P2 this is the first visible result: zero-copy plasmashell/Qt/GTK/glmark2-wayland.
**P4 GBM 后端**
- `mobilegl_gbm.so` + EGL 的 GBM 平台(`eglGetPlatformDisplay(GBM)`、`eglCreateImage` 从 gbm bo)。KWin 的 `GbmGraphicsBufferAllocator` 随之可用 → screencast 走 dma-buf,screencast 补丁可撤。
- 格式:ARGB8888 等 AHB 没有的通道序,按 RGBA8 分配,server 记"声明格式",导入时用纹理 swizzle 对齐语义(两端都是 MobileGL,server 都看得见)。
**P4 GBM backend**
- `mobilegl_gbm.so` + EGL's GBM platform (`eglGetPlatformDisplay(GBM)`, `eglCreateImage` from a gbm bo). KWin's `GbmGraphicsBufferAllocator` then works → screencast over dma-buf, and the screencast patch can be dropped.
- Formats: channel orders AHB does not have, such as ARGB8888, are allocated as RGBA8; the server records the "declared format" and aligns semantics with a texture swizzle on import (both ends are MobileGL and the server sees both).
**P5 同步与性能**
- 去掉 swap 时的 CPU 等待:sync_file 在 server 内做 GPU 等(Espryt `EGL_ANDROID_native_fence_sync` + `eglWaitSyncKHR`;Magma sync-fd 信号量跨会话设备)。
- 可选 `DMA_BUF_IOCTL_IMPORT_SYNC_FILE` 给外部 CPU 映射者隐式同步。
- `EGL_EXT_buffer_age` + 真实损伤区,Qt 局部重绘。
**P5 synchronization and performance**
- Remove the CPU wait at swap: sync_file lets the server do the GPU wait (Espryt `EGL_ANDROID_native_fence_sync` + `eglWaitSyncKHR`; Magma sync-fd semaphores across session devices).
- Optional `DMA_BUF_IOCTL_IMPORT_SYNC_FILE` for implicit synchronization with external CPU mappers.
- `EGL_EXT_buffer_age` + real damage regions, for Qt partial repaints.
**P6 其它消费者(按需)**
- Xwayland glamor 跑在 MobileGL 的 GBM 平台上(现在强制 llvmpipe + `-shm`)。
- Chrome 原生 GBM 路径(它会 `drmGetDeviceFromDevId` 枚举节点——占位节点下可能不行,是风险项;现在的 wl_egl_window 路径在 P2 后已经零拷贝)。
**P6 other consumers (as needed)**
- Xwayland glamor on MobileGL's GBM platform (today it is forced to llvmpipe + `-shm`).
- Chrome's native GBM path (it enumerates nodes with `drmGetDeviceFromDevId` — may not work under a placeholder node, a risk; its current wl_egl_window path is already zero-copy after P2).
## 风险
## Risks
- SELinux:dma-buf fd 从 `untrusted_app` 进 `droidspacesd`(P0 第一项)。
- 方向:GL 默认帧缓冲自下而上、`wl_buffer` 自上而下。P2 的 blit 顺手翻转;直接渲染时可选用 `wl_surface.set_buffer_transform(FLIPPED_180)`(竖直翻转,KWin 着色器里免费),需实测 KWin 处理正确。
- 每会话一个 VkDevice(Magma):同一 AHB 在两个设备上,同步只能靠 sync_file 或 CPU 等。
- 生命周期:客户端崩溃时 KWin 仍持有它的 `wl_buffer`/EGLImage——AHB 引用要由导入者也持有,不能随生产会话一起释放。
- 资源:每窗口 3 块 AHB(2560×1412 约 44 MB),比现在的 pbuffer + 3 块 shm 少不了多少,但不再需要回复槽带宽。
- SELinux: dma-buf fds from `untrusted_app` into `droidspacesd` (the first P0 item).
- Orientation: the GL default framebuffer is bottom-up, `wl_buffer` is top-down. The P2 blit flips for free; when rendering directly, consider `wl_surface.set_buffer_transform(FLIPPED_180)` (a vertical flip, free in KWin's shader), which needs measuring that KWin handles it correctly.
- One VkDevice per session (Magma): the same AHB on two devices can only synchronize through sync_file or a CPU wait.
- Lifetime: when the client crashes, KWin still holds its `wl_buffer`/EGLImage — the importer must also hold an AHB reference, so it cannot be released with the producer session.
- Resources: three AHBs per window (about 44 MB at 2560×1412), not much less than today's pbuffer + three shm buffers, but the reply-slot bandwidth is gone.
@@ -1,120 +1,120 @@
# Runbook:在这台 Windows 机器 + 平板上起 / 迭代 MobileGL + anland + KDE Plasma(2026-10-02)
# Runbook: bring up and iterate MobileGL + anland + KDE Plasma on this Windows machine and the tablet (2026-10-02)
> 操作手册,不是设计笔记。配套 Claude Code skill:`.claude/skills/anland-mobilegl-plasma/`(`SKILL.md` 是本文的精简版,`scripts/` 是本文引用的全部脚本,每个脚本开头注明了参数)。现状、提交与已知问题见 [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md),零拷贝的设计见 [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md)。下文路径与命令按 2026-10-02 设备上的实际状态核对过;标"未验证"的是没跑通过或只从旧记录推出来的。
> An operations manual, not design notes. Companion Claude Code skill: `tools/anland-mobilegl-plasma/` (`SKILL.md` is the condensed version of this document, and `scripts/` holds every script it references, each with its parameters at the top). For status, commits and known issues see [`handoff-legacy-mobilegl-unified.md`](handoff-legacy-mobilegl-unified.md); for the zero-copy design see [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md). The paths and commands below were checked against the device state on 2026-10-02; items marked "unverified" have not been run through or are inferred only from older records.
**设备可能正被别的会话使用。** 凡是会重启 app / daemon / 容器会话、或替换库的操作,先跑只读的 `scripts/device/status.sh` 看一眼,确认没人在测;`ct.sh` 的通道名用自己的(别人在用 c、c2、c3、xd、xr、gbm)。
**The device may be in use by another session.** For anything that restarts the app / daemon / container session or replaces a library, first run the read-only `scripts/device/status.sh` and check that nobody is testing; use your own `ct.sh` channel name (others use c, c2, c3, xd, xr, gbm).
## 架构
## Architecture
- **平板**:Lenovo TB321FU(Y700,Adreno 750,Android 15,内核 6.1),**KernelSU** root(`/data/adb/ksu`、`ksud`;不是 Magisk),`su -c` 可用。USB 序列号 `HA27Q3LQ`,也常走无线调试 `ip:port`。
- **anland APK** `com.anland.consumer.mobilegl`(anland 分支 `legacy-mobilegl-unified` 的 debuggable 构建):UI 进程 + 私有 Service 进程 `:mobilegl`(`MobileGLWorker`)。后者加载 **server** `libMobileGL.so`(NDK 构建,MobileGL 分支 `feat/disaggregated`),从第一个 attach 上来的窗口起监听抽象 socket `@anland-mobilegl`。一个进程服务所有 client(KWin、plasmashell、ksplash、Chrome……),一个进程只服务一种后端。`MobileGLWorker` 是**前台服务**(通知"Linux 桌面正在运行",带"停止桌面"按钮),窗口隐藏、锁屏、从最近任务划掉之后 server 和会话都还在。
- **后端**:app 设置"渲染后端"(设置 > 连接 > MobileGL 桌面;存在 `/data/data/<包名>/files/mobilegl-backend`,默认 DirectGLES;启动参数 `--es mobilegl_backend X` 也会写它)。Espryt = DirectGLES,Magma = DirectVulkan。server 进程启动时读一次;app 把它发布成 `/data/local/tmp/anland-mobilegl/backend`,会话启动前拷进容器的 `/etc/mobilegl/backend`——所有 client 按这个文件报后端,不一致 server 拒绝("Hello.backendType disagrees with pinned backend")。如果有人设了 `debug.mobilegl.backend`,它在下次重启前仍然覆盖设置。
- **实验用 display daemon**:`/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock`(显示 / 输入中介),由 app 拉起(见下)。模块的 `service.sh` 开机只起原版那个(`/data/local/tmp/display_daemon.sock`),原版不要动。
- **容器** `arch-kde-mgl`:Droidspaces 6.4.5,Arch Linux ARM(aarch64 glibc),host 网络;Android 侧 `rootfs.img` 挂在 `/mnt/Droidspaces/arch-kde-mgl`。bind mount:`/data/local/tmp/anland-mobilegl` → `/run/anland-mobilegl`,`/data/local/tmp/display_daemon.sock` → `/run/display.sock`。
- **会话**:`desktop-session.service`(User=swung0x48,**已 enable**:容器一启动就进会话,和原版镜像一样)。drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`(anland 的 `producers/kde/Arch_v5/desktop-session-mobilegl.conf`,由 `sync-build-kwin.sh` 安装并 enable):先 `mobilegl-startup.sh wait`(等 `/proc/net/unix` 里 `@anland-mobilegl` 在监听、daemon socket 存在),再以 root 跑 `sync-backend`,`Restart=always`,ExecStart 是 `/opt/mobilegl/bin/mobilegl-startup.sh plasma`——它删掉粘滞的软件渲染键,整个会话设 `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0`(MobileGL 永不拒绝,glvnd 不会把 KWin / plasmashell 交给别的 GL 栈),它每次写 `plasma-kwin_wayland.service` 的用户 drop-in(经 `kwin_wayland_wrapper` 起 `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`,`ANLAND_MOBILEGL=1`、`MOBILEGL_IPC_SURFACE=server`),然后 `startplasma-wayland`。
- **KWin 6.7.4** 带 anland 的 `anland` 后端,经 server 直接画进 anland 的 Android Surface。窗口不在屏幕上(consumer 断开)时后端把输出关掉(DPMS Off + RenderLoop inhibit:不合成、不发 frame callback),窗口回来再打开。其余 GL 进程都是 `MOBILEGL_IPC_SURFACE=offscreen`,经 KWin 合成上屏。
- **client 库** `/opt/mobilegl/lib/libMobileGL.so`(glibc aarch64),是容器里**全系统**的 GL vendor(见下节):EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json`(排在发行版的 `50_*.json` 前面,MobileGL 拒绝时由它接手)、GLX vendor `/usr/lib/libGLX_mobilegl.so.0`,配置来自 `/etc/mobilegl/client.conf`(`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`)+ `/etc/mobilegl/backend`。进程不需要任何专门的环境变量。
- **零拷贝窗口**:client 的 `wl_egl_window` 帧是 server 分配的 AHardwareBuffer(shared image),以 dma-buf fd 经 `zwp_linux_dmabuf_v1` 交给 KWin;KWin 用 EGL 导入,server 按 inode 认出是自己发出的那块。KWin 的 DRM 设备来自 `GBM_BACKEND=mobilegl` → `/usr/lib/gbm/mobilegl_gbm.so`(节点只当身份,从不 ioctl)。没有 linux-dmabuf 时退回 wl_shm 回读。
- **X11**(Xwayland)走 MobileGL 的 GLX vendor,每帧回读 + `xcb_put_image`。Chrome 用 `mobilegl-startup.sh chrome`:ANGLE GLES 跑在 MobileGL EGL 上,GPU 在浏览器进程内。
- **Tablet**: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), **KernelSU** root (`/data/adb/ksu`, `ksud`; not Magisk), `su -c` works. USB serial `HA27Q3LQ`, but wireless debugging `ip:port` is also common.
- **anland APK** `com.anland.consumer.mobilegl` (debuggable build of the anland branch `legacy-mobilegl-unified`): the UI process plus a private Service process `:mobilegl` (`MobileGLWorker`). The latter loads the **server** `libMobileGL.so` (NDK build, MobileGL branch `feat/disaggregated`) and listens on the abstract socket `@anland-mobilegl` from the first window that attaches. One process serves every client (KWin, plasmashell, ksplash, Chrome, ...), and one process serves only one backend. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running", with a "Stop desktop" button); after the window is hidden, the screen locks, or the app is swiped from recents, the server and the session are still there.
- **Backend**: the app's "render backend" setting (Settings > Connection > MobileGL desktop; stored in `/data/data/<package>/files/mobilegl-backend`, default DirectGLES; the launch argument `--es mobilegl_backend X` also writes it). Espryt = DirectGLES, Magma = DirectVulkan. The server process reads it once at startup; the app publishes it to `/data/local/tmp/anland-mobilegl/backend`, and it is copied into the container's `/etc/mobilegl/backend` before a session starts — every client reports the backend from that file, and the server refuses a mismatch ("Hello.backendType disagrees with pinned backend"). If someone set `debug.mobilegl.backend`, it still overrides the setting until the next restart.
- **Experimental display daemon**: `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (the display / input intermediary), started by the app (see below). The module's `service.sh` only starts the original one at boot (`/data/local/tmp/display_daemon.sock`); leave the original alone.
- **Container** `arch-kde-mgl`: Droidspaces 6.4.5, Arch Linux ARM (aarch64 glibc), host networking; on the Android side `rootfs.img` is mounted at `/mnt/Droidspaces/arch-kde-mgl`. Bind mounts: `/data/local/tmp/anland-mobilegl` → `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` → `/run/display.sock`.
- **Session**: `desktop-session.service` (User=swung0x48, **enabled**: the container enters the session as soon as it starts, like the stock image). The drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland's `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed and enabled by `sync-build-kwin.sh`): first `mobilegl-startup.sh wait` (wait for `@anland-mobilegl` to listen in `/proc/net/unix` and for the daemon socket), then run `sync-backend` as root; `Restart=always`; ExecStart is `/opt/mobilegl/bin/mobilegl-startup.sh plasma` — it deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` for the whole session (MobileGL never declines, so glvnd never hands KWin / plasmashell to another GL stack), writes the user drop-in for `plasma-kwin_wayland.service` on every start (starting `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4` through `kwin_wayland_wrapper`, with `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`), then runs `startplasma-wayland`.
- **KWin 6.7.4** with anland's `anland` backend draws straight into anland's Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers the output down (DPMS Off + RenderLoop inhibit: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
- **Client library** `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64) is the **system-wide** GL vendor in the container (next section): EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which takes over when MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. Processes need no per-process environment variables.
- **Zero-copy windows**: the client's `wl_egl_window` frames are server-allocated AHardwareBuffers (shared images), handed to KWin as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognizes its own buffer by inode. KWin's DRM device comes from `GBM_BACKEND=mobilegl` → `/usr/lib/gbm/mobilegl_gbm.so` (the node is only an identity and is never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
- **X11** (Xwayland) goes through MobileGL's GLX vendor, with per-frame readback + `xcb_put_image`. Chrome uses `mobilegl-startup.sh chrome`: ANGLE GLES on MobileGL EGL, with the GPU inside the browser process.
## 首次准备(Windows 主机)
## First-time setup (Windows host)
- Git Bash、`adb`(SDK platform-tools)、PATH 上有 CMake ≥ 3.22 与 Ninja(本机 CMake 3.27.6、Ninja 1.13.2);`rec.sh` 另需 Python 3 + Pillow。
- Android NDK **27.2.12479018**,默认 `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018`(`ANDROID_NDK` 可覆盖)。
- WSL 发行版 **archlinux**:clang、lld、llvm、cmake、ninja。**lld 的大版本必须等于 clang / llvm-libs**(曾经 lld 23 配 llvm 22,`ld.lld` 直接跑不起来;现在全是 22.1.6)。然后导出一次 sysroot:`bash scripts/xsysroot/refresh-sysroot.sh`(得到 WSL 里的 `~/sysroots/arch-kde-mgl` 与 `~/sysroots/aarch64-arch.cmake`,glibc 2.43、gcc 16;容器升级 glibc / gcc / vulkan / libdrm / gbm 后要重跑,再 `xbuild.sh --reconfigure`)。
- MobileGL 用独立 worktree(主 checkout 和它的构建目录是共用的,别动):`GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`,`git submodule update --init --recursive`,再从主 checkout 拷 `3rdparty/glslang/External/spirv-tools`(submodule 初始化后是空的,configure 会报找不到 SPIR-V tools)。
- anland worktree,分支 `legacy-mobilegl-unified`(KWin patch / 后端源码、启动脚本、`mobilegl-tools/`)。
- 设备侧已经存在、本 runbook **不负责从零搭**的:APK(按 anland `consumers/anland_v5/android_consumer/MOBILEGL.md`,gradle `:app:assemblePlainDebug`,`MOBILEGL_DIST` 指向 server 库,`-PmobileglApplicationId=com.anland.consumer.mobilegl`;换签名重装会换 UID,要重做 KernelSU 授权,见 `mobilegl-tools/ksu_grant.c`)、anland 的 KernelSU 模块(`anland-daemon`、`anland-awl`)、容器本身,以及容器里配置好的 KWin 构建目录 `/root/mobilegl-build/kwin-6.7.4/build` 和原版 KWin 源码 `/root/gbm-dev/kwin-a/src`。从零重建这些未验证。
- 装一次辅助脚本(容器要在跑):`bash scripts/push-tools.sh`——`device/*.sh` 到 `/data/local/tmp/anl`,`mgrun` 到容器 `/usr/local/bin`,`kwprep.sh` 到容器 `/root`。
- Git Bash, `adb` (SDK platform-tools), CMake ≥ 3.22 and Ninja on PATH (this machine has CMake 3.27.6, Ninja 1.13.2); `rec.sh` additionally needs Python 3 + Pillow.
- Android NDK **27.2.12479018**, default `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (`ANDROID_NDK` overrides).
- WSL distro **archlinux**: clang, lld, llvm, cmake, ninja. **lld's major version must match clang / llvm-libs** (once lld 23 was paired with llvm 22 and `ld.lld` would not run; everything is 22.1.6 now). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (produces `~/sysroots/arch-kde-mgl` and `~/sysroots/aarch64-arch.cmake` in WSL; glibc 2.43, gcc 16; rerun after the container upgrades glibc / gcc / vulkan / libdrm / gbm, then `xbuild.sh --reconfigure`).
- Use a separate worktree for MobileGL (the main checkout and its build directory are shared; do not touch them): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, then copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule is empty after initialization and configure reports missing SPIR-V tools).
- anland worktree, branch `legacy-mobilegl-unified` (KWin patch / backend sources, startup scripts, `mobilegl-tools/`).
- Already present on the device, and this runbook does **not** cover building from scratch: the APK (per anland's `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug`, `MOBILEGL_DIST` pointing at the server library, `-PmobileglApplicationId=com.anland.consumer.mobilegl`; reinstalling with a different signature changes the UID and requires redoing the KernelSU grant, see `mobilegl-tools/ksu_grant.c`), anland's KernelSU modules (`anland-daemon`, `anland-awl`), the container itself, and the container's configured KWin build directory `/root/mobilegl-build/kwin-6.7.4/build` and stock KWin sources `/root/gbm-dev/kwin-a/src`. Rebuilding these from scratch is unverified.
- Install the helper scripts once (the container must be running): `bash scripts/push-tools.sh` — `device/*.sh` to `/data/local/tmp/anl`, `mgrun` to the container's `/usr/local/bin`, `kwprep.sh` to the container's `/root`.
主机脚本都在 Git Bash 里 `bash scripts/<x>.sh` 运行。直接敲 adb 时,带设备路径要加 `MSYS_NO_PATHCONV=1`(否则 `/data/...` 被改写成 Git 安装目录),root 命令整个包进一层引号:`adb shell 'su -c "..."'`。设备脚本也可以不推送直接跑:`MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`。
Host scripts all run from Git Bash as `bash scripts/<x>.sh`. When typing adb commands directly, add `MSYS_NO_PATHCONV=1` for device paths (otherwise `/data/...` is rewritten to the Git installation directory), and wrap root commands in one more layer of quotes: `adb shell 'su -c "..."'`. Device scripts can also run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
## 启动、闲置、停止——没有手动步骤
## Start, idle, stop — no manual steps
开机什么都不起(容器 `run_at_boot=0`)。**打开 app**(桌面图标,等同 `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`,不带参数;APK 的默认 socket 是实验 daemon 那个,见 `-PanlandDefaultSocket`)会调 `MobileGLDesktop.ensureStarted()`——唯一的触发点——启动前台服务,并以 root(`su`)跑 APK 里的 `assets/mobilegl-desktop.sh up`:daemon 不在就起(脚本先离开 app 的 cgroup,app 被杀时 daemon 和容器不受牵连)→ 发布后端 → 容器没跑就启动 → `systemctl start --no-block desktop-session`。窗口等 daemon socket 出现;Surface attach 后 server 开始监听;会话的 `wait` 放行,Plasma 起来。全都在跑时再打开只是重新挂上窗口。日志:`/data/local/tmp/anland-mobilegl/desktop.log`,logcat `AnlandMobileGL`("MobileGL desktop up done in N ms")。
Nothing starts at boot (the container has `run_at_boot=0`). **Opening the app** (desktop icon; equivalent to `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no arguments; the APK's default socket is the experimental daemon one, see `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` — the only trigger — which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): start the daemon if absent (the script first leaves the app's cgroup, so killing the app does not take the daemon and container with it) → publish the backend → start the container if not running → `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server starts listening after the Surface attaches; the session's `wait` proceeds and Plasma comes up. Opening it again while everything runs only re-attaches the window. Logs: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
窗口隐藏、熄屏、锁屏、从最近任务划掉:server 与会话都留着(前台服务);KWin 记 "viewer gone: output powered down" 然后闲置,回来时记 "viewer back"。**停止桌面**(通知按钮,或 `am startservice -n <包名>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`)跑 `mobilegl-desktop.sh down`(停会话、停容器,daemon 留着),关窗口,结束 `:mobilegl` 进程。
Hiding the window, screen off, lock screen, swiping from recents: the server and session stay (foreground service); KWin logs "viewer gone: output powered down" and idles, and "viewer back" on return. **Stop desktop** (notification button, or `am startservice -n <package>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (stops the session and container, leaves the daemon), closes the window, and ends the `:mobilegl` process.
```sh
adb connect <ip>:<port> # 只有无线调试才需要
adb connect <ip>:<port> # only needed for wireless debugging
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # 重启后:点亮、解开锁屏、打开 app、打印各步耗时
bash scripts/desktop-verify.sh push cold reopen idle # 冷启动计时、划掉再打开、可见/隐藏/锁屏的 CPU 与 GPU
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, unlock, open the app, print each step's time
bash scripts/desktop-verify.sh push cold reopen idle # cold-start timing, swipe and reopen, CPU and GPU while visible/hidden/locked
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` 没有 `status` 子命令,`droidspaces show` 列出在跑的容器。`device/run-plasma.sh` 是开发用的重启(会话 + logind 用户,再重开 app),重新构建之后需要——KWin 不会重连新的 server 进程。`device/desktop-reset.sh` 把一切停到"刚重启"的状态。以前的手动序列(setprop、起容器、`restart-daemon.sh`、`run-plasma.sh`)不再需要;`restart-daemon.sh` 只留作调试 daemon。
`droidspaces` has no `status` subcommand; `droidspaces show` lists the running containers. `device/run-plasma.sh` is the development restart (session + logind user, then reopen the app), needed after a rebuild — KWin does not reconnect to a new server process. `device/desktop-reset.sh` stops everything back to "just rebooted". The old manual sequence (setprop, start the container, `restart-daemon.sh`, `run-plasma.sh`) is no longer needed; `restart-daemon.sh` remains only for debugging the daemon.
## 构建与部署循环
## Build and deploy loop
### server(Android,NDK)→ 替换 APK 里的库
### Server (Android, NDK) → replace the library in the APK
```sh
bash scripts/build-android.sh # → <worktree>/build-android/libMobileGL.so(RelWithDebInfo,不 strip,约 320 MB,留着给 addr2line)
bash scripts/cyc.sh s1 # 删粘滞软件渲染键 → 推库 → 换进 APK → 重启会话 → 截图 $ANL_DIR/s1.png → 打印 fatal
bash scripts/build-android.sh # → <worktree>/build-android/libMobileGL.so (RelWithDebInfo, not stripped, ~320 MB, keep it for addr2line)
bash scripts/cyc.sh s1 # delete the sticky software-renderer key → push the library → swap it into the APK → restart the session → screenshot $ANL_DIR/s1.png → print fatals
```
`device/swap.sh` 直接 root 覆盖 `/data/app/.../lib/arm64/libMobileGL.so`,保留 owner / 权限 / SELinux context,第一次运行把原件存为 `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`;不重签 APK(重签要重装,UID 会变,授权要重做)。只换库不跑 cyc:推到 `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`,`su -c "sh /data/local/tmp/anl/swap.sh"`,之后照样要 `run-plasma.sh`——server 进程换了,KWin 不会重连。
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` directly as root, preserving owner / mode / SELinux context, and saves the original as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig` on first run; it does not re-sign the APK (re-signing requires reinstalling, which changes the UID and requires redoing the grant). To replace only the library without running cyc: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, and still run `run-plasma.sh` afterwards — the server process changed and KWin will not reconnect.
### client + GBM 后端(容器 glibc),WSL 交叉编译
### Client + GBM backend (container glibc), cross-compiled in WSL
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64;全量约 150 秒,增量几秒
bash scripts/xdeploy.sh # 新 inode 改名就位安装 libMobileGL.so + mobilegl_gbm.so,刷新别名、GLX 链接、vendor JSON、client.conf 和 GLX/GBM 环境 drop-in
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; a full build takes about 150 seconds, incremental a few
bash scripts/xdeploy.sh # install libMobileGL.so + mobilegl_gbm.so under a new inode via atomic rename, refresh aliases, GLX links, vendor JSON, client.conf and the GLX/GBM environment drop-in
```
已经在跑的 client 继续映射旧 inode:重启那个 client,KWin / plasmashell 就 `run-plasma.sh`。server 与 client **同一个 commit** 构建(两边 stamp 都取 `git rev-parse HEAD`),否则日志里 WARN "compatible wire with different build"。容器内构建是备用路径(慢):`scripts/sync-src.sh` 同步改动的文件,再在容器里跑 anland 的 `mobilegl-tools/anland-build-client.sh`。
A running client keeps mapping the old inode: restart that client, and for KWin / plasmashell run `run-plasma.sh`. Build the server and client from the **same commit** (both stamps take `git rev-parse HEAD`); otherwise the log warns "compatible wire with different build". Building inside the container is the fallback (slow): `scripts/sync-src.sh` syncs the changed files, then run anland's `mobilegl-tools/anland-build-client.sh` in the container.
### KWin(anland 后端 / kwin.patch),在容器里编
### KWin (anland backend / kwin.patch), built in the container
```sh
bash scripts/kwin-stage.sh build # 去 CR 后暂存 patch + 后端 + 启动脚本,跑 kwprep.sh,后台起 sync-build-kwin.sh
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # 等到 "== ALL DONE =="
bash scripts/kwin-stage.sh build # strip CR, stage the patch + backend + startup scripts, run kwprep.sh, start sync-build-kwin.sh in the background
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # wait for "== ALL DONE =="
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
```
两处暂存是因为两个容器脚本读的地方不同:`kwprep.sh` 读容器 `/root/kwsync.tgz`,把 `kwin.patch` 重新打到原版 KWin 源码的副本上,只把内容变了的文件拷进活树(保持 ninja 增量);anland 的 `sync-build-kwin.sh`(`producers/kde/Arch_v5/`)读 `/run/anland-mobilegl/kwin-sync/`,只同步后端源码,`ninja -j4`,再以原子改名安装 `kwin_wayland`、`libkwin.so.6.7.4`、打过补丁的 screencast 插件和 `/opt/mobilegl/bin/mobilegl-startup.sh`,不重启合成器。仓库根的 `libdisplay_producer/` 两个脚本都不同步。`kwin-stage.sh` 是把这两步拼起来的,还没整体跑过。
There are two staging areas because the two container scripts read different places: `kwprep.sh` reads the container's `/root/kwsync.tgz`, re-applies `kwin.patch` to a copy of the stock KWin sources, and copies only files whose content changed into the live tree (keeping ninja incremental); anland's `sync-build-kwin.sh` (`producers/kde/Arch_v5/`) reads `/run/anland-mobilegl/kwin-sync/`, syncs only the backend sources, runs `ninja -j4`, then installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` with an atomic rename, without restarting the compositor. Neither script syncs the repository-root `libdisplay_producer/`. `kwin-stage.sh` combines the two steps, but has not been run end to end.
### 单元测试(不用设备)
### Unit tests (no device)
这个分支的 MSVC 构建是坏的,在 WSL 里编:
The MSVC build of this branch is broken; build in WSL:
```sh
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<repo>/tools/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` 编并跑全部单元测试。DirectVulkan 集成测试永远不要跑;两个 ctest 不要同时跑。
`wsl-all.sh` builds and runs every unit test. Never run the DirectVulkan integration tests; never run two ctests at once.
## 切换后端
## Switching the backend
在 app 里:设置 > 连接 > MobileGL 桌面 > 渲染后端,然后通知里"停止桌面",再打开 app。或者 `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`,做的是同一件事(用启动参数保存设置、STOP 服务、重新打开;顺手清掉残留的 `debug.mobilegl.backend`)。不用 setprop,也不用改容器里的文件:会话下次启动时把发布的后端拷进 `/etc/mobilegl/backend`。`cyc.sh` 不改后端。
In the app: Settings > Connection > MobileGL desktop > render backend, then "Stop desktop" in the notification, then open the app again. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same thing (save the setting through the launch argument, STOP the service, reopen; it also clears a leftover `debug.mobilegl.backend`). No setprop and no editing files in the container: the next session copies the published backend into `/etc/mobilegl/backend`. `cyc.sh` does not change the backend.
## 全系统 vendor 与回退
## System-wide vendor and fallback
`xdeploy.sh`(以及 anland 的 `anland-build-client.sh`)安装下面这些,容器里任何进程经原版 libglvnd / libgbm 就用上 MobileGL:
`xdeploy.sh` (and anland's `anland-build-client.sh`) installs the following, so any process in the container that goes through stock libglvnd / libgbm uses MobileGL:
| 文件 | 作用 |
| File | Role |
|---|---|
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor,先于系统的 `50_*.json` 被问 |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | 同一份 JSON,留给 `__EGL_VENDOR_LIBRARY_FILENAMES` 单进程强制 |
| `/etc/mobilegl/client.conf` | 所有 client 的 `KEY=value` 默认值(transport、data、endpoint),每次部署重写 |
| `/etc/mobilegl/backend` | 后端(`switch.sh` 写);比 client.conf 里的 `MOBILEGL_BACKEND_TYPE` 优先 |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`、`GBM_BACKEND=mobilegl`,给 systemd 用户管理器(所有 Plasma 服务及其子进程,包括终端) |
| `/etc/profile.d/mobilegl.sh` | 同样两项,给登录 shell(`bash -l`、`su -l`、ssh、`droidspaces run bash -l`) |
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor, asked before the system's `50_*.json` |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | The same JSON, for forcing one process with `__EGL_VENDOR_LIBRARY_FILENAMES` |
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint), rewritten on each deploy |
| `/etc/mobilegl/backend` | The backend (written by `switch.sh`); takes precedence over `MOBILEGL_BACKEND_TYPE` in client.conf |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl`, for the systemd user manager (all Plasma services and their children, including terminals) |
| `/etc/profile.d/mobilegl.sh` | The same two, for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
优先级:环境变量 > `/etc/mobilegl/backend`(只管后端)> client.conf > 内置默认;`MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` 可指向别的文件(设为空即关闭);server 进程不读这些文件。
Precedence: environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in defaults; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` can point at other files (empty disables them); the server process does not read these files.
**回退**:真正拉起之前 client 先探一次配置的 endpoint(一次非阻塞 connect,`MOBILEGL_IPC_PROBE_TIMEOUT_MS` 默认 250,0 关闭;被拒的抽象 socket 约 0.2 ms 就有结果)。没有 server 时:EGL vendor 返回 `EGL_NO_DISPLAY`、设备数 0(glvnd 换下一个 vendor),GLX vendor 的 `__glx_Main` 失败(libGLX 回退到 Xwayland 报的 vendor),`mobilegl_gbm` 的 `create_device` 失败(libgbm 回退到设备自己的后端)。客户端查询(`eglQueryString(EGL_NO_DISPLAY)`、`eglGetProcAddress`)从不拉起会话。对别的后端创建的 GBM 设备,MobileGL 同样让开。`LIBGL_ALWAYS_SOFTWARE=1` 不再让它让开(918d40dd):那只是 Mesa 的软件渲染器选择,不是关掉 MobileGL 入口的理由;只要有 server,带这个变量的进程(包括被 KWin 补丁设了它的 Xwayland)照样由 MobileGL 服务。不停真 server 的演示:
**Fallback**: before actually bringing anything up, the client probes the configured endpoint once (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS` defaults to 250 and 0 disables it; a refused abstract socket answers in about 0.2 ms). Without a server: the EGL vendor returns `EGL_NO_DISPLAY` with 0 devices (glvnd moves to the next vendor), the GLX vendor's `__glx_Main` fails (libGLX falls back to the vendor Xwayland reports), and `mobilegl_gbm`'s `create_device` fails (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring up a session. MobileGL also steps aside for GBM devices created by another backend. `LIBGL_ALWAYS_SOFTWARE=1` no longer makes it step aside (918d40dd): that is only Mesa's software-renderer choice, not a reason to disable the MobileGL entry point; with a server present, processes carrying that variable (including Xwayland, which the KWin patch sets it for) are still served by MobileGL. A demonstration that does not stop the real server:
```sh
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor 变成另一个,约 0.1 s
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # the vendor becomes another one in about 0.1 s
```
## 跑测试 client 与验证
## Running test clients and verifying
在容器里以 root 执行(`printf '...' | bash scripts/ct.sh <通道>`),`mgrun offscreen <命令>` 以 swung0x48 身份在当前会话里起一个 client(日志 `/tmp/mgl-run.client.log`,`MGLOG=/tmp/x.log` 可换):
Run as root in the container (`printf '...' | bash scripts/ct.sh <channel>`); `mgrun offscreen <command>` starts a client as swung0x48 in the current session (log `/tmp/mgl-run.client.log`, override with `MGLOG=/tmp/x.log`):
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
@@ -122,57 +122,57 @@ timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome:`bash scripts/chrome-launch.sh <tag> [参数]`(日志 `/tmp/mgl-chrome-<tag>.client.log`,截图 `ch-<tag>.png`)。探针:`scripts/probes/texfmt.c`(各纹理格式是否保住 alpha)、`dmahash.c`(对某进程的 dma-buf 取样哈希);放进容器 `/root` 编,例如 `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`,用 `mgrun offscreen` 跑(之前的确切编译参数未验证)。
Chrome: `bash scripts/chrome-launch.sh <tag> [args]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`). Probes: `scripts/probes/texfmt.c` (whether texture formats preserve alpha), `dmahash.c` (hash a process's dma-bufs); compile them in the container's `/root`, for example `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, and run with `mgrun offscreen` (the exact older compiler flags are unverified).
视频:Chrome 走硬解(VaapiVideoDecoder)。容器的 VA-API 驱动(`msm_drm_drv_video.so`,V4L2 解码)自己分配 surface 并导出 NV12 dma-buf(单 fd,R8 + GR88 两层),Chrome 以 NV12/P010 EGLImage 绑到 `GL_TEXTURE_EXTERNAL_OES`。这种**外来** YUV dma-buf 平台导入不了,MobileGL 走明确标注的 CPU 拷贝兜底:server 映射它、每帧首次使用时拷进自己的 YUV AHB(`AHardwareBuffer_lockPlanes`),再在 GPU 上转换进纹理的 RGBA 层(Espryt:`GL_EXT_YUV_target` + 导入时的色彩 hint,`MOBILEGL_YUV_DRIVER_CONVERSION=1` 改用驱动的 external sampler;Magma:AHB external format 上的 `VkSamplerYcbcrConversion`)。server 自己分配的 NV12/P010(GBM)零拷贝。`MOBILEGL_CHROME_VIDEO_DECODE=software` 恢复 `--disable-accelerated-video-decode`。检查:容器里 `python3 scripts/probes/cdpmedia.py <port> <秒>`(CDP `Media.enable`,`kVideoDecoderName`),`scripts/probes/yuvprobe.c` 像素校验外来 NV12/P010 导入、逐帧刷新和 GBM NV12。KWin 自己的 NV12 路径(拆成 R8 + GR88 两个导入)不支持,KWin 不通告 YUV 格式。
Video: Chrome decodes in hardware (`VaapiVideoDecoder`). The container's VA-API driver (`msm_drm_drv_video.so`, V4L2 decode) allocates its own surfaces and exports NV12 dma-bufs (a single fd with R8 + GR88 layers), and Chrome binds them as NV12/P010 EGLImages to `GL_TEXTURE_EXTERNAL_OES`. The platform cannot import such **foreign** YUV dma-bufs, so MobileGL uses its explicitly-labeled CPU-copy fallback: the server maps the buffer, copies it into its own YUV AHB at first use in each frame (`AHardwareBuffer_lockPlanes`), then converts it on the GPU into the texture's RGBA level (Espryt: `GL_EXT_YUV_target` plus the import's color hints, with `MOBILEGL_YUV_DRIVER_CONVERSION=1` using the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 (GBM) is zero-copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` restores `--disable-accelerated-video-decode`. To check: in the container `python3 scripts/probes/cdpmedia.py <port> <seconds>` (CDP `Media.enable`, `kVideoDecoderName`), and `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh, and a GBM NV12 buffer. KWin's own NV12 path (split into two R8 + GR88 imports) is not supported, so KWin advertises no YUV formats.
结论成立要同时满足:
A conclusion holds only when all of these hold:
- client 日志是 `presented through linux-dmabuf shared images`,不是 `wl_shm`;出现 `could not be presented through a linux-dmabuf shared image` 说明退回了;
- client 在动的时候 `bash scripts/shot.sh g 2 1` 两张截图哈希不同——dma-buf 下分数"很快"可能是帧根本没上屏;
- `adb logcat -b crash -d` 与 `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` 干净——窗口冻住多半是 server 死了;
- 对 plasmashell 下结论前,粘滞软件渲染已关(见下),且 `/tmp/mgl-plasmashell.client.log` 是新写的。
- the client log says `presented through linux-dmabuf shared images`, not `wl_shm`; `could not be presented through a linux-dmabuf shared image` means it fell back;
- while the client animates, two `bash scripts/shot.sh g 2 1` screenshots hash differently — with dma-buf a "fast" score may mean frames never reached the screen;
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean — a frozen window usually means the server died;
- before concluding anything about plasmashell, the sticky software renderer is off (see below) and `/tmp/mgl-plasmashell.client.log` was freshly written.
参考(glmark2-es2-wayland 1280x720,零拷贝 vs wl_shm):Espryt 187 vs 49,Magma 158 vs 34。出乎意料的通过要复跑。
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. An unexpected pass must be rerun.
## 日志
## Logs
| 内容 | 位置 |
| Content | Location |
|---|---|
| KWin 的 client 侧 | 容器 `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`、`/tmp/mgl-ksplash.client.log`(用户 drop-in `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`、`/tmp/mgl-chrome-<tag>.client.log`(+ `/tmp/chrome-<tag>-out.log`) |
| server | `adb logcat -s MobileGL`;文件 `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| native 崩溃 | `adb logcat -b crash -d`;容器里 `coredumpctl list` |
| 会话 / KWin stderr | 容器 `journalctl -u desktop-session --no-pager`、`journalctl --no-pager --since -2min \| grep kwin_wayland` |
| KWin client side | container `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`, `/tmp/mgl-ksplash.client.log` (user drop-ins `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`, `/tmp/mgl-chrome-<tag>.client.log` (plus `/tmp/chrome-<tag>-out.log`) |
| server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| native crash | `adb logcat -b crash -d`; in the container `coredumpctl list` |
| session / KWin stderr | container `journalctl -u desktop-session --no-pager`, `journalctl --no-pager --since -2min \| grep kwin_wayland` |
| display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` 实际写 `/tmp/x.client.log`(server 转发的行进 `.server.log`),打开时截断,所以每个进程一个 base。client 构建的日志级别是 INFO。server 栈帧用 `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <偏移>` 符号化。
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` actually writes `/tmp/x.client.log` (lines forwarded by the server go to `.server.log`), truncating on open, so each process gets one base name. Client builds log at INFO. Symbolize server frames with `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <offset>`.
## 常见坑
## Common pitfalls
- **pkill -f 误杀自己**:`ct.sh` / `droidspaces run bash -lc "<脚本>"` 的进程参数里带着整段脚本,`pkill -f chrome` 会先把执行者杀掉。用 `pkill -x`。
- **/tmp 里 root 的文件**:root 跑 client 留下的日志,之后 swung0x48 打不开(日志不出现)。先以 root `rm -f`。
- **Plasma 软件渲染是粘滞的**:server 一崩,Plasma 就把 `[QtQuickRendererSettings] SceneGraphBackend=software` 写进 `~swung0x48/.config/kdeglobals`,之后 plasmashell 全用 CPU 渲染,截图照样正常(托盘里有"正在使用的软件渲染器"图标)。`mobilegl-startup.sh plasma` 每次会话启动都会删掉它;一个已经崩过的会话里,下结论前先重启会话(`run-plasma.sh`)。
- **锁屏**:自动锁屏已关(`kscreenlockerrc` 的 `[Daemon] Autolock=false`);真锁上只能输密码——这种 logind 会话不支持 lock-session,`device/unlock.sh` 的 unlock-session 能否解开未验证。测 greeter 用 `/usr/lib/kscreenlocker_greet --testing` 再杀掉。
- **CRLF**:本机 `core.autocrlf=true`,anland 的后端源码和 `sync-build-kwin.sh` 在 Windows checkout 里是 CRLF。凡是要在 Linux / Android 上执行的先 `tr -d '\r'`(`ct.sh`、`push-tools.sh`、`kwin-stage.sh` 已处理);skill 目录有 `.gitattributes` 强制 LF。
- **display daemon**:由 app 拉起;会话的 `wait` 步骤保证 KWin 不会在它之前启动(连不上时 KWin 报 "failed to connect to display daemon at /run/anland-mobilegl/display.sock",约 3 秒退出)。从 app 的 `su` 拉起的东西必须先离开 app 的 cgroup(`echo $$ > /sys/fs/cgroup/cgroup.procs`),否则 force-stop 会一并杀掉。
- **电量 / 连接**:电量低或熄屏(默认 30 秒)可能让 SurfaceView 被回收——`svc power stayon true`;无线调试的端口每次配对都变——脚本里不要写死 `-s HA27Q3LQ`,多设备时设 `ANDROID_SERIAL`。
- **容器与 Android 路径**:容器的 `/tmp` 和平板的 `/data/local/tmp` 不是一个地方;文件经 `/mnt/Droidspaces/arch-kde-mgl/root`(= 容器 `/root`)或 `/data/local/tmp/anland-mobilegl`(= 容器 `/run/anland-mobilegl`)中转。
- **正在映射的 .so 不要原地覆盖**(容器里或运行中的 APK):拷成新 inode 再 `mv -f`;APK 换库前先 force-stop。
- **输入注入**:普通 `adb shell input` 被拦,要 `su -c input`;anland 抓了触摸屏,`input tap` 无效时用 `input mouse tap x y`。不要对容器进程用 `debuggerd -b`(信号 35 会直接杀掉 glibc 进程)。
- **linux-dmabuf**:client 必须 `create` + roundtrip,不能 `create_immed`(KWin 对失败的立即导入回协议错误,直接断开 client)。
- **跨会话 bug**:某个 client 起来或退出后出现噪点 / 别的窗口的残影,先怀疑多会话 server 里的进程级状态(handle 是每 client 各自的,FBO / VAO 是每 context 的)。复现用 `kscreenlocker_greet --testing`。
- `/etc/environment` 经 pam_env(以及 `/usr/lib/environment.d/99-environment.conf` 进用户管理器)带进基线会话的驱动变量,`mobilegl-startup.sh` 里把它们 unset 了,改脚本时保留。
- **共用日志路径**:Xwayland 继承 KWin 的 `MOBILEGL_LOG_FILE_PATH`,进程写第一行日志就会打开(截断)那个文件。MobileGL 在不服务的进程里不写日志,但额外起的 client 要给自己的 `MOBILEGL_LOG_FILE_PATH`。
- **残留的单进程覆盖**:unit drop-in 或用户管理器环境(`systemctl --user show-environment`)里遗留的 `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` 仍比系统文件优先;`run-plasma.sh`(terminate-user)会清掉管理器里的。
- handoff 里提到的 `kwin_wayland` 日志 shim 与 `/tmp/mgl-kwin-<pid>.log` 已过时:`/opt/mobilegl/kwin/bin/kwin_wayland` 现在就是真二进制,KWin 的 client 日志在 `/tmp/mobilegl-compositor.client.log`。
- **pkill -f kills itself**: the process arguments of `ct.sh` / `droidspaces run bash -lc "<script>"` contain the whole script, so `pkill -f chrome` kills the executor first. Use `pkill -x`.
- **Root-owned files in /tmp**: logs left by a root-run client cannot be opened later by swung0x48 (no log appears). Remove them as root first (`rm -f`).
- **Plasma software rendering is sticky**: once the server crashes, Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`, and plasmashell then renders entirely on the CPU while screenshots still look fine (the tray shows a "software renderer in use" icon). `mobilegl-startup.sh plasma` deletes it at every session start; in a session that has already crashed, restart the session (`run-plasma.sh`) before any verdict.
- **Lock screen**: automatic locking is off (`[Daemon] Autolock=false` in `kscreenlockerrc`); a real lock can only be left by typing the password — this logind session type does not support lock-session, and whether `device/unlock.sh`'s unlock-session works is unverified. Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and then kill it.
- **CRLF**: this machine has `core.autocrlf=true`, so anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Anything that runs on Linux / Android must go through `tr -d '\r'` first (`ct.sh`, `push-tools.sh` and `kwin-stage.sh` already do); the skill directory has a `.gitattributes` forcing LF.
- **display daemon**: started by the app; the session's `wait` step ensures KWin does not start before it (when it cannot connect, KWin reports "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits after about 3 seconds). Anything started by the app's `su` must first leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`), otherwise force-stop kills it too.
- **Battery / connection**: low battery or the screen turning off (30 seconds by default) can reclaim the SurfaceView — `svc power stayon true`; the wireless debugging port changes on every pairing — never hardcode `-s HA27Q3LQ` in scripts, and set `ANDROID_SERIAL` when several devices are attached.
- **Container vs Android paths**: the container's `/tmp` and the tablet's `/data/local/tmp` are different places; move files through `/mnt/Droidspaces/arch-kde-mgl/root` (= container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
- **Do not overwrite a mapped .so in place** (in the container or a running APK): copy to a new inode and `mv -f`; force-stop the APK before replacing its library.
- **Input injection**: plain `adb shell input` is blocked, use `su -c input`; anland grabs the touchscreen, so when `input tap` does nothing use `input mouse tap x y`. Do not use `debuggerd -b` on container processes (signal 35 kills glibc processes outright).
- **linux-dmabuf**: the client must `create` + roundtrip, not `create_immed` (KWin answers a failed immediate import with a protocol error and disconnects the client).
- **Cross-session bugs**: after a client starts or exits, noise or another window's remnants usually mean process-level state in the multi-session server (handles are per client, FBOs / VAOs are per context). Reproduce with `kscreenlocker_greet --testing`.
- `/etc/environment` enters the baseline session through pam_env (and `/usr/lib/environment.d/99-environment.conf` into the user manager) with driver variables; `mobilegl-startup.sh` unsets them, so keep that when editing the script.
- **Shared log paths**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`, and the first log line a process writes opens (truncates) that file. MobileGL does not log in processes it does not serve, but extra clients need their own `MOBILEGL_LOG_FILE_PATH`.
- **Leftover single-process overrides**: `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager environment (`systemctl --user show-environment`) still beat the system files; `run-plasma.sh` (terminate-user) clears the manager ones.
- The `kwin_wayland` log shim and `/tmp/mgl-kwin-<pid>.log` mentioned in the handoff are obsolete: `/opt/mobilegl/kwin/bin/kwin_wayland` is the real binary now, and KWin's client log is `/tmp/mobilegl-compositor.client.log`.
## 规矩
## Rules
- 在 git worktree 里干活,不碰共用的主 checkout 和它的构建目录;worktree 要 `GIT_LFS_SKIP_SMUDGE=1`。
- 提交用 `git commit --no-verify`,一句话的短消息,**不加** `Co-Authored-By`。
- DirectVulkan 集成测试不跑(`ctest -E DirectVulkan`)。
- 源码和注释里不写外部项目名("Mesa"、"Zink" 等),只描述手法。
- 零拷贝路径只用厂商中立的设施:AHB、EGL_ANDROID_*、VK_ANDROID 的 AHB 导入、sync_file、dma-buf、libgbm ABI;不碰 kgsl / gralloc 私有布局 / 高通修饰符。
- 身份缓存用单调递增的生命期 id 作键,不用 GL 名字或指针。
- Work in a git worktree, never in the shared main checkout or its build directories; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
- Commit as `CONTRIBUTING.md` describes (no trailer), with `git commit --no-verify`.
- Do not run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
- Do not name external projects ("Mesa", "Zink", ...) in source code or comments; describe the technique.
- The zero-copy path uses only vendor-neutral facilities: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, the libgbm ABI; never kgsl / gralloc private layouts / Qualcomm modifiers.
- Key identity caches on a monotonic lifetime id, never a GL name or a pointer.
@@ -69,7 +69,7 @@ check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
cleanup
# The page: WebGL clears that change colour every frame plus a moving block, so a frame that
# The page: WebGL clears that change color every frame plus a moving block, so a frame that
# reaches the screen changes the screenshot.
ct <<IN
cat > $PAGE <<'HTML'
+1 -1
View File
@@ -19,7 +19,7 @@
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
# ANDROID_SERIAL Honored by adb itself; leave unset when only one device is attached
# (USB serial or a wireless "ip:port").
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
@@ -15,9 +15,9 @@
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its color hints, bound to
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
// back: each bar must come out its color. The buffer is then rewritten with the bars in
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
// again: the new order must show (the server refreshes a foreign buffer at each frame).
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
+1 -1
View File
@@ -137,7 +137,7 @@ framebuffer** returns all zeros, with no GL error, both before and after
`eglSwapBuffers`. `DirectGLES` on the identical window is correct, and readback
from a **user FBO is correct on both backends**.
Verified on two SoCs and two drivers, so this is MobileGL's behaviour rather than
Verified on two SoCs and two drivers, so this is MobileGL's behavior rather than
a driver quirk:
| device | GPU | driver | default-FB | user FBO |
+1 -1
View File
@@ -17,7 +17,7 @@
* \file
* \brief MobileGL platform.
*
* Modelled on the surfaceless platform, but adapted to MobileGL, which ships
* Modeled on the surfaceless platform, but adapted to MobileGL, which ships
* its own EGL implementation inside libMobileGL.so:
*
* - Every EGL call goes through the dynamically loaded library. The
+1 -1
View File
@@ -338,7 +338,7 @@ int main(int argc, char** argv) {
printf(" user-FBO incomplete status=0x%x\n", fbst);
}
/* FBO with a RENDERBUFFER colour attachment. This is what dEQP's
/* FBO with a RENDERBUFFER color attachment. This is what dEQP's
* FboRenderContext allocates for --deqp-surface-type=fbo, so it is the path
* that actually decides a conformance run - a texture-attached FBO working
* says nothing about it. */
+1 -1
View File
@@ -7,7 +7,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Summarise any number of GL CTS suites and MobileGL backends.
"""Summarize any number of GL CTS suites and MobileGL backends.
Each repeatable suite specification consists of four values: backend, label,
caselist, and result directory. For example::
+1 -1
View File
@@ -7,7 +7,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Summarise dEQP/glcts .qpa logs into a conformance pass rate.
"""Summarize dEQP/glcts .qpa logs into a conformance pass rate.
Handles the two ways a case can end in a .qpa: a normal
``#beginTestCaseResult``/``#endTestCaseResult`` pair carrying a
+1 -1
View File
@@ -838,7 +838,7 @@ def execute(args: argparse.Namespace) -> int:
if empty_streak >= args.max_empty_streak:
print(
f"[run_cts_windows] aborting after {empty_streak} consecutive chunks made no "
"case progress; no unobserved case was labelled Crash/Hang",
"case progress; no unobserved case was labeled Crash/Hang",
file=sys.stderr,
)
break
+1 -1
View File
@@ -142,7 +142,7 @@ complete one.
| Flag | Why it is not optional |
| --- | --- |
| `--deqp-surface-type=fbo` | On DirectVulkan, `glReadPixels` from the **default framebuffer returns all zeros** with no GL error. dEQP verifies nearly everything through `glReadPixels`, so rendering to the surface scores DirectVulkan near zero for a reason unrelated to conformance. Use it for **both** backends so the two numbers stay comparable. |
| `MOBILEGL_CTS_FBO_COLOR_TEXTURE=1` | **`--deqp-surface-type=fbo` alone is not enough.** dEQP's `FboRenderContext` allocates a *renderbuffer* colour attachment, and DirectVulkan returns zeros from a renderbuffer-attached FBO too — only a *texture*-attached FBO reads back correctly. This env var (a patch to `framework/opengl/gluFboRenderContext.cpp`, off by default) switches the attachment to a texture and isolates that single defect. Measured effect: `KHR-GL33.shaders.loops.for_constant_iterations.*` goes 0/62 → 62/62, and the whole-suite DirectVulkan conformance rate goes 46.15% → 72.74%. DirectGLES is bit-identical either way (93.05%), which is the control proving the switch is neutral where readback works. |
| `MOBILEGL_CTS_FBO_COLOR_TEXTURE=1` | **`--deqp-surface-type=fbo` alone is not enough.** dEQP's `FboRenderContext` allocates a *renderbuffer* color attachment, and DirectVulkan returns zeros from a renderbuffer-attached FBO too — only a *texture*-attached FBO reads back correctly. This env var (a patch to `framework/opengl/gluFboRenderContext.cpp`, off by default) switches the attachment to a texture and isolates that single defect. Measured effect: `KHR-GL33.shaders.loops.for_constant_iterations.*` goes 0/62 → 62/62, and the whole-suite DirectVulkan conformance rate goes 46.15% → 72.74%. DirectGLES is bit-identical either way (93.05%), which is the control proving the switch is neutral where readback works. |
| `--deqp-terminate-on-device-lost=disable` | Defaults to *enable*, which calls `glGetGraphicsResetStatus()` after every case. That is GL 4.5 / `KHR_robustness`, absent from GL 3.3 core, so the pointer is null and the process segfaults on the first case. Desktop drivers expose the extension, which is why upstream never trips on it. |
## Cases that take the device down
@@ -54,7 +54,7 @@ cmake --build "$MG/build-linux" --parallel "$(nproc)"
Add `-DCMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE=FALSE` when iterating. The
Release configuration turns LTO on, which makes every relink cost minutes for no
behavioural difference; a conformance number is identical either way.
behavioral difference; a conformance number is identical either way.
## Step 2 — get VK-GL-CTS and build glcts
+1 -1
View File
@@ -98,7 +98,7 @@ PROFILE_VERIFIED=0
require_verified_profile() {
# The default is UNVERIFIED. A profile that simply omits the key is a profile nobody has
# confirmed against its device, and defaulting it to "verified" would hand exactly the
# fail-open behaviour this guard exists to prevent to the most likely way a new profile is
# fail-open behavior this guard exists to prevent to the most likely way a new profile is
# written - by copying an existing one and editing the serial. The variable is reset to 0
# immediately before the profile is sourced, so this test reads the FILE and not the shell.
if [ "${PROFILE_VERIFIED:-0}" = "1" ]; then return 0; fi
+1 -1
View File
@@ -7,7 +7,7 @@
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
"""Render the perf-compare report tables from summary.json + per-run artefacts.
"""Render the perf-compare report tables from summary.json + per-run artifacts.
Usage: make_report.py [results_dir]
Defaults to <repo>/.trace-work/perf-compare (run_matrix.sh's output root).
+2 -2
View File
@@ -570,7 +570,7 @@ class Arm:
self.summary["boot_id"] = self.adb.boot_id()
self.summary["started_utc"] = datetime.now(timezone.utc).isoformat()
# PURGE THE PREVIOUS ARM'S ARTEFACTS BEFORE THIS ONE STARTS, and not after it ends.
# PURGE THE PREVIOUS ARM'S ARTIFACTS BEFORE THIS ONE STARTS, and not after it ends.
# The runner's result directory is keyed by case and backend, not by arm, so arm N+1
# overwrites arm N's benchmark.json in place - and if arm N+1 fails early it leaves arm N's
# file sitting there under arm N+1's name. `read_benchmark` cannot tell: it reads whatever is
@@ -715,7 +715,7 @@ class Arm:
json.dumps(self.summary, indent=2), encoding="utf-8")
def assert_arm(self):
"""Does the library's own log prove the arm it is labelled as?
"""Does the library's own log prove the arm it is labeled as?
Two questions, and the second is the one that matters for `spawn`:
1. did the transport resolve to what this arm claims (the ConfigLoader marker), and
+2 -2
View File
@@ -49,7 +49,7 @@ def steady_state_cpu(arm):
`tailFrames`:
- `medianFrameCpuMs` / `p95FrameCpuMs`: the client thread's own `clock_gettime(CLOCK_THREAD_
CPUTIME_ID)` deltas per frame, summarised over the tail. Host-side per-frame sampling cannot
CPUTIME_ID)` deltas per frame, summarized over the tail. Host-side per-frame sampling cannot
reproduce this - a 0.5 s poll interval cannot resolve an 8 ms frame - and it is the primary
metric (MEASUREMENTS.md section 9).
- `medianFrameMs` / `p95FrameMs`: wall time per frame, which is what the frame budget is.
@@ -240,7 +240,7 @@ def main():
# --- headline: frame time and per-thread CPU ------------------------------------------------
lines.append("### Frame time and per-thread CPU (best of N)")
lines.append("")
lines.append("The `frame cpu` column is the CLIENT thread's own per-frame CPU clock, summarised by the")
lines.append("The `frame cpu` column is the CLIENT thread's own per-frame CPU clock, summarized by the")
lines.append("device over the trailing `tailFrames` - the primary metric (MEASUREMENTS.md section 9).")
lines.append("`client total` / `server total` are the sampler's per-role CPU TOTALS over that repeat,")
lines.append("which includes the one-off shader bring-up frame; they are not per-frame rates. See")
+1 -1
View File
@@ -77,7 +77,7 @@ PROFILE_VERIFIED=0
require_verified_profile() {
# The default is UNVERIFIED. A profile that simply omits the key is a profile nobody has
# confirmed against its device, and defaulting it to "verified" would hand exactly the
# fail-open behaviour this guard exists to prevent to the most likely way a new profile is
# fail-open behavior this guard exists to prevent to the most likely way a new profile is
# written - by copying an existing one and editing the serial. The variable is reset to 0
# immediately before the profile is sourced, so this test reads the FILE and not the shell.
if [ "${PROFILE_VERIFIED:-0}" = "1" ]; then return 0; fi
+3 -3
View File
@@ -207,7 +207,7 @@ build-test/tools/trace_replay/mobilegl_trace_replay \
```
At each call boundary it walks every live framebuffer object (or only the named
ones), reads back every colour attachment and the depth attachment, and writes
ones), reads back every color attachment and the depth attachment, and writes
`fbo<N>-att<M>.png` / `fbo<N>-depth.png` plus a `manifest.txt` line per
attachment recording the attached object, size, internal format, component type
and per-channel min/max/mean and a content hash. Attachments are read as floats
@@ -326,7 +326,7 @@ case pass" but "which cases diverge, and is the divergence reproducible":
The archive exists because `.trace-work/android-retrace-result/<case>-<backend>/` is
keyed by case and backend and by nothing else: without `--archive-dir` the only
artefact that survives a three-repeat run is the last one, and "were the three
artifact that survives a three-repeat run is the last one, and "were the three
repeats the same picture?" becomes unanswerable after the fact.
```sh
@@ -539,7 +539,7 @@ its old local 180-second budget. A growing log does not bypass the explicit abso
On timeout the driver terminates and reaps the entire case process group,
including a child that ignores TERM after CMake exits, and stops the matrix to
avoid cascading Busy failures. Check the peer before resuming. Ctrl-C/TERM/HUP
also clean up the group and save a cancelled checkpoint.
also clean up the group and save a canceled checkpoint.
Each attempt has a new directory. `checkpoint.json` is written atomically, and
only a zero exit plus current-attempt successful result, matching backend/call/
+2 -2
View File
@@ -22,7 +22,7 @@
#endif
#include <vector>
// Dumps every colour attachment (and the depth attachment) of every live framebuffer
// Dumps every color attachment (and the depth attachment) of every live framebuffer
// object at a chosen call boundary, on both sides of a driver comparison. The intent is
// to name the first attachment whose contents diverge between two stacks; the manifest is
// formatted so that `diff` over two dump directories points straight at it.
@@ -350,7 +350,7 @@ namespace mobilegl_trace_dump {
return DrainErrors() == 0;
}
// Reads the attachment as floats regardless of its storage: normalised and float targets
// Reads the attachment as floats regardless of its storage: normalized and float targets
// convert on the way out, integer targets are read as integers and widened. The float view
// keeps out-of-[0,1] accumulation buffers legible in the statistics even though the PNG
// itself has to clamp.
+7 -7
View File
@@ -11,7 +11,7 @@
WHY THIS EXISTS. `result.json` carries ONE number per run: this attempt's actual against its
golden. P7-7 E0 asks a different question - "are three repeats of the same case on the same
device the same picture?" - and no artefact in the tree answers it, because every repeat
device the same picture?" - and no artifact in the tree answers it, because every repeat
overwrites the previous one's `.trace-work/android-retrace-result/<case>-<backend>/`. With the
repeats archived (`run_android_retrace_local.py --archive-dir`), this script computes the
missing pair: SSIM(repeat N, repeat 1) beside the recorded SSIM(repeat N, golden).
@@ -19,7 +19,7 @@ missing pair: SSIM(repeat N, repeat 1) beside the recorded SSIM(repeat N, golden
THE SSIM IS NOT A NEW ONE. It is a transcription of the retrace gate's own comparator,
`android-plugin/app/src/trace/cpp/trace_replay_core.cpp:630` (`ComputeChannelSsim`) and `:671`
(`ComputeRgbSsim`) - a single global window per channel, C1 = (0.01*255)^2, C2 = (0.03*255)^2,
the three colour channels averaged and alpha ignored - and the crop comes from the repeat's own
the three color channels averaged and alpha ignored - and the crop comes from the repeat's own
`result.json`, so the window is the one the gate scored. A different SSIM (a gaussian-windowed
one, say) would produce numbers that could not be read beside the recorded ones at all, which is
the whole reason this file does not import a library implementation.
@@ -56,7 +56,7 @@ class ImageError(ValueError):
# PNG decode. Deliberately stdlib-only (zlib): this script has to run in the device window on
# whatever interpreter is at hand, and an unavailable Pillow would turn "the repeats diverged"
# into "the tool did not run", which are not the same finding. The accepted subset is what
# libpng gives trace_replay_core after its own normalisation (ReadPngRgba, :278): 8- and 16-bit
# libpng gives trace_replay_core after its own normalization (ReadPngRgba, :278): 8- and 16-bit
# greyscale/RGB/greyscale+alpha/RGBA and 8-bit palette, non-interlaced. Anything else is
# REFUSED BY NAME rather than guessed at.
# ---------------------------------------------------------------------------------------------
@@ -166,9 +166,9 @@ def read_png_rgba(path):
if interlace != 0:
raise ImageError("interlaced PNG is not supported: " + str(path))
if color_type not in _CHANNELS:
raise ImageError("unsupported PNG colour type %d: %s" % (color_type, path))
raise ImageError("unsupported PNG color type %d: %s" % (color_type, path))
if depth not in (8, 16) or (color_type == 3 and depth != 8):
raise ImageError("unsupported PNG bit depth %d (colour type %d): %s"
raise ImageError("unsupported PNG bit depth %d (color type %d): %s"
% (depth, color_type, path))
channels = _CHANNELS[color_type]
sample_bytes = depth // 8
@@ -241,7 +241,7 @@ def channel_ssim(actual, golden, x0, y0, compare_width, compare_height, channel)
def rgb_ssim(actual, golden, x0, y0, compare_width, compare_height):
"""trace_replay_core.cpp:671 ComputeRgbSsim - the three colour channels, alpha ignored."""
"""trace_replay_core.cpp:671 ComputeRgbSsim - the three color channels, alpha ignored."""
total = 0.0
for channel in range(3):
total += channel_ssim(actual, golden, x0, y0, compare_width, compare_height, channel)
@@ -494,7 +494,7 @@ def self_test():
expect("inverted halves", compare_files(a, b)["ssim"], -0.9964064683569576)
if compare_files(a, b)["mismatch_pixels"] != width * height:
failures.append("inverted halves: every pixel should differ")
# The crop is honoured: the left half of `halves` is black and the left half of
# The crop is honored: the left half of `halves` is black and the left half of
# `inverse` is white, so cropping to it reproduces the black/white number.
cropped = compare_files(a, b, 0, 0, width // 2, height)["ssim"]
expect("cropped half", cropped, 9.999000099990002e-05)
@@ -65,7 +65,7 @@ BACKENDS = {
# canonical id - signed by whichever key was current when it was made - otherwise has to have that
# install destroyed before an A/B can run, which is destructive and, on a shared bench, rude.
#
# ABSENT MEANS THE CANONICAL ID, so every existing caller keeps the behaviour it had. What the
# ABSENT MEANS THE CANONICAL ID, so every existing caller keeps the behavior it had. What the
# override does NOT change: the Activity class, the intent action, the extras, the on-device app dir
# and the arm-proof markers all stay identical, so the lane under test is the real lane. The one
# thing that must move with it is `--package` in the same command, because trace-replay-ci.sh drives
@@ -292,7 +292,7 @@ def run_case(case, backend, extra_args=None, timeout_seconds=None, env_overrides
# P7-7 repeat archiving.
#
# WHY IT IS NOT OPTIONAL FOR E0. `.trace-work/android-retrace-result/<case>-<backend>/` is keyed
# by case and backend and by nothing else, so repeat 2 overwrites repeat 1 and the only artefact
# by case and backend and by nothing else, so repeat 2 overwrites repeat 1 and the only artifact
# that survives a three-repeat run is the last one. That makes "is this case's divergence
# REPRODUCIBLE, and are the three repeats the same picture?" - the first question P7-7 E0 asks -
# unanswerable after the fact, and the device window is the one place where re-running to find
@@ -446,8 +446,8 @@ def cpu_tail(report):
benchmark.json carries the WHOLE frameCpuTimesMs[] array precisely so that p99 - which the
device does not compute, and which the paired A/B publishes beside p50 - is a host-side
reduction over an artefact that already exists. The window is the same trailing tailFrames the
device summarised, so the numbers below sit beside the device's own without being about a
reduction over an artifact that already exists. The window is the same trailing tailFrames the
device summarized, so the numbers below sit beside the device's own without being about a
different set of frames; p50 is recomputed here by the device's own median rule, so it agrees
with medianFrameCpuMs on the same run rather than merely sitting next to it.
"""
@@ -700,9 +700,9 @@ def main():
return 2
if args.benchmark and args.transport != "monolith":
# REFUSED RATHER THAN RUN UNDER A NAME THAT LIES. run_benchmark_case passes args.env
# alone, so --benchmark --transport spawn has always produced a MONOLITH run labelled
# alone, so --benchmark --transport spawn has always produced a MONOLITH run labeled
# spawn (tools/device_bench/p6/README.md records the same pitfall). The knob the
# benchmark path does honour is --env, so say that instead of silently ignoring this.
# benchmark path does honor is --env, so say that instead of silently ignoring this.
print("--benchmark ignores --transport; pass the arm explicitly, e.g. "
f"--env MOBILEGL_TRANSPORT={args.transport} "
"--env MOBILEGL_IPC_ROLE_SPLIT_STATE=1 --env MOBILEGL_IPC_STRICT_ERRORS=1 "
+1 -1
View File
@@ -532,7 +532,7 @@ def main(argv=None):
# ledgers; otherwise the wrapper sees rc=124 but no
# results.json and cannot distinguish cancellation from a
# driver that never wrote evidence.
row.update(status="cancelled", returncode=130,
row.update(status="canceled", returncode=130,
completed_at_ns=time.time_ns())
row.setdefault("seconds", round((row["completed_at_ns"] - row["started_at_ns"]) / 1e9, 3))
atomic_json(checkpoint_path, checkpoint)
+1 -1
View File
@@ -371,7 +371,7 @@ if(DEFINED ENV{MOBILEGL_TRANSPORT} AND NOT "$ENV{MOBILEGL_TRANSPORT}" STREQUAL "
message(FATAL_ERROR
"MOBILEGL_TRANSPORT=$ENV{MOBILEGL_TRANSPORT} is set for ${split_case} and this "
"runner has no marker for it, so the run could not be checked. ConfigLoader "
"recognises monolith|inproc|spawn|unix:<path>|pipe:<name> and REFUSES the last "
"recognizes monolith|inproc|spawn|unix:<path>|pipe:<name> and REFUSES the last "
"two BY NAME, staying on monolith - a run that asked for one of those would "
"have replayed monolith. Add a row above when the transport lands; do not let "
"an unasserted transport reach a green.")
+2 -2
View File
@@ -272,7 +272,7 @@ class Archive(unittest.TestCase):
self.assertEqual({row["arm"] for row in report["rows"]}, {self.arm})
def test_a_repeat_that_produced_nothing_leaves_an_empty_directory_not_the_previous_one(self):
"""A failed repeat must not inherit the previous repeat's artefacts.
"""A failed repeat must not inherit the previous repeat's artifacts.
This is the concrete shape tools/device_bench/p6/README.md records for benchmark.json:
the result root is not cleared between runs, so a run that produced nothing leaves the
@@ -288,7 +288,7 @@ class Archive(unittest.TestCase):
kept = retrace.archive_repeat(archive, self.case, "DirectVulkan", 2)
self.assertFalse((kept / "result.json").exists())
report = ca.summarize(archive)
# Both artefacts are gone; the first missing one is the one named.
# Both artifacts are gone; the first missing one is the one named.
self.assertEqual(report["rows"][1]["error"], "no result.json")
self.assertNotIn("ssim_vs_first", report["rows"][1])
self.assertNotIn("actual_sha256", report["rows"][1])
+2 -2
View File
@@ -167,13 +167,13 @@ class MatrixTests(unittest.TestCase):
self.assertEqual(self.run_main(), 130)
checkpoint = self.checkpoint()
self.assertEqual(len(checkpoint), 1)
self.assertEqual(checkpoint[0]['status'], 'cancelled')
self.assertEqual(checkpoint[0]['status'], 'canceled')
self.assertEqual(checkpoint[0]['returncode'], 130)
self.assertGreaterEqual(checkpoint[0]['seconds'], 0)
self.assertIn('completed_at_ns', checkpoint[0])
result = matrix.read_json(self.out/'results.json')
self.assertEqual(len(result), 1)
self.assertEqual(result[0]['status'], 'cancelled')
self.assertEqual(result[0]['status'], 'canceled')
self.assertFalse(result[0]['resumed'])
self.assertEqual(self.run_main('--resume'), 0)
self.assertEqual(len(self.checkpoint()), 2)
+1 -1
View File
@@ -47,7 +47,7 @@ namespace {
<< " GPU completion and not just CPU submission (default: 1)\n"
<< " --benchmark-result=PATH Timing JSON output (default: OUTPUT/benchmark.json)\n"
<< " --dump-fbo-attachments CALL:DIR[:FBO,FBO,...]\n"
<< " At CALL, write every colour attachment and the depth\n"
<< " At CALL, write every color attachment and the depth\n"
<< " attachment of every live framebuffer object into DIR as\n"
<< " fbo<N>-att<M>.png / fbo<N>-depth.png, plus a manifest.txt\n"
<< " of formats and per-channel statistics. Repeatable.\n";