Compare commits

..
2167 changed files with 9428 additions and 971776 deletions
@@ -1,3 +0,0 @@
# 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
@@ -1,183 +0,0 @@
---
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): `docs/Disaggregated/notes/anland/runbook-plasma.md`.
Background: `docs/Disaggregated/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `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 (`docs/Disaggregated/notes/anland/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/<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` 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 and for `LIBGL_ALWAYS_SOFTWARE=1` - that is how a software Xwayland (`ANLAND_XWAYLAND_GLAMOR=0`, KWin's patch sets it then) keeps the software stack. 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: `docs/MobileVA/plan.md`.
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 with `git commit --no-verify`, one short sentence, **no** `Co-Authored-By` trailer.
- 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.
@@ -1,183 +0,0 @@
# 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.
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# 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"
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# 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"
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
# 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"
@@ -1,29 +0,0 @@
#!/bin/bash
# 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
@@ -1,21 +0,0 @@
#!/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 "$@"
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# 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'"
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,7 +0,0 @@
# 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
@@ -1,17 +0,0 @@
# 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
@@ -1,30 +0,0 @@
# 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
@@ -1,47 +0,0 @@
# 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))"
@@ -1,20 +0,0 @@
# 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
@@ -1,27 +0,0 @@
# 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
"
@@ -1,13 +0,0 @@
# 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"'
@@ -1,17 +0,0 @@
# 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
@@ -1,17 +0,0 @@
# 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)"
@@ -1,8 +0,0 @@
# 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"
@@ -1,47 +0,0 @@
# 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"
@@ -1,41 +0,0 @@
# 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"
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
# 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" ]
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,94 +0,0 @@
#!/usr/bin/env python3
# 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
@@ -1,34 +0,0 @@
// 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;
}
@@ -1,55 +0,0 @@
// 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;
}
@@ -1,150 +0,0 @@
// 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;
}
@@ -1,95 +0,0 @@
/* 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;
}
@@ -1,410 +0,0 @@
// 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;
}
@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# 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'"
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,15 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# 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'"
@@ -1,9 +0,0 @@
#!/bin/bash
# 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
@@ -1,13 +0,0 @@
#!/bin/bash
# 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/<worktree>/.claude/skills/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
@@ -1,135 +0,0 @@
#!/usr/bin/env bash
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/Disaggregated/notes/anland/
# 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
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# 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]}"
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# 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
@@ -1,47 +0,0 @@
# 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} "")
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# 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"
+15 -147
View File
@@ -1,10 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
echo "usage: $0 <trace-case> [fixture-dir]" >&2
exit 2
@@ -13,45 +9,17 @@ fi
case_name="$1"
fixture_dir="${2:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
# Fixture mirrors, tried in order before falling back to Git LFS. Override the
# whole list with MOBILEGL_TRACE_FIXTURE_MIRROR_BASES (whitespace separated);
# MOBILEGL_TRACE_FIXTURE_MIRROR_BASE still works and is tried first.
default_mirror_bases=(
"https://git.hit.moe/swung0x48/MobileGL/media/branch/dev/tools/trace_replay/fixtures"
"https://repo.miawa.cn/mgl/tools/trace_replay/fixtures"
)
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES:-}" ]; then
read -r -a mirror_bases <<< "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASES}"
else
mirror_bases=("${default_mirror_bases[@]}")
fi
if [ -n "${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-}" ]; then
mirror_bases=("${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE}" "${mirror_bases[@]}")
fi
# Optional bearer token for mirrors that require authentication (private Gitea).
mirror_token="${MOBILEGL_TRACE_FIXTURE_MIRROR_TOKEN:-}"
download_attempts="${MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS:-5}"
retry_delay="${MOBILEGL_TRACE_FIXTURE_RETRY_DELAY:-2}"
mirror_base="${MOBILEGL_TRACE_FIXTURE_MIRROR_BASE:-https://repo.miawa.cn/mgl/tools/trace_replay/fixtures}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
if ! [[ "${download_attempts}" =~ ^[1-9][0-9]*$ ]]; then
echo "MOBILEGL_TRACE_FIXTURE_DOWNLOAD_ATTEMPTS must be a positive integer: ${download_attempts}" >&2
exit 2
fi
if ! [[ "${retry_delay}" =~ ^[0-9]+$ ]]; then
echo "MOBILEGL_TRACE_FIXTURE_RETRY_DELAY must be a non-negative integer: ${retry_delay}" >&2
exit 2
fi
fixture_list="$("${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}")"
# Strip CR so the script also works when python emits CRLF (Git Bash on Windows).
mapfile -t files < <(printf '%s\n' "${fixture_list}" | tr -d '\r')
mapfile -t files <<< "${fixture_list}"
include="$(IFS=,; echo "${files[*]}")"
if [ "${case_name}" = "OpenRA" ]; then
@@ -66,134 +34,34 @@ if [ "${case_name}" = "OpenRA" ]; then
exit 0
fi
fetch_file_from_mirror() {
local file="$1"
local url="$2"
local metadata
local expected_oid
local expected_size
local tmp_file="${file}.tmp"
local attempt
local partial_size
local curl_status
local curl_auth
metadata="$(get_lfs_metadata "${file}")" || return 1
read -r expected_oid expected_size <<< "${metadata}"
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
if [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
elif [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
fi
fi
for ((attempt = 1; attempt <= download_attempts; attempt++)); do
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" -gt 0 ]; then
echo "Resuming mirror download for ${file} at byte ${partial_size} (attempt ${attempt}/${download_attempts})"
else
echo "Starting mirror download for ${file} (attempt ${attempt}/${download_attempts})"
fi
curl_auth=()
if [ -n "${mirror_token}" ]; then
curl_auth=(--header "Authorization: token ${mirror_token}")
fi
if curl -L --fail --show-error --continue-at - "${curl_auth[@]}" --output "${tmp_file}" "${url}"; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
echo "Mirror download failed integrity verification; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
else
curl_status=$?
partial_size=0
if [ -f "${tmp_file}" ]; then
partial_size="$(wc -c < "${tmp_file}" | tr -d '[:space:]')"
fi
if [ "${partial_size}" = "${expected_size}" ]; then
if verify_fixture_file "${tmp_file}" "${file}" "${expected_oid}" "${expected_size}"; then
mv "${tmp_file}" "${file}"
return 0
fi
rm -f "${tmp_file}"
partial_size=0
elif [ "${partial_size}" -gt "${expected_size}" ]; then
echo "Discarding oversized partial fixture ${tmp_file}: ${partial_size} > ${expected_size}" >&2
rm -f "${tmp_file}"
partial_size=0
elif [ "${curl_status}" -eq 33 ]; then
echo "Mirror refused the resume request; retrying from the beginning: ${file}" >&2
rm -f "${tmp_file}"
partial_size=0
fi
echo "Mirror download attempt ${attempt}/${download_attempts} failed with curl exit ${curl_status}; retained ${partial_size} bytes for resume: ${file}" >&2
fi
if [ "${attempt}" -lt "${download_attempts}" ]; then
sleep "${retry_delay}"
fi
done
rm -f "${tmp_file}"
return 1
}
# Files no mirror could serve, even after retrying every mirror. Only these fall
# back to Git LFS, so a mirror that served the rest of the case still spares
# GitHub the bandwidth for those files.
mirror_failures=()
fetch_from_mirror() {
mkdir -p "${fixture_dir}"
for file in "${files[@]}"; do
local name
local url
local base
local fetched=0
name="$(basename "${file}")"
for base in "${mirror_bases[@]}"; do
url="${base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if fetch_file_from_mirror "${file}" "${url}"; then
fetched=1
break
fi
echo "Mirror did not serve ${name}; trying the next mirror" >&2
done
if [ "${fetched}" -ne 1 ]; then
mirror_failures+=("${file}")
url="${mirror_base%/}/${name}"
echo "Fetching trace fixture from mirror: ${url}"
if ! curl -L --fail --retry 3 --retry-delay 2 -o "${file}.tmp" "${url}"; then
rm -f "${file}.tmp"
return 1
fi
mv "${file}.tmp" "${file}"
done
[ "${#mirror_failures[@]}" -eq 0 ]
}
if fetch_from_mirror; then
echo "Fetched trace fixture files for ${case_name} from mirror: ${include}"
else
fallback_include="$(IFS=,; echo "${mirror_failures[*]}")"
echo "All mirrors failed for ${#mirror_failures[@]} of ${#files[@]} file(s) of ${case_name}; falling back to Git LFS: ${fallback_include}"
echo "Mirror fetch failed for ${case_name}; falling back to Git LFS: ${include}"
git lfs install --local
git lfs pull --include="${fallback_include}" --exclude=""
git lfs pull --include="${include}" --exclude=""
fi
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
test -s "${file}"
if head -n 1 "${file}" | grep -q "version https://git-lfs.github.com/spec/v1"; then
echo "failed to hydrate LFS fixture: ${file}" >&2
exit 1
fi
done
-117
View File
@@ -1,117 +0,0 @@
#!/usr/bin/env bash
# Cache-side helper for trace fixtures.
#
# key <case> [fixture-dir] derive the actions/cache key and path list
# verify <case> [fixture-dir] check restored fixtures against their pointers
# reset <case> [fixture-dir] drop restored fixtures, leaving the pointers
#
# The cache key is content-addressed on the Git LFS pointer oids tracked at
# HEAD, which are readable from a plain checkout without smudging. Fixture
# content therefore maps 1:1 onto a key: unchanged content hits, changed
# content is a new key and thus a miss, and the download path handles it. The
# key deliberately carries no restore-keys prefix in the workflow - a fixture
# that does not match the pointer exactly must never be restored.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=trace-fixture-lib.sh
. "${script_dir}/trace-fixture-lib.sh"
# Bump when the key derivation changes in a way that must invalidate old
# entries; the content digest alone would not notice a format change.
key_schema="v1"
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
echo "usage: $0 <key|verify|reset> <trace-case> [fixture-dir]" >&2
exit 2
fi
command_name="$1"
case_name="$2"
fixture_dir="${3:-tools/trace_replay/fixtures}"
python_bin="${PYTHON:-python3}"
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
python_bin=python
fi
mapfile -t files < <(trace_fixture_files "${case_name}" "${fixture_dir}" "${python_bin}")
if [ "${#files[@]}" -eq 0 ]; then
echo "no fixture files declared for trace case: ${case_name}" >&2
exit 1
fi
# Writes "name=value" to $GITHUB_OUTPUT when running under Actions, and to
# stdout otherwise so the script stays runnable (and testable) off-CI.
emit_output() {
local name="$1"
local value="$2"
if [ -n "${GITHUB_OUTPUT:-}" ]; then
if [[ "${value}" == *$'\n'* ]]; then
local delimiter="ghadelim_$(date +%s%N)_$$"
{
printf '%s<<%s\n' "${name}" "${delimiter}"
printf '%s\n' "${value}"
printf '%s\n' "${delimiter}"
} >> "${GITHUB_OUTPUT}"
else
printf '%s=%s\n' "${name}" "${value}" >> "${GITHUB_OUTPUT}"
fi
fi
printf '%s=%s\n' "${name}" "${value}"
}
sanitize_case() {
printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g'
}
case "${command_name}" in
key)
manifest=""
for file in "${files[@]}"; do
# A case whose fixtures are committed directly rather than through Git LFS
# (OpenRA) has no pointer oid to key on, and nothing to download either.
# Report it as uncacheable so the workflow skips the cache entirely.
if ! metadata="$(get_lfs_metadata "${file}" 2>/dev/null)"; then
echo "trace case ${case_name} is not stored in Git LFS; skipping fixture cache" >&2
emit_output "cacheable" "false"
emit_output "key" ""
exit 0
fi
read -r expected_oid expected_size <<< "${metadata}"
manifest+="$(basename "${file}") ${expected_oid} ${expected_size}"$'\n'
done
digest="$(printf '%s' "${manifest}" | sha256sum | awk '{ print substr($1, 1, 16) }')"
safe_case="$(sanitize_case "${case_name}")"
emit_output "cacheable" "true"
emit_output "key" "trace-fixture-${key_schema}-${safe_case}-${digest}"
emit_output "paths" "$(printf '%s\n' "${files[@]}")"
;;
verify)
for file in "${files[@]}"; do
metadata="$(get_lfs_metadata "${file}")"
read -r expected_oid expected_size <<< "${metadata}"
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
done
echo "Verified ${#files[@]} fixture file(s) for ${case_name} against the tracked Git LFS pointers."
;;
reset)
# Put the working tree back to the pointer files a fresh checkout would
# have, so that a rejected cache entry falls through to exactly the same
# download path a cache miss takes.
for file in "${files[@]}"; do
rm -f "${file}" "${file}.tmp"
done
git checkout -- "${files[@]}"
echo "Reset ${#files[@]} fixture file(s) for ${case_name} to their tracked Git LFS pointers."
;;
*)
echo "unknown command: ${command_name}" >&2
exit 2
;;
esac
-73
View File
@@ -1,73 +0,0 @@
#!/usr/bin/env bash
# Shared helpers for trace-fixture handling: reading the in-tree Git LFS pointer
# metadata and verifying a fixture file against it. Sourced by
# fetch-trace-fixture-lfs.sh (verify after download) and by
# trace-fixture-cache.sh (cache key derivation and verify after cache restore),
# so both paths agree on what a valid fixture is.
# Reads the Git LFS pointer tracked at HEAD for a fixture path and prints
# "<oid> <size>". Fails if the tracked blob is not a well-formed LFS pointer.
get_lfs_metadata() {
local file="$1"
local pointer
local expected_oid
local expected_size
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
echo "failed to read tracked fixture metadata: ${file}" >&2
return 1
fi
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
return 1
fi
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
echo "invalid Git LFS pointer metadata: ${file}" >&2
return 1
fi
printf '%s %s\n' "${expected_oid}" "${expected_size}"
}
# Checks an on-disk fixture against the size and SHA-256 from its LFS pointer.
verify_fixture_file() {
local downloaded_file="$1"
local display_name="$2"
local expected_oid="$3"
local expected_size="$4"
local actual_oid
local actual_size
if [ ! -f "${downloaded_file}" ]; then
echo "fixture file is missing: ${display_name}" >&2
return 1
fi
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
if [ "${actual_size}" != "${expected_size}" ]; then
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
return 1
fi
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
if [ "${actual_oid}" != "${expected_oid}" ]; then
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
return 1
fi
}
# Prints the fixture file paths of a trace case, one per line. Strips CR so the
# result is usable when python emits CRLF (Git Bash on Windows).
trace_fixture_files() {
local case_name="$1"
local fixture_dir="$2"
local python_bin="${3:-python3}"
"${python_bin}" tools/trace_replay/trace_cases.py \
--format fixture-files \
--case "${case_name}" \
--fixture-root "${fixture_dir}" | tr -d '\r'
}
+5 -30
View File
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
@@ -61,40 +61,15 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
# entry icon and the bottom navigation bar switches to the render server screen. What is
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
# reds here instead of shipping two icons.
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
if [[ "$main_count" -ne 1 ]]; then
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+75 -337
View File
@@ -6,34 +6,11 @@ on:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
# branch runs the full lane on every push so a phase's landing is not gated on
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
- codex/fix-github-ci
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
# workflow keeps running for docs: it hosts the doc citation lint.
paths-ignore:
- 'docs/**'
- '**/*.md'
workflow_dispatch:
env:
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
env:
CCACHE_BASEDIR: ${{ github.workspace }}
CCACHE_COMPRESS: "true"
@@ -41,7 +18,6 @@ jobs:
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -59,20 +35,18 @@ jobs:
distribution: zulu
java-version: '17'
- name: Android replay lifecycle regression
run: python3 tools/trace_replay/test_android_lifecycle.py
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
with:
gradle-version: 8.10.2
- name: Restore ccache
uses: actions/cache/restore@v5
uses: actions/cache@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
${{ runner.os }}-apk-${{ github.job }}-ccache-
- name: Install ccache
@@ -85,8 +59,6 @@ jobs:
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
@@ -101,13 +73,7 @@ jobs:
run: python update_glslang_sources.py
- name: Build plugin APK
run: >-
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
--parallel --max-workers "$(nproc)"
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
@@ -149,14 +115,7 @@ jobs:
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
- name: Build retrace APK
run: >-
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
--parallel --max-workers "$(nproc)"
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
@@ -166,28 +125,6 @@ jobs:
if: always()
run: ccache --show-stats
# Rewrite one rolling entry per job on the default branch. The upload stays
# cumulative - it carries every object restored at the top of this run plus
# the few TUs that actually changed - but Actions cache keys are immutable,
# so the superseded blob has to be released before the same key can be
# re-uploaded. Running after the build means a failed build leaves the
# existing entry untouched. The other trigger branches restore this entry
# rather than each writing a ~4 GB one of their own.
- name: Release superseded ccache entry
if: github.ref_name == github.event.repository.default_branch
env:
GH_TOKEN: ${{ github.token }}
CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
run: gh cache delete "${CACHE_KEY}" || true
- name: Save ccache
if: github.ref_name == github.event.repository.default_branch
continue-on-error: true
uses: actions/cache/save@v5
with:
path: .ccache
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
- name: Verify APK metadata and packaging
run: |
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
@@ -238,8 +175,6 @@ jobs:
name: trace case matrix
runs-on: ubuntu-latest
needs: build
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
outputs:
android: ${{ steps.trace-cases.outputs.android }}
names: ${{ steps.trace-cases.outputs.names }}
@@ -250,12 +185,7 @@ jobs:
- name: Load trace cases
id: trace-cases
run: |
python3 tools/trace_replay/trace_cases.py --self-test-transport-matrices
matrix_format=github-apk-matrix
if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then
matrix_format=github-apk-transport-matrix
fi
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format "${matrix_format}")" >> "$GITHUB_OUTPUT"
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
@@ -271,41 +201,9 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v6
- name: Derive trace fixture cache key
id: fixture-key
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
- name: Restore trace fixture cache
id: fixture-cache
if: steps.fixture-key.outputs.cacheable == 'true'
uses: actions/cache/restore@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Verify restored trace fixture
id: fixture-verify
if: steps.fixture-cache.outputs.cache-hit == 'true'
run: |
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
fi
- name: Fetch trace fixture
if: steps.fixture-verify.outputs.ok != 'true'
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
- name: Save trace fixture cache
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ steps.fixture-key.outputs.paths }}
key: ${{ steps.fixture-key.outputs.key }}
- name: Stage trace fixture
run: |
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
@@ -329,8 +227,6 @@ jobs:
env:
AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
steps:
- name: Checkout repo
uses: actions/checkout@v6
@@ -339,8 +235,6 @@ jobs:
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
@@ -351,11 +245,11 @@ jobs:
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Create AVD
if: steps.android-avd-cache.outputs.cache-hit != 'true'
@@ -368,7 +262,7 @@ jobs:
--avd-name "${AVD_NAME}"
retrace:
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
runs-on: ubuntu-latest
needs:
- build
@@ -378,21 +272,23 @@ jobs:
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
timeout-minutes: 75
env:
MOBILEGL_CI_TRANSPORT: ${{ matrix.transport || 'monolith' }}
MOBILEGL_CI_LANE: ${{ matrix.lane || 'monolith-control' }}
AVD_NAME: mobilegl-ci
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
ANDROID_SDK_ROOT: ${{ github.workspace }}/.android/sdk
strategy:
fail-fast: false
max-parallel: 4
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
matrix:
backend:
- name: DirectGLES
gpu: software
- name: DirectVulkan
gpu: lavapipe
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
steps:
- name: Set swap space
- name: Set Swap Space
uses: pierotofy/set-swap-space@v1.0
with:
swap-size-gb: 8
swap-size-gb: 16
- name: Checkout repo
uses: actions/checkout@v6
@@ -418,8 +314,6 @@ jobs:
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
@@ -430,11 +324,11 @@ jobs:
with:
path: |
${{ env.ANDROID_AVD_HOME }}
${{ env.ANDROID_SDK_ROOT }}/emulator
${{ env.ANDROID_SDK_ROOT }}/platform-tools
${{ env.ANDROID_SDK_ROOT }}/platforms/android-35
${{ env.ANDROID_SDK_ROOT }}/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v2-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
/usr/local/lib/android/sdk/emulator
/usr/local/lib/android/sdk/platform-tools
/usr/local/lib/android/sdk/platforms/android-35
/usr/local/lib/android/sdk/system-images/android-35/google_apis/x86_64
key: ${{ runner.os }}-mobilegl-avd-api35-google_apis-x86_64-pixel_6-v1-${{ hashFiles('android-plugin/run-avd-ci.sh') }}
- name: Download retrace APK
uses: actions/download-artifact@v8
@@ -458,7 +352,7 @@ jobs:
--profile pixel_6 \
--avd-name "${AVD_NAME}"
- name: Launch emulator
- name: Launch Emulator
run: |
sh android-plugin/run-avd-ci.sh start \
--avd-name "${AVD_NAME}" \
@@ -469,115 +363,42 @@ jobs:
- name: Retrace and validate
env:
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
extra_retrace_args=()
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--require-inproc
)
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
# environment by the launcher and read by the client, which is where they belong.
#
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
# into the CLIENT's process and SetWindowHandle is refused on the way across with
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--use-pbuffer
--require-spawn
)
else
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
fi
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
fi
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
fi
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
extra_retrace_args+=(--coherent-as-flush)
fi
run_retrace() {
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
--apk-file "${apk_file}" \
--package top.mobilegl.plugin.trace \
--backend "${{ matrix.backend.name }}" \
--result-root android-retrace-result \
--fixture-root android-retrace-fixture \
--case "${{ matrix.case.name }}" \
--trace-archive "${{ matrix.case.trace_archive }}" \
--trace-file "${{ matrix.case.trace_file }}" \
--golden "${{ matrix.case.golden }}" \
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
--target-call "${{ matrix.case.target_call }}" \
--width "${{ matrix.case.width }}" \
--height "${{ matrix.case.height }}" \
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
--crop-x "${{ matrix.case.crop_x }}" \
--crop-y "${{ matrix.case.crop_y }}" \
--crop-width "${{ matrix.case.crop_width }}" \
--crop-height "${{ matrix.case.crop_height }}" \
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
"${extra_retrace_args[@]}"
}
retrace_status=0
run_retrace || retrace_status=$?
if [ "${retrace_status}" -eq 75 ]; then
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
# Surface-lost is retried rather than failed, so it would otherwise
# be invisible. Report it per job - a healthy run prints nothing and
# a rate spike shows up as a row per affected case.
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
surface_lost_retries=0
if [ -f "${reason_file}" ]; then
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
fi
if [ "${surface_lost_retries}" -gt 0 ]; then
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
>> "${GITHUB_STEP_SUMMARY}"
fi
# The restart truncates EMULATOR_LOG, and the attempt that lost the
# emulator is the one worth reading - the retry usually only shows
# the wreckage. Keep the first attempt's log before it is clobbered.
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
fi
sh android-plugin/run-avd-ci.sh stop \
--avd-name "${AVD_NAME}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}"
adb kill-server || true
sleep 2
sh android-plugin/run-avd-ci.sh start \
--avd-name "${AVD_NAME}" \
--gpu "${{ matrix.backend.gpu }}" \
--emulator-log "${EMULATOR_LOG}" \
--pid-file "${EMULATOR_PID_FILE}" \
--boot-timeout 300
run_retrace
elif [ "${retrace_status}" -ne 0 ]; then
exit "${retrace_status}"
fi
timeout "$(( ${{ matrix.case.timeout_seconds }} + 300 ))" sh android-plugin/trace-replay-ci.sh \
--apk-file "${apk_file}" \
--package top.mobilegl.plugin.trace \
--backend "${{ matrix.backend.name }}" \
--result-root android-retrace-result \
--fixture-root android-retrace-fixture \
--case "${{ matrix.case.name }}" \
--trace-archive "${{ matrix.case.trace_archive }}" \
--trace-file "${{ matrix.case.trace_file }}" \
--golden "${{ matrix.case.golden }}" \
--alternate-golden "${{ matrix.case.alternate_golden || '' }}" \
--target-call "${{ matrix.case.target_call }}" \
--width "${{ matrix.case.width }}" \
--height "${{ matrix.case.height }}" \
--ssim-threshold "${{ matrix.case.ssim_threshold || '0.99' }}" \
--crop-x "${{ matrix.case.crop_x }}" \
--crop-y "${{ matrix.case.crop_y }}" \
--crop-width "${{ matrix.case.crop_width }}" \
--crop-height "${{ matrix.case.crop_height }}" \
--timeout-seconds "${{ matrix.case.timeout_seconds }}" \
"${extra_retrace_args[@]}"
- name: Collect retrace summary inputs
if: always()
@@ -585,12 +406,6 @@ jobs:
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
mkdir -p "${result_dir}"
python3 - "${result_dir}/lane.json" <<'PY'
import json, os, sys
from pathlib import Path
Path(sys.argv[1]).write_text(json.dumps({"lane": os.environ["MOBILEGL_CI_LANE"],
"transport": os.environ["MOBILEGL_CI_TRANSPORT"]}, indent=2))
PY
if [ -s "${{ matrix.case.golden }}" ]; then
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
fi
@@ -607,15 +422,8 @@ jobs:
if [ -f "${EMULATOR_LOG}" ]; then
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
fi
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
fi
# A vanished emulator looks identical whether the host OOM killer took
# qemu or the renderer faulted. These two say which.
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
- name: Stop emulator
- name: Stop Emulator
if: always()
run: |
sh android-plugin/run-avd-ci.sh stop \
@@ -627,7 +435,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
@@ -636,8 +444,6 @@ jobs:
runs-on: ubuntu-latest
needs: retrace
if: always()
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -652,73 +458,25 @@ jobs:
with:
node-version: '22'
- name: Download Android monolith control results
- name: Download Android retrace results
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
path: retrace-artifacts/monolith-control
pattern: MobileGL-android-retrace-result-*
path: retrace-artifacts
- name: Download Android inproc acceptance results
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-*
path: retrace-artifacts/inproc-acceptance
- name: Download Android spawn acceptance results
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
path: retrace-artifacts/spawn-acceptance
- name: Render monolith control summary
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts/monolith-control \
--output-dir android-retrace-summary/monolith-control \
--title "MobileGL Android monolith control" \
--group-label "Android Emulator / monolith control" \
--html mobilegl-android-monolith-control.html
--input retrace-artifacts \
--output-dir android-retrace-summary \
--title "MobileGL Android retrace overview" \
--group-label "Android Emulator" \
--html mobilegl-android-retrace-overview.html
- name: Render inproc acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts/inproc-acceptance \
--output-dir android-retrace-summary/inproc-acceptance \
--title "MobileGL Android inproc acceptance" \
--group-label "Android Emulator / inproc acceptance" \
--html mobilegl-android-inproc-acceptance.html
- name: Render spawn acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
run: |
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
- name: Upload Android monolith control summary
- name: Upload Android retrace summary
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-monolith-control-summary-${{ github.sha }}
path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html
archive: false
if-no-files-found: error
- name: Upload Android inproc acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }}
path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html
archive: false
if-no-files-found: error
- name: Upload Android spawn acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
path: android-retrace-summary/mobilegl-android-retrace-overview.html
archive: false
if-no-files-found: error
@@ -734,61 +492,41 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
# Fail before deletion if either paginated inventory cannot be read.
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
> "${RUNNER_TEMP}/cleanup-jobs.txt"
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv' \
> "${RUNNER_TEMP}/cleanup-artifacts.txt"
declare -A failed_cases=()
while IFS= read -r job_name; do
case_name="${job_name#retrace (*, }"
case_name="${case_name%)}"
failed_cases["${case_name}"]=1
done < "${RUNNER_TEMP}/cleanup-jobs.txt"
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
)
if ((${#failed_cases[@]})); then
echo "Retaining fixtures and results for failed retrace case(s):"
echo "Retaining fixtures for failed retrace case(s):"
printf ' %s\n' "${!failed_cases[@]}"
else
echo "All retrace jobs succeeded; nothing needs to be retained."
echo "All retrace jobs succeeded; no fixtures need to be retained."
fi
deleted=0
retained=0
while IFS=$'\t' read -r artifact_id artifact_name; do
keep=0
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
case_name="${artifact_name#MobileGL-trace-fixture-}"
if [[ -v "failed_cases[${case_name}]" ]]; then
keep=1
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
# The result artifact carries mobilegl.log, retrace.log, logcat,
# the emulator log and the actual/diff images - the only record of
# why a retrace failed. Its name ends in -<backend>-<case>, so a
# suffix match on the case name keeps both backends' results for a
# case that failed on either of them, which is what a comparison
# needs. The match is anchored at the end, so a case name that is a
# prefix of a longer one does not retain the longer one's results.
for case_name in "${!failed_cases[@]}"; do
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
keep=1
break
fi
done
fi
if ((keep)); then
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
((retained += 1))
continue
fi
echo "Deleting ${artifact_name} (${artifact_id})"
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
((deleted += 1))
done < "${RUNNER_TEMP}/cleanup-artifacts.txt"
done < <(
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
)
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
+50 -3175
View File
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
name: Arch WSL GPU smoke
on:
workflow_dispatch:
push:
branches:
- ci/arch-wsl-gpu-runner
paths:
- .github/workflows/wsl-gpu-smoke.yml
- docs/ci/arch-wsl-gpu-runner.md
permissions: {}
concurrency:
group: arch-wsl-gpu-smoke
cancel-in-progress: false
jobs:
gpu-smoke:
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
timeout-minutes: 5
steps:
- name: Report runner identity
shell: bash
run: |
printf 'Runner: %s\n' "$RUNNER_NAME"
uname -n
uname -a
- name: Verify hardware GLES and Vulkan
shell: bash
run: /usr/local/libexec/mobilegl-runner/gpu-smoke
-4
View File
@@ -25,7 +25,3 @@ MobileGL/MG*/cmake-build*
/android-plugin/app/src/trace/jniLibs
/android-plugin/local.properties
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
+3 -9
View File
@@ -7,6 +7,9 @@
[submodule "3rdparty/SPIRV-Cross"]
path = 3rdparty/SPIRV-Cross
url = https://github.com/KhronosGroup/SPIRV-Cross.git
[submodule "include/FastSTL"]
path = include/FastSTL
url = https://github.com/MobileGL-Dev/FastSTL.git
[submodule "3rdparty/tracy"]
path = 3rdparty/tracy
url = https://github.com/wolfpld/tracy.git
@@ -28,12 +31,3 @@
[submodule "3rdparty/apitrace"]
path = 3rdparty/apitrace
url = https://github.com/MobileGL-Dev/apitrace.git
[submodule "3rdparty/asio"]
path = 3rdparty/asio
url = https://github.com/chriskohlhoff/asio.git
[submodule "include/ska"]
path = include/ska
url = https://github.com/MobileGL-Dev/flat_hash_map.git
[submodule "3rdparty/flatbuffers"]
path = 3rdparty/flatbuffers
url = https://github.com/google/flatbuffers.git
-1
Submodule 3rdparty/asio deleted from 8806a6803c
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
-11
View File
@@ -1,11 +0,0 @@
# glvnd vendor ABI headers
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
headers intact.
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
smaller dependency than a build-time download of a source tree.
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
-141
View File
@@ -1,141 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#include <GL/gl.h>
#if !defined(__GL_DISPATCH_ABI_H)
#define __GL_DISPATCH_ABI_H
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup gldispatchabi GL dispatching ABI
*
* This is not a complete ABI, but rather a fragment common to the libEGL and
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
* these client ABIs.
*/
/*!
* Thread-local implementation used by libglvnd. This is passed into the patch
* function callback via the type parameter.
*
* For most architectures, the vendor library can ignore this parameter, since
* it will always be the same value. It's used for systems like ARM, where the
* stubs might be use the ARM or Thumb instruction sets.
*
* The stub type does not make any distinction between TLS and TSD stubs. The
* entire purpose of entrypoint rewriting is to skip the dispatch table in
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
*/
enum {
/*!
* Indicates that the stubs aren't defined in assembly. For example, if the
* dispatch stubs are written in C. Vendor libraries generally won't see
* this value.
*/
__GLDISPATCH_STUB_UNKNOWN,
/*!
* Used for stubs on x86 systems.
*/
__GLDISPATCH_STUB_X86,
/*!
* Used for stubs on x86-64 systems.
*/
__GLDISPATCH_STUB_X86_64,
/*!
* Used for stubs on ARMv7, using the Thumb instruction set.
*/
__GLDISPATCH_STUB_ARMV7_THUMB,
/*!
* Used for stubs on ARMv7, using the normal ARM instruction set.
*/
__GLDISPATCH_STUB_ARMV7_ARM,
/*!
* Used for stubs on ARMv8/aarch64.
*/
__GLDISPATCH_STUB_AARCH64,
/*!
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
*/
__GLDISPATCH_STUB_X32,
/*!
* Used for stubs on PPC64 systems.
*/
__GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
*/
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on LOONGARCH/loongarch64.
*/
__GLDISPATCH_STUB_LOONGARCH64,
};
/*!
* A callback function called by the vendor library to fetch the address of an
* entrypoint.
*
* The function returns two pointers, one writable and one executable. The two
* pointers may or may not be the same virtual address, but they will both be
* mappings of the same physical memory.
*
* The vendor library should write its entrypoint to the address returned by
* \p writePtr, but should use the address from \p execPtr for things like
* calculating PC-relative offsets.
*
* Note that if this function fails, then the vendor library can still try to
* patch other entrypoints.
*
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
* be zero, even if the stub uses the thumb instruction set. The vendor library
* should use the \c type parameter of \c initiatePatch to determine which
* instruction set to use.
*
* \param funcName The function name.
* \param[out] writePtr The pointer that the vendor library can write to.
* \param[out] execPtr The pointer to the executable code.
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
*/
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
void **writePtr, const void **execPtr);
#if defined(__cplusplus)
}
#endif
#endif // __GL_DISPATCH_ABI_H
-464
View File
@@ -1,464 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#if !defined(__LIB_EGL_ABI_H)
#define __LIB_EGL_ABI_H
#include <stdint.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include "glvnd/GLdispatchABI.h"
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup eglvendorabi EGL Vendor ABI
*
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
*
* Each vendor is associated with three distinct dispatch table types:
*
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
* provided by the vendor at load time during the initial handshake.
* - dynamic EGL dispatch table: this is a structure allocated by the API
* library at runtime used to manage EGL extension functions which are not
* present in the static table.
* - core GL dispatch table: this is a structure maintained by the API library
* which contains both GL core (static) and GL extension (dynamic) functions.
*
* @{
*/
/*
* Rendering API handling:
*
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
* other API is added, then the major version number will be incremented.
*
* When the application calls eglBindAPI, libEGL will forward the call to every
* vendor library. In addition, a vendor library can query the current API from
* libEGL using the getCurrentApi callback.
*
* Vendor libraries are not required to support both GL and GLES, but they must
* be able to deal with either one as the current rendering API. If a vendor
* doesn't support the current API, then it should return an error from
* eglCreateContext.
*/
/*!
* Current version of the ABI.
*
* This version number contains a major number in the high-order 16 bits, and
* a minor version number in the low-order 16 bits.
*
* The major version number is incremented when an interface change will break
* backwards compatibility with existing vendor libraries. The minor version
* number is incremented when there's a change but existing vendor libraries
* will still work.
*/
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
{
return version >> 16;
}
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
{
return version & 0xFFFF;
}
/*!
* This opaque structure stores function pointers for EGL extension functions.
* It is allocated at runtime by the API library. Vendor-provided dispatch
* functions retrieve and operate on this structure using the API below.
*/
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
/****************************************************************************
* API library exports *
****************************************************************************/
typedef struct __EGLapiExportsRec {
/************************************************************************
* The following routines are used by vendor-implemented EGL dispatch
* functions to lookup and call into the right vendor.
************************************************************************/
/*!
* This function must be called at the start of every EGL dispatch stub. It
* performs any necessary per-call bookkeeping.
*/
void (* threadInit) (void);
EGLenum (* getCurrentApi)(void);
/*!
* This function retrieves the appropriate current dynamic dispatch table,
* if a GL context is current. Otherwise, this returns NULL.
*/
__EGLvendorInfo *(*getCurrentVendor)(void);
/*!
* This retrieves the current context for this thread.
*/
EGLContext (*getCurrentContext)(void);
/*!
* Returns the current display for this thread.
*/
EGLDisplay (*getCurrentDisplay)(void);
/*!
* Returns the current drawable for this thread.
*/
EGLSurface (*getCurrentSurface)(EGLint readDraw);
/*!
* This function retrieves an entry point from the dynamic dispatch table
* given an index into the table.
*/
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
/*!
* Sets the last error for the current thread. The error will be returned
* the next time the app calls eglGetError().
*
* This function will override a call to \c setLastVendor, and vice-versa.
*/
void (* setEGLError) (EGLint errorCode);
/*!
* Notifies libEGL about the vendor library that an EGL function is
* dispatched to. This is used to look up the last EGL error code from a
* vendor.
*
* A vendor library may generate an EGL error asynchronously, if it
* offloads some EGL functions onto a worker thread. In that case, calling
* \c setEGLError to set the last error would not work.
*
* When an EGL dispatch stubs finds the vendor library, it must call this
* function before it dispatches the function call.
*
* This function will override a call to \c setEGLError, and vice-versa.
*
* \param vendor The vendor library of the current EGL function call.
* \return EGL_TRUE on success, or EGL_FALSE on failure.
*/
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
/************************************************************************
* These routines are used by vendor dispatch functions to look up
* and add mappings between various objects and vendors.
************************************************************************/
// As far as I can tell, all EGL functions that take a context or drawable
// also take an EGLDisplay. So, we don't need any extra functions for
// mapping a context/surface/whatever to a EGLDisplay or vendor.
/*!
* Returns the EGL vendor for an EGLDisplay handle.
*/
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
/*!
* Returns the EGL vendor for an EGLDeviceEXT handle.
*/
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
/**
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
* any function that returns an EGLDeviceEXT handle should call this.
*
* Supported since ABI version 1.2.
*/
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
} __EGLapiExports;
/*****************************************************************************
* API library imports *
*****************************************************************************/
/*!
* The enum values accepted by \c __EGLapiImports::getVendorString.
*/
enum
{
/*!
* The set of platform extensions that the vendor supports.
*
* Platform extensions typically just add enums to the existing
* eglGetPlatformDisplay function, so we don't need any additional logic
* or special handling from libEGL itself.
*
* As a result, vendor libraries can add these extensions without having
* to modify libEGL to match.
*/
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
};
/*!
* This structure stores required and optional vendor library callbacks.
*/
typedef struct __EGLapiImportsRec {
/**
* Creates an EGLDisplay. This function is used to handle both
* \c eglGetDisplay and \c eglGetPlatformDisplay.
*
* For some platform types, libEGL may be able to identify a specific
* vendor to use.
*
* If libEGL can't figure out which vendor to use on its own, then it will
* go through the list of available vendor libraries and call this function
* until one succeeds.
*
* If the application calls eglGetPlatformDisplay, then the parameters are
* passed through from the application.
*
* If the application calls eglGetDisplay, and the native display handle is
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
* type, and will pass that platform to the vendor library.
*
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
* libEGL will call into each vendor library with \p platform set to
* \c EGL_NONE. The vendor library can then select a default display to
* return.
*
* In all cases, if the vendor library can't find a matching EGLDisplay,
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
* through the vendor's \c eglGetError function.
*/
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
const EGLAttrib *attrib_list);
/*!
* Checks if the vendor library supports a given client API (that is, the
* API value passed to \c eglBindAPI).
*/
EGLBoolean (* getSupportsAPI) (EGLenum api);
/*!
* (OPTIONAL) Returns a string from the vendor library. This is used for
* anything that isn't available from eglQueryString.
*/
const char * (* getVendorString) (int name);
/*
* NOTE: libEGL will load all of the vendor libraries up front, so it
* handles extension functions slightly differently than EGL.
*/
/*!
* This retrieves the pointer to the real EGL or core GL function.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function.
*/
void * (* getProcAddress) (const char *procName);
/*!
* This retrieves vendor-neutral functions which use the
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
*
* A vendor library must provide a dispatch function for all EGL display
* extension functions that it supports.
*
* Client extension functions cannot be dispatched based on an EGDisplay,
* so they must be handled in libEGL itself.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function or \p procName is not a EGL display function.
*/
void * (*getDispatchAddress) (const char *procName);
/*!
* This notifies the vendor library which dispatch table index is
* assigned to a particular EGL extension function.
*/
void (*setDispatchIndex) (const char *procName, int index);
/*
* The vendor library may use the isPatchSupported, initiatePatch,
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
* entrypoints at make current time, provided no other contexts are current
* and the TLS model supports this functionality. This is a performance
* optimization that may not be available at runtime; the vendor library
* must not depend on this functionality for correctness.
*
* To use this optimization, the vendor library must provide at least the
* isPatchSupported and initiatePatch entrypoints.
*
* Note that these functions are identical to the ones used in GLX.
*/
/*!
* (OPTIONAL) Checks to see if the vendor library supports patching the
* given stub type and size.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*/
GLboolean (* isPatchSupported)(int type, int stubSize);
/*!
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
* top-level entrypoints.
*
* The vendor library should use the \p lookupStubOffset callback to find
* the addresses of each entrypoint.
*
* This function may be called more than once to patch multiple sets of
* entrypoints. For example, depending on how they're built, libOpenGL.so
* or libGL.so may have their own entrypoints that are separate functions
* from the ones in libGLdispatch.
*
* Note that during this call is the only time that the entrypoints can be
* modified. After the call to \c initiatePatch returns, the vendor library
* should treat the entrypoints as read-only.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*
* \return GL_TRUE if the vendor library supports patching with this type
* and size.
*/
GLboolean (*initiatePatch)(int type,
int stubSize,
DispatchPatchLookupStubOffset lookupStubOffset);
/*!
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
* longer owns the top-level entrypoints.
*
* Libglvnd will take care of the restoring the entrypoints back to their
* original state. The vendor library must not try to modify them.
*/
void (*releasePatch)(void);
/*!
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
* This callback allows vendor libraries to perform any per-thread
* initialization.
*
* This is basically a workaround for broken applications. A lot of apps
* will make one or more invalid GLX/EGL calls on a thread (often including
* a MakeCurrent with invalid parameters), and then will try to call an
* OpenGL function.
*
* A non-libglvnd-based driver would be able to initialize any thread state
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
* past libGLX.
*
* This function is optional. If it's \c NULL, then libGLdispatch will
* simply ignore it.
*
* \note This function may be called concurrently from multiple threads.
*/
void (*patchThreadAttach)(void);
/*!
* (OPTIONAL) Tries to determine the platform type for a native display.
*
* If the vendor library provides this function, then libglvnd will call it
* to determine which platform to use for a native display handle in
* eglGetDisplay.
*
* If no vendor library identifies the platform, then libglvnd will fall
* back to its own platform detection logic.
*
* Libglvnd can call this function for any native display handle except
* \c EGL_DEFAULT_DISPLAY.
*
* No matter what the value of \p native_display, the vendor library must
* not crash, and must not return a false match. If the vendor library
* can't identify the display, then it must return \c EGL_NONE.
*
* In particular, that means that a vendor library must not return any sort
* of default or fallback platform.
*
* \param native_display The native display handle passed to eglGetDisplay.
* \return Either a platform type enum or EGL_NONE.
*/
EGLenum (* findNativeDisplayPlatform) (void *native_display);
} __EGLapiImports;
/*****************************************************************************/
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* Vendor libraries must export a function called __egl_Main() with the
* following prototype.
*
* This function also performs a handshake based on the ABI version number.
* Vendor libraries can optionally use the version number to support older
* versions of the ABI.
*
* \param[in] version The ABI version. The upper 16 bits contains the major version
* number, and the lower 16 bits contains the minor version number.
*
* \param[in] exports The table of functions provided by libEGL. This pointer will
* remain valid for as long as the vendor is loaded.
*
* \param[in] vendor The opaque pointer used to identify this vendor library. This
* may be used in future versions to provide additional per-vendor information.
*
* \param[out] imports The function table that the vendor library should fill
* in. The vendor library must assign every non-optional function in the
* struct.
*
* \return True on success. If the vendor library does not support the
* requested ABI version or if some other error occurs, then it should return
* False.
*/
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* @}
*/
#if defined(__cplusplus)
}
#endif
#endif /* __LIB_EGL_ABI_H */
+4 -732
View File
@@ -4,131 +4,20 @@ project("MobileGL")
option(MOBILEGL_BUILD_TEST "Build MobileGL tests" ON )
option(MOBILEGL_BUILD_BENCHMARK "Build MobileGL benchmarks" ON )
# Headless end-to-end GPU scenarios (MobileGL/MG_IntegrationTest). They need a
# real GPU/ICD to do anything, so they are off by default for CI; every scenario
# skips cleanly where there is none. Registered under the `integration-gpu`
# ctest label so a run can select or exclude them.
option(MOBILEGL_BUILD_INTEGRATION_TEST "Build MobileGL headless GPU integration tests" OFF)
option(MOBILEGL_FORCE_RELEASE_OPT "Enable Release optimization flags in Debug build" ON )
option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling" OFF)
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
# That emptiness is one of the two byte-level equalities the plan's validation
# gates keep (section 10.3).
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
# only make sense when the two roles share an address space. It is a SUPERSET, never a
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
# only decides whether the shims are compiled in.
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" OFF)
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
# the whole migration window; it retires with the pull path itself at P13.
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
if (ANDROID)
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
# ------- Android API level policy: minimum 26, decided here and only here -------
# MobileGL ships against API 26: the codebase must not use any API introduced
# after 26. That usage constraint is enforced where it is real - the shipping
# gradle build compiles at minSdk 26, where a newer API is simply undeclared
# and fails to compile. Configuring at a HIGHER level is therefore allowed
# (nothing in the tree may rely on it), but a LOWER level would change the
# libc contract underneath the shipped library and is refused.
#
# This has to live at configure time because the level cannot be corrected
# from a source header. A `#define __ANDROID_API__ 26` in a common header
# only rewrites the macro for the bionic headers that happen to be included
# after it; any libc++ header pulled in earlier has already latched its
# feature macros at the real configure-time level. libc++ and bionic then
# disagree about which symbols exist - libc++ calls e.g.
# pthread_cond_clockwait while bionic, re-read at the lowered level, has
# hidden its declaration. MobileGL/Defines.h carried exactly that pin from
# the first commit until it was removed; this guard is what replaces it.
#
# Read the level back from the compiler target triple first. Its trailing
# number (aarch64-none-linux-android26) is precisely what clang turns into
# __ANDROID_API__, so it cannot disagree with the compile itself, and it is
# already past every NDK normalisation step - codename aliases, "latest",
# and per-ABI minimum pull-ups. ANDROID_PLATFORM_LEVEL is the fallback for
# generators/languages where the triple variable is not populated.
#
# Note CMAKE_SYSTEM_VERSION is deliberately NOT consulted: it holds the API
# level only under the NDK's newer toolchain path, and is a meaningless 1
# when ANDROID_USE_LEGACY_TOOLCHAIN_FILE is on (which is what AGP has been
# defaulting to). Reading it would fail every legacy-mode build.
set(MOBILEGL_ANDROID_API_LEVEL 26)
set(_mobilegl_android_api "")
foreach (_mobilegl_api_triple "${CMAKE_CXX_COMPILER_TARGET}"
"${CMAKE_C_COMPILER_TARGET}")
if (NOT _mobilegl_android_api AND
_mobilegl_api_triple MATCHES "-android([0-9]+)$")
set(_mobilegl_android_api "${CMAKE_MATCH_1}")
endif()
endforeach()
foreach (_mobilegl_api_var ANDROID_PLATFORM_LEVEL ANDROID_NATIVE_API_LEVEL
ANDROID_PLATFORM)
if (NOT _mobilegl_android_api AND ${_mobilegl_api_var})
string(REGEX REPLACE "^android-" ""
_mobilegl_android_api "${${_mobilegl_api_var}}")
endif()
endforeach()
if (NOT _mobilegl_android_api MATCHES "^[0-9]+$")
message(FATAL_ERROR
"MobileGL: could not determine the Android API level (got "
"\"${_mobilegl_android_api}\"). Configure with the NDK toolchain "
"file and -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL}.")
elseif (_mobilegl_android_api LESS MOBILEGL_ANDROID_API_LEVEL)
message(FATAL_ERROR
"MobileGL requires at least Android API ${MOBILEGL_ANDROID_API_LEVEL}, "
"but this build resolved to API ${_mobilegl_android_api}.\n"
"Configure with -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL} "
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
"check that minSdk instead of adding an override).")
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
message(STATUS
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
"compile.")
endif()
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
unset(_mobilegl_android_api)
unset(_mobilegl_api_var)
unset(_mobilegl_api_triple)
endif()
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
if ((NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT) AND MOBILEGL_ENABLE_LTO)
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug" OR MOBILEGL_FORCE_RELEASE_OPT)
# Check if ThinLTO or LTO is suppported
include(CheckIPOSupported)
include(CheckCCompilerFlag)
@@ -187,61 +76,15 @@ if (MSVC)
endif()
# Generate MGGitHash.h
#
# RESULT_VARIABLE, WHICH THIS CALL DID NOT HAVE (CONTRACT-P6 4.2). Without it a failed
# `git rev-parse` yields "" and nothing says so. Verified live: building this worktree from WSL
# produces an EMPTY hash, because the worktree's `.git` is a file holding a WINDOWS path that
# WSL's git cannot resolve - so two builds from different commits got IDENTICAL ABI
# fingerprints and the stamp silently stopped doing its documented job.
execute_process(
COMMAND git rev-parse HEAD
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE MOBILEGL_GIT_STAMP_RESULT
ERROR_QUIET
)
string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT)
# --- the BUILD STAMP, and why it is a second pair rather than a fix to the first -------------
#
# GIT_COMMIT_HASH_SHORT MAY NOT MOVE. It is baked into glGetString(GL_VERSION)
# (MG_Impl/GLImpl/Getter/GL_Getter.cpp), so redefining it changes an APPLICATION-VISIBLE GL
# string and the pull build's .rodata - a behaviour leak and a G1 break at once. The handshake
# gets its own pair instead, consumed only by the ABI fingerprint, which no pull build compiles.
#
# PRESENT IS A FIRST-CLASS INPUT, not the emptiness of VALUE. "this build has no stamp" and
# "this build's stamp is the empty string" are different claims, and a fingerprint that could
# not tell them apart is how two unrelated builds agreed in the first place.
#
# -DMOBILEGL_BUILD_STAMP=<anything> (or the environment variable) overrides, because a tree git
# cannot read is not always a tree whose commit is unknown - CI has ${GITHUB_SHA} in hand.
set(MOBILEGL_BUILD_STAMP "" CACHE STRING "Explicit ABI-fingerprint build stamp when git cannot be read")
set(MOBILEGL_BUILD_STAMP_VALUE "")
if(NOT "${MOBILEGL_BUILD_STAMP}" STREQUAL "")
set(MOBILEGL_BUILD_STAMP_VALUE "${MOBILEGL_BUILD_STAMP}")
elseif(NOT "$ENV{MOBILEGL_BUILD_STAMP}" STREQUAL "")
set(MOBILEGL_BUILD_STAMP_VALUE "$ENV{MOBILEGL_BUILD_STAMP}")
elseif(MOBILEGL_GIT_STAMP_RESULT EQUAL 0 AND NOT "${GIT_COMMIT_HASH_FULL}" STREQUAL "")
set(MOBILEGL_BUILD_STAMP_VALUE "${GIT_COMMIT_HASH_FULL}")
endif()
if("${MOBILEGL_BUILD_STAMP_VALUE}" STREQUAL "")
set(MOBILEGL_BUILD_STAMP_PRESENT 0)
# A WARNING AND NOT A FATAL: a build with no stamp is still a usable build, it just cannot
# prove that a peer was built from the same tree. Saying so once at configure time is the
# difference between a known limitation and the silent one 4.2 found.
message(WARNING
"MobileGL: no build stamp. `git rev-parse HEAD` failed (result "
"'${MOBILEGL_GIT_STAMP_RESULT}') and MOBILEGL_BUILD_STAMP was not set, so the ABI "
"fingerprint cannot distinguish two builds of the same sizes from different commits - "
"which is exactly what a stale MOBILEGL_IPC_SERVER_PATH looks like. Pass "
"-DMOBILEGL_BUILD_STAMP=<commit> to restore it.")
else()
set(MOBILEGL_BUILD_STAMP_PRESENT 1)
endif()
configure_file(
${CMAKE_SOURCE_DIR}/MobileGL/MG_Util/Miscellany/MGGitHash.h.in
${CMAKE_BINARY_DIR}/generated/MGGitHash.h
@@ -257,7 +100,6 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
@@ -304,18 +146,12 @@ set(SOURCE_FILES
MobileGL/ConfigLoader.cpp
MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/X11/DisplayGuard.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
MobileGL/MG_Util/Math/VectorTypes.cpp
MobileGL/MG_Util/Metrics/TextureMetrics.cpp
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
MobileGL/MG_Util/Metrics/PipeStats.cpp
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
@@ -344,74 +180,30 @@ set(SOURCE_FILES
MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp
MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameSamplerFunctionParameterPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
@@ -421,10 +213,7 @@ set(SOURCE_FILES
MobileGL/MG_Impl/GLImpl/Framebuffer/Validators.cpp
MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
@@ -441,17 +230,14 @@ set(SOURCE_FILES
MobileGL/MG_Backend/Init.cpp
MobileGL/MG_Backend/BackendObject.cpp
MobileGL/MG_Backend/GpuHangWatch.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/Utils.cpp
MobileGL/MG_Backend/DirectGLES/Managers.cpp
MobileGL/MG_Backend/DirectGLES/MultiDraw.cpp
MobileGL/MG_Backend/DirectVulkan/DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp
MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
MobileGL/MG_Backend/DirectVulkan/VmaImpl.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp
@@ -469,10 +255,8 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/GLState/ShareGroupState.cpp
MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
@@ -486,17 +270,10 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
MobileGL/MG_State/GLState/ProgramState/ShaderCompileAdoptionMap.cpp
MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp
MobileGL/MG_State/GLState/RenderState/RenderState.cpp
MobileGL/MG_State/GLState/FramebufferState/FramebufferObject.cpp
@@ -507,205 +284,6 @@ set(SOURCE_FILES
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
)
# ---------------------------------------------------------------------------
# MG_Remote (disaggregated transport). Everything below is gated: with the
# option OFF not one file here is compiled and no include path is added.
# ---------------------------------------------------------------------------
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
# variable, not a forced cache write, for the reason the two implications below give: an
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
# with the transport shadowed off would compile a role-isolation shim over no roles.
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
set(MOBILEGL_BUILD_DISAGGREGATED ON)
endif()
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
# a checkout without the submodule must configure and build, just without the
# disaggregated shape, rather than fail with a missing-header error a hundred
# lines later. Note this only checks for the RUNTIME headers - flatc is never
# built here (see scripts/gen_protocol.py).
if (MOBILEGL_BUILD_DISAGGREGATED AND
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
message(WARNING
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
"disaggregated shape for this configure; the cached ON takes effect once the "
"submodule is present.")
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
# made the plain re-configure after `git submodule update` stay OFF with no message at
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
# was, so the next configure - with the submodule there - honours it.
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
# would still be defined over a build with no MG_Remote in it at all.
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
endif()
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
# at the CMake level from a legitimate transport-only build, which is why it is stated here
# rather than left to whoever hits the link error. Same normal-variable form as the two
# implications above.
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
"this configure: the split path decodes into the MGPipe applier, and the "
"applier is what MOBILEGL_PIPE_PUSH compiles")
set(MOBILEGL_PIPE_PUSH ON)
endif()
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
# cache write, for the same reason as the disaggregated fallback above.
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
set(MOBILEGL_PIPE_PUSH ON)
endif()
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
# A normal variable, not a forced cache write, for the same reason as the two above.
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
"configure: with nothing pushed it is the only arm there is")
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
endif()
if (MOBILEGL_PIPE_PUSH)
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
MobileGL/MG_Impl/Pipe/PipeFill.cpp
# P2's contract: the chunk table and its subset hash, the in-process applier, and
# the client's {slot, gen} allocator. All three are push-only, which is how the
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
MobileGL/MG_Pipe/PipeApply.cpp
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
# VisitFields tables. Push-only for the same G1 reason as the three above - in
# monolith the archive never crosses (create_shader_state hands the two structs over
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
# where the applier serialises, deserialises and field-compares before storing.
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
# the two tables are inline variables that are zero in a pull build and nothing there
# can reach a thunk.
MobileGL/MG_Pipe/PipeRoute.cpp
)
endif()
if (MOBILEGL_BUILD_DISAGGREGATED)
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
list(APPEND SOURCE_FILES
MobileGL/MG_Remote/Transport/ShmLink.cpp
MobileGL/MG_Remote/Transport/LinkMetrics.cpp
MobileGL/MG_Remote/Transport/StreamLink.cpp
MobileGL/MG_Remote/Transport/Ring.cpp
MobileGL/MG_Remote/Transport/Doorbell.cpp
MobileGL/MG_Remote/Transport/ShmSegment.cpp
# Both platform halves are listed unconditionally and each is empty on
# the other OS, so neither can rot behind an `if (WIN32)` nobody
# configures.
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
MobileGL/MG_Remote/Transport/FdPassing.cpp
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
MobileGL/MG_Remote/Transport/SocketTransport.cpp
MobileGL/MG_Remote/Transport/ControlInbox.cpp
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
MobileGL/MG_Remote/Transport/LogForward.cpp
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
# umbrella header - out of the header-only wire code (WireLog.h).
MobileGL/MG_Remote/Transport/WireLog.cpp
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
MobileGL/MG_Remote/Transport/AdoptTier.cpp
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
MobileGL/MG_Remote/Transport/AdoptT0.cpp
# ---- P5: the three new directories ------------------------------
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
# var-tails in and out of SEG_STAGE. [w1]
# Client/ the emitting role: session, the 69-slot emit table, the
# caps mirror. [c1]
# Server/ the applying role: session, the applier bridge onto the
# existing MGPipeApply* free functions, the apply thread. [v1]
#
# Every file below lands in P5 as a HEADER plus a .cpp of named
# Fatal stubs, so that all seven P5 packages compile and link on day
# one against signatures that cannot then move under them. A stub is
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
# emitter that returns quietly is how a split lane runs monolith and
# goes green (ARCHITECTURE.md 10.3).
MobileGL/MG_Remote/CapsCodec.cpp
# P6 dl: the one place a session dies - Session::Fail's funnel (Fatal-family aware,
# publishes SessionFault, one telemetry point).
MobileGL/MG_Remote/FatalFunnel.cpp
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
MobileGL/MG_Remote/Client/ClientSession.cpp
# Whether the configured server is there at all, asked before a display is offered so the
# system's EGL/GLX/GBM loaders can move on to another implementation when it is not.
MobileGL/MG_Remote/Client/ServerProbe.cpp
MobileGL/MG_Remote/Client/EmitTables.cpp
MobileGL/MG_Remote/Client/CapsMirror.cpp
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
# P5 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
# emitters installed over the two generated tables, which is what stops every
# resource/CSO/texture/program record executing synchronously on the GL thread under
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
MobileGL/MG_Remote/Client/WireTables.cpp
# P5 b1's two: the conservative GPU-write set the client must build because all six
# MarkGpuWritten producers are on the server's side of the line, and the
# block-granularity persistent-map push that tier T2 makes mandatory.
MobileGL/MG_Remote/Client/GpuWritePending.cpp
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
MobileGL/MG_Remote/Server/ServerSession.cpp
MobileGL/MG_Remote/Server/PipeApplier.cpp
MobileGL/MG_Remote/Server/ServerLoop.cpp
# P14 S2: the per-session runtime and the thread routing that lets one process serve
# several sessions (the real session a singleton accessor answers with on that
# session's own threads).
MobileGL/MG_Remote/Server/SessionRuntime.cpp
MobileGL/MG_Remote/Server/ServerMain.cpp
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
MobileGL/MG_Remote/Server/PairAcceptor.cpp
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
MobileGL/MG_Remote/Server/AdoptInbox.cpp
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
# (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
MobileGL/MG_Remote/Server/ServerSpawn.cpp
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp
MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp
# P12 (on-screen server window): the process-wide holder of the server's own window
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
# ServerOwned request is refused by name there).
MobileGL/MG_Remote/Server/ServerDisplay.cpp
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
MobileGL/MG_Remote/Server/EmbeddedServer.cpp
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
# arms and the probe that measures the first against the second, shared by the
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
# so it stays out of the pull build (gate G1).
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
)
endif()
if (APPLE AND NOT MOBILEGL_IOS)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
@@ -718,30 +296,9 @@ endif()
if (ANDROID)
list(APPEND SOURCE_FILES
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
list(APPEND SOURCE_FILES
MobileGL/MG_Remote/Server/DisplayServerJni.cpp
)
endif()
endif()
if (WIN32)
list(APPEND SOURCE_FILES
MobileGL/MG_Impl/WGLImpl/WGLImpl.cpp
MobileGL/MG_Impl/WGLImpl/Exporting/Definitions.cpp
)
endif()
# The shader-compile pool runs standalone Asio on real threads. This host's glibc (>= 2.34)
# merged pthread into libc, so it links without asking, but the NDK and musl are not
# guaranteed to be as forgiving - ask for it explicitly rather than rely on the accident.
find_package(Threads REQUIRED)
set(MOBILEGL_LINK_LIBRARIES
glslang::glslang
spirv-cross-c
@@ -751,74 +308,29 @@ set(MOBILEGL_LINK_LIBRARIES
GPUOpen::VulkanMemoryAllocator
Vulkan::UtilityHeaders
spirv-reflect-static
Threads::Threads
)
set(MOBILEGL_COMPILE_DEF
-DVMA_STATIC_VULKAN_FUNCTIONS=0
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
-DVMA_VULKAN_VERSION=1001000
# Header-only Asio, no Boost, no deprecated interfaces. Set on the definition list
# rather than per-target so the shared library and the _s static target agree.
-DASIO_STANDALONE
-DASIO_NO_DEPRECATED
)
if (MOBILEGL_BUILD_DISAGGREGATED)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
endif()
if (MOBILEGL_PIPE_PUSH)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
endif()
if (MOBILEGL_PIPE_VERIFY)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
endif()
if (MOBILEGL_PIPE_LEGACY_MEMOS)
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
endif()
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
set(MOBILEGL_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/include
${CMAKE_SOURCE_DIR}/MobileGL
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
# line above too; this entry lets the client, the backends and MG_Remote spell them
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
${spirv-tools_SOURCE_DIR}
${spirv-tools_SOURCE_DIR}/include
${spirv-tools_BINARY_DIR}
${SPIRV-Headers_SOURCE_DIR}/include
# Header-only submodule: no add_subdirectory, no link target. Only
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
# pimpl so no consumer target needs this path.
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# Header-only runtime: an include path, no add_subdirectory, no link
# target, and above all no flatc in the build graph. protocol_generated.h
# is committed and regenerated by scripts/gen_protocol.py.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
endif()
add_library(${CMAKE_PROJECT_NAME} SHARED
add_library(${CMAKE_PROJECT_NAME} SHARED
${SOURCE_FILES}
)
if (WIN32)
# The wgl* entry points are exported via .def (see the comment in wgl.def);
# only the shared library links it.
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
MobileGL/MG_Impl/WGLImpl/Exporting/wgl.def
)
endif()
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES
C_VISIBILITY_PRESET default
@@ -851,7 +363,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
if(UNIX AND NOT APPLE AND NOT ANDROID)
foreach(MOBILEGL_LOADER_ALIAS
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
libEGL.so libEGL.so.1)
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
@@ -861,18 +373,6 @@ if(UNIX AND NOT APPLE AND NOT ANDROID)
endforeach()
endif()
if(WIN32)
# Drop-in for the classic GL loader path: a copy named opengl32.dll placed
# next to a host executable is what LoadLibrary("opengl32.dll") and gdi32's
# pixel-format forwarding will resolve.
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:${CMAKE_PROJECT_NAME}>"
"$<TARGET_FILE_DIR:${CMAKE_PROJECT_NAME}>/opengl32.dll"
COMMENT "Creating opengl32.dll drop-in copy"
)
endif()
if(NOT ANDROID)
add_library(${CMAKE_PROJECT_NAME}_s STATIC
${SOURCE_FILES}
@@ -915,28 +415,6 @@ if (TRACY_ENABLE)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
endif ()
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
#
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
# With default visibility every reference inside this library resolves against the whole process,
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
# the two visibilities do not mix; this flag is that promise made by the linker instead.
if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
# docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
# it through dlsym. Self-contained: nothing outside this block refers to either.
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
add_subdirectory(MobileGL/MG_Gbm)
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -946,21 +424,8 @@ if (ANDROID)
endif()
if (APPLE AND NOT MOBILEGL_IOS)
# MobileGL statically embeds glslang, SPIRV-Tools, and SPIRV-Cross. When
# this dylib is injected with DYLD_INSERT_LIBRARIES, exporting those C++
# symbols interposes incompatible copies embedded by host libraries such
# as shaderc. Keep only the public GL/EGL/CGL loader surface globally
# visible; GetProcAddress can still return pointers to hidden internals.
set(MOBILEGL_MACOS_EXPORTED_SYMBOLS
"${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Impl/DyldInterpose/ExportedSymbols.txt")
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE
"LINKER:-exported_symbols_list,${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
set_property(TARGET ${CMAKE_PROJECT_NAME} APPEND PROPERTY
LINK_DEPENDS "${MOBILEGL_MACOS_EXPORTED_SYMBOLS}")
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
@@ -968,7 +433,6 @@ if (APPLE AND NOT MOBILEGL_IOS)
if(TARGET ${CMAKE_PROJECT_NAME}_s)
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC
"-framework Cocoa"
"-framework CoreVideo"
"-framework QuartzCore"
"-framework Foundation"
"-framework OpenGL"
@@ -1010,209 +474,17 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
endif ()
endif ()
# Discover gtest cases when ctest starts, one binary after another, instead of in a post-build
# step. The post-build discovery of the CMake releases that never pass the target name down
# writes every binary's `--gtest_output=json` result to ONE per-directory file
# (cmake_test_discovery_<hash of an empty name>.json), so binaries linked in parallel in the same
# directory overwrite and delete each other's list: the registered count then differs between
# runs of the same tree and a binary ends up with none or only some of its cases. The pre-test
# mode reads the list from stdout, serially, and also works when the binary is cross-built.
if (NOT DEFINED CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE)
set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
endif()
if (NOT ANDROID)
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
# at the build-tree root. This lets `ctest` be invoked from the top-level
# build directory (IDE "run all tests", CI) and discover every test in the
# subdirectories below, instead of having to descend into each
# MG_Test/MG_Benchmark subdirectory. Tests are tagged with CTest labels
# (unit / benchmark / integration), so e.g. `ctest -L unit` selects just
# the unit suite.
enable_testing()
if (MOBILEGL_BUILD_TEST)
add_subdirectory(MobileGL/MG_Test)
endif()
# After MG_Test so googletest is already available when the unit tests are
# built; the module fetches its own copy when they are not.
if (MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
if (MOBILEGL_BUILD_BENCHMARK)
add_subdirectory(MobileGL/MG_Benchmark)
endif()
if (MOBILEGL_BUILD_TRACE_REPLAY)
enable_testing()
add_subdirectory(tools/trace_replay)
endif()
endif()
# The integration binary is also useful as a standalone adb-shell executable.
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
# links libMobileGL.so and creates an AImageReader-backed window instead.
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
add_subdirectory(MobileGL/MG_IntegrationTest)
endif()
# The replay runner also builds for Android: an adb-shell executable that can
# dial a render server on the device (unix or TCP) without any Activity.
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
add_subdirectory(tools/trace_replay)
endif()
# ---------------------------------------------------------------------------
# P0 spike A: the Android delivery chain for a second native executable.
#
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
# so a second executable has to be built with an .so name and exec'd out of
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
# chain end to end: it is packaged like a library, exec'd from the app's own
# untrusted_app process, and writes a marker the parent reads back.
#
# Off by default and ANDROID-only, so no shipping configuration builds it. The
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# P6 `sm`: the REAL server image. Unlike MOBILEGL_BUILD_SERVER_SPIKE below - the
# P0 spike, which links no MobileGL library and exists only to prove the Android
# delivery chain - this one links the library and runs mobilegl_server_main.
#
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
# dlsym's the entry point, so one shared library serves both roles. On a desktop
# configure we link it directly instead: there is no APK packaging rule to satisfy
# and a direct link is what makes an undefined symbol a BUILD error rather than a
# runtime dlsym failure in a child whose stderr is /dev/null.
#
# Android keeps the lib*.so naming and the nativeLibraryDir output the spike
# proved is the only exec-able location in an APK.
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT WIN32)
add_executable(MobileGLServer
${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Remote/Server/ServerEntry.cpp)
target_include_directories(MobileGLServer PRIVATE
${MOBILEGL_INCLUDE_DIR})
target_link_libraries(MobileGLServer PRIVATE ${CMAKE_PROJECT_NAME})
set_target_properties(MobileGLServer PROPERTIES
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLServer PRIVATE -fPIE)
target_link_options(MobileGLServer PRIVATE -pie)
# $ORIGIN, OR THE SERVER CANNOT START AT ALL ON ANDROID.
#
# This executable links libMobileGL.so, and it is EXEC'd rather than dlopen'd - so it gets a
# fresh linker namespace with the default search path, not the app's. An APK's lib/<abi>/ is
# not on that path, and the failure is the bluntest one there is:
#
# F linker: CANNOT LINK EXECUTABLE ".../lib/arm64/libMobileGLServer.so":
# library "libMobileGL.so" not found: needed by main executable
#
# It cost a device run to find because it is invisible on desktop: CMake gives build-tree
# targets an absolute RPATH automatically, so the WSL server resolved its dependency through a
# path that does not exist on a phone. $ORIGIN says "beside me", which is true in the APK, true
# in the build tree, and true wherever somebody copies the pair - and it is what makes a
# deployment that is NOT a build tree work at all, which the end state is.
# THE PROPERTIES, NOT target_link_options. A raw `-Wl,-rpath,$ORIGIN` reaches the generator
# unquoted and the SHELL expands $ORIGIN to nothing - measured on device, where the linker
# said `normalize_path - invalid input: "$"` and then failed to find libMobileGL.so anyway.
# BUILD_RPATH / INSTALL_RPATH are the supported spelling and CMake escapes them correctly.
set_target_properties(MobileGLServer PROPERTIES
BUILD_RPATH "$ORIGIN"
INSTALL_RPATH "$ORIGIN"
BUILD_WITH_INSTALL_RPATH TRUE)
if (ANDROID)
# An APK's only exec-able install location is lib/<abi>/, and the packager
# only puts a file there if it is named lib*.so. P0 spike A proved this
# works from the app's own untrusted_app domain on two devices.
set_target_properties(MobileGLServer PROPERTIES
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer")
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
else()
# Desktop: the name the dladdr fallback looks for beside libMobileGL.so.
set_target_properties(MobileGLServer PROPERTIES
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
endif()
endif()
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
# Renamed by P6 sm: the real server took the name it had been holding.
add_executable(MobileGLServerSpike
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
# An executable that is named like a shared library still has to be a real
# PIE executable: Android has refused non-PIE executables since API 21, and
# the name alone does not change what the loader demands of the file.
set_target_properties(MobileGLServerSpike PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServerSpike"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLServerSpike PRIVATE -fPIE)
target_link_options(MobileGLServerSpike PRIVATE -pie)
# AGP packages what the external native build drops into the per-ABI output
# directory, and it selects by the .so extension. CMake puts executables in
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
# the library directory when the generator gave us one.
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLServerSpike PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
# app's own process by the spike hook (`--es mobilegl_spike_spawn
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
# links nothing from MobileGL.
add_executable(MobileGLExtmemProbe
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbe PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLExtmemProbe"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbe PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
if (TARGET MobileGLServer)
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
endif()
add_executable(MobileGLExtmemProbeAsServer
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
PREFIX "lib"
SUFFIX ".so"
OUTPUT_NAME "MobileGLServer"
POSITION_INDEPENDENT_CODE ON)
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
endif()
endif()
endif()
+9 -694
View File
@@ -14,48 +14,12 @@ namespace MobileGL::MG_Config {
inline const String ProjectName = "MobileGL";
inline const String CoreName = "MobileGL Core";
inline const String CoreVendor = "MobileGL-Dev (BZLZHH, Swung0x48, Tungsten)";
inline const Version CoreVersion = {26, 10, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 7, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
extern BackendType ActiveBackendType;
// Tri-state override for device-specific quirks: Auto lets the detected device decide,
// ForceOn/ForceOff bypass the detection in either direction. ForceOn only bypasses the
// device gate - each quirk keeps its structural safety checks.
enum class QuirkOverride : Uint8 {
Auto = 0,
ForceOn,
ForceOff,
};
// Preferred DirectVulkan dispatch tier for the glMultiDraw* families. A preference,
// never a demand: the renderer clamps it to what the device supports at device
// creation, falling down the chain ext -> indirect -> unroll with one log line.
enum class MultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // VK_EXT_multi_draw: one vkCmdDrawMultiEXT / vkCmdDrawMultiIndexedEXT
Indirect, // multiDrawIndirect feature: one vkCmdDraw*Indirect over a transient command array
Unroll, // one vkCmdDraw* per sub-draw
};
// Preferred DirectGLES emulation tier for glMultiDrawElements(BaseVertex). GLES has no
// such entry point in core, so every tier below is an emulation; they differ only in
// which driver capability they lean on and how many driver calls a batch costs. Like
// the Magma knob this is a preference, clamped at resolution time to what the ES
// driver actually supports, with one log line when it falls back.
enum class GLESMultiDrawMode : Uint8 {
Auto = 0, // unset: best supported tier
Ext, // one glMultiDrawElementsBaseVertexEXT
MultiIndirect, // one glMultiDrawElementsIndirectEXT over a scratch command buffer
Indirect, // one glDrawElementsIndirect per sub-draw over that same buffer
BaseVertex, // one glDrawElementsBaseVertex per sub-draw
DrawElements, // baseVertex folded into a scratch index buffer on the CPU, then plain
// glDrawElements per sub-draw (for drivers with no base-vertex draw at all)
Compute, // a compute shader flattens every sub-draw into one rebased index buffer,
// drawn by a single glDrawElements
};
// Feature toggles parsed once from environment variables in MG_ConfigLoader::Init()
// (ConfigLoader.cpp), before the accepted-env map is destroyed. All Bool fields share
// one truthy rule: the variable is set, non-empty, not "0", and not "false"
@@ -69,119 +33,21 @@ namespace MobileGL::MG_Config {
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
// there, because the view's ES internalformat is normalized independently of the storage
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
// so that work can be done without editing the gate.
Bool EsprytEnableTextureView = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool EsprytUseAngle = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds. The pass grows that one array to what the device's topology needs and
// touches nothing else; it only rewrites modules positively matching the pack's
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
// Off by default so an application that checks the string before using doubles keeps
// its float path; on for measuring what the conformance suite makes of the demoted
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
Bool AdvertiseFp64 = false;
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
// into device loss. For diagnosing a driver that copes with a specific module only.
Bool MagmaAllowInvalidSpirv = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
Uint32 MagmaDescriptorTrimFrames = 120;
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool EsprytAvoidSamplerMipmapMinFilter = false;
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
// the uniform turns a compile-time-constant LOD into a runtime expression, which
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
Bool EsprytAvoidExplicitLodBias = false;
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
// match them by block name and member sequence instead. The Mali ES driver delivers
// nothing at all through a located block once a tessellation or geometry stage is in
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
// healthy driver - which is what the integration lane does, since llvmpipe and
// lavapipe carry a located block correctly and would otherwise never run this code -
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
// stages into an ordinary varying (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
// on a device that advertises neither EXT/OES_tessellation_point_size /
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
// natively and would otherwise never run this code, which is what the pinned
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
// negative control). Cross-backend by design: the demotion runs in the shared
// phase-B chain, so one switch covers both. See DemotePointSizePass.
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
@@ -191,561 +57,10 @@ namespace MobileGL::MG_Config {
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool EsprytDisableUboRing = false;
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
// glTexSubImage from the client pointer instead of staging them through the
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
// hatch).
Bool EsprytDisableUnpackRing = false;
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
// (glBufferSubData / map flushes) back to the immediate driver upload instead
// of queueing them for the staged-copy flush through the persistent-mapped
// upload ring (negative control / driver-bug escape hatch; the immediate
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
Bool EsprytDisableUploadRing = false;
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
// to the upload ring's staged glCopyBufferSubData (negative control / escape
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
// on Mali both the immediate glBufferSubData and a staged copy into a busy
// mutable store ghost the whole destination on the CPU.
Bool EsprytDisableInvalidateFlush = false;
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
// Adreno does not), which means the emulation is dead code on exactly the stack the
// headless suite runs on. This forces it live so the scenarios and the CTS can
// exercise the path, and gives the device an A/B lever over the same choice.
Bool EsprytForceDepthStencilReadbackEmulation = false;
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
// texture-name reuse after delete) even on contexts that explicitly requested a core
// profile. Without it, relaxed semantics still apply to every context that did not
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
// version request.
Bool RelaxedSemantics = false;
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
// cross-pipeline vertex position invariance. Sorted-transparency "over" blends,
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
// feature off. It is enabled by default to match GL's defined out-of-range fetch
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
Bool MagmaDisableRobustBufferAccess = false;
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
// unset picks the best supported tier.
MultiDrawMode MagmaMultiDrawMode = MultiDrawMode::Auto;
// MOBILEGL_ESPRYT_MULTIDRAW_MODE: preferred DirectGLES glMultiDrawElements emulation
// tier ("ext" | "multiindirect" | "indirect" | "basevertex" | "drawelements" |
// "compute", see GLESMultiDrawMode). Clamped to driver support; unset picks the best
// supported tier, which never includes "compute" - see the note on its resolution.
GLESMultiDrawMode EsprytMultiDrawMode = GLESMultiDrawMode::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE: overrides asynchronous shader compilation. Unset
// keeps the built-in default (MG_Util::Async::kAsyncShaderCompileDefault); falsy
// forces every glCompileShader/glLinkProgram to run synchronously on the calling
// thread AND withdraws GL_KHR_parallel_shader_compile, so the single switch reverts
// both the threading and the application-visible behaviour change.
QuirkOverride AsyncShaderCompile = QuirkOverride::Auto;
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
// auto, which is min(4, big cores); an explicit value is honoured as given.
Uint32 AsyncShaderCompileThreads = 0;
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
// empty, WITHOUT joining the job (latched per compile - see
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
// a real failure still fails the program link with the compile log quoted. It
// exists for applications that compile hundreds of shaders serially and read the
// status right after each glCompileShader - Iris's shader-pack load - where those
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
// phase issues no program-level query between programs; program-level LINK_STATUS
// and the program info log still join truthfully, so paths that check each link
// immediately stay serial by their own construction). Off by default; never
// advertise it.
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
// ON; ForceOff turns ALL THREE off and makes every translation run from
// scratch. The escape hatch exists because a wrong cache hit is a silently
// miscompiled shader: if a device ever renders differently with the cache
// on, one run with this falsy says so.
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
// emulation - the builtin becomes a flat varying, the fragment stage gets a
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
// it is ON even where the driver advertises GL_OES_viewport_array, because that
// extension only ever gave the SHADER a compilable name: MobileGL has never
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
// and nothing else), so on an extension-capable driver every index rasterized as
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
// the pre-emulation path, extension passthrough where it exists and
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
// the negative control the emulation is measured against.
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
// packed16 allocations with a MIRRORED field order (allocation-scoped and
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
// reversed whenever exactly one endpoint sits in a mirrored allocation
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
// With no 16-bit packed ES image left there is no field order to disagree about; the
// client word still round-trips exactly, because the canonical shadow is already
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
// path); ForceOff keeps the native narrow storage even where the probe fires - the
// negative control that replays the corruption. Costs 2x the memory of the affected
// formats where it engages, which is why Auto is probe-gated rather than always-on.
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
// reroute for draws made while transform feedback is INACTIVE. The stream query
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
// tessellation tests per tree size a capture buffer from the 0 and die on the
// zero-length map. Auto defers to a device probe at renderer bring-up
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
// the same capture-less draws and arms the best proven one: the dedicated
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
// the reroute structurally wherever a pool can exist (the arming-observable lane,
// immune to the probe's verdict moving), and ForceOff is the negative control that
// replays the driver's silence.
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
// 0 - the only shipped value until the migration lands - is "pull everything",
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
// is the "everything before P5e" control P5e's A/B is run against - each phase's
// constant survives as the next phase's control, which is why none of them is ever
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
// bits are listed here (MG_Pipe/MGPipe.h owns them):
// 0x01 render state (create/bind_render_state + set_dynamic_state)
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
// 0x400 texture resources (texture + renderbuffer resource_*,
// set_texture_params) - requires 0x80 AND 0x800
// (the built-in sampler CSO a set_texture_params record names is minted by
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
// bind_sampler_states / set_shader_images) - requires 0x400
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
// classes and the dirty bits 15/16/17) - requires 0x80
// (every MGPBufferRange::Res names a Buffer handle; P5e)
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
// family runs its legacy arm; it is never half-run.
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
// never probed. The negative control the CSO design is measured against.
Uint64 PipePush = 0;
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
// against a snapshot taken from GLContext the old way, printing the first field
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
// the semantic gate that replaces byte identity, and it catches the dangerous
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
Bool PipeVerify = false;
#if MOBILEGL_PIPE_PUSH
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
// counts instead, for triage and for the lane that must survive to read its own
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
// off.
Bool PipeVerifyFatal = true;
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
// green verify run goes red naming it (negative control A). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipeVerifyCorrupt;
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
// the value) of that field for that verb, an omission indistinguishable from a
// forgotten FillPoints.def row, so that verb's read of it is
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
// Fatal{PipeVerifyBadKnob}.
String PipePoisonOmit;
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
// block is never handed back, so that spelling collided with nothing and the control
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
// measurement and for what the control still leaves standing. Under
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
Bool PipeHandleAbaControl = false;
#endif
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
Bool PipeStats = false;
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
// alive so the first handle waves have a real old-versus-new arm to be compared
// against. ON by default for the whole migration window, deleted with the pull
// path itself.
Bool PipeLegacyMemos = true;
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
// holds a complete CPU shadow, so this cache buys latency, never correctness.
Uint32 PipeTexelRetainMb = 0;
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
// the server without shipping index bytes per draw. Over budget it degrades to
// per-draw staging, counted separately in the stats.
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (minecraft-1.21.1-neoforge-create-indirect-in-world-align1024)
// is shorter than 120 frames, so a run that needs its numbers at all sets this low
// enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
// the log whenever PipeStats is on, so a device run needs no writable path.
String PipeStatsFile;
Bool DisableUboRing = false;
};
extern FeaturesTable Features;
// ---------------------------------------------------------------------------------
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
// ---------------------------------------------------------------------------------
//
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
//
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
// behind their own #if.
//
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
// parser below does not exist to complain about it, and putting a complaint in the
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
// shape of "the split lane ran monolith and went green", so the gate against it is a
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
// P5 adds beside build-linux-verify).
enum class TransportMode : Uint8 {
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
};
#if MOBILEGL_BUILD_DISAGGREGATED
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
// transport in is not the same as using it, and every existing lane of a build-split
// must keep running monolith unless it is asked for one.
extern TransportMode Transport;
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
extern String TransportEndpoint;
// P7 F1. DID **THIS PROCESS'S OWN CONFIGURATION** ASK FOR A SPLIT TRANSPORT.
//
// `Transport` alone cannot answer that, and the difference is exactly the one the F1 gate
// turns on. A unit fixture assigns `Transport` by hand to put the code under test on its
// split arm (ServerLoopTest's main does, and says why) while building no client at all; a
// real run gets it from MG_ConfigLoader::Init(), and that run WILL bring a client half up.
// Only the second may be held to "never decide a record family's fate from a placeholder
// caps mirror" - in the first there is no handshake, no mirror to adopt and nothing the
// rule could mean.
//
// Written once, by InitTransport(), and never cleared: a process does not stop having been
// configured. Read by MG_Remote::Client::CapsMirror::RequireFirstSnapshot.
extern Bool SplitTransportRequestedByConfig;
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
// for the G1 reason above and because every field here is meaningless without the
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
//
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
// to P6 or later.
//
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE's two values. See IpcTable::Surface.
enum class IpcSurface : Uint8 {
Offscreen = 0, // a window surface is the client's own window, as before (the default)
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
};
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
inline constexpr Uint32 kAdoptTierUnset = 3;
struct IpcTable {
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
// SPLIT variant already carry it, and an environment variable that nothing parses is
// indistinguishable from one that is parsed and ignored.
String ServerPath;
// Control endpoint and data-plane selection are independent of topology.
String Control = "fork"; // fork | unix:<path> | tcp://host:port
String Data = "auto"; // auto | shm | stream
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
// codec publish a max-record-bytes counter rather than assume that is enough.
Uint32 RingMb = 8;
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
// here ONE BLOB AT A TIME: a row whose content can outgrow the segment cuts it at the
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
Uint32 StageMb = 32;
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
// Stores no command names are destroyed at once and stores whose last submission has
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
// exceed it after a sweep, the server flushes what it has recorded and waits for it
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
// and MagmaWireReclaimScenario's watermark cases must go red.
Uint32 WireDeferredMb = 64;
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
Uint32 SpinUs = 50;
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
// reverse-channel event that finds SEG_EVENT full under run-ahead. The apply thread
// publishes, rings the client and parks until the client drains enough room; a client
// that has not made room when this runs out - it stopped draining (NotDraining) or it
// drains a slot at a time (TooSlow) - FORFEITS the reverse channel: that event and every
// later one is dropped and counted (eventDropped), and the session stops by the ordinary
// stop path rather than by Fatal{EventRingOverflow}. A peer that goes away (PeerGone) or
// a session that is stopped (Stopped) ends the wait at once instead, so the knob may
// exceed ServerLoop::Stop()'s 5000 ms join. It bounds the whole reservation, not one
// park, so a trickling peer cannot stretch it - nor can a shm peer that writes the
// (peer-writable) eventRingFull latch to 0 itself, though that one keeps the apply
// thread spinning rather than parked until the budget runs out. Was a 30000 ms constant
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
Uint32 EventWaitMs = 2000;
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
Uint32 PersistentBlockKb = 64;
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
// range every verb. 0 restores the whole-range push (A/B control).
Uint32 PersistentHashSuppress = 1;
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
// with no reply slot) are published without waiting for their own apply; the barrier
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
// every reply-slot row), which is the only place BARRIER-PULLED fields are read. 0
// restores the per-record barrier of R-1.
Uint32 BatchWaits = 1;
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
// answer is taken when the window fills.
//
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
// measured reasons and one structural one, in the order they were found:
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
// client emits on the order of a thousand records - so the answer was always gone
// (`want=310` on every miss while the ring spanned [1175..1504]);
// 2. a residency budget that tried to take answers back early could not help, because
// what displaces an answer is what the SERVER sends, not what the client emits;
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
//
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
// frame-sent identical per op throughout):
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
// window 2 2,500 17.27 s
// window 4 1,644 11.02 s
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
//
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
// now; the old 4 was derived from a reply pool the window does not run on). Three
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
//
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
// window 5 1,277 11.31 s (10.61-12.12)
// window 6 1,155 10.79 s ( 9.69-10.81)
// window 8 859 10.96 s ( 8.92-11.03)
//
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
// the constant ~350-400 creates that block for a reason depth does not reach, and the
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
// flag keyed on wantReply would disable the very mechanism it exists to enable
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
Uint32 CreateWindow = 10;
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
// wait per read.
Uint32 PboReadbackSync = 0;
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
// arm and the red-once of the no-reply gate (FencePollScenario).
Uint32 PollEscalate = 64;
// MOBILEGL_IPC_RECOVER (client): 1 (default) = after a device loss ends this client's
// session, the application's next eglCreateContext / eglCreate*Surface / eglInitialize
// dials the server for a FRESH session, and contexts created from then on work. Contexts
// (and their objects) from before the loss stay lost, as EGL's power-management event
// has them; surfaces survive and are re-created on the new session when next used.
// 0 = the loss is final for the process (every verb DECLINED for the rest of its life).
Uint32 RecoverAfterLoss = 1;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
Uint32 AdoptTier = kAdoptTierUnset;
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
// saying so.
//
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
// every record are identical on both arms, and the only difference is whether the
// client waits. That is what makes "is the picture the same" a question about the wait
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
// the first barriered row's stale pull.
//
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
// arm that stops filling it.
Uint32 RunAhead = 1;
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
// have in flight before it waits for a swap to come back. 1 = the client publishes
// frame N+1's records while the server applies and swaps frame N - one frame of
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
// client that is already CPU-bound and costs a frame of latency, which is why the
// default is 1 and not "as deep as the ring".
Uint32 PresentCredit = 1;
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
// latch decides dead (device lost) from alive-but-silent (a named diagnostic). The
// contract named this knob from P6 on; the client hard-coded its default until P7.
Uint32 ControlTimeoutMs = 5000;
// MOBILEGL_IPC_COLD_START_MS (P7): the same wait for the three ops a server may bring its
// NATIVE backend up inside - CreatePbufferSurface, CreateWindowSurface, MakeCurrent -
// until the session's first MakeCurrent is answered ok. The bring-up is lazy (Espryt's
// eglInitialize, Magma's Vulkan instance and device), ~100 ms on a workstation and more
// than the steady 5 s on a loaded CI runner (retrace-split spawn legs, runs 35671704873
// and 35706183230). Never shorter than CONTROL_TIMEOUT_MS; its expiry is the same
// non-fatal named answer. The default is the spawn connect budget, for the same reason:
// "the server has to create a backend, and a cold software rasteriser is not fast".
Uint32 ColdStartMs = 20000;
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
// build, the seven sticky forwards that are otherwise exempt - from "count it in
// rsp" to Fatal (R-7.3).
Bool StrictErrors = false;
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
// only because the two roles share one object turns into a named
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
// Meaningless under monolith transport (the two roles are one thread there, so the
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
// (the comparator owns the one fill block it compares against). 0 is not merely the
// default, it is the negative control: the lane's distinctness case must go red
// without it (P5F §6).
Bool RoleSplitState = false;
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
// inproc implementation did not quietly keep using a pointer past its lifetime.
Bool Audit = false;
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
// because the resolved mask is logged by whoever starts the apply thread, and the
// string is what an operator typed.
String ServerAffinity = "auto";
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
// any native window including NULL, send ONE CreateWindowSurface naming
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
IpcSurface Surface = IpcSurface::Offscreen;
};
extern IpcTable Ipc;
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
// surface is the server's when the knob says so AND there is a remote server to own it.
inline Bool ServerOwnedWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
}
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
// discarded statement and the pull build gains no symbol, no branch and no byte.
inline constexpr TransportMode Transport = TransportMode::Monolith;
#endif
} // namespace MobileGL::MG_Config
+5 -564
View File
@@ -6,22 +6,10 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#endif
#include <cerrno>
#include <cstdio>
#include <cstdlib>
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#ifndef _WIN32
extern char** environ;
@@ -31,16 +19,6 @@ namespace MobileGL::MG_Config {
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
FeaturesTable Features;
#if MOBILEGL_BUILD_DISAGGREGATED
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
// and there is nothing to define.
TransportMode Transport = TransportMode::Monolith;
String TransportEndpoint;
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
@@ -50,115 +28,6 @@ namespace MobileGL::MG_ConfigLoader {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
}
} // namespace
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
@@ -189,15 +58,6 @@ namespace MobileGL::MG_ConfigLoader {
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
@@ -226,58 +86,6 @@ namespace MobileGL::MG_ConfigLoader {
return it != acceptedEnvVariablesMap->end() && IsTruthyValue(it->second);
}
// Quirk overrides are tri-state: an unset variable keeps device auto-detection, a truthy
// value forces the quirk on, anything else set ("0", "false", "") forces it off.
inline MG_Config::QuirkOverride QueryEnvQuirkOverride(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::QuirkOverride::Auto;
}
return IsTruthyValue(it->second) ? MG_Config::QuirkOverride::ForceOn
: MG_Config::QuirkOverride::ForceOff;
}
// Multi-draw mode is a named-value preference: unset keeps Auto (best supported tier),
// a recognized name selects that tier as the ceiling, anything else warns and keeps Auto.
inline MG_Config::MultiDrawMode QueryEnvMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::MultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::MultiDrawMode::Ext;
if (lowered == "indirect") return MG_Config::MultiDrawMode::Indirect;
if (lowered == "unroll") return MG_Config::MultiDrawMode::Unroll;
if (lowered.empty() || lowered == "auto") return MG_Config::MultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected ext|indirect|unroll|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::MultiDrawMode::Auto;
}
// Same contract as QueryEnvMultiDrawMode, over the DirectGLES tier names.
inline MG_Config::GLESMultiDrawMode QueryEnvGLESMultiDrawMode(const String& key) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return MG_Config::GLESMultiDrawMode::Auto;
}
String lowered = it->second;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered == "ext") return MG_Config::GLESMultiDrawMode::Ext;
if (lowered == "multiindirect") return MG_Config::GLESMultiDrawMode::MultiIndirect;
if (lowered == "indirect") return MG_Config::GLESMultiDrawMode::Indirect;
if (lowered == "basevertex") return MG_Config::GLESMultiDrawMode::BaseVertex;
if (lowered == "drawelements") return MG_Config::GLESMultiDrawMode::DrawElements;
if (lowered == "compute") return MG_Config::GLESMultiDrawMode::Compute;
if (lowered.empty() || lowered == "auto") return MG_Config::GLESMultiDrawMode::Auto;
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected "
"ext|multiindirect|indirect|basevertex|drawelements|compute|auto, using auto",
key.c_str(), it->second.c_str());
return MG_Config::GLESMultiDrawMode::Auto;
}
inline Uint32 QueryEnvUint32(const String& key, Uint32 defaultValue, Uint32 minValue, Uint32 maxValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
@@ -299,140 +107,26 @@ namespace MobileGL::MG_ConfigLoader {
return static_cast<Uint32>(parsedValue);
}
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
auto it = acceptedEnvVariablesMap->find(key);
if (it == acceptedEnvVariablesMap->end()) {
return defaultValue;
}
const String& value = it->second;
const char* text = value.c_str();
int base = 10;
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
text += 2;
base = 16;
}
char* parseEnd = nullptr;
errno = 0;
const bool negative = value.find('-') != String::npos;
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
return defaultValue;
}
return static_cast<Uint64>(parsedValue);
}
inline void InitFeatures() {
auto& features = MG_Config::Features;
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
#endif
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
features.MagmaFixIterationRPSubgroupScratch =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
features.EsprytAvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
features.AvoidSamplerMipmapMinFilter =
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
features.AsyncOptimisticShaderStatus =
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
features.EsprytViewportArrayEmulation =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
features.EsprytWidenPacked16Storage =
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
// that starts with MOBILEGL_ is visible to these queries by construction.
#if MOBILEGL_PIPE_PUSH
// A push build with the knob unset runs every subsystem migrated so far, so the
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
// subsystems off, everything P3a landed still on.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
#else
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
// "pull everything" and that stays literally true.
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
#endif
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
#if MOBILEGL_PIPE_PUSH
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
features.PipeVerifyFatal =
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
#endif
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
// truthy check: unset must keep the memos, and only an explicitly falsy value may
// drop them.
features.PipeLegacyMemos =
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
}
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#if defined(__ANDROID__)
// An Android app that hosts the library sets the environment itself, before the library
// loads; the debug property is how a developer picks the backend for such a process
// without rebuilding it (`adb shell setprop debug.mobilegl.backend DirectVulkan`).
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.backend", property) > 0) {
MGLOG_I("Config: debug.mobilegl.backend=%s overrides MOBILEGL_BACKEND_TYPE=%s", property,
backendTypeStr.c_str());
backendTypeStr = property;
}
#endif
#define ENTRY(backendType) \
if (backendTypeStr == #backendType) { \
MG_Config::ActiveBackendType = BackendType::backendType; \
@@ -446,265 +140,12 @@ namespace MobileGL::MG_ConfigLoader {
#undef ENTRY
}
#if MOBILEGL_BUILD_DISAGGREGATED
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
// one fallback that names what it did instead. The two prefixed forms are the only
// reason this is not literally that function's ENTRY macro.
//
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
// rather than silent, because the failure this avoids is a P6 lane that set
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
// The mode is left at Monolith so nothing half-initializes.
inline void InitTransport() {
String value;
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
MG_Config::TransportEndpoint.clear();
if (lowered.empty() || lowered == "monolith") {
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
if (lowered == "inproc") {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
// one, and NOT on the two that name a transport and then stay monolith - a process
// that was refused its transport brings no client half up either.
MG_Config::SplitTransportRequestedByConfig = true;
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
"ring to an apply thread");
return;
}
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
// rather than "unknown", which are different bugs on the operator's side.
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
// rendezvous of its own and connects to it - the two processes are
// independent, so this is `Dial = Connect` with a launcher attached
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
if (lowered == "spawn") {
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
// run_trace_case.cmake asserts on a distinctive sentence from this
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
// every MOBILEGL_* in EVERY build including the pull one - so the
// bare KEY=VALUE is satisfied by a monolith library that parsed
// nothing (review M-5). A spawn arm with no line of its own could
// only have been asserted with the inproc marker, which spawn never
// emits: the arm would have been red for a missing sentence rather
// than green for a working transport.
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
"ring to an apply thread in ANOTHER PROCESS");
return;
}
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
// ELSE is listening on needs the server lifecycle to be somebody
// else's too, which is P12's. Named, never a silent degrade.
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
"staying on monolith. This run is NOT a split run.",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
}
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
// how an A/B stops being one, and the ranges below are the diagnostics.
inline void InitIpc() {
auto& ipc = MG_Config::Ipc;
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
// create_shader_state archive already needs far more than a toy ring. The ceilings
// are sanity, not policy.
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
// patience at all would forfeit a healthy run-ahead client the first time its ring
// filled between two of the client's drains, which is an ordinary backlog.
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
// was inert before, the device numbers, and the sweep that put the default at 6 rather
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
// declared answers, and the device sweep showed the wall clock still falling at the old
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
// StreamLink's crossing log are the checks on this number.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// A lost session is replaced by a fresh one on the application's next create (Config.h).
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
// run-ahead's whole point is that the client stops filling gPipeInputs for an
// unbarriered record, and the comparator has nothing left to compare when it does.
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
// Both are floored at 100 ms: a zero bound would declare every live server silent.
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
// verify harness: the comparator's entry compare and compare-at-read hook are built on
// there being ONE filled block (the hook pins itself to &gPipeInputs,
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
"the dual-block rehearsal is OFF for this run");
ipc.RoleSplitState = false;
}
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
//
// The device-lost latch is deliberately one-way - a session whose server died has
// nothing to recover into, because every handle the client minted names an object in a
// process that no longer exists. Re-pushing the world onto a fresh server is the work
// this knob would turn on, and no stage has written it.
//
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
// the whole IPC family: an environment variable nothing consumes is indistinguishable
// from one that is consumed and does nothing, and an operator who set this one would
// otherwise conclude that recovery had been tried and had not helped.
{
String respawn;
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
if (!respawn.empty() && respawn != "0") {
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
"this client minted names an object inside the server process, so a new "
"server would have to be re-pushed the entire world before a single verb "
"could land. This run will latch device-lost and stay there.",
respawn.c_str());
}
}
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
// the semantics). An unknown value is named and read as the default, like every knob here.
{
String surface;
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
std::transform(surface.begin(), surface.end(), surface.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (surface.empty() || surface == "offscreen") {
ipc.Surface = MG_Config::IpcSurface::Offscreen;
} else if (surface == "server") {
ipc.Surface = MG_Config::IpcSurface::Server;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server, using offscreen",
surface.c_str());
ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
// (Not said by a server process: a spawned server inherits the client's environment,
// parses this before RunSession sets its own transport, and has no client half anyway.)
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// Said, not silently honoured: there is no remote server here to own a window.
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
"and this run has none. Window surfaces stay the client's own");
}
}
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
// missing while the value was a number nobody varied, and the moment it became one that
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
if (ipc.WireDeferredMb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
"server never forces a sync for orphaned wire buffer stores, so a long frame "
"that respecifies and draws holds every one of them until the frame ends");
}
if (ipc.PersistentBlockKb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
#endif
void Init() {
MGLOG_D("Loading configuration from environment variables...");
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
#if MOBILEGL_BUILD_DISAGGREGATED
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
InitTransport();
InitIpc();
#endif
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
+4 -43
View File
@@ -9,20 +9,10 @@
#pragma once
// ============== Platform-specific definitions and macros ============== //
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
// by the configure-time guard in CMakeLists.txt), not by a macro.
//
// History: this used to `#define __ANDROID_API__ 26` to *raise* the level back
// when the build configured something lower, so that pthread_getname_np (which
// bionic guards with __INTRODUCED_IN(26)) would be declared. Once a later
// change added an `#undef` in front of it, the same line started *lowering* the
// level whenever the build configured higher than 26 - and that is an
// include-order split-brain, not a compatibility knob: a TU that includes any
// libc++ header before Includes.h latches libc++'s feature macros at the
// configure-time level, and only the bionic headers pulled in afterwards see
// the lowered value. The two halves then disagree (e.g. libc++ believes
// pthread_cond_clockwait exists while bionic has since hidden its declaration).
#ifdef __ANDROID__
#undef __ANDROID_API__
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
#endif
#ifdef _WIN32
#ifndef NOMINMAX
@@ -34,11 +24,6 @@
#ifdef _WIN32
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
#else
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
// points with. The one place where that is not enough - a vendor library handing glvnd its own
// entry points, where a reference resolving against the process instead of this library would be a
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
// here, because a second visibility on the same declaration does not compile.
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
#endif
@@ -49,26 +34,8 @@
#define MOBILEGL_EGL_API MOBILEGL_API
#define MOBILEGL_CGL_API MOBILEGL_API
#define MOBILEGL_NSOPENGL_API MOBILEGL_API
#define MOBILEGL_WGL_API MOBILEGL_API
// ====================== MobileGL configurations ======================= //
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
// documented to be compiled out of. Log.h redefines them identically, which is legal.
//
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
// drops only D. Any edit here must be mirrored in Log.h.
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_LOG_LEVEL_DEBUG 0
#define MOBILEGL_LOG_LEVEL_INFO 1
#define MOBILEGL_LOG_LEVEL_WARN 2
#define MOBILEGL_LOG_LEVEL_ERROR 3
#define MOBILEGL_LOG_LEVEL_FATAL 4
#endif
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
#endif
@@ -100,12 +67,6 @@
#endif
// =============================== Utils ================================ //
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
// 2026-08-13 renumbering unchanged; the contract is and stays
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
#define MOBILEGL_ASSERT(condition, ...) \
do { \
+1 -7
View File
@@ -14,13 +14,7 @@ namespace MobileGL {
} // namespace MG_Config
namespace MG_Backend {
// Leak-at-exit storage: the UniquePtr itself lives on the heap and is
// never destroyed by the runtime, so process exit runs no backend
// destructors (static destruction order across TUs is undefined).
// Deterministic teardown happens inside the EGL lifecycle instead:
// the last eglTerminate calls MobileGL::Destroy(), which .reset()s
// these singletons while the process is still healthy.
UniquePtr<BackendObject>& pActiveBackendObject = *new UniquePtr<BackendObject>();
UniquePtr<BackendObject> pActiveBackendObject;
GlobalBackendFunctionsTable gBackendFunctionsTable;
} // namespace MG_Backend
} // namespace MobileGL
+2 -2
View File
@@ -49,8 +49,8 @@
#include <stacktrace>
#endif
// Include ska::flat_hash_map
#include <ska/flat_hash_map.hpp>
// Include FastSTL
#include <FastSTL/UnorderedMap.h>
// Include xxHash
#include <xxhash.h>
+33 -190
View File
@@ -9,37 +9,14 @@
#include "Init.h"
#include "Config.h"
#include <MG_Backend/BackendObjects.h>
#include <MG_Backend/DirectVulkan/DirectVulkan.h>
#include <MG_State/GLState/Core.h>
#include <MG_State/EGLState/Core.h>
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
#include <MG_Impl/GLImpl/Query/GL_Query.h>
#include <MG_Util/Async/ShaderCompilePool.h>
#include <MG_Util/Metrics/PipeStats.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
#if MOBILEGL_PIPE_VERIFY
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/ServerProbe.h>
#endif
#include <atomic>
#include <mutex>
namespace MobileGL {
namespace {
std::atomic<Bool> g_isInitialized = false;
thread_local Bool tl_initializing = false;
std::mutex& InitMutex() {
static std::mutex mutex;
return mutex;
}
Bool g_isInitialized = false;
void DestroyImpl(Bool logLifecycle) {
if (!g_isInitialized) {
@@ -49,74 +26,14 @@ namespace MobileGL {
if (logLifecycle) {
MGLOG_I("MobileGL closing...");
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
// symbol and no resized one (the first version called it unconditionally and moved
// DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than
// the call is the load-bearing part.
MG_Backend::ShutdownSplitRoles();
#endif
// Before any subsystem the counters name goes away, and before the last frame's
// numbers can be lost: emits the final summary line and, when
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
// off, and idempotent.
MG_Util::PipeStats::Shutdown();
// First, before anything else is torn down. In-flight compile/link jobs own
// their own inputs and are safe against everything below EXCEPT glslang's
// process globals and the TShader/TProgram objects hanging off pGLContext,
// both of which this function is about to destroy. This is the one
// cancellation path in the whole design that waits.
MG_Util::Async::ShaderCompilePool::Get().StopAndDrain();
// GL syncs die with their contexts, and every context is gone by the
// time full teardown runs: drain the live-sync registry while the
// backend function table can still release the backend handles (and
// before a re-initialized library could pair them with the wrong
// backend's DeleteSync).
MG_Impl::GLImpl::DestroyAllSyncObjects();
// Queries die with their contexts for the same reason, and their registry
// is the same shape of process-global map: drain it here too, while the
// function table can still pair each backend handle with the backend that
// minted it.
MG_Impl::GLImpl::DestroyAllQueryObjects();
glslang::FinalizeProcess();
MG_Backend::pActiveBackendObject.reset();
MG_State::pGLContext.reset();
MG_State::pEGLContext.reset();
MG_Impl::GLImpl::TextureImpl::pProxyTextureManager.reset();
MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo.reset();
// Must run AFTER pGLContext.reset(). FinalizeProcess -> ShFinalize deletes
// glslang's process-wide pool allocator and every cached built-in symbol table,
// while the TShader/TProgram objects owned by the shader and program objects
// still reference levels adopted from those tables. Finalizing first left live
// glslang objects pointing at freed memory for the rest of the teardown.
glslang::FinalizeProcess();
// Immediately after, and never apart from it: FinalizeProcess just deleted the
// built-in symbol tables the prewarm latch stands for, so leaving it set would
// make the next Initialize() skip a prewarm it genuinely needs.
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
// The two-level translation memo. Nothing in it references a glslang object -
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
// fordebug build gets one line per level saying how the run went.
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
MG_State::GLState::LogProgramTranslationCacheStats();
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// A later re-initialization asks whether the server is there again.
MG_Remote::Client::ResetServerProbeCache();
#endif
#if MOBILEGL_PIPE_VERIFY
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
// FATAL=0 summary the comparator used to write from __run_exit_handlers truncated the
// client half of every such run to that one line. Verify builds only - the guard is
// the compile definition, not a runtime knob, and the pull build gains no byte (G1).
MG_Pipe::MGPipeVerifyFlushSummary();
#endif
if (logLifecycle) {
MG_Util::Debug::Close();
}
@@ -135,126 +52,52 @@ namespace MobileGL {
MGLOG_I("Initializing MobileGL...");
MG_ConfigLoader::Init();
MGLOG_I("Config loaded");
// Immediately after the config load and before anything can count: the MGPipe
// boundary counters latch their enable flag here, so every counting site in the
// two backends is a load of an already-settled global for the rest of the run.
MG_Util::PipeStats::Init();
#if MOBILEGL_BUILD_DISAGGREGATED
// F1 (P7 wave 2). THE WIRE COMES UP BEFORE THE FIRST FRONTEND OBJECT IS BORN, and the
// ORDER is the fix - not a wait, and not a friendlier default mask.
//
// MG_State::Init() constructs GLContext, and GLContext's TextureState constructor
// materialises one default texture object per target (GL 3.3 core 3.8,
// TextureState.cpp:62-71). TextureObjectBase's constructor emits resource_create for
// every one of them (TextureState/TextureObject.cpp:168-169), and that emission is
// gated, through PipeFill's FamilyIsLive, on
// CapsMirror::ServerConsumes(kMGPipeSubsystemTextureResources). In the old order the
// mirror was still GENERATION 0 when it was asked - it had no snapshot because the
// session had not been started yet, MG_Backend::Init() being what starts it - so it
// answered from the placeholder, every one of those creates was withheld, and the log
// said so once and then never again:
//
// MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
// family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
// caps generation 0
//
// That is not a race and it never was one: the line reproduces byte-for-byte on
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
// thread. And "the legacy pull path runs for it" is false under split - the client has
// no pull path to fall back to - so the records are simply lost, and only a later
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
//
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
// MG_State::Init() no handshake is in flight, and the one that will land is started
// LATER BY THIS SAME THREAD. The only correct answer is to have already brought it up,
// which is what this hook does. ServerConsumes now REFUSES to answer from a
// placeholder (CapsMirror.cpp), so a regression of this order is a named Fatal rather
// than a whole family of silently dropped records.
//
// In a build without MOBILEGL_BUILD_DISAGGREGATED this statement does not exist, so
// the pull build gains no symbol, no branch and no byte - which is what G1 measures.
const Bool splitBackendIsUp = MG_Backend::InitSplitRolesBeforeState();
#endif
MG_State::Init();
MGLOG_D("MG_State initialized");
#if MOBILEGL_BUILD_DISAGGREGATED
if (!splitBackendIsUp)
#endif
{
MG_Backend::Init();
}
MG_Backend::Init();
MGLOG_D("MG_Backend initialized");
MG_Impl::Init();
MGLOG_D("MG_Impl initialized");
glslang::InitializeProcess();
// On the GL thread, before any worker can exist. glslang builds its built-in symbol
// tables lazily under a process-wide lock held for the whole build, so without this
// the first concurrent compiles of a shaderpack all serialize behind the very first
// parse and asynchronous compilation looks like it is doing nothing.
//
// Gated on the flag, because the problem it solves only exists when there are
// workers: with compilation synchronous, nothing ever contends for that lock and the
// three throwaway parses buy nothing - they just add to every eglInitialize. Read the
// flag here rather than inside PrewarmBuiltins so ShaderCompiler keeps no dependency
// on the async subsystem (ProgramUtilTest compiles that file without it).
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
MG_Util::ShaderTranspiler::ShaderCompiler::PrewarmBuiltins();
}
MGLOG_D("glslang initialized");
g_isInitialized = true;
MGLOG_I("MobileGL initialized");
}
void EnsureInitialized() {
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
// Re-entrant call while this thread is already inside Initialize()
// (e.g. an init step routing back through a public entry point).
if (tl_initializing) {
return;
}
const std::lock_guard<std::mutex> lock(InitMutex());
if (g_isInitialized.load(std::memory_order_acquire)) {
return;
}
tl_initializing = true;
Initialize();
tl_initializing = false;
}
void Destroy() {
DestroyImpl(true);
}
Bool ImplementationAvailable() {
if (g_isInitialized.load(std::memory_order_acquire)) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Initialized: under a split transport the session either came up (a backend object
// exists) or it did not, and a display on a session that never came up cannot draw.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Backend::pActiveBackendObject != nullptr;
}
#endif
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Not yet: the bring-up would dial the configured server and wait for it. Ask first.
return MG_Remote::Client::ConfiguredServerAvailable();
#else
return true;
#endif
#if defined(__linux__) || defined(__APPLE__)
__attribute__((constructor)) static void AutoInit() {
Initialize();
}
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
// when the last EGL display is terminated with nothing current (EGLImpl
// calls Destroy()). There is intentionally no backend-initializing static
// constructor, no static destructor, and no DllMain: the global singletons
// use leak-at-exit storage (see GlobalObjects.cpp), so a process that exits
// without eglTerminate simply leaks them to the OS instead of running
// backend destructors during static teardown. macOS has a lightweight
// dyld constructor that installs NSOpenGL dispatch hooks only; full backend
// initialization still enters here from the first hooked CGL context.
__attribute__((destructor)) static void AutoDestroy() {
if (MG_Config::Features.TraceSkipAutodestroy) {
return;
}
#if defined(__APPLE__)
// macOS injected dylibs can run destructors after logging/backend static state is already torn down.
return;
#else
DestroyImpl(false);
#endif
}
#endif
#ifdef _WIN32
BOOL WINAPI DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
Initialize();
break;
case DLL_PROCESS_DETACH:
Destroy();
break;
}
return TRUE;
}
#endif
} // namespace MobileGL
-23
View File
@@ -11,13 +11,6 @@
namespace MobileGL {
void Initialize();
// Thread-safe, idempotent, and re-entrant wrapper around Initialize().
// Host layers (EGL/WGL/CGL entry points) call this lazily on first use so
// full backend initialization never depends on ELF/DLL static constructors,
// and so a fresh init can follow a full Destroy() (e.g. after the last
// eglTerminate). The macOS dyld bootstrap installs only lightweight
// NSOpenGL method hooks.
void EnsureInitialized();
void Destroy();
namespace MG_Util::Debug {
@@ -26,29 +19,13 @@ namespace MobileGL {
namespace MG_ConfigLoader {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // namespace MG_ConfigLoader
// Whether this process can be served right now. False when MobileGL is configured to reach a
// server that is not there (or a split session already failed to come up), or when the process
// asked for software rendering (LIBGL_ALWAYS_SOFTWARE, desktop Linux), so a
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
Bool ImplementationAvailable();
namespace MG_Backend {
void Init();
} // namespace MG_Backend
namespace MG_Impl {
void Init();
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
// Init) and installed as the one every later context starts with. Init's step, and the
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
void BuildDefaultFramebuffer();
} // namespace MG_Impl
} // namespace MobileGL
+11 -57
View File
@@ -97,8 +97,6 @@ namespace MobileGL::MG_Backend {
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
// An external texture is a 2D one wherever a format can go.
if (target == TextureTarget::External) return static_cast<SizeT>(TextureTarget::Texture2D);
return static_cast<SizeT>(target);
}
@@ -174,15 +172,11 @@ namespace MobileGL::MG_Backend {
return false;
}
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
// refused a caller for a reason that is not there: the surface and context tables are keyed
// by the display each was created on, every operation carries its own display handle, and
// this field has no reader anywhere - it was the whole of the rule.
//
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
// probing another, so the second eglInitialize is the one that matters, and a refusal there
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
// and the real display having failed, with nothing in between that says which was refused.
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -202,15 +196,12 @@ namespace MobileGL::MG_Backend {
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
@@ -288,7 +279,6 @@ namespace MobileGL::MG_Backend {
return true;
}
m_activatingSurface = surface;
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
@@ -308,14 +298,8 @@ namespace MobileGL::MG_Backend {
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
// own (a multi-window client renders each window on its own thread), and wiping them made
// that thread's next eglSwapBuffers fail with "no current context attached".
auto activatedIt = m_eglSurfaces.find(surface);
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
activatedIt->second.Activated = true;
m_backendCapabilitiesInitialized = false;
}
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
return true;
}
@@ -382,24 +366,6 @@ namespace MobileGL::MG_Backend {
m_eglCurrentThreads.clear();
}
void BackendObject::ForgetActiveEGLSurface() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglSurfaceInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
}
namespace {
MG_Util::Damage::Region& PresentDamageSlot() {
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
return damage;
}
} // namespace
const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); }
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; }
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
@@ -416,16 +382,10 @@ namespace MobileGL::MG_Backend {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
// The calling thread's surface, which another thread's make-current may have moved the
// backend off: bring it back before presenting it.
if ((!m_eglSurfaceInitialized || draw != m_eglSurface) && !ActivateEGLSurface(draw)) {
MGLOG_E("SwapEGLBuffers failed: the draw surface could not be re-activated");
return false;
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
@@ -466,9 +426,8 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ForgetActiveEGLSurface();
ResetEGLRuntimeState();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
@@ -499,9 +458,8 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ForgetActiveEGLSurface();
ResetEGLRuntimeState();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLResources() {
@@ -534,8 +492,4 @@ namespace MobileGL::MG_Backend {
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend
+16 -541
View File
@@ -9,16 +9,11 @@
#pragma once
#include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_Util/Damage/Damage.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
@@ -29,34 +24,6 @@ namespace MobileGL {
};
namespace MG_Backend {
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
// one of the two pointers is set; neither is set when the name named nothing, which is
// the INVALID_VALUE the frontend validator reports.
struct CopyImageEndpoint {
SharedPtr<MG_State::GLState::ITextureObject> Texture;
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
#endif
Bool IsRenderbuffer() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
#endif
return Renderbuffer != nullptr;
}
Bool Exists() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
#endif
return Texture != nullptr || Renderbuffer != nullptr;
}
};
enum class FormatCapability : Uint64 {
Creatable = 1ull << 0,
@@ -178,10 +145,6 @@ namespace MobileGL {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void (*ClearNamedFramebufferfi)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void (*ClearNamedFramebufferiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void (*ClearNamedFramebufferuiv)(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void (*BlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0,
GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter);
void (*BlitNamedFramebuffer)(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -193,9 +156,9 @@ namespace MobileGL {
GLsizei height, GLint border);
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height);
void (*CopyImageSubData)(const CopyImageEndpoint& src,
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void (*GenerateMipmap)(GLenum target);
@@ -211,35 +174,18 @@ namespace MobileGL {
void (*MemoryBarrierByRegion)(GLbitfield barriers);
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
GLenum access, GLenum format);
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
// 64-bit and float/double widths are derived there from the same answer, which is why
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
// case for at all.
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
// size - all of which are frontend link artifacts on ProgramObject, and
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
// one, or a SPIR-V module), in a namespace the application never sees.
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
// a backend query: it describes the program the application wrote, in the
// application's namespace, which neither backend program is in. It is answered
// entirely by MG_Impl/GLImpl/Program/ProgramInterface from the frontend reflection.
// Takes the block's GL NAME, not glShaderStorageBlockBinding's index. The index
// the application passes is the frontend interface-query enumeration's, and no
// backend shares that index space: DirectVulkan enumerates SPIR-V descriptor
// bindings and DirectGLES asks a real driver about SPIRV-Cross-generated ESSL.
// The name is the one coordinate all three agree on, so the frontend resolves the
// index against its own enumeration and each backend maps the name to its own.
void (*ShaderStorageBlockBinding)(GLuint program, const GLchar* storageBlockName,
GLuint storageBlockBinding);
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
void (*GetProgramInterfaceiv)(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint (*GetProgramResourceIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*GetProgramResourceName)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void (*GetProgramResourceiv)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint (*GetProgramResourceLocation)(GLuint program, GLenum programInterface, const GLchar* name);
GLint (*GetProgramResourceLocationIndex)(GLuint program, GLenum programInterface, const GLchar* name);
void (*ShaderStorageBlockBinding)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
// GL fence sync objects. All entries are optional (may be null); the
// frontend then falls back to always-signaled sync semantics.
// FenceSync may itself return null when the backend cannot create a
@@ -274,39 +220,6 @@ namespace MobileGL {
// and leave the query readable later.
Bool (*GetQueryResult64)(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds);
void (*DeleteBackendQuery)(BackendQueryHandle query);
// GL_SAMPLES_PASSED occlusion queries (optional; null = unsupported,
// the frontend then rejects the target). Results/deletion flow through
// GetQueryResult64 / DeleteBackendQuery like timer queries.
BackendQueryHandle (*BeginOcclusionQuery)();
void (*EndOcclusionQuery)(BackendQueryHandle query);
// Transform feedback primitive queries backed by real GPU query pools
// (optional; null = frontend falls back to CPU accounting).
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
// frontend's own accounting wherever that accounting is exact - a capture with no
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
// the written count for a vertex-only capture that follows a large render pass,
// where the desktop-exact answer is the one the frontend already computed. Defaults
// to false, so a backend that never sets it keeps using its GPU result.
Bool PrefersCpuXfbPrimitiveAccounting = false;
// Transform feedback capture spans, for backends whose own GL/ES driver
// performs the capture (DirectGLES). Both optional; null means the backend
// drives capture from its draw recording instead (DirectVulkan). End is
// called while the frontend capture state is still active, so the backend
// can still see the capture program and buffer bindings.
// GL_PATCH_VERTICES; ES 3.2 spells it the same way.
void (*PatchParameteri)(GLenum pname, GLint value);
void (*BeginTransformFeedback)(GLenum primitiveMode);
void (*EndTransformFeedback)();
// ARB_transform_feedback2. A backend that leaves these null keeps the single
// implicit capture span the frontend has always modelled; the frontend state
// (paused flag, per-object bindings) is tracked either way.
void (*PauseTransformFeedback)();
void (*ResumeTransformFeedback)();
void (*BindTransformFeedback)(GLuint name);
void (*DeleteTransformFeedback)(GLuint name);
Int64 (*GetGpuTimestampNs)(); // glGetInteger64v(GL_TIMESTAMP); 0 if unsupported
};
struct GlobalBackendFunctionsTable {
@@ -317,32 +230,8 @@ namespace MobileGL {
void (*SetSwapInterval)(Int interval);
};
// Coarse GPU vendor identity for gating device-specific quirks. Detected from the
// Vulkan physical-device vendorID or the GLES GL_VENDOR/GL_RENDERER strings; stays
// Unknown when detection is inconclusive, in which case auto-gated quirks stay off.
enum class GpuVendorKind : Uint8 {
Unknown = 0,
Qualcomm,
Arm,
Nvidia,
Amd,
Intel,
ImgTec,
// Software rasterizers (llvmpipe/lavapipe, SwiftShader).
Software,
};
struct DynamicBackendParameters {
Uint64 UniformBufferOffsetAlignment = 256;
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
// 64 for storage buffers. Answering the storage query with the uniform value let an
// application bind a storage range at an offset the driver cannot address, which it
// accepted without error and then wrote somewhere else entirely.
Uint64 ShaderStorageBufferOffsetAlignment = 256;
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
// which is also why the extension is not advertised in that case.
Float MaxTextureMaxAnisotropy = 1.0f;
SizeT UniformBufferOffsetAlignment = 256;
Float AliasedLineWidthRangeMin = 1.0f;
Float AliasedLineWidthRangeMax = 1.0f;
Float SmoothLineWidthRangeMin = 1.0f;
@@ -365,13 +254,6 @@ namespace MobileGL {
Int MaxIntegerSamples = 1;
Int MaxSamples = 1;
Int MaxSampleMaskWords = 1;
// Tessellation limits; defaults are the GL 4.0 core minimums.
Int MaxPatchVertices = 32;
Int MaxTessGenLevel = 64;
// GL_MIN/MAX_PROGRAM_TEXTURE_GATHER_OFFSET. Defaults are the GL 4.0 core
// minimums, which every ES 3.1 driver also guarantees.
Int MinProgramTextureGatherOffset = -8;
Int MaxProgramTextureGatherOffset = 7;
Int MaxTextureImageUnits = 32;
Int MaxVertexTextureImageUnits = 32;
Int MaxComputeTextureImageUnits = 32;
@@ -379,200 +261,37 @@ namespace MobileGL {
Int MaxVertexAttribs = 16;
Int MaxComputeShaderStorageBlocks = 8;
Int MaxCombinedShaderStorageBlocks = 32;
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
// non-compute, non-fragment stages and these defaults are the spec minimums, not
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
// backend that cannot honour a graphics-stage storage block MUST report 0 here
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
// does not make the block work; it only moves the failure from an honest
// "unsupported" at query time to a backend link error the frontend never surfaces,
// after which every draw with that program silently renders nothing.
Int MaxVertexShaderStorageBlocks = 0;
Int MaxTessControlShaderStorageBlocks = 0;
Int MaxTessEvaluationShaderStorageBlocks = 0;
Int MaxGeometryShaderStorageBlocks = 0;
Int MaxFragmentShaderStorageBlocks = 8;
Int MaxComputeUniformBlocks = 12;
Int MaxComputeWorkGroupInvocations = 128;
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
// axis. These six, with the invocations limit above, are the only indexed limits a
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
// answers, like the invocations limit: GL_Getter and the compile environment floor
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
// MaxClipDistances' does.
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
Int MaxShaderStorageBufferBindings = 8;
Int MaxTextureBufferSize = 65536;
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
Int TextureBufferOffsetAlignment = 1;
Int MaxUniformBufferBindings = 24;
Int MaxUniformBlockSize = 16384;
Int MaxImageUnits = 8;
Int MaxCombinedImageUniforms = 8;
Int MaxVertexImageUniforms = 0;
Int MaxGeometryImageUniforms = 0;
Int MaxFragmentImageUniforms = 8;
Int MaxComputeImageUniforms = 8;
Int MaxDrawBuffers = 8;
Int MaxColorAttachments = 8;
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
// backend that cannot host a clip distance MUST report it: advertising eight the
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
// from an honest "unsupported" at query time to a backend shader-compile error the
// frontend never surfaces, after which every draw with that program silently renders
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
// because it describes the no-backend case (standalone shader compiles, unit tests),
// where there is no device to be honest about and BuildTBuiltInResource still has to
// hand glslang a workable gl_MaxClipDistances.
Int MaxClipDistances = 8;
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
// backend that cannot host a cull distance MUST report it. The failure this prevents
// is worse than the clip one, because cull distance discards the whole primitive:
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
// gl_MaxCullDistances from it, SPIRV-Cross then emits
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
// without that extension rejects the program in an info log nobody surfaces. These
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
// is: they describe the no-backend case (standalone compiles, unit tests).
Int MaxCullDistances = 8;
Int MaxCombinedClipAndCullDistances = 8;
Int MaxViewports = 16;
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
// a convention is a statement about behaviour, so a backend that does not pin one
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
// capability is absent: without the viewport array extension only viewport 0 is ever
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
// both - which vertex provokes is decided per pipeline by
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
// provokingVertexModePerPipeline and the topology, so no single convention is true
// of the backend.
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
Int MaxViewportWidth = 16384;
Int MaxViewportHeight = 16384;
Float ViewportBoundsRangeMin = 0.0f;
Float ViewportBoundsRangeMax = 0.0f;
Int ViewportSubpixelBits = 0;
// GL 4.x fragment-interpolation offset limits. These defaults are the
// core minimums and are replaced by live GLES/Vulkan device limits.
Float MinFragmentInterpolationOffset = -0.5f;
// For four fractional bits the greatest required legal offset is
// 0.5 - 2^-4 = 0.4375 (GL 4.6 table 23.70).
Float MaxFragmentInterpolationOffset = 0.4375f;
Int FragmentInterpolationOffsetBits = 4;
Bool SupportsWideLines = false;
// Whether a framebuffer whose depth and stencil attachments are distinct
// images can be rendered to. GL only requires support when both refer to the
// same image and lets an implementation answer GL_FRAMEBUFFER_UNSUPPORTED
// otherwise, which is what DirectVulkan (one combined attachment) and the
// real ES drivers behind DirectGLES both do. Defaults to true so a backend
// that never sets it keeps the permissive behaviour.
Bool SupportsDistinctDepthStencilAttachments = true;
// Whether attaching a single layer of a 3D or array texture to a framebuffer actually
// renders to that layer. DirectGLES hands the layer straight to
// glFramebufferTextureLayer, so it does; DirectVulkan maps a GL layer onto a Vulkan
// array layer with no notion of a 3D depth slice, so it does not yet. Defaults to false
// so a backend that never sets it gets the conservative answer.
// Which layered texture targets this backend can attach ONE layer of to a framebuffer
// and then really clear, render and read back that layer. Bit (1u << TextureTarget) is
// set for each supported target. Deliberately per target rather than one flag: the three
// ways a GL layer maps onto Vulkan are independent capabilities. A 2D or 2D multisample
// array layer IS a VkImage array layer and needs nothing extra; a 3D texture's layer is
// a z slice, which needs a 2D-array-compatible image and a per-slice clear that
// vkCmdClearColorImage cannot express; a cube map array needs an image shape and the
// imageCubeArray feature before it can be attached at any layer at all. Defaults to 0 so
// a backend that never sets it gets the conservative answer.
Uint32 PerLayerFramebufferAttachmentTargets = 0;
static constexpr Uint32 PerLayerFramebufferAttachmentBit(TextureTarget target) {
return (static_cast<Int>(target) >= 0 &&
static_cast<Int>(target) < static_cast<Int>(TextureTarget::TextureTargetCount))
? (1u << static_cast<Uint32>(target))
: 0u;
}
Bool SupportsPerLayerFramebufferAttachment(TextureTarget target) const {
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
}
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
// both report VK_FALSE, so no real mobile device does.
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
// profile") and the demotion there is mathematically mandatory, always.
// Defaults to false so a backend that never sets it - and the no-backend case, which
// is what standalone shader compiles and the unit tests run under - keeps the
// demotion, which is the behaviour that works everywhere.
Bool SupportsShaderFloat64 = false;
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
// all. Defaults to false so a backend that never sets it gets the conservative answer.
//
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
// still see one consistent world.
Bool SupportsFloat64VertexAttributes = false;
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
// whether a module declaring OpCapability TessellationPointSize can reach the
// driver at all. DirectVulkan sets both this and the geometry twin from the one
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
// independently from the EXT/OES_tessellation_point_size /
// geometry_point_size extension pairs (PointSizeTier), which really do come
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
// varying program-wide (ShaderCompiler::
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
// overrides the detection in either direction at backend init.
//
// Defaults TRUE, deliberately against the house "assume absent" rule: false
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
// unit tests) is the one that leaves modules untouched. A backend that never
// sets it gets standard modules and, at worst, the old honest declines.
Bool SupportsTessellationPointSize = true;
// The geometry-stage twin (OpCapability GeometryPointSize).
Bool SupportsGeometryPointSize = true;
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
Uint32 SubgroupSize = 0;
Uint32 SubgroupSupportedStages = 0;
Uint32 SubgroupSupportedFeatures = 0;
Bool SubgroupQuadOperationsInAllStages = false;
GpuVendorKind GpuVendor = GpuVendorKind::Unknown;
};
enum class WindowBackend {
Android,
X11,
MetalLayer,
Win32, // Handle is an HWND
// TODO: Wayland, etc.
// TODO: Wayland, Windows, etc.
WindowBackendCount,
Unknown = -1
};
@@ -584,256 +303,26 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
struct SharedImageView {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
void* NativeBuffer = nullptr;
};
// The client-side answer of an allocation: the image's id and its exported descriptor
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
struct SharedImageExport {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint64 Modifier = 0;
// A YUV image's Cb Cr plane: in the same descriptor, at this offset and pitch.
Uint32 Plane1Stride = 0;
Uint32 Plane1Offset = 0;
};
// An import of a two-plane (YUV) dma-buf: where its planes are in the one descriptor, and
// EGL_EXT_image_dma_buf_import's colour hints (0 = not given).
struct SharedImageImportLayout {
Uint32 PlaneCount = 1;
Uint32 Offset[2] = {0, 0};
Uint32 Pitch[2] = {0, 0};
Uint32 ColorSpace = 0;
Uint32 Range = 0;
Uint32 SitingX = 0;
Uint32 SitingY = 0;
// The DRM format modifier the import named; HasModifier false = none (the implicit one).
Bool HasModifier = false;
Uint64 Modifier = 0;
};
// THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it
// around the backend's Present (from the present record), and a backend that can hand damage
// to its native present reads it there. Full whenever nothing set it.
const MG_Util::Damage::Region& CurrentPresentDamage();
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage);
class BackendObject {
public:
virtual ~BackendObject() = default;
// ---- shared images, SERVER side (a backend implements these) ---------------------
//
// Copies the default framebuffer of the surface current on this (the apply) thread into
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
// differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy
// waits for the image's pending read fences before it writes, and its own completion
// is published with SharedImages::PublishWrite as a sync_file - the call returns once
// the copy is submitted, never waiting for it on the CPU (a backend with no fence export
// waits and publishes -1). A reader binding the image waits for that fence on its GPU
// when the write generation moved, and at its frame boundary publishes its frame's fence
// with PublishRead / ReadTracker. The application's GL state is untouched. False = not
// supported / failed (logged).
//
// `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy;
// Full copies all of it. The rest of the image is left as it is - the client sends a
// region only for an image that already holds the frame outside it - and a backend that
// cannot be sure the image's contents survived (a target it has never written) copies
// all of it regardless.
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
(void)image, (void)region;
return false;
}
// EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply)
// thread: how many presents ago the buffer the next frame draws into held the frame then
// presented; 0 = unknown (the application repaints everything). Asking says the client
// relies on the answer, so from then on the backend keeps what it presents.
//
// `damageRegionFollows`: the client declares the region it draws (SetCurrentDamageRegion)
// before drawing. A driver whose only buffer age is EGL_KHR_partial_update's keeps a
// buffer's content only outside that region, so without one coming it is not asked.
virtual Int32 QueryCurrentBufferAge(Bool damageRegionFollows) {
(void)damageRegionFollows;
return 0;
}
// eglSetDamageRegionKHR for that draw surface: the frame draws `region` (GL window
// coordinates; Full = all of it) and nothing outside it. A backend without the notion
// ignores it.
virtual Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return true;
}
// IMPLICIT SYNC BY FLUSH (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the session
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
// its last boundary is published as written and read by one fence of everything it
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
// use of an image in a frame waits for that image's pending reads (it may be about to
// overwrite what another session still samples). A backend without shared images has
// nothing to publish.
virtual Bool PublishSharedImageAccesses() { return true; }
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
// (the caller's to close) that signals once everything the session submitted so far has
// completed - and, for a session that writes images, what PublishSharedImageAccesses
// does. -1 when there is none to give: the backend cannot export one, or the work has
// already completed - in both cases it HAS completed when this returns.
virtual Bool ExportNativeFence(int* fence) {
*fence = -1;
return PublishSharedImageAccesses();
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
// A new image, allocated by the server; `out->Fd` is the caller's to close.
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
(void)width, (void)height, (void)fourcc, (void)out;
return false;
}
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
// server's images, or the size/format disagree.
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
return false;
}
// The same for a YUV fourcc, whose planes and hints `layout` gives. A YUV buffer the
// server did not export is taken too (its labelled CPU-copy fallback).
virtual Bool ImportSharedImagePlanes(int fd, Uint32 width, Uint32 height, Uint32 fourcc,
const SharedImageImportLayout& layout, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)layout, (void)outId;
return false;
}
// Drops this client's reference (an allocation's or an import's).
virtual Bool ReleaseSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the current context's default framebuffer, copied top row first into
// the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for
// all. Returns when it completed.
virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
(void)id, (void)region;
return false;
}
// In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge
// on the server). False: the backend has no answer (the caller reports 0).
virtual Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) {
(void)damageRegionFollows, (void)age;
return false;
}
// In the GL stream: eglSetDamageRegionKHR for the current draw surface
// (SetCurrentDamageRegion on the server).
virtual Bool SetDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return false;
}
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
(void)textureLifetimeId, (void)id;
return false;
}
// In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server,
// returning once the fence is published. Skipped (true) when nothing reached the stream
// since the last one.
virtual Bool FlushSharedImageAccesses() { return false; }
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
// caller's to close), or -1 when it had none - the fenced work has then completed.
virtual Bool CreateNativeFence(int* fence) {
*fence = -1;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0;
// A served session's apply thread - the one thread that runs this backend from here on -
// calls this first, in a process that may serve several sessions at once. A backend whose
// native state is process-global by construction gives this object its own copy and binds
// it to the calling thread. The default has nothing per process to separate.
virtual void BindSessionStateToThisThread() {}
// A served session's apply thread asks this each time it wakes, BEFORE it applies another
// record: true when this session's own GPU work faulted or hung the GPU and the backend has
// latched the session for it (MGPipeSessionLatch, family DeviceLost), so the next frame -
// which would very likely stop the GPU again - is never applied. Cheap by contract: no
// wait, at most a status read. The default knows of no fault.
virtual Bool LatchIfGpuFaulted() { return false; }
// A served session's ring moved to another of its client's contexts (bind_context). A
// backend whose surface follows the native context it binds needs nothing; one that keeps
// a single native surface binding re-binds the surface that context was last made current
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
virtual void OnClientContextBound(Uint64 token) { (void)token; }
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native
// present. A backend with nothing to hand it to swaps as SwapEGLBuffers does.
virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) {
(void)damage;
return SwapEGLBuffers(dpy, draw);
}
// Forwards the app-requested eglSwapInterval to the backend's native
// presentation path (no-op for backends without a SetSwapInterval hook).
virtual void SetEGLSwapInterval(Int interval);
virtual void ReleaseEGLSurface(EGLSurface surface);
virtual void ReleaseEGLResources();
// ---- the server's own display window going away and coming back -------------------
//
// `window` (a window surface's native window) is about to be destroyed - the display's
// screen went off. Release ONLY what is bound to it: every window surface on it keeps its
// client handle and draws into a placeholder from here on, and every context, object and
// device stays. Called on the thread that runs this backend; nothing may reference the
// window once it returns true. False: not supported (the caller ends the session instead).
virtual Bool SuspendServerWindow(void* window) {
(void)window;
return false;
}
// The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window,
// `width`x`height`), and their new extent is published as a creation publishes it. False:
// they could not be (the caller ends the session).
virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// `window` changed size to `width`x`height` (the display's report - the one size to publish).
// Its surfaces publish that extent to the client now, without waiting for a present; a
// swapchain that has to follow is rebuilt when no frame is half-recorded, else at the
// present. False: nothing was published (no surface on it, or not supported).
virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents
// change under the application (a remote server's window) applies what it has received.
virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; }
void SetWindowHandle(const WindowHandle& handle);
virtual const RendererInfo& GetRendererInfo() const = 0;
@@ -860,28 +349,18 @@ namespace MobileGL {
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
// Activated at least once: a re-activation (a multi-window client switching back)
// is a re-bind and does not re-run the backend's capability probe.
Bool Activated = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
// any more, but every OTHER thread's current context and surface stay as they were (a
// multi-window client closing one popup must not unbind its other windows' threads).
void ForgetActiveEGLSurface();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
// Every registered surface the base class drops, current or not, after it is gone from
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
@@ -889,10 +368,6 @@ namespace MobileGL {
WindowHandle m_windowHandle;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLSurface m_eglSurface = EGL_NO_SURFACE;
// The client surface ActivateEGLSurface is bringing up while it calls InitWindowSurface /
// InitPbufferSurface, which take no surface argument: a backend that maps client surfaces
// to native ones reads it to re-bind an existing native surface instead of making another.
EGLSurface m_activatingSurface = EGL_NO_SURFACE;
Bool m_eglDisplayInitialized = false;
Bool m_eglSurfaceInitialized = false;
Bool m_backendCapabilitiesInitialized = false;
+1 -42
View File
@@ -13,47 +13,6 @@
#include "DirectVulkan/BackendObject_DirectVulkan.h"
namespace MobileGL::MG_Backend {
extern UniquePtr<BackendObject>& pActiveBackendObject;
extern UniquePtr<BackendObject> pActiveBackendObject;
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
#if MOBILEGL_BUILD_DISAGGREGATED
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
//
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
// to happen at its top. Two consequences are load-bearing rather than tidy:
//
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
// a counter while the final line is produced;
// * the apply thread is joined before pActiveBackendObject.reset(), so the server's own
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
// owns the context, by ServerLoop::Stop, rather than on the app thread.
//
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
void ShutdownSplitRoles();
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
//
// Returns true when this process took the split path - whether the bring-up succeeded or
// was refused by name - and the caller must then NOT call Init() a second time. Returns
// false under MG_Config::TransportMode::Monolith, where there is nothing to bring up and
// Init() keeps its own place after MG_State::Init().
//
// WHY IT EXISTS AT ALL, rather than MobileGL::Initialize simply calling Init() earlier:
// the monolith arm of Init() must keep running after MG_State::Init(), because
// BackendObject_DirectGLES::Initialize() is the tree's own order for it and moving it is a
// pull-build change G1 forbids. So the split arm - and only the split arm - moves.
//
// It exists ONLY in a split build, for the reason InitServerRoleForSpawn states: a
// definition guarded only on the inside still emits a symbol, and G1 measured that.
Bool InitSplitRolesBeforeState();
#endif
} // namespace MobileGL::MG_Backend
File diff suppressed because it is too large Load Diff
@@ -18,52 +18,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
// allocation call has to come through here. The shadow state keeps the requested count, so
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples);
class BackendObject_DirectGLES : public BackendObject {
public:
~BackendObject_DirectGLES() override;
// The driver's verdict on this session's context (DirectGLES::LatchIfDeviceLost).
Bool LatchIfGpuFaulted() override;
void Initialize() override;
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
// the image's EGLImage, ordered after the image's pending reads and published as a native
// fence without a CPU wait (DirectGLES.h, SharedImageImpl).
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content,
// so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update.
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: each window surface on it swaps its native EGL
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -73,28 +41,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESFunctionsTable& GetGLESFunctions() const;
const MG_External::EGLFunctionsTable& GetEGLFunctions() const;
void ApplyGLESCapabilitiesForTesting(const MG_External::GLESCapabilities& capabilities);
private:
void UpdateDynamicBackendParameters();
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void OnEGLSurfaceForgotten(EGLSurface surface) override;
// An existing native surface for the client surface being activated, re-bound; false when
// there is none yet (the caller creates it).
Bool RebindNativeSurfaceFor(EGLSurface surface);
// EGL: creating a surface does not change what is current. The creation bound the new
// surface natively; put the previous one back for the context that held it.
void RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw);
Bool m_initialized = false;
// P14: ONE NATIVE SURFACE PER CLIENT SURFACE, created on its first activation and re-bound
// on every later one. A multi-window client switches surfaces every frame; making a new
// native surface each time leaked them and lost what the window had drawn.
UnorderedMap<EGLSurface, EGLSurface> m_nativeSurfaces;
// Window surfaces whose window is gone (SuspendServerWindow): their native surface is a
// placeholder pbuffer until ResumeServerWindow.
Vector<EGLSurface> m_suspendedWindowSurfaces;
MG_External::EGLFunctionsTable m_EGLFunctions;
MG_External::GLESFunctionsTable m_GLESFunctions;
MG_External::GLESCapabilities m_GLESCapabilities;
@@ -113,13 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
// for a device whose timer queries / anisotropic filtering / native indirect draws /
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported);
// for a device whose timer queries are (or are not) usable. The
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
@@ -1,23 +0,0 @@
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
#pragma once
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Pipe/PipeApply.h>
namespace MobileGL::MG_Backend::DirectGLES {
extern Uint g_backendContextGeneration;
struct ContextEpoch {
Uint Native = 0;
Uint64 Served = 0;
Bool Server = false;
Bool operator==(const ContextEpoch&) const = default;
};
inline ContextEpoch CurrentContextEpoch() {
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
}
}
#endif
File diff suppressed because it is too large Load Diff
+13 -436
View File
@@ -19,26 +19,6 @@
operation Utils::CheckGLESError();
namespace MobileGL::MG_Backend::DirectGLES {
// Re-establishes the frontend texture-unit bindings on the native ES context.
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
// Driver-side color/depth level readback. Returns tightly packed, owned
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
// The caller supplies identity and extent, and whether the SOURCE allocation
// already uses an image carrier.
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
// refuses answers true, keeping the caller on its readback path.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent);
#endif
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
@@ -56,8 +36,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride);
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
GLsizei stride);
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex);
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
@@ -77,10 +55,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer,
GLenum buffer, GLint drawbuffer, const GLuint* value);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1,
GLint dstY1, GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
@@ -92,9 +66,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -108,75 +82,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei* length,
GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
// The calling thread's session's current native draw surface (the one the last Init*Surface
// created, or the last BindSessionSurface named).
EGLSurface CurrentSessionDrawSurface();
// The surface the calling thread's ACTIVE context last drew to (EGL_NO_SURFACE for none).
EGLSurface CurrentContextDrawSurface();
// EGL_BUFFER_AGE_EXT of the session's current draw surface for the next frame: the driver's
// answer for a window surface, 1 for a pbuffer that has been presented (a pbuffer's content stays
// across swaps), 0 otherwise. A driver whose buffer age is only EGL_KHR_partial_update's keeps
// nothing inside the frame's damage region - the whole surface unless one is set - so it is asked
// only when `damageRegionFollows` (SetCurrentDrawDamageRegion before the first draw).
Int32 CurrentDrawBufferAge(Bool damageRegionFollows);
// eglSetDamageRegionKHR(region) on the session's current window surface (a pbuffer, or a driver
// without EGL_KHR_partial_update, ignores it). The driver wants the age asked first in the frame;
// when the client was answered without asking it, it is asked here.
Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region);
// Makes an EXISTING native surface of the calling thread's session the one it draws to, and
// binds it to the active context. False for a surface the session does not own.
Bool BindSessionSurface(EGLSurface surface);
// The same selection without the bind (the caller binds - MakeCurrent - unconditionally).
Bool SelectSessionSurface(EGLSurface surface);
// Destroys one native surface of the calling thread's session - and only it; the session's
// contexts and other surfaces are untouched.
void DestroySessionSurface(EGLSurface surface);
// A new pbuffer of the calling thread's session, created with the active context's config and
// bound to nothing (EGL: creating a surface changes no binding).
EGLSurface CreateSessionPbuffer(EGLint width, EGLint height);
// Every context of the session that drew to `previous` draws to `replacement` instead, and
// `previous` is destroyed (a pbuffer-backed window's resize).
void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement);
// The server window going away (screen off): every context of the session that drew to the
// window surface `windowSurface` draws to a new 1x1 placeholder pbuffer instead, and the window
// surface is destroyed - no longer current anywhere once this returns. The contexts and their
// objects stay. Returns the placeholder, or EGL_NO_SURFACE (nothing changed) when none could be
// made.
EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface);
// The way back: a window surface on `window` replaces `placeholder` (EGL_NO_SURFACE: none to
// replace) for every context that drew to it, is bound, and its extent is published as a
// creation publishes it. Returns the new surface, or EGL_NO_SURFACE.
EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window);
// The window under `surface` changed size: its extent (the display's) is published again, at once.
Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window);
Bool MakeCurrent();
Bool ReleaseCurrent();
// True when the backend ES context is current on the calling thread, i.e.
// immediate buffer ops may issue GL calls right now.
Bool IsBackendContextCurrentOnThisThread();
#if MOBILEGL_BUILD_DISAGGREGATED
// DEVICE LOSS, Espryt's half (Magma's is VulkanRenderer::LatchWireDeviceLoss). Asks the
// driver whether the calling thread's context was reset (glGetGraphicsResetStatus); a reset -
// or the debug knob MGPipeDebugDeviceLossDue - latches THIS session through
// MGPipeSessionLatch and answers true, and the caller returns from its verb without issuing
// more GL. Asked at the frame boundaries (Present, a shared-image present), before a readback,
// and by the apply loop each time it wakes (LatchIfGpuFaulted), never per draw. The contexts ask
// for reset notification (NativeContextsNotifyResets) so the driver can name the GUILTY one;
// only GUILTY (or a reset of unknown cause) ends the session - an INNOCENT report, a context
// whose work merely waited behind another's hang (the compositor's among them), is only noted.
// `debugKnob` false: the knob is not consulted here (the on-screen Present, so an injected loss
// lands on an offscreen client's shared-image present or readback, never on the compositor's).
Bool LatchIfDeviceLost(const char* site, Bool debugKnob = true);
// The apply loop's form of it (BackendObject::LatchIfGpuFaulted), asked each time the apply thread
// wakes: the driver's report only, no debug knob, and no EGL ground-truth verification spent.
Bool LatchIfGuiltyBeforeApply();
#endif
// True once the process's native contexts were created with EGL_LOSE_CONTEXT_ON_RESET (decided
// by the first one; the driver must offer EGL_EXT_create_context_robustness).
Bool NativeContextsNotifyResets();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
@@ -194,24 +117,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// capability read needs no current ES context, and it stays false until
// the ES capabilities have been filled in.
Bool AreTimerQueriesSupported();
// True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or
// EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as
// core since 3.1, so the frontend advertises them unconditionally and an app may call
// glTexBuffer whenever it likes; this is the only thing standing between that call and a
// null entry point. False also means every shader declaring a samplerBuffer is
// uncompilable on this driver, which the program build reports by name.
Bool AreBufferTexturesSupported();
// Human-readable name of the buffer-texture tier for diagnostics and the driver POST:
// "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported".
const char* GetBufferTextureTierName();
// glTexBuffer / glTexBufferRange through whichever spelling this driver's buffer-texture
// support actually ships: the unsuffixed names are ES 3.2 core, while an EXT/OES driver
// exports glTexBuffer{,Range}EXT / OES. Callers must have checked
// AreBufferTexturesSupported() first. CallTexBufferRange reports whether it could honour
// the range - no tier is required to expose the range form, and the whole-buffer form is
// the documented fallback.
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer);
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size);
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
// creators return null (the frontend then falls back to an immediately
// available zero result) when the calling thread does not own the ES
@@ -221,16 +126,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
BackendQueryHandle BeginTimeElapsedQuery();
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
// GL_ANY_SAMPLES_PASSED(_CONSERVATIVE) occlusion queries: core ES3, independent of
// GL_EXT_disjoint_timer_query and of MOBILEGL_DISABLE_TIMERQUERY. Results/deletion
// flow through GetQueryResult64/DeleteBackendQuery like the timer queries above.
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
// GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN / GL_PRIMITIVES_GENERATED, also core ES
// (GL_PRIMITIVES_GENERATED from ES 3.2 on). Null when the target is unavailable, in
// which case the frontend falls back to counting primitives from the draw calls.
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
Bool IsQueryResultAvailable(BackendQueryHandle query);
// Returns true when a final value landed in *outNanoseconds (a zero for
// null or stale-generation handles IS final: the frontend may cache it
@@ -247,332 +142,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// A buffer retired during frame N is safe to recycle once CompletedFrameSerial() >= N.
Uint64 CurrentFrameSerial();
Uint64 CompletedFrameSerial();
// Block (up to timeoutNs) until the given frame serial provably retired on the
// GPU, using the per-frame fence ring. False when no usable fence covers the
// serial (fence-less context, foreign thread, or the slot was recycled);
// completion state is untouched in that case.
Bool WaitForFrameSerialCompleted(Uint64 serial, Uint64 timeoutNs);
// Applies (or defers until the window surface exists) the app-requested
// eglSwapInterval on the native EGL surface.
void SetSwapInterval(Int interval);
#if MOBILEGL_BUILD_DISAGGREGATED
// P12 review fix: the swap interval an ended server session asked for is not the next one's.
// The request outlives the context on purpose (a surface re-created mid-session re-applies it),
// so the in-process display server's session end forgets it: the next session starts at the
// driver's default until it asks (BackendObject_DirectGLES's destructor under a transport).
void ForgetRequestedSwapInterval();
#endif
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
// ---------------------------------------------------------------------------------
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
// ---------------------------------------------------------------------------------
//
// The display is process state and stays one object (eglInitialize once per process, terminated
// when the last native context leaves). The native EGLContext, the config it was created with
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
// the first one's native context down, which is what lets two contexts (and two share groups)
// be live at once in one process.
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
// that session has bound. Installed once by the server layer; with no resolver installed -
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
// exactly the process singleton this replaces. `false` also means {0,0}.
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
#if MOBILEGL_PIPE_PUSH
// ---------------------------------------------------------------------------------
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
// context's object namespace; the identity a twin table must be keyed on is therefore
// {session, share group} - the SAME two words the applier's OBJECT RECORDS are filed under
// (PipeApply.h's MGPipeApplierKey, derived by the same probe and the same `0 means its own
// group` rule, so the two agree by construction rather than by two conventions).
//
// Answers, for the CALLING THREAD, which session it belongs to and which share group that
// session's current context is in. Installed once by the server layer beside the native and
// applier probes; with no probe installed - or on a thread that is no session's own, which
// is the client process, the in-process client+server shape and every unit case - the
// answer is the single key {0, 0}, exactly the process-wide table this replaces.
struct TwinKey {
Uint64 SessionKey = 0;
Uint64 ShareGroupKey = 0;
Bool operator==(const TwinKey& other) const {
return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey;
}
Bool operator!=(const TwinKey& other) const { return !(*this == other); }
};
struct TwinKeyHash {
SizeT operator()(const TwinKey& key) const {
// The 64-bit mix SlotAllocator and the applier's registry already use for their own
// pair keys, so every keyed table in this tree hashes a session the same way.
Uint64 mixed = key.SessionKey ^ (key.ShareGroupKey + 0x9E3779B97F4A7C15ull +
(key.SessionKey << 6) + (key.SessionKey >> 2));
mixed ^= mixed >> 33;
mixed *= 0xFF51AFD7ED558CCDull;
mixed ^= mixed >> 33;
return static_cast<SizeT>(mixed);
}
};
using TwinKeyResolver = Bool (*)(TwinKey* outKey);
void SetTwinKeyResolver(TwinKeyResolver resolver);
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
// also the answer when no probe has ever been installed.
Bool CurrentTwinKey(TwinKey* outKey);
#endif
// Builds the native context for `contextToken` under the calling thread's session. A
// `shareGroupToken` whose group already has a live native context in this session hands that
// context to eglCreateContext as the share argument - which is what makes two contexts see one
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
// contexts are untouched; the display is terminated only when the last context in the process
// has gone.
void DestroyNativeContextFor(Uint64 contextToken);
// ---- objects that belong to ONE native context ----
//
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
// shares them, not even between contexts of one share group, and every context numbers its own
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
// names THAT context's own framebuffer - often an application's - and the scratch path's
// attach/scrub then rewires it onto staging textures. With one process serving several
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
//
// The calling thread's current native context as a lifetime serial: assigned when the context
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
// made current through MakeCurrent.
Uint64 CurrentNativeContextSerial();
class PerNativeContextBase {
public:
PerNativeContextBase();
PerNativeContextBase(const PerNativeContextBase&) = delete;
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
// The context died, and its names with it: forget them (no GL call - there is no context
// left to make one in).
virtual void ForgetContext(Uint64 serial) = 0;
protected:
~PerNativeContextBase();
};
// Called where a native context is destroyed.
void ForgetNativeContextObjects(Uint64 serial);
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
// it has not moved.
Uint64 PerNativeContextEpoch();
void BumpPerNativeContextEpoch();
// One T per native context, created on first use from that context.
template <typename T>
class PerNativeContext final : public PerNativeContextBase {
public:
T& Current() {
const Uint64 serial = CurrentNativeContextSerial();
// The draw path asks for the same entry over and over from one thread: a one-entry
// thread-local cache, invalidated by any drop anywhere (rare).
thread_local CurrentCache cache;
const Uint64 epoch = PerNativeContextEpoch();
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
const std::lock_guard<std::mutex> lock(m_mutex);
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
cache = {this, serial, epoch, &entry};
return entry;
}
template <typename F>
void ForEach(F&& visit) {
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto& entry : m_bySerial) visit(entry.second);
}
void Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.clear();
}
void ForgetContext(Uint64 serial) override {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.erase(serial);
}
private:
struct CurrentCache {
const PerNativeContext* Owner = nullptr;
Uint64 Serial = 0;
Uint64 Epoch = 0;
T* Entry = nullptr;
};
std::mutex m_mutex;
std::unordered_map<Uint64, T> m_bySerial;
};
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
class NativeContextName {
public:
operator GLuint() const { return m_names.Current(); }
NativeContextName& operator=(GLuint name) {
m_names.Current() = name;
return *this;
}
GLuint* operator&() { return &m_names.Current(); }
private:
mutable PerNativeContext<GLuint> m_names;
};
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
// records belong to; this makes the calling thread's native tuple follow it, binding the
// session's current surface to the newly named context's native EGLContext. A no-op when the
// named context has no native context yet (the session never sent CreateContext) or when the
// native context already current on this thread IS that one - so the single-context world pays
// nothing for it. Called from the bind_context path on the apply thread.
Bool MakeNativeContextCurrentForBoundToken();
#if MOBILEGL_BUILD_DISAGGREGATED
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
// binding shadow, which still describes the previous holder's native context.
void OnBackendTurnHandoff();
#endif
// How many native contexts this process holds, and how many of them belong to one session. The
// test observables for "a second context did not tear the first one down".
Uint64 NativeContextCount();
Uint64 NativeContextCountForSession(Uint64 sessionKey);
// The calling thread's session, as the resolver above reports it (0 with no resolver).
Uint64 CurrentNativeSessionKey();
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
// hold" query and no way to ask a context for its share group, so a case that must tell "the
// same native context still serves" from "a fresh one was built", or a sharing pair from two
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
// a production path and neither changes behaviour.
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
// tuple it means when no session runtime is installed.
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
// capture set is declared on the backend program at link time; the driver-side
// begin is deferred to the first draw of the span (ES needs the capturing
// program current and the capture buffers bound), and the end also mirrors the
// captured bytes back into the frontend buffer shadows.
void PatchParameteri(GLenum pname, GLint value);
namespace XfbImpl {
Bool AreTransformFeedbacksSupported();
// True while a capture span is open on the current transform feedback object
// (frontend Begin seen and not paused), whether or not the deferred driver-side
// Begin has been issued yet. Draw paths that would restructure the primitive
// stream, or that need to dispatch compute mid-draw, decline while it is set.
Bool IsCaptureSpanOpen();
void BeginTransformFeedback(GLenum primitiveMode);
void EndTransformFeedback();
void PauseTransformFeedback();
void ResumeTransformFeedback();
void BindTransformFeedback(GLuint name);
void DeleteTransformFeedback(GLuint name);
void OnBackendContextDestroyed();
} // namespace XfbImpl
namespace RenderStateImpl {
// Pushes the frontend's render-state block to the ES driver, diffed against what was
// last pushed.
//
// `forColorClear` names the CALLER, and the only thing it changes is the colour write
// mask handed to the driver. A draw into a colour attachment the backend widened from
// three channels to four gets that buffer's alpha channel masked OFF, so nothing can
// move the stored alpha away from the 1.0 the application's three-channel format
// implies (see FramebufferImpl::g_alphaWidenedDrawBufferMask). A CLEAR is how that 1.0
// gets there in the first place, so it must be allowed to write alpha - hence the flag
// rather than an unconditional doctoring. It is part of the sync memo, so a clear
// followed by a draw re-pushes the mask instead of early-outing on an unchanged
// frontend version.
//
// The application's own colour mask is never modified: glGet(GL_COLOR_WRITEMASK)
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
// GL_SCISSOR_TEST as the driver has it in the native context current on this thread -
// what a guard that turns the test off around its own blit, and puts it back, needs to
// know. The shadow answers only while it describes this context (synced since the last
// invalidation, in this context epoch); otherwise the driver is asked. The shadow is one
// per process: once another session's context synced into it, or a context switch
// invalidated it, its bits are some other context's.
Bool DriverScissorTestEnabled();
// TESTING: the shadow as SyncRenderState leaves it in the current context epoch, with
// GL_SCISSOR_TEST `enabled`.
void SetSyncedScissorTestForTesting(Bool enabled);
// TESTING: `inside` runs within the guard the shared-image present and the readbacks
// copy under (ScopedScissorDisable).
void RunScissorDisabledForTesting(void (*inside)(void*), void* data);
} // namespace RenderStateImpl
#if MOBILEGL_BUILD_DISAGGREGATED
// ---- SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md) ----
//
// A shared image reaches the driver only through the Android import path: its AHardwareBuffer
// becomes an EGLImage (EGL_ANDROID_get_native_client_buffer + EGL_ANDROID_image_native_buffer)
// and that EGLImage the storage of a texture or renderbuffer (GL_OES_EGL_image). On a host
// build there is no such path and every entry below declines.
namespace SharedImageImpl {
// One EGLImage over one shared image, holding the image's registry reference for as long
// as the EGLImage lives (SharedImageRegistry.h's lifetime rule). The last reference
// destroys the EGLImage, unless the display that made it has been terminated since (which
// destroyed it already). Opaque outside DirectGLES.cpp.
struct EglImage;
using EglImageRef = SharedPtr<EglImage>;
// An EGLImage for the live shared image `id`, or null with `why`. Needs a current context
// (the first call resolves the extension entry points).
EglImageRef CreateEglImage(Uint64 id, String& why);
// glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image) on what the active unit has bound to
// GL_TEXTURE_2D. False when the driver refused it (its GL error is consumed).
Bool TargetBoundTexture2D(const EglImageRef& image);
// BackendObject::BlitDefaultFramebufferToSharedImage, for the calling thread's context.
Bool BlitDefaultFramebufferTo(const SharedImageView& image, const MG_Util::Damage::Region& region);
// THE READER'S HALF of the image's sync state (SharedImageRegistry.h). Called on every use
// of a name whose level 0 is `image`, before the command that samples it: when a write
// landed since this context last waited, the context's later commands wait for its fence
// on the GPU; and the image is noted as read by the calling session's current frame.
void AcquireForSampling(const EglImageRef& image);
// YUV IMAGES (NV12/P010, MG_Remote/Server/SharedImageYuv.h) are never a name's level 0: the
// name keeps its own RGBA8 storage `target` (width x height), into which the image is
// converted at its first use in each of the calling session's frames - the image bound as
// an external texture, one triangle drawn - after the registry's CPU copy for a foreign
// buffer. Every touched binding is put back. Called after AcquireForSampling.
Bool IsYuv(const EglImageRef& image);
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height);
// The calling session's frame boundary for what it has read: hands a fence for everything
// the current context has submitted to every image noted since the last boundary. Called
// at Present and wherever the current native context stops being current (its reads can
// only be fenced from it). Nothing noted = nothing done.
void PublishPendingReads();
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): everything the
// calling session used since its last boundary is published as written and read by one
// fence, and the session's first use of an image in each frame from now on also waits for
// the image's pending reads.
Bool PublishPendingAccesses();
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): a
// sync_file of everything the current context submitted in `*fence` (-1: none, and the work
// is waited out here), and - for a session that writes images - the boundary
// PublishPendingAccesses makes.
Bool ExportNativeFence(int* fence);
} // namespace SharedImageImpl
#endif
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,64 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectGLES/MultiDraw.h
// 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
#pragma once
#include <Includes.h>
#include <Config.h>
#include "DirectGLES.h"
// Emulation of the desktop glMultiDrawElements / glMultiDrawElementsBaseVertex entry
// points on OpenGL ES, which has neither in core.
//
// Every strategy below is an emulation; they differ only in which driver capability
// they lean on and in how many driver entries a batch of N sub-draws costs. The design
// follows MobileGlues (MobileGL-Dev/MobileGlues, gl/multidraw.cpp) tier for tier, plus
// the native GL_EXT_multi_draw_arrays interaction that MobileGL already had:
//
// Ext one glMultiDrawElementsBaseVertexEXT 1 driver entry
// MultiIndirect one glMultiDrawElementsIndirectEXT 1 driver entry + 1 upload
// Indirect N x glDrawElementsIndirect N + 1 upload
// BaseVertex N x glDrawElementsBaseVertex N
// DrawElements N x glDrawElements over CPU-rebased indices N + 1 upload
// Compute 1 x glDrawElements over a GPU-flattened, 1 dispatch + 1 entry
// rebased index stream
//
// Which one runs is resolved once per ES context from the driver's capabilities,
// capped by MOBILEGL_ESPRYT_MULTIDRAW_MODE, and can additionally be demoted per batch
// when the batch's own shape rules a tier out (see ResolveTierForBatch in the .cpp).
namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// The tier this ES context resolved to, computed on first use and stable after.
MG_Config::GLESMultiDrawMode ResolvedTier();
// "multiindirect", "compute", ... - stable identifiers, also used by the POST row.
const char* TierName(MG_Config::GLESMultiDrawMode tier);
// One line naming the resolved tier, the tiers the driver can support, and the env
// clamp if one applied. For DriverPost and the startup log.
String DescribeTierResolution();
// The resolution itself, as a pure function of a capability set: the backend feeds
// it the live ES context's capabilities, DriverPost feeds it the ones it probed
// standalone, and both therefore report the same tier. `explanation`, when non-null,
// receives the "requested -> resolved (driver supports: ...)" line.
MG_Config::GLESMultiDrawMode ResolveTier(const MG_External::GLESCapabilities& caps,
const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode requested, String* explanation);
// Whether one tier is runnable on the given capability set, for per-row POST output.
Bool IsTierSupported(const MG_External::GLESCapabilities& caps, const MG_External::GLESFunctionsTable& funcs,
MG_Config::GLESMultiDrawMode tier);
// Runs `drawcount` indexed sub-draws as one glMultiDrawElements(BaseVertex) call
// would. `basevertex` is null for the plain glMultiDrawElements entry point (every
// base vertex is 0). Owns the whole draw, preparation included: callers must not
// have run PrepareForDraw, because the compute tier has to dispatch before the
// draw state is established.
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount, const GLint* basevertex);
// The ES context is gone: every scratch buffer and the compute program belonged to
// it, so drop the names without deleting them (the dead context reclaims them).
void OnBackendContextDestroyed();
} // namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-495
View File
@@ -9,23 +9,8 @@
#pragma once
#include <Includes.h>
#include <MG_State/GLState/Core.h>
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
#include <MG_Util/Texture/TextureFormatProcessor.h>
namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
// THIS ROLE's backend. Under an active transport the server's own private
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
// lookup must read ITS probed cache - not pActiveBackendObject's, which under split is the
// CLIENT's BackendObject_Remote mirror (a caps snapshot, generation-lagged, and on an
// independent server not usable at all). Monolith build/transport: pActiveBackendObject, so a
// pull build never sees this symbol. Null when no backend is up. The seven table-3 reads
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
// this instead of dereferencing pActiveBackendObject directly.
const FormatCapabilityCache* ActiveBackendFormatCaps();
#endif
namespace DebugImpl {
class ErrorLopper {
public:
@@ -49,504 +34,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
} // namespace VertexArrayImpl
namespace TextureImpl {
// Whether images on this format-capability target can back a colour attachment, and so
// need a colour-renderable storage format even when the frontend asked for a
// three-channel one ES never renders to. Shared by the capability probe (which passes the
// capabilities it has just queried, before the globals are published) and by the
// allocation path (which reads the active backend's), so the format the cache was probed
// with is always the format the image is created with.
Bool TargetRequiresRenderableFormat(SizeT targetIndex);
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
// Whether this format's ES storage is widened to 8-bit-per-channel because the
// driver stores some packed16 allocations with a mirrored field order
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
// is no longer 16-bit packed.
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType,
TextureTarget target = TextureTarget::Unknown);
void GenerateRenderbufferFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
GLenum* outFormat, GLenum* outType);
Bool ShouldUseCaveatTextureFormat(TextureInternalFormat internalFormat, TextureTarget target);
// True when the format the image is actually created with has an alpha channel the
// frontend format does not (the three-channel colour-renderable widening). GL reads such
// a channel back as 1.0, so any swizzle source of ALPHA has to be answered with ONE and
// any readback of the image has to overwrite the alpha the draw happened to leave there.
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
// GLSL ES cannot spell as an image is carried by one it can.
//
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
// Seventeen have a core format of the SAME per-channel width and component type,
// differing only in channel count, and in one of those the emulation is EXACT: GL already
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
// dropping the components the format does not have, so the carrier's surplus channels
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
//
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
// diagnostic rather than a silent approximation), and on a driver that HAS
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
// happen behind it.
//
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
// renderability substitution: an image that cannot be image-bound is useless whatever its
// attachment behaviour, so the image constraint wins. In practice that only bites
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
// signed-normalized instead of becoming the half float that fallback would have picked -
// so an image-bound texture in one of those two formats is no longer attachable, and
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
// does not update. Accepted deliberately: before the widening, an image binding in either
// format was refused outright by every driver tested and the stage that declared it never
// compiled at all, so nothing that works today is being given up.
//
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
// from "alpha" to a channel count, which is its own change.
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
// shadow already holds SourceChannels components of exactly the carrier's own type, so
// padding it out to four is the whole conversion. The packed entries do not - their shadow
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
// type takes twelve or sixteen bytes out of four and shears the level.
enum class ImageWidenSourceEncoding : Uint8 {
Components = 0,
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
PackedFloat11f11f10f,
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
// only in what the codes MEAN, which is the shader's business and not the transfer's.
PackedInt2101010Rev,
};
struct ImageBindableStorageWidening {
GLenum InternalFormat = GL_UNKNOWN_MGL;
GLenum Format = GL_UNKNOWN_MGL;
GLenum Type = GL_UNKNOWN_MGL;
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
Uint SourceChannels = 0;
// Whether that implied 1 is the integer one or a saturated normalized field - the
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
// RG8UI), so the carrier decides.
Bool IntegerData = false;
// What the upload has to do to the frontend shadow before it describes the level to
// the driver (PrepareImageWidenedUpload).
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
// has no image format of any width for and which a float carrier would requantise.
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
//
// Two things depend on it, both because the ES storage no longer shares the frontend
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
// one), and glGetTexImage divides the codes back out into the floats the application
// is still owed.
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
Bool SignedNormalized = false;
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
};
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
//
// A buffer texture cannot be widened: its texels are the application's buffer object, at
// the size and layout the application gave it, and it is usually also a vertex, index or
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
// nothing.
//
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
// reason the storage widening's gaps are - on a driver where the split applies at all
// there is no legal ESSL for the image declaration, so such a program did not compile.
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
} // namespace TextureImpl
namespace FramebufferImpl {} // namespace FramebufferImpl
// Pure CPU helpers of the client-format readback conversion (ReadPixels/GetTexImage repack a wide
// RGBA(_INTEGER) read into the caller's (format, type) layout). Kept context-free so unit tests can
// exercise the exact packing the GL CTS packed_pixels oracle compares against.
namespace ReadbackImpl {
struct ReadbackChannelMapping {
Int sourceChannel[4]; // RGBA source channel feeding each destination component
Int channelCount; // destination component count
Bool isInteger;
};
Bool GetReadbackChannelMapping(GLenum format, ReadbackChannelMapping& outMapping);
// Byte size of one destination component of `type`; packed types report the packed word size.
// 0 = type not supported by the conversion path.
SizeT GetReadbackComponentSize(GLenum type);
// Bit-field layout of a GL packed pixel type. width/shift are indexed in the client format's
// component order (matching ReadbackChannelMapping); shift is the LSB position of the field in
// the packed word: non-REV types pack the first component from the MSB, *_REV types from the
// LSB (GL 3.3 table 3.6; field positions mirror the GL CTS glcPackedPixelsTests pack_* oracle).
struct PackedReadbackLayout {
Int fieldCount; // format components stored in the packed word
Int width[4]; // bit width of each component's field
Int shift[4]; // LSB bit position of each component's field
SizeT byteSize; // packed word size in bytes (1, 2 or 4)
Bool isFloatPacked; // 10F_11F_11F_REV / 5_9_9_9_REV: fields hold unsigned small floats
};
Bool GetPackedReadbackLayout(GLenum type, PackedReadbackLayout& out);
// Unsigned small-float encoders (EXT_packed_float / EXT_texture_shared_exponent semantics).
Uint32 EncodeFloatToUnsignedF11(Float value);
Uint32 EncodeFloatToUnsignedF10(Float value);
Uint32 EncodeSharedExponentRGB9E5(const Float rgb[3]);
// Destination bytes per pixel for a (format mapping, type) readback pair; 0 when the pair is
// not convertible (unknown type, packed field count != format component count, floating-point
// or packed-float type with an integer format).
SizeT GetReadbackDstPixelSize(const ReadbackChannelMapping& mapping, GLenum type);
// Repacks one row of wide RGBA(_INTEGER) texels (4 components of wideType each) into the
// client's (format, type) layout. src holds width * 4 * GetReadbackComponentSize(wideType)
// bytes, dst receives width * GetReadbackDstPixelSize(mapping, type) bytes.
void ConvertWideReadbackRow(const Uint8* src, Uint8* dst, SizeT width, GLenum wideType,
const ReadbackChannelMapping& mapping, GLenum type);
// Stores wide RGBA(_INTEGER) rows into the client pointer or the bound PACK pixel buffer,
// honoring the client-side PACK pixel-store parameters (row length, alignment, skips,
// swap-bytes, and - when applyPackImageParams - image height/skip images). Shared by the
// DirectGLES and DirectVulkan readback conversion paths.
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
void* pixels, Bool applyPackImageParams);
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
// False when `type` is not a 4-byte packed type.
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
GLenum type, void* pixels, Bool applyPackImageParams);
} // namespace ReadbackImpl
namespace PrgramImpl {
String ProcessOutColorLocations(const String& glslCode);
String ForceSupporterOutput(const String& glslCode);
String ClampNormFallbackOutputs(String glslCode, GLenum shaderType, Uint32 snormOutputMask,
Uint32 unormOutputMask);
String ForceFlatIntegerVaryings(const String& glslCode, GLenum shaderType);
// Legacy GLSL's gl_FragColor is broadcast to every enabled draw buffer (GL 4.6
// 15.2.3), but ShaderSourceProcessor lowers it to the single output mg_FragColor,
// which only ever reaches draw buffer 0. Replicates it across `drawBufferCount`
// outputs and copies the value into them at the end of main. A no-op for
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
// enables several draw buffers, so the ordinary single-target shader is untouched.
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
// SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
// sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
// On a driver that advertises only GL_OES_texture_buffer that directive is a compile
// error, so the name is retargeted in the emitted source. A no-op on every other tier:
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
String RetargetTextureBufferExtension(String glslCode,
MG_External::GLESCapabilities::TextureBufferTier tier);
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
// because only it knows which formats are in play AND whether the driver advertises the
// extension - requesting an unadvertised extension is itself a compile error, so this is
// never emitted speculatively. A no-op when not needed or already present.
String RequestExtendedImageFormats(String glslCode, Bool needed);
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
// spelling for it at any version, so the request has to be made here or the stage does
// not compile - which loses the whole program, not just the multi-viewport routing.
// `needed` is the caller's answer for the same reason as above: only it knows whether the
// driver advertises the extension, and requesting an unadvertised one is itself a compile
// error, so this is never emitted speculatively. A no-op when not needed or already
// present.
String RequestViewportArrayExtension(String glslCode, Bool needed);
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
// advertises neither spelling, because requesting an unadvertised extension is itself a
// compile error. A no-op when nullptr or already present.
String RequestPointSizeExtension(String glslCode, const char* extensionName);
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
// ones; the two extensions are separate and neither implies the other.
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
// Writes a format layout qualifier into the image declarations named in
// `esslFormatByUniformName` that still have none. The completion half of the image-format
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
// format in, but SPIRV-Cross throws rather than printing the formats it calls
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
// stage. So those formats stay out of the module and are spelled here instead, on the
// emitted text, where nothing can refuse them.
//
// Declarations that already carry a format are left exactly as they are, whoever wrote
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
// stops being safe to edit by hand.
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
String RemoveLayoutBinding(const String& glslCode);
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
// image array into; the suffix is the array's own name and the element's index.
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
// One image ARRAY whose elements the application pointed at units that are not
// consecutive-from-element-zero.
struct ImageArrayUnitPlan {
String name; // the array's name, exactly as the emitted ESSL declares it
Vector<Int> units; // the frontend image unit element k has to reach
};
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
// (glUniform1i per element). ES has no such call at all - "ES image units come
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
// wrong value and three were never written, with no GL error and no link log. The same
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
// INVALID_OPERATION.
//
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
// element k. One declaration carries one binding, so one declaration per unit is the
// only spelling that reaches an arbitrary set of them.
//
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
// widening the array to cover the whole span of units and routing each subscript through
// a `const highp int` offset table - was written before that pass covered images, and
// the table lookup was itself one of the non-constant expressions the same probe refuses.
// The split also costs exactly the image uniforms the application declared, where the
// widening cost the whole SPAN (seven for the four elements of
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
// fail to fit in.
//
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
// caller to report - when the emitted extent disagrees with the reflection, when the
// array is reached by anything other than a subscript, or when a subscript is not a
// literal element index. Silence was the whole defect here, so a decline must be audible.
//
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
// key on the GL uniform name and on a binding already being stamped) and BEFORE
// SplitReadWriteImageUniforms (so each element that is both read and written is split
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
// per-program units it reads need no entry in BuildEsslTranslationKey.
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
Vector<String>* outDeclined = nullptr);
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
// `out` one.
//
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
// which members, depends on what the application's shader touched; a synthesized stage
// that redeclares a different shape than its neighbours is an ES link error against a
// program that has no other problem.
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
//
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
// bound in its place, and every draw silently renders nothing.
//
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
// alone, which is what matching a neighbour that did not redeclare requires.
//
// All four outer levels and both inner levels are written unconditionally: writing a
// level the evaluation stage's domain does not use is legal and ignored, and it saves
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
//
// The same stage, for the same reason, that DirectVulkan synthesizes in
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
// tessellation stages. Kept as two generators rather than one because the two targets
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
// VkShaderModule against a driver shader object.
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
const String& inPerVertexMembers,
const String& outPerVertexMembers,
const FloatVec4& defaultOuterLevel,
const FloatVec2& defaultInnerLevel);
// Prefix of the writeonly half a read+write image uniform is split into (see
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
// own accesses, so two stages that use an image the same way arrive at the SAME name and
// two that use it differently arrive at different ones - which is exactly the property
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
// ESSL refuses an image variable that carries a format qualifier other than r32f /
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
// speculatively decorates every storage image NonWritable+NonReadable
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
// up as a device compile failure - and then as a silently no-op draw.
//
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
// * both -> emit TWO declarations on the same binding and of the
// same type, `coherent readonly
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
// every imageStore at the second one, and follow each of
// those stores with `memoryBarrierImage();`. Several image
// variables may share an image unit as long as they have
// the same type and format, which is exactly what the pair
// is.
//
// The rename is the other half of the repair and applies to all three cases. The qualifier
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
// declared in two stages to be declared identically - so a shader that stores an image from
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
// still reads back its initial contents
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
//
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
// point: two stages that use an image the same way emit byte-identical declarations, so
// letting them keep one shared name costs nothing and merging them is correct, while two
// stages that use it differently land on different prefixes and cannot be merged at all.
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
// binding_images_texture_type_* declares three read+write images in each of its five
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
// glslang's link, surfaced only as every draw silently doing nothing and the images
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
// catches this.
//
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
// nothing but the identifier changed. Stages that agree still reach the same alias and
// stay merged, so this costs no shader an extra image uniform.
//
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
// readonly and writeonly (a spelling no access analysis produces, so it came from the
// application and is identical everywhere), and one carrying NEITHER, which is legal only
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
//
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
// guarantees a write through one image variable is visible to a read through a DIFFERENT
// one when both are coherent, and the split is what makes a same-variable
// read-after-write cross-variable. The single-declaration repairs above do not get it -
// nothing aliases them.
//
// The barrier is the other half of the same problem, and coherent alone did not cover it:
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
// variable and a read of another it has no reason to believe alias, and is free to serve
// the read from before the write - which is what advanced-memory-order's store/load/
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
// non-uniform control flow. It costs something in a shader that stores to a read+write
// image in a loop, which is why it is confined to the split pair.
//
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
// uniforms, so a shader with more than half the limit in read+write images is the case
// to watch.
//
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
// already rewritten to and must run before those bindings are stripped - see the call
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
//
// `outSplitCount`, when given, receives the number of declarations that were actually
// doubled - i.e. exactly how many image uniforms this stage gained over what the
// application declared. Zero for every shader but a handful, and the only number the
// budget note above can be reported with.
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
// ES has no per-texture/sampler LOD bias at all (GL_TEXTURE_LOD_BIAS is desktop
// only; Vulkan spells it VkSamplerCreateInfo::mipLodBias), so it has to reach the
// shader as a uniform and be folded into every lookup's level of detail. Declares
// one `uniform highp float mg_lodBias_<sampler>;` per mip-capable sampler and adds
// it to the bias / explicit-LOD argument of every lookup that takes one. Draws push
// the bound texture's (or sampler object's) value into it; a shader whose samplers
// all have a zero bias is therefore unaffected. Returns the source unchanged when
// there is nothing to rewrite.
//
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
// so their constant level stays constant; only the implicit-LOD forms take the bias.
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl
namespace Utils {
@@ -1,463 +0,0 @@
// Included in DirectGLES.cpp after the native packed-word readback helper.
// A storage promotion needs the driver's own texels as owned tight rows, not
// client PACK layout or a fallback into a frontend texture's potentially stale
// shadow.
class ScopedWireTextureReadLevel {
public:
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
: m_texture(texture), m_target(target) {
switch (target) {
case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return;
}
GLint previous = 0;
DrainESErrors();
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
g_GLESFuncs.glBindTexture(m_target, m_texture);
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel);
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel);
m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
if (m_saved) {
// ES framebuffer completeness can reject a defined level whose
// base level or intervening mip chain is missing/inconsistent.
// Read this existing native image as the sole base level. The
// logical GL parameters and every cached copy remain unchanged.
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level);
m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR;
}
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
}
~ScopedWireTextureReadLevel() {
if (!m_saved) return;
// The packed-word path can use its own texture scratch binding.
// Save the binding at restoration time too; do not assume an active
// texture unit or make the backend's binding cache forget a bind.
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
g_GLESFuncs.glBindTexture(m_target, m_texture);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel);
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel);
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
}
Bool Valid() const { return m_valid; }
private:
GLuint m_texture;
GLenum m_target;
GLenum m_binding = GL_NONE;
GLint m_baseLevel = 0, m_maxLevel = 0;
Bool m_saved = false, m_valid = false;
};
static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type,
Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier,
Vector<Uint8>& bytes) {
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) return false;
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
if (!pixelBytes || width <= 0 || height <= 0) return false;
const GLenum componentType = QueryReadAttachmentComponentType();
const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT;
if (carrier.Active() ? (mapping.isInteger || !integerAttachment)
: mapping.isInteger != integerAttachment) return false;
GLint implFormat = 0, implType = 0;
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
struct Candidate { GLenum format, type; };
Vector<Candidate> candidates;
if (carrier.Active()) {
candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT});
} else if (mapping.isInteger) {
if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) &&
(implType == GL_INT || implType == GL_UNSIGNED_INT))
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)});
} else {
if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) &&
(CanDecodeWideSourceType(implType) ||
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV)))
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT});
if (componentType == GL_SIGNED_NORMALIZED) {
candidates.push_back({GL_RGBA, GL_SHORT});
candidates.push_back({GL_RGBA, GL_BYTE});
} else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE});
}
ScopedPixelPackBuffer packBuffer(0);
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
const SizeT pixels = static_cast<SizeT>(width) * static_cast<SizeT>(height);
Vector<Uint8> wide;
GLenum wideType = GL_NONE;
Int channels = 0;
DrainESErrors();
for (const auto candidate : candidates) {
channels = GetWideReadChannelCount(candidate.format);
wide.resize(pixels * static_cast<SizeT>(channels) * GetReadbackComponentSize(candidate.type));
g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data());
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; }
}
if (wideType == GL_NONE) return false;
if (carrier.Active()) {
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(decoded.data());
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
for (SizeT i = 0; i < pixels; ++i) {
for (SizeT c = 0; c < 4; ++c) {
const Float divisor = static_cast<Float>(carrier.ChannelMax[c]);
dst[i * 4 + c] = carrier.SignedNormalized
? std::max(static_cast<Float>(static_cast<Int16>(static_cast<Uint16>(src[i * 4 + c]))) / divisor, -1.0f)
: static_cast<Float>(src[i * 4 + c]) / divisor;
}
}
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
} else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
auto* dst = reinterpret_cast<Float*>(decoded.data());
for (SizeT i = 0; i < pixels; ++i) {
Uint32 word = 0;
Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word));
dst[i * 4] = static_cast<Float>(word & 1023u) / 1023.0f;
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 1023u) / 1023.0f;
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 1023u) / 1023.0f;
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 3u) / 3.0f;
}
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
}
if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType);
if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType);
Vector<Uint8> converted(pixels * pixelBytes);
const SizeT sourceRowBytes = static_cast<SizeT>(width) * 4 * GetReadbackComponentSize(wideType);
for (GLsizei row = 0; row < height; ++row)
ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast<SizeT>(row) * sourceRowBytes,
converted.data() + static_cast<SizeT>(row) * width * pixelBytes, width, wideType, mapping, type);
bytes = Move(converted);
return true;
}
static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target,
TextureUploadTarget uploadTarget, GLint level,
const IntVec3& extent, GLenum format, GLenum type,
Vector<Uint8>& bytes) {
const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel(
MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (!pixelBytes) return false;
const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL;
const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
ScopedFramebufferBinding bindings(true, false);
GLuint fbo = 0;
g_GLESFuncs.glGenFramebuffers(1, &fbo);
struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo};
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo);
ScopedPixelPackBuffer packBuffer(0);
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT)
: GL_STENCIL_ATTACHMENT;
const SizeT count = static_cast<SizeT>(extent.x()) * extent.y();
Vector<Uint8> result(count * extent.z() * pixelBytes);
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
g_GLESFuncs.glFinish();
for (GLint z = 0; z < extent.z(); ++z) {
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z);
else
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point,
TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level);
g_GLESFuncs.glReadBuffer(GL_NONE);
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
Vector<Float> depth;
Vector<Uint8> stencil;
if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false;
if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false;
auto* dst = result.data() + count * z * pixelBytes;
for (SizeT i = 0; i < count; ++i) {
if (format == GL_DEPTH_STENCIL) {
const Uint32 s = stencil[i];
if (type == GL_UNSIGNED_INT_24_8) {
const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s;
Memcpy(dst + i * pixelBytes, &packed, sizeof(packed));
} else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) {
Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float));
Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s));
} else return false;
} else if (type == GL_FLOAT) {
const Float value = needDepth ? depth[i] : static_cast<Float>(stencil[i]);
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
} else if (type == GL_HALF_FLOAT) {
const Uint16 value = MG_Util::EncodeFloatToHalfBits(
needDepth ? depth[i] : static_cast<Float>(stencil[i]));
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
} else {
const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT;
const Double maximum = signedType
? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0)
: (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0);
const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i];
if (pixelBytes == 1) dst[i] = static_cast<Uint8>(value);
else if (pixelBytes == 2) { const Uint16 v = static_cast<Uint16>(value); Memcpy(dst + i * 2, &v, 2); }
else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4);
else return false;
}
}
}
bytes = Move(result);
return true;
}
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes) {
if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level);
if (!readLevel.Valid()) return false;
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX)
return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes);
ReadbackChannelMapping mapping{};
if (!GetReadbackChannelMapping(format, mapping)) return false;
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false;
const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
const auto widening = sourceUsesImageCarrier
? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{};
const SizeT sliceBytes = static_cast<SizeT>(extent.x()) * extent.y() * pixelBytes;
Vector<Uint8> result(sliceBytes * static_cast<SizeT>(extent.z()));
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
g_GLESFuncs.glFinish();
// A raw packed word may have redundant encodings or NaN payloads. Read
// it through R32UI when the source really still uses that native layout.
const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
if (rawPacked && pixelBytes == sizeof(Uint32)) {
const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
uploadTarget <= TextureUploadTarget::CubeMapNegativeZ
? static_cast<GLint>(uploadTarget) - static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX) : 0;
for (GLint layer = 0; layer < extent.z(); ++layer)
if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(),
reinterpret_cast<Uint32*>(result.data() + static_cast<SizeT>(layer) * sliceBytes))) return false;
bytes = Move(result);
return true;
}
NormalizedImageCarrierRead carrier{};
if (widening && widening.CarriesNormalizedCodes()) {
for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c];
carrier.SignedNormalized = widening.SignedNormalized;
}
TempFBOBinder binder(true);
auto& framebuffer = binder.Framebuffer();
for (GLint layer = 0; layer < extent.z(); ++layer) {
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer);
else
ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level);
ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0);
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE) {
MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x",
status, texture, level, nativeTarget);
return false;
}
Vector<Uint8> slice;
if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type,
TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) ||
slice.size() != sliceBytes) return false;
Memcpy(result.data() + static_cast<SizeT>(layer) * sliceBytes, slice.data(), sliceBytes);
}
bytes = Move(result);
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
// its FBO attachment is incomplete - which used to end the session at
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
GLint level, const IntVec3& logicalExtent) {
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
GLenum binding = GL_NONE;
switch (nativeTarget) {
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
default: return true;
}
// A cube's levels are per face; every other target answers for the whole level.
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
DrainESErrors();
GLint previous = 0;
g_GLESFuncs.glGetIntegerv(binding, &previous);
g_GLESFuncs.glBindTexture(nativeTarget, texture);
GLint width = 0, height = 0, depth = 0;
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
if (!answered) return true;
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
}
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
static const IntVec3 s_refused = [] {
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
if (value == nullptr) return IntVec3{0, 0, 0};
char* end = nullptr;
const long width = std::strtol(value, &end, 10);
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
const long height = std::strtol(end + 1, nullptr, 10);
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
}();
return s_refused.x() > 0 && extent == s_refused;
}
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
// written by the driver except by copies the store followed (!IsDriverWritten - see
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
// Anything else DECLINES BY NAME: the reply is an error, and the client's
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
// since overwritten.
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
GLenum format, GLenum type, Vector<Uint8>& bytes) {
auto& store = MG_Remote::Server::ServerStagedTexture();
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
const auto target = static_cast<Uint16>(uploadTarget);
const auto mip = static_cast<Uint16>(level);
ReadbackChannelMapping mapping{};
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
static_cast<SizeT>(std::max(extent.z(), 0));
const char* declined = nullptr;
if (!store.CopiesIntoServerStorage()) {
declined = "this arm keeps no staged store";
} else if (store.IsDriverWritten(key)) {
declined = "the driver has written the texture since its bytes crossed";
} else if (!store.IsCovered(key, target, mip)) {
declined = "not every byte of the level crossed";
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
declined = "the staged level is not the extent asked for";
} else if (pixelBytes == 0 || texels == 0) {
declined = "the format/type pair has no readback mapping";
}
Vector<Uint8> out;
if (declined == nullptr) {
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
declined = "the staged words are not the client's word size";
} else {
out.assign(staged, staged + texels * pixelBytes);
}
} else {
Vector<Uint8> wide;
Bool isInteger = false;
Bool isSigned = false;
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
isInteger, isSigned)) {
declined = "the staged format has no wide decoding";
} else if (mapping.isInteger != isInteger) {
declined = "an integer read of a normalized level, or the reverse";
} else {
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
const SizeT rowTexels = static_cast<SizeT>(extent.x());
const SizeT rows = texels / rowTexels;
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
out.resize(texels * pixelBytes);
for (SizeT row = 0; row < rows; ++row) {
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
out.data() + row * rowTexels * pixelBytes, rowTexels,
wideType, mapping, type);
}
}
}
}
if (declined != nullptr) {
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
res.Slot, res.Gen, level, declined);
return false;
}
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
"answered from the server's staged store (texture-readback-from-store)",
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
bytes = Move(out);
return true;
}
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
const auto* record = PipeTextureRecordForHandle(info.Res);
if (!record) return false;
TextureTarget target = TextureTarget::Unknown;
for (Int i = 0; i < static_cast<Int>(TextureTarget::TextureTargetCount); ++i)
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)) == record->Desc.Target)
target = static_cast<TextureTarget>(i);
if (target == TextureTarget::Unknown) return false;
const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res);
if (!twin) return false;
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
// ReadTextureLevelTight's first choice for exactly these reads).
const Bool packedWordRead =
!twin->RequiresImageBindableStorage() &&
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
Vector<Uint8> result;
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
const auto uploadTarget = wholeCube
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
Vector<Uint8> slice;
if (refusedForTesting ||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
info.Format, info.Type, slice)) return false;
}
result.insert(result.end(), slice.begin(), slice.end());
}
bytes = Move(result);
return true;
}
#endif // MOBILEGL_BUILD_DISAGGREGATED
File diff suppressed because it is too large Load Diff
@@ -9,7 +9,6 @@
#pragma once
#include <Includes.h>
#include "../BackendObject.h"
#include "MagmaSession.h"
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -27,10 +26,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~BackendObject_DirectVulkan() override;
void Initialize() override;
void BindSessionStateToThisThread() override;
// The GPU hang watch's verdict on this session's device (VulkanRenderer::LatchIfGpuHung).
Bool LatchIfGpuFaulted() override;
void OnClientContextBound(Uint64 token) override;
Bool InitWindowSurface() override;
Bool InitCapabilities() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
@@ -39,22 +34,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void SetEGLSwapInterval(Int interval) override;
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// A glFlush that ends a shared-image producer's frame (VulkanRenderer::PublishSharedImageAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (VulkanRenderer::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: the window surface's target (VkSurfaceKHR,
// swapchain) is destroyed and a pbuffer target of the same extent stands in for it, and back.
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -66,11 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void OnEGLSurfaceForgotten(EGLSurface surface) override;
// A served session keeps one renderer across all its client's surfaces and switches the
// renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget).
Bool UsesSurfaceTargets() const;
static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast<Uint64>(surface); }
void UpdateAdvertisedExtensions();
void UpdateDynamicBackendParameters();
@@ -78,21 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
// A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its
// apply thread. Null in the monolith and every other single-session shape.
UniquePtr<MagmaSession> m_magmaSession;
// The binding each client context was last made current with, for OnClientContextBound.
UnorderedMap<EGLContext, EGLCurrentState> m_contextBindings;
// Window surfaces whose window is gone (SuspendServerWindow), with the extent their
// placeholder pbuffer target is built at - the window's, so the default framebuffer the
// client knows keeps its size.
struct SuspendedWindowSurface {
Uint32 Width = 1;
Uint32 Height = 1;
};
UnorderedMap<EGLSurface, SuspendedWindowSurface> m_suspendedWindowSurfaces;
// The placeholder pbuffer target of a suspended surface, made active. False: it could not be built.
Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent);
};
// Single-source-of-truth helpers shared with the driver POST
@@ -101,21 +62,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// POST screen shows.
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
// capabilities). A live backend copies this in its constructor and
// versions, ExtraVendor) with the baseline extension advertisement (no shader
// subgroup, no timer queries). A live backend copies this in its constructor and
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
// exist; callers that need the advertised list for a known capability set must
// use BuildAdvertisedExtensions instead.
const RendererInfo& GetRendererIdentity();
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
// the detected device support (passing an already-gated value is harmless).
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported);
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported);
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
// string an initialized backend returns from GetBackendAPIVersionString (and that
@@ -1,29 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DescriptorPoolCensus.h
// 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
#pragma once
#include <cstdint>
namespace MobileGL::MG_Backend::DirectVulkan {
// A count of Magma's per-frame descriptor pools and the sets they hold (UniformManager), for
// tests. Dependency-free on purpose: the integration scenarios include it next to the system GL
// headers. They link the static library on desktop; the shipping shared library is built with
// hidden visibility, so on Android nothing outside the library can call GetDescriptorPoolCensus.
struct DescriptorPoolCensus {
bool available = false; // false when no Magma renderer is live (e.g. the DirectGLES backend)
uint32_t frameSlots = 0;
uint32_t pools = 0; // descriptor pools across every frame slot
uint32_t maxPoolsInOneSlot = 0;
uint64_t capacitySets = 0; // sum of maxSets over those pools
uint64_t cachedSets = 0; // sets allocated from them and held in the per-layout caches
uint64_t slotTrims = 0; // whole-slot trims since the renderer came up
};
DescriptorPoolCensus GetDescriptorPoolCensus();
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff
+14 -62
View File
@@ -9,32 +9,10 @@
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include "MagmaSession.h"
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
// The renderer of the calling thread's Magma session (MagmaSession.h): a served session's own on
// its apply thread, the process-wide one everywhere else. Spelled like the UniquePtr it used to
// be so the call sites read the same.
class RendererSlot {
public:
UniquePtr<VulkanRenderer>& Slot() const { return m_slot.Get(); }
VulkanRenderer* get() const { return Slot().get(); }
VulkanRenderer* operator->() const { return get(); }
VulkanRenderer& operator*() const { return *get(); }
explicit operator bool() const { return get() != nullptr; }
RendererSlot& operator=(UniquePtr<VulkanRenderer>&& renderer) {
Slot() = std::move(renderer);
return *this;
}
void reset() { Slot().reset(); }
friend Bool operator==(const RendererSlot& slot, std::nullptr_t) { return slot.get() == nullptr; }
friend Bool operator!=(const RendererSlot& slot, std::nullptr_t) { return slot.get() != nullptr; }
private:
SessionLocal<UniquePtr<VulkanRenderer>> m_slot;
};
extern RendererSlot pVulkanRenderer;
extern UniquePtr<VulkanRenderer> pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -45,34 +23,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint64 GetRendererGeneration();
void BumpRendererGeneration();
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
// non-null on every Magma role that owns a backend. See the definition for what the one
// arm does and for the inproc gap it closes. Push-only and disaggregated-only: a pull
// build declares no notice at all (StateObjectDeathNotice.h) and has nothing to emit.
void InstallStateObjectDeathOps();
// True when the notice currently installed is this backend's. Read by MG_Test so the
// install can be asserted without a death having to happen first.
Bool StateObjectDeathOpsInstalled();
#endif
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
// safe because GL calls are serialized in this codebase, and any still-live
// program rebuilds its entry from the retained generated SPIR-V on demand.
void ClearProgramResourceCaches();
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferfv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLfloat* value);
void ClearNamedFramebufferiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLint* value);
void ClearNamedFramebufferuiv(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, const GLuint* value);
void ClearNamedFramebufferfi(const SharedPtr<MG_State::GLState::FramebufferObject>& framebuffer, GLenum buffer,
GLint drawbuffer, GLfloat depth, GLint stencil);
void Clear(GLbitfield mask);
@@ -116,9 +72,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -129,7 +85,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
GLenum format);
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
void GetProgramInterfaceiv(GLuint program, GLenum programInterface, GLenum pname, GLint* params);
GLuint GetProgramResourceIndex(GLuint program, GLenum programInterface, const GLchar* name);
void GetProgramResourceName(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize,
GLsizei* length, GLchar* name);
void GetProgramResourceiv(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount,
const GLenum* props, GLsizei bufSize, GLsizei* length, GLint* params);
GLint GetProgramResourceLocation(GLuint program, GLenum programInterface, const GLchar* name);
GLint GetProgramResourceLocationIndex(GLuint program, GLenum programInterface, const GLchar* name);
void ShaderStorageBlockBinding(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding);
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture, TextureUploadTarget uploadTarget,
@@ -151,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only while a live renderer exists whose device can actually time.
Bool IsTimerQuerySupported();
BackendQueryHandle BeginTimeElapsedQuery();
BackendQueryHandle BeginXfbPrimitivesQuery(Bool generated);
void EndXfbPrimitivesQuery(BackendQueryHandle query);
BackendQueryHandle BeginOcclusionQuery();
void EndOcclusionQuery(BackendQueryHandle query);
void EndTimeElapsedQuery(BackendQueryHandle query);
BackendQueryHandle QueryCounterTimestamp();
Bool IsQueryResultAvailable(BackendQueryHandle query);
@@ -169,14 +131,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
Int64 GetGpuTimestampNs();
void Present();
// eglSwapInterval. Remembered across renderers: one created for a later window surface
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
void SetSwapInterval(Int interval);
Optional<Int> GetRequestedSwapInterval();
#if MOBILEGL_BUILD_DISAGGREGATED
// The swap interval an ended server session asked for is not the next one's, the same rule as
// DirectGLES::ForgetRequestedSwapInterval: under a transport ~BackendObject_DirectVulkan forgets
// it, so the next session starts at the default until it asks.
void ForgetRequestedSwapInterval();
#endif
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,87 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
// 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
#include "MagmaSession.h"
#include <atomic>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
struct SessionLocalEntry {
Detail::SessionLocalConstruct construct;
Detail::SessionLocalDestroy destroy;
};
// Construct-on-first-use: SessionLocals are namespace-scope objects in several translation
// units and register during static initialization, in no particular order.
Vector<SessionLocalEntry>& Registry() {
static auto* registry = new Vector<SessionLocalEntry>();
return *registry;
}
std::atomic<Uint64> g_nextSessionId{1};
thread_local MagmaSession* t_boundSession = nullptr;
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
// monolith's renderer down from a static destructor after the driver is gone.
MagmaSession& ProcessSession() {
static auto* session = new MagmaSession();
return *session;
}
} // namespace
Uint32 Detail::RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy) {
auto& registry = Registry();
registry.push_back({construct, destroy});
return static_cast<Uint32>(registry.size() - 1);
}
MagmaSession& Detail::CurrentMagmaSession() {
MagmaSession* bound = t_boundSession;
return bound != nullptr ? *bound : ProcessSession();
}
void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
void*& slot = session.m_slots[index];
if (slot == nullptr) slot = Registry()[index].construct();
return slot;
}
MagmaSession::MagmaSession() : m_id(g_nextSessionId.fetch_add(1, std::memory_order_relaxed)) {
// Every slot is built up front: the process-wide session is reached from whichever thread
// the app renders on, and a lazily built slot would be a first-touch race between two.
const auto& registry = Registry();
m_slots.resize(registry.size(), nullptr);
for (SizeT i = 0; i < registry.size(); ++i) m_slots[i] = registry[i].construct();
}
MagmaSession::~MagmaSession() {
MagmaSession* const previous = t_boundSession;
t_boundSession = this;
const auto& registry = Registry();
for (SizeT i = m_slots.size(); i-- > 0;) {
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
m_slots[i] = nullptr;
}
// Never back to this session: it is gone.
t_boundSession = previous == this ? nullptr : previous;
}
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(t_boundSession) {
t_boundSession = session;
}
MagmaSessionScope::~MagmaSessionScope() { t_boundSession = m_previous; }
void BindMagmaSessionToThisThread(MagmaSession* session) { t_boundSession = session; }
MagmaSession* BoundMagmaSession() { return t_boundSession; }
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,100 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.h
// 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
// MAGMA'S PER-SESSION STATE. Magma was written for one renderer per process: the renderer slot,
// the active buffer manager, the dynamic-state shadow, the render-pass manager's statics and the
// renderer generation are all file-scope. A multi-session server (several clients served from one
// process, each on its own apply thread with its own BackendObject_DirectVulkan) needs one copy of
// each per session, or one session's work lands in another's tables.
//
// SessionLocal<T> is that copy. It resolves through the CALLING THREAD's bound MagmaSession, and to
// the process-wide default session on every thread nothing bound - which is the monolith, the client
// role and every unit case, i.e. exactly the old behaviour, on whatever thread the app renders from.
// A served session's backend owns its MagmaSession and binds it on its apply thread (the only thread
// that runs that session's backend), so the two never mix.
#pragma once
#include <Includes.h>
#include <cstddef>
#include <new>
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
class MagmaSession;
namespace Detail {
using SessionLocalConstruct = void* (*)();
using SessionLocalDestroy = void (*)(void*);
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
// The calling thread's session, or the process-wide default.
MagmaSession& CurrentMagmaSession();
void* SessionLocalSlot(MagmaSession& session, Uint32 index);
} // namespace Detail
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
// destroyed in reverse registration order with the session still bound, so a destructor that
// reaches another SessionLocal finds this session's copy and not the binding thread's.
class MagmaSession {
public:
MagmaSession();
~MagmaSession();
MagmaSession(const MagmaSession&) = delete;
MagmaSession& operator=(const MagmaSession&) = delete;
// A monotonic id, unique for the process lifetime. Never an address: a later session can
// be allocated where an ended one lived.
Uint64 Id() const { return m_id; }
private:
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
Vector<void*> m_slots;
Uint64 m_id = 0;
};
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
// previous binding (usually none) afterwards.
class MagmaSessionScope {
public:
explicit MagmaSessionScope(MagmaSession* session);
~MagmaSessionScope();
MagmaSessionScope(const MagmaSessionScope&) = delete;
MagmaSessionScope& operator=(const MagmaSessionScope&) = delete;
private:
MagmaSession* m_previous = nullptr;
};
// Binds `session` to the calling thread until the next call (nullptr unbinds). For the apply
// thread, which binds its backend once and runs it for the rest of its life.
void BindMagmaSessionToThisThread(MagmaSession* session);
// The calling thread's bound session, or nullptr (the process-wide default answers).
MagmaSession* BoundMagmaSession();
template <class T>
class SessionLocal {
public:
SessionLocal() : m_index(Detail::RegisterSessionLocal(&Construct, &Destroy)) {}
SessionLocal(const SessionLocal&) = delete;
SessionLocal& operator=(const SessionLocal&) = delete;
T& Get() const {
return *static_cast<T*>(Detail::SessionLocalSlot(Detail::CurrentMagmaSession(), m_index));
}
T& operator*() const { return Get(); }
T* operator->() const { return &Get(); }
private:
static void* Construct() { return new T(); }
static void Destroy(void* object) { delete static_cast<T*>(object); }
const Uint32 m_index;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -7,9 +7,6 @@
// End of Source File Header
#include "BufferArena.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
@@ -114,11 +111,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
if (buffer.IsValid()) {
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
// still names it, and those slices stay in service until the frame slot is rewound
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
// memos). The release therefore has to survive every mid-frame reclaim and land on
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
m_deferredReleases[frameIndex].push_back(std::move(buffer));
}
@@ -128,14 +120,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
bufferDesc.usage = m_desc.usage;
bufferDesc.memoryUsage = m_desc.memoryUsage;
bufferDesc.allocationFlags = m_desc.allocationFlags;
#if MOBILEGL_BUILD_DISAGGREGATED
// Split staging may also be the destination of a GPU UBO range copy.
// Persistent host writes must be coherent: Upload deliberately writes
// the mapped pointer without a per-range VMA flush.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
}
#endif
if (!buffer.Create(bufferDesc)) {
return false;
}
@@ -7,9 +7,6 @@
// End of Source File Header
#include "FrameContext.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
@@ -19,19 +16,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_device = device;
m_commandPool = commandPool;
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = frameCount * 2;
allocInfo.commandBufferCount = frameCount;
VkResult result = vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data());
if (result != VK_SUCCESS) {
return result;
}
for (Uint32 i = 0; i < frameCount; ++i) {
m_frames[i].commandBuffer = commandBuffers[i];
m_frames[i].preCommandBuffer = commandBuffers[frameCount + i];
}
VkSemaphoreCreateInfo semaphoreInfo{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
@@ -51,10 +47,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FrameContext::Destroy(VkDevice device, VkCommandPool commandPool) {
const Uint32 frameCount = static_cast<Uint32>(m_frames.size());
Vector<VkCommandBuffer> commandBuffers(frameCount * 2, VK_NULL_HANDLE);
Vector<VkCommandBuffer> commandBuffers(frameCount, VK_NULL_HANDLE);
for (Uint32 i = 0; i < frameCount; ++i) {
commandBuffers[i] = m_frames[i].commandBuffer;
commandBuffers[frameCount + i] = m_frames[i].preCommandBuffer;
}
for (Uint32 i = 0; i < frameCount; ++i) {
@@ -65,12 +60,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
if (!m_spareCommandBuffers.empty())
vkFreeCommandBuffers(device, commandPool, static_cast<Uint32>(m_spareCommandBuffers.size()),
m_spareCommandBuffers.data());
vkFreeCommandBuffers(device, commandPool, frameCount, commandBuffers.data());
}
m_spareCommandBuffers.clear();
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
@@ -96,8 +87,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
currentFrameIndex = (currentFrameIndex + 1) % static_cast<Uint32>(m_frames.size());
GetCurrent().isCommandRecording = false;
GetCurrent().hasCommandBufferRecorded = false;
GetCurrent().isPreCommandRecording = false;
GetCurrent().hasPreCommandBufferRecorded = false;
}
VkCommandBuffer& FrameContext::BeginCommandRecording(VkCommandBufferUsageFlags flags,
@@ -129,41 +118,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.hasCommandBufferRecorded = true;
}
VkCommandBuffer FrameContext::BeginPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
return frame.preCommandBuffer;
}
MOBILEGL_ASSERT(!frame.hasPreCommandBufferRecorded,
"BeginPreCommandRecording: a recorded pre stream is still awaiting submission");
VK_VERIFY(vkResetCommandBuffer(frame.preCommandBuffer, 0), "BeginPreCommandRecording, vkResetCommandBuffer");
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
VK_VERIFY(vkBeginCommandBuffer(frame.preCommandBuffer, &beginInfo),
"BeginPreCommandRecording, vkBeginCommandBuffer");
frame.isPreCommandRecording = true;
return frame.preCommandBuffer;
}
void FrameContext::EndPreCommandRecordingIfOpen() {
auto& frame = GetCurrent();
if (!frame.isPreCommandRecording) {
return;
}
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "EndPreCommandRecordingIfOpen, vkEndCommandBuffer");
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = true;
}
void FrameContext::AbandonPreCommandRecording() {
auto& frame = GetCurrent();
if (frame.isPreCommandRecording) {
VK_VERIFY(vkEndCommandBuffer(frame.preCommandBuffer), "AbandonPreCommandRecording, vkEndCommandBuffer");
}
frame.isPreCommandRecording = false;
frame.hasPreCommandBufferRecorded = false;
}
VkResult FrameContext::InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount) {
DestroySwapchainSemaphores(device);
if (swapchainImageCount == 0) {
@@ -196,41 +150,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool FrameContext::TransitionToPresent(VkImage image, VkImageLayout oldLayout, VkImageLayout presentLayout) {
auto& frame = GetCurrent();
if (oldLayout == presentLayout || oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
if (frame.hasCommandBufferRecorded || frame.isCommandRecording || oldLayout == presentLayout ||
oldLayout == VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR) {
return false;
}
// The barrier belongs in the frame's own recording. Bailing out because
// something was already recorded (the previous behaviour) dropped the
// transition entirely for every frame that never ran a default-framebuffer
// render pass - the only other thing that carries the image to
// PRESENT_SRC_KHR, via that pass's finalLayout - so the swapchain image was
// handed to the WSI still in the layout it was acquired in.
// A closed-but-unsubmitted buffer can only come from a submit that already
// failed (SubmitPendingCommandBuffer leaves the flag set on error), and
// appending to it is illegal while reopening would reset the frame's own
// commands away. The device is gone on that path anyway - stay silent-safe
// rather than trade a lost device for a barrier into a closed buffer.
if (frame.hasCommandBufferRecorded) {
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
return false;
}
// Reopening a recording here would vkResetCommandBuffer this frame's own
// commands away, so append to the open one and let the caller close it.
const Bool openedRecording = !frame.isCommandRecording;
VkCommandBuffer commandBuffer = openedRecording ? BeginCommandRecording() : frame.commandBuffer;
auto& commandBuffer = BeginCommandRecording();
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
presentBarrier.srcAccessMask = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
// A layout transition writes the image too. Order it after the wire
// render pass's storeOp, clears and copies instead of relying on the old
// per-draw queue-idle path. This applies even when contents are discarded.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
#endif
presentBarrier.dstAccessMask = 0;
presentBarrier.oldLayout = oldLayout;
presentBarrier.newLayout = presentLayout;
@@ -242,19 +171,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
presentBarrier.subresourceRange.levelCount = 1;
presentBarrier.subresourceRange.baseArrayLayer = 0;
presentBarrier.subresourceRange.layerCount = 1;
vkCmdPipelineBarrier(commandBuffer,
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Config::Transport != MG_Config::TransportMode::Monolith
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
#else
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
#endif
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
nullptr, 0, nullptr, 1, &presentBarrier);
if (openedRecording) {
EndCommandRecording();
}
EndCommandRecording();
return true;
}
@@ -262,27 +182,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 swapchainImageIndex) const {
const auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording, "GetSubmitInfo called while command buffer recording is still active");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"GetSubmitInfo called while the pre-pass stream is still recording");
AssertValidSwapchainImageIndex(swapchainImageIndex);
SubmitInfoPacket packet{};
packet.waitSemaphore = frame.imageAvailableSemaphore;
packet.signalSemaphore = m_swapchainImageRenderFinishedSemaphores[swapchainImageIndex];
Uint32 commandBufferCount = 0;
// The pre-pass stream executes strictly before the frame's commands.
if (frame.hasPreCommandBufferRecorded) {
packet.commandBuffers[commandBufferCount++] = frame.preCommandBuffer;
}
if (shouldSubmitCommandBuffer) {
packet.commandBuffers[commandBufferCount++] = frame.commandBuffer;
}
packet.commandBuffer = frame.commandBuffer;
packet.submitInfo.waitSemaphoreCount = frame.imageAvailableSemaphoreConsumed ? 0U : 1U;
packet.submitInfo.pWaitSemaphores = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitSemaphore;
packet.submitInfo.pWaitDstStageMask = frame.imageAvailableSemaphoreConsumed ? nullptr : &packet.waitDstStageMask;
packet.submitInfo.commandBufferCount = commandBufferCount;
packet.submitInfo.pCommandBuffers = commandBufferCount > 0 ? packet.commandBuffers : nullptr;
packet.submitInfo.commandBufferCount = shouldSubmitCommandBuffer ? 1U : 0U;
packet.submitInfo.pCommandBuffers = shouldSubmitCommandBuffer ? &packet.commandBuffer : nullptr;
packet.submitInfo.signalSemaphoreCount = 1;
packet.submitInfo.pSignalSemaphores = &packet.signalSemaphore;
return packet;
@@ -304,15 +214,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return packet;
}
VkResult FrameContext::AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain,
Uint32& outImageIndex) {
auto& frame = GetCurrent();
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
VK_NULL_HANDLE, &outImageIndex);
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false;
return result;
}
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout, VkFence acquireFence) {
auto& frame = GetCurrent();
@@ -320,29 +221,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (result != VK_SUCCESS) {
return result;
}
// Under disaggregation VulkanRenderer waits every registered fence
// through this slot's last submit before calling here. That aggregate
// proof covers mid-frame pooled-fence flushes too; the slot fence alone
// would not. Initialize's first acquire has no prior submissions.
// The slot's fence has been waited: every command buffer this slot
// submitted (including mid-frame flushes) has finished executing.
FreeRetiredCommandBuffers(frame);
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
&outImageIndex);
// VK_SUBOPTIMAL_KHR is a success code: an image *was* acquired and
// imageAvailableSemaphore *will* be signaled. Bailing out on it skipped both
// the consumed-flag reset (leaving a stale "already consumed", so the next
// submit never waited on the pending signal) and the fence reset (leaving
// the slot's fence signaled for the next submit to reuse). Only a genuine
// failure - VK_ERROR_OUT_OF_DATE_KHR and friends, where nothing is acquired
// and nothing is signaled - skips the bookkeeping.
if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
if (result != VK_SUCCESS) {
return result;
}
frame.imageAvailableSemaphoreConsumed = false;
const VkResult resetResult = vkResetFences(device, 1, &frame.imageInFlightFence);
// Hand the acquire's own code back so the caller can schedule a rebuild.
return resetResult == VK_SUCCESS ? result : resetResult;
return vkResetFences(device, 1, &frame.imageInFlightFence);
}
Uint32 FrameContext::GetCurrentFrameIndex() const {
@@ -357,33 +247,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_recordingObserver = observer;
}
VkResult FrameContext::RetireCurrentCommandBuffer(Bool retirePreCommandBuffer) {
VkResult FrameContext::RetireCurrentCommandBuffer() {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE,
"RetireCurrentCommandBuffer requires an initialized FrameContext");
auto& frame = GetCurrent();
MOBILEGL_ASSERT(!frame.isCommandRecording,
"RetireCurrentCommandBuffer called while the command buffer is still recording");
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
VkResult result = TakeCommandBuffer(replacement);
const VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = TakeCommandBuffer(preReplacement);
if (result != VK_SUCCESS) {
RecycleCommandBuffer(replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
frame.preCommandBuffer = preReplacement;
}
// lastSubmitIndex was just written by the renderer for the submission
// that carried this command buffer.
frame.retiredCommandBuffers.push_back({frame.commandBuffer, frame.lastSubmitIndex});
frame.retiredCommandBuffers.push_back(frame.commandBuffer);
frame.commandBuffer = replacement;
return VK_SUCCESS;
}
@@ -393,64 +274,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return;
}
if (m_device != VK_NULL_HANDLE && m_commandPool != VK_NULL_HANDLE) {
for (const auto& retired : frame.retiredCommandBuffers) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &retired.commandBuffer);
}
vkFreeCommandBuffers(m_device, m_commandPool, static_cast<Uint32>(frame.retiredCommandBuffers.size()),
frame.retiredCommandBuffers.data());
}
frame.retiredCommandBuffers.clear();
}
void FrameContext::FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex) {
if (m_device == VK_NULL_HANDLE || m_commandPool == VK_NULL_HANDLE) {
return;
}
for (auto& frame : m_frames) {
// Retired buffers are appended in submit order, so the completed
// ones form a prefix.
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
RecycleCommandBuffer(frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
frame.retiredCommandBuffers.erase(frame.retiredCommandBuffers.begin(),
frame.retiredCommandBuffers.begin() + completedCount);
}
}
}
VkResult FrameContext::TakeCommandBuffer(VkCommandBuffer& out) {
if (!m_spareCommandBuffers.empty()) {
out = m_spareCommandBuffers.back();
m_spareCommandBuffers.pop_back();
return VK_SUCCESS;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
return vkAllocateCommandBuffers(m_device, &allocInfo, &out);
}
void FrameContext::RecycleCommandBuffer(VkCommandBuffer commandBuffer) {
// A handful covers the buffers in flight between two completions; beyond that, free.
constexpr SizeT kSpareCommandBuffers = 8;
if (commandBuffer == VK_NULL_HANDLE) return;
if (m_spareCommandBuffers.size() < kSpareCommandBuffers) {
m_spareCommandBuffers.push_back(commandBuffer);
return;
}
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
}
}
void FrameContext::AssertValidFrameIndex(Uint32 frameIndex) const {
MOBILEGL_ASSERT(frameIndex < m_frames.size(), "FrameContext index out of range");
}
@@ -29,9 +29,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipelineStageFlags waitDstStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
VkSemaphore waitSemaphore = VK_NULL_HANDLE;
VkSemaphore signalSemaphore = VK_NULL_HANDLE;
// [0] = pre-pass command buffer (when recorded), then the frame
// command buffer; submitInfo.pCommandBuffers points here.
VkCommandBuffer commandBuffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
VkSubmitInfo submitInfo{VK_STRUCTURE_TYPE_SUBMIT_INFO};
};
@@ -42,35 +40,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPresentInfoKHR presentInfo{VK_STRUCTURE_TYPE_PRESENT_INFO_KHR};
};
// A command buffer submitted mid-frame (FlushPendingCommands), tagged
// with the submit-tracker index it was submitted under so it can be
// freed as soon as that submission is observed complete - without
// waiting for the slot's fence to be waited again (present-less flush
// loops never wait it).
struct RetiredCommandBuffer {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Uint64 submitIndex = 0;
};
struct FrameData {
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
// Pre-pass work stream: out-of-pass commands (deferred clear
// materialization, sampled-layout transitions) for resources the
// frame's recording has not touched yet. Submitted immediately
// BEFORE commandBuffer in the same vkQueueSubmit, so recording
// into it never has to split the frame's active render pass.
VkCommandBuffer preCommandBuffer = VK_NULL_HANDLE;
VkSemaphore imageAvailableSemaphore = VK_NULL_HANDLE;
VkFence imageInFlightFence = VK_NULL_HANDLE;
Bool isCommandRecording = false;
Bool hasCommandBufferRecorded = false;
Bool isPreCommandRecording = false;
Bool hasPreCommandBufferRecorded = false;
Bool imageAvailableSemaphoreConsumed = false;
// Command buffers submitted mid-frame (FlushPendingCommands),
// appended in submit order; freed once their submission is known
// complete (fence wait or completion poll).
Vector<RetiredCommandBuffer> retiredCommandBuffers;
// Command buffers submitted mid-frame (FlushPendingCommands) whose
// execution is only known complete once this slot's fence has been
// waited again; freed at that point.
Vector<VkCommandBuffer> retiredCommandBuffers;
// Submit-tracker index of this slot's most recent queue submission
// (written by the renderer at submit time).
Uint64 lastSubmitIndex = 0;
@@ -87,45 +67,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkCommandBuffer& BeginCommandRecording(VkCommandBufferUsageFlags flags = 0,
const VkCommandBufferInheritanceInfo* pInheritanceInfo = nullptr);
void EndCommandRecording();
// Lazily opens the pre-pass work stream (see FrameData::preCommandBuffer).
VkCommandBuffer BeginPreCommandRecording();
// Closes the pre stream if open, marking it for submission ahead of the
// frame command buffer. Safe to call when it never opened.
void EndPreCommandRecordingIfOpen();
// Drops an in-progress or recorded-but-unsubmitted pre stream (dropped
// frame recordings, swapchain recreation).
void AbandonPreCommandRecording();
VkResult InitializeSwapchainSemaphores(VkDevice device, Uint32 swapchainImageCount);
void DestroySwapchainSemaphores(VkDevice device);
Bool TransitionToPresent(VkImage image, VkImageLayout oldLayout,
VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const;
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const;
// Acquires into the current slot's semaphore WITHOUT the slot-fence wait: for a surface
// brought up mid-frame, whose slot fence this frame has already waited and reset. The
// slot's semaphore must be unsignaled with no pending signal (consumed).
VkResult AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex);
// The per-swapchain-image render-finished semaphores belong to one swapchain; a renderer
// that parks a surface parks them with it.
void ExchangeSwapchainSemaphores(Vector<VkSemaphore>& other) { std::swap(m_swapchainImageRenderFinishedSemaphores, other); }
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
// Parks the current (already ended and submitted) command buffer on the
// slot's retired list and installs a freshly allocated one, so recording
// can restart while the submitted buffer is still executing. Retired
// buffers are freed after the slot's fence is next waited, or as soon
// as their submission is observed complete.
VkResult RetireCurrentCommandBuffer(Bool retirePreCommandBuffer = false);
// Frees every retired command buffer whose tagged submission index is
// known complete. Driven by the renderer's submit tracker on completion
// events (fence waits and non-blocking polls), so present-less flush
// loops reclaim their buffers without any extra wait.
void FreeRetiredCommandBuffersCompletedUpTo(Uint64 completedSubmitIndex);
// Frees every slot's retired command buffers. Only valid when the
// caller has proven every queue submission complete.
void FreeAllRetiredCommandBuffers();
// buffers are freed after the slot's fence is next waited.
VkResult RetireCurrentCommandBuffer();
Uint32 GetCurrentFrameIndex() const;
Uint32 GetFrameCount() const;
@@ -150,10 +105,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
// Completed command buffers kept for the next retirement instead of being freed and
// reallocated: the pool resets a buffer implicitly at its next vkBeginCommandBuffer.
Vector<VkCommandBuffer> m_spareCommandBuffers;
VkResult TakeCommandBuffer(VkCommandBuffer& out);
void RecycleCommandBuffer(VkCommandBuffer commandBuffer);
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,202 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
// 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
#include "GpuProgressMarkers.h"
#include <MG_Util/Debug/Log.h>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace Watch = MG_Backend::GpuHangWatch;
void GpuProgressMarkers::Init(VkDevice device, Uint32 queueFamilyIndex) {
if (m_device != VK_NULL_HANDLE || device == VK_NULL_HANDLE) return;
if (Watch::BudgetMs() == 0) {
MGLOG_I("Magma: GPU hang watch off (MOBILEGL_GPU_HANG_BUDGET_MS=0)");
return;
}
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.queueFamilyIndex = queueFamilyIndex;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &m_pool) != VK_SUCCESS) {
MGLOG_W("Magma: GPU hang watch off - no command pool for its markers");
m_pool = VK_NULL_HANDLE;
return;
}
m_device = device;
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!AddSlot()) {
MGLOG_W("Magma: GPU hang watch off - the device cannot make its progress markers");
vkDestroyCommandPool(m_device, m_pool, nullptr);
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
return;
}
}
m_watch = Watch::Register(this, &Sample, &OnHung);
MGLOG_I("Magma: GPU hang watch on - a submission the GPU started and has not finished after %u ms ends "
"its session",
Watch::BudgetMs());
}
void GpuProgressMarkers::Shutdown() {
// The watch first, outside m_mutex: Unregister waits out a sample in progress, which takes it.
Watch::Unregister(m_watch);
m_watch = 0;
if (m_device == VK_NULL_HANDLE) return;
const std::lock_guard<std::mutex> lock(m_mutex);
for (Slot& slot : m_slots) {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
}
m_slots.clear();
if (m_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, m_pool, nullptr); // frees the buffers
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
}
Bool GpuProgressMarkers::AddSlot() {
if (m_slots.size() >= kMaxSlots) return false;
Slot slot;
const VkEventCreateInfo eventInfo{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO};
VkCommandBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocInfo.commandPool = m_pool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 2;
const auto fail = [&] {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
if (buffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(m_device, m_pool, 2, buffers);
return false;
};
if (vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Started) != VK_SUCCESS ||
vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Finished) != VK_SUCCESS ||
vkAllocateCommandBuffers(m_device, &allocInfo, buffers) != VK_SUCCESS)
return fail();
// Prerecorded once and resubmitted for every submission the slot brackets; SIMULTANEOUS_USE
// because the slot is reused once its `finished` event is set, which can be a moment before
// the submission that set it formally leaves the pending state.
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
if (vkBeginCommandBuffer(buffers[0], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[0], slot.Started, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
if (vkEndCommandBuffer(buffers[0]) != VK_SUCCESS) return fail();
if (vkBeginCommandBuffer(buffers[1], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[1], slot.Finished, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
if (vkEndCommandBuffer(buffers[1]) != VK_SUCCESS) return fail();
slot.Begin = buffers[0];
slot.End = buffers[1];
m_slots.push_back(slot);
return true;
}
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire() {
Bracket bracket;
if (m_device == VK_NULL_HANDLE) return bracket;
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_lost) return bracket;
Slot* free = nullptr;
for (Slot& slot : m_slots) {
if (slot.Claimed) continue;
// Retire what finished since the watch last looked: frames come faster than its polls.
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
if (!slot.InFlight && free == nullptr) free = &slot;
}
if (free == nullptr) {
if (!AddSlot()) return bracket;
free = &m_slots.back();
}
if (free->Dirty) {
if (vkResetEvent(m_device, free->Started) != VK_SUCCESS ||
vkResetEvent(m_device, free->Finished) != VK_SUCCESS)
return bracket;
free->Dirty = false;
}
free->Claimed = true;
bracket.Begin = free->Begin;
bracket.End = free->End;
bracket.Slot = static_cast<Uint32>(free - m_slots.data());
return bracket;
}
void GpuProgressMarkers::Submitted(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
Slot& slot = m_slots[bracket.Slot];
slot.Claimed = false;
slot.InFlight = true;
slot.Dirty = true;
slot.Id = m_nextId++;
}
void GpuProgressMarkers::Abandon(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
// A failed vkQueueSubmit may still have consumed nothing or something: reset before reuse.
m_slots[bracket.Slot].Claimed = false;
m_slots[bracket.Slot].Dirty = true;
}
Bool GpuProgressMarkers::Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
const std::lock_guard<std::mutex> lock(markers->m_mutex);
if (markers->m_lost || markers->m_device == VK_NULL_HANDLE) return false;
for (Slot& slot : markers->m_slots) {
if (!slot.InFlight) continue;
const VkResult finished = vkGetEventStatus(markers->m_device, slot.Finished);
const VkResult started = vkGetEventStatus(markers->m_device, slot.Started);
if ((finished != VK_EVENT_SET && finished != VK_EVENT_RESET) ||
(started != VK_EVENT_SET && started != VK_EVENT_RESET)) {
// VK_ERROR_DEVICE_LOST: the driver already reset the device and says so itself.
markers->m_lost = true;
return false;
}
if (finished == VK_EVENT_SET) {
slot.InFlight = false;
continue;
}
out.push_back({slot.Id, started == VK_EVENT_SET, false});
}
return true;
}
void GpuProgressMarkers::OnHung(void* self, Uint64 submissionId, Uint64 runningMs) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
markers->m_hungRunningMs.store(runningMs, std::memory_order_release);
markers->m_hung.store(true, std::memory_order_release);
MGLOG_E("Magma: submission %llu of this session's VkDevice has run %llu ms on the GPU and has not finished - "
"it is what stops the GPU for every process; the session ends at its next record and submits "
"nothing more (budget %u ms, MOBILEGL_GPU_HANG_BUDGET_MS)",
static_cast<unsigned long long>(submissionId), static_cast<unsigned long long>(runningMs),
MG_Backend::GpuHangWatch::BudgetMs());
}
VkResult GpuProgressMarkers::SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence,
const Bracket& bracket) {
constexpr Uint32 kMaxOwn = 6;
if (!bracket.Valid() || info.commandBufferCount == 0 || info.commandBufferCount > kMaxOwn)
return vkQueueSubmit(queue, 1, &info, fence);
VkCommandBuffer buffers[kMaxOwn + 2];
Uint32 count = 0;
buffers[count++] = bracket.Begin;
for (Uint32 i = 0; i < info.commandBufferCount; ++i) buffers[count++] = info.pCommandBuffers[i];
buffers[count++] = bracket.End;
const VkCommandBuffer* own = info.pCommandBuffers;
const Uint32 ownCount = info.commandBufferCount;
info.pCommandBuffers = buffers;
info.commandBufferCount = count;
const VkResult result = vkQueueSubmit(queue, 1, &info, fence);
info.pCommandBuffers = own;
info.commandBufferCount = ownCount;
return result;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,95 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.h
// 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
#pragma once
#include "../VkIncludes.h"
#include <Includes.h>
#include <MG_Backend/GpuHangWatch.h>
#include <atomic>
#include <mutex>
namespace MobileGL::MG_Backend::DirectVulkan {
// WHICH OF THIS DEVICE'S SUBMISSIONS THE GPU HAS STARTED, AND WHICH IT HAS FINISHED
// (GpuHangWatch.h says why that is the whole attribution of a GPU hang).
//
// A frame's submission is bracketed by two prerecorded one-command buffers: the first sets a
// `started` event at TOP_OF_PIPE, the last sets a `finished` event once everything before it in
// the queue has completed. The host reads both with vkGetEventStatus from the watch thread - no
// fence is shared with the renderer's own waits and resets. On Adreno 750 a submission whose
// fragment shader never ends reads started=SET from its first poll until the kernel resets the
// GPU, and a submission queued behind it reads started=RESET for the whole stall.
//
// The renderer's thread brackets submissions (Acquire / Submitted / Abandon) and asks Hung(); the
// watch thread samples. Everything Vulkan the two share - the events - is touched under m_mutex.
class GpuProgressMarkers {
public:
struct Bracket {
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint32 Slot = ~0u;
Bool Valid() const { return Slot != ~0u; }
};
GpuProgressMarkers() = default;
~GpuProgressMarkers() { Shutdown(); }
GpuProgressMarkers(const GpuProgressMarkers&) = delete;
GpuProgressMarkers& operator=(const GpuProgressMarkers&) = delete;
// Off (no brackets, never hung) when the budget is 0 or the device cannot make events.
void Init(VkDevice device, Uint32 queueFamilyIndex);
// Before the device goes; its queue must be idle. Stops the watch first.
void Shutdown();
// The two buffers to put first and last in one submission (an invalid bracket when off or
// when every slot is in flight: the submission then goes unwatched).
Bracket Acquire();
// The submission carrying `bracket` was accepted by vkQueueSubmit / was not submitted.
void Submitted(const Bracket& bracket);
void Abandon(const Bracket& bracket);
// The watch named one of this device's submissions as hanging the GPU.
Bool Hung() const { return m_hung.load(std::memory_order_acquire); }
Uint64 HungRunningMs() const { return m_hungRunningMs.load(std::memory_order_acquire); }
Bool Active() const { return m_watch != 0; }
// vkQueueSubmit of `info` with `bracket` wrapped around its command buffers (which it puts
// back before returning). At most 6 buffers of the caller's.
static VkResult SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence, const Bracket& bracket);
private:
struct Slot {
VkEvent Started = VK_NULL_HANDLE;
VkEvent Finished = VK_NULL_HANDLE;
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint64 Id = 0;
Bool InFlight = false; // submitted and not yet seen finished
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
Bool Dirty = false; // its events were set by a past submission: reset before reuse
};
static constexpr Uint32 kMaxSlots = 16;
Bool AddSlot();
static Bool Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out);
static void OnHung(void* self, Uint64 submissionId, Uint64 runningMs);
std::mutex m_mutex;
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_pool = VK_NULL_HANDLE;
Vector<Slot> m_slots;
Uint64 m_nextId = 1;
Uint64 m_watch = 0;
Bool m_lost = false;
std::atomic<Bool> m_hung{false};
std::atomic<Uint64> m_hungRunningMs{0};
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,611 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
// 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
#pragma once
#include <Includes.h>
#include <Config.h>
#if MOBILEGL_PIPE_PUSH
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
// header-only constant/POD declarations, and both are push-only, so the pull build's include
// graph is unchanged (G1).
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeHandles.h>
#endif
#include <cstdlib>
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
// {slot, gen} mint the re-keyed sites are written against.
//
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
//
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
//
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
//
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
// nothing outside Track H consults the legacy-memo lever at all - see
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
//
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
// below is behind it, and the pull build's translation units are byte-identical (G1).
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_PIPE_PUSH
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
return (MG_Config::Features.PipePush & subsystemBit) != 0;
}
// ---------------------------------------------------------------------------------
// D14's startup gate
// ---------------------------------------------------------------------------------
//
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
// that is actually running. It answers exactly one question and it answers it before the
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
//
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
//
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
// not kill an Espryt one.
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
// a clear bit there simply means the client is not pushing render-state CSOs in this
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
// and 6 "Track H" and labels bit 0 nothing of the sort.
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
// branch nobody needs on the hottest path in the backend.
//
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
// longer does it SILENTLY: the combination is named once, at startup, right here.
inline void MagmaPipeValidateSubsystemConfiguration() {
if (!MG_Config::Features.PipeLegacyMemos &&
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
"to measure it.",
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemRenderState));
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
if (MG_Config::Features.PipeLegacyMemos) return;
#endif
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
#if MOBILEGL_PIPE_LEGACY_MEMOS
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
#else
const char* const why =
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
#endif
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
why,
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
std::abort();
}
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
// site asks, so that they cannot disagree with each other or with the startup gate.
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
#if MOBILEGL_PIPE_LEGACY_MEMOS
return MagmaPipeSubsystemOn(trackHBit);
#else
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
// running process can be on.
(void)trackHBit;
return true;
#endif
}
// ---------------------------------------------------------------------------------
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
// ---------------------------------------------------------------------------------
//
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
// this run?" - the ONE question the control's sites ask, for the same reason
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
// and a control that defeats two of three guards proves nothing.
//
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
// RATHER THAN "USE THE HEAP ADDRESS".
//
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
// the freed heap block and so reproduces the key. Measured, it does not: in
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
// allocation the replacement path makes. Four create/delete cycles in one run produced
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
// "hash the address" to collide with: the replacement hashes differently, indexes a
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
//
// So the control no longer asks the allocator for the collision; it manufactures it. On
// both arms the object identity is replaced by a constant, which is the strongest form of
// "the allocator handed the block back" and is deterministic. That covers strictly more
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
// Defeating only the retired lifetime-id/address guards would leave that key untested,
// which is exactly the vacuity this control exists to catch.
//
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
//
// * THIS MINT still has no death notification, and that is now a narrower statement than
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
// from that is unchanged;
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
// draw, which necessarily puts the two draws in different frames - and the only memo
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
// across frames by design. The two requirements are mutually exclusive, so the
// generation is out of reach of any same-frame pixel reproducer for this memo.
//
// The generation is covered where it IS expressible, over this mint and the claim rule
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
// suite; it is the only place in the tree where that deletion is caught.
//
// Everything the control does NOT defeat is as load-bearing as what it does. It never
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
// AbaControl arm still means "a replacement object was handed its dead predecessor's
// resolved vertex bindings because the identity halves of the keys were defeated", not
// "every safety net was switched off until something broke".
//
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
//
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
// of P4a's memo keys as well", because a control that only defeats the guards a phase
// RETIRED says nothing about the key that phase SHIPS.
//
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
// are all still reached from their frontend objects on this backend, and moving them onto
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
// code rather than as a comment so that a caller cannot read the blanket answer above and
// conclude the knob covers its kind:
//
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
// not controlling anything for that kind. It does not assert a corruption that no code
// on this tree can produce, which would be a permanently red always-on lane.
//
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
// has for vertex input - this function's per-kind answer becomes that consumer's, the
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
return MG_Config::Features.PipeHandleAbaControl;
}
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
//
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
// "covered" direction is the expensive one - a control asserting a corruption nobody can
// produce is a permanently red always-on lane - so an undecided kind must never read true,
// and with no `default:` there is no arm for it to read true from.
//
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
// asserts say so in both directions, and the file no longer compiles if the tables and this
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
// and could not make anything red or green.)
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
switch (kind) {
// The two MagmaPipeIdentityTables really mints.
case MG_Pipe::MGPipeKind::VertexElementsCso:
case MG_Pipe::MGPipeKind::Buffer:
return true;
// P4a's six object classes: still reached from their frontend objects on this
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
// the knob to defeat.
case MG_Pipe::MGPipeKind::Texture:
case MG_Pipe::MGPipeKind::Renderbuffer:
case MG_Pipe::MGPipeKind::Framebuffer:
case MG_Pipe::MGPipeKind::SamplerCso:
case MG_Pipe::MGPipeKind::SamplerViewCso:
case MG_Pipe::MGPipeKind::ShaderCso:
// ...and everything else this backend does not mint a handle for.
case MG_Pipe::MGPipeKind::None:
case MG_Pipe::MGPipeKind::Xfb:
case MG_Pipe::MGPipeKind::RenderStateCso:
case MG_Pipe::MGPipeKind::Fence:
case MG_Pipe::MGPipeKind::Query:
case MG_Pipe::MGPipeKind::Context:
case MG_Pipe::MGPipeKind::KindCount:
return false;
}
return false;
}
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
"defeat for it");
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
"Wiring one of them is what flips this assert, this predicate and that arm - and "
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
"carries the answer into the lane");
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
// statements above - and it had no caller anywhere in the tree: the knob's only two
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
//
// The two pieces stand alone instead, and each is pinned by something that runs:
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
// Identity is the knob, and its two consumers are what make it true or false. A call site
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
// handle onto it), so nothing about the tables changes shape for the control's sake.
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
// ---------------------------------------------------------------------------------
// The {slot, gen} mint
// ---------------------------------------------------------------------------------
//
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
// properties, and the third is the one review v2 got wrong:
//
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
// never match a live object even if the allocator hands back the same heap address
// (the ABA HandleRecycleScenario reproduces);
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
// no probe and no mix;
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
// drawn, whatever the working set size.
//
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
// and every consumer memo keyed on the handle died with it: measured on a verbatim
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
// draw. That is a worse leak than the one it was introduced to avoid.
//
// So: grow on demand, and reclaim by AGE instead of by capacity.
//
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
// * OnFrameBoundary retires slots whose object has not been drawn for
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
// plus one map node per object EVER created, for the life of the process, on a
// platform with an LMK. Age-based reclamation is the stand-in for the death
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
// * A retire costs at most one memo recompute if the object is drawn again - the same
// price a cache miss costs - and it is charged only to objects that went idle for
// ~1024 frames, never to a hot one.
//
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
// no capacity to size off a device measurement because there is no capacity; what the
// device run in D.4.2 can still want is the number itself, so the high-water mark is
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
// draw - ROADMAP.md:7).
//
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
// clock, across two live contexts.
class MagmaPipeIdentityTable {
public:
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
// Objects currently holding a slot - the live working set this table tracks.
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
// that a zero can never be minted into a slot and then indexed with.
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
// that has been handed back to the free list.
if (lifetimeId == m_lastLifetimeId) {
m_entries[m_lastIndex].LastUse = m_boundary;
return m_lastHandle;
}
Uint32 index = 0;
const auto it = m_index.find(lifetimeId);
if (it != m_index.end()) {
index = it->second;
} else {
index = ClaimSlot();
m_entries[index].LifetimeId = lifetimeId;
m_index.emplace(lifetimeId, index);
}
Entry& entry = m_entries[index];
entry.LastUse = m_boundary;
m_lastLifetimeId = lifetimeId;
m_lastIndex = index;
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
entry.Gen};
return m_lastHandle;
}
// Ages the table and returns idle slots to the free list. Same shape and the same
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
// slots' consumers use.
void OnFrameBoundary() {
++m_boundary;
if ((m_boundary % kSweepInterval) != 0) return;
SizeT retired = 0;
for (auto it = m_index.begin(); it != m_index.end();) {
Entry& entry = m_entries[it->second];
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
entry.LifetimeId = 0;
m_freeSlots.push_back(it->second);
it = m_index.erase(it);
++retired;
} else {
++it;
}
}
if (retired != 0) {
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
// so a front memo pointing at one would still hand out a handle the consumer
// tables would accept. Drop it.
m_lastLifetimeId = 0;
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
m_kindName, retired, LiveCount(), Count());
}
}
private:
// Sweep cadence and retirement age, deliberately the same numbers
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
// retired earlier than its cache entry would mint a new handle for an object whose
// entry is still live and still correct, which is a pure waste.
static constexpr Uint64 kSweepInterval = 256;
static constexpr Uint64 kRetireAgeBoundaries = 1024;
struct Entry {
Uint64 LifetimeId = 0;
Uint64 LastUse = 0;
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
// its generation, which is what makes a memo survive a reconfiguration.
Uint32 Gen = 0;
};
Uint32 ClaimSlot() {
while (!m_freeSlots.empty()) {
const Uint32 index = m_freeSlots.back();
m_freeSlots.pop_back();
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
// permanently retired rather than wrapped, because a wrapped Gen would let a
// stale handle match a live object. It costs one slot.
if (m_entries[index].Gen == ~Uint32{0}) {
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
"retired for good; {slot, gen} stays unique",
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
continue;
}
++m_entries[index].Gen;
return index;
}
const Uint32 index = static_cast<Uint32>(m_entries.size());
m_entries.push_back(Entry{});
m_entries[index].Gen = 1;
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
//
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
// one is I, not D, because D is compiled out of every build that ships and of every
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
// the live-object high-water mark of minecraft-1.21.4-in-world and
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
// The structure no longer has a capacity to size off it, so the number is evidence
// rather than a tuning input - but D.4.2 should still read it out of the device log,
// and it cannot read a line that was compiled away.
const SizeT minted = m_entries.size();
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
m_kindName, minted, LiveCount());
}
return index;
}
const char* m_kindName = "";
Uint64 m_boundary = 0;
Vector<Entry> m_entries;
Vector<Uint32> m_freeSlots;
UnorderedMap<Uint64, Uint32> m_index;
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
// object's lifetime id is never 0.
Uint64 m_lastLifetimeId = 0;
Uint32 m_lastIndex = 0;
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
};
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
// table and one reclamation clock, and both consumer tables are per-instance already.
class MagmaPipeIdentityTables {
public:
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
: m_vaos.Acquire(lifetimeId);
}
void OnFrameBoundary() {
m_vaos.OnFrameBoundary();
m_buffers.OnFrameBoundary();
}
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
private:
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
MagmaPipeIdentityTable m_buffers{"Buffer"};
};
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
//
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"a null MGPipeHandle has no slot to index a per-slot table with");
return MG_Pipe::MGPipeHandleIsNull(handle)
? 0u
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
}
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
//
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
// references into these entries across nested calls. Chunks of kChunkEntries are appended
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
// reference is valid for the life of the table. That is the same guarantee the fixed table
// it replaces gave, without the fixed capacity.
template <typename T, Uint32 kChunkEntries = 256>
class MagmaPipeSlotTable {
public:
T& operator[](Uint32 index) {
const Uint32 chunk = index / kChunkEntries;
while (m_chunks.size() <= chunk) {
m_chunks.push_back(MakeUnique<Chunk>());
}
return m_chunks[chunk]->Entries[index % kChunkEntries];
}
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
private:
struct Chunk {
T Entries[kChunkEntries] = {};
};
Vector<UniquePtr<Chunk>> m_chunks;
};
// The claim rule every per-slot memo table uses, in one place so that the rule and the
// negative control that defeats it cannot drift apart between consumers - and so that the
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
// this code rather than a copy of it.
//
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
// this object's. A slot the mint recycled for a different object comes back with a moved
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
//
// With negative control C on, every object collapses onto one entry and the entry is handed
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
// slot" and "the slot was reused and Gen did not move".
//
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
template <typename Memos, Uint32 kChunkEntries>
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
const MG_Pipe::MGPipeHandle& handle) {
if (MagmaPipeAbaControlDefeatsIdentity()) {
return table[kMagmaPipeAbaControlSlotIndex];
}
Memos& memos = table[MagmaPipeSlotIndex(handle)];
if (!(memos.Owner == handle)) {
memos = Memos{};
memos.Owner = handle;
}
return memos;
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,224 +0,0 @@
// MobileGL - server-owned program input for Magma (P5f fm).
#pragma once
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include "../DirectVulkanResourceState.h"
// P7 wave 2 package C, OQ-8: spirv_reflect.h is GONE from this header, and its absence is the
// gate. Everything this class needed it for - the storage-block index space - now travels in
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
// A borrowed view, valid only while its frontend object or server record lives.
// It never constructs a ProgramObject on the server. Post-link mutable values
// come from the record tails, not from the archive's link-time defaults.
class MagmaProgramSource {
public:
using Program = MG_State::GLState::ProgramObject;
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
: m_handle(handle), m_record(&record) {
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
}
Bool IsWire() const { return m_record != nullptr; }
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
const Program* Frontend() const { return m_frontend; }
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
Uint64 GetLifetimeId() const {
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
}
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
Bool GetSpirvValidationEnabled() const {
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
}
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
}
Vector<ShaderStage> GetLinkedShaderStages() const {
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
Vector<ShaderStage> result;
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
return result;
}
Bool HasLinkedShaderStage(ShaderStage stage) const {
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
const auto& stages = m_record->Archive->LinkedStages;
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
}
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
// A view has no lifetime in which a memo can safely persist. The server
// factory's content cache still owns compiled programs; hash from current
// archive + binding tails rather than borrowing the client's mutable memo.
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
}
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
Int GetActiveUniformBlocksCount() const {
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
}
Uint GetUniformBlockIndex(const char* name) const {
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
auto found = Link().uniformBlockIndexByName.find(name);
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
}
Uint GetUniformBlockBinding(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
}
const String& GetUniformBlockName(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
const auto* block = Block(index);
static const String empty;
return block ? block->name : empty;
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
const auto* block = Block(index);
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
}
Bool IsValidUniformLocation(Int location) const {
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
}
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
}
GLenum GetUniformType(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformType(location);
return IsValidUniformLocation(static_cast<Int>(location))
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
}
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
const auto& units = m_record->SamplerUnits;
const auto found = std::lower_bound(units.begin(), units.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
return found != units.end() && found->Location == location ? found->Unit : -1;
}
Int GetUniformLocation(const String& name) const {
if (!IsWire()) return m_frontend->GetUniformLocation(name);
const auto& locations = Link().uniformLocations;
auto found = locations.find(name);
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.empty()) return -1;
found = locations.find(name + "[0]");
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.back() != ']' || name.size() < 4) return -1;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.size() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0fffffffu) return -1;
}
found = locations.find(name.substr(0, bracket) + "[0]");
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
if (found == locations.end()) return -1;
const Int base = static_cast<Int>(found->second);
if (!IsValidUniformLocation(base)) return -1;
const Int tIndex = Link().uniformIndexInTProgram[base];
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
const Int location = base + static_cast<Int>(element);
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
}
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
}
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
Uint32 GetTransformFeedbackStride(Uint index) const {
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
}
GLenum GetTransformFeedbackBufferMode() const {
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
}
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
//
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
// function of the linked program, so it is now built once at link time and travels in
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
Uint GetShaderStorageBlockIndex(const String& name) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
const auto& blocks = Link().storageBlocks;
// The archive carries NORMALISED names (the array subscript stripped), because
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
// ProgramFactory::ReflectLayout looks up. A caller that still holds a GL-style
// "Foo[2]" gets the same second chance it always had.
const auto find = [&blocks](const String& key) {
return std::find_if(blocks.begin(), blocks.end(),
[&](const MG_State::GLState::StorageBlockReflection& block) {
return block.name == key;
});
};
auto found = find(name);
if (found == blocks.end() && !name.empty() && name.back() == ']') {
const auto bracket = name.rfind('[');
if (bracket != String::npos) found = find(name.substr(0, bracket));
}
return found == blocks.end() ? GL_INVALID_INDEX
: static_cast<Uint>(found - blocks.begin());
}
Uint GetShaderStorageBlockBinding(Uint index) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
const auto& blocks = Link().storageBlocks;
if (index >= blocks.size()) return 0;
const auto& block = blocks[index];
// THE OVERRIDE STILL WINS, and it still comes off the RECORD rather than the
// archive: glShaderStorageBlockBinding is a post-link mutable and the archive
// carries link-time defaults only (rule on MagmaProgramSource, and the reason
// StorageBlockReflection::binding is documented as the DECLARED binding).
for (const auto& override : m_record->StorageOverrides) {
if (override.Name == block.name) return override.Binding;
}
return block.binding;
}
private:
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
const MG_State::GLState::BlockReflection* Block(Uint index) const {
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
const Int tIndex = Link().glBlockIndexToTProgram[index];
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
? &Link().blockReflection[tIndex] : nullptr;
}
const Program* m_frontend = nullptr;
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
// were `mutable` precisely because this class is a borrowed const view - and that is
// also why they never helped: a view lives for one draw, so the latch was cold every
// time it was read.
};
#else
using MagmaProgramSource = MG_State::GLState::ProgramObject;
#endif
}
@@ -1,37 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h
// 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
// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the
// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled
// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client
// surface, or the placeholder a suspended server window draws into) must take the AImageReader
// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins
// it without a driver.
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class OffscreenSurfaceRoute : Uint8 {
Headless, // vkCreateHeadlessSurfaceEXT
AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays
Unavailable, // neither: the target cannot be built (the caller refuses, by name)
};
// `headlessEnabled`: the instance was created with VK_EXT_headless_surface.
// `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT.
// `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface.
constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint,
Bool androidSurfaceEnabled) {
if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless;
if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader;
return OffscreenSurfaceRoute::Unavailable;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,8 +8,6 @@
#include "PipelineFactory.h"
#include <algorithm>
namespace MobileGL::MG_Backend::DirectVulkan {
static const char* PrimitiveTopologyToString(VkPrimitiveTopology topology) {
switch (topology) {
@@ -110,81 +108,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"vkCreatePipelineCache");
}
// Must be called once, before any pipeline is created: the flag is not part of the
// pipeline hash, so flipping it mid-life would serve cached pipelines built under the
// old value.
void PipelineFactory::SetSuppressBlendedDepthWrite(Bool enabled) {
s_suppressBlendedDepthWrite = enabled;
}
Bool PipelineFactory::ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId) {
static constexpr Uint32 kVendorIdQualcomm = 0x5143;
switch (quirkOverride) {
case MG_Config::QuirkOverride::ForceOn:
return true;
case MG_Config::QuirkOverride::ForceOff:
return false;
case MG_Config::QuirkOverride::Auto:
default:
return vendorId == kVendorIdQualcomm;
}
}
namespace {
// MIN/MAX extremum blending: the signature of a depth-bounds accumulation pass
// (MC 26.3 OIT writes vec4(-linD, linD, deviceZ, 0) under GL_MAX while writing
// depth for its equality chain). MIN/MAX ignore blend factors per the Vulkan spec.
//
// Deliberately the ONLY shape stripped. A quirk should touch as little unrelated
// content as possible, and a trace sweep of every fixture showed the wider
// alternatives all cost more than they fix:
// - additive ONE+ONE with a depth write matched zero draws of the 26.3 chain
// (its transmittance/accumulate passes disable depth writes themselves) - the
// only real content it caught was harmless additive glow effects (Create);
// - sorted-transparency "over" blends (SRC_ALPHA-style) are order-dependent,
// drawn once per surface, and rely on their depth writes for occlusion;
// - separate-alpha accumulation over an over-blending color channel has no
// known pairing with a depth-equality chain (color channel only, see tests).
// If a future workload pairs another blend shape with an equality chain, widen
// this with that evidence in hand rather than pre-emptively.
Bool IsAccumulationBlend(const VkPipelineColorBlendAttachmentState& attachment) {
return attachment.colorBlendOp == VK_BLEND_OP_MIN ||
attachment.colorBlendOp == VK_BLEND_OP_MAX;
}
} // namespace
Bool PipelineFactory::ShouldSuppressDepthWrite(const PipelineCreatePayload& payload) {
if (!payload.depthWriteEnable) {
return false;
}
// A shader that assigns gl_FragDepth supplies depth itself rather than taking the
// pipeline's interpolated Z, so a driver that varies the vertex position math
// between pipelines cannot desynchronize it. (A gl_FragDepth = gl_FragCoord.z
// passthrough is the exception that stays exposed; no known content pairs one with
// an equality chain, and 26.3's composite is a genuine computed-depth writer.)
if (payload.fragmentReplacesDepth) {
return false;
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const VkPipelineColorBlendAttachmentState& attachment = payload.colorBlendAttachments[i];
if (attachment.blendEnable != VK_TRUE) {
continue;
}
// All color writes masked: blending is moot (depth-prepass pattern that left
// GL_BLEND enabled); stripping the depth write would delete the whole prepass.
if (attachment.colorWriteMask == 0) {
continue;
}
// Any attachment qualifies, not just attachment 0: the 26.3 transmittance pass
// accumulates into a 2-target MRT and must stay stripped.
if (IsAccumulationBlend(attachment)) {
return true;
}
}
return false;
}
PipelineFactory::~PipelineFactory() {
DestroyAll();
if (m_pipelineCache != VK_NULL_HANDLE) {
@@ -198,35 +121,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
#if MOBILEGL_BUILD_DISAGGREGATED
if (payload.wireRenderPassCompatibilityId != 0) {
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
sizeof(payload.wireRenderPassCompatibilityId)));
} else
#endif
{
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
}
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
sizeof(payload.passthroughTessControlKey)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.provokingVertexMode, sizeof(payload.provokingVertexMode)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthTestEnable, sizeof(payload.depthTestEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthWriteEnable, sizeof(payload.depthWriteEnable)));
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.depthBiasEnable, sizeof(payload.depthBiasEnable)));
@@ -248,8 +152,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
XXH64_update(m_hashState, &payload.backStencilDepthFailOp, sizeof(payload.backStencilDepthFailOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.backStencilCompareOp, sizeof(payload.backStencilCompareOp)));
XXHASH_VERIFY(
XXH64_update(m_hashState, &payload.fragmentReplacesDepth, sizeof(payload.fragmentReplacesDepth)));
if (payload.colorAttachmentCount > 0) {
XXHASH_VERIFY(XXH64_update(
m_hashState,
@@ -263,143 +165,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const HashType hash = ComputeHash(payload);
auto it = m_cache.find(hash);
if (it != m_cache.end()) {
it->second.lastUsedFrame = m_frameCounter;
return it->second.pipeline;
return it->second;
}
VkPipeline pipeline = CreatePipeline(payload);
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
// the rest of the process, so one transient driver rejection turned every later draw with
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
// is the correct price for a broken pipeline and is bounded by the draw itself being
// skipped.
if (pipeline == VK_NULL_HANDLE) {
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
// assembled and the driver refused is a broken invariant, not an expected failure,
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
// Log.h ordering fix made MGLOG_E live in INFO builds.
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
"programHash=0x%llx; not caching the failure",
static_cast<unsigned long long>(hash),
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
#if MOBILEGL_BUILD_DISAGGREGATED
// Wire pipelines outlive their creator pass and
// may serve another compatible pass in flight.
// Never evict them by a recycled native handle.
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
#endif
payload.renderPass,
m_frameCounter});
m_cache.emplace(hash, pipeline);
return pipeline;
}
void PipelineFactory::DestroyAll() {
for (auto& pair : m_cache) {
if (pair.second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second.pipeline, nullptr);
if (pair.second != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, pair.second, nullptr);
}
}
m_cache.clear();
}
Uint32 PipelineFactory::OnFrameBoundary() {
++m_frameCounter;
// Sweep cadence and retire age mirror VkRenderPassManager::OnPresent: an entry
// idle for more than kRetireAgeFrames frame boundaries cannot be referenced by
// any in-flight command buffer (frames-in-flight <= MOBILEGL_MAGMA_FRAMESINFLIGHT),
// so immediate vkDestroyPipeline is safe. The caller must drop its "last
// pipeline" memo when this returns non-zero: the memo can return a cached
// handle without touching this cache, so an evicted pipeline may still be
// memoized (present-less flush loops never reset the memo per frame).
constexpr Uint64 kSweepInterval = 256;
constexpr Uint64 kRetireAgeFrames = 1024;
if ((m_frameCounter % kSweepInterval) != 0) {
return 0;
}
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::OnFrameBoundary: evicted %u idle pipelines (%zu remain)", evicted,
m_cache.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses) {
if (renderPasses.empty() || m_cache.empty()) {
return 0;
}
// Sorted-batch membership test keeps a mass eviction (shader-pack switch,
// dimension exit) at one O(cache * log batch) scan instead of one full scan
// per dying pass.
Vector<VkRenderPass> sortedPasses = renderPasses;
std::sort(sortedPasses.begin(), sortedPasses.end());
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (std::binary_search(sortedPasses.begin(), sortedPasses.end(), it->second.renderPass)) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByRenderPasses: evicted %u pipelines for %zu destroyed render passes",
evicted, sortedPasses.size());
}
return evicted;
}
Uint32 PipelineFactory::EvictByProgramHash(HashType programHash) {
Uint32 evicted = 0;
for (auto it = m_cache.begin(); it != m_cache.end();) {
if (it->second.programHash == programHash) {
if (it->second.pipeline != VK_NULL_HANDLE) {
vkDestroyPipeline(m_device, it->second.pipeline, nullptr);
}
it = m_cache.erase(it);
++evicted;
} else {
++it;
}
}
if (evicted > 0) {
MGLOG_D("PipelineFactory::EvictByProgramHash: evicted %u pipelines for program hash 0x%llx",
evicted, static_cast<unsigned long long>(programHash));
}
return evicted;
}
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
// The assert compiles out of shipping builds, and a program declined for an invalid
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
if (payload.stages == nullptr || payload.stages->empty()) {
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
"(programHash=0x%llx); its draws are skipped",
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
@@ -432,18 +216,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
ia.topology = payload.topology;
ia.primitiveRestartEnable = payload.primitiveRestartEnable ? VK_TRUE : VK_FALSE;
// Only a patch topology has a tessellation stage to configure; leaving the pointer null
// otherwise is what the spec expects.
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
tessellation.patchControlPoints = payload.patchControlPoints;
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
// requires viewportCount == scissorCount whenever both are dynamic
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
// clamped this to the device's multiViewport capability.
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
vpci.scissorCount = vpci.viewportCount;
vpci.viewportCount = 1;
vpci.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
raster.polygonMode = payload.polygonMode;
@@ -452,28 +227,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
raster.depthBiasEnable = payload.depthBiasEnable ? VK_TRUE : VK_FALSE;
raster.rasterizerDiscardEnable = payload.rasterizerDiscardEnable ? VK_TRUE : VK_FALSE;
raster.lineWidth = 1.0f;
// Only chain the struct when the mode is not Vulkan's implicit default: a device without
// VK_EXT_provoking_vertex enabled must never see this pNext entry, and the renderer's
// selector already collapses to FIRST in exactly that case - so a device without the
// extension produces a byte-identical VkGraphicsPipelineCreateInfo to before.
VkPipelineRasterizationProvokingVertexStateCreateInfoEXT provokingVertexState{
VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_PROVOKING_VERTEX_STATE_CREATE_INFO_EXT};
if (payload.provokingVertexMode != VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
provokingVertexState.provokingVertexMode = payload.provokingVertexMode;
provokingVertexState.pNext = raster.pNext;
raster.pNext = &provokingVertexState;
}
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
ms.rasterizationSamples = payload.rasterizationSamples;
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
// struct's bytes match the hash the payload was keyed by.
ms.minSampleShading = payload.minSampleShading;
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
ms.pSampleMask = payload.sampleMask;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
@@ -502,77 +258,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
colorAttachments[i] = payload.colorBlendAttachments[i];
}
// Suppress depth writes on accumulation-blended pipelines when the active driver
// cannot keep vertex positions invariant across the pipelines of a multi-pass
// depth-equality chain (see SetSuppressBlendedDepthWrite). The decision is narrowed
// in ShouldSuppressDepthWrite: sorted-transparency "over" blends (vanilla MC water),
// gl_FragDepth writers, and masked-out attachments keep their depth writes.
// This bakes the decision into the pipeline, which only works because depth write is
// static state here - adding VK_DYNAMIC_STATE_DEPTH_WRITE_ENABLE to kDynamicStates
// would let the record-time value override it and silently disable the quirk.
if (s_suppressBlendedDepthWrite && ShouldSuppressDepthWrite(payload)) {
depthStencil.depthWriteEnable = VK_FALSE;
}
VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO};
blend.logicOpEnable = payload.logicOpEnable ? VK_TRUE : VK_FALSE;
blend.logicOp = payload.logicOp;
blend.attachmentCount = payload.colorAttachmentCount;
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
//
// The refusal below is what keeps the half-tessellated shape away from the driver when
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
// takes: the draw is skipped, nothing is memoised, and the process survives.
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
stagesWithPassthrough = *payload.stages;
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
effectiveStages = &stagesWithPassthrough;
}
{
VkShaderStageFlags stagesPresent = 0;
for (const auto& stageInfo : *effectiveStages) {
stagesPresent |= stageInfo.stage;
}
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
if (hasTessControl != hasTessEval) {
// Latched, and the latch is the point: a failed creation is deliberately never
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
// noise, not a diagnostic. One line names the program; the draws it explains are
// all the same draw.
static Bool s_warnedHalfTessellatedPipeline = false;
if (!s_warnedHalfTessellatedPipeline) {
s_warnedHalfTessellatedPipeline = true;
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
"patchControlPoints=%u. Its draws are skipped; logged once.",
hasTessEval ? "an evaluation" : "a control",
hasTessEval ? "control" : "evaluation",
static_cast<unsigned long long>(payload.programHash),
payload.patchControlPoints);
}
return VK_NULL_HANDLE;
}
}
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
gpi.pStages = effectiveStages->data();
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
gpi.pStages = payload.stages->data();
gpi.pVertexInputState = payload.vertexInputState;
gpi.pInputAssemblyState = &ia;
gpi.pTessellationState =
payload.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST ? &tessellation : nullptr;
gpi.pViewportState = &vpci;
gpi.pRasterizationState = &raster;
gpi.pMultisampleState = &ms;
@@ -585,13 +281,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline pipeline = VK_NULL_HANDLE;
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
// does not apply: while this is reachable it should keep saying so on every draw.
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
if (result != VK_SUCCESS) {
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
VkResultToString(result),
@@ -622,36 +311,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
payload.vertexInputState->vertexBindingDescriptionCount,
payload.vertexInputState->vertexAttributeDescriptionCount);
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
// should-never-happen report and must survive in the INFO-level builds that CTS
// actually runs against, alongside the MGLOG_F lines above.
if (payload.stageSpirvDigests) {
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
const auto& digest = (*payload.stageSpirvDigests)[i];
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
"hash=0x%llx",
i, digest.stage, digest.wordCount,
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
static_cast<unsigned long long>(digest.hash));
}
} else {
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
}
if (payload.stages) {
for (SizeT i = 0; i < payload.stages->size(); ++i) {
const auto& stage = (*payload.stages)[i];
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
"specialization=%d",
i, static_cast<Uint32>(stage.stage),
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
}
}
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
const auto& attachment = payload.colorBlendAttachments[i];
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
@@ -14,16 +14,6 @@
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
// of any pipeline or program hash.
struct ShaderStageSpirvDigest {
Uint32 stage = 0; // VkShaderStageFlagBits
Uint32 wordCount = 0;
Uint64 hash = 0;
};
class PipelineFactory {
public:
using HashType = Uint64;
@@ -35,62 +25,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
HashType vertexInputHash = 0;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkRenderPass renderPass = VK_NULL_HANDLE;
#if MOBILEGL_BUILD_DISAGGREGATED
// Nonzero is an exact, renderer-lifetime compatibility identity.
// renderPass remains the valid creation handle, not the cache key.
Uint64 wireRenderPassCompatibilityId = 0;
#endif
Uint32 colorAttachmentCount = 1;
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
// identity and both are hashed. The renderer leaves the enable false unless the
// device's sampleRateShading feature was enabled
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
Bool sampleShadingEnable = false;
Float minSampleShading = 0.0f;
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
// pSampleMask always did.
//
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
// single Uint32 here let such a pipeline read one word past the member (the next
// struct field). The second word is all-ones: full coverage for samples 32..63, which
// is the only honest answer when GL has no state describing them.
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
Uint32 subpass = 0;
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
Bool primitiveRestartEnable = false;
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
Uint32 patchControlPoints = 3;
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
// the levels glPatchParameterfv set are compiled INTO that module and are not a
// function of the program or of patchControlPoints - see the note on
// passthroughTessControlStage.
Uint64 passthroughTessControlKey = 0;
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
// every program that never assigns gl_ViewportIndex, which is all of them outside the
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
Uint32 viewportCount = 1;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
// GL's provoking vertex, baked into the pipeline (VK_EXT_provoking_vertex). It selects
// which vertex a flat varying takes AND the vertex order transform feedback records for
// strips/fans, so it is part of the pipeline's identity, not dynamic state. Defaults to
// Vulkan's own convention, which is what a device without the extension gets.
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
Bool depthTestEnable = false;
Bool depthWriteEnable = false;
Bool depthBiasEnable = false;
@@ -107,26 +49,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkStencilOp backStencilPassOp = VK_STENCIL_OP_KEEP;
VkStencilOp backStencilDepthFailOp = VK_STENCIL_OP_KEEP;
VkCompareOp backStencilCompareOp = VK_COMPARE_OP_ALWAYS;
// The fragment module writes gl_FragDepth (SPIR-V DepthReplacing); exempts the
// pipeline from the blended depth-write quirk (see ShouldSuppressDepthWrite).
Bool fragmentReplacesDepth = false;
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
// The tessellation control stage this renderer synthesized for a program that has
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
// fixed-function pass-through; Vulkan has no such thing and
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
// pipeline outright). Appended to `stages` at creation. A null module means the
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
// refusal it applies when `stages` itself is half-tessellated.
//
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
// of the default tessellation levels - the first two of which ComputeHash already
// mixes in, and the third of which arrives through passthroughTessControlKey above.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
};
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
@@ -137,69 +62,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload);
void DestroyAll();
// Frame boundary hook: ages the pipeline cache and destroys long-unused entries
// (their command buffers retired many frames ago), mirroring
// VkRenderPassManager::OnPresent's sweep. Returns the number of pipelines
// destroyed so the caller can drop any memoized VkPipeline handle.
Uint32 OnFrameBoundary();
// Destroys every cached pipeline hashed on one of `renderPasses`. Only safe
// when the caller guarantees GPU idleness for them - the render-pass manager
// calls this (via the renderer) for passes its own >1024-boundary-idle sweep
// just evicted, and a pipeline hashed on those handles is only ever bound by
// draws that also hit the render-pass entries. Also closes the handle-recycling
// hazard: a recycled VkRenderPass value must never serve a stale pipeline.
// Batched: one cache scan regardless of how many passes died in the sweep.
// Returns the number destroyed (callers invalidate memos when non-zero).
Uint32 EvictByRenderPasses(const Vector<VkRenderPass>& renderPasses);
// Destroys every cached pipeline built from the program with content hash
// `programHash`. Called from the ProgramFactory eviction path, which proves the
// same >1024-boundary idleness (the program's pipelines are only bound by draws
// that stamp its factory entry). Returns the number destroyed.
Uint32 EvictByProgramHash(HashType programHash);
// Driver quirk: suppress depth writes on accumulation-blended pipelines. Multi-pass
// depth-equality rendering (a blended prepass writes depth that later passes re-test
// with an equality-inclusive compare on the re-rasterized geometry) requires
// cross-pipeline position invariance that some mobile compilers do not provide, even
// with the SPIR-V Invariant decoration; whole primitives then drop out of the later
// passes. Only MIN/MAX extremum blends are stripped - the signature of such a
// chain's depth-bounds pass (MC 26.3 OIT), and per a fixture-wide trace sweep the
// only depth-writing shape the chain actually uses - so every other blend
// (sorted-transparency "over" like vanilla MC water, additive glows, ...) keeps
// its depth writes. Set at renderer initialization based on the active driver.
static void SetSuppressBlendedDepthWrite(Bool enabled);
static Bool IsSuppressBlendedDepthWriteEnabled() { return s_suppressBlendedDepthWrite; }
// Device gate for the quirk: ForceOn/ForceOff bypass detection, Auto enables it on
// the known-affected vendor (Qualcomm).
static Bool ShouldSuppressBlendedDepthWriteForDevice(MG_Config::QuirkOverride quirkOverride,
Uint32 vendorId);
// Pure per-pipeline strip decision (exempts gl_FragDepth writers, masked-out and
// non-accumulation blends); combined with the device flag in CreatePipeline. Static
// and payload-only so tests can pin the contract without a VkDevice.
static Bool ShouldSuppressDepthWrite(const PipelineCreatePayload& payload);
private:
struct PipelineCacheEntry {
VkPipeline pipeline = VK_NULL_HANDLE;
// The hashed inputs the eviction paths key on: programHash ties the entry to
// its ProgramFactory entry, renderPass records the exact handle the hash
// folded in (the hash is one-way, so targeted eviction needs them verbatim).
HashType programHash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
// Frame-boundary counter value of the last GetOrCreatePipeline hit; drives
// cache eviction (see OnFrameBoundary).
Uint64 lastUsedFrame = 0;
};
VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const;
VkDevice m_device = VK_NULL_HANDLE;
const VulkanRendererConfig& m_config;
VkPipelineCache m_pipelineCache = VK_NULL_HANDLE;
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
UnorderedMap<HashType, VkPipeline> m_cache;
static inline XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More