mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 18:13:23 +09:00
Compare commits
169
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5331106e90 | ||
|
|
ca981a8fc1 | ||
|
|
c90383db16 | ||
|
|
698f04574b | ||
|
|
f497ff9741 | ||
|
|
946e63d810 | ||
|
|
918d40dd4f | ||
|
|
f5b8fe48aa | ||
|
|
c5d25868cf | ||
|
|
c464947bdd | ||
|
|
4fb75f4e00 | ||
|
|
4d6904184d | ||
|
|
2f1a673513 | ||
|
|
88bc35b711 | ||
|
|
880be57bb6 | ||
|
|
70e4691b3a | ||
|
|
2d42414667 | ||
|
|
32d27b1491 | ||
|
|
78c5f8dae7 | ||
|
|
96bd540ba1 | ||
|
|
710beefd02 | ||
|
|
c1e957cb98 | ||
|
|
eb71b669b3 | ||
|
|
bb31f374a3 | ||
|
|
e5ee31b022 | ||
|
|
915882b293 | ||
|
|
963f4d8177 | ||
|
|
d627fca387 | ||
|
|
2de0d6dd25 | ||
|
|
66b47a101b | ||
|
|
4722fa38bb | ||
|
|
3a289222af | ||
|
|
18a5dba0e4 | ||
|
|
7218bacb49 | ||
|
|
4633ca223c | ||
|
|
0ef13ab0a6 | ||
|
|
287e1df8dc | ||
|
|
4f6f4125ac | ||
|
|
2a1889b817 | ||
|
|
07e7730085 | ||
|
|
03648c2783 | ||
|
|
f22ae625cb | ||
|
|
f52a546707 | ||
|
|
77c604061e | ||
|
|
36190b7cc0 | ||
|
|
cf763b4081 | ||
|
|
2acc6c41bb | ||
|
|
a84104a40f | ||
|
|
1dd888cb21 | ||
|
|
393002a46e | ||
|
|
25a76c1b20 | ||
|
|
a9f22ebb63 | ||
|
|
2fe011adf6 | ||
|
|
11668b02f2 | ||
|
|
30977c181f | ||
|
|
e5f097f9e1 | ||
|
|
90e63c9e2a | ||
|
|
af2b9f4c1c | ||
|
|
cd58f048b6 | ||
|
|
b0dad54cf1 | ||
|
|
95aa618d22 | ||
|
|
5de6ecd4e1 | ||
|
|
7fd965ceda | ||
|
|
9e7305b885 | ||
|
|
bd4c5b5770 | ||
|
|
198d6feaf3 | ||
|
|
08f23543aa | ||
|
|
316ffe02d0 | ||
|
|
fdf9252b53 | ||
|
|
8024b645cb | ||
|
|
9f30fc500f | ||
|
|
e3120af6a9 | ||
|
|
c60fcc17c5 | ||
|
|
b06c2a9bf2 | ||
|
|
8b5295401f | ||
|
|
22266c47cf | ||
|
|
4a4e53d9a9 | ||
|
|
b7cc56dd44 | ||
|
|
7823bcd987 | ||
|
|
caa00995e7 | ||
|
|
1ff7ca5331 | ||
|
|
6125f941d1 | ||
|
|
bfa7987ca6 | ||
|
|
b1405e17ad | ||
|
|
b64c2de3c8 | ||
|
|
2b95a9697a | ||
|
|
228bd24c67 | ||
|
|
c822a9fef7 | ||
|
|
815217cc07 | ||
|
|
7a5b0755b3 | ||
|
|
3d69c37504 | ||
|
|
4f679d669d | ||
|
|
4095624f87 | ||
|
|
321e61ca0b | ||
|
|
552dd3d39b | ||
|
|
6c742eba8a | ||
|
|
69e96f5b8d | ||
|
|
d24abe9287 | ||
|
|
d6d75c9951 | ||
|
|
bb4e3e0ec6 | ||
|
|
233e2f8ced | ||
|
|
74919a3bc1 | ||
|
|
7090e31dee | ||
|
|
5accb0985c | ||
|
|
58685bc116 | ||
|
|
34090097a1 | ||
|
|
58c3d9bde5 | ||
|
|
d03fe4c9dd | ||
|
|
239fc228fc | ||
|
|
8da082f805 | ||
|
|
258b946086 | ||
|
|
16b07cf4b5 | ||
|
|
51c37bcbc9 | ||
|
|
5a7b23f661 | ||
|
|
15f9b27a42 | ||
|
|
82d96a63c9 | ||
|
|
7520e53b5e | ||
|
|
b5cab44100 | ||
|
|
672261764e | ||
|
|
0e314f645e | ||
|
|
350fc699a1 | ||
|
|
3d17e8c10b | ||
|
|
38842985f8 | ||
|
|
e06aabe812 | ||
|
|
67a97065b8 | ||
|
|
ec084959fa | ||
|
|
4b388950e3 | ||
|
|
7e76a98d36 | ||
|
|
15dbe78ce6 | ||
|
|
168808d1a7 | ||
|
|
75e1da4d1b | ||
|
|
cd610934d7 | ||
|
|
335637af38 | ||
|
|
4236b42a57 | ||
|
|
0b6842ac8b | ||
|
|
47db68d557 | ||
|
|
ce11c9accd | ||
|
|
7bb9d9829f | ||
|
|
ccf8eb6571 | ||
|
|
0b314ddae7 | ||
|
|
d5acece005 | ||
|
|
30fef23d52 | ||
|
|
cbb87b968a | ||
|
|
d554c87c45 | ||
|
|
7a48521d37 | ||
|
|
6946ff3a5c | ||
|
|
5a0f13fdfa | ||
|
|
ff2120616d | ||
|
|
5c6399e240 | ||
|
|
4ed1c9cef2 | ||
|
|
ebc552a048 | ||
|
|
6fb880cdf2 | ||
|
|
d52c031c50 | ||
|
|
4156ca6e36 | ||
|
|
2f64ede8c6 | ||
|
|
a5b463a5e8 | ||
|
|
32c79f4135 | ||
|
|
f458924360 | ||
|
|
0dad3fcaa1 | ||
|
|
75f83b20fc | ||
|
|
57781688ce | ||
|
|
7308668488 | ||
|
|
134791b4b9 | ||
|
|
c24926cafe | ||
|
|
c5fa722be7 | ||
|
|
b7aa8d2011 | ||
|
|
378020adf3 | ||
|
|
028cf4eb62 | ||
|
|
08124c99f1 |
@@ -0,0 +1,3 @@
|
||||
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
|
||||
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
|
||||
* text=auto eol=lf
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
name: anland-mobilegl-plasma
|
||||
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
|
||||
---
|
||||
|
||||
# MobileGL on anland KDE Plasma
|
||||
|
||||
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
|
||||
Human-readable runbook (Chinese, same content): `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.
|
||||
@@ -0,0 +1,183 @@
|
||||
# Reproducing the MobileGL + anland + KDE Plasma setup from source
|
||||
|
||||
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
|
||||
day-to-day loop once everything below exists. Paths and names that are specific to the original
|
||||
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
|
||||
|
||||
## 0. What you need
|
||||
|
||||
**Phone.**
|
||||
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
|
||||
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
|
||||
- About 20 GB free for the container image.
|
||||
|
||||
**Development host.**
|
||||
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
|
||||
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
|
||||
- Tools:
|
||||
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
|
||||
- Android NDK **27.2.12479018**.
|
||||
- JDK 17 and Android SDK platform 36 (for the APK).
|
||||
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
|
||||
|
||||
## 1. Sources
|
||||
|
||||
```sh
|
||||
# MobileGL: branch feat/disaggregated (everything here is on it)
|
||||
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
|
||||
git checkout feat/disaggregated
|
||||
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
|
||||
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
|
||||
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
|
||||
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
|
||||
|
||||
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
|
||||
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
|
||||
git checkout legacy-mobilegl-unified
|
||||
```
|
||||
|
||||
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
|
||||
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
|
||||
|
||||
## 2. Phone base: root, Droidspaces, display daemon
|
||||
|
||||
1. **Root** with KernelSU (or another `su` provider).
|
||||
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
|
||||
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
|
||||
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
|
||||
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
|
||||
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
|
||||
|
||||
## 3. The container
|
||||
|
||||
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
|
||||
|
||||
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
|
||||
```
|
||||
enable_gpu_mode=1
|
||||
net_mode=host
|
||||
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
|
||||
run_at_boot=0
|
||||
```
|
||||
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
|
||||
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
|
||||
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
|
||||
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
|
||||
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
|
||||
|
||||
## 4. MobileGL server and the anland APK
|
||||
|
||||
```sh
|
||||
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
|
||||
```
|
||||
The script runs, with the NDK toolchain:
|
||||
```
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
|
||||
```
|
||||
|
||||
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
|
||||
```sh
|
||||
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
|
||||
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
|
||||
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
|
||||
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
|
||||
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
|
||||
```
|
||||
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
|
||||
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
|
||||
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
|
||||
|
||||
## 5. MobileGL client in the container
|
||||
|
||||
Pick one of these two:
|
||||
- **Cross build** (fast, from WSL or Linux):
|
||||
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
|
||||
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
|
||||
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
|
||||
|
||||
Either route installs the same files, renamed into place so running clients keep their old mapping:
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
|
||||
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
|
||||
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
|
||||
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
|
||||
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
|
||||
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
|
||||
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
|
||||
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
|
||||
|
||||
## 6. KWin 6.7.4 with the anland backend, and Xwayland
|
||||
|
||||
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
|
||||
|
||||
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
|
||||
2. **Configure:**
|
||||
```sh
|
||||
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
|
||||
```
|
||||
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
|
||||
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
|
||||
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
|
||||
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
|
||||
```ini
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
|
||||
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
|
||||
Environment=QT_LOGGING_RULES=kwin_*.info=true
|
||||
```
|
||||
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
|
||||
|
||||
## 7. Bring it up
|
||||
|
||||
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
|
||||
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
|
||||
That is the single trigger. The app starts its foreground service and runs its bundled
|
||||
`mobilegl-desktop.sh up` as root, which:
|
||||
1. starts the display daemon if it is not running;
|
||||
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
|
||||
`/data/local/tmp/anland-mobilegl/backend`;
|
||||
3. starts the container if it is stopped;
|
||||
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
|
||||
`/etc/mobilegl/backend` and runs Plasma.
|
||||
|
||||
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
|
||||
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
|
||||
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
|
||||
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
|
||||
(the notification action) stops the session and the container.
|
||||
|
||||
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
|
||||
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
|
||||
that, open the app as above. See SKILL.md "Starting, idling, stopping".
|
||||
|
||||
## 8. Verify
|
||||
|
||||
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
|
||||
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
|
||||
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
|
||||
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
|
||||
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
|
||||
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
|
||||
|
||||
## 9. Machine-specific values to change
|
||||
|
||||
| Value | Original | Where |
|
||||
|---|---|---|
|
||||
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
|
||||
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
|
||||
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
|
||||
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
|
||||
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
|
||||
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
|
||||
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
|
||||
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
|
||||
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
|
||||
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
|
||||
|
||||
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/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"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
|
||||
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
|
||||
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
|
||||
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
|
||||
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
|
||||
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
|
||||
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
|
||||
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
|
||||
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
|
||||
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
|
||||
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
|
||||
S=$1; shift
|
||||
DU=${DESKTOP_USER:-swung0x48}
|
||||
U=$(id -u "$DU")
|
||||
cd /tmp
|
||||
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
|
||||
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
|
||||
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
|
||||
bash -lc 'exec "$@"' mgrun "$@"
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/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'"
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,7 @@
|
||||
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
|
||||
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
|
||||
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
|
||||
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
|
||||
input keyevent KEYCODE_WAKEUP
|
||||
wm dismiss-keyguard
|
||||
sh "$(dirname "$0")/desktop-timeline.sh" 300
|
||||
@@ -0,0 +1,17 @@
|
||||
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
|
||||
# reboot: the app force-stopped (its foreground service and server with it), the container
|
||||
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
|
||||
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
am force-stop $P
|
||||
$D --name=$C stop > /dev/null 2>&1
|
||||
for p in $(pidof display_daemon); do
|
||||
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
|
||||
done
|
||||
rm -f $SOCK
|
||||
sleep 2
|
||||
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
|
||||
$D show
|
||||
@@ -0,0 +1,30 @@
|
||||
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
|
||||
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
|
||||
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
|
||||
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
|
||||
# Pieces that already run show 0.0. Read-only apart from the am start.
|
||||
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
|
||||
T=${1:-180}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
now() { awk '{print $1}' /proc/uptime; }
|
||||
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
|
||||
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
|
||||
t0=$(now)
|
||||
am start -n $P/com.anland.consumer.MainActivity > /dev/null
|
||||
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
|
||||
while :; do
|
||||
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
|
||||
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
|
||||
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
|
||||
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
|
||||
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
|
||||
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
|
||||
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
|
||||
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
|
||||
fi
|
||||
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
|
||||
sleep 0.3
|
||||
done
|
||||
@@ -0,0 +1,47 @@
|
||||
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
|
||||
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
|
||||
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
|
||||
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
|
||||
# /proc (Droidspaces only gives them their own pid namespace).
|
||||
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
|
||||
L=${1:-sample}
|
||||
N=${2:-20}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
|
||||
|
||||
pids_named() { # exact process names (comm or argv0 basename)
|
||||
for n in "$@"; do pidof "$n"; done
|
||||
}
|
||||
pids_matching() { # argv contains the word
|
||||
for d in /proc/[0-9]*; do
|
||||
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
|
||||
done
|
||||
}
|
||||
ticks() { # utime+stime of the pids, in clock ticks
|
||||
t=0
|
||||
for p in "$@"; do
|
||||
[ -r /proc/$p/stat ] || continue
|
||||
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
|
||||
t=$((t + v))
|
||||
done
|
||||
echo $t
|
||||
}
|
||||
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
|
||||
|
||||
KW="$(pids_named kwin_wayland Xwayland)"
|
||||
SH="$(pids_named plasmashell)"
|
||||
CH="$(pids_matching /opt/google/chrome/)"
|
||||
SV="$(pidof $P:mobilegl)"
|
||||
UI="$(pidof $P)"
|
||||
|
||||
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
|
||||
g=0; i=0
|
||||
while [ $i -lt $N ]; do
|
||||
sleep 1
|
||||
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
|
||||
done
|
||||
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
|
||||
# CLK_TCK is 100: ticks per second == percent of one core.
|
||||
pct() { echo $(( ($2 - $1) / N )); }
|
||||
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
|
||||
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
|
||||
@@ -0,0 +1,20 @@
|
||||
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
|
||||
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
|
||||
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
|
||||
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
|
||||
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
|
||||
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
|
||||
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
|
||||
DIR=$(dirname $SOCK)
|
||||
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
|
||||
sleep 1
|
||||
rm -f $SOCK
|
||||
mkdir -p $DIR
|
||||
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
|
||||
sleep 1
|
||||
chmod 777 $DIR
|
||||
chmod 666 $SOCK
|
||||
echo "daemon restarted:"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
|
||||
@@ -0,0 +1,27 @@
|
||||
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
|
||||
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
|
||||
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
|
||||
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
|
||||
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
|
||||
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
|
||||
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
U=${DESKTOP_USER:-swung0x48}
|
||||
$D --name=$C run bash -lc "
|
||||
systemctl stop desktop-session.service 2>/dev/null
|
||||
loginctl terminate-user $U 2>/dev/null; sleep 30
|
||||
systemctl reset-failed desktop-session.service
|
||||
rm -f /tmp/mobilegl-compositor.log
|
||||
date +%s > /tmp/plasma-start
|
||||
"
|
||||
am force-stop $P
|
||||
am start -n $P/com.anland.consumer.MainActivity > /dev/null
|
||||
sleep 30
|
||||
$D --name=$C run bash -lc "
|
||||
systemctl --no-pager status desktop-session.service | head -8
|
||||
UID_=\$(id -u $U)
|
||||
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
|
||||
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
|
||||
"
|
||||
@@ -0,0 +1,13 @@
|
||||
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
|
||||
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
|
||||
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
|
||||
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
|
||||
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
|
||||
$D show
|
||||
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
|
||||
@@ -0,0 +1,17 @@
|
||||
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
|
||||
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
|
||||
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
|
||||
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
|
||||
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
|
||||
set -e
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
|
||||
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
|
||||
ls -laZ $D
|
||||
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
|
||||
am force-stop $P
|
||||
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
|
||||
OWN=$(stat -c %u:%g $D/libMobileGL.so)
|
||||
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
|
||||
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
|
||||
ls -laZ $D/libMobileGL.so
|
||||
@@ -0,0 +1,17 @@
|
||||
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
|
||||
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
|
||||
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
|
||||
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
|
||||
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
|
||||
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
|
||||
# the setting until the next reboot - this clears it.
|
||||
# Env: PKG, SOCK.
|
||||
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
|
||||
if pidof $P:mobilegl > /dev/null; then
|
||||
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
|
||||
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
|
||||
fi
|
||||
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
|
||||
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
|
||||
@@ -0,0 +1,8 @@
|
||||
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
|
||||
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
|
||||
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
|
||||
# Env: DS, CONTAINER, DESKTOP_USER.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
U=${DESKTOP_USER:-swung0x48}
|
||||
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
|
||||
@@ -0,0 +1,47 @@
|
||||
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
|
||||
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
|
||||
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
|
||||
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
|
||||
# below 40% battery: the phone drains faster under load than USB charges it.
|
||||
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
|
||||
L=${1:-sample}
|
||||
APP=${2:-glxgears}
|
||||
N=${3:-10}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
|
||||
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
|
||||
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
|
||||
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
|
||||
exit 3
|
||||
fi
|
||||
ticks() {
|
||||
t=0
|
||||
for p in "$@"; do
|
||||
[ -r /proc/$p/stat ] || continue
|
||||
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
|
||||
t=$((t + v))
|
||||
done
|
||||
echo $t
|
||||
}
|
||||
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
|
||||
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
|
||||
n=0
|
||||
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
|
||||
echo $n
|
||||
}
|
||||
AP="$(pidof $APP)"
|
||||
XW="$(pidof Xwayland)"
|
||||
KW="$(pidof kwin_wayland)"
|
||||
SV="$(pidof $P:mobilegl)"
|
||||
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
|
||||
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
|
||||
g=0; i=0
|
||||
while [ $i -lt $N ]; do
|
||||
sleep 1
|
||||
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
|
||||
done
|
||||
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
|
||||
pct() { echo $(( ($2 - $1) / N )); }
|
||||
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
|
||||
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
|
||||
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
|
||||
@@ -0,0 +1,41 @@
|
||||
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
|
||||
# Sourced, never run. Every value can be overridden from the environment before calling a script.
|
||||
#
|
||||
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
|
||||
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
|
||||
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
|
||||
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
|
||||
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
|
||||
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
|
||||
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
|
||||
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
|
||||
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
|
||||
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
|
||||
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
|
||||
# (USB serial or a wireless "ip:port").
|
||||
|
||||
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
if [ -z "${MGL_WORKTREE:-}" ]; then
|
||||
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
|
||||
fi
|
||||
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
|
||||
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
|
||||
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
|
||||
: "${MGL_CONTAINER:=arch-kde-mgl}"
|
||||
: "${MGL_DESKTOP_USER:=swung0x48}"
|
||||
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
|
||||
: "${DEV_TOOLS:=/data/local/tmp/anl}"
|
||||
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
|
||||
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
|
||||
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
|
||||
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
|
||||
|
||||
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
|
||||
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
|
||||
export MSYS_NO_PATHCONV=1
|
||||
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
|
||||
|
||||
# C:/x/y -> /mnt/c/x/y (for WSL).
|
||||
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
|
||||
|
||||
mkdir -p "$ANL_DIR"
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/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" ]
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,34 @@
|
||||
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
char dir[64];
|
||||
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
|
||||
DIR* d = opendir(dir);
|
||||
if (!d) { perror("opendir"); return 1; }
|
||||
struct dirent* e;
|
||||
while ((e = readdir(d))) {
|
||||
char p[128], l[128] = {0};
|
||||
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
||||
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
|
||||
int fd = open(p, O_RDONLY);
|
||||
if (fd < 0) continue;
|
||||
struct stat st; fstat(fd, &st);
|
||||
if (st.st_size < (1 << 20)) { close(fd); continue; }
|
||||
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
||||
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
|
||||
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
|
||||
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
|
||||
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
|
||||
(unsigned long long)h, nz);
|
||||
munmap((void*)m, st.st_size); close(fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
|
||||
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
|
||||
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
|
||||
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
|
||||
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
|
||||
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
|
||||
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
|
||||
#endif
|
||||
|
||||
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int seconds = argc > 1 ? atoi(argv[1]) : 20;
|
||||
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
|
||||
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
|
||||
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
eglInitialize(d, NULL, NULL);
|
||||
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_NONE};
|
||||
EGLConfig c; EGLint n;
|
||||
eglChooseConfig(d, ca, &c, 1, &n);
|
||||
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
|
||||
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
|
||||
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
|
||||
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
|
||||
if (x == EGL_NO_CONTEXT) { printf("no ctx 0x%x\n", eglGetError()); return 1; }
|
||||
eglMakeCurrent(d, s, s, x);
|
||||
EGLint got = 0; eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
|
||||
const double t0 = now_ms(); double maxRt = 0; long iters = 0, slow = 0;
|
||||
printf("[probe] start %.0f prio=0x%x\n", t0, got);
|
||||
while (now_ms() - t0 < seconds * 1000.0) {
|
||||
const double a = now_ms();
|
||||
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glFinish();
|
||||
const double rt = now_ms() - a;
|
||||
if (rt > maxRt) maxRt = rt;
|
||||
if (rt > 50) { ++slow; printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms()); }
|
||||
++iters;
|
||||
usleep(10000);
|
||||
}
|
||||
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// 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, ®_l, NULL);
|
||||
wl_display_roundtrip(dpy);
|
||||
if (!g_comp || !g_wm) { fprintf(stderr, "no compositor/xdg_wm_base\n"); return 1; }
|
||||
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
|
||||
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
|
||||
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
|
||||
xdg_surface_add_listener(xs, &xs_l, NULL);
|
||||
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
|
||||
xdg_toplevel_add_listener(tl, &tl_l, NULL);
|
||||
xdg_toplevel_set_title(tl, tag);
|
||||
wl_surface_commit(surf);
|
||||
while (!g_configured) wl_display_dispatch(dpy);
|
||||
|
||||
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
|
||||
if (!eglInitialize(ed, NULL, NULL)) { fprintf(stderr, "eglInitialize\n"); return 1; }
|
||||
eglBindAPI(EGL_OPENGL_API);
|
||||
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig cfg; EGLint n = 0;
|
||||
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) { fprintf(stderr, "no config\n"); return 1; }
|
||||
const EGLint xa[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
|
||||
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
|
||||
if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "no context 0x%x\n", eglGetError()); return 1; }
|
||||
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
|
||||
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
|
||||
eglMakeCurrent(ed, es, es, ctx);
|
||||
eglSwapInterval(ed, 1);
|
||||
|
||||
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
|
||||
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
|
||||
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
|
||||
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
|
||||
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
|
||||
GLuint prog = glCreateProgram();
|
||||
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
|
||||
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
|
||||
glLinkProgram(prog);
|
||||
GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
|
||||
if (!ok) { fprintf(stderr, "link failed\n"); return 2; }
|
||||
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
|
||||
GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao);
|
||||
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
|
||||
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
|
||||
glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
|
||||
|
||||
const double t0 = now_ms(); double last = t0, maxGap = 0; long frames = 0, gaps100 = 0;
|
||||
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt, hangN, every, iters);
|
||||
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
|
||||
wl_display_dispatch_pending(dpy);
|
||||
const double t = (now_ms() - t0) / 1000.0;
|
||||
int hang = 0;
|
||||
if (hangAt >= 0 && frames >= hangAt) {
|
||||
long rel = frames - hangAt;
|
||||
if (every > 0) rel %= every;
|
||||
hang = rel < hangN;
|
||||
}
|
||||
glViewport(0, 0, g_w, g_h);
|
||||
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glUseProgram(prog);
|
||||
glUniform1i(uIt, hang ? (int)iters : 0);
|
||||
glUniform1f(uT, (float)t);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
|
||||
if (finish) glFinish();
|
||||
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
|
||||
const GLenum rs = glGetError();
|
||||
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
|
||||
const double nowv = now_ms(), gap = nowv - last;
|
||||
last = nowv; ++frames;
|
||||
if (gap > maxGap) maxGap = gap;
|
||||
if (gap > 100.0) { ++gaps100; printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames); }
|
||||
if (frames % 120 == 0) printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
|
||||
}
|
||||
const double total = now_ms() - t0;
|
||||
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap, gaps100);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
|
||||
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
|
||||
* left half red and the right half WHITE; a black right half means the alpha was lost. */
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#define P(T, n) T n = (T)eglGetProcAddress(#n)
|
||||
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
|
||||
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
|
||||
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
|
||||
int main(void) {
|
||||
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
|
||||
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
|
||||
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
|
||||
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
|
||||
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
|
||||
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
|
||||
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
|
||||
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
|
||||
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
|
||||
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
|
||||
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
|
||||
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
|
||||
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
|
||||
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
|
||||
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
|
||||
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
|
||||
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
|
||||
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
|
||||
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
|
||||
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
|
||||
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
|
||||
GLint lm = glGetUniformLocation(pr, "mode");
|
||||
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
|
||||
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
|
||||
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
|
||||
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
|
||||
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
|
||||
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
|
||||
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
|
||||
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
while (glGetError()) {}
|
||||
if (cases[i].storage) {
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
} else {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
}
|
||||
GLenum err = glGetError();
|
||||
glUniform1i(lm, cases[i].mode);
|
||||
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
|
||||
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
|
||||
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
|
||||
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
|
||||
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
|
||||
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
|
||||
};
|
||||
glUniform1i(lm, 0);
|
||||
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
|
||||
GLenum err = glGetError();
|
||||
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
int ok = 1;
|
||||
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
|
||||
//
|
||||
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
|
||||
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
|
||||
//
|
||||
// What it does, in an ES 3.0 context on a pbuffer:
|
||||
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
|
||||
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
|
||||
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
|
||||
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
|
||||
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
|
||||
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
|
||||
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
|
||||
// again: the new order must show (the server refreshes a foreign buffer at each frame).
|
||||
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
|
||||
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
|
||||
// check is that every call succeeds).
|
||||
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
|
||||
#define _GNU_SOURCE
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <fcntl.h>
|
||||
#include <gbm.h>
|
||||
#include <linux/dma-buf.h>
|
||||
#include <linux/dma-heap.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef DRM_FORMAT_NV12
|
||||
#define DRM_FORMAT_NV12 0x3231564E
|
||||
#endif
|
||||
#ifndef DRM_FORMAT_P010
|
||||
#define DRM_FORMAT_P010 0x30313050
|
||||
#endif
|
||||
|
||||
static int g_failures = 0;
|
||||
#define CHECK(cond, ...) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf("FAIL: " __VA_ARGS__); \
|
||||
printf("\n"); \
|
||||
++g_failures; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
|
||||
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
|
||||
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
|
||||
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
|
||||
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
|
||||
static PFNEGLCREATESYNCKHRPROC pCreateSync;
|
||||
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
|
||||
static EGLDisplay g_display;
|
||||
|
||||
enum { W = 256, H = 128, BARS = 4 };
|
||||
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
|
||||
|
||||
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
|
||||
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
|
||||
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
|
||||
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
|
||||
const double y = kr * r + kg * g + kb * b;
|
||||
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
|
||||
const double scale = (double)(1 << (bits - 8));
|
||||
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
|
||||
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
|
||||
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
|
||||
}
|
||||
|
||||
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
|
||||
// samples with the 10 significant bits high.
|
||||
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
|
||||
const int bits = bytes == 1 ? 8 : 10;
|
||||
for (int y = 0; y < H; ++y) {
|
||||
for (int x = 0; x < W; ++x) {
|
||||
int bar = x * BARS / W;
|
||||
if (reverse) bar = BARS - 1 - bar;
|
||||
int code[3];
|
||||
Encode709(kBars[bar], bits, code);
|
||||
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
|
||||
if (bytes == 1) py[0] = (uint8_t)code[0];
|
||||
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
|
||||
if ((x & 1) == 0 && (y & 1) == 0) {
|
||||
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
|
||||
if (bytes == 1) {
|
||||
pc[0] = (uint8_t)code[1];
|
||||
pc[1] = (uint8_t)code[2];
|
||||
} else {
|
||||
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
|
||||
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static GLuint Compile(GLenum type, const char* source) {
|
||||
GLuint shader = glCreateShader(type);
|
||||
glShaderSource(shader, 1, &source, NULL);
|
||||
glCompileShader(shader);
|
||||
GLint ok = 0;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
|
||||
if (!ok) {
|
||||
char log[2048];
|
||||
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
|
||||
printf("shader: %s\n", log);
|
||||
}
|
||||
return shader;
|
||||
}
|
||||
|
||||
static GLuint g_program, g_fbo, g_rgba;
|
||||
|
||||
static void SetUpDraw(void) {
|
||||
const char* vs = "#version 300 es\n"
|
||||
"out vec2 uv;\n"
|
||||
"void main() {\n"
|
||||
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
|
||||
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
|
||||
" uv = p;\n"
|
||||
"}\n";
|
||||
const char* fs = "#version 300 es\n"
|
||||
"#extension GL_OES_EGL_image_external_essl3 : require\n"
|
||||
"precision mediump float;\n"
|
||||
"uniform samplerExternalOES tex;\n"
|
||||
"in vec2 uv;\n"
|
||||
"out vec4 color;\n"
|
||||
"void main() { color = texture(tex, uv); }\n";
|
||||
g_program = glCreateProgram();
|
||||
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
|
||||
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
|
||||
glLinkProgram(g_program);
|
||||
GLint linked = 0;
|
||||
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
|
||||
CHECK(linked, "the samplerExternalOES program did not link");
|
||||
GLint type = 0, size = 0;
|
||||
char name[64];
|
||||
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
|
||||
printf("uniform 0: %s type 0x%04x\n", name, type);
|
||||
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
|
||||
glGenTextures(1, &g_rgba);
|
||||
glBindTexture(GL_TEXTURE_2D, g_rgba);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
|
||||
glGenFramebuffers(1, &g_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
|
||||
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
|
||||
GLuint vao;
|
||||
glGenVertexArrays(1, &vao);
|
||||
glBindVertexArray(vao);
|
||||
}
|
||||
|
||||
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
|
||||
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
|
||||
glViewport(0, 0, W, H);
|
||||
glUseProgram(g_program);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
static unsigned char pixels[W * H * 4];
|
||||
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
|
||||
if (tolerance < 0) {
|
||||
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
|
||||
pixels[(H / 2 * W + W / 2) * 4 + 3]);
|
||||
return;
|
||||
}
|
||||
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
|
||||
// so any row does; take two to catch a broken chroma row pitch.
|
||||
const int rows[2] = {H / 4, (3 * H) / 4};
|
||||
for (int r = 0; r < 2; ++r) {
|
||||
for (int bar = 0; bar < BARS; ++bar) {
|
||||
const int x = bar * (W / BARS) + W / (2 * BARS);
|
||||
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
|
||||
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
|
||||
int worst = 0;
|
||||
for (int c = 0; c < 3; ++c) {
|
||||
int d = abs((int)p[c] - (int)want[c]);
|
||||
if (d > worst) worst = d;
|
||||
}
|
||||
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
|
||||
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
|
||||
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int AllocateDmaHeap(size_t size) {
|
||||
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
|
||||
if (heap < 0) {
|
||||
perror("open /dev/dma_heap/system");
|
||||
return -1;
|
||||
}
|
||||
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
|
||||
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
|
||||
perror("DMA_HEAP_IOCTL_ALLOC");
|
||||
close(heap);
|
||||
return -1;
|
||||
}
|
||||
close(heap);
|
||||
return (int)data.fd;
|
||||
}
|
||||
|
||||
static void Sync(int fd, int start) {
|
||||
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
|
||||
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
|
||||
}
|
||||
|
||||
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
|
||||
EGLint attribs[64];
|
||||
int n = 0;
|
||||
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
|
||||
A(EGL_WIDTH, W);
|
||||
A(EGL_HEIGHT, H);
|
||||
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
|
||||
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
|
||||
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
|
||||
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
|
||||
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
|
||||
if (modifier) {
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
|
||||
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
|
||||
}
|
||||
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
|
||||
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
|
||||
attribs[n] = EGL_NONE;
|
||||
#undef A
|
||||
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
|
||||
return image;
|
||||
}
|
||||
|
||||
static GLuint ExternalTexture(EGLImageKHR image) {
|
||||
GLuint texture;
|
||||
glGenTextures(1, &texture);
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
|
||||
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
|
||||
GLenum error = glGetError();
|
||||
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
|
||||
GLint bound = 0;
|
||||
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
|
||||
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A frame boundary as an offscreen producer makes one: a native fence.
|
||||
static void EndFrame(void) {
|
||||
EGLint attribs[] = {EGL_NONE};
|
||||
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
|
||||
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
|
||||
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
|
||||
}
|
||||
|
||||
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
|
||||
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
|
||||
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
|
||||
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
|
||||
const uint32_t offsetC = pitchY * (H + 16);
|
||||
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
|
||||
int fd = AllocateDmaHeap(size);
|
||||
if (fd < 0) {
|
||||
CHECK(0, "%s: no dma-buf to test with", what);
|
||||
return;
|
||||
}
|
||||
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
|
||||
if (map == MAP_FAILED) return;
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
|
||||
Sync(fd, 0);
|
||||
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
|
||||
if (image == EGL_NO_IMAGE_KHR) return;
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, what, 0, tolerance);
|
||||
EndFrame();
|
||||
// New content, same buffer: shows after the frame boundary.
|
||||
Sync(fd, 1);
|
||||
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
|
||||
Sync(fd, 0);
|
||||
EndFrame();
|
||||
DrawAndCheck(texture, what, 1, tolerance);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
munmap(map, size);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void GbmTest(void) {
|
||||
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
|
||||
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
|
||||
struct gbm_device* gbm = gbm_create_device(node);
|
||||
if (gbm == NULL) {
|
||||
CHECK(0, "gbm_create_device failed");
|
||||
return;
|
||||
}
|
||||
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
|
||||
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
|
||||
printf("gbm NV12 supported: %d, P010: %d\n", supported,
|
||||
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
|
||||
CHECK(supported == 1, "gbm does not support NV12");
|
||||
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
|
||||
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
|
||||
if (bo != NULL) {
|
||||
const int planes = gbm_bo_get_plane_count(bo);
|
||||
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
|
||||
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
|
||||
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
|
||||
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
|
||||
int fd = gbm_bo_get_fd(bo);
|
||||
EGLImageKHR image =
|
||||
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
|
||||
gbm_bo_get_stride_for_plane(bo, 1), 0);
|
||||
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
|
||||
if (image != EGL_NO_IMAGE_KHR) {
|
||||
GLuint texture = ExternalTexture(image);
|
||||
DrawAndCheck(texture, "gbm NV12", 0, -1);
|
||||
glDeleteTextures(1, &texture);
|
||||
pDestroyImage(g_display, image);
|
||||
}
|
||||
close(fd);
|
||||
gbm_bo_destroy(bo);
|
||||
}
|
||||
gbm_device_destroy(gbm);
|
||||
close(node);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
EGLint major, minor;
|
||||
if (!eglInitialize(g_display, &major, &minor)) {
|
||||
printf("FAIL: eglInitialize\n");
|
||||
return 2;
|
||||
}
|
||||
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
|
||||
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
|
||||
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
|
||||
eglBindAPI(EGL_OPENGL_ES_API);
|
||||
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
|
||||
EGLConfig config;
|
||||
EGLint count = 0;
|
||||
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
|
||||
if (count < 1) {
|
||||
printf("FAIL: no config\n");
|
||||
return 2;
|
||||
}
|
||||
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
|
||||
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
|
||||
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
|
||||
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
|
||||
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
|
||||
printf("FAIL: no ES 3 context\n");
|
||||
return 2;
|
||||
}
|
||||
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
|
||||
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
|
||||
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
|
||||
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
|
||||
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
|
||||
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
|
||||
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
|
||||
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
|
||||
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
|
||||
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
|
||||
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
|
||||
|
||||
EGLint formats[32], formatCount = 0;
|
||||
pQueryFormats(g_display, 32, formats, &formatCount);
|
||||
int nv12 = 0;
|
||||
for (int i = 0; i < formatCount; ++i) {
|
||||
printf("dma-buf format %.4s", (const char*)&formats[i]);
|
||||
EGLuint64KHR modifiers[4];
|
||||
EGLBoolean externalOnly[4];
|
||||
EGLint modifierCount = 0;
|
||||
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
|
||||
for (int m = 0; m < modifierCount; ++m)
|
||||
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
|
||||
printf("\n");
|
||||
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
|
||||
}
|
||||
CHECK(nv12, "NV12 is not a listed dma-buf format");
|
||||
|
||||
SetUpDraw();
|
||||
ForeignTest(DRM_FORMAT_NV12, 1, 12);
|
||||
ForeignTest(DRM_FORMAT_P010, 2, 16);
|
||||
GbmTest();
|
||||
|
||||
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
|
||||
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglTerminate(g_display);
|
||||
return g_failures ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/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'"
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/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'"
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/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]}"
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,47 @@
|
||||
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
|
||||
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(_triple aarch64-unknown-linux-gnu)
|
||||
if (DEFINED ENV{MGL_SYSROOT})
|
||||
set(_sysroot "$ENV{MGL_SYSROOT}")
|
||||
else()
|
||||
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
|
||||
endif()
|
||||
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
|
||||
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
|
||||
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
|
||||
list(GET _gcc_dirs 0 _gcc_install_dir)
|
||||
|
||||
set(CMAKE_SYSROOT "${_sysroot}")
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
set(CMAKE_ASM_COMPILER clang)
|
||||
set(CMAKE_C_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_AR llvm-ar)
|
||||
set(CMAKE_RANLIB llvm-ranlib)
|
||||
set(CMAKE_NM llvm-nm)
|
||||
set(CMAKE_OBJCOPY llvm-objcopy)
|
||||
set(CMAKE_STRIP llvm-strip)
|
||||
|
||||
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
|
||||
set(CMAKE_C_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_LINKER_TYPE LLD)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
|
||||
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
|
||||
set(ENV{PKG_CONFIG_PATH} "")
|
||||
@@ -0,0 +1,26 @@
|
||||
#!/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"
|
||||
@@ -18,6 +18,10 @@ on:
|
||||
- '**/*.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:
|
||||
@@ -477,7 +481,7 @@ jobs:
|
||||
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"
|
||||
--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
|
||||
@@ -493,12 +497,12 @@ jobs:
|
||||
# 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"
|
||||
--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")
|
||||
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.
|
||||
|
||||
+35
-90
@@ -21,6 +21,10 @@ on:
|
||||
required: false
|
||||
default: "087685d1"
|
||||
|
||||
env:
|
||||
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
|
||||
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
|
||||
|
||||
# ONE LIVE RUN PER BRANCH. The disaggregation branch pushes (often docs-only) commits minutes apart
|
||||
# and each push fans out to ~330 retrace legs here and ~275 jobs in apk.yml; with no group, runs
|
||||
# sat queued for 10+ hours behind their own successors. A manual dispatch gets a group of its own,
|
||||
@@ -413,6 +417,12 @@ jobs:
|
||||
--discovery "${RUNNER_TEMP}/primary-inproc-lane.json" \
|
||||
--junit "${RUNNER_TEMP}/primary-inproc.xml" --output "${RUNNER_TEMP}/primary-inproc-proof.json"
|
||||
|
||||
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
|
||||
# with the lines of its output that say why, where anyone reading the run can see them.
|
||||
- name: Annotate failed entries
|
||||
if: failure()
|
||||
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
|
||||
|
||||
- name: Upload integration lane evidence
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1169,6 +1179,12 @@ jobs:
|
||||
# arm proof read; verify-split-logs/ is whatever the control steps left behind, which for
|
||||
# twelve entries is a deliberately red re-run under the same name. Uploading only the
|
||||
# second - which is what this step did before the copy above existed - loses the first.
|
||||
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
|
||||
# with the lines of its output that say why, where anyone reading the run can see them.
|
||||
- name: Annotate failed entries
|
||||
if: failure()
|
||||
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
|
||||
|
||||
- name: Upload verify-split lane logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1481,7 +1497,7 @@ jobs:
|
||||
working-directory: build-split
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
|
||||
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml"
|
||||
|
||||
# P7 wave 2-F, PH-7 (1). THE TCP SUPERVISOR LANE NEEDS flatc, AND CI HAD NONE.
|
||||
#
|
||||
@@ -1648,6 +1664,12 @@ jobs:
|
||||
CONTROL_TMPDIR: ${{ runner.temp }}
|
||||
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
|
||||
|
||||
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
|
||||
# with the lines of its output that say why, where anyone reading the run can see them.
|
||||
- name: Annotate failed entries
|
||||
if: failure()
|
||||
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
|
||||
|
||||
- name: Upload split lane logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -1737,7 +1759,7 @@ jobs:
|
||||
env:
|
||||
MOBILEGL_ITEST_REQUIRE_GPU: "1"
|
||||
MOBILEGL_IPC_STRICT_ERRORS: "1"
|
||||
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
|
||||
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-strict.xml"
|
||||
|
||||
# ---- P5e (ra), MG_Remote/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
|
||||
#
|
||||
@@ -2246,6 +2268,12 @@ jobs:
|
||||
slp.require_green(lane, sys.argv[3], names)
|
||||
PY
|
||||
|
||||
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
|
||||
# with the lines of its output that say why, where anyone reading the run can see them.
|
||||
- name: Annotate failed entries
|
||||
if: failure()
|
||||
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
|
||||
|
||||
- name: Upload strict lane logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -2751,7 +2779,11 @@ jobs:
|
||||
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
|
||||
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
|
||||
fi
|
||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
|
||||
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$' --output-junit "${RUNNER_TEMP}/retrace.xml"
|
||||
|
||||
- name: Annotate failed entries
|
||||
if: failure()
|
||||
run: python3 scripts/ci/junit_annotate.py --excerpts 1 "${RUNNER_TEMP}"
|
||||
|
||||
- name: Upload core dumps
|
||||
if: failure()
|
||||
@@ -3506,27 +3538,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
# Deliberately independent of build-linux: these are source-level gates, they take
|
||||
# seconds, and a broken build must not hide a drifted interface.
|
||||
env:
|
||||
# THE CURRENT PHASE's base ref, for the two G5 region gates below. It is 37da3c3a - P4a's
|
||||
# base ref, INTEGRATOR-DECISIONS ID-1 - and NOT the workflow's baseline_sha input: that input
|
||||
# is the SYMBOL baseline (087685d1, P1's G1 reading) and it is empty on a push, whereas these
|
||||
# gates ask "did the do-not-touch list move since the phase started".
|
||||
#
|
||||
# IT MOVED FROM P3a's 44c2b5cf TO P4a's 37da3c3a WITH THE PHASE, and that is a deliberate
|
||||
# narrowing rather than a loss: P3a's eleven functions were compared against 44c2b5cf at P3a's
|
||||
# own exit and were byte-identical there, so 37da3c3a carries the same bodies (measured: the
|
||||
# eleven shas at 37da3c3a are the eleven shas at 44c2b5cf, and FlushPendingRangesFrom's is
|
||||
# still the sha pinned in the script at 3e298c9a). What the two gates now both answer is "did
|
||||
# anything on the list move during P4a", which is the question this phase can act on.
|
||||
BASELINE: "37da3c3a"
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# The G5 gate reads Managers.cpp at BASELINE with `git show`, which a depth-1 checkout
|
||||
# does not have. Nothing else in this job needs history.
|
||||
fetch-depth: 0
|
||||
|
||||
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
|
||||
# keeps the two interface tables, the wire records, the verify comparators, the
|
||||
@@ -3655,76 +3670,6 @@ jobs:
|
||||
python3 tools/trace_replay/test_run_tcp_matrix.py
|
||||
python3 tools/trace_replay/test_compare_actuals.py
|
||||
|
||||
# A GATE as of P3a (G5). "pool 与延迟释放原样搬" (ROADMAP.md:19) is meant literally: the
|
||||
# buffer pool, the deferred-release drain and the three persistently mapped rings move
|
||||
# VERBATIM, and ARCHITECTURE.md:515 says why - their retire happens only inside Present, so
|
||||
# a batching or ordering change there starves them, and nothing else in this workflow can
|
||||
# see it. P3a rewrites the rest of Managers.cpp by design, so a file diff says nothing; the
|
||||
# script extracts the ELEVEN named bodies and compares their hashes on their own.
|
||||
#
|
||||
# Eleven and not ten (ID-15): Managers.cpp carries the three-tier flush drain TWICE, once
|
||||
# per preprocessor arm, and a push build compiles only FlushPendingRangesFrom while the
|
||||
# untouched FlushPendingRangesNow lives in the `#else`. Hashing the pull name alone would
|
||||
# protect text no shipping build compiles, so both are hashed - the pull ladder against
|
||||
# BASELINE, the push ladder against a sha pinned in the script at 3dadd4c1 (ID-41), because
|
||||
# that one was born in P3a and is compared against the reviewed body rather than against the
|
||||
# base ref.
|
||||
#
|
||||
# Scoped to the disaggregation branch and to a manual dispatch, deliberately: the question
|
||||
# is "did these eleven move since P3a started", and BASELINE is P3a's base ref. On dev,
|
||||
# where unrelated buffer fixes land on their own schedule, the same comparison would be
|
||||
# asking a question nobody posed - it belongs with the TEMPORARY trigger lines at the top
|
||||
# of this file and retires with them.
|
||||
#
|
||||
# --self-test is the half that keeps it honest, and it is not optional: a comparison that
|
||||
# silently stopped comparing produces exactly the same green as eleven untouched bodies. It
|
||||
# runs eight canned controls - eleven bodies extracted, an untouched copy compared equal, an
|
||||
# edit OUTSIDE them ignored, each of the three perturbation targets (ClearBufferPool and BOTH
|
||||
# flush ladders) reported BY NAME, plus the pin-precedence control and a one-token edit to the
|
||||
# pinned ladder compared against the pin - and fails if any of them does not answer.
|
||||
# Same shape as gen_pipe.py --self-test above.
|
||||
- name: P3a regions unchanged (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p3a_untouched_regions.sh "${BASELINE}" HEAD
|
||||
|
||||
- name: P3a untouched-region self-test (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p3a_untouched_regions.sh --self-test
|
||||
|
||||
# A GATE AS OF P4a (G5), and a SECOND script rather than an edit to the one above. P4a extends
|
||||
# the same claim to the rest of ARCHITECTURE.md:318's do-not-touch list - the unpack PBO ring's
|
||||
# staging repack and its two helpers, the attachment permutation, the D24S8 sampling-emulation
|
||||
# core and the format-caveat handler - which is SEVENTEEN regions across THREE files
|
||||
# (BRIEF-P4A.md D-N: P3a's eleven, which P4a must not touch either, plus P4a's six). The
|
||||
# parent's SOURCE_PATH is a single file, so the extension needed a per-region source path and
|
||||
# a region KIND (DepthStencilSamplingReadImpl is a namespace, not a function); everything else
|
||||
# about the extraction is its parent's, verbatim.
|
||||
#
|
||||
# Both scripts run. The parent keeps answering its own question against its own eleven, so a
|
||||
# regression in either half names itself, and neither gate can be silenced by editing the
|
||||
# other's list.
|
||||
#
|
||||
# Same feat/disaggregated-or-dispatch guard as the P3a step, for the same reason: the question
|
||||
# is "did these move since the phase started", and on dev - where unrelated buffer and texture
|
||||
# fixes land on their own schedule - it would be a question nobody posed. It belongs with the
|
||||
# TEMPORARY trigger lines at the top of this file and retires with them.
|
||||
#
|
||||
# --self-test is the half that keeps it honest and is not optional: a comparison that silently
|
||||
# stopped comparing produces exactly the same green as seventeen untouched regions. It runs
|
||||
# three positive controls (seventeen regions extracted, an untouched copy compared equal, an
|
||||
# edit OUTSIDE them invisible in all three files) and NINE negative ones - ClearBufferPool,
|
||||
# FlushPendingRangesNow, RecomputeBackendColorSlots and StageBlocksIntoUnpackRing, each
|
||||
# perturbed at its HEAD and at its TAIL and each required to be named BY NAME, plus a
|
||||
# one-token edit to the pinned FlushPendingRangesFrom body compared against the pin - and
|
||||
# fails if any of them does not answer.
|
||||
- name: P4a regions unchanged (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p4a_untouched_regions.sh "${BASELINE}" HEAD
|
||||
|
||||
- name: P4a untouched-region self-test (G5)
|
||||
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
|
||||
run: bash scripts/p4a_untouched_regions.sh --self-test
|
||||
|
||||
# Warning only for now: the disaggregation documents are still being written, and a
|
||||
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
|
||||
# --strict when the documents settle.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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
|
||||
Vendored
-538
@@ -1,538 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2011 Intel Corporation
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the "Software"),
|
||||
* to deal in the Software without restriction, including without limitation
|
||||
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
* and/or sell copies of the Software, and to permit persons to whom the
|
||||
* Software is furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice (including the next
|
||||
* paragraph) shall be included in all copies or substantial portions of the
|
||||
* Software.
|
||||
*
|
||||
* THE SOFTWARE IS 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 SOFTWARE OR THE USE OR OTHER
|
||||
* DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* Authors:
|
||||
* Benjamin Franzke <benjaminfranzke@googlemail.com>
|
||||
*/
|
||||
|
||||
#ifndef _GBM_H_
|
||||
#define _GBM_H_
|
||||
|
||||
#define __GBM__ 1
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
/**
|
||||
* \file gbm.h
|
||||
* \brief Generic Buffer Manager
|
||||
*/
|
||||
|
||||
struct gbm_device;
|
||||
struct gbm_bo;
|
||||
struct gbm_surface;
|
||||
|
||||
/**
|
||||
* \mainpage The Generic Buffer Manager
|
||||
*
|
||||
* This module provides an abstraction that the caller can use to request a
|
||||
* buffer from the underlying memory management system for the platform.
|
||||
*
|
||||
* This allows the creation of portable code whilst still allowing access to
|
||||
* the underlying memory manager.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstraction representing the handle to a buffer allocated by the
|
||||
* manager
|
||||
*/
|
||||
union gbm_bo_handle {
|
||||
void *ptr;
|
||||
int32_t s32;
|
||||
uint32_t u32;
|
||||
int64_t s64;
|
||||
uint64_t u64;
|
||||
};
|
||||
|
||||
/** Format of the allocated buffer */
|
||||
enum gbm_bo_format {
|
||||
/** RGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_XRGB8888,
|
||||
/** ARGB with 8 bits per channel in a 32 bit value */
|
||||
GBM_BO_FORMAT_ARGB8888
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* The FourCC format codes are taken from the drm_fourcc.h definition, and
|
||||
* re-namespaced. New GBM formats must not be added, unless they are
|
||||
* identical ports from drm_fourcc.
|
||||
*/
|
||||
#define __gbm_fourcc_code(a,b,c,d) ((uint32_t)(a) | ((uint32_t)(b) << 8) | \
|
||||
((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
|
||||
|
||||
#define GBM_FORMAT_BIG_ENDIAN (1<<31) /* format is big endian instead of little endian */
|
||||
|
||||
/* color index */
|
||||
#define GBM_FORMAT_C8 __gbm_fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
|
||||
|
||||
/* 8 bpp Red */
|
||||
#define GBM_FORMAT_R8 __gbm_fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
|
||||
|
||||
/* 16 bpp Red */
|
||||
#define GBM_FORMAT_R16 __gbm_fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
|
||||
|
||||
/* 16 bpp RG */
|
||||
#define GBM_FORMAT_GR88 __gbm_fourcc_code('G', 'R', '8', '8') /* [15:0] G:R 8:8 little endian */
|
||||
|
||||
/* 32 bpp RG */
|
||||
#define GBM_FORMAT_RG1616 __gbm_fourcc_code('R', 'G', '3', '2') /* [31:0] R:G 16:16 little endian */
|
||||
#define GBM_FORMAT_GR1616 __gbm_fourcc_code('G', 'R', '3', '2') /* [31:0] G:R 16:16 little endian */
|
||||
|
||||
/* 8 bpp RGB */
|
||||
#define GBM_FORMAT_RGB332 __gbm_fourcc_code('R', 'G', 'B', '8') /* [7:0] R:G:B 3:3:2 */
|
||||
#define GBM_FORMAT_BGR233 __gbm_fourcc_code('B', 'G', 'R', '8') /* [7:0] B:G:R 2:3:3 */
|
||||
|
||||
/* 16 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB4444 __gbm_fourcc_code('X', 'R', '1', '2') /* [15:0] x:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_XBGR4444 __gbm_fourcc_code('X', 'B', '1', '2') /* [15:0] x:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBX4444 __gbm_fourcc_code('R', 'X', '1', '2') /* [15:0] R:G:B:x 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRX4444 __gbm_fourcc_code('B', 'X', '1', '2') /* [15:0] B:G:R:x 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB4444 __gbm_fourcc_code('A', 'R', '1', '2') /* [15:0] A:R:G:B 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_ABGR4444 __gbm_fourcc_code('A', 'B', '1', '2') /* [15:0] A:B:G:R 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_RGBA4444 __gbm_fourcc_code('R', 'A', '1', '2') /* [15:0] R:G:B:A 4:4:4:4 little endian */
|
||||
#define GBM_FORMAT_BGRA4444 __gbm_fourcc_code('B', 'A', '1', '2') /* [15:0] B:G:R:A 4:4:4:4 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB1555 __gbm_fourcc_code('X', 'R', '1', '5') /* [15:0] x:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_XBGR1555 __gbm_fourcc_code('X', 'B', '1', '5') /* [15:0] x:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBX5551 __gbm_fourcc_code('R', 'X', '1', '5') /* [15:0] R:G:B:x 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRX5551 __gbm_fourcc_code('B', 'X', '1', '5') /* [15:0] B:G:R:x 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB1555 __gbm_fourcc_code('A', 'R', '1', '5') /* [15:0] A:R:G:B 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_ABGR1555 __gbm_fourcc_code('A', 'B', '1', '5') /* [15:0] A:B:G:R 1:5:5:5 little endian */
|
||||
#define GBM_FORMAT_RGBA5551 __gbm_fourcc_code('R', 'A', '1', '5') /* [15:0] R:G:B:A 5:5:5:1 little endian */
|
||||
#define GBM_FORMAT_BGRA5551 __gbm_fourcc_code('B', 'A', '1', '5') /* [15:0] B:G:R:A 5:5:5:1 little endian */
|
||||
|
||||
#define GBM_FORMAT_RGB565 __gbm_fourcc_code('R', 'G', '1', '6') /* [15:0] R:G:B 5:6:5 little endian */
|
||||
#define GBM_FORMAT_BGR565 __gbm_fourcc_code('B', 'G', '1', '6') /* [15:0] B:G:R 5:6:5 little endian */
|
||||
|
||||
/* 24 bpp RGB */
|
||||
#define GBM_FORMAT_RGB888 __gbm_fourcc_code('R', 'G', '2', '4') /* [23:0] R:G:B little endian */
|
||||
#define GBM_FORMAT_BGR888 __gbm_fourcc_code('B', 'G', '2', '4') /* [23:0] B:G:R little endian */
|
||||
|
||||
/* 32 bpp RGB */
|
||||
#define GBM_FORMAT_XRGB8888 __gbm_fourcc_code('X', 'R', '2', '4') /* [31:0] x:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_XBGR8888 __gbm_fourcc_code('X', 'B', '2', '4') /* [31:0] x:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBX8888 __gbm_fourcc_code('R', 'X', '2', '4') /* [31:0] R:G:B:x 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRX8888 __gbm_fourcc_code('B', 'X', '2', '4') /* [31:0] B:G:R:x 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB8888 __gbm_fourcc_code('A', 'R', '2', '4') /* [31:0] A:R:G:B 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_ABGR8888 __gbm_fourcc_code('A', 'B', '2', '4') /* [31:0] A:B:G:R 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_RGBA8888 __gbm_fourcc_code('R', 'A', '2', '4') /* [31:0] R:G:B:A 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_BGRA8888 __gbm_fourcc_code('B', 'A', '2', '4') /* [31:0] B:G:R:A 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_XRGB2101010 __gbm_fourcc_code('X', 'R', '3', '0') /* [31:0] x:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_XBGR2101010 __gbm_fourcc_code('X', 'B', '3', '0') /* [31:0] x:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBX1010102 __gbm_fourcc_code('R', 'X', '3', '0') /* [31:0] R:G:B:x 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRX1010102 __gbm_fourcc_code('B', 'X', '3', '0') /* [31:0] B:G:R:x 10:10:10:2 little endian */
|
||||
|
||||
#define GBM_FORMAT_ARGB2101010 __gbm_fourcc_code('A', 'R', '3', '0') /* [31:0] A:R:G:B 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_ABGR2101010 __gbm_fourcc_code('A', 'B', '3', '0') /* [31:0] A:B:G:R 2:10:10:10 little endian */
|
||||
#define GBM_FORMAT_RGBA1010102 __gbm_fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
|
||||
#define GBM_FORMAT_BGRA1010102 __gbm_fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
|
||||
|
||||
/* 64 bpp RGB */
|
||||
#define GBM_FORMAT_XBGR16161616 __gbm_fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616 __gbm_fourcc_code('A', 'B', '4', '8') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/*
|
||||
* Floating point 64bpp RGB
|
||||
* IEEE 754-2008 binary16 half-precision float
|
||||
* [15:0] sign:exponent:mantissa 1:5:10
|
||||
*/
|
||||
#define GBM_FORMAT_XBGR16161616F __gbm_fourcc_code('X', 'B', '4', 'H') /* [63:0] x:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
#define GBM_FORMAT_ABGR16161616F __gbm_fourcc_code('A', 'B', '4', 'H') /* [63:0] A:B:G:R 16:16:16:16 little endian */
|
||||
|
||||
/* packed YCbCr */
|
||||
#define GBM_FORMAT_YUYV __gbm_fourcc_code('Y', 'U', 'Y', 'V') /* [31:0] Cr0:Y1:Cb0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_YVYU __gbm_fourcc_code('Y', 'V', 'Y', 'U') /* [31:0] Cb0:Y1:Cr0:Y0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_UYVY __gbm_fourcc_code('U', 'Y', 'V', 'Y') /* [31:0] Y1:Cr0:Y0:Cb0 8:8:8:8 little endian */
|
||||
#define GBM_FORMAT_VYUY __gbm_fourcc_code('V', 'Y', 'U', 'Y') /* [31:0] Y1:Cb0:Y0:Cr0 8:8:8:8 little endian */
|
||||
|
||||
#define GBM_FORMAT_AYUV __gbm_fourcc_code('A', 'Y', 'U', 'V') /* [31:0] A:Y:Cb:Cr 8:8:8:8 little endian */
|
||||
|
||||
/*
|
||||
* 2 plane YCbCr
|
||||
* index 0 = Y plane, [7:0] Y
|
||||
* index 1 = Cr:Cb plane, [15:0] Cr:Cb little endian
|
||||
* or
|
||||
* index 1 = Cb:Cr plane, [15:0] Cb:Cr little endian
|
||||
*/
|
||||
#define GBM_FORMAT_NV12 __gbm_fourcc_code('N', 'V', '1', '2') /* 2x2 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV21 __gbm_fourcc_code('N', 'V', '2', '1') /* 2x2 subsampled Cb:Cr plane */
|
||||
#define GBM_FORMAT_NV16 __gbm_fourcc_code('N', 'V', '1', '6') /* 2x1 subsampled Cr:Cb plane */
|
||||
#define GBM_FORMAT_NV61 __gbm_fourcc_code('N', 'V', '6', '1') /* 2x1 subsampled Cb:Cr plane */
|
||||
|
||||
/*
|
||||
* 3 plane YCbCr
|
||||
* index 0: Y plane, [7:0] Y
|
||||
* index 1: Cb plane, [7:0] Cb
|
||||
* index 2: Cr plane, [7:0] Cr
|
||||
* or
|
||||
* index 1: Cr plane, [7:0] Cr
|
||||
* index 2: Cb plane, [7:0] Cb
|
||||
*/
|
||||
#define GBM_FORMAT_YUV410 __gbm_fourcc_code('Y', 'U', 'V', '9') /* 4x4 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU410 __gbm_fourcc_code('Y', 'V', 'U', '9') /* 4x4 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV411 __gbm_fourcc_code('Y', 'U', '1', '1') /* 4x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU411 __gbm_fourcc_code('Y', 'V', '1', '1') /* 4x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV420 __gbm_fourcc_code('Y', 'U', '1', '2') /* 2x2 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU420 __gbm_fourcc_code('Y', 'V', '1', '2') /* 2x2 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV422 __gbm_fourcc_code('Y', 'U', '1', '6') /* 2x1 subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU422 __gbm_fourcc_code('Y', 'V', '1', '6') /* 2x1 subsampled Cr (1) and Cb (2) planes */
|
||||
#define GBM_FORMAT_YUV444 __gbm_fourcc_code('Y', 'U', '2', '4') /* non-subsampled Cb (1) and Cr (2) planes */
|
||||
#define GBM_FORMAT_YVU444 __gbm_fourcc_code('Y', 'V', '2', '4') /* non-subsampled Cr (1) and Cb (2) planes */
|
||||
|
||||
struct gbm_format_name_desc {
|
||||
char name[5];
|
||||
};
|
||||
|
||||
/**
|
||||
* Flags to indicate the intended use for the buffer - these are passed into
|
||||
* gbm_bo_create(). The caller must set the union of all the flags that are
|
||||
* appropriate
|
||||
*
|
||||
* \sa Use gbm_device_is_format_supported() to check if the combination of format
|
||||
* and use flags are supported
|
||||
*/
|
||||
enum gbm_bo_flags {
|
||||
/**
|
||||
* Buffer is going to be presented to the screen using an API such as KMS
|
||||
*/
|
||||
GBM_BO_USE_SCANOUT = (1 << 0),
|
||||
/**
|
||||
* Buffer is going to be used as cursor
|
||||
*/
|
||||
GBM_BO_USE_CURSOR = (1 << 1),
|
||||
/**
|
||||
* Deprecated
|
||||
*/
|
||||
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
|
||||
/**
|
||||
* Buffer is to be used for rendering - for example it is going to be used
|
||||
* as the storage for a color buffer
|
||||
*/
|
||||
GBM_BO_USE_RENDERING = (1 << 2),
|
||||
/**
|
||||
* Buffer can be used for gbm_bo_write. This is guaranteed to work
|
||||
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
|
||||
*/
|
||||
GBM_BO_USE_WRITE = (1 << 3),
|
||||
/**
|
||||
* Buffer is linear, i.e. not tiled.
|
||||
*/
|
||||
GBM_BO_USE_LINEAR = (1 << 4),
|
||||
/**
|
||||
* Buffer is protected, i.e. encrypted and not readable by CPU or any
|
||||
* other non-secure / non-trusted components nor by non-trusted OpenGL,
|
||||
* OpenCL, and Vulkan applications.
|
||||
*/
|
||||
GBM_BO_USE_PROTECTED = (1 << 5),
|
||||
|
||||
/**
|
||||
* The buffer will be used for front buffer rendering. On some
|
||||
* platforms this may (for example) disable framebuffer compression
|
||||
* to avoid problems with compression flags data being out of sync
|
||||
* with pixel data.
|
||||
*/
|
||||
GBM_BO_USE_FRONT_RENDERING = (1 << 6),
|
||||
|
||||
/**
|
||||
* Allow the driver to select fixed-rate compression parameters.
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_DEFAULT = (1 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 1bpc, less than 2bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_1BPC = (2 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 2bpc, less than 3bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_2BPC = (3 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 3bpc, less than 4bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_3BPC = (4 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 4bpc, less than 5bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_4BPC = (5 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 5bpc, less than 6bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_5BPC = (6 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 6bpc, less than 7bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_6BPC = (7 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 7bpc, less than 8bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_7BPC = (8 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 8bpc, less than 9bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_8BPC = (9 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 9bpc, less than 10bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_9BPC = (10 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 10bpc, less than 11bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_10BPC = (11 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 11bpc, less than 12bpc
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_11BPC = (12 << 7),
|
||||
|
||||
/**
|
||||
* Fixed-rate compression: at least 12bpc, no maximum rate
|
||||
*/
|
||||
GBM_BO_FIXED_COMPRESSION_12BPC = (13 << 7),
|
||||
|
||||
/* next available value is (1 << 11) */
|
||||
};
|
||||
|
||||
#define GBM_BO_FIXED_COMPRESSION_MASK (((1 << 11) - 1) & ~((1 << 7) - 1))
|
||||
|
||||
int
|
||||
gbm_device_get_fd(struct gbm_device *gbm);
|
||||
|
||||
const char *
|
||||
gbm_device_get_backend_name(struct gbm_device *gbm);
|
||||
|
||||
int
|
||||
gbm_device_is_format_supported(struct gbm_device *gbm,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
int
|
||||
gbm_device_get_format_modifier_plane_count(struct gbm_device *gbm,
|
||||
uint32_t format,
|
||||
uint64_t modifier);
|
||||
|
||||
void
|
||||
gbm_device_destroy(struct gbm_device *gbm);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_create_device(int fd);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
#define GBM_BO_IMPORT_WL_BUFFER 0x5501
|
||||
#define GBM_BO_IMPORT_EGL_IMAGE 0x5502
|
||||
#define GBM_BO_IMPORT_FD 0x5503
|
||||
#define GBM_BO_IMPORT_FD_MODIFIER 0x5504
|
||||
|
||||
struct gbm_import_fd_data {
|
||||
int fd;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t stride;
|
||||
uint32_t format;
|
||||
};
|
||||
|
||||
#define GBM_MAX_PLANES 4
|
||||
|
||||
struct gbm_import_fd_modifier_data {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t format;
|
||||
uint32_t num_fds;
|
||||
int fds[GBM_MAX_PLANES];
|
||||
int strides[GBM_MAX_PLANES];
|
||||
int offsets[GBM_MAX_PLANES];
|
||||
uint64_t modifier;
|
||||
};
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_bo_import(struct gbm_device *gbm, uint32_t type,
|
||||
void *buffer, uint32_t flags);
|
||||
|
||||
/**
|
||||
* Flags to indicate the type of mapping for the buffer - these are
|
||||
* passed into gbm_bo_map(). The caller must set the union of all the
|
||||
* flags that are appropriate.
|
||||
*
|
||||
* These flags are independent of the GBM_BO_USE_* creation flags. However,
|
||||
* mapping the buffer may require copying to/from a staging buffer.
|
||||
*
|
||||
* See also: pipe_map_flags
|
||||
*/
|
||||
enum gbm_bo_transfer_flags {
|
||||
/**
|
||||
* Buffer contents read back (or accessed directly) at transfer
|
||||
* create time.
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ = (1 << 0),
|
||||
/**
|
||||
* Buffer contents will be written back at unmap time
|
||||
* (or modified as a result of being accessed directly).
|
||||
*/
|
||||
GBM_BO_TRANSFER_WRITE = (1 << 1),
|
||||
/**
|
||||
* Read/modify/write
|
||||
*/
|
||||
GBM_BO_TRANSFER_READ_WRITE = (GBM_BO_TRANSFER_READ | GBM_BO_TRANSFER_WRITE),
|
||||
};
|
||||
|
||||
void *
|
||||
gbm_bo_map(struct gbm_bo *bo,
|
||||
uint32_t x, uint32_t y, uint32_t width, uint32_t height,
|
||||
uint32_t flags, uint32_t *stride, void **map_data);
|
||||
|
||||
void
|
||||
gbm_bo_unmap(struct gbm_bo *bo, void *map_data);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_width(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_height(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_stride_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_format(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_bpp(struct gbm_bo *bo);
|
||||
|
||||
uint32_t
|
||||
gbm_bo_get_offset(struct gbm_bo *bo, int plane);
|
||||
|
||||
struct gbm_device *
|
||||
gbm_bo_get_device(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd(struct gbm_bo *bo);
|
||||
|
||||
uint64_t
|
||||
gbm_bo_get_modifier(struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_bo_get_plane_count(struct gbm_bo *bo);
|
||||
|
||||
union gbm_bo_handle
|
||||
gbm_bo_get_handle_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_get_fd_for_plane(struct gbm_bo *bo, int plane);
|
||||
|
||||
int
|
||||
gbm_bo_write(struct gbm_bo *bo, const void *buf, size_t count);
|
||||
|
||||
void
|
||||
gbm_bo_set_user_data(struct gbm_bo *bo, void *data,
|
||||
void (*destroy_user_data)(struct gbm_bo *, void *));
|
||||
|
||||
void *
|
||||
gbm_bo_get_user_data(struct gbm_bo *bo);
|
||||
|
||||
void
|
||||
gbm_bo_destroy(struct gbm_bo *bo);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format, uint32_t flags);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count);
|
||||
|
||||
struct gbm_surface *
|
||||
gbm_surface_create_with_modifiers2(struct gbm_device *gbm,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t format,
|
||||
const uint64_t *modifiers,
|
||||
const unsigned int count,
|
||||
uint32_t flags);
|
||||
|
||||
struct gbm_bo *
|
||||
gbm_surface_lock_front_buffer(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_release_buffer(struct gbm_surface *surface, struct gbm_bo *bo);
|
||||
|
||||
int
|
||||
gbm_surface_has_free_buffers(struct gbm_surface *surface);
|
||||
|
||||
void
|
||||
gbm_surface_destroy(struct gbm_surface *surface);
|
||||
|
||||
char *
|
||||
gbm_format_get_name(uint32_t gbm_format, struct gbm_format_name_desc *desc);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
Vendored
+1
-1
Submodule 3rdparty/glslang updated: d89cf443bc...2cc0c4541b
+42
-86
@@ -304,6 +304,7 @@ 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
|
||||
@@ -371,6 +372,7 @@ set(SOURCE_FILES
|
||||
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
|
||||
@@ -399,7 +401,10 @@ set(SOURCE_FILES
|
||||
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
|
||||
@@ -436,19 +441,17 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Backend/Init.cpp
|
||||
MobileGL/MG_Backend/BackendObject.cpp
|
||||
|
||||
MobileGL/MG_Backend/HostFrameBridge.cpp
|
||||
MobileGL/MG_Backend/GbmFrameChannel.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/HostFrameTarget.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
|
||||
@@ -466,8 +469,10 @@ 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
|
||||
@@ -647,7 +652,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
MobileGL/MG_Remote/Client/DmaBufImport.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
|
||||
@@ -667,12 +674,19 @@ MobileGL/MG_Remote/Client/DmaBufImport.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).
|
||||
@@ -682,6 +696,8 @@ MobileGL/MG_Remote/Client/DmaBufImport.cpp
|
||||
# (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 -
|
||||
@@ -826,17 +842,6 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
|
||||
${MOBILEGL_LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
# A LINUX BUILD COMPILES THE DirectVulkan BACKEND BUT HAS NO VULKAN LOADER TO LINK AGAINST, so the
|
||||
# resulting shared library carries undefined vk* symbols - and that is not theoretical: glvnd dlopens
|
||||
# this file as the EGL vendor with RTLD_NOW, so the load fails outright with "undefined symbol:
|
||||
# vkGetQueryPoolResults" (measured on the phone's container). Android links the loader already; on
|
||||
# Linux the caller passes the runtime's own libvulkan.so.1, and leaving the variable empty keeps the
|
||||
# previous behaviour for every build that has no Vulkan runtime to point at.
|
||||
set(MOBILEGL_VULKAN_LOADER "" CACHE FILEPATH "Vulkan loader (libvulkan.so.1) to link into the GL library")
|
||||
if (UNIX AND NOT ANDROID AND MOBILEGL_VULKAN_LOADER)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${MOBILEGL_VULKAN_LOADER})
|
||||
endif()
|
||||
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
PUBLIC
|
||||
${MOBILEGL_COMPILE_DEF}
|
||||
@@ -846,7 +851,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
|
||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
foreach(MOBILEGL_LOADER_ALIAS
|
||||
libEGL.so libEGL.so.1)
|
||||
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
|
||||
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
|
||||
@@ -903,68 +908,6 @@ if(NOT ANDROID)
|
||||
)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MobileGLGbm: the GBM drop-in the container's compositor loads.
|
||||
#
|
||||
# kwin - the Wayland compositor in the container half of the split - allocates the
|
||||
# buffers it renders into and scans out through libgbm, and the dma-buf fds it gets
|
||||
# back are what the Anland display producer ships to the phone. Mesa's libgbm cannot
|
||||
# serve that: its buffers belong to the host GPU, and the phone would receive a copy
|
||||
# through a path that was never designed to carry one. This target is the library
|
||||
# that is loaded in its place, under the name a loader looks for.
|
||||
#
|
||||
# It is deliberately NOT part of libMobileGL.so. The compositor needs one file named
|
||||
# libgbm.so that it loads instead of Mesa's, and folding this code into the GL
|
||||
# library would put a second GL frontend, a second Vulkan instance and a second
|
||||
# glslang in the compositor's address space to serve an API that shares none of them.
|
||||
#
|
||||
# The include paths below are borrowed rather than linked. Includes.h - which this
|
||||
# library includes for the MobileGL types and the MGLOG macros - pulls in the GL,
|
||||
# EGL, Vulkan, glslang, SPIRV-Cross and xxHash HEADERS transitively, and headers are
|
||||
# all this library wants from them: linking any of those static libraries into a
|
||||
# libgbm would embed a copy of each in a process that already has one.
|
||||
#
|
||||
# -Bsymbolic-functions, for the same reason the main target carries it: the point of
|
||||
# this library is to REPLACE a libgbm that may already be loaded in the process, so
|
||||
# every internal reference has to bind to this file's own definitions rather than to
|
||||
# whatever else the process has under the same names.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_library(MobileGLGbm SHARED
|
||||
MobileGL/MG_Gbm/GbmDevice.cpp
|
||||
# The display host's socket protocol, kept in its own translation unit so that the
|
||||
# allocation paths never have to know how a frame arrives - only that one did.
|
||||
MobileGL/MG_Gbm/GbmHostChannel.cpp
|
||||
MobileGL/MG_Gbm/GbmApi.cpp
|
||||
# MGLOG_I/W/E expand to a call to MobileGL::MG_Util::Debug::Log, and this is the
|
||||
# one translation unit that defines it. It is compiled in rather than linked from
|
||||
# libMobileGL.so because this library has to load into a compositor that has no
|
||||
# libMobileGL in it at all - and because a libgbm with an undefined symbol is a
|
||||
# library that fails on the first diagnostic rather than at a link step somebody
|
||||
# could have read.
|
||||
MobileGL/MG_Util/Debug/Log.cpp
|
||||
)
|
||||
|
||||
# The vendored gbm.h first: this library implements THAT declaration set, and a
|
||||
# system <gbm.h> from another Mesa version on the include path ahead of it would
|
||||
# change the ABI under the implementations in GbmApi.cpp.
|
||||
target_include_directories(MobileGLGbm PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/gbm/include
|
||||
${MOBILEGL_INCLUDE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/glslang
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Cross
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/xxHash
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
|
||||
)
|
||||
|
||||
set_target_properties(MobileGLGbm PROPERTIES
|
||||
OUTPUT_NAME gbm
|
||||
POSITION_INDEPENDENT_CODE ON
|
||||
)
|
||||
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(MobileGLGbm PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
if (TRACY_ENABLE)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC Tracy::TracyClient)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME}_s PUBLIC Tracy::TracyClient)
|
||||
@@ -985,6 +928,15 @@ 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
|
||||
@@ -1058,6 +1010,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
|
||||
@@ -1253,10 +1216,3 @@ if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The GBM library carries the tree's logger, which reaches Android's own log. The main target gets
|
||||
# these libraries in its own Android block; a library built from the same sources needs them for the
|
||||
# same reason, and without them it is the LINK that fails rather than anything in the code.
|
||||
if (ANDROID)
|
||||
target_link_libraries(MobileGLGbm PRIVATE log android)
|
||||
endif()
|
||||
|
||||
+16
-13
@@ -14,7 +14,7 @@ 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, 9, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 10, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -130,6 +130,11 @@ namespace MobileGL::MG_Config {
|
||||
Bool AdvertiseFp64 = 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
|
||||
@@ -423,8 +428,9 @@ namespace MobileGL::MG_Config {
|
||||
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 (create-indirect) is shorter than 120 frames, so a run that
|
||||
// needs its numbers at all sets this low enough to land at least one window.
|
||||
// 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
|
||||
@@ -499,7 +505,6 @@ namespace MobileGL::MG_Config {
|
||||
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
|
||||
Host = 2, // a window surface draws into frames the DISPLAY HOST owns; the host presents
|
||||
};
|
||||
// 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.
|
||||
@@ -629,6 +634,13 @@ namespace MobileGL::MG_Config {
|
||||
// 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);
|
||||
@@ -730,15 +742,6 @@ namespace MobileGL::MG_Config {
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
|
||||
// P12: the other on-screen shape, and the mirror image of the one above. The display host
|
||||
// (the Android side that owns the screen) allocates the frames, offers each one over the
|
||||
// host frame bridge, and puts it on the glass when this side says it is drawn - so a
|
||||
// client that asks for this has no window of its own either, and the server takes the
|
||||
// frames rather than making a window.
|
||||
inline Bool HostFrameWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Host;
|
||||
}
|
||||
#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
|
||||
|
||||
+144
-15
@@ -6,6 +6,9 @@
|
||||
// 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
|
||||
@@ -15,12 +18,10 @@
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
|
||||
// unchanged (G1).
|
||||
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
@@ -49,6 +50,115 @@ 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>>();
|
||||
@@ -79,6 +189,15 @@ 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) {
|
||||
@@ -229,6 +348,7 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
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);
|
||||
@@ -302,6 +422,17 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
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; \
|
||||
@@ -428,6 +559,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
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
|
||||
@@ -494,8 +627,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen): MOBILEGL_IPC_SURFACE = offscreen | server | host (Config.h has the
|
||||
// semantics). An unknown value is named and read as the default, like every knob here.
|
||||
// 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");
|
||||
@@ -505,11 +638,9 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else if (surface == "host") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Host;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server|host, using offscreen",
|
||||
"offscreen|server, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
@@ -517,14 +648,12 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
// 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::Offscreen && !serverProcess &&
|
||||
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=%s is IGNORED - it asks for an on-screen "
|
||||
"surface (the server's own window, or the frames the display host owns) and "
|
||||
"this run has no remote server to provide one: MOBILEGL_TRANSPORT is not "
|
||||
"spawn. Window surfaces stay the client's own",
|
||||
ipc.Surface == MG_Config::IpcSurface::Server ? "server" : "host");
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -8,8 +8,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
|
||||
// ============== 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
|
||||
@@ -112,8 +110,8 @@
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
if (!(condition)) { \
|
||||
fprintf(stderr, "Assertion failed" __VA_OPT__(": ") "%s", "" __VA_ARGS__); \
|
||||
fprintf(stderr, " at %s:%d (%s)\n", __FILE__, __LINE__, __func__); \
|
||||
MGLOG_F("Assertion failed" __VA_OPT__(": ") __VA_ARGS__); \
|
||||
MGLOG_F(" at %s:%d (%s)", __FILE__, __LINE__, __func__); \
|
||||
TRAP; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
#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>
|
||||
@@ -102,6 +105,10 @@ namespace MobileGL {
|
||||
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
|
||||
@@ -220,6 +227,25 @@ namespace MobileGL {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -26,13 +26,29 @@ 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
|
||||
|
||||
@@ -97,6 +97,8 @@ 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);
|
||||
}
|
||||
|
||||
@@ -200,12 +202,15 @@ 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::Window) {
|
||||
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
|
||||
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;
|
||||
@@ -213,28 +218,6 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
// The frame itself is the backend's business (InitHostFrameSurface); what happens here is
|
||||
// the registration every surface kind shares, so the swap path and the state mirrors see
|
||||
// a host-framed surface exactly like any other.
|
||||
Bool BackendObject::CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglDisplayInitialized) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: EGL display is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
MGLOG_E("CreateEGLHostFrameSurface failed: invalid EGLSurface");
|
||||
return false;
|
||||
}
|
||||
auto& state = m_eglSurfaces[surface];
|
||||
state = EGLSurfaceState{
|
||||
.Kind = SurfaceKind::HostFrame,
|
||||
.Width = static_cast<EGLint>(std::max<Uint32>(width, 1)),
|
||||
.Height = static_cast<EGLint>(std::max<Uint32>(height, 1)),
|
||||
};
|
||||
return ActivateEGLSurface(surface);
|
||||
}
|
||||
|
||||
Bool BackendObject::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return RegisterEGLPbufferSurface(surface, width, height) && ActivateEGLSurface(surface);
|
||||
@@ -305,6 +288,7 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_activatingSurface = surface;
|
||||
if (surfaceState->Kind == SurfaceKind::Window) {
|
||||
SetWindowHandle(surfaceState->Window);
|
||||
if (!InitWindowSurface()) {
|
||||
@@ -316,11 +300,6 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitPbufferSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else if (surfaceState->Kind == SurfaceKind::HostFrame) {
|
||||
if (!InitHostFrameSurface(surfaceState->Width, surfaceState->Height)) {
|
||||
MGLOG_E("ActivateEGLSurface failed: backend InitHostFrameSurface failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
MGLOG_E("ActivateEGLSurface failed: unsupported surface kind");
|
||||
return false;
|
||||
@@ -329,8 +308,14 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurface = surface;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = surfaceState->Kind;
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
// 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;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -346,29 +331,6 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
// EGL_KHR_surfaceless_context. A context whose draw and read surfaces are both absent has
|
||||
// no framebuffer of its own to be bound to: the caller draws into framebuffers it binds
|
||||
// itself, and this side only has to have its own GL context current on this thread for the
|
||||
// records that follow. Everything below this point describes a surface, so none of it
|
||||
// applies - and what is recorded is exactly what was asked for, so a later swap on a
|
||||
// surface that does not exist is never made to look like one that does.
|
||||
if (draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT) {
|
||||
if (!m_backendCapabilitiesInitialized) {
|
||||
if (!InitCapabilities()) {
|
||||
MGLOG_E("MakeEGLCurrent failed: InitCapabilities failed");
|
||||
return false;
|
||||
}
|
||||
m_backendCapabilitiesInitialized = true;
|
||||
}
|
||||
ReleaseEGLCurrentThread(threadKey);
|
||||
m_eglCurrentThreads[threadKey] = EGLCurrentState{
|
||||
.Display = dpy,
|
||||
.DrawSurface = EGL_NO_SURFACE,
|
||||
.ReadSurface = EGL_NO_SURFACE,
|
||||
.Context = ctx,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
if (draw != read || !ActivateEGLSurface(draw)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
@@ -420,6 +382,24 @@ 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) {
|
||||
@@ -436,15 +416,15 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
|
||||
return false;
|
||||
}
|
||||
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
|
||||
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
|
||||
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
|
||||
return false;
|
||||
}
|
||||
|
||||
// A host-framed surface has no swapchain: presenting it means telling the host the frame
|
||||
// is drawn, because the host is what puts it on the glass.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame) {
|
||||
return PresentHostFrame();
|
||||
// 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();
|
||||
@@ -464,27 +444,6 @@ namespace MobileGL::MG_Backend {
|
||||
}
|
||||
}
|
||||
|
||||
// A backend that has no host-framed surfaces says so by name: the server may ask for one
|
||||
// (MOBILEGL_IPC_SURFACE=host) against a build that cannot serve it, and that has to be a
|
||||
// refusal with a reason rather than a surface that draws nowhere.
|
||||
Bool BackendObject::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: this backend has no host-framed surfaces");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject::PresentHostFrameIfCurrent() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglSurfaceInitialized || m_eglSurfaceKind != SurfaceKind::HostFrame) return true;
|
||||
return PresentHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject::IsEGLSurfaceCurrent(EGLSurface surface) const {
|
||||
if (surface == EGL_NO_SURFACE) {
|
||||
return false;
|
||||
@@ -507,8 +466,9 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ResetEGLRuntimeState();
|
||||
ForgetActiveEGLSurface();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
|
||||
@@ -539,8 +499,9 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ResetEGLRuntimeState();
|
||||
ForgetActiveEGLSurface();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLResources() {
|
||||
@@ -573,4 +534,8 @@ namespace MobileGL::MG_Backend {
|
||||
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
|
||||
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#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
|
||||
@@ -583,38 +584,256 @@ 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);
|
||||
// The host owns the screen: this side never gets a window of the client's to draw
|
||||
// into, so the surface it draws into is a frame the host allocated and offered over
|
||||
// the host frame bridge. Each swap tells the host the frame is done and takes the
|
||||
// next; presentation is the host's, not a swapchain's.
|
||||
virtual Bool CreateEGLHostFrameSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool InitHostFrameSurface(EGLint width, EGLint height);
|
||||
virtual Bool PresentHostFrame();
|
||||
// The present RECORD's half of a host-framed swap. A split client's swap reaches the
|
||||
// server as a present record (ServerVerbSink::OnPresent), never as SwapEGLBuffers, so
|
||||
// without this the host was never told a frame was drawn. True when the current
|
||||
// surface is not host-framed: there is nothing to present then.
|
||||
Bool PresentHostFrameIfCurrent();
|
||||
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;
|
||||
@@ -628,11 +847,7 @@ namespace MobileGL {
|
||||
enum class SurfaceKind {
|
||||
None,
|
||||
Window,
|
||||
Pbuffer,
|
||||
// A surface whose drawing target is a frame the display host owns. There is no
|
||||
// swapchain behind it: the host puts the frame on the glass when this side says
|
||||
// it is drawn, which is why presenting one is its own call and not a Present.
|
||||
HostFrame
|
||||
Pbuffer
|
||||
};
|
||||
|
||||
struct EGLCurrentState {
|
||||
@@ -645,18 +860,28 @@ 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;
|
||||
@@ -664,6 +889,10 @@ 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;
|
||||
|
||||
@@ -6,12 +6,6 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
// The role's own backend is resolved the way Utils.cpp (C6 / ID-52) resolves it; that needs this
|
||||
// header, and it has to be included at file scope - a header included inside this file's namespace
|
||||
// would nest its own namespace under it.
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include "BackendObject_DirectGLES.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "MG_Backend/BackendObjects.h"
|
||||
@@ -912,16 +906,69 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) {
|
||||
const auto it = m_nativeSurfaces.find(surface);
|
||||
if (it == m_nativeSurfaces.end()) return false;
|
||||
if (DirectGLES::BindSessionSurface(it->second)) return true;
|
||||
// The session no longer owns it (destroyed with a window loss): forget and recreate.
|
||||
m_nativeSurfaces.erase(it);
|
||||
return false;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) {
|
||||
// The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to
|
||||
// whichever context the session had bound (often another window's thread), and putting back
|
||||
// "the surface activated last" instead re-attached THAT context to a third window's surface -
|
||||
// a desktop drawing into and reading back from the panel's pbuffer.
|
||||
if (previousNativeDraw == EGL_NO_SURFACE) return;
|
||||
if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return;
|
||||
for (const auto& [client, native] : m_nativeSurfaces) {
|
||||
if (native != previousNativeDraw) continue;
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) break;
|
||||
m_eglSurface = client;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = state->second.Kind;
|
||||
if (state->second.Kind == SurfaceKind::Window) m_windowHandle = state->second.Window;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitWindowSurface() {
|
||||
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
|
||||
// A suspended surface whose placeholder went: another placeholder - never the window, which
|
||||
// is gone.
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) !=
|
||||
m_suspendedWindowSurfaces.end()) {
|
||||
const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1);
|
||||
if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) {
|
||||
MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p",
|
||||
reinterpret_cast<void*>(m_activatingSurface));
|
||||
return false;
|
||||
}
|
||||
m_nativeSurfaces[m_activatingSurface] = placeholder;
|
||||
return true;
|
||||
}
|
||||
// Only use EGL for now
|
||||
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
||||
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
|
||||
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
|
||||
return false;
|
||||
}
|
||||
if (m_activatingSurface != EGL_NO_SURFACE) {
|
||||
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::LatchIfGpuFaulted() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return LatchIfGuiltyBeforeApply();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::Initialize() {
|
||||
MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
|
||||
MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
|
||||
@@ -982,8 +1029,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
|
||||
//
|
||||
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
|
||||
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
|
||||
// the native context - and with it every buffer, texture and program the application had
|
||||
// built. The native context now belongs to the (session, context token) tuple the backend
|
||||
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
|
||||
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
|
||||
// bookkeeping (which surface is active, which threads made it current) does describe the
|
||||
// surface that is being replaced.
|
||||
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
|
||||
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
|
||||
if (m_eglSurfaceInitialized) {
|
||||
DestroyEGLContext();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
@@ -992,7 +1050,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
|
||||
// pull arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
|
||||
if (created) {
|
||||
if (created && previous != EGL_NO_SURFACE && previous != surface) {
|
||||
// Another surface was current: the creation must not leave the context that held it
|
||||
// drawing to the new one (P14 multi-window clients).
|
||||
RestoreSurfaceAfterCreation(previousNativeDraw);
|
||||
NoteNativeContextGone();
|
||||
} else if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
@@ -1014,8 +1077,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
// P14 S4: as for the window surface above - the native context is the (session, context
|
||||
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
|
||||
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
|
||||
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
|
||||
if (m_eglSurfaceInitialized) {
|
||||
DestroyEGLContext();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
|
||||
@@ -1023,7 +1089,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
|
||||
// arm below is the original statement, byte for byte (G1).
|
||||
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
|
||||
if (created) {
|
||||
if (created && previous != EGL_NO_SURFACE && previous != surface) {
|
||||
RestoreSurfaceAfterCreation(previousNativeDraw);
|
||||
NoteNativeContextGone();
|
||||
} else if (created) {
|
||||
NoteNativeContextFreshFromSurfaceCreation();
|
||||
} else {
|
||||
NoteNativeContextGone();
|
||||
@@ -1034,243 +1103,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
namespace {
|
||||
// The host may still be starting when the client asks for its surface: it is a separate
|
||||
// app that has to come up and allocate its pool. Waiting is better than refusing a
|
||||
// surface a client is entitled to, and the wait is answered by name if it expires.
|
||||
constexpr Int kHostFrameWaitMs = 30000;
|
||||
}
|
||||
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height) {
|
||||
// THIS ROLE'S OWN BACKEND, resolved exactly the way ActiveBackendFormatCaps (Utils.cpp,
|
||||
// C6 / ID-52) resolves it: under a live split session the SERVER's private backend owns the
|
||||
// context on the apply thread, and pActiveBackendObject is the CLIENT's mirror there.
|
||||
MG_Backend::BackendObject* backend = nullptr;
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
} else {
|
||||
backend = MG_Backend::pActiveBackendObject.get();
|
||||
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
const auto state = m_eglSurfaces.find(surface);
|
||||
const auto native = m_nativeSurfaces.find(surface);
|
||||
if (state == m_eglSurfaces.end() || state->second.Kind != SurfaceKind::Pbuffer || native == m_nativeSurfaces.end()) {
|
||||
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
if (backend == nullptr) return false;
|
||||
return backend->InitHostFrameSurface(static_cast<EGLint>(width), static_cast<EGLint>(height));
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
// The EGL context comes from the same place a pbuffer session's does. What changes is not
|
||||
// how this side draws but what it draws INTO: a frame the display host allocated, bound as
|
||||
// this context's framebuffer. The pbuffer stays as the draw target for the moment before
|
||||
// the first frame arrives, which is the only moment it is used.
|
||||
if (!DirectGLES::InitPbufferSurface(width, height)) {
|
||||
MGLOG_E("InitHostFrameSurface: the context the host frames are drawn in did not come up");
|
||||
// A pbuffer has no resize: the client surface gets a new native pbuffer at the new size,
|
||||
// and every context that drew to the old one draws to the new one.
|
||||
const EGLint w = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
const EGLint h = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
const EGLSurface replacement = DirectGLES::CreateSessionPbuffer(w, h);
|
||||
if (replacement == EGL_NO_SURFACE) {
|
||||
MGLOG_E("ResizeEGLWindowSurface: no %dx%d pbuffer could replace the old drawable", w, h);
|
||||
return false;
|
||||
}
|
||||
if (!m_hostFrameBridgeOpen) {
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Open(MG_Backend::kHostFrameSocket, kHostFrameWaitMs, why)) {
|
||||
MGLOG_E("InitHostFrameSurface: no display host: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
m_hostFrameBridgeOpen = true;
|
||||
}
|
||||
return TakeHostFrame();
|
||||
DirectGLES::ReplaceSessionSurface(native->second, replacement);
|
||||
native->second = replacement;
|
||||
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() {
|
||||
String why;
|
||||
struct AHardwareBuffer* frame = nullptr;
|
||||
MG_Backend::HostFrameOffer offer{};
|
||||
if (!m_hostFrameBridge.Acquire(&frame, offer, why)) {
|
||||
MGLOG_E("TakeHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
HostFrameTarget target;
|
||||
if (!HostFrameTargetCreate(frame, target, why)) {
|
||||
MGLOG_E("TakeHostFrame: the host frame %ux%u did not become a draw target: %s", offer.Width,
|
||||
offer.Height, why.c_str());
|
||||
AHardwareBuffer_release(frame);
|
||||
return false;
|
||||
}
|
||||
DestroyHostFrame();
|
||||
m_hostFrameTarget = target;
|
||||
m_hostFrameBuffer = frame;
|
||||
m_hostFrameOffer = offer;
|
||||
// The canvas is made at the first frame's size and kept; a failure leaves the frame itself
|
||||
// as the draw target, which is how this path behaved before the canvas existed.
|
||||
if (!EnsureHostFrameCanvas(target.Width, target.Height))
|
||||
MGLOG_E_ONCE("TakeHostFrame: no canvas in front of the host frames; drawing into the frame itself");
|
||||
const HostFrameTarget& drawTarget = HostFrameDrawTarget();
|
||||
// BEFORE the bind: every default-framebuffer bind the engine makes from here on has to
|
||||
// resolve to this target, and that resolution is what this global is.
|
||||
g_hostFrameFramebufferId = drawTarget.Framebuffer;
|
||||
// Published together with the framebuffer, because they describe the same target: the STORAGE
|
||||
// an attachment is bound to when the client's texture has none of its own, and the SIZE that
|
||||
// decides whether binding it is right at all.
|
||||
g_hostFrameRenderbuffer = drawTarget.Renderbuffer;
|
||||
g_hostFrameWidth = drawTarget.Width;
|
||||
g_hostFrameHeight = drawTarget.Height;
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(drawTarget);
|
||||
// AND ON TO THE CONTAINER, as the dma-buf descriptor behind it. The Wayland compositor
|
||||
// over there renders into THESE frames - it runs this project's own GBM, whose
|
||||
// gbm_bo_create answers with the phone's frame instead of an allocation of its own - so
|
||||
// every frame taken here is useless to it until its descriptor has crossed. Open() binds
|
||||
// the listening socket a container dials, and the IsUp() test keeps the ordinary frame
|
||||
// down to an integer compare: the socket outlives every container and every connection,
|
||||
// while OfferFrame accepts whoever dialled since the last frame and opens the socket
|
||||
// itself when the first frame arrives before any container has.
|
||||
//
|
||||
// NEITHER ANSWER IS EXAMINED, and that is the channel's contract rather than an
|
||||
// oversight: no container running, a container that has died, or one that has taken the
|
||||
// descriptor and stopped reading must all leave this frame drawn and presented exactly as
|
||||
// it would have been without a channel - the descriptor is a copy of the pixels' route,
|
||||
// not a step in producing them.
|
||||
if (!m_gbmFrameChannel.IsUp()) m_gbmFrameChannel.Open();
|
||||
(void)m_gbmFrameChannel.OfferFrame(frame, offer);
|
||||
MGLOG_I("host frame %u is the draw target: %ux%u", offer.Index, target.Width, target.Height);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {
|
||||
// The canvas outlives the frame, so with one the globals keep naming it: the attachments
|
||||
// made over it stay valid between this frame being handed back and the next one arriving.
|
||||
g_hostFrameFramebufferId = m_hostFrameCanvas.Framebuffer;
|
||||
g_hostFrameRenderbuffer = m_hostFrameCanvas.Renderbuffer;
|
||||
g_hostFrameWidth = m_hostFrameCanvas.Width;
|
||||
g_hostFrameHeight = m_hostFrameCanvas.Height;
|
||||
HostFrameTargetDestroy(m_hostFrameTarget);
|
||||
if (m_hostFrameBuffer != nullptr) {
|
||||
AHardwareBuffer_release(m_hostFrameBuffer);
|
||||
m_hostFrameBuffer = nullptr;
|
||||
}
|
||||
// AND NOTHING IS SAID TO THE CHANNEL HERE, which is the fix rather than an omission. This
|
||||
// runs once per presented frame: it used to Close() the channel (unlinking the container's
|
||||
// socket node and dropping its connection sixty times a second - measured: connect()
|
||||
// answered EAGAIN(11) on every attempt) and it used to tell the channel to forget the frame,
|
||||
// which left the ONE window a compositor's first allocations arrive in - between this frame
|
||||
// being presented and the host handing the next one over, i.e. inside Complete() above -
|
||||
// with nothing to answer a dialer with. A compositor allocates its swapchain ONCE and keeps
|
||||
// those buffers for the life of the process, so a dialer that finds nothing here renders
|
||||
// its whole life into buffers nobody displays. The frame the channel remembers is the last
|
||||
// one the host handed over: the next offer replaces it and Close() releases it.
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
// When this returns, the pixels are complete: glFinish is the fence the host is told
|
||||
// about, and the host's own readback - which Complete reads - is what proves the drawing
|
||||
// landed where the screen will look.
|
||||
BlitHostFrameCanvas();
|
||||
if (g_GLESFuncs.glFinish != nullptr) g_GLESFuncs.glFinish();
|
||||
// WHAT THE CONTAINER SAID WHILE THIS FRAME WAS BEING DRAWN. This is the one place the
|
||||
// server's apply loop passes through the host channel - the swap it runs lands here for a
|
||||
// host-framed surface - and a release is the container's compositor saying it is done
|
||||
// reading a frame this side handed over. Poll() is a bare recvmsg on a non-blocking
|
||||
// socket: with no container connected it returns on its first test, and a container that
|
||||
// is between frames answers EAGAIN, so a pass costs nothing when there is nothing to
|
||||
// read. Nothing here is waited for and nothing is required to arrive; the count of
|
||||
// frames offered and not released is a number the channel keeps for the log, never a
|
||||
// frame the render path waits on.
|
||||
m_gbmFrameChannel.Poll();
|
||||
String why;
|
||||
if (!m_hostFrameBridge.Complete(m_hostFrameOffer, /*imported=*/true, /*drawn=*/true, /*fenceOk=*/true, why)) {
|
||||
MGLOG_E("PresentHostFrame: %s", why.c_str());
|
||||
return false;
|
||||
}
|
||||
// The answer has been read, so the host has already put this frame on the glass: it is
|
||||
// this side's to drop, and the next one becomes the drawing target.
|
||||
DestroyHostFrame();
|
||||
return TakeHostFrame();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint width, Uint height) {
|
||||
if (m_hostFrameCanvas.Framebuffer != 0) return true;
|
||||
if (width == 0 || height == 0 || !g_GLESFuncs.glRenderbufferStorage || !g_GLESFuncs.glBlitFramebuffer ||
|
||||
!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glGenFramebuffers || !g_GLESFuncs.glCheckFramebufferStatus)
|
||||
return false;
|
||||
// RAW binds, as HostFrameTargetCreate makes them: the caller invalidates the engine's
|
||||
// framebuffer shadow and binds through it right after.
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
// The frames are R8G8B8A8_UNORM (the display host allocates them so), and so is this.
|
||||
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height));
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E("host frame canvas %ux%u: framebuffer status 0x%04x", width, height, status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
return false;
|
||||
}
|
||||
m_hostFrameCanvas.Renderbuffer = renderbuffer;
|
||||
m_hostFrameCanvas.Framebuffer = framebuffer;
|
||||
m_hostFrameCanvas.Width = width;
|
||||
m_hostFrameCanvas.Height = height;
|
||||
MGLOG_I("host frame canvas: %ux%u as framebuffer %u (renderbuffer %u), copied into each frame at present",
|
||||
width, height, framebuffer, renderbuffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {
|
||||
if (m_hostFrameCanvas.Framebuffer == 0 || m_hostFrameTarget.Framebuffer == 0) return;
|
||||
// Whatever the compositor had bound is put back as it was, raw for raw, so the engine's
|
||||
// shadow (which never saw this) is still right afterwards. The scissor test and
|
||||
// rasterizer discard both apply to a blit, so they are lifted for it.
|
||||
GLint readBinding = 0;
|
||||
GLint drawBinding = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readBinding);
|
||||
g_GLESFuncs.glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawBinding);
|
||||
const GLboolean scissor = g_GLESFuncs.glIsEnabled(GL_SCISSOR_TEST);
|
||||
const GLboolean discard = g_GLESFuncs.glIsEnabled(GL_RASTERIZER_DISCARD);
|
||||
if (scissor) g_GLESFuncs.glDisable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glDisable(GL_RASTERIZER_DISCARD);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, m_hostFrameCanvas.Framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_hostFrameTarget.Framebuffer);
|
||||
const Bool sameSize = m_hostFrameCanvas.Width == m_hostFrameTarget.Width &&
|
||||
m_hostFrameCanvas.Height == m_hostFrameTarget.Height;
|
||||
g_GLESFuncs.glBlitFramebuffer(0, 0, static_cast<GLint>(m_hostFrameCanvas.Width),
|
||||
static_cast<GLint>(m_hostFrameCanvas.Height), 0, 0,
|
||||
static_cast<GLint>(m_hostFrameTarget.Width),
|
||||
static_cast<GLint>(m_hostFrameTarget.Height), GL_COLOR_BUFFER_BIT,
|
||||
sameSize ? GL_NEAREST : GL_LINEAR);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_READ_FRAMEBUFFER, static_cast<GLuint>(readBinding));
|
||||
g_GLESFuncs.glBindFramebuffer(GL_DRAW_FRAMEBUFFER, static_cast<GLuint>(drawBinding));
|
||||
if (scissor) g_GLESFuncs.glEnable(GL_SCISSOR_TEST);
|
||||
if (discard) g_GLESFuncs.glEnable(GL_RASTERIZER_DISCARD);
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
// The other arm's half: on a build with no host frames there is no frame to take, and the
|
||||
// attach site's ordinary path (a storage-less external texture, exactly as before) is the honest
|
||||
// behaviour there rather than a link error.
|
||||
Bool TakeHostFrameForExternalTexture(Uint, Uint) { return false; }
|
||||
|
||||
Bool BackendObject_DirectGLES::InitHostFrameSurface(EGLint width, EGLint height) {
|
||||
(void)width;
|
||||
(void)height;
|
||||
MGLOG_E("InitHostFrameSurface: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::TakeHostFrame() { return false; }
|
||||
void BackendObject_DirectGLES::DestroyHostFrame() {}
|
||||
Bool BackendObject_DirectGLES::EnsureHostFrameCanvas(Uint, Uint) { return false; }
|
||||
void BackendObject_DirectGLES::BlitHostFrameCanvas() {}
|
||||
|
||||
Bool BackendObject_DirectGLES::PresentHostFrame() {
|
||||
MGLOG_E("PresentHostFrame: host frames are a disaggregated Android path");
|
||||
return false;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
|
||||
return DirectGLES::InitPbufferSurface(width, height);
|
||||
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
|
||||
if (!DirectGLES::InitPbufferSurface(width, height)) return false;
|
||||
if (m_activatingSurface != EGL_NO_SURFACE) {
|
||||
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
@@ -1293,42 +1153,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL display mismatch or not initialized");
|
||||
return false;
|
||||
}
|
||||
// EGL_KHR_surfaceless_context, WHICH IS THE SHAPE A COMPOSITOR ASKS FOR. Its context has no
|
||||
// draw surface of its own - every frame goes into a framebuffer it binds itself, and under
|
||||
// this project that framebuffer is one of the display host's frames - so the surface checks
|
||||
// below describe a surface that legitimately does not exist. What the server still needs is
|
||||
// its own GL context current on this thread for the records that follow, and that is what
|
||||
// the native bind underneath them does.
|
||||
const Bool surfaceless = draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx != EGL_NO_CONTEXT;
|
||||
if (surfaceless && !m_eglSurfaceInitialized) {
|
||||
// A DRAWABLE OF THIS SIDE'S OWN, WHICH IS WHAT A SURFACELESS CLIENT LEAVES TO US. The
|
||||
// backend's native context has to be current on a surface of some kind before a single
|
||||
// record of the client's can run - that is what the Init*Surface calls establish - and a
|
||||
// client that makes its context current with no surface at all supplies none of its own.
|
||||
// A pbuffer is the one that costs nothing to keep: it is never scanned out and never
|
||||
// read, because every frame the client draws goes into a framebuffer it binds itself.
|
||||
// The client is NOT told it has a surface - m_eglSurface stays EGL_NO_SURFACE - so a
|
||||
// swap on one is still the refusal it was.
|
||||
if (!InitPbufferSurface(1, 1)) {
|
||||
MGLOG_E("MakeEGLCurrent failed: the backend's own drawable for a surfaceless context could not "
|
||||
"be created");
|
||||
return false;
|
||||
}
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = SurfaceKind::Pbuffer;
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (!surfaceless) {
|
||||
if (!m_eglSurfaceInitialized) {
|
||||
MGLOG_E("MakeEGLCurrent failed: EGL surface is not initialized");
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || read == EGL_NO_SURFACE || ctx == EGL_NO_CONTEXT) {
|
||||
MGLOG_E("MakeEGLCurrent failed: draw/read/context is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -1356,10 +1187,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
|
||||
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
|
||||
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
|
||||
m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
DirectGLES::IsBackendContextCurrentOnThisThread() &&
|
||||
(surfaceless || (m_eglSurfaceInitialized && m_eglSurface == draw &&
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx)));
|
||||
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
|
||||
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
|
||||
// The draw surface's own native surface (P14 multi-window): a known one is re-bound, so the
|
||||
// bind goes to it and not to whichever surface the session drew to last.
|
||||
// The real bind still runs whenever the skip above says so (ID-67: a different tuple onto
|
||||
// the same surface needs MakeCurrent's invalidations even though the driver triple is equal).
|
||||
const auto nativeDraw = m_nativeSurfaces.find(draw);
|
||||
if (!nativelyCurrentAlready && nativeDraw != m_nativeSurfaces.end()) {
|
||||
(void)DirectGLES::SelectSessionSurface(nativeDraw->second);
|
||||
}
|
||||
const Bool bound = nativelyCurrentAlready || DirectGLES::MakeCurrent();
|
||||
if (!bound) {
|
||||
NoteNativeContextGone();
|
||||
return false;
|
||||
}
|
||||
@@ -1377,18 +1217,44 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
// A HOST-FRAMED SURFACE HAS TO BE PUT BACK AFTER EVERY MAKE-CURRENT. The native call
|
||||
// binds the surface's own default framebuffer - the pbuffer this path makes its context
|
||||
// with - and that is not where these pixels go. Both the driver and the engine's binding
|
||||
// shadow have to be told again, or the next default-framebuffer bind is deduped away and
|
||||
// every draw lands in a pbuffer nobody sees.
|
||||
if (m_eglSurfaceKind == SurfaceKind::HostFrame && m_hostFrameTarget.Framebuffer != 0) {
|
||||
FramebufferImpl::InvalidateFramebufferBindingCache();
|
||||
HostFrameTargetBind(HostFrameDrawTarget());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region);
|
||||
#else
|
||||
(void)image, (void)region;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Int32 BackendObject_DirectGLES::QueryCurrentBufferAge(Bool damageRegionFollows) {
|
||||
return DirectGLES::CurrentDrawBufferAge(damageRegionFollows);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
|
||||
return DirectGLES::SetCurrentDrawDamageRegion(region);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::PublishSharedImageAccesses() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::PublishPendingAccesses();
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return DirectGLES::SharedImageImpl::ExportNativeFence(fence);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
@@ -1404,15 +1270,116 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
m_suspendedWindowSurfaces.clear();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
DestroyEGLContext();
|
||||
Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Vector<EGLSurface> onWindow;
|
||||
for (const auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
|
||||
m_suspendedWindowSurfaces.end())
|
||||
continue;
|
||||
onWindow.push_back(client);
|
||||
}
|
||||
for (const EGLSurface client : onWindow) {
|
||||
// A surface never activated has no native surface: only its record names the window.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native != m_nativeSurfaces.end()) {
|
||||
const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second);
|
||||
if (placeholder == EGL_NO_SURFACE) return false;
|
||||
native->second = placeholder;
|
||||
}
|
||||
m_suspendedWindowSurfaces.push_back(client);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The native binding moved onto the placeholder: the next make-current binds for real.
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
|
||||
"contexts and their objects stay",
|
||||
onWindow.size(), window);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Vector<EGLSurface> suspended;
|
||||
suspended.swap(m_suspendedWindowSurfaces);
|
||||
for (const EGLSurface client : suspended) {
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) continue; // released while suspended
|
||||
state->second.Window.Handle = window;
|
||||
state->second.Window.Width = width;
|
||||
state->second.Window.Height = height;
|
||||
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state->second.Window;
|
||||
// Never activated: its first activation creates it on the new window.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native == m_nativeSurfaces.end()) continue;
|
||||
const EGLSurface surface =
|
||||
DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast<NativeWindowType>(window));
|
||||
if (surface == EGL_NO_SURFACE) return false;
|
||||
native->second = surface;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
|
||||
window, width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr) return false;
|
||||
Bool published = false;
|
||||
for (auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
|
||||
m_suspendedWindowSurfaces.end())
|
||||
continue;
|
||||
state.Window.Width = width;
|
||||
state.Window.Height = height;
|
||||
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state.Window;
|
||||
// The window surface's buffers follow the window by themselves (from the next one the
|
||||
// driver dequeues); only the client has to be told.
|
||||
const auto native = m_nativeSurfaces.find(client);
|
||||
if (native == m_nativeSurfaces.end()) continue;
|
||||
published = DirectGLES::RepublishWindowSurfaceShape(native->second,
|
||||
reinterpret_cast<NativeWindowType>(window)) ||
|
||||
published;
|
||||
}
|
||||
return published;
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
// P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the
|
||||
// contexts. This used to be DestroyEGLContext() - every native context of the session, with
|
||||
// every object the client had built - which a multi-window client hit on its first closed
|
||||
// popup.
|
||||
(void)surface;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
NoteNativeContextGone();
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
m_suspendedWindowSurfaces.erase(
|
||||
std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface),
|
||||
m_suspendedWindowSurfaces.end());
|
||||
const auto it = m_nativeSurfaces.find(surface);
|
||||
if (it == m_nativeSurfaces.end()) return;
|
||||
const EGLSurface native = it->second;
|
||||
m_nativeSurfaces.erase(it);
|
||||
DirectGLES::DestroySessionSurface(native);
|
||||
}
|
||||
|
||||
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
|
||||
@@ -1444,6 +1411,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
|
||||
// trivially true for every context this backend claims (4.6). KWin 6's
|
||||
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
|
||||
// strings, so withholding them fails its renderer bring-up on an otherwise capable
|
||||
// implementation.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
|
||||
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
|
||||
@@ -1542,40 +1516,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend now rather than a test unlock: every
|
||||
// procedure they define is implemented in MG_Impl/GLImpl/Exporting/Definitions.cpp over
|
||||
// the same shader and program objects the core entry points drive - the 39 of
|
||||
// ARB_shader_objects, ARB_vertex_shader's three attrib-location entry points, and none
|
||||
// at all for ARB_fragment_shader ("New Procedures and Functions: None" - it is the
|
||||
// capability half, and the reason kwin's conjunction names it).
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: core since GL 2.0, and every host this backend can run on provides
|
||||
// them. Full non-power-of-two support - mipmapping and every wrap mode, not ES 2.0's
|
||||
// CLAMP_TO_EDGE-only exception - is required from ES 3.0 on, and the entries this list
|
||||
// already advertises without any gate (compute shaders, shader storage buffers,
|
||||
// indirect draws) need an ES 3.1 host. Nothing in the texture path rejects or pads a
|
||||
// non-power-of-two size - storage and sampling are forwarded to that host - so the
|
||||
// string describes what an application actually gets.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Minecraft 26.3 checks this prerequisite before it even considers
|
||||
// GL_ARB_multi_draw_indirect. ES 3.1 supplies both single-draw entry points; the loader
|
||||
// folds the version and pointer checks into SupportsDrawIndirect.
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "HostFrameTarget.h"
|
||||
#include "MG_Backend/HostFrameBridge.h"
|
||||
#include "MG_Backend/GbmFrameChannel.h"
|
||||
#include "../BackendObject.h"
|
||||
#include <MG_Util/BackendLoaders/OpenGL/Loader.h>
|
||||
|
||||
@@ -35,16 +32,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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;
|
||||
@@ -59,47 +78,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
private:
|
||||
void UpdateDynamicBackendParameters();
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
// A host-framed surface draws into a frame the display host owns; presenting one
|
||||
// tells the host the frame is drawn, because the host is what puts it on the glass.
|
||||
// NOT CreateEGLHostFrameSurface: the base class registers the surface and activates it,
|
||||
// and activation is what reaches InitHostFrameSurface below. There is nothing about the
|
||||
// EGL side of a host-framed surface this backend needs to do its own way.
|
||||
Bool InitHostFrameSurface(EGLint width, EGLint height) override;
|
||||
Bool PresentHostFrame() override;
|
||||
// Takes the next frame from the host and makes it this context's framebuffer.
|
||||
Bool TakeHostFrame();
|
||||
void DestroyHostFrame();
|
||||
// The canvas the compositor draws into, made once at the first frame's size.
|
||||
Bool EnsureHostFrameCanvas(Uint width, Uint height);
|
||||
// Copies the canvas into the frame about to be presented.
|
||||
void BlitHostFrameCanvas();
|
||||
// What draws resolve to: the canvas when there is one, the frame itself otherwise.
|
||||
const HostFrameTarget& HostFrameDrawTarget() const {
|
||||
return m_hostFrameCanvas.Framebuffer != 0 ? m_hostFrameCanvas : m_hostFrameTarget;
|
||||
}
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: the frames the display host owns, one at a time, which
|
||||
// is what the host offers - it hands one over and waits for the answer before it
|
||||
// presents it and offers the next.
|
||||
MG_Backend::HostFrameBridge m_hostFrameBridge;
|
||||
// The frames the bridge just took, on to the container: the Wayland compositor over there
|
||||
// renders into this same memory, and the only route to it that end has is the dma-buf
|
||||
// descriptor this channel offers per frame and reads a release back from.
|
||||
MG_Backend::GbmFrameChannel m_gbmFrameChannel;
|
||||
HostFrameTarget m_hostFrameTarget;
|
||||
// A STABLE TARGET IN FRONT OF THE FRAMES. Every present hands the frame back and takes
|
||||
// a different one, and its renderbuffer is deleted with it - but a compositor attaches
|
||||
// its framebuffers ONCE (kwin: one per daemon buffer, for the life of the process), so
|
||||
// attachments bound to a frame's own renderbuffer named a deleted object from the second
|
||||
// frame on. The attachments name this instead, and each present copies it into the
|
||||
// frame. No EGLImage behind it (Image stays null).
|
||||
HostFrameTarget m_hostFrameCanvas;
|
||||
struct AHardwareBuffer* m_hostFrameBuffer = nullptr;
|
||||
MG_Backend::HostFrameOffer m_hostFrameOffer{};
|
||||
Bool m_hostFrameBridgeOpen = false;
|
||||
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;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,11 +111,72 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, 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
|
||||
@@ -204,6 +265,200 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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
|
||||
@@ -247,28 +502,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// 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
|
||||
|
||||
// MOBILEGL_IPC_SURFACE=host: what the client's DEFAULT framebuffer means while a host-framed
|
||||
// surface is current. Zero everywhere else, because the driver's own 0 IS the EGL surface
|
||||
// then - but on a host-framed surface the pixels belong to a frame the display host owns,
|
||||
// and drawing into the driver's 0 would draw into a pbuffer nobody ever sees.
|
||||
extern Uint g_hostFrameFramebufferId;
|
||||
// WHAT THE HOST FRAME'S STORAGE IS, for the one caller that has to bind it in place of a
|
||||
// storage-less attachment: an imported dma-buf is the display host's frame (same memory, same
|
||||
// size), and the frame's storage is a RENDERBUFFER (HostFrameTarget.cpp binds it with
|
||||
// glFramebufferRenderbuffer). A texture argument cannot name it, which is why these exist and
|
||||
// why g_hostFrameFramebufferId alone was not enough. Zero whenever no frame is held.
|
||||
extern Uint g_hostFrameRenderbuffer;
|
||||
extern Uint g_hostFrameWidth;
|
||||
extern Uint g_hostFrameHeight;
|
||||
// TAKE THE FRAME WHEN NOTHING ELSE WILL. On the anland route the container's compositor never
|
||||
// creates a window surface, so ActivateEGLSurface never runs and InitHostFrameSurface is never
|
||||
// reached: the FIRST proof that the display host's frame is wanted is an ATTACHMENT of an
|
||||
// imported external texture (Managers.cpp's 0x8D65 arm), and that is where the frame has to be
|
||||
// taken. The host's own offer carries the width/height, so the numbers passed here are only
|
||||
// what the bridge logs. Answers false (and changes nothing) whenever no backend is up.
|
||||
Bool TakeHostFrameForExternalTexture(Uint width, Uint height);
|
||||
#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;
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.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 "HostFrameTarget.h"
|
||||
#include "DirectGLES.h"
|
||||
#include "Managers.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
|
||||
|
||||
#include <android/hardware_buffer.h>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
namespace {
|
||||
// The four entry points this is spelled in are optional everywhere: the loader's own
|
||||
// tables do not carry them (a missing EGL function there is MGLOG_F), so the extension
|
||||
// strings decide first and the pointers are taken only once they have.
|
||||
using GetNativeClientBufferFn = EGLClientBuffer (*)(const struct AHardwareBuffer*);
|
||||
using CreateImageFn = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using DestroyImageFn = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
using ImageTargetRenderbufferFn = void (*)(GLenum, GLeglImageOES);
|
||||
|
||||
Bool HasExtensionWord(const char* list, const char* word) {
|
||||
if (list == nullptr) return false;
|
||||
const SizeT n = std::strlen(word);
|
||||
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
|
||||
const Bool startOk = at == list || at[-1] == ' ';
|
||||
const Bool endOk = !at[n] || at[n] == ' ';
|
||||
if (startOk && endOk) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct EntryPoints {
|
||||
GetNativeClientBufferFn GetNativeClientBuffer = nullptr;
|
||||
CreateImageFn CreateImage = nullptr;
|
||||
DestroyImageFn DestroyImage = nullptr;
|
||||
ImageTargetRenderbufferFn TargetRenderbuffer = nullptr;
|
||||
};
|
||||
|
||||
// Resolved once per process and cached together with the refusal: a driver that lacks one
|
||||
// of these does not grow it later, and the answer names what is missing.
|
||||
const EntryPoints& Resolved(String& why) {
|
||||
static EntryPoints points;
|
||||
static Bool resolved = false;
|
||||
static String refusal;
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
if (!g_EGLFuncs.eglGetProcAddress || !g_EGLFuncs.eglQueryString || !g_EGLFuncs.eglGetCurrentDisplay ||
|
||||
!g_GLESFuncs.glGetString) {
|
||||
refusal = "the loader has no eglGetProcAddress/eglQueryString/eglGetCurrentDisplay/glGetString";
|
||||
} else {
|
||||
const char* egl = g_EGLFuncs.eglQueryString(g_EGLFuncs.eglGetCurrentDisplay(), EGL_EXTENSIONS);
|
||||
const char* gl = reinterpret_cast<const char*>(g_GLESFuncs.glGetString(GL_EXTENSIONS));
|
||||
if (!HasExtensionWord(egl, "EGL_ANDROID_get_native_client_buffer")) {
|
||||
refusal = "EGL_ANDROID_get_native_client_buffer is not advertised";
|
||||
} else if (!HasExtensionWord(egl, "EGL_KHR_image_base")) {
|
||||
refusal = "EGL_KHR_image_base is not advertised";
|
||||
} else if (!HasExtensionWord(gl, "GL_OES_EGL_image")) {
|
||||
refusal = "GL_OES_EGL_image is not advertised";
|
||||
} else {
|
||||
points.GetNativeClientBuffer = reinterpret_cast<GetNativeClientBufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("eglGetNativeClientBufferANDROID"));
|
||||
points.CreateImage =
|
||||
reinterpret_cast<CreateImageFn>(g_EGLFuncs.eglGetProcAddress("eglCreateImageKHR"));
|
||||
points.DestroyImage =
|
||||
reinterpret_cast<DestroyImageFn>(g_EGLFuncs.eglGetProcAddress("eglDestroyImageKHR"));
|
||||
points.TargetRenderbuffer = reinterpret_cast<ImageTargetRenderbufferFn>(
|
||||
g_EGLFuncs.eglGetProcAddress("glEGLImageTargetRenderbufferStorageOES"));
|
||||
if (points.GetNativeClientBuffer == nullptr || points.CreateImage == nullptr ||
|
||||
points.DestroyImage == nullptr || points.TargetRenderbuffer == nullptr) {
|
||||
refusal = "eglGetProcAddress gave no eglGetNativeClientBufferANDROID / eglCreateImageKHR / "
|
||||
"eglDestroyImageKHR / glEGLImageTargetRenderbufferStorageOES";
|
||||
points = EntryPoints{};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
why = refusal;
|
||||
return points;
|
||||
}
|
||||
|
||||
void DrainGLErrors() {
|
||||
if (!g_GLESFuncs.glGetError) return;
|
||||
for (Uint guard = 0; guard < 16 && g_GLESFuncs.glGetError() != GL_NO_ERROR; ++guard) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
const EntryPoints& points = Resolved(why);
|
||||
return points.GetNativeClientBuffer != nullptr;
|
||||
}
|
||||
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why) {
|
||||
target = HostFrameTarget{};
|
||||
if (buffer == nullptr) {
|
||||
why = "no buffer";
|
||||
return false;
|
||||
}
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.GetNativeClientBuffer == nullptr) return false;
|
||||
|
||||
if (!g_GLESFuncs.glGenRenderbuffers || !g_GLESFuncs.glBindRenderbuffer || !g_GLESFuncs.glGenFramebuffers ||
|
||||
!g_GLESFuncs.glBindFramebuffer || !g_GLESFuncs.glFramebufferRenderbuffer ||
|
||||
!g_GLESFuncs.glCheckFramebufferStatus) {
|
||||
why = "the loader has no glGenRenderbuffers/glBindRenderbuffer/glGenFramebuffers/glBindFramebuffer/"
|
||||
"glFramebufferRenderbuffer/glCheckFramebufferStatus";
|
||||
return false;
|
||||
}
|
||||
|
||||
AHardwareBuffer_Desc desc{};
|
||||
AHardwareBuffer_describe(buffer, &desc);
|
||||
|
||||
DrainGLErrors();
|
||||
const EGLClientBuffer clientBuffer = points.GetNativeClientBuffer(buffer);
|
||||
if (clientBuffer == nullptr) {
|
||||
why = "eglGetNativeClientBufferANDROID returned null";
|
||||
return false;
|
||||
}
|
||||
const EGLint imageAttribs[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE};
|
||||
const EGLImageKHR image =
|
||||
points.CreateImage(g_EGLFuncs.eglGetCurrentDisplay(), EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID,
|
||||
clientBuffer, imageAttribs);
|
||||
if (image == EGL_NO_IMAGE_KHR) {
|
||||
why = "eglCreateImageKHR(EGL_NATIVE_BUFFER_ANDROID) refused the host frame";
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint renderbuffer = 0;
|
||||
g_GLESFuncs.glGenRenderbuffers(1, &renderbuffer);
|
||||
g_GLESFuncs.glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
|
||||
points.TargetRenderbuffer(GL_RENDERBUFFER, reinterpret_cast<GLeglImageOES>(image));
|
||||
const GLenum renderbufferError = g_GLESFuncs.glGetError ? g_GLESFuncs.glGetError() : GL_NO_ERROR;
|
||||
if (renderbufferError != GL_NO_ERROR) {
|
||||
why = std::format("glEGLImageTargetRenderbufferStorageOES -> 0x{:04x}", renderbufferError);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
GLuint framebuffer = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
why = std::format("the host frame as a color attachment -> framebuffer status 0x{:04x}", status);
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &framebuffer);
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &renderbuffer);
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), image);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.Image = image;
|
||||
target.Renderbuffer = renderbuffer;
|
||||
target.Framebuffer = framebuffer;
|
||||
target.Width = desc.width;
|
||||
target.Height = desc.height;
|
||||
MGLOG_I("host frame target: %ux%d as framebuffer %u (stride %u, format 0x%x)", desc.width, desc.height,
|
||||
framebuffer, desc.stride, desc.format);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HostFrameTargetBind(const HostFrameTarget& target) {
|
||||
if (target.Framebuffer == 0) return;
|
||||
// THROUGH THE ENGINE'S SHADOW, never raw: a raw bind would leave the shadow claiming
|
||||
// the previous framebuffer and false-skip the next re-bind (FramebufferImpl, Managers.h).
|
||||
FramebufferImpl::BindFramebufferId(GL_FRAMEBUFFER, target.Framebuffer);
|
||||
}
|
||||
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) {
|
||||
if (target.Framebuffer != 0 && g_GLESFuncs.glDeleteFramebuffers)
|
||||
g_GLESFuncs.glDeleteFramebuffers(1, &target.Framebuffer);
|
||||
if (target.Renderbuffer != 0 && g_GLESFuncs.glDeleteRenderbuffers)
|
||||
g_GLESFuncs.glDeleteRenderbuffers(1, &target.Renderbuffer);
|
||||
if (target.Image != nullptr) {
|
||||
String why;
|
||||
const EntryPoints& points = Resolved(why);
|
||||
if (points.DestroyImage != nullptr && g_EGLFuncs.eglGetCurrentDisplay)
|
||||
points.DestroyImage(g_EGLFuncs.eglGetCurrentDisplay(), static_cast<EGLImageKHR>(target.Image));
|
||||
}
|
||||
target = HostFrameTarget{};
|
||||
}
|
||||
}
|
||||
|
||||
#else // !(MOBILEGL_BUILD_DISAGGREGATED && __ANDROID__)
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool HostFrameTargetSupported(String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer*, HostFrameTarget&, String& why) {
|
||||
why = "host frames are a disaggregated Android path";
|
||||
return false;
|
||||
}
|
||||
void HostFrameTargetBind(const HostFrameTarget&) {}
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target) { target = HostFrameTarget{}; }
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,52 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectGLES/HostFrameTarget.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 <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
struct AHardwareBuffer;
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// THE FRAME THE DISPLAY HOST IS ABOUT TO SCAN OUT, AS A DRAW TARGET.
|
||||
//
|
||||
// The display host (the Android side that owns the screen) allocates its frames itself and
|
||||
// offers each one over a socket as an AHardwareBuffer. That is not a convenience: measured
|
||||
// on the device, AHardwareBuffer_createFromHandle accepts an AHardwareBuffer this side
|
||||
// allocated and refuses one the display queue handed over (gralloc BAD_VALUE), so the frames
|
||||
// a render server can draw into are the ones the host made for it.
|
||||
//
|
||||
// What this makes of such a frame: an EGLImage the driver accepts as a renderbuffer, and a
|
||||
// framebuffer that is the drawing target while that frame is the current one. None of
|
||||
// Android's window machinery is involved - no ANativeWindow, no SurfaceFlinger - which is
|
||||
// the point: the host puts the frame on the glass when this side says it is drawn.
|
||||
struct HostFrameTarget {
|
||||
void* Image = nullptr; // EGLImageKHR; opaque here so this header stays free of EGL types
|
||||
Uint Renderbuffer = 0;
|
||||
Uint Framebuffer = 0;
|
||||
Uint Width = 0;
|
||||
Uint Height = 0;
|
||||
};
|
||||
|
||||
// Whether the driver advertises what this is spelled in. False is answered with the missing
|
||||
// name in `why`, never an abort: a server that cannot take host frames has to be able to
|
||||
// say so.
|
||||
Bool HostFrameTargetSupported(String& why);
|
||||
|
||||
// Imports `buffer` as a render target. On failure `target` is left empty and `why` says
|
||||
// which call refused; no GL name is leaked.
|
||||
Bool HostFrameTargetCreate(const struct AHardwareBuffer* buffer, HostFrameTarget& target, String& why);
|
||||
|
||||
// Binds the target as the current draw framebuffer, called when its frame becomes current.
|
||||
void HostFrameTargetBind(const HostFrameTarget& target);
|
||||
|
||||
// Drops the GL names and the EGLImage. The AHardwareBuffer belongs to the bridge, which
|
||||
// releases it; this never touches it.
|
||||
void HostFrameTargetDestroy(HostFrameTarget& target);
|
||||
}
|
||||
@@ -206,6 +206,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#else
|
||||
Bool InProcessTeardown() { return g_processTeardown; }
|
||||
#endif
|
||||
Bool InProcessExit() { return g_processTeardown; }
|
||||
void EnsureProcessTeardownSentinel() {
|
||||
std::call_once(g_teardownSentinelOnce,
|
||||
[] { std::atexit(+[] { g_processTeardown = true; }); });
|
||||
@@ -416,17 +417,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the textures they view, framebuffers before their attachments, VAOs before the buffers they
|
||||
// fetch from), so the last reference to each twin goes here and its destructor - answered
|
||||
// InProcessTeardown() - touches neither the driver nor another TU's statics.
|
||||
//
|
||||
// P14 S6: "EVERY TABLE" IS NOW "EVERY BUCKET OF THIS SESSION". The tables are keyed by
|
||||
// {session, share group}, so the same rebuild of the eight registries would take a NEIGHBOUR
|
||||
// session's twins with it - and with S2's in-process supervisor serving several sessions at
|
||||
// once that is not a hypothetical: this runs from ~BackendObject_DirectGLES, on the apply
|
||||
// thread of the session that is ending. So the drop is scoped to the calling thread's
|
||||
// session when it has one, and is the process-wide drop (key {0, 0} and nothing else) when it
|
||||
// does not - which is the shape every unit case and the fork-worker world still has.
|
||||
void DropEveryTwinForEndedServerSession() {
|
||||
if (g_processTeardown) return;
|
||||
g_serverSessionTwinTeardown = true;
|
||||
SamplerViewImpl::g_backendSamplerViews = {};
|
||||
FramebufferImpl::g_backendFramebufferObjects = {};
|
||||
VertexArrayImpl::g_backendVertexArrayObjects = {};
|
||||
PrgramImpl::g_backendProgramObjects = {};
|
||||
SamplerImpl::g_backendSamplerObjects = {};
|
||||
TextureImpl::g_backendTextureObjects = {};
|
||||
RenderbufferImpl::g_backendRenderbufferObjects = {};
|
||||
BufferImpl::g_backendBufferResources = {};
|
||||
TwinKey key;
|
||||
if (CurrentTwinKey(&key) && key.SessionKey != 0) {
|
||||
Uint32 buckets = 0;
|
||||
buckets += SamplerViewImpl::g_backendSamplerViews.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += FramebufferImpl::g_backendFramebufferObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += VertexArrayImpl::g_backendVertexArrayObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += PrgramImpl::g_backendProgramObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += SamplerImpl::g_backendSamplerObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += TextureImpl::g_backendTextureObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += RenderbufferImpl::g_backendRenderbufferObjects.DropBucketsOfSession(key.SessionKey);
|
||||
buckets += BufferImpl::g_backendBufferResources.DropBucketsOfSession(key.SessionKey);
|
||||
MGLOG_I("DirectGLES: the ended server session dropped %u twin group(s) across eight kinds "
|
||||
"(no driver call); the other sessions in this process keep theirs",
|
||||
buckets);
|
||||
} else {
|
||||
SamplerViewImpl::g_backendSamplerViews.ResetBuckets();
|
||||
FramebufferImpl::g_backendFramebufferObjects.ResetBuckets();
|
||||
VertexArrayImpl::g_backendVertexArrayObjects.ResetBuckets();
|
||||
PrgramImpl::g_backendProgramObjects.ResetBuckets();
|
||||
SamplerImpl::g_backendSamplerObjects.ResetBuckets();
|
||||
TextureImpl::g_backendTextureObjects.ResetBuckets();
|
||||
RenderbufferImpl::g_backendRenderbufferObjects.ResetBuckets();
|
||||
BufferImpl::g_backendBufferResources.ResetBuckets();
|
||||
}
|
||||
// P12 review fix (major): AND THE UNIT SHADOWS THAT POINT AT THEM. A texture or sampler twin
|
||||
// scrubs itself out of g_boundTexturesCache / g_boundSamplersCache in its destructor - but
|
||||
// not under InProcessTeardown(), which answered true for every twin just dropped. Left
|
||||
@@ -438,8 +463,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
TextureImpl::g_activeTextureUnit = 0;
|
||||
SamplerImpl::g_boundSamplersCache = {};
|
||||
g_serverSessionTwinTeardown = false;
|
||||
MGLOG_I("DirectGLES: the ended server session's twins are dropped (every kind, no driver call); the next "
|
||||
"session in this process starts from empty twin tables");
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1061,14 +1084,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const char* label = "";
|
||||
};
|
||||
|
||||
PersistentRing g_uboRing{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
|
||||
PersistentRing g_unpackRing{{},
|
||||
{},
|
||||
{},
|
||||
kUnpackRingInitialBytes,
|
||||
kUnpackRingMaxBytes,
|
||||
kUnpackRingAlignment,
|
||||
"Texture unpack ring"};
|
||||
// THE RINGS ARE PER NATIVE CONTEXT. Their slots are reclaimed by frame marks recorded at
|
||||
// Present, which say "everything this context wrote before its fence N is done once N is".
|
||||
// One process-wide set used to be shared by every session's contexts: a fence from one
|
||||
// context retired bytes another context's not-yet-executed copies still had to read
|
||||
// (a glTexSubImage from the unpack ring is a deferred GPU read), and the next session's
|
||||
// staging overwrote them - its pixels landed, at the wrong offsets, in the first one's
|
||||
// textures. With one set per context, a context's bytes wait for that context's own
|
||||
// fence. (The buffers themselves are shared objects; only who stages into them is split.)
|
||||
// Staging ring for app buffer updates whose destination store may still be
|
||||
// referenced by in-flight GPU work. Mali's glBufferSubData resolves that WAR
|
||||
// hazard by BLOCKING in the call (osup_sync_object_wait) until every
|
||||
@@ -1077,13 +1100,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// chunks stream in). Staging the bytes here and issuing a
|
||||
// glCopyBufferSubData instead keeps the hazard on the GPU timeline where it
|
||||
// is just job ordering, and the CPU never waits.
|
||||
PersistentRing g_uploadRing{{},
|
||||
{},
|
||||
{},
|
||||
kUploadRingInitialBytes,
|
||||
kUploadRingMaxBytes,
|
||||
kUploadRingAlignment,
|
||||
"Buffer upload ring"};
|
||||
struct RingSet {
|
||||
PersistentRing ubo{{}, {}, {}, kUboRingInitialBytes, kUboRingMaxBytes, 0, "Global-UBO ring"};
|
||||
PersistentRing unpack{{},
|
||||
{},
|
||||
{},
|
||||
kUnpackRingInitialBytes,
|
||||
kUnpackRingMaxBytes,
|
||||
kUnpackRingAlignment,
|
||||
"Texture unpack ring"};
|
||||
PersistentRing upload{{},
|
||||
{},
|
||||
{},
|
||||
kUploadRingInitialBytes,
|
||||
kUploadRingMaxBytes,
|
||||
kUploadRingAlignment,
|
||||
"Buffer upload ring"};
|
||||
RingSet();
|
||||
~RingSet();
|
||||
RingSet(const RingSet&) = delete;
|
||||
RingSet& operator=(const RingSet&) = delete;
|
||||
};
|
||||
|
||||
// A dead context's ring buffers: shared objects, so they outlive it; deleted at the next
|
||||
// Present of any context of the same share-group generation.
|
||||
struct OrphanRingBuffer {
|
||||
Uint id = 0;
|
||||
Uint contextGeneration = 0;
|
||||
};
|
||||
std::mutex g_orphanRingBuffersMutex;
|
||||
Vector<OrphanRingBuffer>& OrphanRingBuffers() {
|
||||
static auto* orphans = new Vector<OrphanRingBuffer>();
|
||||
return *orphans;
|
||||
}
|
||||
|
||||
// Each set's generations start in a range of their own: a per-program slot cache keyed
|
||||
// by (generation, offset) must never take one context's ring for another's.
|
||||
std::atomic<Uint32> g_ringGenerationBases{0};
|
||||
|
||||
RingSet::RingSet() {
|
||||
const Uint32 base = (g_ringGenerationBases.fetch_add(1, std::memory_order_relaxed) + 1) << 12;
|
||||
ubo.store.generation = base;
|
||||
unpack.store.generation = base;
|
||||
upload.store.generation = base;
|
||||
}
|
||||
|
||||
RingSet::~RingSet() {
|
||||
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
|
||||
for (const PersistentRing* ring : {&ubo, &unpack, &upload}) {
|
||||
if (ring->store.id != 0) {
|
||||
OrphanRingBuffers().push_back({ring->store.id, ring->store.contextGeneration});
|
||||
}
|
||||
for (const auto& retired : ring->retired) {
|
||||
if (retired.id != 0) OrphanRingBuffers().push_back({retired.id, retired.contextGeneration});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DirectGLES::PerNativeContext<RingSet> g_ringSets;
|
||||
PersistentRing& UboRing() { return g_ringSets.Current().ubo; }
|
||||
PersistentRing& UnpackRing() { return g_ringSets.Current().unpack; }
|
||||
PersistentRing& UploadRing() { return g_ringSets.Current().upload; }
|
||||
|
||||
// The ES context the ring's id/map belonged to is gone (or was never
|
||||
// seen): drop every handle without GL calls and re-arm creation. The
|
||||
@@ -1361,7 +1438,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool UploadRingUsableNow() {
|
||||
if (MG_Config::Features.EsprytDisableUploadRing) return false;
|
||||
if (!g_GLESFuncs.glCopyBufferSubData) return false;
|
||||
return RingAvailable(g_uploadRing);
|
||||
return RingAvailable(UploadRing());
|
||||
}
|
||||
|
||||
// A partial range below this goes through the staging ring instead of a
|
||||
@@ -1510,9 +1587,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, hostBase + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
RingAllocate(UploadRing(), size, ringOffset)) {
|
||||
Memcpy(UploadRing().store.mappedPtr + ringOffset, hostBase + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
|
||||
@@ -1589,9 +1666,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
RingAllocate(UploadRing(), size, ringOffset)) {
|
||||
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)write.offset,
|
||||
@@ -1692,7 +1769,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool UploadRingUsableNow() {
|
||||
if (MG_Config::Features.EsprytDisableUploadRing) return false;
|
||||
if (!g_GLESFuncs.glCopyBufferSubData) return false;
|
||||
return RingAvailable(g_uploadRing);
|
||||
return RingAvailable(UploadRing());
|
||||
}
|
||||
|
||||
// A partial range below this goes through the staging ring instead of a
|
||||
@@ -1798,9 +1875,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
RingAllocate(UploadRing(), size, ringOffset)) {
|
||||
Memcpy(UploadRing().store.mappedPtr + ringOffset, bufferObject.MappedData() + start, size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)start, (GLsizeiptr)size);
|
||||
@@ -1844,9 +1921,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
SizeT ringOffset = 0;
|
||||
if (ringUsable && size <= kUploadRingMaxBytes &&
|
||||
RingAllocate(g_uploadRing, size, ringOffset)) {
|
||||
Memcpy(g_uploadRing.store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, g_uploadRing.store.id);
|
||||
RingAllocate(UploadRing(), size, ringOffset)) {
|
||||
Memcpy(UploadRing().store.mappedPtr + ringOffset, write.bytes.data(), size);
|
||||
BindBufferId(GL_COPY_READ_BUFFER, UploadRing().store.id);
|
||||
BindBufferId(GL_COPY_WRITE_BUFFER, resource.id);
|
||||
g_GLESFuncs.glCopyBufferSubData(GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER,
|
||||
(GLintptr)ringOffset, (GLintptr)write.offset,
|
||||
@@ -2821,16 +2898,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit | kDynamicStorageBit;
|
||||
constexpr GLbitfield kT0MapFlags = kT0MapReadBit | kT0MapWriteBit | kMapPersistentBit | kMapCoherentBit;
|
||||
|
||||
Bool HasExtensionWord(const char* list, const char* word) {
|
||||
if (list == nullptr) return false;
|
||||
const SizeT n = std::strlen(word);
|
||||
for (const char* at = std::strstr(list, word); at != nullptr; at = std::strstr(at + 1, word)) {
|
||||
const Bool startOk = at == list || at[-1] == ' ';
|
||||
const Bool endOk = at[n] == '\0' || at[n] == ' ';
|
||||
if (startOk && endOk) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
using MG_Util::BackendLoader::HasExtensionWord;
|
||||
|
||||
Bool ResolveT0EntryPoints(String& why) {
|
||||
if (g_t0GetNativeClientBuffer != nullptr && g_t0BufferStorageExternal != nullptr) return true;
|
||||
@@ -2983,6 +3051,67 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return imported;
|
||||
}
|
||||
|
||||
// The POST's second round: the CPU writes region 1 through the held lock only now, after
|
||||
// the import and a GPU use of it, and reads region 4 (the first round's fill) back into
|
||||
// its cache; a second dispatch copies region 1 out and overwrites region 4. Appends its
|
||||
// counts to `detail`.
|
||||
Bool SelfTestExternalSteady(GLuint imported, GLuint out, void* heldPtr, char* detail, SizeT detailBytes) {
|
||||
constexpr Uint32 kWords = 1024;
|
||||
constexpr Uint32 kSteadyAtWord = kWords; // region 1
|
||||
constexpr Uint32 kFillAtWord = 4 * kWords; // region 4
|
||||
auto* held = static_cast<volatile Uint32*>(heldPtr);
|
||||
Uint32 cached = 0;
|
||||
for (Uint32 i = 0; i < kWords; ++i) {
|
||||
held[kSteadyAtWord + i] = 0xA5000000u ^ (i * 40503u);
|
||||
cached ^= held[kFillAtWord + i];
|
||||
}
|
||||
(void)cached;
|
||||
static const char* kSource =
|
||||
"#version 310 es\n"
|
||||
"layout(local_size_x = 64) in;\n"
|
||||
"layout(std430, binding = 0) buffer Ext { uint w[]; } ext;\n"
|
||||
"layout(std430, binding = 1) buffer Out { uint w[]; } outb;\n"
|
||||
"void main() {\n"
|
||||
" uint i = gl_GlobalInvocationID.x;\n"
|
||||
" outb.w[i] = ext.w[1024u + i];\n"
|
||||
" ext.w[4096u + i] = 0x3D5EA11Eu ^ i;\n"
|
||||
"}\n";
|
||||
const GLuint program = g_GLESFuncs.glCreateShaderProgramv(GL_COMPUTE_SHADER, 1, &kSource);
|
||||
GLint linked = 0;
|
||||
if (program != 0) g_GLESFuncs.glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
const SizeT used = std::strlen(detail);
|
||||
if (!linked) {
|
||||
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
|
||||
std::snprintf(detail + used, detailBytes - used, "; steady: the second program did not link");
|
||||
return false;
|
||||
}
|
||||
g_GLESFuncs.glUseProgram(program);
|
||||
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, imported);
|
||||
g_GLESFuncs.glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, out);
|
||||
g_GLESFuncs.glMemoryBarrier(0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
|
||||
g_GLESFuncs.glDispatchCompute(kWords / 64, 1, 1);
|
||||
g_GLESFuncs.glMemoryBarrier(GL_BUFFER_UPDATE_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT |
|
||||
0x00004000 /*GL_CLIENT_MAPPED_BUFFER_BARRIER_BIT_EXT*/);
|
||||
g_GLESFuncs.glFinish();
|
||||
Uint32 steadyRead = 0, steadySaw = 0;
|
||||
for (Uint32 i = 0; i < kWords; ++i)
|
||||
if (held[kFillAtWord + i] == (0x3D5EA11Eu ^ i)) ++steadySaw;
|
||||
g_GLESFuncs.glBindBuffer(GL_SHADER_STORAGE_BUFFER, out);
|
||||
const auto* copied = static_cast<const Uint32*>(
|
||||
g_GLESFuncs.glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, kWords * 4, GL_MAP_READ_BIT));
|
||||
if (copied != nullptr) {
|
||||
for (Uint32 i = 0; i < kWords; ++i)
|
||||
if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead;
|
||||
g_GLESFuncs.glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
|
||||
}
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
std::snprintf(detail + used, detailBytes - used,
|
||||
"; with the import in use, GPU read %u/%u words the CPU wrote after it, GPU write "
|
||||
"seen through the held lock %u/%u words",
|
||||
steadyRead, kWords, steadySaw, kWords);
|
||||
return steadyRead == kWords && steadySaw == kWords;
|
||||
}
|
||||
|
||||
// THE POST (B2, the standing rule: decide by probe, never by driver name). The AHB API
|
||||
// promises CPU coherence only across an unlock/lock; T0 keeps the client's lock for the
|
||||
// store's life, so the pattern it depends on is exactly what this exercises, on this
|
||||
@@ -2993,7 +3122,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// gpu - a compute dispatch copies that pattern out of the import (the GPU read) and
|
||||
// fills another region of it (the GPU write);
|
||||
// back - after glFinish the fill is read through the HELD pointer and the copy is
|
||||
// read back from the dispatch's output.
|
||||
// read back from the dispatch's output;
|
||||
// steady - the same again with bytes the CPU wrote AFTER the import and its first GPU
|
||||
// use (VkBufferManager::SelfTestWireImport says why the first round is not enough).
|
||||
// In a throwaway ES 3.1 pbuffer context, so none of this context's state or Espryt's
|
||||
// binding caches is disturbed; the caller's bind is restored before returning.
|
||||
Bool Ops_H_SelfTestExternal(char* whyOut, Uint64 whyBytes) {
|
||||
@@ -3046,7 +3177,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLuint out = 0, program = 0;
|
||||
void* hostMap = nullptr;
|
||||
const GLuint imported = ImportAhbAsBuffer(held.ahb, kBytes, &hostMap, why, /*throwaway=*/true);
|
||||
char detail[256] = {};
|
||||
char detail[512] = {};
|
||||
if (imported == 0) {
|
||||
std::snprintf(detail, sizeof(detail), "import: %s", why.c_str());
|
||||
} else if (std::memcmp(hostMap, pattern.data(), kWords * 4) != 0) {
|
||||
@@ -3096,6 +3227,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
"64 KiB AHB, lock held throughout: host map read ok, GPU read %u/%u words of "
|
||||
"the CPU's pattern, GPU write seen through the held lock %u/%u words",
|
||||
gpuRead, kWords, heldSaw, kWords);
|
||||
if (ok) ok = SelfTestExternalSteady(imported, out, held.ptr, detail, sizeof(detail));
|
||||
}
|
||||
}
|
||||
if (program != 0) g_GLESFuncs.glDeleteProgram(program);
|
||||
@@ -3183,7 +3315,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
case MG_Pipe::MGPipeResourceTarget::Tex2DMSArray: return TextureTarget::Texture2DMultisampleArray;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexRect: return TextureTarget::TextureRectangle;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexBuffer: return TextureTarget::TextureBuffer;
|
||||
case MG_Pipe::MGPipeResourceTarget::TexExternalOES: return TextureTarget::TextureExternalOES;
|
||||
default: return TextureTarget::Unknown;
|
||||
}
|
||||
}
|
||||
@@ -3763,8 +3894,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
MG_Pipe::MGPipeSetResourceOps(&g_glesResourceOps);
|
||||
#endif
|
||||
// Frontend writes issued while ops were unregistered advanced change
|
||||
// serials with no per-op bump; re-open every draw-clean memo.
|
||||
BumpBufferMutationEpoch();
|
||||
// serials with no per-op bump; re-open every draw-clean memo. Advanced directly,
|
||||
// past BumpBufferMutationEpoch's apply-thread pin: a registration mutates no buffer
|
||||
// and only makes every memo probe again, and with several sessions in one process
|
||||
// a new session's backend registers on its own thread while another session's
|
||||
// apply thread is already running - which the pin would otherwise read as a
|
||||
// client-thread producer and, under the strict lane, abort the server for.
|
||||
g_bufferMutationEpoch.fetch_add(1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void UnregisterBufferBackendOps() {
|
||||
@@ -3787,6 +3923,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_hasDeferredBufferReleases.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void OnServerSessionEndedWithOthersLive() {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Only what belongs to the ending session: its staged bytes (both stores are
|
||||
// session-scoped). The rings, the ops table and the buffer generation are the
|
||||
// process's and every other session's GL buffers are still alive in the shared group.
|
||||
MGL_SERVER_STAGED_DROP_ALL();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Remote::Server::ServerStagedTexture().DropAll();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void OnBackendContextDestroyed() {
|
||||
UnregisterBufferBackendOps(); // also bumps the buffer-mutation epoch
|
||||
++g_bufferContextGeneration;
|
||||
@@ -3798,8 +3946,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
InvalidatePixelBufferBindingCaches();
|
||||
// The rings' ids and persistent maps died with the context; drop the
|
||||
// handles (no GL) and let the next draw / texture upload recreate them.
|
||||
ResetRingForNewContext(g_uboRing);
|
||||
ResetRingForNewContext(g_unpackRing);
|
||||
ResetRingForNewContext(UboRing());
|
||||
ResetRingForNewContext(UnpackRing());
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// R-11's server copies die with the context for the same reason the rings' ids do:
|
||||
// the resource twins they are keyed by are about to be rebuilt against a new
|
||||
@@ -4808,9 +4956,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeleteOrphanRingBuffers() {
|
||||
const std::lock_guard<std::mutex> lock(g_orphanRingBuffersMutex);
|
||||
auto& orphans = OrphanRingBuffers();
|
||||
if (orphans.empty()) return;
|
||||
for (const OrphanRingBuffer& orphan : orphans) {
|
||||
// A buffer of a share group that is gone was reclaimed with it.
|
||||
if (orphan.contextGeneration != g_bufferContextGeneration) continue;
|
||||
ScrubBufferBindingShadowsForId(orphan.id);
|
||||
g_GLESFuncs.glDeleteBuffers(1, &orphan.id);
|
||||
}
|
||||
orphans.clear();
|
||||
}
|
||||
|
||||
// Present()-time upkeep shared by both rings.
|
||||
void RingOnPresent(PersistentRing& ring) {
|
||||
if (!CanTouchGLNow()) return;
|
||||
DeleteOrphanRingBuffers();
|
||||
|
||||
// Delete grown-away stores the GPU is provably done with.
|
||||
const Uint64 completed = DirectGLES::CompletedFrameSerial();
|
||||
@@ -4855,36 +5017,36 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Bool UboRingAvailable() {
|
||||
if (MG_Config::Features.EsprytDisableUboRing) return false;
|
||||
return RingAvailable(g_uboRing);
|
||||
return RingAvailable(UboRing());
|
||||
}
|
||||
|
||||
Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(g_uboRing, size, outOffset); }
|
||||
Bool UboRingAllocate(SizeT size, SizeT& outOffset) { return RingAllocate(UboRing(), size, outOffset); }
|
||||
|
||||
void* UboRingMappedPtr() { return g_uboRing.store.mappedPtr; }
|
||||
Uint UboRingBufferId() { return g_uboRing.store.id; }
|
||||
Uint32 UboRingGeneration() { return g_uboRing.store.generation; }
|
||||
void* UboRingMappedPtr() { return UboRing().store.mappedPtr; }
|
||||
Uint UboRingBufferId() { return UboRing().store.id; }
|
||||
Uint32 UboRingGeneration() { return UboRing().store.generation; }
|
||||
|
||||
void UboRingOnPresent() { RingOnPresent(g_uboRing); }
|
||||
void UboRingOnPresent() { RingOnPresent(UboRing()); }
|
||||
|
||||
Bool UnpackRingAvailable() {
|
||||
if (MG_Config::Features.EsprytDisableUnpackRing) return false;
|
||||
return RingAvailable(g_unpackRing);
|
||||
return RingAvailable(UnpackRing());
|
||||
}
|
||||
|
||||
Bool UnpackRingAllocate(SizeT size, SizeT& outOffset) {
|
||||
// A request the ring could never satisfy even empty would otherwise walk
|
||||
// the whole grow/drain ladder before failing.
|
||||
if (size > kUnpackRingMaxBytes) return false;
|
||||
return RingAllocate(g_unpackRing, size, outOffset);
|
||||
return RingAllocate(UnpackRing(), size, outOffset);
|
||||
}
|
||||
|
||||
void* UnpackRingMappedPtr() { return g_unpackRing.store.mappedPtr; }
|
||||
Uint UnpackRingBufferId() { return g_unpackRing.store.id; }
|
||||
void* UnpackRingMappedPtr() { return UnpackRing().store.mappedPtr; }
|
||||
Uint UnpackRingBufferId() { return UnpackRing().store.id; }
|
||||
SizeT UnpackRingMaxBytes() { return kUnpackRingMaxBytes; }
|
||||
|
||||
void UnpackRingOnPresent() { RingOnPresent(g_unpackRing); }
|
||||
void UnpackRingOnPresent() { RingOnPresent(UnpackRing()); }
|
||||
|
||||
void UploadRingOnPresent() { RingOnPresent(g_uploadRing); }
|
||||
void UploadRingOnPresent() { RingOnPresent(UploadRing()); }
|
||||
} // namespace BufferImpl
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
@@ -7422,6 +7584,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A shared image's EGLImage lives exactly as long as the name it was bound to.
|
||||
m_sharedImage = nullptr;
|
||||
#endif
|
||||
g_GLESFuncs.glGenTextures(1, &m_backendTextureId);
|
||||
m_contextGeneration = g_backendContextGeneration;
|
||||
if (m_backendTextureId == 0) {
|
||||
@@ -8672,6 +8838,167 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#define MGB_STORAGE_KIND(obj) ((obj)->GetStorageType())
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md). The client defined
|
||||
// level 0 as WxH RGBA8 with no data and then attached the image; the record's descriptor
|
||||
// still says so, but the texels are the image's, so nothing below may allocate or upload.
|
||||
Bool BackendTextureObject::SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record) {
|
||||
const Uint64 imageId = record.SharedImageId;
|
||||
if (imageId == 0) {
|
||||
// Respecified away from the image. The ES name is an EGLImage sibling, which must not
|
||||
// be redefined in place: the immutable flag sends the normal path through
|
||||
// RecreateBackendTexture, and the EGLImage goes with the name it was bound to.
|
||||
m_sharedImageId = 0;
|
||||
m_isInitialized = false;
|
||||
m_backendStorageImmutable = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Steady state: bound to this image already (the serial moves with sub-data and with a
|
||||
// metadata respecify, neither of which changes what the name samples). A failed bind is
|
||||
// retried only once the record moves. Pending sub-data is the caller's
|
||||
// (ReplaySharedImageUploads), after the image's sync state has been acquired.
|
||||
if (m_sharedImageId == imageId && m_isInitialized &&
|
||||
(m_sharedImage != nullptr || m_syncedResourceSerial == record.Serial)) {
|
||||
m_syncedResourceSerial = record.Serial;
|
||||
return true;
|
||||
}
|
||||
|
||||
// A fresh mutable name: the current one may hold immutable storage (which no EGLImage
|
||||
// can replace) or another image's sibling. RecreateBackendTexture also moves the FBO
|
||||
// twins' attachment generation and forces the parameter resync the new name needs.
|
||||
RecreateBackendTexture();
|
||||
m_sharedImage = nullptr;
|
||||
m_sharedImageId = imageId;
|
||||
m_isInitialized = true;
|
||||
// Whatever comes next redefines storage through a re-mint (see the imageId == 0 arm).
|
||||
m_backendStorageImmutable = true;
|
||||
m_syncedResourceSerial = record.Serial;
|
||||
|
||||
const TextureTarget target = BufferImpl::StagedTextureTargetForPipeTarget(record.Desc.Target);
|
||||
if (ConvertTextureTargetToBackendGLEnum(target) != GL_TEXTURE_2D) {
|
||||
MGLOG_E_ONCE("Shared image texture %u: target %s cannot take an EGLImage; left empty",
|
||||
record.Desc.GlNameForDiag, MG_Util::ConvertTextureTargetToString(target).c_str());
|
||||
return true;
|
||||
}
|
||||
String why;
|
||||
SharedImageImpl::EglImageRef image = SharedImageImpl::CreateEglImage(imageId, why);
|
||||
if (image == nullptr) {
|
||||
MGLOG_E_ONCE("Shared image texture %u: image %llu cannot be bound (%s); left empty",
|
||||
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId), why.c_str());
|
||||
return true;
|
||||
}
|
||||
Bind(GL_TEXTURE_2D);
|
||||
if (SharedImageImpl::IsYuv(image)) {
|
||||
// A YUV image is CONVERTED into the name's own RGBA8 level at each frame's first use
|
||||
// (SharedImageImpl::ConvertYuvForSampling), never made its storage.
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
g_GLESFuncs.glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, static_cast<GLsizei>(record.Desc.Width),
|
||||
static_cast<GLsizei>(record.Desc.Height));
|
||||
if (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
MGLOG_E_ONCE("Shared image texture %u: no %ux%u RGBA8 storage for YUV image %llu; left empty",
|
||||
record.Desc.GlNameForDiag, record.Desc.Width, record.Desc.Height,
|
||||
static_cast<unsigned long long>(imageId));
|
||||
return true;
|
||||
}
|
||||
} else if (!SharedImageImpl::TargetBoundTexture2D(image)) {
|
||||
MGLOG_E_ONCE("Shared image texture %u: glEGLImageTargetTexture2DOES refused image %llu; left empty",
|
||||
record.Desc.GlNameForDiag, static_cast<unsigned long long>(imageId));
|
||||
return true;
|
||||
}
|
||||
m_sharedImage = std::move(image);
|
||||
m_sharedImageUploadsRgb = false;
|
||||
// The shape the name now has: the descriptor's level 0, and that one level only.
|
||||
m_prevTextureInfo = {static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
|
||||
record.Desc.Width,
|
||||
record.Desc.Height,
|
||||
record.Desc.Depth,
|
||||
1,
|
||||
0,
|
||||
record.Desc.Samples,
|
||||
record.Desc.FixedSampleLocations != 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// SUB-DATA ON A SHARED IMAGE IS WRITTEN INTO IT. An X server drawing with glamor uploads
|
||||
// its clients' pixels (MIT-SHM / PutImage) into window pixmaps whose level 0 is a shared
|
||||
// image, with glTexSubImage2D: the texels are in the staged store (the client defined the
|
||||
// level without data, so only the uploaded boxes are covered), and they go into the
|
||||
// EGLImage-backed name box by box. Every pending entry leaves the set either way, or the
|
||||
// per-draw clean gate would never close. Called after AcquireForSampling, so the upload is
|
||||
// ordered after the image's pending reads and writes like any other use.
|
||||
void BackendTextureObject::ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record) {
|
||||
if (record.PendingUploads.empty()) return;
|
||||
Vector<std::pair<Uint16, Uint16>> keys;
|
||||
Vector<std::pair<Uint16, MG_Pipe::MGPBox>> boxes;
|
||||
keys.reserve(record.PendingUploads.size());
|
||||
for (const auto& pending : record.PendingUploads) {
|
||||
const Uint16 uploadHalf = MG_Pipe::MGPipeSubDataUploadTargetOf(pending.UploadTarget);
|
||||
keys.emplace_back(uploadHalf, pending.Level);
|
||||
if (pending.Level != 0) continue; // an image has one level
|
||||
if (pending.Regions.empty()) {
|
||||
boxes.emplace_back(uploadHalf, pending.UnionBox);
|
||||
} else {
|
||||
for (const auto& region : pending.Regions)
|
||||
boxes.emplace_back(uploadHalf, MG_Pipe::MGPBox{region.X, region.Y, region.Z, region.W, region.H,
|
||||
region.D});
|
||||
}
|
||||
}
|
||||
if (m_sharedImage != nullptr && !boxes.empty()) {
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(record.Desc.Resource);
|
||||
Bind(GL_TEXTURE_2D);
|
||||
ScopedDefaultUnpackState unpackState;
|
||||
BufferImpl::BindPixelUnpackBufferId(0);
|
||||
for (const auto& [uploadHalf, box] : boxes) {
|
||||
Int rowTexels = 0;
|
||||
// A shared image is RGBA8 to every client that names it (the level the client
|
||||
// defined over it): four bytes a texel.
|
||||
const Uint8* bytes =
|
||||
store.CoveredBoxBytes(key, uploadHalf, 0, box.X, box.Y, box.W, box.H, 4, &rowTexels);
|
||||
if (bytes == nullptr) {
|
||||
MGLOG_E_ONCE("Shared image texture %u: an upload of %ux%u at (%d,%d) has no staged texels; "
|
||||
"not written",
|
||||
record.Desc.GlNameForDiag, box.W, box.H, box.X, box.Y);
|
||||
continue;
|
||||
}
|
||||
const Uint8* origin =
|
||||
bytes + (static_cast<SizeT>(box.Y) * static_cast<SizeT>(rowTexels) + static_cast<SizeT>(box.X)) * 4;
|
||||
if (!m_sharedImageUploadsRgb) {
|
||||
while (g_GLESFuncs.glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, rowTexels);
|
||||
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
|
||||
static_cast<GLsizei>(box.H), GL_RGBA, GL_UNSIGNED_BYTE, origin);
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
// An image of an X format (no alpha) is an RGB8 texture to the driver, which
|
||||
// takes only RGB texels: this image's uploads are converted from now on.
|
||||
if (g_GLESFuncs.glGetError() != GL_INVALID_OPERATION) continue;
|
||||
m_sharedImageUploadsRgb = true;
|
||||
}
|
||||
Vector<Uint8> rgb(static_cast<SizeT>(box.W) * box.H * 3);
|
||||
for (Uint32 row = 0; row < box.H; ++row) {
|
||||
const Uint8* src = origin + static_cast<SizeT>(row) * static_cast<SizeT>(rowTexels) * 4;
|
||||
Uint8* dst = rgb.data() + static_cast<SizeT>(row) * box.W * 3;
|
||||
for (Uint32 x = 0; x < box.W; ++x) {
|
||||
dst[x * 3 + 0] = src[x * 4 + 0];
|
||||
dst[x * 3 + 1] = src[x * 4 + 1];
|
||||
dst[x * 3 + 2] = src[x * 4 + 2];
|
||||
}
|
||||
}
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
g_GLESFuncs.glTexSubImage2D(GL_TEXTURE_2D, 0, box.X, box.Y, static_cast<GLsizei>(box.W),
|
||||
static_cast<GLsizei>(box.H), GL_RGB, GL_UNSIGNED_BYTE, rgb.data());
|
||||
g_GLESFuncs.glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
}
|
||||
MGLOG_I_ONCE("Shared image texture %u: sub-data written into its image (%s texels)",
|
||||
record.Desc.GlNameForDiag, m_sharedImageUploadsRgb ? "RGB" : "RGBA");
|
||||
}
|
||||
for (const auto& [uploadHalf, level] : keys) ConsumePipeTextureUpload(record.Desc.Resource, uploadHalf, level);
|
||||
}
|
||||
#endif
|
||||
|
||||
void BackendTextureObject::SyncMipmapsToBackend(
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& stateTextureObject) {
|
||||
// P5e (tx2): null IS the by-handle arm once a handle has been noted on this twin;
|
||||
@@ -8714,6 +9041,22 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// SHARED IMAGES: ahead of the view test and of the staged-store and serial gates below,
|
||||
// none of which knows the texels belong to an image - the staged store holds none of
|
||||
// them, and the storage path would allocate fresh, empty storage over the image.
|
||||
if (pushedStorage != nullptr && (pushedStorage->SharedImageId != 0 || m_sharedImageId != 0) &&
|
||||
SyncSharedImageStorage(*pushedStorage)) {
|
||||
// EVERY use, not only the attach (the draw-clean gates never pass a bound image):
|
||||
// a write that landed since the last one is waited for on this context's GPU
|
||||
// before the command this sync is for, and the session's frame notes the read.
|
||||
if (m_sharedImage != nullptr) SharedImageImpl::AcquireForSampling(m_sharedImage);
|
||||
ReplaySharedImageUploads(*pushedStorage);
|
||||
// A YUV image is converted into the name at the frame's first use.
|
||||
if (m_sharedImage != nullptr && SharedImageImpl::IsYuv(m_sharedImage))
|
||||
SharedImageImpl::ConvertYuvForSampling(m_sharedImage, GetBackendTextureId(),
|
||||
pushedStorage->Desc.Width, pushedStorage->Desc.Height);
|
||||
return;
|
||||
}
|
||||
// P5e (tx2), CONTRACT-P5E §5.2 (scout G-S2-2): THE VIEW TEST MOVES BEHIND THE
|
||||
// RECORD. `IsTextureView()` was the one frontend read this function made BEFORE it
|
||||
// had resolved anything at all, and Desc.ViewOf is the carrier - it names the storage
|
||||
@@ -9612,8 +9955,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// entry's rects are then read out of the SERVER'S WHOLE LEVEL IMAGE -
|
||||
// which is what the derived pointer names - and those piece-relative
|
||||
// offsets are precisely the ones that must not be used here.
|
||||
//
|
||||
// GATED ON regionsAddressable, NOT subRectEligible: the per-region arm
|
||||
// for a level the GPU may have written (m_driverMayHoldGpuWrites below)
|
||||
// reads MGB_RECT_SRC_PTR too, and it runs exactly when the union box is
|
||||
// the whole level - which is what two or more slab pieces always add up
|
||||
// to. Checked only under subRectEligible, a damage rect split across
|
||||
// pieces kept the later pieces' run-relative offsets and read their
|
||||
// rows from the top of the level image (KWin's wl_shm window textures:
|
||||
// the panel repeated down the desktop). bpp is valid whenever the
|
||||
// regions are addressable.
|
||||
Bool pendingOffsetsAgree = true;
|
||||
if (pendingUpload != nullptr && subRectEligible && carriedLayoutApplies) {
|
||||
if (pendingUpload != nullptr && regionsAddressable && carriedLayoutApplies) {
|
||||
for (const auto& region : pendingUpload->Regions) {
|
||||
const SizeT derivedOffset = static_cast<SizeT>(region.Z) * levelSliceBytes +
|
||||
static_cast<SizeT>(region.Y) * levelRowBytes +
|
||||
@@ -10152,7 +10505,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
MGLOG_D("Texture state changed significantly or not initialized, regenerating texture buffer with "
|
||||
"ID: %u, buffer ID: %u, buffer size: %zu, format: %s",
|
||||
m_backendTextureId, backendId, buffer ? buffer->GetSize() : (SizeT)0,
|
||||
m_backendTextureId, backendId, buffer ? buffer->GetSize() : rangeSize,
|
||||
MG_Util::ConvertGLEnumToString(glInternalFormat).c_str());
|
||||
// A texture that names a window of the buffer needs the range form; the
|
||||
// whole-buffer forms report offset 0 and the buffer's current size, which
|
||||
@@ -10971,6 +11324,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
g_boundTexturesCache;
|
||||
TwinRegistry<MG_State::GLState::ITextureObject, BackendTextureObject, MG_Pipe::MGPipeKind::Texture> g_backendTextureObjects;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void WritePendingSharedImageUploads() {
|
||||
auto& applier = MG_Pipe::MGPipeApplier();
|
||||
for (SizeT slot = 0; slot < applier.TextureResources.size(); ++slot) {
|
||||
const auto& record = applier.TextureResources[slot];
|
||||
if (!record.Live || record.SharedImageId == 0 || record.PendingUploads.empty()) continue;
|
||||
const MG_Pipe::MGPipeHandle handle{static_cast<Uint32>(slot), record.Gen};
|
||||
auto* twin = g_backendTextureObjects.FindByHandle(handle);
|
||||
if (twin == nullptr || !*twin) continue;
|
||||
(*twin)->SyncMipmapsToBackendByHandle(handle);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject) {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> storage = textureObject;
|
||||
// Bounded: GL resolves a view of a view to the original storage, so this takes one step.
|
||||
@@ -11283,45 +11650,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||
// THE TEXTURE'S OWN TARGET, not the attach target: an external texture is attached
|
||||
// through the GL_TEXTURE_2D upload target (TextureEnumConverter maps 0x8D65 there so the
|
||||
// attach validates), so glTextureTarget is never 0x8D65 here and the arms below were
|
||||
// dead - kwin's framebuffers got a storage-less texture and every draw into them failed
|
||||
// with GL_INVALID_FRAMEBUFFER_OPERATION.
|
||||
const Bool externalTexture =
|
||||
glTextureTarget == 0x8D65 || textureObject->GetTarget() == TextureTarget::TextureExternalOES;
|
||||
if (externalTexture && g_hostFrameRenderbuffer == 0) {
|
||||
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
|
||||
// here never creates a window surface, so this attachment is the first place
|
||||
// the display host's frame is known to be wanted. A failed take leaves the
|
||||
// ordinary arm below exactly as it was.
|
||||
// The numbers size only the fallback pbuffer that is the draw target for the
|
||||
// moment before the first frame arrives; the frame's real extent comes from the
|
||||
// display host's own offer. They are the panel's, because that is the surface
|
||||
// this route exists to fill.
|
||||
(void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
}
|
||||
if (externalTexture && g_hostFrameRenderbuffer != 0) {
|
||||
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
|
||||
// this branch exists: kwin_wayland in the container imports the display daemon's
|
||||
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
|
||||
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
|
||||
// that arrived this way has NO storage on this side, so the attachment was bound
|
||||
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
|
||||
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
|
||||
// host handed this process (same memory, same size), and that frame's storage is
|
||||
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
|
||||
// to the frame itself. The gate is the measured shape of that case: target
|
||||
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
|
||||
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
|
||||
// moment with no frame - takes the ordinary path below, unchanged.
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
} else {
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
}
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(attachmentObject.GetTextureLevel()));
|
||||
}
|
||||
} else if (attachmentObject.IsRenderbuffer()) {
|
||||
const auto& renderbufferObject = attachmentObject.GetRenderbuffer();
|
||||
@@ -11637,23 +11968,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// nothing here is the whole of the agreement. (The legacy arm does the same.)
|
||||
return true;
|
||||
}
|
||||
// AN IMPORTED dma-buf AT A COLOUR POINT IS THE DISPLAY HOST'S FRAME. The client marks the
|
||||
// texture it bound an imported EGLImage to as GL_TEXTURE_EXTERNAL_OES (TextureObject2D's
|
||||
// target), the server never received that dma-buf's storage, and the compositor's
|
||||
// framebuffer over it was incomplete (measured: status 0x8cd7, every draw
|
||||
// GL_INVALID_FRAMEBUFFER_OPERATION). It is bound to the host frame canvas instead - the
|
||||
// storage the display host's frames are copied out of at present - before any twin or
|
||||
// storage sync, which have nothing to offer a texture with no storage.
|
||||
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture &&
|
||||
surface.TextureTarget ==
|
||||
static_cast<decltype(surface.TextureTarget)>(TextureTarget::TextureExternalOES)) {
|
||||
if (g_hostFrameRenderbuffer == 0) (void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
if (g_hostFrameRenderbuffer != 0) {
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (surface.Kind == MG_Pipe::kMGPipeSurfaceKindTexture) {
|
||||
// The record's handle is validated BEFORE the frontend is consulted (N-3): it is
|
||||
// the untrusted half, the frontend cross-check below is the corroboration, and
|
||||
@@ -11737,38 +12051,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const Bool isCubeFace = glTextureTarget >= GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
|
||||
glTextureTarget <= GL_TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
||||
backendTextureObject->Bind(isCubeFace ? GL_TEXTURE_CUBE_MAP : glTextureTarget);
|
||||
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer == 0) {
|
||||
// NOBODY ELSE TAKES THE FRAME ON THIS ROUTE (DirectGLES.h): the compositor
|
||||
// here never creates a window surface, so this attachment is the first place
|
||||
// the display host's frame is known to be wanted. A failed take leaves the
|
||||
// ordinary arm below exactly as it was.
|
||||
// The numbers size only the fallback pbuffer that is the draw target for the
|
||||
// moment before the first frame arrives; the frame's real extent comes from the
|
||||
// display host's own offer. They are the panel's, because that is the surface
|
||||
// this route exists to fill.
|
||||
(void)TakeHostFrameForExternalTexture(1440, 3200);
|
||||
}
|
||||
if (glTextureTarget == 0x8D65 && g_hostFrameRenderbuffer != 0) {
|
||||
// THE IMPORTED dma-buf IS THE DISPLAY HOST'S FRAME. Measured, and the whole reason
|
||||
// this branch exists: kwin_wayland in the container imports the display daemon's
|
||||
// dma-buf as an EGLImage ("taking target 0x8d65 over an EGLImage of 1440x2937
|
||||
// fourcc 0x34324241") and builds a framebuffer over that texture - and a texture
|
||||
// that arrived this way has NO storage on this side, so the attachment was bound
|
||||
// as an empty texture and kwin refused its own framebuffer ("framebuffer for
|
||||
// dmabuf 0 is not complete"). The buffer it imports IS the frame the display
|
||||
// host handed this process (same memory, same size), and that frame's storage is
|
||||
// a RENDERBUFFER - a texture argument cannot name it - so the attachment is bound
|
||||
// to the frame itself. The gate is the measured shape of that case: target
|
||||
// GL_TEXTURE_EXTERNAL_OES (0x8d65), which nothing else in this stack binds, and a
|
||||
// frame currently held. Every other attachment - a normal GL_TEXTURE_2D, or any
|
||||
// moment with no frame - takes the ordinary path below, unchanged.
|
||||
g_GLESFuncs.glFramebufferRenderbuffer(glFBOTarget, glBackendAttachment, GL_RENDERBUFFER,
|
||||
g_hostFrameRenderbuffer);
|
||||
} else {
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(surface.Level));
|
||||
}
|
||||
g_GLESFuncs.glFramebufferTexture2D(glFBOTarget, glBackendAttachment, glTextureTarget,
|
||||
backendTextureObject->GetBackendTextureId(),
|
||||
static_cast<GLint>(surface.Level));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -12866,11 +13151,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
namespace ScratchFBOImpl {
|
||||
namespace {
|
||||
ScratchFramebuffer g_tempFramebuffer;
|
||||
ScratchFramebuffer g_blitReadFramebuffer;
|
||||
ScratchFramebuffer g_blitDrawFramebuffer;
|
||||
Uint g_completeTinyFBOId = 0;
|
||||
Uint g_completeTinyRBOId = 0;
|
||||
// ONE SET PER NATIVE CONTEXT: a framebuffer belongs to the context that made it
|
||||
// (DirectGLES.h, PerNativeContext), and so does the attachment shadow kept for it.
|
||||
struct ScratchSet {
|
||||
ScratchFramebuffer temp;
|
||||
ScratchFramebuffer blitRead;
|
||||
ScratchFramebuffer blitDraw;
|
||||
Uint completeTinyFBOId = 0;
|
||||
Uint completeTinyRBOId = 0;
|
||||
};
|
||||
PerNativeContext<ScratchSet> g_scratchSets;
|
||||
|
||||
// Detach every point the shadow no longer vouches for. Used when the
|
||||
// shadow is unknown (context reset, texture id deleted while attached).
|
||||
@@ -12925,13 +13215,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
} // namespace
|
||||
|
||||
ScratchFramebuffer& TempFramebuffer() {
|
||||
return g_tempFramebuffer;
|
||||
return g_scratchSets.Current().temp;
|
||||
}
|
||||
ScratchFramebuffer& BlitReadFramebuffer() {
|
||||
return g_blitReadFramebuffer;
|
||||
return g_scratchSets.Current().blitRead;
|
||||
}
|
||||
ScratchFramebuffer& BlitDrawFramebuffer() {
|
||||
return g_blitDrawFramebuffer;
|
||||
return g_scratchSets.Current().blitDraw;
|
||||
}
|
||||
|
||||
Uint EnsureId(ScratchFramebuffer& fb) {
|
||||
@@ -13100,6 +13390,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
|
||||
Uint EnsureCompleteTinyFramebufferId() {
|
||||
ScratchSet& set = g_scratchSets.Current();
|
||||
Uint& g_completeTinyFBOId = set.completeTinyFBOId;
|
||||
Uint& g_completeTinyRBOId = set.completeTinyRBOId;
|
||||
if (g_completeTinyFBOId != 0) {
|
||||
return g_completeTinyFBOId;
|
||||
}
|
||||
@@ -13127,19 +13420,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
if (textureId == 0) {
|
||||
return;
|
||||
}
|
||||
for (ScratchFramebuffer* fb : {&g_tempFramebuffer, &g_blitReadFramebuffer, &g_blitDrawFramebuffer}) {
|
||||
if (fb->colorTex == textureId || fb->depthTex == textureId) {
|
||||
fb->attachmentsKnown = false;
|
||||
// Textures ARE shared, so the deleted id may sit in any context's scratch FBO.
|
||||
g_scratchSets.ForEach([textureId](ScratchSet& set) {
|
||||
for (ScratchFramebuffer* fb : {&set.temp, &set.blitRead, &set.blitDraw}) {
|
||||
if (fb->colorTex == textureId || fb->depthTex == textureId) {
|
||||
fb->attachmentsKnown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void OnBackendContextDestroyed() {
|
||||
g_tempFramebuffer = {};
|
||||
g_blitReadFramebuffer = {};
|
||||
g_blitDrawFramebuffer = {};
|
||||
g_completeTinyFBOId = 0;
|
||||
g_completeTinyRBOId = 0;
|
||||
g_scratchSets.Clear();
|
||||
}
|
||||
} // namespace ScratchFBOImpl
|
||||
|
||||
@@ -14133,7 +14425,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// re-serialised copy either way, and adopting that copy for every vertex
|
||||
// shader would put every one of them through a round trip they do not need
|
||||
// - which is not free: it cost the create-indirect retrace 0.15 SSIM the
|
||||
// first time this gate was missing.
|
||||
// first time this gate was missing (the original capture; the case is now
|
||||
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024).
|
||||
effectiveSpirv = &splitArrayInputSpirv;
|
||||
}
|
||||
|
||||
@@ -14141,8 +14434,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// blocks - the optimizer hands back a re-serialised copy either way, and taking
|
||||
// that copy for a module it did not rewrite is not free (it cost the
|
||||
// create-indirect retrace 0.15 SSIM when the array-input split first missed
|
||||
// this gate). The report has to be per stage, not cumulative: a fragment shader
|
||||
// consuming the same block reports a name the vertex stage already reported,
|
||||
// this gate; the case is now
|
||||
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024). The report has to be
|
||||
// per stage, not cumulative: a fragment shader consuming the same block reports a
|
||||
// name the vertex stage already reported,
|
||||
// and its own rewrite must still be taken or the two stages stop matching.
|
||||
Vector<unsigned int> flattenedXfbSpirv;
|
||||
if (!xfbCaptureBlockNames.empty()) {
|
||||
@@ -14861,8 +15156,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// per stage below so a producer and its consumer keep naming the same block.
|
||||
// Gated twice over, because a re-serialised module is not free (it cost the
|
||||
// create-indirect retrace 0.15 SSIM the first time the array-input split missed
|
||||
// its gate): only a tessellation or geometry stage can declare blocks in both
|
||||
// directions at all, and even then the probe has to FIND a collision before any
|
||||
// its gate; the case is now
|
||||
// minecraft-1.21.1-neoforge-create-indirect-in-world-align1024): only a tessellation
|
||||
// or geometry stage can declare blocks in both directions at all, and even then the
|
||||
// probe has to FIND a collision before any
|
||||
// stage is rewritten.
|
||||
std::set<String> collidingIoBlockNames;
|
||||
std::set<String> declaredIoBlockNames;
|
||||
@@ -16730,6 +17027,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// PipeTextureStorageRecordForRecord and CONTRACT-P5E.md §5.2.
|
||||
const MG_Pipe::MGPipeResourceRecord* storage = PipeTextureStorageRecordForRecord(record);
|
||||
if (storage == nullptr) return false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Never clean while bound to a shared image: see IsDrawSyncClean.
|
||||
if (m_sharedImage != nullptr) return false;
|
||||
#endif
|
||||
// SyncMipmapsToBackend's own early-out, verbatim.
|
||||
if (!m_isInitialized || m_syncedResourceSerial == 0 || m_syncedResourceSerial != storage->Serial ||
|
||||
!storage->PendingUploads.empty()) {
|
||||
|
||||
@@ -74,6 +74,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// flag while the process is very much alive.
|
||||
Bool InProcessTeardown();
|
||||
void EnsureProcessTeardownSentinel();
|
||||
// The exit() half of InProcessTeardown alone, for an object whose release needs only the
|
||||
// display, not a current context (an EGLImage): a server session's twin teardown has no
|
||||
// context current but a live display, and skipping the release there would leak the image.
|
||||
Bool InProcessExit();
|
||||
|
||||
// Generation of the backend ES context that owns the driver ids currently handed
|
||||
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
|
||||
@@ -332,6 +336,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
using const_iterator = typename BackendMap::const_iterator;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
using SlotTable = BackendSlotTable<StateObject, BackendObject, kKind>;
|
||||
// P14 S6: the ONE bucket of this kind the calling thread is in. Every handle-keyed entry
|
||||
// below resolves through it, so the six twin registries answer per {session, share
|
||||
// group} without a single call site naming a bucket.
|
||||
using SlotTables = SlotTableRegistry<StateObject, BackendObject, kKind>;
|
||||
using TwinKey = DirectGLES::TwinKey;
|
||||
#endif
|
||||
|
||||
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
|
||||
@@ -348,7 +357,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// insertion (D13; SlotTables.h) - so a table used outside a registry arms
|
||||
// it too, which is right: it is the twin, not the registry, that owns the
|
||||
// driver id a guarded destructor exists for.
|
||||
return m_slotTable.GetOrCreate(stateObj);
|
||||
return m_slotTables.ForCallingThread().GetOrCreate(stateObj);
|
||||
}
|
||||
#endif
|
||||
// Twin creation is the moment a driver-owned id starts needing a guarded
|
||||
@@ -407,7 +416,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
return m_slotTable.Find(stateObj);
|
||||
return m_slotTables.ForCallingThread().Find(stateObj);
|
||||
}
|
||||
#endif
|
||||
const auto entryIt = m_entries.find(stateObj);
|
||||
@@ -445,7 +454,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// backend memo stores instead of a raw pointer, a GL name or a bare lifetime id.
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
return m_slotTable.HandleOf(stateObj);
|
||||
return m_slotTables.HandleOf(stateObj);
|
||||
}
|
||||
return MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
@@ -454,7 +463,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the lookup a backend memo that already holds a handle wants: no lifetime-id probe.
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
return m_slotTable.FindByHandle(handle);
|
||||
return m_slotTables.FindByHandle(handle);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -490,7 +499,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
const Uint32 liveGen = m_slotTable.LiveGenAt(handle.Slot);
|
||||
const Uint32 liveGen = m_slotTables.LiveGenAt(handle.Slot);
|
||||
if (liveGen != 0 && liveGen > handle.Gen) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
|
||||
@@ -502,7 +511,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
return &m_slotTable.GetOrCreate(handle);
|
||||
return &m_slotTables.GetOrCreate(handle);
|
||||
}
|
||||
|
||||
// The generation of the LIVE entry at this slot, or 0. It exists so a caller can
|
||||
@@ -510,7 +519,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// performs silently.
|
||||
Uint32 LiveGenAt(Uint32 slot) const {
|
||||
if (!EsprytSlotTablesEnabled()) return 0;
|
||||
return m_slotTable.LiveGenAt(slot);
|
||||
return m_slotTables.LiveGenAt(slot);
|
||||
}
|
||||
|
||||
// P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding
|
||||
@@ -524,12 +533,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// which is the answer the legacy walk's own weak_ptr test already gives.
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
m_slotTable.NoteStateForHandle(handle, stateObj);
|
||||
m_slotTables.NoteStateForHandle(handle, stateObj);
|
||||
}
|
||||
}
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
return m_slotTable.StateForHandle(handle);
|
||||
return m_slotTables.StateForHandle(handle);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -591,9 +600,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
if (EsprytSlotTablesEnabled()) {
|
||||
m_slotTable.ForEachLive(fn);
|
||||
m_slotTables.ForEachLive(fn);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- P14 S6: the KEYED shape's own controls ----------------------------------------
|
||||
//
|
||||
// Everything above is the shape a CALL SITE sees, and it is deliberately the shape it
|
||||
// saw before the rekey. These four are the shape only a caller that has to reason about
|
||||
// buckets needs: the teardown of a session, a test asking whether two contexts landed in
|
||||
// two namespaces, and a diagnostic.
|
||||
//
|
||||
// THE ONE BUCKET THIS THREAD RESOLVES TO. A by-value copy of it is exactly what a
|
||||
// fixture's saved registry was, and it is what makes `= {}` and "put it back" both mean
|
||||
// something under a key.
|
||||
SlotTable& BucketForCallingThread() {
|
||||
if (EsprytSlotTablesEnabled()) return m_slotTables.ForCallingThread();
|
||||
// The legacy arm has no buckets; the empty table's Reset() is the only honest
|
||||
// answer, and no shipping path asks (the arm is compiled out or the bit is clear).
|
||||
return m_legacyEmptyTable;
|
||||
}
|
||||
|
||||
Vector<SharedPtr<SlotTable>> BucketSnapshot() const {
|
||||
if (EsprytSlotTablesEnabled()) return m_slotTables.Snapshot();
|
||||
return {};
|
||||
}
|
||||
SizeT BucketCount() const {
|
||||
return EsprytSlotTablesEnabled() ? m_slotTables.BucketCount() : 0;
|
||||
}
|
||||
// Drop one {session, share group}. Answers whether such a bucket existed.
|
||||
Bool DropBucket(const TwinKey& key) {
|
||||
return EsprytSlotTablesEnabled() && m_slotTables.Drop(key);
|
||||
}
|
||||
// Drop every bucket of one session - the difference between "this session's twins go"
|
||||
// and "the process's do" - and answer how many groups that took with it.
|
||||
Uint32 DropBucketsOfSession(Uint64 sessionKey) {
|
||||
return EsprytSlotTablesEnabled() ? m_slotTables.DropSession(sessionKey) : 0;
|
||||
}
|
||||
// Empty every bucket. The teardown of a process-shaped world, where the key is {0, 0}.
|
||||
void ResetBuckets() {
|
||||
if (EsprytSlotTablesEnabled()) m_slotTables.Reset();
|
||||
}
|
||||
// Every bucket, walked with its key.
|
||||
template <typename Fn>
|
||||
void ForEachBucket(Fn&& fn) const {
|
||||
if (EsprytSlotTablesEnabled()) m_slotTables.ForEachBucket(fn);
|
||||
}
|
||||
#endif
|
||||
|
||||
// The seven DirectGLES.cpp call sites drive the LEGACY arm and nothing else. On the
|
||||
@@ -664,7 +716,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Uint32 m_creationTick = 0;
|
||||
Bool m_isCollecting = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
BackendSlotTable<StateObject, BackendObject, kKind> m_slotTable;
|
||||
// P14 S6: the ONE bucket per {session, share group}, hidden behind the same member names
|
||||
// the single table had, so every handle-keyed call site above and every caller below is
|
||||
// unchanged. See SlotTables.h's SlotTableRegistry for the resolution chain.
|
||||
SlotTables m_slotTables;
|
||||
// The legacy arm's stand-in for BucketForCallingThread(). Never read after a Reset and
|
||||
// never populated: the two entries that could write it are the handle-keyed ones, which
|
||||
// return early on that arm.
|
||||
SlotTable m_legacyEmptyTable;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -1081,8 +1140,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// HandleOf, no ForEachLive), and the handle overloads never look at it. Death is
|
||||
// announced by the family's own ResourceDestroy call, not by the shared death notice
|
||||
// (D-L), and the slot is freed by the CLIENT after that call returns.
|
||||
//
|
||||
// P14 S6: A BUCKET REGISTRY, like the six TwinRegistry tables. A buffer twin owns a
|
||||
// driver buffer id, so the SAME {slot, gen} in two contexts that do not share a group
|
||||
// must be two resources - and it was one process-wide table until this slice. The
|
||||
// handle-keyed member-for-member forwarding in SlotTables.h is what leaves every
|
||||
// `g_backendBufferResources.X(...)` call site below untouched.
|
||||
using BackendBufferResourceTable =
|
||||
BackendSlotTable<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
|
||||
SlotTableRegistry<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
|
||||
extern BackendBufferResourceTable g_backendBufferResources;
|
||||
|
||||
// Resolved once per process and latched, exactly like EsprytSlotTablesEnabled() and
|
||||
@@ -1305,6 +1370,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void UnregisterBufferBackendOps();
|
||||
// The ES context died: unregister ops, invalidate all outstanding GL ids
|
||||
// (they belonged to the dead context) and drop deferred deletes.
|
||||
// P14: a server session ended while another session's native context is still alive (one
|
||||
// process-wide share group): drops only the ending session's own staged bytes.
|
||||
void OnServerSessionEndedWithOthersLive();
|
||||
void OnBackendContextDestroyed();
|
||||
|
||||
// Get-or-create the backend resource and bring its storage up to date
|
||||
@@ -1876,12 +1944,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
switch (target) {
|
||||
case TextureTarget::Texture1D:
|
||||
case TextureTarget::TextureRectangle:
|
||||
case TextureTarget::External:
|
||||
return TextureTarget::Texture2D;
|
||||
case TextureTarget::Texture1DArray:
|
||||
return TextureTarget::Texture2DArray;
|
||||
// An external texture's storage is an imported EGLImage; to the driver it is 2D.
|
||||
case TextureTarget::TextureExternalOES:
|
||||
return TextureTarget::Texture2D;
|
||||
default:
|
||||
return target;
|
||||
}
|
||||
@@ -2038,6 +2104,18 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// level when the store covers it (notes/p9/W2-REMINT.md).
|
||||
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
|
||||
const MG_Pipe::MGPipeResourceRecord& record);
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// SHARED IMAGES (MGPipeResourceRecord::SharedImageId). True when this call settled the
|
||||
// storage - the ES name's level 0 IS the image (glEGLImageTargetTexture2DOES), or the
|
||||
// image could not be bound and the name is left empty - and SyncMipmapsToBackend must
|
||||
// not go on to allocate or upload. False when the record names no image: a twin that
|
||||
// was bound to one has been told to re-mint, and the normal storage path takes over.
|
||||
Bool SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record);
|
||||
// Pending sub-data written into the attached shared image (Managers.cpp).
|
||||
void ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record);
|
||||
// The attached image takes RGB texels only (an X format): uploads are converted.
|
||||
Bool m_sharedImageUploadsRgb = false;
|
||||
#endif
|
||||
// Whether this texture's ES storage was minted in an image carrier rather than in the
|
||||
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
|
||||
@@ -2072,6 +2150,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
|
||||
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
|
||||
Uint64 samplingGeneration) const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A shared image's texels move with its producer's presents, which nothing here
|
||||
// sees: every use goes through SyncMipmapsToBackend's shared-image arm, which waits
|
||||
// for a write that landed since the last one (SharedImageImpl::AcquireForSampling).
|
||||
if (m_sharedImage != nullptr) return false;
|
||||
#endif
|
||||
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
|
||||
m_syncedShapeGeneration != samplingGeneration) {
|
||||
return false;
|
||||
@@ -2163,6 +2247,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool m_isInitialized = false;
|
||||
Bool m_imageBindableStorageRequired = false;
|
||||
Bool m_backendStorageImmutable = false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The shared image the ES name's level 0 was last bound to (0 = none; image ids are
|
||||
// never reused, so this is a lifetime key), and the EGLImage that binding went through,
|
||||
// which holds the image for as long as the name samples it. Null with a nonzero id when
|
||||
// the image could not be bound: the name is left empty until the record moves.
|
||||
Uint64 m_sharedImageId = 0;
|
||||
SharedImageImpl::EglImageRef m_sharedImage;
|
||||
#endif
|
||||
// Whether the driver texture may hold texels the shadow never saw. Set the first time
|
||||
// anything other than a texel upload can write it - an application framebuffer
|
||||
// attachment, a writable image binding, a copy or blit into it, a driver-side mipmap
|
||||
@@ -2189,10 +2281,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// unconditionally bumps it), the content version on every CPU pixel mutation, and
|
||||
// the params version covers SetSamples/SetFixedSampleLocations, which bump neither
|
||||
// of the other two but feed the shape probe. The context id pins the generation to
|
||||
// the context that produced it - generations restart at 0 with a new context, and a
|
||||
// texture is owned by exactly one context (share groups are not implemented), so a
|
||||
// mutation can never happen under a context this key does not name. 0 = never
|
||||
// stamped (real context ids start at 1). Backend-side invalidation rides on
|
||||
// the context that produced it - generations restart at 0 with a new context, so the
|
||||
// id is part of the key. IT IS NOT AN OWNERSHIP CLAIM: a texture belongs to a SHARE
|
||||
// GROUP (P14 S3/S6 - share groups ARE implemented, `ShareGroupState` holds the object
|
||||
// tables and the twin tables are keyed by {session, share group}), so two contexts of
|
||||
// one group both legitimately mutate this texture. What the id buys is only the
|
||||
// cheaper half of the gate: a mutation made under a context sharing this texture
|
||||
// stamps a different id and re-derives the shape, which is a spurious re-derivation
|
||||
// (safe, and only on a shape mutation) rather than a missed one - the per-mutation
|
||||
// versions below are what actually catch a change, and they are group-independent.
|
||||
// 0 = never stamped (real context ids start at 1). Backend-side invalidation rides on
|
||||
// m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it.
|
||||
Uint64 m_syncedShapeContextId = 0;
|
||||
Uint64 m_syncedShapeGeneration = 0;
|
||||
@@ -2390,6 +2488,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
|
||||
// documented at their definitions. Declared here so tests can drive those memos directly.
|
||||
void SyncNeccessaryTextures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Every texture of this session bound to a shared image with sub-data still pending is
|
||||
// synced now, so the uploads are in the images before a flush publishes them (an X
|
||||
// server's PutImage into a window pixmap no later draw of its own samples).
|
||||
void WritePendingSharedImageUploads();
|
||||
#endif
|
||||
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
|
||||
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
|
||||
g_boundTexturesCache;
|
||||
@@ -3642,7 +3746,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// what makes HandleOf below resolve at all. Not one member that would dereference it is
|
||||
// instantiated - no Find(StateObject*), no ForEachLive - and the handle overloads never
|
||||
// look at it.
|
||||
using BackendSamplerViewTable = BackendSlotTable<MG_State::GLState::ITextureObject,
|
||||
// P14 S6: and a bucket registry for the same reason the buffer one above is - a sampler
|
||||
// view's twin is a driver object of the texture's group.
|
||||
using BackendSamplerViewTable = SlotTableRegistry<MG_State::GLState::ITextureObject,
|
||||
BackendSamplerViewObject,
|
||||
MG_Pipe::MGPipeKind::SamplerViewCso>;
|
||||
extern BackendSamplerViewTable g_backendSamplerViews;
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
|
||||
// P14 S6: the key this table is filed under includes the calling thread's {session, share
|
||||
// group}, and the probe that answers it lives on the backend's public surface beside S4's
|
||||
// native-tuple key (DirectGLES.h). Included here so this header is self-contained for the two
|
||||
// translation units that include it without copying Managers.h's include order.
|
||||
#include "DirectGLES.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
#include <MG_Impl/Pipe/SlotAllocator.h>
|
||||
#endif
|
||||
@@ -155,6 +161,250 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLE IS PER
|
||||
// {session, share group}, NOT PER PROCESS.
|
||||
// =====================================================================================
|
||||
//
|
||||
// WHAT WAS WRONG, in one sentence: a twin is a DRIVER object - a GL buffer id, a program, a
|
||||
// texture, the storage behind them - so it belongs to one native context's object namespace,
|
||||
// and a table keyed by {slot, gen} ALONE resolves the same handle value to the same twin
|
||||
// whichever context asked. The frontend gives two contexts that do NOT share a group two
|
||||
// independent name spaces, so the same {slot, gen} in both is legal and means two different
|
||||
// objects; one process-wide table made the second context adopt the first's twin (including
|
||||
// its driver ids, its persistent map and its pooled store). At the same time two contexts of
|
||||
// ONE group - which must see one set of objects - were served by whichever of them twinned
|
||||
// first, i.e. by luck rather than by the group the two agree on.
|
||||
//
|
||||
// THE KEY IS the same pair the applier files its OBJECT RECORDS under: {session, share
|
||||
// group}. Same probe, same `0 means this context started its own group` rule (see
|
||||
// DirectGLES.h's TwinKey and SessionRuntime.cpp's ResolveThreadTwinKey), so a record and the
|
||||
// twin it describes are always in the same bucket. Session is in the key because a twin
|
||||
// names a driver id and no driver id crosses a connection - two connections in one process
|
||||
// are two clients, and nothing may be shared with either from the other.
|
||||
//
|
||||
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
|
||||
// OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach
|
||||
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
|
||||
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
|
||||
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
|
||||
// every bucket of every table.
|
||||
//
|
||||
// THE SAME TABLE OBJECT IS REUSED, NOT REPLACED, and that is what keeps ~200 call sites
|
||||
// alive: `g_backendTextureObjects.Find(...)` still answers from the bucket the calling thread
|
||||
// is in, because the object the global names multiplexes on the key at each lookup. A caller
|
||||
// that must address a bucket OTHER than the calling thread's - the death walks, the
|
||||
// session-teardown drop - takes Shared() or calls the static entries, which walk buckets;
|
||||
// and a caller that wants the whole process's view (the teardown, the tests) takes Snapshot().
|
||||
// FORWARD-DECLARED, because the registry below is DEFINED before the table it holds; the
|
||||
// members that name the table are defined out of line at the bottom of this header.
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable;
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class SlotTableRegistry {
|
||||
public:
|
||||
using Table = BackendSlotTable<StateObject, BackendObject, kKind>;
|
||||
using Key = TwinKey;
|
||||
using StatePtr = SharedPtr<StateObject>;
|
||||
using BackendPtr = SharedPtr<BackendObject>;
|
||||
// The bound the two tables that ARE a bucket registry have always spelled by name; the
|
||||
// six registries reach it through their own `SlotTable` alias.
|
||||
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
|
||||
|
||||
// One bucket. SHARED, so a caller may keep it across other lookups: the map owns the
|
||||
// pointee and never moves it.
|
||||
SharedPtr<Table> Shared(const Key& key) const {
|
||||
// The common case on the hot path: the same key as last time. One compare, no probe.
|
||||
if (m_memoTable != nullptr && m_memoKey == key) return m_memoTable;
|
||||
const auto found = m_buckets.find(key);
|
||||
if (found != m_buckets.end()) {
|
||||
m_memoKey = key;
|
||||
m_memoTable = found->second;
|
||||
return found->second;
|
||||
}
|
||||
SharedPtr<Table> fresh = MakeShared<Table>();
|
||||
fresh->SetOwnerSession(key.SessionKey);
|
||||
m_buckets.emplace(key, fresh);
|
||||
m_memoKey = key;
|
||||
m_memoTable = fresh;
|
||||
return fresh;
|
||||
}
|
||||
|
||||
// The calling thread's bucket - the ONE entry every in-place call site below resolves
|
||||
// through, and the process-wide answer on a thread that belongs to no session.
|
||||
//
|
||||
// CONST, AND IT ANSWERS A MUTABLE BUCKET: the index is `mutable` because a LOOKUP may
|
||||
// have to create the bucket it is about to miss in (a handle with no twin yet is the
|
||||
// ordinary case), and the bucket's own entries are the table's business rather than the
|
||||
// registry's. The alternative - a const overload returning "no table" - would make every
|
||||
// read-only caller handle a case that cannot arise.
|
||||
Table& ForCallingThread() const {
|
||||
Key key;
|
||||
CurrentTwinKey(&key);
|
||||
return *Shared(key);
|
||||
}
|
||||
|
||||
// Every bucket, in no particular order.
|
||||
Vector<SharedPtr<Table>> Snapshot() const {
|
||||
Vector<SharedPtr<Table>> all;
|
||||
all.reserve(m_buckets.size());
|
||||
for (const auto& bucket : m_buckets) all.push_back(bucket.second);
|
||||
return all;
|
||||
}
|
||||
|
||||
SizeT BucketCount() const { return m_buckets.size(); }
|
||||
|
||||
// THE PER-BUCKET ONE-SHOT, because a bucket held by SharedPtr is never dropped by an
|
||||
// assignment: a caller that means "this context/group is gone" has to be able to say so,
|
||||
// and the key is how it says which one. The bucket LEAVES the index before its twins are
|
||||
// released, so a twin destructor that re-entered Shared() cannot find a half-erased one.
|
||||
Bool Drop(const Key& key) {
|
||||
const auto found = m_buckets.find(key);
|
||||
if (found == m_buckets.end()) return false;
|
||||
SharedPtr<Table> dropped = found->second;
|
||||
m_buckets.erase(found);
|
||||
if (m_memoTable == dropped) {
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
}
|
||||
dropped->Reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
// EVERY bucket of one session, for the teardown of a session that owned several groups.
|
||||
// The session is the key's high half, so this is one pass and no per-group bookkeeping -
|
||||
// and it is the difference between "this session's twins go" and "the process's do".
|
||||
Uint32 DropSession(Uint64 sessionKey) {
|
||||
Uint32 dropped = 0;
|
||||
for (auto bucket = m_buckets.begin(); bucket != m_buckets.end();) {
|
||||
if (bucket->first.SessionKey != sessionKey) {
|
||||
++bucket;
|
||||
continue;
|
||||
}
|
||||
SharedPtr<Table> table = bucket->second;
|
||||
if (m_memoTable == table) {
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
}
|
||||
bucket = m_buckets.erase(bucket);
|
||||
table->Reset();
|
||||
++dropped;
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
// Replace this registry with an empty one, releasing every bucket's twins. This is what
|
||||
// `registry = {}` meant when the registry WAS one table; a caller that must keep the
|
||||
// tables alive past the call takes a Snapshot() first.
|
||||
void Reset() {
|
||||
const Vector<SharedPtr<Table>> all = Snapshot();
|
||||
m_buckets.clear();
|
||||
m_memoKey = Key{};
|
||||
m_memoTable = nullptr;
|
||||
for (const SharedPtr<Table>& table : all) table->Reset();
|
||||
}
|
||||
|
||||
// ---- the calling thread's bucket, member for member ---------------------------------
|
||||
//
|
||||
// THIS IS WHY THE SHAPE COST THE CALL SITES NOTHING. Every handle-keyed entry the table
|
||||
// has is repeated here and forwarded to the bucket the calling thread resolves to, so
|
||||
// `g_backendTextureObjects.FindByHandle(h)` and `g_backendBufferResources.GetOrCreate(h)`
|
||||
// - unchanged source - answer from the right context's object namespace. A caller that
|
||||
// must name a bucket OTHER than its own (a death walk, a case) takes Shared()/Snapshot()
|
||||
// and calls the table directly.
|
||||
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().GetOrCreate(handle);
|
||||
}
|
||||
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().FindByHandle(handle);
|
||||
}
|
||||
const BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
return ForCallingThread().FindByHandle(handle);
|
||||
}
|
||||
// Resolve-or-create in a bucket the caller NAMES rather than the one it is in. The
|
||||
// monolith-glue frontend-keyed entry (StateBackendObjectRegistry::GetOrCreate below)
|
||||
// needs it: a state object handed over on the client side of a sync is described by a
|
||||
// record in the CALLING thread's group, and a twin built for it must land in the same
|
||||
// bucket as that record.
|
||||
BackendPtr& GetOrCreateIn(const Key& key, MG_Pipe::MGPipeHandle handle) {
|
||||
return Shared(key)->GetOrCreate(handle);
|
||||
}
|
||||
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
|
||||
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
|
||||
return ForCallingThread().HandleOf(stateObj);
|
||||
}
|
||||
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
|
||||
ForCallingThread().NoteStateForHandle(handle, stateObj);
|
||||
}
|
||||
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
|
||||
return ForCallingThread().StateForHandle(handle);
|
||||
}
|
||||
template <typename Fn>
|
||||
void ForEachLive(Fn&& fn) const {
|
||||
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
|
||||
// rather than a shortcut: a caller walking twins is asking "which of the objects I
|
||||
// can see need this", and what it can see is its own share group. Every bucket in
|
||||
// one process would walk a neighbour session's objects.
|
||||
ForCallingThread().ForEachLive(fn);
|
||||
}
|
||||
// Every bucket, in one pass. The caller receives the key with the handle so it can tell
|
||||
// which context a twin belongs to - the only walks that need the whole process's view
|
||||
// are a diagnostic and a teardown.
|
||||
template <typename Fn>
|
||||
void ForEachBucket(Fn&& fn) const {
|
||||
for (const auto& bucket : m_buckets) {
|
||||
bucket.second->ForEachLive(
|
||||
[&](MG_Pipe::MGPipeHandle handle, const BackendPtr& twin) { fn(bucket.first, handle, twin); });
|
||||
}
|
||||
}
|
||||
// The process-wide answer for the two statics: a notice is about an OBJECT, so it must
|
||||
// reach the twin in EVERY bucket of EVERY holder. They forward to the table's own
|
||||
// statics, which walk the holder list - each SHARED holder of it is one bucket.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return Table::ReleaseTwinByHandle(handle);
|
||||
}
|
||||
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
|
||||
return Table::OnFrontendObjectDestroyed(lifetimeId);
|
||||
}
|
||||
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
|
||||
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
|
||||
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
|
||||
// what happens to the driver id the twin owns. The bucket is the CALLING THREAD'S, which
|
||||
// is the bucket that twin was created in.
|
||||
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
return ForCallingThread().ReleaseByHandle(handle);
|
||||
}
|
||||
static Uint32 HolderCount() { return Table::HolderCount(); }
|
||||
Uint32 LiveCount() const { return ForCallingThread().LiveCount(); }
|
||||
Uint32 CompositeLiveCount() const { return ForCallingThread().CompositeLiveCount(); }
|
||||
SizeT OrdinaryCapacityForTest() const { return ForCallingThread().OrdinaryCapacityForTest(); }
|
||||
SizeT CompositeCapacityForTest() const { return ForCallingThread().CompositeCapacityForTest(); }
|
||||
|
||||
// ---- the bucket index's own controls, for the two tables that ARE a registry --------
|
||||
//
|
||||
// The six TwinRegistry registries carry the same three under the names in Managers.h
|
||||
// (DropBucket / DropBucketsOfSession / ResetBuckets); they are repeated here because
|
||||
// SlotTableRegistry IS the public type of the buffer and sampler-view tables, so a caller
|
||||
// of those has no registry object in between.
|
||||
Bool DropBucket(const Key& key) { return Drop(key); }
|
||||
// Drop every bucket of one session; answers how many groups that took with it.
|
||||
Uint32 DropBucketsOfSession(Uint64 sessionKey) { return DropSession(sessionKey); }
|
||||
// Empty every bucket (the process-shaped world's teardown, key {0, 0}).
|
||||
void ResetBuckets() { Reset(); }
|
||||
|
||||
private:
|
||||
// ONE BUCKET PER LIVE {session, share group}, held by SHARED pointer so a caller may
|
||||
// snapshot one across other lookups and so `Drop` can take a bucket's twins away
|
||||
// explicitly - an embedded table could do neither. `std::unordered_map` rather than the
|
||||
// tree's own flat Vector: this map is probed once per twin lookup on the draw path.
|
||||
mutable std::unordered_map<Key, SharedPtr<Table>, TwinKeyHash> m_buckets;
|
||||
// Hot-path memo. A registry is per KIND and each kind is touched several times per draw;
|
||||
// a thread only ever moves between buckets when its session binds another context.
|
||||
mutable SharedPtr<Table> m_memoTable;
|
||||
mutable Key m_memoKey;
|
||||
};
|
||||
|
||||
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
|
||||
class BackendSlotTable {
|
||||
public:
|
||||
@@ -209,6 +459,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// ENTRIES and the memo; the links are the table's own and are never copied.
|
||||
BackendSlotTable() { LinkHolder(); }
|
||||
BackendSlotTable(const BackendSlotTable& other):
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
m_slots(other.m_slots),
|
||||
m_band(other.m_band),
|
||||
m_nullTwin(other.m_nullTwin),
|
||||
@@ -217,6 +468,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
LinkHolder();
|
||||
}
|
||||
BackendSlotTable(BackendSlotTable&& other) noexcept:
|
||||
m_ownerSession(other.m_ownerSession),
|
||||
m_slots(std::move(other.m_slots)),
|
||||
m_band(std::move(other.m_band)),
|
||||
m_nullTwin(std::move(other.m_nullTwin)),
|
||||
@@ -252,6 +504,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
~BackendSlotTable() { UnlinkHolder(); }
|
||||
|
||||
// The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a
|
||||
// table that belongs to none (the process-wide registry on a sessionless thread, a
|
||||
// fixture's copy of one).
|
||||
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
|
||||
Uint64 OwnerSession() const { return m_ownerSession; }
|
||||
|
||||
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
|
||||
// object's lifetime id, so two calls for the same live object always land on the same
|
||||
// slot, and a successor object at the same heap address never does.
|
||||
@@ -553,12 +811,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// double Free would be refused by generation anyway. Idempotent against the kind's
|
||||
// own delete opcode, exactly as the notice arm is: a twin the delete already released
|
||||
// fails ReleaseTwinAt's generation check and the walk moves on.
|
||||
//
|
||||
// ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own
|
||||
// client's allocator, so with several clients served by one process the same {slot, gen}
|
||||
// names a live, unrelated object in every other session: walking every holder released
|
||||
// THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's
|
||||
// driver texture with the same handle, and its windows sampled freed memory. On a thread
|
||||
// that belongs to a session only that session's buckets answer; a sessionless thread
|
||||
// (the in-library backend, a unit fixture) keeps the process-wide walk.
|
||||
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
|
||||
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
|
||||
TwinKey key;
|
||||
const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0;
|
||||
Bool released = false;
|
||||
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
|
||||
BackendSlotTable* const next = holder->m_nextHolder;
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
if (!sessionScoped || holder->m_ownerSession == key.SessionKey) {
|
||||
released = holder->ReleaseTwinAt(handle) || released;
|
||||
}
|
||||
holder = next;
|
||||
}
|
||||
return released;
|
||||
@@ -616,6 +886,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty this table, releasing every twin it holds. THE SPACES ARE MOVED OUT FIRST and
|
||||
// that is the whole of the care taken here: a twin's destructor is a driver call that
|
||||
// could re-enter GetOrCreate on this table (SlotTables.h's ReleaseTwinAt says why), and
|
||||
// destroying the entries in place would leave that re-entry resizing the vector the
|
||||
// destructors are being walked out of. Here the table is already empty when the entries
|
||||
// die, so a re-entrant insert grows a fresh vector.
|
||||
//
|
||||
// This is what `table = {}` used to mean, spelled so a caller cannot miss the ordering.
|
||||
void Reset() {
|
||||
Vector<Entry> slots = std::move(m_slots);
|
||||
Vector<Entry> band = std::move(m_band);
|
||||
BackendPtr nullTwin = std::move(m_nullTwin);
|
||||
ForgetHandle();
|
||||
slots.clear();
|
||||
band.clear();
|
||||
nullTwin.reset();
|
||||
}
|
||||
|
||||
Uint32 LiveCount() const {
|
||||
Uint32 count = 0;
|
||||
for (const Entry& entry : m_slots) {
|
||||
@@ -745,6 +1033,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
static inline BackendSlotTable* s_firstHolder = nullptr;
|
||||
BackendSlotTable* m_prevHolder = nullptr;
|
||||
BackendSlotTable* m_nextHolder = nullptr;
|
||||
Uint64 m_ownerSession = 0;
|
||||
|
||||
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
|
||||
Vector<Entry> m_slots;
|
||||
|
||||
@@ -572,7 +572,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
|
||||
#endif
|
||||
// The name is the marker: ShaderSourceProcessor only emits it when the source
|
||||
// wrote gl_FragColor, and such a shader can have no other output.
|
||||
// names gl_FragColor, and a shader that writes it can have no other output. One that
|
||||
// names it only in an inactive preprocessor arm keeps the declaration unwritten, and
|
||||
// the IO resolver moves that inactive output off location 0, so the match below
|
||||
// (which wants location 0) does not take it.
|
||||
static const char* const kLoweredName = "mg_FragColor";
|
||||
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
|
||||
return glslCode;
|
||||
|
||||
@@ -332,6 +332,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() {
|
||||
// Everything below reaches this session's renderer slot, whichever thread destroys it.
|
||||
MagmaSession* const ownSession = m_magmaSession.get();
|
||||
MagmaSession* const previousSession = BoundMagmaSession();
|
||||
if (ownSession != nullptr) BindMagmaSessionToThisThread(ownSession);
|
||||
// P12 review fix: A SERVER SESSION THAT ENDS WITHOUT A CLEAN TEARDOWN LEAVES NO RENDERER ON
|
||||
// THE WINDOW. The renderer - swapchain and VkSurfaceKHR on the window - is a process global
|
||||
// that only ReleaseEGLResources / a released surface drops, and a client that went away
|
||||
@@ -349,6 +353,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
ForgetRequestedSwapInterval();
|
||||
}
|
||||
// The session's state goes with it, and no thread may keep answering with it.
|
||||
m_magmaSession.reset();
|
||||
if (ownSession != nullptr) {
|
||||
BindMagmaSessionToThisThread(previousSession == ownSession ? nullptr : previousSession);
|
||||
}
|
||||
}
|
||||
#else
|
||||
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() = default;
|
||||
@@ -357,6 +366,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
|
||||
|
||||
Bool BackendObject_DirectVulkan::InitWindowSurface() {
|
||||
// A suspended surface activates its placeholder - never the window, which is gone.
|
||||
const auto suspended = m_suspendedWindowSurfaces.find(m_activatingSurface);
|
||||
if (suspended != m_suspendedWindowSurfaces.end()) {
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second);
|
||||
}
|
||||
return InitPbufferSurface(static_cast<EGLint>(suspended->second.Width),
|
||||
static_cast<EGLint>(suspended->second.Height));
|
||||
}
|
||||
if (!m_windowHandle.Handle) {
|
||||
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
|
||||
return false;
|
||||
@@ -364,14 +382,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
|
||||
|
||||
VulkanRendererConfig config;
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config);
|
||||
}
|
||||
// Any renderer instance this assignment replaces is destroyed here;
|
||||
// fence/timer-query handles stamped with the old generation go stale.
|
||||
BumpRendererGeneration();
|
||||
VulkanRendererConfig config;
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -379,15 +401,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
|
||||
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) {
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{},
|
||||
config);
|
||||
}
|
||||
// Any renderer instance this assignment replaces is destroyed here;
|
||||
// fence/timer-query handles stamped with the old generation go stale.
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
|
||||
return true;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::BindSessionStateToThisThread() {
|
||||
if (m_magmaSession == nullptr) m_magmaSession = MakeUnique<MagmaSession>();
|
||||
BindMagmaSessionToThisThread(m_magmaSession.get());
|
||||
MGLOG_I("DirectVulkan: session state %llu bound to this apply thread",
|
||||
static_cast<unsigned long long>(m_magmaSession->Id()));
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::Initialize() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager::RegisterWireResourceOps();
|
||||
@@ -463,6 +505,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
|
||||
return false;
|
||||
}
|
||||
const EGLSurfaceState* state = GetRegisteredEGLSurface(surface);
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer && state != nullptr && state->Kind == SurfaceKind::Pbuffer) {
|
||||
// A pbuffer-backed window (a Wayland window presented through wl_shm) whose target is an
|
||||
// image reader of FIXED size: a swapchain rebuild cannot follow the resize, so the target
|
||||
// is rebuilt at the new size - as DirectGLES replaces its native pbuffer. A parked one is
|
||||
// rebuilt when it is next activated, from the size registered above.
|
||||
const Bool active = m_eglSurfaceInitialized && m_eglSurface == surface;
|
||||
pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
|
||||
if (active) {
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = static_cast<Uint32>(state->Width);
|
||||
config.SurfaceHeight = static_cast<Uint32>(state->Height);
|
||||
(void)pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (pVulkanRenderer && m_eglSurface == surface) {
|
||||
pVulkanRenderer->RequestSwapchainResize(width, height);
|
||||
}
|
||||
@@ -480,7 +538,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
const Bool ok = BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
if (ok && UsesSurfaceTargets() && ctx != EGL_NO_CONTEXT && draw != EGL_NO_SURFACE) {
|
||||
m_contextBindings[ctx] = EGLCurrentState{.Display = dpy, .DrawSurface = draw, .ReadSurface = read,
|
||||
.Context = ctx};
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnClientContextBound(Uint64 token) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!UsesSurfaceTargets() || token == 0) return;
|
||||
// Each of a multi-window client's contexts draws to the surface IT was made current with
|
||||
// (Qt renders every window on its own thread and context); the ring interleaves them with
|
||||
// a bind_context between, and no make-current. Follow the context back to its surface.
|
||||
const auto found = m_contextBindings.find(reinterpret_cast<EGLContext>(token));
|
||||
if (found == m_contextBindings.end()) return;
|
||||
const EGLCurrentState binding = found->second;
|
||||
if (!GetRegisteredEGLSurface(binding.DrawSurface)) {
|
||||
m_contextBindings.erase(found);
|
||||
return;
|
||||
}
|
||||
const auto current = m_eglCurrentThreads.find(std::this_thread::get_id());
|
||||
if (current != m_eglCurrentThreads.end() && current->second.Context == binding.Context &&
|
||||
current->second.DrawSurface == binding.DrawSurface && m_eglSurface == binding.DrawSurface) {
|
||||
return;
|
||||
}
|
||||
(void)BackendObject::MakeEGLCurrent(binding.Display, binding.DrawSurface, binding.ReadSurface, binding.Context);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
@@ -492,6 +576,50 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return BackendObject::SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Same lock as SwapEGLBuffers: the copy reads the current surface target's image.
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!pVulkanRenderer) {
|
||||
MGLOG_E("DirectVulkan: shared image %llu: the renderer is not initialized",
|
||||
static_cast<unsigned long long>(image.Id));
|
||||
return false;
|
||||
}
|
||||
return pVulkanRenderer->BlitDefaultFramebufferToSharedImage(image, region);
|
||||
#else
|
||||
(void)image, (void)region;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::PublishSharedImageAccesses() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->PublishSharedImageAccesses() : true;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ExportNativeFence(int* fence) {
|
||||
*fence = -1;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->ExportNativeFence(fence) : true;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// A swapchain image keeps its content through a present whatever region is drawn next: the
|
||||
// damage region the client may declare is not needed here.
|
||||
Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge(Bool damageRegionFollows) {
|
||||
(void)damageRegionFollows;
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
return pVulkanRenderer ? pVulkanRenderer->CurrentDrawBufferAge() : 0;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
|
||||
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
@@ -512,11 +640,131 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The reflection cache is file-scope, not renderer-owned; without this the
|
||||
// deleted programs' reflection strings survive full context teardown.
|
||||
ClearProgramResourceCaches();
|
||||
m_suspendedWindowSurfaces.clear();
|
||||
BackendObject::ReleaseEGLResources();
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ActivateSuspendedPlaceholder(EGLSurface surface,
|
||||
const SuspendedWindowSurface& extent) {
|
||||
// A pbuffer target at the window's extent: every default-framebuffer path (render passes,
|
||||
// read-back, the shared-image blit, the acquire and the present) keeps a live swapchain to
|
||||
// work on, and the extent it publishes is the one the client already has.
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = std::max<Uint32>(extent.Width, 1);
|
||||
config.SurfaceHeight = std::max<Uint32>(extent.Height, 1);
|
||||
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
// The monolith's one renderer IS its window; only surface targets can be swapped under it.
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
Vector<EGLSurface> onWindow;
|
||||
for (const auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
|
||||
onWindow.push_back(client);
|
||||
}
|
||||
for (const EGLSurface client : onWindow) {
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
SuspendedWindowSurface extent;
|
||||
extent.Width = static_cast<Uint32>(std::max<EGLint>(state->second.Width, 1));
|
||||
extent.Height = static_cast<Uint32>(std::max<EGLint>(state->second.Height, 1));
|
||||
if (pVulkanRenderer) {
|
||||
const Uint64 key = SurfaceTargetKey(client);
|
||||
const VkExtent2D built = pVulkanRenderer->SurfaceTargetExtent(key);
|
||||
if (built.width != 0 && built.height != 0) {
|
||||
extent.Width = built.width;
|
||||
extent.Height = built.height;
|
||||
}
|
||||
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
|
||||
// Flushes, waits the device idle, purges the render passes on the target and drops its
|
||||
// swapchain and VkSurfaceKHR - nothing references the window after this.
|
||||
pVulkanRenderer->DestroySurfaceTarget(key);
|
||||
m_suspendedWindowSurfaces[client] = extent;
|
||||
// A placeholder that cannot be built is a suspend that failed: the window is already let
|
||||
// go of, and the caller ends the session by name instead (its EGL resources go).
|
||||
if (active && !ActivateSuspendedPlaceholder(client, extent)) return false;
|
||||
} else {
|
||||
m_suspendedWindowSurfaces[client] = extent;
|
||||
}
|
||||
}
|
||||
MGLOG_I("DirectVulkan: %zu window surface(s) on server window %p now draw into placeholder pbuffer targets; "
|
||||
"the renderer, the device and every object stay",
|
||||
onWindow.size(), window);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
UnorderedMap<EGLSurface, SuspendedWindowSurface> suspended;
|
||||
suspended.swap(m_suspendedWindowSurfaces);
|
||||
for (const auto& [client, extent] : suspended) {
|
||||
(void)extent;
|
||||
const auto state = m_eglSurfaces.find(client);
|
||||
if (state == m_eglSurfaces.end()) continue; // released while suspended
|
||||
state->second.Window.Handle = window;
|
||||
state->second.Window.Width = width;
|
||||
state->second.Window.Height = height;
|
||||
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state->second.Window;
|
||||
if (!pVulkanRenderer) continue;
|
||||
const Uint64 key = SurfaceTargetKey(client);
|
||||
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
|
||||
pVulkanRenderer->DestroySurfaceTarget(key); // the placeholder
|
||||
// A parked surface is built on the new window when it is next activated (InitWindowSurface).
|
||||
if (!active) continue;
|
||||
// The creation's own path: a new VkSurfaceKHR and swapchain with the surface's current
|
||||
// pre-transform, a new target serial (render passes rebuilt), and the extent published.
|
||||
VulkanRendererConfig config;
|
||||
config.SurfaceWidth = std::max<Uint32>(width, 1);
|
||||
config.SurfaceHeight = std::max<Uint32>(height, 1);
|
||||
config.SwapInterval = GetRequestedSwapInterval();
|
||||
if (!pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(window), config)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
|
||||
window, width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (window == nullptr || !UsesSurfaceTargets()) return false;
|
||||
Bool followed = false;
|
||||
for (auto& [client, state] : m_eglSurfaces) {
|
||||
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
|
||||
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
|
||||
state.Window.Width = width;
|
||||
state.Window.Height = height;
|
||||
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == client) m_windowHandle = state.Window;
|
||||
// Magma's extent is its swapchain's, published when it is built: the active target follows
|
||||
// the window now (or at the half-recorded frame's present); a parked one at its next present.
|
||||
if (pVulkanRenderer && pVulkanRenderer->IsActiveSurfaceTarget(SurfaceTargetKey(client))) {
|
||||
followed = pVulkanRenderer->FollowActiveWindowResize() || followed;
|
||||
}
|
||||
}
|
||||
return followed;
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const {
|
||||
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
m_suspendedWindowSurfaces.erase(surface);
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
// The renderer outlives its surfaces here: only this surface's target goes (in
|
||||
// OnEGLSurfaceForgotten, which follows), and every fence stays valid.
|
||||
if (UsesSurfaceTargets() && pVulkanRenderer) return;
|
||||
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
@@ -565,6 +813,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
|
||||
V_OpenGL44, V_OpenGL45, V_OpenGL46,
|
||||
E_GL_ARB_draw_buffers_blend,
|
||||
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
|
||||
// trivially true for every context this backend claims (4.6). KWin 6's
|
||||
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
|
||||
// strings, so withholding them fails its renderer bring-up. Same block as DirectGLES.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
|
||||
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
|
||||
E_GL_ARB_multi_draw_indirect,
|
||||
@@ -659,38 +913,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Advertised with GL_NUM_PROGRAM_BINARY_FORMATS = 0, which the
|
||||
// extension explicitly permits. It is also the only thing that
|
||||
// exposes glProgramParameteri before GL 4.1.
|
||||
E_GL_ARB_get_program_binary,
|
||||
// kwin_wayland 6.7.3 will not use a context it cannot drive, and it asks for these
|
||||
// three names first: EglContext::checkSupported() (src/opengl/eglcontext.cpp) takes the
|
||||
// DESKTOP-OpenGL branch for every context whose GL_VERSION is not an "OpenGL ES"
|
||||
// string, and that branch requires GL_ARB_shader_objects, GL_ARB_fragment_shader and
|
||||
// GL_ARB_vertex_shader. GL_VERSION here is "4.6.0 MobileGL ...", so with the names
|
||||
// missing checkSupported() answered false, EglContext::create() returned nullptr,
|
||||
// EglBackend::createContext() returned false, and kwin printed "Could not initialize
|
||||
// rendering context" and quit before drawing a frame.
|
||||
//
|
||||
// The three are true statements about this backend too, and the entry points behind
|
||||
// them are the same ones the DirectGLES half of this list is advertised with: the
|
||||
// frontend implements all 39 ARB_shader_objects procedures, ARB_vertex_shader's three
|
||||
// attrib-location entry points, and nothing for ARB_fragment_shader, whose "New
|
||||
// Procedures and Functions" section reads "None". The shader and program objects live in
|
||||
// MG_Impl/MG_State, above the backend that draws with them, so a name that is true on
|
||||
// one backend is true on the other.
|
||||
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
|
||||
// NPOT textures: Vulkan puts no power-of-two restriction on an image, its mip chain or
|
||||
// its samplers, so this is the strongest form of the promise the string makes. Kept
|
||||
// identical to the DirectGLES group so the two backends do not disagree about what
|
||||
// MobileGL is.
|
||||
E_GL_ARB_texture_non_power_of_two,
|
||||
// GL_OES_EGL_image: the entry points are real (MG_Impl/GLImpl/Exporting/Definitions.cpp)
|
||||
// and this name is what makes them reachable - libepoxy in kwin resolves
|
||||
// glEGLImageTargetTexture2DOES BY NAME and refuses the import path without it, printing
|
||||
// "No provider of glEGLImageTargetTexture2DOES found. Requires one of: GL_OES_EGL_image".
|
||||
// That import path is how the compositor's scene reaches the display daemon's dma-bufs.
|
||||
// The render-server half of the import (fd over the GBM socket, then the server-side
|
||||
// EGLImage) is the next change; until it lands an imported texture has no storage behind
|
||||
// it, which is logged once at the entry point rather than left to look like a black frame.
|
||||
E_GL_OES_EGL_image};
|
||||
E_GL_ARB_get_program_binary};
|
||||
// Vulkan's drawIndirectFirstInstance feature is optional. Direct base-instance calls work
|
||||
// without it, but ARB_base_instance also promises non-zero firstInstance in GPU indirect
|
||||
// commands; the renderer supplies true only when that word is legal and gl_InstanceID can
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "../BackendObject.h"
|
||||
#include "MagmaSession.h"
|
||||
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -26,6 +27,10 @@ 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;
|
||||
@@ -35,8 +40,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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;
|
||||
@@ -48,6 +66,11 @@ 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();
|
||||
|
||||
@@ -55,6 +78,21 @@ 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
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
#include <spirv_reflect.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Leak-at-exit storage; see GlobalObjects.cpp.
|
||||
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
|
||||
// Per Magma session (MagmaSession.h); the process-wide session is leak-at-exit storage, see
|
||||
// GlobalObjects.cpp.
|
||||
RendererSlot pVulkanRenderer;
|
||||
|
||||
namespace {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -59,7 +60,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// current one (a new renderer restarts its frame-serial counter and
|
||||
// reuses pool indices). Atomic because handles may be polled from a
|
||||
// thread other than the EGL thread that recreates the renderer.
|
||||
std::atomic<Uint64> g_rendererGeneration{1};
|
||||
//
|
||||
// One per Magma session, drawn from one process-wide source: a handle stamped by one
|
||||
// session's renderer must read as stale against every other session's, never equal.
|
||||
std::atomic<Uint64> g_rendererGenerationSource{1};
|
||||
struct RendererGeneration {
|
||||
std::atomic<Uint64> value{g_rendererGenerationSource.fetch_add(1, std::memory_order_acq_rel)};
|
||||
};
|
||||
SessionLocal<RendererGeneration> g_rendererGeneration;
|
||||
} // namespace
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
@@ -123,11 +131,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
|
||||
Uint64 GetRendererGeneration() {
|
||||
return g_rendererGeneration.load(std::memory_order_acquire);
|
||||
return g_rendererGeneration->value.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void BumpRendererGeneration() {
|
||||
g_rendererGeneration.fetch_add(1, std::memory_order_acq_rel);
|
||||
g_rendererGeneration->value.store(g_rendererGenerationSource.fetch_add(1, std::memory_order_acq_rel),
|
||||
std::memory_order_release);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -1274,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
payload.mode = mode;
|
||||
|
||||
// TODO: allocate draw cmd buf elsewhere
|
||||
static Vector<DrawCmdParam> params;
|
||||
static thread_local Vector<DrawCmdParam> params;
|
||||
params.clear();
|
||||
params.resize(drawcount);
|
||||
|
||||
@@ -1347,7 +1356,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
|
||||
|
||||
// TODO: allocate draw cmd buf elsewhere
|
||||
static Vector<DrawIndexedCmdParam> params;
|
||||
static thread_local Vector<DrawIndexedCmdParam> params;
|
||||
params.clear();
|
||||
params.resize(drawcount);
|
||||
|
||||
@@ -1751,7 +1760,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void Present() {
|
||||
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
|
||||
// A served session's renderer is gone once its client released its resources; a present
|
||||
// that still reaches it has nothing to show and is dropped rather than dereferenced.
|
||||
if (!pVulkanRenderer) {
|
||||
MGLOG_E_ONCE("DirectVulkan: a present reached a session whose renderer is already released; dropped");
|
||||
return;
|
||||
}
|
||||
pVulkanRenderer->Present();
|
||||
// THE frame boundary for the MGPipe counters, at the backend entry point rather
|
||||
// than inside VulkanRenderer::Present: that function has an early return for the
|
||||
@@ -1763,23 +1777,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
namespace {
|
||||
Optional<Int> g_requestedSwapInterval;
|
||||
// Per session: each client asks for its own interval.
|
||||
SessionLocal<Optional<Int>> g_requestedSwapInterval;
|
||||
} // namespace
|
||||
|
||||
void SetSwapInterval(Int interval) {
|
||||
g_requestedSwapInterval = interval;
|
||||
*g_requestedSwapInterval = interval;
|
||||
if (pVulkanRenderer) {
|
||||
pVulkanRenderer->SetSwapInterval(interval);
|
||||
}
|
||||
}
|
||||
|
||||
Optional<Int> GetRequestedSwapInterval() {
|
||||
return g_requestedSwapInterval;
|
||||
return *g_requestedSwapInterval;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void ForgetRequestedSwapInterval() {
|
||||
g_requestedSwapInterval.reset();
|
||||
g_requestedSwapInterval->reset();
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -9,10 +9,32 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include "MagmaSession.h"
|
||||
#include "Renderer/VulkanRenderer.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
|
||||
// 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;
|
||||
|
||||
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
||||
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// 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
|
||||
@@ -0,0 +1,100 @@
|
||||
// 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
|
||||
@@ -66,7 +66,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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());
|
||||
}
|
||||
m_spareCommandBuffers.clear();
|
||||
m_frames.clear();
|
||||
currentFrameIndex = 0;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
@@ -300,6 +304,15 @@ 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();
|
||||
@@ -353,21 +366,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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 = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
|
||||
VkResult result = TakeCommandBuffer(replacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
if (retirePreCommandBuffer) {
|
||||
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
|
||||
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
|
||||
result = TakeCommandBuffer(preReplacement);
|
||||
if (result != VK_SUCCESS) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
|
||||
RecycleCommandBuffer(replacement);
|
||||
return result;
|
||||
}
|
||||
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
|
||||
@@ -402,8 +410,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
SizeT completedCount = 0;
|
||||
while (completedCount < frame.retiredCommandBuffers.size() &&
|
||||
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
|
||||
vkFreeCommandBuffers(m_device, m_commandPool, 1,
|
||||
&frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
RecycleCommandBuffer(frame.retiredCommandBuffers[completedCount].commandBuffer);
|
||||
++completedCount;
|
||||
}
|
||||
if (completedCount > 0) {
|
||||
@@ -413,6 +420,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -101,6 +101,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
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);
|
||||
|
||||
@@ -143,5 +150,10 @@ 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
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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
|
||||
@@ -391,6 +391,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
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");
|
||||
|
||||
@@ -199,7 +199,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
|
||||
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
|
||||
#include "MG_Util/ShaderTranspiler/Types.h"
|
||||
#include <Config.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
@@ -1313,6 +1315,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
});
|
||||
if (!ok) return false;
|
||||
// An access chain into the builtin (gl_FragCoord.y is one) carries an Input
|
||||
// pointer type. Based on the Private copy it has to carry a Private one, and so
|
||||
// does every chain built on top of it - otherwise the module hands the driver a
|
||||
// pointer whose storage class disagrees with its base, which is invalid SPIR-V
|
||||
// and reads an undefined value. Plan every retype before touching anything, so a
|
||||
// shape this pass does not model declines the whole rewrite.
|
||||
Vector<Pair<Instruction*, Uint32>> retypes;
|
||||
Vector<Instruction*> chains;
|
||||
for (Instruction* user : users) {
|
||||
if (IsAccessChain(user->opcode())) chains.push_back(user);
|
||||
}
|
||||
while (!chains.empty()) {
|
||||
Instruction* chain = chains.back();
|
||||
chains.pop_back();
|
||||
const Uint32 privateType = PrivatePointerTypeFor(chain->type_id());
|
||||
if (privateType == 0) return false;
|
||||
retypes.emplace_back(chain, privateType);
|
||||
context()->get_def_use_mgr()->ForEachUser(chain, [&](Instruction* next) {
|
||||
if (IsAccessChain(next->opcode()) && next->NumInOperands() > 0 &&
|
||||
next->GetSingleWordInOperand(0) == chain->result_id()) {
|
||||
chains.push_back(next);
|
||||
}
|
||||
});
|
||||
}
|
||||
for (Instruction* user : users) {
|
||||
for (Uint32 i = 0; i < user->NumInOperands(); ++i) {
|
||||
auto& operand = user->GetInOperand(i);
|
||||
@@ -1323,9 +1349,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
context()->AnalyzeUses(user);
|
||||
}
|
||||
for (auto& [chain, privateType] : retypes) {
|
||||
chain->SetResultType(privateType);
|
||||
context()->AnalyzeUses(chain);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool IsAccessChain(spv::Op opcode) {
|
||||
return opcode == spv::Op::OpAccessChain || opcode == spv::Op::OpInBoundsAccessChain ||
|
||||
opcode == spv::Op::OpPtrAccessChain || opcode == spv::Op::OpInBoundsPtrAccessChain;
|
||||
}
|
||||
|
||||
// The Private-storage twin of an Input pointer type, or 0 when `pointerTypeId` is not
|
||||
// an Input pointer (then the chain is not the shape this pass rewrites).
|
||||
Uint32 PrivatePointerTypeFor(Uint32 pointerTypeId) {
|
||||
using namespace spvtools::opt;
|
||||
Instruction* pointerType = context()->get_def_use_mgr()->GetDef(pointerTypeId);
|
||||
if (!pointerType || pointerType->opcode() != spv::Op::OpTypePointer) return 0;
|
||||
if (static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) !=
|
||||
spv::StorageClass::Input) {
|
||||
return 0;
|
||||
}
|
||||
return context()->get_type_mgr()->FindPointerToType(pointerType->GetSingleWordInOperand(1),
|
||||
spv::StorageClass::Private);
|
||||
}
|
||||
|
||||
Uint32 m_framebufferHeight = 0;
|
||||
};
|
||||
|
||||
@@ -1861,35 +1910,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
|
||||
}
|
||||
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
Uint32 framebufferHeight) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
if (framebufferHeight == 0) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(
|
||||
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
Uint32 framebufferHeight) {
|
||||
if (input.empty()) {
|
||||
output.clear();
|
||||
return true;
|
||||
}
|
||||
if (framebufferHeight == 0) {
|
||||
output = input;
|
||||
return true;
|
||||
}
|
||||
|
||||
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
|
||||
spvtools::OptimizerOptions options;
|
||||
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
|
||||
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
|
||||
const char* message) {
|
||||
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
|
||||
});
|
||||
optimizer.RegisterPass(
|
||||
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
|
||||
|
||||
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
|
||||
if (!success) {
|
||||
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
|
||||
output = input;
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
|
||||
const MagmaProgramSource& program) {
|
||||
if (input.empty()) {
|
||||
@@ -2223,8 +2275,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ScreenModuleForDriver's memory. Keyed by content, never by program: the same bytes get
|
||||
// the same answer whichever program, variant or session carries them.
|
||||
struct DriverModuleVerdictCache {
|
||||
struct Entry {
|
||||
Uint64 wordCount = 0;
|
||||
Bool valid = true;
|
||||
String reason;
|
||||
};
|
||||
// Generous for any one application's distinct stage modules; past it the whole table
|
||||
// is dropped rather than tracked per entry - a re-validation is the only cost.
|
||||
static constexpr SizeT kMaxEntries = 8192;
|
||||
std::mutex mutex;
|
||||
UnorderedMap<Uint64, Entry> entries;
|
||||
std::atomic<Uint64> validatorRuns{0};
|
||||
};
|
||||
|
||||
DriverModuleVerdictCache& GetDriverModuleVerdictCache() {
|
||||
// Leaked on purpose: screened from session threads that may outlive static destruction.
|
||||
static auto* cache = new DriverModuleVerdictCache();
|
||||
return *cache;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ProgramFactory::DriverModuleVerdict ProgramFactory::ScreenModuleForDriver(const Vector<Uint>& spirv,
|
||||
Bool allowInvalid, String* outReason) {
|
||||
if (outReason != nullptr) outReason->clear();
|
||||
if (spirv.empty()) return DriverModuleVerdict::Valid;
|
||||
|
||||
auto& cache = GetDriverModuleVerdictCache();
|
||||
const Uint64 key = XXH64(spirv.data(), spirv.size() * sizeof(Uint), 0);
|
||||
Bool valid = true;
|
||||
String reason;
|
||||
Bool known = false;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
const auto it = cache.entries.find(key);
|
||||
if (it != cache.entries.end() && it->second.wordCount == spirv.size()) {
|
||||
known = true;
|
||||
valid = it->second.valid;
|
||||
reason = it->second.reason;
|
||||
}
|
||||
}
|
||||
if (!known) {
|
||||
// Outside the lock: two threads screening the same new module both validate, and
|
||||
// agree; nothing waits on another session's spirv-val.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
|
||||
cache.validatorRuns.fetch_add(1, std::memory_order_relaxed);
|
||||
spv_context context = spvContextCreate(GetSpirvTargetEnv(spirv));
|
||||
spv_validator_options options = spvValidatorOptionsCreate();
|
||||
spvValidatorOptionsSetFriendlyNames(options, true);
|
||||
spv_const_binary_t binary = {spirv.data(), spirv.size()};
|
||||
spv_diagnostic diagnostic = nullptr;
|
||||
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
|
||||
valid = result == SPV_SUCCESS;
|
||||
if (!valid) {
|
||||
reason = diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<no diagnostic>";
|
||||
// The process-wide counter the validating test lanes assert on.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
|
||||
}
|
||||
spvDiagnosticDestroy(diagnostic);
|
||||
spvValidatorOptionsDestroy(options);
|
||||
spvContextDestroy(context);
|
||||
|
||||
const std::lock_guard<std::mutex> lock(cache.mutex);
|
||||
if (cache.entries.size() >= DriverModuleVerdictCache::kMaxEntries) cache.entries.clear();
|
||||
cache.entries[key] = DriverModuleVerdictCache::Entry{spirv.size(), valid, reason};
|
||||
}
|
||||
|
||||
if (valid) return DriverModuleVerdict::Valid;
|
||||
if (outReason != nullptr) *outReason = Move(reason);
|
||||
return allowInvalid ? DriverModuleVerdict::AdmittedInvalid : DriverModuleVerdict::DeclinedInvalid;
|
||||
}
|
||||
|
||||
Uint64 ProgramFactory::DriverModuleValidatorRuns() {
|
||||
return GetDriverModuleVerdictCache().validatorRuns.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
|
||||
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
|
||||
// position math between pipelines cannot desynchronize it; the blended depth-write
|
||||
@@ -2401,6 +2529,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return SamplerNumericDomain::UnsignedInteger;
|
||||
case GL_SAMPLER_1D:
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_EXTERNAL_OES:
|
||||
case GL_SAMPLER_3D:
|
||||
case GL_SAMPLER_CUBE:
|
||||
case GL_SAMPLER_2D_RECT:
|
||||
@@ -2591,6 +2720,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY:
|
||||
return TextureTarget::TextureCubeMapArray;
|
||||
case GL_SAMPLER_2D:
|
||||
case GL_SAMPLER_EXTERNAL_OES: // a 2D texture below the frontend
|
||||
case GL_SAMPLER_2D_SHADOW:
|
||||
case GL_INT_SAMPLER_2D:
|
||||
case GL_UNSIGNED_INT_SAMPLER_2D:
|
||||
@@ -3764,27 +3894,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
|
||||
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
|
||||
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only in
|
||||
// DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed, and for every stage BEFORE any
|
||||
// module is created. By here the descriptor bindings have been remapped and the layout
|
||||
// about to be reflected describes the remapped module, so there is no module left that
|
||||
// is both valid and consistent with it to fall back to (the recovery that exists lives
|
||||
// one step earlier, at the clip/XFB fixups). What is left is to keep the bytes away from
|
||||
// the driver: a driver is not a validating entry point, and an invalid module it accepts
|
||||
// can fault the GPU into device loss (a frontend translation once put a sampler inside
|
||||
// the default uniform block, and Adreno page-faulted reading its "descriptor" out of
|
||||
// uniform-buffer bytes). One invalid stage declines the whole program: no shader module
|
||||
// of it is created, and every draw or dispatch of it is refused by name
|
||||
// (VkProgramObject::invalidSpirvDeclined). MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV hands the
|
||||
// bytes over anyway, for diagnosing a driver that copes.
|
||||
for (SizeT i = 0; i < moduleCount; ++i) {
|
||||
String reason;
|
||||
const DriverModuleVerdict verdict = ScreenModuleForDriver(
|
||||
moduleSpirvs[i], MG_Config::Features.MagmaAllowInvalidSpirv, &reason);
|
||||
if (verdict == DriverModuleVerdict::DeclinedInvalid) {
|
||||
MGLOG_E_ONCE("ProgramFactory: declining program %u - its stage %d module fails SPIR-V validation (%s); "
|
||||
"no shader module is created and its draws are refused. "
|
||||
"MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 hands such modules to the driver anyway.",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
|
||||
entry.invalidSpirvDeclined = true;
|
||||
break;
|
||||
}
|
||||
if (verdict == DriverModuleVerdict::AdmittedInvalid) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d "
|
||||
"because MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV is set (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
for (SizeT i = 0; i < moduleCount && !entry.invalidSpirvDeclined; ++i) {
|
||||
auto& moduleSpv = moduleSpirvs[i];
|
||||
if (moduleSpv.empty()) continue;
|
||||
|
||||
// Last look at the exact bytes the driver receives, in EVERY build rather than only
|
||||
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
|
||||
// by here the descriptor bindings have been remapped and the layout about to be
|
||||
// reflected describes the remapped module, so there is no module left that is both
|
||||
// valid and consistent with it to fall back to. The recovery lives one step earlier,
|
||||
// at the clip/XFB fixups (see the revert there) - which is where a transform can
|
||||
// introduce a reference to a delisted interface variable, the failure this whole
|
||||
// guard exists for. Anything that reaches this line names itself in the log of a
|
||||
// shipping build instead of dying anonymously inside the driver.
|
||||
SpirvValidationFailure finalFailure{};
|
||||
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
|
||||
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
|
||||
"a backend transform after the clip/XFB fixups broke it (%s)",
|
||||
program.GetExternalIndex(), static_cast<Int>(stages[i]),
|
||||
finalFailure.message.c_str());
|
||||
}
|
||||
|
||||
// Does the stage the driver will treat as the last pre-rasterization one actually
|
||||
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
|
||||
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "../MagmaSession.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
@@ -228,6 +229,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = false;
|
||||
// One of the program's final modules fails SPIR-V validation, so none of its modules
|
||||
// was created and every draw or dispatch of it is refused by name (the ProgramInvalidSpirv /
|
||||
// ComputeProgramInvalidSpirv wire declines) - see ProgramFactory::ScreenModuleForDriver.
|
||||
// Sticky like the flag above: it lives on the cache entry, so the one log line is honest.
|
||||
Bool invalidSpirvDeclined = false;
|
||||
Bool needsPassthroughTessControl = false;
|
||||
// ...and the pass-through this renderer can synthesize carries gl_Position and
|
||||
// nothing else, so it is only correct when the evaluation stage's inputs are
|
||||
@@ -251,7 +257,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
mutable Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
|
||||
|
||||
VkProgramObject() = default;
|
||||
VkProgramObject(const VkProgramObject&) = delete;
|
||||
@@ -288,6 +294,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
// The three refusal flags travel like every other field: a robin-hood insert swaps
|
||||
// entries, and a flag left behind would refuse (or admit) the wrong program.
|
||||
invalidSpirvDeclined = other.invalidSpirvDeclined;
|
||||
xfbCaptureDeclined = other.xfbCaptureDeclined;
|
||||
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -309,6 +320,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.invalidSpirvDeclined = false;
|
||||
other.xfbCaptureDeclined = false;
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -352,6 +366,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
// The three refusal flags travel like every other field: a robin-hood insert swaps
|
||||
// entries, and a flag left behind would refuse (or admit) the wrong program.
|
||||
invalidSpirvDeclined = other.invalidSpirvDeclined;
|
||||
xfbCaptureDeclined = other.xfbCaptureDeclined;
|
||||
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -373,6 +392,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.invalidSpirvDeclined = false;
|
||||
other.xfbCaptureDeclined = false;
|
||||
other.pointSizeCapabilityUnsupported = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -395,18 +417,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
void Destroy() {
|
||||
if (s_device != VK_NULL_HANDLE) {
|
||||
if (s_device.Get() != VK_NULL_HANDLE) {
|
||||
if (pipelineLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
|
||||
vkDestroyPipelineLayout(s_device.Get(), pipelineLayout, nullptr);
|
||||
pipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
if (descriptorSetLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(s_device.Get(), descriptorSetLayout, nullptr);
|
||||
descriptorSetLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
for (auto module : modules) {
|
||||
if (module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(s_device, module, nullptr);
|
||||
vkDestroyShaderModule(s_device.Get(), module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +485,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
VkProgramObject::s_device = device;
|
||||
VkProgramObject::s_device.Get() = device;
|
||||
}
|
||||
// Destroys the pass-through tessellation control modules. Runs while the device is
|
||||
// still alive for the same reason ~VkProgramObject's does: this factory outlives
|
||||
@@ -503,6 +525,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VkRenderPassManager::OnPresent's sweep.
|
||||
void OnFrameBoundary();
|
||||
|
||||
// What the last look at a module's final bytes - the exact words vkCreateShaderModule
|
||||
// would receive - decided.
|
||||
enum class DriverModuleVerdict : Uint8 {
|
||||
Valid, // validates: handed to the driver
|
||||
DeclinedInvalid, // fails validation: never handed over, the program's work is declined
|
||||
AdmittedInvalid, // fails validation, handed over anyway (MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV)
|
||||
};
|
||||
// Validates `spirv` for the Vulkan environment its version word names and turns the
|
||||
// result into a verdict. A driver is not a validating entry point: Adreno page-faulted
|
||||
// into device loss on a module that carried a sampler inside a uniform block, Mali
|
||||
// SIGSEGVs inside pipeline creation. So an invalid module is declined unless
|
||||
// `allowInvalid` (the escape hatch) says to hand it over regardless.
|
||||
//
|
||||
// The validator's answer is remembered per module content (64-bit hash plus word count),
|
||||
// process-wide: a program variant, an evicted-then-rebuilt entry and two programs that
|
||||
// share a stage all re-screen the same bytes, and only the first pays for spirv-val.
|
||||
// `outReason` receives the validator's message for an invalid module.
|
||||
static DriverModuleVerdict ScreenModuleForDriver(const Vector<Uint>& spirv, Bool allowInvalid,
|
||||
String* outReason = nullptr);
|
||||
// How many times ScreenModuleForDriver actually ran the validator (cache misses), for
|
||||
// the tests that pin the cache.
|
||||
static Uint64 DriverModuleValidatorRuns();
|
||||
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
@@ -655,6 +700,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// and adds only a handful of values, so it does not move the cap in practice.
|
||||
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
|
||||
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
|
||||
// The FragCoordYFlip variant's module transform: every read of gl_FragCoord goes through a
|
||||
// Private copy whose y is (framebufferHeight - y). Fails open (output = input, false). Exposed
|
||||
// so a unit case can validate the bytes it hands the driver.
|
||||
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output, Uint32 framebufferHeight);
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/RenderPassGuard.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>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Ends the render pass that is active on `commandBuffer`, if there is one. Called right before
|
||||
// recording a command a render pass may not contain (a barrier, a copy, a blit, a clear of an
|
||||
// image) into a command buffer that may hold an open pass: the wire draw path keeps its pass
|
||||
// open across consecutive draws to the same attachments, so the resource work that falls
|
||||
// between two draws must close it first. A pass on another command buffer is left alone.
|
||||
void EndActiveRenderPassOn(VkCommandBuffer commandBuffer);
|
||||
|
||||
// A counter that moves whenever images may have been written since it was last read: a render
|
||||
// pass ended (its attachments), resource work recorded outside a pass (copies, clears, blits,
|
||||
// dispatches all close the pass first), or a draw with storage images. A sampled image needs
|
||||
// the memory barrier that makes earlier writes visible only when this moved since the last one.
|
||||
Uint64 WireImageWriteEpoch();
|
||||
void BumpWireImageWriteEpoch();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -292,6 +292,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Fresh swapchain images hold garbage until a render pass stores into them.
|
||||
m_imageContentDefined.assign(imageCount, false);
|
||||
m_depthStencilContentDefined.assign(imageCount, false);
|
||||
// Nothing presented from this chain yet: every buffer age is unknown.
|
||||
m_ages.Reset(imageCount);
|
||||
|
||||
CreateImageViews(device);
|
||||
CreateDepthStencilResources(device, physicalDevice);
|
||||
@@ -299,6 +301,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height,
|
||||
imageCount);
|
||||
|
||||
PublishDefaultFramebufferInfo();
|
||||
}
|
||||
|
||||
void SwapchainObject::PublishDefaultFramebufferInfo() const {
|
||||
const VkExtent2D defaultFramebufferExtent = m_surfaceExtent;
|
||||
// Properly initialize Default FBO here
|
||||
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
|
||||
const Int extentWidth = static_cast<Int>(defaultFramebufferExtent.width);
|
||||
@@ -488,6 +495,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_imageLayouts.clear();
|
||||
m_imageContentDefined.clear();
|
||||
m_depthStencilContentDefined.clear();
|
||||
m_ages.Reset(0);
|
||||
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -38,6 +39,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
|
||||
Optional<Int> swapInterval);
|
||||
void Shutdown(VkDevice device);
|
||||
// Tells the frontend (or, under a transport, the client) the default framebuffer's size
|
||||
// and formats. Create does this; a renderer switching between surfaces repeats it.
|
||||
void PublishDefaultFramebufferInfo() const;
|
||||
|
||||
VkSwapchainKHR GetHandle() const { return m_swapchain; }
|
||||
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
|
||||
@@ -77,6 +81,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
|
||||
void SetAllDepthStencilContentUndefined();
|
||||
|
||||
// EGL_EXT_buffer_age over this chain's images. Ages start unknown with the chain (Create).
|
||||
// Until a client asks for an age (NoteBufferAgeAsked) a presented image's content is
|
||||
// treated as undefined, as before; once one has, a presented image keeps it - its age is
|
||||
// counted and its content stays defined, so the next frame loads it.
|
||||
void NoteBufferAgeAsked() { m_bufferAgeAsked = true; }
|
||||
Bool BufferAgeAsked() const { return m_bufferAgeAsked; }
|
||||
Int32 BufferAgeOf(Uint32 index) const { return m_ages.AgeOf(index); }
|
||||
// Image `index` was presented.
|
||||
void OnImagePresented(Uint32 index) { m_ages.OnPresented(index, m_bufferAgeAsked); }
|
||||
// A swapped frame reached no image: every age is unknown.
|
||||
void OnFrameDropped() { m_ages.OnFrameDropped(); }
|
||||
|
||||
private:
|
||||
void CreateImageViews(VkDevice device);
|
||||
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
|
||||
@@ -106,5 +122,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<VkImageLayout> m_depthStencilImageLayouts;
|
||||
Vector<Bool> m_imageContentDefined;
|
||||
Vector<Bool> m_depthStencilContentDefined;
|
||||
MG_Util::Damage::SwapchainAgeTracker m_ages;
|
||||
// Kept across a rebuild: it is the application's habit, not the chain's.
|
||||
Bool m_bufferAgeAsked = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "RenderPassGuard.h"
|
||||
#include "UniformManager.h"
|
||||
|
||||
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
|
||||
@@ -251,18 +252,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
WireDescriptorFatal("image-descriptor-transition");
|
||||
// A second descriptor/draw can keep GENERAL after a shader write. A layout
|
||||
// equality fast return is not a memory dependency for that prior write.
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
// Only when something may have written an image since the last such barrier
|
||||
// (WireImageWriteEpoch): with nothing written in between, consecutive draws keep their
|
||||
// render pass open instead of closing it for a barrier that orders nothing.
|
||||
if (WireImageWriteEpoch() != m_wireImageBarrierEpoch) {
|
||||
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &memory, 0, nullptr, 0, nullptr);
|
||||
m_wireImageBarrierEpoch = WireImageWriteEpoch();
|
||||
}
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = resource->image;
|
||||
info.viewType = type;
|
||||
info.format = format;
|
||||
info.subresourceRange = {aspect, level, levels, layer, layers};
|
||||
if (!storage) {
|
||||
const Bool alphaIsOne = ResolveTextureFormatInfo(
|
||||
// An X-format shared image carries undefined alpha bytes, exactly like an expanded RGB.
|
||||
const Bool alphaIsOne = resource->sharedImageAlphaOne || ResolveTextureFormatInfo(
|
||||
static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba;
|
||||
const auto swizzle = [&](Uint32 channel) {
|
||||
const auto value = record.Params.Swizzle[channel];
|
||||
@@ -293,11 +302,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else {
|
||||
auto samplerHandle = state.BoundSamplerStates[unit];
|
||||
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle)) samplerHandle = record.Params.BuiltinSampler;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle) || samplerHandle.Slot >= state.SamplerCsos.size())
|
||||
WireDescriptorFatal("sampler-record");
|
||||
const auto& sampler = state.SamplerCsos[samplerHandle.Slot];
|
||||
if (!sampler.Live || sampler.Gen != samplerHandle.Gen) WireDescriptorFatal("sampler-record-generation");
|
||||
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(sampler.Params,
|
||||
// A texture's built-in sampler CSO can be missing or gone while the texture still names
|
||||
// it: sampler CSOs are content-addressed and recycled at capacity, and the texture's
|
||||
// record is only re-pointed by its next set_texture_params (a texture whose params never
|
||||
// crossed names none at all). Sample with GL's default parameters rather than end the
|
||||
// session - the DirectGLES arm declines the push in both cases.
|
||||
using SamplerCsoRecord = std::decay_t<decltype(state.SamplerCsos[0])>;
|
||||
static const decltype(SamplerCsoRecord::Params) kDefaultSamplerParams{};
|
||||
const Bool known = !MG_Pipe::MGPipeHandleIsNull(samplerHandle) &&
|
||||
samplerHandle.Slot < state.SamplerCsos.size();
|
||||
const SamplerCsoRecord* sampler = known ? &state.SamplerCsos[samplerHandle.Slot] : nullptr;
|
||||
const Bool stale = sampler == nullptr || !sampler->Live || sampler->Gen != samplerHandle.Gen;
|
||||
if (stale) {
|
||||
MGLOG_E_ONCE("Magma wire: sampler CSO {%u, %u} named by texture unit %u is missing or no longer "
|
||||
"live; sampling with default parameters", samplerHandle.Slot, samplerHandle.Gen, unit);
|
||||
}
|
||||
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(stale ? kDefaultSamplerParams : sampler->Params,
|
||||
static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
|
||||
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger,
|
||||
levels);
|
||||
@@ -3183,7 +3203,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// preparing the texture would decline a storage-less one and lose the pass.
|
||||
if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit]))
|
||||
continue;
|
||||
if (!m_textureManager->SyncTextureResourceByHandle(handle, false, storage)) return false;
|
||||
const auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage);
|
||||
if (resource == nullptr) return false;
|
||||
// Another session writes it: the renderer acquires it before this use.
|
||||
if (resource->sharedImageId != 0 || resource->yuvImageId != 0)
|
||||
m_textureManager->NoteSharedImageUse(*resource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -660,5 +660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_samplerResolveMemoHighWater = binding + 1;
|
||||
}
|
||||
}
|
||||
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
|
||||
mutable Uint64 m_wireImageBarrierEpoch = 0;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -217,11 +218,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// memo table is owned by this factory and dies with it, so a per-instance counter is
|
||||
// enough there and the epoch shrinks back to what it looks like it should be.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
static inline Uint64 s_evictionEpochSource = 0;
|
||||
static inline std::atomic<Uint64> s_evictionEpochSource{0};
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "RenderPassGuard.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "../DirectVulkan.h"
|
||||
#include "VulkanRenderer.h"
|
||||
@@ -113,30 +114,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
using MG_State::GLState::BufferObject;
|
||||
|
||||
// The manager owned by the active VulkanRenderer; immediate ops route here.
|
||||
VkBufferManager* g_activeBufferManager = nullptr;
|
||||
SessionLocal<VkBufferManager*> g_activeBufferManager; // per Magma session (MagmaSession.h)
|
||||
|
||||
void Ops_Respecify(BufferObject& bufferObject) {
|
||||
if (g_activeBufferManager) {
|
||||
g_activeBufferManager->OnRespecify(bufferObject);
|
||||
if (g_activeBufferManager.Get()) {
|
||||
g_activeBufferManager.Get()->OnRespecify(bufferObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
|
||||
if (g_activeBufferManager) {
|
||||
g_activeBufferManager->OnSubData(bufferObject, offset, size);
|
||||
if (g_activeBufferManager.Get()) {
|
||||
g_activeBufferManager.Get()->OnSubData(bufferObject, offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) {
|
||||
if (g_activeBufferManager) {
|
||||
g_activeBufferManager->OnResidentSubData(bufferObject, offset, data);
|
||||
if (g_activeBufferManager.Get()) {
|
||||
g_activeBufferManager.Get()->OnResidentSubData(bufferObject, offset, data);
|
||||
}
|
||||
}
|
||||
|
||||
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> appAccess) {
|
||||
if (g_activeBufferManager) {
|
||||
g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
|
||||
if (g_activeBufferManager.Get()) {
|
||||
g_activeBufferManager.Get()->OnFlushMappedRange(bufferObject, range, appAccess);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,15 +155,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
|
||||
if (g_activeBufferManager) {
|
||||
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
|
||||
if (g_activeBufferManager.Get()) {
|
||||
return g_activeBufferManager.Get()->AcquirePersistentMap(bufferObject);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
|
||||
if (g_activeBufferManager) {
|
||||
g_activeBufferManager->OnResourceDestroyed(std::move(resource));
|
||||
if (g_activeBufferManager.Get()) {
|
||||
g_activeBufferManager.Get()->OnResourceDestroyed(std::move(resource));
|
||||
}
|
||||
// No active manager: the device/allocator is gone or going away and
|
||||
// Shutdown() already destroyed the storage; dropping the handle here
|
||||
@@ -181,34 +182,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager& WireManager() {
|
||||
if (g_activeBufferManager == nullptr) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"wire-buffer-manager\"}");
|
||||
std::abort();
|
||||
// The session's manager, or null once its renderer is gone - a client's records can still
|
||||
// arrive after its eglTerminate released it (or, surfaceless, before its first surface built
|
||||
// one). Such a record is declined with one line rather than taking the whole server down.
|
||||
VkBufferManager* WireManager(const char* op) {
|
||||
VkBufferManager* manager = g_activeBufferManager.Get();
|
||||
if (manager == nullptr) {
|
||||
MGLOG_E_ONCE("Magma: a wire buffer %s reached a session with no renderer; declined", op);
|
||||
}
|
||||
return *g_activeBufferManager;
|
||||
return manager;
|
||||
}
|
||||
|
||||
const MG_Pipe::MGPipeResourceOps g_vulkanWireResourceOps = {
|
||||
.Create = [](auto res, const auto& desc) { WireManager().CreateWireBuffer(res, desc); },
|
||||
.Create = [](auto res, const auto& desc) { if (auto* m = WireManager("create")) m->CreateWireBuffer(res, desc); },
|
||||
.Respecify = [](auto res, const auto& desc, const void* bytes) {
|
||||
WireManager().RespecifyWireBuffer(res, desc, bytes);
|
||||
if (auto* m = WireManager("respecify")) m->RespecifyWireBuffer(res, desc, bytes);
|
||||
},
|
||||
.SubData = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
if (auto* m = WireManager("write"))
|
||||
m->WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
},
|
||||
.SubDataResident = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
if (auto* m = WireManager("write"))
|
||||
m->WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
|
||||
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
|
||||
},
|
||||
.FlushRange = [](auto res, const auto& record, const void* bytes) {
|
||||
WireManager().FlushWireBuffer(res, record.Offset, record.Size, bytes);
|
||||
if (auto* m = WireManager("flush")) m->FlushWireBuffer(res, record.Offset, record.Size, bytes);
|
||||
},
|
||||
.Readback = [](auto res, const auto& record) {
|
||||
WireManager().ReadbackWireBuffer(res, record.Offset, record.Size);
|
||||
if (auto* m = WireManager("readback")) m->ReadbackWireBuffer(res, record.Offset, record.Size);
|
||||
},
|
||||
.Destroy = [](auto res) { WireManager().DestroyWireBuffer(res); },
|
||||
.Destroy = [](auto res) { if (auto* m = WireManager("destroy")) m->DestroyWireBuffer(res); },
|
||||
// Split mapping remains T2: the client owns its map and pushes exact
|
||||
// modified ranges. A server pointer is never donated across the wire.
|
||||
.MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; },
|
||||
@@ -217,7 +223,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// P11 B2 (T0): the one door a server pointer is still never donated through stays shut;
|
||||
// what T0 adds is the other direction - the CLIENT's pages imported as the store.
|
||||
.ImportExternal = [](MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) -> Bool {
|
||||
return WireManager().ImportWireBuffer(res, ahb, size);
|
||||
auto* m = WireManager("import");
|
||||
return m != nullptr && m->ImportWireBuffer(res, ahb, size);
|
||||
},
|
||||
.SelfTestExternal = [](char* why, Uint64 whyBytes) -> Bool {
|
||||
return VkBufferManager::SelfTestWireImport(why, whyBytes);
|
||||
@@ -234,6 +241,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Diagnostic memory for DescribeWireBufferHistory: the last handles created and destroyed.
|
||||
struct WireBufferHistory {
|
||||
static constexpr SizeT kDepth = 256;
|
||||
MG_Pipe::MGPipeHandle created[kDepth]{};
|
||||
MG_Pipe::MGPipeHandle destroyed[kDepth]{};
|
||||
SizeT createdCount = 0;
|
||||
SizeT destroyedCount = 0;
|
||||
};
|
||||
SessionLocal<WireBufferHistory> g_wireBufferHistory;
|
||||
} // namespace
|
||||
|
||||
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
|
||||
const auto found = m_wireBuffers.find(WireBufferKey(res));
|
||||
return found == m_wireBuffers.end() ? nullptr : &found->second;
|
||||
@@ -246,6 +265,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::abort();
|
||||
}
|
||||
m_wireBuffers.try_emplace(WireBufferKey(res));
|
||||
{
|
||||
auto& history = *g_wireBufferHistory;
|
||||
history.created[history.createdCount++ % 256] = res;
|
||||
}
|
||||
RespecifyWireBuffer(res, desc, nullptr);
|
||||
}
|
||||
|
||||
@@ -315,6 +338,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
|
||||
0, 1, &barrier, 0, nullptr, 0, nullptr);
|
||||
// P8-D: counted here, past the idle early-out, so `whw` is waits TAKEN, not calls.
|
||||
@@ -403,7 +427,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploaded = StagedWireRangeCopy(*resource, bytes, static_cast<SizeT>(offset),
|
||||
static_cast<SizeT>(size));
|
||||
}
|
||||
if (!uploaded && !WaitForWireBufferHostAccess(*resource)) WireBufferSyncFatal("host-write");
|
||||
if (!uploaded && !WaitForWireBufferHostAccess(*resource)) {
|
||||
if (LatchedOnDeviceLoss("buffer-host-write")) return;
|
||||
WireBufferSyncFatal("host-write");
|
||||
}
|
||||
}
|
||||
if (!uploaded) {
|
||||
uploaded = resource->buffer.Upload(bytes, size, offset);
|
||||
@@ -412,6 +439,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
if (!uploaded) {
|
||||
if (LatchedOnDeviceLoss("buffer-upload")) return;
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-upload\"}");
|
||||
std::abort();
|
||||
}
|
||||
@@ -505,13 +533,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy copy{offset, dst.offset, size};
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), dst.buffer, 1, ©);
|
||||
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &after, 0, nullptr, 0, nullptr);
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
@@ -549,22 +580,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
|
||||
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy widen{alignedOffset, staging.offset, stagedSize};
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), staging.buffer, 1, &widen);
|
||||
// The shift reads what the widening copy just wrote, so it needs its own edge even
|
||||
// though both halves are transfers on one command buffer.
|
||||
VkMemoryBarrier staged{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
staged.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
staged.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
0, 1, &staged, 0, nullptr, 0, nullptr);
|
||||
const VkBufferCopy shift{staging.offset + headPad, dst.offset + dstSkip, size};
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdCopyBuffer(commands, staging.buffer, dst.buffer, 1, &shift);
|
||||
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
|
||||
0, 1, &after, 0, nullptr, 0, nullptr);
|
||||
resource->lastUseSerial = m_frameSerial;
|
||||
@@ -582,6 +618,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool t0 = resource != nullptr && resource->imported;
|
||||
Vector<Uint8> bytes(t0 ? 0 : static_cast<SizeT>(size));
|
||||
if (t0 ? !WaitForWireBufferHostAccess(*resource) : !ReadWireBuffer(res, offset, size, bytes.data())) {
|
||||
// A lost device: the session is latched and closing; its client reads a lost context.
|
||||
if (LatchedOnDeviceLoss("buffer-readback")) return;
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
@@ -786,7 +824,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// read - this server's own host map of the import reads what the CPU wrote through the lock;
|
||||
// gpu - one submission copies that pattern out of the import (the GPU read) and fills another
|
||||
// region of it (the GPU write), then a transfer->host barrier and a fence wait;
|
||||
// back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer.
|
||||
// back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer;
|
||||
// steady - the same again with bytes the CPU wrote AFTER the import and its first GPU use (the
|
||||
// block at the end says why the first round alone is not enough).
|
||||
Bool VkBufferManager::SelfTestWireImport(char* whyOut, Uint64 whyBytes) {
|
||||
const auto answer = [&](Bool ok, const String& text) {
|
||||
std::snprintf(whyOut, static_cast<SizeT>(whyBytes), "%s", text.c_str());
|
||||
@@ -878,14 +918,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier hostWrites{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
hostWrites.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
|
||||
hostWrites.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&hostWrites, 0, nullptr, 0, nullptr);
|
||||
VkBufferCopy region{0, 0, kWords * 4};
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, ®ion);
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kFillWord);
|
||||
VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
|
||||
0, nullptr, 0, nullptr);
|
||||
vkEndCommandBuffer(commands);
|
||||
@@ -909,6 +953,59 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
String(imported.coherent ? "coherent" : "non-coherent") + " memory), GPU read " +
|
||||
std::to_string(gpuRead) + "/1024 words of the CPU's pattern, GPU write seen through the "
|
||||
"held lock " + std::to_string(heldSaw) + "/1024 words";
|
||||
if (!ok) break;
|
||||
|
||||
// STEADY STATE. The round above reads bytes the CPU wrote BEFORE the import, and an import
|
||||
// can clean the CPU's caches on its way in, so it passes on a device whose GPU never sees a
|
||||
// write made through the held lock afterwards - which is all of T0's real traffic (Mali-G1
|
||||
// on a Dimensity: the first round passed, then Flywheel's staging scatter read stale bytes
|
||||
// and the create-indirect trace lost every instanced object). So the CPU now writes a fresh
|
||||
// pattern into region 1 and reads region 4 back into its cache, and one more submission
|
||||
// copies region 1 out and overwrites region 4.
|
||||
constexpr Uint64 kSteadyAt = 1 * 4096; // region 1
|
||||
constexpr Uint32 kSteadyFillWord = 0x3D5EA11Eu;
|
||||
auto* heldWrite = static_cast<volatile Uint32*>(held.ptr);
|
||||
Uint32 cached = 0;
|
||||
for (Uint32 i = 0; i < kWords; ++i) {
|
||||
heldWrite[kSteadyAt / 4 + i] = 0xA5000000u ^ (i * 40503u);
|
||||
cached ^= heldWrite[kFillAt / 4 + i];
|
||||
}
|
||||
(void)cached;
|
||||
std::memset(staging.mapped, 0, kWords * 4);
|
||||
if (vkResetFences(ctx.device, 1, &fence) != VK_SUCCESS ||
|
||||
vkResetCommandPool(ctx.device, pool, 0) != VK_SUCCESS ||
|
||||
vkBeginCommandBuffer(commands, &begin) != VK_SUCCESS) {
|
||||
ok = false;
|
||||
verdict = "steady: the second submission could not be recorded";
|
||||
break;
|
||||
}
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&hostWrites, 0, nullptr, 0, nullptr);
|
||||
VkBufferCopy steadyRegion{kSteadyAt, 0, kWords * 4};
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, &steadyRegion);
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kSteadyFillWord);
|
||||
EndActiveRenderPassOn(commands);
|
||||
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
|
||||
0, nullptr, 0, nullptr);
|
||||
vkEndCommandBuffer(commands);
|
||||
if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS ||
|
||||
vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) {
|
||||
ok = false;
|
||||
verdict = "steady: the second submission did not complete within 2 s";
|
||||
break;
|
||||
}
|
||||
Uint32 steadyRead = 0, steadySaw = 0;
|
||||
for (Uint32 i = 0; i < kWords; ++i) {
|
||||
if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead;
|
||||
if (heldWords[kFillAt / 4 + i] == kSteadyFillWord) ++steadySaw;
|
||||
}
|
||||
ok = steadyRead == kWords && steadySaw == kWords;
|
||||
verdict += "; with the import in use, GPU read " + std::to_string(steadyRead) +
|
||||
"/1024 words the CPU wrote after it, GPU write seen through the held lock " +
|
||||
std::to_string(steadySaw) + "/1024 words";
|
||||
} while (false);
|
||||
if (fence != VK_NULL_HANDLE) vkDestroyFence(ctx.device, fence, nullptr);
|
||||
if (pool != VK_NULL_HANDLE) vkDestroyCommandPool(ctx.device, pool, nullptr);
|
||||
@@ -921,7 +1018,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
void VkBufferManager::DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const {
|
||||
const auto& history = *g_wireBufferHistory;
|
||||
Int createdAgo = -1, destroyedAgo = -1;
|
||||
for (SizeT i = 0; i < std::min(history.createdCount, WireBufferHistory::kDepth); ++i) {
|
||||
const auto& h = history.created[(history.createdCount - 1 - i) % WireBufferHistory::kDepth];
|
||||
if (h.Slot == res.Slot && h.Gen == res.Gen) { createdAgo = static_cast<Int>(i); break; }
|
||||
}
|
||||
for (SizeT i = 0; i < std::min(history.destroyedCount, WireBufferHistory::kDepth); ++i) {
|
||||
const auto& h = history.destroyed[(history.destroyedCount - 1 - i) % WireBufferHistory::kDepth];
|
||||
if (h.Slot == res.Slot && h.Gen == res.Gen) { destroyedAgo = static_cast<Int>(i); break; }
|
||||
}
|
||||
MGLOG_E("Magma wire buffer {slot=%u, gen=%u}: live stores %zu; created %d creates ago, destroyed %d "
|
||||
"destroys ago (-1 = not in the last %zu); %zu creates, %zu destroys in this session",
|
||||
res.Slot, res.Gen, m_wireBuffers.size(), createdAgo, destroyedAgo, WireBufferHistory::kDepth,
|
||||
history.createdCount, history.destroyedCount);
|
||||
}
|
||||
|
||||
void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) {
|
||||
{
|
||||
auto& history = *g_wireBufferHistory;
|
||||
history.destroyed[history.destroyedCount++ % WireBufferHistory::kDepth] = res;
|
||||
}
|
||||
const auto found = m_wireBuffers.find(WireBufferKey(res));
|
||||
if (found == m_wireBuffers.end()) return;
|
||||
DeferWireRelease(std::move(found->second.buffer),
|
||||
@@ -979,6 +1098,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// budget's worth of orphans, not one per glBufferData.
|
||||
if (pVulkanRenderer == nullptr ||
|
||||
!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
|
||||
if (LatchedOnDeviceLoss("buffer-deferred-watermark")) return;
|
||||
WireBufferSyncFatal("deferred-watermark");
|
||||
}
|
||||
++m_wireDeferredSyncs;
|
||||
@@ -999,6 +1119,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredSyncs, m_wireDeferredSyncs);
|
||||
}
|
||||
|
||||
Bool VkBufferManager::LatchedOnDeviceLoss(const char* site) {
|
||||
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchWireDeviceLoss(site);
|
||||
}
|
||||
|
||||
void VkBufferManager::WireBufferSyncFatal(const char* site) {
|
||||
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"} {site=%s}", site);
|
||||
std::abort();
|
||||
@@ -1072,7 +1196,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (!InitializeTransientArenas()) {
|
||||
return false;
|
||||
}
|
||||
g_activeBufferManager = this;
|
||||
g_activeBufferManager.Get() = this;
|
||||
MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
RegisterWireResourceOps();
|
||||
@@ -1081,10 +1205,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferManager::Shutdown() {
|
||||
if (g_activeBufferManager == this) {
|
||||
g_activeBufferManager = nullptr;
|
||||
if (g_activeBufferManager.Get() == this) {
|
||||
g_activeBufferManager.Get() = nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) {
|
||||
// Under a transport the table is the PROCESS's and serves every session: it dispatches
|
||||
// to the calling session's own manager (WireManager), so one session's renderer going
|
||||
// away must not take it from the others - their buffer creates would be dropped from
|
||||
// then on, and every buffer born after that would be a handle the server never saw.
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith &&
|
||||
MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) {
|
||||
MG_Pipe::MGPipeSetResourceOps(nullptr);
|
||||
}
|
||||
#endif
|
||||
@@ -1377,6 +1506,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&beforeBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
@@ -1384,11 +1514,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
region.srcOffset = staging.offset;
|
||||
region.dstOffset = static_cast<VkDeviceSize>(offset);
|
||||
region.size = static_cast<VkDeviceSize>(size);
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion);
|
||||
|
||||
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
|
||||
&afterBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
@@ -1423,6 +1555,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
|
||||
&beforeBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
@@ -1430,11 +1563,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
region.srcOffset = staging.offset;
|
||||
region.dstOffset = static_cast<VkDeviceSize>(offset);
|
||||
region.size = static_cast<VkDeviceSize>(size);
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, ®ion);
|
||||
|
||||
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
|
||||
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
|
||||
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
|
||||
&afterBarrier, 0, nullptr, 0, nullptr);
|
||||
|
||||
|
||||
@@ -110,6 +110,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// frontend BufferObject. Acquires expose the full GPU store, without a CPU
|
||||
// pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes.
|
||||
Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice);
|
||||
// The handle names a store this session created (whatever its size, zero included).
|
||||
Bool IsKnownWireBuffer(MG_Pipe::MGPipeHandle res) { return FindWireBuffer(res) != nullptr; }
|
||||
// Names, for a Fatal about an unknown handle, whether this manager ever saw it.
|
||||
void DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const;
|
||||
Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst);
|
||||
Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
const BufferSlice& dst);
|
||||
@@ -358,6 +362,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// for the census to count), shared by the host write that cannot wait for the GPU and
|
||||
// the watermark that cannot. `site` says which.
|
||||
[[noreturn]] static void WireBufferSyncFatal(const char* site);
|
||||
// True when the wait or copy that just failed failed because the device is lost: the
|
||||
// session has been latched (VulkanRenderer::LatchWireDeviceLoss) and the caller returns
|
||||
// instead of taking its Fatal.
|
||||
static Bool LatchedOnDeviceLoss(const char* site);
|
||||
// The wbuf[] gauges (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs). The peaks are
|
||||
// taken at the two points the numbers can rise - a park and a mint - and published when
|
||||
// the stats channel is on; MagmaWireReclaimScenario reads them off the server's line.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VkRenderPassManager.h"
|
||||
#include "RenderPassGuard.h"
|
||||
#include <atomic>
|
||||
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
@@ -235,11 +237,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject):
|
||||
m_device(device), m_physicalDevice(physicalDevice), m_allocator(allocator), m_config(config),
|
||||
m_clearManager(clearManager), m_textureManager(textureManager), m_swapchainObject(swapchainObject) {
|
||||
RenderPassEntry::s_device = m_device;
|
||||
s_clearManager = &m_clearManager;
|
||||
s_textureManager = &m_textureManager;
|
||||
s_swapchainObject = &m_swapchainObject;
|
||||
s_renderPassManager = this;
|
||||
RenderPassEntry::s_device.Get() = m_device;
|
||||
s_clearManager.Get() = &m_clearManager;
|
||||
s_textureManager.Get() = &m_textureManager;
|
||||
s_swapchainObject.Get() = &m_swapchainObject;
|
||||
s_renderPassManager.Get() = this;
|
||||
}
|
||||
|
||||
VkRenderPassManager::~VkRenderPassManager() {}
|
||||
@@ -257,8 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||
m_pendingRenderbufferClears.clear();
|
||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||
s_activeRenderPass = {};
|
||||
s_hasActiveRenderPass = false;
|
||||
s_activeRenderPass.Get() = {};
|
||||
s_hasActiveRenderPass.Get() = false;
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
|
||||
@@ -607,6 +609,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
if (isDefaultFbo) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferTarget, sizeof(m_defaultFramebufferTarget)));
|
||||
}
|
||||
// sRGB attachments switch between their sRGB and UNORM-twin views with this
|
||||
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
|
||||
@@ -1528,6 +1531,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::PurgeRenderPasses() {
|
||||
Vector<VkRenderPass> destroyedRenderPasses;
|
||||
destroyedRenderPasses.reserve(m_renderPasses.size());
|
||||
for (auto& [_, entry] : m_renderPasses) destroyedRenderPasses.push_back(entry.renderPass);
|
||||
m_renderPasses.clear();
|
||||
m_rpFastValid = false;
|
||||
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
|
||||
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
|
||||
}
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
++m_frameCounter;
|
||||
|
||||
@@ -1554,9 +1568,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// immediately - and a single batched notification costs one pipeline-cache
|
||||
// scan instead of one per evicted pass.
|
||||
Vector<VkRenderPass> destroyedRenderPasses;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass.Get() ? s_activeRenderPass.Get().hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
const Bool isActive = s_hasActiveRenderPass.Get() && it->first == activeHash;
|
||||
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
@@ -1601,8 +1615,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture path below is immune because it re-reads the live payload).
|
||||
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
|
||||
// pending clear, fall back to the snapshot only when none is queued.
|
||||
if (s_renderPassManager != nullptr &&
|
||||
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
||||
if (s_renderPassManager.Get() != nullptr &&
|
||||
s_renderPassManager.Get()->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
|
||||
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
|
||||
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
|
||||
3) {
|
||||
@@ -1614,7 +1628,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
} else {
|
||||
if (pending.key.texture == nullptr ||
|
||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||
!s_clearManager.Get()->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1636,48 +1650,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
|
||||
// Pre-pass stream bookkeeping: this pass's attachment images are now
|
||||
// referenced by the open frame recording.
|
||||
if (s_textureManager != nullptr) {
|
||||
if (s_textureManager.Get() != nullptr) {
|
||||
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
||||
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
||||
if (const auto texture = tracked.texture.lock()) {
|
||||
s_textureManager->StampTextureRecordingUse(texture.get());
|
||||
s_textureManager.Get()->StampTextureRecordingUse(texture.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||
if (pending.hasInlinePayload) {
|
||||
if (s_renderPassManager != nullptr) {
|
||||
s_renderPassManager->PopPendingRenderbufferClear(pending.renderbuffer);
|
||||
if (s_renderPassManager.Get() != nullptr) {
|
||||
s_renderPassManager.Get()->PopPendingRenderbufferClear(pending.renderbuffer);
|
||||
}
|
||||
} else {
|
||||
s_clearManager->PopPendingClear(pending.key);
|
||||
s_clearManager.Get()->PopPendingClear(pending.key);
|
||||
}
|
||||
}
|
||||
s_activeRenderPass.hash = renderPassEntry.hash;
|
||||
s_activeRenderPass.compatibilityHash = renderPassEntry.compatibilityHash;
|
||||
s_activeRenderPass.trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
|
||||
s_activeRenderPass.extent = renderPassEntry.extent;
|
||||
s_hasActiveRenderPass = true;
|
||||
s_activeRenderPass.Get().commandBuffer = commandBuffer;
|
||||
s_activeRenderPass.Get().hash = renderPassEntry.hash;
|
||||
s_activeRenderPass.Get().compatibilityHash = renderPassEntry.compatibilityHash;
|
||||
s_activeRenderPass.Get().trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
|
||||
s_activeRenderPass.Get().extent = renderPassEntry.extent;
|
||||
s_hasActiveRenderPass.Get() = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool VkRenderPassManager::EndRenderPass(VkCommandBuffer commandBuffer) {
|
||||
BumpWireImageWriteEpoch();
|
||||
auto* activeRenderPass = GetActiveRenderPass();
|
||||
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
|
||||
if (activeRenderPass != nullptr) {
|
||||
trackedAttachmentLayouts = Move(activeRenderPass->trackedAttachmentLayouts);
|
||||
}
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
// Layout reconciliation below may record barriers, whose guard closes an active
|
||||
// pass. Detach the ended pass first so that guard cannot recursively end it again.
|
||||
s_activeRenderPass.Get() = {};
|
||||
s_hasActiveRenderPass.Get() = false;
|
||||
// The fast-path memo reuses the ACTIVE render pass; once the pass ends it must not carry
|
||||
// over (the next span may be a different FBO resolved before its render pass is begun).
|
||||
if (s_renderPassManager != nullptr) {
|
||||
s_renderPassManager->m_rpFastValid = false;
|
||||
if (s_renderPassManager.Get() != nullptr) {
|
||||
s_renderPassManager.Get()->m_rpFastValid = false;
|
||||
}
|
||||
if (activeRenderPass != nullptr && !activeRenderPass->trackedAttachmentLayouts.empty()) {
|
||||
for (const auto& trackedAttachment : activeRenderPass->trackedAttachmentLayouts) {
|
||||
if (!trackedAttachmentLayouts.empty()) {
|
||||
for (const auto& trackedAttachment : trackedAttachmentLayouts) {
|
||||
switch (trackedAttachment.target) {
|
||||
case TrackedAttachmentTarget::Texture:
|
||||
MOBILEGL_ASSERT(s_textureManager != nullptr, "EndRenderPass: texture manager is null");
|
||||
MOBILEGL_ASSERT(s_textureManager.Get() != nullptr, "EndRenderPass: texture manager is null");
|
||||
if (const auto texture = trackedAttachment.texture.lock()) {
|
||||
s_textureManager->UpdateTrackedImageLayoutAfterAttachmentWrite(
|
||||
s_textureManager.Get()->UpdateTrackedImageLayoutAfterAttachmentWrite(
|
||||
commandBuffer,
|
||||
texture.get(),
|
||||
trackedAttachment.textureMipLevel,
|
||||
@@ -1685,27 +1709,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
break;
|
||||
case TrackedAttachmentTarget::Renderbuffer:
|
||||
MOBILEGL_ASSERT(s_renderPassManager != nullptr, "EndRenderPass: render pass manager is null");
|
||||
MOBILEGL_ASSERT(s_renderPassManager.Get() != nullptr, "EndRenderPass: render pass manager is null");
|
||||
if (const auto renderbuffer = trackedAttachment.renderbuffer.lock()) {
|
||||
auto resourceIt =
|
||||
s_renderPassManager->m_renderbufferResources.find(renderbuffer.get());
|
||||
if (resourceIt != s_renderPassManager->m_renderbufferResources.end()) {
|
||||
s_renderPassManager.Get()->m_renderbufferResources.find(renderbuffer.get());
|
||||
if (resourceIt != s_renderPassManager.Get()->m_renderbufferResources.end()) {
|
||||
resourceIt->second.layout = trackedAttachment.finalLayout;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainColor:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject.Get()->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
// The pass stored into the attachment: its content is defined
|
||||
// until the image is next presented.
|
||||
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
s_swapchainObject.Get()->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject.Get()->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
trackedAttachment.finalLayout);
|
||||
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
s_swapchainObject.Get()->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||
@@ -1714,12 +1738,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
}
|
||||
s_activeRenderPass = {};
|
||||
s_hasActiveRenderPass = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
ActiveRenderPassInfo* VkRenderPassManager::GetActiveRenderPass() {
|
||||
return s_hasActiveRenderPass ? &s_activeRenderPass : nullptr;
|
||||
return s_hasActiveRenderPass.Get() ? &s_activeRenderPass.Get() : nullptr;
|
||||
}
|
||||
namespace {
|
||||
std::atomic<Uint64> g_wireImageWriteEpoch{1};
|
||||
}
|
||||
Uint64 WireImageWriteEpoch() { return g_wireImageWriteEpoch.load(std::memory_order_relaxed); }
|
||||
void BumpWireImageWriteEpoch() { g_wireImageWriteEpoch.fetch_add(1, std::memory_order_relaxed); }
|
||||
|
||||
void EndActiveRenderPassOn(VkCommandBuffer commandBuffer) {
|
||||
BumpWireImageWriteEpoch();
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
if (active != nullptr && active->commandBuffer == commandBuffer && commandBuffer != VK_NULL_HANDLE)
|
||||
VkRenderPassManager::EndRenderPass(commandBuffer);
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -66,8 +66,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkExtent2D swapchainExtent);
|
||||
|
||||
struct RenderPassEntry {
|
||||
static inline VkDevice s_device;
|
||||
static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
|
||||
// Per Magma session (MagmaSession.h): each session's renderer is its own device.
|
||||
static inline SessionLocal<VkDevice> s_device;
|
||||
static inline thread_local Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
|
||||
Uint64 hash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
@@ -165,10 +166,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
~RenderPassEntry() {
|
||||
if (renderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(s_device, renderPass, nullptr);
|
||||
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
|
||||
}
|
||||
if (framebuffer != VK_NULL_HANDLE) {
|
||||
vkDestroyFramebuffer(s_device, framebuffer, nullptr);
|
||||
vkDestroyFramebuffer(s_device.Get(), framebuffer, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +185,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
struct ActiveRenderPassInfo {
|
||||
Uint64 hash = 0;
|
||||
Uint64 compatibilityHash = 0;
|
||||
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
|
||||
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
|
||||
IntVec2 extent = {0, 0};
|
||||
|
||||
@@ -226,6 +228,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
Bool Initialize();
|
||||
void Shutdown();
|
||||
// The surface target the default framebuffer currently names (VulkanRenderer's surface
|
||||
// targets): a default-framebuffer pass of one surface is not another's.
|
||||
void SetDefaultFramebufferTarget(Uint64 serial) {
|
||||
m_defaultFramebufferTarget = serial;
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
// Destroys every cached pass and framebuffer (not the renderbuffers). The device must be
|
||||
// idle and no pass may be open: a surface target's image views are going away.
|
||||
void PurgeRenderPasses();
|
||||
|
||||
HashType ComputeHash(
|
||||
const MG_State::GLState::FramebufferObject& fbo,
|
||||
@@ -302,6 +313,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
|
||||
// version change, swapchain rotation, any attachment image recreation (the two epochs),
|
||||
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
|
||||
Uint64 m_defaultFramebufferTarget = 0;
|
||||
Bool m_rpFastValid = false;
|
||||
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
|
||||
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
|
||||
@@ -411,12 +423,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline ActiveRenderPassInfo s_activeRenderPass{};
|
||||
static inline Bool s_hasActiveRenderPass = false;
|
||||
static inline VkClearManager* s_clearManager = nullptr;
|
||||
static inline VkTextureManager* s_textureManager = nullptr;
|
||||
static inline SwapchainObject* s_swapchainObject = nullptr;
|
||||
static inline VkRenderPassManager* s_renderPassManager = nullptr;
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
// Per Magma session (MagmaSession.h): one session's open render pass and managers are not
|
||||
// another's.
|
||||
static inline SessionLocal<ActiveRenderPassInfo> s_activeRenderPass;
|
||||
static inline SessionLocal<Bool> s_hasActiveRenderPass;
|
||||
static inline SessionLocal<VkClearManager*> s_clearManager;
|
||||
static inline SessionLocal<VkTextureManager*> s_textureManager;
|
||||
static inline SessionLocal<SwapchainObject*> s_swapchainObject;
|
||||
static inline SessionLocal<VkRenderPassManager*> s_renderPassManager;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -138,6 +138,6 @@ private:
|
||||
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "RenderPassGuard.h"
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
@@ -22,10 +23,16 @@
|
||||
// (shape) and the server's staged-texture store (texels).
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_Remote/Server/StagedTextureStore.h>
|
||||
// Shared images: a texture whose level 0 is a server-allocated AHardwareBuffer.
|
||||
#include <MG_Remote/Server/SharedImageRegistry.h>
|
||||
#include "../DirectVulkan.h"
|
||||
// P7 wave 2 package B3: rule I's tally for the silent exits on this file's wire arm.
|
||||
#include "WireDeclineTally.h"
|
||||
#if defined(__ANDROID__)
|
||||
#include <android/hardware_buffer.h>
|
||||
#endif
|
||||
#endif
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -42,9 +49,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
|
||||
// would also serialize against non-shader stages. The default only matters before a device
|
||||
// exists, when nothing records barriers.
|
||||
static VkPipelineStageFlags s_sampledReadStages =
|
||||
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
// Per Magma session: each session's renderer is its own device.
|
||||
struct SampledReadStages {
|
||||
VkPipelineStageFlags value = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
|
||||
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
|
||||
};
|
||||
static SessionLocal<SampledReadStages> s_sampledReadStagesSlot;
|
||||
|
||||
static VkImageUsageFlags ResolveTextureUsage(VkImageAspectFlags aspect,
|
||||
VkFormatFeatureFlags formatFeatures,
|
||||
Bool supportsStorageImage) {
|
||||
VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
if (supportsStorageImage) usage |= VK_IMAGE_USAGE_STORAGE_BIT;
|
||||
// A color aspect does not imply renderability (for example, RGB9_E5).
|
||||
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) &&
|
||||
(formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
|
||||
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
}
|
||||
if (aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
|
||||
usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
|
||||
}
|
||||
return usage;
|
||||
}
|
||||
|
||||
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
|
||||
Int maxDimension = std::max<Int>(baseTexelSize.x(),
|
||||
@@ -203,7 +229,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outSrcStageMask = s_sampledReadStages;
|
||||
outSrcStageMask = s_sampledReadStagesSlot->value;
|
||||
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -267,7 +293,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
|
||||
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
|
||||
outDstStageMask = s_sampledReadStages;
|
||||
outDstStageMask = s_sampledReadStagesSlot->value;
|
||||
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
|
||||
return;
|
||||
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
|
||||
@@ -367,6 +393,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
copyRegions.push_back(copy);
|
||||
}
|
||||
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyImage(commandBuffer,
|
||||
oldResource.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
|
||||
newResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
@@ -646,7 +673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_commandPool = initInfo.commandPool;
|
||||
m_graphicsQueue = initInfo.graphicsQueue;
|
||||
m_imageFormatListSupported = initInfo.imageFormatListSupported;
|
||||
s_sampledReadStages = initInfo.sampledReadStageMask;
|
||||
s_sampledReadStagesSlot->value = initInfo.sampledReadStageMask;
|
||||
m_currentFrameIndex = 0;
|
||||
m_deferredReleases.clear();
|
||||
m_deferredReleases.resize(initInfo.frameCount);
|
||||
@@ -659,8 +686,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MOBILEGL_ASSERT(initInfo.frameCount > 0,
|
||||
"VkTextureManager::Initialize failed: frameCount must be > 0");
|
||||
|
||||
TextureResource::s_device = m_device;
|
||||
TextureResource::s_allocator = m_allocator;
|
||||
TextureResource::s_device.Get() = m_device;
|
||||
TextureResource::s_allocator.Get() = m_allocator;
|
||||
|
||||
// Own pool for the recycled upload-batch command buffers. Parking a
|
||||
// dozen reset-but-alive command buffers in the renderer's shared pool
|
||||
@@ -698,6 +725,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Destroy images while the device/allocator still exist, including after context death.
|
||||
m_wireTextureResources.clear();
|
||||
m_wireRenderbufferResources.clear();
|
||||
m_sharedImageUses.clear();
|
||||
#endif
|
||||
m_aliveObjects.clear();
|
||||
m_storageImageTextures.clear();
|
||||
@@ -1477,7 +1505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
|
||||
s_sampledReadStages, srcAccessMask,
|
||||
s_sampledReadStagesSlot->value, srcAccessMask,
|
||||
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
|
||||
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
|
||||
@@ -1648,6 +1676,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::max(source->depth >> level, 1u)};
|
||||
copyRegions.push_back(copy);
|
||||
}
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
|
||||
|
||||
@@ -1762,6 +1791,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
|
||||
// validation layer warns about that literal 1 by name.
|
||||
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
|
||||
trackedLayout = newLayout;
|
||||
@@ -2105,6 +2135,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool VkTextureManager::SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record,
|
||||
TextureResource& resource, Bool requireStorage) {
|
||||
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
|
||||
// A YUV shared image is converted INTO the texture's own RGBA8 storage, which the rest of
|
||||
// this function defines as for any texture; the image is only named here, and the
|
||||
// renderer converts it at its first use in each frame (WireYuvImage.inc).
|
||||
if (record.SharedImageId != resource.yuvImageId && !m_syncingYuvTextureStorage) {
|
||||
// Respecified away from (or to another) YUV image: its source goes with it, once the
|
||||
// GPU is done with it (a released resource's Reset is deferred).
|
||||
if (resource.yuvSource != nullptr) {
|
||||
TextureResource retired;
|
||||
retired.yuvSource = std::move(resource.yuvSource);
|
||||
DeferResourceRelease(Move(retired));
|
||||
}
|
||||
resource.yuvImageId = 0;
|
||||
resource.yuvOwner.reset();
|
||||
resource.yuvSource.reset();
|
||||
resource.yuvConvertedFrame = 0;
|
||||
}
|
||||
if (record.SharedImageId != 0 && !m_syncingYuvTextureStorage) {
|
||||
const auto yuv = MG_Remote::Server::SharedImages::Find(record.SharedImageId);
|
||||
if (yuv != nullptr && MG_Remote::Server::SharedImages::FourccIsYuv(yuv->Fourcc)) {
|
||||
m_syncingYuvTextureStorage = true;
|
||||
const Bool defined = SyncWireTextureShape(record, resource, requireStorage);
|
||||
m_syncingYuvTextureStorage = false;
|
||||
if (!defined) return false;
|
||||
if (resource.yuvImageId != record.SharedImageId) {
|
||||
resource.yuvImageId = record.SharedImageId;
|
||||
resource.yuvOwner = yuv;
|
||||
resource.yuvSource.reset();
|
||||
resource.yuvConvertedFrame = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Level 0 is a shared image: bind its buffer. An image that cannot be bound here leaves
|
||||
// the texture the storage its NULL-data definition asked for, below.
|
||||
if (record.SharedImageId != 0 && !m_syncingYuvTextureStorage) {
|
||||
const SharedImageBind bound = SyncWireSharedImage(record, resource, requireStorage);
|
||||
if (bound != SharedImageBind::Unresolved) return bound == SharedImageBind::Bound;
|
||||
}
|
||||
if (desc.StorageKind != static_cast<Uint8>(TextureStorageType::Mipmap)) {
|
||||
MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined",
|
||||
desc.Resource.Slot, desc.Resource.Gen);
|
||||
@@ -2204,13 +2272,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkImageUsageFlags desiredUsage =
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) &&
|
||||
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)
|
||||
? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT))
|
||||
? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
|
||||
: 0);
|
||||
ResolveTextureUsage(aspect, formatProperties.optimalTilingFeatures, supportsStorageImage);
|
||||
// Multisample attachments are also transfer sources for vkCmdResolveImage.
|
||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
|
||||
@@ -2273,7 +2335,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
: (desc.Levels > 1 ? std::min(std::max<Uint32>(desc.Levels, fullMipLevels), fullMipLevels)
|
||||
: 1u);
|
||||
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
// A shared image never stands in for allocated storage: the attach is over (or never
|
||||
// resolved), so the texture gets storage of its own, and none of the image's texels.
|
||||
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
|
||||
resource.format == format &&
|
||||
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
|
||||
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
@@ -2288,7 +2353,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A named-level respecify changes ONLY that level; a BindMask update changes no
|
||||
// storage at all. Preserve every compatible old level when growing/upgrading the
|
||||
// allocation. A whole-store redefinition permits (but does not require) old texels.
|
||||
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.format == format &&
|
||||
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
|
||||
resource.format == format &&
|
||||
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
|
||||
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
|
||||
resource.viewType == shapeInfo.viewType &&
|
||||
@@ -2682,9 +2748,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
stencilCopy.bufferOffset = stagingBase + item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
||||
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
||||
} else {
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
}
|
||||
@@ -2693,7 +2761,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask);
|
||||
VkImageLayout uploadLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
ok = TransitionImageLayout(commandBuffer, resource.image, uploadLayout, finalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStages,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStagesSlot->value,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0,
|
||||
resource.mipLevels);
|
||||
MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to the sampled layout failed");
|
||||
@@ -2766,6 +2834,201 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return MG_Pipe::kMGPipeNullHandle;
|
||||
}
|
||||
|
||||
Bool VkTextureManager::ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height,
|
||||
ImportedSharedImage& out, String& why) {
|
||||
out = ImportedSharedImage{};
|
||||
#if defined(__ANDROID__)
|
||||
auto* ahb = static_cast<AHardwareBuffer*>(nativeBuffer);
|
||||
if (ahb == nullptr) {
|
||||
why = "the image has no AHardwareBuffer";
|
||||
return false;
|
||||
}
|
||||
VulkanRenderer::WireAhbImport ctx;
|
||||
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
|
||||
why = "the device did not take VK_ANDROID_external_memory_android_hardware_buffer";
|
||||
return false;
|
||||
}
|
||||
AHardwareBuffer_Desc desc{};
|
||||
AHardwareBuffer_describe(ahb, &desc);
|
||||
if (desc.width != width || desc.height != height || desc.layers != 1) {
|
||||
why = "the buffer is " + std::to_string(desc.width) + "x" + std::to_string(desc.height) + "x" +
|
||||
std::to_string(desc.layers) + ", not " + std::to_string(width) + "x" + std::to_string(height);
|
||||
return false;
|
||||
}
|
||||
const auto getProperties =
|
||||
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
|
||||
VkAndroidHardwareBufferFormatPropertiesANDROID formatProperties{
|
||||
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
|
||||
VkAndroidHardwareBufferPropertiesANDROID properties{VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
|
||||
properties.pNext = &formatProperties;
|
||||
VkResult result = getProperties(ctx.device, ahb, &properties);
|
||||
if (result != VK_SUCCESS) {
|
||||
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
// The RGBA8/RGBX8 buffers the registry allocates have a Vulkan format; an external-only
|
||||
// format would need a YCbCr conversion and is not a colour buffer this path can write.
|
||||
if (formatProperties.format == VK_FORMAT_UNDEFINED) {
|
||||
why = "the buffer has no Vulkan format (external format only)";
|
||||
return false;
|
||||
}
|
||||
// Usage is bounded by the buffer's own usage (the AHardwareBuffer usage equivalence);
|
||||
// transfers need no buffer usage bit.
|
||||
VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
||||
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) != 0 &&
|
||||
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0)
|
||||
usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT) != 0 &&
|
||||
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0)
|
||||
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
|
||||
VkExternalMemoryImageCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
|
||||
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.pNext = &external;
|
||||
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
imageInfo.format = formatProperties.format;
|
||||
imageInfo.extent = {width, height, 1};
|
||||
imageInfo.mipLevels = 1;
|
||||
imageInfo.arrayLayers = 1;
|
||||
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
|
||||
imageInfo.usage = usage;
|
||||
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
result = vkCreateImage(ctx.device, &imageInfo, nullptr, &out.image);
|
||||
if (result != VK_SUCCESS) {
|
||||
out.image = VK_NULL_HANDLE;
|
||||
why = "vkCreateImage(external AHB) -> " + std::to_string(static_cast<int>(result));
|
||||
return false;
|
||||
}
|
||||
Int32 type = -1;
|
||||
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
|
||||
if ((properties.memoryTypeBits & (1u << i)) != 0 &&
|
||||
(ctx.memory.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0)
|
||||
type = static_cast<Int32>(i);
|
||||
}
|
||||
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
|
||||
if ((properties.memoryTypeBits & (1u << i)) != 0) type = static_cast<Int32>(i);
|
||||
}
|
||||
if (type < 0) {
|
||||
why = "no memory type can import it";
|
||||
DestroyImportedSharedImage(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
|
||||
dedicated.image = out.image;
|
||||
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
|
||||
import.pNext = &dedicated;
|
||||
import.buffer = ahb;
|
||||
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocateInfo.pNext = &import;
|
||||
allocateInfo.allocationSize = properties.allocationSize;
|
||||
allocateInfo.memoryTypeIndex = static_cast<Uint32>(type);
|
||||
result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory);
|
||||
if (result != VK_SUCCESS) {
|
||||
out.memory = VK_NULL_HANDLE;
|
||||
why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast<int>(result));
|
||||
DestroyImportedSharedImage(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
result = vkBindImageMemory(ctx.device, out.image, out.memory, 0);
|
||||
if (result != VK_SUCCESS) {
|
||||
why = "vkBindImageMemory -> " + std::to_string(static_cast<int>(result));
|
||||
DestroyImportedSharedImage(ctx.device, out);
|
||||
return false;
|
||||
}
|
||||
out.format = formatProperties.format;
|
||||
out.usage = usage;
|
||||
out.features = formatProperties.formatFeatures;
|
||||
out.queueFamily = ctx.queueFamily;
|
||||
return true;
|
||||
#else
|
||||
(void)nativeBuffer, (void)width, (void)height;
|
||||
why = "shared images are imported on Android only";
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void VkTextureManager::DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image) {
|
||||
if (image.image != VK_NULL_HANDLE) vkDestroyImage(device, image.image, nullptr);
|
||||
if (image.memory != VK_NULL_HANDLE) vkFreeMemory(device, image.memory, nullptr);
|
||||
image = ImportedSharedImage{};
|
||||
}
|
||||
|
||||
VkTextureManager::SharedImageBind VkTextureManager::SyncWireSharedImage(
|
||||
const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource, Bool requireStorage) {
|
||||
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
|
||||
const Uint64 id = record.SharedImageId;
|
||||
if (requireStorage) {
|
||||
// The buffer's usage has no storage equivalent; a reallocation would drop the image.
|
||||
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is bound as a shader image, "
|
||||
"which it cannot back; declined",
|
||||
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
|
||||
return SharedImageBind::Failed;
|
||||
}
|
||||
// Already this image: the serial moved for something else (params, the bind mask).
|
||||
if (resource.sharedImageId == id && resource.image != VK_NULL_HANDLE) return SharedImageBind::Bound;
|
||||
|
||||
const auto image = MG_Remote::Server::SharedImages::Find(id);
|
||||
if (image == nullptr) {
|
||||
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is no longer live; the texture "
|
||||
"keeps its own (undefined) level 0",
|
||||
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
|
||||
return SharedImageBind::Unresolved;
|
||||
}
|
||||
const Bool flat2D = desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D) ||
|
||||
desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::TexRect);
|
||||
if (!flat2D || desc.Width != image->Width || desc.Height != image->Height ||
|
||||
!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) {
|
||||
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pipe target %u at %ux%u cannot take shared image "
|
||||
"%llu (%ux%u); the texture keeps its own level 0",
|
||||
desc.Resource.Slot, desc.Resource.Gen, static_cast<Uint32>(desc.Target), desc.Width,
|
||||
desc.Height, static_cast<unsigned long long>(id), image->Width, image->Height);
|
||||
return SharedImageBind::Unresolved;
|
||||
}
|
||||
ImportedSharedImage imported;
|
||||
String why;
|
||||
if (!ImportSharedImage(image->Native, image->Width, image->Height, imported, why)) {
|
||||
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu not imported - %s; the texture "
|
||||
"keeps its own level 0",
|
||||
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id), why.c_str());
|
||||
return SharedImageBind::Unresolved;
|
||||
}
|
||||
|
||||
// NOT ACQUIRED HERE. The writer (a client session's present, on its own device) releases
|
||||
// the image to VK_QUEUE_FAMILY_FOREIGN_EXT in GENERAL - also the layout every wire
|
||||
// descriptor samples in - once per frame it writes. Each use acquires it again when the
|
||||
// write generation moved, waiting for that write's fence on the GPU, and the frame boundary
|
||||
// releases it back (VulkanRenderer::AcquireSharedImage, WireSharedImage.inc).
|
||||
|
||||
TextureResource replacement;
|
||||
replacement.image = imported.image;
|
||||
replacement.importedMemory = imported.memory;
|
||||
replacement.layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
replacement.extent = {image->Width, image->Height};
|
||||
replacement.depth = 1;
|
||||
replacement.arrayLayers = 1;
|
||||
replacement.mipLevels = 1;
|
||||
replacement.sampledBaseMipLevel = 0;
|
||||
replacement.sampledLevelCount = 1;
|
||||
replacement.format = imported.format;
|
||||
replacement.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
replacement.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
replacement.sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
replacement.imageCreateFlags = 0;
|
||||
replacement.usageFlags = imported.usage;
|
||||
replacement.sharedImageId = id;
|
||||
replacement.sharedImageOwner = image;
|
||||
replacement.sharedImageAlphaOne = MG_Remote::Server::SharedImages::FourccIgnoresAlpha(image->Fourcc);
|
||||
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
|
||||
DeferResourceRelease(Move(resource));
|
||||
std::destroy_at(&resource);
|
||||
std::construct_at(&resource, Move(replacement));
|
||||
MGLOG_D("Magma wire texture {slot=%u, gen=%u}: level 0 is shared image %llu (%ux%u)", desc.Resource.Slot,
|
||||
desc.Resource.Gen, static_cast<unsigned long long>(id), image->Width, image->Height);
|
||||
return SharedImageBind::Bound;
|
||||
}
|
||||
|
||||
VkTextureManager::TextureResource* VkTextureManager::SyncTextureResourceByHandle(
|
||||
MG_Pipe::MGPipeHandle handle, Bool renderbuffer, Bool requireStorage) {
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureResourceByHandle: m_device == VK_NULL_HANDLE");
|
||||
@@ -2842,6 +3105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
handle.Slot, handle.Gen);
|
||||
return nullptr;
|
||||
}
|
||||
resource.sharedImageKey = resource.sharedImageId != 0 || resource.yuvImageId != 0 ? key : 0;
|
||||
resource.syncedWireSerial = record.Serial;
|
||||
return &resource;
|
||||
}
|
||||
@@ -2963,6 +3227,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if ((aspect & copyAspect) == 0) continue;
|
||||
copy.srcSubresource.aspectMask = copyAspect;
|
||||
copy.dstSubresource.aspectMask = copyAspect;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyImage(commandBuffer, old.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, grown.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ©);
|
||||
}
|
||||
@@ -3258,12 +3523,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkImageUsageFlags desiredUsage =
|
||||
VK_IMAGE_USAGE_SAMPLED_BIT |
|
||||
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
|
||||
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
|
||||
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
|
||||
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
|
||||
0);
|
||||
ResolveTextureUsage(aspect, formatProperties.optimalTilingFeatures, supportsStorageImage);
|
||||
if (!isMultisampleTexture) {
|
||||
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
|
||||
}
|
||||
@@ -3558,7 +3818,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
resource.sampledView == VK_NULL_HANDLE &&
|
||||
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
|
||||
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
|
||||
resource.storageImageViews.empty()) {
|
||||
resource.storageImageViews.empty()
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
&& resource.yuvSource == nullptr
|
||||
#endif
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4518,6 +4782,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
rectCopies[rectCopyCount++] = rectCopy;
|
||||
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
|
||||
}
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies.data());
|
||||
continue;
|
||||
@@ -4559,10 +4824,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
|
||||
stencilCopy.bufferOffset = stagingBase + item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
|
||||
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
|
||||
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
|
||||
continue;
|
||||
}
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
|
||||
1, ©);
|
||||
}
|
||||
@@ -4573,7 +4840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
uploadLayout,
|
||||
finalLayout,
|
||||
VK_PIPELINE_STAGE_TRANSFER_BIT,
|
||||
s_sampledReadStages,
|
||||
s_sampledReadStagesSlot->value,
|
||||
VK_ACCESS_TRANSFER_WRITE_BIT,
|
||||
VK_ACCESS_SHADER_READ_BIT,
|
||||
aspectMask, 0, outResource.mipLevels);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "../MagmaSession.h"
|
||||
#include <Includes.h>
|
||||
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
|
||||
#include <MG_State/GLState/TextureState/TextureObject.h>
|
||||
@@ -356,6 +357,30 @@ public:
|
||||
// on every mutation, so the first handle sync of a fresh image always re-checks.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 syncedWireSerial = 0;
|
||||
// SHARED IMAGES: level 0 is a server shared image (an imported AHardwareBuffer), not a
|
||||
// VMA allocation. The resource owns the image and its dedicated imported memory, and
|
||||
// holds the registry's reference so the buffer outlives every use (deferred releases
|
||||
// included). `sharedImageAlphaOne`: an X-format image, whose alpha bytes are undefined.
|
||||
VkDeviceMemory importedMemory = VK_NULL_HANDLE;
|
||||
Uint64 sharedImageId = 0;
|
||||
SharedPtr<const void> sharedImageOwner;
|
||||
Bool sharedImageAlphaOne = false;
|
||||
// The reading side's ownership of the image (VulkanRenderer::AcquireSharedImage): whether
|
||||
// this device holds it now - acquired from the foreign family and not yet released back at
|
||||
// the frame boundary - the writer's generation it last acquired, and the
|
||||
// m_wireTextureResources key it lives under, by which the frame boundary finds it again.
|
||||
Bool sharedImageHeld = false;
|
||||
Uint64 sharedImageGeneration = 0;
|
||||
Uint64 sharedImageKey = 0;
|
||||
// A YUV shared image (NV12/P010) is never the texture's image: the texture keeps its own
|
||||
// RGBA8 level 0, into which the renderer converts the image at its first use in each frame
|
||||
// (WireYuvImage.inc) - `yuvSource` being that conversion's import of the buffer, made on
|
||||
// first use, and `yuvConvertedFrame` the frame it last ran in. `yuvOwner` holds the
|
||||
// registry's image for as long as the texture names it.
|
||||
Uint64 yuvImageId = 0;
|
||||
SharedPtr<const void> yuvOwner;
|
||||
SharedPtr<void> yuvSource;
|
||||
Uint64 yuvConvertedFrame = 0;
|
||||
#endif
|
||||
|
||||
TextureResource() = default;
|
||||
@@ -392,44 +417,72 @@ public:
|
||||
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::swap(this->syncedWireSerial, that.syncedWireSerial);
|
||||
std::swap(this->importedMemory, that.importedMemory);
|
||||
std::swap(this->sharedImageId, that.sharedImageId);
|
||||
std::swap(this->sharedImageOwner, that.sharedImageOwner);
|
||||
std::swap(this->sharedImageAlphaOne, that.sharedImageAlphaOne);
|
||||
std::swap(this->sharedImageHeld, that.sharedImageHeld);
|
||||
std::swap(this->sharedImageGeneration, that.sharedImageGeneration);
|
||||
std::swap(this->sharedImageKey, that.sharedImageKey);
|
||||
std::swap(this->yuvImageId, that.yuvImageId);
|
||||
std::swap(this->yuvOwner, that.yuvOwner);
|
||||
std::swap(this->yuvSource, that.yuvSource);
|
||||
std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
if (fullView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, fullView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), fullView, nullptr);
|
||||
}
|
||||
if (sampledView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, sampledView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
|
||||
}
|
||||
for (const auto attachmentView : perMipViews) {
|
||||
if (attachmentView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, attachmentView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), attachmentView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto sampledView : perMipSampledViews) {
|
||||
if (sampledView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, sampledView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, attachmentView] : attachmentViews) {
|
||||
if (attachmentView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, attachmentView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), attachmentView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, sampledView] : alternateSampledViews) {
|
||||
if (sampledView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, sampledView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
|
||||
}
|
||||
}
|
||||
for (const auto& [_, storageImageView] : storageImageViews) {
|
||||
if (storageImageView != VK_NULL_HANDLE) {
|
||||
vkDestroyImageView(s_device, storageImageView, nullptr);
|
||||
vkDestroyImageView(s_device.Get(), storageImageView, nullptr);
|
||||
}
|
||||
}
|
||||
if (image != VK_NULL_HANDLE && allocation != nullptr) {
|
||||
vmaDestroyImage(s_allocator, image, allocation);
|
||||
vmaDestroyImage(s_allocator.Get(), image, allocation);
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (importedMemory != VK_NULL_HANDLE) {
|
||||
if (image != VK_NULL_HANDLE) vkDestroyImage(s_device.Get(), image, nullptr);
|
||||
vkFreeMemory(s_device.Get(), importedMemory, nullptr);
|
||||
}
|
||||
importedMemory = VK_NULL_HANDLE;
|
||||
sharedImageId = 0;
|
||||
sharedImageOwner.reset();
|
||||
sharedImageAlphaOne = false;
|
||||
sharedImageHeld = false;
|
||||
sharedImageGeneration = 0;
|
||||
sharedImageKey = 0;
|
||||
yuvImageId = 0;
|
||||
yuvOwner.reset();
|
||||
yuvSource.reset();
|
||||
yuvConvertedFrame = 0;
|
||||
#endif
|
||||
fullView = VK_NULL_HANDLE;
|
||||
sampledView = VK_NULL_HANDLE;
|
||||
perMipViews.clear();
|
||||
@@ -466,8 +519,8 @@ public:
|
||||
Reset();
|
||||
}
|
||||
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
|
||||
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
|
||||
static inline SessionLocal<VmaAllocator> s_allocator;
|
||||
};
|
||||
|
||||
struct SampledTextureSnapshot {
|
||||
@@ -591,6 +644,41 @@ public:
|
||||
// ranges select cube faces; array and 3D levels share one staged level shadow.
|
||||
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): a server shared image's
|
||||
// AHardwareBuffer bound as a single-level 2D colour image on THIS session's device, on dedicated
|
||||
// memory imported from the buffer. Android only (false with `why` elsewhere, or when the device
|
||||
// did not take the AHardwareBuffer extension). The image is born UNDEFINED; whoever uses it
|
||||
// acquires it from the foreign queue family first, and a writer releases it back after.
|
||||
struct ImportedSharedImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageUsageFlags usage = 0;
|
||||
VkFormatFeatureFlags features = 0;
|
||||
Uint32 queueFamily = 0;
|
||||
};
|
||||
static Bool ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height, ImportedSharedImage& out,
|
||||
String& why);
|
||||
static void DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image);
|
||||
// The reading side's per-use hook: PrepareWireTextureResources notes every shared-image
|
||||
// texture a draw or dispatch samples, and the renderer takes the list right after
|
||||
// (VulkanRenderer::AcquireNotedSharedImages) to acquire each one ahead of the sampling.
|
||||
void NoteSharedImageUse(const TextureResource& resource) {
|
||||
if (std::find(m_sharedImageUses.begin(), m_sharedImageUses.end(), resource.sharedImageKey) ==
|
||||
m_sharedImageUses.end())
|
||||
m_sharedImageUses.push_back(resource.sharedImageKey);
|
||||
}
|
||||
Bool HasNotedSharedImageUses() const { return !m_sharedImageUses.empty(); }
|
||||
void TakeNotedSharedImageUses(Vector<Uint64>& out) {
|
||||
out.clear();
|
||||
std::swap(out, m_sharedImageUses);
|
||||
}
|
||||
// The live handle-keyed texture resource under `key`, or null.
|
||||
TextureResource* FindWireTextureResource(Uint64 key) {
|
||||
const auto it = m_wireTextureResources.find(key);
|
||||
return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr;
|
||||
}
|
||||
#endif
|
||||
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
|
||||
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
|
||||
@@ -777,10 +865,18 @@ private:
|
||||
Bool requireStorage);
|
||||
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
|
||||
TextureResource& resource);
|
||||
// SyncWireTextureShape's arm for a record whose level 0 IS a shared image (SharedImageId):
|
||||
// binds the imported buffer instead of allocating. Unresolved = the image is gone or cannot be
|
||||
// imported here; `resource` is then untouched and the caller allocates as for any texture.
|
||||
enum class SharedImageBind : Uint8 { Bound, Failed, Unresolved };
|
||||
SharedImageBind SyncWireSharedImage(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource,
|
||||
Bool requireStorage);
|
||||
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
|
||||
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
|
||||
// across map-touching calls.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireTextureResources;
|
||||
// Shared-image textures sampled since the renderer last took the list (NoteSharedImageUse).
|
||||
Vector<Uint64> m_sharedImageUses;
|
||||
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
|
||||
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
|
||||
#endif
|
||||
@@ -803,6 +899,8 @@ private:
|
||||
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
|
||||
// textures already fully synced in the current draw (small N -> flat scan).
|
||||
Bool m_drawSyncScopeActive = false;
|
||||
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
|
||||
Bool m_syncingYuvTextureStorage = false;
|
||||
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
|
||||
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
|
||||
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,9 @@
|
||||
#include "VkTextureManager.h"
|
||||
#include "VkTimerQueryManager.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "GpuProgressMarkers.h"
|
||||
#include "WireRenderPassCompatibility.h"
|
||||
#include <MG_Remote/Server/SharedImageRegistry.h>
|
||||
#endif
|
||||
#include "MG_Util/Math/VectorTypes.h"
|
||||
#include <Includes.h>
|
||||
@@ -144,6 +146,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// IBufferCopyCommandProvider: recording command buffer, outside any
|
||||
// render pass, for immediate staged buffer copies.
|
||||
VkCommandBuffer AcquireBufferCopyCommandBuffer() override;
|
||||
// EGL_BUFFER_AGE_EXT of the active target's default framebuffer for the next frame: the age
|
||||
// of the swapchain image acquired for it (SwapchainObject::BufferAgeOf). Asking makes the
|
||||
// target keep its presented images' content from then on.
|
||||
Int32 CurrentDrawBufferAge();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager& GetWireBufferManager() { return m_bufferManager; }
|
||||
Bool FlushWirePendingCommandsForTextureUpdate() {
|
||||
@@ -179,6 +185,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void* getAhbProperties = nullptr; // PFN_vkGetAndroidHardwareBufferPropertiesANDROID
|
||||
};
|
||||
Bool GetWireAhbImport(WireAhbImport& out) const;
|
||||
// SHARED IMAGES (BackendObject::BlitDefaultFramebufferToSharedImage): the active surface
|
||||
// target's default framebuffer, copied into `image` top row first and scaled to its
|
||||
// extent. Returns once the copy is submitted, with its fence published to the image
|
||||
// (SharedImages::PublishWrite); waits for it on the CPU only on a device without sync_file
|
||||
// export. False (logged) when it cannot be done here: no AHardwareBuffer import, a
|
||||
// quarter-turned surface, an unblittable format.
|
||||
//
|
||||
// `region` (GL window coordinates; Full = all) is the part to copy into an image this
|
||||
// renderer has written before - the rest of it is left as it is; a first write, a scaled
|
||||
// or turned copy copies all of it.
|
||||
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image, const MG_Util::Damage::Region& region);
|
||||
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): every image acquired
|
||||
// since the last boundary goes back to the foreign family, the recording is submitted with
|
||||
// the exportable semaphore, and its sync_file is published to each as a write and a read.
|
||||
// From then on an image's first acquire per frame also waits for its pending reads.
|
||||
Bool PublishSharedImageAccesses();
|
||||
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): the
|
||||
// submission's sync_file in `*fence` (-1: none, and the work is waited out here), and - for
|
||||
// a session that writes images - the boundary PublishSharedImageAccesses makes.
|
||||
Bool ExportNativeFence(int* fence);
|
||||
// The reading side's per-use hook (WireSharedImage.inc): every shared-image texture the
|
||||
// wire draw or dispatch being set up samples (VkTextureManager::NoteSharedImageUse) is
|
||||
// acquired from the foreign family before it, when it moved or is not held.
|
||||
void AcquireNotedSharedImages() {
|
||||
if (m_textureManager && m_textureManager->HasNotedSharedImageUses()) AcquireNotedSharedImagesSlow();
|
||||
}
|
||||
// One shared-image texture, before a use: a new write generation, or an image released at
|
||||
// the last frame boundary, is acquired here - an ownership barrier recorded ahead of the
|
||||
// use, the write's fence a wait of the submission carrying it - and noted as read.
|
||||
void AcquireSharedImage(VkTextureManager::TextureResource& resource);
|
||||
#endif
|
||||
|
||||
// FrameContext::IRecordingObserver: prepares the frame's timer-query
|
||||
@@ -358,6 +394,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// one is retired until the frame slot's fence is next waited. Returns
|
||||
// true when a submission was made.
|
||||
Bool FlushPendingCommands();
|
||||
// DEVICE LOSS. A GPU fault or hang the driver reset leaves this renderer's VkDevice lost:
|
||||
// every later submit and wait answers VK_ERROR_DEVICE_LOST. NoteDeviceLoss records it at
|
||||
// the submit/wait sites (cheap, any result); IsDeviceLost answers from the record and,
|
||||
// when nothing was recorded, asks the device once (only failure paths call it, so the
|
||||
// probe's wait costs nothing on a healthy frame). Each session owns its own VkDevice, so a
|
||||
// loss here is this session's alone.
|
||||
void NoteDeviceLoss(VkResult result, const char* where);
|
||||
Bool IsDeviceLost();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// For a wire site whose submit or wait just failed: true when the failure is a lost
|
||||
// device, in which case this session has been latched (MGPipeSessionLatch - it ends, the
|
||||
// display server's other sessions do not) and the caller must return from its verb
|
||||
// without recording or submitting anything more. False: an ordinary failure, the site
|
||||
// keeps the death it had. `site` names it in the latch line.
|
||||
Bool LatchWireDeviceLoss(const char* site);
|
||||
// THE GPU HANG WATCH (GpuProgressMarkers.h): true once the watch named one of this device's
|
||||
// submissions as the one that stops the GPU. The device is then treated as lost, this session
|
||||
// is latched, and the caller returns from its verb without submitting anything more. Asked
|
||||
// before every frame submission and by the apply loop before it applies another record.
|
||||
Bool LatchIfGpuHung(const char* site);
|
||||
#endif
|
||||
// Flush gated on usefulness: only flushes when `submitIndex` is still
|
||||
// unsubmitted, so poll loops on already-submitted fences do not split
|
||||
// the frame's render pass (a full tile load/store on TBDR GPUs).
|
||||
@@ -427,7 +484,65 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// no new swapchain is installed and presentation must stay suspended.
|
||||
Bool RecreateSwapchain();
|
||||
|
||||
// SURFACE TARGETS. One renderer - one device and every GL object's resources - serves
|
||||
// every EGL surface of its context's client; only the presentation side (VkSurfaceKHR,
|
||||
// swapchain, its render-finished semaphores, the acquired image, the pbuffer's image
|
||||
// reader) belongs to a surface. `key` names the client surface. The renderer starts with
|
||||
// the surface it was constructed for as its active target (SetActiveSurfaceTargetKey).
|
||||
// Activating another key parks the active target - its recorded work submitted, its
|
||||
// pending acquire consumed - and brings the other back, or builds it on first use.
|
||||
void SetActiveSurfaceTargetKey(Uint64 key) { m_activeTargetKey = key; }
|
||||
// False when a NEW target's surface could not be built (logged); the renderer is then left
|
||||
// with no active target - every present drops its frame - and the caller refuses by name.
|
||||
Bool ActivateSurfaceTarget(Uint64 key, NativeWindowType window, const VulkanRendererConfig& surfaceConfig);
|
||||
// Drops a surface's target, active or parked. A no-op for a key that has none.
|
||||
void DestroySurfaceTarget(Uint64 key);
|
||||
// The default framebuffer's extent (surface space, what PublishDefaultFramebufferInfo
|
||||
// publishes) of `key`'s target, active or parked; 0x0 when it has none or no swapchain.
|
||||
VkExtent2D SurfaceTargetExtent(Uint64 key) const;
|
||||
// The active WINDOW target's window changed size. Between frames (nothing recorded since the last
|
||||
// present) its swapchain is rebuilt now - which publishes the new extent - so a client that has
|
||||
// nothing to redraw learns it without presenting; with a frame half-recorded the rebuild waits
|
||||
// for that frame's present (it would otherwise land half in each swapchain). False: no active
|
||||
// window target.
|
||||
Bool FollowActiveWindowResize();
|
||||
// Whether `key` names the active target.
|
||||
Bool IsActiveSurfaceTarget(Uint64 key) const { return key == m_activeTargetKey && m_surface != VK_NULL_HANDLE; }
|
||||
|
||||
private:
|
||||
// The presentation half of the renderer, parked while another surface is active. Its
|
||||
// fields mirror the renderer members ExchangeActiveTarget swaps them with.
|
||||
struct SurfaceTarget {
|
||||
Uint64 serial = 0;
|
||||
NativeWindowType window = 0;
|
||||
void* platformDisplay = nullptr;
|
||||
void* platformLibrary = nullptr;
|
||||
void* platformCloseDisplay = nullptr;
|
||||
void* fallbackImageReader = nullptr;
|
||||
Uint32 surfaceWidth = 0;
|
||||
Uint32 surfaceHeight = 0;
|
||||
Optional<Int> swapInterval;
|
||||
Bool presentsToAppWindow = false;
|
||||
Optional<Int> swapchainSwapInterval;
|
||||
Bool swapchainResizeRequested = false;
|
||||
Bool presentSuspended = false;
|
||||
VkSurfaceKHR surface = VK_NULL_HANDLE;
|
||||
SwapchainObject swapchain;
|
||||
Vector<VkSemaphore> renderFinishedSemaphores;
|
||||
Uint imageIndexAcquired = 0;
|
||||
Uint defaultFramebufferImageIndex = 0;
|
||||
};
|
||||
void ExchangeActiveTarget(SurfaceTarget& target);
|
||||
// Submits the active target's work, consumes its pending acquire and moves it into
|
||||
// m_parkedTargets. Leaves the renderer with no target.
|
||||
void ParkActiveTarget();
|
||||
void OnActiveTargetChanged();
|
||||
void DestroyParkedTarget(SurfaceTarget& target);
|
||||
UnorderedMap<Uint64, SurfaceTarget> m_parkedTargets;
|
||||
Uint64 m_activeTargetKey = 0;
|
||||
Uint64 m_activeTargetSerial = 1;
|
||||
Uint64 m_nextTargetSerial = 2;
|
||||
|
||||
// Tiered emission for an already-set-up multi-draw batch (state bound, index
|
||||
// buffer bound for the indexed form). Tier 1: VK_EXT_multi_draw. Tier 2: one
|
||||
// vkCmdDraw(Indexed)Indirect over a transient command array. Tier 3: unrolled
|
||||
@@ -513,7 +628,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
void QueueClearBufferPayload(GLenum buffer, GLint drawbuffer, const ClearAttachmentPayload& clearPayload);
|
||||
// See NoteDeviceLoss. Set once, never cleared: a lost VkDevice stays lost.
|
||||
Bool m_deviceLost = false;
|
||||
Bool m_deviceLossLatched = false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Brackets this device's frame submissions for the GPU hang watch (LatchIfGpuHung).
|
||||
GpuProgressMarkers m_progressMarkers;
|
||||
struct WireImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
@@ -597,6 +717,94 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// one entry point the import needs (PFN_vkGetAndroidHardwareBufferPropertiesANDROID).
|
||||
Bool m_wireAhbImport = false;
|
||||
void* m_wireGetAhbProperties = nullptr;
|
||||
// YUV shared images (WireYuvImage.inc): the device enabled samplerYcbcrConversion, and the
|
||||
// conversion pipelines made so far (one per buffer format and colour conversion).
|
||||
Bool m_samplerYcbcrConversion = false;
|
||||
struct WireYuvResources;
|
||||
WireYuvResources* m_wireYuvResources = nullptr;
|
||||
void DestroyWireYuvResources();
|
||||
// A YUV texture's first use in a frame converts its image (yuvConvertedFrame against this,
|
||||
// which every frame boundary - present, publishing flush, native fence - moves).
|
||||
Uint64 m_yuvFrameSerial = 1;
|
||||
Bool ConvertYuvSharedImage(VkTextureManager::TextureResource& resource);
|
||||
// Shared images this session presents into, imported once each and keyed by the image's
|
||||
// id (monotonic, never reused). `owner` watches the registry's image: an entry whose image
|
||||
// died is dropped at a later present once `lastSubmit`, its last copy, has completed.
|
||||
struct SharedImagePresentTarget {
|
||||
VkTextureManager::ImportedSharedImage image;
|
||||
WeakPtr<const void> owner;
|
||||
Uint64 lastSubmit = 0;
|
||||
// A copy has written the image through this import: what it holds is a frame.
|
||||
Bool written = false;
|
||||
};
|
||||
UnorderedMap<Uint64, SharedImagePresentTarget> m_sharedImagePresentTargets;
|
||||
void DestroySharedImagePresentTargets(Bool onlyDead);
|
||||
|
||||
// SHARED-IMAGE SYNCHRONISATION (WireSharedImage.inc). The sync_file fences other sessions
|
||||
// publish become binary semaphores: a temporary SYNC_FD import is a WAIT, an exportable
|
||||
// semaphore a SIGNAL, of the next frame submission - whichever of SubmitPendingCommandBuffer
|
||||
// and Present's submit comes first, which is the one carrying what was recorded since.
|
||||
// Each semaphore is recycled once the submission that used it has completed.
|
||||
// m_sharedImageSyncFd: the device imports and exports SYNC_FD binary semaphores; without
|
||||
// it the waits happen on the CPU and nothing is exported (see WireSharedImage.inc).
|
||||
Bool m_sharedImageSyncFd = false;
|
||||
// The session writes images - it ends frames at publishing flushes
|
||||
// (PublishSharedImageAccesses), or it reached one through a framebuffer (ResolveWireImage):
|
||||
// a first acquire waits for the image's pending reads as well, and its boundaries publish
|
||||
// what it used as written.
|
||||
Bool m_sharedImageImplicitSync = false;
|
||||
PFN_vkImportSemaphoreFdKHR m_importSemaphoreFd = nullptr;
|
||||
PFN_vkGetSemaphoreFdKHR m_getSemaphoreFd = nullptr;
|
||||
struct SharedImageWait {
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
VkPipelineStageFlags stages = 0;
|
||||
};
|
||||
Vector<SharedImageWait> m_sharedImageWaits; // staged for the next submission
|
||||
VkSemaphore m_sharedImageSignal = VK_NULL_HANDLE; // staged for the next submission
|
||||
VkSemaphore m_sharedImageSignalSubmitted = VK_NULL_HANDLE; // the last one submitted, to export
|
||||
struct SharedImageSemaphoreInFlight {
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
Uint64 submitIndex = 0;
|
||||
Bool exportable = false;
|
||||
Bool exported = false; // a signal whose sync_file was taken (which unsignals it)
|
||||
};
|
||||
Vector<SharedImageSemaphoreInFlight> m_sharedImageSemaphoresInFlight; // submit order
|
||||
Vector<VkSemaphore> m_freeSharedImageWaitSemaphores;
|
||||
Vector<VkSemaphore> m_freeSharedImageSignalSemaphores;
|
||||
// The reading side, per session: the shared-image textures this frame holds (acquired, not
|
||||
// yet released; keyed as in VkTextureManager's handle map, with the submission that carries
|
||||
// the acquire), and the frame's ReadTracker, published at Present.
|
||||
struct SharedImageHold {
|
||||
Uint64 key = 0;
|
||||
Uint64 acquireSubmit = 0;
|
||||
};
|
||||
Vector<SharedImageHold> m_sharedImagesHeld;
|
||||
MG_Remote::Server::SharedImages::ReadTracker m_sharedImageReads;
|
||||
Vector<Uint64> m_sharedImageUseScratch;
|
||||
// What one vkQueueSubmit carries beyond its own semaphores (AttachSharedImageSync).
|
||||
struct SharedImageSubmitSync {
|
||||
Bool attached = false;
|
||||
Vector<VkSemaphore> waits;
|
||||
Vector<VkPipelineStageFlags> stages;
|
||||
Vector<VkSemaphore> signals;
|
||||
};
|
||||
void AttachSharedImageSync(VkSubmitInfo& info, SharedImageSubmitSync& storage);
|
||||
// After the submission `storage` was attached to was submitted and registered.
|
||||
void CommitSharedImageSync(const SharedImageSubmitSync& storage);
|
||||
// Takes ownership of `fence` (a sync_file; -1 = nothing to wait for).
|
||||
void StageSharedImageWait(int fence, VkPipelineStageFlags stages);
|
||||
VkSemaphore TakeSharedImageSemaphore(Bool exportable);
|
||||
// The submitted signal's sync_file (-1: it has already signaled). False: not exported.
|
||||
Bool ExportSharedImageSignal(VkSemaphore semaphore, int& fence);
|
||||
void RecycleSharedImageSemaphores(Uint64 completedSubmit);
|
||||
void DestroySharedImageSync();
|
||||
void AcquireNotedSharedImagesSlow();
|
||||
// The reading side's frame boundary: release every held image back to the foreign family
|
||||
// in the open recording, and - after the submission carrying it - publish its fence.
|
||||
void ReleaseHeldSharedImages();
|
||||
void PublishSharedImageReads();
|
||||
// A frame dropped unsubmitted (Present with no usable swapchain).
|
||||
void AbandonSharedImageReads();
|
||||
// Runs the probe (memoized per device identity) and sets the member above. Called at the end of
|
||||
// device creation, after ArmPrimGenReroute: it records on m_graphicsQueue.
|
||||
void ArmWireDepthResolveOrder();
|
||||
@@ -696,7 +904,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// P8-SV: glDispatchComputeIndirect on the wire arm - vkCmdDispatchIndirect from the
|
||||
// dispatch-indirect store, as the monolith arm issues it (WireDraw.inc).
|
||||
void DispatchWireComputeIndirect(GLintptr offset);
|
||||
void RewindWireDescriptorSetsIfDue();
|
||||
// False: the device was lost and the session latched; the caller drops its draw/dispatch.
|
||||
Bool RewindWireDescriptorSetsIfDue();
|
||||
UniquePtr<RenderPassEntry> m_wireDrawPass;
|
||||
Vector<VkImageView> m_wireDrawViews;
|
||||
VkPipeline GetOrCreatePipelineWithInput(GLenum mode, const MagmaProgramSource& program,
|
||||
@@ -805,7 +1014,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a substituted window. (Android is the one exception and has its own path -
|
||||
// no Mali/Adreno driver seen so far exposes the extension, so a windowless
|
||||
// context is given an AImageReader ANativeWindow that is never displayed.)
|
||||
Bool m_headlessSurfaceSupported = true;
|
||||
// ENABLED ON THIS INSTANCE, not merely offered: a renderer created for a window never asks
|
||||
// for the extension, and its later offscreen targets must not call an entry point the
|
||||
// instance does not have (OffscreenSurfaceRoute.h). Hence false until CreateInstance says so.
|
||||
Bool m_headlessSurfaceSupported = false;
|
||||
// VK_KHR_android_surface was enabled on this instance (the AImageReader route needs it).
|
||||
Bool m_androidSurfaceEnabled = false;
|
||||
// Android has the same shortfall: no Mali/Adreno driver seen so far exposes
|
||||
// VK_EXT_headless_surface, so a windowless (EGL pbuffer) context gets an
|
||||
// AImageReader's ANativeWindow to hand the WSI instead. Nothing is ever
|
||||
@@ -935,17 +1149,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDeviceSize offset, VkBuffer countBuffer,
|
||||
VkDeviceSize countBufferOffset, Uint32 maxDrawCount,
|
||||
Uint32 stride);
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
PFNDrawIndexedIndirectCountFunc s_vkCmdDrawIndexedIndirectCount = nullptr;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P8-D: vkCmdDrawIndirectCount (same signature, same extension), for the wire arm's
|
||||
// glMultiDrawArraysIndirectCount. The monolith arm never loads it: it reads that count on
|
||||
// the CPU (DirectVulkan.cpp's MultiDrawArraysIndirectCount).
|
||||
static inline PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
|
||||
PFNDrawIndexedIndirectCountFunc s_vkCmdWireDrawIndirectCount = nullptr;
|
||||
#endif
|
||||
// VK_EXT_multi_draw entry points, loaded at device creation when the extension
|
||||
// (and its multiDraw feature) is enabled; null otherwise.
|
||||
static inline PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
||||
static inline PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||
PFN_vkCmdDrawMultiEXT s_vkCmdDrawMultiEXT = nullptr;
|
||||
PFN_vkCmdDrawMultiIndexedEXT s_vkCmdDrawMultiIndexedEXT = nullptr;
|
||||
|
||||
// VK_EXT_transform_feedback (GL transform feedback capture)
|
||||
Bool m_transformFeedbackFeatureEnabled = false;
|
||||
@@ -976,9 +1190,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// VK_EXT_vertex_attribute_divisor: without it every non-zero glVertexAttribDivisor
|
||||
// behaves as 1, because that is all Vulkan's instance input rate can express.
|
||||
Bool m_vertexAttributeDivisorEnabled = false;
|
||||
static inline PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
static inline PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
static inline PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
PFN_vkCmdBindTransformFeedbackBuffersEXT s_vkCmdBindTransformFeedbackBuffersEXT = nullptr;
|
||||
PFN_vkCmdBeginTransformFeedbackEXT s_vkCmdBeginTransformFeedbackEXT = nullptr;
|
||||
PFN_vkCmdEndTransformFeedbackEXT s_vkCmdEndTransformFeedbackEXT = nullptr;
|
||||
// Counter buffers (one 4-byte slot per capture binding) let consecutive
|
||||
// draws within one glBeginTransformFeedback append GL-style. Transform feedback
|
||||
// objects can each hold an open, paused span at the same time, so the counters are
|
||||
@@ -1824,7 +2038,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult SetupDebugReportCallback();
|
||||
void DestroyDebugReportCallback();
|
||||
VkDebugUtilsMessengerCreateInfoEXT PopulateDebugMessengerCreateInfo();
|
||||
void CreateSurface();
|
||||
// False (logged) when the target's surface cannot be built; the renderer then has no surface.
|
||||
Bool CreateSurface();
|
||||
void PickPhysicalDevice();
|
||||
void CreateLogicalDeviceAndQueues();
|
||||
void CreateAllocator();
|
||||
|
||||
@@ -378,6 +378,7 @@
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
EndActiveRenderPassOn(frame.commandBuffer);
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
|
||||
@@ -126,3 +126,11 @@ MGL_WIRE_DECLINE(MipmapDepthStencilAspect)
|
||||
// is missing, or the three group-count words lie outside it or are misaligned.
|
||||
MGL_WIRE_DECLINE(DispatchIndirectBufferUnbound)
|
||||
MGL_WIRE_DECLINE(DispatchIndirectRange)
|
||||
|
||||
// ---- ProgramFactory::ScreenModuleForDriver ------------------------------------------------
|
||||
// One of the bound program's final modules fails SPIR-V validation, so no shader module of it was
|
||||
// created (VkProgramObject::invalidSpirvDeclined): SetupWireDraw refuses the draw, and the two
|
||||
// dispatch arms the dispatch. A driver handed the module could fault the GPU into device loss.
|
||||
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 builds the module anyway, and then neither row moves.
|
||||
MGL_WIRE_DECLINE(ProgramInvalidSpirv)
|
||||
MGL_WIRE_DECLINE(ComputeProgramInvalidSpirv)
|
||||
|
||||
@@ -299,6 +299,7 @@
|
||||
begin.renderPass = target->renderPass;
|
||||
begin.framebuffer = framebuffer;
|
||||
begin.renderArea = {{0, 0}, destination.extent};
|
||||
EndActiveRenderPassOn(frame.commandBuffer);
|
||||
vkCmdBeginRenderPass(frame.commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, target->pipeline);
|
||||
const VkViewport viewport{0.0f, 0.0f, static_cast<float>(destination.extent.width),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// PrimitivesGeneratedNoXfbProbe.cpp): one throwaway command buffer, a bounded fence wait that
|
||||
// leaks rather than idle-waits a hung GPU, teardown on every other path.
|
||||
|
||||
#include "RenderPassGuard.h"
|
||||
#include "WireDepthResolveProbe.h"
|
||||
|
||||
#include <MG_Util/Debug/Log.h>
|
||||
@@ -237,6 +238,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The RECTANGLE is the render area, exactly as the render-pass arm's renderArea is - the
|
||||
// scratch outside it is never read.
|
||||
begin.renderArea = rect;
|
||||
EndActiveRenderPassOn(commandBuffer);
|
||||
vkCmdBeginRenderPass(commandBuffer, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
// THE VIEWPORT IS THE WHOLE ATTACHMENT AND THE SCISSOR IS THE RECT, and the two are not
|
||||
@@ -746,6 +748,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
begin.renderPass = clear.pass;
|
||||
begin.framebuffer = clear.framebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
EndActiveRenderPassOn(cmd);
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
VkClearAttachment attachment{};
|
||||
attachment.aspectMask = clear.aspects;
|
||||
@@ -772,6 +775,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
begin.renderPass = item.resolvePass;
|
||||
begin.framebuffer = item.resolveFramebuffer;
|
||||
begin.renderArea = fullRect;
|
||||
EndActiveRenderPassOn(cmd);
|
||||
vkCmdBeginRenderPass(cmd, &begin, VK_SUBPASS_CONTENTS_INLINE);
|
||||
vkCmdEndRenderPass(cmd);
|
||||
}
|
||||
|
||||
@@ -215,7 +215,10 @@
|
||||
MakeXfbWritesVisible();
|
||||
const auto* fbo = state.DrawFramebuffer();
|
||||
if (!fbo) MagmaWireFatal("draw-framebuffer-record");
|
||||
RetireWireDrawPass();
|
||||
// THE PREVIOUS DRAW'S RENDER PASS STAYS OPEN until this draw's attachments are known: a
|
||||
// draw to the same attachments continues it (below), and on a tiler a pass per draw also
|
||||
// loaded and stored every attachment once per draw. Resource work in between that may not
|
||||
// sit inside a pass (a barrier, a copy, a clear) closes it itself (RenderPassGuard.h).
|
||||
// CPU preparation may synchronize GPU-produced vertex/index bytes. Do
|
||||
// not rewind an arena while this draw owns as-yet-unrecorded slices.
|
||||
WirePreparationScope preparing(m_wirePreparationDepth);
|
||||
@@ -230,6 +233,10 @@
|
||||
flags |= ProgramFactory::CompileOptionBit::ZeroBaseVertex;
|
||||
programObject = &m_programFactory->GetOrCreateProgram(program, flags);
|
||||
}
|
||||
if (programObject->invalidSpirvDeclined) {
|
||||
MGL_WIRE_DECLINE_AT(ProgramInvalidSpirv, "a module of the program fails SPIR-V validation and was never built");
|
||||
return false;
|
||||
}
|
||||
if (programObject->pointSizeCapabilityUnsupported) {
|
||||
MGL_WIRE_DECLINE_AT(PointSizeCapability, "the program writes gl_PointSize and the device cannot express it");
|
||||
return false;
|
||||
@@ -240,6 +247,9 @@
|
||||
MGL_WIRE_DECLINE_AT(TextureResourcesUnprepared, "a sampled/storage image could not be made ready; see the Tex* tally");
|
||||
return false;
|
||||
}
|
||||
// Shared images another session writes: acquired ahead of the recording that samples them
|
||||
// (WireSharedImage.inc).
|
||||
AcquireNotedSharedImages();
|
||||
// Lazy image uploads/promotions can submit the old recording. Resolve
|
||||
// ALL attachments before descriptor binding captures the current cmd.
|
||||
for (Uint32 i = 0; i < MG_Pipe::kMGPipeMaxColorAttachments; ++i) {
|
||||
@@ -306,8 +316,20 @@
|
||||
"MGPipe: Fatal{ProtocolCorruption, \"Magma:client-vertex-array\"} location=%u: an "
|
||||
"attribute the program reads carries no buffer handle", location);
|
||||
BufferSlice slice{};
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice))
|
||||
MagmaWireFatal("vertex-buffer-resource");
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Vertex, resource.Res, slice)) {
|
||||
// A buffer with no data store is legal GL to bind (glGenBuffers + glBindBuffer, no
|
||||
// glBufferData), and the client mints its handle at construction while the create
|
||||
// only crosses with the first respecify (PipeFill.cpp's M-1) - so the handle may be
|
||||
// one this server never saw, or one it holds with zero bytes. Either way the draw
|
||||
// reads no storage: it is declined, as the GL draws nothing defined from it.
|
||||
static Bool s_described = false;
|
||||
if (!s_described && !m_bufferManager.IsKnownWireBuffer(resource.Res)) {
|
||||
s_described = true;
|
||||
m_bufferManager.DescribeWireBufferHistory(resource.Res);
|
||||
}
|
||||
MGL_WIRE_DECLINE_AT(VertexAttributeWindow, "an enabled attribute's buffer has no storage");
|
||||
return false;
|
||||
}
|
||||
const VkDeviceSize offset = input.bindingBaseOffsets[binding];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
const SizeT elementSize = VertexInputStateFactory::GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
@@ -387,8 +409,16 @@
|
||||
const void* bytes = nullptr;
|
||||
if (cpuIndices) bytes = reinterpret_cast<const void*>(indices->indexByteOffset);
|
||||
else {
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice))
|
||||
MagmaWireFatal("index-buffer-resource");
|
||||
if (!m_bufferManager.AcquireWireSlice(BufferKind::Index, state.IndexBuffer.Res, indexSlice)) {
|
||||
// As for a vertex buffer: a bound element buffer with no data store yet.
|
||||
static Bool s_described = false;
|
||||
if (!s_described && !m_bufferManager.IsKnownWireBuffer(state.IndexBuffer.Res)) {
|
||||
s_described = true;
|
||||
m_bufferManager.DescribeWireBufferHistory(state.IndexBuffer.Res);
|
||||
}
|
||||
MGL_WIRE_DECLINE_AT(IndexWindow, "the element buffer has no storage");
|
||||
return false;
|
||||
}
|
||||
const Uint64 offset = state.IndexBuffer.Offset + indices->indexByteOffset;
|
||||
if (offset < state.IndexBuffer.Offset || offset > indexSlice.size ||
|
||||
indices->indexByteSize > indexSlice.size - offset) {
|
||||
@@ -463,6 +493,7 @@
|
||||
// attachments below resolve the same index, so this key has to be taken after it.
|
||||
passKey.swapchainImageIndex = DefaultFramebufferWriteIndex(true);
|
||||
}
|
||||
Vector<WireImage> sameLayoutAttachments;
|
||||
const auto add = [&](const MG_Pipe::MGPSurface& surface, VkImageAspectFlags aspect) -> Uint32 {
|
||||
WireImage image = ResolveWireImage(*fbo, surface, aspect, /*isWriteTarget=*/true);
|
||||
if (!image.image) return VK_ATTACHMENT_UNUSED;
|
||||
@@ -472,7 +503,10 @@
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::FramebufferSrgb));
|
||||
const VkImageLayout layout = aspect == VK_IMAGE_ASPECT_COLOR_BIT ?
|
||||
VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_GENERAL;
|
||||
TransitionWireImage(image, layout);
|
||||
// Already in its attachment layout: its barrier (which closes any open pass) waits until
|
||||
// it is known whether the open pass continues with this very attachment.
|
||||
if (image.layout == layout) sameLayoutAttachments.push_back(image);
|
||||
else TransitionWireImage(image, layout);
|
||||
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
|
||||
info.image = image.image;
|
||||
info.viewType = image.viewType;
|
||||
@@ -538,7 +572,17 @@
|
||||
passInfo.pAttachments = attachments.data();
|
||||
passInfo.subpassCount = 1;
|
||||
passInfo.pSubpasses = &subpass;
|
||||
if (!AcquireCachedWireDrawPass(passKey)) {
|
||||
const Bool continuing = m_wireDrawPass && m_wireDrawPassKey == passKey;
|
||||
{
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
const Bool stillOpen = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer;
|
||||
// A new pass orders itself after earlier writes to its attachments; a continued one
|
||||
// is the same pass, whose attachment writes are ordered by rasterization.
|
||||
if (!stillOpen)
|
||||
for (auto& image : sameLayoutAttachments) TransitionWireImage(image, VK_IMAGE_LAYOUT_GENERAL);
|
||||
}
|
||||
if (!continuing) RetireWireDrawPass();
|
||||
if (!continuing && !AcquireCachedWireDrawPass(passKey)) {
|
||||
m_wireDrawPassKey = Move(passKey);
|
||||
Vector<VkImageView> views;
|
||||
views.reserve(viewInfos.size());
|
||||
@@ -602,13 +646,20 @@
|
||||
vkCmdBindVertexBuffers(frame.commandBuffer, binding++, 1, &slice.buffer, &slice.offset);
|
||||
}
|
||||
if (indexSlice.IsValid()) vkCmdBindIndexBuffer(frame.commandBuffer, indexSlice.buffer, indexSlice.offset, indexType);
|
||||
if (!VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) {
|
||||
// Still open from the previous draw (nothing closed it, same command buffer): continue.
|
||||
const auto* active = VkRenderPassManager::GetActiveRenderPass();
|
||||
const Bool open = continuing && active != nullptr && active->commandBuffer == frame.commandBuffer &&
|
||||
active->hash == pass.hash;
|
||||
if (!open && active != nullptr) VkRenderPassManager::EndRenderPass(frame.commandBuffer);
|
||||
if (!open && !VkRenderPassManager::BeginRenderPass(frame.commandBuffer, pass)) {
|
||||
MGL_WIRE_DECLINE_AT(BeginRenderPass, "the render pass for this draw could not be begun");
|
||||
return false;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
ApplyDynamicDrawStateTail(frame, pass.extent, fbo->IsDefault,
|
||||
ResolveDrawViewportCount(programObj.writesViewportIndexBuiltin));
|
||||
// Storage images this draw writes are what the next sampled-image barrier must cover.
|
||||
if (programObj.hasStorageImages) BumpWireImageWriteEpoch();
|
||||
if (MG_Util::PipeStats::Enabled()) MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::Draws, 1);
|
||||
return true;
|
||||
}
|
||||
@@ -752,10 +803,15 @@
|
||||
return;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
|
||||
if (programObj.invalidSpirvDeclined) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
|
||||
return;
|
||||
}
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
|
||||
return;
|
||||
}
|
||||
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.inc)
|
||||
const auto pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (!pipeline) {
|
||||
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
|
||||
@@ -768,6 +824,7 @@
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipeline(frame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
EndActiveRenderPassOn(frame.commandBuffer);
|
||||
vkCmdDispatch(frame.commandBuffer, x, y, z);
|
||||
}
|
||||
|
||||
@@ -823,10 +880,15 @@
|
||||
return;
|
||||
}
|
||||
const auto& programObj = m_programFactory->GetOrCreateProgram(program, {});
|
||||
if (programObj.invalidSpirvDeclined) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeProgramInvalidSpirv, "a module of the compute program fails SPIR-V validation and was never built");
|
||||
return;
|
||||
}
|
||||
if (!m_uniformManager->PrepareWireTextureResources(program, programObj)) {
|
||||
MGL_WIRE_DECLINE_AT(ComputeTextureResourcesUnprepared, "a sampled/storage image could not be made ready for the dispatch");
|
||||
return;
|
||||
}
|
||||
AcquireNotedSharedImages(); // shared images another session writes (WireSharedImage.inc)
|
||||
const auto pipeline = GetOrCreateComputePipeline(programObj);
|
||||
if (!pipeline) {
|
||||
MGL_WIRE_DECLINE_AT(ComputePipelineUnavailable, "no compute pipeline could be created for this dispatch");
|
||||
@@ -841,6 +903,7 @@
|
||||
return;
|
||||
}
|
||||
vkCmdBindPipeline(dispatchFrame.commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, pipeline);
|
||||
EndActiveRenderPassOn(dispatchFrame.commandBuffer);
|
||||
vkCmdDispatchIndirect(dispatchFrame.commandBuffer, parameters.buffer,
|
||||
parameters.offset + static_cast<VkDeviceSize>(offset));
|
||||
if (MG_Util::PipeStats::Enabled())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user