[Chore, Docs] (...): drop the archived notes and move the skill under tools

- the archive freeze rule goes with the directory
- code comments lose citations to the removed notes
This commit is contained in:
BZLZHH
2026-10-31 19:29:06 +08:00
parent b5d64175a1
commit 6e36bf2c1b
1064 changed files with 1022 additions and 200727 deletions
@@ -0,0 +1,3 @@
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
* text=auto eol=lf
+183
View File
@@ -0,0 +1,183 @@
---
name: anland-mobilegl-plasma
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
---
# MobileGL on anland KDE Plasma
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
Human-readable runbook (Chinese, same content): `reference/runbook-plasma.md`.
Background: `reference/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `reference/plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
parameters are documented at the top of each script and in `scripts/env.sh`.
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
## Architecture (10 lines)
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl` from the first attached window on. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running" / action "Stop desktop"), so the server and the session outlive a hidden, locked or swiped-away window.
3. Backend = the app setting "Renderer backend" (Settings > Connection > MobileGL desktop; file `/data/data/<pkg>/files/mobilegl-backend`, default DirectGLES; also `am start ... --es mobilegl_backend X`). Espryt = DirectGLES, Magma = DirectVulkan. The server reads it when its process starts; the app publishes it as `/data/local/tmp/anland-mobilegl/backend` and the session copies that into the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend"). `debug.mobilegl.backend`, if someone sets it, still overrides the setting until the next reboot.
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker), started by the app (below). The module only starts the original one on `/data/local/tmp/display_daemon.sock` at boot; leave that alone.
5. Container `arch-kde-mgl` (Droidspaces 6.4.5, Arch Linux ARM aarch64 glibc, host net, `rootfs.img` mounted at `/mnt/Droidspaces/arch-kde-mgl` on the Android side). Bind mounts: `/data/local/tmp/anland-mobilegl` -> `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` -> `/run/display.sock`.
6. `desktop-session.service` (User=swung0x48, **enabled**: the container boots straight into it, as the original image does) has drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed + enabled by `sync-build-kwin.sh`): `ExecStartPre=mobilegl-startup.sh wait` (until `@anland-mobilegl` listens in `/proc/net/unix` and the daemon socket exists), `ExecStartPre=+... sync-backend`, `Restart=always`, then `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` session-wide (MobileGL never declines, so glvnd never falls through to another GL stack), writes a user drop-in for `plasma-kwin_wayland.service` (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`) and runs `startplasma-wayland`.
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: 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.
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever 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`. No per-process environment is needed.
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
10. X11 (`reference/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
## First-time prerequisites (Windows host)
- Git Bash, `adb` (SDK platform-tools), CMake >= 3.22 and Ninja on PATH (seen: CMake 3.27.6, Ninja 1.13.2), Python 3 + Pillow (only `rec.sh`).
- Android NDK **27.2.12479018** at `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (override `ANDROID_NDK`).
- WSL distro **archlinux** with clang, lld, llvm, cmake, ninja. **lld's major version must equal clang/llvm-libs'** (lld 23 on llvm 22 broke `ld.lld`; working set: all 22.1.6). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (-> `~/sysroots/arch-kde-mgl` + `~/sysroots/aarch64-arch.cmake`; re-run after container package upgrades, then `xbuild.sh --reconfigure`).
- MobileGL worktree (never build in the shared main checkout): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, and copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule leaves it empty).
- anland worktree on `legacy-mobilegl-unified` (KWin patch/backend, startup script, `mobilegl-tools/`).
- Phone side that already exists and is NOT recreated by this skill: the APK (build: anland `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug` with `MOBILEGL_DIST`, `-PmobileglApplicationId=com.anland.consumer.mobilegl -PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock`, from Git Bash with `MSYS_NO_PATHCONV=1`; signed with this machine's `~/.android/debug.keystore`, so `adb install -r` works; reinstalling with another key changes the UID and needs the KernelSU grant again - `mobilegl-tools/ksu_grant.c`), the anland KernelSU modules (`anland-daemon`, `anland-awl`), the container with KWin's configured build tree `/root/mobilegl-build/kwin-6.7.4/build` and pristine `/root/gbm-dev/kwin-a/src`.
- Install the helpers once (container must be running): `bash scripts/push-tools.sh` (device/*.sh -> `/data/local/tmp/anl`, `mgrun` -> container `/usr/local/bin`, `kwprep.sh` -> container `/root`).
Run host scripts from Git Bash as `bash scripts/<x>.sh`. Raw adb with device paths needs `MSYS_NO_PATHCONV=1` and the whole root command in one quoted string: `adb shell 'su -c "..."'`. A device script can also be run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
## Starting, idling, stopping - no manual steps
Nothing starts at phone boot (container `run_at_boot=0`). **Opening the app** (launcher icon = `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`, no extras; the APK's default socket is the experiment's, `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` - the single trigger - which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): display daemon if absent (the script leaves the app's cgroup first, so it survives the app) -> backend published -> container started if stopped -> `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server listens once the Surface is attached; the session's `wait` step then lets Plasma start. A reopen with everything up only reattaches the window. Log: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
Hidden window, screen off, lock screen or swiped-away task: server and session stay (foreground service); KWin turns the workspace's DPMS off (logs "viewer gone: output powered down"; windows get xdg_toplevel `suspended`, no frame callbacks) and back on ("viewer back"). Client swaps are paced by frame callbacks (`MG_Impl/EGLImpl/FrameThrottle.h`): interval >= 1 waits like any Wayland EGL; interval 0 (Chrome) drops to ~1 fps once a callback goes unanswered for 200 ms.
Measured 2026-10-03 (Espryt, Chrome on webglsamples aquarium; % of one core, phone = all of Android): open-to-desktop cold 7.5-12.5 s (daemon 0.5, server 1.0, container 1.5, kwin ~5, plasmashell ~6-7), after a phone reboot 8.5 s, reopen of a running desktop 0.2 s; visible phone 249% / kwin 13 / chrome 99 / server 57 / GPU 22%, hidden 54 / 0 / 6 / 2 / 0%, locked 38 / 0 / 8 / 3 / 0%. Stop desktop 1.7 s. Wireless adb does not come back by itself after a reboot (re-enable it on the phone). **Stop desktop** (notification action, or `am startservice -n <pkg>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (session + container stopped, daemon stays), closes the windows, ends `:mobilegl`.
```sh
adb connect <ip>:<port> # only for wireless debugging
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, dismiss keyguard, open the app, print the timeline
bash scripts/desktop-verify.sh push cold reopen idle # timed cold start, swipe-away + reopen, CPU/GPU visible/hidden/locked
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` has no `status` command; `droidspaces show` lists running containers. `device/run-plasma.sh` is a developer restart (session + logind user, then the app), needed after a rebuild because KWin does not reconnect to a new server process. `device/desktop-reset.sh` takes everything down to the post-reboot state. The old manual sequence (setprop, start container, `restart-daemon.sh`, `run-plasma.sh`) is gone; `restart-daemon.sh` remains for daemon debugging only.
## Build and deploy loops
**Server (Android, NDK) -> APK lib**
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so (RelWithDebInfo, unstripped)
bash scripts/cyc.sh s1 # clear sticky SW-renderer key, push, swap into APK, restart session, screenshot $ANL_DIR/s1.png, print fatals
```
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` keeping owner/mode/SELinux context (original saved as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`); no re-signing. To swap without the session restart: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, then `run-plasma.sh` anyway (KWin does not reconnect to a new server process).
**Client + GBM backend (container glibc) via WSL cross-build**
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; ~150 s full, seconds incremental
bash scripts/xdeploy.sh # rename-into-place install of libMobileGL.so + mobilegl_gbm.so, aliases, vendor JSONs, client.conf, GLX/GBM env drop-ins
```
Running clients keep the old inode; restart the client, or `run-plasma.sh` for KWin/plasmashell. Build server and client from the **same commit** (both stamp `git rev-parse HEAD`), else the logs WARN "compatible wire with different build". Fallback in-container build (slow): `scripts/sync-src.sh` + anland `mobilegl-tools/anland-build-client.sh`.
**KWin (anland backend / kwin.patch) - built inside the container**
```sh
bash scripts/kwin-stage.sh build # stage CR-stripped patch+backend+startup script, kwprep.sh, background sync-build-kwin.sh
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"'
```
`sync-build-kwin.sh` (anland `producers/kde/Arch_v5/`) copies only the backend sources, then `ninja -j4`, installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` by atomic rename. `kwprep.sh` is what applies changed `kwin.patch` hunks to core KWin files. Repo-root `libdisplay_producer/` is not synced by either. `kwin-stage.sh` is assembled from those two scripts and was not run as one unit yet.
**Unit tests (no device)** - in WSL (the MSVC build of this branch is broken):
```sh
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` builds and runs every unit test. Never run DirectVulkan integration tests; never two ctest runs at once.
## Switching backends
In the app: Settings > Connection > MobileGL desktop > Renderer backend, then Stop desktop and open the app. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same (saves the setting through the launch extra, STOPs the service, reopens; clears a stale `debug.mobilegl.backend`). No setprop, no file to edit in the container: the session copies the published backend into `/etc/mobilegl/backend` on its next start. `cyc.sh` keeps whatever is set.
## System-wide vendor and fallback
Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any process in the container gets MobileGL through the stock libglvnd/libgbm:
| File | Does |
|---|---|
| `/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` | same JSON, for per-process forcing with `__EGL_VENDOR_LIBRARY_FILENAMES` |
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint); rewritten on deploy |
| `/etc/mobilegl/backend` | the backend (the session's `sync-backend` step copies the app's choice in); wins over a `MOBILEGL_BACKEND_TYPE` line 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 what they start, terminals included) |
| `/etc/profile.d/mobilegl.sh` | the same two for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created. It no longer steps aside for `LIBGL_ALWAYS_SOFTWARE=1` (918d40dd): that is Mesa's software-renderer selection, not a reason to disable MobileGL's entry points, so a process that sets it (a software Xwayland included) is still served by MobileGL while a server answers. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping 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 = the other one, ~0.1 s
```
## Test clients and how to verify
X11 on the GPU has its own driver, `scripts/x11-verify.sh` (preflight, measure before/after with glxgears at `vblank_mode=0` on both GLX paths (presentation throughput only: glxgears is fixed-function GL, which MobileGL does not implement, so its window is black by design), deploy-client/-kwin/-server, check, apps, stop); `scripts/device/x11-measure.sh` refuses to load the phone below 40% battery.
Inside the container as root (`printf '...' | bash scripts/ct.sh <chan>`), `mgrun offscreen <cmd>` runs a client as swung0x48 in the live session through a login shell, with nothing but the surface mode and the log path set (log `/tmp/mgl-run.client.log`, override base with `MGLOG=/tmp/x.log`); a plain `env -i ... bash -l -c '<cmd>'` as swung0x48 gets MobileGL the same way:
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
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.
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).
A result counts only if ALL hold:
- client log says `presented through linux-dmabuf shared images` (not `wl_shm`; `could not be presented through a linux-dmabuf shared image` = fell back);
- `bash scripts/shot.sh g 2 1` gives two different hashes while the client animates (a "fast" score can mean frames are not shown);
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean (a frozen window is usually a dead server);
- for plasmashell verdicts, the sticky software renderer is off (below) and `/tmp/mgl-plasmashell.client.log` is freshly written.
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. Rerun any surprising pass.
## Logs
| What | Where |
|---|---|
| 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` (+ `/tmp/chrome-<tag>-out.log`) |
| Server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| Native crashes | `adb logcat -b crash -d`; 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` |
| Desktop start/stop (app's su script) | `/data/local/tmp/anland-mobilegl/desktop.log`, `container-start.log`; logcat `-s AnlandMobileGL` |
| Session wait / backend sync | container `journalctl -u desktop-session --no-pager` ("waiting for the MobileGL server ...") |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` produces `/tmp/x.client.log` (and `.server.log` for forwarded lines); files are truncated on open, so one base per process. 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>`.
## Pitfalls
- **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.
- **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.
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
## House rules
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
- Commit as `CONTRIBUTING.md` describes (`[Type] (Scope): content`, no trailer), with `git commit --no-verify`.
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
@@ -0,0 +1,15 @@
# 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
`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.
| File | Original path |
|---|---|
| `handoff-legacy-mobilegl-unified.md` | `docs/archive/notes/anland/handoff-legacy-mobilegl-unified.md` |
| `plan-ahb-dmabuf.md` | `docs/archive/notes/anland/plan-ahb-dmabuf.md` |
| `plan-x11-gpu.md` | `docs/archive/notes/anland/plan-x11-gpu.md` |
| `runbook-plasma.md` | `docs/archive/notes/anland/runbook-plasma.md` |
@@ -0,0 +1,186 @@
# Handoff: anland 5.x + MobileGL unified server
Updated 2026-10-04. This replaces the 2026-10-02 (post-P14) handoff; git history keeps the old text.
## What this is
MobileGL runs disaggregated on a rooted Android tablet:
- **Client:** the glibc library in the Droidspaces container `arch-kde-mgl`, where it is the system-wide glvnd EGL/GLX vendor and the GBM backend.
- **Transport:** the client streams pipe records over abstract socket `@anland-mobilegl`.
- **Server:** runs in-process in the anland APK's `:mobilegl` foreground service.
- **Backends:** Espryt (DirectGLES) or Magma (DirectVulkan), chosen in the app settings.
KDE Plasma runs on top with zero-copy windows (server-allocated AHB dma-bufs over linux-dmabuf). Everything is vendor-agnostic: AHB, sync_file, EGL_ANDROID_*, VK_ANDROID_*; no kgsl or qcom specifics.
## Remotes and heads
- MobileGL: <https://github.com/MobileGL-Dev/MobileGL> `feat/disaggregated`, head `2d424146`. Integration worktree: `.claude/worktrees/anland-unified`.
- anland: <https://github.com/MobileGL-Dev/anland> `legacy-mobilegl-unified`, head `41ec066`. Integration worktree: `anland/.claude/worktrees/mgl-unified`.
- Version: 26.10 (`MobileGL/Config.h`, `android-plugin/app/build.gradle.kts`).
## Where to start reading
| Topic | Doc |
|---|---|
| Reproduce the whole setup on a new machine or phone | `.claude/skills/anland-mobilegl-plasma/reproduce.md` (and `SKILL.md`) |
| Day-to-day bring-up and checks | `runbook-plasma.md` |
| Zero-copy windows | `plan-ahb-dmabuf.md` |
| X11 on the GPU (glamor, DRI3/Present GLX) | `plan-x11-gpu.md` |
| glmark2 performance: baseline and the optimization handoff | `../../../Performance/glmark2-baseline.md`, `../../../Performance/glmark2-handoff.md` |
| VA-API driver on MediaCodec (plan only, nothing implemented) | `../../../MobileVA/plan.md` |
## State of the desktop (verified on the device, 2026-10-04)
- **App-driven bring-up:** opening the anland app starts daemon → server → container → Plasma. A foreground service keeps it alive, the server idles while the app is hidden (KWin DPMS off), and the notification has a "Stop desktop" action.
- **Both backends** run the desktop: KWin, plasmashell, Kickoff, notifications, the lock screen, resize on extra-keys-bar toggle, and suspend/resume on window loss.
- **Buffer age and damage** work end to end on both backends.
- **glvnd vendor:** MobileGL is the system-wide vendor (`10_mobilegl.json`, `/etc/mobilegl/client.conf`). It steps aside for `LIBGL_ALWAYS_SOFTWARE`, foreign GBM devices, and when no server answers (falls through in about 0.1 s).
- **X11:** Xwayland glamor on MobileGL. GLX presents through DRI3 + Present shared images, with a MIT-SHM readback fallback.
- **Chrome:**
- runs its GPU in a separate process with real EGL native fences, with no flicker;
- recovers from device loss (as do KWin and plasmashell);
- Shadertoy-style GPU faults are contained per session;
- a hanging client is cut off on Magma (watchdog `MOBILEGL_GPU_HANG_BUDGET_MS`) and on Espryt (GUILTY reset).
- **Chrome video:** hardware decode works through the container's VA-API driver. NV12/P010 dma-bufs are sampled via `GL_OES_EGL_image_external` on both backends. Foreign YUV buffers are CPU-copied into server YUV images, and 4K60 is fine. `MOBILEGL_CHROME_VIDEO_DECODE=software` opts out.
- **Device state at handoff:**
- MobileGL anland on Magma, Plasma up, server/client build `bb31f374`; later commits are docs, CI and a DirectGLES multi-session fix that is not yet deployed;
- clocks NOT pinned (restored); the original anland APK is stopped;
- pin and restore helpers: `su -c "sh /data/local/tmp/anl/pbclk.sh pin|restore"`.
## Work streams and where each one stopped
### 1. glmark2 performance (stopped at a handoff)
Details: `docs/Performance/glmark2-handoff.md`.
- **Now:** MobileGL beats the stock kgsl stack in windowed glmark2 on both backends (es2 800x600: Espryt ~1887, Magma ~1213, stock ~1100). Fullscreen is still about half of stock (Espryt ~561, Magma ~404, stock ~1177).
- **Next, ranked:**
1. render straight into the window's buffers (removes the fullscreen copy);
2. non-blocking present;
3. merge Magma's per-frame submissions;
4. Magma descriptor and pipeline caching;
5. profile terrain;
6. Espryt per-draw state sync;
7. non-blocking fences.
- 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.
- **Landed:**
| Commit | What |
|---|---|
| `eb71b669` | The pull and verify builds compile again |
| `c1e957cb` | Retired the P3a/P4a untouched-region gates; those phases closed and later work legitimately changed the regions |
| `78c5f8da` | Census records `Refuse{Recover}` as a local refusal |
| `32d27b14` | Real bug: a session's backend registration no longer trips the strict apply-thread pin while another session is applying (DirectGLES) |
| `2d424146` | Failed ctest entries are annotated with their failure lines, so failures can be read from the check-run annotations without a signed-in log reader |
- **Run on `2d424146`:** in progress at handoff. Gates, include-closure and flatc are green; the builds and integration jobs were still running.
- **Still open:**
- 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;
- anonymous-struct sampler handling in glslang;
- sampler CSO publishing;
- the scissor-shadow fix;
- the inactive-fragment-output location fix;
- the external-texture and YUV changes;
- the perf commits.
- **User guidance on judging checks:**
- phase-era process gates may be overly strict, and superseded ones may be removed with a reason;
- retraces and OpenGL-level checks must pass;
- a feature a backend does not implement (e.g. iterationRP) may be excluded narrowly per backend.
- **Reading CI without `gh`:** `curl https://api.github.com/repos/MobileGL-Dev/MobileGL/actions/runs?branch=feat/disaggregated`, then `/actions/runs/<id>/jobs` and `/check-runs/<job>/annotations`. Raw logs need sign-in.
### 3. Chrome hardware video (landed) → vendor-agnostic VA-API (planned only)
- **Container driver:** `/usr/lib/dri/msm_drm_drv_video.so` is an unpackaged third-party, Qualcomm-only driver. It drives V4L2 at `/dev/video32` with Qualcomm modes and copies frames on the CPU itself. libva picks it because the render node's DRM driver name is `msm_drm`.
- **Plan in `docs/MobileVA/plan.md`:** a new `MobileVA/` directory; MediaCodec decode in the `:mobilegl` process; H.264 first, then VP9, AV1, HEVC/10-bit; the old driver kept as A/B fallback.
- **Prerequisites the plan found:**
- per-plane R8/GR88 dma-buf import, which mpv's GL path and KWin's NV12 path need;
- YUV sampling paths that wait on the shared image's write fence (they appear not to).
### 4. MobileGL work landed since the previous handoff (newest first, grouped)
- **Perf:**
- `bb31f374` Magma keeps render passes open across draws;
- `e5ee31b0` android-30 for native TLS;
- `915882b2` record timing only with stats on;
- `963f4d81` Magma command buffer reuse;
- `d627fca3` the server wakes a parked client only when its sequence is applied;
- `2de0d6dd` client-array buffer reuse;
- `66b47a10` glFinish waits for the GPU, plus ES OES strings.
- **YUV:** `7218bacb` NV12/P010 shared images, `GL_OES_EGL_image_external`, Espryt `GL_EXT_YUV_target`, Magma Ycbcr conversion.
- **Device loss and hangs:**
- `287e1df8` cut off the session that hangs the GPU;
- `7823bcd9` client fresh-session recovery;
- `90e63c9e` robustness extensions;
- `58c3d9bd` per-session device-loss latch;
- `36190b7c`, `cf763b40`, `1dd888cb`, `f22ae625`, `f52a5467` loss and teardown edge cases.
- **Multi-thread and multi-context fixes:** `a9f22ebb`, `25a76c1b`, `393002a4`, `a84104a4`, `2acc6c41`, `8024b645`.
- **Chrome:**
- `4f6f4125` real EGL native fences;
- `30977c18` opt-in ES identity for ES contexts (the GPU process on ANGLE);
- `5accb098` glslang anonymous-struct samplers (Shadertoy page fault).
- **X11:**
- `198d6fea` GLX via DRI3/Present;
- `08f23543` glFlush implicit sync for glamor;
- `5de6ecd4`, `7fd965ce` shared-image upload and publish;
- `cd58f048` inactive fragment output location;
- `316ffe02` never dial its own X display.
- **glvnd:** `69e96f5b`, `4095624f`, `4f679d66`, `3d69c375`, `552dd3d3`.
- **Present:**
- `74919a3b` / `7090e31d` buffer age and damage;
- Espryt fixes `815217cc` (scissor shadow is per-session), `c822a9fe`, `228bd24c`;
- `1ff7ca53` / `caa00995` frame-callback pacing;
- `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);
- `51c37bcb` sampler param conversion;
- `15f9b27a` sampler CSO per share group;
- `239fc228` gl_FragCoord access chains;
- `bfa7987c` Magma declines invalid SPIR-V;
- `6125f941` surfaceless/no-config EGL;
- `4633ca22` frame-stats log gated (`MOBILEGL_SERVER_FRAME_STATS`);
- `2a1889b8` gtest discovery at ctest start.
### 5. anland work landed (newest first)
- `41ec066` / `f60b261`: Chrome video decode modes.
- `e48874c`: KWin recovers from its own session loss.
- `b9f8162` / `cf07bc7`: Chrome GPU process by default.
- `3d8c4dd` / `5d30340`: Xwayland glamor.
- `89894f1` / `a55e975`: DPMS off while hidden.
- `74bd2d3`: the app starts the desktop, foreground service.
- `0fd5d01` / `144f55c`: APK build guards.
- `90c44d7`: system-wide vendor.
- `23fc73f`: buffer age and damage in KWin.
- `9773d1e` / `2dfda4a`: resize follow.
- `f9e41df`: GBM device for linux-dmabuf.
- `5f15240`: backend choice.
## Known open issues (rough priority)
1. **CI:** retrace failures (see stream 2) and whatever the `2d424146` run still shows red.
2. **Fullscreen performance** at about half of stock (see stream 1).
3. **Espryt's ~10 s Preemption-Fault hang path:** the driver doesn't report the reset to the server's contexts, so a hanging client stalls the desktop for 10 s per hang there.
4. **YUV gaps:**
- YUV sampling doesn't wait on the write fence (suspected);
- no single-channel (R8/GR88) dma-buf import, so KWin advertises no YUV formats and mpv's GL interop won't work;
- the CPU copy for foreign YUV buffers is a fallback, not zero-copy.
5. **Chrome** doesn't use buffer age (full-frame repaints). The Magma text-caret frame costs about 24 ms.
6. **A rare Magma X11 rectangle glitch.** The readback GLX fallback is slower with glamor.
7. **Two GoldControllerTest failures** on Windows hosts. MSVC builds of this branch are broken; build tests in WSL with clang.
8. **Leftovers:**
- container probe files under `~swung0x48/yuvtest` and the local `v.html` tabs in the Chrome profile;
- an empty directory `.claude/worktrees/quirky-poincare-df3f3e` (locked by Windows);
- WSL `~/mgl-espryt*` / `~/mgl-magma*` directories of unknown owner (kept).
## Rules this work follows
- **Commits:** `--no-verify`, one short sentence, no Co-Authored-By trailer. No external project names in code or comments.
- **Tests:** never run the DirectVulkan integration tests (`ctest -E DirectVulkan`); never run two ctest suites at once.
- **Worktrees:** one worktree and build directory per task; remove the worktree, branch and build directories right after merging. Keep `anland-unified`, `mgl-unified`, WSL `~/mgl-xbuild-a64`, `~/mgl-anl-bld` and `~/sysroots`. Don't touch `wgl-host` or `~/repos/mobilegl-cts-gl33`.
- **Core profile only:** don't use fixed-function test apps such as glxgears.
- **Device:**
- stop device work while the user is using the phone;
- on USB power, watch the battery (stop below 40%);
- wireless adb needs re-enabling after a reboot.
@@ -0,0 +1,95 @@
# 计划:AHB 当 dma-buf,server 当分配器(窗口零拷贝)(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 私有布局 / 厂商修饰符。
## 现状:每个窗口每帧 GPU→CPU→GPU
KWin 自己(`MOBILEGL_IPC_SURFACE=server`)直接画在 Android Surface 上,没问题。其余所有 Wayland 客户端:
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 的空指针崩溃补丁)。
2560×1412 的窗口一帧 14.5 MB,上面合计 ~8–10 次整帧 CPU 遍历 + 若干阻塞往返。
## 目标形状
```
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
```
关键点:**生产者和消费者都是同一个 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 规范要求的,厂商中立。
## 设备探针(HA27Q3LQ,内核 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`。
## 厂商中立的几条规则
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` 默认值同样改成跟这个配置走。
## 归属:这事主要是 MobileGL 的
| 部分 | 在哪 | 内容 |
|---|---|---|
| 分配器 + 身份登记 | **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` |
ANativeWindow 只剩 KWin 输出那一块,不变。
## 分阶段
**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。
**P1 server 分配器 + 登记表**
- `SharedImageRegistry`:`Allocate(w,h,format,usage) → {id, fds, stride, modifier}`;按 inode(或 name 标签)查找;归属会话 + 引用计数;会话死掉释放(键用单调 id,不用客户端句柄——P14 的跨会话句柄冲突教训)。
- 线协议新动词:分配 / 释放 / 按 fd 导入(fd 走现成的 `ITransport::ShareFd`)。
- 单元测试:注册、身份、跨会话释放只放自己的。
**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",生产者重用前等它。
**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 零拷贝。
**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 都看得见)。
**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 局部重绘。
**P6 其它消费者(按需)**
- Xwayland glamor 跑在 MobileGL 的 GBM 平台上(现在强制 llvmpipe + `-shm`)。
- Chrome 原生 GBM 路径(它会 `drmGetDeviceFromDevId` 枚举节点——占位节点下可能不行,是风险项;现在的 wl_egl_window 路径在 P2 后已经零拷贝)。
## 风险
- 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 少不了多少,但不再需要回复槽带宽。
@@ -0,0 +1,174 @@
# Plan: X11 apps on the GPU, zero-copy (Xwayland glamor + GLX DRI3/Present) (2026-10-03)
> Follows [`plan-ahb-dmabuf.md`](plan-ahb-dmabuf.md) (the Wayland half: server-allocated AHB
> "shared images" exported as dma-bufs). Same hard rules: vendor-neutral (AHB, dma-buf, sync_file,
> the libgbm backend ABI, linux-dmabuf, DRI3/Present), no layout assumptions (modifier INVALID).
## Today
```
GLX app --glXSwapBuffers--> ReadPixels (server->client CPU) --xcb_put_image--> Xwayland (-shm, llvmpipe)
Xwayland --wl_shm--> KWin --glTexSubImage2D upload--> server
```
Two full-frame CPU trips per frame for every X11 GL app, and every ordinary X11 app is drawn by
pixman/llvmpipe and uploaded again by KWin.
## Target
```
GLX app --render--> pbuffer --GPU copy (PresentToSharedImage)--> [AHB image]
--DRI3 PixmapFromBuffer(s)(dma-buf, once per buffer)--> Xwayland pixmap (glamor: gbm_bo_import -> EGLImage)
--Present PresentPixmap--> Xwayland flips it (attaches the pixmap's dma-buf wl_buffer) or GPU-copies it
Xwayland (glamor on MobileGL EGL/GBM) --linux-dmabuf wl_buffer (window pixmaps are shared images)--> KWin
```
Every hop is a session of the same MobileGL server sampling or writing the same AHardwareBuffer.
CPU copies: zero.
## Stage 1 - Xwayland on MobileGL (glamor)
What Xwayland 24.1.13 (`hw/xwayland/xwayland-glamor-gbm.c`, `glamor/glamor.c`) needs, and where it is:
| Need | Status |
|---|---|
| render node from linux-dmabuf v4 feedback `main_device` (drmGetDeviceFromDevId) | KWin's thin node: must be a real DRM render node (`MOBILEGL_GBM_NODE`=/dev/dri/renderD128). `/dev/null` makes glamor fail -> shm fallback |
| `gbm_create_device(fd)` with `GBM_BACKEND=mobilegl` | `mobilegl_gbm.so` (exists) |
| EGL client ext `EGL_KHR/MESA_platform_gbm`, `eglGetPlatformDisplay(GBM, gbm)` | exists; the foreign-device decline keys on the backend name `mobilegl` |
| `eglCreateContext(EGL_NO_CONFIG_KHR, core 3.1)` + surfaceless `eglMakeCurrent` | cherry-picked from the surfaceless branch (6125f941), to be dropped on rebase |
| GL >= 2.1 desktop, `GL_ARB_vertex_array_object`, `GL_OES_EGL_image`; renderer not llvmpipe | exists on a split client |
| `EGL_EXT_image_dma_buf_import(_modifiers)` -> dmabuf_capable | exists (shared images) |
| `gbm_bo_create_with_modifiers2(XRGB8888/ARGB8888, {INVALID, LINEAR})`, `gbm_bo_get_fd_for_plane`, `_handle_for_plane`, `_stride_for_plane`, `_offset`, `_modifier`, `gbm_bo_import(FD / FD_MODIFIER)` | exists; now covered by a host test through the real libgbm loader |
| `eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT)` of its own bos, `glEGLImageTargetTexture2DOES` | exists (server identifies its image by inode) |
| KWin's tranche lists XRGB8888/ARGB8888 with modifier INVALID | KWin adds INVALID+LINEAR when the EGL reports no modifiers (ours) |
| **implicit sync**: glamor renders into window pixmaps and commits after `glFlush`, assuming the kernel orders KWin's read after it | **new**: see below |
| **Xwayland must never dial its own display** | **new**: see below |
### Implicit sync by flush (new shared_image op `Flush`)
The server already orders an image between sessions: a writer publishes a sync_file per write
(`PublishWrite`), a reader waits on it at its first use and publishes its frame's read fence at
its frame boundary (swap). glamor has neither a swap nor an explicit write: it renders into the
image through an FBO and calls `glFlush` before committing.
- Client: `glFlush`/`glFinish` in a process that has bound a shared image to a texture
(`glEGLImageTargetTexture2DOES`) and does not own the server window (not KWin) send the new
in-stream `shared_image Flush` op and wait for its reply. Policy is a pure function
(`SharedImageFlushPolicy`), overridable with `MOBILEGL_SHARED_IMAGE_FLUSH_SYNC=0/1`.
- Server: `BackendObject::PublishSharedImageAccesses()` - every image the session used since its
last boundary gets the flush's fence as a write (generation moves) and as a read.
Espryt: `ExportFence` (EGL_ANDROID_native_fence_sync), Magma: release held images, submit with
the exportable semaphore, export it. A session that has flushed this way also waits (GPU) for
the pending read fences of an image before its first use in a frame (write-after-read).
- The reply comes after the fence is published, so KWin (which samples after Xwayland's commit,
which follows the reply) always finds the new generation.
### X-connection safety (`MG_Util/X11/DisplayGuard`)
One rule, one function: `MayDialX11Display(name)` is false when this process **serves** that
display: it holds a listening unix socket named `/tmp/.X11-unix/X<n>` (path or abstract) per
`/proc/net/unix` + `/proc/self/fd`, or its executable is an X server (Xwayland, Xorg, Xvfb, ...).
`MOBILEGL_X11_DIAL=0` turns all dialing off. Every place MobileGL opens an X connection asks it
first: EGLState's default-visual query (eglGetDisplay of a non-X native display has platform NONE,
so the old "only on X displays" test still dialed for `eglGetDisplay(gbm_device)`), DirectGLES's
visual query and window-surface display, Magma's Xlib surface. GLX never opens a connection (it
uses the application's). KWin's launcher also drops `DISPLAY` from Xwayland's environment.
### KWin (anland `kwin.patch`, `mobilegl-startup.sh`)
`ANLAND_MOBILEGL=1` and `ANLAND_XWAYLAND_GLAMOR != 0`: Xwayland starts **without** `-shm`, with
`MOBILEGL_IPC_SURFACE=offscreen`, its own `MOBILEGL_LOG_FILE_PATH=/tmp/mgl-xwayland`, `GBM_BACKEND`
kept, no `LIBGL_ALWAYS_SOFTWARE`/`GALLIUM_DRIVER`, no `DISPLAY`. Otherwise the old `-shm` +
software environment. If glamor cannot initialise, Xwayland itself falls back to shm rendering.
## Stage 2 - GLX presentation through DRI3 + Present
Per GLX window drawable (`GLXImpl/X11Present.{h,cpp}`, logic behind two small interfaces so the
host tests drive it with fakes):
- **Path selection** (`SelectPresentPath`): DRI3 >= 1.0 and Present >= 1.0 on the connection,
shared images available, depth 24/32, `MOBILEGL_GLX_PRESENT` not `readback` -> `Dri3Present`;
else MIT-SHM >= 1.2 with fd passing -> `ShmPutImage`; else `PutImage` (today's bands).
- **Buffers**: up to 3 (4 when Xwayland holds all three past a 1 s wait) shared images of the
window's format (depth 24 -> XRGB8888, 32 -> ARGB8888: exactly what glamor imports them as), each
imported once as a pixmap (`PixmapFromBuffers` with modifier INVALID on DRI3 >= 1.2, else
`PixmapFromBuffer`, checked). A buffer is busy from its PresentPixmap until its `IdleNotify`.
- **Present**: `PresentToSharedImage` (GPU copy, rows top first, returns once submitted with the
write fence published), then `PresentPixmap` with an XFixes update region from the swap's
damage (flipped to X's top-left origin; none = whole), serial per frame. Events arrive on a
special-event queue (`xcb_register_for_special_xge`), never in the application's queue.
- **Pacing**: `FrameThrottle` (the Wayland rules) on `CompleteNotify`: interval >= 1 waits for the
previous frame's completion, `target_msc = last_msc + interval`; interval 0 uses
`PresentOptionAsync`, unpaced while completions come, ~1 fps once one is 200 ms overdue.
- **Errors**: a refused `PixmapFromBuffer`, a `PresentPixmap` error (checked lazily, no round
trip), or the window's destruction (`ConfigureNotify` with `PresentWindowDestroyed`) latch the
drawable onto the readback path for good, logged once.
- **Resize**: `ConfigureNotify` (and the existing throttled `XGetGeometry`) resize the pbuffer;
buffers of the old size are dropped as they come back idle.
- **Fallback** (no DRI3/Present: Xwayland `-shm`, a remote X server): readback into a MIT-SHM
segment (memfd, `xcb_shm_attach_fd`) + `xcb_shm_put_image`; without MIT-SHM, today's bands.
- `GLX_EXT_buffer_age` (`GLX_BACK_BUFFER_AGE_EXT`) answers the EGL buffer age of the drawable.
## Tests (host, WSL)
`X11DisplayGuardTest` (display names, /proc parsing, the Xwayland case), `X11PresentTest` (path
selection, rotation, idle/complete/configure handling, pacing, resize, error latching, fake xcb
and fake images), `SharedImageSync` additions (accesses publish as write + read),
`SharedImageFlushPolicyTest`, `GbmGlamorTest` (the real libgbm loader with `GBM_BACKEND=mobilegl`
over a fake shared-image library: glamor's create/export/import calls).
## Device verification (phase B)
`scripts/x11/` in the skill: deploy, check Xwayland's glamor log lines and its dma-buf wl_buffers in
KWin (`WAYLAND_DEBUG`-free: `/proc/<kwin>/fdinfo` dma-buf count and the server log's import lines),
glxgears/glxinfo/an X11 GL app before/after (fps, `top` CPU of app/Xwayland/KWin/server), and
ordinary X apps (xterm, xeyes) for glamor rendering.
## Risks
- `drmGetDeviceFromDevId` on the borrowed render node inside the container (sysfs visibility).
- Adreno: cross-context ordering relies on the Flush op's fence; a missed flush shows as tearing,
not corruption of other windows.
- The extra round trip per Xwayland `glFlush` (glamor flushes once per main-loop wakeup).
- DRI3 clients that are not MobileGL (Mesa Vulkan WSI) hand Xwayland dma-bufs the server never
allocated: `PixmapFromBuffers` fails with BadAlloc for them, as on any driver mismatch.
## Device results (2026-10-03, TB321FU / Adreno 750, x11-gpu 95aa618d server + client)
glxgears 1280x720, % of one core, `x11-verify.sh measure`; "before" = the old stack (`-shm`
llvmpipe Xwayland, GLX readback + PutImage).
> glxgears is fixed-function GL (glBegin/glEnd display lists, glLight), which MobileGL does not
> implement (Core Profile only), so its window stays black. These numbers measure presentation
> throughput of empty frames through each path, not rendering. Check X11 rendering with a
> core-profile or shader-based GLX client, and verify it from pixels (mgl-xgrab / XGetImage).
| run | fps | phone | glxgears | Xwayland | kwin | server | GPU |
|---|---|---|---|---|---|---|---|
| before, unpaced | 160 | 229% | 28% | 42% | 71% | 37% | 15% |
| Espryt DRI3+Present, interval 1 | 98-101 | 210% | 12% | 5% | 28% | 32% | 12% |
| Espryt DRI3+Present, vblank_mode=0 | 1440-1780 | 296% | 29% | 29% | 36% | 62% | 72% |
| Espryt readback + MIT-SHM on glamor | 46 | 168-188% | 5% | 78% | 11% | 35% | 5% |
| Magma DRI3+Present, interval 1 | 105-112 | 210% | 12% | 5% | 24% | 43% | 16% |
| Magma DRI3+Present, vblank_mode=0 | 940-970 | 282% | 24% | 18% | 33% | 60% | 77% |
| Magma readback + MIT-SHM on glamor | 27 | 161% | 2% | 50% | 7% | 70% | 5% |
- Glamor engages on both backends: Xwayland runs without `-shm`, offers DRI3 1.2 + Present 1.2,
its windows are linux-dmabuf shared images; GLX windows log `presented through DRI3+Present`.
- Found on the device and fixed: uploads into a texture bound to a shared image were dropped
(Espryt) - an X server's PutImage into a window pixmap showed black windows. They are now
drained at the producer's flush and written into the image (RGB-converted for X formats, which
the driver holds as RGB8).
- The forced readback path is slower on a glamor Xwayland than on the old llvmpipe one (glamor
uploads every frame through the server); it is only the fallback now.
- Fixed (cd58f048): after a hide/show or lock/unlock round, X11 Qt windows kept stale or black
regions in their window pixmap. Root cause: every glamor copy and fill drew nothing. glamor's
fragment shaders name gl_FragColor only in the inactive arm of a version guard and write their
own `frag_color`; the legacy lowering still declared `mg_FragColor` ahead of it, and the IO
resolver gave that dead output location 0 and `frag_color` location 1, which no draw buffer
reads. Uploads (PutImage) still landed, so windows looked right until Xwayland copied between
window buffers (it swaps the window pixmap on every commit and reallocates buffers after a
hide), and then showed old content or zeros. Inactive fragment outputs now take the top of the
draw-buffer range. Host test: ProgramTest.InactiveLoweredFragColorDoesNotDisplaceTheActiveOutput.
On the device, an X client checking its own XGetImage against a CPU model (PutImage, CopyArea
and FillRectangle; MIT-SHM and plain; depth 24 and 32) sees 0 bad pixels through hide/show/lock
on both backends. One run in about 20 on Magma (no SHM, depth 24) left a 2751-pixel rect
mismatched; it did not come back in the repeats.
@@ -0,0 +1,178 @@
# Runbook:在这台 Windows 机器 + 平板上起 / 迭代 MobileGL + anland + KDE Plasma(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 设备上的实际状态核对过;标"未验证"的是没跑通过或只从旧记录推出来的。
**设备可能正被别的会话使用。** 凡是会重启 app / daemon / 容器会话、或替换库的操作,先跑只读的 `scripts/device/status.sh` 看一眼,确认没人在测;`ct.sh` 的通道名用自己的(别人在用 c、c2、c3、xd、xr、gbm)。
## 架构
- **平板**: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 在浏览器进程内。
## 首次准备(Windows 主机)
- 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 里 `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`。
## 启动、闲置、停止——没有手动步骤
开机什么都不起(容器 `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")。
窗口隐藏、熄屏、锁屏、从最近任务划掉: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` 进程。
```sh
adb connect <ip>:<port> # 只有无线调试才需要
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/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。
## 构建与部署循环
### server(Android,NDK)→ 替换 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
```
`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 不会重连。
### client + GBM 后端(容器 glibc),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
```
已经在跑的 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`。
### KWin(anland 后端 / kwin.patch),在容器里编
```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 =="
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` 是把这两步拼起来的,还没整体跑过。
### 单元测试(不用设备)
这个分支的 MSVC 构建是坏的,在 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 -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` 编并跑全部单元测试。DirectVulkan 集成测试永远不要跑;两个 ctest 不要同时跑。
## 切换后端
在 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` 不改后端。
## 全系统 vendor 与回退
`xdeploy.sh`(以及 anland 的 `anland-build-client.sh`)安装下面这些,容器里任何进程经原版 libglvnd / libgbm 就用上 MobileGL:
| 文件 | 作用 |
|---|---|
| `/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`) |
优先级:环境变量 > `/etc/mobilegl/backend`(只管后端)> client.conf > 内置默认;`MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` 可指向别的文件(设为空即关闭);server 进程不读这些文件。
**回退**:真正拉起之前 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 的演示:
```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
```
## 跑测试 client 与验证
在容器里以 root 执行(`printf '...' | bash scripts/ct.sh <通道>`),`mgrun offscreen <命令>` 以 swung0x48 身份在当前会话里起一个 client(日志 `/tmp/mgl-run.client.log`,`MGLOG=/tmp/x.log` 可换):
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
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 走硬解(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 格式。
结论成立要同时满足:
- 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` 是新写的。
参考(glmark2-es2-wayland 1280x720,零拷贝 vs wl_shm):Espryt 187 vs 49,Magma 158 vs 34。出乎意料的通过要复跑。
## 日志
| 内容 | 位置 |
|---|---|
| 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` |
| 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 <偏移>` 符号化。
## 常见坑
- **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`。
## 规矩
- 在 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 名字或指针。
+183
View File
@@ -0,0 +1,183 @@
# Reproducing the MobileGL + anland + KDE Plasma setup from source
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
day-to-day loop once everything below exists. Paths and names that are specific to the original
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
## 0. What you need
**Phone.**
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
- About 20 GB free for the container image.
**Development host.**
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
- Tools:
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
- Android NDK **27.2.12479018**.
- JDK 17 and Android SDK platform 36 (for the APK).
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
## 1. Sources
```sh
# MobileGL: branch feat/disaggregated (everything here is on it)
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
git checkout feat/disaggregated
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
git checkout legacy-mobilegl-unified
```
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
## 2. Phone base: root, Droidspaces, display daemon
1. **Root** with KernelSU (or another `su` provider).
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
## 3. The container
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
```
enable_gpu_mode=1
net_mode=host
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
run_at_boot=0
```
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
## 4. MobileGL server and the anland APK
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
```
The script runs, with the NDK toolchain:
```
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
```
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
```sh
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
```
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
## 5. MobileGL client in the container
Pick one of these two:
- **Cross build** (fast, from WSL or Linux):
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
Either route installs the same files, renamed into place so running clients keep their old mapping:
| File | What it is |
|---|---|
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
## 6. KWin 6.7.4 with the anland backend, and Xwayland
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
2. **Configure:**
```sh
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
```
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
```ini
[Service]
ExecStart=
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
Environment=QT_LOGGING_RULES=kwin_*.info=true
```
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
## 7. Bring it up
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
That is the single trigger. The app starts its foreground service and runs its bundled
`mobilegl-desktop.sh up` as root, which:
1. starts the display daemon if it is not running;
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
`/data/local/tmp/anland-mobilegl/backend`;
3. starts the container if it is stopped;
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
`/etc/mobilegl/backend` and runs Plasma.
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
(the notification action) stops the session and the container.
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
that, open the app as above. See SKILL.md "Starting, idling, stopping".
## 8. Verify
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
## 9. Machine-specific values to change
| Value | Original | Where |
|---|---|---|
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/build-android.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
# Log level is compiled to INFO. android-30 = the APK's minSdk: thread_local is then the platform's
# own TLS rather than the emulated one (which cost 7-12% of an apply thread on the API 26 build).
set -euo pipefail
. "$(dirname "$0")/env.sh"
W=$MGL_WORKTREE
B=$W/${BUILD_DIR:-build-android}
STAMP=$(git -C "$W" rev-parse HEAD)
if [ ! -f "$B/build.ninja" ]; then
cmake -S "$W" -B "$B" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_STAMP="$STAMP"
else
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
ls -la "$B/libMobileGL.so"
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/chrome-launch.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL), client log
# /tmp/mgl-chrome-<tag>.client.log, then print its "presented through" line and save a screenshot
# to $ANL_DIR/ch-<tag>.png. Kills the running chrome first (pkill -x, never -f). MobileGL is the
# container's system-wide vendor (10_mobilegl.json + /etc/mobilegl/client.conf), so only the log is set here.
# Env: WAIT (s before checking, default 20), CHANNEL (ct.sh channel, default chrome),
# CHROME_GPU (process, the default: the GPU in its own process, which presents
# through GBM dma-bufs and is restarted by Chrome after a device loss; in-process opts out -
# needs the anland startup script that knows MOBILEGL_CHROME_GPU; in process mode the GPU process and the browser
# write the one client log, the GPU process last); CHROME_GPU_SANDBOX=on keeps its GPU sandbox.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
TAG=${1:?usage: chrome-launch.sh <tag> [chrome flags...]}; shift
bash "$here/ct.sh" "${CHANNEL:-chrome}" <<IN
pkill -x chrome; sleep 2
U=\$(id -u $MGL_DESKTOP_USER)
rm -f /tmp/mgl-chrome-$TAG.client.log /tmp/chrome-$TAG-out.log
sudo -u $MGL_DESKTOP_USER env XDG_RUNTIME_DIR=/run/user/\$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$U/bus MOBILEGL_LOG_FILE_PATH=/tmp/mgl-chrome-$TAG.log MOBILEGL_CHROME_GPU=${CHROME_GPU:-process} MOBILEGL_CHROME_GPU_SANDBOX=${CHROME_GPU_SANDBOX:-off} bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
sleep ${WAIT:-20}
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
IN
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/chrome-recover.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# chrome-recover.sh <in-process|process> [rounds] - does Chrome repaint after a GPU device loss,
# without a restart? Starts Chrome (chrome-launch.sh, CHROME_GPU=<mode>) on an animated WebGL page,
# then per round: screenshots (the page must animate), loses the session of Chrome's GPU client with
# the server's pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid; the pid as Chrome sees
# itself in the container), waits, and screenshots again. A round PASSES when the server latched
# that pid's session, the browser process is the same one (no restart), a GPU client is drawing on a
# NEW server session, and the screen still animates. Stops early below MIN_BATTERY percent.
#
# in-process the GPU in the browser process (the startup script's default today): the target is
# the browser itself; recovery needs ANGLE to make a new native context.
# process the GPU in its own process (MOBILEGL_CHROME_GPU=process): the target is the GPU
# process; Chrome restarts it after the loss, so the next round's target is a new pid.
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# STARTUP=<anland worktree>/producers/kde/Arch_v5/mobilegl-startup.sh bash scripts/chrome-recover.sh deploy-startup
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 8), MIN_BATTERY (default 40),
# CHANNEL (ct.sh channel, default crec), TAG (log tag, default crec-<mode>).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
MODE=${1:?usage: chrome-recover.sh <in-process|process|deploy-startup> [rounds]}
ROUNDS=${2:-${ROUNDS:-2}}
SETTLE=${SETTLE:-8}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-crec}
TAG=${TAG:-crec-$MODE}
PAGE=/tmp/mgl-spin.html
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; cleanup; exit 3
fi
}
cleanup() { adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true; }
if [ "$MODE" = deploy-startup ]; then
# The anland startup script that knows MOBILEGL_CHROME_GPU, installed by rename (a running
# session keeps its open copy).
src=${STARTUP:?STARTUP=<path to the anland mobilegl-startup.sh>}
tr -d '\r' < "$src" > "$ANL_DIR/mobilegl-startup.sh"
adb_ push "$ANL_DIR/mobilegl-startup.sh" "$DEV_SHARE/mobilegl-startup.sh.new" >/dev/null
ct <<'IN'
install -m 755 /run/anland-mobilegl/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh.new
mv -f /opt/mobilegl/bin/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh
grep -c MOBILEGL_CHROME_GPU /opt/mobilegl/bin/mobilegl-startup.sh
IN
exit 0
fi
case "$MODE" in in-process|process) ;; *) echo "mode must be in-process or process" >&2; exit 2;; esac
trap cleanup EXIT
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
# reaches the screen changes the screenshot.
ct <<IN
cat > $PAGE <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl');
let t = 0, lost = 0;
document.getElementById('c').addEventListener('webglcontextlost', e => { e.preventDefault(); lost++; });
function frame() {
t++;
if (gl && !gl.isContextLost()) {
gl.clearColor((t % 60) / 60, ((t + 20) % 60) / 60, ((t + 40) % 60) / 60, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST); gl.scissor((t * 7) % 600, 100, 40, 160);
gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.SCISSOR_TEST);
}
document.title = 'spin ' + t + ' lost ' + lost;
requestAnimationFrame(frame);
}
frame();
</script></body></html>
HTML
chmod 644 $PAGE
IN
WAIT=${WAIT:-20} CHROME_GPU=$MODE CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file://$PAGE"
# The target pid inside the container: the browser (in-process) or the GPU process.
target_pid() {
ct <<'IN' | tr -d '\r' | tail -1
for p in $(pgrep -x chrome); do
args=$(tr '\0' ' ' < /proc/$p/cmdline)
case "$args" in
*--type=gpu-process*) echo "gpu $p" ;;
*--type=*) ;;
*) echo "browser $p" ;;
esac
done | sort > /tmp/mgl-crec-pids
cat /tmp/mgl-crec-pids >&2
echo "$(awk '/^browser/ {print $2; exit}' /tmp/mgl-crec-pids) $(awk '/^gpu/ {print $2; exit}' /tmp/mgl-crec-pids)"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS ($MODE)"
check_battery
read -r browser gpu < <(target_pid)
if [ "$MODE" = process ]; then target=$gpu; else target=$browser; fi
echo "browser=$browser gpu=${gpu:-none} target=$target"
if [ -z "$target" ]; then echo "FAIL: no target process"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
adb_ logcat -c
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $target'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
latched=$(adb_ logcat -d | grep -c "injected:pid=$target" || true)
started=$(adb_ logcat -d | grep -c "session #[0-9]* started" || true)
adb_ logcat -d | grep -E "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)" | cut -c1-220 | head -12 || true
read -r browser2 gpu2 < <(target_pid)
echo "after: browser=$browser2 gpu=${gpu2:-none}"
ct <<IN || true
echo "--- client log ($TAG):"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|failed to make|session #" /tmp/mgl-chrome-$TAG.client.log | tail -8
echo "--- chrome stderr:"; grep -aiE "context lost|GPU process|make.*current|lost" /tmp/chrome-$TAG-out.log | tail -8
IN
adb_ logcat -b crash -d | tail -5
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched pid $target"; verdict=FAIL; }
[ "$browser2" = "$browser" ] || { echo " - the browser process changed ($browser -> $browser2): Chrome restarted"; verdict=FAIL; }
[ "$started" -ge 1 ] || { echo " - no new server session started after the loss"; verdict=FAIL; }
if [ "$MODE" = process ]; then
[ -n "$gpu2" ] && [ "$gpu2" != "$gpu" ] || { echo " - no new GPU process"; verdict=FAIL; }
fi
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
done
echo "=== $pass/$ROUNDS rounds passed ($MODE); screenshots in $ANL_DIR/$TAG-r*-*.png"
@@ -0,0 +1,37 @@
#!/bin/bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/container/kwprep.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
#
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
set -e
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
P=/root/kwsync/Arch_v5/kwin.patch
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
cd /root/kp && patch -p1 -s < $P
LIVE=/root/mobilegl-build/kwin-6.7.4
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
for f in $FILES; do
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
done
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
done
echo PREP_DONE
@@ -0,0 +1,21 @@
#!/bin/bash
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
S=$1; shift
DU=${DESKTOP_USER:-swung0x48}
U=$(id -u "$DU")
cd /tmp
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
bash -lc 'exec "$@"' mgrun "$@"
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/ct.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
#
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
# bash ct.sh mychan ./something.cmd
#
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
#
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
# script on its command line and kills itself. Use `pkill -x <name>`.
set -euo pipefail
. "$(dirname "$0")/env.sh"
ch=${1:?usage: ct.sh <channel> [script-file]}
src=${2:--}
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/cyc.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
shot=${1:?usage: cyc.sh <shot-name>}
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
adb_ logcat -c
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/desktop-verify.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# desktop-verify.sh <phase>... - check that the MobileGL desktop starts, idles and stops by itself
# (anland app: foreground service + mobilegl-desktop.sh; container: desktop-session.service).
# Phases run in the order given; screenshots go to $ANL_DIR/dv-<phase>.png.
#
# push install device/*.sh into $DEV_TOOLS (push-tools.sh's device half)
# reset everything down: app force-stopped (its service with it), container stopped,
# experiment display daemon killed - the state after a phone reboot
# cold reset, then open the app like its icon does and time each piece (desktop-timeline.sh)
# open open the app without resetting (after a reboot: the reboot was the reset)
# chrome (re)start Chrome in the session on an animated WebGL page (kills a running Chrome)
# idle CPU/GPU for visible, hidden (HOME), locked (screen off), visible again (measure.sh)
# reopen swipe the app away (remove its task), check KWin survives, open it again, time it
# stop Stop desktop from the notification (the service's STOP action) and check it all ends
# rtest lock/unlock + window resize regression ($ANL_DIR/rtest.sh)
# Env: SECS (idle window, default 20), URL (chrome page).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
SECS=${SECS:-20}
URL=${URL:-https://webglsamples.org/aquarium/aquarium.html}
SOCK=$DEV_SHARE/display.sock
su_() { adb_ shell "su -c '$*'"; }
shot() {
su_ "screencap -p $DEV_SHARE/dv-$1.png"
adb_ pull "$DEV_SHARE/dv-$1.png" "$ANL_DIR/dv-$1.png" > /dev/null && echo "screenshot $ANL_DIR/dv-$1.png"
}
kwin_log() { # the anland backend's lines since <time>
printf '%s\n' "journalctl --no-pager --since '$1' 2>/dev/null | grep -a -E 'viewer (gone|back)|consumer (reconnected|disconnected)|failed to connect' | tail -${2:-8}" |
bash "$here/ct.sh" dv 2>/dev/null | grep -v pushed || true
}
task_id() {
su_ "am stack list" | tr -d '\r' | grep -m1 "$MGL_PKG/" | sed -n 's/.*taskId=\([0-9]*\).*/\1/p'
}
wake() {
su_ "input keyevent KEYCODE_WAKEUP"; sleep 1; su_ "wm dismiss-keyguard"; sleep 2
}
for phase in "$@"; do
echo "=== $phase"
case $phase in
push)
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" > /dev/null
done
rm -f "$ANL_DIR/push.tmp"; echo "device scripts in $DEV_TOOLS" ;;
reset)
su_ "sh $DEV_TOOLS/desktop-reset.sh" ;;
cold)
bash "$0" reset
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 240"
sleep 3; shot cold ;;
open)
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 300"
sleep 3; shot open ;;
chrome)
WAIT=15 bash "$here/chrome-launch.sh" dv "$URL" ;;
idle)
su_ "sh $DEV_TOOLS/measure.sh visible $SECS"
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
su_ "input keyevent KEYCODE_HOME"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh hidden $SECS"
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "input keyevent KEYCODE_SLEEP"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh locked $SECS"
wake
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "sh $DEV_TOOLS/measure.sh visible2 $SECS"
shot idle
kwin_log "$t" 12 ;;
reopen)
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
id=$(task_id); echo "removing task $id"
[ -n "$id" ] && su_ "am stack remove $id"
sleep 8
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG) kwin: \$(pidof kwin_wayland) plasmashell: \$(pidof plasmashell)"
su_ "sh $DEV_TOOLS/measure.sh closed 10"
su_ "sh $DEV_TOOLS/desktop-timeline.sh 120"
sleep 3; shot reopen
kwin_log "$t" ;;
stop)
su_ "am startservice -n $MGL_PKG/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP" || true
sleep 25
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG); $DS_BIN show"
su_ "tail -3 $DEV_SHARE/desktop.log" ;;
rtest)
bash "$ANL_DIR/rtest.sh" dv ;;
*)
echo "unknown phase $phase" >&2; exit 2 ;;
esac
done
@@ -0,0 +1,15 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/bringup.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
input keyevent KEYCODE_WAKEUP
wm dismiss-keyguard
sh "$(dirname "$0")/desktop-timeline.sh" 300
@@ -0,0 +1,25 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/desktop-reset.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
# reboot: the app force-stopped (its foreground service and server with it), the container
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
am force-stop $P
$D --name=$C stop > /dev/null 2>&1
for p in $(pidof display_daemon); do
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
done
rm -f $SOCK
sleep 2
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
$D show
@@ -0,0 +1,38 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/desktop-timeline.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
# Pieces that already run show 0.0. Read-only apart from the am start.
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
T=${1:-180}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
now() { awk '{print $1}' /proc/uptime; }
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
t0=$(now)
am start -n $P/com.anland.consumer.MainActivity > /dev/null
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
while :; do
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
fi
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
sleep 0.3
done
@@ -0,0 +1,55 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/measure.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
# /proc (Droidspaces only gives them their own pid namespace).
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
L=${1:-sample}
N=${2:-20}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
pids_named() { # exact process names (comm or argv0 basename)
for n in "$@"; do pidof "$n"; done
}
pids_matching() { # argv contains the word
for d in /proc/[0-9]*; do
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
done
}
ticks() { # utime+stime of the pids, in clock ticks
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
KW="$(pids_named kwin_wayland Xwayland)"
SH="$(pids_named plasmashell)"
CH="$(pids_matching /opt/google/chrome/)"
SV="$(pidof $P:mobilegl)"
UI="$(pidof $P)"
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
# CLK_TCK is 100: ticks per second == percent of one core.
pct() { echo $(( ($2 - $1) / N )); }
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
@@ -0,0 +1,28 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/restart-daemon.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
DIR=$(dirname $SOCK)
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
sleep 1
rm -f $SOCK
mkdir -p $DIR
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
sleep 1
chmod 777 $DIR
chmod 666 $SOCK
echo "daemon restarted:"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
@@ -0,0 +1,35 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/run-plasma.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "
systemctl stop desktop-session.service 2>/dev/null
loginctl terminate-user $U 2>/dev/null; sleep 30
systemctl reset-failed desktop-session.service
rm -f /tmp/mobilegl-compositor.log
date +%s > /tmp/plasma-start
"
am force-stop $P
am start -n $P/com.anland.consumer.MainActivity > /dev/null
sleep 30
$D --name=$C run bash -lc "
systemctl --no-pager status desktop-session.service | head -8
UID_=\$(id -u $U)
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
"
@@ -0,0 +1,21 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/status.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
$D show
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
@@ -0,0 +1,25 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/swap.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
set -e
P=${PKG:-com.anland.consumer.mobilegl}
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
ls -laZ $D
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
am force-stop $P
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
OWN=$(stat -c %u:%g $D/libMobileGL.so)
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
ls -laZ $D/libMobileGL.so
@@ -0,0 +1,25 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/switch.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
# the setting until the next reboot - this clears it.
# Env: PKG, SOCK.
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
P=${PKG:-com.anland.consumer.mobilegl}
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
if pidof $P:mobilegl > /dev/null; then
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
fi
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
@@ -0,0 +1,16 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/unlock.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
# Env: DS, CONTAINER, DESKTOP_USER.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
@@ -0,0 +1,55 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/device/x11-measure.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
# below 40% battery: the phone drains faster under load than USB charges it.
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
L=${1:-sample}
APP=${2:-glxgears}
N=${3:-10}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
exit 3
fi
ticks() {
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
n=0
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
echo $n
}
AP="$(pidof $APP)"
XW="$(pidof Xwayland)"
KW="$(pidof kwin_wayland)"
SV="$(pidof $P:mobilegl)"
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
pct() { echo $(( ($2 - $1) / N )); }
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
@@ -0,0 +1,49 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/env.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
# Sourced, never run. Every value can be overridden from the environment before calling a script.
#
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
# 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
# (USB serial or a wireless "ip:port").
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
if [ -z "${MGL_WORKTREE:-}" ]; then
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
fi
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
: "${MGL_CONTAINER:=arch-kde-mgl}"
: "${MGL_DESKTOP_USER:=swung0x48}"
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
: "${DEV_TOOLS:=/data/local/tmp/anl}"
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
export MSYS_NO_PATHCONV=1
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
# C:/x/y -> /mnt/c/x/y (for WSL).
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
mkdir -p "$ANL_DIR"
@@ -0,0 +1,154 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/kwin-recover.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# kwin-recover.sh [rounds] - does the desktop keep working when KWin's OWN MobileGL session is lost?
# Starts animated clients (glmark2-es2-wayland: Wayland, zero-copy shared images; glxgears: X11 through
# Xwayland's GLX; with CHROME=1 also Chrome on an animated WebGL page), then per round loses the server
# session of KWin with the pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid, KWin's pid as
# it sees itself in the container), waits, and checks. A round PASSES when:
# - the server latched KWin's session (the server log file: "injected:pid=<kwin>"; logcat is too small);
# - KWin, plasmashell and every test client are the SAME processes (nothing restarted);
# - KWin restarted compositing on a fresh session (its journal: "compositing restarted on a fresh
# MobileGL session"; its client log: one more "RECOVERED");
# - the screen still animates (shot.sh hashes differ) and the clients keep presenting;
# - no native crash (logcat -b crash).
# Between rounds the knob is reset to 0 while the clients run, which re-arms it for the same pid.
# Stops early below MIN_BATTERY percent (the phone drains faster under load than USB charges it).
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# ANLAND_WORKTREE=<anland worktree> bash scripts/kwin-stage.sh build # kwin.patch + anland backend
# (wait for "== ALL DONE ==" in /root/kwin-build.log), then: adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 10), REARM (s with the knob at 0 between
# rounds, default 4), MIN_BATTERY (default 40), CHROME (1: add Chrome), CHANNEL (ct.sh channel,
# default krec), TAG (screenshot/log tag, default krec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
ROUNDS=${1:-${ROUNDS:-2}}
SETTLE=${SETTLE:-10}
REARM=${REARM:-4}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-krec}
TAG=${TAG:-krec}
COMPOSITOR_LOG=/tmp/mobilegl-compositor.client.log
SERVER_LOG=/data/data/$MGL_PKG/files/mobilegl-server.server.log
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
stop_clients() {
ct <<'IN' || true
pkill -x glmark2-es2-way 2>/dev/null; pkill -x glmark2-es2-wayland 2>/dev/null; pkill -x glxgears 2>/dev/null
true
IN
}
cleanup() {
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true
stop_clients
}
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; exit 3
fi
}
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
# The clients, each with its own log (mgrun's MGLOG), in the background for the whole run.
ct <<'IN'
rm -f /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log /tmp/krec-gm.out /tmp/krec-gx.out
MGLOG=/tmp/mgl-krec-gm.log nohup timeout 900 mgrun offscreen glmark2-es2-wayland -s 960x540 --run-forever \
> /tmp/krec-gm.out 2>&1 < /dev/null &
MGLOG=/tmp/mgl-krec-gx.log nohup timeout 900 mgrun offscreen glxgears -info > /tmp/krec-gx.out 2>&1 < /dev/null &
sleep 8
echo "glmark2: $(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland) glxgears: $(pgrep -x glxgears)"
grep -aE "presented through|could not be presented" /tmp/mgl-krec-gm.client.log | head -2
grep -aE "presented through|readback" /tmp/mgl-krec-gx.client.log | head -2
IN
if [ "${CHROME:-0}" = 1 ]; then
ct <<'IN'
cat > /tmp/mgl-krec-spin.html <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl'); let t = 0;
function frame() { t++; if (gl && !gl.isContextLost()) { gl.clearColor((t % 60) / 60, 0.2, 0.6, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
requestAnimationFrame(frame); }
frame();
</script></body></html>
HTML
chmod 644 /tmp/mgl-krec-spin.html
IN
WAIT=${WAIT:-20} CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file:///tmp/mgl-krec-spin.html"
fi
# KWin, plasmashell and the clients, as pids inside the container; how often KWin recovered so far.
snapshot() {
ct <<IN | tr -d '\r' | tail -1
k=\$(pgrep -x kwin_wayland | head -1); p=\$(pgrep -x plasmashell | head -1)
g=\$(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland); x=\$(pgrep -x glxgears | head -1)
c=\$(pgrep -x chrome | head -1)
r=\$(grep -ac "RECOVERED" $COMPOSITOR_LOG 2>/dev/null || true)
f=\$(grep -ac "FPS" /tmp/krec-gm.out 2>/dev/null || true)
echo "\${k:-none} \${p:-none} \${g:-none} \${x:-none} \${c:-none} \${r:-0} \${f:-0}"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS"
check_battery
read -r kwin plasma gm gx chrome recovered fps < <(snapshot)
echo "kwin=$kwin plasmashell=$plasma glmark2=$gm glxgears=$gx chrome=$chrome recoveries=$recovered fps-lines=$fps"
if [ "$kwin" = none ]; then echo "FAIL: KWin is not running"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
srvlines=$(adb_ shell "su -c 'wc -l < $SERVER_LOG'" | tr -dc '0-9')
adb_ logcat -b crash -c || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $kwin'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
read -r kwin2 plasma2 gm2 gx2 chrome2 recovered2 fps2 < <(snapshot)
echo "after: kwin=$kwin2 plasmashell=$plasma2 glmark2=$gm2 glxgears=$gx2 chrome=$chrome2 recoveries=$recovered2 fps-lines=$fps2"
adb_ shell "su -c 'tail -n +$((srvlines + 1)) $SERVER_LOG'" > "$ANL_DIR/$TAG-r$round-server.log"
latched=$(grep -c "injected:pid=$kwin" "$ANL_DIR/$TAG-r$round-server.log" || true)
grep -aE "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)|owner=server|lease" \
"$ANL_DIR/$TAG-r$round-server.log" | cut -c1-220 | head -14 || true
ct <<IN || true
echo "--- KWin's MobileGL client log:"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|re-created on the fresh|Refuse" $COMPOSITOR_LOG | tail -6
echo "--- KWin journal:"; journalctl --no-pager --since -40s 2>/dev/null | grep -aE "kwin_wayland" | grep -aiE "reset|restarted|MobileGL swap|context|fatal|abort" | tail -10
echo "--- clients:"; grep -aE "DEVICE LOST|lost|Fatal" /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log | tail -4
IN
crashes=$(adb_ logcat -b crash -d | grep -c "Fatal signal\|Abort message" || true)
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched KWin's session (pid $kwin)"; verdict=FAIL; }
[ "$kwin2" = "$kwin" ] || { echo " - KWin is a different process ($kwin -> $kwin2): it restarted"; verdict=FAIL; }
[ "$plasma2" = "$plasma" ] || { echo " - plasmashell restarted ($plasma -> $plasma2)"; verdict=FAIL; }
[ "$gm2" = "$gm" ] && [ "$gx2" = "$gx" ] || { echo " - a test client restarted or died"; verdict=FAIL; }
[ "${CHROME:-0}" != 1 ] || [ "$chrome2" = "$chrome" ] || { echo " - Chrome restarted"; verdict=FAIL; }
[ "$recovered2" -gt "$recovered" ] || { echo " - KWin's client log shows no new RECOVERED session"; verdict=FAIL; }
[ "$fps2" -gt "$fps" ] || { echo " - glmark2 stopped reporting frames"; verdict=FAIL; }
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
[ "$crashes" -eq 0 ] || { echo " - native crash(es):"; adb_ logcat -b crash -d | tail -8; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
# Re-arm the knob for the same pid: a session must observe 0 before the next value counts.
sleep "$REARM"
done
echo "=== $pass/$ROUNDS rounds passed; screenshots in $ANL_DIR/$TAG-r*-*.png"
[ "$pass" = "$ROUNDS" ]
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/kwin-stage.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
#
# Two staging places, because the two container scripts read different ones:
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
# sync-build-kwin.sh (bash then fails on $'\r').
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
# CHANNEL (ct.sh channel, default kw).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
K=$ANLAND_WORKTREE/producers/kde
st=$ANL_DIR/kwstage
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
lf() { tr -d '\r' < "$1" > "$2"; }
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
lf "$f" "$st/kwin-sync/anland/$b"
done
for f in kwin.patch mobilegl-startup.sh desktop-session-mobilegl.conf; do
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
done
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
if [ "${1:-}" = build ]; then
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
bash /root/kwprep.sh || exit 1
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
IN
fi
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
# MobileGL - tools/anland-mobilegl-plasma/scripts/probes/cdpmedia.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
# module in the container): a minimal RFC 6455 client.
# python3 cdpmedia.py 9333 6
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
# they arrive, then the page's stats text.
import base64, json, os, socket, struct, sys, time, urllib.request
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
page = next(t for t in targets if t.get("type") == "page")
url = page["webSocketDebuggerUrl"]
path = url.split(f"127.0.0.1:{port}", 1)[1]
sock = socket.create_connection(("127.0.0.1", port))
key = base64.b64encode(os.urandom(16)).decode()
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
response = b""
while b"\r\n\r\n" not in response:
response += sock.recv(4096)
buffer = response.split(b"\r\n\r\n", 1)[1]
def send(message):
data = json.dumps(message).encode()
header = bytes([0x81])
mask = os.urandom(4)
if len(data) < 126:
header += bytes([0x80 | len(data)])
elif len(data) < 65536:
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
else:
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
global buffer
while len(buffer) < n:
chunk = sock.recv(65536)
if not chunk:
raise EOFError
buffer += chunk
out, buffer = buffer[:n], buffer[n:]
return out
def recv():
first, second = recv_exact(2)
length = second & 0x7F
if length == 126:
length = struct.unpack(">H", recv_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", recv_exact(8))[0]
payload = recv_exact(length)
return json.loads(payload) if first & 0x0F == 1 else None
send({"id": 1, "method": "Media.enable"})
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
if os.environ.get("CDP_NAVIGATE"):
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
elif os.environ.get("CDP_RELOAD", "1") == "1":
send({"id": 3, "method": "Page.reload"})
deadline = time.time() + seconds
sock.settimeout(0.5)
seen = set()
while time.time() < deadline:
try:
message = recv()
except socket.timeout:
continue
if not message or "method" not in message:
continue
if message["method"] == "Media.playerPropertiesChanged":
for prop in message["params"]["properties"]:
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
line = f"{prop['name']} = {prop['value']}"
if line not in seen:
seen.add(line)
print(line)
elif message["method"] in ("Media.playerErrorsRaised",):
print("media error:", json.dumps(message["params"])[:300])
send({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
sock.settimeout(3)
while True:
message = recv()
if message and message.get("id") == 2:
print("page:", message.get("result", {}).get("result", {}).get("value"))
break
@@ -0,0 +1,58 @@
// MobileGL - tools/anland-mobilegl-plasma/scripts/probes/dmahash.c
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char** argv) {
char dir[64];
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
DIR* d = opendir(dir);
if (!d) {
perror("opendir");
return 1;
}
struct dirent* e;
while ((e = readdir(d))) {
char p[128], l[128] = {0};
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
int fd = open(p, O_RDONLY);
if (fd < 0) continue;
struct stat st;
fstat(fd, &st);
if (st.st_size < (1 << 20)) {
close(fd);
continue;
}
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) {
printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino);
close(fd);
continue;
}
uint64_t h = 1469598103934665603ull;
unsigned nz = 0;
for (off_t i = 0; i < st.st_size; i += 64) {
h = (h ^ m[i]) * 1099511628211ull;
nz += m[i] != 0;
}
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
(unsigned long long)h, nz);
munmap((void*)m, st.st_size);
close(fd);
}
return 0;
}
@@ -0,0 +1,78 @@
// MobileGL - tools/anland-mobilegl-plasma/scripts/probes/gpuprobe.c
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
#endif
static double now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
}
int main(int argc, char** argv) {
int seconds = argc > 1 ? atoi(argv[1]) : 20;
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
setvbuf(stdout, NULL, _IOLBF, 0);
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(d, NULL, NULL);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8,
EGL_NONE};
EGLConfig c;
EGLint n;
eglChooseConfig(d, ca, &c, 1, &n);
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
if (x == EGL_NO_CONTEXT) {
printf("no ctx 0x%x\n", eglGetError());
return 1;
}
eglMakeCurrent(d, s, s, x);
EGLint got = 0;
eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
const double t0 = now_ms();
double maxRt = 0;
long iters = 0, slow = 0;
printf("[probe] start %.0f prio=0x%x\n", t0, got);
while (now_ms() - t0 < seconds * 1000.0) {
const double a = now_ms();
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
const double rt = now_ms() - a;
if (rt > maxRt) maxRt = rt;
if (rt > 50) {
++slow;
printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms());
}
++iters;
usleep(10000);
}
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
return 0;
}
@@ -0,0 +1,249 @@
// MobileGL - tools/anland-mobilegl-plasma/scripts/probes/hangclient.c
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// hangclient - a Wayland xdg-toplevel, EGL + desktop GL 3.3 core client that animates and, on request,
// submits fragment work that never ends (the driver has to reset it). Prints per-frame gaps so the same
// binary measures "other app" frame pacing (frame callbacks come from the compositor) when run with -H -1.
//
// hangclient -T tag [-H hang_at_frame] [-N hang_repeat] [-I iters] [-S seconds] [-W w] [-X h] [-F]
// -H n frame at which the hanging draw is submitted (-1: never; default -1)
// -N k how many consecutive hanging frames (default 1)
// -E e re-arm a hang every e frames after the first (0: once)
// -I i loop count of the hanging shader (default 2^31-1)
// -S s run time in seconds (default 30)
// -F glFinish after every frame (puts the wait inside the client's own session)
#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-shell-client-protocol.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static struct wl_compositor* g_comp;
static struct xdg_wm_base* g_wm;
static int g_configured, g_closed;
static int g_w = 480, g_h = 360;
static void reg_global(void* d, struct wl_registry* r, uint32_t name, const char* iface, uint32_t ver) {
(void)d;
if (!strcmp(iface, "wl_compositor"))
g_comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if (!strcmp(iface, "xdg_wm_base"))
g_wm = wl_registry_bind(r, name, &xdg_wm_base_interface, 1);
}
static void reg_remove(void* d, struct wl_registry* r, uint32_t n) {
(void)d;
(void)r;
(void)n;
}
static const struct wl_registry_listener reg_l = {reg_global, reg_remove};
static void wm_ping(void* d, struct xdg_wm_base* wm, uint32_t s) {
(void)d;
xdg_wm_base_pong(wm, s);
}
static const struct xdg_wm_base_listener wm_l = {wm_ping};
static void xs_conf(void* d, struct xdg_surface* s, uint32_t serial) {
(void)d;
xdg_surface_ack_configure(s, serial);
g_configured = 1;
}
static const struct xdg_surface_listener xs_l = {xs_conf};
static void tl_conf(void* d, struct xdg_toplevel* t, int32_t w, int32_t h, struct wl_array* st) {
(void)d;
(void)t;
(void)st;
(void)w;
(void)h;
}
static void tl_close(void* d, struct xdg_toplevel* t) {
(void)d;
(void)t;
g_closed = 1;
}
static const struct xdg_toplevel_listener tl_l = {tl_conf, tl_close};
static double now_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6;
}
static GLuint sh(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, NULL);
glCompileShader(s);
GLint ok = 0;
glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog(s, sizeof log, NULL, log);
fprintf(stderr, "shader: %s\n", log);
exit(2);
}
return s;
}
int main(int argc, char** argv) {
const char* tag = "hc";
int hangAt = -1, hangN = 1, every = 0, seconds = 30, finish = 0;
long iters = 2147483647L;
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "-T") && i + 1 < argc)
tag = argv[++i];
else if (!strcmp(argv[i], "-H") && i + 1 < argc)
hangAt = atoi(argv[++i]);
else if (!strcmp(argv[i], "-N") && i + 1 < argc)
hangN = atoi(argv[++i]);
else if (!strcmp(argv[i], "-E") && i + 1 < argc)
every = atoi(argv[++i]);
else if (!strcmp(argv[i], "-I") && i + 1 < argc)
iters = atol(argv[++i]);
else if (!strcmp(argv[i], "-S") && i + 1 < argc)
seconds = atoi(argv[++i]);
else if (!strcmp(argv[i], "-W") && i + 1 < argc)
g_w = atoi(argv[++i]);
else if (!strcmp(argv[i], "-X") && i + 1 < argc)
g_h = atoi(argv[++i]);
else if (!strcmp(argv[i], "-F"))
finish = 1;
}
setvbuf(stdout, NULL, _IOLBF, 0);
struct wl_display* dpy = wl_display_connect(NULL);
if (!dpy) {
fprintf(stderr, "no wayland\n");
return 1;
}
struct wl_registry* reg = wl_display_get_registry(dpy);
wl_registry_add_listener(reg, &reg_l, NULL);
wl_display_roundtrip(dpy);
if (!g_comp || !g_wm) {
fprintf(stderr, "no compositor/xdg_wm_base\n");
return 1;
}
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
xdg_surface_add_listener(xs, &xs_l, NULL);
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
xdg_toplevel_add_listener(tl, &tl_l, NULL);
xdg_toplevel_set_title(tl, tag);
wl_surface_commit(surf);
while (!g_configured)
wl_display_dispatch(dpy);
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
if (!eglInitialize(ed, NULL, NULL)) {
fprintf(stderr, "eglInitialize\n");
return 1;
}
eglBindAPI(EGL_OPENGL_API);
const EGLint ca[] = {EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_NONE};
EGLConfig cfg;
EGLint n = 0;
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) {
fprintf(stderr, "no config\n");
return 1;
}
const EGLint xa[] = {
EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK,
EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
if (ctx == EGL_NO_CONTEXT) {
fprintf(stderr, "no context 0x%x\n", eglGetError());
return 1;
}
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
eglMakeCurrent(ed, es, es, ctx);
eglSwapInterval(ed, 1);
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
GLuint prog = glCreateProgram();
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
glLinkProgram(prog);
GLint ok = 0;
glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) {
fprintf(stderr, "link failed\n");
return 2;
}
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
GLuint vao, vbo;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
const double t0 = now_ms();
double last = t0, maxGap = 0;
long frames = 0, gaps100 = 0;
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt,
hangN, every, iters);
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
wl_display_dispatch_pending(dpy);
const double t = (now_ms() - t0) / 1000.0;
int hang = 0;
if (hangAt >= 0 && frames >= hangAt) {
long rel = frames - hangAt;
if (every > 0) rel %= every;
hang = rel < hangN;
}
glViewport(0, 0, g_w, g_h);
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(prog);
glUniform1i(uIt, hang ? (int)iters : 0);
glUniform1f(uT, (float)t);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
if (finish) glFinish();
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
const GLenum rs = glGetError();
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
const double nowv = now_ms(), gap = nowv - last;
last = nowv;
++frames;
if (gap > maxGap) maxGap = gap;
if (gap > 100.0) {
++gaps100;
printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames);
}
if (frames % 120 == 0)
printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
}
const double total = now_ms() - t0;
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap,
gaps100);
return 0;
}
@@ -0,0 +1,208 @@
// MobileGL - tools/anland-mobilegl-plasma/scripts/probes/texfmt.c
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
* left half red and the right half WHITE; a black right half means the alpha was lost. */
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define P(T, n) T n = (T)eglGetProcAddress(#n)
static const char* vs =
"#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
static const char* fs =
"#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
int main(void) {
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(d, 0, 0);
eglBindAPI(EGL_OPENGL_ES_API);
EGLint ca[] = {EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_NONE};
EGLConfig c;
EGLint n;
eglChooseConfig(d, ca, &c, 1, &n);
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
eglMakeCurrent(d, s, s, x);
P(PFNGLCREATESHADERPROC, glCreateShader);
P(PFNGLSHADERSOURCEPROC, glShaderSource);
P(PFNGLCOMPILESHADERPROC, glCompileShader);
P(PFNGLCREATEPROGRAMPROC, glCreateProgram);
P(PFNGLATTACHSHADERPROC, glAttachShader);
P(PFNGLLINKPROGRAMPROC, glLinkProgram);
P(PFNGLUSEPROGRAMPROC, glUseProgram);
P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation);
P(PFNGLUNIFORM1IPROC, glUniform1i);
P(PFNGLGENBUFFERSPROC, glGenBuffers);
P(PFNGLBINDBUFFERPROC, glBindBuffer);
P(PFNGLBUFFERDATAPROC, glBufferData);
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer);
P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
P(PFNGLDRAWARRAYSPROC, glDrawArrays);
P(PFNGLCLEARCOLORPROC, glClearColor);
P(PFNGLCLEARPROC, glClear);
P(PFNGLREADPIXELSPROC, glReadPixels);
P(PFNGLVIEWPORTPROC, glViewport);
P(PFNGLGENTEXTURESPROC, glGenTextures);
P(PFNGLBINDTEXTUREPROC, glBindTexture);
P(PFNGLTEXIMAGE2DPROC, glTexImage2D);
P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
P(PFNGLENABLEPROC, glEnable);
P(PFNGLBLENDFUNCPROC, glBlendFunc);
P(PFNGLGETERRORPROC, glGetError);
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D);
P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D);
P(PFNGLGETSTRINGPROC, glGetString);
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
printf("BGRA ext: %s\n",
strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(v, 1, &vs, 0);
glCompileShader(v);
glShaderSource(f, 1, &fs, 0);
glCompileShader(f);
GLuint pr = glCreateProgram();
glAttachShader(pr, v);
glAttachShader(pr, f);
glLinkProgram(pr);
glUseProgram(pr);
float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
GLuint vbo;
glGenBuffers(1, &vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0);
glEnableVertexAttribArray(0);
GLint lm = glGetUniformLocation(pr, "mode");
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
struct {
const char* name;
GLint ifmt;
GLenum fmt;
int bpp;
int mode;
int storage;
GLenum sized;
unsigned char px[8];
} cases[] = {
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255, 0, 0, 255, 0, 0, 0, 0}},
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0, 0, 255, 255, 0, 0, 0, 0}},
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0, 0, 255, 255, 0, 0, 0, 0}},
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255, 0, 0, 255, 0, 0, 0, 0}},
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255, 255, 0, 0}},
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
};
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
GLuint t;
glGenTextures(1, &t);
glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
while (glGetError()) {}
if (cases[i].storage) {
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
} else {
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
}
GLenum err = glGetError();
glUniform1i(lm, cases[i].mode);
glViewport(0, 0, 64, 64);
glClearColor(1, 1, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l);
glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0],
r[1], r[2], ok ? "ok" : "BAD");
}
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
struct {
const char* name;
GLenum sized, fmt;
int bpp;
GLint sw[4];
unsigned char px[4];
unsigned char wantL[3], wantR[3];
} sz[] = {
{"R8 swizzled as ALPHA8",
GL_R8,
GL_RED,
1,
{GL_ZERO, GL_ZERO, GL_ZERO, GL_RED},
{255, 0},
{0, 0, 0},
{255, 255, 255}},
{"RG8 swizzled as LUM_ALPHA",
GL_RG8,
GL_RG,
2,
{GL_RED, GL_RED, GL_RED, GL_GREEN},
{255, 255, 0, 0},
{255, 255, 255},
{255, 255, 255}},
{"RG8 as LUM_ALPHA (lum 0)",
GL_RG8,
GL_RG,
2,
{GL_RED, GL_RED, GL_RED, GL_GREEN},
{0, 255, 0, 0},
{0, 0, 0},
{255, 255, 255}},
};
glUniform1i(lm, 0);
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
GLuint t;
glGenTextures(1, &t);
glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
GLenum err = glGetError();
glClearColor(1, 1, 1, 1);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l);
glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
int ok = 1;
for (int k = 0; k < 3; ++k)
ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0],
r[1], r[2], ok ? "ok" : "BAD");
}
return 0;
}
@@ -0,0 +1,431 @@
// MobileGL - tools/anland-mobilegl-plasma/scripts/probes/yuvprobe.c
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
//
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
//
// What it does, in an ES 3.0 context on a pbuffer:
// 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
// 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
// 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
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
// check is that every call succeeds).
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
#define _GNU_SOURCE
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <fcntl.h>
#include <gbm.h>
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef DRM_FORMAT_NV12
#define DRM_FORMAT_NV12 0x3231564E
#endif
#ifndef DRM_FORMAT_P010
#define DRM_FORMAT_P010 0x30313050
#endif
static int g_failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("FAIL: " __VA_ARGS__); \
printf("\n"); \
++g_failures; \
} \
} while (0)
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
static PFNEGLCREATESYNCKHRPROC pCreateSync;
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
static EGLDisplay g_display;
enum {
W = 256,
H = 128,
BARS = 4
};
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
const double y = kr * r + kg * g + kb * b;
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
const double scale = (double)(1 << (bits - 8));
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
}
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
// samples with the 10 significant bits high.
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
const int bits = bytes == 1 ? 8 : 10;
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
int bar = x * BARS / W;
if (reverse) bar = BARS - 1 - bar;
int code[3];
Encode709(kBars[bar], bits, code);
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
if (bytes == 1)
py[0] = (uint8_t)code[0];
else
*(uint16_t*)py = (uint16_t)(code[0] << 6);
if ((x & 1) == 0 && (y & 1) == 0) {
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
if (bytes == 1) {
pc[0] = (uint8_t)code[1];
pc[1] = (uint8_t)code[2];
} else {
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
}
}
}
}
}
static GLuint Compile(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
printf("shader: %s\n", log);
}
return shader;
}
static GLuint g_program, g_fbo, g_rgba;
static void SetUpDraw(void) {
const char* vs = "#version 300 es\n"
"out vec2 uv;\n"
"void main() {\n"
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
" uv = p;\n"
"}\n";
const char* fs = "#version 300 es\n"
"#extension GL_OES_EGL_image_external_essl3 : require\n"
"precision mediump float;\n"
"uniform samplerExternalOES tex;\n"
"in vec2 uv;\n"
"out vec4 color;\n"
"void main() { color = texture(tex, uv); }\n";
g_program = glCreateProgram();
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
glLinkProgram(g_program);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
CHECK(linked, "the samplerExternalOES program did not link");
GLint type = 0, size = 0;
char name[64];
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
printf("uniform 0: %s type 0x%04x\n", name, type);
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
glGenTextures(1, &g_rgba);
glBindTexture(GL_TEXTURE_2D, g_rgba);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
glGenFramebuffers(1, &g_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
}
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glViewport(0, 0, W, H);
glUseProgram(g_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
static unsigned char pixels[W * H * 4];
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
if (tolerance < 0) {
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
pixels[(H / 2 * W + W / 2) * 4 + 3]);
return;
}
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
// so any row does; take two to catch a broken chroma row pitch.
const int rows[2] = {H / 4, (3 * H) / 4};
for (int r = 0; r < 2; ++r) {
for (int bar = 0; bar < BARS; ++bar) {
const int x = bar * (W / BARS) + W / (2 * BARS);
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
int worst = 0;
for (int c = 0; c < 3; ++c) {
int d = abs((int)p[c] - (int)want[c]);
if (d > worst) worst = d;
}
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
}
}
}
static int AllocateDmaHeap(size_t size) {
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
if (heap < 0) {
perror("open /dev/dma_heap/system");
return -1;
}
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
close(heap);
return -1;
}
close(heap);
return (int)data.fd;
}
static void Sync(int fd, int start) {
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
}
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
EGLint attribs[64];
int n = 0;
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
A(EGL_WIDTH, W);
A(EGL_HEIGHT, H);
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
if (modifier) {
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
}
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
attribs[n] = EGL_NONE;
#undef A
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
return image;
}
static GLuint ExternalTexture(EGLImageKHR image) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
return texture;
}
// A frame boundary as an offscreen producer makes one: a native fence.
static void EndFrame(void) {
EGLint attribs[] = {EGL_NONE};
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
}
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
const uint32_t offsetC = pitchY * (H + 16);
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
int fd = AllocateDmaHeap(size);
if (fd < 0) {
CHECK(0, "%s: no dma-buf to test with", what);
return;
}
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
if (map == MAP_FAILED) return;
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
Sync(fd, 0);
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
if (image == EGL_NO_IMAGE_KHR) return;
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, what, 0, tolerance);
EndFrame();
// New content, same buffer: shows after the frame boundary.
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
Sync(fd, 0);
EndFrame();
DrawAndCheck(texture, what, 1, tolerance);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
munmap(map, size);
close(fd);
}
static void GbmTest(void) {
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
struct gbm_device* gbm = gbm_create_device(node);
if (gbm == NULL) {
CHECK(0, "gbm_create_device failed");
return;
}
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
printf("gbm NV12 supported: %d, P010: %d\n", supported, gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
CHECK(supported == 1, "gbm does not support NV12");
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
if (bo != NULL) {
const int planes = gbm_bo_get_plane_count(bo);
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
int fd = gbm_bo_get_fd(bo);
EGLImageKHR image = Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
gbm_bo_get_stride_for_plane(bo, 1), 0);
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
if (image != EGL_NO_IMAGE_KHR) {
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, "gbm NV12", 0, -1);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
}
close(fd);
gbm_bo_destroy(bo);
}
gbm_device_destroy(gbm);
close(node);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major, minor;
if (!eglInitialize(g_display, &major, &minor)) {
printf("FAIL: eglInitialize\n");
return 2;
}
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE,
EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_NONE};
EGLConfig config;
EGLint count = 0;
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
if (count < 1) {
printf("FAIL: no config\n");
return 2;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
printf("FAIL: no ES 3 context\n");
return 2;
}
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
EGLint formats[32], formatCount = 0;
pQueryFormats(g_display, 32, formats, &formatCount);
int nv12 = 0;
for (int i = 0; i < formatCount; ++i) {
printf("dma-buf format %.4s", (const char*)&formats[i]);
EGLuint64KHR modifiers[4];
EGLBoolean externalOnly[4];
EGLint modifierCount = 0;
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
for (int m = 0; m < modifierCount; ++m)
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
printf("\n");
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
}
CHECK(nv12, "NV12 is not a listed dma-buf format");
SetUpDraw();
ForeignTest(DRM_FORMAT_NV12, 1, 12);
ForeignTest(DRM_FORMAT_P010, 2, 16);
GbmTest();
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglTerminate(g_display);
return g_failures ? 1 : 0;
}
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/push-tools.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
# container/mgrun -> <container>/usr/local/bin/mgrun
# container/kwprep.sh -> <container>/root/kwprep.sh
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
echo "pushed $DEV_TOOLS/$(basename "$f")"
done
for f in mgrun kwprep.sh; do
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
done
rm -f "$ANL_DIR/push.tmp"
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/rec.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
# `input mouse tap x y`. Env CHANNEL (default rec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
IN
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
import sys, os
from PIL import Image
d, out = sys.argv[1], sys.argv[2]
fs = sorted(os.listdir(d)); n = len(fs)
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
ims = [Image.open(os.path.join(d, f)) for f in pick]
w, h = ims[0].size; tw, th = w // 2, h // 2
g = Image.new('RGB', (tw * 3, th * 4))
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
g.save(out); print(n, pick)
P
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/shot.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
set -euo pipefail
. "$(dirname "$0")/env.sh"
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
cmd=""; i=1
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
adb_ shell "su -c '$cmd'"
i=1
while [ "$i" -le "$n" ]; do
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
done
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/sync-src.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
# worktree changed since the container's source stamp into the container's MobileGL source tree
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
# then build/install inside the container with anland's anland-build-client.sh
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
set -euo pipefail
. "$(dirname "$0")/env.sh"
SRC=$CT_ROOT/root/mobilegl-unified-src
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
cd "$MGL_WORKTREE"
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
echo "$(echo "$FILES" | wc -l) files since $STAMP"
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
HEAD=$(git rev-parse HEAD)
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
@@ -0,0 +1,17 @@
#!/bin/bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/wsl-all.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
# (they need a live device + manual intervention); never run two ctest suites at once.
B=${B:-$HOME/mgl-anl-bld}
cd $B
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
@@ -0,0 +1,21 @@
#!/bin/bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/wsl-test.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<repo>/tools/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
set -e
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
B=${B:-$HOME/mgl-anl-bld}
if [ ! -f $B/build.ninja ]; then
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
fi
cmake --build $B --target "$@" -j16
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/x11-verify.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (reference/plan-x11-gpu.md):
# Xwayland drawing with glamor on MobileGL, and GLX windows presented through
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
#
# preflight read-only: battery, stack status, Xwayland package, render node, xcb libs,
# test apps, Xwayland's extensions and its GLX vendor
# measure <label> [s] glxgears 1280x720 (GEARS_ARGS; vblank_mode=0) for s seconds (10): its fps, then CPU/GPU/dma-buf
# counts (device/x11-measure.sh); also with MOBILEGL_GLX_PRESENT=readback
# deploy-client xbuild.sh + xdeploy.sh of this worktree's client (MGL_XBUILD_DIR)
# deploy-kwin kwin-stage.sh build from ANLAND_WORKTREE; waits for "== ALL DONE =="
# deploy-server build-android.sh + cyc.sh x11-server (swaps the APK lib, RESTARTS the session)
# check the evidence: Xwayland took glamor (journal), its MobileGL log, GLX's path
# line, dma-buf counts in Xwayland/KWin, the server's import lines
# apps X11 clients on screen: konsole and kate on Qt's xcb platform (glamor-drawn
# core X), glxgears (GLX), xterm/xeyes when installed; two screenshots
# stop kill the test apps
#
# Typical run: preflight; measure before; deploy-client; deploy-kwin; deploy-server; check; measure
# after; apps; stop. Battery: every load step refuses below MIN_BATTERY (40) %.
# Env: CHANNEL (ct.sh channel, default x11), ANLAND_WORKTREE (anland x11-gpu worktree, Windows
# form), MGL_XBUILD_DIR (default /home/swung/mgl-xbuild-x11), MIN_BATTERY.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
CH=${CHANNEL:-x11}
: "${MGL_XBUILD_DIR:=/home/swung/mgl-xbuild-x11}"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/x11-gpu}"
export MGL_XBUILD_DIR ANLAND_WORKTREE
battery_ok() {
local level
level=$(adb_ shell "dumpsys battery" | sed -n 's/^ *level: *//p' | tr -d '\r')
echo "battery: ${level}%"
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "below ${MIN_BATTERY:-40}%: stopping GPU-heavy work; charge the phone first" >&2
return 1
fi
}
push_measure() {
tr -d '\r' < "$here/device/x11-measure.sh" > "$ANL_DIR/x11-measure.sh"
adb_ push "$ANL_DIR/x11-measure.sh" "$DEV_TOOLS/x11-measure.sh" >/dev/null
}
step=${1:?usage: x11-verify.sh <preflight|measure|deploy-client|deploy-kwin|deploy-server|check|apps|stop>}
shift
case "$step" in
preflight)
battery_ok || true
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
bash "$here/ct.sh" "$CH" <<'IN'
echo "== packages"; pacman -Q xorg-xwayland libxcb mesa-utils xorg-xdpyinfo qt6-base 2>&1 | sed 's/^/ /'
echo "== render nodes"; ls -l /dev/dri/ 2>&1 | sed 's/^/ /'
U=swung0x48; for n in /dev/dri/renderD*; do sudo -u $U test -r "$n" -a -w "$n" && echo " $U can open $n"; done
echo "== KWin's thin node"; tr '\0' '\n' < /proc/$(pgrep -x kwin_wayland)/environ 2>/dev/null | grep -E "MOBILEGL_GBM_NODE|ANLAND_XWAYLAND_GLAMOR|GBM_BACKEND" | sed 's/^/ /'
echo "== xcb libraries"; for l in libxcb-dri3.so.0 libxcb-present.so.0 libxcb-xfixes.so.0 libxcb-shm.so.0; do ls /usr/lib/$l >/dev/null 2>&1 && echo " $l ok" || echo " $l MISSING"; done
echo "== Xwayland"; pgrep -a -x Xwayland | sed 's/^/ /'
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM|XFIXES|GLX)$" | sed 's/^/ ext/'
mgrun offscreen glxinfo -B 2>&1 | grep -E "vendor|renderer|direct rendering" | sed 's/^/ /'
IN
;;
measure)
label=${1:?usage: x11-verify.sh measure <label> [seconds]}
secs=${2:-10}
battery_ok
push_measure
for mode in auto readback; do
printf '%s\n' \
"pkill -x glxgears; rm -f /tmp/mgl-x11-gears.client.log /tmp/x11-gears.out" \
"MGLOG=/tmp/mgl-x11-gears.log timeout $((secs + 8)) mgrun offscreen env vblank_mode=${VBLANK_MODE:-0} MOBILEGL_GLX_PRESENT=$mode glxgears ${GEARS_ARGS:--geometry 1280x720} > /tmp/x11-gears.out 2>&1 &" \
"sleep 3" | bash "$here/ct.sh" "$CH" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/x11-measure.sh $label-$mode glxgears $secs'" || true
sleep 6
printf '%s\n' "grep -a 'frames in' /tmp/x11-gears.out | tail -3; grep -a 'presented through' /tmp/mgl-x11-gears.client.log | head -1; pkill -x glxgears; true" \
| bash "$here/ct.sh" "$CH"
done
;;
deploy-client)
bash "$here/xbuild.sh"
CHANNEL=$CH bash "$here/xdeploy.sh"
;;
deploy-kwin)
CHANNEL=$CH bash "$here/kwin-stage.sh" build
for _ in $(seq 1 120); do
out=$(printf 'tail -3 /root/kwin-build.log\n' | bash "$here/ct.sh" "$CH" 2>/dev/null || true)
echo "$out" | tail -1
echo "$out" | grep -q "== ALL DONE ==" && exit 0
echo "$out" | grep -qiE "error|failed" && { echo "KWin build failed" >&2; exit 1; }
sleep 30
done
echo "KWin build did not finish in an hour" >&2; exit 1
;;
deploy-server)
battery_ok
bash "$here/build-android.sh"
CHANNEL=$CH bash "$here/cyc.sh" x11-server
;;
check)
bash "$here/ct.sh" "$CH" <<'IN'
echo "== Xwayland's command line (no -shm with glamor)"; pgrep -a -x Xwayland | sed 's/^/ /'
echo "== Xwayland's glamor (journal)"
journalctl --no-pager --since -30min 2>/dev/null | grep -aiE "glamor|Xwayland.*(EGL|gbm|dri3|falling back)" | tail -10 | sed 's/^/ /'
echo "== Xwayland's MobileGL client log"; grep -aE "presented|shared image|declin|Fatal|ERROR" /tmp/mgl-xwayland.client.log /tmp/mgl-xwayland.client 2>/dev/null | tail -8 | sed 's/^/ /' || echo " (none)"
X=$(pgrep -x Xwayland); K=$(pgrep -x kwin_wayland)
echo "== dma-bufs held: Xwayland $(ls -l /proc/$X/fd 2>/dev/null | grep -c dmabuf), kwin $(ls -l /proc/$K/fd 2>/dev/null | grep -c dmabuf)"
echo "== Xwayland's GLX vendor and extensions"
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM)$" | sed 's/^/ ext/'
echo "== a GLX window's path"
rm -f /tmp/mgl-x11-check.client.log
MGLOG=/tmp/mgl-x11-check.log timeout 6 mgrun offscreen glxgears >/dev/null 2>&1
grep -a "presented through\|leaves DRI3" /tmp/mgl-x11-check.client.log | sed 's/^/ /'
IN
adb_ logcat -d | grep -aE "MobileGL.*(shared image|dma-buf import|refused)" | tail -8 || true
adb_ logcat -b crash -d | tail -5 || true
;;
apps)
battery_ok
printf '%s\n' \
"pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate" \
"MGLOG=/tmp/mgl-x11-konsole.log mgrun offscreen env QT_QPA_PLATFORM=xcb konsole >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-kate.log mgrun offscreen env QT_QPA_PLATFORM=xcb kate >/dev/null 2>&1 &" \
"command -v xterm >/dev/null && MGLOG=/tmp/mgl-x11-xterm.log mgrun offscreen xterm >/dev/null 2>&1 &" \
"command -v xeyes >/dev/null && MGLOG=/tmp/mgl-x11-xeyes.log mgrun offscreen xeyes >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-gears.log mgrun offscreen glxgears >/dev/null 2>&1 &" \
"sleep 6; pgrep -a -x konsole; pgrep -a -x kate; pgrep -a -x xterm; pgrep -a -x xeyes; pgrep -a -x glxgears; true" | bash "$here/ct.sh" "$CH"
bash "$here/shot.sh" x11-apps 2 1
;;
stop)
printf '%s\n' "pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate; true" | bash "$here/ct.sh" "$CH"
;;
*)
echo "unknown step $step" >&2; exit 2
;;
esac
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/xbuild.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
# MGL_JOBS (16), WSL_DISTRO (archlinux).
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
set -euo pipefail
. "$(dirname "$0")/env.sh"
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
RECONF=0
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
EXTRA="$*"
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -euo pipefail
OUT=${OUT:-$HOME/mgl-xbuild-a64}
TC=$HOME/sysroots/aarch64-arch.cmake
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
t0=$(date +%s)
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
t1=$(date +%s)
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
t2=$(date +%s)
echo "stamp: $STAMP"
echo "configure: $((t1-t0))s build: $((t2-t1))s"
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
done
IN
exit "${PIPESTATUS[0]}"
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/xdeploy.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
# keep the old inode; restart the client (or the session) to pick up the new library.
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
mkdir -p "$ANL_DIR/xdist"
# 1. Copy the build outputs out of WSL.
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
IN
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
# 3. Install inside the container.
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
set -euo pipefail
in=/root/xdeploy
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
mkdir -p /usr/share/glvnd/egl_vendor.d
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
# Client settings every process reads when its environment does not say otherwise (the backend
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
mkdir -p /etc/mobilegl
cat >/etc/mobilegl/client.conf <<'CONF'
# MobileGL client configuration, read by every process that loads libMobileGL.so.
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
MOBILEGL_TRANSPORT=spawn
MOBILEGL_IPC_DATA=shm
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
CONF
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
mkdir -p /etc/environment.d /etc/profile.d
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
mkdir -p /usr/lib/gbm
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
echo DEPLOYED
IN
@@ -0,0 +1,55 @@
# MobileGL - tools/anland-mobilegl-plasma/scripts/xsysroot/aarch64-arch.cmake
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(_triple aarch64-unknown-linux-gnu)
if (DEFINED ENV{MGL_SYSROOT})
set(_sysroot "$ENV{MGL_SYSROOT}")
else()
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
endif()
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
list(GET _gcc_dirs 0 _gcc_install_dir)
set(CMAKE_SYSROOT "${_sysroot}")
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${_triple})
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
set(CMAKE_AR llvm-ar)
set(CMAKE_RANLIB llvm-ranlib)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJCOPY llvm-objcopy)
set(CMAKE_STRIP llvm-strip)
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
set(CMAKE_C_FLAGS_INIT "${_xflags}")
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_LINKER_TYPE LLD)
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "")
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# MobileGL - tools/anland-mobilegl-plasma/scripts/xsysroot/refresh-sysroot.sh
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/../env.sh"
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
set -e; cd /
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
echo SYSROOT_TAR_DONE
IN
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
IN
rm -f "$ANL_DIR/xsysroot.tar.gz"
@@ -1,23 +0,0 @@
# GL CTS multi-suite conformance report
| Backend | Suite | Expected | Result | Pass | Accepted | Crash | Hang | Unrun | Duplicate | Coverage | Strict Pass-only | Conformance-accepted | Validation |
|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|:---:|
| DirectVulkan | shader-image | 125 | 125 | 63 | 119 | 0 | 0 | 0 | 0 | 100.00% (125/125) | 50.40% (63/125) | 95.20% (119/125) | OK |
| DirectVulkan | ssbo | 124 | 124 | 124 | 124 | 0 | 0 | 0 | 0 | 100.00% (124/124) | 100.00% (124/124) | 100.00% (124/124) | OK |
| DirectVulkan | dsa | 371 | 371 | 368 | 369 | 0 | 0 | 0 | 0 | 100.00% (371/371) | 99.19% (368/371) | 99.46% (369/371) | OK |
| DirectVulkan | texture | 1055 | 1054 | 815 | 1036 | 0 | 0 | 1 | 0 | 99.91% (1054/1055) | 77.25% (815/1055) | 98.20% (1036/1055) | INCOMPLETE |
| DirectVulkan | packed-pixels | 4732 | 4732 | 4732 | 4732 | 0 | 0 | 0 | 0 | 100.00% (4732/4732) | 100.00% (4732/4732) | 100.00% (4732/4732) | OK |
| **DirectVulkan** | **DirectVulkan weighted subtotal** | **6407** | **6406** | **6102** | **6380** | **0** | **0** | **1** | **0** | **99.98% (6406/6407)** | **95.24% (6102/6407)** | **99.58% (6380/6407)** | **INCOMPLETE** |
| **All backends** | **Overall weighted** | **6407** | **6406** | **6102** | **6380** | **0** | **0** | **1** | **0** | **99.98% (6406/6407)** | **95.24% (6102/6407)** | **99.58% (6380/6407)** | **INCOMPLETE** |
Rates use unique **Expected** caselist cases as the denominator. Unrun cases remain in the denominator and are not accepted.
Accepted statuses: `Pass`, `NotSupported`, `QualityWarning`, `CompatibilityWarning`, `Waiver`.
Duplicate is the number of extra QPA observations; the final observation wins.
## Validation details
- **DirectVulkan/shader-image OK**: result directory has no run_state.json; backend provenance is unverified
- **DirectVulkan/ssbo OK**: result directory has no run_state.json; backend provenance is unverified
- **DirectVulkan/dsa OK**: result directory has no run_state.json; backend provenance is unverified
- **DirectVulkan/texture INCOMPLETE**: result directory has no run_state.json; backend provenance is unverified
- **DirectVulkan/packed-pixels OK**: result directory has no run_state.json; backend provenance is unverified
+3 -3
View File
@@ -82,7 +82,7 @@ FCL's Minecraft renders into the Render Server's on-screen window on the same ph
`fcl_accept.sh <backend> kill` kills the server and checks the client latches DEVICE LOST and FCL
survives. Needs the FCL fork's version setting "keepRunningInBackground" ON in `config.json`
(FCL pauses the game the moment its Activity leaves the foreground). The server is started through
the Render Server screen's documented extras. Results: `docs/archive/notes/p12/FCL-ACCEPTANCE.md`.
the Render Server screen's documented extras. Results: the phase notes.
## crosshost_accept.sh + crosshost_report.py — P12 exit gate (b)
@@ -90,7 +90,7 @@ A WSL client replays the openra and rd12 traces against the phone's render serve
TCP + stream (ssim gate, then `PRESENT_CREDIT` 1/2/3 benchmarks). Adds a temporary /32 route
around the host's TUN proxy, checks the path with `tcp_path_check.sh`, and removes the route and
stops the server on exit. `crosshost_report.py` reduces the output to a table. Results and the
P6.5 link numbers: `docs/archive/notes/p12/CROSSHOST-ACCEPTANCE.md`.
P6.5 link numbers: the phase notes.
## netbench.sh — Wi-Fi TCP throughput baseline
@@ -133,7 +133,7 @@ Reducers, all reading a probe directory:
- `fcl_stacks.py`: top stacks.
The simpleperf reducers need `SIMPLEPERF_DIR` and a `binary_cache` from `binary_cache_builder.py`.
Findings: `docs/archive/notes/perf-p15/PLAN-P15.md` section 2.
Findings: the phase notes §2.
## Clocks, bench sessions and run validity (P15)
+3 -3
View File
@@ -13,7 +13,7 @@ Its first use was `CONTRACT-P6.md` section 9 item 7 (the paired device gate) and
doorbell ledger), and it is not specific to those: the thing it computes is "N transports, one
thermal window, identical protocol" for any transport/backend/case triple the runner accepts.
Everything it does is a step of the protocol in docs/Disaggregated/MEASUREMENTS.md sections 4.4 / 9
/ 11 plus docs/archive/notes/p5f/device-report.md.
/ 11 plus the phase notes.
Why it is a script and not a shell transcript: a run that wants to be quotable needs the CPU
samples, the pin evidence and the library's own wait ledger to come from the SAME window, and the
@@ -26,7 +26,7 @@ WHAT IT DELIBERATELY DOES NOT DO.
- It does not decide anything about correctness. Benchmark mode takes no snapshot and compares no
golden (--benchmark replays the whole trace for timing), so an SSIM column here would be a
fabrication. Correctness on this device is section 3 of the P6 closing snapshot
(docs/archive/notes/p6/README.md) and the retrace lanes.
and the retrace lanes.
- It does not read `MOBILEGL_TRANSPORT` out of `--transport` on the runner. That flag is applied by
run_android_retrace_local.py's run_case() path only; run_benchmark_case() passes `args.env`
alone, so `--transport spawn` alongside `--benchmark` is silently ignored and the arm runs
@@ -821,7 +821,7 @@ def main():
# The fan is a CONSTANT of the protocol, not a treatment: both arms run under the same airflow,
# and on this device it is the difference between a 60 s cool-down between arms and a 20-minute
# one (docs/archive/notes/p4a/INTEGRATOR-DECISIONS.md). `real_speed` is read back because target_level alone
# one. `real_speed` is read back because target_level alone
# is a request, not a fact.
fan = "/sys/class/xm_power/hw_monitor/pwm_fan"
adb.su(f"echo {args.fan_level} > {fan}/target_level")