mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3025284a6e |
@@ -1,3 +0,0 @@
|
||||
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
|
||||
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
|
||||
* text=auto eol=lf
|
||||
@@ -1,176 +0,0 @@
|
||||
---
|
||||
name: anland-mobilegl-plasma
|
||||
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
|
||||
---
|
||||
|
||||
# MobileGL on anland KDE Plasma
|
||||
|
||||
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
|
||||
Human-readable runbook (Chinese, same content): `docs/Disaggregated/notes/anland/runbook-plasma.md`.
|
||||
Background: `docs/Disaggregated/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
|
||||
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
|
||||
parameters are documented at the top of each script and in `scripts/env.sh`.
|
||||
|
||||
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
|
||||
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
|
||||
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
|
||||
|
||||
## Architecture (10 lines)
|
||||
|
||||
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
|
||||
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl`. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process.
|
||||
3. Backend = `setprop debug.mobilegl.backend DirectGLES|DirectVulkan` (Espryt = DirectGLES, Magma = DirectVulkan; prop lost on reboot) and must equal the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend").
|
||||
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker; NOT started at boot - the module only starts the original one on `/data/local/tmp/display_daemon.sock`, 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) has drop-in `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which 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. 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 apps (Xwayland) use the MobileGL GLX vendor with per-frame readback + `xcb_put_image`. Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in-process (`mobilegl-startup.sh chrome`).
|
||||
|
||||
## 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`; 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`.
|
||||
|
||||
## Bring-up after a phone reboot
|
||||
|
||||
Nothing of the session survives a reboot. Either `adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh [DirectGLES|DirectVulkan]"'`, or by hand, in this order:
|
||||
|
||||
```sh
|
||||
adb connect <ip>:<port> # only for wireless debugging (e.g. USB cannot power it)
|
||||
export MSYS_NO_PATHCONV=1
|
||||
adb shell 'su -c "svc power stayon true"'
|
||||
adb shell 'su -c "setprop debug.mobilegl.backend DirectVulkan"' # or DirectGLES; match /etc/mobilegl/backend
|
||||
adb shell 'su -c "am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity --es socket_path /data/local/tmp/anland-mobilegl/display.sock"'
|
||||
adb shell 'su -c "/data/local/Droidspaces/bin/droidspaces -C /data/local/Droidspaces/Containers/arch-kde-mgl/container.config start"'
|
||||
adb shell 'su -c "sh /data/local/tmp/anl/restart-daemon.sh"' # experiment display daemon, chmod 777 dir / 666 socket
|
||||
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"' # restarts app + desktop-session, ~90 s
|
||||
```
|
||||
|
||||
`droidspaces` has no `status` command; `droidspaces show` lists running containers. Do not use the old
|
||||
`/data/local/tmp/restart-daemon.sh` (it kills a hard-coded PID). Check with `device/status.sh`.
|
||||
|
||||
## 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
|
||||
|
||||
`adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'` (sets the prop, writes `/etc/mobilegl/backend`, restarts the app), then `run-plasma.sh` - the running session's clients are pinned to the old backend. `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 (`switch.sh` writes it); 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 Xwayland (KWin's patch sets it) keeps the software stack. 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
|
||||
|
||||
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`).
|
||||
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
|
||||
|
||||
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` |
|
||||
|
||||
`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 "正在使用的软件渲染器"). Delete before any verdict: `sudo -u swung0x48 kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete` (cyc.sh does it), restart the session.
|
||||
- **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 not auto-started** after reboot: KWin logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session exits in ~3 s.
|
||||
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
|
||||
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
|
||||
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
|
||||
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
|
||||
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
|
||||
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
|
||||
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
|
||||
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
|
||||
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
|
||||
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
|
||||
|
||||
## House rules
|
||||
|
||||
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
|
||||
- Commit with `git commit --no-verify`, one short sentence, **no** `Co-Authored-By` trailer.
|
||||
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
|
||||
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
|
||||
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
|
||||
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
|
||||
@@ -1,170 +0,0 @@
|
||||
# Reproducing the MobileGL + anland + KDE Plasma setup from source
|
||||
|
||||
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
|
||||
day-to-day loop once everything below exists. Paths and names that are specific to the original
|
||||
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
|
||||
|
||||
## 0. What you need
|
||||
|
||||
**Phone.**
|
||||
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
|
||||
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
|
||||
- About 20 GB free for the container image.
|
||||
|
||||
**Development host.**
|
||||
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
|
||||
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
|
||||
- Tools:
|
||||
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
|
||||
- Android NDK **27.2.12479018**.
|
||||
- JDK 17 and Android SDK platform 36 (for the APK).
|
||||
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
|
||||
|
||||
## 1. Sources
|
||||
|
||||
```sh
|
||||
# MobileGL: branch feat/disaggregated (everything here is on it)
|
||||
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
|
||||
git checkout feat/disaggregated
|
||||
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
|
||||
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
|
||||
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
|
||||
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
|
||||
|
||||
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
|
||||
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
|
||||
git checkout legacy-mobilegl-unified
|
||||
```
|
||||
|
||||
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
|
||||
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
|
||||
|
||||
## 2. Phone base: root, Droidspaces, display daemon
|
||||
|
||||
1. **Root** with KernelSU (or another `su` provider).
|
||||
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
|
||||
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
|
||||
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
|
||||
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
|
||||
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
|
||||
|
||||
## 3. The container
|
||||
|
||||
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
|
||||
|
||||
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
|
||||
```
|
||||
enable_gpu_mode=1
|
||||
net_mode=host
|
||||
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
|
||||
run_at_boot=0
|
||||
```
|
||||
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
|
||||
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
|
||||
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
|
||||
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
|
||||
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
|
||||
|
||||
## 4. MobileGL server and the anland APK
|
||||
|
||||
```sh
|
||||
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
|
||||
```
|
||||
The script runs, with the NDK toolchain:
|
||||
```
|
||||
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-26 -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
|
||||
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
|
||||
|
||||
Until the app-driven auto-start lands, the order matters:
|
||||
1. **Backend:** `setprop debug.mobilegl.backend DirectVulkan`; it must match `/etc/mobilegl/backend`.
|
||||
2. **App:** start it with `--es socket_path /data/local/tmp/anland-mobilegl/display.sock`.
|
||||
3. **Container:** start it.
|
||||
4. **Display daemon:** start the MobileGL daemon instance.
|
||||
5. **Plasma:** `run-plasma.sh`.
|
||||
|
||||
`scripts/device/bringup.sh [DirectVulkan|DirectGLES]` does all five. See SKILL.md "Bring-up after a phone reboot".
|
||||
|
||||
## 8. Verify
|
||||
|
||||
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
|
||||
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
|
||||
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
|
||||
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
|
||||
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
|
||||
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
|
||||
|
||||
## 9. Machine-specific values to change
|
||||
|
||||
| Value | Original | Where |
|
||||
|---|---|---|
|
||||
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
|
||||
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
|
||||
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
|
||||
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
|
||||
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
|
||||
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
|
||||
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
|
||||
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
|
||||
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
|
||||
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
|
||||
|
||||
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
|
||||
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
|
||||
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
|
||||
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
|
||||
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
|
||||
# Log level is compiled to INFO.
|
||||
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-26 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
|
||||
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
|
||||
-DMOBILEGL_BUILD_STAMP="$STAMP"
|
||||
else
|
||||
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
|
||||
fi
|
||||
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
|
||||
ls -la "$B/libMobileGL.so"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
|
||||
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL, in-process GPU), 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).
|
||||
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)
|
||||
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 bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
|
||||
sleep ${WAIT:-20}
|
||||
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
|
||||
IN
|
||||
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
|
||||
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
|
||||
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
|
||||
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
|
||||
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
|
||||
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
|
||||
#
|
||||
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
|
||||
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
|
||||
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
|
||||
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
|
||||
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
|
||||
set -e
|
||||
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
|
||||
P=/root/kwsync/Arch_v5/kwin.patch
|
||||
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
|
||||
cd /root/kp && patch -p1 -s < $P
|
||||
LIVE=/root/mobilegl-build/kwin-6.7.4
|
||||
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
|
||||
for f in $FILES; do
|
||||
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
|
||||
done
|
||||
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
|
||||
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
|
||||
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
|
||||
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
|
||||
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
|
||||
done
|
||||
echo PREP_DONE
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
|
||||
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
|
||||
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
|
||||
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
|
||||
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
|
||||
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
|
||||
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
|
||||
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
|
||||
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
|
||||
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
|
||||
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
|
||||
S=$1; shift
|
||||
DU=${DESKTOP_USER:-swung0x48}
|
||||
U=$(id -u "$DU")
|
||||
cd /tmp
|
||||
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
|
||||
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
|
||||
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
|
||||
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
|
||||
bash -lc 'exec "$@"' mgrun "$@"
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
|
||||
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
|
||||
#
|
||||
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
|
||||
# bash ct.sh mychan ./something.cmd
|
||||
#
|
||||
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
|
||||
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
|
||||
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
|
||||
#
|
||||
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
|
||||
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
|
||||
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
|
||||
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
|
||||
# script on its command line and kills itself. Use `pkill -x <name>`.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
ch=${1:?usage: ct.sh <channel> [script-file]}
|
||||
src=${2:--}
|
||||
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
|
||||
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
|
||||
adb_ shell "mkdir -p $DEV_TOOLS"
|
||||
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
|
||||
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
|
||||
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
|
||||
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
|
||||
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
|
||||
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
|
||||
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
shot=${1:?usage: cyc.sh <shot-name>}
|
||||
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
|
||||
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
|
||||
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
|
||||
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
|
||||
adb_ logcat -c
|
||||
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
|
||||
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
|
||||
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
|
||||
@@ -1,29 +0,0 @@
|
||||
# bringup.sh [DirectGLES|DirectVulkan] - (Android root shell) bring the stack up after a phone reboot:
|
||||
# stay-awake, container, backend property, experiment display daemon, anland app, then the Plasma
|
||||
# session (run-plasma.sh, ~90 s; skip with NO_PLASMA=1). The backend defaults to the container's
|
||||
# /etc/mobilegl/backend (persists across reboots; the property does not).
|
||||
# Assembled from the documented manual steps; not exercised as one unit yet. Re-running restarts the
|
||||
# daemon, the app and the session. Run: su -c "sh /data/local/tmp/anl/bringup.sh"
|
||||
# Env: PKG, CONTAINER, DS, SOCK, TOOLS (dir holding run-plasma.sh / restart-daemon.sh; default: this dir).
|
||||
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}
|
||||
T=${TOOLS:-$(dirname $0)}
|
||||
svc power stayon true
|
||||
if $D show 2>/dev/null | grep -q " $C "; then
|
||||
echo "container $C already running"
|
||||
else
|
||||
$D -C /data/local/Droidspaces/Containers/$C/container.config start
|
||||
sleep 5
|
||||
fi
|
||||
B=${1:-$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null)}
|
||||
B=${B:-DirectGLES}
|
||||
setprop debug.mobilegl.backend $B
|
||||
echo "$B" > /mnt/Droidspaces/$C/etc/mobilegl/backend
|
||||
echo "backend: $(getprop debug.mobilegl.backend)"
|
||||
sh $T/restart-daemon.sh
|
||||
am start -n $P/com.anland.consumer.MainActivity --es socket_path $SOCK >/dev/null
|
||||
sleep 3
|
||||
if pidof $P:mobilegl >/dev/null; then echo "server process $P:mobilegl up"; else echo "WARNING: $P:mobilegl not running"; fi
|
||||
[ "${NO_PLASMA:-0}" = 1 ] || sh $T/run-plasma.sh
|
||||
@@ -1,20 +0,0 @@
|
||||
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
|
||||
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
|
||||
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
|
||||
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
|
||||
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
|
||||
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
|
||||
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
|
||||
DIR=$(dirname $SOCK)
|
||||
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
|
||||
sleep 1
|
||||
rm -f $SOCK
|
||||
mkdir -p $DIR
|
||||
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
|
||||
sleep 1
|
||||
chmod 777 $DIR
|
||||
chmod 666 $SOCK
|
||||
echo "daemon restarted:"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
|
||||
@@ -1,26 +0,0 @@
|
||||
# run-plasma.sh - (Android root shell) restart the anland app and the container's Plasma session
|
||||
# (desktop-session.service -> mobilegl-startup.sh plasma). Takes ~90 s (two 40 s waits: logind must
|
||||
# finish tearing the old user session down before the new one starts). Prints the unit status, the
|
||||
# plasma user units and recent coredumps. Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
|
||||
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), SOCK, 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}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
am force-stop $P
|
||||
am start -n $P/com.anland.consumer.MainActivity --es socket_path $SOCK >/dev/null
|
||||
sleep 3
|
||||
$D --name=$C run bash -lc "
|
||||
systemctl stop desktop-session.service 2>/dev/null
|
||||
loginctl terminate-user $U 2>/dev/null; sleep 40
|
||||
systemctl reset-failed desktop-session.service
|
||||
rm -f /tmp/mobilegl-compositor.log
|
||||
date +%s > /tmp/plasma-start
|
||||
systemctl start desktop-session.service
|
||||
sleep 40
|
||||
systemctl --no-pager status desktop-session.service | head -8
|
||||
UID_=\$(id -u $U)
|
||||
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
|
||||
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
|
||||
"
|
||||
@@ -1,12 +0,0 @@
|
||||
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
|
||||
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
|
||||
echo "prop debug.mobilegl.backend=$(getprop debug.mobilegl.backend) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null)"
|
||||
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
|
||||
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
|
||||
$D show
|
||||
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
|
||||
@@ -1,17 +0,0 @@
|
||||
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
|
||||
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
|
||||
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
|
||||
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
|
||||
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
|
||||
set -e
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
|
||||
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
|
||||
ls -laZ $D
|
||||
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
|
||||
am force-stop $P
|
||||
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
|
||||
OWN=$(stat -c %u:%g $D/libMobileGL.so)
|
||||
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
|
||||
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
|
||||
ls -laZ $D/libMobileGL.so
|
||||
@@ -1,15 +0,0 @@
|
||||
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) select the server backend: Espryt =
|
||||
# DirectGLES, Magma = DirectVulkan. Sets the system property the server reads (lost on reboot), writes
|
||||
# the container's /etc/mobilegl/backend (what every client and the KWin drop-in report; it must match or
|
||||
# the server refuses the client: "Hello.backendType disagrees with pinned backend"), restarts the app.
|
||||
# The running Plasma session still speaks the old backend: run run-plasma.sh afterwards.
|
||||
# Env: PKG, CONTAINER (arch-kde-mgl), SOCK (display daemon socket).
|
||||
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
|
||||
P=${PKG:-com.anland.consumer.mobilegl}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
|
||||
setprop debug.mobilegl.backend $B
|
||||
echo $B > /mnt/Droidspaces/$C/etc/mobilegl/backend
|
||||
am force-stop $P
|
||||
am start -n $P/com.anland.consumer.MainActivity --es socket_path $SOCK >/dev/null
|
||||
sleep 4
|
||||
@@ -1,8 +0,0 @@
|
||||
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
|
||||
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
|
||||
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
|
||||
# Env: DS, CONTAINER, DESKTOP_USER.
|
||||
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
|
||||
C=${CONTAINER:-arch-kde-mgl}
|
||||
U=${DESKTOP_USER:-swung0x48}
|
||||
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
|
||||
@@ -1,41 +0,0 @@
|
||||
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
|
||||
# Sourced, never run. Every value can be overridden from the environment before calling a script.
|
||||
#
|
||||
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
|
||||
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
|
||||
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
|
||||
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
|
||||
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
|
||||
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
|
||||
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
|
||||
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
|
||||
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
|
||||
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
|
||||
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
|
||||
# (USB serial or a wireless "ip:port").
|
||||
|
||||
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
if [ -z "${MGL_WORKTREE:-}" ]; then
|
||||
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
|
||||
fi
|
||||
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
|
||||
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
|
||||
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
|
||||
: "${MGL_CONTAINER:=arch-kde-mgl}"
|
||||
: "${MGL_DESKTOP_USER:=swung0x48}"
|
||||
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
|
||||
: "${DEV_TOOLS:=/data/local/tmp/anl}"
|
||||
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
|
||||
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
|
||||
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
|
||||
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
|
||||
|
||||
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
|
||||
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
|
||||
export MSYS_NO_PATHCONV=1
|
||||
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
|
||||
|
||||
# C:/x/y -> /mnt/c/x/y (for WSL).
|
||||
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
|
||||
|
||||
mkdir -p "$ANL_DIR"
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
|
||||
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
|
||||
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
|
||||
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
|
||||
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
|
||||
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
|
||||
#
|
||||
# Two staging places, because the two container scripts read different ones:
|
||||
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
|
||||
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
|
||||
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
|
||||
# sync-build-kwin.sh (bash then fails on $'\r').
|
||||
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
|
||||
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
|
||||
# CHANNEL (ct.sh channel, default kw).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
|
||||
K=$ANLAND_WORKTREE/producers/kde
|
||||
st=$ANL_DIR/kwstage
|
||||
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
|
||||
lf() { tr -d '\r' < "$1" > "$2"; }
|
||||
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
|
||||
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
|
||||
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
|
||||
lf "$f" "$st/kwin-sync/anland/$b"
|
||||
done
|
||||
for f in kwin.patch mobilegl-startup.sh; do
|
||||
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
|
||||
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
|
||||
done
|
||||
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
|
||||
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
|
||||
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
|
||||
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
|
||||
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
|
||||
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
|
||||
if [ "${1:-}" = build ]; then
|
||||
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
|
||||
bash /root/kwprep.sh || exit 1
|
||||
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
|
||||
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
|
||||
IN
|
||||
fi
|
||||
@@ -1,34 +0,0 @@
|
||||
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
|
||||
#include <dirent.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
char dir[64];
|
||||
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
|
||||
DIR* d = opendir(dir);
|
||||
if (!d) { perror("opendir"); return 1; }
|
||||
struct dirent* e;
|
||||
while ((e = readdir(d))) {
|
||||
char p[128], l[128] = {0};
|
||||
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
||||
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
|
||||
int fd = open(p, O_RDONLY);
|
||||
if (fd < 0) continue;
|
||||
struct stat st; fstat(fd, &st);
|
||||
if (st.st_size < (1 << 20)) { close(fd); continue; }
|
||||
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
|
||||
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
|
||||
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
|
||||
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
|
||||
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
|
||||
(unsigned long long)h, nz);
|
||||
munmap((void*)m, st.st_size); close(fd);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
|
||||
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
|
||||
* left half red and the right half WHITE; a black right half means the alpha was lost. */
|
||||
#include <EGL/egl.h>
|
||||
#include <GLES3/gl3.h>
|
||||
#include <GLES2/gl2ext.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#define P(T, n) T n = (T)eglGetProcAddress(#n)
|
||||
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
|
||||
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
|
||||
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
|
||||
int main(void) {
|
||||
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
|
||||
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
|
||||
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
|
||||
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
|
||||
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
|
||||
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
|
||||
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
|
||||
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
|
||||
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
|
||||
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
|
||||
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
|
||||
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
|
||||
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
|
||||
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
|
||||
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
|
||||
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
|
||||
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
|
||||
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
|
||||
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
|
||||
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
|
||||
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
|
||||
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
|
||||
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
|
||||
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
|
||||
GLint lm = glGetUniformLocation(pr, "mode");
|
||||
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
|
||||
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
|
||||
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
|
||||
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
|
||||
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
|
||||
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
|
||||
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
|
||||
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
|
||||
};
|
||||
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
while (glGetError()) {}
|
||||
if (cases[i].storage) {
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
} else {
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
|
||||
}
|
||||
GLenum err = glGetError();
|
||||
glUniform1i(lm, cases[i].mode);
|
||||
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
|
||||
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
|
||||
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
|
||||
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
|
||||
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
|
||||
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
|
||||
};
|
||||
glUniform1i(lm, 0);
|
||||
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
|
||||
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
|
||||
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
|
||||
GLenum err = glGetError();
|
||||
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
|
||||
unsigned char l[4], r[4];
|
||||
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
|
||||
int ok = 1;
|
||||
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
|
||||
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
|
||||
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
|
||||
# container/mgrun -> <container>/usr/local/bin/mgrun
|
||||
# container/kwprep.sh -> <container>/root/kwprep.sh
|
||||
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
|
||||
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
adb_ shell "mkdir -p $DEV_TOOLS"
|
||||
for f in "$here"/device/*.sh; do
|
||||
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
|
||||
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
|
||||
echo "pushed $DEV_TOOLS/$(basename "$f")"
|
||||
done
|
||||
for f in mgrun kwprep.sh; do
|
||||
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
|
||||
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
|
||||
done
|
||||
rm -f "$ANL_DIR/push.tmp"
|
||||
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
|
||||
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
|
||||
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
|
||||
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
|
||||
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
|
||||
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
|
||||
# `input mouse tap x y`. Env CHANNEL (default rec).
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
|
||||
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
|
||||
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
|
||||
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
|
||||
IN
|
||||
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
|
||||
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
|
||||
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
|
||||
import sys, os
|
||||
from PIL import Image
|
||||
d, out = sys.argv[1], sys.argv[2]
|
||||
fs = sorted(os.listdir(d)); n = len(fs)
|
||||
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
|
||||
ims = [Image.open(os.path.join(d, f)) for f in pick]
|
||||
w, h = ims[0].size; tw, th = w // 2, h // 2
|
||||
g = Image.new('RGB', (tw * 3, th * 4))
|
||||
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
|
||||
g.save(out); print(n, pick)
|
||||
P
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
|
||||
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
|
||||
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
|
||||
cmd=""; i=1
|
||||
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
|
||||
adb_ shell "su -c '$cmd'"
|
||||
i=1
|
||||
while [ "$i" -le "$n" ]; do
|
||||
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
|
||||
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
|
||||
done
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
|
||||
# worktree changed since the container's source stamp into the container's MobileGL source tree
|
||||
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
|
||||
# then build/install inside the container with anland's anland-build-client.sh
|
||||
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
|
||||
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
SRC=$CT_ROOT/root/mobilegl-unified-src
|
||||
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
|
||||
cd "$MGL_WORKTREE"
|
||||
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
|
||||
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
|
||||
echo "$(echo "$FILES" | wc -l) files since $STAMP"
|
||||
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
|
||||
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
|
||||
HEAD=$(git rev-parse HEAD)
|
||||
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
|
||||
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
|
||||
# (they need a live device + manual intervention); never run two ctest suites at once.
|
||||
B=${B:-$HOME/mgl-anl-bld}
|
||||
cd $B
|
||||
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
|
||||
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
|
||||
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
|
||||
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
|
||||
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
|
||||
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
|
||||
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
|
||||
set -e
|
||||
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
|
||||
B=${B:-$HOME/mgl-anl-bld}
|
||||
if [ ! -f $B/build.ninja ]; then
|
||||
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
|
||||
fi
|
||||
cmake --build $B --target "$@" -j16
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
|
||||
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
|
||||
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
|
||||
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
|
||||
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
|
||||
# MGL_JOBS (16), WSL_DISTRO (archlinux).
|
||||
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
|
||||
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/env.sh"
|
||||
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
|
||||
RECONF=0
|
||||
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
|
||||
EXTRA="$*"
|
||||
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
|
||||
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -euo pipefail
|
||||
OUT=${OUT:-$HOME/mgl-xbuild-a64}
|
||||
TC=$HOME/sysroots/aarch64-arch.cmake
|
||||
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
|
||||
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
|
||||
t0=$(date +%s)
|
||||
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
|
||||
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
|
||||
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
|
||||
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
|
||||
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
|
||||
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
|
||||
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
|
||||
fi
|
||||
t1=$(date +%s)
|
||||
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
|
||||
t2=$(date +%s)
|
||||
echo "stamp: $STAMP"
|
||||
echo "configure: $((t1-t0))s build: $((t2-t1))s"
|
||||
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
|
||||
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
|
||||
done
|
||||
IN
|
||||
exit "${PIPESTATUS[0]}"
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
|
||||
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
|
||||
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
|
||||
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
|
||||
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
|
||||
# keep the old inode; restart the client (or the session) to pick up the new library.
|
||||
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/env.sh"
|
||||
mkdir -p "$ANL_DIR/xdist"
|
||||
# 1. Copy the build outputs out of WSL.
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
|
||||
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
|
||||
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
|
||||
IN
|
||||
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
|
||||
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
|
||||
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
|
||||
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
|
||||
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
|
||||
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
|
||||
# 3. Install inside the container.
|
||||
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
|
||||
set -euo pipefail
|
||||
in=/root/xdeploy
|
||||
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
|
||||
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
|
||||
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
|
||||
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
|
||||
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
|
||||
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
|
||||
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
|
||||
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
|
||||
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
|
||||
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
|
||||
mkdir -p /usr/share/glvnd/egl_vendor.d
|
||||
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
|
||||
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
|
||||
# Client settings every process reads when its environment does not say otherwise (the backend
|
||||
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
|
||||
mkdir -p /etc/mobilegl
|
||||
cat >/etc/mobilegl/client.conf <<'CONF'
|
||||
# MobileGL client configuration, read by every process that loads libMobileGL.so.
|
||||
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
|
||||
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
|
||||
MOBILEGL_TRANSPORT=spawn
|
||||
MOBILEGL_IPC_DATA=shm
|
||||
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
|
||||
CONF
|
||||
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
|
||||
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
|
||||
mkdir -p /etc/environment.d /etc/profile.d
|
||||
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
|
||||
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
|
||||
mkdir -p /usr/lib/gbm
|
||||
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
|
||||
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
|
||||
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
|
||||
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
|
||||
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
|
||||
echo DEPLOYED
|
||||
IN
|
||||
@@ -1,47 +0,0 @@
|
||||
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
|
||||
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(_triple aarch64-unknown-linux-gnu)
|
||||
if (DEFINED ENV{MGL_SYSROOT})
|
||||
set(_sysroot "$ENV{MGL_SYSROOT}")
|
||||
else()
|
||||
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
|
||||
endif()
|
||||
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
|
||||
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
|
||||
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
|
||||
list(GET _gcc_dirs 0 _gcc_install_dir)
|
||||
|
||||
set(CMAKE_SYSROOT "${_sysroot}")
|
||||
set(CMAKE_C_COMPILER clang)
|
||||
set(CMAKE_CXX_COMPILER clang++)
|
||||
set(CMAKE_ASM_COMPILER clang)
|
||||
set(CMAKE_C_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
|
||||
set(CMAKE_AR llvm-ar)
|
||||
set(CMAKE_RANLIB llvm-ranlib)
|
||||
set(CMAKE_NM llvm-nm)
|
||||
set(CMAKE_OBJCOPY llvm-objcopy)
|
||||
set(CMAKE_STRIP llvm-strip)
|
||||
|
||||
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
|
||||
set(CMAKE_C_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
|
||||
set(CMAKE_LINKER_TYPE LLD)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
|
||||
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
|
||||
set(ENV{PKG_CONFIG_PATH} "")
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
|
||||
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
|
||||
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
|
||||
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
|
||||
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
|
||||
set -euo pipefail
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
. "$here/../env.sh"
|
||||
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
|
||||
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
|
||||
set -e; cd /
|
||||
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
|
||||
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
|
||||
echo SYSROOT_TAR_DONE
|
||||
IN
|
||||
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
|
||||
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
|
||||
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
|
||||
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
|
||||
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
|
||||
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
|
||||
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
|
||||
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
|
||||
IN
|
||||
rm -f "$ANL_DIR/xsysroot.tar.gz"
|
||||
@@ -1,10 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=trace-fixture-lib.sh
|
||||
. "${script_dir}/trace-fixture-lib.sh"
|
||||
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||
echo "usage: $0 <trace-case> [fixture-dir]" >&2
|
||||
exit 2
|
||||
@@ -66,6 +62,57 @@ if [ "${case_name}" = "OpenRA" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_file_from_mirror() {
|
||||
local file="$1"
|
||||
local url="$2"
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Cache-side helper for trace fixtures.
|
||||
#
|
||||
# key <case> [fixture-dir] derive the actions/cache key and path list
|
||||
# verify <case> [fixture-dir] check restored fixtures against their pointers
|
||||
# reset <case> [fixture-dir] drop restored fixtures, leaving the pointers
|
||||
#
|
||||
# The cache key is content-addressed on the Git LFS pointer oids tracked at
|
||||
# HEAD, which are readable from a plain checkout without smudging. Fixture
|
||||
# content therefore maps 1:1 onto a key: unchanged content hits, changed
|
||||
# content is a new key and thus a miss, and the download path handles it. The
|
||||
# key deliberately carries no restore-keys prefix in the workflow - a fixture
|
||||
# that does not match the pointer exactly must never be restored.
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=trace-fixture-lib.sh
|
||||
. "${script_dir}/trace-fixture-lib.sh"
|
||||
|
||||
# Bump when the key derivation changes in a way that must invalidate old
|
||||
# entries; the content digest alone would not notice a format change.
|
||||
key_schema="v1"
|
||||
|
||||
if [ "$#" -lt 2 ] || [ "$#" -gt 3 ]; then
|
||||
echo "usage: $0 <key|verify|reset> <trace-case> [fixture-dir]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
command_name="$1"
|
||||
case_name="$2"
|
||||
fixture_dir="${3:-tools/trace_replay/fixtures}"
|
||||
python_bin="${PYTHON:-python3}"
|
||||
|
||||
if ! command -v "${python_bin}" >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
|
||||
python_bin=python
|
||||
fi
|
||||
|
||||
mapfile -t files < <(trace_fixture_files "${case_name}" "${fixture_dir}" "${python_bin}")
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "no fixture files declared for trace case: ${case_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Writes "name=value" to $GITHUB_OUTPUT when running under Actions, and to
|
||||
# stdout otherwise so the script stays runnable (and testable) off-CI.
|
||||
emit_output() {
|
||||
local name="$1"
|
||||
local value="$2"
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
if [[ "${value}" == *$'\n'* ]]; then
|
||||
local delimiter="ghadelim_$(date +%s%N)_$$"
|
||||
{
|
||||
printf '%s<<%s\n' "${name}" "${delimiter}"
|
||||
printf '%s\n' "${value}"
|
||||
printf '%s\n' "${delimiter}"
|
||||
} >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
printf '%s=%s\n' "${name}" "${value}" >> "${GITHUB_OUTPUT}"
|
||||
fi
|
||||
fi
|
||||
printf '%s=%s\n' "${name}" "${value}"
|
||||
}
|
||||
|
||||
sanitize_case() {
|
||||
printf '%s' "$1" | sed 's/[^A-Za-z0-9._-]/_/g'
|
||||
}
|
||||
|
||||
case "${command_name}" in
|
||||
key)
|
||||
manifest=""
|
||||
for file in "${files[@]}"; do
|
||||
# A case whose fixtures are committed directly rather than through Git LFS
|
||||
# (OpenRA) has no pointer oid to key on, and nothing to download either.
|
||||
# Report it as uncacheable so the workflow skips the cache entirely.
|
||||
if ! metadata="$(get_lfs_metadata "${file}" 2>/dev/null)"; then
|
||||
echo "trace case ${case_name} is not stored in Git LFS; skipping fixture cache" >&2
|
||||
emit_output "cacheable" "false"
|
||||
emit_output "key" ""
|
||||
exit 0
|
||||
fi
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
manifest+="$(basename "${file}") ${expected_oid} ${expected_size}"$'\n'
|
||||
done
|
||||
|
||||
digest="$(printf '%s' "${manifest}" | sha256sum | awk '{ print substr($1, 1, 16) }')"
|
||||
safe_case="$(sanitize_case "${case_name}")"
|
||||
|
||||
emit_output "cacheable" "true"
|
||||
emit_output "key" "trace-fixture-${key_schema}-${safe_case}-${digest}"
|
||||
emit_output "paths" "$(printf '%s\n' "${files[@]}")"
|
||||
;;
|
||||
|
||||
verify)
|
||||
for file in "${files[@]}"; do
|
||||
metadata="$(get_lfs_metadata "${file}")"
|
||||
read -r expected_oid expected_size <<< "${metadata}"
|
||||
verify_fixture_file "${file}" "${file}" "${expected_oid}" "${expected_size}"
|
||||
done
|
||||
echo "Verified ${#files[@]} fixture file(s) for ${case_name} against the tracked Git LFS pointers."
|
||||
;;
|
||||
|
||||
reset)
|
||||
# Put the working tree back to the pointer files a fresh checkout would
|
||||
# have, so that a rejected cache entry falls through to exactly the same
|
||||
# download path a cache miss takes.
|
||||
for file in "${files[@]}"; do
|
||||
rm -f "${file}" "${file}.tmp"
|
||||
done
|
||||
git checkout -- "${files[@]}"
|
||||
echo "Reset ${#files[@]} fixture file(s) for ${case_name} to their tracked Git LFS pointers."
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "unknown command: ${command_name}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Shared helpers for trace-fixture handling: reading the in-tree Git LFS pointer
|
||||
# metadata and verifying a fixture file against it. Sourced by
|
||||
# fetch-trace-fixture-lfs.sh (verify after download) and by
|
||||
# trace-fixture-cache.sh (cache key derivation and verify after cache restore),
|
||||
# so both paths agree on what a valid fixture is.
|
||||
|
||||
# Reads the Git LFS pointer tracked at HEAD for a fixture path and prints
|
||||
# "<oid> <size>". Fails if the tracked blob is not a well-formed LFS pointer.
|
||||
get_lfs_metadata() {
|
||||
local file="$1"
|
||||
local pointer
|
||||
local expected_oid
|
||||
local expected_size
|
||||
|
||||
if ! pointer="$(git show "HEAD:${file}" 2>/dev/null)"; then
|
||||
echo "failed to read tracked fixture metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
if ! grep -q '^version https://git-lfs.github.com/spec/v1$' <<< "${pointer}"; then
|
||||
echo "tracked fixture is not a Git LFS pointer: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
expected_oid="$(awk '$1 == "oid" && $2 ~ /^sha256:/ { sub(/^sha256:/, "", $2); print $2 }' <<< "${pointer}")"
|
||||
expected_size="$(awk '$1 == "size" { print $2 }' <<< "${pointer}")"
|
||||
if ! [[ "${expected_oid}" =~ ^[0-9a-f]{64}$ ]] || ! [[ "${expected_size}" =~ ^[0-9]+$ ]]; then
|
||||
echo "invalid Git LFS pointer metadata: ${file}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s %s\n' "${expected_oid}" "${expected_size}"
|
||||
}
|
||||
|
||||
# Checks an on-disk fixture against the size and SHA-256 from its LFS pointer.
|
||||
verify_fixture_file() {
|
||||
local downloaded_file="$1"
|
||||
local display_name="$2"
|
||||
local expected_oid="$3"
|
||||
local expected_size="$4"
|
||||
local actual_oid
|
||||
local actual_size
|
||||
|
||||
if [ ! -f "${downloaded_file}" ]; then
|
||||
echo "fixture file is missing: ${display_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_size="$(wc -c < "${downloaded_file}" | tr -d '[:space:]')"
|
||||
if [ "${actual_size}" != "${expected_size}" ]; then
|
||||
echo "fixture size mismatch for ${display_name}: expected ${expected_size}, got ${actual_size}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
actual_oid="$(sha256sum "${downloaded_file}" | awk '{ print $1 }')"
|
||||
if [ "${actual_oid}" != "${expected_oid}" ]; then
|
||||
echo "fixture SHA-256 mismatch for ${display_name}: expected ${expected_oid}, got ${actual_oid}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Prints the fixture file paths of a trace case, one per line. Strips CR so the
|
||||
# result is usable when python emits CRLF (Git Bash on Windows).
|
||||
trace_fixture_files() {
|
||||
local case_name="$1"
|
||||
local fixture_dir="$2"
|
||||
local python_bin="${3:-python3}"
|
||||
|
||||
"${python_bin}" tools/trace_replay/trace_cases.py \
|
||||
--format fixture-files \
|
||||
--case "${case_name}" \
|
||||
--fixture-root "${fixture_dir}" | tr -d '\r'
|
||||
}
|
||||
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
|
||||
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
|
||||
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
|
||||
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
|
||||
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
|
||||
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
|
||||
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
|
||||
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
|
||||
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
|
||||
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
|
||||
|
||||
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
|
||||
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
|
||||
@@ -61,40 +61,15 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The render server is not trace-only: both flavors carry the control screen, the offscreen
|
||||
# service, the on-screen display activity, and (split native builds) the supervisor executable.
|
||||
# The plugin APK ships to users, so its build keeps the three components private (manifest
|
||||
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
|
||||
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
|
||||
require "$component" "$plugin_manifest" "plugin render server component ($component)"
|
||||
require "$component" "$trace_manifest" "trace render server component ($component)"
|
||||
done
|
||||
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
|
||||
trace_contents=$(unzip -Z1 "$trace_apk")
|
||||
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
|
||||
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
|
||||
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
|
||||
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
|
||||
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
|
||||
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
|
||||
exit 1
|
||||
fi
|
||||
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
|
||||
# entry icon and the bottom navigation bar switches to the render server screen. What is
|
||||
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
|
||||
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
|
||||
# reds here instead of shipping two icons.
|
||||
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
|
||||
if [[ "$main_count" -ne 1 ]]; then
|
||||
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
|
||||
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
|
||||
echo '::error::Trace APK must not expose a launcher activity' >&2
|
||||
exit 1
|
||||
fi
|
||||
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
|
||||
|
||||
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
|
||||
|
||||
+43
-273
@@ -6,30 +6,11 @@ on:
|
||||
- dev
|
||||
- Feat/Backend-Direct-GLES
|
||||
- Feat/Backend-Direct-Vulkan
|
||||
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
|
||||
# branch runs the full lane on every push so a phase's landing is not gated on
|
||||
# someone remembering to dispatch the workflow by hand.
|
||||
- feat/disaggregated
|
||||
- codex/fix-github-ci
|
||||
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
|
||||
# workflow keeps running for docs: it hosts the doc citation lint.
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
|
||||
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
env:
|
||||
CCACHE_BASEDIR: ${{ github.workspace }}
|
||||
CCACHE_COMPRESS: "true"
|
||||
@@ -37,7 +18,6 @@ jobs:
|
||||
CCACHE_MAXSIZE: 4G
|
||||
CCACHE_NOHASHDIR: "true"
|
||||
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -55,20 +35,18 @@ jobs:
|
||||
distribution: zulu
|
||||
java-version: '17'
|
||||
|
||||
- name: Android replay lifecycle regression
|
||||
run: python3 tools/trace_replay/test_android_lifecycle.py
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-version: 8.10.2
|
||||
|
||||
- name: Restore ccache
|
||||
uses: actions/cache/restore@v5
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-${{ github.run_id }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-${{ github.ref_name }}-
|
||||
${{ runner.os }}-apk-${{ github.job }}-ccache-
|
||||
|
||||
- name: Install ccache
|
||||
@@ -81,8 +59,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -97,13 +73,7 @@ jobs:
|
||||
run: python update_glslang_sources.py
|
||||
|
||||
- name: Build plugin APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -145,14 +115,7 @@ jobs:
|
||||
66fdc867e552192d553d59095ea2e3cef4829de65c356f1fd826027b1905972e
|
||||
|
||||
- name: Build retrace APK
|
||||
run: >-
|
||||
gradle --no-daemon -p android-plugin :app:assembleTraceRelease
|
||||
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all
|
||||
-Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
|
||||
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
|
||||
--parallel --max-workers "$(nproc)"
|
||||
run: gradle --no-daemon -p android-plugin :app:assembleTraceRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.abis=all -Pmobilegl.debuggableRelease=true -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
|
||||
env:
|
||||
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
@@ -162,28 +125,6 @@ jobs:
|
||||
if: always()
|
||||
run: ccache --show-stats
|
||||
|
||||
# Rewrite one rolling entry per job on the default branch. The upload stays
|
||||
# cumulative - it carries every object restored at the top of this run plus
|
||||
# the few TUs that actually changed - but Actions cache keys are immutable,
|
||||
# so the superseded blob has to be released before the same key can be
|
||||
# re-uploaded. Running after the build means a failed build leaves the
|
||||
# existing entry untouched. The other trigger branches restore this entry
|
||||
# rather than each writing a ~4 GB one of their own.
|
||||
- name: Release superseded ccache entry
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CACHE_KEY: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
||||
run: gh cache delete "${CACHE_KEY}" || true
|
||||
|
||||
- name: Save ccache
|
||||
if: github.ref_name == github.event.repository.default_branch
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: .ccache
|
||||
key: ${{ runner.os }}-apk-${{ github.job }}-ccache-v1
|
||||
|
||||
- name: Verify APK metadata and packaging
|
||||
run: |
|
||||
AAPT2="$(find "$ANDROID_HOME/build-tools" -name aapt2 -type f | sort -V | tail -n 1)"
|
||||
@@ -234,8 +175,6 @@ jobs:
|
||||
name: trace case matrix
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
outputs:
|
||||
android: ${{ steps.trace-cases.outputs.android }}
|
||||
names: ${{ steps.trace-cases.outputs.names }}
|
||||
@@ -246,12 +185,7 @@ jobs:
|
||||
- name: Load trace cases
|
||||
id: trace-cases
|
||||
run: |
|
||||
python3 tools/trace_replay/trace_cases.py --self-test-transport-matrices
|
||||
matrix_format=github-apk-matrix
|
||||
if [ "${MOBILEGL_CI_DISAGGREGATED}" = "ON" ]; then
|
||||
matrix_format=github-apk-transport-matrix
|
||||
fi
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format "${matrix_format}")" >> "$GITHUB_OUTPUT"
|
||||
echo "android=$(python3 tools/trace_replay/trace_cases.py --ci --format github-apk)" >> "$GITHUB_OUTPUT"
|
||||
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
trace-fixtures:
|
||||
@@ -267,41 +201,9 @@ jobs:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Derive trace fixture cache key
|
||||
id: fixture-key
|
||||
run: bash .github/scripts/trace-fixture-cache.sh key '${{ matrix.case }}'
|
||||
|
||||
- name: Restore trace fixture cache
|
||||
id: fixture-cache
|
||||
if: steps.fixture-key.outputs.cacheable == 'true'
|
||||
uses: actions/cache/restore@v5
|
||||
with:
|
||||
path: ${{ steps.fixture-key.outputs.paths }}
|
||||
key: ${{ steps.fixture-key.outputs.key }}
|
||||
|
||||
- name: Verify restored trace fixture
|
||||
id: fixture-verify
|
||||
if: steps.fixture-cache.outputs.cache-hit == 'true'
|
||||
run: |
|
||||
if bash .github/scripts/trace-fixture-cache.sh verify '${{ matrix.case }}'; then
|
||||
echo "ok=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ok=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning::Cached fixture for ${{ matrix.case }} failed verification; falling back to the download path"
|
||||
bash .github/scripts/trace-fixture-cache.sh reset '${{ matrix.case }}'
|
||||
fi
|
||||
|
||||
- name: Fetch trace fixture
|
||||
if: steps.fixture-verify.outputs.ok != 'true'
|
||||
run: bash .github/scripts/fetch-trace-fixture-lfs.sh '${{ matrix.case }}'
|
||||
|
||||
- name: Save trace fixture cache
|
||||
if: steps.fixture-key.outputs.cacheable == 'true' && steps.fixture-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v5
|
||||
with:
|
||||
path: ${{ steps.fixture-key.outputs.paths }}
|
||||
key: ${{ steps.fixture-key.outputs.key }}
|
||||
|
||||
- name: Stage trace fixture
|
||||
run: |
|
||||
safe_case="$(printf '%s' '${{ matrix.case }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
@@ -335,8 +237,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -364,7 +264,7 @@ jobs:
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
retrace:
|
||||
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
|
||||
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build
|
||||
@@ -374,8 +274,6 @@ jobs:
|
||||
if: ${{ always() && needs.build.result == 'success' && needs.android-avd.result == 'success' && needs.trace-cases.result == 'success' }}
|
||||
timeout-minutes: 75
|
||||
env:
|
||||
MOBILEGL_CI_TRANSPORT: ${{ matrix.transport || 'monolith' }}
|
||||
MOBILEGL_CI_LANE: ${{ matrix.lane || 'monolith-control' }}
|
||||
AVD_NAME: mobilegl-ci
|
||||
ANDROID_AVD_HOME: ${{ github.workspace }}/.android/avd
|
||||
ANDROID_HOME: ${{ github.workspace }}/.android/sdk
|
||||
@@ -383,9 +281,15 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 4
|
||||
matrix: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
matrix:
|
||||
backend:
|
||||
- name: DirectGLES
|
||||
gpu: software
|
||||
- name: DirectVulkan
|
||||
gpu: lavapipe
|
||||
case: ${{ fromJSON(needs.trace-cases.outputs.android) }}
|
||||
steps:
|
||||
- name: Set swap space
|
||||
- name: Set Swap Space
|
||||
uses: pierotofy/set-swap-space@v1.0
|
||||
with:
|
||||
swap-size-gb: 8
|
||||
@@ -414,8 +318,6 @@ jobs:
|
||||
uses: android-actions/setup-android@v4
|
||||
with:
|
||||
accept-android-sdk-licenses: false
|
||||
packages: platform-tools
|
||||
cmdline-tools-version: 13114758
|
||||
|
||||
- name: Accept Android SDK licenses
|
||||
run: yes | sdkmanager --licenses >/dev/null
|
||||
@@ -454,7 +356,7 @@ jobs:
|
||||
--profile pixel_6 \
|
||||
--avd-name "${AVD_NAME}"
|
||||
|
||||
- name: Launch emulator
|
||||
- name: Launch Emulator
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh start \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
@@ -465,49 +367,18 @@ jobs:
|
||||
|
||||
- name: Retrace and validate
|
||||
env:
|
||||
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
|
||||
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
|
||||
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
|
||||
run: |
|
||||
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
|
||||
test -f "${apk_file}"
|
||||
extra_retrace_args=()
|
||||
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--require-inproc
|
||||
)
|
||||
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
|
||||
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
|
||||
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
|
||||
# environment by the launcher and read by the client, which is where they belong.
|
||||
#
|
||||
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
|
||||
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
|
||||
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
|
||||
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
|
||||
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
|
||||
# into the CLIENT's process and SetWindowHandle is refused on the way across with
|
||||
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
|
||||
extra_retrace_args+=(
|
||||
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
|
||||
--use-pbuffer
|
||||
--require-spawn
|
||||
)
|
||||
else
|
||||
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
|
||||
fi
|
||||
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
|
||||
# min-filter downgrading on ANGLE llvmpipe.
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
|
||||
extra_retrace_args+=(--avoid-angle-llvmpipe-sampler-mipmap-min-filter)
|
||||
fi
|
||||
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.avoid_angle_llvmpipe_explicit_lod_bias || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--avoid-angle-llvmpipe-explicit-lod-bias)
|
||||
fi
|
||||
if [ "${{ matrix.case.coherent_as_flush || false }}" = "true" ]; then
|
||||
extra_retrace_args+=(--coherent-as-flush)
|
||||
fi
|
||||
@@ -540,24 +411,6 @@ jobs:
|
||||
run_retrace || retrace_status=$?
|
||||
if [ "${retrace_status}" -eq 75 ]; then
|
||||
echo "::warning::Android emulator infrastructure failed; restarting it and retrying this retrace once."
|
||||
# Surface-lost is retried rather than failed, so it would otherwise
|
||||
# be invisible. Report it per job - a healthy run prints nothing and
|
||||
# a rate spike shows up as a row per affected case.
|
||||
reason_file="android-retrace-result/infrastructure-failure-reason.txt"
|
||||
surface_lost_retries=0
|
||||
if [ -f "${reason_file}" ]; then
|
||||
surface_lost_retries="$(grep -c 'angle-surface-lost' "${reason_file}" || true)"
|
||||
fi
|
||||
if [ "${surface_lost_retries}" -gt 0 ]; then
|
||||
echo "surface-lost retries: ${surface_lost_retries} (${{ matrix.backend.name }}, ${{ matrix.case.name }})" \
|
||||
>> "${GITHUB_STEP_SUMMARY}"
|
||||
fi
|
||||
# The restart truncates EMULATOR_LOG, and the attempt that lost the
|
||||
# emulator is the one worth reading - the retry usually only shows
|
||||
# the wreckage. Keep the first attempt's log before it is clobbered.
|
||||
if [ -f "${EMULATOR_LOG}" ]; then
|
||||
cp "${EMULATOR_LOG}" "${EMULATOR_LOG}.first-attempt" || true
|
||||
fi
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
--avd-name "${AVD_NAME}" \
|
||||
--emulator-log "${EMULATOR_LOG}" \
|
||||
@@ -581,12 +434,6 @@ jobs:
|
||||
safe_case="$(printf '%s' '${{ matrix.case.name }}' | sed 's/[^A-Za-z0-9._-]/_/g')"
|
||||
result_dir="android-retrace-result/${safe_case}-${{ matrix.backend.name }}"
|
||||
mkdir -p "${result_dir}"
|
||||
python3 - "${result_dir}/lane.json" <<'PY'
|
||||
import json, os, sys
|
||||
from pathlib import Path
|
||||
Path(sys.argv[1]).write_text(json.dumps({"lane": os.environ["MOBILEGL_CI_LANE"],
|
||||
"transport": os.environ["MOBILEGL_CI_TRANSPORT"]}, indent=2))
|
||||
PY
|
||||
if [ -s "${{ matrix.case.golden }}" ]; then
|
||||
cp "${{ matrix.case.golden }}" "${result_dir}/${safe_case}-${{ matrix.backend.name }}-golden.png"
|
||||
fi
|
||||
@@ -603,15 +450,8 @@ jobs:
|
||||
if [ -f "${EMULATOR_LOG}" ]; then
|
||||
cp "${EMULATOR_LOG}" android-retrace-result/diagnostics/emulator.log
|
||||
fi
|
||||
if [ -f "${EMULATOR_LOG}.first-attempt" ]; then
|
||||
cp "${EMULATOR_LOG}.first-attempt" android-retrace-result/diagnostics/emulator-first-attempt.log
|
||||
fi
|
||||
# A vanished emulator looks identical whether the host OOM killer took
|
||||
# qemu or the renderer faulted. These two say which.
|
||||
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
|
||||
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
|
||||
|
||||
- name: Stop emulator
|
||||
- name: Stop Emulator
|
||||
if: always()
|
||||
run: |
|
||||
sh android-plugin/run-avd-ci.sh stop \
|
||||
@@ -623,7 +463,7 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
|
||||
path: android-retrace-result/**
|
||||
if-no-files-found: warn
|
||||
|
||||
@@ -632,8 +472,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: retrace
|
||||
if: always()
|
||||
env:
|
||||
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
@@ -648,73 +486,25 @@ jobs:
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Download Android monolith control results
|
||||
- name: Download Android retrace results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
|
||||
path: retrace-artifacts/monolith-control
|
||||
pattern: MobileGL-android-retrace-result-*
|
||||
path: retrace-artifacts
|
||||
|
||||
- name: Download Android inproc acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-*
|
||||
path: retrace-artifacts/inproc-acceptance
|
||||
|
||||
- name: Download Android spawn acceptance results
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
|
||||
path: retrace-artifacts/spawn-acceptance
|
||||
|
||||
- name: Render monolith control summary
|
||||
- name: Render retrace summary
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/monolith-control \
|
||||
--output-dir android-retrace-summary/monolith-control \
|
||||
--title "MobileGL Android monolith control" \
|
||||
--group-label "Android Emulator / monolith control" \
|
||||
--html mobilegl-android-monolith-control.html
|
||||
--input retrace-artifacts \
|
||||
--output-dir android-retrace-summary \
|
||||
--title "MobileGL Android retrace overview" \
|
||||
--group-label "Android Emulator" \
|
||||
--html mobilegl-android-retrace-overview.html
|
||||
|
||||
- name: Render inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs \
|
||||
--input retrace-artifacts/inproc-acceptance \
|
||||
--output-dir android-retrace-summary/inproc-acceptance \
|
||||
--title "MobileGL Android inproc acceptance" \
|
||||
--group-label "Android Emulator / inproc acceptance" \
|
||||
--html mobilegl-android-inproc-acceptance.html
|
||||
|
||||
- name: Render spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
run: |
|
||||
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
|
||||
|
||||
- name: Upload Android monolith control summary
|
||||
- name: Upload Android retrace summary
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-monolith-control-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android inproc acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload Android spawn acceptance summary
|
||||
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
|
||||
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
|
||||
path: android-retrace-summary/mobilegl-android-retrace-overview.html
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -730,61 +520,41 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Fail before deletion if either paginated inventory cannot be read.
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name' \
|
||||
> "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv' \
|
||||
> "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
declare -A failed_cases=()
|
||||
while IFS= read -r job_name; do
|
||||
case_name="${job_name#retrace (*, }"
|
||||
case_name="${case_name%)}"
|
||||
failed_cases["${case_name}"]=1
|
||||
done < "${RUNNER_TEMP}/cleanup-jobs.txt"
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?per_page=100" \
|
||||
--jq '.jobs[] | select(.name | startswith("retrace (")) | select(.conclusion == "failure" or .conclusion == "cancelled" or .conclusion == "timed_out" or .conclusion == "action_required") | .name'
|
||||
)
|
||||
|
||||
if ((${#failed_cases[@]})); then
|
||||
echo "Retaining fixtures and results for failed retrace case(s):"
|
||||
echo "Retaining fixtures for failed retrace case(s):"
|
||||
printf ' %s\n' "${!failed_cases[@]}"
|
||||
else
|
||||
echo "All retrace jobs succeeded; nothing needs to be retained."
|
||||
echo "All retrace jobs succeeded; no fixtures need to be retained."
|
||||
fi
|
||||
|
||||
deleted=0
|
||||
retained=0
|
||||
while IFS=$'\t' read -r artifact_id artifact_name; do
|
||||
keep=0
|
||||
if [[ "${artifact_name}" == MobileGL-trace-fixture-* ]]; then
|
||||
case_name="${artifact_name#MobileGL-trace-fixture-}"
|
||||
if [[ -v "failed_cases[${case_name}]" ]]; then
|
||||
keep=1
|
||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||
((retained += 1))
|
||||
continue
|
||||
fi
|
||||
elif [[ "${artifact_name}" == MobileGL-android-retrace-result-* ]]; then
|
||||
# The result artifact carries mobilegl.log, retrace.log, logcat,
|
||||
# the emulator log and the actual/diff images - the only record of
|
||||
# why a retrace failed. Its name ends in -<backend>-<case>, so a
|
||||
# suffix match on the case name keeps both backends' results for a
|
||||
# case that failed on either of them, which is what a comparison
|
||||
# needs. The match is anchored at the end, so a case name that is a
|
||||
# prefix of a longer one does not retain the longer one's results.
|
||||
for case_name in "${!failed_cases[@]}"; do
|
||||
if [[ "${artifact_name}" == *-"${case_name}" ]]; then
|
||||
keep=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ((keep)); then
|
||||
echo "Retaining ${artifact_name} (${artifact_id}) for failed retrace."
|
||||
((retained += 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Deleting ${artifact_name} (${artifact_id})"
|
||||
gh api --method DELETE "repos/${GITHUB_REPOSITORY}/actions/artifacts/${artifact_id}"
|
||||
((deleted += 1))
|
||||
done < "${RUNNER_TEMP}/cleanup-artifacts.txt"
|
||||
done < <(
|
||||
gh api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100" \
|
||||
--jq '.artifacts[] | select(.name | startswith("MobileGL-trace-fixture-") or startswith("MobileGL-android-retrace-result-") or startswith("trace-fixture-") or startswith("retrace-result-")) | [.id, .name] | @tsv'
|
||||
)
|
||||
|
||||
echo "Deleted ${deleted} intermediate Android artifact(s); retained ${retained} failed-retrace fixture(s)."
|
||||
|
||||
+44
-3210
File diff suppressed because it is too large
Load Diff
@@ -27,5 +27,3 @@ MobileGL/MG*/cmake-build*
|
||||
tools/trace_replay/work/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
/.gradle
|
||||
/.trace-work/
|
||||
|
||||
+6
-6
@@ -7,6 +7,9 @@
|
||||
[submodule "3rdparty/SPIRV-Cross"]
|
||||
path = 3rdparty/SPIRV-Cross
|
||||
url = https://github.com/KhronosGroup/SPIRV-Cross.git
|
||||
[submodule "include/FastSTL"]
|
||||
path = include/FastSTL
|
||||
url = https://github.com/MobileGL-Dev/FastSTL.git
|
||||
[submodule "3rdparty/tracy"]
|
||||
path = 3rdparty/tracy
|
||||
url = https://github.com/wolfpld/tracy.git
|
||||
@@ -31,9 +34,6 @@
|
||||
[submodule "3rdparty/asio"]
|
||||
path = 3rdparty/asio
|
||||
url = https://github.com/chriskohlhoff/asio.git
|
||||
[submodule "include/ska"]
|
||||
path = include/ska
|
||||
url = https://github.com/MobileGL-Dev/flat_hash_map.git
|
||||
[submodule "3rdparty/flatbuffers"]
|
||||
path = 3rdparty/flatbuffers
|
||||
url = https://github.com/google/flatbuffers.git
|
||||
[submodule "3rdparty/libfork"]
|
||||
path = 3rdparty/libfork
|
||||
url = https://github.com/ConorWilliams/libfork.git
|
||||
|
||||
Vendored
+1
-1
Submodule 3rdparty/apitrace updated: c8036190fc...10935bb5e4
Vendored
-1
Submodule 3rdparty/flatbuffers deleted from 7e163021e5
Vendored
+1
-1
Submodule 3rdparty/glslang updated: 2cc0c4541b...900b29d449
Vendored
-11
@@ -1,11 +0,0 @@
|
||||
# glvnd vendor ABI headers
|
||||
|
||||
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
|
||||
headers intact.
|
||||
|
||||
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
|
||||
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
|
||||
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
|
||||
smaller dependency than a build-time download of a source tree.
|
||||
|
||||
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#include <GL/gl.h>
|
||||
|
||||
#if !defined(__GL_DISPATCH_ABI_H)
|
||||
#define __GL_DISPATCH_ABI_H
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup gldispatchabi GL dispatching ABI
|
||||
*
|
||||
* This is not a complete ABI, but rather a fragment common to the libEGL and
|
||||
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
|
||||
* these client ABIs.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Thread-local implementation used by libglvnd. This is passed into the patch
|
||||
* function callback via the type parameter.
|
||||
*
|
||||
* For most architectures, the vendor library can ignore this parameter, since
|
||||
* it will always be the same value. It's used for systems like ARM, where the
|
||||
* stubs might be use the ARM or Thumb instruction sets.
|
||||
*
|
||||
* The stub type does not make any distinction between TLS and TSD stubs. The
|
||||
* entire purpose of entrypoint rewriting is to skip the dispatch table in
|
||||
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
|
||||
*/
|
||||
enum {
|
||||
/*!
|
||||
* Indicates that the stubs aren't defined in assembly. For example, if the
|
||||
* dispatch stubs are written in C. Vendor libraries generally won't see
|
||||
* this value.
|
||||
*/
|
||||
__GLDISPATCH_STUB_UNKNOWN,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x86-64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_X86_64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the Thumb instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_THUMB,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv7, using the normal ARM instruction set.
|
||||
*/
|
||||
__GLDISPATCH_STUB_ARMV7_ARM,
|
||||
|
||||
/*!
|
||||
* Used for stubs on ARMv8/aarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_AARCH64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
|
||||
*/
|
||||
__GLDISPATCH_STUB_X32,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64 systems.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
|
||||
*/
|
||||
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
|
||||
|
||||
/*!
|
||||
* Used for stubs on LOONGARCH/loongarch64.
|
||||
*/
|
||||
__GLDISPATCH_STUB_LOONGARCH64,
|
||||
};
|
||||
|
||||
/*!
|
||||
* A callback function called by the vendor library to fetch the address of an
|
||||
* entrypoint.
|
||||
*
|
||||
* The function returns two pointers, one writable and one executable. The two
|
||||
* pointers may or may not be the same virtual address, but they will both be
|
||||
* mappings of the same physical memory.
|
||||
*
|
||||
* The vendor library should write its entrypoint to the address returned by
|
||||
* \p writePtr, but should use the address from \p execPtr for things like
|
||||
* calculating PC-relative offsets.
|
||||
*
|
||||
* Note that if this function fails, then the vendor library can still try to
|
||||
* patch other entrypoints.
|
||||
*
|
||||
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
|
||||
* be zero, even if the stub uses the thumb instruction set. The vendor library
|
||||
* should use the \c type parameter of \c initiatePatch to determine which
|
||||
* instruction set to use.
|
||||
*
|
||||
* \param funcName The function name.
|
||||
* \param[out] writePtr The pointer that the vendor library can write to.
|
||||
* \param[out] execPtr The pointer to the executable code.
|
||||
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
|
||||
*/
|
||||
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
|
||||
void **writePtr, const void **execPtr);
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // __GL_DISPATCH_ABI_H
|
||||
-464
@@ -1,464 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2013, NVIDIA CORPORATION.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and/or associated documentation files (the
|
||||
* "Materials"), to deal in the Materials without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
* permit persons to whom the Materials are furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* unaltered in all copies or substantial portions of the Materials.
|
||||
* Any additions, deletions, or changes to the original source files
|
||||
* must be clearly indicated in accompanying documentation.
|
||||
*
|
||||
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
#if !defined(__LIB_EGL_ABI_H)
|
||||
#define __LIB_EGL_ABI_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <EGL/egl.h>
|
||||
#include <EGL/eglext.h>
|
||||
|
||||
#include "glvnd/GLdispatchABI.h"
|
||||
|
||||
#if defined(__cplusplus)
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* \defgroup eglvendorabi EGL Vendor ABI
|
||||
*
|
||||
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
|
||||
*
|
||||
* Each vendor is associated with three distinct dispatch table types:
|
||||
*
|
||||
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
|
||||
* provided by the vendor at load time during the initial handshake.
|
||||
* - dynamic EGL dispatch table: this is a structure allocated by the API
|
||||
* library at runtime used to manage EGL extension functions which are not
|
||||
* present in the static table.
|
||||
* - core GL dispatch table: this is a structure maintained by the API library
|
||||
* which contains both GL core (static) and GL extension (dynamic) functions.
|
||||
*
|
||||
* @{
|
||||
*/
|
||||
|
||||
/*
|
||||
* Rendering API handling:
|
||||
*
|
||||
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
|
||||
* other API is added, then the major version number will be incremented.
|
||||
*
|
||||
* When the application calls eglBindAPI, libEGL will forward the call to every
|
||||
* vendor library. In addition, a vendor library can query the current API from
|
||||
* libEGL using the getCurrentApi callback.
|
||||
*
|
||||
* Vendor libraries are not required to support both GL and GLES, but they must
|
||||
* be able to deal with either one as the current rendering API. If a vendor
|
||||
* doesn't support the current API, then it should return an error from
|
||||
* eglCreateContext.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* Current version of the ABI.
|
||||
*
|
||||
* This version number contains a major number in the high-order 16 bits, and
|
||||
* a minor version number in the low-order 16 bits.
|
||||
*
|
||||
* The major version number is incremented when an interface change will break
|
||||
* backwards compatibility with existing vendor libraries. The minor version
|
||||
* number is incremented when there's a change but existing vendor libraries
|
||||
* will still work.
|
||||
*/
|
||||
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
|
||||
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
|
||||
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version >> 16;
|
||||
}
|
||||
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
|
||||
{
|
||||
return version & 0xFFFF;
|
||||
}
|
||||
|
||||
|
||||
/*!
|
||||
* This opaque structure stores function pointers for EGL extension functions.
|
||||
* It is allocated at runtime by the API library. Vendor-provided dispatch
|
||||
* functions retrieve and operate on this structure using the API below.
|
||||
*/
|
||||
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
|
||||
|
||||
/****************************************************************************
|
||||
* API library exports *
|
||||
****************************************************************************/
|
||||
|
||||
typedef struct __EGLapiExportsRec {
|
||||
/************************************************************************
|
||||
* The following routines are used by vendor-implemented EGL dispatch
|
||||
* functions to lookup and call into the right vendor.
|
||||
************************************************************************/
|
||||
|
||||
/*!
|
||||
* This function must be called at the start of every EGL dispatch stub. It
|
||||
* performs any necessary per-call bookkeeping.
|
||||
*/
|
||||
void (* threadInit) (void);
|
||||
|
||||
EGLenum (* getCurrentApi)(void);
|
||||
|
||||
/*!
|
||||
* This function retrieves the appropriate current dynamic dispatch table,
|
||||
* if a GL context is current. Otherwise, this returns NULL.
|
||||
*/
|
||||
__EGLvendorInfo *(*getCurrentVendor)(void);
|
||||
|
||||
/*!
|
||||
* This retrieves the current context for this thread.
|
||||
*/
|
||||
EGLContext (*getCurrentContext)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current display for this thread.
|
||||
*/
|
||||
EGLDisplay (*getCurrentDisplay)(void);
|
||||
|
||||
/*!
|
||||
* Returns the current drawable for this thread.
|
||||
*/
|
||||
EGLSurface (*getCurrentSurface)(EGLint readDraw);
|
||||
|
||||
/*!
|
||||
* This function retrieves an entry point from the dynamic dispatch table
|
||||
* given an index into the table.
|
||||
*/
|
||||
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
|
||||
|
||||
/*!
|
||||
* Sets the last error for the current thread. The error will be returned
|
||||
* the next time the app calls eglGetError().
|
||||
*
|
||||
* This function will override a call to \c setLastVendor, and vice-versa.
|
||||
*/
|
||||
void (* setEGLError) (EGLint errorCode);
|
||||
|
||||
/*!
|
||||
* Notifies libEGL about the vendor library that an EGL function is
|
||||
* dispatched to. This is used to look up the last EGL error code from a
|
||||
* vendor.
|
||||
*
|
||||
* A vendor library may generate an EGL error asynchronously, if it
|
||||
* offloads some EGL functions onto a worker thread. In that case, calling
|
||||
* \c setEGLError to set the last error would not work.
|
||||
*
|
||||
* When an EGL dispatch stubs finds the vendor library, it must call this
|
||||
* function before it dispatches the function call.
|
||||
*
|
||||
* This function will override a call to \c setEGLError, and vice-versa.
|
||||
*
|
||||
* \param vendor The vendor library of the current EGL function call.
|
||||
* \return EGL_TRUE on success, or EGL_FALSE on failure.
|
||||
*/
|
||||
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
|
||||
|
||||
/************************************************************************
|
||||
* These routines are used by vendor dispatch functions to look up
|
||||
* and add mappings between various objects and vendors.
|
||||
************************************************************************/
|
||||
|
||||
// As far as I can tell, all EGL functions that take a context or drawable
|
||||
// also take an EGLDisplay. So, we don't need any extra functions for
|
||||
// mapping a context/surface/whatever to a EGLDisplay or vendor.
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDisplay handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
|
||||
|
||||
/*!
|
||||
* Returns the EGL vendor for an EGLDeviceEXT handle.
|
||||
*/
|
||||
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
|
||||
|
||||
/**
|
||||
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
|
||||
* any function that returns an EGLDeviceEXT handle should call this.
|
||||
*
|
||||
* Supported since ABI version 1.2.
|
||||
*/
|
||||
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
|
||||
} __EGLapiExports;
|
||||
|
||||
/*****************************************************************************
|
||||
* API library imports *
|
||||
*****************************************************************************/
|
||||
|
||||
/*!
|
||||
* The enum values accepted by \c __EGLapiImports::getVendorString.
|
||||
*/
|
||||
enum
|
||||
{
|
||||
/*!
|
||||
* The set of platform extensions that the vendor supports.
|
||||
*
|
||||
* Platform extensions typically just add enums to the existing
|
||||
* eglGetPlatformDisplay function, so we don't need any additional logic
|
||||
* or special handling from libEGL itself.
|
||||
*
|
||||
* As a result, vendor libraries can add these extensions without having
|
||||
* to modify libEGL to match.
|
||||
*/
|
||||
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
|
||||
};
|
||||
|
||||
/*!
|
||||
* This structure stores required and optional vendor library callbacks.
|
||||
*/
|
||||
typedef struct __EGLapiImportsRec {
|
||||
/**
|
||||
* Creates an EGLDisplay. This function is used to handle both
|
||||
* \c eglGetDisplay and \c eglGetPlatformDisplay.
|
||||
*
|
||||
* For some platform types, libEGL may be able to identify a specific
|
||||
* vendor to use.
|
||||
*
|
||||
* If libEGL can't figure out which vendor to use on its own, then it will
|
||||
* go through the list of available vendor libraries and call this function
|
||||
* until one succeeds.
|
||||
*
|
||||
* If the application calls eglGetPlatformDisplay, then the parameters are
|
||||
* passed through from the application.
|
||||
*
|
||||
* If the application calls eglGetDisplay, and the native display handle is
|
||||
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
|
||||
* type, and will pass that platform to the vendor library.
|
||||
*
|
||||
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
|
||||
* libEGL will call into each vendor library with \p platform set to
|
||||
* \c EGL_NONE. The vendor library can then select a default display to
|
||||
* return.
|
||||
*
|
||||
* In all cases, if the vendor library can't find a matching EGLDisplay,
|
||||
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
|
||||
* through the vendor's \c eglGetError function.
|
||||
*/
|
||||
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
|
||||
const EGLAttrib *attrib_list);
|
||||
|
||||
/*!
|
||||
* Checks if the vendor library supports a given client API (that is, the
|
||||
* API value passed to \c eglBindAPI).
|
||||
*/
|
||||
EGLBoolean (* getSupportsAPI) (EGLenum api);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Returns a string from the vendor library. This is used for
|
||||
* anything that isn't available from eglQueryString.
|
||||
*/
|
||||
const char * (* getVendorString) (int name);
|
||||
|
||||
/*
|
||||
* NOTE: libEGL will load all of the vendor libraries up front, so it
|
||||
* handles extension functions slightly differently than EGL.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* This retrieves the pointer to the real EGL or core GL function.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function.
|
||||
*/
|
||||
void * (* getProcAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This retrieves vendor-neutral functions which use the
|
||||
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
|
||||
*
|
||||
* A vendor library must provide a dispatch function for all EGL display
|
||||
* extension functions that it supports.
|
||||
*
|
||||
* Client extension functions cannot be dispatched based on an EGDisplay,
|
||||
* so they must be handled in libEGL itself.
|
||||
*
|
||||
* \param procName The name of the function.
|
||||
* \return A pointer to a function, or \c NULL if the vendor does not
|
||||
* support the function or \p procName is not a EGL display function.
|
||||
*/
|
||||
void * (*getDispatchAddress) (const char *procName);
|
||||
|
||||
/*!
|
||||
* This notifies the vendor library which dispatch table index is
|
||||
* assigned to a particular EGL extension function.
|
||||
*/
|
||||
void (*setDispatchIndex) (const char *procName, int index);
|
||||
|
||||
/*
|
||||
* The vendor library may use the isPatchSupported, initiatePatch,
|
||||
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
|
||||
* entrypoints at make current time, provided no other contexts are current
|
||||
* and the TLS model supports this functionality. This is a performance
|
||||
* optimization that may not be available at runtime; the vendor library
|
||||
* must not depend on this functionality for correctness.
|
||||
*
|
||||
* To use this optimization, the vendor library must provide at least the
|
||||
* isPatchSupported and initiatePatch entrypoints.
|
||||
*
|
||||
* Note that these functions are identical to the ones used in GLX.
|
||||
*/
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Checks to see if the vendor library supports patching the
|
||||
* given stub type and size.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*/
|
||||
GLboolean (* isPatchSupported)(int type, int stubSize);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
|
||||
* top-level entrypoints.
|
||||
*
|
||||
* The vendor library should use the \p lookupStubOffset callback to find
|
||||
* the addresses of each entrypoint.
|
||||
*
|
||||
* This function may be called more than once to patch multiple sets of
|
||||
* entrypoints. For example, depending on how they're built, libOpenGL.so
|
||||
* or libGL.so may have their own entrypoints that are separate functions
|
||||
* from the ones in libGLdispatch.
|
||||
*
|
||||
* Note that during this call is the only time that the entrypoints can be
|
||||
* modified. After the call to \c initiatePatch returns, the vendor library
|
||||
* should treat the entrypoints as read-only.
|
||||
*
|
||||
* \param type The type of entrypoints. This will be a one of the
|
||||
* __GLDISPATCH_STUB_* values.
|
||||
* \param stubSize The maximum size of the stub that the vendor library can
|
||||
* write, in bytes.
|
||||
* \param lookupStubOffset A callback into libglvnd to look up the address
|
||||
* of each entrypoint.
|
||||
*
|
||||
* \return GL_TRUE if the vendor library supports patching with this type
|
||||
* and size.
|
||||
*/
|
||||
GLboolean (*initiatePatch)(int type,
|
||||
int stubSize,
|
||||
DispatchPatchLookupStubOffset lookupStubOffset);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
|
||||
* longer owns the top-level entrypoints.
|
||||
*
|
||||
* Libglvnd will take care of the restoring the entrypoints back to their
|
||||
* original state. The vendor library must not try to modify them.
|
||||
*/
|
||||
void (*releasePatch)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
|
||||
* This callback allows vendor libraries to perform any per-thread
|
||||
* initialization.
|
||||
*
|
||||
* This is basically a workaround for broken applications. A lot of apps
|
||||
* will make one or more invalid GLX/EGL calls on a thread (often including
|
||||
* a MakeCurrent with invalid parameters), and then will try to call an
|
||||
* OpenGL function.
|
||||
*
|
||||
* A non-libglvnd-based driver would be able to initialize any thread state
|
||||
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
|
||||
* past libGLX.
|
||||
*
|
||||
* This function is optional. If it's \c NULL, then libGLdispatch will
|
||||
* simply ignore it.
|
||||
*
|
||||
* \note This function may be called concurrently from multiple threads.
|
||||
*/
|
||||
void (*patchThreadAttach)(void);
|
||||
|
||||
/*!
|
||||
* (OPTIONAL) Tries to determine the platform type for a native display.
|
||||
*
|
||||
* If the vendor library provides this function, then libglvnd will call it
|
||||
* to determine which platform to use for a native display handle in
|
||||
* eglGetDisplay.
|
||||
*
|
||||
* If no vendor library identifies the platform, then libglvnd will fall
|
||||
* back to its own platform detection logic.
|
||||
*
|
||||
* Libglvnd can call this function for any native display handle except
|
||||
* \c EGL_DEFAULT_DISPLAY.
|
||||
*
|
||||
* No matter what the value of \p native_display, the vendor library must
|
||||
* not crash, and must not return a false match. If the vendor library
|
||||
* can't identify the display, then it must return \c EGL_NONE.
|
||||
*
|
||||
* In particular, that means that a vendor library must not return any sort
|
||||
* of default or fallback platform.
|
||||
*
|
||||
* \param native_display The native display handle passed to eglGetDisplay.
|
||||
* \return Either a platform type enum or EGL_NONE.
|
||||
*/
|
||||
EGLenum (* findNativeDisplayPlatform) (void *native_display);
|
||||
} __EGLapiImports;
|
||||
|
||||
/*****************************************************************************/
|
||||
|
||||
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
|
||||
|
||||
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* Vendor libraries must export a function called __egl_Main() with the
|
||||
* following prototype.
|
||||
*
|
||||
* This function also performs a handshake based on the ABI version number.
|
||||
* Vendor libraries can optionally use the version number to support older
|
||||
* versions of the ABI.
|
||||
*
|
||||
* \param[in] version The ABI version. The upper 16 bits contains the major version
|
||||
* number, and the lower 16 bits contains the minor version number.
|
||||
*
|
||||
* \param[in] exports The table of functions provided by libEGL. This pointer will
|
||||
* remain valid for as long as the vendor is loaded.
|
||||
*
|
||||
* \param[in] vendor The opaque pointer used to identify this vendor library. This
|
||||
* may be used in future versions to provide additional per-vendor information.
|
||||
*
|
||||
* \param[out] imports The function table that the vendor library should fill
|
||||
* in. The vendor library must assign every non-optional function in the
|
||||
* struct.
|
||||
*
|
||||
* \return True on success. If the vendor library does not support the
|
||||
* requested ABI version or if some other error occurs, then it should return
|
||||
* False.
|
||||
*/
|
||||
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
|
||||
__EGLvendorInfo *vendor, __EGLapiImports *imports);
|
||||
|
||||
/*!
|
||||
* @}
|
||||
*/
|
||||
|
||||
#if defined(__cplusplus)
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __LIB_EGL_ABI_H */
|
||||
+1
Submodule 3rdparty/libfork added at 9b2b844a5f
+9
-617
@@ -14,116 +14,12 @@ option(MOBILEGL_ENABLE_TRACY "Enable tracy for profiling"
|
||||
option(MOBILEGL_BUILD_TRACE_REPLAY "Build desktop apitrace replay runner" OFF)
|
||||
option(MOBILEGL_TRACE_ANGLE_VARIANTS "Enable signed trace-APK ANGLE variant loading" OFF)
|
||||
option(MOBILEGL_IOS "Build MobileGL for iOS instead of macOS when APPLE is set" OFF)
|
||||
# The disaggregated (two-process) shape. OFF is the shipping default and OFF
|
||||
# must stay byte-comparable to a tree without MG_Remote at all: nothing under
|
||||
# MobileGL/MG_Remote/ is compiled, no include path is added, and no library is
|
||||
# linked, so `nm --defined-only libMobileGL.so | grep -i MG_Remote` is empty.
|
||||
# That emptiness is one of the two byte-level equalities the plan's validation
|
||||
# gates keep (section 10.3).
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-process shape)" OFF)
|
||||
# The CI / debugging shape (ARCHITECTURE.md:581): both roles in ONE process, talking over the
|
||||
# same SEG_CMD ring and the same G3 codec a spawned server would use. It IMPLIES
|
||||
# MOBILEGL_BUILD_DISAGGREGATED (below) and additionally admits the role-isolation shims that
|
||||
# only make sense when the two roles share an address space. It is a SUPERSET, never a
|
||||
# substitute: MOBILEGL_TRANSPORT=inproc is what selects the shape at run time, and this option
|
||||
# only decides whether the shims are compiled in.
|
||||
option(MOBILEGL_BUILD_DISAGGREGATED_INPROC "Compile the in-process (one-process, two-role) split shims; implies MOBILEGL_BUILD_DISAGGREGATED" OFF)
|
||||
option(MOBILEGL_BUILD_SERVER_SPIKE "Build the P0 spike-A MobileGLServer delivery-chain executable (Android only)" OFF)
|
||||
option(MOBILEGL_BUILD_EXTMEM_PROBE "Package tools/spikes/extmem_probe as libMobileGLExtmemProbe.so for the trace APK's exec hook (P11 A3, Android only)" OFF)
|
||||
option(MOBILEGL_EXTMEM_PROBE_AS_SERVER "Spike only (P11 B2 step 1): also package the probe as libMobileGLServer.so, the real server as libMobileGLServerReal.so" OFF)
|
||||
# The PipeInputs strangler (ARCHITECTURE.md 9.2). OFF is the pull build and must stay
|
||||
# byte-identical to a tree without either option: MGB_CTX is the live GLContext, no
|
||||
# MGPipe/PipeInputs source is compiled, every MGP_FILL is ((void)0).
|
||||
option(MOBILEGL_PIPE_PUSH "Backends read frontend state through the MGPipe PipeInputs block instead of MG_State::pGLContext (ARCHITECTURE.md 9.2 phase A)" OFF)
|
||||
option(MOBILEGL_PIPE_VERIFY "Compile SnapshotFromGLContext() and the G4 per-verb shadow comparator; implies MOBILEGL_PIPE_PUSH; never shipped" OFF)
|
||||
# Track H's old-versus-new arm (ARCHITECTURE.md 9.6). With a MOBILEGL_PIPE_PUSH bit clear
|
||||
# the backend would still run the RE-KEYED memo code, so the bitmask alone stops being a
|
||||
# valid A/B the moment a handle wave lands: this option compiles the pre-handle arm - the
|
||||
# registries, OwnerEquals, the TwinLookupMemos, g_fbSlotCache, ComputePipelineStateHash,
|
||||
# the address-keyed VaoDrawMemo - beside it, behind the same PipeInputs interface. ON for
|
||||
# the whole migration window; it retires with the pull path itself at P13.
|
||||
option(MOBILEGL_PIPE_LEGACY_MEMOS "Compile the pre-handle memo arm beside the {slot, gen} arm so Track H has a real A/B (ARCHITECTURE.md 9.6)" ON)
|
||||
set(MOBILEGL_LOG_ACTIVE_LEVEL "MOBILEGL_LOG_LEVEL_INFO" CACHE STRING "MobileGL active log level macro")
|
||||
set(MOBILEGL_VULKAN_LIBRARY "" CACHE FILEPATH "Vulkan loader/MoltenVK library to link for iOS builds")
|
||||
|
||||
if (ANDROID)
|
||||
set(MOBILEGL_BUILD_TEST OFF CACHE BOOL "Build MobileGL tests" FORCE)
|
||||
set(MOBILEGL_BUILD_BENCHMARK OFF CACHE BOOL "Build MobileGL benchmarks" FORCE)
|
||||
|
||||
# ------- Android API level policy: minimum 26, decided here and only here -------
|
||||
# MobileGL ships against API 26: the codebase must not use any API introduced
|
||||
# after 26. That usage constraint is enforced where it is real - the shipping
|
||||
# gradle build compiles at minSdk 26, where a newer API is simply undeclared
|
||||
# and fails to compile. Configuring at a HIGHER level is therefore allowed
|
||||
# (nothing in the tree may rely on it), but a LOWER level would change the
|
||||
# libc contract underneath the shipped library and is refused.
|
||||
#
|
||||
# This has to live at configure time because the level cannot be corrected
|
||||
# from a source header. A `#define __ANDROID_API__ 26` in a common header
|
||||
# only rewrites the macro for the bionic headers that happen to be included
|
||||
# after it; any libc++ header pulled in earlier has already latched its
|
||||
# feature macros at the real configure-time level. libc++ and bionic then
|
||||
# disagree about which symbols exist - libc++ calls e.g.
|
||||
# pthread_cond_clockwait while bionic, re-read at the lowered level, has
|
||||
# hidden its declaration. MobileGL/Defines.h carried exactly that pin from
|
||||
# the first commit until it was removed; this guard is what replaces it.
|
||||
#
|
||||
# Read the level back from the compiler target triple first. Its trailing
|
||||
# number (aarch64-none-linux-android26) is precisely what clang turns into
|
||||
# __ANDROID_API__, so it cannot disagree with the compile itself, and it is
|
||||
# already past every NDK normalisation step - codename aliases, "latest",
|
||||
# and per-ABI minimum pull-ups. ANDROID_PLATFORM_LEVEL is the fallback for
|
||||
# generators/languages where the triple variable is not populated.
|
||||
#
|
||||
# Note CMAKE_SYSTEM_VERSION is deliberately NOT consulted: it holds the API
|
||||
# level only under the NDK's newer toolchain path, and is a meaningless 1
|
||||
# when ANDROID_USE_LEGACY_TOOLCHAIN_FILE is on (which is what AGP has been
|
||||
# defaulting to). Reading it would fail every legacy-mode build.
|
||||
set(MOBILEGL_ANDROID_API_LEVEL 26)
|
||||
|
||||
set(_mobilegl_android_api "")
|
||||
foreach (_mobilegl_api_triple "${CMAKE_CXX_COMPILER_TARGET}"
|
||||
"${CMAKE_C_COMPILER_TARGET}")
|
||||
if (NOT _mobilegl_android_api AND
|
||||
_mobilegl_api_triple MATCHES "-android([0-9]+)$")
|
||||
set(_mobilegl_android_api "${CMAKE_MATCH_1}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
foreach (_mobilegl_api_var ANDROID_PLATFORM_LEVEL ANDROID_NATIVE_API_LEVEL
|
||||
ANDROID_PLATFORM)
|
||||
if (NOT _mobilegl_android_api AND ${_mobilegl_api_var})
|
||||
string(REGEX REPLACE "^android-" ""
|
||||
_mobilegl_android_api "${${_mobilegl_api_var}}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if (NOT _mobilegl_android_api MATCHES "^[0-9]+$")
|
||||
message(FATAL_ERROR
|
||||
"MobileGL: could not determine the Android API level (got "
|
||||
"\"${_mobilegl_android_api}\"). Configure with the NDK toolchain "
|
||||
"file and -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL}.")
|
||||
elseif (_mobilegl_android_api LESS MOBILEGL_ANDROID_API_LEVEL)
|
||||
message(FATAL_ERROR
|
||||
"MobileGL requires at least Android API ${MOBILEGL_ANDROID_API_LEVEL}, "
|
||||
"but this build resolved to API ${_mobilegl_android_api}.\n"
|
||||
"Configure with -DANDROID_PLATFORM=android-${MOBILEGL_ANDROID_API_LEVEL} "
|
||||
"(gradle builds get this from minSdk ${MOBILEGL_ANDROID_API_LEVEL}, so "
|
||||
"check that minSdk instead of adding an override).")
|
||||
elseif (_mobilegl_android_api GREATER MOBILEGL_ANDROID_API_LEVEL)
|
||||
message(STATUS
|
||||
"MobileGL: configuring at Android API ${_mobilegl_android_api} "
|
||||
"(> shipping minimum ${MOBILEGL_ANDROID_API_LEVEL}). Allowed, but the "
|
||||
"tree must not use post-${MOBILEGL_ANDROID_API_LEVEL} APIs - the "
|
||||
"minSdk-${MOBILEGL_ANDROID_API_LEVEL} gradle build is the enforcing "
|
||||
"compile.")
|
||||
endif()
|
||||
|
||||
message(STATUS "MobileGL: Android API level ${_mobilegl_android_api}")
|
||||
|
||||
unset(_mobilegl_android_api)
|
||||
unset(_mobilegl_api_var)
|
||||
unset(_mobilegl_api_triple)
|
||||
endif()
|
||||
|
||||
option(MOBILEGL_ENABLE_LTO "Build with ThinLTO/IPO" OFF)
|
||||
@@ -187,61 +83,15 @@ if (MSVC)
|
||||
endif()
|
||||
|
||||
# Generate MGGitHash.h
|
||||
#
|
||||
# RESULT_VARIABLE, WHICH THIS CALL DID NOT HAVE (CONTRACT-P6 4.2). Without it a failed
|
||||
# `git rev-parse` yields "" and nothing says so. Verified live: building this worktree from WSL
|
||||
# produces an EMPTY hash, because the worktree's `.git` is a file holding a WINDOWS path that
|
||||
# WSL's git cannot resolve - so two builds from different commits got IDENTICAL ABI
|
||||
# fingerprints and the stamp silently stopped doing its documented job.
|
||||
execute_process(
|
||||
COMMAND git rev-parse HEAD
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_COMMIT_HASH_FULL
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE MOBILEGL_GIT_STAMP_RESULT
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
string(SUBSTRING "${GIT_COMMIT_HASH_FULL}" 0 7 GIT_COMMIT_HASH_SHORT)
|
||||
|
||||
# --- the BUILD STAMP, and why it is a second pair rather than a fix to the first -------------
|
||||
#
|
||||
# GIT_COMMIT_HASH_SHORT MAY NOT MOVE. It is baked into glGetString(GL_VERSION)
|
||||
# (MG_Impl/GLImpl/Getter/GL_Getter.cpp), so redefining it changes an APPLICATION-VISIBLE GL
|
||||
# string and the pull build's .rodata - a behaviour leak and a G1 break at once. The handshake
|
||||
# gets its own pair instead, consumed only by the ABI fingerprint, which no pull build compiles.
|
||||
#
|
||||
# PRESENT IS A FIRST-CLASS INPUT, not the emptiness of VALUE. "this build has no stamp" and
|
||||
# "this build's stamp is the empty string" are different claims, and a fingerprint that could
|
||||
# not tell them apart is how two unrelated builds agreed in the first place.
|
||||
#
|
||||
# -DMOBILEGL_BUILD_STAMP=<anything> (or the environment variable) overrides, because a tree git
|
||||
# cannot read is not always a tree whose commit is unknown - CI has ${GITHUB_SHA} in hand.
|
||||
set(MOBILEGL_BUILD_STAMP "" CACHE STRING "Explicit ABI-fingerprint build stamp when git cannot be read")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "")
|
||||
if(NOT "${MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${MOBILEGL_BUILD_STAMP}")
|
||||
elseif(NOT "$ENV{MOBILEGL_BUILD_STAMP}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "$ENV{MOBILEGL_BUILD_STAMP}")
|
||||
elseif(MOBILEGL_GIT_STAMP_RESULT EQUAL 0 AND NOT "${GIT_COMMIT_HASH_FULL}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_VALUE "${GIT_COMMIT_HASH_FULL}")
|
||||
endif()
|
||||
|
||||
if("${MOBILEGL_BUILD_STAMP_VALUE}" STREQUAL "")
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 0)
|
||||
# A WARNING AND NOT A FATAL: a build with no stamp is still a usable build, it just cannot
|
||||
# prove that a peer was built from the same tree. Saying so once at configure time is the
|
||||
# difference between a known limitation and the silent one 4.2 found.
|
||||
message(WARNING
|
||||
"MobileGL: no build stamp. `git rev-parse HEAD` failed (result "
|
||||
"'${MOBILEGL_GIT_STAMP_RESULT}') and MOBILEGL_BUILD_STAMP was not set, so the ABI "
|
||||
"fingerprint cannot distinguish two builds of the same sizes from different commits - "
|
||||
"which is exactly what a stale MOBILEGL_IPC_SERVER_PATH looks like. Pass "
|
||||
"-DMOBILEGL_BUILD_STAMP=<commit> to restore it.")
|
||||
else()
|
||||
set(MOBILEGL_BUILD_STAMP_PRESENT 1)
|
||||
endif()
|
||||
|
||||
configure_file(
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Util/Miscellany/MGGitHash.h.in
|
||||
${CMAKE_BINARY_DIR}/generated/MGGitHash.h
|
||||
@@ -257,7 +107,6 @@ set(ENABLE_SPVREMAPPER OFF CACHE BOOL "Enable SPVRemapper" FORCE)
|
||||
set(ENABLE_OPT ON CACHE BOOL "Enable SPIRV-Tools opt usage in glslang" FORCE)
|
||||
set(BUILD_EXTERNAL ON CACHE BOOL "Build external deps in External/" FORCE)
|
||||
set(ENABLE_GLSLANG_INSTALL OFF CACHE BOOL "Install glslang targets" FORCE)
|
||||
set(SPIRV_SKIP_EXECUTABLES ON CACHE BOOL "Skip building SPIRV-Tools executables" FORCE)
|
||||
|
||||
set(SPIRV_CROSS_C_API ON CACHE BOOL "Enable C API" FORCE)
|
||||
set(SPIRV_CROSS_ENABLE_GLSL ON CACHE BOOL "Enable GLSL backend" FORCE)
|
||||
@@ -313,8 +162,6 @@ set(SOURCE_FILES
|
||||
|
||||
MobileGL/MG_Util/Metrics/BufferMetrics.cpp
|
||||
|
||||
MobileGL/MG_Util/Metrics/PipeStats.cpp
|
||||
|
||||
MobileGL/MG_Util/Converters/GLToStr/GLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/EGLToStr/EGLEnumConverter.cpp
|
||||
MobileGL/MG_Util/Converters/MGToStr/DataTypeConverter.cpp
|
||||
@@ -347,7 +194,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenInterfaceStructPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EliminateFloatEqualsZeroPass.cpp
|
||||
@@ -355,59 +201,28 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RenameBuiltinShadowingFunctionsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
|
||||
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
|
||||
|
||||
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
|
||||
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
|
||||
|
||||
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
|
||||
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPost.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
|
||||
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
|
||||
|
||||
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
|
||||
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
|
||||
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
|
||||
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
|
||||
|
||||
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
|
||||
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
|
||||
|
||||
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
|
||||
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
|
||||
@@ -420,7 +235,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Impl/GLImpl/Program/ProgramInterface.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Debug/GL_Debug.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/Validators.cpp
|
||||
MobileGL/MG_Impl/GLImpl/Texture/ProxyTexture.cpp
|
||||
MobileGL/MG_Impl/GLImpl/VertexArray/GL_VertexArray.cpp
|
||||
@@ -446,7 +260,6 @@ set(SOURCE_FILES
|
||||
|
||||
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,7 +279,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.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
|
||||
@@ -480,13 +292,10 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject2DCube.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObject3D.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectBuffer.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureObjectView.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureUnit.cpp
|
||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramTranslationCache.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||
@@ -501,205 +310,6 @@ set(SOURCE_FILES
|
||||
MobileGL/MG_State/GLState/RenderbufferState/RenderbufferState.cpp
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MG_Remote (disaggregated transport). Everything below is gated: with the
|
||||
# option OFF not one file here is compiled and no include path is added.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED_INPROC implies MOBILEGL_BUILD_DISAGGREGATED. A normal
|
||||
# variable, not a forced cache write, for the reason the two implications below give: an
|
||||
# operator's cache entry stays theirs and only this configure is shadowed. Ordered BEFORE the
|
||||
# flatbuffers guard so that a missing submodule turns BOTH off together - an INPROC build
|
||||
# with the transport shadowed off would compile a role-isolation shim over no roles.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED_INPROC AND NOT MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED_INPROC=ON forces "
|
||||
"MOBILEGL_BUILD_DISAGGREGATED ON for this configure")
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED ON)
|
||||
endif()
|
||||
|
||||
# FlatBuffers is a submodule and its runtime is header-only. Guard both ways:
|
||||
# a checkout without the submodule must configure and build, just without the
|
||||
# disaggregated shape, rather than fail with a missing-header error a hundred
|
||||
# lines later. Note this only checks for the RUNTIME headers - flatc is never
|
||||
# built here (see scripts/gen_protocol.py).
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND
|
||||
NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h")
|
||||
message(WARNING
|
||||
"MOBILEGL_BUILD_DISAGGREGATED=ON but 3rdparty/flatbuffers/include is missing. "
|
||||
"Run `git submodule update --init 3rdparty/flatbuffers`. Building without the "
|
||||
"disaggregated shape for this configure; the cached ON takes effect once the "
|
||||
"submodule is present.")
|
||||
# A NORMAL variable, deliberately not `CACHE BOOL ... FORCE`: forcing OFF into the cache
|
||||
# made the plain re-configure after `git submodule update` stay OFF with no message at
|
||||
# all. Shadowing the cache entry for this configure only keeps the operator's ON where it
|
||||
# was, so the next configure - with the submodule there - honours it.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED OFF)
|
||||
# And with it the shim option, or the `-DMOBILEGL_BUILD_DISAGGREGATED_INPROC=1` below
|
||||
# would still be defined over a build with no MG_Remote in it at all.
|
||||
set(MOBILEGL_BUILD_DISAGGREGATED_INPROC OFF)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_BUILD_DISAGGREGATED implies MOBILEGL_PIPE_PUSH (P5 c0). The split path IS the
|
||||
# pushed path: MG_Remote's server decodes records into the MGPipeApply* entry points, which
|
||||
# live in MG_Pipe/PipeApply.cpp, which the PIPE_PUSH block below is what compiles. Without
|
||||
# this, `-DMOBILEGL_BUILD_DISAGGREGATED=ON` alone configures and then fails to link the
|
||||
# applier - and the shape it fails in (MG_Remote compiled, no applier) is indistinguishable
|
||||
# at the CMake level from a legitimate transport-only build, which is why it is stated here
|
||||
# rather than left to whoever hits the link error. Same normal-variable form as the two
|
||||
# implications above.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_BUILD_DISAGGREGATED=ON forces MOBILEGL_PIPE_PUSH ON for "
|
||||
"this configure: the split path decodes into the MGPipe applier, and the "
|
||||
"applier is what MOBILEGL_PIPE_PUSH compiles")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# MOBILEGL_PIPE_VERIFY implies MOBILEGL_PIPE_PUSH: the comparator compares the pushed block
|
||||
# against a snapshot, so there has to be a pushed block. A normal variable, not a forced
|
||||
# cache write, for the same reason as the disaggregated fallback above.
|
||||
if (MOBILEGL_PIPE_VERIFY AND NOT MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_VERIFY=ON forces MOBILEGL_PIPE_PUSH ON for this configure")
|
||||
set(MOBILEGL_PIPE_PUSH ON)
|
||||
endif()
|
||||
|
||||
# In a pull build the legacy arm is the ONLY arm, so the option cannot be off there.
|
||||
# A normal variable, not a forced cache write, for the same reason as the two above.
|
||||
if (NOT MOBILEGL_PIPE_PUSH AND NOT MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
message(STATUS "MobileGL: MOBILEGL_PIPE_PUSH=OFF forces MOBILEGL_PIPE_LEGACY_MEMOS ON for this "
|
||||
"configure: with nothing pushed it is the only arm there is")
|
||||
set(MOBILEGL_PIPE_LEGACY_MEMOS ON)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
message(STATUS "MobileGL: PipeInputs push ON, appending the MGPipe fill sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Backend/MGPipe/PipeInputs.cpp
|
||||
MobileGL/MG_Impl/Pipe/PipeFill.cpp
|
||||
# P2's contract: the chunk table and its subset hash, the in-process applier, and
|
||||
# the client's {slot, gen} allocator. All three are push-only, which is how the
|
||||
# pull build gains no symbol from P2 (G1) - a declaration emits nothing.
|
||||
MobileGL/MG_Pipe/MGPipeRenderStateSpans.cpp
|
||||
MobileGL/MG_Pipe/PipeApply.cpp
|
||||
MobileGL/MG_Impl/Pipe/SlotAllocator.cpp
|
||||
# P4a's contract: the reflection-archive serializer over ProgramArtifacts.h's
|
||||
# VisitFields tables. Push-only for the same G1 reason as the three above - in
|
||||
# monolith the archive never crosses (create_shader_state hands the two structs over
|
||||
# by pointer beside the record), so the codec is live code only in the VERIFY lane,
|
||||
# where the applier serialises, deserialises and field-compares before storing.
|
||||
MobileGL/MG_State/GLState/ProgramState/ProgramArtifactsCodec.cpp
|
||||
# P5's contract (integrator ruling R-17, package c1): the MONOLITH arm of the
|
||||
# client->wire routing - the thirty-seven adapters that install gMGPipeScreen /
|
||||
# gMGPipeContext over the MGPipeApply* entry points, and the reply mailbox the four
|
||||
# acceptance rows answer through. Push-only for the same G1 reason as the five above:
|
||||
# the two tables are inline variables that are zero in a pull build and nothing there
|
||||
# can reach a thunk.
|
||||
MobileGL/MG_Pipe/PipeRoute.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
message(STATUS "MobileGL: disaggregated transport ON, appending MG_Remote sources")
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Transport/ShmLink.cpp
|
||||
MobileGL/MG_Remote/Transport/LinkMetrics.cpp
|
||||
MobileGL/MG_Remote/Transport/StreamLink.cpp
|
||||
MobileGL/MG_Remote/Transport/Ring.cpp
|
||||
MobileGL/MG_Remote/Transport/Doorbell.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegment.cpp
|
||||
# Both platform halves are listed unconditionally and each is empty on
|
||||
# the other OS, so neither can rot behind an `if (WIN32)` nobody
|
||||
# configures.
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentPosix.cpp
|
||||
MobileGL/MG_Remote/Transport/ShmSegmentWin32.cpp
|
||||
MobileGL/MG_Remote/Transport/FdPassing.cpp
|
||||
MobileGL/MG_Remote/Transport/InProcessTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/SocketTransport.cpp
|
||||
MobileGL/MG_Remote/Transport/ControlInbox.cpp
|
||||
# P9 W3: the server's log forward - a queue and a sender thread, graded by severity
|
||||
# (<= WARN lossy, >= ERROR lossless and paced), so no logging thread writes the socket.
|
||||
MobileGL/MG_Remote/Transport/LogForward.cpp
|
||||
# Keeps MG_Util/Debug/Log.h - and through it the GL frontend's
|
||||
# umbrella header - out of the header-only wire code (WireLog.h).
|
||||
MobileGL/MG_Remote/Transport/WireLog.cpp
|
||||
# P11 A1: the adopt tier settled at the handshake from the knob and the data plane, by
|
||||
# both roles (so SERVER-partition code in the link ratchet, not the client's tracker).
|
||||
MobileGL/MG_Remote/Transport/AdoptTier.cpp
|
||||
# P11 B2: T0 - the client's AHardwareBuffer store and its hop descriptor (both roles).
|
||||
MobileGL/MG_Remote/Transport/AdoptT0.cpp
|
||||
# ---- P5: the three new directories ------------------------------
|
||||
# Wire/ the G3 codec: MGPWireRec_* in and out of SEG_CMD, blobs and
|
||||
# var-tails in and out of SEG_STAGE. [w1]
|
||||
# Client/ the emitting role: session, the 69-slot emit table, the
|
||||
# caps mirror. [c1]
|
||||
# Server/ the applying role: session, the applier bridge onto the
|
||||
# existing MGPipeApply* free functions, the apply thread. [v1]
|
||||
#
|
||||
# Every file below lands in P5 as a HEADER plus a .cpp of named
|
||||
# Fatal stubs, so that all seven P5 packages compile and link on day
|
||||
# one against signatures that cannot then move under them. A stub is
|
||||
# MGLOG_F + std::abort, never a silent no-op: an unimplemented
|
||||
# emitter that returns quietly is how a split lane runs monolith and
|
||||
# goes green (ARCHITECTURE.md 10.3).
|
||||
MobileGL/MG_Remote/CapsCodec.cpp
|
||||
# P6 dl: the one place a session dies - Session::Fail's funnel (Fatal-family aware,
|
||||
# publishes SessionFault, one telemetry point).
|
||||
MobileGL/MG_Remote/FatalFunnel.cpp
|
||||
MobileGL/MG_Remote/Wire/PipeWireCodec.cpp
|
||||
MobileGL/MG_Remote/Client/ClientSession.cpp
|
||||
# Whether the configured server is there at all, asked before a display is offered so the
|
||||
# system's EGL/GLX/GBM loaders can move on to another implementation when it is not.
|
||||
MobileGL/MG_Remote/Client/ServerProbe.cpp
|
||||
MobileGL/MG_Remote/Client/EmitTables.cpp
|
||||
MobileGL/MG_Remote/Client/CapsMirror.cpp
|
||||
# P5 c1: the client role's BackendObject. pActiveBackendObject holds one of these
|
||||
# under split (table 3); the hook that installs it is v1's, in MG_Backend/Init.cpp.
|
||||
MobileGL/MG_Remote/Client/BackendObject_Remote.cpp
|
||||
# P5 c1, ruling R-17: the ENCODE TWIN of gMGPipeWireRecordApply - the thirty-seven
|
||||
# emitters installed over the two generated tables, which is what stops every
|
||||
# resource/CSO/texture/program record executing synchronously on the GL thread under
|
||||
# inproc. The monolith arm of the same routing is MG_Pipe/PipeRoute.cpp, in the
|
||||
# PIPE_PUSH list above, because it must exist in a push build that has no MG_Remote.
|
||||
MobileGL/MG_Remote/Client/WireTables.cpp
|
||||
# P5 b1's two: the conservative GPU-write set the client must build because all six
|
||||
# MarkGpuWritten producers are on the server's side of the line, and the
|
||||
# block-granularity persistent-map push that tier T2 makes mandatory.
|
||||
MobileGL/MG_Remote/Client/GpuWritePending.cpp
|
||||
MobileGL/MG_Remote/Client/PersistentMapTracker.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSession.cpp
|
||||
MobileGL/MG_Remote/Server/PipeApplier.cpp
|
||||
MobileGL/MG_Remote/Server/ServerLoop.cpp
|
||||
# P14 S2: the per-session runtime and the thread routing that lets one process serve
|
||||
# several sessions (the real session a singleton accessor answers with on that
|
||||
# session's own threads).
|
||||
MobileGL/MG_Remote/Server/SessionRuntime.cpp
|
||||
MobileGL/MG_Remote/Server/ServerMain.cpp
|
||||
# P11 PAIR: a socket client's control and aux connections paired by the nonce each
|
||||
# presents first (PairBind), not by arrival order - both unix supervisors take pairs here.
|
||||
MobileGL/MG_Remote/Server/PairAcceptor.cpp
|
||||
# P11 B2: T0's Offers on the aux socket, matched to their map_persistent records by seq.
|
||||
MobileGL/MG_Remote/Server/AdoptInbox.cpp
|
||||
# Shared images: server-allocated AHardwareBuffers exported to clients as dma-buf descriptors
|
||||
# (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md).
|
||||
MobileGL/MG_Remote/Server/SharedImageRegistry.cpp
|
||||
MobileGL/MG_Remote/Server/ServerSpawn.cpp
|
||||
# P5f fc: the value frame that replaced the control mailbox's function-pointer payload,
|
||||
# and its codec against protocol.fbs's SurfaceOp/SurfaceReply (the spawn arm's seam).
|
||||
MobileGL/MG_Remote/Server/SurfaceControlFrame.cpp
|
||||
MobileGL/MG_Remote/Protocol/SurfaceOpCodec.cpp
|
||||
# P12 (on-screen server window): the process-wide holder of the server's own window
|
||||
# (the display Activity's SurfaceView on Android; nobody installs one on the host, so a
|
||||
# ServerOwned request is refused by name there).
|
||||
MobileGL/MG_Remote/Server/ServerDisplay.cpp
|
||||
# Package-neutral C ABI for an APK that owns the server and its ANativeWindow.
|
||||
MobileGL/MG_Remote/Server/EmbeddedServer.cpp
|
||||
# P7 gate 5 (g5-msprobe): the Magma wire arm's two multisample depth/stencil resolve
|
||||
# arms and the probe that measures the first against the second, shared by the
|
||||
# renderer's bring-up and the Vulkan POST. Wire-only - the monolith records neither arm -
|
||||
# so it stays out of the pull build (gate G1).
|
||||
MobileGL/MG_Backend/DirectVulkan/Renderer/WireDepthResolveProbe.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
if (APPLE AND NOT MOBILEGL_IOS)
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Impl/CGLImpl/CGLImpl.cpp
|
||||
@@ -714,14 +324,6 @@ if (ANDROID)
|
||||
MobileGL/MG_Util/SelfTest/DriverPostJni.cpp
|
||||
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
|
||||
)
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# P12 (on-screen server window): the APK's MobileGLDisplayActivity - the in-process
|
||||
# display server's thread, its SurfaceView's ANativeWindow in and out of ServerDisplay, and
|
||||
# the geometry up-call. Split build only: nothing else has an in-process server to drive.
|
||||
list(APPEND SOURCE_FILES
|
||||
MobileGL/MG_Remote/Server/DisplayServerJni.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
@@ -758,29 +360,11 @@ set(MOBILEGL_COMPILE_DEF
|
||||
-DASIO_NO_DEPRECATED
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_BUILD_DISAGGREGATED=1)
|
||||
endif()
|
||||
|
||||
if (MOBILEGL_PIPE_PUSH)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_PUSH=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_VERIFY)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_VERIFY=1)
|
||||
endif()
|
||||
if (MOBILEGL_PIPE_LEGACY_MEMOS)
|
||||
list(APPEND MOBILEGL_COMPILE_DEF -DMOBILEGL_PIPE_LEGACY_MEMOS=1)
|
||||
endif()
|
||||
|
||||
message(STATUS "MOBILEGL_COMPILE_DEF=${MOBILEGL_COMPILE_DEF}")
|
||||
|
||||
set(MOBILEGL_INCLUDE_DIR
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
${CMAKE_SOURCE_DIR}/MobileGL
|
||||
# The MGPipe boundary headers. They are reachable as <MG_Pipe/MGPipe.h> through the
|
||||
# line above too; this entry lets the client, the backends and MG_Remote spell them
|
||||
# as <MGPipe.h> once MG_Pipe stops being a leaf of the frontend tree.
|
||||
${CMAKE_SOURCE_DIR}/MobileGL/MG_Pipe
|
||||
${spirv-tools_SOURCE_DIR}
|
||||
${spirv-tools_SOURCE_DIR}/include
|
||||
${spirv-tools_BINARY_DIR}
|
||||
@@ -788,19 +372,16 @@ set(MOBILEGL_INCLUDE_DIR
|
||||
# Header-only submodule: no add_subdirectory, no link target. Only
|
||||
# MG_Util/Async/ShaderCompilePool.cpp includes it, and it stays behind that file's
|
||||
# pimpl so no consumer target needs this path.
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/include
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/asio/asio/include
|
||||
# The second shader-compile execution engine (MOBILEGL_ASYNC_POOL=libfork), on the
|
||||
# same terms as Asio above: header-only, no add_subdirectory (its CMakeLists only
|
||||
# declares an INTERFACE target plus install/test scaffolding we do not want), no link
|
||||
# target, and reachable from exactly one translation unit. libfork's own
|
||||
# target_compile_features asks for cxx_std_23, which this project already sets
|
||||
# globally, so its C++20 coroutines need no per-source standard override.
|
||||
${CMAKE_SOURCE_DIR}/3rdparty/libfork/include
|
||||
)
|
||||
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED)
|
||||
# Header-only runtime: an include path, no add_subdirectory, no link
|
||||
# target, and above all no flatc in the build graph. protocol_generated.h
|
||||
# is committed and regenerated by scripts/gen_protocol.py.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/flatbuffers/include)
|
||||
# The vendored glvnd vendor-ABI headers (3rdparty/glvnd): the entry point a vendor library
|
||||
# exposes so the system libEGL - which IS glvnd - can dispatch to it.
|
||||
list(APPEND MOBILEGL_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/glvnd/include)
|
||||
endif()
|
||||
|
||||
add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
${SOURCE_FILES}
|
||||
)
|
||||
@@ -845,7 +426,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
|
||||
|
||||
if(UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
foreach(MOBILEGL_LOADER_ALIAS
|
||||
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
|
||||
libEGL.so libEGL.so.1)
|
||||
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
|
||||
@@ -909,28 +490,6 @@ if (TRACY_ENABLE)
|
||||
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
|
||||
endif ()
|
||||
|
||||
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
|
||||
#
|
||||
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
|
||||
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
|
||||
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
|
||||
# With default visibility every reference inside this library resolves against the whole process,
|
||||
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
|
||||
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
|
||||
# the two visibilities do not mix; this flag is that promise made by the linker instead.
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
|
||||
endif()
|
||||
|
||||
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
|
||||
# docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
|
||||
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
|
||||
# it through dlsym. Self-contained: nothing outside this block refers to either.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
|
||||
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
|
||||
add_subdirectory(MobileGL/MG_Gbm)
|
||||
endif()
|
||||
|
||||
if (ANDROID)
|
||||
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
|
||||
android
|
||||
@@ -1032,170 +591,3 @@ if (NOT ANDROID)
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The integration binary is also useful as a standalone adb-shell executable.
|
||||
# Android cannot use the desktop-only MobileGL_s target, so its CMake module
|
||||
# links libMobileGL.so and creates an AImageReader-backed window instead.
|
||||
if (ANDROID AND MOBILEGL_BUILD_INTEGRATION_TEST)
|
||||
add_subdirectory(MobileGL/MG_IntegrationTest)
|
||||
endif()
|
||||
|
||||
# The replay runner also builds for Android: an adb-shell executable that can
|
||||
# dial a render server on the device (unix or TCP) without any Activity.
|
||||
if (ANDROID AND MOBILEGL_BUILD_TRACE_REPLAY)
|
||||
add_subdirectory(tools/trace_replay)
|
||||
endif()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P0 spike A: the Android delivery chain for a second native executable.
|
||||
#
|
||||
# The disaggregated design needs a server process on Android (PLAN-B.md §8.1,
|
||||
# inheriting PLAN.md §11.1-§11.6). An APK's only exec-able install location is
|
||||
# lib/<abi>/, and the packager only puts a file there if it is named lib*.so -
|
||||
# so a second executable has to be built with an .so name and exec'd out of
|
||||
# getApplicationInfo().nativeLibraryDir. This target is the stub that proves the
|
||||
# chain end to end: it is packaged like a library, exec'd from the app's own
|
||||
# untrusted_app process, and writes a marker the parent reads back.
|
||||
#
|
||||
# Off by default and ANDROID-only, so no shipping configuration builds it. The
|
||||
# trace flavour of the plugin APK turns it on (android-plugin/build.gradle).
|
||||
# ---------------------------------------------------------------------------
|
||||
# ---------------------------------------------------------------------------
|
||||
# P6 `sm`: the REAL server image. Unlike MOBILEGL_BUILD_SERVER_SPIKE below - the
|
||||
# P0 spike, which links no MobileGL library and exists only to prove the Android
|
||||
# delivery chain - this one links the library and runs mobilegl_server_main.
|
||||
#
|
||||
# ARCHITECTURE.md:496 called for a ~30-line stub that dlopen's libMobileGL.so and
|
||||
# dlsym's the entry point, so one shared library serves both roles. On a desktop
|
||||
# configure we link it directly instead: there is no APK packaging rule to satisfy
|
||||
# and a direct link is what makes an undefined symbol a BUILD error rather than a
|
||||
# runtime dlsym failure in a child whose stderr is /dev/null.
|
||||
#
|
||||
# Android keeps the lib*.so naming and the nativeLibraryDir output the spike
|
||||
# proved is the only exec-able location in an APK.
|
||||
if (MOBILEGL_BUILD_DISAGGREGATED AND NOT WIN32)
|
||||
add_executable(MobileGLServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/MobileGL/MG_Remote/Server/ServerEntry.cpp)
|
||||
|
||||
target_include_directories(MobileGLServer PRIVATE
|
||||
${MOBILEGL_INCLUDE_DIR})
|
||||
|
||||
target_link_libraries(MobileGLServer PRIVATE ${CMAKE_PROJECT_NAME})
|
||||
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServer PRIVATE -pie)
|
||||
|
||||
# $ORIGIN, OR THE SERVER CANNOT START AT ALL ON ANDROID.
|
||||
#
|
||||
# This executable links libMobileGL.so, and it is EXEC'd rather than dlopen'd - so it gets a
|
||||
# fresh linker namespace with the default search path, not the app's. An APK's lib/<abi>/ is
|
||||
# not on that path, and the failure is the bluntest one there is:
|
||||
#
|
||||
# F linker: CANNOT LINK EXECUTABLE ".../lib/arm64/libMobileGLServer.so":
|
||||
# library "libMobileGL.so" not found: needed by main executable
|
||||
#
|
||||
# It cost a device run to find because it is invisible on desktop: CMake gives build-tree
|
||||
# targets an absolute RPATH automatically, so the WSL server resolved its dependency through a
|
||||
# path that does not exist on a phone. $ORIGIN says "beside me", which is true in the APK, true
|
||||
# in the build tree, and true wherever somebody copies the pair - and it is what makes a
|
||||
# deployment that is NOT a build tree work at all, which the end state is.
|
||||
# THE PROPERTIES, NOT target_link_options. A raw `-Wl,-rpath,$ORIGIN` reaches the generator
|
||||
# unquoted and the SHELL expands $ORIGIN to nothing - measured on device, where the linker
|
||||
# said `normalize_path - invalid input: "$"` and then failed to find libMobileGL.so anyway.
|
||||
# BUILD_RPATH / INSTALL_RPATH are the supported spelling and CMake escapes them correctly.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
BUILD_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE)
|
||||
|
||||
if (ANDROID)
|
||||
# An APK's only exec-able install location is lib/<abi>/, and the packager
|
||||
# only puts a file there if it is named lib*.so. P0 spike A proved this
|
||||
# works from the app's own untrusted_app domain on two devices.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer")
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
else()
|
||||
# Desktop: the name the dladdr fallback looks for beside libMobileGL.so.
|
||||
set_target_properties(MobileGLServer PROPERTIES
|
||||
PREFIX "lib" SUFFIX ".so" OUTPUT_NAME "MobileGLServer"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
|
||||
# Renamed by P6 sm: the real server took the name it had been holding.
|
||||
add_executable(MobileGLServerSpike
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/server_stub/main.cpp)
|
||||
|
||||
# An executable that is named like a shared library still has to be a real
|
||||
# PIE executable: Android has refused non-PIE executables since API 21, and
|
||||
# the name alone does not change what the loader demands of the file.
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServerSpike"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLServerSpike PRIVATE -fPIE)
|
||||
target_link_options(MobileGLServerSpike PRIVATE -pie)
|
||||
|
||||
# AGP packages what the external native build drops into the per-ABI output
|
||||
# directory, and it selects by the .so extension. CMake puts executables in
|
||||
# CMAKE_RUNTIME_OUTPUT_DIRECTORY, which is not the directory AGP hands to
|
||||
# CMAKE_LIBRARY_OUTPUT_DIRECTORY, so point this target's runtime output at
|
||||
# the library directory when the generator gave us one.
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLServerSpike PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (ANDROID AND MOBILEGL_BUILD_EXTMEM_PROBE)
|
||||
# P11 A3 (open question 3): the spike-B external-memory probe, exec'd from the trace
|
||||
# app's own process by the spike hook (`--es mobilegl_spike_spawn
|
||||
# libMobileGLExtmemProbe.so`), so its rows answer for the untrusted_app domain. The
|
||||
# same PIE-executable-named-like-a-library shape as MobileGLServerSpike above; it
|
||||
# links nothing from MobileGL.
|
||||
add_executable(MobileGLExtmemProbe
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
|
||||
set_target_properties(MobileGLExtmemProbe PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLExtmemProbe"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLExtmemProbe PRIVATE -fPIE)
|
||||
target_link_options(MobileGLExtmemProbe PRIVATE -pie)
|
||||
target_link_libraries(MobileGLExtmemProbe PRIVATE vulkan EGL GLESv3 android log)
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLExtmemProbe PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
if (MOBILEGL_EXTMEM_PROBE_AS_SERVER)
|
||||
# P11 B2 step 1, spike APKs only: MobileGLServerService execs `libMobileGLServer.so
|
||||
# <endpoint> --serve` and B1's broker connects to whatever that listens on, so the probe
|
||||
# takes the server's file name and answers "does a client's AHardwareBuffer cross this
|
||||
# route into the server app" on the real route (tools/spikes/extmem_probe, route mode).
|
||||
if (TARGET MobileGLServer)
|
||||
set_target_properties(MobileGLServer PROPERTIES OUTPUT_NAME "MobileGLServerReal")
|
||||
endif()
|
||||
add_executable(MobileGLExtmemProbeAsServer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/spikes/extmem_probe/extmem_probe.cpp)
|
||||
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
|
||||
PREFIX "lib"
|
||||
SUFFIX ".so"
|
||||
OUTPUT_NAME "MobileGLServer"
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
target_compile_options(MobileGLExtmemProbeAsServer PRIVATE -fPIE)
|
||||
target_link_options(MobileGLExtmemProbeAsServer PRIVATE -pie)
|
||||
target_link_libraries(MobileGLExtmemProbeAsServer PRIVATE vulkan EGL GLESv3 android log)
|
||||
if (CMAKE_LIBRARY_OUTPUT_DIRECTORY)
|
||||
set_target_properties(MobileGLExtmemProbeAsServer PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
+21
-613
@@ -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, 10, 0, "-dev", VersionType::Development};
|
||||
inline const Version CoreVersion = {26, 8, 0, "-dev", VersionType::Development};
|
||||
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
|
||||
inline const Uint64 CacheVersion = 0;
|
||||
|
||||
@@ -66,117 +66,30 @@ namespace MobileGL::MG_Config {
|
||||
// - DISPLAY: X11 session variable, not MobileGL configuration.
|
||||
// - MOBILEGL_LOG_FILE_PATH: log-file init runs before MG_ConfigLoader::Init
|
||||
// (see MG_Util/Debug/Log.cpp).
|
||||
// - MOBILEGL_ASYNC_POOL: a ShaderCompilePool is constructed by binaries that never call
|
||||
// MobileGL::Initialize() and so never run MG_ConfigLoader::Init - MG_Test's
|
||||
// JobNodeTest builds pools directly, and it is the suite that runs the whole async
|
||||
// matrix against both execution engines. Mirroring it here would resolve to the
|
||||
// default in exactly the tests that exist to tell the engines apart (see
|
||||
// MG_Util/Async/ShaderCompilePool.cpp, DetectAsyncPoolEngine).
|
||||
struct FeaturesTable {
|
||||
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
|
||||
Bool DisableTimerQuery = false;
|
||||
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
|
||||
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
|
||||
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
|
||||
// there, because the view's ES internalformat is normalized independently of the storage
|
||||
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
|
||||
// so that work can be done without editing the gate.
|
||||
Bool EsprytEnableTextureView = false;
|
||||
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
|
||||
// Disabled by default because validation is a diagnostics-only cost.
|
||||
Bool EnableSpirvValidation = false;
|
||||
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool EsprytUseAngle = false;
|
||||
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
|
||||
Bool UseAngle = false;
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
|
||||
String TraceAngleVariant;
|
||||
#endif
|
||||
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
|
||||
// including the opt-in emulated compute path below.
|
||||
Bool MagmaDisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
|
||||
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
|
||||
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
|
||||
// engages when this flag is set AND the device has no native subgroup support at
|
||||
// all - a device with real subgroup operations always uses them natively,
|
||||
// whatever their width (the known iterationRP defect is patched by
|
||||
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
|
||||
Bool MagmaEmulateSubgroup = false;
|
||||
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
|
||||
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
|
||||
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
|
||||
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
|
||||
// bounds. The pass grows that one array to what the device's topology needs and
|
||||
// touches nothing else; it only rewrites modules positively matching the pack's
|
||||
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
|
||||
// so every other shader passes through byte-identical - as does iterationRP
|
||||
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
|
||||
// verbatim.
|
||||
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
|
||||
// rendezvous between its two reductions over prefixSumCache. Off by default and
|
||||
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
|
||||
Bool MagmaIterationRPFixBarrier = false;
|
||||
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
|
||||
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
|
||||
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
|
||||
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
|
||||
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
|
||||
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
|
||||
// does whenever local_size_x is a multiple of the native width). ForceOff returns
|
||||
// to the raw driver builtin.
|
||||
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
|
||||
// MOBILEGL_ADVERTISE_FP64: add GL_ARB_gpu_shader_fp64 to the advertised extension
|
||||
// string. `double` in a shader always WORKS - it is narrowed to 32 bits before any
|
||||
// module reaches a backend (ShaderTranspiler::DemoteFloat64Pass) - but the extension
|
||||
// promises 64-bit precision, and that is the one thing the narrowing cannot deliver.
|
||||
// Off by default so an application that checks the string before using doubles keeps
|
||||
// its float path; on for measuring what the conformance suite makes of the demoted
|
||||
// precision. See the DemoteFloat64Pass header and the "fp64" POST row.
|
||||
Bool AdvertiseFp64 = false;
|
||||
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
|
||||
Bool DisableSubgroup = false;
|
||||
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
|
||||
Bool MagmaR11G11B10FFallback = false;
|
||||
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
|
||||
Uint32 MagmaFramesInFlight = 3;
|
||||
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
|
||||
// one command buffer before it submits it and continues on a fresh one. 0 = unbounded.
|
||||
// Drivers back a command buffer with GPU memory that is only returned when the buffer is
|
||||
// freed; Adreno maps it into the process in 16 KiB chunks, so one ~1M-draw loading frame
|
||||
// exhausts vm.max_map_count. See VulkanRenderer::SplitOversizedRecording.
|
||||
Uint32 MagmaMaxDrawsPerCommandBuffer = 16384;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES: how many consecutive frames (strictly, BeginFrame
|
||||
// epochs) a frame slot's descriptor pools must stay under a quarter full before Magma
|
||||
// hands the slot's whole set cache back and lets it re-grow. 0 = never trim. A loading
|
||||
// frame otherwise keeps its burst of sets (rd12: ~15k, where steady frames use a few dozen)
|
||||
// for the life of the process. See UniformManager::TrimFrameDescriptorPools.
|
||||
Uint32 MagmaDescriptorTrimFrames = 120;
|
||||
// MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER: avoid mipmap min filters in samplers,
|
||||
// resolves certain rendering bugs on ANGLE + llvmpipe.
|
||||
Bool EsprytAvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS: leave an already-explicit LOD argument alone when
|
||||
// emulating GL_TEXTURE_LOD_BIAS, instead of adding the bias uniform to it. Injecting
|
||||
// the uniform turns a compile-time-constant LOD into a runtime expression, which
|
||||
// sends ANGLE + llvmpipe down a mip-selection path that dereferences a NULL
|
||||
// descriptor and kills the process. Deviates from spec (Vulkan adds the bias to
|
||||
// OpImageSampleExplicitLod), so it is an avoidance for that stack only.
|
||||
Bool EsprytAvoidExplicitLodBias = false;
|
||||
// MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS: emit a tessellation/geometry program's
|
||||
// inter-stage interface blocks WITHOUT their layout(location=) qualifier, letting ES
|
||||
// match them by block name and member sequence instead. The Mali ES driver delivers
|
||||
// nothing at all through a located block once a tessellation or geometry stage is in
|
||||
// the pipeline; the driver POST measures that and turns this on by itself, so Auto is
|
||||
// the right setting everywhere. ForceOn exists so the emulation can be exercised on a
|
||||
// healthy driver - which is what the integration lane does, since llvmpipe and
|
||||
// lavapipe carry a located block correctly and would otherwise never run this code -
|
||||
// and ForceOff is the negative control. See StripIoBlockLocationsPass.
|
||||
QuirkOverride EsprytUnlocatedIoBlocks = QuirkOverride::Auto;
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION: demote gl_PointSize out of tessellation/geometry
|
||||
// stages into an ordinary varying (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram) instead of declining such programs
|
||||
// on a device that advertises neither EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size (DirectGLES) nor shaderTessellationAndGeometryPointSize
|
||||
// (DirectVulkan). Auto arms it exactly where the detection says the capability is
|
||||
// absent, which is the right setting everywhere. ForceOn exists so the demotion can
|
||||
// be exercised on a healthy driver - llvmpipe and lavapipe host the built-in
|
||||
// natively and would otherwise never run this code, which is what the pinned
|
||||
// integration lane uses - and ForceOff restores the plain declines (escape hatch /
|
||||
// negative control). Cross-backend by design: the demotion runs in the shared
|
||||
// phase-B chain, so one switch covers both. See DemotePointSizePass.
|
||||
QuirkOverride PointSizeDemotion = QuirkOverride::Auto;
|
||||
Bool AvoidSamplerMipmapMinFilter = false;
|
||||
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
|
||||
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
|
||||
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
|
||||
@@ -186,54 +99,20 @@ namespace MobileGL::MG_Config {
|
||||
Bool CoherentAsFlush = false;
|
||||
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
|
||||
Bool TraceSkipAutodestroy = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
|
||||
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
|
||||
// (negative control / driver-bug escape hatch).
|
||||
Bool EsprytDisableUboRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
|
||||
// glTexSubImage from the client pointer instead of staging them through the
|
||||
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
|
||||
// hatch).
|
||||
Bool EsprytDisableUnpackRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
|
||||
// (glBufferSubData / map flushes) back to the immediate driver upload instead
|
||||
// of queueing them for the staged-copy flush through the persistent-mapped
|
||||
// upload ring (negative control / driver-bug escape hatch; the immediate
|
||||
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
|
||||
Bool EsprytDisableUploadRing = false;
|
||||
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
|
||||
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
|
||||
// to the upload ring's staged glCopyBufferSubData (negative control / escape
|
||||
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
|
||||
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
|
||||
// on Mali both the immediate glBufferSubData and a staged copy into a busy
|
||||
// mutable store ghost the whole destination on the CPU.
|
||||
Bool EsprytDisableInvalidateFlush = false;
|
||||
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
|
||||
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
|
||||
// persistently+coherently mapped storage at definition time (negative control /
|
||||
// escape hatch). Frontend-scoped: it engages only where the active backend
|
||||
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
|
||||
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
|
||||
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
|
||||
// tore the frames still reading the old bytes) - still no driver-mediated write
|
||||
// into the busy mutable store, which on this class of Mali driver stalls the
|
||||
// thread or ghost-copies the whole arena, and the arena stops costing its size
|
||||
// again in RAM.
|
||||
Bool DisableLargeBufferAdoption = false;
|
||||
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
|
||||
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
|
||||
// ES has no depth or stencil readback, but some drivers accept it anyway (Mesa does,
|
||||
// Adreno does not), which means the emulation is dead code on exactly the stack the
|
||||
// headless suite runs on. This forces it live so the scenarios and the CTS can
|
||||
// exercise the path, and gives the device an A/B lever over the same choice.
|
||||
Bool EsprytForceDepthStencilReadbackEmulation = false;
|
||||
Bool DisableUboRing = false;
|
||||
// MOBILEGL_RELAXED_SEMANTICS: relax strict core-profile rules (e.g. VAO-0 draws,
|
||||
// texture-name reuse after delete) even on contexts that explicitly requested a core
|
||||
// profile. Without it, relaxed semantics still apply to every context that did not
|
||||
// explicitly request a core profile via EGL_CONTEXT_OPENGL_PROFILE_MASK / a >=3.1
|
||||
// version request.
|
||||
Bool RelaxedSemantics = false;
|
||||
// MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN: overrides the shader-source quirk that
|
||||
// rewrites the recognized workgroup prefix-scan template on Qualcomm devices with
|
||||
// subgroups wider than 32 lanes (see ShaderSourceProcessor's quirk registry).
|
||||
QuirkOverride SubgroupPrefixScanQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE: overrides the DirectVulkan quirk that
|
||||
// strips depth writes from accumulation-blended pipelines (MIN/MAX or additive
|
||||
// ONE+ONE - the multi-pass depth-equality signature) on drivers without
|
||||
@@ -241,10 +120,10 @@ namespace MobileGL::MG_Config {
|
||||
// gl_FragDepth writers, and fully color-masked attachments are exempt (see
|
||||
// PipelineFactory::ShouldSuppressDepthWrite). Auto detects Qualcomm.
|
||||
QuirkOverride MagmaDisableBlendedDepthWriteQuirk = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS: leave the Vulkan robustBufferAccess device
|
||||
// feature off. It is enabled by default to match GL's defined out-of-range fetch
|
||||
// behavior; this escape hatch exists to measure or dodge its GPU cost on a device.
|
||||
Bool MagmaDisableRobustBufferAccess = false;
|
||||
Bool DisableRobustBufferAccess = false;
|
||||
// MOBILEGL_MAGMA_MULTIDRAW_MODE: preferred DirectVulkan multi-draw dispatch tier
|
||||
// ("ext" | "indirect" | "unroll", see MultiDrawMode). Clamped to device support;
|
||||
// unset picks the best supported tier.
|
||||
@@ -263,477 +142,6 @@ namespace MobileGL::MG_Config {
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
||||
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||
Uint32 AsyncShaderCompileThreads = 0;
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
|
||||
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
|
||||
// empty, WITHOUT joining the job (latched per compile - see
|
||||
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
|
||||
// a real failure still fails the program link with the compile log quoted. It
|
||||
// exists for applications that compile hundreds of shaders serially and read the
|
||||
// status right after each glCompileShader - Iris's shader-pack load - where those
|
||||
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
|
||||
// phase issues no program-level query between programs; program-level LINK_STATUS
|
||||
// and the program info log still join truthfully, so paths that check each link
|
||||
// immediately stay serial by their own construction). Off by default; never
|
||||
// advertise it.
|
||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||
// MOBILEGL_SHADER_CACHE: the three-level, in-memory shader translation memo
|
||||
// (MG_Util/ShaderTranspiler/TranslationCache.h). The levels follow the GL
|
||||
// entry points - L1c memoizes one glCompileShader's PARSE VERDICT, L1 a
|
||||
// linked program's whole front end, L2 DirectGLES's emitted ESSL. Auto is
|
||||
// ON; ForceOff turns ALL THREE off and makes every translation run from
|
||||
// scratch. The escape hatch exists because a wrong cache hit is a silently
|
||||
// miscompiled shader: if a device ever renders differently with the cache
|
||||
// on, one run with this falsy says so.
|
||||
QuirkOverride ShaderTranslationCache = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION: DirectGLES' gl_ViewportIndex routing
|
||||
// emulation - the builtin becomes a flat varying, the fragment stage gets a
|
||||
// per-pass gate, and a routed draw is REPLAYED once per distinct viewport state
|
||||
// with the real glViewport/glScissor/glDepthRangef set for it. Auto is ON, and
|
||||
// it is ON even where the driver advertises GL_OES_viewport_array, because that
|
||||
// extension only ever gave the SHADER a compilable name: MobileGL has never
|
||||
// programmed a driver's INDEXED viewport state (SyncRenderState pushes index 0
|
||||
// and nothing else), so on an extension-capable driver every index rasterized as
|
||||
// index 0 exactly as it did without one. ForceOff returns to that behaviour -
|
||||
// the pre-emulation path, extension passthrough where it exists and
|
||||
// LowerViewportIndexPass' demote-to-a-plain-global where it does not - and is
|
||||
// the negative control the emulation is measured against.
|
||||
QuirkOverride EsprytViewportArrayEmulation = QuirkOverride::Auto;
|
||||
// MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE: DirectGLES stores GL_RGB565/GL_RGB5(A1)/GL_RGBA4
|
||||
// images as 8-bit-per-channel ES storage (GL_RGB8/GL_RGBA8) instead of the driver's
|
||||
// native 16-bit packed formats. Auto defers to a POST driver-bug probe
|
||||
// (SelfTest::CopyImageMirrorsPacked16FieldOrder): some Mali drivers store SOME
|
||||
// packed16 allocations with a MIRRORED field order (allocation-scoped and
|
||||
// shape/context dependent - the failing 30x30x12 GL_TEXTURE_2D_ARRAYs are mirrored
|
||||
// at every level), so glCopyImageSubData - a raw texel-block move - lands R/G/B/A
|
||||
// reversed whenever exactly one endpoint sits in a mirrored allocation
|
||||
// (KHR-GL4x.copy_image.functional rgb5/rgb5_a1/rgba4 x every *2d_array* pair).
|
||||
// With no 16-bit packed ES image left there is no field order to disagree about; the
|
||||
// client word still round-trips exactly, because the canonical shadow is already
|
||||
// UNorm8 and an n-bit field encodes to UNorm8 and back losslessly for n <= 8.
|
||||
// ForceOn widens on any driver (the llvmpipe suites use it to exercise the widened
|
||||
// path); ForceOff keeps the native narrow storage even where the probe fires - the
|
||||
// negative control that replays the corruption. Costs 2x the memory of the affected
|
||||
// formats where it engages, which is why Auto is probe-gated rather than always-on.
|
||||
QuirkOverride EsprytWidenPacked16Storage = QuirkOverride::Auto;
|
||||
// MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE: DirectVulkan's GL_PRIMITIVES_GENERATED
|
||||
// reroute for draws made while transform feedback is INACTIVE. The stream query
|
||||
// (VK_QUERY_TYPE_TRANSFORM_FEEDBACK_STREAM_EXT primitivesNeeded) is defined to count
|
||||
// them, but a Mali driver - and Mesa lavapipe - answers 0 unless a capture span is
|
||||
// open, which is exactly the shape the CTS uses to measure the tessellator, so ~29
|
||||
// tessellation tests per tree size a capture buffer from the 0 and die on the
|
||||
// zero-length map. Auto defers to a device probe at renderer bring-up
|
||||
// (SelfTest::RunPrimitivesGeneratedNoXfbProbe), which measures two substitutes on
|
||||
// the same capture-less draws and arms the best proven one: the dedicated
|
||||
// VK_EXT_primitives_generated_query (exact semantics by definition; lavapipe passes
|
||||
// it, rasterizer discard included), else a clipping-invocations pipeline-statistics
|
||||
// pool (see the verdict vocabulary for its rasterizer-discard split). ForceOn pins
|
||||
// the reroute structurally wherever a pool can exist (the arming-observable lane,
|
||||
// immune to the probe's verdict moving), and ForceOff is the negative control that
|
||||
// replays the driver's silence.
|
||||
QuirkOverride MagmaPrimGenQueryReroute = QuirkOverride::Auto;
|
||||
// --- MGPipe (the disaggregation plan's explicit frontend/backend boundary) ---
|
||||
// MOBILEGL_PIPE_PUSH: per-subsystem bitmask selecting which state the frontend
|
||||
// PUSHES over MGPipe instead of leaving the backend to pull it out of GLContext.
|
||||
// 0 - the only shipped value until the migration lands - is "pull everything",
|
||||
// i.e. exactly today's behaviour, and is the default of a PULL build, where the
|
||||
// knob is meaningless anyway. A PUSH build defaults to every subsystem migrated so
|
||||
// far (MG_Pipe::kMGPipeSubsystemsMigratedAtP5e = 0x3fff), so MOBILEGL_PIPE_PUSH=0 in
|
||||
// the environment is the all-pull control and 0x1fff (kMGPipeSubsystemsMigratedAtP4a)
|
||||
// is the "everything before P5e" control P5e's A/B is run against - each phase's
|
||||
// constant survives as the next phase's control, which is why none of them is ever
|
||||
// edited. Accepts decimal or 0x-prefixed hex, and operators pass it as hex, so the
|
||||
// bits are listed here (MG_Pipe/MGPipe.h owns them):
|
||||
// 0x01 render state (create/bind_render_state + set_dynamic_state)
|
||||
// 0x02 pixel pack 0x04 patch state 0x08 vertex attrib defaults
|
||||
// 0x10 residual values 0x20 Espryt slots 0x40 Magma vertex input
|
||||
// 0x80 resources (the resource_* family: the seven BufferBackendOps hooks)
|
||||
// 0x100 vertex input (vertex elements / vertex buffers / index buffer)
|
||||
// 0x200 framebuffer (set_framebuffer_state) - requires 0x400
|
||||
// 0x400 texture resources (texture + renderbuffer resource_*,
|
||||
// set_texture_params) - requires 0x80 AND 0x800
|
||||
// (the built-in sampler CSO a set_texture_params record names is minted by
|
||||
// the sampler family alone, ID-15; the four rows are MG_Impl/Pipe/PipeFill.cpp's
|
||||
// kMGPipeP4aFamilyDependencies, mirrored bit for bit by Espryt's resolvers)
|
||||
// 0x800 samplers (sampler CSO, sampler view, set_sampler_views /
|
||||
// bind_sampler_states / set_shader_images) - requires 0x400
|
||||
// 0x1000 programs (shader CSO, set_draw/dispatch_program, global constants)
|
||||
// 0x2000 buffer binding points (set_shader_buffers, the three indexed binding-point
|
||||
// classes and the dirty bits 15/16/17) - requires 0x80
|
||||
// (every MGPBufferRange::Res names a Buffer handle; P5e)
|
||||
// A dependency that is not met is REFUSED with one ERROR naming both bits and the
|
||||
// family runs its legacy arm; it is never half-run.
|
||||
// 1<<63 NOT a subsystem, a BEHAVIOUR: turn OFF client-side content addressing of
|
||||
// CSOs, so every pipeline-version change mints a fresh CSO and the map is
|
||||
// never probed. The negative control the CSO design is measured against.
|
||||
Uint64 PipePush = 0;
|
||||
// MOBILEGL_PIPE_VERIFY: per-draw, per-FIELD shadow comparison of the pushed state
|
||||
// against a snapshot taken from GLContext the old way, printing the first field
|
||||
// that differs and the draw serial. Roughly 5-10x slower and never shipped; it is
|
||||
// the semantic gate that replaces byte identity, and it catches the dangerous
|
||||
// direction - a dirty bit that fires too RARELY - which no purity gate can see.
|
||||
Bool PipeVerify = false;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The three knobs of the MOBILEGL_PIPE_VERIFY build (P1 brief D2). Compiled only
|
||||
// under MOBILEGL_PIPE_PUSH so the pull build's FeaturesTable does not change size.
|
||||
// MOBILEGL_PIPE_VERIFY_FATAL: the first divergence aborts (default). 0 logs and
|
||||
// counts instead, for triage and for the lane that must survive to read its own
|
||||
// log. Tri-state parse like PipeLegacyMemos: only an explicit falsy value turns it
|
||||
// off.
|
||||
Bool PipeVerifyFatal = true;
|
||||
// MOBILEGL_PIPE_VERIFY_CORRUPT: a field name from kMGPipeInputFieldNames[]; the
|
||||
// comparator perturbs that field in the SNAPSHOT arm before the entry compare, so a
|
||||
// green verify run goes red naming it (negative control A). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipeVerifyCorrupt;
|
||||
// MOBILEGL_PIPE_POISON_OMIT: <Verb>:<FieldName>; the filler skips the STAMP (not
|
||||
// the value) of that field for that verb, an omission indistinguishable from a
|
||||
// forgotten FillPoints.def row, so that verb's read of it is
|
||||
// Fatal{UnmigratedPipeInput} (negative control B). Unknown name is
|
||||
// Fatal{PipeVerifyBadKnob}.
|
||||
String PipePoisonOmit;
|
||||
// MOBILEGL_PIPE_HANDLE_ABA_CONTROL (negative control C, P2 brief D18): replace the
|
||||
// OBJECT IDENTITY in every DirectVulkan vertex-input memo key with a constant, on
|
||||
// whichever arm the run is on - the pre-handle (address, lifetime id) pair AND the
|
||||
// handle arm's {slot, gen} generation - so a replacement object inherits its dead
|
||||
// predecessor's resolved vertex bindings and HandleRecycleScenario.AbaControl asserts
|
||||
// the WRONG pixels. That is what proves the reproducer still reproduces. D18 wrote
|
||||
// this as "hash the raw BufferObject* instead of its lifetime id"; measured, the heap
|
||||
// block is never handed back, so that spelling collided with nothing and the control
|
||||
// went vacuous - see MagmaPipeArms.h's MagmaPipeAbaControlDefeatsIdentity for the
|
||||
// measurement and for what the control still leaves standing. Under
|
||||
// MOBILEGL_PIPE_PUSH only, so it cannot exist in a shipping pull build.
|
||||
Bool PipeHandleAbaControl = false;
|
||||
#endif
|
||||
// MOBILEGL_PIPE_STATS: dump the boundary counters (bytes, calls, roundtrips,
|
||||
// texture pulls, upload shapes, residual-block bytes, index mirror bytes).
|
||||
Bool PipeStats = false;
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS: keep the pre-handle registries and TwinLookupMemos
|
||||
// alive so the first handle waves have a real old-versus-new arm to be compared
|
||||
// against. ON by default for the whole migration window, deleted with the pull
|
||||
// path itself.
|
||||
Bool PipeLegacyMemos = true;
|
||||
// MOBILEGL_PIPE_TEXEL_RETAIN_MB: LRU budget for texels retained against a
|
||||
// server-initiated texture re-send. Default 0, i.e. OFF: MipmapStorage already
|
||||
// holds a complete CPU shadow, so this cache buys latency, never correctness.
|
||||
Uint32 PipeTexelRetainMb = 0;
|
||||
// MOBILEGL_PIPE_INDEX_MIRROR_MB: budget for the server-side index host mirror,
|
||||
// which is what lets primitive-restart rewriting and multi-draw flattening stay on
|
||||
// the server without shipping index bytes per draw. Over budget it degrades to
|
||||
// per-draw staging, counted separately in the stats.
|
||||
Uint32 PipeIndexMirrorMb = 64;
|
||||
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
|
||||
// steady-state cadence; the device retrace harness never reaches the teardown dump
|
||||
// and a trimmed fixture (minecraft-1.21.1-neoforge-create-indirect-in-world-align1024)
|
||||
// is shorter than 120 frames, so a run that needs its numbers at all sets this low
|
||||
// enough to land at least one window.
|
||||
Uint32 PipeStatsPeriod = 120;
|
||||
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
|
||||
// Empty (the default) means no dump; the per-120-frame summary line still goes to
|
||||
// the log whenever PipeStats is on, so a device run needs no writable path.
|
||||
String PipeStatsFile;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P5: the transport selector and the MOBILEGL_IPC_* family (ARCHITECTURE.md 16, 附 A)
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>.
|
||||
//
|
||||
// WHY `Transport` IS NOT A FeaturesTable MEMBER. ARCHITECTURE.md:580 requires that with
|
||||
// MOBILEGL_BUILD_DISAGGREGATED=OFF it be a `constexpr Monolith`, so that the single hook
|
||||
// in MG_Backend/Init.cpp compiles away entirely rather than becoming a branch nobody can
|
||||
// take. A FeaturesTable member is a runtime field in every build, which is the opposite
|
||||
// of that; it would also resize MG_Config::Features and break G1 (the pull build's
|
||||
// symbol set must not move) for the same reason the MOBILEGL_PIPE_VERIFY knobs above sit
|
||||
// behind their own #if.
|
||||
//
|
||||
// ONE CONSEQUENCE, STATED SO IT IS NOT REDISCOVERED: in a build without the option,
|
||||
// MOBILEGL_TRANSPORT=inproc is ACCEPTED BY THE ENVIRONMENT AND SILENTLY IGNORED - the
|
||||
// parser below does not exist to complain about it, and putting a complaint in the
|
||||
// unconditional part of ConfigLoader would move a pull-build symbol. That is the exact
|
||||
// shape of "the split lane ran monolith and went green", so the gate against it is a
|
||||
// BUILD-level check, not a runtime one: `nm --defined-only libMobileGL.so | grep -i
|
||||
// MG_Remote` must be non-empty in build-split (CONTRACT-P5.md table 3, and the CI job
|
||||
// P5 adds beside build-linux-verify).
|
||||
enum class TransportMode : Uint8 {
|
||||
Monolith = 0, // today's in-library backend; no MG_Remote object is constructed
|
||||
InProcess = 1, // P5: a real apply thread in this process, over the same G3 codec
|
||||
Spawn = 2, // P6: fork/exec MobileGLServer, socketpair
|
||||
UnixSocket = 3,// P6: connect to an existing AF_UNIX endpoint (Endpoint = <path>)
|
||||
NamedPipe = 4, // P6: Windows named pipe (Endpoint = <name>)
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Parsed once by MG_ConfigLoader::Init(). Defaults to Monolith even here: building the
|
||||
// transport in is not the same as using it, and every existing lane of a build-split
|
||||
// must keep running monolith unless it is asked for one.
|
||||
extern TransportMode Transport;
|
||||
// The <path> of `unix:` / the <name> of `pipe:`. Empty for the other three modes.
|
||||
extern String TransportEndpoint;
|
||||
|
||||
// P7 F1. DID **THIS PROCESS'S OWN CONFIGURATION** ASK FOR A SPLIT TRANSPORT.
|
||||
//
|
||||
// `Transport` alone cannot answer that, and the difference is exactly the one the F1 gate
|
||||
// turns on. A unit fixture assigns `Transport` by hand to put the code under test on its
|
||||
// split arm (ServerLoopTest's main does, and says why) while building no client at all; a
|
||||
// real run gets it from MG_ConfigLoader::Init(), and that run WILL bring a client half up.
|
||||
// Only the second may be held to "never decide a record family's fate from a placeholder
|
||||
// caps mirror" - in the first there is no handshake, no mirror to adopt and nothing the
|
||||
// rule could mean.
|
||||
//
|
||||
// Written once, by InitTransport(), and never cleared: a process does not stop having been
|
||||
// configured. Read by MG_Remote::Client::CapsMirror::RequireFirstSnapshot.
|
||||
extern Bool SplitTransportRequestedByConfig;
|
||||
|
||||
// The MOBILEGL_IPC_* family. A separate table rather than more FeaturesTable members,
|
||||
// for the G1 reason above and because every field here is meaningless without the
|
||||
// transport: a build that cannot reach the MG_Remote code cannot honour one of them.
|
||||
//
|
||||
// P5 lands exactly the knobs P5's own packages read. A later phase's knob is added HERE,
|
||||
// through the integrator, and not invented at its call site - ARCHITECTURE.md:615 holds
|
||||
// the full planned inventory (PRESENT_CREDIT, POLL_ESCALATE, SHADOW_SHM,
|
||||
// INLINE_PAYLOADS, TRACE, ATTACH, RESPAWN, IDLE_EXIT_S), and every one of those belongs
|
||||
// to P6 or later.
|
||||
//
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE's two values. See IpcTable::Surface.
|
||||
enum class IpcSurface : Uint8 {
|
||||
Offscreen = 0, // a window surface is the client's own window, as before (the default)
|
||||
Server = 1, // a window surface is the SERVER's window: WindowKind::ServerOwned
|
||||
};
|
||||
// IpcTable::AdoptTier when MOBILEGL_IPC_ADOPT_TIER is unset (or unparseable): T0 by default,
|
||||
// with QUIET fallbacks. Not a tier a user can type - the knob admits 0..2.
|
||||
inline constexpr Uint32 kAdoptTierUnset = 3;
|
||||
struct IpcTable {
|
||||
// MOBILEGL_IPC_SERVER_PATH: where to find libMobileGLServer. P6 consumes it; P5
|
||||
// lands the parse because t1's ctest ENVIRONMENT blocks and add_trace_replay_test's
|
||||
// SPLIT variant already carry it, and an environment variable that nothing parses is
|
||||
// indistinguishable from one that is parsed and ignored.
|
||||
String ServerPath;
|
||||
// Control endpoint and data-plane selection are independent of topology.
|
||||
String Control = "fork"; // fork | unix:<path> | tcp://host:port
|
||||
String Data = "auto"; // auto | shm | stream
|
||||
// MOBILEGL_IPC_RING_MB: SEG_CMD size. A RECORD MAY BE AT MOST HALF OF THIS
|
||||
// (RingProducer::MaxRecordBytes), so 8 MiB caps one record at 4 MiB; R-10 makes the
|
||||
// codec publish a max-record-bytes counter rather than assume that is enough.
|
||||
Uint32 RingMb = 8;
|
||||
// MOBILEGL_IPC_STAGE_MB: SEG_STAGE size. Every blob and every var-tail's bytes live
|
||||
// here ONE BLOB AT A TIME: a row whose content can outgrow the segment cuts it at the
|
||||
// stage chunk budget (MGPipeStageChunkBytes, a quarter of this), and a record type with
|
||||
// no cut is Fatal{RingOverrun, "SEG_STAGE"} rather than allowed to exceed it.
|
||||
Uint32 StageMb = 32;
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (P7 wave 4 M2, ID-P7-32): the SERVER's budget, in MiB,
|
||||
// for orphaned wire buffer stores - the old VkBuffer every glBufferData that crosses the
|
||||
// wire leaves behind - that a GPU command recorded but not yet retired may still name.
|
||||
// Stores no command names are destroyed at once and stores whose last submission has
|
||||
// retired are destroyed at the next park; this bounds the REST. When the parked bytes
|
||||
// exceed it after a sweep, the server flushes what it has recorded and waits for it
|
||||
// (WaitForWireBufferHostAccess's sync point, mid-frame), which retires every one. It is
|
||||
// not a frame count because a frame is not bounded: a snapshot-exiting pbuffer replay
|
||||
// delivers one present for 1.3 M calls. The same sync point also fires above a fixed
|
||||
// 1024 parked stores (VkBufferManager::kWireDeferredCountCeiling), because small
|
||||
// orphans never reach a byte budget. 0 IS THE NEGATIVE CONTROL for both, not "unlimited
|
||||
// by design": no forced sync, so a one-frame respecify-and-draw loop grows without bound
|
||||
// and MagmaWireReclaimScenario's watermark cases must go red.
|
||||
Uint32 WireDeferredMb = 64;
|
||||
// MOBILEGL_IPC_SPIN_US: spin before parking on a doorbell, either direction.
|
||||
Uint32 SpinUs = 50;
|
||||
// MOBILEGL_IPC_EVENT_WAIT_MS (PH-6, ID-P7-2): the SERVER's patience, in ms, for ONE
|
||||
// reverse-channel event that finds SEG_EVENT full under run-ahead. The apply thread
|
||||
// publishes, rings the client and parks until the client drains enough room; a client
|
||||
// that has not made room when this runs out - it stopped draining (NotDraining) or it
|
||||
// drains a slot at a time (TooSlow) - FORFEITS the reverse channel: that event and every
|
||||
// later one is dropped and counted (eventDropped), and the session stops by the ordinary
|
||||
// stop path rather than by Fatal{EventRingOverflow}. A peer that goes away (PeerGone) or
|
||||
// a session that is stopped (Stopped) ends the wait at once instead, so the knob may
|
||||
// exceed ServerLoop::Stop()'s 5000 ms join. It bounds the whole reservation, not one
|
||||
// park, so a trickling peer cannot stretch it - nor can a shm peer that writes the
|
||||
// (peer-writable) eventRingFull latch to 0 itself, though that one keeps the apply
|
||||
// thread spinning rather than parked until the budget runs out. Was a 30000 ms constant
|
||||
// spent twice. Read by the server only (ServerSpawn.cpp passes it to a spawned child);
|
||||
// the lockstep arm (no kCapRunAheadApply, i.e. Magma) never waits and keeps P5C's Fatal.
|
||||
Uint32 EventWaitMs = 2000;
|
||||
// MOBILEGL_IPC_PERSISTENT_BLOCK_KB: block granularity of the persistent-map push.
|
||||
// 0 IS A NEGATIVE CONTROL, NOT "unlimited": it disables the push, and
|
||||
// PersistentCoherentMapScenario must go RED under it (exit gate E3(a)).
|
||||
Uint32 PersistentBlockKb = 64;
|
||||
// MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS: 1 = the persistent-map push ships only
|
||||
// blocks whose xxHash64 changed since the last push, instead of the whole mapped
|
||||
// range every verb. 0 restores the whole-range push (A/B control).
|
||||
Uint32 PersistentHashSuppress = 1;
|
||||
// MOBILEGL_IPC_BATCH_WAITS: 1 = value-class records (kCtxState / kCtxCso / kCtxObject
|
||||
// with no reply slot) are published without waiting for their own apply; the barrier
|
||||
// is taken at the next pull-reading verb (kCtxVerb syncs, queries, screen rows, and
|
||||
// every reply-slot row), which is the only place BARRIER-PULLED fields are read. 0
|
||||
// restores the per-record barrier of R-1.
|
||||
Uint32 BatchWaits = 1;
|
||||
// MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the
|
||||
// client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N
|
||||
// creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest
|
||||
// answer is taken when the window fills.
|
||||
//
|
||||
// DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two
|
||||
// measured reasons and one structural one, in the order they were found:
|
||||
// 1. the reply buffer was a ring of the last 8 answers, and between two creates this
|
||||
// client emits on the order of a thousand records - so the answer was always gone
|
||||
// (`want=310` on every miss while the ring spanned [1175..1504]);
|
||||
// 2. a residency budget that tried to take answers back early could not help, because
|
||||
// what displaces an answer is what the SERVER sends, not what the client emits;
|
||||
// 3. and so the fix was RETENTION - a link keeps a declared answer until its reader
|
||||
// takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store).
|
||||
//
|
||||
// MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097,
|
||||
// frame-sent identical per op throughout):
|
||||
// window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped)
|
||||
// window 2 2,500 17.27 s
|
||||
// window 4 1,644 11.02 s
|
||||
// and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672
|
||||
// at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away
|
||||
// rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x.
|
||||
//
|
||||
// 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the
|
||||
// one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8
|
||||
// now; the old 4 was derived from a reply pool the window does not run on). Three
|
||||
// interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm:
|
||||
//
|
||||
// window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29)
|
||||
// window 5 1,277 11.31 s (10.61-12.12)
|
||||
// window 6 1,155 10.79 s ( 9.69-10.81)
|
||||
// window 8 859 10.96 s ( 8.92-11.03)
|
||||
//
|
||||
// 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise
|
||||
// of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a
|
||||
// deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE:
|
||||
// window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move.
|
||||
// The store is not the constraint either - its peak is logged as it crosses (StreamLink's
|
||||
// P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is
|
||||
// the constant ~350-400 creates that block for a reason depth does not reach, and the
|
||||
// 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests
|
||||
// on: an answer is retained because the wire DECLARED it will be read - NOT because its
|
||||
// caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a
|
||||
// flag keyed on wantReply would disable the very mechanism it exists to enable
|
||||
// (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11).
|
||||
Uint32 CreateWindow = 10;
|
||||
// MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels
|
||||
// or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again -
|
||||
// the client waits for the pixels and uploads them into the buffer itself, as before P9.
|
||||
// 0 (default) sends read_pixels_to_buffer / get_texture_image_to_buffer, waits for
|
||||
// nothing, and marks the buffer GPU-written. The 1 arm is the A/B control and the
|
||||
// red-once of the wait-count gate (PackBufferReadbackScenario): it must show a reply
|
||||
// wait per read.
|
||||
Uint32 PboReadbackSync = 0;
|
||||
// MOBILEGL_IPC_POLL_ESCALATE (P10, MG_Remote/CONTRACT-P10.md §1): a glClientWaitSync with a
|
||||
// zero timeout or a glGetSynciv(GL_SYNC_STATUS) is answered by the client from what the
|
||||
// server reported (kEventFenceSignaled) - "signaled" when it has been, "not yet" otherwise
|
||||
// - and only the Nth "not yet" in a row on one fence crosses as a real round trip, so a
|
||||
// poll loop cannot outlive a late report. 0 = every poll crosses, as before P10: the A/B
|
||||
// arm and the red-once of the no-reply gate (FencePollScenario).
|
||||
Uint32 PollEscalate = 64;
|
||||
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
|
||||
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
|
||||
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
|
||||
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
|
||||
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
|
||||
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
|
||||
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
|
||||
Uint32 AdoptTier = kAdoptTierUnset;
|
||||
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
|
||||
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
|
||||
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
|
||||
Uint32 AllowAdoptT0 = 1;
|
||||
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
|
||||
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
|
||||
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
|
||||
// GLContext by the client's residual fill and a free-running queue lets the server
|
||||
// read a FUTURE value of them.
|
||||
Uint32 VerbBarrier = 1;
|
||||
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
|
||||
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
|
||||
// It is one half of a conjunction and never a switch on its own - the client arms
|
||||
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
|
||||
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
|
||||
// saying so.
|
||||
//
|
||||
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
|
||||
// every record are identical on both arms, and the only difference is whether the
|
||||
// client waits. That is what makes "is the picture the same" a question about the wait
|
||||
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
|
||||
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
|
||||
// the first barriered row's stale pull.
|
||||
//
|
||||
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
|
||||
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
|
||||
// arm that stops filling it.
|
||||
Uint32 RunAhead = 1;
|
||||
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
|
||||
// have in flight before it waits for a swap to come back. 1 = the client publishes
|
||||
// frame N+1's records while the server applies and swaps frame N - one frame of
|
||||
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
|
||||
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
|
||||
// client that is already CPU-bound and costs a frame of latency, which is why the
|
||||
// default is 1 and not "as deep as the ring".
|
||||
Uint32 PresentCredit = 1;
|
||||
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
|
||||
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
|
||||
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
|
||||
// latch decides dead (device lost) from alive-but-silent (a named diagnostic). The
|
||||
// contract named this knob from P6 on; the client hard-coded its default until P7.
|
||||
Uint32 ControlTimeoutMs = 5000;
|
||||
// MOBILEGL_IPC_COLD_START_MS (P7): the same wait for the three ops a server may bring its
|
||||
// NATIVE backend up inside - CreatePbufferSurface, CreateWindowSurface, MakeCurrent -
|
||||
// until the session's first MakeCurrent is answered ok. The bring-up is lazy (Espryt's
|
||||
// eglInitialize, Magma's Vulkan instance and device), ~100 ms on a workstation and more
|
||||
// than the steady 5 s on a loaded CI runner (retrace-split spawn legs, runs 35671704873
|
||||
// and 35706183230). Never shorter than CONTROL_TIMEOUT_MS; its expiry is the same
|
||||
// non-fatal named answer. The default is the spawn connect budget, for the same reason:
|
||||
// "the server has to create a backend, and a cold software rasteriser is not fast".
|
||||
Uint32 ColdStartMs = 20000;
|
||||
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
|
||||
// build, the seven sticky forwards that are otherwise exempt - from "count it in
|
||||
// rsp" to Fatal (R-7.3).
|
||||
Bool StrictErrors = false;
|
||||
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
|
||||
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
|
||||
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
|
||||
// only because the two roles share one object turns into a named
|
||||
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
|
||||
// Meaningless under monolith transport (the two roles are one thread there, so the
|
||||
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
|
||||
// (the comparator owns the one fill block it compares against). 0 is not merely the
|
||||
// default, it is the negative control: the lane's distinctness case must go red
|
||||
// without it (P5F §6).
|
||||
Bool RoleSplitState = false;
|
||||
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
|
||||
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
|
||||
// inproc implementation did not quietly keep using a pointer past its lifetime.
|
||||
Bool Audit = false;
|
||||
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
|
||||
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
|
||||
// because the resolved mask is logged by whoever starts the apply thread, and the
|
||||
// string is what an operator typed.
|
||||
String ServerAffinity = "auto";
|
||||
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
|
||||
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
|
||||
// any native window including NULL, send ONE CreateWindowSurface naming
|
||||
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
|
||||
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
|
||||
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
|
||||
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
|
||||
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
|
||||
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
|
||||
IpcSurface Surface = IpcSurface::Offscreen;
|
||||
};
|
||||
extern IpcTable Ipc;
|
||||
|
||||
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
|
||||
// surface is the server's when the knob says so AND there is a remote server to own it.
|
||||
inline Bool ServerOwnedWindowSurfaces() {
|
||||
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
|
||||
}
|
||||
#else
|
||||
// The whole point: in a build without MG_Remote this folds at compile time, so
|
||||
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
|
||||
// discarded statement and the pull build gains no symbol, no branch and no byte.
|
||||
inline constexpr TransportMode Transport = TransportMode::Monolith;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
+7
-502
@@ -6,22 +6,10 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <sys/system_properties.h>
|
||||
#endif
|
||||
#include "Config.h"
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
|
||||
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
|
||||
// pull build's translation unit is unchanged.
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#endif
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
|
||||
#include <cstring>
|
||||
|
||||
#ifndef _WIN32
|
||||
extern char** environ;
|
||||
@@ -31,16 +19,6 @@ namespace MobileGL::MG_Config {
|
||||
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
|
||||
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
|
||||
FeaturesTable Features;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
|
||||
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
|
||||
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
|
||||
// and there is nothing to define.
|
||||
TransportMode Transport = TransportMode::Monolith;
|
||||
String TransportEndpoint;
|
||||
Bool SplitTransportRequestedByConfig = false;
|
||||
IpcTable Ipc;
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
namespace MobileGL::MG_ConfigLoader {
|
||||
@@ -50,115 +28,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
|
||||
}
|
||||
|
||||
// ---- the system configuration -----------------------------------------------------------
|
||||
//
|
||||
// A library that is every process's GL implementation (installed as the system's EGL/GLX
|
||||
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
|
||||
// most are started by something that has never heard of it. So the settings a whole machine
|
||||
// shares live in files, and the environment only overrides them:
|
||||
//
|
||||
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
|
||||
//
|
||||
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
|
||||
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
|
||||
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
|
||||
// holds just the backend's name (the file the backend switch rewrites), and names it over a
|
||||
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
|
||||
//
|
||||
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
|
||||
// file off. The defaults exist only on desktop Linux: an Android app configures its process
|
||||
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
|
||||
namespace {
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
|
||||
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
|
||||
#else
|
||||
constexpr const char* kDefaultConfigFile = "";
|
||||
constexpr const char* kDefaultBackendFile = "";
|
||||
#endif
|
||||
|
||||
String FilePathSetting(const char* variable, const char* fallback) {
|
||||
const char* value = std::getenv(variable);
|
||||
return value != nullptr ? String(value) : String(fallback);
|
||||
}
|
||||
|
||||
String Trimmed(const String& text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == String::npos) return String();
|
||||
const auto last = text.find_last_not_of(" \t\r\n");
|
||||
return text.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
Bool IsServerProcess() {
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
return role != nullptr && std::strcmp(role, "server") == 0;
|
||||
}
|
||||
|
||||
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
|
||||
// not an error: the defaults are the absence of one.
|
||||
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
|
||||
if (path.empty()) return;
|
||||
std::FILE* file = std::fopen(path.c_str(), "r");
|
||||
if (file == nullptr) return;
|
||||
char buffer[4096];
|
||||
Uint32 lineNumber = 0;
|
||||
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
|
||||
++lineNumber;
|
||||
const String line = Trimmed(buffer);
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
const auto equals = line.find('=');
|
||||
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
|
||||
if (key.empty() || !IsAcceptedPrefix(key)) {
|
||||
if (log) {
|
||||
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
|
||||
lineNumber);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
String value = Trimmed(line.substr(equals + 1));
|
||||
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
|
||||
value = value.substr(1, value.size() - 2);
|
||||
}
|
||||
out.emplace(key, value);
|
||||
}
|
||||
std::fclose(file);
|
||||
}
|
||||
|
||||
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
|
||||
if (path.empty()) return;
|
||||
std::FILE* file = std::fopen(path.c_str(), "r");
|
||||
if (file == nullptr) return;
|
||||
char buffer[256];
|
||||
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
|
||||
std::fclose(file);
|
||||
if (!read) return;
|
||||
const String name = Trimmed(buffer);
|
||||
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
|
||||
}
|
||||
|
||||
// Everything the system files say, the backend file ahead of client.conf (first emplace
|
||||
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
|
||||
// and a file that changed between two processes' starts must not be served stale.
|
||||
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
|
||||
if (IsServerProcess()) return;
|
||||
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
|
||||
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Bool LookupSetting(const char* key, String& outValue) {
|
||||
if (const char* value = std::getenv(key)) {
|
||||
outValue = value;
|
||||
return true;
|
||||
}
|
||||
UnorderedMap<String, String> system;
|
||||
ReadSystemConfig(system, false);
|
||||
const auto it = system.find(key);
|
||||
if (it == system.end()) return false;
|
||||
outValue = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void InitializeAcceptedEnvVariables() {
|
||||
if (!acceptedEnvVariablesMap) {
|
||||
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
|
||||
@@ -189,15 +58,6 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Below the environment: a key the environment set is never replaced.
|
||||
UnorderedMap<String, String> system;
|
||||
ReadSystemConfig(system, true);
|
||||
for (const auto& [key, value] : system) {
|
||||
if (acceptedEnvVariablesMap->emplace(key, value).second) {
|
||||
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
|
||||
@@ -299,139 +159,35 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
return static_cast<Uint32>(parsedValue);
|
||||
}
|
||||
|
||||
// Same contract as QueryEnvUint32, over 64 bits and accepting an explicit 0x prefix: the
|
||||
// one consumer is a subsystem BITMASK, and a bitmask written in decimal is unreadable.
|
||||
// Decimal otherwise - never strtoull's base 0, whose "leading zero means octal" rule
|
||||
// silently read MOBILEGL_PIPE_PUSH=010 as 8 - and a '-' anywhere is rejected rather than
|
||||
// wrapped, which strtoull would otherwise do without complaint (-1 -> every bit set).
|
||||
inline Uint64 QueryEnvUint64(const String& key, Uint64 defaultValue) {
|
||||
auto it = acceptedEnvVariablesMap->find(key);
|
||||
if (it == acceptedEnvVariablesMap->end()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
const String& value = it->second;
|
||||
const char* text = value.c_str();
|
||||
int base = 10;
|
||||
if (value.size() > 2 && text[0] == '0' && (text[1] == 'x' || text[1] == 'X')) {
|
||||
text += 2;
|
||||
base = 16;
|
||||
}
|
||||
char* parseEnd = nullptr;
|
||||
errno = 0;
|
||||
const bool negative = value.find('-') != String::npos;
|
||||
const unsigned long long parsedValue = negative ? 0 : std::strtoull(text, &parseEnd, base);
|
||||
if (negative || parseEnd == text || *parseEnd != '\0' || errno == ERANGE) {
|
||||
MGLOG_W("Config: Ignoring invalid env variable %s='%s'; expected a non-negative integer "
|
||||
"(decimal, or 0x-prefixed hexadecimal), using default %llu",
|
||||
key.c_str(), value.c_str(), static_cast<unsigned long long>(defaultValue));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return static_cast<Uint64>(parsedValue);
|
||||
}
|
||||
|
||||
inline void InitFeatures() {
|
||||
auto& features = MG_Config::Features;
|
||||
features.DisableTimerQuery = QueryEnvFlag("MOBILEGL_DISABLE_TIMERQUERY");
|
||||
features.EsprytEnableTextureView = QueryEnvFlag("MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW");
|
||||
features.EnableSpirvValidation = QueryEnvFlag("MOBILEGL_ENABLE_SPIRV_VALIDATION");
|
||||
features.EsprytUseAngle = QueryEnvFlag("MOBILEGL_ESPRYT_USE_ANGLE");
|
||||
features.UseAngle = QueryEnvFlag("MOBILEGL_USE_ANGLE");
|
||||
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
|
||||
QueryEnvVariable("MOBILEGL_TRACE_ANGLE_VARIANT", features.TraceAngleVariant, "");
|
||||
#endif
|
||||
features.MagmaDisableSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_SUBGROUP");
|
||||
features.MagmaEmulateSubgroup = QueryEnvFlag("MOBILEGL_MAGMA_EMULATE_SUBGROUP");
|
||||
features.MagmaFixIterationRPSubgroupScratch =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH");
|
||||
features.MagmaIterationRPFixBarrier = QueryEnvFlag("MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER");
|
||||
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
|
||||
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
|
||||
features.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
|
||||
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
|
||||
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
|
||||
features.MagmaMaxDrawsPerCommandBuffer =
|
||||
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
|
||||
features.MagmaDescriptorTrimFrames = QueryEnvUint32("MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES", 120, 0, 1u << 20);
|
||||
features.EsprytAvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.EsprytAvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS");
|
||||
features.EsprytUnlocatedIoBlocks = QueryEnvQuirkOverride("MOBILEGL_ESPRYT_UNLOCATED_IO_BLOCKS");
|
||||
features.PointSizeDemotion = QueryEnvQuirkOverride("MOBILEGL_POINT_SIZE_DEMOTION");
|
||||
features.AvoidSamplerMipmapMinFilter =
|
||||
QueryEnvFlag("MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER");
|
||||
features.CoherentAsFlush = QueryEnvFlag("MOBILEGL_COHERENT_AS_FLUSH");
|
||||
features.TraceSkipAutodestroy = QueryEnvFlag("MOBILEGL_TRACE_SKIP_AUTODESTROY");
|
||||
features.EsprytDisableUboRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UBO_RING");
|
||||
features.EsprytDisableUnpackRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UNPACK_RING");
|
||||
features.EsprytDisableUploadRing = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING");
|
||||
features.EsprytDisableInvalidateFlush = QueryEnvFlag("MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH");
|
||||
features.DisableLargeBufferAdoption = QueryEnvFlag("MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION");
|
||||
features.EsprytForceDepthStencilReadbackEmulation =
|
||||
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
|
||||
features.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
|
||||
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
|
||||
features.SubgroupPrefixScanQuirk = QueryEnvQuirkOverride("MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN");
|
||||
features.MagmaDisableBlendedDepthWriteQuirk =
|
||||
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
|
||||
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.DisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_DISABLE_ROBUST_BUFFER_ACCESS");
|
||||
features.MagmaMultiDrawMode = QueryEnvMultiDrawMode("MOBILEGL_MAGMA_MULTIDRAW_MODE");
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
features.ShaderTranslationCache = QueryEnvQuirkOverride("MOBILEGL_SHADER_CACHE");
|
||||
features.EsprytViewportArrayEmulation =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_FORCE_VIEWPORT_ARRAY_EMULATION");
|
||||
features.EsprytWidenPacked16Storage =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE");
|
||||
features.MagmaPrimGenQueryReroute = QueryEnvQuirkOverride("MOBILEGL_MAGMA_PRIMGEN_QUERY_REROUTE");
|
||||
// MGPipe. Nothing here needs adding to an allow-list: InitializeAcceptedEnvVariables
|
||||
// accepts every MOBILEGL_ / LIBGL_ prefixed variable in the environment, so a name
|
||||
// that starts with MOBILEGL_ is visible to these queries by construction.
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// A push build with the knob unset runs every subsystem migrated so far, so the
|
||||
// shipped path is the one the gates measure; MOBILEGL_PIPE_PUSH=0 in the
|
||||
// environment is the all-subsystems-pull control that reproduces P1 exactly, and
|
||||
// kMGPipeSubsystemsMigratedAtP3a (0x1ff) is the phase-by-phase control - P4a's four
|
||||
// subsystems off, everything P3a landed still on.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", MG_Pipe::kMGPipeSubsystemsMigratedAtP5e);
|
||||
#else
|
||||
// Meaningless in a pull build: there is nothing to push. Config.h documents 0 as
|
||||
// "pull everything" and that stays literally true.
|
||||
features.PipePush = QueryEnvUint64("MOBILEGL_PIPE_PUSH", 0);
|
||||
#endif
|
||||
features.PipeVerify = QueryEnvFlag("MOBILEGL_PIPE_VERIFY");
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Defaults ON: read as a tri-state so only an explicitly falsy value turns it off.
|
||||
features.PipeVerifyFatal =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_VERIFY_FATAL") != MG_Config::QuirkOverride::ForceOff;
|
||||
QueryEnvVariable("MOBILEGL_PIPE_VERIFY_CORRUPT", features.PipeVerifyCorrupt, "");
|
||||
QueryEnvVariable("MOBILEGL_PIPE_POISON_OMIT", features.PipePoisonOmit, "");
|
||||
features.PipeHandleAbaControl = QueryEnvFlag("MOBILEGL_PIPE_HANDLE_ABA_CONTROL");
|
||||
#endif
|
||||
features.PipeStats = QueryEnvFlag("MOBILEGL_PIPE_STATS");
|
||||
// Defaults ON, so the flag has to be read as a tri-state rather than as a plain
|
||||
// truthy check: unset must keep the memos, and only an explicitly falsy value may
|
||||
// drop them.
|
||||
features.PipeLegacyMemos =
|
||||
QueryEnvQuirkOverride("MOBILEGL_PIPE_LEGACY_MEMOS") != MG_Config::QuirkOverride::ForceOff;
|
||||
features.PipeTexelRetainMb = QueryEnvUint32("MOBILEGL_PIPE_TEXEL_RETAIN_MB", 0, 0, 4096);
|
||||
features.PipeIndexMirrorMb = QueryEnvUint32("MOBILEGL_PIPE_INDEX_MIRROR_MB", 64, 0, 4096);
|
||||
features.PipeStatsPeriod = QueryEnvUint32("MOBILEGL_PIPE_STATS_PERIOD", 120, 1, 1000000);
|
||||
QueryEnvVariable("MOBILEGL_PIPE_STATS_FILE", features.PipeStatsFile, "");
|
||||
}
|
||||
|
||||
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; \
|
||||
@@ -445,263 +201,12 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
#undef ENTRY
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
|
||||
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
|
||||
// one fallback that names what it did instead. The two prefixed forms are the only
|
||||
// reason this is not literally that function's ENTRY macro.
|
||||
//
|
||||
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
|
||||
// rather than silent, because the failure this avoids is a P6 lane that set
|
||||
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
|
||||
// The mode is left at Monolith so nothing half-initializes.
|
||||
inline void InitTransport() {
|
||||
String value;
|
||||
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
|
||||
String lowered = value;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
|
||||
MG_Config::TransportEndpoint.clear();
|
||||
if (lowered.empty() || lowered == "monolith") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
if (lowered == "inproc") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::InProcess;
|
||||
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
|
||||
// one, and NOT on the two that name a transport and then stay monolith - a process
|
||||
// that was refused its transport brings no client half up either.
|
||||
MG_Config::SplitTransportRequestedByConfig = true;
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread");
|
||||
return;
|
||||
}
|
||||
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
|
||||
// rather than "unknown", which are different bugs on the operator's side.
|
||||
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
|
||||
// rendezvous of its own and connects to it - the two processes are
|
||||
// independent, so this is `Dial = Connect` with a launcher attached
|
||||
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
|
||||
if (lowered == "spawn") {
|
||||
MG_Config::Transport = MG_Config::TransportMode::Spawn;
|
||||
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
|
||||
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
|
||||
// run_trace_case.cmake asserts on a distinctive sentence from this
|
||||
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
|
||||
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
|
||||
// every MOBILEGL_* in EVERY build including the pull one - so the
|
||||
// bare KEY=VALUE is satisfied by a monolith library that parsed
|
||||
// nothing (review M-5). A spawn arm with no line of its own could
|
||||
// only have been asserted with the inproc marker, which spawn never
|
||||
// emits: the arm would have been red for a missing sentence rather
|
||||
// than green for a working transport.
|
||||
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
|
||||
"ring to an apply thread in ANOTHER PROCESS");
|
||||
return;
|
||||
}
|
||||
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
|
||||
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
|
||||
// ELSE is listening on needs the server lifecycle to be somebody
|
||||
// else's too, which is P12's. Named, never a silent degrade.
|
||||
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
|
||||
"staying on monolith. This run is NOT a split run.",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
return;
|
||||
}
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
|
||||
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
|
||||
value.c_str());
|
||||
MG_Config::Transport = MG_Config::TransportMode::Monolith;
|
||||
}
|
||||
|
||||
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
|
||||
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
|
||||
// how an A/B stops being one, and the ranges below are the diagnostics.
|
||||
inline void InitIpc() {
|
||||
auto& ipc = MG_Config::Ipc;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
|
||||
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
|
||||
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
|
||||
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
|
||||
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
|
||||
// create_shader_state archive already needs far more than a toy ring. The ceilings
|
||||
// are sanity, not policy.
|
||||
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
|
||||
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
|
||||
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
|
||||
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
|
||||
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
|
||||
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
|
||||
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
|
||||
// patience at all would forfeit a healthy run-ahead client the first time its ring
|
||||
// filled between two of the client's drains, which is an ordinary backlog.
|
||||
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
|
||||
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
|
||||
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
|
||||
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
|
||||
// Whole-range push is the 0 arm; with it on (default) only blocks whose
|
||||
// xxHash64 changed since the last push are shipped.
|
||||
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
|
||||
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
|
||||
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
|
||||
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
|
||||
// was inert before, the device numbers, and the sweep that put the default at 6 rather
|
||||
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
|
||||
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
|
||||
// declared answers, and the device sweep showed the wall clock still falling at the old
|
||||
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
|
||||
// StreamLink's crossing log are the checks on this number.
|
||||
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
|
||||
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
|
||||
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
|
||||
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
|
||||
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
|
||||
// The verify harness compares the pushed block against the applier per verb; a
|
||||
// batched queue lets the comparer read a supplied field mid-apply, which is a
|
||||
// torn read rather than a divergence. The batch is therefore off whenever the
|
||||
// shadow comparer is armed.
|
||||
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
|
||||
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
|
||||
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
|
||||
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
|
||||
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
|
||||
// P11 B2: the server's own T0 allow switch (Config.h).
|
||||
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
|
||||
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
|
||||
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
|
||||
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
|
||||
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
|
||||
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
|
||||
// run-ahead's whole point is that the client stops filling gPipeInputs for an
|
||||
// unbarriered record, and the comparator has nothing left to compare when it does.
|
||||
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
|
||||
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
|
||||
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
|
||||
// admitted: a credit of zero would mean "publish no present at all".
|
||||
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
|
||||
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
|
||||
// Both are floored at 100 ms: a zero bound would declare every live server silent.
|
||||
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
|
||||
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
|
||||
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
|
||||
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
|
||||
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
|
||||
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
|
||||
// verify harness: the comparator's entry compare and compare-at-read hook are built on
|
||||
// there being ONE filled block (the hook pins itself to &gPipeInputs,
|
||||
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
|
||||
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
|
||||
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
|
||||
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
|
||||
"the dual-block rehearsal is OFF for this run");
|
||||
ipc.RoleSplitState = false;
|
||||
}
|
||||
|
||||
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
|
||||
//
|
||||
// The device-lost latch is deliberately one-way - a session whose server died has
|
||||
// nothing to recover into, because every handle the client minted names an object in a
|
||||
// process that no longer exists. Re-pushing the world onto a fresh server is the work
|
||||
// this knob would turn on, and no stage has written it.
|
||||
//
|
||||
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
|
||||
// the whole IPC family: an environment variable nothing consumes is indistinguishable
|
||||
// from one that is consumed and does nothing, and an operator who set this one would
|
||||
// otherwise conclude that recovery had been tried and had not helped.
|
||||
{
|
||||
String respawn;
|
||||
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
|
||||
if (!respawn.empty() && respawn != "0") {
|
||||
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
|
||||
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
|
||||
"this client minted names an object inside the server process, so a new "
|
||||
"server would have to be re-pushed the entire world before a single verb "
|
||||
"could land. This run will latch device-lost and stay there.",
|
||||
respawn.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
|
||||
// the semantics). An unknown value is named and read as the default, like every knob here.
|
||||
{
|
||||
String surface;
|
||||
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
|
||||
std::transform(surface.begin(), surface.end(), surface.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
if (surface.empty() || surface == "offscreen") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
} else if (surface == "server") {
|
||||
ipc.Surface = MG_Config::IpcSurface::Server;
|
||||
} else {
|
||||
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
|
||||
"offscreen|server, using offscreen",
|
||||
surface.c_str());
|
||||
ipc.Surface = MG_Config::IpcSurface::Offscreen;
|
||||
}
|
||||
// (Not said by a server process: a spawned server inherits the client's environment,
|
||||
// parses this before RunSession sets its own transport, and has no client half anyway.)
|
||||
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
|
||||
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
|
||||
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
|
||||
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
|
||||
// Said, not silently honoured: there is no remote server here to own a window.
|
||||
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
|
||||
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
|
||||
"and this run has none. Window surfaces stay the client's own");
|
||||
}
|
||||
}
|
||||
|
||||
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
|
||||
// One line, on the arm where these numbers decide behaviour, because every one of
|
||||
// them is a number a bug report has to quote.
|
||||
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
|
||||
// missing while the value was a number nobody varied, and the moment it became one that
|
||||
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
|
||||
// log that does not name it makes every run ambiguous about its own configuration.
|
||||
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
|
||||
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
|
||||
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
|
||||
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
|
||||
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
|
||||
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
|
||||
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
|
||||
ipc.AllowAdoptT0,
|
||||
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
|
||||
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
|
||||
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
|
||||
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
|
||||
if (ipc.VerbBarrier == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
|
||||
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
|
||||
"live GLContext, so an unbarriered queue lets the server read future values");
|
||||
}
|
||||
if (ipc.WireDeferredMb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
|
||||
"server never forces a sync for orphaned wire buffer stores, so a long frame "
|
||||
"that respecifies and draws holds every one of them until the frame ends");
|
||||
}
|
||||
if (ipc.PersistentBlockKb == 0) {
|
||||
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
|
||||
"the persistent-map push is OFF and a coherent-map scenario must go red");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Init() {
|
||||
MGLOG_D("Loading configuration from environment variables...");
|
||||
InitializeAcceptedEnvVariables();
|
||||
|
||||
InitBackendType();
|
||||
InitFeatures();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
|
||||
// configuration, and before the accepted-env map is destroyed just below.
|
||||
InitTransport();
|
||||
InitIpc();
|
||||
#endif
|
||||
|
||||
// Destroy the map since we won't need it anymore
|
||||
acceptedEnvVariablesMap.reset();
|
||||
|
||||
+4
-42
@@ -9,20 +9,10 @@
|
||||
#pragma once
|
||||
|
||||
// ============== Platform-specific definitions and macros ============== //
|
||||
// No __ANDROID_API__ pin here on purpose. The effective API level is owned by
|
||||
// the build system (gradle minSdk 26 -> -DANDROID_PLATFORM=android-26, enforced
|
||||
// by the configure-time guard in CMakeLists.txt), not by a macro.
|
||||
//
|
||||
// History: this used to `#define __ANDROID_API__ 26` to *raise* the level back
|
||||
// when the build configured something lower, so that pthread_getname_np (which
|
||||
// bionic guards with __INTRODUCED_IN(26)) would be declared. Once a later
|
||||
// change added an `#undef` in front of it, the same line started *lowering* the
|
||||
// level whenever the build configured higher than 26 - and that is an
|
||||
// include-order split-brain, not a compatibility knob: a TU that includes any
|
||||
// libc++ header before Includes.h latches libc++'s feature macros at the
|
||||
// configure-time level, and only the bionic headers pulled in afterwards see
|
||||
// the lowered value. The two halves then disagree (e.g. libc++ believes
|
||||
// pthread_cond_clockwait exists while bionic has since hidden its declaration).
|
||||
#ifdef __ANDROID__
|
||||
#undef __ANDROID_API__
|
||||
#define __ANDROID_API__ 26 // force Android API level to 26 for compatibility
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
@@ -34,11 +24,6 @@
|
||||
#ifdef _WIN32
|
||||
#define MOBILEGL_EXPORT extern "C" __declspec(dllexport)
|
||||
#else
|
||||
// Default visibility, which is what the platform's own GL and EGL headers declare their entry
|
||||
// points with. The one place where that is not enough - a vendor library handing glvnd its own
|
||||
// entry points, where a reference resolving against the process instead of this library would be a
|
||||
// call back into the dispatcher - is answered at link time with -Bsymbolic-functions rather than
|
||||
// here, because a second visibility on the same declaration does not compile.
|
||||
#define MOBILEGL_EXPORT extern "C" __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
@@ -52,23 +37,6 @@
|
||||
#define MOBILEGL_WGL_API MOBILEGL_API
|
||||
|
||||
// ====================== MobileGL configurations ======================= //
|
||||
// The numeric log levels live here, not only in Log.h: MOBILEGL_ASSERT below compares
|
||||
// MOBILEGL_LOG_ACTIVE_LEVEL against MOBILEGL_LOG_LEVEL_DEBUG, and in a translation unit
|
||||
// that includes Defines.h without Log.h both tokens would silently evaluate to 0 in the
|
||||
// preprocessor conditional - enabling the assert in exactly the INFO-level builds it is
|
||||
// documented to be compiled out of. Log.h redefines them identically, which is legal.
|
||||
//
|
||||
// Severity order, ascending: DEBUG < INFO < WARN < ERROR < FATAL. MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
// names the lowest severity compiled in, so the production default INFO keeps I/W/E/F and
|
||||
// drops only D. Any edit here must be mirrored in Log.h.
|
||||
#ifndef MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_LOG_LEVEL_DEBUG 0
|
||||
#define MOBILEGL_LOG_LEVEL_INFO 1
|
||||
#define MOBILEGL_LOG_LEVEL_WARN 2
|
||||
#define MOBILEGL_LOG_LEVEL_ERROR 3
|
||||
#define MOBILEGL_LOG_LEVEL_FATAL 4
|
||||
#endif
|
||||
|
||||
#ifndef MOBILEGL_LOG_ACTIVE_LEVEL
|
||||
#define MOBILEGL_LOG_ACTIVE_LEVEL MOBILEGL_LOG_LEVEL_INFO
|
||||
#endif
|
||||
@@ -100,12 +68,6 @@
|
||||
#endif
|
||||
|
||||
// =============================== Utils ================================ //
|
||||
// Asserts are live in exactly the builds where MGLOG_D is live, i.e. DEBUG builds only;
|
||||
// an INFO build (the production default) compiles them out. DEBUG is the lowest severity
|
||||
// in the ordering above, so "ACTIVE <= DEBUG" is true only for ACTIVE == DEBUG - the same
|
||||
// gate MGLOG_D uses in Log.h. That equivalence is what makes this gate survive the
|
||||
// 2026-08-13 renumbering unchanged; the contract is and stays
|
||||
// "INFO builds: asserts OFF; DEBUG builds: asserts ON".
|
||||
#if MOBILEGL_LOG_ACTIVE_LEVEL <= MOBILEGL_LOG_LEVEL_DEBUG
|
||||
#define MOBILEGL_ASSERT(condition, ...) \
|
||||
do { \
|
||||
|
||||
+2
-2
@@ -49,8 +49,8 @@
|
||||
#include <stacktrace>
|
||||
#endif
|
||||
|
||||
// Include ska::flat_hash_map
|
||||
#include <ska/flat_hash_map.hpp>
|
||||
// Include FastSTL
|
||||
#include <FastSTL/UnorderedMap.h>
|
||||
|
||||
// Include xxHash
|
||||
#include <xxhash.h>
|
||||
|
||||
+1
-126
@@ -15,18 +15,8 @@
|
||||
#include <MG_Impl/GLImpl/Texture/ProxyTexture.h>
|
||||
#include <MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h>
|
||||
#include <MG_Impl/GLImpl/Sync/GL_Sync.h>
|
||||
#include <MG_Impl/GLImpl/Query/GL_Query.h>
|
||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramTranslationCache.h>
|
||||
#include <MG_Util/ShaderTranspiler/TranslationCache.h>
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
|
||||
#endif
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Client/ServerProbe.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
@@ -49,20 +39,6 @@ namespace MobileGL {
|
||||
if (logLifecycle) {
|
||||
MGLOG_I("MobileGL closing...");
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5: the split roles come down FIRST - ARCHITECTURE.md:537's order puts the whole
|
||||
// of it before MobileGL::Destroy(), and this function IS MobileGL::Destroy. A no-op
|
||||
// in a monolith RUN; absent from a monolith BUILD, because G1 admits no new pull
|
||||
// symbol and no resized one (the first version called it unconditionally and moved
|
||||
// DestroyImpl by 32 bytes). See BackendObjects.h for why the position rather than
|
||||
// the call is the load-bearing part.
|
||||
MG_Backend::ShutdownSplitRoles();
|
||||
#endif
|
||||
// Before any subsystem the counters name goes away, and before the last frame's
|
||||
// numbers can be lost: emits the final summary line and, when
|
||||
// MOBILEGL_PIPE_STATS_FILE is set, the JSON dump. A no-op when the counters are
|
||||
// off, and idempotent.
|
||||
MG_Util::PipeStats::Shutdown();
|
||||
// First, before anything else is torn down. In-flight compile/link jobs own
|
||||
// their own inputs and are safe against everything below EXCEPT glslang's
|
||||
// process globals and the TShader/TProgram objects hanging off pGLContext,
|
||||
@@ -75,11 +51,6 @@ namespace MobileGL {
|
||||
// before a re-initialized library could pair them with the wrong
|
||||
// backend's DeleteSync).
|
||||
MG_Impl::GLImpl::DestroyAllSyncObjects();
|
||||
// Queries die with their contexts for the same reason, and their registry
|
||||
// is the same shape of process-global map: drain it here too, while the
|
||||
// function table can still pair each backend handle with the backend that
|
||||
// minted it.
|
||||
MG_Impl::GLImpl::DestroyAllQueryObjects();
|
||||
MG_Backend::pActiveBackendObject.reset();
|
||||
MG_State::pGLContext.reset();
|
||||
MG_State::pEGLContext.reset();
|
||||
@@ -95,28 +66,8 @@ namespace MobileGL {
|
||||
// built-in symbol tables the prewarm latch stands for, so leaving it set would
|
||||
// make the next Initialize() skip a prewarm it genuinely needs.
|
||||
MG_Util::ShaderTranspiler::ShaderCompiler::ResetPrewarmLatch();
|
||||
// The two-level translation memo. Nothing in it references a glslang object -
|
||||
// both levels hold plain bytes - so this is RSS hygiene rather than a lifetime
|
||||
// requirement, and it is safe either side of FinalizeProcess. Stats first: an
|
||||
// fordebug build gets one line per level saying how the run went.
|
||||
MG_Util::ShaderTranspiler::LogShaderTranslationCacheStats();
|
||||
MG_Util::ShaderTranspiler::ClearShaderTranslationCaches();
|
||||
MG_State::GLState::LogProgramTranslationCacheStats();
|
||||
MG_State::GLState::ClearProgramTranslationCache();
|
||||
MG_Backend::gBackendFunctionsTable = {};
|
||||
g_isInitialized = false;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A later re-initialization asks whether the server is there again.
|
||||
MG_Remote::Client::ResetServerProbeCache();
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_VERIFY
|
||||
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
|
||||
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
|
||||
// FATAL=0 summary the comparator used to write from __run_exit_handlers truncated the
|
||||
// client half of every such run to that one line. Verify builds only - the guard is
|
||||
// the compile definition, not a runtime knob, and the pull build gains no byte (G1).
|
||||
MG_Pipe::MGPipeVerifyFlushSummary();
|
||||
#endif
|
||||
if (logLifecycle) {
|
||||
MG_Util::Debug::Close();
|
||||
}
|
||||
@@ -135,54 +86,9 @@ namespace MobileGL {
|
||||
MGLOG_I("Initializing MobileGL...");
|
||||
MG_ConfigLoader::Init();
|
||||
MGLOG_I("Config loaded");
|
||||
// Immediately after the config load and before anything can count: the MGPipe
|
||||
// boundary counters latch their enable flag here, so every counting site in the
|
||||
// two backends is a load of an already-settled global for the rest of the run.
|
||||
MG_Util::PipeStats::Init();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// F1 (P7 wave 2). THE WIRE COMES UP BEFORE THE FIRST FRONTEND OBJECT IS BORN, and the
|
||||
// ORDER is the fix - not a wait, and not a friendlier default mask.
|
||||
//
|
||||
// MG_State::Init() constructs GLContext, and GLContext's TextureState constructor
|
||||
// materialises one default texture object per target (GL 3.3 core 3.8,
|
||||
// TextureState.cpp:62-71). TextureObjectBase's constructor emits resource_create for
|
||||
// every one of them (TextureState/TextureObject.cpp:168-169), and that emission is
|
||||
// gated, through PipeFill's FamilyIsLive, on
|
||||
// CapsMirror::ServerConsumes(kMGPipeSubsystemTextureResources). In the old order the
|
||||
// mirror was still GENERATION 0 when it was asked - it had no snapshot because the
|
||||
// session had not been started yet, MG_Backend::Init() being what starts it - so it
|
||||
// answered from the placeholder, every one of those creates was withheld, and the log
|
||||
// said so once and then never again:
|
||||
//
|
||||
// MG_Remote client: the server does not consume MGPipe subsystem 0x400 - this
|
||||
// family emits NOTHING and the legacy pull path runs for it (R-8). callMask=0x0,
|
||||
// caps generation 0
|
||||
//
|
||||
// That is not a race and it never was one: the line reproduces byte-for-byte on
|
||||
// lavapipe and on an Adreno 830, because the two Init calls are two statements in one
|
||||
// thread. And "the legacy pull path runs for it" is false under split - the client has
|
||||
// no pull path to fall back to - so the records are simply lost, and only a later
|
||||
// respecify's self-healing create (TextureEmit.h:872) ever brings one back.
|
||||
//
|
||||
// A BOUNDED WAIT INSIDE ServerConsumes CANNOT FIX THIS AND WOULD DEADLOCK: during
|
||||
// MG_State::Init() no handshake is in flight, and the one that will land is started
|
||||
// LATER BY THIS SAME THREAD. The only correct answer is to have already brought it up,
|
||||
// which is what this hook does. ServerConsumes now REFUSES to answer from a
|
||||
// placeholder (CapsMirror.cpp), so a regression of this order is a named Fatal rather
|
||||
// than a whole family of silently dropped records.
|
||||
//
|
||||
// In a build without MOBILEGL_BUILD_DISAGGREGATED this statement does not exist, so
|
||||
// the pull build gains no symbol, no branch and no byte - which is what G1 measures.
|
||||
const Bool splitBackendIsUp = MG_Backend::InitSplitRolesBeforeState();
|
||||
#endif
|
||||
MG_State::Init();
|
||||
MGLOG_D("MG_State initialized");
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!splitBackendIsUp)
|
||||
#endif
|
||||
{
|
||||
MG_Backend::Init();
|
||||
}
|
||||
MG_Backend::Init();
|
||||
MGLOG_D("MG_Backend initialized");
|
||||
MG_Impl::Init();
|
||||
MGLOG_D("MG_Impl initialized");
|
||||
@@ -227,37 +133,6 @@ 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 defined(__linux__) && !defined(__ANDROID__)
|
||||
// LIBGL_ALWAYS_SOFTWARE is how a process (or whoever launched it) asks the system's GL for a
|
||||
// software renderer. MobileGL is a GPU forwarder, so it steps aside and lets the system's
|
||||
// other implementation give the process what it asked for.
|
||||
{
|
||||
String software;
|
||||
if (MG_ConfigLoader::LookupSetting("LIBGL_ALWAYS_SOFTWARE", software) && !software.empty() &&
|
||||
software != "0" && software != "false") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#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,20 +26,8 @@ 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
|
||||
|
||||
@@ -172,15 +172,11 @@ namespace MobileGL::MG_Backend {
|
||||
return false;
|
||||
}
|
||||
|
||||
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
|
||||
// refused a caller for a reason that is not there: the surface and context tables are keyed
|
||||
// by the display each was created on, every operation carries its own display handle, and
|
||||
// this field has no reader anywhere - it was the whole of the rule.
|
||||
//
|
||||
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
|
||||
// probing another, so the second eglInitialize is the one that matters, and a refusal there
|
||||
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
|
||||
// and the real display having failed, with nothing in between that says which was refused.
|
||||
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
|
||||
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_eglDisplay = dpy;
|
||||
m_eglDisplayInitialized = true;
|
||||
if (major) {
|
||||
@@ -200,15 +196,12 @@ namespace MobileGL::MG_Backend {
|
||||
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
auto surfaceIt = m_eglSurfaces.find(surface);
|
||||
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
|
||||
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
|
||||
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
|
||||
return false;
|
||||
}
|
||||
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
|
||||
surfaceIt->second.Window.Width = width;
|
||||
surfaceIt->second.Window.Height = height;
|
||||
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
|
||||
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
|
||||
if (m_eglSurface == surface) {
|
||||
m_windowHandle.Width = width;
|
||||
m_windowHandle.Height = height;
|
||||
@@ -286,7 +279,6 @@ namespace MobileGL::MG_Backend {
|
||||
return true;
|
||||
}
|
||||
|
||||
m_activatingSurface = surface;
|
||||
if (surfaceState->Kind == SurfaceKind::Window) {
|
||||
SetWindowHandle(surfaceState->Window);
|
||||
if (!InitWindowSurface()) {
|
||||
@@ -306,14 +298,8 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurface = surface;
|
||||
m_eglSurfaceInitialized = true;
|
||||
m_eglSurfaceKind = surfaceState->Kind;
|
||||
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
|
||||
// own (a multi-window client renders each window on its own thread), and wiping them made
|
||||
// that thread's next eglSwapBuffers fail with "no current context attached".
|
||||
auto activatedIt = m_eglSurfaces.find(surface);
|
||||
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
|
||||
activatedIt->second.Activated = true;
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
}
|
||||
m_eglCurrentThreads.clear();
|
||||
m_backendCapabilitiesInitialized = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -380,24 +366,6 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglCurrentThreads.clear();
|
||||
}
|
||||
|
||||
void BackendObject::ForgetActiveEGLSurface() {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
m_eglSurfaceInitialized = false;
|
||||
m_eglSurfaceKind = SurfaceKind::None;
|
||||
m_eglSurface = EGL_NO_SURFACE;
|
||||
m_windowHandle = {};
|
||||
}
|
||||
|
||||
namespace {
|
||||
MG_Util::Damage::Region& PresentDamageSlot() {
|
||||
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
|
||||
return damage;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); }
|
||||
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; }
|
||||
|
||||
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
|
||||
@@ -414,16 +382,10 @@ namespace MobileGL::MG_Backend {
|
||||
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
|
||||
return false;
|
||||
}
|
||||
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
|
||||
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
|
||||
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
|
||||
return false;
|
||||
}
|
||||
// The calling thread's surface, which another thread's make-current may have moved the
|
||||
// backend off: bring it back before presenting it.
|
||||
if ((!m_eglSurfaceInitialized || draw != m_eglSurface) && !ActivateEGLSurface(draw)) {
|
||||
MGLOG_E("SwapEGLBuffers failed: the draw surface could not be re-activated");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& backendFunctions = GetBackendFunctions();
|
||||
if (!backendFunctions.Present) {
|
||||
@@ -464,9 +426,8 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ForgetActiveEGLSurface();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
|
||||
@@ -497,9 +458,8 @@ namespace MobileGL::MG_Backend {
|
||||
m_eglSurfaces.erase(surfaceIt);
|
||||
if (m_eglSurface == surface) {
|
||||
OnEGLSurfaceReleased(surface);
|
||||
ForgetActiveEGLSurface();
|
||||
ResetEGLRuntimeState();
|
||||
}
|
||||
OnEGLSurfaceForgotten(surface);
|
||||
}
|
||||
|
||||
void BackendObject::ReleaseEGLResources() {
|
||||
@@ -532,8 +492,4 @@ namespace MobileGL::MG_Backend {
|
||||
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
|
||||
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
|
||||
(void)surface;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
@@ -9,16 +9,11 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL {
|
||||
namespace MG_State::GLState {
|
||||
class FramebufferObject;
|
||||
class ITextureObject;
|
||||
class RenderbufferObject;
|
||||
}
|
||||
|
||||
enum class BackendType {
|
||||
@@ -29,34 +24,6 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
namespace MG_Backend {
|
||||
// One endpoint of a glCopyImageSubData. GL 4.6 core 18.3.2 accepts GL_RENDERBUFFER
|
||||
// alongside the ten whole-image texture targets, and a renderbuffer name lives in a
|
||||
// namespace of its own - so an endpoint is a sum type, not an ITextureObject. At most
|
||||
// one of the two pointers is set; neither is set when the name named nothing, which is
|
||||
// the INVALID_VALUE the frontend validator reports.
|
||||
struct CopyImageEndpoint {
|
||||
SharedPtr<MG_State::GLState::ITextureObject> Texture;
|
||||
SharedPtr<MG_State::GLState::RenderbufferObject> Renderbuffer;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Pipe::MGPipeHandle TextureHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MG_Pipe::MGPipeHandle RenderbufferHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
#endif
|
||||
|
||||
Bool IsRenderbuffer() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Renderbuffer != nullptr;
|
||||
}
|
||||
Bool Exists() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(TextureHandle)) return true;
|
||||
if (!MG_Pipe::MGPipeHandleIsNull(RenderbufferHandle)) return true;
|
||||
#endif
|
||||
return Texture != nullptr || Renderbuffer != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
enum class FormatCapability : Uint64 {
|
||||
Creatable = 1ull << 0,
|
||||
|
||||
@@ -193,9 +160,9 @@ namespace MobileGL {
|
||||
GLsizei height, GLint border);
|
||||
void (*CopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
|
||||
GLsizei width, GLsizei height);
|
||||
void (*CopyImageSubData)(const CopyImageEndpoint& src,
|
||||
void (*CopyImageSubData)(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void (*GenerateMipmap)(GLenum target);
|
||||
@@ -211,23 +178,9 @@ namespace MobileGL {
|
||||
void (*MemoryBarrierByRegion)(GLbitfield barriers);
|
||||
void (*BindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer,
|
||||
GLenum access, GLenum format);
|
||||
// The ONLY indexed query that is genuinely a backend one, and only for the pnames
|
||||
// MG_Impl/GLImpl/Getter/GL_Getter.cpp does not already own. Every indexed pname that
|
||||
// names FRONTEND state - the indexed buffer bindings, the per-unit texture/sampler
|
||||
// bindings, the image-unit bindings, the viewport rectangles, the indexed capabilities
|
||||
// - is answered in GL_Getter::GetIntegeri_v and never reaches this entry; the
|
||||
// 64-bit and float/double widths are derived there from the same answer, which is why
|
||||
// no GetInteger64i_v/GetFloati_v/GetDoublei_v table entry exists. In practice this
|
||||
// leaves GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE (also asked directly by
|
||||
// MG_Util/ShaderTranspiler/CompileEnv.cpp) plus whatever pname the frontend has no
|
||||
// case for at all.
|
||||
void (*GetIntegeri_v)(GLenum target, GLuint index, GLint* data);
|
||||
// There is deliberately NO GetProgramiv entry: glGetProgramiv describes the program
|
||||
// the APPLICATION wrote - link status, the transform-feedback mode, the compute local
|
||||
// size - all of which are frontend link artifacts on ProgramObject, and
|
||||
// MG_Impl/GLImpl/Program/GL_Program.cpp answers every one of them from there. Asking a
|
||||
// backend would mean asking about a DIFFERENT program (a SPIRV-Cross-generated ESSL
|
||||
// one, or a SPIR-V module), in a namespace the application never sees.
|
||||
void (*GetInteger64i_v)(GLenum target, GLuint index, GLint64* data);
|
||||
void (*GetProgramiv)(GLuint program, GLenum pname, GLint* params);
|
||||
// The GL program interface (glGetProgramInterfaceiv / glGetProgramResource*) is NOT
|
||||
// a backend query: it describes the program the application wrote, in the
|
||||
// application's namespace, which neither backend program is in. It is answered
|
||||
@@ -283,14 +236,6 @@ namespace MobileGL {
|
||||
// (optional; null = frontend falls back to CPU accounting).
|
||||
BackendQueryHandle (*BeginXfbPrimitivesQuery)(Bool generated);
|
||||
void (*EndXfbPrimitivesQuery)(BackendQueryHandle query);
|
||||
// Whether GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN should be answered from the
|
||||
// frontend's own accounting wherever that accounting is exact - a capture with no
|
||||
// geometry stage - instead of from the query above. Set by DirectGLES, whose result
|
||||
// is whatever the ES driver's PRIMITIVES_WRITTEN counter says: Adreno reports twice
|
||||
// the written count for a vertex-only capture that follows a large render pass,
|
||||
// where the desktop-exact answer is the one the frontend already computed. Defaults
|
||||
// to false, so a backend that never sets it keeps using its GPU result.
|
||||
Bool PrefersCpuXfbPrimitiveAccounting = false;
|
||||
// Transform feedback capture spans, for backends whose own GL/ES driver
|
||||
// performs the capture (DirectGLES). Both optional; null means the backend
|
||||
// drives capture from its draw recording instead (DirectVulkan). End is
|
||||
@@ -333,13 +278,7 @@ namespace MobileGL {
|
||||
};
|
||||
|
||||
struct DynamicBackendParameters {
|
||||
Uint64 UniformBufferOffsetAlignment = 256;
|
||||
// GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT, which is a SEPARATE limit from the
|
||||
// uniform one and is routinely larger: Adreno 830 reports 32 for uniform buffers and
|
||||
// 64 for storage buffers. Answering the storage query with the uniform value let an
|
||||
// application bind a storage range at an offset the driver cannot address, which it
|
||||
// accepted without error and then wrote somewhere else entirely.
|
||||
Uint64 ShaderStorageBufferOffsetAlignment = 256;
|
||||
SizeT UniformBufferOffsetAlignment = 256;
|
||||
// GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT. 1.0 means the backend cannot filter anisotropically,
|
||||
// which is also why the extension is not advertised in that case.
|
||||
Float MaxTextureMaxAnisotropy = 1.0f;
|
||||
@@ -379,37 +318,8 @@ namespace MobileGL {
|
||||
Int MaxVertexAttribs = 16;
|
||||
Int MaxComputeShaderStorageBlocks = 8;
|
||||
Int MaxCombinedShaderStorageBlocks = 32;
|
||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four
|
||||
// non-compute, non-fragment stages and these defaults are the spec minimums, not
|
||||
// placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for
|
||||
// vertex, tessellation control, tessellation evaluation and geometry at 0, and only
|
||||
// fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM
|
||||
// GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a
|
||||
// backend that cannot honour a graphics-stage storage block MUST report 0 here
|
||||
// rather than a hopeful number. Advertising a non-zero count the driver will refuse
|
||||
// does not make the block work; it only moves the failure from an honest
|
||||
// "unsupported" at query time to a backend link error the frontend never surfaces,
|
||||
// after which every draw with that program silently renders nothing.
|
||||
Int MaxVertexShaderStorageBlocks = 0;
|
||||
Int MaxTessControlShaderStorageBlocks = 0;
|
||||
Int MaxTessEvaluationShaderStorageBlocks = 0;
|
||||
Int MaxGeometryShaderStorageBlocks = 0;
|
||||
Int MaxFragmentShaderStorageBlocks = 8;
|
||||
Int MaxComputeUniformBlocks = 12;
|
||||
Int MaxComputeWorkGroupInvocations = 128;
|
||||
// GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE, one value per
|
||||
// axis. These six, with the invocations limit above, are the only indexed limits a
|
||||
// backend genuinely OWNS - the device answers them (glGetIntegeri_v on DirectGLES,
|
||||
// VkPhysicalDeviceLimits::maxComputeWorkGroupCount/Size on DirectVulkan) - and so
|
||||
// the only ones that survive the retirement of the GetIntegeri_v table entry: they
|
||||
// cross the MGPipe boundary inside MGPCaps, by inclusion of this struct (plan B
|
||||
// section 4.4.1). Every other indexed pname names frontend state. RAW driver
|
||||
// answers, like the invocations limit: GL_Getter and the compile environment floor
|
||||
// them at the shared MIN_COMPUTE_WORK_GROUP_* minimums themselves. The defaults are
|
||||
// the GL 4.3 core minimums (table 23.60) and describe the no-backend case, as
|
||||
// MaxClipDistances' does.
|
||||
Int MaxComputeWorkGroupCount[3] = {65535, 65535, 65535};
|
||||
Int MaxComputeWorkGroupSize[3] = {1024, 1024, 64};
|
||||
Int MaxShaderStorageBufferBindings = 8;
|
||||
Int MaxTextureBufferSize = 65536;
|
||||
// GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT; 1 means the offset is unconstrained.
|
||||
@@ -424,45 +334,8 @@ namespace MobileGL {
|
||||
Int MaxComputeImageUniforms = 8;
|
||||
Int MaxDrawBuffers = 8;
|
||||
Int MaxColorAttachments = 8;
|
||||
// GL_MAX_CLIP_DISTANCES. Zero is a legal answer here, not a placeholder, and a
|
||||
// backend that cannot host a clip distance MUST report it: advertising eight the
|
||||
// backend will refuse does not make gl_ClipDistance work, it only moves the failure
|
||||
// from an honest "unsupported" at query time to a backend shader-compile error the
|
||||
// frontend never surfaces, after which every draw with that program silently renders
|
||||
// nothing. DirectGLES fills it from GL_EXT_clip_cull_distance, DirectVulkan from the
|
||||
// shaderClipDistance device feature. The DEFAULT stays at the GL 4.3 core minimum
|
||||
// because it describes the no-backend case (standalone shader compiles, unit tests),
|
||||
// where there is no device to be honest about and BuildTBuiltInResource still has to
|
||||
// hand glslang a workable gl_MaxClipDistances.
|
||||
Int MaxClipDistances = 8;
|
||||
// GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly
|
||||
// the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a
|
||||
// backend that cannot host a cull distance MUST report it. The failure this prevents
|
||||
// is worse than the clip one, because cull distance discards the whole primitive:
|
||||
// glslang bounds gl_CullDistance[i] against maxCullDistances and expands
|
||||
// gl_MaxCullDistances from it, SPIRV-Cross then emits
|
||||
// `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver
|
||||
// without that extension rejects the program in an info log nobody surfaces. These
|
||||
// used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all.
|
||||
// The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances'
|
||||
// is: they describe the no-backend case (standalone compiles, unit tests).
|
||||
Int MaxCullDistances = 8;
|
||||
Int MaxCombinedClipAndCullDistances = 8;
|
||||
Int MaxViewports = 16;
|
||||
// GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a
|
||||
// primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes
|
||||
// GL_UNDEFINED_VERTEX a legal answer for both, and it is the honest default - naming
|
||||
// a convention is a statement about behaviour, so a backend that does not pin one
|
||||
// must not claim it does. DirectGLES fills the layer one from the ES 3.2 query and
|
||||
// the viewport one from GL_OES_viewport_array, and leaves UNDEFINED where the
|
||||
// capability is absent: without the viewport array extension only viewport 0 is ever
|
||||
// rasterized, so no convention selects anything. DirectVulkan keeps UNDEFINED for
|
||||
// both - which vertex provokes is decided per pipeline by
|
||||
// VulkanRenderer::SelectProvokingVertexMode out of VK_EXT_provoking_vertex,
|
||||
// provokingVertexModePerPipeline and the topology, so no single convention is true
|
||||
// of the backend.
|
||||
Uint32 LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Uint32 ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
Int MaxViewportWidth = 16384;
|
||||
Int MaxViewportHeight = 16384;
|
||||
Float ViewportBoundsRangeMin = 0.0f;
|
||||
@@ -510,56 +383,14 @@ namespace MobileGL {
|
||||
const Uint32 bit = PerLayerFramebufferAttachmentBit(target);
|
||||
return bit != 0 && (PerLayerFramebufferAttachmentTargets & bit) != 0;
|
||||
}
|
||||
// Whether this backend can CONSUME a shader module that still declares 64-bit floats,
|
||||
// i.e. whether `double` survives the transpile instead of being narrowed to `float`
|
||||
// (ShaderTranspiler::DemoteFloat64Pass). Detected, never assumed:
|
||||
// * DirectVulkan sets it from VkPhysicalDeviceFeatures::shaderFloat64, the feature
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 requires before a module declaring
|
||||
// OpCapability Float64 may be created at all. lavapipe has it; Adreno and Mali
|
||||
// both report VK_FALSE, so no real mobile device does.
|
||||
// * DirectGLES can NEVER have it. GLSL ES has no 64-bit float type in any version
|
||||
// or extension, so SPIRV-Cross cannot emit one ("FP64 not supported in ES
|
||||
// profile") and the demotion there is mathematically mandatory, always.
|
||||
// Defaults to false so a backend that never sets it - and the no-backend case, which
|
||||
// is what standalone shader compiles and the unit tests run under - keeps the
|
||||
// demotion, which is the behaviour that works everywhere.
|
||||
Bool SupportsShaderFloat64 = false;
|
||||
// Whether glVertexAttribLFormat / glVertexArrayAttribLFormat can be honoured, i.e.
|
||||
// whether a 64-bit vertex attribute can actually reach a shader unconverted. Detected,
|
||||
// never assumed: DirectVulkan needs VkPhysicalDeviceFeatures::shaderFloat64 (the
|
||||
// attribute travels as its 32-bit word pair, so no VK_FORMAT_R64* is required, but the
|
||||
// bitcast result is Float64); DirectGLES can never have it, ESSL having no fp64 type at
|
||||
// all. Defaults to false so a backend that never sets it gets the conservative answer.
|
||||
//
|
||||
// INDEPENDENT of SupportsShaderFloat64, and it has to be: this flag decides a VkFormat
|
||||
// from the VAO ATTRIBUTE alone, which does not know what type the shader declared, and
|
||||
// glVertexAttribFormat(GL_DOUBLE) feeding a plain `in vec4` is both legal and common
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4/5, advanced-bindingUpdate). A
|
||||
// backend with native fp64 that still cannot FETCH 64 bits keeps this false and relies
|
||||
// on the per-MODULE rule in ShaderCompiler::SanitizeAndOptimizeBinary instead: a vertex
|
||||
// module that declares a 64-bit float INPUT is demoted whole, so the two shader-side
|
||||
// halves (PackDoubleVertexInputsPass and VertexInputStateFactory::ToVkVertexFormat)
|
||||
// still see one consistent world.
|
||||
Bool SupportsFloat64VertexAttributes = false;
|
||||
// Whether a TESSELLATION stage of this backend may access gl_PointSize - i.e.
|
||||
// whether a module declaring OpCapability TessellationPointSize can reach the
|
||||
// driver at all. DirectVulkan sets both this and the geometry twin from the one
|
||||
// shaderTessellationAndGeometryPointSize feature; DirectGLES sets them
|
||||
// independently from the EXT/OES_tessellation_point_size /
|
||||
// geometry_point_size extension pairs (PointSizeTier), which really do come
|
||||
// separately. When absent, ProgramSpirvTask demotes the built-in to an ordinary
|
||||
// varying program-wide (ShaderCompiler::
|
||||
// DemoteTessellationGeometryPointSizeForProgram); MOBILEGL_POINT_SIZE_DEMOTION
|
||||
// overrides the detection in either direction at backend init.
|
||||
//
|
||||
// Defaults TRUE, deliberately against the house "assume absent" rule: false
|
||||
// ARMS a rewrite, so the conservative no-backend answer (standalone compiles,
|
||||
// unit tests) is the one that leaves modules untouched. A backend that never
|
||||
// sets it gets standard modules and, at worst, the old honest declines.
|
||||
Bool SupportsTessellationPointSize = true;
|
||||
// The geometry-stage twin (OpCapability GeometryPointSize).
|
||||
Bool SupportsGeometryPointSize = true;
|
||||
Uint64 MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024;
|
||||
Uint32 SubgroupSize = 0;
|
||||
Uint32 SubgroupSupportedStages = 0;
|
||||
Uint32 SubgroupSupportedFeatures = 0;
|
||||
@@ -584,197 +415,26 @@ namespace MobileGL {
|
||||
Uint32 Height = 0;
|
||||
};
|
||||
|
||||
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
|
||||
// buffers exported to clients as dma-buf descriptors.
|
||||
//
|
||||
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
|
||||
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
|
||||
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
|
||||
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
|
||||
struct SharedImageView {
|
||||
Uint64 Id = 0;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Fourcc = 0;
|
||||
void* NativeBuffer = nullptr;
|
||||
};
|
||||
|
||||
// The client-side answer of an allocation: the image's id and its exported descriptor
|
||||
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
|
||||
struct SharedImageExport {
|
||||
Uint64 Id = 0;
|
||||
int Fd = -1;
|
||||
Uint32 Width = 0;
|
||||
Uint32 Height = 0;
|
||||
Uint32 Fourcc = 0;
|
||||
Uint32 Stride = 0;
|
||||
Uint32 Offset = 0;
|
||||
Uint64 Modifier = 0;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ---- 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;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 ring moved to another of its client's contexts (bind_context). A
|
||||
// backend whose surface follows the native context it binds needs nothing; one that keeps
|
||||
// a single native surface binding re-binds the surface that context was last made current
|
||||
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
|
||||
virtual void OnClientContextBound(Uint64 token) { (void)token; }
|
||||
|
||||
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
|
||||
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
|
||||
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
|
||||
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
|
||||
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
|
||||
// eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native
|
||||
// present. A backend with nothing to hand it to swaps as SwapEGLBuffers does.
|
||||
virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) {
|
||||
(void)damage;
|
||||
return SwapEGLBuffers(dpy, draw);
|
||||
}
|
||||
// Forwards the app-requested eglSwapInterval to the backend's native
|
||||
// presentation path (no-op for backends without a SetSwapInterval hook).
|
||||
virtual void SetEGLSwapInterval(Int interval);
|
||||
virtual void ReleaseEGLSurface(EGLSurface surface);
|
||||
virtual void ReleaseEGLResources();
|
||||
|
||||
// ---- the server's own display window going away and coming back -------------------
|
||||
//
|
||||
// `window` (a window surface's native window) is about to be destroyed - the display's
|
||||
// screen went off. Release ONLY what is bound to it: every window surface on it keeps its
|
||||
// client handle and draws into a placeholder from here on, and every context, object and
|
||||
// device stays. Called on the thread that runs this backend; nothing may reference the
|
||||
// window once it returns true. False: not supported (the caller ends the session instead).
|
||||
virtual Bool SuspendServerWindow(void* window) {
|
||||
(void)window;
|
||||
return false;
|
||||
}
|
||||
// The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window,
|
||||
// `width`x`height`), and their new extent is published as a creation publishes it. False:
|
||||
// they could not be (the caller ends the session).
|
||||
virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
|
||||
(void)window, (void)width, (void)height;
|
||||
return false;
|
||||
}
|
||||
// `window` changed size to `width`x`height` (the display's report - the one size to publish).
|
||||
// Its surfaces publish that extent to the client now, without waiting for a present; a
|
||||
// swapchain that has to follow is rebuilt when no frame is half-recorded, else at the
|
||||
// present. False: nothing was published (no surface on it, or not supported).
|
||||
virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) {
|
||||
(void)window, (void)width, (void)height;
|
||||
return false;
|
||||
}
|
||||
// CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents
|
||||
// change under the application (a remote server's window) applies what it has received.
|
||||
virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; }
|
||||
|
||||
void SetWindowHandle(const WindowHandle& handle);
|
||||
|
||||
virtual const RendererInfo& GetRendererInfo() const = 0;
|
||||
@@ -801,28 +461,18 @@ namespace MobileGL {
|
||||
struct EGLSurfaceState {
|
||||
SurfaceKind Kind = SurfaceKind::None;
|
||||
Bool DestroyPending = false;
|
||||
// Activated at least once: a re-activation (a multi-window client switching back)
|
||||
// is a re-bind and does not re-run the backend's capability probe.
|
||||
Bool Activated = false;
|
||||
WindowHandle Window;
|
||||
EGLint Width = 1;
|
||||
EGLint Height = 1;
|
||||
};
|
||||
|
||||
void ResetEGLRuntimeState();
|
||||
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
|
||||
// any more, but every OTHER thread's current context and surface stay as they were (a
|
||||
// multi-window client closing one popup must not unbind its other windows' threads).
|
||||
void ForgetActiveEGLSurface();
|
||||
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
|
||||
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
|
||||
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
|
||||
Bool ActivateEGLSurface(EGLSurface surface);
|
||||
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
|
||||
virtual void OnEGLSurfaceReleased(EGLSurface surface);
|
||||
// Every registered surface the base class drops, current or not, after it is gone from
|
||||
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
|
||||
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
|
||||
FormatCapabilityCache& MutableFormatCapabilities();
|
||||
|
||||
mutable std::recursive_mutex m_eglStateMutex;
|
||||
@@ -830,10 +480,6 @@ namespace MobileGL {
|
||||
WindowHandle m_windowHandle;
|
||||
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
|
||||
EGLSurface m_eglSurface = EGL_NO_SURFACE;
|
||||
// The client surface ActivateEGLSurface is bringing up while it calls InitWindowSurface /
|
||||
// InitPbufferSurface, which take no surface argument: a backend that maps client surfaces
|
||||
// to native ones reads it to re-bind an existing native surface instead of making another.
|
||||
EGLSurface m_activatingSurface = EGL_NO_SURFACE;
|
||||
Bool m_eglDisplayInitialized = false;
|
||||
Bool m_eglSurfaceInitialized = false;
|
||||
Bool m_backendCapabilitiesInitialized = false;
|
||||
|
||||
@@ -15,45 +15,4 @@
|
||||
namespace MobileGL::MG_Backend {
|
||||
extern UniquePtr<BackendObject>& pActiveBackendObject;
|
||||
extern GlobalBackendFunctionsTable gBackendFunctionsTable;
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5 v1. The counterpart of Init()'s single split hook, and a NO-OP in every run that is
|
||||
// not split. It exists ONLY in a split build: G1 admits no new symbol in the pull build,
|
||||
// so MobileGL/Init.cpp's call sits under the same #if rather than calling a no-op.
|
||||
//
|
||||
// IT MUST RUN FIRST, BEFORE ANYTHING ELSE IN DestroyImpl. ARCHITECTURE.md:537's order is
|
||||
// publish -> the server drains and acks -> stop the apply thread (Kill, then join) -> close
|
||||
// the transport -> the client drains the compile pool -> MobileGL::Destroy() -> release the
|
||||
// sync/query handles; DestroyImpl IS MobileGL::Destroy, so everything before that arrow has
|
||||
// to happen at its top. Two consequences are load-bearing rather than tidy:
|
||||
//
|
||||
// * the apply thread is JOINED before PipeStats::Shutdown() dumps, so nothing is writing
|
||||
// a counter while the final line is produced;
|
||||
// * the apply thread is joined before pActiveBackendObject.reset(), so the server's own
|
||||
// BackendObject - which is NOT that global (table 3) - is destroyed on the thread that
|
||||
// owns the context, by ServerLoop::Stop, rather than on the app thread.
|
||||
//
|
||||
// The sync/query registries stay where they are, drained at MobileGL/Init.cpp:62 and :67
|
||||
// BEFORE pActiveBackendObject.reset(). ARCHITECTURE.md:537 puts them after
|
||||
// MobileGL::Destroy(); the two are only reconcilable if a split sync handle is
|
||||
// client-minted and needs no backend call, which is P10's. CONTRACT-P5 4 flags this as a
|
||||
// KNOWN OPEN ITEM and asks v1 to record which way it went: P5 keeps today's order.
|
||||
void ShutdownSplitRoles();
|
||||
|
||||
// F1 (P7 wave 2). Init()'s SPLIT ARM, run BEFORE MG_State::Init() rather than after it.
|
||||
//
|
||||
// Returns true when this process took the split path - whether the bring-up succeeded or
|
||||
// was refused by name - and the caller must then NOT call Init() a second time. Returns
|
||||
// false under MG_Config::TransportMode::Monolith, where there is nothing to bring up and
|
||||
// Init() keeps its own place after MG_State::Init().
|
||||
//
|
||||
// WHY IT EXISTS AT ALL, rather than MobileGL::Initialize simply calling Init() earlier:
|
||||
// the monolith arm of Init() must keep running after MG_State::Init(), because
|
||||
// BackendObject_DirectGLES::Initialize() is the tree's own order for it and moving it is a
|
||||
// pull-build change G1 forbids. So the split arm - and only the split arm - moves.
|
||||
//
|
||||
// It exists ONLY in a split build, for the reason InitServerRoleForSpawn states: a
|
||||
// definition guarded only on the inside still emits a symbol, and G1 measured that.
|
||||
Bool InitSplitRolesBeforeState();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,16 +18,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const MG_External::GLESCapabilities& capabilities,
|
||||
FormatCapabilityCache& cache);
|
||||
|
||||
// Clamps a requested sample count down to what the ES driver can really deliver for this
|
||||
// format on this format-capability target: the probed per-format list when there is one, the
|
||||
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
|
||||
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
|
||||
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
|
||||
// allocation call has to come through here. The shadow state keeps the requested count, so
|
||||
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
|
||||
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
|
||||
Int samples);
|
||||
|
||||
class BackendObject_DirectGLES : public BackendObject {
|
||||
public:
|
||||
~BackendObject_DirectGLES() override;
|
||||
@@ -38,26 +28,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
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;
|
||||
void ReleaseEGLSurface(EGLSurface surface) override;
|
||||
void ReleaseEGLResources() override;
|
||||
// The server window going and coming back: each window surface on it swaps its native EGL
|
||||
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
|
||||
Bool SuspendServerWindow(void* window) override;
|
||||
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
|
||||
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
|
||||
|
||||
const RendererInfo& GetRendererInfo() const override;
|
||||
String GetBackendAPIVersionString() const override;
|
||||
@@ -73,22 +47,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void UpdateDynamicBackendParameters();
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
void OnEGLSurfaceForgotten(EGLSurface surface) override;
|
||||
// An existing native surface for the client surface being activated, re-bound; false when
|
||||
// there is none yet (the caller creates it).
|
||||
Bool RebindNativeSurfaceFor(EGLSurface surface);
|
||||
// EGL: creating a surface does not change what is current. The creation bound the new
|
||||
// surface natively; put the previous one back for the context that held it.
|
||||
void RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw);
|
||||
|
||||
Bool m_initialized = false;
|
||||
// P14: ONE NATIVE SURFACE PER CLIENT SURFACE, created on its first activation and re-bound
|
||||
// on every later one. A multi-window client switches surfaces every frame; making a new
|
||||
// native surface each time leaked them and lost what the window had drawn.
|
||||
UnorderedMap<EGLSurface, EGLSurface> m_nativeSurfaces;
|
||||
// Window surfaces whose window is gone (SuspendServerWindow): their native surface is a
|
||||
// placeholder pbuffer until ResumeServerWindow.
|
||||
Vector<EGLSurface> m_suspendedWindowSurfaces;
|
||||
MG_External::EGLFunctionsTable m_EGLFunctions;
|
||||
MG_External::GLESFunctionsTable m_GLESFunctions;
|
||||
MG_External::GLESCapabilities m_GLESCapabilities;
|
||||
@@ -107,13 +67,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Espryt advertises (glGetString(GL_EXTENSIONS))
|
||||
// for a device whose timer queries / anisotropic filtering / native indirect draws /
|
||||
// non-zero indirect baseInstance semantics / EXT-OES texture views are (or are not) usable.
|
||||
// for a device whose timer queries / anisotropic filtering are (or are not) usable.
|
||||
// The MOBILEGL_DISABLE_TIMERQUERY escape hatch is applied inside.
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
|
||||
Bool drawIndirectSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool textureViewSupported, Bool cubeMapArraySupported);
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
|
||||
// initialized backend returns from GetBackendAPIVersionString (and that ends up
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
|
||||
#pragma once
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
extern Uint g_backendContextGeneration;
|
||||
|
||||
struct ContextEpoch {
|
||||
Uint Native = 0;
|
||||
Uint64 Served = 0;
|
||||
Bool Server = false;
|
||||
Bool operator==(const ContextEpoch&) const = default;
|
||||
};
|
||||
|
||||
inline ContextEpoch CurrentContextEpoch() {
|
||||
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
|
||||
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+1022
-12567
File diff suppressed because it is too large
Load Diff
@@ -23,22 +23,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Content uploads use scratch bindings, so draws and dispatches call this after
|
||||
// texture synchronization.
|
||||
void BindCurrentTextures();
|
||||
// Driver-side color/depth level readback. Returns tightly packed, owned
|
||||
// bytes in the requested pair; never consults a frontend texture or PACK/PBO.
|
||||
// The caller supplies identity and extent, and whether the SOURCE allocation
|
||||
// already uses an image carrier.
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
|
||||
// Whether the NATIVE texture currently holds `level` of `uploadTarget` at `logicalExtent`
|
||||
// (glGetTexLevelParameteriv). False for a level the driver never allocated - one defined after
|
||||
// the texture's last sync - so a re-mint does not read what is not there. A query the driver
|
||||
// refuses answers true, keeping the caller on its readback path.
|
||||
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
GLint level, const IntVec3& logicalExtent);
|
||||
#endif
|
||||
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil);
|
||||
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value);
|
||||
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value);
|
||||
@@ -56,8 +40,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect, GLintptr drawcount,
|
||||
GLsizei maxdrawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride);
|
||||
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount, GLsizei maxdrawcount,
|
||||
GLsizei stride);
|
||||
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
|
||||
const void* indices, GLint basevertex);
|
||||
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void* indices);
|
||||
@@ -92,9 +74,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -108,68 +90,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
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) and before a
|
||||
// readback, never per draw. The contexts keep the driver's default reset strategy: asking
|
||||
// every context to lose itself on reset would also end the INNOCENT ones - the compositor's
|
||||
// among them - on any other client's GPU fault.
|
||||
// `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);
|
||||
#endif
|
||||
// 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
|
||||
@@ -187,24 +117,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// capability read needs no current ES context, and it stays false until
|
||||
// the ES capabilities have been filled in.
|
||||
Bool AreTimerQueriesSupported();
|
||||
// True when the host ES driver can back a GL_TEXTURE_BUFFER at all - ES 3.2 core, or
|
||||
// EXT/OES_texture_buffer, with glTexBuffer resolved. Desktop GL has had buffer textures as
|
||||
// core since 3.1, so the frontend advertises them unconditionally and an app may call
|
||||
// glTexBuffer whenever it likes; this is the only thing standing between that call and a
|
||||
// null entry point. False also means every shader declaring a samplerBuffer is
|
||||
// uncompilable on this driver, which the program build reports by name.
|
||||
Bool AreBufferTexturesSupported();
|
||||
// Human-readable name of the buffer-texture tier for diagnostics and the driver POST:
|
||||
// "core (ES 3.2)", "GL_EXT_texture_buffer", "GL_OES_texture_buffer" or "unsupported".
|
||||
const char* GetBufferTextureTierName();
|
||||
// glTexBuffer / glTexBufferRange through whichever spelling this driver's buffer-texture
|
||||
// support actually ships: the unsuffixed names are ES 3.2 core, while an EXT/OES driver
|
||||
// exports glTexBuffer{,Range}EXT / OES. Callers must have checked
|
||||
// AreBufferTexturesSupported() first. CallTexBufferRange reports whether it could honour
|
||||
// the range - no tier is required to expose the range form, and the whole-buffer form is
|
||||
// the documented fallback.
|
||||
void CallTexBuffer(GLenum target, GLenum internalFormat, GLuint buffer);
|
||||
Bool CallTexBufferRange(GLenum target, GLenum internalFormat, GLuint buffer, GLintptr offset, GLsizeiptr size);
|
||||
// GL timer-query objects, backed by GL_EXT_disjoint_timer_query. The
|
||||
// creators return null (the frontend then falls back to an immediately
|
||||
// available zero result) when the calling thread does not own the ES
|
||||
@@ -248,210 +160,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// Applies (or defers until the window surface exists) the app-requested
|
||||
// eglSwapInterval on the native EGL surface.
|
||||
void SetSwapInterval(Int interval);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P12 review fix: the swap interval an ended server session asked for is not the next one's.
|
||||
// The request outlives the context on purpose (a surface re-created mid-session re-applies it),
|
||||
// so the in-process display server's session end forgets it: the next session starts at the
|
||||
// driver's default until it asks (BackendObject_DirectGLES's destructor under a transport).
|
||||
void ForgetRequestedSwapInterval();
|
||||
#endif
|
||||
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
|
||||
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
|
||||
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// The display is process state and stays one object (eglInitialize once per process, terminated
|
||||
// when the last native context leaves). The native EGLContext, the config it was created with
|
||||
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
|
||||
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
|
||||
// the first one's native context down, which is what lets two contexts (and two share groups)
|
||||
// be live at once in one process.
|
||||
|
||||
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
|
||||
// that session has bound. Installed once by the server layer; with no resolver installed -
|
||||
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
|
||||
// exactly the process singleton this replaces. `false` also means {0,0}.
|
||||
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
|
||||
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---------------------------------------------------------------------------------
|
||||
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
|
||||
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
|
||||
// context's object namespace; the identity a twin table must be keyed on is therefore
|
||||
// {session, share group} - the SAME two words the applier's OBJECT RECORDS are filed under
|
||||
// (PipeApply.h's MGPipeApplierKey, derived by the same probe and the same `0 means its own
|
||||
// group` rule, so the two agree by construction rather than by two conventions).
|
||||
//
|
||||
// Answers, for the CALLING THREAD, which session it belongs to and which share group that
|
||||
// session's current context is in. Installed once by the server layer beside the native and
|
||||
// applier probes; with no probe installed - or on a thread that is no session's own, which
|
||||
// is the client process, the in-process client+server shape and every unit case - the
|
||||
// answer is the single key {0, 0}, exactly the process-wide table this replaces.
|
||||
struct TwinKey {
|
||||
Uint64 SessionKey = 0;
|
||||
Uint64 ShareGroupKey = 0;
|
||||
Bool operator==(const TwinKey& other) const {
|
||||
return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey;
|
||||
}
|
||||
Bool operator!=(const TwinKey& other) const { return !(*this == other); }
|
||||
};
|
||||
struct TwinKeyHash {
|
||||
SizeT operator()(const TwinKey& key) const {
|
||||
// The 64-bit mix SlotAllocator and the applier's registry already use for their own
|
||||
// pair keys, so every keyed table in this tree hashes a session the same way.
|
||||
Uint64 mixed = key.SessionKey ^ (key.ShareGroupKey + 0x9E3779B97F4A7C15ull +
|
||||
(key.SessionKey << 6) + (key.SessionKey >> 2));
|
||||
mixed ^= mixed >> 33;
|
||||
mixed *= 0xFF51AFD7ED558CCDull;
|
||||
mixed ^= mixed >> 33;
|
||||
return static_cast<SizeT>(mixed);
|
||||
}
|
||||
};
|
||||
using TwinKeyResolver = Bool (*)(TwinKey* outKey);
|
||||
void SetTwinKeyResolver(TwinKeyResolver resolver);
|
||||
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
|
||||
// also the answer when no probe has ever been installed.
|
||||
Bool CurrentTwinKey(TwinKey* outKey);
|
||||
#endif
|
||||
|
||||
// Builds the native context for `contextToken` under the calling thread's session. A
|
||||
// `shareGroupToken` whose group already has a live native context in this session hands that
|
||||
// context to eglCreateContext as the share argument - which is what makes two contexts see one
|
||||
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
|
||||
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
|
||||
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
|
||||
// contexts are untouched; the display is terminated only when the last context in the process
|
||||
// has gone.
|
||||
void DestroyNativeContextFor(Uint64 contextToken);
|
||||
|
||||
// ---- objects that belong to ONE native context ----
|
||||
//
|
||||
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
|
||||
// shares them, not even between contexts of one share group, and every context numbers its own
|
||||
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
|
||||
// names THAT context's own framebuffer - often an application's - and the scratch path's
|
||||
// attach/scrub then rewires it onto staging textures. With one process serving several
|
||||
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
|
||||
//
|
||||
// The calling thread's current native context as a lifetime serial: assigned when the context
|
||||
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
|
||||
// made current through MakeCurrent.
|
||||
Uint64 CurrentNativeContextSerial();
|
||||
|
||||
class PerNativeContextBase {
|
||||
public:
|
||||
PerNativeContextBase();
|
||||
PerNativeContextBase(const PerNativeContextBase&) = delete;
|
||||
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
|
||||
// The context died, and its names with it: forget them (no GL call - there is no context
|
||||
// left to make one in).
|
||||
virtual void ForgetContext(Uint64 serial) = 0;
|
||||
|
||||
protected:
|
||||
~PerNativeContextBase();
|
||||
};
|
||||
|
||||
// Called where a native context is destroyed.
|
||||
void ForgetNativeContextObjects(Uint64 serial);
|
||||
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
|
||||
// it has not moved.
|
||||
Uint64 PerNativeContextEpoch();
|
||||
void BumpPerNativeContextEpoch();
|
||||
|
||||
// One T per native context, created on first use from that context.
|
||||
template <typename T>
|
||||
class PerNativeContext final : public PerNativeContextBase {
|
||||
public:
|
||||
T& Current() {
|
||||
const Uint64 serial = CurrentNativeContextSerial();
|
||||
// The draw path asks for the same entry over and over from one thread: a one-entry
|
||||
// thread-local cache, invalidated by any drop anywhere (rare).
|
||||
thread_local CurrentCache cache;
|
||||
const Uint64 epoch = PerNativeContextEpoch();
|
||||
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
|
||||
cache = {this, serial, epoch, &entry};
|
||||
return entry;
|
||||
}
|
||||
template <typename F>
|
||||
void ForEach(F&& visit) {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto& entry : m_bySerial) visit(entry.second);
|
||||
}
|
||||
void Clear() {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
BumpPerNativeContextEpoch();
|
||||
m_bySerial.clear();
|
||||
}
|
||||
void ForgetContext(Uint64 serial) override {
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
BumpPerNativeContextEpoch();
|
||||
m_bySerial.erase(serial);
|
||||
}
|
||||
|
||||
private:
|
||||
struct CurrentCache {
|
||||
const PerNativeContext* Owner = nullptr;
|
||||
Uint64 Serial = 0;
|
||||
Uint64 Epoch = 0;
|
||||
T* Entry = nullptr;
|
||||
};
|
||||
std::mutex m_mutex;
|
||||
std::unordered_map<Uint64, T> m_bySerial;
|
||||
};
|
||||
|
||||
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
|
||||
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
|
||||
class NativeContextName {
|
||||
public:
|
||||
operator GLuint() const { return m_names.Current(); }
|
||||
NativeContextName& operator=(GLuint name) {
|
||||
m_names.Current() = name;
|
||||
return *this;
|
||||
}
|
||||
GLuint* operator&() { return &m_names.Current(); }
|
||||
|
||||
private:
|
||||
mutable PerNativeContext<GLuint> m_names;
|
||||
};
|
||||
|
||||
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
|
||||
// records belong to; this makes the calling thread's native tuple follow it, binding the
|
||||
// session's current surface to the newly named context's native EGLContext. A no-op when the
|
||||
// named context has no native context yet (the session never sent CreateContext) or when the
|
||||
// native context already current on this thread IS that one - so the single-context world pays
|
||||
// nothing for it. Called from the bind_context path on the apply thread.
|
||||
Bool MakeNativeContextCurrentForBoundToken();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
|
||||
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
|
||||
// binding shadow, which still describes the previous holder's native context.
|
||||
void OnBackendTurnHandoff();
|
||||
#endif
|
||||
// How many native contexts this process holds, and how many of them belong to one session. The
|
||||
// test observables for "a second context did not tear the first one down".
|
||||
Uint64 NativeContextCount();
|
||||
Uint64 NativeContextCountForSession(Uint64 sessionKey);
|
||||
|
||||
// The calling thread's session, as the resolver above reports it (0 with no resolver).
|
||||
Uint64 CurrentNativeSessionKey();
|
||||
|
||||
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
|
||||
// hold" query and no way to ask a context for its share group, so a case that must tell "the
|
||||
// same native context still serves" from "a fresh one was built", or a sharing pair from two
|
||||
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
|
||||
// a production path and neither changes behaviour.
|
||||
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
|
||||
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
|
||||
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
|
||||
// tuple it means when no session runtime is installed.
|
||||
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
|
||||
|
||||
void DestroyEGLContext();
|
||||
|
||||
// Transform feedback capture spans, performed by the real ES driver. The
|
||||
@@ -495,60 +206,8 @@ 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
|
||||
|
||||
#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);
|
||||
// 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();
|
||||
} // namespace SharedImageImpl
|
||||
#endif
|
||||
|
||||
extern MG_External::EGLFunctionsTable g_EGLFuncs;
|
||||
extern MG_External::GLESFunctionsTable g_GLESFuncs;
|
||||
extern MG_External::GLESCapabilities g_GLESCapabilities;
|
||||
|
||||
+658
-12896
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,6 @@
|
||||
#include "MultiDraw.h"
|
||||
#include "Managers.h"
|
||||
#include <MG_State/GLState/Core.h>
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include <MG_Util/Metrics/PipeStats.h>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
@@ -32,26 +29,21 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// The index value this batch restarts on, compared at 32 bits against the zero-extended
|
||||
// source index. Normally the all-ones value of the source type, which is what
|
||||
// GL_PRIMITIVE_RESTART_FIXED_INDEX and GLES both restart on; with desktop
|
||||
// GL_PRIMITIVE_RESTART it is instead whatever glPrimitiveRestartIndex named. The rebased
|
||||
// tier turns whichever it is into 0xFFFFFFFF in its widened stream, which is what the
|
||||
// driver restarts on.
|
||||
//
|
||||
// No truncation, deliberately, and the same rule ResolveRestartSubstitution applies: a
|
||||
// restart index the source type cannot hold simply matches nothing, so returning it
|
||||
// verbatim is already "this batch restarts nowhere".
|
||||
// The all-ones value of an index type, which is what GL restarts on once
|
||||
// primitive restart is in play. CheckPrimitiveRestartSupported has already
|
||||
// rejected the arbitrary-index form of GL_PRIMITIVE_RESTART, so an enabled
|
||||
// restart always restarts here and nowhere else.
|
||||
Uint32 RestartSentinelFor(GLenum type) {
|
||||
if (ResolveRestartSubstitution(type) != RestartSubstitutionKind::None) {
|
||||
return MGB_CTX->GetPrimitiveRestartIndex();
|
||||
switch (type) {
|
||||
case GL_UNSIGNED_BYTE: return 0xFFu;
|
||||
case GL_UNSIGNED_SHORT: return 0xFFFFu;
|
||||
default: return 0xFFFFFFFFu;
|
||||
}
|
||||
return MG_Util::FixedRestartIndexForGLType(type);
|
||||
}
|
||||
|
||||
Bool RestartActive() {
|
||||
return MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MGB_CTX->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
return MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestart) ||
|
||||
MG_State::pGLContext->IsCapabilityEnabled(CapabilityInput::PrimitiveRestartFixedIndex);
|
||||
}
|
||||
|
||||
// Vertices per primitive for the modes whose sub-draws may be concatenated into a
|
||||
@@ -84,173 +76,19 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kMaxComputeWorkGroups = 65535;
|
||||
constexpr SizeT kMaxComputeFlattenedIndices = kMaxComputeWorkGroups * kComputeWorkGroupSize;
|
||||
|
||||
// BoundDrawIndirectBufferId MOVED (P5e ra2, ID-136) to sit beside BoundIndexBufferId,
|
||||
// which is the same question about the other target and now has the same two arms.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The bound index buffer, and WHICH SIDE ANSWERS FOR IT
|
||||
//
|
||||
// P5e (mv), CONTRACT-P5E §5.1 + §5.8 (ruling ID-81): this file asks the bound element
|
||||
// array buffer exactly three questions - "is one bound at all", "what GL name did
|
||||
// PrepareForDraw leave on GL_ELEMENT_ARRAY_BUFFER (and how big is its store)", and
|
||||
// "give me its bytes on the CPU" - and until now it asked all three of the FRONTEND
|
||||
// VAO, on the apply thread, once per indexed multi-draw. That was the last unguarded
|
||||
// `MGB_CTX->GetBoundVertexArray()` in the backend: package vi retired the ordinary
|
||||
// draw path's copy (DirectGLES.cpp's SyncCurrentVertexAttributeValues / PrepareForDraw)
|
||||
// and this one survived only because these batches used to die earlier, on the
|
||||
// framebuffer row fb has since retired.
|
||||
//
|
||||
// THE ARM IS STATED, NEVER INFERRED. BufferImpl::VertexInputReadsRecords() is the
|
||||
// family's own selector and reduces to `Transport != Monolith && the vertex-input bit`;
|
||||
// it is the same conjunction RefuseElementArrayBufferFromTheFrontend guards the
|
||||
// single-draw path with. Nothing below decides an arm by noticing that a handle or a
|
||||
// pointer happens to be null - conflating "a handle was noted" with "the record arm is
|
||||
// selected" is what ID-107 cost this phase 137 scenarios.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// How much of the index buffer a caller needs. DELIBERATELY not nested levels: each
|
||||
// value is exactly the work its original call site did, in its original order, so the
|
||||
// monolith arm makes the same calls in the same sequence it made before this package.
|
||||
enum class IndexBufferQuestion {
|
||||
Presence, // is an element array buffer bound at all
|
||||
DriverName, // + the GL name currently bound to GL_ELEMENT_ARRAY_BUFFER
|
||||
DriverNameAndSize,// + the store's size in bytes (the compute tier's source)
|
||||
HostBytes, // a CPU-readable copy of the whole store, and its size
|
||||
};
|
||||
|
||||
struct BoundIndexBufferView {
|
||||
Bool Present = false;
|
||||
Uint Id = 0; // DriverName*: 0 when there is no backend store yet
|
||||
SizeT Size = 0; // DriverNameAndSize / HostBytes
|
||||
const Uint8* HostBytes = nullptr; // HostBytes: null when there is no CPU copy
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A missing record on the handle arm is a NAMED refusal and never a quiet fall-back to
|
||||
// the frontend (TASK-mv requirement 2; the shape is Managers.cpp's
|
||||
// RefuseNullFrontendTextureOffTheHandleArm). The frontend element slot is not a second
|
||||
// opinion on this side: it answers for whatever the CLIENT has bound NOW, which is a
|
||||
// later draw than the one being applied. Drawing a batch from it would be a silently
|
||||
// wrong picture, which is precisely what MultiDrawScenario's batch-matches-unrolled
|
||||
// assertions exist to catch and what a fall-back would hide from them.
|
||||
[[noreturn]] void RefuseMissingIndexBufferRecord(const char* entry, MG_Pipe::MGPipeHandle res,
|
||||
const char* missing) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-index-buffer-arm\"} - %s found no %s for "
|
||||
"the index buffer {%u, %u} this batch's record named. The index buffer of this "
|
||||
"family is MGPipeApplier().IndexBuffer.Res (CONTRACT-P5E §5.1) and the arm is "
|
||||
"selected by Transport != Monolith AND the vertex-input subsystem bit (§5.8, "
|
||||
"ID-81); falling back to the frontend VAO's element slot here would draw this "
|
||||
"multi-draw from whatever the CLIENT has bound now",
|
||||
entry, missing, res.Slot, res.Gen);
|
||||
std::abort();
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
const auto& indirect =
|
||||
MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
// ---- P5e (ra2), ID-136: THE SAME REFUSAL FOR THE INDIRECT TARGET -------------------
|
||||
//
|
||||
// A sibling of mv's rather than a second idiom, because it is the same sentence about
|
||||
// the other buffer target: a missing record on the handle arm aborts by name and never
|
||||
// falls back to the frontend binding slot, which answers for whatever the CLIENT has
|
||||
// bound NOW - a later verb than the one being applied.
|
||||
[[noreturn]] void RefuseMissingIndirectBufferRecord(const char* entry,
|
||||
MG_Pipe::MGPipeHandle res) {
|
||||
MGLOG_F("MGPipe: Fatal{RoleViolation, \"multidraw-indirect-buffer-arm\"} - %s found no "
|
||||
"backend resource for the indirect buffer {%u, %u} this verb's record named. "
|
||||
"The indirect buffer of this family is MGPipeApplier().VerbIndirectBuffer "
|
||||
"(CONTRACT-P5E §2.1) and the arm is selected by Transport != Monolith "
|
||||
"(ID-81); falling back to the frontend GL_DRAW_INDIRECT_BUFFER slot here "
|
||||
"would restore whatever the CLIENT has bound now",
|
||||
entry, res.Slot, res.Gen);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// Does the applier's record say the application SUPPLIED this store's content? The M-3
|
||||
// rule ScopedRestartIndexSubstitution spells: for a supplied store a coverage gap in the
|
||||
// server shadow is a missing record and Fatal by name; for one the application ORPHANED
|
||||
// the gap is its own undefined content, and reading the shadow's zero-fill is exactly
|
||||
// what the monolith arm's MappedData() hands back.
|
||||
Bool IndexBufferRecordHasDefinedContent(const MG_Pipe::MGPipeApplierState& st,
|
||||
MG_Pipe::MGPipeHandle res) {
|
||||
if (res.Slot >= st.Resources.size()) return false;
|
||||
const auto& candidate = st.Resources[res.Slot];
|
||||
if (!candidate.Live || candidate.Gen != res.Gen) return false;
|
||||
return candidate.Desc.HasDefinedContent != 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
BoundIndexBufferView ResolveBoundIndexBuffer(IndexBufferQuestion question, const char* entry) {
|
||||
BoundIndexBufferView view;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (BufferImpl::VertexInputReadsRecords()) {
|
||||
const auto& st = MG_Pipe::MGPipeApplier();
|
||||
const MG_Pipe::MGPipeHandle res = BufferImpl::ResolveDrawIndexBufferFromRecord(st).Res;
|
||||
// A null Res is "no element array buffer bound", exactly as a null frontend slot
|
||||
// is on the arm below - the batch's indices are a client array, and every caller
|
||||
// already has an arm for that. It is NOT the arm test; the arm was decided above.
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return view;
|
||||
view.Present = true;
|
||||
if (question == IndexBufferQuestion::Presence) return view;
|
||||
|
||||
if (question == IndexBufferQuestion::HostBytes) {
|
||||
// The applier has already consumed the persistent-map blocks for this
|
||||
// resource before the draw verb replayed; the one Sync* this arm owes is the
|
||||
// monolith's SyncGpuWrites, and (P8-C) SplitHostBytesForCpuRead is it: a store
|
||||
// a shader wrote on this side is read back from this server's own GL buffer,
|
||||
// because no writeback ever reaches the server's staged copy. That is also why
|
||||
// this arm cannot be expressed as "find the object and run the monolith body".
|
||||
auto* resource = BufferImpl::FindBufferResourceForHandle(res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
view.HostBytes = BufferImpl::SplitHostBytesForCpuRead(*resource, res, "multidraw_index_rebase");
|
||||
if (view.HostBytes != nullptr && IndexBufferRecordHasDefinedContent(st, res)) {
|
||||
BufferImpl::RequireStagedCoverage(*resource, view.HostBytes, 0, view.Size,
|
||||
"multidraw_index_rebase");
|
||||
}
|
||||
// A null HostBytes is not a refusal: MappedData() on the monolith arm is
|
||||
// equally allowed to be null (an adopted coherent map keeps its bytes
|
||||
// elsewhere), and the one caller that asks declines the tier for it.
|
||||
return view;
|
||||
}
|
||||
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) RefuseMissingIndexBufferRecord(entry, res, "backend resource");
|
||||
view.Id = resource->id;
|
||||
if (question == IndexBufferQuestion::DriverNameAndSize) {
|
||||
view.Size = BufferImpl::ResourceWidthForHandle(res);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what each call site did before.
|
||||
const auto& vao = MGB_CTX->GetBoundVertexArray();
|
||||
if (!vao) return view;
|
||||
const auto& ibo = vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
if (!ibo) return view;
|
||||
view.Present = true;
|
||||
switch (question) {
|
||||
case IndexBufferQuestion::Presence:
|
||||
break;
|
||||
case IndexBufferQuestion::DriverName: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
break;
|
||||
}
|
||||
case IndexBufferQuestion::DriverNameAndSize: {
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
view.Id = resource ? resource->id : 0;
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
case IndexBufferQuestion::HostBytes:
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
ibo->SyncPersistentMappedRange();
|
||||
ibo->SyncGpuWrites();
|
||||
view.HostBytes = ibo->MappedData();
|
||||
view.Size = ibo->GetSize();
|
||||
break;
|
||||
}
|
||||
(void)entry;
|
||||
return view;
|
||||
const SharedPtr<MG_State::GLState::BufferObject>& BoundIndexBuffer() {
|
||||
static const SharedPtr<MG_State::GLState::BufferObject> none;
|
||||
const auto& vao = MG_State::pGLContext->GetBoundVertexArray();
|
||||
if (!vao) return none;
|
||||
return vao->GetIndexBufferBindingSlot().GetBoundObject();
|
||||
}
|
||||
|
||||
// The GL name PrepareForDraw left on GL_ELEMENT_ARRAY_BUFFER, i.e. what a tier
|
||||
@@ -258,57 +96,9 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// matters beyond tidiness: the VAO twin memoises that it already synced this
|
||||
// index binding and will not re-issue it on the next draw.
|
||||
Uint BoundIndexBufferId() {
|
||||
return ResolveBoundIndexBuffer(IndexBufferQuestion::DriverName, "BoundIndexBufferId").Id;
|
||||
}
|
||||
|
||||
// The GL name on GL_DRAW_INDIRECT_BUFFER, i.e. what a tier that swaps in its own scratch
|
||||
// COMMAND buffer has to put back - the exact twin of BoundIndexBufferId above, and now
|
||||
// with the same two arms.
|
||||
//
|
||||
// ---- P5e (ra2), ID-136: THE SEAT THIS FILE'S OWN RULE HAD MISSED -------------------
|
||||
//
|
||||
// This was the LAST unguarded frontend read on the multi-draw apply path, and it sat
|
||||
// twenty lines above the function that retired its neighbour. It asked
|
||||
// `MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect)` and then
|
||||
// `EnsureBufferResource(<frontend object>)` - a registry lookup keyed by the client's
|
||||
// identity, which is §4.4's rule and not only the allocator's. Under run-ahead that is
|
||||
// `Fatal{UnmigratedPipeInput, "GetBufferBindingSlot@DrawArrays"}` on every batch the
|
||||
// indirect tiers execute: 18 lane entries, and the only thing standing between the flip
|
||||
// and a green lane once the fill race was fixed.
|
||||
//
|
||||
// WHAT IT IS NOT: a data dependency. The tier does not want the client's indirect
|
||||
// buffer - it never reads a byte of it. It binds its OWN scratch command buffer
|
||||
// (g_indirectCommands) and wants to put back the name that was there. So the answer is
|
||||
// not "migrate the value" (which is what P8 owes for the ordinary indirect draw path);
|
||||
// it is "ask the side that did the binding". ID-133's escalation (iii) legalised the
|
||||
// pull instead, at the price of a rendezvous on every plain glMultiDraw* on the DEFAULT
|
||||
// tier - a real cost on the shipping arm, paid to make a lane green. ID-136 withdraws
|
||||
// that escalation and retires the read, in the commit that adds this arm.
|
||||
//
|
||||
// WHY MGPipeApplier().VerbIndirectBuffer IS THE WHOLE ANSWER ON THIS ARM: with a
|
||||
// transport, the ONLY writer of this process's GL_DRAW_INDIRECT_BUFFER outside this
|
||||
// function is DirectGLES.cpp's DrawSyncBit::IndirectBuffer arm, which binds from exactly
|
||||
// that handle and nothing else. So "what was bound" IS "what the verb's record named",
|
||||
// and a null handle is "this verb bound none" - the same 0 the monolith arm returns for
|
||||
// an empty slot, and not the arm test (ID-110: the arm was decided by the transport
|
||||
// above, never inferred from a null).
|
||||
Uint BoundDrawIndirectBufferId() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const MG_Pipe::MGPipeHandle res = MG_Pipe::MGPipeApplier().VerbIndirectBuffer;
|
||||
if (MG_Pipe::MGPipeHandleIsNull(res)) return 0;
|
||||
auto* resource = BufferImpl::EnsureBufferResourceForHandle(nullptr, res);
|
||||
if (resource == nullptr) {
|
||||
RefuseMissingIndirectBufferRecord("BoundDrawIndirectBufferId", res);
|
||||
}
|
||||
return resource->id;
|
||||
}
|
||||
#endif
|
||||
// MONOLITH GLUE from here down, token for token what this function did before.
|
||||
const auto& indirect =
|
||||
MGB_CTX->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
|
||||
if (!indirect) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(indirect);
|
||||
const auto& ibo = BoundIndexBuffer();
|
||||
if (!ibo) return 0;
|
||||
const auto* resource = BufferImpl::EnsureBufferResource(ibo);
|
||||
return resource ? resource->id : 0;
|
||||
}
|
||||
|
||||
@@ -361,10 +151,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// are bound as storage blocks. Respecifies rather than sub-updates: glBufferData
|
||||
// orphans the previous store, so the upload never waits on a dispatch still reading
|
||||
// the old contents out of the same name.
|
||||
// statsClass: which MGPipe byte population these bytes belong to. Counted here
|
||||
// rather than at the four call sites so a new tier cannot forget it.
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass) {
|
||||
Bool UploadScratch(ScratchBuffer& buffer, SizeT bytes, const void* data) {
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
BufferImpl::BindBufferId(BufferImpl::TempBufferTarget, buffer.id);
|
||||
@@ -377,9 +164,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
buffer.cursor = 0;
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, 0, static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -394,8 +178,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
constexpr SizeT kRingAlignment = 16; // >= 4, so both command and uint32-index offsets stay legal
|
||||
constexpr SizeT kMinRingBytes = 1u << 16;
|
||||
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data,
|
||||
MG_Util::PipeStats::ByteClass statsClass, SizeT& outOffset) {
|
||||
Bool UploadScratchRing(ScratchBuffer& buffer, SizeT bytes, const void* data, SizeT& outOffset) {
|
||||
outOffset = 0;
|
||||
if (bytes == 0) return true;
|
||||
if (!EnsureScratchName(buffer)) return false;
|
||||
@@ -419,9 +202,6 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (data) {
|
||||
g_GLESFuncs.glBufferSubData(BufferImpl::TempBufferTarget, static_cast<GLintptr>(outOffset),
|
||||
static_cast<GLsizeiptr>(bytes), data);
|
||||
if (MG_Util::PipeStats::Enabled()) {
|
||||
MG_Util::PipeStats::AddBytes(statsClass, static_cast<Uint64>(bytes));
|
||||
}
|
||||
}
|
||||
buffer.cursor += aligned;
|
||||
return true;
|
||||
@@ -472,7 +252,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
g_resolvedTier =
|
||||
ResolveTier(g_GLESCapabilities, g_GLESFuncs, MG_Config::Features.EsprytMultiDrawMode,
|
||||
&g_tierResolution);
|
||||
MGLOG_D("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
MGLOG_I("DirectGLES multi-draw: %s", g_tierResolution.c_str());
|
||||
}
|
||||
|
||||
// Which tiers have already announced themselves, one bit per GLESMultiDrawMode.
|
||||
@@ -487,39 +267,24 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint32 bit = 1u << static_cast<Uint32>(tier);
|
||||
if (g_announcedTiers & bit) return;
|
||||
g_announcedTiers |= bit;
|
||||
MGLOG_D("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
MGLOG_I("DirectGLES multi-draw: first batch executed via tier \"%s\"", TierName(tier));
|
||||
}
|
||||
|
||||
// The tier this particular batch can actually take. A tier is demoted here when
|
||||
// the batch's own shape - not the driver - rules it out; the compute tier keeps
|
||||
// its remaining feasibility checks inside its implementation, where the data it
|
||||
// has to walk is already in hand.
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool perSubDrawBaseVertex,
|
||||
Bool hasIndexBuffer, Bool arbitraryRestart) {
|
||||
GLESMultiDrawMode ResolveTierForBatch(Bool programReadsDrawID, Bool hasIndexBuffer) {
|
||||
ResolveTierOnce();
|
||||
GLESMultiDrawMode tier = g_resolvedTier;
|
||||
|
||||
// Desktop GL_PRIMITIVE_RESTART restarts on an application-chosen index; the driver
|
||||
// only ever restarts on the all-ones value. Every tier but the rebased one hands
|
||||
// the application's own index data to the driver, which would then see no restarts
|
||||
// at all and weld the primitives together. The rebased tier is the one that
|
||||
// REWRITES the stream, and RestartSentinelFor already tells it which value to
|
||||
// translate, so it is the only tier this batch can take.
|
||||
if (arbitraryRestart) {
|
||||
return GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
|
||||
// Batched tiers issue one driver entry for the whole batch, so the emulated
|
||||
// gl_DrawID uniform can only hold one value across every sub-draw. A program
|
||||
// that reads gl_DrawID gets an unrolled tier, which feeds each sub-draw its
|
||||
// own index (the spec's value); nothing else observes the difference. The
|
||||
// emulated gl_BaseVertex is one uniform for the same reason, so a batch whose
|
||||
// sub-draws carry their own base vertices unrolls too - even the Ext tier,
|
||||
// which hands the driver the whole basevertex array, can only leave ONE value
|
||||
// in the uniform the shader reads.
|
||||
// own index (the spec's value); nothing else observes the difference.
|
||||
const Bool batched = tier == GLESMultiDrawMode::Ext || tier == GLESMultiDrawMode::MultiIndirect ||
|
||||
tier == GLESMultiDrawMode::Compute;
|
||||
if (batched && (programReadsDrawID || perSubDrawBaseVertex)) {
|
||||
if (batched && programReadsDrawID) {
|
||||
tier = SupportsTier(GLESMultiDrawMode::BaseVertex) ? GLESMultiDrawMode::BaseVertex
|
||||
: GLESMultiDrawMode::DrawElements;
|
||||
}
|
||||
@@ -564,18 +329,13 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// CPU-readable bytes of one sub-draw's indices, from the CPU copy of the bound index
|
||||
// buffer (the frontend shadow on the monolith arm, the server's staged shadow on the
|
||||
// record arm) or straight from the client array. Null when the sub-draw would read
|
||||
// outside the buffer.
|
||||
//
|
||||
// P5e (mv): `hasIndexBuffer` is a Bool and not the frontend object it used to be,
|
||||
// because presence is the only thing this function ever asked of it - and on the record
|
||||
// arm there is no such object on this side to hand it.
|
||||
const Uint8* ResolveSubDrawIndices(Bool hasIndexBuffer, const Uint8* indexBufferBytes,
|
||||
SizeT indexBufferSize, const void* indices, SizeT indexCount,
|
||||
SizeT indexSize) {
|
||||
if (!hasIndexBuffer) {
|
||||
// CPU-readable bytes of one sub-draw's indices, from the frontend shadow of the
|
||||
// bound index buffer or straight from the client array. Null when the sub-draw
|
||||
// would read outside the buffer.
|
||||
const Uint8* ResolveSubDrawIndices(const SharedPtr<MG_State::GLState::BufferObject>& indexBuffer,
|
||||
const Uint8* indexBufferBytes, SizeT indexBufferSize, const void* indices,
|
||||
SizeT indexCount, SizeT indexSize) {
|
||||
if (!indexBuffer) {
|
||||
return static_cast<const Uint8*>(indices);
|
||||
}
|
||||
if (!indexBufferBytes) return nullptr;
|
||||
@@ -611,14 +371,14 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunIndirect(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool batched, Bool feedDrawID) {
|
||||
if (!SupportsTier(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect)) return false;
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
// Indirect commands address indices as an element offset into the bound element
|
||||
// array buffer, and an indirect draw is not defined without one.
|
||||
if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "RunIndirect").Present) return false;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return false;
|
||||
|
||||
g_commandStaging.resize(static_cast<SizeT>(drawcount));
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
@@ -636,8 +396,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
|
||||
const SizeT commandBytes = g_commandStaging.size() * sizeof(DrawElementsIndirectCommand);
|
||||
SizeT commandBase = 0;
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd, commandBase)) {
|
||||
if (!UploadScratchRing(g_indirectCommands, commandBytes, g_commandStaging.data(), commandBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -649,21 +408,15 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
const Uint previousIndirectBinding = BoundDrawIndirectBufferId();
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, g_indirectCommands.id);
|
||||
if (batched) {
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||
drawcount, 0);
|
||||
});
|
||||
g_GLESFuncs.glMultiDrawElementsIndirectEXT(mode, type, reinterpret_cast<const void*>(commandBase),
|
||||
drawcount, 0);
|
||||
} else {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
const SizeT commandOffset = commandBase + static_cast<SizeT>(i) * sizeof(DrawElementsIndirectCommand);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
});
|
||||
g_GLESFuncs.glDrawElementsIndirect(mode, type, reinterpret_cast<const void*>(commandOffset));
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
}
|
||||
BufferImpl::BindBufferId(GL_DRAW_INDIRECT_BUFFER, previousIndirectBinding);
|
||||
NoteTierExecuted(batched ? GLESMultiDrawMode::MultiIndirect : GLESMultiDrawMode::Indirect);
|
||||
@@ -675,19 +428,15 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunBaseVertexLoop(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID, Bool feedBaseVertex) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
if (!SupportsTier(GLESMultiDrawMode::BaseVertex)) return false;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
});
|
||||
g_GLESFuncs.glDrawElementsBaseVertex(mode, count[i], type, indices[i],
|
||||
basevertex ? basevertex[i] : 0);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
NoteTierExecuted(GLESMultiDrawMode::BaseVertex);
|
||||
return true;
|
||||
}
|
||||
@@ -697,8 +446,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Bool RunRebasedDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID,
|
||||
Bool feedBaseVertex) {
|
||||
GLsizei drawcount, const GLint* basevertex, Bool feedDrawID) {
|
||||
const SizeT indexSize = IndexTypeSize(type);
|
||||
if (indexSize == 0) return false;
|
||||
|
||||
@@ -709,32 +457,29 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
if (total == 0) return true;
|
||||
if (total > kMaxFlattenedIndices) return false;
|
||||
|
||||
const BoundIndexBufferView indexBuffer =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::HostBytes, "RunRebasedDrawElements");
|
||||
const Uint8* const indexBufferBytes = indexBuffer.HostBytes;
|
||||
const SizeT indexBufferSize = indexBuffer.Size;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
const Uint8* indexBufferBytes = nullptr;
|
||||
SizeT indexBufferSize = 0;
|
||||
if (indexBuffer) {
|
||||
// The shadow is the source of truth for CPU reads, but a persistent map or
|
||||
// a shader write may have moved past it since the last sync.
|
||||
indexBuffer->SyncPersistentMappedRange();
|
||||
indexBuffer->SyncGpuWrites();
|
||||
indexBufferBytes = indexBuffer->MappedData();
|
||||
indexBufferSize = indexBuffer->GetSize();
|
||||
}
|
||||
|
||||
const Bool restartActive = RestartActive();
|
||||
const Uint32 restartSentinel = RestartSentinelFor(type);
|
||||
// Widening to GL_UNSIGNED_INT gives a UBYTE/USHORT source a sentinel it can never
|
||||
// spell, so those batches are lossless. A UINT source that already uses 0xFFFFFFFF as
|
||||
// a real vertex index while restarting on a different one is the one shape 32 bits
|
||||
// cannot express - the same corner the single-draw substitution reports.
|
||||
if (restartActive && indexSize == 4 && restartSentinel != 0xFFFFFFFFu) {
|
||||
MGLOG_E_ONCE("GL_PRIMITIVE_RESTART with restart index %u over GL_UNSIGNED_INT multi-draw indices: "
|
||||
"any index that is already 0xFFFFFFFF will restart too, because the rewritten stream "
|
||||
"has no wider sentinel to move to.",
|
||||
restartSentinel);
|
||||
}
|
||||
g_indexStaging.resize(total);
|
||||
SizeT cursor = 0;
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
const SizeT subDrawCount = static_cast<SizeT>(count[i]);
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer.Present, indexBufferBytes, indexBufferSize,
|
||||
indices[i], subDrawCount, indexSize);
|
||||
const Uint8* source = ResolveSubDrawIndices(indexBuffer, indexBufferBytes, indexBufferSize, indices[i],
|
||||
subDrawCount, indexSize);
|
||||
if (!source) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
MGLOG_E("DirectGLES multi-draw (drawelements tier): sub-draw %d reads outside the bound index "
|
||||
"buffer; skipping the batch",
|
||||
i);
|
||||
return false;
|
||||
@@ -745,8 +490,7 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
}
|
||||
|
||||
SizeT indexBase = 0;
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient, indexBase)) {
|
||||
if (!UploadScratchRing(g_rebasedIndices, total * sizeof(Uint32), g_indexStaging.data(), indexBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -756,18 +500,11 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
|
||||
for (GLsizei i = 0; i < drawcount; ++i) {
|
||||
if (count[i] <= 0) continue;
|
||||
if (feedDrawID) SetCurrentDrawID(static_cast<Uint32>(i));
|
||||
// The base vertex is folded into the rewritten index stream here, so the
|
||||
// driver sees none - but gl_BaseVertex still has to report the value the
|
||||
// application passed for this sub-draw.
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(basevertex ? basevertex[i] : 0);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
});
|
||||
g_GLESFuncs.glDrawElements(mode, count[i], GL_UNSIGNED_INT,
|
||||
reinterpret_cast<const void*>(indexBase + cursor * sizeof(Uint32)));
|
||||
cursor += static_cast<SizeT>(count[i]);
|
||||
}
|
||||
if (feedDrawID) SetCurrentDrawID(0);
|
||||
if (feedBaseVertex) SetCurrentBaseVertex(0);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
NoteTierExecuted(GLESMultiDrawMode::DrawElements);
|
||||
return true;
|
||||
@@ -843,7 +580,7 @@ void main() {
|
||||
|
||||
const GLuint shader = g_GLESFuncs.glCreateShader(GL_COMPUTE_SHADER);
|
||||
if (shader == 0) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateShader(GL_COMPUTE_SHADER) failed");
|
||||
return false;
|
||||
}
|
||||
const char* source = kFlattenComputeSource;
|
||||
@@ -854,14 +591,14 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetShaderInfoLog(shader, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening shader failed to compile: %s", log);
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint program = g_GLESFuncs.glCreateProgram();
|
||||
if (program == 0) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): glCreateProgram failed");
|
||||
g_GLESFuncs.glDeleteShader(shader);
|
||||
return false;
|
||||
}
|
||||
@@ -872,7 +609,7 @@ void main() {
|
||||
if (status != GL_TRUE) {
|
||||
char log[1024] = {};
|
||||
g_GLESFuncs.glGetProgramInfoLog(program, sizeof(log) - 1, nullptr, log);
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
MGLOG_E("DirectGLES multi-draw (compute tier): index-flattening program failed to link: %s", log);
|
||||
g_GLESFuncs.glDeleteProgram(program);
|
||||
return false;
|
||||
}
|
||||
@@ -882,7 +619,7 @@ void main() {
|
||||
g_uDrawCount = g_GLESFuncs.glGetUniformLocation(program, "uDrawCount");
|
||||
g_uTotalIndices = g_GLESFuncs.glGetUniformLocation(program, "uTotalIndices");
|
||||
g_computeProgramFailed = false;
|
||||
MGLOG_D("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
MGLOG_I("DirectGLES multi-draw: index-flattening compute program ready (id %u)", program);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -914,18 +651,16 @@ void main() {
|
||||
|
||||
// The shader reads the source indices as a storage buffer, so there has to be
|
||||
// a real buffer to read - a client-memory index array has none.
|
||||
if (!ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "FlattenWithCompute").Present) return;
|
||||
const auto& indexBuffer = BoundIndexBuffer();
|
||||
if (!indexBuffer) return;
|
||||
|
||||
// A dispatch inside an open capture span is not legal, and the span would also
|
||||
// observe one merged draw rather than the batch it asked for.
|
||||
if (XfbImpl::IsCaptureSpanOpen()) return;
|
||||
|
||||
// Asked a second time, and one question later: the ensure below may do GL work, so
|
||||
// it stays BEHIND the capture-span check exactly as it was before P5e (mv).
|
||||
const BoundIndexBufferView source =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::DriverNameAndSize, "FlattenWithCompute");
|
||||
if (source.Id == 0) return;
|
||||
const SizeT sourceSize = source.Size;
|
||||
auto* sourceResource = BufferImpl::EnsureBufferResource(indexBuffer);
|
||||
if (!sourceResource || sourceResource->id == 0) return;
|
||||
const SizeT sourceSize = indexBuffer->GetSize();
|
||||
// std430 addresses the source as uint[]; a tail shorter than a word is not
|
||||
// reachable, so a narrow index type needs a word-multiple buffer.
|
||||
if (indexSize < 4 && (sourceSize % 4) != 0) return;
|
||||
@@ -953,18 +688,12 @@ void main() {
|
||||
if (total == 0) return; // nothing to draw; the ordinary tiers no-op just as well
|
||||
|
||||
if (!EnsureComputeProgram()) return;
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data(),
|
||||
MG_Util::PipeStats::ByteClass::StageIndirectCmd)) {
|
||||
return;
|
||||
}
|
||||
// data == nullptr: pure respecify, the compute pass writes the contents, so no
|
||||
// host bytes cross here and nothing is counted.
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr,
|
||||
MG_Util::PipeStats::ByteClass::StageIndexClient)) {
|
||||
if (!UploadScratch(g_drawInfo, g_drawInfoStaging.size() * sizeof(Uint32), g_drawInfoStaging.data())) {
|
||||
return;
|
||||
}
|
||||
if (!UploadScratch(g_flattenedIndices, total * sizeof(Uint32), nullptr)) return;
|
||||
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, source.Id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 0, sourceResource->id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 1, g_drawInfo.id);
|
||||
BufferImpl::BindBufferBaseCached(GL_SHADER_STORAGE_BUFFER, 2, g_flattenedIndices.id);
|
||||
|
||||
@@ -1099,31 +828,17 @@ void main() {
|
||||
void DrawElementsBatch(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
|
||||
GLsizei drawcount, const GLint* basevertex) {
|
||||
if (drawcount <= 0 || !count || !indices) return;
|
||||
// Read before any GL work, because it decides the tier below: a desktop restart index
|
||||
// the driver does not know about can only be honoured by the tier that rewrites the
|
||||
// index stream (see ResolveTierForBatch). A restart index this index type cannot hold
|
||||
// needs no rewrite at all - nothing can match it - but it does need the driver's own
|
||||
// fixed-index restart held off for the batch, which is what the scope below does.
|
||||
const RestartSubstitutionKind restartKind = ResolveRestartSubstitution(type);
|
||||
const Bool arbitraryRestart = restartKind == RestartSubstitutionKind::RewriteIndices;
|
||||
const ScopedSuppressedPrimitiveRestart restartCapOverride(restartKind);
|
||||
// State-independent and possibly throwing, so it runs before any GL work.
|
||||
CheckPrimitiveRestartSupported(type);
|
||||
|
||||
const Bool hasIndexBuffer =
|
||||
ResolveBoundIndexBuffer(IndexBufferQuestion::Presence, "DrawElementsBatch").Present;
|
||||
const Bool hasIndexBuffer = BoundIndexBuffer() != nullptr;
|
||||
|
||||
// The compute tier dispatches BEFORE the draw state is established: doing it
|
||||
// afterwards would mean unpicking the program, SSBO and index bindings
|
||||
// PrepareForDraw just made, and a dispatch inside an open transform feedback
|
||||
// span is not legal at all. On success it hands back a flattened index stream.
|
||||
// A batch whose sub-draws carry their own base vertices cannot be flattened either
|
||||
// when the program reads gl_BaseVertex: one draw call leaves one uniform value.
|
||||
// Asked conservatively because this decision precedes PrepareForDraw - see
|
||||
// CurrentProgramMayNeedPerSubDrawBuiltins. Flattening is the irreversible half:
|
||||
// once the batch is one draw the values are gone, whereas declining to flatten only
|
||||
// costs the unrolled tier.
|
||||
FlattenedStream flattened;
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute &&
|
||||
!CurrentProgramMayNeedPerSubDrawBuiltins(basevertex != nullptr)) {
|
||||
if (ResolvedTier() == GLESMultiDrawMode::Compute && !CurrentProgramReadsDrawID()) {
|
||||
FlattenWithCompute(mode, count, type, indices, drawcount, basevertex, flattened);
|
||||
}
|
||||
|
||||
@@ -1132,19 +847,13 @@ void main() {
|
||||
if (flattened.indexCount != 0) {
|
||||
const Uint previousIndexBinding = BoundIndexBufferId();
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, flattened.bufferId);
|
||||
ForEachViewportRoutingPass([&] {
|
||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||
});
|
||||
g_GLESFuncs.glDrawElements(mode, static_cast<GLsizei>(flattened.indexCount), GL_UNSIGNED_INT, nullptr);
|
||||
BufferImpl::BindBufferId(GL_ELEMENT_ARRAY_BUFFER, previousIndexBinding);
|
||||
return;
|
||||
}
|
||||
|
||||
// Now that PrepareForDraw has synced the program, both questions have real answers;
|
||||
// the tier choice and the per-sub-draw feeds use those, not the guess above.
|
||||
const Bool feedDrawID = CurrentProgramReadsDrawID();
|
||||
const Bool feedBaseVertex = basevertex != nullptr && CurrentProgramReadsBaseVertex();
|
||||
const GLESMultiDrawMode tier =
|
||||
ResolveTierForBatch(feedDrawID, feedBaseVertex, hasIndexBuffer, arbitraryRestart);
|
||||
const GLESMultiDrawMode tier = ResolveTierForBatch(feedDrawID, hasIndexBuffer);
|
||||
|
||||
Bool drawn = false;
|
||||
switch (tier) {
|
||||
@@ -1152,19 +861,16 @@ void main() {
|
||||
drawn = RunExt(mode, count, type, indices, drawcount, basevertex);
|
||||
break;
|
||||
case GLESMultiDrawMode::MultiIndirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID,
|
||||
feedBaseVertex);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/true, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::Indirect:
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID,
|
||||
feedBaseVertex);
|
||||
drawn = RunIndirect(mode, count, type, indices, drawcount, basevertex, /*batched=*/false, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::BaseVertex:
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::DrawElements:
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
break;
|
||||
case GLESMultiDrawMode::Compute:
|
||||
// Its pre-pass ran above; reaching here means it declined this batch's shape.
|
||||
@@ -1176,18 +882,11 @@ void main() {
|
||||
// Every tier above may decline a batch whose shape it cannot express. The two
|
||||
// below are the floor: a base-vertex replay where the driver has one, and the
|
||||
// rewritten index stream where it does not. Both are safe for any batch these
|
||||
// entry points can receive - except that the base-vertex replay hands the
|
||||
// application's own indices to the driver, which cannot restart on a desktop
|
||||
// restart index, so that batch has only the rewriting floor.
|
||||
if (!drawn && !arbitraryRestart) {
|
||||
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
|
||||
}
|
||||
// entry points can receive.
|
||||
if (!drawn) drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID);
|
||||
if (!drawn) {
|
||||
drawn = RunRebasedDrawElements(mode, count, type, indices, drawcount, basevertex, feedDrawID,
|
||||
feedBaseVertex);
|
||||
}
|
||||
if (!drawn) {
|
||||
MGLOG_E_ONCE("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
MGLOG_E("DirectGLES multi-draw: no usable tier for a %d sub-draw batch (mode 0x%x, type 0x%x); "
|
||||
"the batch was dropped",
|
||||
drawcount, mode, type);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -13,19 +13,6 @@
|
||||
#include <MG_Util/Texture/TextureFormatProcessor.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectGLES {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// C6 / ID-52 / CONTRACT-P5 table 3's pActiveBackendObject row. The format-capability cache of
|
||||
// THIS ROLE's backend. Under an active transport the server's own private
|
||||
// BackendObject_DirectGLES owns the context on the apply thread, so a backend-internal format
|
||||
// lookup must read ITS probed cache - not pActiveBackendObject's, which under split is the
|
||||
// CLIENT's BackendObject_Remote mirror (a caps snapshot, generation-lagged, and on an
|
||||
// independent server not usable at all). Monolith build/transport: pActiveBackendObject, so a
|
||||
// pull build never sees this symbol. Null when no backend is up. The seven table-3 reads
|
||||
// (five in Utils.cpp, ClampSamplesToBackendSupport in BackendObject_DirectGLES.cpp) go through
|
||||
// this instead of dereferencing pActiveBackendObject directly.
|
||||
const FormatCapabilityCache* ActiveBackendFormatCaps();
|
||||
#endif
|
||||
|
||||
namespace DebugImpl {
|
||||
class ErrorLopper {
|
||||
public:
|
||||
@@ -59,15 +46,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Flags<PixelFormatNormalizeOptionBit> GetRenderTargetNormalizeOptions(
|
||||
const MG_External::GLESCapabilities& capabilities, SizeT targetIndex);
|
||||
|
||||
// Whether this format's ES storage is widened to 8-bit-per-channel because the
|
||||
// driver stores some packed16 allocations with a mirrored field order
|
||||
// (PixelFormatNormalizeOptionBit::WidenPacked16Norm). True only for
|
||||
// GL_RGB565/GL_RGB5(_A1)/GL_RGBA4, and only where the POST probe measured the
|
||||
// divergence (or MOBILEGL_ESPRYT_WIDEN_PACKED16_STORAGE forces it). The transfer paths
|
||||
// consult it too: the packed-norm re-upload leg must stand down when the ES storage
|
||||
// is no longer 16-bit packed.
|
||||
Bool UsesWidenedPacked16NormStorage(TextureInternalFormat internalFormat);
|
||||
|
||||
void GenerateTextureFormatInfo(TextureInternalFormat internalFormat, GLenum* outInternalFormat,
|
||||
GLenum* outFormat, GLenum* outType,
|
||||
TextureTarget target = TextureTarget::Unknown);
|
||||
@@ -82,115 +60,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target);
|
||||
Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat);
|
||||
Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat);
|
||||
|
||||
// The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format
|
||||
// GLSL ES cannot spell as an image is carried by one it can.
|
||||
//
|
||||
// GL has forty image formats, GLSL ES core has thirteen, and no test device advertises
|
||||
// GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal
|
||||
// ESSL at all and glBindImageTexture rejects the narrow format outright for most of them
|
||||
// (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis).
|
||||
// Seventeen have a core format of the SAME per-channel width and component type,
|
||||
// differing only in channel count, and in one of those the emulation is EXACT: GL already
|
||||
// defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as
|
||||
// dropping the components the format does not have, so the carrier's surplus channels
|
||||
// hold values GL has already named. WidenImageFormatsPass pins them in the shader; this
|
||||
// is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind
|
||||
// half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers.
|
||||
//
|
||||
// Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already,
|
||||
// for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16,
|
||||
// r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling"
|
||||
// diagnostic rather than a silent approximation), and on a driver that HAS
|
||||
// GL_NV_image_formats, where the shader keeps the declared format and no widening may
|
||||
// happen behind it.
|
||||
//
|
||||
// The widened triple REPLACES what GenerateTextureFormatInfo chose, including any
|
||||
// renderability substitution: an image that cannot be image-bound is useless whatever its
|
||||
// attachment behaviour, so the image constraint wins. In practice that only bites
|
||||
// RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays
|
||||
// signed-normalized instead of becoming the half float that fallback would have picked -
|
||||
// so an image-bound texture in one of those two formats is no longer attachable, and
|
||||
// glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore
|
||||
// does not update. Accepted deliberately: before the widening, an image binding in either
|
||||
// format was refused outright by every driver tested and the stage that declared it never
|
||||
// compiled at all, so nothing that works today is being given up.
|
||||
//
|
||||
// KNOWN GAP, for the same "all three layers move together" reason: a widened texture that
|
||||
// is ALSO an FBO colour attachment gains one to three writable channels, and a draw into
|
||||
// it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered
|
||||
// (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a
|
||||
// glReadPixels/glGetTexImage that asks for more channels than the frontend format has
|
||||
// would see them. Closing it needs the per-draw-buffer colour mask the three-channel
|
||||
// widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized
|
||||
// from "alpha" to a channel count, which is its own change.
|
||||
// How the FRONTEND's CPU shadow for a widened format is laid out relative to the carrier's
|
||||
// transfer, i.e. what the upload has to do to it. Almost every entry is `Components`: the
|
||||
// shadow already holds SourceChannels components of exactly the carrier's own type, so
|
||||
// padding it out to four is the whole conversion. The packed entries do not - their shadow
|
||||
// is ONE 32-bit word per texel - and reading such a word as components of the carrier's
|
||||
// type takes twelve or sixteen bytes out of four and shears the level.
|
||||
enum class ImageWidenSourceEncoding : Uint8 {
|
||||
Components = 0,
|
||||
// r11f_g11f_b10f: GL_UNSIGNED_INT_10F_11F_11F_REV -> four GL_FLOATs of an rgba16f.
|
||||
PackedFloat11f11f10f,
|
||||
// rgb10_a2 and rgb10_a2ui: GL_UNSIGNED_INT_2_10_10_10_REV -> four GL_UNSIGNED_SHORT
|
||||
// channel CODES of an rgba16ui. The same split serves both: the two formats differ
|
||||
// only in what the codes MEAN, which is the shader's business and not the transfer's.
|
||||
PackedInt2101010Rev,
|
||||
};
|
||||
|
||||
struct ImageBindableStorageWidening {
|
||||
GLenum InternalFormat = GL_UNKNOWN_MGL;
|
||||
GLenum Format = GL_UNKNOWN_MGL;
|
||||
GLenum Type = GL_UNKNOWN_MGL;
|
||||
// Channels the FRONTEND format has, i.e. how many of the carrier's four the client
|
||||
// data fills. The rest are uploaded as 0, and the fourth as the format's implied 1.
|
||||
Uint SourceChannels = 0;
|
||||
// Whether that implied 1 is the integer one or a saturated normalized field - the
|
||||
// transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and
|
||||
// RG8UI), so the carrier decides.
|
||||
Bool IntegerData = false;
|
||||
// What the upload has to do to the frontend shadow before it describes the level to
|
||||
// the driver (PrepareImageWidenedUpload).
|
||||
ImageWidenSourceEncoding SourceEncoding = ImageWidenSourceEncoding::Components;
|
||||
// Non-zero when the carrier holds this format's channels as the INTEGER CODES of a
|
||||
// NORMALIZED value - the seven 16-bit and 10-bit normalized formats, which core ESSL
|
||||
// has no image format of any width for and which a float carrier would requantise.
|
||||
// Each entry is the largest code that channel can hold, i.e. the denominator of GL 4.6
|
||||
// 2.3.5; SignedNormalized picks which of the two conversions it is the denominator of.
|
||||
//
|
||||
// Two things depend on it, both because the ES storage no longer shares the frontend
|
||||
// format's component class: the upload pads a missing alpha with ChannelMax[3] instead
|
||||
// of the transfer type's own "one" (through a uint carrier the saturated field IS the
|
||||
// one), and glGetTexImage divides the codes back out into the floats the application
|
||||
// is still owed.
|
||||
Uint ChannelMax[4] = {0u, 0u, 0u, 0u};
|
||||
Bool SignedNormalized = false;
|
||||
|
||||
Bool CarriesNormalizedCodes() const { return ChannelMax[0] != 0u; }
|
||||
explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; }
|
||||
};
|
||||
ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat);
|
||||
|
||||
// The single-channel core format an image-bindable BUFFER texture's view is SPLIT into, or
|
||||
// GL_UNKNOWN_MGL for a format that needs no split (or has no core base).
|
||||
//
|
||||
// A buffer texture cannot be widened: its texels are the application's buffer object, at
|
||||
// the size and layout the application gave it, and it is usually also a vertex, index or
|
||||
// storage buffer whose bytes are not ours to restride. But an rg32f view of N texels and
|
||||
// an r32f view of 2N texels describe exactly the SAME bytes, so the split changes only
|
||||
// how the shader subscripts them - component j of texel i is texel 2i + j of the base
|
||||
// view - which WidenImageFormatsPass rewrites every access to do. The same rule as the
|
||||
// widening decides WHETHER: a driver that can spell rg32f for an imageBuffer needs
|
||||
// nothing.
|
||||
//
|
||||
// KNOWN GAP, and the reason this is not applied to a texture that is merely sampled: a
|
||||
// buffer texture that is BOTH image-bound and read through a samplerBuffer would have its
|
||||
// sampled view split too, and the sampler side is not rewritten. Accepted for the same
|
||||
// reason the storage widening's gaps are - on a driver where the split applies at all
|
||||
// there is no legal ESSL for the image declaration, so such a program did not compile.
|
||||
GLenum GetImageBindableBufferSplitFormat(TextureInternalFormat internalFormat);
|
||||
} // namespace TextureImpl
|
||||
|
||||
namespace FramebufferImpl {} // namespace FramebufferImpl
|
||||
@@ -246,16 +115,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
Bool StoreWideRowsToClient(const Uint8* wide, GLenum wideType, GLsizei width, GLsizei sliceHeight,
|
||||
GLsizei sliceCount, const ReadbackChannelMapping& mapping, GLenum type,
|
||||
void* pixels, Bool applyPackImageParams);
|
||||
|
||||
// Stores packed 32-bit source words verbatim, with the same destination addressing, PACK
|
||||
// parameters and pixel-pack-buffer handling as StoreWideRowsToClient. For the sources whose
|
||||
// storage word already IS the client word (MG_Util::IsRawPackedPixelTransfer): routing those
|
||||
// through the wide float intermediate re-encodes them, and the RGB9_E5 encoder canonicalizes
|
||||
// the shared exponent, so glGetTexImage would answer with different bits than were stored.
|
||||
// `srcWords` holds sliceHeight * sliceCount tightly stacked rows of `width` 32-bit words.
|
||||
// False when `type` is not a 4-byte packed type.
|
||||
Bool StorePackedWordsToClient(const Uint8* srcWords, GLsizei width, GLsizei sliceHeight, GLsizei sliceCount,
|
||||
GLenum type, void* pixels, Bool applyPackImageParams);
|
||||
} // namespace ReadbackImpl
|
||||
|
||||
namespace PrgramImpl {
|
||||
@@ -271,265 +130,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// drawBufferCount <= 1, i.e. for everything but a framebuffer that actually
|
||||
// enables several draw buffers, so the ordinary single-target shader is untouched.
|
||||
String BroadcastLegacyFragColor(String glslCode, GLenum shaderType, Uint drawBufferCount);
|
||||
// SPIRV-Cross emits `#extension GL_EXT_texture_buffer : require` for every buffer-texture
|
||||
// sampler when it targets ESSL below 320, and offers no way to ask for the OES spelling.
|
||||
// On a driver that advertises only GL_OES_texture_buffer that directive is a compile
|
||||
// error, so the name is retargeted in the emitted source. A no-op on every other tier:
|
||||
// ES 3.2 needs no directive at all and an EXT driver already has the right one.
|
||||
String RetargetTextureBufferExtension(String glslCode,
|
||||
MG_External::GLESCapabilities::TextureBufferTier tier);
|
||||
// Adds `#extension GL_NV_image_formats : require` when the shader carries an image
|
||||
// format qualifier GLSL ES has no core spelling for. SPIRV-Cross prints the format and
|
||||
// asks for nothing, so the request has to be made here. `needed` is the caller's answer,
|
||||
// because only it knows which formats are in play AND whether the driver advertises the
|
||||
// extension - requesting an unadvertised extension is itself a compile error, so this is
|
||||
// never emitted speculatively. A no-op when not needed or already present.
|
||||
String RequestExtendedImageFormats(String glslCode, Bool needed);
|
||||
// Adds `#extension GL_OES_viewport_array : require` when the emitted ESSL names
|
||||
// gl_ViewportIndex. SPIRV-Cross prints that identifier and asks for nothing (unlike
|
||||
// gl_Layer, which it backs with GL_NV_viewport_array2 on ES) and ESSL has no core
|
||||
// spelling for it at any version, so the request has to be made here or the stage does
|
||||
// not compile - which loses the whole program, not just the multi-viewport routing.
|
||||
// `needed` is the caller's answer for the same reason as above: only it knows whether the
|
||||
// driver advertises the extension, and requesting an unadvertised one is itself a compile
|
||||
// error, so this is never emitted speculatively. A no-op when not needed or already
|
||||
// present.
|
||||
String RequestViewportArrayExtension(String glslCode, Bool needed);
|
||||
// Adds `#extension <extensionName> : require` when a TESSELLATION or GEOMETRY stage's
|
||||
// emitted ESSL names gl_PointSize. Desktop GL has that built-in in gl_PerVertex for every
|
||||
// vertex-processing stage; ESSL does NOT have it in those two at any version - not even
|
||||
// 320, where the stages themselves are core - until EXT/OES_tessellation_point_size resp.
|
||||
// EXT/OES_geometry_point_size is requested. SPIRV-Cross prints the identifier bare and
|
||||
// asks for nothing, exactly as it does for gl_ViewportIndex, so without this the stage
|
||||
// fails to compile with "`gl_PointSize' undeclared" and the WHOLE program is replaced by
|
||||
// program 0 - the draw renders nothing and any transform-feedback capture it was carrying
|
||||
// is rejected outright. `extensionName` is the caller's answer, nullptr when the driver
|
||||
// advertises neither spelling, because requesting an unadvertised extension is itself a
|
||||
// compile error. A no-op when nullptr or already present.
|
||||
String RequestPointSizeExtension(String glslCode, const char* extensionName);
|
||||
// The extension name RequestPointSizeExtension should be given for `tier`, or nullptr for
|
||||
// PointSizeTier::None. `tessellation` picks the tessellation spellings over the geometry
|
||||
// ones; the two extensions are separate and neither implies the other.
|
||||
const char* PointSizeExtensionName(MG_External::GLESCapabilities::PointSizeTier tier, Bool tessellation);
|
||||
// Writes a format layout qualifier into the image declarations named in
|
||||
// `esslFormatByUniformName` that still have none. The completion half of the image-format
|
||||
// bake, and ONLY that: the SPIR-V pass (BakeImageFormatsPass) is what normally puts the
|
||||
// format in, but SPIRV-Cross throws rather than printing the formats it calls
|
||||
// desktop-only when it targets ESSL - r8ui among them, which is what the stencil half of
|
||||
// KHR-GL4x.packed_depth_stencil.stencil_texturing binds - and a throw loses the whole
|
||||
// stage. So those formats stay out of the module and are spelled here instead, on the
|
||||
// emitted text, where nothing can refuse them.
|
||||
//
|
||||
// Declarations that already carry a format are left exactly as they are, whoever wrote
|
||||
// it. Must run before RemoveLayoutBinding, which is where an image's layout qualifier
|
||||
// stops being safe to edit by hand.
|
||||
String BakeImageFormatQualifiers(String glslCode, const UnorderedMap<String, String>& esslFormatByUniformName);
|
||||
String RemoveLayoutBinding(const String& glslCode);
|
||||
// Prefix of the per-element scalar declarations RemapImageArrayElementUnits splits an
|
||||
// image array into; the suffix is the array's own name and the element's index.
|
||||
constexpr const char* IMAGE_ARRAY_ELEMENT_PREFIX = "mg_imageElem_";
|
||||
// One image ARRAY whose elements the application pointed at units that are not
|
||||
// consecutive-from-element-zero.
|
||||
struct ImageArrayUnitPlan {
|
||||
String name; // the array's name, exactly as the emitted ESSL declares it
|
||||
Vector<Int> units; // the frontend image unit element k has to reach
|
||||
};
|
||||
// Desktop GL lets an application give each element of an image array an ARBITRARY unit
|
||||
// (glUniform1i per element). ES has no such call at all - "ES image units come
|
||||
// exclusively from the layout(binding=N) qualifier" - and one declaration carries one
|
||||
// binding, so ESSL nails an array's elements to the CONSECUTIVE units N, N+1, N+2, ...
|
||||
// MobileGL used to stamp element [0]'s unit as the binding and let the rest fall where
|
||||
// they fell: KHR-GL4x.shader_image_load_store.advanced-sso-simple assigns 0,2,4,6 and
|
||||
// 1,3,5,7, so its two programs actually addressed 0,1,2,3 and 1,2,3,4 - one layer got the
|
||||
// wrong value and three were never written, with no GL error and no link log. The same
|
||||
// defect for SAMPLER arrays was fixed API-side (SubscriptUniformNameForElement); an image
|
||||
// array has no API side to fix, because ES makes glUniform1i on an image uniform an
|
||||
// INVALID_OPERATION.
|
||||
//
|
||||
// Repaired by SPLITTING the array into one SCALAR image uniform per element, each with
|
||||
// its own layout(binding = N), and rewriting `name[k]` to the scalar declared for
|
||||
// element k. One declaration carries one binding, so one declaration per unit is the
|
||||
// only spelling that reaches an arbitrary set of them.
|
||||
//
|
||||
// That rewrite needs every k in the emitted text to be a LITERAL, and it is:
|
||||
// LegalizeResourceArrayIndexingForEssl has already folded or lowered every dynamic
|
||||
// image-array subscript in the module, because ESSL forbids one outright ("image arrays
|
||||
// indexed with non-constant expressions are forbidden in GLSL ES", Mesa 26.1.4 at
|
||||
// ES 3.2, on a raw GLES probe with no MobileGL in the loop). The earlier shape here -
|
||||
// widening the array to cover the whole span of units and routing each subscript through
|
||||
// a `const highp int` offset table - was written before that pass covered images, and
|
||||
// the table lookup was itself one of the non-constant expressions the same probe refuses.
|
||||
// The split also costs exactly the image uniforms the application declared, where the
|
||||
// widening cost the whole SPAN (seven for the four elements of
|
||||
// KHR-GL42.shader_image_load_store.advanced-sso-simple), so there is no budget for it to
|
||||
// fail to fit in.
|
||||
//
|
||||
// Declines - leaving the array exactly as it was, and naming it in `outDeclined` for the
|
||||
// caller to report - when the emitted extent disagrees with the reflection, when the
|
||||
// array is reached by anything other than a subscript, or when a subscript is not a
|
||||
// literal element index. Silence was the whole defect here, so a decline must be audible.
|
||||
//
|
||||
// Must run AFTER RebindImageUniformsToFrontendUnits and BakeImageFormatQualifiers (both
|
||||
// key on the GL uniform name and on a binding already being stamped) and BEFORE
|
||||
// SplitReadWriteImageUniforms (so each element that is both read and written is split
|
||||
// with its own binding already on it) and RemoveLayoutBinding (which is what preserves
|
||||
// image bindings). Like them, it is downstream of the L2 shader-translation memo, so the
|
||||
// per-program units it reads need no entry in BuildEsslTranslationKey.
|
||||
String RemapImageArrayElementUnits(const String& glslCode, const Vector<ImageArrayUnitPlan>& plans,
|
||||
Vector<String>* outDeclined = nullptr);
|
||||
// The member list of a `gl_PerVertex { ... }` redeclaration in already-emitted ESSL -
|
||||
// the text between the braces, verbatim - or nullopt when the shader does not redeclare
|
||||
// the block in that direction. `input` selects the `in gl_PerVertex` form over the
|
||||
// `out` one.
|
||||
//
|
||||
// Exists so BuildPassthroughTessControlEssl can MIRROR the stages it has to sit between
|
||||
// rather than guess at them. Whether SPIRV-Cross redeclares the built-in block, and with
|
||||
// which members, depends on what the application's shader touched; a synthesized stage
|
||||
// that redeclares a different shape than its neighbours is an ES link error against a
|
||||
// program that has no other problem.
|
||||
std::optional<String> ExtractPerVertexBlockMembers(const String& essl, Bool input);
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes: "the input
|
||||
// patch is passed through unmodified", the output patch has PATCH_VERTICES vertices, and
|
||||
// the levels come from the PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL state.
|
||||
//
|
||||
// Desktop GL makes the control stage OPTIONAL. OpenGL ES 3.2 does not: it has no
|
||||
// PATCH_DEFAULT_*_LEVEL state at all (only glPatchParameteri, for PATCH_VERTICES) and
|
||||
// rejects a program that has an evaluation stage without a control stage - with an EMPTY
|
||||
// info log, verified on an Adreno 830 with no MobileGL in the process. MobileGL's own
|
||||
// frontend link succeeds, so the program reports GL_LINK_STATUS = TRUE, program 0 is
|
||||
// bound in its place, and every draw silently renders nothing.
|
||||
//
|
||||
// `inPerVertexMembers` / `outPerVertexMembers` are the member lists to redeclare gl_in
|
||||
// and gl_out with - normally taken from the neighbouring stages' own emitted ESSL via
|
||||
// ExtractPerVertexBlockMembers, and empty to leave the driver's built-in declaration
|
||||
// alone, which is what matching a neighbour that did not redeclare requires.
|
||||
//
|
||||
// All four outer levels and both inner levels are written unconditionally: writing a
|
||||
// level the evaluation stage's domain does not use is legal and ignored, and it saves
|
||||
// this from having to know the domain. They are the GL_PATCH_DEFAULT_OUTER_LEVEL /
|
||||
// GL_PATCH_DEFAULT_INNER_LEVEL state, baked in as literals - ES has no such state and no
|
||||
// glPatchParameterfv to forward to, so compiling them in is the only way to honour them.
|
||||
// That makes them part of what a built program is stale against, exactly as PATCH_VERTICES
|
||||
// is: see the staleness clause in DirectGLES.cpp's SyncCurrentProgram, which compares both.
|
||||
//
|
||||
// The same stage, for the same reason, that DirectVulkan synthesizes in
|
||||
// ProgramFactory::BuildPassthroughTessControlSource - Vulkan likewise requires both
|
||||
// tessellation stages. Kept as two generators rather than one because the two targets
|
||||
// disagree on everything but the algorithm: desktop GLSL 450 against ESSL, a fixed
|
||||
// gl_PerVertex shape that Vulkan matches structurally against a mirrored one, and a
|
||||
// VkShaderModule against a driver shader object.
|
||||
String BuildPassthroughTessControlEssl(Uint esslVersion, Uint patchVertices,
|
||||
const String& inPerVertexMembers,
|
||||
const String& outPerVertexMembers,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel);
|
||||
// Prefix of the writeonly half a read+write image uniform is split into (see
|
||||
// SplitReadWriteImageUniforms); the suffix is the image's own (already access-tagged) name.
|
||||
constexpr const char* IMAGE_WRITE_ALIAS_PREFIX = "mg_imageWrite_";
|
||||
// The three names SplitReadWriteImageUniforms renames a rewritten image declaration
|
||||
// under, one per REPAIR it can apply. Which one a stage picks is decided by that stage's
|
||||
// own accesses, so two stages that use an image the same way arrive at the SAME name and
|
||||
// two that use it differently arrive at different ones - which is exactly the property
|
||||
// the rename exists for, at no cost to the stages that agree. Exposed for the tests.
|
||||
constexpr const char* IMAGE_READONLY_ALIAS_PREFIX = "mg_imageRo_";
|
||||
constexpr const char* IMAGE_WRITEONLY_ALIAS_PREFIX = "mg_imageWo_";
|
||||
constexpr const char* IMAGE_SPLIT_READ_ALIAS_PREFIX = "mg_imageRw_";
|
||||
// ESSL refuses an image variable that carries a format qualifier other than r32f /
|
||||
// r32i / r32ui unless it also carries `readonly` or `writeonly` (GLSL ES 3.10 4.9 /
|
||||
// 3.20 4.10; glslang enforces it verbatim in ParseHelper.cpp's layoutObjectCheck).
|
||||
// SPIRV-Cross emits NEITHER for an image the shader both reads and writes: it
|
||||
// speculatively decorates every storage image NonWritable+NonReadable
|
||||
// (fixup_image_load_store_access), then OpImageRead clears NonReadable and
|
||||
// OpImageWrite clears NonWritable, and to_qualifiers_glsl only prints `readonly`
|
||||
// from NonWritable and `writeonly` from NonReadable. Desktop GLSL is happy with the
|
||||
// bare declaration, so the frontend raises no error and the illegal ESSL only shows
|
||||
// up as a device compile failure - and then as a silently no-op draw.
|
||||
//
|
||||
// Restores a legal declaration, and RENAMES it after the repair it applied while doing so:
|
||||
// * loaded only -> add `readonly`, rename under IMAGE_READONLY_ALIAS_PREFIX
|
||||
// * stored only -> add `writeonly`, rename under IMAGE_WRITEONLY_ALIAS_PREFIX
|
||||
// * both -> emit TWO declarations on the same binding and of the
|
||||
// same type, `coherent readonly
|
||||
// <IMAGE_SPLIT_READ_ALIAS_PREFIX><name>` and `coherent
|
||||
// writeonly <IMAGE_WRITE_ALIAS_PREFIX><that name>`, point
|
||||
// every imageStore at the second one, and follow each of
|
||||
// those stores with `memoryBarrierImage();`. Several image
|
||||
// variables may share an image unit as long as they have
|
||||
// the same type and format, which is exactly what the pair
|
||||
// is.
|
||||
//
|
||||
// The rename is the other half of the repair and applies to all three cases. The qualifier
|
||||
// chosen above is a decision about ONE STAGE's accesses, and GLSL requires a uniform
|
||||
// declared in two stages to be declared identically - so a shader that stores an image from
|
||||
// the vertex stage and loads it from the fragment stage came out of here `writeonly` in one
|
||||
// and `readonly` in the other. Adreno merges the two same-named declarations and silently
|
||||
// drops the vertex-stage STORES: no GL error, no link log, LINK_STATUS = 1, and the image
|
||||
// still reads back its initial contents
|
||||
// (KHR-GL4x.shader_image_load_store.advanced-memory-dependentInvocation; a raw-ES probe
|
||||
// isolated the trigger to the same-name/mismatched-qualifier pair, and only when both
|
||||
// carry `coherent`). Renaming leaves no cross-stage variable to merge.
|
||||
//
|
||||
// The name is keyed on the REPAIR, not on the stage, and that distinction is the whole
|
||||
// point: two stages that use an image the same way emit byte-identical declarations, so
|
||||
// letting them keep one shared name costs nothing and merging them is correct, while two
|
||||
// stages that use it differently land on different prefixes and cannot be merged at all.
|
||||
// A per-STAGE tag also satisfied the first requirement but violated the second: it made
|
||||
// the SAME image a distinct uniform in every stage that named it, and Adreno allocates
|
||||
// image LOCATIONS per distinct uniform. KHR-GL43.shading_language_420pack.
|
||||
// binding_images_texture_type_* declares three read+write images in each of its five
|
||||
// stages; merged that is 6 image uniforms, per-stage-tagged it is 30, and the Adreno 830
|
||||
// linker answered "Error: Image Image location or component exceeds max allowed. Error:
|
||||
// Linking failed." - which, the frontend having already published LINK_STATUS = TRUE from
|
||||
// glslang's link, surfaced only as every draw silently doing nothing and the images
|
||||
// reading back zero. Mali and Mesa link the same text, so nothing but a device gate
|
||||
// catches this.
|
||||
//
|
||||
// A declaration SPIRV-Cross already tagged `readonly` or `writeonly` needs no qualifier
|
||||
// repair, but it is NOT stage-independent: that tag is derived from the accesses of the
|
||||
// stage being emitted, so an image stored in the vertex stage and loaded in the fragment
|
||||
// stage arrives here as `coherent writeonly g_image` and `coherent readonly g_image` -
|
||||
// one name, two spellings, which is exactly the pair Adreno merges. Those declarations
|
||||
// are therefore renamed too, keyed on the qualifier they already carry (readonly ->
|
||||
// IMAGE_READONLY_ALIAS_PREFIX, writeonly -> IMAGE_WRITEONLY_ALIAS_PREFIX) and with
|
||||
// nothing but the identifier changed. Stages that agree still reach the same alias and
|
||||
// stay merged, so this costs no shader an extra image uniform.
|
||||
//
|
||||
// The declarations this pass still leaves untouched keep their names: one carrying BOTH
|
||||
// readonly and writeonly (a spelling no access analysis produces, so it came from the
|
||||
// application and is identical everywhere), and one carrying NEITHER, which is legal only
|
||||
// for the r32f/r32i/r32ui formats and is likewise spelled the same in every stage.
|
||||
//
|
||||
// The `coherent` on both halves of the pair is load-bearing, not decoration: GLSL only
|
||||
// guarantees a write through one image variable is visible to a read through a DIFFERENT
|
||||
// one when both are coherent, and the split is what makes a same-variable
|
||||
// read-after-write cross-variable. The single-declaration repairs above do not get it -
|
||||
// nothing aliases them.
|
||||
//
|
||||
// The barrier is the other half of the same problem, and coherent alone did not cover it:
|
||||
// visibility is not ORDER. Within one invocation the ES compiler sees a write to one
|
||||
// variable and a read of another it has no reason to believe alias, and is free to serve
|
||||
// the read from before the write - which is what advanced-memory-order's store/load/
|
||||
// compare loop measured on Adreno. memoryBarrierImage() orders exactly those two, is core
|
||||
// GLSL ES 3.10 in every stage, and is not an execution barrier, so it is legal in
|
||||
// non-uniform control flow. It costs something in a shader that stores to a read+write
|
||||
// image in a loop, which is why it is confined to the split pair.
|
||||
//
|
||||
// Budget note: the split DOUBLES the image-uniform count of the stage it fires in, so
|
||||
// a driver advertising a tight GL_MAX_{FRAGMENT,VERTEX,...}_IMAGE_UNIFORMS can turn a
|
||||
// shader that used to compile into a link failure. ES only guarantees 4 fragment image
|
||||
// uniforms, so a shader with more than half the limit in read+write images is the case
|
||||
// to watch.
|
||||
//
|
||||
// Runs on the transpiled ESSL, so it must see the bindings the frontend units were
|
||||
// already rewritten to and must run before those bindings are stripped - see the call
|
||||
// site in Managers.cpp. Its output is a function of the emitted text alone - it needs no
|
||||
// stage and no per-program state - so it adds nothing to BuildEsslTranslationKey either.
|
||||
//
|
||||
// `outSplitCount`, when given, receives the number of declarations that were actually
|
||||
// doubled - i.e. exactly how many image uniforms this stage gained over what the
|
||||
// application declared. Zero for every shader but a handful, and the only number the
|
||||
// budget note above can be reported with.
|
||||
String SplitReadWriteImageUniforms(const String& glslCode, Uint* outSplitCount = nullptr);
|
||||
// Prefix of the per-sampler float uniform that carries GL_TEXTURE_LOD_BIAS into
|
||||
// the shader (see EmulateTextureLodBias); the suffix is the sampler's own name.
|
||||
constexpr const char* LOD_BIAS_UNIFORM_PREFIX = "mg_lodBias_";
|
||||
@@ -541,12 +142,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
||||
// the bound texture's (or sampler object's) value into it; a shader whose samplers
|
||||
// all have a zero bias is therefore unaffected. Returns the source unchanged when
|
||||
// there is nothing to rewrite.
|
||||
//
|
||||
// avoidExplicitLodBias leaves lookups that already carry an explicit LOD untouched,
|
||||
// so their constant level stays constant; only the implicit-LOD forms take the bias.
|
||||
// Off by default and only ever set on ANGLE + llvmpipe, where injecting the uniform
|
||||
// into a constant LOD crashes the driver (MOBILEGL_ESPRYT_AVOID_EXPLICIT_LOD_BIAS).
|
||||
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
|
||||
String EmulateTextureLodBias(const String& glslCode);
|
||||
} // namespace PrgramImpl
|
||||
|
||||
namespace Utils {
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
// Included in DirectGLES.cpp after the native packed-word readback helper.
|
||||
// A storage promotion needs the driver's own texels as owned tight rows, not
|
||||
// client PACK layout or a fallback into a frontend texture's potentially stale
|
||||
// shadow.
|
||||
class ScopedWireTextureReadLevel {
|
||||
public:
|
||||
ScopedWireTextureReadLevel(GLuint texture, GLenum target, GLint level)
|
||||
: m_texture(texture), m_target(target) {
|
||||
switch (target) {
|
||||
case GL_TEXTURE_2D: m_binding = GL_TEXTURE_BINDING_2D; break;
|
||||
case GL_TEXTURE_3D: m_binding = GL_TEXTURE_BINDING_3D; break;
|
||||
case GL_TEXTURE_2D_ARRAY: m_binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
|
||||
case GL_TEXTURE_CUBE_MAP: m_binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY: m_binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
|
||||
default: return;
|
||||
}
|
||||
GLint previous = 0;
|
||||
DrainESErrors();
|
||||
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(m_target, m_texture);
|
||||
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_BASE_LEVEL, &m_baseLevel);
|
||||
g_GLESFuncs.glGetTexParameteriv(m_target, GL_TEXTURE_MAX_LEVEL, &m_maxLevel);
|
||||
m_saved = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
if (m_saved) {
|
||||
// ES framebuffer completeness can reject a defined level whose
|
||||
// base level or intervening mip chain is missing/inconsistent.
|
||||
// Read this existing native image as the sole base level. The
|
||||
// logical GL parameters and every cached copy remain unchanged.
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, level);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, level);
|
||||
m_valid = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
}
|
||||
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
|
||||
}
|
||||
|
||||
~ScopedWireTextureReadLevel() {
|
||||
if (!m_saved) return;
|
||||
// The packed-word path can use its own texture scratch binding.
|
||||
// Save the binding at restoration time too; do not assume an active
|
||||
// texture unit or make the backend's binding cache forget a bind.
|
||||
GLint previous = 0;
|
||||
g_GLESFuncs.glGetIntegerv(m_binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(m_target, m_texture);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_BASE_LEVEL, m_baseLevel);
|
||||
g_GLESFuncs.glTexParameteri(m_target, GL_TEXTURE_MAX_LEVEL, m_maxLevel);
|
||||
g_GLESFuncs.glBindTexture(m_target, static_cast<GLuint>(previous));
|
||||
}
|
||||
|
||||
Bool Valid() const { return m_valid; }
|
||||
|
||||
private:
|
||||
GLuint m_texture;
|
||||
GLenum m_target;
|
||||
GLenum m_binding = GL_NONE;
|
||||
GLint m_baseLevel = 0, m_maxLevel = 0;
|
||||
Bool m_saved = false, m_valid = false;
|
||||
};
|
||||
|
||||
static Bool ReadWireColorRowsTight(GLsizei width, GLsizei height, GLenum format, GLenum type,
|
||||
Bool forceOpaqueAlpha, const NormalizedImageCarrierRead& carrier,
|
||||
Vector<Uint8>& bytes) {
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) return false;
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (!pixelBytes || width <= 0 || height <= 0) return false;
|
||||
const GLenum componentType = QueryReadAttachmentComponentType();
|
||||
const Bool integerAttachment = componentType == GL_INT || componentType == GL_UNSIGNED_INT;
|
||||
if (carrier.Active() ? (mapping.isInteger || !integerAttachment)
|
||||
: mapping.isInteger != integerAttachment) return false;
|
||||
|
||||
GLint implFormat = 0, implType = 0;
|
||||
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_FORMAT, &implFormat);
|
||||
g_GLESFuncs.glGetIntegerv(GL_IMPLEMENTATION_COLOR_READ_TYPE, &implType);
|
||||
struct Candidate { GLenum format, type; };
|
||||
Vector<Candidate> candidates;
|
||||
if (carrier.Active()) {
|
||||
candidates.push_back({GL_RGBA_INTEGER, GL_UNSIGNED_INT});
|
||||
} else if (mapping.isInteger) {
|
||||
if (GetWideReadChannelCount(implFormat) > 0 && IsIntegerReadFormat(implFormat) &&
|
||||
(implType == GL_INT || implType == GL_UNSIGNED_INT))
|
||||
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
|
||||
candidates.push_back({GL_RGBA_INTEGER, componentType == GL_INT ? GLenum(GL_INT) : GLenum(GL_UNSIGNED_INT)});
|
||||
} else {
|
||||
if (GetWideReadChannelCount(implFormat) > 0 && !IsIntegerReadFormat(implFormat) &&
|
||||
(CanDecodeWideSourceType(implType) ||
|
||||
(implFormat == GL_RGBA && implType == GL_UNSIGNED_INT_2_10_10_10_REV)))
|
||||
candidates.push_back({static_cast<GLenum>(implFormat), static_cast<GLenum>(implType)});
|
||||
if (componentType == GL_FLOAT) candidates.push_back({GL_RGBA, GL_FLOAT});
|
||||
if (componentType == GL_SIGNED_NORMALIZED) {
|
||||
candidates.push_back({GL_RGBA, GL_SHORT});
|
||||
candidates.push_back({GL_RGBA, GL_BYTE});
|
||||
} else candidates.push_back({GL_RGBA, GL_UNSIGNED_BYTE});
|
||||
}
|
||||
ScopedPixelPackBuffer packBuffer(0);
|
||||
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||
const SizeT pixels = static_cast<SizeT>(width) * static_cast<SizeT>(height);
|
||||
Vector<Uint8> wide;
|
||||
GLenum wideType = GL_NONE;
|
||||
Int channels = 0;
|
||||
DrainESErrors();
|
||||
for (const auto candidate : candidates) {
|
||||
channels = GetWideReadChannelCount(candidate.format);
|
||||
wide.resize(pixels * static_cast<SizeT>(channels) * GetReadbackComponentSize(candidate.type));
|
||||
g_GLESFuncs.glReadPixels(0, 0, width, height, candidate.format, candidate.type, wide.data());
|
||||
if (g_GLESFuncs.glGetError() == GL_NO_ERROR) { wideType = candidate.type; break; }
|
||||
}
|
||||
if (wideType == GL_NONE) return false;
|
||||
|
||||
if (carrier.Active()) {
|
||||
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(decoded.data());
|
||||
const auto* src = reinterpret_cast<const Uint32*>(wide.data());
|
||||
for (SizeT i = 0; i < pixels; ++i) {
|
||||
for (SizeT c = 0; c < 4; ++c) {
|
||||
const Float divisor = static_cast<Float>(carrier.ChannelMax[c]);
|
||||
dst[i * 4 + c] = carrier.SignedNormalized
|
||||
? std::max(static_cast<Float>(static_cast<Int16>(static_cast<Uint16>(src[i * 4 + c]))) / divisor, -1.0f)
|
||||
: static_cast<Float>(src[i * 4 + c]) / divisor;
|
||||
}
|
||||
}
|
||||
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
|
||||
} else if (wideType == GL_UNSIGNED_INT_2_10_10_10_REV) {
|
||||
Vector<Uint8> decoded(pixels * 4 * sizeof(Float));
|
||||
auto* dst = reinterpret_cast<Float*>(decoded.data());
|
||||
for (SizeT i = 0; i < pixels; ++i) {
|
||||
Uint32 word = 0;
|
||||
Memcpy(&word, wide.data() + i * sizeof(word), sizeof(word));
|
||||
dst[i * 4] = static_cast<Float>(word & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 1] = static_cast<Float>((word >> 10) & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 2] = static_cast<Float>((word >> 20) & 1023u) / 1023.0f;
|
||||
dst[i * 4 + 3] = static_cast<Float>((word >> 30) & 3u) / 3.0f;
|
||||
}
|
||||
wide = Move(decoded); wideType = GL_FLOAT; channels = 4;
|
||||
}
|
||||
if (channels < 4) ExpandNarrowWideRead(wide, pixels, channels, wideType);
|
||||
if (forceOpaqueAlpha) ForceWideReadAlphaToOne(wide, pixels, wideType);
|
||||
Vector<Uint8> converted(pixels * pixelBytes);
|
||||
const SizeT sourceRowBytes = static_cast<SizeT>(width) * 4 * GetReadbackComponentSize(wideType);
|
||||
for (GLsizei row = 0; row < height; ++row)
|
||||
ReadbackImpl::ConvertWideReadbackRow(wide.data() + static_cast<SizeT>(row) * sourceRowBytes,
|
||||
converted.data() + static_cast<SizeT>(row) * width * pixelBytes, width, wideType, mapping, type);
|
||||
bytes = Move(converted);
|
||||
return true;
|
||||
}
|
||||
|
||||
static Bool ReadWireDepthStencilLevelTight(GLuint texture, TextureTarget target,
|
||||
TextureUploadTarget uploadTarget, GLint level,
|
||||
const IntVec3& extent, GLenum format, GLenum type,
|
||||
Vector<Uint8>& bytes) {
|
||||
const SizeT pixelBytes = MG_Util::GetInputBytesPerPixel(
|
||||
MG_Util::ConvertGLEnumToTextureInputFormat(format), MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (!pixelBytes) return false;
|
||||
const Bool needDepth = format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL;
|
||||
const Bool needStencil = format == GL_STENCIL_INDEX || format == GL_DEPTH_STENCIL;
|
||||
ScopedFramebufferBinding bindings(true, false);
|
||||
GLuint fbo = 0;
|
||||
g_GLESFuncs.glGenFramebuffers(1, &fbo);
|
||||
struct DeleteFbo { GLuint id; ~DeleteFbo() { g_GLESFuncs.glDeleteFramebuffers(1, &id); } } cleanup{fbo};
|
||||
FramebufferImpl::BindFramebufferId(GL_READ_FRAMEBUFFER, fbo);
|
||||
ScopedPixelPackBuffer packBuffer(0);
|
||||
ScopedPackState packState(PixelStoreImpl::PackState{1, 0, 0, 0});
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
const GLenum point = needDepth ? (needStencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT)
|
||||
: GL_STENCIL_ATTACHMENT;
|
||||
const SizeT count = static_cast<SizeT>(extent.x()) * extent.y();
|
||||
Vector<Uint8> result(count * extent.z() * pixelBytes);
|
||||
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
g_GLESFuncs.glFinish();
|
||||
for (GLint z = 0; z < extent.z(); ++z) {
|
||||
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
|
||||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
|
||||
g_GLESFuncs.glFramebufferTextureLayer(GL_READ_FRAMEBUFFER, point, texture, level, z);
|
||||
else
|
||||
g_GLESFuncs.glFramebufferTexture2D(GL_READ_FRAMEBUFFER, point,
|
||||
TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget), texture, level);
|
||||
g_GLESFuncs.glReadBuffer(GL_NONE);
|
||||
if (g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) return false;
|
||||
Vector<Float> depth;
|
||||
Vector<Uint8> stencil;
|
||||
if (needDepth && !ReadDepthValuesNative(0, 0, extent.x(), extent.y(), depth)) return false;
|
||||
if (needStencil && !ReadStencilBytesNative(0, 0, extent.x(), extent.y(), stencil)) return false;
|
||||
auto* dst = result.data() + count * z * pixelBytes;
|
||||
for (SizeT i = 0; i < count; ++i) {
|
||||
if (format == GL_DEPTH_STENCIL) {
|
||||
const Uint32 s = stencil[i];
|
||||
if (type == GL_UNSIGNED_INT_24_8) {
|
||||
const Uint32 packed = (NormalizedDepthToUnsigned(depth[i], 16777215.0) << 8) | s;
|
||||
Memcpy(dst + i * pixelBytes, &packed, sizeof(packed));
|
||||
} else if (type == GL_FLOAT_32_UNSIGNED_INT_24_8_REV) {
|
||||
Memcpy(dst + i * pixelBytes, &depth[i], sizeof(Float));
|
||||
Memcpy(dst + i * pixelBytes + sizeof(Float), &s, sizeof(s));
|
||||
} else return false;
|
||||
} else if (type == GL_FLOAT) {
|
||||
const Float value = needDepth ? depth[i] : static_cast<Float>(stencil[i]);
|
||||
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
|
||||
} else if (type == GL_HALF_FLOAT) {
|
||||
const Uint16 value = MG_Util::EncodeFloatToHalfBits(
|
||||
needDepth ? depth[i] : static_cast<Float>(stencil[i]));
|
||||
Memcpy(dst + i * pixelBytes, &value, sizeof(value));
|
||||
} else {
|
||||
const Bool signedType = type == GL_BYTE || type == GL_SHORT || type == GL_INT;
|
||||
const Double maximum = signedType
|
||||
? (pixelBytes == 1 ? 127.0 : pixelBytes == 2 ? 32767.0 : 2147483647.0)
|
||||
: (pixelBytes == 1 ? 255.0 : pixelBytes == 2 ? 65535.0 : 4294967295.0);
|
||||
const Uint32 value = needDepth ? NormalizedDepthToUnsigned(depth[i], maximum) : stencil[i];
|
||||
if (pixelBytes == 1) dst[i] = static_cast<Uint8>(value);
|
||||
else if (pixelBytes == 2) { const Uint16 v = static_cast<Uint16>(value); Memcpy(dst + i * 2, &v, 2); }
|
||||
else if (pixelBytes == 4) Memcpy(dst + i * 4, &value, 4);
|
||||
else return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureLevelTight(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
TextureInternalFormat logicalFormat, GLint level, const IntVec3& logicalExtent,
|
||||
Bool sourceUsesImageCarrier, GLenum format, GLenum type, Vector<Uint8>& bytes) {
|
||||
if (!texture || level < 0 || TextureImpl::IsMultisampleTextureTarget(target)) return false;
|
||||
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
ScopedWireTextureReadLevel readLevel(texture, nativeTarget, level);
|
||||
if (!readLevel.Valid()) return false;
|
||||
if (format == GL_DEPTH_COMPONENT || format == GL_DEPTH_STENCIL || format == GL_STENCIL_INDEX)
|
||||
return ReadWireDepthStencilLevelTight(texture, target, uploadTarget, level, extent, format, type, bytes);
|
||||
ReadbackChannelMapping mapping{};
|
||||
if (!GetReadbackChannelMapping(format, mapping)) return false;
|
||||
const SizeT pixelBytes = GetReadbackDstPixelSize(mapping, type);
|
||||
if (!pixelBytes || extent.x() <= 0 || extent.y() <= 0 || extent.z() <= 0) return false;
|
||||
const GLenum faceTarget = TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget);
|
||||
const auto widening = sourceUsesImageCarrier
|
||||
? TextureImpl::GetImageBindableStorageWidening(logicalFormat) : TextureImpl::ImageBindableStorageWidening{};
|
||||
const SizeT sliceBytes = static_cast<SizeT>(extent.x()) * extent.y() * pixelBytes;
|
||||
Vector<Uint8> result(sliceBytes * static_cast<SizeT>(extent.z()));
|
||||
if (g_GLESFuncs.glMemoryBarrier) g_GLESFuncs.glMemoryBarrier(GL_ALL_BARRIER_BITS);
|
||||
g_GLESFuncs.glFinish();
|
||||
|
||||
// A raw packed word may have redundant encodings or NaN payloads. Read
|
||||
// it through R32UI when the source really still uses that native layout.
|
||||
const Bool rawPacked = !widening && MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type));
|
||||
if (rawPacked && pixelBytes == sizeof(Uint32)) {
|
||||
const GLint face = uploadTarget >= TextureUploadTarget::CubeMapPositiveX &&
|
||||
uploadTarget <= TextureUploadTarget::CubeMapNegativeZ
|
||||
? static_cast<GLint>(uploadTarget) - static_cast<GLint>(TextureUploadTarget::CubeMapPositiveX) : 0;
|
||||
for (GLint layer = 0; layer < extent.z(); ++layer)
|
||||
if (!ReadPackedLevelWordsViaScratch(texture, nativeTarget, level, face + layer, extent.x(), extent.y(),
|
||||
reinterpret_cast<Uint32*>(result.data() + static_cast<SizeT>(layer) * sliceBytes))) return false;
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
NormalizedImageCarrierRead carrier{};
|
||||
if (widening && widening.CarriesNormalizedCodes()) {
|
||||
for (SizeT c = 0; c < 4; ++c) carrier.ChannelMax[c] = widening.ChannelMax[c];
|
||||
carrier.SignedNormalized = widening.SignedNormalized;
|
||||
}
|
||||
TempFBOBinder binder(true);
|
||||
auto& framebuffer = binder.Framebuffer();
|
||||
for (GLint layer = 0; layer < extent.z(); ++layer) {
|
||||
if (nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY || nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY)
|
||||
ScratchFBOImpl::EnsureColorAttachmentLayer(framebuffer, GL_READ_FRAMEBUFFER, texture, level, layer);
|
||||
else
|
||||
ScratchFBOImpl::EnsureColorAttachment2D(framebuffer, GL_READ_FRAMEBUFFER, texture, faceTarget, level);
|
||||
ScratchFBOImpl::EnsureReadBuffer(framebuffer, GL_COLOR_ATTACHMENT0);
|
||||
const GLenum status = g_GLESFuncs.glCheckFramebufferStatus(GL_READ_FRAMEBUFFER);
|
||||
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||
MGLOG_E_ONCE("Espryt wire texture readback: framebuffer status=0x%x texture=%u level=%d target=0x%x",
|
||||
status, texture, level, nativeTarget);
|
||||
return false;
|
||||
}
|
||||
Vector<Uint8> slice;
|
||||
if (!ReadWireColorRowsTight(extent.x(), extent.y(), format, type,
|
||||
TextureImpl::BackendTextureFormatAddsAlpha(logicalFormat, target), carrier, slice) ||
|
||||
slice.size() != sliceBytes) return false;
|
||||
Memcpy(result.data() + static_cast<SizeT>(layer) * sliceBytes, slice.data(), sliceBytes);
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P9 W2-a. The store's Defined-ness follows the client's respecifies; the driver's follows the
|
||||
// twin's last sync. A re-mint asks this before reading a level back, because a level defined
|
||||
// after that sync (null data, or null data plus a partial upload) is not on the driver yet and
|
||||
// its FBO attachment is incomplete - which used to end the session at
|
||||
// Fatal{ResourceUnavailable, "image-promotion-readback"} on legal GL.
|
||||
Bool NativeTextureLevelHasExtent(GLuint texture, TextureTarget target, TextureUploadTarget uploadTarget,
|
||||
GLint level, const IntVec3& logicalExtent) {
|
||||
if (!texture || level < 0 || !g_GLESFuncs.glGetTexLevelParameteriv) return true;
|
||||
const IntVec3 extent = TextureImpl::GetBackendUploadSize(target, logicalExtent);
|
||||
const GLenum nativeTarget = TextureImpl::ConvertTextureTargetToBackendGLEnum(target);
|
||||
GLenum binding = GL_NONE;
|
||||
switch (nativeTarget) {
|
||||
case GL_TEXTURE_2D: binding = GL_TEXTURE_BINDING_2D; break;
|
||||
case GL_TEXTURE_3D: binding = GL_TEXTURE_BINDING_3D; break;
|
||||
case GL_TEXTURE_2D_ARRAY: binding = GL_TEXTURE_BINDING_2D_ARRAY; break;
|
||||
case GL_TEXTURE_CUBE_MAP: binding = GL_TEXTURE_BINDING_CUBE_MAP; break;
|
||||
case GL_TEXTURE_CUBE_MAP_ARRAY: binding = GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; break;
|
||||
default: return true;
|
||||
}
|
||||
// A cube's levels are per face; every other target answers for the whole level.
|
||||
const GLenum queryTarget = nativeTarget == GL_TEXTURE_CUBE_MAP
|
||||
? TextureImpl::ConvertTextureUploadTargetToBackendGLEnum(uploadTarget) : nativeTarget;
|
||||
DrainESErrors();
|
||||
GLint previous = 0;
|
||||
g_GLESFuncs.glGetIntegerv(binding, &previous);
|
||||
g_GLESFuncs.glBindTexture(nativeTarget, texture);
|
||||
GLint width = 0, height = 0, depth = 0;
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_WIDTH, &width);
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_HEIGHT, &height);
|
||||
g_GLESFuncs.glGetTexLevelParameteriv(queryTarget, level, GL_TEXTURE_DEPTH, &depth);
|
||||
const Bool answered = g_GLESFuncs.glGetError() == GL_NO_ERROR;
|
||||
g_GLESFuncs.glBindTexture(nativeTarget, static_cast<GLuint>(previous));
|
||||
if (!answered) return true;
|
||||
const Bool layered = nativeTarget == GL_TEXTURE_3D || nativeTarget == GL_TEXTURE_2D_ARRAY ||
|
||||
nativeTarget == GL_TEXTURE_CUBE_MAP_ARRAY;
|
||||
return width == extent.x() && height == extent.y() && (!layered || depth == extent.z());
|
||||
}
|
||||
|
||||
// P8-E (notes/p8/E.md). MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT=<w>x<h>, READ BY THE SERVER:
|
||||
// a colour level of exactly that extent is treated as one the driver refused to attach - what an
|
||||
// Adreno does for RGB16 and the 16-bit SNORM formats, and what no host driver can be made to do.
|
||||
// Scoped by the extent so the tcp lane's shared supervisor can carry it for the whole lane: only
|
||||
// the scenario that builds that extent (CopyImageStoreReadbackScenario) ever meets it.
|
||||
static Bool WireTextureReadbackRefusedForTesting(const IntVec3& extent) {
|
||||
static const IntVec3 s_refused = [] {
|
||||
const char* value = std::getenv("MGITEST_ESPRYT_REFUSE_TEXTURE_READBACK_EXTENT");
|
||||
if (value == nullptr) return IntVec3{0, 0, 0};
|
||||
char* end = nullptr;
|
||||
const long width = std::strtol(value, &end, 10);
|
||||
if (end == value || *end != 'x') return IntVec3{0, 0, 0};
|
||||
const long height = std::strtol(end + 1, nullptr, 10);
|
||||
if (width <= 0 || height <= 0) return IntVec3{0, 0, 0};
|
||||
return IntVec3{static_cast<Int>(width), static_cast<Int>(height), 1};
|
||||
}();
|
||||
return s_refused.x() > 0 && extent == s_refused;
|
||||
}
|
||||
|
||||
// P8-E: THE STORE ROUTE, for a level the driver refused to read back. The server's staged store
|
||||
// is the only CPU copy of the level on this side, and it answers only while its bytes ARE the
|
||||
// level: every byte crossed (IsCovered), at the extent asked for, and the texture was never
|
||||
// written by the driver except by copies the store followed (!IsDriverWritten - see
|
||||
// StagedTextureStore's P8-E block and FollowCopyImageInStagedStore). The conversion is
|
||||
// GetTexImageViaShadowConversion's, into tight rows: a packed format read with its own type
|
||||
// takes the stored words verbatim, everything else decodes to wide RGBA and converts row by row.
|
||||
// Anything else DECLINES BY NAME: the reply is an error, and the client's
|
||||
// Fatal{ReplyError, "ReadPixels"} ends the session instead of handing back bytes the driver has
|
||||
// since overwritten.
|
||||
static Bool ReadWireLevelFromStagedStore(MG_Pipe::MGPipeHandle res, TextureInternalFormat logicalFormat,
|
||||
TextureUploadTarget uploadTarget, GLint level, const IntVec3& extent,
|
||||
GLenum format, GLenum type, Vector<Uint8>& bytes) {
|
||||
auto& store = MG_Remote::Server::ServerStagedTexture();
|
||||
const Uint64 key = MG_Remote::Server::StagedTextureStore::KeyForHandle(res);
|
||||
const auto target = static_cast<Uint16>(uploadTarget);
|
||||
const auto mip = static_cast<Uint16>(level);
|
||||
ReadbackChannelMapping mapping{};
|
||||
const SizeT pixelBytes = GetReadbackChannelMapping(format, mapping) ? GetReadbackDstPixelSize(mapping, type) : 0;
|
||||
const SizeT texels = static_cast<SizeT>(std::max(extent.x(), 0)) * static_cast<SizeT>(std::max(extent.y(), 0)) *
|
||||
static_cast<SizeT>(std::max(extent.z(), 0));
|
||||
const char* declined = nullptr;
|
||||
if (!store.CopiesIntoServerStorage()) {
|
||||
declined = "this arm keeps no staged store";
|
||||
} else if (store.IsDriverWritten(key)) {
|
||||
declined = "the driver has written the texture since its bytes crossed";
|
||||
} else if (!store.IsCovered(key, target, mip)) {
|
||||
declined = "not every byte of the level crossed";
|
||||
} else if (store.LevelExtentOrUndefined(key, target, mip) != extent) {
|
||||
declined = "the staged level is not the extent asked for";
|
||||
} else if (pixelBytes == 0 || texels == 0) {
|
||||
declined = "the format/type pair has no readback mapping";
|
||||
}
|
||||
Vector<Uint8> out;
|
||||
if (declined == nullptr) {
|
||||
const Uint8* staged = store.RequireLevelBytes(key, target, mip, "texture-readback-from-store");
|
||||
if (MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(type))) {
|
||||
if (store.LevelByteSize(key, target, mip) != texels * pixelBytes) {
|
||||
declined = "the staged words are not the client's word size";
|
||||
} else {
|
||||
out.assign(staged, staged + texels * pixelBytes);
|
||||
}
|
||||
} else {
|
||||
Vector<Uint8> wide;
|
||||
Bool isInteger = false;
|
||||
Bool isSigned = false;
|
||||
if (!MG_Util::PixelStoreProcessor::DecodeShadowDataToWideRGBA(logicalFormat, staged, texels, wide,
|
||||
isInteger, isSigned)) {
|
||||
declined = "the staged format has no wide decoding";
|
||||
} else if (mapping.isInteger != isInteger) {
|
||||
declined = "an integer read of a normalized level, or the reverse";
|
||||
} else {
|
||||
const GLenum wideType = isInteger ? (isSigned ? GL_INT : GL_UNSIGNED_INT) : GL_FLOAT;
|
||||
const SizeT rowTexels = static_cast<SizeT>(extent.x());
|
||||
const SizeT rows = texels / rowTexels;
|
||||
const SizeT wideRowBytes = rowTexels * 4 * GetReadbackComponentSize(wideType);
|
||||
out.resize(texels * pixelBytes);
|
||||
for (SizeT row = 0; row < rows; ++row) {
|
||||
ReadbackImpl::ConvertWideReadbackRow(wide.data() + row * wideRowBytes,
|
||||
out.data() + row * rowTexels * pixelBytes, rowTexels,
|
||||
wideType, mapping, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (declined != nullptr) {
|
||||
MGLOG_E_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback and the staged store "
|
||||
"cannot answer it (%s) - DECLINED (texture-readback-store-declined)",
|
||||
res.Slot, res.Gen, level, declined);
|
||||
return false;
|
||||
}
|
||||
MGLOG_W_ONCE("MGPipe: texture {%u,%u} level %d: the driver refused the readback (internal format %u); "
|
||||
"answered from the server's staged store (texture-readback-from-store)",
|
||||
res.Slot, res.Gen, level, static_cast<unsigned>(logicalFormat));
|
||||
bytes = Move(out);
|
||||
return true;
|
||||
}
|
||||
|
||||
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes) {
|
||||
const auto* record = PipeTextureRecordForHandle(info.Res);
|
||||
if (!record) return false;
|
||||
TextureTarget target = TextureTarget::Unknown;
|
||||
for (Int i = 0; i < static_cast<Int>(TextureTarget::TextureTargetCount); ++i)
|
||||
if (MG_Pipe::MGPipeResourceTargetForTextureTarget(static_cast<TextureTarget>(i)) == record->Desc.Target)
|
||||
target = static_cast<TextureTarget>(i);
|
||||
if (target == TextureTarget::Unknown) return false;
|
||||
const auto twin = TextureImpl::SyncTextureToBackendByHandle(info.Res);
|
||||
if (!twin) return false;
|
||||
const Bool wholeCube = info.Target == GL_TEXTURE_CUBE_MAP;
|
||||
const IntVec3 extent{static_cast<Int>(info.Box.W), static_cast<Int>(info.Box.H),
|
||||
wholeCube ? 1 : static_cast<Int>(info.Box.D)};
|
||||
const auto logicalFormat = static_cast<TextureInternalFormat>(record->Desc.InternalFormat);
|
||||
// The R32UI word leg never goes through a framebuffer, so the knob leaves it alone (it is
|
||||
// ReadTextureLevelTight's first choice for exactly these reads).
|
||||
const Bool packedWordRead =
|
||||
!twin->RequiresImageBindableStorage() &&
|
||||
MG_Util::PixelStoreProcessor::IsRawPackedPixelTransfer(
|
||||
logicalFormat, MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) &&
|
||||
MG_Util::GetInputBytesPerPixel(MG_Util::ConvertGLEnumToTextureInputFormat(info.Format),
|
||||
MG_Util::ConvertGLEnumToTexturePixelDataType(info.Type)) == sizeof(Uint32);
|
||||
const Bool refusedForTesting = !packedWordRead && WireTextureReadbackRefusedForTesting(extent);
|
||||
Vector<Uint8> result;
|
||||
for (Int face = 0; face < (wholeCube ? 6 : 1); ++face) {
|
||||
const auto uploadTarget = wholeCube
|
||||
? static_cast<TextureUploadTarget>(static_cast<Int>(TextureUploadTarget::CubeMapPositiveX) + face)
|
||||
: MG_Util::ConvertGLEnumToTextureUploadTarget(info.Target);
|
||||
Vector<Uint8> slice;
|
||||
if (refusedForTesting ||
|
||||
!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget, logicalFormat, info.Level,
|
||||
extent, twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) {
|
||||
if (!ReadWireLevelFromStagedStore(info.Res, logicalFormat, uploadTarget, info.Level, extent,
|
||||
info.Format, info.Type, slice)) return false;
|
||||
}
|
||||
result.insert(result.end(), slice.begin(), slice.end());
|
||||
}
|
||||
bytes = Move(result);
|
||||
return true;
|
||||
}
|
||||
#endif // MOBILEGL_BUILD_DISAGGREGATED
|
||||
@@ -9,10 +9,7 @@
|
||||
#include "BackendObject_DirectVulkan.h"
|
||||
#include "MG_Backend/BackendObject.h"
|
||||
#include "DirectVulkan.h"
|
||||
#include "SubgroupSupportPolicy.h"
|
||||
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_State/GLState/TextureState/TextureState.h"
|
||||
#include "MG_Util/Classifiers/TextureEnumClassifier.h"
|
||||
#include "MG_Util/Converters/MGToGL/TextureEnumConverter.h"
|
||||
@@ -330,51 +327,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
PopulateFormatCapabilitiesImpl(physicalDevice, getFormatProperties, capabilities, cache);
|
||||
}
|
||||
|
||||
#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
|
||||
// without eglTerminate (a crash, a kill, a dropped connection) sends neither. Under a
|
||||
// transport this object is the server's and its destruction is the session's end (ServerLoop's
|
||||
// apply thread, after the final drain, before the display lease ends), so the renderer goes
|
||||
// here: nothing of the ended session keeps presenting into, or holding a surface on, the
|
||||
// server's window once its lease is over. Monolith keeps its renderer.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && pVulkanRenderer != nullptr) {
|
||||
BumpRendererGeneration();
|
||||
pVulkanRenderer.reset();
|
||||
ClearProgramResourceCaches();
|
||||
}
|
||||
// And the swap interval it asked for (DirectVulkan.h).
|
||||
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;
|
||||
#endif
|
||||
|
||||
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;
|
||||
@@ -382,18 +339,12 @@ 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();
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
|
||||
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
|
||||
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
|
||||
pVulkanRenderer->Initialize();
|
||||
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -401,40 +352,16 @@ 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()));
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::Initialize() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkBufferManager::RegisterWireResourceOps();
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). BESIDE the resource ops and for the same
|
||||
// reason: this is the one place both Magma server roles pass through - the inproc
|
||||
// role and the spawn/TCP session child - and ServerLoop::CreateBackend calls it at
|
||||
// step 1 of InitServerRoleCommon, before the client session exists and therefore
|
||||
// before any frontend object can die unheard. A per-transport install is how one of
|
||||
// the two roles ends up without it.
|
||||
InstallStateObjectDeathOps();
|
||||
#endif
|
||||
#endif
|
||||
m_initialized = true;
|
||||
}
|
||||
|
||||
@@ -456,9 +383,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
|
||||
MutableFormatCapabilities());
|
||||
PrintFormatCapabilities(GetFormatCapabilities());
|
||||
@@ -497,22 +421,6 @@ 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);
|
||||
}
|
||||
@@ -530,33 +438,7 @@ 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);
|
||||
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);
|
||||
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
|
||||
}
|
||||
|
||||
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
|
||||
@@ -568,37 +450,6 @@ 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
|
||||
}
|
||||
|
||||
// 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);
|
||||
BackendObject::SetEGLSwapInterval(interval);
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
|
||||
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
|
||||
BackendObject::ReleaseEGLSurface(surface);
|
||||
@@ -613,131 +464,11 @@ 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) {
|
||||
// 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;
|
||||
(void)surface;
|
||||
// Outstanding fence/timer-query handles now refer to a dead renderer;
|
||||
// treat them as signaled/available with zero results from here on.
|
||||
BumpRendererGeneration();
|
||||
@@ -764,137 +495,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.RendererName = "Magma",
|
||||
.BackendName = "Direct (Vulkan)",
|
||||
.ExtraVendor = Nullopt,
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 6, 0},
|
||||
.RendererGLInfo = {.TargetGLVersion = {4, 0, 0},
|
||||
.TargetGLSLVersion = {4, 6, 0},
|
||||
// Baseline advertisement (no runtime-gated capabilities); a live
|
||||
// backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false),
|
||||
// Baseline advertisement (no shader subgroup, no timer queries); a
|
||||
// live backend reconciles its copy in UpdateAdvertisedExtensions.
|
||||
.Extensions = BuildAdvertisedExtensions(false, false, false),
|
||||
.IsCompatibilityProfile = false},
|
||||
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
|
||||
return rendererInfo;
|
||||
}
|
||||
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported) {
|
||||
Bool anisotropicFilteringSupported) {
|
||||
Vector<GLExtension> extensions = {
|
||||
// The version tokens have to reach the version the backend actually claims:
|
||||
// TargetGLVersion is {4,6,0}, and a list that stopped at OpenGL40 told an
|
||||
// application feature-detecting off these tokens the opposite of what
|
||||
// GL_MAJOR_VERSION / GL_MINOR_VERSION told it.
|
||||
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,
|
||||
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, E_GL_ARB_draw_buffers_blend,
|
||||
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,
|
||||
E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_multi_draw_indirect,
|
||||
E_GL_ARB_indirect_parameters, E_GL_EXT_framebuffer_object, E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage,
|
||||
E_GL_ARB_texture_storage, E_GL_ARB_texture_storage_multisample, E_GL_ARB_texture_multisample,
|
||||
E_GL_ARB_clear_texture, E_GL_ARB_direct_state_access, E_GL_ARB_shader_draw_parameters,
|
||||
E_GL_ARB_gpu_shader_int64, E_GL_KHR_debug, E_GL_ARB_gpu_shader5, E_GL_ARB_multi_bind,
|
||||
E_GL_ARB_shading_language_420pack, E_GL_ARB_vertex_attrib_binding, E_GL_ARB_shader_image_size,
|
||||
E_GL_ARB_explicit_attrib_location,
|
||||
// Core since GL 3.1 and implemented for every version advertised here. The string
|
||||
// matters because applications gate the ENTRY POINTS on it rather than on the
|
||||
// version: a caller that finds the extension missing never resolves
|
||||
// glGetUniformBlockIndex / glUniformBlockBinding, and one that then uses uniform
|
||||
// blocks anyway calls through a null pointer.
|
||||
E_GL_ARB_uniform_buffer_object,
|
||||
// Sampling the stencil aspect through DEPTH_STENCIL_TEXTURE_MODE. Core from 4.3,
|
||||
// so on a 4.0 context the string is the only way to reach it.
|
||||
E_GL_ARB_stencil_texturing,
|
||||
// Unconditional, unlike DirectGLES: a GL texture view is a second set of VkImageViews
|
||||
// over the same VkImage with a sub-range and possibly a reinterpreted VkFormat, which
|
||||
// is core Vulkan on every device MobileGL runs on. Format-reinterpreting views need
|
||||
// VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT on the image, which SyncTextureResource sets for
|
||||
// every immutable-storage texture (see the comment there).
|
||||
E_GL_ARB_texture_view,
|
||||
// Core since 3.2 and implemented here on both backends - glDrawElementsBaseVertex,
|
||||
// glDrawRangeElementsBaseVertex, glDrawElementsInstancedBaseVertex and
|
||||
// glMultiDrawElementsBaseVertex all reach real per-draw vertex rebasing. The string
|
||||
// was simply never emitted, which left KHR-GL4*.draw_elements_base_vertex_tests
|
||||
// NotSupported on a feature that works.
|
||||
E_GL_ARB_draw_elements_base_vertex,
|
||||
// The whole sync-object family is real and core since 3.2: glFenceSync, glIsSync,
|
||||
// glDeleteSync, glClientWaitSync, glWaitSync and glGetSynciv all live in GLImpl over a
|
||||
// backend fence (a VkFence here, an EGLSync/GLsync on DirectGLES), and glGetInteger64v
|
||||
// answers GL_MAX_SERVER_WAIT_TIMEOUT. The string matters for the same reason
|
||||
// ARB_uniform_buffer_object's does: LWJGL builds GLCapabilities from the extension
|
||||
// list, and a caller that finds GL_ARB_sync missing never resolves the entry points -
|
||||
// then calls through null if it uses fences anyway. Nothing in the CTS gates on this
|
||||
// string, so it is advertised on the strength of the implementation, not a test unlock.
|
||||
E_GL_ARB_sync,
|
||||
// Atomic counters, core since 4.2. glGetActiveAtomicCounterBufferiv and the whole
|
||||
// GL_ATOMIC_COUNTER_BUFFER_* query family are real in GLImpl, and the counter buffer
|
||||
// now reaches the shader on BOTH backends - Magma resolves the lowered
|
||||
// gl_AtomicCounterBlock_<N> from the atomic-counter binding points rather than the
|
||||
// shader-storage ones (see ResolveStorageBufferDescriptor). Withheld here until that
|
||||
// landed, because the counter silently read whatever was bound as SSBO N instead.
|
||||
E_GL_ARB_shader_atomic_counters,
|
||||
// glVertexAttribDivisor, core since 3.3 and real on both backends. Applications
|
||||
// (Better Clouds' GLCompat among them) accept the extension string as an
|
||||
// ALTERNATIVE to a 3.3 context when deciding whether instanced rendering is
|
||||
// available, so withholding it makes MobileGL look less capable than it is.
|
||||
E_GL_ARB_instanced_arrays,
|
||||
// Core GL 3.0-4.3 plumbing that has been real here for as long as the backend has
|
||||
// existed, and that was simply never named. None of these unlocks a single CTS case -
|
||||
// the conformance suite reaches all of them through the version - so they are
|
||||
// advertised for the OTHER consumer of this list: LWJGL builds GLCapabilities from the
|
||||
// string set, and an application that gates its ENTRY POINTS on the string rather than
|
||||
// on the version never resolves them and then calls through null. Each is backed by
|
||||
// the entry points named beside it. Kept identical to the DirectGLES block so the two
|
||||
// backends do not disagree about what MobileGL is.
|
||||
//
|
||||
// glBindVertexArray / glGenVertexArrays / glDeleteVertexArrays / glIsVertexArray.
|
||||
E_GL_ARB_vertex_array_object,
|
||||
// The 14 glSamplerParameter* / glGetSamplerParameter* entry points, including the
|
||||
// integer-valued Iiv/Iuiv forms.
|
||||
E_GL_ARB_sampler_objects,
|
||||
// glMapBufferRange + glFlushMappedBufferRange, which ARB_buffer_storage's persistent
|
||||
// maps are already built on top of.
|
||||
E_GL_ARB_map_buffer_range,
|
||||
// glCopyBufferSubData plus the GL_COPY_READ_BUFFER / GL_COPY_WRITE_BUFFER targets.
|
||||
E_GL_ARB_copy_buffer,
|
||||
// glCopyImageSubData, wired to a real backend hook on both backends.
|
||||
E_GL_ARB_copy_image,
|
||||
// GL_TEXTURE_SWIZZLE_{R,G,B,A,RGBA}, which map onto a VkImageView's component swizzle.
|
||||
E_GL_ARB_texture_swizzle,
|
||||
// GL_INT_2_10_10_10_REV / GL_UNSIGNED_INT_2_10_10_10_REV on glVertexAttribPointer plus
|
||||
// the eight glVertexAttribP* entry points.
|
||||
E_GL_ARB_vertex_type_2_10_10_10_rev,
|
||||
// The R/RG internal formats. Named separately from the float ones because an
|
||||
// application may check either.
|
||||
E_GL_ARB_texture_rg,
|
||||
// GL_DEPTH_COMPONENT32F and GL_DEPTH32F_STENCIL8.
|
||||
E_GL_ARB_depth_buffer_float,
|
||||
// The floating-point colour formats. Unlike the rest of this block this string DOES
|
||||
// gate CTS cases - KHR-GL4*.internalformat.texture2d.*{16f,32f} is keyed on it with no
|
||||
// core-version fallback, so eight cases per version list were NotSupported on formats
|
||||
// the backend has always had.
|
||||
E_GL_ARB_texture_float,
|
||||
// glViewportArrayv / glViewportIndexedf{,v} / glScissorArrayv / glScissorIndexed{,v} /
|
||||
// glDepthRangeArrayv / glDepthRangeIndexed / glGetFloati_v / glGetDoublei_v, over the
|
||||
// 16 viewports GL_MAX_VIEWPORTS reports.
|
||||
E_GL_ARB_viewport_array,
|
||||
// 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};
|
||||
// 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
|
||||
// be rebased to OpenGL's zero-based semantics.
|
||||
if (nonZeroIndirectBaseInstanceSupported) {
|
||||
extensions.push_back(E_GL_ARB_base_instance);
|
||||
}
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.MagmaDisableSubgroup) {
|
||||
if (shaderSubgroupSupported && !MG_Config::Features.DisableSubgroup) {
|
||||
extensions.push_back(E_GL_KHR_shader_subgroup);
|
||||
}
|
||||
// GL_KHR_parallel_shader_compile is MobileGL's own capability, not the Vulkan
|
||||
@@ -912,16 +539,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (MG_Util::Async::AsyncShaderCompileEnabled()) {
|
||||
extensions.push_back(E_GL_KHR_parallel_shader_compile);
|
||||
}
|
||||
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64), and stays opt-in even on a
|
||||
// device that HAS shaderFloat64. Every `double` in a shader compiles and runs either way
|
||||
// - narrowed to 32 bits where the device has no 64-bit floats, kept whole where it does -
|
||||
// so an application that simply uses doubles needs nothing advertised. What the extension
|
||||
// additionally promises is the whole GL_ARB_gpu_shader_fp64 SURFACE (glUniform*d
|
||||
// conformance, the fp64 built-ins, the state queries), and turning the string on is a
|
||||
// decision about all of it rather than about the shader path alone.
|
||||
if (MG_Config::Features.AdvertiseFp64) {
|
||||
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
|
||||
}
|
||||
// GL_ARB_timer_query gates MC's F3 GPU% (LWJGL checks the extension string);
|
||||
// only advertised when the device actually supports timestamp queries and the
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatch is off.
|
||||
@@ -935,18 +552,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extensions.push_back(E_GL_EXT_texture_filter_anisotropic);
|
||||
extensions.push_back(E_GL_ARB_texture_filter_anisotropic);
|
||||
}
|
||||
// A cube map array is a 6n-layer VkImage viewed as VK_IMAGE_VIEW_TYPE_CUBE_ARRAY, and that
|
||||
// view type cannot be created without the imageCubeArray device feature - so the string
|
||||
// follows the feature, not the version, exactly as the per-layer attachment bit does.
|
||||
//
|
||||
// Named for the application's benefit rather than the suite's: measured on Adreno 830,
|
||||
// KHR-GL43.texture_gather.plain-gather-*-cube-array already passed without the string, so
|
||||
// this unlocks no conformance case. It is advertised because the feature is real and
|
||||
// because an application that feature-detects cube map arrays off the string (rather than
|
||||
// off the 4.0 version) would otherwise decline a path this backend serves.
|
||||
if (cubeMapArraySupported) {
|
||||
extensions.push_back(E_GL_ARB_texture_cube_map_array);
|
||||
}
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@@ -966,7 +571,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool funcsTableInitialized = false;
|
||||
if (!funcsTableInitialized) {
|
||||
funcsTable.Present = Present;
|
||||
funcsTable.SetSwapInterval = SetSwapInterval;
|
||||
funcsTable.GL.DrawArrays = DrawArrays;
|
||||
funcsTable.GL.DrawElements = DrawElements;
|
||||
funcsTable.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
|
||||
@@ -1011,6 +615,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
funcsTable.GL.MemoryBarrierByRegion = MemoryBarrierByRegion;
|
||||
funcsTable.GL.BindImageTexture = BindImageTexture;
|
||||
funcsTable.GL.GetIntegeri_v = GetIntegeri_v;
|
||||
funcsTable.GL.GetInteger64i_v = GetInteger64i_v;
|
||||
funcsTable.GL.GetProgramiv = GetProgramiv;
|
||||
funcsTable.GL.ShaderStorageBlockBinding = ShaderStorageBlockBinding;
|
||||
funcsTable.GL.FenceSync = FenceSync;
|
||||
funcsTable.GL.ClientWaitSync = ClientWaitSync;
|
||||
@@ -1054,9 +660,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_vulkanCaps = capabilities;
|
||||
UpdateDynamicBackendParameters();
|
||||
UpdateAdvertisedExtensions();
|
||||
if (MGB_CTX_LIVE) {
|
||||
MGB_CTX->InvalidateCompileEnv();
|
||||
}
|
||||
MutableFormatCapabilities().Clear();
|
||||
}
|
||||
|
||||
@@ -1067,17 +670,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// real device timestamp support. ApplyVulkanCapabilitiesForTesting may
|
||||
// run without a renderer; no timer query is advertised then. Rebuilding
|
||||
// the whole list keeps re-runs idempotent.
|
||||
// The opt-in emulated compute path (SubgroupSupportPolicy.h) carries the
|
||||
// extension by itself on devices with no native subgroup support at all; a
|
||||
// device with native subgroups always advertises - and uses - those.
|
||||
const Bool subgroupSupportAdvertised =
|
||||
m_vulkanCaps.SupportsShaderSubgroup ||
|
||||
ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup);
|
||||
m_rendererInfo.RendererGLInfo.Extensions = BuildAdvertisedExtensions(
|
||||
subgroupSupportAdvertised, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
|
||||
m_vulkanCaps.SupportsImageCubeArray);
|
||||
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
|
||||
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported());
|
||||
}
|
||||
|
||||
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
|
||||
@@ -1125,7 +720,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static constexpr SizeT kMaxAdvertisedShaderStorageBlockSize = 512ull * 1024ull * 1024ull;
|
||||
m_dynamicParameters.UniformBufferOffsetAlignment = m_vulkanCaps.UniformBufferOffsetAlignment;
|
||||
m_dynamicParameters.ShaderStorageBufferOffsetAlignment = m_vulkanCaps.ShaderStorageBufferOffsetAlignment;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMin = m_vulkanCaps.AliasedLineWidthRangeMin;
|
||||
m_dynamicParameters.AliasedLineWidthRangeMax = m_vulkanCaps.AliasedLineWidthRangeMax;
|
||||
// Without the samplerAnisotropy feature the limit is unusable, so report 1.0 (no anisotropy)
|
||||
@@ -1174,89 +768,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// the Uint32 attribute masks the draw path passes around are both bounded by MAX_VERTEX_ATTRIBS.
|
||||
m_dynamicParameters.MaxVertexAttribs = std::min(
|
||||
m_vulkanCaps.MaxVertexAttribs, static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
|
||||
// Vulkan descriptor limits are not GL limits, and a GL application reads an advertised
|
||||
// limit as an amount it may actually USE. Adreno answers the per-stage/per-set descriptor
|
||||
// queries at descriptor-indexing scale - the same driver whose
|
||||
// GL_MAX_SHADER_STORAGE_BLOCK_SIZE is clamped from 2147483647 further down - so
|
||||
// KHR-GL44.multi_bind.dispatch_bind_buffers_base read GL_MAX_COMPUTE_UNIFORM_BLOCKS,
|
||||
// created that many buffers and spliced that many UBO declarations into a single compute
|
||||
// shader: ~14 s of allocation, then death on std::bad_alloc. Its sibling
|
||||
// dispatch_bind_buffers_range hard-codes 4 buffers and passes, which is the clean
|
||||
// discriminator. Every ceiling below is far above what any desktop driver advertises for
|
||||
// these (84-96 for the binding families) and far below a descriptor-indexing count, so it
|
||||
// can only lower a limit that was never usable in the first place. The zero floor is not
|
||||
// decoration: a driver reporting UINT32_MAX used to arrive here as -1.
|
||||
const auto clampLimit = [](const char* name, Int reported, Int ceiling) {
|
||||
const Int clamped = std::min(std::max(reported, 0), ceiling);
|
||||
if (clamped != reported) {
|
||||
MGLOG_I("DirectVulkan: clamped %s from %d to %d", name, reported, clamped);
|
||||
}
|
||||
return clamped;
|
||||
};
|
||||
// GL 4.6 required minimums, for the record: MAX_COMPUTE_UNIFORM_BLOCKS 12,
|
||||
// MAX_COMPUTE/COMBINED_SHADER_STORAGE_BLOCKS 8, MAX_SHADER_STORAGE_BUFFER_BINDINGS 8,
|
||||
// MAX_UNIFORM_BUFFER_BINDINGS 84, MAX_TEXTURE_BUFFER_SIZE 65536.
|
||||
constexpr Int kMaxAdvertisedBufferBlocks = 256;
|
||||
constexpr Int kMaxAdvertisedTextureBufferSize = 1 << 27; // texels; what desktop GL reports
|
||||
m_dynamicParameters.MaxComputeShaderStorageBlocks =
|
||||
clampLimit("GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxComputeShaderStorageBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks =
|
||||
clampLimit("GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", m_vulkanCaps.MaxCombinedShaderStorageBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeUniformBlocks =
|
||||
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_vulkanCaps.MaxComputeShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_vulkanCaps.MaxCombinedShaderStorageBlocks;
|
||||
m_dynamicParameters.MaxComputeUniformBlocks = m_vulkanCaps.MaxComputeUniformBlocks;
|
||||
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_vulkanCaps.MaxComputeWorkGroupInvocations;
|
||||
// The six per-axis compute limits, from the same VkPhysicalDeviceLimits fields
|
||||
// GLFunctionsTable::GetIntegeri_v (DirectVulkan.cpp) reads live. Carried here so that
|
||||
// MGPCaps has them once the table entry retires (plan B section 4.4.1); GL_Getter floors
|
||||
// them. Not clamped: unlike the block counts these are not amounts an application
|
||||
// allocates, and the frontend already raises them to the GL minimum.
|
||||
for (SizeT axis = 0; axis < 3; ++axis) {
|
||||
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_vulkanCaps.MaxComputeWorkGroupCount[axis];
|
||||
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_vulkanCaps.MaxComputeWorkGroupSize[axis];
|
||||
}
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings =
|
||||
clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings,
|
||||
kMaxAdvertisedBufferBlocks);
|
||||
// Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every
|
||||
// stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks
|
||||
// carries), so the stage limits differ only by whether the stage can have blocks at all.
|
||||
//
|
||||
// Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image
|
||||
// uniforms below. That gate reads as the obvious one and is wrong here in practice: a
|
||||
// Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND
|
||||
// enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly
|
||||
// through this backend - those write their result through a storage block declared in a
|
||||
// tessellation stage. Gating would report 0 and turn 433 passing cases into
|
||||
// "unsupported", removing function that demonstrably works.
|
||||
//
|
||||
// The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program
|
||||
// the driver refuses outright at link time; the honest limit converts a silent
|
||||
// wrong-render into a capability an application can route around. Here there is no such
|
||||
// failure to prevent, so the limit stays at what the device can address. If a Vulkan
|
||||
// device is ever found that genuinely rejects such a pipeline, the gate belongs at
|
||||
// pipeline creation where the rejection is observable, not on a feature bit this driver
|
||||
// reports inaccurately.
|
||||
{
|
||||
const Int maxPerStageStorageBlocks =
|
||||
std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0),
|
||||
std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
|
||||
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)));
|
||||
m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
// The one hard capability in the set: no geometry stage means no blocks in it.
|
||||
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
|
||||
m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0;
|
||||
m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks;
|
||||
}
|
||||
m_dynamicParameters.MaxTextureBufferSize = clampLimit(
|
||||
"GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize);
|
||||
m_dynamicParameters.MaxShaderStorageBufferBindings = m_vulkanCaps.MaxShaderStorageBufferBindings;
|
||||
m_dynamicParameters.MaxTextureBufferSize = m_vulkanCaps.MaxTextureBufferSize;
|
||||
m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
|
||||
m_dynamicParameters.MaxUniformBufferBindings = clampLimit(
|
||||
"GL_MAX_UNIFORM_BUFFER_BINDINGS", m_vulkanCaps.MaxUniformBufferBindings, kMaxAdvertisedBufferBlocks);
|
||||
m_dynamicParameters.MaxUniformBufferBindings = m_vulkanCaps.MaxUniformBufferBindings;
|
||||
m_dynamicParameters.MaxUniformBlockSize = m_vulkanCaps.MaxUniformBlockSize;
|
||||
m_dynamicParameters.MaxImageUnits = std::max(std::min(m_vulkanCaps.MaxImageUnits, maxSupportedTextureUnits), 0);
|
||||
m_dynamicParameters.MaxCombinedImageUniforms = std::max(m_vulkanCaps.MaxCombinedImageUniforms, 0);
|
||||
@@ -1278,35 +797,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Int maxSupportedDrawBuffers = static_cast<Int>(MG_State::GLState::FramebufferObject::MAX_DRAW_BUFFERS);
|
||||
m_dynamicParameters.MaxDrawBuffers = std::min(m_vulkanCaps.MaxDrawBuffers, maxSupportedDrawBuffers);
|
||||
m_dynamicParameters.MaxColorAttachments = std::min(m_vulkanCaps.MaxColorAttachments, maxSupportedDrawBuffers);
|
||||
// Same shape as the image-uniform limits three lines above: maxClipDistances is reported
|
||||
// by every device, but declaring ClipDistance in a module needs the shaderClipDistance
|
||||
// FEATURE, which VulkanRenderer enables exactly where the physical device has it. Without
|
||||
// it the limit describes a capacity no shader may use, so report none.
|
||||
m_dynamicParameters.MaxClipDistances =
|
||||
m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0;
|
||||
// The cull pair, gated on its own feature. shaderCullDistance is separate from
|
||||
// shaderClipDistance and VulkanRenderer enables it independently, so it gets its own
|
||||
// gate rather than riding on the clip one.
|
||||
m_dynamicParameters.MaxCullDistances =
|
||||
m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0;
|
||||
// GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device
|
||||
// with only one of the two features must not report a combined capacity that implies the
|
||||
// other, so the gate is "either feature" and the value never drops below what is enabled.
|
||||
m_dynamicParameters.MaxCombinedClipAndCullDistances =
|
||||
(m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance)
|
||||
? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances,
|
||||
m_dynamicParameters.MaxCullDistances})
|
||||
: 0;
|
||||
m_dynamicParameters.MaxClipDistances = m_vulkanCaps.MaxClipDistances;
|
||||
m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports;
|
||||
// Assigned explicitly rather than left to the struct's defaults, like every other
|
||||
// parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is
|
||||
// the truthful answer for DirectVulkan and a legal one (GL 4.6 table 23.65): which vertex
|
||||
// provokes is chosen per pipeline by VulkanRenderer::SelectProvokingVertexMode out of
|
||||
// VK_EXT_provoking_vertex, provokingVertexModePerPipeline and the topology, so there is no
|
||||
// one convention to name. Vulkan's own default is FIRST, which is the opposite of the
|
||||
// GL_LAST_VERTEX_CONVENTION this used to claim unconditionally.
|
||||
m_dynamicParameters.LayerProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
m_dynamicParameters.ViewportIndexProvokingVertex = GL_UNDEFINED_VERTEX;
|
||||
m_dynamicParameters.MaxViewportWidth = m_vulkanCaps.MaxViewportWidth;
|
||||
m_dynamicParameters.MaxViewportHeight = m_vulkanCaps.MaxViewportHeight;
|
||||
m_dynamicParameters.ViewportBoundsRangeMin = m_vulkanCaps.ViewportBoundsRangeMin;
|
||||
@@ -1351,60 +843,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
|
||||
}
|
||||
}
|
||||
// The device feature the whole fp64 story hangs off. With it, a module keeps its
|
||||
// OpCapability Float64 and real doubles reach the driver; without it the transpile
|
||||
// narrows every 64-bit float to 32 (ShaderTranspiler::DemoteFloat64Pass), because
|
||||
// VUID-VkShaderModuleCreateInfo-pCode-08740 forbids the capability outright and no
|
||||
// pipeline could be built from such a module. lavapipe reports it; Adreno and Mali both
|
||||
// report VK_FALSE, so on every real mobile device this is false and the demotion runs
|
||||
// exactly as it always has.
|
||||
m_dynamicParameters.SupportsShaderFloat64 = m_vulkanCaps.SupportsShaderFloat64;
|
||||
// shaderTessellationAndGeometryPointSize, both stage families from the one feature.
|
||||
// False arms the shared phase-B point-size demotion, whose modules then carry no
|
||||
// TessellationPointSize/GeometryPointSize capability and build without the feature.
|
||||
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends it is absent so the demotion can be
|
||||
// exercised on a healthy driver (lavapipe advertises the feature); =0 restores the
|
||||
// detected answer's declines.
|
||||
{
|
||||
Bool supportsStagePointSize = m_vulkanCaps.SupportsTessellationAndGeometryPointSize;
|
||||
switch (MG_Config::Features.PointSizeDemotion) {
|
||||
case MG_Config::QuirkOverride::ForceOn:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
|
||||
"gl_PointSize as unhosted so the demotion runs on this driver");
|
||||
supportsStagePointSize = false;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::ForceOff:
|
||||
MGLOG_I("DirectVulkan: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
|
||||
"plain declines regardless of the device feature");
|
||||
supportsStagePointSize = true;
|
||||
break;
|
||||
case MG_Config::QuirkOverride::Auto:
|
||||
break;
|
||||
}
|
||||
m_dynamicParameters.SupportsTessellationPointSize = supportsStagePointSize;
|
||||
m_dynamicParameters.SupportsGeometryPointSize = supportsStagePointSize;
|
||||
}
|
||||
// Never, on any device, and DELIBERATELY NOT COUPLED to the line above even though it
|
||||
// once tracked the same feature. It used to, because a `dvec` input needed Float64 to
|
||||
// exist in the module at all; a 64-bit vertex FETCH was already impossible
|
||||
// (VK_FORMAT_R64*_SFLOAT is optional and lavapipe reports zero bufferFeatures for all
|
||||
// four), so the attribute arrived as its 32-bit word pair and PackDoubleVertexInputsPass
|
||||
// bitcast it back.
|
||||
//
|
||||
// Re-coupling it does not work, and the reason is worth recording because it is not
|
||||
// obvious: this flag decides the VkFormat from the VAO ATTRIBUTE alone, and the attribute
|
||||
// does not know what the shader declared. glVertexAttribFormat(GL_DOUBLE) against a plain
|
||||
// `in vec4` is not only legal but the common case
|
||||
// (KHR-GL43.vertex_attrib_binding.basic-input-case4 does exactly that, and case5 adds
|
||||
// normalized=GL_TRUE), and advanced-bindingUpdate feeds a dvec3 the same way - GL defines
|
||||
// all of them as "doubles in memory, converted to float". Turning the flag on turns the
|
||||
// narrowing OFF for every one of them and the attributes come back unfetched.
|
||||
//
|
||||
// What keeps the two halves honest instead is a per-MODULE decision: a vertex module that
|
||||
// declares a 64-bit float INPUT is demoted whole, even where the backend has native fp64,
|
||||
// so `dvec` inputs are `vec` inputs on this backend exactly as they always were. See
|
||||
// ShaderCompiler::SanitizeAndOptimizeBinary.
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
|
||||
m_dynamicParameters.SupportsFloat64VertexAttributes = m_vulkanCaps.SupportsShaderFloat64;
|
||||
m_dynamicParameters.MaxShaderStorageBlockSize =
|
||||
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
|
||||
if (m_vulkanCaps.SupportsShaderSubgroup) {
|
||||
@@ -1413,18 +852,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_dynamicParameters.SubgroupSupportedFeatures =
|
||||
mapSubgroupFeatures(m_vulkanCaps.SubgroupSupportedOperations);
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = m_vulkanCaps.SubgroupQuadOperationsInAllStages;
|
||||
} else if (ShouldEmulateSubgroups(m_vulkanCaps.SupportsShaderSubgroup)) {
|
||||
// MOBILEGL_MAGMA_EMULATE_SUBGROUP on a device with no native subgroups: the
|
||||
// advertised values describe the 32-lane virtual subgroup the compute
|
||||
// lowering implements (SubgroupSupportPolicy.h / EmulateSubgroupsPass).
|
||||
// GL requires the advertisement and the execution to agree, and on this
|
||||
// path the emulation is what executes; only the compute stage is offered.
|
||||
m_dynamicParameters.SubgroupSize = kEmulatedSubgroupSize;
|
||||
m_dynamicParameters.SubgroupSupportedStages = kEmulatedSubgroupStages;
|
||||
m_dynamicParameters.SubgroupSupportedFeatures = kEmulatedSubgroupFeatures;
|
||||
m_dynamicParameters.SubgroupQuadOperationsInAllStages = false;
|
||||
MGLOG_I("DirectVulkan: emulating 32-lane compute subgroups "
|
||||
"(MOBILEGL_MAGMA_EMULATE_SUBGROUP, no native subgroup support)");
|
||||
} else {
|
||||
m_dynamicParameters.SubgroupSize = 0;
|
||||
m_dynamicParameters.SubgroupSupportedStages = 0;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include "../BackendObject.h"
|
||||
#include "MagmaSession.h"
|
||||
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -27,8 +26,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
~BackendObject_DirectVulkan() override;
|
||||
|
||||
void Initialize() override;
|
||||
void BindSessionStateToThisThread() override;
|
||||
void OnClientContextBound(Uint64 token) override;
|
||||
Bool InitWindowSurface() override;
|
||||
Bool InitCapabilities() override;
|
||||
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
|
||||
@@ -37,18 +34,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
|
||||
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
|
||||
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
|
||||
void SetEGLSwapInterval(Int interval) override;
|
||||
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
|
||||
const MG_Util::Damage::Region& region) override;
|
||||
// The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge).
|
||||
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
|
||||
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;
|
||||
@@ -60,11 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
private:
|
||||
Bool InitPbufferSurface(EGLint width, EGLint height) override;
|
||||
void OnEGLSurfaceReleased(EGLSurface surface) override;
|
||||
void OnEGLSurfaceForgotten(EGLSurface surface) override;
|
||||
// A served session keeps one renderer across all its client's surfaces and switches the
|
||||
// renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget).
|
||||
Bool UsesSurfaceTargets() const;
|
||||
static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast<Uint64>(surface); }
|
||||
void UpdateAdvertisedExtensions();
|
||||
void UpdateDynamicBackendParameters();
|
||||
|
||||
@@ -72,21 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
DynamicBackendParameters m_dynamicParameters;
|
||||
MG_External::VulkanCapabilities m_vulkanCaps;
|
||||
RendererInfo m_rendererInfo;
|
||||
// A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its
|
||||
// apply thread. Null in the monolith and every other single-session shape.
|
||||
UniquePtr<MagmaSession> m_magmaSession;
|
||||
// The binding each client context was last made current with, for OnClientContextBound.
|
||||
UnorderedMap<EGLContext, EGLCurrentState> m_contextBindings;
|
||||
// Window surfaces whose window is gone (SuspendServerWindow), with the extent their
|
||||
// placeholder pbuffer target is built at - the window's, so the default framebuffer the
|
||||
// client knows keeps its size.
|
||||
struct SuspendedWindowSurface {
|
||||
Uint32 Width = 1;
|
||||
Uint32 Height = 1;
|
||||
};
|
||||
UnorderedMap<EGLSurface, SuspendedWindowSurface> m_suspendedWindowSurfaces;
|
||||
// The placeholder pbuffer target of a suspended surface, made active. False: it could not be built.
|
||||
Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent);
|
||||
};
|
||||
|
||||
// Single-source-of-truth helpers shared with the driver POST
|
||||
@@ -95,21 +62,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// POST screen shows.
|
||||
|
||||
// Static identity of the Magma renderer (renderer/backend names, target GL/GLSL
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no runtime-gated
|
||||
// capabilities). A live backend copies this in its constructor and
|
||||
// versions, ExtraVendor) with the baseline extension advertisement (no shader
|
||||
// subgroup, no timer queries). A live backend copies this in its constructor and
|
||||
// reconciles the Extensions in UpdateAdvertisedExtensions once real capabilities
|
||||
// exist; callers that need the advertised list for a known capability set must
|
||||
// use BuildAdvertisedExtensions instead.
|
||||
const RendererInfo& GetRendererIdentity();
|
||||
|
||||
// The full OpenGL extension list Magma advertises (glGetString(GL_EXTENSIONS)) for
|
||||
// a device with the given raw capabilities. The MOBILEGL_MAGMA_DISABLE_SUBGROUP and
|
||||
// a device with the given raw capabilities. The MOBILEGL_DISABLE_SUBGROUP and
|
||||
// MOBILEGL_DISABLE_TIMERQUERY escape hatches are applied inside, so callers pass
|
||||
// the detected device support (passing an already-gated value is harmless).
|
||||
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
|
||||
Bool anisotropicFilteringSupported,
|
||||
Bool nonZeroIndirectBaseInstanceSupported,
|
||||
Bool cubeMapArraySupported);
|
||||
Bool anisotropicFilteringSupported);
|
||||
|
||||
// Format: <GPU Name>, Vulkan <Vulkan Version>, Driver <Driver Version> — the exact
|
||||
// string an initialized backend returns from GetBackendAPIVersionString (and that
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/DescriptorPoolCensus.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// A count of Magma's per-frame descriptor pools and the sets they hold (UniformManager), for
|
||||
// tests. Dependency-free on purpose: the integration scenarios include it next to the system GL
|
||||
// headers. They link the static library on desktop; the shipping shared library is built with
|
||||
// hidden visibility, so on Android nothing outside the library can call GetDescriptorPoolCensus.
|
||||
struct DescriptorPoolCensus {
|
||||
bool available = false; // false when no Magma renderer is live (e.g. the DirectGLES backend)
|
||||
uint32_t frameSlots = 0;
|
||||
uint32_t pools = 0; // descriptor pools across every frame slot
|
||||
uint32_t maxPoolsInOneSlot = 0;
|
||||
uint64_t capacitySets = 0; // sum of maxSets over those pools
|
||||
uint64_t cachedSets = 0; // sets allocated from them and held in the per-layout caches
|
||||
uint64_t slotTrims = 0; // whole-slot trims since the renderer came up
|
||||
};
|
||||
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus();
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,32 +9,10 @@
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
#include <MG_Backend/BackendObject.h>
|
||||
#include "MagmaSession.h"
|
||||
#include "Renderer/VulkanRenderer.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// The renderer of the calling thread's Magma session (MagmaSession.h): a served session's own on
|
||||
// its apply thread, the process-wide one everywhere else. Spelled like the UniquePtr it used to
|
||||
// be so the call sites read the same.
|
||||
class RendererSlot {
|
||||
public:
|
||||
UniquePtr<VulkanRenderer>& Slot() const { return m_slot.Get(); }
|
||||
VulkanRenderer* get() const { return Slot().get(); }
|
||||
VulkanRenderer* operator->() const { return get(); }
|
||||
VulkanRenderer& operator*() const { return *get(); }
|
||||
explicit operator bool() const { return get() != nullptr; }
|
||||
RendererSlot& operator=(UniquePtr<VulkanRenderer>&& renderer) {
|
||||
Slot() = std::move(renderer);
|
||||
return *this;
|
||||
}
|
||||
void reset() { Slot().reset(); }
|
||||
friend Bool operator==(const RendererSlot& slot, std::nullptr_t) { return slot.get() == nullptr; }
|
||||
friend Bool operator!=(const RendererSlot& slot, std::nullptr_t) { return slot.get() != nullptr; }
|
||||
|
||||
private:
|
||||
SessionLocal<UniquePtr<VulkanRenderer>> m_slot;
|
||||
};
|
||||
extern RendererSlot pVulkanRenderer;
|
||||
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
|
||||
|
||||
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
|
||||
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
|
||||
@@ -45,18 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 GetRendererGeneration();
|
||||
void BumpRendererGeneration();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
|
||||
// P7 wave 2 package C (CONTRACT-P7 §5.5). Magma's StateObjectDeathOps table, installed
|
||||
// from BackendObject_DirectVulkan::Initialize() so that GetStateObjectDeathOps() is
|
||||
// non-null on every Magma role that owns a backend. See the definition for what the one
|
||||
// arm does and for the inproc gap it closes. Push-only and disaggregated-only: a pull
|
||||
// build declares no notice at all (StateObjectDeathNotice.h) and has nothing to emit.
|
||||
void InstallStateObjectDeathOps();
|
||||
// True when the notice currently installed is this backend's. Read by MG_Test so the
|
||||
// install can be asserted without a death having to happen first.
|
||||
Bool StateObjectDeathOpsInstalled();
|
||||
#endif
|
||||
|
||||
// Drops every cached program-resource reflection entry (CPU-side strings/vectors
|
||||
// only, no Vulkan handles). Called at EGL teardown next to the renderer reset;
|
||||
// safe because GL calls are serialized in this codebase, and any still-live
|
||||
@@ -116,9 +82,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
GLsizei height, GLint border);
|
||||
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
|
||||
GLsizei height);
|
||||
void CopyImageSubData(const CopyImageEndpoint& src,
|
||||
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
|
||||
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
|
||||
const CopyImageEndpoint& dst,
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
|
||||
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
|
||||
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
|
||||
void GenerateMipmap(GLenum target);
|
||||
@@ -129,6 +95,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindImageTexture(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access,
|
||||
GLenum format);
|
||||
void GetIntegeri_v(GLenum target, GLuint index, GLint* data);
|
||||
void GetInteger64i_v(GLenum target, GLuint index, GLint64* data);
|
||||
void GetProgramiv(GLuint program, GLenum pname, GLint* params);
|
||||
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
|
||||
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels);
|
||||
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels);
|
||||
@@ -169,14 +137,4 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// only exist as vkCmdWriteTimestamp results); the frontend falls back.
|
||||
Int64 GetGpuTimestampNs();
|
||||
void Present();
|
||||
// eglSwapInterval. Remembered across renderers: one created for a later window surface
|
||||
// starts from the app's last request (GetRequestedSwapInterval) instead of the default.
|
||||
void SetSwapInterval(Int interval);
|
||||
Optional<Int> GetRequestedSwapInterval();
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// The swap interval an ended server session asked for is not the next one's, the same rule as
|
||||
// DirectGLES::ForgetRequestedSwapInterval: under a transport ~BackendObject_DirectVulkan forgets
|
||||
// it, so the next session starts at the default until it asks.
|
||||
void ForgetRequestedSwapInterval();
|
||||
#endif
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#include "MagmaSession.h"
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
struct SessionLocalEntry {
|
||||
Detail::SessionLocalConstruct construct;
|
||||
Detail::SessionLocalDestroy destroy;
|
||||
};
|
||||
|
||||
// Construct-on-first-use: SessionLocals are namespace-scope objects in several translation
|
||||
// units and register during static initialization, in no particular order.
|
||||
Vector<SessionLocalEntry>& Registry() {
|
||||
static auto* registry = new Vector<SessionLocalEntry>();
|
||||
return *registry;
|
||||
}
|
||||
|
||||
std::atomic<Uint64> g_nextSessionId{1};
|
||||
|
||||
thread_local MagmaSession* t_boundSession = nullptr;
|
||||
|
||||
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
|
||||
// monolith's renderer down from a static destructor after the driver is gone.
|
||||
MagmaSession& ProcessSession() {
|
||||
static auto* session = new MagmaSession();
|
||||
return *session;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Uint32 Detail::RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy) {
|
||||
auto& registry = Registry();
|
||||
registry.push_back({construct, destroy});
|
||||
return static_cast<Uint32>(registry.size() - 1);
|
||||
}
|
||||
|
||||
MagmaSession& Detail::CurrentMagmaSession() {
|
||||
MagmaSession* bound = t_boundSession;
|
||||
return bound != nullptr ? *bound : ProcessSession();
|
||||
}
|
||||
|
||||
void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
|
||||
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
|
||||
void*& slot = session.m_slots[index];
|
||||
if (slot == nullptr) slot = Registry()[index].construct();
|
||||
return slot;
|
||||
}
|
||||
|
||||
MagmaSession::MagmaSession() : m_id(g_nextSessionId.fetch_add(1, std::memory_order_relaxed)) {
|
||||
// Every slot is built up front: the process-wide session is reached from whichever thread
|
||||
// the app renders on, and a lazily built slot would be a first-touch race between two.
|
||||
const auto& registry = Registry();
|
||||
m_slots.resize(registry.size(), nullptr);
|
||||
for (SizeT i = 0; i < registry.size(); ++i) m_slots[i] = registry[i].construct();
|
||||
}
|
||||
|
||||
MagmaSession::~MagmaSession() {
|
||||
MagmaSession* const previous = t_boundSession;
|
||||
t_boundSession = this;
|
||||
const auto& registry = Registry();
|
||||
for (SizeT i = m_slots.size(); i-- > 0;) {
|
||||
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
|
||||
m_slots[i] = nullptr;
|
||||
}
|
||||
// Never back to this session: it is gone.
|
||||
t_boundSession = previous == this ? nullptr : previous;
|
||||
}
|
||||
|
||||
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(t_boundSession) {
|
||||
t_boundSession = session;
|
||||
}
|
||||
|
||||
MagmaSessionScope::~MagmaSessionScope() { t_boundSession = m_previous; }
|
||||
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session) { t_boundSession = session; }
|
||||
|
||||
MagmaSession* BoundMagmaSession() { return t_boundSession; }
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,100 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// MAGMA'S PER-SESSION STATE. Magma was written for one renderer per process: the renderer slot,
|
||||
// the active buffer manager, the dynamic-state shadow, the render-pass manager's statics and the
|
||||
// renderer generation are all file-scope. A multi-session server (several clients served from one
|
||||
// process, each on its own apply thread with its own BackendObject_DirectVulkan) needs one copy of
|
||||
// each per session, or one session's work lands in another's tables.
|
||||
//
|
||||
// SessionLocal<T> is that copy. It resolves through the CALLING THREAD's bound MagmaSession, and to
|
||||
// the process-wide default session on every thread nothing bound - which is the monolith, the client
|
||||
// role and every unit case, i.e. exactly the old behaviour, on whatever thread the app renders from.
|
||||
// A served session's backend owns its MagmaSession and binds it on its apply thread (the only thread
|
||||
// that runs that session's backend), so the two never mix.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
class MagmaSession;
|
||||
|
||||
namespace Detail {
|
||||
using SessionLocalConstruct = void* (*)();
|
||||
using SessionLocalDestroy = void (*)(void*);
|
||||
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
|
||||
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
|
||||
// The calling thread's session, or the process-wide default.
|
||||
MagmaSession& CurrentMagmaSession();
|
||||
void* SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
} // namespace Detail
|
||||
|
||||
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
|
||||
// destroyed in reverse registration order with the session still bound, so a destructor that
|
||||
// reaches another SessionLocal finds this session's copy and not the binding thread's.
|
||||
class MagmaSession {
|
||||
public:
|
||||
MagmaSession();
|
||||
~MagmaSession();
|
||||
MagmaSession(const MagmaSession&) = delete;
|
||||
MagmaSession& operator=(const MagmaSession&) = delete;
|
||||
|
||||
// A monotonic id, unique for the process lifetime. Never an address: a later session can
|
||||
// be allocated where an ended one lived.
|
||||
Uint64 Id() const { return m_id; }
|
||||
|
||||
private:
|
||||
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
|
||||
Vector<void*> m_slots;
|
||||
Uint64 m_id = 0;
|
||||
};
|
||||
|
||||
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
|
||||
// previous binding (usually none) afterwards.
|
||||
class MagmaSessionScope {
|
||||
public:
|
||||
explicit MagmaSessionScope(MagmaSession* session);
|
||||
~MagmaSessionScope();
|
||||
MagmaSessionScope(const MagmaSessionScope&) = delete;
|
||||
MagmaSessionScope& operator=(const MagmaSessionScope&) = delete;
|
||||
|
||||
private:
|
||||
MagmaSession* m_previous = nullptr;
|
||||
};
|
||||
|
||||
// Binds `session` to the calling thread until the next call (nullptr unbinds). For the apply
|
||||
// thread, which binds its backend once and runs it for the rest of its life.
|
||||
void BindMagmaSessionToThisThread(MagmaSession* session);
|
||||
// The calling thread's bound session, or nullptr (the process-wide default answers).
|
||||
MagmaSession* BoundMagmaSession();
|
||||
|
||||
template <class T>
|
||||
class SessionLocal {
|
||||
public:
|
||||
SessionLocal() : m_index(Detail::RegisterSessionLocal(&Construct, &Destroy)) {}
|
||||
SessionLocal(const SessionLocal&) = delete;
|
||||
SessionLocal& operator=(const SessionLocal&) = delete;
|
||||
|
||||
T& Get() const {
|
||||
return *static_cast<T*>(Detail::SessionLocalSlot(Detail::CurrentMagmaSession(), m_index));
|
||||
}
|
||||
T& operator*() const { return Get(); }
|
||||
T* operator->() const { return &Get(); }
|
||||
|
||||
private:
|
||||
static void* Construct() { return new T(); }
|
||||
static void Destroy(void* object) { delete static_cast<T*>(object); }
|
||||
const Uint32 m_index;
|
||||
};
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "BufferArena.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool BufferArena::Initialize(const BufferArenaDesc& desc) {
|
||||
@@ -114,11 +111,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (buffer.IsValid()) {
|
||||
// Outgrown, not dead: every BufferSlice handed out from this frame's arena so far
|
||||
// still names it, and those slices stay in service until the frame slot is rewound
|
||||
// (VkBufferResource::transientSlice, the converted-vertex-stream cache, the draw
|
||||
// memos). The release therefore has to survive every mid-frame reclaim and land on
|
||||
// the next ResetFrame of this slot - see VkBufferManager::CollectAllDeferredReleases.
|
||||
m_deferredReleases[frameIndex].push_back(std::move(buffer));
|
||||
}
|
||||
|
||||
@@ -128,14 +120,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
bufferDesc.usage = m_desc.usage;
|
||||
bufferDesc.memoryUsage = m_desc.memoryUsage;
|
||||
bufferDesc.allocationFlags = m_desc.allocationFlags;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Split staging may also be the destination of a GPU UBO range copy.
|
||||
// Persistent host writes must be coherent: Upload deliberately writes
|
||||
// the mapped pointer without a per-range VMA flush.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith && m_desc.persistentlyMapped) {
|
||||
bufferDesc.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
|
||||
}
|
||||
#endif
|
||||
if (!buffer.Create(bufferDesc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "FrameContext.h"
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkResult FrameContext::Initialize(VkDevice device, VkCommandPool commandPool, Uint32 frameCount) {
|
||||
@@ -208,7 +205,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// commands away. The device is gone on that path anyway - stay silent-safe
|
||||
// rather than trade a lost device for a barrier into a closed buffer.
|
||||
if (frame.hasCommandBufferRecorded) {
|
||||
MGLOG_E_ONCE("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
MGLOG_E("TransitionToPresent: command buffer already closed; skipping the present barrier");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -220,13 +217,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkImageMemoryBarrier presentBarrier{};
|
||||
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
|
||||
presentBarrier.srcAccessMask = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A layout transition writes the image too. Order it after the wire
|
||||
// render pass's storeOp, clears and copies instead of relying on the old
|
||||
// per-draw queue-idle path. This applies even when contents are discarded.
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith)
|
||||
presentBarrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
#endif
|
||||
presentBarrier.dstAccessMask = 0;
|
||||
presentBarrier.oldLayout = oldLayout;
|
||||
presentBarrier.newLayout = presentLayout;
|
||||
@@ -238,14 +228,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
presentBarrier.subresourceRange.levelCount = 1;
|
||||
presentBarrier.subresourceRange.baseArrayLayer = 0;
|
||||
presentBarrier.subresourceRange.layerCount = 1;
|
||||
vkCmdPipelineBarrier(commandBuffer,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? VK_PIPELINE_STAGE_ALL_COMMANDS_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#else
|
||||
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
|
||||
#endif
|
||||
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &presentBarrier);
|
||||
|
||||
if (openedRecording) {
|
||||
@@ -300,15 +283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return packet;
|
||||
}
|
||||
|
||||
VkResult FrameContext::AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain,
|
||||
Uint32& outImageIndex) {
|
||||
auto& frame = GetCurrent();
|
||||
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
|
||||
VK_NULL_HANDLE, &outImageIndex);
|
||||
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
|
||||
Uint64 timeout, VkFence acquireFence) {
|
||||
auto& frame = GetCurrent();
|
||||
@@ -316,10 +290,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (result != VK_SUCCESS) {
|
||||
return result;
|
||||
}
|
||||
// Under disaggregation VulkanRenderer waits every registered fence
|
||||
// through this slot's last submit before calling here. That aggregate
|
||||
// proof covers mid-frame pooled-fence flushes too; the slot fence alone
|
||||
// would not. Initialize's first acquire has no prior submissions.
|
||||
// The slot's fence has been waited: every command buffer this slot
|
||||
// submitted (including mid-frame flushes) has finished executing.
|
||||
FreeRetiredCommandBuffers(frame);
|
||||
|
||||
result = vkAcquireNextImageKHR(device, swapchain, timeout, frame.imageAvailableSemaphore, acquireFence,
|
||||
|
||||
@@ -101,13 +101,6 @@ 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);
|
||||
|
||||
|
||||
@@ -1,611 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
#include <Config.h>
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
|
||||
// header-only constant/POD declarations, and both are push-only, so the pull build's include
|
||||
// graph is unchanged (G1).
|
||||
#include <MG_Pipe/MGPipe.h>
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#endif
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
|
||||
// {slot, gen} mint the re-keyed sites are written against.
|
||||
//
|
||||
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
|
||||
//
|
||||
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
|
||||
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
|
||||
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
|
||||
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
|
||||
//
|
||||
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
|
||||
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
|
||||
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
|
||||
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
|
||||
//
|
||||
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
|
||||
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
|
||||
// nothing outside Track H consults the legacy-memo lever at all - see
|
||||
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
|
||||
//
|
||||
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
|
||||
// below is behind it, and the pull build's translation units are byte-identical (G1).
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
|
||||
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
|
||||
return (MG_Config::Features.PipePush & subsystemBit) != 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// D14's startup gate
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
|
||||
// that is actually running. It answers exactly one question and it answers it before the
|
||||
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
|
||||
//
|
||||
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
|
||||
//
|
||||
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
|
||||
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
|
||||
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
|
||||
// not kill an Espryt one.
|
||||
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
|
||||
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
|
||||
// a clear bit there simply means the client is not pushing render-state CSOs in this
|
||||
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
|
||||
// and 6 "Track H" and labels bit 0 nothing of the sort.
|
||||
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
|
||||
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
|
||||
// branch nobody needs on the hottest path in the backend.
|
||||
//
|
||||
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
|
||||
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
|
||||
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
|
||||
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
|
||||
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
|
||||
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
|
||||
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
|
||||
// longer does it SILENTLY: the combination is named once, at startup, right here.
|
||||
inline void MagmaPipeValidateSubsystemConfiguration() {
|
||||
if (!MG_Config::Features.PipeLegacyMemos &&
|
||||
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
|
||||
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
|
||||
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
|
||||
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
|
||||
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
|
||||
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
|
||||
"to measure it.",
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemRenderState));
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
|
||||
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
|
||||
if (MG_Config::Features.PipeLegacyMemos) return;
|
||||
#endif
|
||||
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
|
||||
#else
|
||||
const char* const why =
|
||||
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
|
||||
#endif
|
||||
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
|
||||
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
|
||||
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
|
||||
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
|
||||
why,
|
||||
static_cast<unsigned long long>(MG_Config::Features.PipePush |
|
||||
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
|
||||
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
|
||||
std::abort();
|
||||
}
|
||||
|
||||
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
|
||||
// site asks, so that they cannot disagree with each other or with the startup gate.
|
||||
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return MagmaPipeSubsystemOn(trackHBit);
|
||||
#else
|
||||
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
|
||||
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
|
||||
// running process can be on.
|
||||
(void)trackHBit;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
|
||||
// this run?" - the ONE question the control's sites ask, for the same reason
|
||||
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
|
||||
// and a control that defeats two of three guards proves nothing.
|
||||
//
|
||||
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
|
||||
// RATHER THAN "USE THE HEAP ADDRESS".
|
||||
//
|
||||
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
|
||||
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
|
||||
// the freed heap block and so reproduces the key. Measured, it does not: in
|
||||
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
|
||||
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
|
||||
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
|
||||
// allocation the replacement path makes. Four create/delete cycles in one run produced
|
||||
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
|
||||
// "hash the address" to collide with: the replacement hashes differently, indexes a
|
||||
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
|
||||
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
|
||||
//
|
||||
// So the control no longer asks the allocator for the collision; it manufactures it. On
|
||||
// both arms the object identity is replaced by a constant, which is the strongest form of
|
||||
// "the allocator handed the block back" and is deterministic. That covers strictly more
|
||||
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
|
||||
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
|
||||
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
|
||||
// Defeating only the retired lifetime-id/address guards would leave that key untested,
|
||||
// which is exactly the vacuity this control exists to catch.
|
||||
//
|
||||
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
|
||||
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
|
||||
//
|
||||
// * THIS MINT still has no death notification, and that is now a narrower statement than
|
||||
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
|
||||
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
|
||||
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
|
||||
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
|
||||
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
|
||||
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
|
||||
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
|
||||
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
|
||||
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
|
||||
// from that is unchanged;
|
||||
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
|
||||
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
|
||||
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
|
||||
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
|
||||
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
|
||||
// draw, which necessarily puts the two draws in different frames - and the only memo
|
||||
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
|
||||
// across frames by design. The two requirements are mutually exclusive, so the
|
||||
// generation is out of reach of any same-frame pixel reproducer for this memo.
|
||||
//
|
||||
// The generation is covered where it IS expressible, over this mint and the claim rule
|
||||
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
|
||||
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
|
||||
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
|
||||
// suite; it is the only place in the tree where that deletion is caught.
|
||||
//
|
||||
// Everything the control does NOT defeat is as load-bearing as what it does. It never
|
||||
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
|
||||
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
|
||||
// AbaControl arm still means "a replacement object was handed its dead predecessor's
|
||||
// resolved vertex bindings because the identity halves of the keys were defeated", not
|
||||
// "every safety net was switched off until something broke".
|
||||
//
|
||||
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
|
||||
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
|
||||
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
|
||||
//
|
||||
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
|
||||
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
|
||||
// of P4a's memo keys as well", because a control that only defeats the guards a phase
|
||||
// RETIRED says nothing about the key that phase SHIPS.
|
||||
//
|
||||
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
|
||||
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
|
||||
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
|
||||
// are all still reached from their frontend objects on this backend, and moving them onto
|
||||
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
|
||||
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
|
||||
// code rather than as a comment so that a caller cannot read the blanket answer above and
|
||||
// conclude the knob covers its kind:
|
||||
//
|
||||
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
|
||||
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
|
||||
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
|
||||
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
|
||||
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
|
||||
// not controlling anything for that kind. It does not assert a corruption that no code
|
||||
// on this tree can produce, which would be a permanently red always-on lane.
|
||||
//
|
||||
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
|
||||
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
|
||||
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
|
||||
// has for vertex input - this function's per-kind answer becomes that consumer's, the
|
||||
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
|
||||
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
|
||||
return MG_Config::Features.PipeHandleAbaControl;
|
||||
}
|
||||
|
||||
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
|
||||
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
|
||||
//
|
||||
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
|
||||
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
|
||||
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
|
||||
// "covered" direction is the expensive one - a control asserting a corruption nobody can
|
||||
// produce is a permanently red always-on lane - so an undecided kind must never read true,
|
||||
// and with no `default:` there is no arm for it to read true from.
|
||||
//
|
||||
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
|
||||
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
|
||||
// asserts say so in both directions, and the file no longer compiles if the tables and this
|
||||
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
|
||||
// and could not make anything red or green.)
|
||||
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
|
||||
switch (kind) {
|
||||
// The two MagmaPipeIdentityTables really mints.
|
||||
case MG_Pipe::MGPipeKind::VertexElementsCso:
|
||||
case MG_Pipe::MGPipeKind::Buffer:
|
||||
return true;
|
||||
// P4a's six object classes: still reached from their frontend objects on this
|
||||
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
|
||||
// the knob to defeat.
|
||||
case MG_Pipe::MGPipeKind::Texture:
|
||||
case MG_Pipe::MGPipeKind::Renderbuffer:
|
||||
case MG_Pipe::MGPipeKind::Framebuffer:
|
||||
case MG_Pipe::MGPipeKind::SamplerCso:
|
||||
case MG_Pipe::MGPipeKind::SamplerViewCso:
|
||||
case MG_Pipe::MGPipeKind::ShaderCso:
|
||||
// ...and everything else this backend does not mint a handle for.
|
||||
case MG_Pipe::MGPipeKind::None:
|
||||
case MG_Pipe::MGPipeKind::Xfb:
|
||||
case MG_Pipe::MGPipeKind::RenderStateCso:
|
||||
case MG_Pipe::MGPipeKind::Fence:
|
||||
case MG_Pipe::MGPipeKind::Query:
|
||||
case MG_Pipe::MGPipeKind::Context:
|
||||
case MG_Pipe::MGPipeKind::KindCount:
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
|
||||
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
|
||||
"defeat for it");
|
||||
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
|
||||
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
|
||||
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
|
||||
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
|
||||
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
|
||||
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
|
||||
"Wiring one of them is what flips this assert, this predicate and that arm - and "
|
||||
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
|
||||
"carries the answer into the lane");
|
||||
|
||||
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
|
||||
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
|
||||
// statements above - and it had no caller anywhere in the tree: the knob's only two
|
||||
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
|
||||
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
|
||||
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
|
||||
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
|
||||
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
|
||||
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
|
||||
//
|
||||
// The two pieces stand alone instead, and each is pinned by something that runs:
|
||||
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
|
||||
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
|
||||
// Identity is the knob, and its two consumers are what make it true or false. A call site
|
||||
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
|
||||
|
||||
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
|
||||
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
|
||||
// handle onto it), so nothing about the tables changes shape for the control's sake.
|
||||
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------------
|
||||
// The {slot, gen} mint
|
||||
// ---------------------------------------------------------------------------------
|
||||
//
|
||||
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
|
||||
// properties, and the third is the one review v2 got wrong:
|
||||
//
|
||||
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
|
||||
// never match a live object even if the allocator hands back the same heap address
|
||||
// (the ABA HandleRecycleScenario reproduces);
|
||||
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
|
||||
// no probe and no mix;
|
||||
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
|
||||
// drawn, whatever the working set size.
|
||||
//
|
||||
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
|
||||
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
|
||||
// and every consumer memo keyed on the handle died with it: measured on a verbatim
|
||||
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
|
||||
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
|
||||
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
|
||||
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
|
||||
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
|
||||
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
|
||||
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
|
||||
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
|
||||
// draw. That is a worse leak than the one it was introduced to avoid.
|
||||
//
|
||||
// So: grow on demand, and reclaim by AGE instead of by capacity.
|
||||
//
|
||||
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
|
||||
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
|
||||
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
|
||||
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
|
||||
// * OnFrameBoundary retires slots whose object has not been drawn for
|
||||
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
|
||||
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
|
||||
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
|
||||
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
|
||||
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
|
||||
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
|
||||
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
|
||||
// plus one map node per object EVER created, for the life of the process, on a
|
||||
// platform with an LMK. Age-based reclamation is the stand-in for the death
|
||||
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
|
||||
// * A retire costs at most one memo recompute if the object is drawn again - the same
|
||||
// price a cache miss costs - and it is charged only to objects that went idle for
|
||||
// ~1024 frames, never to a hot one.
|
||||
//
|
||||
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
|
||||
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
|
||||
// no capacity to size off a device measurement because there is no capacity; what the
|
||||
// device run in D.4.2 can still want is the number itself, so the high-water mark is
|
||||
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
|
||||
// draw - ROADMAP.md:7).
|
||||
//
|
||||
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
|
||||
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
|
||||
// clock, across two live contexts.
|
||||
class MagmaPipeIdentityTable {
|
||||
public:
|
||||
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
|
||||
|
||||
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
|
||||
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
|
||||
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
|
||||
// Objects currently holding a slot - the live working set this table tracks.
|
||||
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
|
||||
|
||||
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
|
||||
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
|
||||
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
|
||||
// that a zero can never be minted into a slot and then indexed with.
|
||||
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
|
||||
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
|
||||
// that has been handed back to the free list.
|
||||
if (lifetimeId == m_lastLifetimeId) {
|
||||
m_entries[m_lastIndex].LastUse = m_boundary;
|
||||
return m_lastHandle;
|
||||
}
|
||||
Uint32 index = 0;
|
||||
const auto it = m_index.find(lifetimeId);
|
||||
if (it != m_index.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = ClaimSlot();
|
||||
m_entries[index].LifetimeId = lifetimeId;
|
||||
m_index.emplace(lifetimeId, index);
|
||||
}
|
||||
Entry& entry = m_entries[index];
|
||||
entry.LastUse = m_boundary;
|
||||
m_lastLifetimeId = lifetimeId;
|
||||
m_lastIndex = index;
|
||||
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
|
||||
entry.Gen};
|
||||
return m_lastHandle;
|
||||
}
|
||||
|
||||
// Ages the table and returns idle slots to the free list. Same shape and the same
|
||||
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
|
||||
// slots' consumers use.
|
||||
void OnFrameBoundary() {
|
||||
++m_boundary;
|
||||
if ((m_boundary % kSweepInterval) != 0) return;
|
||||
SizeT retired = 0;
|
||||
for (auto it = m_index.begin(); it != m_index.end();) {
|
||||
Entry& entry = m_entries[it->second];
|
||||
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
|
||||
entry.LifetimeId = 0;
|
||||
m_freeSlots.push_back(it->second);
|
||||
it = m_index.erase(it);
|
||||
++retired;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (retired != 0) {
|
||||
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
|
||||
// so a front memo pointing at one would still hand out a handle the consumer
|
||||
// tables would accept. Drop it.
|
||||
m_lastLifetimeId = 0;
|
||||
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
|
||||
m_kindName, retired, LiveCount(), Count());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Sweep cadence and retirement age, deliberately the same numbers
|
||||
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
|
||||
// retired earlier than its cache entry would mint a new handle for an object whose
|
||||
// entry is still live and still correct, which is a pure waste.
|
||||
static constexpr Uint64 kSweepInterval = 256;
|
||||
static constexpr Uint64 kRetireAgeBoundaries = 1024;
|
||||
|
||||
struct Entry {
|
||||
Uint64 LifetimeId = 0;
|
||||
Uint64 LastUse = 0;
|
||||
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
|
||||
// its generation, which is what makes a memo survive a reconfiguration.
|
||||
Uint32 Gen = 0;
|
||||
};
|
||||
|
||||
Uint32 ClaimSlot() {
|
||||
while (!m_freeSlots.empty()) {
|
||||
const Uint32 index = m_freeSlots.back();
|
||||
m_freeSlots.pop_back();
|
||||
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
|
||||
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
|
||||
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
|
||||
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
|
||||
// permanently retired rather than wrapped, because a wrapped Gen would let a
|
||||
// stale handle match a live object. It costs one slot.
|
||||
if (m_entries[index].Gen == ~Uint32{0}) {
|
||||
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
|
||||
"retired for good; {slot, gen} stays unique",
|
||||
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
|
||||
continue;
|
||||
}
|
||||
++m_entries[index].Gen;
|
||||
return index;
|
||||
}
|
||||
const Uint32 index = static_cast<Uint32>(m_entries.size());
|
||||
m_entries.push_back(Entry{});
|
||||
m_entries[index].Gen = 1;
|
||||
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
|
||||
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
|
||||
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
|
||||
//
|
||||
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
|
||||
// one is I, not D, because D is compiled out of every build that ships and of every
|
||||
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
|
||||
// the live-object high-water mark of minecraft-1.21.4-in-world and
|
||||
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
|
||||
// The structure no longer has a capacity to size off it, so the number is evidence
|
||||
// rather than a tuning input - but D.4.2 should still read it out of the device log,
|
||||
// and it cannot read a line that was compiled away.
|
||||
const SizeT minted = m_entries.size();
|
||||
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
|
||||
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
|
||||
m_kindName, minted, LiveCount());
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
const char* m_kindName = "";
|
||||
Uint64 m_boundary = 0;
|
||||
Vector<Entry> m_entries;
|
||||
Vector<Uint32> m_freeSlots;
|
||||
UnorderedMap<Uint64, Uint32> m_index;
|
||||
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
|
||||
// object's lifetime id is never 0.
|
||||
Uint64 m_lastLifetimeId = 0;
|
||||
Uint32 m_lastIndex = 0;
|
||||
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
|
||||
};
|
||||
|
||||
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
|
||||
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
|
||||
// table and one reclamation clock, and both consumer tables are per-instance already.
|
||||
class MagmaPipeIdentityTables {
|
||||
public:
|
||||
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
|
||||
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
|
||||
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
|
||||
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
|
||||
: m_vaos.Acquire(lifetimeId);
|
||||
}
|
||||
void OnFrameBoundary() {
|
||||
m_vaos.OnFrameBoundary();
|
||||
m_buffers.OnFrameBoundary();
|
||||
}
|
||||
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
|
||||
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
|
||||
|
||||
private:
|
||||
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
|
||||
MagmaPipeIdentityTable m_buffers{"Buffer"};
|
||||
};
|
||||
|
||||
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
|
||||
//
|
||||
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
|
||||
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
|
||||
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
|
||||
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
|
||||
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
|
||||
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
|
||||
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
|
||||
"a null MGPipeHandle has no slot to index a per-slot table with");
|
||||
return MG_Pipe::MGPipeHandleIsNull(handle)
|
||||
? 0u
|
||||
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
|
||||
}
|
||||
|
||||
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
|
||||
//
|
||||
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
|
||||
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
|
||||
// references into these entries across nested calls. Chunks of kChunkEntries are appended
|
||||
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
|
||||
// reference is valid for the life of the table. That is the same guarantee the fixed table
|
||||
// it replaces gave, without the fixed capacity.
|
||||
template <typename T, Uint32 kChunkEntries = 256>
|
||||
class MagmaPipeSlotTable {
|
||||
public:
|
||||
T& operator[](Uint32 index) {
|
||||
const Uint32 chunk = index / kChunkEntries;
|
||||
while (m_chunks.size() <= chunk) {
|
||||
m_chunks.push_back(MakeUnique<Chunk>());
|
||||
}
|
||||
return m_chunks[chunk]->Entries[index % kChunkEntries];
|
||||
}
|
||||
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
|
||||
|
||||
private:
|
||||
struct Chunk {
|
||||
T Entries[kChunkEntries] = {};
|
||||
};
|
||||
Vector<UniquePtr<Chunk>> m_chunks;
|
||||
};
|
||||
|
||||
// The claim rule every per-slot memo table uses, in one place so that the rule and the
|
||||
// negative control that defeats it cannot drift apart between consumers - and so that the
|
||||
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
|
||||
// this code rather than a copy of it.
|
||||
//
|
||||
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
|
||||
// this object's. A slot the mint recycled for a different object comes back with a moved
|
||||
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
|
||||
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
|
||||
//
|
||||
// With negative control C on, every object collapses onto one entry and the entry is handed
|
||||
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
|
||||
// slot" and "the slot was reused and Gen did not move".
|
||||
//
|
||||
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
|
||||
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
|
||||
template <typename Memos, Uint32 kChunkEntries>
|
||||
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
|
||||
const MG_Pipe::MGPipeHandle& handle) {
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
return table[kMagmaPipeAbaControlSlotIndex];
|
||||
}
|
||||
Memos& memos = table[MagmaPipeSlotIndex(handle)];
|
||||
if (!(memos.Owner == handle)) {
|
||||
memos = Memos{};
|
||||
memos.Owner = handle;
|
||||
}
|
||||
return memos;
|
||||
}
|
||||
#endif // MOBILEGL_PIPE_PUSH
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -1,224 +0,0 @@
|
||||
// MobileGL - server-owned program input for Magma (P5f fm).
|
||||
#pragma once
|
||||
|
||||
#include <MG_State/GLState/ProgramState/ProgramObject.h>
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
|
||||
#include "../DirectVulkanResourceState.h"
|
||||
// P7 wave 2 package C, OQ-8: spirv_reflect.h is GONE from this header, and its absence is the
|
||||
// gate. Everything this class needed it for - the storage-block index space - now travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
|
||||
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
|
||||
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A borrowed view, valid only while its frontend object or server record lives.
|
||||
// It never constructs a ProgramObject on the server. Post-link mutable values
|
||||
// come from the record tails, not from the archive's link-time defaults.
|
||||
class MagmaProgramSource {
|
||||
public:
|
||||
using Program = MG_State::GLState::ProgramObject;
|
||||
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
|
||||
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
|
||||
: m_handle(handle), m_record(&record) {
|
||||
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
|
||||
}
|
||||
|
||||
Bool IsWire() const { return m_record != nullptr; }
|
||||
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
|
||||
const Program* Frontend() const { return m_frontend; }
|
||||
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
|
||||
Uint64 GetLifetimeId() const {
|
||||
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
|
||||
}
|
||||
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
|
||||
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
|
||||
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
|
||||
Bool GetSpirvValidationEnabled() const {
|
||||
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
|
||||
}
|
||||
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
|
||||
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
|
||||
}
|
||||
Vector<ShaderStage> GetLinkedShaderStages() const {
|
||||
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
|
||||
Vector<ShaderStage> result;
|
||||
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
|
||||
return result;
|
||||
}
|
||||
Bool HasLinkedShaderStage(ShaderStage stage) const {
|
||||
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
|
||||
const auto& stages = m_record->Archive->LinkedStages;
|
||||
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
|
||||
}
|
||||
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
|
||||
// A view has no lifetime in which a memo can safely persist. The server
|
||||
// factory's content cache still owns compiled programs; hash from current
|
||||
// archive + binding tails rather than borrowing the client's mutable memo.
|
||||
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
|
||||
}
|
||||
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
|
||||
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
|
||||
}
|
||||
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
|
||||
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
|
||||
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
|
||||
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
|
||||
|
||||
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
|
||||
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
|
||||
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
|
||||
Int GetActiveUniformBlocksCount() const {
|
||||
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
|
||||
}
|
||||
Uint GetUniformBlockIndex(const char* name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
|
||||
auto found = Link().uniformBlockIndexByName.find(name);
|
||||
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
|
||||
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
|
||||
}
|
||||
Uint GetUniformBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
|
||||
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
|
||||
}
|
||||
const String& GetUniformBlockName(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
|
||||
const auto* block = Block(index);
|
||||
static const String empty;
|
||||
return block ? block->name : empty;
|
||||
}
|
||||
Uint GetUBOSizeAt(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
|
||||
const auto* block = Block(index);
|
||||
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
|
||||
}
|
||||
Bool IsValidUniformLocation(Int location) const {
|
||||
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
|
||||
}
|
||||
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
|
||||
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
|
||||
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
|
||||
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
|
||||
}
|
||||
GLenum GetUniformType(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformType(location);
|
||||
return IsValidUniformLocation(static_cast<Int>(location))
|
||||
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
|
||||
}
|
||||
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
|
||||
const auto& units = m_record->SamplerUnits;
|
||||
const auto found = std::lower_bound(units.begin(), units.end(), location,
|
||||
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
|
||||
return found != units.end() && found->Location == location ? found->Unit : -1;
|
||||
}
|
||||
Int GetUniformLocation(const String& name) const {
|
||||
if (!IsWire()) return m_frontend->GetUniformLocation(name);
|
||||
const auto& locations = Link().uniformLocations;
|
||||
auto found = locations.find(name);
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.empty()) return -1;
|
||||
found = locations.find(name + "[0]");
|
||||
if (found != locations.end()) return static_cast<Int>(found->second);
|
||||
if (name.back() != ']' || name.size() < 4) return -1;
|
||||
const SizeT bracket = name.rfind('[');
|
||||
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
|
||||
Uint element = 0;
|
||||
for (SizeT i = bracket + 1; i < name.size() - 1; ++i) {
|
||||
if (name[i] < '0' || name[i] > '9') return -1;
|
||||
element = element * 10 + static_cast<Uint>(name[i] - '0');
|
||||
if (element > 0x0fffffffu) return -1;
|
||||
}
|
||||
found = locations.find(name.substr(0, bracket) + "[0]");
|
||||
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
|
||||
if (found == locations.end()) return -1;
|
||||
const Int base = static_cast<Int>(found->second);
|
||||
if (!IsValidUniformLocation(base)) return -1;
|
||||
const Int tIndex = Link().uniformIndexInTProgram[base];
|
||||
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
|
||||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
|
||||
const Int location = base + static_cast<Int>(element);
|
||||
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
|
||||
}
|
||||
|
||||
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
|
||||
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
|
||||
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
|
||||
}
|
||||
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
|
||||
Uint32 GetTransformFeedbackStride(Uint index) const {
|
||||
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
|
||||
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
|
||||
}
|
||||
GLenum GetTransformFeedbackBufferMode() const {
|
||||
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
|
||||
}
|
||||
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
|
||||
//
|
||||
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
|
||||
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
|
||||
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
|
||||
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
|
||||
// function of the linked program, so it is now built once at link time and travels in
|
||||
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
|
||||
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
|
||||
Uint GetShaderStorageBlockIndex(const String& name) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
// The archive carries NORMALISED names (the array subscript stripped), because
|
||||
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
|
||||
// ProgramFactory::ReflectLayout looks up. A caller that still holds a GL-style
|
||||
// "Foo[2]" gets the same second chance it always had.
|
||||
const auto find = [&blocks](const String& key) {
|
||||
return std::find_if(blocks.begin(), blocks.end(),
|
||||
[&](const MG_State::GLState::StorageBlockReflection& block) {
|
||||
return block.name == key;
|
||||
});
|
||||
};
|
||||
auto found = find(name);
|
||||
if (found == blocks.end() && !name.empty() && name.back() == ']') {
|
||||
const auto bracket = name.rfind('[');
|
||||
if (bracket != String::npos) found = find(name.substr(0, bracket));
|
||||
}
|
||||
return found == blocks.end() ? GL_INVALID_INDEX
|
||||
: static_cast<Uint>(found - blocks.begin());
|
||||
}
|
||||
Uint GetShaderStorageBlockBinding(Uint index) const {
|
||||
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
|
||||
const auto& blocks = Link().storageBlocks;
|
||||
if (index >= blocks.size()) return 0;
|
||||
const auto& block = blocks[index];
|
||||
// THE OVERRIDE STILL WINS, and it still comes off the RECORD rather than the
|
||||
// archive: glShaderStorageBlockBinding is a post-link mutable and the archive
|
||||
// carries link-time defaults only (rule on MagmaProgramSource, and the reason
|
||||
// StorageBlockReflection::binding is documented as the DECLARED binding).
|
||||
for (const auto& override : m_record->StorageOverrides) {
|
||||
if (override.Name == block.name) return override.Binding;
|
||||
}
|
||||
return block.binding;
|
||||
}
|
||||
|
||||
private:
|
||||
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
|
||||
const MG_State::GLState::BlockReflection* Block(Uint index) const {
|
||||
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
|
||||
const Int tIndex = Link().glBlockIndexToTProgram[index];
|
||||
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
|
||||
? &Link().blockReflection[tIndex] : nullptr;
|
||||
}
|
||||
const Program* m_frontend = nullptr;
|
||||
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
|
||||
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
|
||||
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
|
||||
// were `mutable` precisely because this class is a borrowed const view - and that is
|
||||
// also why they never helped: a view lives for one draw, so the latch was cold every
|
||||
// time it was read.
|
||||
};
|
||||
#else
|
||||
using MagmaProgramSource = MG_State::GLState::ProgramObject;
|
||||
#endif
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the
|
||||
// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled
|
||||
// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client
|
||||
// surface, or the placeholder a suspended server window draws into) must take the AImageReader
|
||||
// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins
|
||||
// it without a driver.
|
||||
|
||||
#pragma once
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
enum class OffscreenSurfaceRoute : Uint8 {
|
||||
Headless, // vkCreateHeadlessSurfaceEXT
|
||||
AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays
|
||||
Unavailable, // neither: the target cannot be built (the caller refuses, by name)
|
||||
};
|
||||
|
||||
// `headlessEnabled`: the instance was created with VK_EXT_headless_surface.
|
||||
// `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT.
|
||||
// `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface.
|
||||
constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint,
|
||||
Bool androidSurfaceEnabled) {
|
||||
if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless;
|
||||
if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader;
|
||||
return OffscreenSurfaceRoute::Unavailable;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -198,30 +198,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout)));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (payload.wireRenderPassCompatibilityId != 0) {
|
||||
constexpr Uint64 wireDomain = 0x5749524552504b59ull;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &wireDomain, sizeof(wireDomain)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.wireRenderPassCompatibilityId,
|
||||
sizeof(payload.wireRenderPassCompatibilityId)));
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
}
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.colorAttachmentCount, sizeof(payload.colorAttachmentCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.rasterizationSamples, sizeof(payload.rasterizationSamples)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleShadingEnable, sizeof(payload.sampleShadingEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.minSampleShading, sizeof(payload.minSampleShading)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.sampleMask, sizeof(payload.sampleMask)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology)));
|
||||
XXHASH_VERIFY(
|
||||
XXH64_update(m_hashState, &payload.primitiveRestartEnable, sizeof(payload.primitiveRestartEnable)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.patchControlPoints, sizeof(payload.patchControlPoints)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.passthroughTessControlKey,
|
||||
sizeof(payload.passthroughTessControlKey)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.viewportCount, sizeof(payload.viewportCount)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.polygonMode, sizeof(payload.polygonMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.cullMode, sizeof(payload.cullMode)));
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &payload.frontFace, sizeof(payload.frontFace)));
|
||||
@@ -268,31 +252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkPipeline pipeline = CreatePipeline(payload);
|
||||
// A failed creation must never be memoized. Caching VK_NULL_HANDLE served the null back for
|
||||
// the rest of the process, so one transient driver rejection turned every later draw with
|
||||
// the same state into a vkCmdBindPipeline(VK_NULL_HANDLE) - the SIGSEGV behind 9 of the 15
|
||||
// CTS process deaths. Retrying costs one failed vkCreateGraphicsPipelines per draw, which
|
||||
// is the correct price for a broken pipeline and is bounded by the draw itself being
|
||||
// skipped.
|
||||
if (pipeline == VK_NULL_HANDLE) {
|
||||
// Unlatched, like the CreatePipeline report it accompanies: a pipeline MobileGL
|
||||
// assembled and the driver refused is a broken invariant, not an expected failure,
|
||||
// so it stays loud for as long as it is reachable. Raised from MGLOG_I once the
|
||||
// Log.h ordering fix made MGLOG_E live in INFO builds.
|
||||
MGLOG_E("PipelineFactory::GetOrCreatePipeline: creation failed for hash=0x%llx "
|
||||
"programHash=0x%llx; not caching the failure",
|
||||
static_cast<unsigned long long>(hash),
|
||||
static_cast<unsigned long long>(payload.programHash));
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash,
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Wire pipelines outlive their creator pass and
|
||||
// may serve another compatible pass in flight.
|
||||
// Never evict them by a recycled native handle.
|
||||
payload.wireRenderPassCompatibilityId != 0 ? VK_NULL_HANDLE :
|
||||
#endif
|
||||
payload.renderPass,
|
||||
m_cache.emplace(hash, PipelineCacheEntry{pipeline, payload.programHash, payload.renderPass,
|
||||
m_frameCounter});
|
||||
return pipeline;
|
||||
}
|
||||
@@ -429,12 +389,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
tessellation.patchControlPoints = payload.patchControlPoints;
|
||||
|
||||
VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO};
|
||||
// Both counts move together: GL has one scissor rectangle per viewport, and Vulkan
|
||||
// requires viewportCount == scissorCount whenever both are dynamic
|
||||
// (VUID-VkPipelineViewportStateCreateInfo-scissorCount-04136). The caller has already
|
||||
// clamped this to the device's multiViewport capability.
|
||||
vpci.viewportCount = std::max<Uint32>(payload.viewportCount, 1u);
|
||||
vpci.scissorCount = vpci.viewportCount;
|
||||
vpci.viewportCount = 1;
|
||||
vpci.scissorCount = 1;
|
||||
|
||||
VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO};
|
||||
raster.polygonMode = payload.polygonMode;
|
||||
@@ -457,14 +413,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
|
||||
ms.rasterizationSamples = payload.rasterizationSamples;
|
||||
ms.sampleShadingEnable = payload.sampleShadingEnable ? VK_TRUE : VK_FALSE;
|
||||
// Ignored by Vulkan unless sampleShadingEnable is set, but written unconditionally so the
|
||||
// struct's bytes match the hash the payload was keyed by.
|
||||
ms.minSampleShading = payload.minSampleShading;
|
||||
// GL_SAMPLE_MASK / glSampleMaski. Left at nullptr - which Vulkan reads as all-ones - until
|
||||
// now, so glSampleMaski was a silent no-op on this backend while DirectGLES forwarded it.
|
||||
// The pointer has to outlive the vkCreateGraphicsPipelines call, which the payload does.
|
||||
ms.pSampleMask = payload.sampleMask;
|
||||
|
||||
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
|
||||
depthStencil.depthTestEnable = payload.depthTestEnable ? VK_TRUE : VK_FALSE;
|
||||
@@ -510,56 +458,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
blend.attachmentCount = payload.colorAttachmentCount;
|
||||
blend.pAttachments = colorAttachments.empty() ? nullptr : colorAttachments.data();
|
||||
|
||||
// A GL program may have a tessellation EVALUATION stage and no CONTROL stage: GL 4.6 core
|
||||
// 11.2.2 gives it a fixed-function pass-through instead. Vulkan has no such stage, and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 requires both tessellation stages or
|
||||
// neither - so the renderer synthesizes the pass-through GL describes and hands it in
|
||||
// here (see ProgramFactory::GetOrCreatePassthroughTessControlStage).
|
||||
//
|
||||
// The refusal below is what keeps the half-tessellated shape away from the driver when
|
||||
// there is no synthesized stage to add - because Mali does not reject it, it dereferences
|
||||
// null INSIDE vkCreateGraphicsPipelines and takes the process down (SIGSEGV, fault addr
|
||||
// 0x34, on Mali-G715/r54p2 and Mali-G925/r49p1 alike; Adreno and lavapipe merely render
|
||||
// wrong). Returning VK_NULL_HANDLE routes this through the same path a driver rejection
|
||||
// takes: the draw is skipped, nothing is memoised, and the process survives.
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* effectiveStages = payload.stages;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stagesWithPassthrough;
|
||||
if (payload.passthroughTessControlStage.module != VK_NULL_HANDLE) {
|
||||
stagesWithPassthrough = *payload.stages;
|
||||
stagesWithPassthrough.push_back(payload.passthroughTessControlStage);
|
||||
effectiveStages = &stagesWithPassthrough;
|
||||
}
|
||||
{
|
||||
VkShaderStageFlags stagesPresent = 0;
|
||||
for (const auto& stageInfo : *effectiveStages) {
|
||||
stagesPresent |= stageInfo.stage;
|
||||
}
|
||||
const Bool hasTessControl = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT) != 0;
|
||||
const Bool hasTessEval = (stagesPresent & VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT) != 0;
|
||||
if (hasTessControl != hasTessEval) {
|
||||
// Latched, and the latch is the point: a failed creation is deliberately never
|
||||
// memoised (see GetOrCreatePipeline), so a program in this state re-enters here
|
||||
// once per draw, every frame - and a refusal diagnostic that repeats per draw is
|
||||
// noise, not a diagnostic. One line names the program; the draws it explains are
|
||||
// all the same draw.
|
||||
static Bool s_warnedHalfTessellatedPipeline = false;
|
||||
if (!s_warnedHalfTessellatedPipeline) {
|
||||
s_warnedHalfTessellatedPipeline = true;
|
||||
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with %s tessellation stage and "
|
||||
"no %s stage (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). programHash=0x%llx "
|
||||
"patchControlPoints=%u. Its draws are skipped; logged once.",
|
||||
hasTessEval ? "an evaluation" : "a control",
|
||||
hasTessEval ? "control" : "evaluation",
|
||||
static_cast<unsigned long long>(payload.programHash),
|
||||
payload.patchControlPoints);
|
||||
}
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO};
|
||||
gpi.stageCount = static_cast<Uint32>(effectiveStages->size());
|
||||
gpi.pStages = effectiveStages->data();
|
||||
gpi.stageCount = static_cast<Uint32>(payload.stages->size());
|
||||
gpi.pStages = payload.stages->data();
|
||||
gpi.pVertexInputState = payload.vertexInputState;
|
||||
gpi.pInputAssemblyState = &ia;
|
||||
gpi.pTessellationState =
|
||||
@@ -576,13 +477,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkPipeline pipeline = VK_NULL_HANDLE;
|
||||
const VkResult result = vkCreateGraphicsPipelines(m_device, m_pipelineCache, 1, &gpi, nullptr, &pipeline);
|
||||
// Loud, at MGLOG_F, and deliberately NOT latched. vkCreateGraphicsPipelines refusing a
|
||||
// pipeline MobileGL assembled is a should-never-happen state, and the driver's own
|
||||
// answer is VK_ERROR_UNKNOWN - no information at all - so this dump is the entire
|
||||
// diagnosis. It is not an expected failure mode, so the one-shot rule that quiets W/E
|
||||
// does not apply: while this is reachable it should keep saying so on every draw.
|
||||
// GetOrCreatePipeline deliberately does not cache the failure, which is what makes that
|
||||
// repetition happen; if the repetition ever needs to stop, fix the pipeline, not the log.
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline failed: result=%s (%d) programHash=0x%llx vertexInputHash=0x%llx stageCount=%u topology=%s(%d) colorAttachmentCount=%u samples=%s(%d) subpass=%u",
|
||||
VkResultToString(result),
|
||||
@@ -613,36 +507,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
MGLOG_F("PipelineFactory::CreatePipeline vertex input: bindingCount=%u attributeCount=%u",
|
||||
payload.vertexInputState->vertexBindingDescriptionCount,
|
||||
payload.vertexInputState->vertexAttributeDescriptionCount);
|
||||
// The driver's own answer is VK_ERROR_UNKNOWN, i.e. no information at all, so the only
|
||||
// way to work out WHICH shader it choked on (the open sampler-array-in-struct
|
||||
// investigation) is to name the modules. MGLOG_I, not _D: this is part of a
|
||||
// should-never-happen report and must survive in the INFO-level builds that CTS
|
||||
// actually runs against, alongside the MGLOG_F lines above.
|
||||
if (payload.stageSpirvDigests) {
|
||||
for (SizeT i = 0; i < payload.stageSpirvDigests->size(); ++i) {
|
||||
const auto& digest = (*payload.stageSpirvDigests)[i];
|
||||
MGLOG_I("PipelineFactory::CreatePipeline spirv[%zu]: stage=0x%x words=%u bytes=%zu "
|
||||
"hash=0x%llx",
|
||||
i, digest.stage, digest.wordCount,
|
||||
static_cast<SizeT>(digest.wordCount) * sizeof(Uint32),
|
||||
static_cast<unsigned long long>(digest.hash));
|
||||
}
|
||||
} else {
|
||||
MGLOG_I("PipelineFactory::CreatePipeline: no SPIR-V digests attached to the payload");
|
||||
}
|
||||
if (payload.stages) {
|
||||
for (SizeT i = 0; i < payload.stages->size(); ++i) {
|
||||
const auto& stage = (*payload.stages)[i];
|
||||
// VkShaderModule is a non-dispatchable handle: a pointer on 64-bit but a
|
||||
// plain uint64_t on 32-bit ABIs, where a cast to const void* is ill-formed
|
||||
// (broke the armeabi-v7a build). Print it as the 64-bit value it is.
|
||||
MGLOG_I("PipelineFactory::CreatePipeline stage[%zu]: stage=0x%x module=0x%llx entry=%s "
|
||||
"specialization=%d",
|
||||
i, static_cast<Uint32>(stage.stage),
|
||||
static_cast<unsigned long long>(reinterpret_cast<Uint64>(stage.module)),
|
||||
stage.pName ? stage.pName : "(null)", stage.pSpecializationInfo ? 1 : 0);
|
||||
}
|
||||
}
|
||||
for (Uint32 i = 0; i < payload.colorAttachmentCount; ++i) {
|
||||
const auto& attachment = payload.colorBlendAttachments[i];
|
||||
MGLOG_F("PipelineFactory::CreatePipeline colorAttachment[%u]: blend=%d colorWriteMask=0x%x srcColor=%d dstColor=%d colorOp=%d srcAlpha=%d dstAlpha=%d alphaOp=%d",
|
||||
|
||||
@@ -14,16 +14,6 @@
|
||||
#include <Includes.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Enough of a fingerprint to identify the exact module the driver rejected without keeping the
|
||||
// SPIR-V alive for every program in the cache: a driver that answers VK_ERROR_UNKNOWN tells us
|
||||
// nothing, so the log has to carry the shader's identity itself. Diagnostic only - never part
|
||||
// of any pipeline or program hash.
|
||||
struct ShaderStageSpirvDigest {
|
||||
Uint32 stage = 0; // VkShaderStageFlagBits
|
||||
Uint32 wordCount = 0;
|
||||
Uint64 hash = 0;
|
||||
};
|
||||
|
||||
class PipelineFactory {
|
||||
public:
|
||||
using HashType = Uint64;
|
||||
@@ -35,54 +25,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
HashType vertexInputHash = 0;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Nonzero is an exact, renderer-lifetime compatibility identity.
|
||||
// renderPass remains the valid creation handle, not the cache key.
|
||||
Uint64 wireRenderPassCompatibilityId = 0;
|
||||
#endif
|
||||
Uint32 colorAttachmentCount = 1;
|
||||
VkSampleCountFlagBits rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
|
||||
// glEnable(GL_SAMPLE_SHADING) + glMinSampleShading, which Vulkan bakes into the
|
||||
// pipeline rather than exposing as dynamic state - so both are part of the pipeline's
|
||||
// identity and both are hashed. The renderer leaves the enable false unless the
|
||||
// device's sampleRateShading feature was enabled
|
||||
// (VUID-VkPipelineMultisampleStateCreateInfo-sampleShadingEnable-00784).
|
||||
Bool sampleShadingEnable = false;
|
||||
Float minSampleShading = 0.0f;
|
||||
// glEnable(GL_SAMPLE_MASK) + glSampleMaski, the fixed-function coverage mask, already
|
||||
// reduced to what GL says this draw gets (VulkanRenderer::ResolveEffectiveSampleMask:
|
||||
// all-ones unless the target is genuinely multisampled). Pipeline state like the two
|
||||
// above - Vulkan has no dynamic sample mask before VK_EXT_extended_dynamic_state3 -
|
||||
// so it is hashed with them, and all-ones has to keep producing the pipeline a null
|
||||
// pSampleMask always did.
|
||||
//
|
||||
// TWO words, though GL only ever fills the first. GL_MAX_SAMPLE_MASK_WORDS is clamped
|
||||
// to 1 on both backends, so glSampleMaski writes index 0 and nothing else - but the
|
||||
// count Vulkan READS is ceil(rasterizationSamples / 32), which is 2 on a 64-sample
|
||||
// target, and GetAdvertisedMaxSamples does not cap the driver's sample count. A
|
||||
// single Uint32 here let such a pipeline read one word past the member (the next
|
||||
// struct field). The second word is all-ones: full coverage for samples 32..63, which
|
||||
// is the only honest answer when GL has no state describing them.
|
||||
Uint32 sampleMask[2] = {0xffffffffu, 0xffffffffu};
|
||||
Uint32 subpass = 0;
|
||||
VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
Bool primitiveRestartEnable = false;
|
||||
// GL_PATCH_VERTICES; only read for a PATCH_LIST topology.
|
||||
Uint32 patchControlPoints = 3;
|
||||
// ProgramFactory::ComputePassthroughTessControlKey of the synthesized pass-through
|
||||
// tessellation control stage below, or 0 when this pipeline has none. Hashed, because
|
||||
// the levels glPatchParameterfv set are compiled INTO that module and are not a
|
||||
// function of the program or of patchControlPoints - see the note on
|
||||
// passthroughTessControlStage.
|
||||
Uint64 passthroughTessControlKey = 0;
|
||||
// How many of ARB_viewport_array's viewports this pipeline rasterizes into. 1 for
|
||||
// every program that never assigns gl_ViewportIndex, which is all of them outside the
|
||||
// conformance suite - the wide shape costs a longer vkCmdSetViewport/Scissor per state
|
||||
// change and can cost hardware fast paths, so it is opt-in per program. Baked into the
|
||||
// pipeline (viewportCount is not dynamic without VK_EXT_extended_dynamic_state) and
|
||||
// therefore hashed; the DYNAMIC viewport/scissor arrays the draw pushes must have
|
||||
// exactly this many elements (VUID-vkCmdDraw-viewportCount-03417/-03418).
|
||||
Uint32 viewportCount = 1;
|
||||
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
|
||||
VkCullModeFlags cullMode = VK_CULL_MODE_BACK_BIT;
|
||||
VkFrontFace frontFace = VK_FRONT_FACE_CLOCKWISE;
|
||||
@@ -112,21 +61,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool fragmentReplacesDepth = false;
|
||||
Array<VkPipelineColorBlendAttachmentState, kMaxColorAttachments> colorBlendAttachments{};
|
||||
const Vector<VkPipelineShaderStageCreateInfo>* stages = nullptr;
|
||||
// The tessellation control stage this renderer synthesized for a program that has
|
||||
// an evaluation stage and none of its own (GL 4.6 core 11.2.2 gives such a program a
|
||||
// fixed-function pass-through; Vulkan has no such thing and
|
||||
// VUID-VkGraphicsPipelineCreateInfo-pStages-00730 forbids the half-tessellated
|
||||
// pipeline outright). Appended to `stages` at creation. A null module means the
|
||||
// renderer could not build one, and CreatePipeline refuses the pipeline - the same
|
||||
// refusal it applies when `stages` itself is half-tessellated.
|
||||
//
|
||||
// NOT hashed directly: it is a pure function of the program, of patchControlPoints and
|
||||
// of the default tessellation levels - the first two of which ComputeHash already
|
||||
// mixes in, and the third of which arrives through passthroughTessControlKey above.
|
||||
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
|
||||
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
|
||||
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
|
||||
const Vector<ShaderStageSpirvDigest>* stageSpirvDigests = nullptr;
|
||||
};
|
||||
|
||||
explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config);
|
||||
@@ -199,7 +134,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 thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline Bool s_suppressBlendedDepthWrite = false;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../VkIncludes.h"
|
||||
#include "../MagmaSession.h"
|
||||
#include "PipelineFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "MG_State/GLState/ProgramState/ProgramObject.h"
|
||||
#include "MG_State/GLState/ProgramState/ShaderObject.h"
|
||||
#include "MG_State/GLState/TextureState/TextureEnum.h"
|
||||
|
||||
@@ -34,13 +32,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CombinedImageSampler,
|
||||
UniformTexelBuffer,
|
||||
StorageBuffer,
|
||||
StorageImage,
|
||||
// GLSL `imageBuffer` - a buffer texture reached through an IMAGE unit rather than a
|
||||
// texture unit. Vulkan spells it VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, which is a
|
||||
// VkBufferView like UniformTexelBuffer and not a VkImageView like StorageImage: it is
|
||||
// the one image uniform whose descriptor is a buffer. Appended, never inserted -
|
||||
// DescriptorKeyHash mixes the enumerator's value.
|
||||
StorageTexelBuffer
|
||||
StorageImage
|
||||
};
|
||||
|
||||
enum class CompileOptionBit : Uint {
|
||||
@@ -60,73 +52,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recorded while GL transform feedback is active, so plain draws keep the
|
||||
// undecorated variant.
|
||||
XfbCapture = 1 << 6,
|
||||
// Rewrites the fragment stage's gl_FragCoord reads to GL's bottom-left window
|
||||
// origin. Vulkan's gl_FragCoord.y IS the framebuffer row being written, and the
|
||||
// default framebuffer's image is stored in display (top-left) order, so a shader
|
||||
// that reads gl_FragCoord there sees `height - y_GL`. Set together with
|
||||
// PositionYFlip (the two are the same fact about the same draws) except under a
|
||||
// quarter turn, which this renderer does not convert rectangles for either.
|
||||
FragCoordYFlip = 1 << 7,
|
||||
// Replaces the vertex stage's gl_BaseVertex reads with zero. GL defines the builtin
|
||||
// as zero for every drawing command that has no baseVertex parameter - all the
|
||||
// DrawArrays forms - while Vulkan's BaseVertex reports firstVertex there. Set only
|
||||
// for a non-indexed draw whose program actually reads the builtin, so nothing else
|
||||
// acquires a second program/pipeline variant. See ZeroBaseVertexPass.
|
||||
ZeroBaseVertex = 1 << 8,
|
||||
};
|
||||
using CompileOptionFlags = Flags<CompileOptionBit>;
|
||||
using HashType = Uint64;
|
||||
|
||||
// The gl_PerVertex members a pass-through tessellation control stage may have to carry,
|
||||
// in the order glslang declares them - which is the order a redeclaration must use.
|
||||
// Which of them exist is a function of the neighbouring stage's GLSL VERSION
|
||||
// (gl_CullDistance joins the block at #version 450), so the mask is read off that
|
||||
// stage's SPIR-V rather than assumed. See ReflectPerVertexInputMembers.
|
||||
enum class PerVertexMemberBit : Uint32 {
|
||||
Position = 1u << 0,
|
||||
PointSize = 1u << 1,
|
||||
ClipDistance = 1u << 2,
|
||||
CullDistance = 1u << 3,
|
||||
};
|
||||
// What a program parsed below #version 450 carries, and the fallback when a module's
|
||||
// block cannot be read.
|
||||
static constexpr Uint32 kDefaultPerVertexMembers =
|
||||
static_cast<Uint32>(PerVertexMemberBit::Position) | static_cast<Uint32>(PerVertexMemberBit::PointSize) |
|
||||
static_cast<Uint32>(PerVertexMemberBit::ClipDistance);
|
||||
|
||||
struct UpdateAfterBindLimits {
|
||||
Bool enabled = false;
|
||||
Uint32 maxPerStageSamplers = 0;
|
||||
Uint32 maxPerStageUniformBuffers = 0;
|
||||
Uint32 maxPerStageStorageBuffers = 0;
|
||||
Uint32 maxPerStageSampledImages = 0;
|
||||
Uint32 maxPerStageStorageImages = 0;
|
||||
Uint32 maxPerStageResources = 0;
|
||||
Uint32 maxSetSamplers = 0;
|
||||
Uint32 maxSetUniformBuffers = 0;
|
||||
Uint32 maxSetUniformBuffersDynamic = 0;
|
||||
Uint32 maxSetStorageBuffers = 0;
|
||||
Uint32 maxSetStorageBuffersDynamic = 0;
|
||||
Uint32 maxSetSampledImages = 0;
|
||||
Uint32 maxSetStorageImages = 0;
|
||||
};
|
||||
|
||||
struct VkProgramObject {
|
||||
static constexpr Uint32 kMaxVertexInputLocations = 32;
|
||||
|
||||
HashType hash = 0;
|
||||
Vector<VkPipelineShaderStageCreateInfo> stages;
|
||||
Vector<VkShaderModule> modules;
|
||||
// Parallel to stages; identifies the exact module bytes handed to the driver when a
|
||||
// pipeline creation fails. Sixteen bytes per stage instead of keeping the SPIR-V.
|
||||
Vector<ShaderStageSpirvDigest> stageSpirvDigests;
|
||||
|
||||
// Layout data (previously in separate VkProgramLayout)
|
||||
VkDescriptorSetLayout descriptorSetLayout = VK_NULL_HANDLE;
|
||||
// True only when this layout passed every descriptor-indexing feature and
|
||||
// update-after-bind limit gate at reflection time. It controls both the
|
||||
// layout/binding flags and the pool class used by UniformManager.
|
||||
Bool usesUpdateAfterBind = false;
|
||||
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
|
||||
Vector<DescriptorBindingKind> bindingKinds;
|
||||
// The bindings this program actually declares, ascending. bindingKinds is sized to the
|
||||
@@ -137,9 +75,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Uint32> activeBindings;
|
||||
Vector<Uint32> dynamicBindings;
|
||||
Vector<Int> uniformBlockIndexByBinding;
|
||||
// Descriptor count per binding (1 except for a descriptor ARRAY - a UBO or storage
|
||||
// block instance array, an image uniform array or a sampler uniform array - each of
|
||||
// which occupies one binding with descriptorCount = N).
|
||||
// Descriptor count per binding (1 except for UBO instance arrays, which occupy one
|
||||
// binding with descriptorCount = N).
|
||||
Vector<Uint16> bindingDescriptorCounts;
|
||||
// Per-element GL uniform block indices for arrayed UBO bindings (count > 1);
|
||||
// element 0 of a non-arrayed binding stays in uniformBlockIndexByBinding.
|
||||
@@ -148,11 +85,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Vector<Int> samplerUniformLocationByBinding;
|
||||
Vector<TextureTarget> samplerTextureTargetByBinding;
|
||||
Vector<SamplerNumericDomain> samplerNumericDomainByBinding;
|
||||
// Shared by StorageImage and StorageTexelBuffer bindings: a binding is one kind or
|
||||
// the other, never both, and both need exactly the same thing - the format the
|
||||
// shader declared, so the per-draw resolve can tell a typed declaration from a
|
||||
// formatless one. Kept as one pair rather than two so the move operations below
|
||||
// cannot drift out of sync with a field that only one kind populates.
|
||||
Vector<VkFormat> storageImageFormatByBinding;
|
||||
Vector<Bool> storageImageUsesBindingFormatByBinding;
|
||||
Vector<String> storageBlockNameByBinding;
|
||||
@@ -160,19 +92,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Set once during ReflectLayout so the per-draw path can skip the whole
|
||||
// storage-image preparation for the overwhelming majority of programs.
|
||||
Bool hasStorageImages = false;
|
||||
// Something about this program's descriptors could not be resolved - an opaque
|
||||
// uniform array whose elements have no addressable uniform locations (the
|
||||
// multi-dimensional case), or a binding remap that failed outright. The binding
|
||||
// STAYS DECLARED in the descriptor set layout; declining is done here, by refusing
|
||||
// every draw, and BindProgramUniformBuffers returns false so the draw setup skips
|
||||
// the draw exactly as it does for any other bind failure.
|
||||
//
|
||||
// Keeping the layout intact is the load-bearing half. Shrinking it instead - which
|
||||
// is what the first cut of this did - leaves the shader reading a descriptor the
|
||||
// layout never declared, and lavapipe segfaults on that inside PIPELINE CREATION,
|
||||
// in a JIT worker thread, before any draw runs where a refusal could help. The
|
||||
// reason was logged once at MGLOG_I when the descriptor was declined.
|
||||
Bool declinedDescriptors = false;
|
||||
Int globalUboBinding = -1;
|
||||
Uint32 activeVertexInputLocationMask = 0;
|
||||
Array<GLenum, kMaxVertexInputLocations> vertexInputTypes{};
|
||||
@@ -185,74 +104,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// gl_FragDepth); shader-computed depth is immune to the cross-pipeline
|
||||
// position-invariance quirk (see PipelineFactory::ShouldSuppressDepthWrite).
|
||||
Bool fragmentReplacesDepth = false;
|
||||
// The vertex module declares the BaseVertex builtin. Selects the ZeroBaseVertex
|
||||
// program variant for non-indexed draws, and is deliberately a property of the
|
||||
// PROGRAM rather than of the variant: the zeroed variant leaves the variable
|
||||
// declared, so both variants answer the same and the draw path can ask either.
|
||||
Bool readsBaseVertexBuiltin = false;
|
||||
// Some pre-rasterization stage assigns gl_ViewportIndex. Its pipeline declares
|
||||
// viewportCount = the renderer's rasterizable viewport count instead of 1, and its
|
||||
// draws push the whole viewport/scissor array; every other program keeps the
|
||||
// single-viewport fast path untouched. Part of the program's identity (folded into
|
||||
// the pipeline hash through programHash), so no memo can serve the wrong shape.
|
||||
Bool writesViewportIndexBuiltin = false;
|
||||
// This program has a tessellation EVALUATION stage and no tessellation CONTROL
|
||||
// stage. GL allows that (4.6 core 11.2.2: with no control shader the input patch
|
||||
// is passed through unmodified, the output patch size is PATCH_VERTICES, and the
|
||||
// levels come from the PATCH_DEFAULT_*_LEVEL state); Vulkan does not - either both
|
||||
// tessellation stages are present or neither
|
||||
// (VUID-VkGraphicsPipelineCreateInfo-pStages-00730). So the draw path has to supply
|
||||
// the pass-through stage GL describes; see GetOrCreatePassthroughTessControlStage.
|
||||
// True when this program was built AS a transform-feedback capture variant but its
|
||||
// last pre-rasterization module does NOT carry the Xfb execution mode - so the
|
||||
// renderer must decline the capture span instead of issuing
|
||||
// vkCmdBeginTransformFeedbackEXT against it
|
||||
// (VUID-vkCmdBeginTransformFeedbackEXT-None-04128).
|
||||
//
|
||||
// Two ways to get here, and neither is visible from GL state, which is all
|
||||
// BeginXfbCaptureForDraw otherwise consults: the clip/XFB validation backstop had to
|
||||
// rewind past the capture decoration, or XfbCaptureDecoratePass resolved none of the
|
||||
// requested varyings and returned without changing anything (its own MGLOG_E path)
|
||||
// while its runner still reported success. Both used to ship a non-Xfb module under
|
||||
// an Xfb-flagged cache entry - the flag and the layout are part of the program cache
|
||||
// key, so it was sticky for every later captured draw of the program, not a glitch.
|
||||
Bool xfbCaptureDeclined = false;
|
||||
// The program has a tessellation or geometry module declaring TessellationPointSize /
|
||||
// GeometryPointSize on a device whose shaderTessellationAndGeometryPointSize feature
|
||||
// is off, so a pipeline built from it is invalid usage
|
||||
// (VUID-RuntimeSpirv-PointSize-06439). Its draws are refused in SetupDraw rather than
|
||||
// handed to the driver - the same contract PipelineFactory's half-tessellated refusal
|
||||
// implements one level up, and the counterpart of the DirectGLES arm that reports a
|
||||
// driver with neither point-size extension by name.
|
||||
//
|
||||
// Sticky by construction, which is what makes ONE log line honest: the flag lives on
|
||||
// the cache entry, so every later draw of the same program variant reads the same
|
||||
// answer instead of re-deciding it.
|
||||
Bool pointSizeCapabilityUnsupported = 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
|
||||
// built-ins. A user-defined varying would arrive at the evaluation stage
|
||||
// UNWRITTEN once a control stage sits between it and the vertex stage, which is
|
||||
// silently wrong pixels rather than a crash - so those programs are declined
|
||||
// instead (PipelineFactory::CreatePipeline refuses the pipeline and the draw is
|
||||
// skipped). See ReflectPassthroughTessControlNeed.
|
||||
Bool passthroughTessControlEmulatable = false;
|
||||
// Which gl_PerVertex members the evaluation stage's `in gl_PerVertex gl_in[]` block
|
||||
// actually carries, as a PerVertexMemberBit mask read off its SPIR-V. The synthesized
|
||||
// control stage has to redeclare the SAME shape: glslang appends gl_CullDistance to
|
||||
// that block from #version 450 upward, so a 450/460 program - and every ESSL program,
|
||||
// which the source processor rewrites to "#version 460 core" - carries four members
|
||||
// where a 430 program carries three. A fixed three-member pass-through fed the
|
||||
// evaluation stage a differently-shaped block, which is the black-frame-no-error case
|
||||
// this whole family is written around.
|
||||
Uint32 passthroughPerVertexMembers = 0;
|
||||
// Frame-boundary counter value of the last GetOrCreateProgram hit; drives
|
||||
// cache eviction (see OnFrameBoundary). Mutable: the draw snapshot's memoised
|
||||
// entry pointer re-stamps use through a const reference (StampProgramUse).
|
||||
mutable Uint64 lastUsedFrame = 0;
|
||||
|
||||
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
|
||||
static inline VkDevice s_device = VK_NULL_HANDLE;
|
||||
|
||||
VkProgramObject() = default;
|
||||
VkProgramObject(const VkProgramObject&) = delete;
|
||||
@@ -261,16 +118,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
modules = std::move(other.modules);
|
||||
// Must travel with `modules`: these digests name the SPIR-V those exact
|
||||
// shader modules were built from, and the pipeline-failure diagnostics
|
||||
// print the two together. Leaving it behind used to merely lose the
|
||||
// digests on a rehash; now that the cache is a robin-hood table, insertion
|
||||
// SWAPS two entries, and a field that no move touches stays behind in the
|
||||
// slot - pairing one program's modules with another program's digests, so
|
||||
// a pipeline failure would be reported against the wrong SPIR-V.
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests);
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -288,7 +136,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -298,18 +145,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -317,11 +157,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
}
|
||||
VkProgramObject& operator=(VkProgramObject&& other) noexcept {
|
||||
@@ -332,9 +167,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
hash = other.hash;
|
||||
stages = std::move(other.stages);
|
||||
modules = std::move(other.modules);
|
||||
stageSpirvDigests = std::move(other.stageSpirvDigests); // travels with `modules` - see the move ctor
|
||||
descriptorSetLayout = other.descriptorSetLayout;
|
||||
usesUpdateAfterBind = other.usesUpdateAfterBind;
|
||||
pipelineLayout = other.pipelineLayout;
|
||||
bindingKinds = std::move(other.bindingKinds);
|
||||
activeBindings = std::move(other.activeBindings);
|
||||
@@ -352,7 +185,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
storageBlockNameByBinding = std::move(other.storageBlockNameByBinding);
|
||||
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
|
||||
hasStorageImages = other.hasStorageImages;
|
||||
declinedDescriptors = other.declinedDescriptors;
|
||||
globalUboBinding = other.globalUboBinding;
|
||||
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
|
||||
vertexInputTypes = other.vertexInputTypes;
|
||||
@@ -362,18 +194,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
producerOutputComponentCount = other.producerOutputComponentCount;
|
||||
fragmentInputComponentCount = other.fragmentInputComponentCount;
|
||||
fragmentReplacesDepth = other.fragmentReplacesDepth;
|
||||
readsBaseVertexBuiltin = other.readsBaseVertexBuiltin;
|
||||
writesViewportIndexBuiltin = other.writesViewportIndexBuiltin;
|
||||
needsPassthroughTessControl = other.needsPassthroughTessControl;
|
||||
passthroughTessControlEmulatable = other.passthroughTessControlEmulatable;
|
||||
passthroughPerVertexMembers = other.passthroughPerVertexMembers;
|
||||
lastUsedFrame = other.lastUsedFrame;
|
||||
other.hash = 0;
|
||||
other.descriptorSetLayout = VK_NULL_HANDLE;
|
||||
other.usesUpdateAfterBind = false;
|
||||
other.pipelineLayout = VK_NULL_HANDLE;
|
||||
other.hasStorageImages = false;
|
||||
other.declinedDescriptors = false;
|
||||
other.globalUboBinding = -1;
|
||||
other.activeVertexInputLocationMask = 0;
|
||||
other.activeFragmentOutputLocationMask = 0;
|
||||
@@ -381,11 +206,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
other.producerOutputComponentCount = 0;
|
||||
other.fragmentInputComponentCount = 0;
|
||||
other.fragmentReplacesDepth = false;
|
||||
other.readsBaseVertexBuiltin = false;
|
||||
other.writesViewportIndexBuiltin = false;
|
||||
other.needsPassthroughTessControl = false;
|
||||
other.passthroughTessControlEmulatable = false;
|
||||
other.passthroughPerVertexMembers = 0;
|
||||
other.lastUsedFrame = 0;
|
||||
return *this;
|
||||
}
|
||||
@@ -396,24 +216,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
private:
|
||||
void Destroy() {
|
||||
if (s_device.Get() != VK_NULL_HANDLE) {
|
||||
if (s_device != VK_NULL_HANDLE) {
|
||||
if (pipelineLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyPipelineLayout(s_device.Get(), pipelineLayout, nullptr);
|
||||
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
|
||||
pipelineLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
if (descriptorSetLayout != VK_NULL_HANDLE) {
|
||||
vkDestroyDescriptorSetLayout(s_device.Get(), descriptorSetLayout, nullptr);
|
||||
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
|
||||
descriptorSetLayout = VK_NULL_HANDLE;
|
||||
}
|
||||
for (auto module : modules) {
|
||||
if (module != VK_NULL_HANDLE) {
|
||||
vkDestroyShaderModule(s_device.Get(), module, nullptr);
|
||||
vkDestroyShaderModule(s_device, module, nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
modules.clear();
|
||||
stages.clear();
|
||||
stageSpirvDigests.clear(); // the modules they describe are gone
|
||||
}
|
||||
};
|
||||
|
||||
@@ -429,63 +248,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
virtual void OnProgramEvicted(HashType programHash, VkDescriptorSetLayout descriptorSetLayout) = 0;
|
||||
};
|
||||
|
||||
// How this factory's compute modules implement GL_KHR_shader_subgroup. Computed
|
||||
// once at renderer initialization (SubgroupSupportPolicy.h + the device's
|
||||
// subgroup properties) so lowering can never disagree with the advertised
|
||||
// capabilities. Native subgroup operations always execute natively; the two
|
||||
// repair passes patch modules AROUND them, and the emulation only replaces them
|
||||
// on opted-in devices with no subgroup support at all.
|
||||
struct SubgroupLoweringPolicy {
|
||||
Bool emulateSubgroups = false; // MOBILEGL_MAGMA_EMULATE_SUBGROUP, no-native-support devices
|
||||
Bool fixIterationRPSubgroupScratch = false; // patch iterationRP's under-declared scratch
|
||||
Bool fixIterationRPBarrier = false; // repair Program 203's shared-scratch race
|
||||
Bool deriveNumSubgroups = false; // repair the NumSubgroups builtin
|
||||
Bool requireFullSubgroups = false; // computeFullSubgroups enabled on the device
|
||||
Uint32 nativeSubgroupSize = 0;
|
||||
// Full-subgroup launches are bounded by this device limit; a dispatch whose
|
||||
// workgroup needs more subgroups than this cannot request the flag.
|
||||
Uint32 maxComputeWorkgroupSubgroups = 0;
|
||||
// VkPhysicalDeviceLimits::maxComputeSharedMemorySize; bounds the scratch the
|
||||
// emulation pass may add (0 falls back to the Vulkan minimum, 16384).
|
||||
Uint32 maxComputeSharedMemoryBytes = 0;
|
||||
};
|
||||
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings,
|
||||
Bool shaderDrawParametersEnabled,
|
||||
Bool unformattedFloatStorageImagesEnabled,
|
||||
Bool tessellationAndGeometryPointSizeEnabled,
|
||||
Bool enableSpirvValidation,
|
||||
UpdateAfterBindLimits updateAfterBindLimits,
|
||||
SubgroupLoweringPolicy subgroupPolicy)
|
||||
explicit ProgramFactory(VkDevice device, const VulkanRendererConfig& config, Uint32 maxBindings = 16,
|
||||
Bool shaderDrawParametersEnabled = false,
|
||||
Bool unformattedFloatStorageImagesEnabled = false)
|
||||
: m_device(device), m_maxBindings(maxBindings), m_config(config),
|
||||
m_shaderDrawParametersEnabled(shaderDrawParametersEnabled),
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled),
|
||||
m_tessellationAndGeometryPointSizeEnabled(tessellationAndGeometryPointSizeEnabled),
|
||||
m_enableSpirvValidation(enableSpirvValidation),
|
||||
m_updateAfterBindLimits(updateAfterBindLimits),
|
||||
m_subgroupPolicy(subgroupPolicy) {
|
||||
VkProgramObject::s_device.Get() = device;
|
||||
m_unformattedFloatStorageImagesEnabled(unformattedFloatStorageImagesEnabled) {
|
||||
VkProgramObject::s_device = 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
|
||||
// nothing that owns the device.
|
||||
~ProgramFactory();
|
||||
~ProgramFactory() = default;
|
||||
ProgramFactory(const ProgramFactory&) = delete;
|
||||
|
||||
HashType ComputeHash(const MagmaProgramSource& program, CompileOptionFlags flags) const;
|
||||
HashType ComputeHash(const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags) const;
|
||||
const VkProgramObject& GetOrCreateProgram(
|
||||
const MagmaProgramSource& program, CompileOptionFlags flags);
|
||||
|
||||
// The default framebuffer's current image height, baked as a literal into every
|
||||
// FragCoordYFlip variant (there is no push-constant or specialization channel here, and
|
||||
// adding one for a value that changes only on swapchain recreation would cost the draw
|
||||
// path more than a recompile costs a resize). It is therefore part of those variants'
|
||||
// identity: ComputeHash mixes it in when the bit is set, so a height change re-keys them
|
||||
// and leaves every other program's hash untouched. Setting a NEW height also bumps the
|
||||
// cache-structure epoch, because a caller holding a memoised VkProgramObject* would
|
||||
// otherwise keep using a module compiled against the old height.
|
||||
void SetDefaultFramebufferHeight(Uint32 height);
|
||||
Uint32 GetDefaultFramebufferHeight() const { return m_defaultFramebufferHeight; }
|
||||
const MG_State::GLState::ProgramObject& program, CompileOptionFlags flags);
|
||||
|
||||
// Bumped whenever m_cache's STRUCTURE changes (any insert or erase): the cache is
|
||||
// an open-addressing map holding entries by value, so both moves existing entries.
|
||||
@@ -507,14 +283,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
|
||||
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
|
||||
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
|
||||
// The same question for an IMAGE uniform (`image2D`, `uimageBuffer`, ...), which the
|
||||
// sampler form above deliberately does not answer. Kept separate rather than folded in
|
||||
// because the two are asked in different places for different reasons: a sampler's domain
|
||||
// decides a sampled VIEW format, an image's decides what a placeholder descriptor for an
|
||||
// UNBOUND image unit must be (see UniformManager::AcquireUnboundTexelBufferView and
|
||||
// GetUnboundStorageImageTexture) - a formatless `writeonly` declaration reflects no
|
||||
// format at all, and the numeric domain is then the only thing that constrains it.
|
||||
static SamplerNumericDomain UniformTypeToImageNumericDomain(GLenum glType);
|
||||
// True when any entry point declares the DepthReplacing execution mode, i.e. the
|
||||
// shader assigns gl_FragDepth. Exposed so the blended depth-write quirk's exemption
|
||||
// can be pinned by tests. A false negative loses the exemption, so such a shader is
|
||||
@@ -523,61 +291,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True when an entry point reads the InstanceIndex builtin. Only gates a diagnostic:
|
||||
// without shaderDrawParameters such a shader cannot have gl_InstanceID rebased.
|
||||
static Bool ReflectedReadsInstanceIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// True when an entry point declares the BaseVertex builtin, i.e. when a non-indexed
|
||||
// draw with this program has to take the ZeroBaseVertex variant.
|
||||
static Bool ReflectedReadsBaseVertexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
// Shared by the two above: does any entry point list an input variable decorated with
|
||||
// this builtin?
|
||||
static Bool ReflectedDeclaresInputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
// True when an entry point writes the ViewportIndex builtin (gl_ViewportIndex), i.e. when
|
||||
// the program can route primitives to a viewport other than 0 and its pipeline therefore
|
||||
// has to declare more than one. Asks about OUTPUT variables because that is the direction
|
||||
// a pre-rasterization stage declares it in.
|
||||
static Bool ReflectedWritesViewportIndexBuiltin(const SpvReflectShaderModule& reflectModule);
|
||||
static Bool ReflectedDeclaresOutputBuiltin(const SpvReflectShaderModule& reflectModule, SpvBuiltIn builtin);
|
||||
|
||||
// The pass-through tessellation control stage GL 4.6 core 11.2.2 describes for a
|
||||
// program that has an evaluation stage and no control stage, for an input patch of
|
||||
// `patchVertices` control points. Returned BY VALUE (a stage description is a POD, and
|
||||
// the cache below is a rehashing map, so a pointer into it would not survive the next
|
||||
// distinct patch size). `.module == VK_NULL_HANDLE` means the stage could not be built:
|
||||
// the caller then has no control stage to inject, and CreatePipeline refuses the
|
||||
// pipeline rather than handing the driver a half-tessellated one.
|
||||
//
|
||||
// Keyed on the patch size, the six default tessellation levels AND the gl_PerVertex
|
||||
// member set, because all three decide what the generator emits. The size comes from
|
||||
// PATCH_VERTICES and the levels from PATCH_DEFAULT_OUTER_LEVEL / PATCH_DEFAULT_INNER_LEVEL
|
||||
// - draw state rather than link state, and the CTS case that motivated this links at the
|
||||
// default 3 and draws at 4. The member set comes from the neighbouring evaluation stage's
|
||||
// own SPIR-V, so two programs at different GLSL versions need different modules. The
|
||||
// pipeline cache re-keys on the same inputs, so the module a pipeline was built with is
|
||||
// part of that pipeline's identity. Compiling is bounded by the number of distinct
|
||||
// (size, levels, members) combinations a program draws with - one or two in practice -
|
||||
// and only ever happens for the rare program that has no control stage at all.
|
||||
VkPipelineShaderStageCreateInfo GetOrCreatePassthroughTessControlStage(Uint32 patchVertices,
|
||||
const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel,
|
||||
Uint32 perVertexMembers);
|
||||
|
||||
// Source of the module above. Exposed for tests: the generated GLSL is the whole
|
||||
// contract with the evaluation stage, so it is worth pinning independently of a device.
|
||||
static String BuildPassthroughTessControlSource(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The identity of one such module: everything the generator bakes in, folded into a
|
||||
// 64-bit key over the raw bits (so -0.0 and +0.0 key apart, which is harmless, and NaN
|
||||
// keys to itself, which is what matters). Shared with PipelineFactory, which mixes the
|
||||
// same value into the pipeline hash so a pipeline can never be handed a module built for
|
||||
// different levels or a different block shape.
|
||||
static Uint64 ComputePassthroughTessControlKey(Uint32 patchVertices, const FloatVec4& defaultOuterLevel,
|
||||
const FloatVec2& defaultInnerLevel, Uint32 perVertexMembers);
|
||||
|
||||
// The PerVertexMemberBit mask of the INPUT per-vertex block a module declares, read
|
||||
// straight out of its SPIR-V (OpMemberDecorate ... BuiltIn on the struct behind the one
|
||||
// Input variable that is an array of a Block-decorated struct). Zero when the module has
|
||||
// no such block. Exposed for tests, which is the only way to pin the shape agreement
|
||||
// without a device.
|
||||
static Uint32 ReflectPerVertexInputMembers(const Vector<Uint>& spirv);
|
||||
|
||||
private:
|
||||
struct ProgramLookupCache {
|
||||
@@ -588,27 +301,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
static TextureTarget UniformTypeToTextureTarget(GLenum glType);
|
||||
// `stages` is ALWAYS ProgramObject::GetLinkedShaderStages() - one entry per module of
|
||||
// `spirv`, at the same index. Taking the stages rather than the shader objects is what
|
||||
// keeps the program's live attach list, which is a longer and differently-indexed list
|
||||
// the moment a glAttachShader lands after the link, from being passed here by mistake.
|
||||
void ReflectVertexInputs(const Vector<ShaderStage>& stages,
|
||||
void ReflectVertexInputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectViewportIndexUsage(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectFragmentOutputs(const Vector<ShaderStage>& stages,
|
||||
void ReflectFragmentOutputs(const Vector<SharedPtr<MG_State::GLState::ShaderObject>>& shaders,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
void ReflectLayout(const MagmaProgramSource& program, const Vector<Vector<Uint>>& spirv,
|
||||
void ReflectLayout(const MG_State::GLState::ProgramObject& program, const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
// Fills needsPassthroughTessControl / passthroughTessControlEmulatable off the linked
|
||||
// modules. Const and reflection-only: it decides nothing about the pipeline, it only
|
||||
// records what the evaluation stage's input interface is made of.
|
||||
void ReflectPassthroughTessControlNeed(const Vector<ShaderStage>& stages,
|
||||
const Vector<Vector<Uint>>& spirv,
|
||||
VkProgramObject& entry) const;
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
Uint32 m_maxBindings = 0;
|
||||
@@ -620,47 +320,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// True only when the logical device enabled both
|
||||
// shaderStorageImageReadWithoutFormat and shaderStorageImageWriteWithoutFormat.
|
||||
Bool m_unformattedFloatStorageImagesEnabled = false;
|
||||
// True when the logical device enabled shaderTessellationAndGeometryPointSize. When it is
|
||||
// FALSE a program whose tessellation or geometry module declares TessellationPointSize /
|
||||
// GeometryPointSize is refused at build time (see VkProgramObject::
|
||||
// pointSizeCapabilityUnsupported) instead of being handed to the driver as invalid usage.
|
||||
Bool m_tessellationAndGeometryPointSizeEnabled = false;
|
||||
// Startup snapshot used only by internally synthesized shader modules, which do not
|
||||
// originate from a ProgramLinkTask.
|
||||
Bool m_enableSpirvValidation = false;
|
||||
// Device feature and limit gate resolved before vkCreateDevice. Keeping it in
|
||||
// the factory lets each reflected layout choose ordinary descriptors when its
|
||||
// own counts would exceed the update-after-bind budget.
|
||||
UpdateAfterBindLimits m_updateAfterBindLimits{};
|
||||
SubgroupLoweringPolicy m_subgroupPolicy{};
|
||||
// See SetDefaultFramebufferHeight. 0 means "not known yet"; the FragCoordYFlip bit is
|
||||
// never set before the swapchain exists, so no variant can be compiled against it.
|
||||
Uint32 m_defaultFramebufferHeight = 0;
|
||||
mutable ProgramLookupCache m_lastLookup;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameCounter = 0;
|
||||
// See GetCacheStructureEpoch(). Starts at 1 so a zero-initialized memo can never match.
|
||||
Uint64 m_cacheStructureEpoch = 1;
|
||||
IEvictionObserver* m_evictionObserver = nullptr;
|
||||
// Pass-through tessellation control stages by the identity of what was compiled into
|
||||
// them - the input patch size and the six default tessellation levels, folded into one
|
||||
// 64-bit key by ComputePassthroughTessControlKey (the levels are float state, so the map
|
||||
// cannot simply be keyed on the patch size any more). A failed build is cached as
|
||||
// VK_NULL_HANDLE so a broken generator costs one compile, not one per draw.
|
||||
//
|
||||
// Hard-capped, because the key is application-controlled: glPatchParameterfv clamps
|
||||
// nothing, so an application that recomputes a level per frame mints a new key per frame.
|
||||
// Reaching the cap destroys every module and starts over (see the flush in
|
||||
// GetOrCreatePassthroughTessControlStage); the cap is far above what any program that
|
||||
// holds its levels still will ever need. The gl_PerVertex member set is in the key too
|
||||
// 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 thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline 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
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/SubmitFencePrefix.h
|
||||
// Copyright (c) 2025-2026 MobileGL-Dev
|
||||
// Licensed under the GNU Lesser General Public License v3.0:
|
||||
// https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
// https://www.gnu.org/licenses/lgpl-3.0.txt
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
// Collect every fence in the ordered submission prefix. Waiting on only the last matching
|
||||
// frame-serial record is insufficient: the same serial may have an earlier in-flight submit.
|
||||
template <typename Records, typename Fence>
|
||||
bool CollectSubmitFencePrefix(const Records& records, std::uint64_t submitIndex,
|
||||
Fence nullFence, std::vector<Fence>& fences) {
|
||||
fences.clear();
|
||||
bool foundTarget = false;
|
||||
for (const auto& record : records) {
|
||||
if (record.submitIndex > submitIndex) {
|
||||
break;
|
||||
}
|
||||
if (record.fence == nullFence) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
fences.push_back(record.fence);
|
||||
foundTarget = foundTarget || record.submitIndex == submitIndex;
|
||||
}
|
||||
if (!foundTarget) {
|
||||
fences.clear();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
@@ -8,16 +8,9 @@
|
||||
|
||||
#include "SwapchainObject.h"
|
||||
|
||||
#include <span>
|
||||
|
||||
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
|
||||
#include "MG_State/GLState/TextureState/TextureObject2D.h"
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P5c ev: the surface-changed event's producer callback, installed by the server session.
|
||||
#include <MG_Pipe/MGPipeCallbacks.h>
|
||||
#endif
|
||||
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<vulkan/vk_enum_string_helper.h>)
|
||||
#include <vulkan/vk_enum_string_helper.h>
|
||||
@@ -142,26 +135,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
|
||||
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
|
||||
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
|
||||
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
|
||||
VK_PRESENT_MODE_FIFO_KHR};
|
||||
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
|
||||
if (swapInterval) {
|
||||
if (*swapInterval == 0) {
|
||||
ranked = s_unthrottledPresentModes;
|
||||
} else if (*swapInterval > 0) {
|
||||
ranked = s_vsyncPresentModes;
|
||||
} else {
|
||||
ranked = s_adaptiveVsyncPresentModes;
|
||||
}
|
||||
}
|
||||
for (auto desiredPresentMode : ranked) {
|
||||
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
|
||||
availablePresentModes.end()) {
|
||||
return desiredPresentMode;
|
||||
const Vector<VkPresentModeKHR>& availablePresentModes) {
|
||||
for (auto desiredPresentMode : s_desiredPresentModes) {
|
||||
for (const auto& presentMode : availablePresentModes) {
|
||||
if (presentMode == desiredPresentMode) {
|
||||
return presentMode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,20 +148,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return availablePresentModes[0];
|
||||
}
|
||||
|
||||
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
|
||||
return string_VkPresentModeKHR(presentMode);
|
||||
}
|
||||
|
||||
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
|
||||
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
|
||||
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
|
||||
VkExtent2D desiredExtent) {
|
||||
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
|
||||
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
|
||||
"SwapchainObject::Create failed: incomplete swapchain capabilities");
|
||||
|
||||
MGLOG_I("Got %d surface formats:", swapchainCapabilities.surfaceFormats.size());
|
||||
for (const auto& sf : swapchainCapabilities.surfaceFormats) {
|
||||
MGLOG_D(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
MGLOG_I(" [%s, %s]", string_VkFormat(sf.format), string_VkColorSpaceKHR(sf.colorSpace));
|
||||
}
|
||||
|
||||
const auto pickedSurfaceFormat = ChooseSwapchainSurfaceFormat(swapchainCapabilities.surfaceFormats);
|
||||
@@ -191,13 +166,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
MGLOG_I("Got %d present modes:", swapchainCapabilities.presentModes.size());
|
||||
for (const auto& pm : swapchainCapabilities.presentModes) {
|
||||
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
|
||||
MGLOG_I(" %s", string_VkPresentModeKHR(pm));
|
||||
}
|
||||
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
|
||||
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
|
||||
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
|
||||
m_presentMode = presentMode;
|
||||
m_supportedPresentModes = swapchainCapabilities.presentModes;
|
||||
|
||||
const auto& swapchainCaps = swapchainCapabilities.capabilities;
|
||||
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
|
||||
@@ -292,8 +265,6 @@ 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);
|
||||
@@ -301,11 +272,6 @@ 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);
|
||||
@@ -314,6 +280,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static_cast<SizeT>(defaultFramebufferExtent.width) *
|
||||
static_cast<SizeT>(defaultFramebufferExtent.height) * 4;
|
||||
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
TextureInternalFormat depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
switch (m_depthStencilFormat) {
|
||||
case VK_FORMAT_D24_UNORM_S8_UINT:
|
||||
@@ -329,6 +300,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthFormat = TextureInternalFormat::Depth24Stencil8;
|
||||
break;
|
||||
}
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
// The default FBO's stencil attachment must track the swapchain extent:
|
||||
// FramebufferObject::CheckCompleteness requires every valid attachment
|
||||
@@ -348,44 +324,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
stencilFormat = depthFormat;
|
||||
break;
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged != nullptr) {
|
||||
// P5c ev (CONTRACT-P5C §4.2): with an active transport the default FBO's
|
||||
// attachments are CLIENT memory and this thread may not write them - the backend
|
||||
// fills MGPSurfaceInfo and posts, and the client's consumer replays exactly the
|
||||
// allocate/format writes of the monolith arm below on the GL thread. The two
|
||||
// formats are always equal by the switches above, so one InternalFormat carries
|
||||
// both. The callback's presence IS the transport probe - the server session
|
||||
// installs it at Accept, and under monolith nobody ever does.
|
||||
MG_Pipe::MGPSurfaceInfo info{};
|
||||
info.Width = defaultFramebufferExtent.width;
|
||||
info.Height = defaultFramebufferExtent.height;
|
||||
info.InternalFormat = static_cast<Uint32>(depthFormat);
|
||||
info.Samples = 1;
|
||||
info.Layers = 1;
|
||||
info.IsDefault = 1;
|
||||
MG_Pipe::gMGPipeCallbacks.OnSurfaceChanged(&info);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
auto* colorTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->colorAttachment.get());
|
||||
colorTex->AllocateStorage(
|
||||
TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
auto* depthTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->depthAttachment.get());
|
||||
depthTex->SetInternalFormat(depthFormat);
|
||||
depthTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
}
|
||||
auto* stencilTex = static_cast<MG_State::GLState::TextureObject2D*>(defaultFBOInfo->stencilAttachment.get());
|
||||
stencilTex->SetInternalFormat(stencilFormat);
|
||||
stencilTex->AllocateStorage(TextureUploadTarget::Texture2D, 0, {
|
||||
{extentWidth, extentHeight, 1},
|
||||
defaultAttachmentByteSize}); // TODO: 4 is format size
|
||||
|
||||
}
|
||||
|
||||
@@ -495,7 +438,6 @@ 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,7 +9,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Includes.h>
|
||||
#include <MG_Util/Damage/Damage.h>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -27,26 +26,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
|
||||
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
|
||||
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
|
||||
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
|
||||
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
|
||||
// vsync) wants FIFO_RELAXED, else FIFO.
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
|
||||
Optional<Int> swapInterval = Nullopt);
|
||||
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
|
||||
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
|
||||
|
||||
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
|
||||
Optional<Int> swapInterval);
|
||||
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
|
||||
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; }
|
||||
// The surface's present modes as queried when this swapchain was created.
|
||||
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
|
||||
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
|
||||
VkExtent2D GetExtent() const { return m_extent; }
|
||||
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
|
||||
@@ -81,18 +67,6 @@ 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);
|
||||
@@ -105,8 +79,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
|
||||
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
|
||||
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
|
||||
Vector<VkPresentModeKHR> m_supportedPresentModes;
|
||||
VkSurfaceFormatKHR m_surfaceFormat{};
|
||||
VkExtent2D m_extent{};
|
||||
VkExtent2D m_surfaceExtent{};
|
||||
@@ -122,8 +94,5 @@ 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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "ProgramFactory.h"
|
||||
#include "MagmaProgramSource.h"
|
||||
#include "VkBufferManager.h"
|
||||
#include "VkSamplerManager.h"
|
||||
#include "VkTextureManager.h"
|
||||
#include "WirePlaceholderKey.h"
|
||||
#include "../DescriptorPoolCensus.h"
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
|
||||
@@ -29,26 +26,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
public:
|
||||
struct SamplerBindingOverride {
|
||||
Uint32 binding = 0;
|
||||
Uint32 element = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
VkImageView imageView = VK_NULL_HANDLE;
|
||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Bool forceNearestFiltering = false;
|
||||
};
|
||||
|
||||
struct SamplerImageFeedbackBinding {
|
||||
Uint32 samplerBinding = 0;
|
||||
Uint32 samplerElement = 0;
|
||||
MG_State::GLState::ITextureObject* texture = nullptr;
|
||||
const MG_State::GLState::SamplerObject* sampler = nullptr;
|
||||
SamplerNumericDomain numericDomain = SamplerNumericDomain::Unknown;
|
||||
};
|
||||
|
||||
// `physicalDevice` is only ever asked for format properties: a placeholder descriptor for
|
||||
// an unbound texel-buffer binding has to be built from a format the DEVICE accepts as a
|
||||
// texel buffer, and there is no other route to that answer from here.
|
||||
Bool Initialize(VkDevice device, VkPhysicalDevice physicalDevice, VkBufferManager* bufferManager,
|
||||
Bool Initialize(VkDevice device, VkBufferManager* bufferManager,
|
||||
ProgramFactory* programFactory,
|
||||
VkDeviceSize minUniformBufferOffsetAlignment, Uint32 frameCount,
|
||||
Uint32 maxBindings = 16, Uint32 setsPerFrame = 64,
|
||||
@@ -56,25 +39,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void Shutdown();
|
||||
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A present-less wire frame may run arbitrarily many draws. Before its
|
||||
// descriptor cursor reaches this budget, the renderer retires the
|
||||
// submission that last used the sets and rewinds every layout cursor.
|
||||
static constexpr Uint32 kWireDescriptorSetBudget = 2048;
|
||||
Bool WireDescriptorSetBudgetReached(Uint32 frameIndex) const;
|
||||
SizeT RewindWireDescriptorSets(Uint32 frameIndex);
|
||||
// How the wire arm answers a STORAGE image unit that GL 4.6 core 8.26 makes invalid (no
|
||||
// texture, or a (level, layer) its texture lacks): a load returns zero, a store and an
|
||||
// atomic are discarded. `deviceNullDescriptor` is the renderer's enablement of
|
||||
// VK_EXT_robustness2's nullDescriptor at device creation. With it the unit binds a NULL
|
||||
// storage-image descriptor, which is exactly that rule. Without it (or under
|
||||
// MGITEST_MAGMA_FORCE_PRIVATE_IMAGE_PLACEHOLDER=1, the host lanes' way onto this arm) the
|
||||
// unit binds a placeholder PRIVATE to its unit (per shape), cleared before each use, so no
|
||||
// two units ever alias; what that cannot give is a load of the SAME unit after its own
|
||||
// store inside one pass reading zero, nor privacy past kWirePrivateStoragePlaceholderCap
|
||||
// placeholders (CONTRACT-P7 §12). Called after Initialize.
|
||||
void SetWireInvalidStorageImageArm(Bool deviceNullDescriptor);
|
||||
#endif
|
||||
// A command buffer (re)began recording: descriptor bindings recorded into
|
||||
// the previous buffer do not carry over, so drop the bind-dedup shadow.
|
||||
void OnCommandBufferBoundary() { m_lastBindValid = false; }
|
||||
@@ -85,24 +49,19 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
|
||||
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
|
||||
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
|
||||
// references its sets. This is the only PER-LAYOUT eviction path for the
|
||||
// caches - a live layout's entry must never be purged on its own (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here. The
|
||||
// other path, TrimFrameDescriptorPools, drops a slot's whole cache together with
|
||||
// the pools behind it.
|
||||
// references its sets. This is the only eviction path for the per-layout
|
||||
// caches - a live layout's entry must never be purged (its sets would be
|
||||
// unreachable pool slots), so there is deliberately no age-based sweep here.
|
||||
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
|
||||
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
|
||||
// in binding order, and within a binding in array-element order): the resolved texture
|
||||
// and effective sampler, as never-reused lifetime ids so a freed-and-reallocated object
|
||||
// at the same heap address can only MISS a comparison, never false-hit it (same ABA
|
||||
// rule as SamplerResolveMemo). An arrayed binding contributes one record per element -
|
||||
// element granularity is required, or swapping the textures of two elements of the same
|
||||
// array would leave the record list identical and the fast path would keep a stale set.
|
||||
// One record per visited CombinedImageSampler binding (post fallback substitution,
|
||||
// in binding order): the resolved texture and effective sampler, as never-reused
|
||||
// lifetime ids so a freed-and-reallocated object at the same heap address can only
|
||||
// MISS a comparison, never false-hit it (same ABA rule as SamplerResolveMemo).
|
||||
struct SampledBindingRecord {
|
||||
Uint64 textureLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
};
|
||||
Bool CollectSampledTextures(const MagmaProgramSource& program,
|
||||
Bool CollectSampledTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures,
|
||||
Vector<SampledBindingRecord>* outBindingRecords = nullptr);
|
||||
@@ -111,25 +70,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// (texture, effective sampler) pair. A texture bind generation bump alone (e.g. a
|
||||
// redundant glBindSampler, which always bumps it) does not prove the sampled set
|
||||
// moved; this walk does, without rebuilding the set or falling off the fast path.
|
||||
Bool SampledBindingsUnchanged(const MagmaProgramSource& program,
|
||||
Bool SampledBindingsUnchanged(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
const Vector<SampledBindingRecord>& previousRecords) const;
|
||||
Bool CollectStorageImageTextures(const MagmaProgramSource& program,
|
||||
Bool CollectStorageImageTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<MG_State::GLState::ITextureObject*>& outTextures) const;
|
||||
Bool CollectSamplerImageFeedback(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Vector<SamplerImageFeedbackBinding>& outBindings) const;
|
||||
static Bool SamplerOverlapsWritableImageSubresource(Int samplerBaseLevel, Int samplerMaxLevel,
|
||||
GLint imageLevel, GLenum imageAccess);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Resolve lazy texture uploads/promotions before the caller captures the
|
||||
// command buffer passed to BindProgramUniformBuffers. Preparation may
|
||||
// submit older work, but descriptor recording must never rotate it.
|
||||
Bool PrepareWireTextureResources(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
#endif
|
||||
// samplerDescriptorsUnchangedHint: the caller (SetupDraw fast path) proved that
|
||||
// every input of every combined-image-sampler resolution is unchanged since the
|
||||
// previous draw's resolve - same (texture, sampler) per binding, texture params
|
||||
@@ -137,13 +83,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// epochs AND per-resource layout values - so the per-binding cached
|
||||
// VkDescriptorImageInfo may be reused without re-running the resolve chain.
|
||||
Bool BindProgramUniformBuffers(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj,
|
||||
Uint32 frameIndex,
|
||||
VkPipelineBindPoint bindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
const SamplerBindingOverride* samplerBindingOverride = nullptr,
|
||||
Bool samplerDescriptorsUnchangedHint = false,
|
||||
const Vector<SamplerBindingOverride>* samplerBindingOverrides = nullptr);
|
||||
Bool samplerDescriptorsUnchangedHint = false);
|
||||
|
||||
// Pure format-policy helper kept public for host regression tests. Formatted storage
|
||||
// images use their shader qualifier; transformed float images use glBindImageTexture's
|
||||
@@ -158,49 +103,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// ExplicitLod0Sampling SPIR-V rewrite safe to request. Deliberately conservative: it reads
|
||||
// only GL state, so a texture that ends up single-level for another reason (one uploaded
|
||||
// level under a wide level range) merely misses the rewrite.
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
|
||||
static Bool ProgramSamplesOnlySingleLevelTextures(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj);
|
||||
|
||||
// ---- per-frame descriptor pools ----------------------------------------------------
|
||||
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
|
||||
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
|
||||
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
|
||||
// updateAfterBind flavour, because a set can only come from a pool created with the flag
|
||||
// its layout was.
|
||||
private:
|
||||
struct DescriptorPoolBucket {
|
||||
VkDescriptorPool handle = VK_NULL_HANDLE;
|
||||
Uint32 maxSets = 0;
|
||||
Uint32 allocatedSets = 0;
|
||||
Bool updateAfterBind = false;
|
||||
};
|
||||
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
|
||||
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
|
||||
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
|
||||
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
|
||||
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
|
||||
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
|
||||
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
|
||||
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
|
||||
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
|
||||
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
|
||||
// smallest one, and doubling IT made every grown pool the same size.
|
||||
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
|
||||
Uint32 initialMaxSets);
|
||||
// Trim policy (see TrimFrameDescriptorPools). A slot is "quiet" in an epoch whose peak set
|
||||
// count times kDescriptorTrimHeadroom still fits its capacity; a slot that has been quiet for
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES consecutive epochs, and holds at least
|
||||
// kDescriptorTrimMinCapacity sets, is trimmed. The headroom is what keeps the trim from
|
||||
// thrashing: geometric growth leaves a re-grown slot at least a third full (the pool a growth
|
||||
// adds is at most twice the largest before it, so at most twice everything the slot held, and
|
||||
// the epoch that grew it needed more than that), so the epoch that grew a slot is never quiet
|
||||
// for it. The floor keeps a slot sized for ordinary
|
||||
// frames from being trimmed at all - the trim exists to give back a burst.
|
||||
static constexpr Uint32 kDescriptorTrimHeadroom = 4;
|
||||
static constexpr Uint32 kDescriptorTrimMinCapacity = 1024;
|
||||
// For tests (DescriptorPoolCensus.h): pools, capacity, cached sets and trims over every slot.
|
||||
DescriptorPoolCensus GetDescriptorPoolCensus() const;
|
||||
|
||||
private:
|
||||
|
||||
// A cached descriptor set together with the pool it was allocated from, so a
|
||||
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
|
||||
@@ -215,90 +126,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 cursor = 0;
|
||||
};
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// ResolveWireImageDescriptor emits no pNext chain. Key every view-create
|
||||
// value explicitly: hash collisions must never alias different windows,
|
||||
// formats or swizzles. The root handle and allocation epoch also prevent
|
||||
// a recycled native image handle from reviving an older view.
|
||||
struct WireImageViewKey {
|
||||
MG_Pipe::MGPipeHandle root{};
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
Uint64 imageEpoch = 0;
|
||||
VkImageViewCreateFlags flags = 0;
|
||||
VkImageViewType type = VK_IMAGE_VIEW_TYPE_2D;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkComponentMapping components{};
|
||||
VkImageSubresourceRange range{};
|
||||
|
||||
Bool operator==(const WireImageViewKey& other) const {
|
||||
return root == other.root && image == other.image && imageEpoch == other.imageEpoch &&
|
||||
flags == other.flags && type == other.type && format == other.format &&
|
||||
components.r == other.components.r && components.g == other.components.g &&
|
||||
components.b == other.components.b && components.a == other.components.a &&
|
||||
range.aspectMask == other.range.aspectMask && range.baseMipLevel == other.range.baseMipLevel &&
|
||||
range.levelCount == other.range.levelCount && range.baseArrayLayer == other.range.baseArrayLayer &&
|
||||
range.layerCount == other.range.layerCount;
|
||||
}
|
||||
};
|
||||
|
||||
struct WireImageViewKeyHash {
|
||||
SizeT operator()(const WireImageViewKey& key) const;
|
||||
};
|
||||
|
||||
struct WirePlaceholderImage {
|
||||
VkImage image = VK_NULL_HANDLE;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
VkRenderPass clearPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer clearFramebuffer = VK_NULL_HANDLE;
|
||||
VkFormat format = VK_FORMAT_UNDEFINED;
|
||||
VkImageAspectFlags aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
||||
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
Uint32 layers = 1;
|
||||
};
|
||||
// Keyed by shape, and a storage placeholder on the arm without a null descriptor by its
|
||||
// image unit too (WirePlaceholderKey.h: ChooseWirePlaceholderKey, and its cap).
|
||||
mutable UnorderedMap<WirePlaceholderKey, WirePlaceholderImage, WirePlaceholderKeyHash> m_wirePlaceholderImages;
|
||||
// How many of those are private to a unit (against kWirePrivateStoragePlaceholderCap).
|
||||
mutable SizeT m_wirePrivateStoragePlaceholders = 0;
|
||||
// See SetWireInvalidStorageImageArm.
|
||||
Bool m_wireInvalidStorageImagesBindNull = false;
|
||||
// boundStorageFormat: for a storage binding with no reflected format, the format the
|
||||
// unit's glBindImageTexture named, when the unit holds a texture it cannot address
|
||||
// (GL 4.6 core 8.26); UNDEFINED keeps the numeric-domain R32 default. `unit` is the image
|
||||
// unit of a storage binding (what its private placeholder is private to); unused for a sampler.
|
||||
Bool ResolveWirePlaceholderImage(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Bool storage,
|
||||
VkDescriptorImageInfo& out, VkFormat boundStorageFormat = VK_FORMAT_UNDEFINED, Uint32 unit = ~0u) const;
|
||||
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
|
||||
#endif
|
||||
|
||||
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
|
||||
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
|
||||
// the cache only ever grows - except through TrimFrameDescriptorPools, which gives a slot's
|
||||
// whole burst back at once (and OnDescriptorSetLayoutDestroyed, one dead layout at a time).
|
||||
struct FrameResources {
|
||||
Vector<DescriptorPoolBucket> descriptorPools;
|
||||
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
|
||||
Vector<VkBufferView> texelBufferViews;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
mutable Vector<VkImageView> wireImageViews;
|
||||
// Own views in the vector above until this slot's fence/idle proof.
|
||||
// Cache hits preserve the handle, enabling descriptor-content reuse.
|
||||
mutable UnorderedMap<WireImageViewKey, VkImageView, WireImageViewKeyHash> wireImageViewCache;
|
||||
#endif
|
||||
Uint32 activeDescriptorPoolIndex = 0;
|
||||
Uint32 allocatedSetsThisFrame = 0;
|
||||
Uint32 peakAllocatedSetsThisFrame = 0;
|
||||
// Sum of maxSets over descriptorPools, kept in step by Initialize, Grow and Trim.
|
||||
Uint64 capacitySets = 0;
|
||||
// Consecutive epochs (see NoteEndedEpochForTrim) that were quiet for THIS slot's capacity.
|
||||
Uint32 quietEpochs = 0;
|
||||
};
|
||||
|
||||
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
|
||||
static Bool ResolveSamplerTexture(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
SharedPtr<MG_State::GLState::ITextureObject>& outTexture);
|
||||
// Shared per-binding resolution for CollectSampledTextures and
|
||||
@@ -306,105 +143,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// texture after the fallback substitution (may still be null when no fallback
|
||||
// exists), effective sampler = unit override else the texture's own sampler.
|
||||
// False = the binding is skipped (unbound with a non-2D fallback target).
|
||||
// `element` indexes a sampler array inside the binding; see ResolveSamplerDescriptor.
|
||||
Bool ResolveSampledBinding(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element,
|
||||
Bool ResolveSampledBinding(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
MG_State::GLState::ITextureObject*& outTexture,
|
||||
const MG_State::GLState::SamplerObject*& outSampler) const;
|
||||
// Raw-pointer variant for the per-draw sampled-texture walk (CollectSampledTextures):
|
||||
// the bound texture stays alive through the draw via GL binding state, so callers that
|
||||
// only need the pointer skip the SharedPtr copy's atomic refcount churn.
|
||||
static MG_State::GLState::ITextureObject* ResolveSamplerTextureRaw(
|
||||
const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding, Uint32 element);
|
||||
// `numericDomain` is the sampler's class, and it matters only for the multisample arm -
|
||||
// see GetFallbackMultisampleTexture for why the single-sampled fallback can ignore it.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// The multisample arm of GetFallbackTexture. One object per (target, numeric domain) and
|
||||
// no upload path: a multisample image cannot be written by a transfer, so its texels stay
|
||||
// undefined - which is what GL promises for a texelFetch on an incomplete multisample
|
||||
// texture - and it cannot carry MUTABLE_FORMAT, so its format has to match the sampler's
|
||||
// class outright rather than being reinterpreted at view time.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackMultisampleTexture(
|
||||
TextureTarget target, SamplerNumericDomain numericDomain) const;
|
||||
// ---- placeholders for UNBOUND image-backed descriptors -------------------------
|
||||
// GL lets a program declare `samplerBuffer`, `imageBuffer` or `image2D` and bind nothing
|
||||
// to the unit it names: the fetch is then undefined (GL 4.6 core 8.9 for an incomplete
|
||||
// buffer texture, 8.26 for an image unit with no texture) - undefined VALUES, not a
|
||||
// dropped draw. Vulkan has no unwritten descriptor, so something valid has to sit in the
|
||||
// set or the whole draw or dispatch is lost, which is what these two build. Same shape as
|
||||
// VkBufferManager::AcquireUnboundStorageDescriptor, one level up: per FORMAT rather than
|
||||
// one shared object, because a descriptor whose format disagrees with the shader's
|
||||
// declaration is invalid Vulkan even when nothing ever reads it.
|
||||
//
|
||||
// `declaredFormat` is the format the SHADER declared (VK_FORMAT_UNDEFINED for a sampled
|
||||
// texel buffer, which never carries one, or for a formatless `writeonly` image);
|
||||
// `numericDomain` decides the format when there is no declaration and is the fallback
|
||||
// class when the device cannot use the declared one as a texel buffer.
|
||||
VkBufferView AcquireUnboundTexelBufferView(VkFormat declaredFormat, SamplerNumericDomain numericDomain,
|
||||
Bool storage);
|
||||
// A 1x1 (x1 layer, or 6 faces for a cube) texture of `format`, shaped for `target` so the
|
||||
// view the descriptor gets has the view type the shader's image declaration demands.
|
||||
// Null for a target with no single-sampled placeholder shape - multisample images, whose
|
||||
// descriptor needs a multisample view that this cannot stand in for.
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetUnboundStorageImageTexture(TextureTarget target,
|
||||
VkFormat format) const;
|
||||
// The (target, format) pair a storage-image binding's placeholder is keyed by, resolved
|
||||
// from reflection alone. False when the binding has no placeholder shape.
|
||||
Bool ResolveUnboundStorageImagePlaceholder(const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
TextureTarget& outTarget, VkFormat& outFormat) const;
|
||||
// `element` indexes a sampler ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL texture unit, so it selects the texture, the sampler
|
||||
// override and the fallback separately from its neighbours.
|
||||
//
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> GetFallbackTexture(TextureTarget target) const;
|
||||
// trustUnchangedHint: reuse this binding's cached VkDescriptorImageInfo outright
|
||||
// (see BindProgramUniformBuffers' samplerDescriptorsUnchangedHint for the proof
|
||||
// obligations the caller carries). The cache is keyed by binding alone, so it is
|
||||
// used ONLY for single-descriptor bindings - see m_samplerResolveMemo.
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
// obligations the caller carries).
|
||||
Bool ResolveSamplerDescriptor(VkCommandBuffer commandBuffer, const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo,
|
||||
VkDescriptorImageInfo& outImageInfo,
|
||||
Bool trustUnchangedHint = false) const;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireImageDescriptor(VkCommandBuffer commandBuffer, const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, Bool storage, VkDescriptorImageInfo& out) const;
|
||||
Bool ResolveWireTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, Bool storage, VkBufferView& out);
|
||||
Uint32 m_wireFrameIndex = 0;
|
||||
VkDeviceSize m_wireStorageOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireTexelOffsetAlignment = 1;
|
||||
VkDeviceSize m_wireMaxUniformRange = ~VkDeviceSize{0};
|
||||
VkDeviceSize m_wireMaxStorageRange = ~VkDeviceSize{0};
|
||||
Uint32 m_wireMaxTexelElements = ~Uint32{0};
|
||||
#endif
|
||||
Bool ResolveSamplerDescriptorOverride(const SamplerBindingOverride& samplerBindingOverride,
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
Bool ResolveTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveTexelBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// GLSL `imageBuffer`: the same VkBufferView descriptor as the sampled texel buffer above,
|
||||
// but resolved from an IMAGE unit (glBindImageTexture) rather than a texture unit, and
|
||||
// made GPU-resident-writable because the shader may store to it. No `element` parameter:
|
||||
// an imageBuffer ARRAY is refused at program creation, so a binding is always one
|
||||
// descriptor (see the array gate in RemapDescriptorBindingsForVulkan).
|
||||
Bool ResolveStorageTexelBufferDescriptor(const MagmaProgramSource& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 frameIndex, VkBufferView& outBufferView);
|
||||
// `element` indexes a block INSTANCE array's descriptors; it is 0 for every ordinary
|
||||
// block. Each element resolves through its own GL storage block, and so its own GL
|
||||
// binding point, buffer and glBindBufferRange window.
|
||||
Bool ResolveStorageBufferDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveStorageBufferDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
// `element` indexes an image ARRAY inside one binding; each element carries its own
|
||||
// independently assigned GL image unit.
|
||||
VkDescriptorBufferInfo& outBufferInfo) const;
|
||||
Bool ResolveStorageImageDescriptor(VkCommandBuffer commandBuffer,
|
||||
const MagmaProgramSource& program,
|
||||
const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 element, VkDescriptorImageInfo& outImageInfo) const;
|
||||
VkDescriptorImageInfo& outImageInfo) const;
|
||||
// Result of resolving a UBO binding: either a zero-copy direct bind to the app's resident
|
||||
// VkBuffer (the GLES backend's approach - no per-draw copy) or the CPU payload to upload.
|
||||
struct UboBindResult {
|
||||
@@ -415,18 +183,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const void* payload = nullptr; // fallback UploadTransient path
|
||||
VkDeviceSize payloadSize = 0;
|
||||
};
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool ResolveWireUniformBufferPayload(const MagmaProgramSource& program, Uint32 blockIndex,
|
||||
Uint32 bindingPoint, UboBindResult& out) const;
|
||||
#endif
|
||||
Bool ResolveUniformBufferPayload(const MagmaProgramSource& program,
|
||||
Bool ResolveUniformBufferPayload(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, UboBindResult& out) const;
|
||||
// Shared resolution of one dynamic-UBO binding element into the
|
||||
// (buffer, range, dynamicOffset) triple the descriptor consumes: direct
|
||||
// bind, global-slice reuse, or transient upload. Used by the full walk
|
||||
// and by the dynamic-offset-only rebind (see FastRebindMemo).
|
||||
Bool ResolveDynamicUboDescriptor(const MagmaProgramSource& program,
|
||||
Bool ResolveDynamicUboDescriptor(const MG_State::GLState::ProgramObject& program,
|
||||
const ProgramFactory::VkProgramObject& programObj, Uint32 binding,
|
||||
Uint32 arrayElement, Uint32 frameIndex, VkBuffer& outBuffer,
|
||||
VkDeviceSize& outRange, Uint32& outDynamicOffset);
|
||||
@@ -437,30 +201,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void BindDescriptorSetDeduped(VkCommandBuffer commandBuffer, VkPipelineBindPoint bindPoint,
|
||||
VkPipelineLayout pipelineLayout, VkDescriptorSet descriptorSet,
|
||||
const Vector<Uint32>& dynamicOffsets);
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
|
||||
// An epoch is the stretch between two BeginFrame calls: one frame, or the part of one between
|
||||
// two drains (VulkanRenderer::TryDrainFrameTransients). Called by BeginFrame with the epoch
|
||||
// that just ended, it updates every slot's quiet count - a burst in any slot counts against
|
||||
// all of them, so bursts that land in the other slots still hold a slot's trim off.
|
||||
void NoteEndedEpochForTrim(Uint32 epochPeakSets);
|
||||
// Gives a slot's whole descriptor burst back: clears its per-layout cache, destroys every
|
||||
// grown pool and resets the base pool, so the slot re-grows geometrically from setsPerFrame.
|
||||
// Whole-slot rather than per-set: vkFreeDescriptorSets fragments a pool and can never empty
|
||||
// a grown one. Called only from BeginFrame, which runs only when no pending command buffer
|
||||
// references the slot's sets:
|
||||
// - Present path (VulkanRenderer::Present, after WaitAndAcquireNextImage): the slot's
|
||||
// imageInFlightFence has been waited. That fence rides the slot's LAST submission - the
|
||||
// Present submit - and every mid-frame submission of the slot (FlushPendingCommands,
|
||||
// SplitOversizedRecording, readbacks) went to the same queue earlier, so the fence's
|
||||
// signal covers them (vkQueueSubmit's fence scope includes all earlier submissions).
|
||||
// - Drain path (VulkanRenderer::TryDrainFrameTransients): every submission has been
|
||||
// observed complete and no recording is open.
|
||||
// Sets are per slot: a slot's sets are only ever bound into recordings made while it is the
|
||||
// current slot, so trimming one slot never touches another slot's in-flight work.
|
||||
void TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex);
|
||||
// Drops every handle this class holds to a descriptor set outside the per-layout caches.
|
||||
void InvalidateDescriptorSetMemos();
|
||||
Bool CreateDescriptorPool(Uint32 maxSets, VkDescriptorPool& outPool) const;
|
||||
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex);
|
||||
VkResult AllocateDescriptorSetsFromActivePool(
|
||||
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
|
||||
VkResult AcquireDescriptorSet(Uint32 frameIndex,
|
||||
@@ -468,7 +210,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkDescriptorSet& outDescriptorSet);
|
||||
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
VkBufferManager* m_bufferManager = nullptr;
|
||||
ProgramFactory* m_programFactory = nullptr;
|
||||
Vector<FrameResources> m_frames;
|
||||
@@ -478,28 +219,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint32 m_maxBindings = 0;
|
||||
Uint32 m_setsPerFrame = 0;
|
||||
Uint32 m_peakDescriptorSetsObserved = 0;
|
||||
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES, latched at Initialize (0 = never trim).
|
||||
Uint32 m_trimQuietEpochs = 0;
|
||||
// The slot whose epoch is open (the last one BeginFrame was called for); valid once
|
||||
// m_epochOpen is set. Its peak is the ended epoch's peak at the next BeginFrame.
|
||||
Uint32 m_epochSlot = 0;
|
||||
Bool m_epochOpen = false;
|
||||
Uint64 m_descriptorSlotTrims = 0;
|
||||
VkTextureManager* m_textureManager = nullptr;
|
||||
VkSamplerManager* m_samplerManager = nullptr;
|
||||
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
|
||||
// Keyed by (arrayed, numeric domain); see GetFallbackMultisampleTexture. Lazily populated,
|
||||
// never evicted - at most six tiny 1x1 images - and torn down with the manager.
|
||||
mutable UnorderedMap<Uint32, SharedPtr<MG_State::GLState::ITextureObject>> m_fallbackMultisampleTextures;
|
||||
// See AcquireUnboundTexelBufferView / GetUnboundStorageImageTexture. Both are lazily
|
||||
// populated, never evicted (a program's declared formats are a fixed, tiny set) and torn
|
||||
// down with the manager. The texel views are keyed by format AND by storage-vs-sampled
|
||||
// because the two descriptor kinds demand different format FEATURES of the device, so one
|
||||
// format can be usable for one and not the other. Deliberately NOT the per-frame
|
||||
// texelBufferViews list: those are destroyed at every frame boundary, and these must
|
||||
// outlive it or the placeholder would be rebuilt for every unbound binding every frame.
|
||||
UnorderedMap<Uint64, VkBufferView> m_unboundTexelBufferViews;
|
||||
mutable UnorderedMap<Uint64, SharedPtr<MG_State::GLState::ITextureObject>> m_unboundStorageImageTextures;
|
||||
|
||||
// Per-draw scratch buffers for BindProgramUniformBuffers: reused (clear keeps
|
||||
// capacity) so the descriptor-write path stops allocating on every draw.
|
||||
@@ -545,15 +267,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// drops the hint upstream; an arena wrap or growth resolves a different
|
||||
// VkBuffer and misses. AcquireDescriptorSet's per-frame cursor only
|
||||
// advances, so the recorded set is never re-written within its frame.
|
||||
// P7 M2 round 2 (ID-P7-43): a WIRE store can die MID-FRAME on the reclaim
|
||||
// path (VkBufferManager::DeferredWireRelease), and the next mint can hand
|
||||
// its VkBuffer handle value back - a heap pointer under lavapipe - so "the
|
||||
// same VkBuffer+range" is no longer proof of the same store. The memo
|
||||
// therefore records VkBufferManager::GetWireStoreDestroyEpoch() and misses
|
||||
// once any wire store has been destroyed since; the descriptor-reuse
|
||||
// signature below folds the same epoch in. On the wire arm this memo is
|
||||
// never consulted (SetupWireDraw passes no sampler hint), so the field is
|
||||
// the defensive half; the signature is the half the wire arm reaches.
|
||||
struct FastRebindMemo {
|
||||
Bool valid = false;
|
||||
Uint32 frameIndex = 0;
|
||||
@@ -563,9 +276,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkBuffer uboBuffer = VK_NULL_HANDLE;
|
||||
VkDeviceSize uboRange = 0;
|
||||
VkDescriptorSet set = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Uint64 wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
};
|
||||
FastRebindMemo m_fastRebindMemo;
|
||||
|
||||
@@ -609,11 +319,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// lifetime id, so a freed-and-reallocated sampler or texture at the same heap address
|
||||
// always gets a fresh id and misses (a raw pointer would false-hit that ABA) - so a
|
||||
// stale guess can only miss and fall through to the hash, never resolve wrong. Still
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding, but the
|
||||
// whole-descriptor entry is additionally keyed by program lifetime: Vulkan binding
|
||||
// numbers are layout-local and unrelated programs routinely reuse binding 0/1.
|
||||
// reset each frame alongside the descriptor-set cache. Indexed by binding.
|
||||
struct SamplerResolveMemo {
|
||||
Uint64 infoProgramLifetimeId = 0;
|
||||
Uint64 samplerLifetimeId = 0;
|
||||
Uint64 textureLifetimeId = 0;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
@@ -634,14 +341,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// proves every resolve input unchanged; cleared with the per-frame reset
|
||||
// (the cached VkSampler outlives a frame only via a fresh resolve, which
|
||||
// also re-stamps it against VkSamplerManager's frame-boundary sweep).
|
||||
//
|
||||
// This one field is keyed by binding but describes ONE descriptor, so it is
|
||||
// written and read only for single-descriptor bindings. A sampler ARRAY's
|
||||
// elements share the binding and would overwrite each other here - the last
|
||||
// element resolved would then be handed to element 0 on the next hinted draw.
|
||||
// Every other field above is self-validating (each compares its full key
|
||||
// before reuse, and the view-format entry is a pure function of format and
|
||||
// numeric domain), so an arrayed binding may keep using those.
|
||||
VkDescriptorImageInfo info{};
|
||||
Bool infoValid = false;
|
||||
};
|
||||
|
||||
@@ -7,13 +7,7 @@
|
||||
// End of Source File Header
|
||||
|
||||
#include "VertexInputStateFactory.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "MG_Util/Converters/MGToStr/DataTypeConverter.h"
|
||||
#include <MG_Backend/BackendObjects.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <Config.h>
|
||||
#include <MG_Remote/Server/ServerLoop.h>
|
||||
#endif
|
||||
#include <utility>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -50,149 +44,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// capture came back holding a dead VAO's vertex data (0,0,0,1 - the previous
|
||||
// test's positions) instead of its own.
|
||||
// Zero for client memory (no buffer), which is a distinct identity of its own.
|
||||
//
|
||||
// P2 D12.4 / ARCHITECTURE.md 9.5: under the handle arm the identity is the
|
||||
// buffer's {slot, gen} rather than its lifetime id - "lifetimeId -> gen mixed
|
||||
// into every server-side content hash". The two are equally ABA-proof (the
|
||||
// allocator maps one onto the other and bumps Gen only on slot REUSE); what
|
||||
// changes is that the key is now the identity the SERVER will be handed once
|
||||
// buffers travel as handles, instead of a number only the client can mint.
|
||||
Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
if (attr.Buffer) {
|
||||
// The SAME arm question the other four re-keyed sites ask, through the same
|
||||
// helper: a site that decided for itself could silently key on the pre-handle
|
||||
// identity while its neighbours keyed on the handle.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::Buffer, attr.Buffer->GetLifetimeId());
|
||||
bufferKey = static_cast<Uint64>(handle.Slot) | (static_cast<Uint64>(handle.Gen) << 32);
|
||||
}
|
||||
if (MagmaPipeAbaControlDefeatsIdentity()) {
|
||||
// Negative control C (P2 brief D18), on WHICHEVER arm this run is on - the
|
||||
// pre-handle lifetime id and the handle's {slot, gen} are the same guard
|
||||
// wearing two hats, and a control that defeated only the retired one would
|
||||
// say nothing about the key P2 ships.
|
||||
//
|
||||
// The identity is replaced by a constant rather than by the raw
|
||||
// BufferObject*, because the address is not recycled in practice and so
|
||||
// never collides (see MagmaPipeAbaControlDefeatsIdentity). Zero is what a
|
||||
// key with NO buffer identity in it looks like - the exact defect this
|
||||
// hash was fixed for: "the hash is what TryBindResolvedVertexBindings
|
||||
// accepts as proof that a memoised binding still reads the buffer it was
|
||||
// resolved from", and with the identity gone it accepts a binding resolved
|
||||
// from a different buffer. HandleRecycleScenario.AbaControl then draws a
|
||||
// replacement VAO and gets its dead predecessor's vertex data.
|
||||
bufferKey = 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const Uint64 bufferKey = attr.Buffer ? attr.Buffer->GetLifetimeId() : 0;
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &bufferKey, sizeof(bufferKey)));
|
||||
}
|
||||
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
VertexInputStateFactory::VaoBackendMemos& VertexInputStateFactory::MemosFor(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
const MG_Pipe::MGPipeHandle handle =
|
||||
m_identity->HandleOf(MG_Pipe::MGPipeKind::VertexElementsCso, vao.GetLifetimeId());
|
||||
// One entry per mintable slot, grown on demand: the mint has no capacity, so neither
|
||||
// does this, and no two live VAOs can share an entry however large the working set is.
|
||||
// There is no probe in front of it because the mint itself is one - a one-entry memo
|
||||
// hit for every acquisition after this draw's first, and a hash probe otherwise.
|
||||
//
|
||||
// The claim rule - the slot picks the entry, the whole handle (Gen included) decides
|
||||
// whose it is - and negative control C's defeat of it are MagmaPipeArms.h's
|
||||
// MagmaPipeClaimSlotMemos, so that the unit suite which drives a REAL slot reuse
|
||||
// (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) exercises this code and not a copy of it.
|
||||
// What the control defeats HERE is the identity that SELECTS the entry: every VAO
|
||||
// collapses onto one, handed back uncleared, so the replacement inherits the dead
|
||||
// VAO's content hash and its resolved-entry pointer. The GENERATION half is the unit
|
||||
// suite's business, for the reason MagmaPipeAbaControlDefeatsIdentity spells out.
|
||||
return MagmaPipeClaimSlotMemos(m_vaoMemos, handle);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
Bool VertexInputStateFactory::TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao,
|
||||
Uint64& outHash) const {
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
const VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion != vao.GetConfigVersion()) return false;
|
||||
outHash = memos.Hash;
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
return vao.GetBackendHashMemo(outHash);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
VertexInputStateFactory::HashType VertexInputStateFactory::GetOrComputeHash(
|
||||
const MG_State::GLState::VertexArrayObject& vao) const {
|
||||
HashType hash = 0;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same memo, on the backend's side of the boundary.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.HashConfigVersion == vao.GetConfigVersion()) {
|
||||
return memos.Hash;
|
||||
}
|
||||
hash = ComputeHash(vao);
|
||||
memos.Hash = hash;
|
||||
memos.HashConfigVersion = vao.GetConfigVersion();
|
||||
return hash;
|
||||
}
|
||||
#endif
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
if (!vao.GetBackendHashMemo(hash)) {
|
||||
hash = ComputeHash(vao);
|
||||
vao.SetBackendHashMemo(hash);
|
||||
}
|
||||
#endif
|
||||
return hash;
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao) {
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// P2 D12.5: the same per-draw fast path, but the resolved-entry pointer lives in this
|
||||
// factory's slot-indexed table instead of on the frontend VAO. The eviction epoch
|
||||
// survives the move and is still what stops a stale pointer being dereferenced: the
|
||||
// POINTEE is a cache entry this factory can erase at a frame boundary, and moving the
|
||||
// memo does not change that.
|
||||
if (MagmaPipeTrackHArmIsHandles(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) {
|
||||
VaoBackendMemos& memos = MemosFor(vao);
|
||||
if (memos.StateConfigVersion == vao.GetConfigVersion() && memos.State != nullptr &&
|
||||
memos.StateEpoch == m_evictionEpoch) {
|
||||
const auto* memoEntry = static_cast<const BackendVertexInputState*>(memos.State);
|
||||
memoEntry->lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
return *memoEntry;
|
||||
}
|
||||
const BackendVertexInputState& resolved =
|
||||
GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
// MemosFor is re-taken rather than kept live across GetOrCreateVertexInputState:
|
||||
// the reference is not worth holding across a call that can resize the table.
|
||||
VaoBackendMemos& stamp = MemosFor(vao);
|
||||
stamp.State = &resolved;
|
||||
stamp.StateEpoch = m_evictionEpoch;
|
||||
stamp.StateConfigVersion = vao.GetConfigVersion();
|
||||
// The AUX memo is deliberately NOT stamped here: its two words already live in
|
||||
// VulkanRenderer::VaoDrawMemo (layoutHash / layoutAuxMasks) and its getter has no
|
||||
// live reader anywhere, so the handle arm retires it rather than moving it.
|
||||
return resolved;
|
||||
}
|
||||
#endif
|
||||
#if !MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
// Unreachable: with no legacy arm compiled MagmaPipeTrackHArmIsHandles is a compile-
|
||||
// time true, so the handle arm above always returns. Written out rather than left to
|
||||
// fall off the end so the function still has a return on every path a compiler sees.
|
||||
return GetOrCreateVertexInputState(vao, GetOrComputeHash(vao));
|
||||
#else
|
||||
// Per-draw fast path: the VAO carries a pointer to its resolved entry,
|
||||
// valid while its config version and the cache's eviction epoch both
|
||||
// match - no re-hash, no map lookup.
|
||||
@@ -212,7 +82,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vao.SetBackendAuxMemo(entry.layoutHash,
|
||||
PackVertexInputAuxMasks(entry.unsupportedAttribMask, entry.attributeLocationMask));
|
||||
return entry;
|
||||
#endif // MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
}
|
||||
|
||||
const VertexInputStateFactory::BackendVertexInputState& VertexInputStateFactory::GetOrCreateVertexInputState(
|
||||
@@ -238,46 +107,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
}
|
||||
|
||||
VkFormat sourceVkFormat =
|
||||
const VkFormat sourceVkFormat =
|
||||
ToVkVertexFormat(attr.Type, attr.Size, attr.Normalized, attr.IsInteger, attr.IsBgra, attr.IsLong);
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
// Gated on the SAME flag ToVkVertexFormat gates its 64-bit path on, and that is
|
||||
// load-bearing rather than belt-and-braces: the narrowing is only correct because the
|
||||
// shader's `dvec` input is a `vec` by the time the pipeline is built, and what
|
||||
// guarantees that is the flag being clear. It is clear on every backend today, and a
|
||||
// program with a 64-bit float vertex input is demoted WHOLE for the same reason even
|
||||
// where the device has native fp64 (ProgramSpirvTask::GenerateSpirv). With the flag
|
||||
// set, a dvec3/dvec4 would be declined by ToVkVertexFormat AND left 64-bit in the
|
||||
// module, so a float32 stream would be fed to a Float64 input.
|
||||
const Bool narrowFloat64Arrays =
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): with an active transport the answer is the
|
||||
// SERVER's own backend's - the client caps mirror is client memory (rule E).
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes;
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED && attr.Type == DataType::Float64 && narrowFloat64Arrays) {
|
||||
// No native 64-bit fetch here (see ToVkVertexFormat's Float64 case), but the
|
||||
// source bytes are ordinary IEEE-754 doubles and DemoteFloat64Pass has already
|
||||
// narrowed every dvec input to a vec, so the array is narrowed to match rather
|
||||
// than dropped. Mirrors what DirectGLES does for the same state.
|
||||
const VkFormat narrowedFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (narrowedFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(narrowedFormat)) {
|
||||
sourceVkFormat = narrowedFormat;
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u is a 64-bit (GL_DOUBLE) array; fetching it at "
|
||||
"float32 precision through format=%d (size=%d long=%s)",
|
||||
location, static_cast<Int>(narrowedFormat), attr.Size, attr.IsLong ? "true" : "false");
|
||||
}
|
||||
}
|
||||
if (sourceVkFormat == VK_FORMAT_UNDEFINED) {
|
||||
MGLOG_E_ONCE("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
MGLOG_E("Unsupported vertex attribute layout (location=%u, type=%s, size=%d): the array is "
|
||||
"enabled but cannot be mapped to a VkFormat",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
@@ -285,13 +118,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
VkFormat vkFormat = sourceVkFormat;
|
||||
if (conversion == VertexStreamConversion::None && !SupportsVertexBufferFormat(vkFormat)) {
|
||||
VertexStreamConversion conversion = VertexStreamConversion::None;
|
||||
if (!SupportsVertexBufferFormat(vkFormat)) {
|
||||
if (IsScaledIntegerVertexFormat(vkFormat)) {
|
||||
const VkFormat fallbackFormat = ToFloat32VertexFormat(attr.Size);
|
||||
if (fallbackFormat != VK_FORMAT_UNDEFINED && SupportsVertexBufferFormat(fallbackFormat)) {
|
||||
vkFormat = fallbackFormat;
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u format=%d lacks "
|
||||
MGLOG_W("Vertex attribute location=%u format=%d lacks "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT; using float32 stream format=%d "
|
||||
"(type=%s size=%d normalized=%s integer=%s)",
|
||||
location, static_cast<Int>(sourceVkFormat), static_cast<Int>(vkFormat),
|
||||
@@ -301,7 +135,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (conversion == VertexStreamConversion::None) {
|
||||
MGLOG_E_ONCE("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
MGLOG_E("Unsupported Vulkan vertex format (location=%u, format=%d, type=%s, size=%d): "
|
||||
"VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT is unavailable and no semantic fallback exists",
|
||||
location, static_cast<Int>(sourceVkFormat),
|
||||
MG_Util::ConvertDataTypeToString(attr.Type).c_str(), attr.Size);
|
||||
@@ -312,21 +146,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const SizeT attribByteSize = GetAttributeByteSize(attr.Type, attr.Size, attr.IsBgra);
|
||||
if (attribByteSize == 0) {
|
||||
MGLOG_E_ONCE("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
MGLOG_E("Vertex attribute with unknown component size (location=%u, type=%s): the array is "
|
||||
"enabled but cannot be sized",
|
||||
location, MG_Util::ConvertDataTypeToString(attr.Type).c_str());
|
||||
unsupportedAttribMask |= (1u << location);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbatim, zero included. The frontend already resolved a pointer call's
|
||||
// "tightly packed" stride 0 into the element size (see VertexAttribute::Stride),
|
||||
// so a zero here is the binding model's stride 0 - every vertex reads the same
|
||||
// element - which is exactly what a zero VkVertexInputBindingDescription::stride
|
||||
// means. Substituting the element size fetched a fresh element per vertex and ran
|
||||
// off the end of the buffer (KHR-GL43.vertex_attrib_binding.basic-input-case7/8).
|
||||
// Client-memory arrays cannot reach zero: they only exist on the pointer path.
|
||||
const Uint32 sourceStride = static_cast<Uint32>(attr.Stride);
|
||||
const Uint32 sourceStride =
|
||||
attr.Stride > 0 ? static_cast<Uint32>(attr.Stride) : static_cast<Uint32>(attribByteSize);
|
||||
const Bool packedAttribute = attr.Type == DataType::Int2101010Rev ||
|
||||
attr.Type == DataType::Uint2101010Rev;
|
||||
const SizeT requiredAlignment = packedAttribute ? attribByteSize : GetComponentSize(attr.Type);
|
||||
@@ -341,23 +169,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// unless VK_EXT_legacy_vertex_attributes is available, so deinterleave this one
|
||||
// attribute into a tightly packed transient stream without changing its format.
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
MGLOG_W_ONCE("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
MGLOG_W("Vertex attribute location=%u uses Vulkan-incompatible alignment "
|
||||
"(offset=%zu stride=%u required=%zu); using a tightly packed stream",
|
||||
location, attr.Offset, sourceStride, requiredAlignment);
|
||||
}
|
||||
|
||||
Uint32 stride = sourceStride;
|
||||
// A converted stream is tightly packed, so its stride is the converted element
|
||||
// size - unless the source stride is zero, which does not describe a packing at
|
||||
// all but "never advance". That survives the conversion unchanged: the draw path
|
||||
// converts exactly one element and every vertex reads it.
|
||||
if (sourceStride != 0) {
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32 ||
|
||||
conversion == VertexStreamConversion::Float64ToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
if (conversion == VertexStreamConversion::Repack) {
|
||||
stride = static_cast<Uint32>(attribByteSize);
|
||||
} else if (conversion == VertexStreamConversion::ScaledIntegerToFloat32) {
|
||||
stride = static_cast<Uint32>(attr.Size * static_cast<Int>(sizeof(Float)));
|
||||
}
|
||||
const VkVertexInputRate inputRate =
|
||||
(attr.Divisor == 0) ? VK_VERTEX_INPUT_RATE_VERTEX : VK_VERTEX_INPUT_RATE_INSTANCE;
|
||||
@@ -435,146 +256,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return entry;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool VertexInputStateFactory::BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask, BackendVertexInputState& out) const {
|
||||
out = BackendVertexInputState{};
|
||||
for (Uint32 location = 0; location < elements.AttributeCount; ++location) {
|
||||
const auto& attr = elements.Attributes[location];
|
||||
if (!attr.Enabled || !(activeMask & (1u << location))) continue;
|
||||
// P7 wave 2 package C, CONTRACT-P7 §3.2 `vertex-layout`: THE ONE STRING BECAME TWO
|
||||
// VERDICTS, and the split is by what the reason is ABOUT rather than by severity.
|
||||
//
|
||||
// Every reason here used to return false, and the single caller answered that with
|
||||
// one P7-marked MagmaWireFatal - an abort, bypassing Session::Fail, invisible
|
||||
// to the census gate and with no equivalent on the monolith arm at all. But the
|
||||
// reasons are not one kind of thing:
|
||||
//
|
||||
// * `buffer-window` is a statement about the RECORD: an enabled attribute the
|
||||
// program reads names a slot outside the window set_vertex_buffers published.
|
||||
// Nothing about the device or the format is involved; the two halves of the
|
||||
// protocol disagree about what crossed. It stays a named Fatal through the hook
|
||||
// (see the caller), and it is load-bearing rather than defensive - it is what
|
||||
// caught the Redmi VAO hash collision fixed in 376c04be.
|
||||
//
|
||||
// * every other reason is a statement about what VULKAN CAN EXPRESS on this
|
||||
// device: a GL type with no VkFormat, a format without
|
||||
// VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT, a component size we cannot compute, an
|
||||
// offset sum that does not fit. The monolith arm has had an answer for these
|
||||
// since it was written, and it is not an abort: mask the attribute out of the
|
||||
// vertex input state and carry on (the `unsupportedAttribMask |= ...; continue;`
|
||||
// sites in GetOrCreateVertexInputState above). The draw path then decides,
|
||||
// loudly and once, whether the masked attribute was one the program actually
|
||||
// reads. This arm now does exactly that, which is what "same observable as the
|
||||
// monolith lane" means for this row.
|
||||
//
|
||||
// THE COUNT: the contract says "the other six"; there are SEVEN return sites below
|
||||
// it, because `offset-overflow` was added after the audit's count and is an
|
||||
// arithmetic guard rather than a shape one. It is masked with the rest: an
|
||||
// attribute whose base offsets cannot be added is one Vulkan cannot fetch, the
|
||||
// monolith arm never computes that sum at all, and masking is strictly safer than
|
||||
// a Fatal for a number no application can reach on purpose. Recorded in
|
||||
// notes/p7/magma-c.md rather than silently reconciled.
|
||||
const auto describe = [&](const char* reason, VkFormat format) {
|
||||
MGLOG_E_ONCE("Magma wire vertex layout: %s location=%u type=%u size=%u normalized=%u integer=%u long=%u bgra=%u stride=%d offset=%llu format=%d bufferWindow=%u+%u activeMask=0x%x",
|
||||
reason, location, attr.Type, static_cast<Uint32>(attr.Size), static_cast<Uint32>(attr.Normalized),
|
||||
static_cast<Uint32>(attr.IsInteger), static_cast<Uint32>(attr.IsLong), static_cast<Uint32>(attr.IsBgra),
|
||||
attr.Stride, static_cast<unsigned long long>(attr.Offset), static_cast<Int>(format),
|
||||
state.VertexBufferStart, state.VertexBufferCount, activeMask);
|
||||
};
|
||||
// The protocol verdict: the caller turns a false into the named Fatal.
|
||||
const auto reject = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
return false;
|
||||
};
|
||||
// The device verdict: MGLOG_E_ONCE + mask + continue, the monolith arm's shape.
|
||||
// MGLOG_E_ONCE and not MGLOG_E, also the monolith arm's: an unmappable attribute is
|
||||
// a property of the VAO and the device, so it repeats every draw, and the per-draw
|
||||
// line is what made the old abort look preferable to whoever wrote it.
|
||||
const auto maskOut = [&](const char* reason, VkFormat format = VK_FORMAT_UNDEFINED) {
|
||||
describe(reason, format);
|
||||
out.unsupportedAttribMask |= 1u << location;
|
||||
};
|
||||
// set_vertex_buffers is flattened PER ATTRIBUTE (VertexInputEmit.h), not
|
||||
// indexed by the original ARB binding point in attr.BindingIndex.
|
||||
if (location < state.VertexBufferStart ||
|
||||
location - state.VertexBufferStart >= state.VertexBufferCount) return reject("buffer-window");
|
||||
const auto& buffer = state.VertexBuffers[location];
|
||||
const auto type = static_cast<DataType>(attr.Type);
|
||||
if (attr.Stride < 0 || attr.Size < 1 || attr.Size > 4) { maskOut("attribute-shape"); continue; }
|
||||
VkFormat format = ToVkVertexFormat(type, attr.Size, attr.Normalized, attr.IsInteger,
|
||||
attr.IsBgra, attr.IsLong);
|
||||
auto conversion = VertexStreamConversion::None;
|
||||
if (format == VK_FORMAT_UNDEFINED && type == DataType::Float64) {
|
||||
const auto* backend = MG_Remote::Server::ServerLoopInstance().Backend();
|
||||
if (backend && backend->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
// Exactly the monolith arm's answer for the same state: with native fp64
|
||||
// the module KEPT its 64-bit inputs (DemoteFloat64Pass did not run), so
|
||||
// narrowing the stream would feed float32 to a Float64 input - and the
|
||||
// monolith build therefore leaves sourceVkFormat UNDEFINED and falls into
|
||||
// its own mask-out below. Same place, same mask.
|
||||
maskOut("native-fp64-format");
|
||||
continue;
|
||||
}
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::Float64ToFloat32;
|
||||
}
|
||||
if (format == VK_FORMAT_UNDEFINED) { maskOut("format-map"); continue; }
|
||||
if (!SupportsVertexBufferFormat(format)) {
|
||||
if (!IsScaledIntegerVertexFormat(format)) { maskOut("native-format-feature", format); continue; }
|
||||
format = ToFloat32VertexFormat(attr.Size);
|
||||
conversion = VertexStreamConversion::ScaledIntegerToFloat32;
|
||||
if (!SupportsVertexBufferFormat(format)) { maskOut("converted-format-feature", format); continue; }
|
||||
}
|
||||
const SizeT elementSize = GetAttributeByteSize(type, attr.Size, attr.IsBgra);
|
||||
if (!elementSize) { maskOut("element-size", format); continue; }
|
||||
const Uint64 offset = attr.Offset + buffer.Offset;
|
||||
if (offset < attr.Offset) { maskOut("offset-overflow", format); continue; }
|
||||
const SizeT alignment = (type == DataType::Int2101010Rev || type == DataType::Uint2101010Rev)
|
||||
? elementSize : GetComponentSize(type);
|
||||
if (conversion == VertexStreamConversion::None && alignment > 1 &&
|
||||
(offset % alignment || static_cast<Uint32>(attr.Stride) % alignment))
|
||||
conversion = VertexStreamConversion::Repack;
|
||||
Uint32 stride = static_cast<Uint32>(attr.Stride);
|
||||
if (stride && conversion != VertexStreamConversion::None)
|
||||
stride = conversion == VertexStreamConversion::Repack ? static_cast<Uint32>(elementSize)
|
||||
: static_cast<Uint32>(attr.Size) * sizeof(Float);
|
||||
const Uint32 binding = static_cast<Uint32>(out.bindings.size());
|
||||
out.bindings.push_back({binding, stride,
|
||||
buffer.Divisor ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
out.attributes.push_back({location, binding, format, 0});
|
||||
out.bindingAttributeLocations.push_back(location);
|
||||
out.bindingBaseOffsets.push_back(static_cast<SizeT>(offset));
|
||||
out.bindingConversions.push_back(conversion);
|
||||
if (buffer.Divisor > 1) out.bindingDivisors.push_back({binding, buffer.Divisor});
|
||||
out.attributeLocationMask |= 1u << location;
|
||||
}
|
||||
// Native handles/offsets do not decide the pipeline layout. Include only the
|
||||
// resolved binding/attribute/divisor values, including a legal zero stride.
|
||||
Uint64 hash = XXH64(out.bindings.data(), out.bindings.size() * sizeof(out.bindings[0]), 0);
|
||||
hash = XXH64(out.attributes.data(), out.attributes.size() * sizeof(out.attributes[0]), hash);
|
||||
hash = XXH64(out.bindingDivisors.data(),
|
||||
out.bindingDivisors.size() * sizeof(out.bindingDivisors[0]), hash);
|
||||
// THE MASK IS PART OF THE LAYOUT NOW, for the same reason the monolith entry's hash
|
||||
// carries it (the XXH64_update over unsupportedAttribMask in GetOrCreateVertexInputState
|
||||
// above). Before this package the mask was always zero here, so leaving it out of the
|
||||
// hash was free; now two VAOs can produce the SAME bindings, attributes and divisors
|
||||
// and differ only in which enabled attribute was masked out, and a hash blind to that
|
||||
// would serve one of them the other's pipeline.
|
||||
out.layoutHash = XXH64(&out.unsupportedAttribMask, sizeof(out.unsupportedAttribMask), hash);
|
||||
out.state.vertexBindingDescriptionCount = static_cast<Uint32>(out.bindings.size());
|
||||
out.state.pVertexBindingDescriptions = out.bindings.data();
|
||||
out.state.vertexAttributeDescriptionCount = static_cast<Uint32>(out.attributes.size());
|
||||
out.state.pVertexAttributeDescriptions = out.attributes.data();
|
||||
if (!out.bindingDivisors.empty()) {
|
||||
out.divisorState.vertexBindingDivisorCount = static_cast<Uint32>(out.bindingDivisors.size());
|
||||
out.divisorState.pVertexBindingDivisors = out.bindingDivisors.data();
|
||||
out.state.pNext = &out.divisorState;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void VertexInputStateFactory::OnFrameBoundary() {
|
||||
++m_frameBoundaryCounter;
|
||||
|
||||
@@ -594,16 +275,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_frameBoundaryCounter - it->second->lastUsedFrameBoundary > kRetireAgeBoundaries) {
|
||||
it = m_cache.erase(it);
|
||||
// Invalidate every VAO's state-pointer memo: the erased node's
|
||||
// address may be reused by a future insert. Advance through the
|
||||
// process-wide source so the value stays unique across factory
|
||||
// instances (see the member comment). With no legacy arm the memos
|
||||
// live in this factory and die with it, so a per-instance bump is
|
||||
// enough - P2 D12.5.
|
||||
#if MOBILEGL_PIPE_LEGACY_MEMOS
|
||||
m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
// address may be reused by a future insert.
|
||||
++m_evictionEpoch;
|
||||
#endif
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
@@ -643,30 +316,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// for every R64 float format, so a native 64-bit vertex fetch is simply unavailable there
|
||||
// while shaderFloat64 is not. Both halves key off nothing but the attribute being long,
|
||||
// so they always agree without extra plumbing.
|
||||
//
|
||||
// ... as long as the shader half still runs. It does not when the backend has declared
|
||||
// no 64-bit vertex attribute support: DemoteFloat64Pass has already narrowed every
|
||||
// `dvec` input to a `vec` by then, so PackDoubleVertexInputsPass finds nothing to pack
|
||||
// and a UINT-formatted attribute would be fed to a float input - garbage with no
|
||||
// diagnostic anywhere. Declining here hands the attribute to the caller's
|
||||
// Float64ToFloat32 fallback instead, which narrows the source doubles to match the
|
||||
// demoted `vec` input - the same thing DirectGLES does for the same state. The
|
||||
// frontend RECORDS the format either way, so this gate is the only thing standing
|
||||
// between a legal glVertexAttribLFormat and a mismatched pipeline.
|
||||
if (
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P5c (hd, CONTRACT-P5C §3.7): see the narrowFloat64Arrays site above.
|
||||
MG_Config::Transport != MG_Config::TransportMode::Monolith
|
||||
? (MG_Remote::Server::ServerLoopInstance().Backend() == nullptr ||
|
||||
!MG_Remote::Server::ServerLoopInstance().Backend()
|
||||
->GetDynamicParameters()
|
||||
.SupportsFloat64VertexAttributes)
|
||||
:
|
||||
#endif
|
||||
MG_Backend::pActiveBackendObject == nullptr ||
|
||||
!MG_Backend::pActiveBackendObject->GetDynamicParameters().SupportsFloat64VertexAttributes) {
|
||||
return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
if (!isLong || isInteger || normalized) return VK_FORMAT_UNDEFINED;
|
||||
switch (size) {
|
||||
case 1: return VK_FORMAT_R32G32_UINT;
|
||||
@@ -847,17 +496,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_physicalDevice == VK_NULL_HANDLE || format == VK_FORMAT_UNDEFINED) {
|
||||
return false;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
|
||||
const auto found = m_wireVertexFormatSupport.find(format);
|
||||
if (found != m_wireVertexFormatSupport.end()) return found->second;
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
const Bool supported = (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
m_wireVertexFormatSupport.emplace(format, supported);
|
||||
return supported;
|
||||
}
|
||||
#endif
|
||||
VkFormatProperties properties{};
|
||||
vkGetPhysicalDeviceFormatProperties(m_physicalDevice, format, &properties);
|
||||
return (properties.bufferFeatures & VK_FORMAT_FEATURE_VERTEX_BUFFER_BIT) != 0;
|
||||
|
||||
@@ -7,19 +7,11 @@
|
||||
// End of Source File Header
|
||||
|
||||
#pragma once
|
||||
// MG_Pipe::MGPipeHandle for the P2 D12.5 memo table below. A header of constexpr constants,
|
||||
// so the pull build gains nothing from it.
|
||||
#include <MG_Pipe/MGPipeHandles.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Pipe/PipeApply.h>
|
||||
#endif
|
||||
|
||||
#include "Config.h"
|
||||
#include "MagmaPipeArms.h"
|
||||
#include "VertexInputStateBuilder.h"
|
||||
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
|
||||
#include <Includes.h>
|
||||
#include <atomic>
|
||||
#include "../VkIncludes.h"
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
@@ -31,9 +23,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
None = 0,
|
||||
Repack,
|
||||
ScaledIntegerToFloat32,
|
||||
// GL_DOUBLE source data narrowed to a tightly packed float32 stream: the fetch half
|
||||
// of the fp64 demotion the shader side already does unconditionally.
|
||||
Float64ToFloat32,
|
||||
};
|
||||
|
||||
struct BackendVertexInputState {
|
||||
@@ -78,18 +67,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
};
|
||||
};
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The mint is the RENDERER's (MagmaPipeIdentityTables), not a process-global and not
|
||||
// this factory's: VulkanRenderer::LookupVaoDrawMemo has to derive the same {slot, gen}
|
||||
// for the same VAO, and a table that outlived the context it was minted for would share
|
||||
// one reclamation clock across two live contexts (review v2 minor 4).
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice,
|
||||
MagmaPipeIdentityTables& identity):
|
||||
m_config(config), m_physicalDevice(physicalDevice), m_identity(&identity) {}
|
||||
#else
|
||||
VertexInputStateFactory(const VulkanRendererConfig& config, VkPhysicalDevice physicalDevice):
|
||||
m_config(config), m_physicalDevice(physicalDevice) {}
|
||||
#endif
|
||||
~VertexInputStateFactory() = default;
|
||||
VertexInputStateFactory(const VertexInputStateFactory&) = delete;
|
||||
|
||||
@@ -104,13 +83,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Memoized ComputeHash: reuses the VAO's cached hash while its config version
|
||||
// is unchanged. Use this on per-draw paths.
|
||||
HashType GetOrComputeHash(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// The VAO's content hash IF it has already been memoized, without computing one.
|
||||
// P2 D12.5: the three draw-path readers that used to ask the VAO object this
|
||||
// question ask the factory instead, because that is where the memo lives once the
|
||||
// frontend object stops carrying the backend's state.
|
||||
Bool TryGetMemoizedHash(const MG_State::GLState::VertexArrayObject& vao, Uint64& outHash) const;
|
||||
#endif
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(
|
||||
const MG_State::GLState::VertexArrayObject& vao, HashType hash);
|
||||
const BackendVertexInputState& GetOrCreateVertexInputState(const MG_State::GLState::VertexArrayObject& vao);
|
||||
@@ -129,13 +101,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// normal types, and 4 (one packed word) for the 2_10_10_10 types and GL_BGRA. Returns 0 for
|
||||
// an unknown/unsupported type.
|
||||
static SizeT GetAttributeByteSize(DataType type, Int size, Bool isBgra);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// A wire VAO is a layout record plus a separate per-attribute buffer window.
|
||||
// No frontend VAO identity, address or memo participates in this layout.
|
||||
Bool BuildWireVertexInput(const MG_Pipe::MGPipeVertexElementsRecord& elements,
|
||||
const MG_Pipe::MGPipeApplierState& state, Uint32 activeMask,
|
||||
BackendVertexInputState& out) const;
|
||||
#endif
|
||||
|
||||
private:
|
||||
static VkFormat ToVkVertexFormat(DataType type, Int size, Bool normalized, Bool isInteger, Bool isBgra = false,
|
||||
@@ -144,57 +109,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static VkFormat ToFloat32VertexFormat(Int componentCount);
|
||||
Bool SupportsVertexBufferFormat(VkFormat format) const;
|
||||
|
||||
#if MOBILEGL_PIPE_PUSH
|
||||
// ---- P2 D12.5: the backend's memos, off the frontend VAO and into the backend ----
|
||||
//
|
||||
// The two facts that used to live as `mutable` fields on VertexArrayObject
|
||||
// (Get/SetBackendHashMemo and Get/SetBackendStateMemo), kept here instead, keyed on
|
||||
// the VAO's {slot, gen} and guarded by exactly the same config version. A frontend
|
||||
// state object holding the backend's raw pointer is what P2 retires: under split the
|
||||
// backend is in another process and its cache entry has no address a client could
|
||||
// store, so the memo has to live on the side that owns the pointee.
|
||||
//
|
||||
// The AUX memo is not carried over: its two words moved into VaoDrawMemo::layoutHash
|
||||
// and layoutAuxMasks long ago and its getter has no live reader anywhere in the tree,
|
||||
// so the handle arm simply stops writing it (D12.5 says delete rather than move).
|
||||
struct VaoBackendMemos {
|
||||
// Whose memos these are. The identity table can recycle a slot for a different
|
||||
// VAO under LRU pressure, and the handle compare - Gen included - is what says
|
||||
// the contents are this object's and not its predecessor's.
|
||||
MG_Pipe::MGPipeHandle Owner = MG_Pipe::kMGPipeNullHandle;
|
||||
Uint64 Hash = 0;
|
||||
Uint32 HashConfigVersion = ~0u;
|
||||
const void* State = nullptr;
|
||||
Uint64 StateEpoch = 0;
|
||||
Uint32 StateConfigVersion = ~0u;
|
||||
};
|
||||
// Grow-on-demand (D12.4), one entry per slot the renderer's mint has ever handed
|
||||
// out, and NO CAPACITY: these two memos had none before this package either - they
|
||||
// were unbounded mutable fields on the VertexArrayObject itself - and re-introducing
|
||||
// eviction here is what review v2 rejected. MagmaPipeSlotTable grows in chunks so an
|
||||
// entry reference stays valid across the nested GetOrCreateVertexInputState call.
|
||||
// 48 B per live VAO, reclaimed with the slot when the object goes idle.
|
||||
mutable MagmaPipeSlotTable<VaoBackendMemos> m_vaoMemos;
|
||||
// The renderer's {slot, gen} mint (see the constructor). Never null under push.
|
||||
MagmaPipeIdentityTables* m_identity = nullptr;
|
||||
// The entry belonging to `vao`, claimed (and cleared) if the slot currently holds
|
||||
// someone else's.
|
||||
VaoBackendMemos& MemosFor(const MG_State::GLState::VertexArrayObject& vao) const;
|
||||
#endif
|
||||
|
||||
const VulkanRendererConfig& m_config;
|
||||
VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Physical-device format capabilities never change during this factory's
|
||||
// lifetime. The wire path rebuilds layouts without the legacy VAO memo.
|
||||
mutable UnorderedMap<VkFormat, Bool> m_wireVertexFormatSupport;
|
||||
#endif
|
||||
// Values are heap-allocated: UnorderedMap is open-addressing, so INSERT
|
||||
// invalidates references to stored values - and so does ERASE, which shifts
|
||||
// the rest of the probe cluster into the hole and therefore moves entries
|
||||
// other than the erased one. The draw path (and the VAOs' state-pointer
|
||||
// memos) hold entry pointers across both; only the unique_ptr cell moves,
|
||||
// never the pointee.
|
||||
// Values are heap-allocated: FastSTL::unordered_map is open-addressing,
|
||||
// so INSERT invalidates references to stored values. The draw path (and
|
||||
// the VAOs' state-pointer memos) hold entry pointers across inserts;
|
||||
// only the unique_ptr cell moves, never the pointee.
|
||||
UnorderedMap<HashType, UniquePtr<BackendVertexInputState>> m_cache;
|
||||
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
|
||||
Uint64 m_frameBoundaryCounter = 0;
|
||||
@@ -203,26 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// construction); a memo is honored only while its recorded epoch
|
||||
// matches, so an evicted entry can never be dereferenced through a
|
||||
// stale memo.
|
||||
//
|
||||
// Drawn from a process-wide source, never a per-instance counter: the VAO
|
||||
// memos outlive this factory (they live on the frontend context's VAOs, the renderer
|
||||
// is destroyed and recreated on EGL surface release/re-create), so a fresh
|
||||
// factory restarting at a dead factory's epoch value would honor its
|
||||
// dangling entry pointers. The constructor takes a value strictly greater
|
||||
// than anything a predecessor ever stamped, so a dead factory's memo can
|
||||
// never compare equal here - the same never-reused idiom as the lifetime ids.
|
||||
// Single-threaded like the rest of the factory (renderer-thread only).
|
||||
//
|
||||
// P2 D12.5: the process-wide source is the LEGACY arm's need. It exists because the
|
||||
// memos live on the frontend VAOs and therefore outlive the factory. The handle arm's
|
||||
// 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 std::atomic<Uint64> s_evictionEpochSource{0};
|
||||
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
|
||||
#else
|
||||
Uint64 m_evictionEpoch = 1;
|
||||
#endif
|
||||
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
|
||||
static inline XXH64_state_t* m_hashState = XXH64_createState();
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,10 +13,6 @@
|
||||
#include "../VkIncludes.h"
|
||||
#include <Includes.h>
|
||||
#include <vk_mem_alloc.h>
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include "MG_Pipe/MGPipeTypes.h"
|
||||
#include <unordered_map>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
enum class BufferKind : Uint8 {
|
||||
@@ -103,68 +99,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool Initialize(const VkBufferManagerInitInfo& initInfo);
|
||||
void Shutdown();
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Registered before caps publication; the initialized renderer owns the storage.
|
||||
static void RegisterWireResourceOps();
|
||||
// Transport resources are owned by their complete wire handle, never by a
|
||||
// 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);
|
||||
// P7 A.1, the sub-word half of the one above: the same copy for a window whose start
|
||||
// or end does not land on a four-byte boundary. The read of the APPLICATION's store is
|
||||
// rounded OUT to whole words and clamped to the store's end, so the widened window can
|
||||
// never touch a byte the application does not own; it lands in a transient staging
|
||||
// slice, and only the staging -> dst shift is sub-word - inside our own arena, where an
|
||||
// unaligned region cannot alias anything else. vkCmdCopyBuffer places no alignment rule
|
||||
// on a region's offsets or size (unlike vkCmdUpdateBuffer / vkCmdFillBuffer), so the
|
||||
// shift is a plain legal copy. `dstSkip` is the byte inside `dst` the window starts at.
|
||||
// This retires `uniform-buffer-byte-tail`.
|
||||
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
|
||||
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
|
||||
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
// P8-D: whether a shader write was marked on this store since the last indirect draw
|
||||
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
|
||||
// barrier before the draw when this answers true. Transfer writes need no mark: their own
|
||||
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
|
||||
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
|
||||
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
|
||||
// the attribution base a caller reads before and after its own ReadWireBuffer.
|
||||
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
|
||||
|
||||
// Resource-op entry points. All run on the server apply owner.
|
||||
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
|
||||
void RespecifyWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc,
|
||||
const void* initialBytes);
|
||||
void WriteWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
|
||||
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
|
||||
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
// P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer
|
||||
// `res`'s store - a VkBuffer on memory imported through
|
||||
// VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every
|
||||
// wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers)
|
||||
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
|
||||
// imported (the T2 store is untouched and the client is DECLINED).
|
||||
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
|
||||
// The POST self-test of the sustained-lock pattern on this device (see the definition).
|
||||
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
|
||||
#endif
|
||||
|
||||
// Recreate all per-frame transient arenas
|
||||
Bool RecreateTransientArenas(Uint32 frameCount);
|
||||
void BeginFrame(Uint32 frameIndex);
|
||||
// Drains every frame slot's deferred buffer/resource releases. Only valid when
|
||||
// the caller has proven every queue submission complete; used by the present-less
|
||||
// frame-boundary drain. Deliberately does NOT touch the transient arena's parked
|
||||
// superseded blocks: those are still named by this frame's slices (see the
|
||||
// definition), and only a frame rewind retires them.
|
||||
// Drains every frame slot's deferred buffer/resource releases (and the
|
||||
// transient arena's parked superseded blocks). Only valid when the
|
||||
// caller has proven every queue submission complete; used by the
|
||||
// present-less frame-boundary drain.
|
||||
void CollectAllDeferredReleases();
|
||||
// All previously submitted GPU work has completed (vkDeviceWaitIdle).
|
||||
void NotifyDeviceIdle();
|
||||
@@ -178,27 +119,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool UploadTransient(BufferKind kind, Uint32 frameIndex, const void* data, VkDeviceSize size,
|
||||
VkDeviceSize alignment, BufferSlice& outSlice);
|
||||
|
||||
// The descriptor a shader storage block gets when the program declares it and the
|
||||
// application bound no buffer at its GL binding point. GL 4.6 core 7.8 makes that a
|
||||
// legal state - the block simply has no store, so reads are undefined and writes go
|
||||
// nowhere - whereas Vulkan has no such thing as an unwritten descriptor, so something
|
||||
// real has to sit in the set or the whole draw/dispatch is lost. One zero-filled
|
||||
// buffer, created once and shared by every unbound binding: bindings that are only
|
||||
// declared (the case this exists for) never touch it, and one that is actually read
|
||||
// sees zeros, which is inside GL's "undefined". robustBufferAccess bounds anything
|
||||
// that indexes past it.
|
||||
BufferSlice AcquireUnboundStorageDescriptor();
|
||||
|
||||
// The store a texel-buffer descriptor - `samplerBuffer` or `imageBuffer` - gets when the
|
||||
// unit the program's uniform names has no buffer texture on it, or the buffer texture on
|
||||
// it has no GL buffer attached. Both are legal GL states that make a fetch return
|
||||
// undefined values (GL 4.6 core 8.9: a buffer texture with no attached buffer object is
|
||||
// incomplete, and sampling an incomplete texture is undefined - not a lost draw), and both
|
||||
// used to take the whole draw or dispatch with them. The VIEW over this - one per format,
|
||||
// and the descriptor is a VkBufferView, not a buffer - is built by
|
||||
// UniformManager::AcquireUnboundTexelBufferView.
|
||||
BufferSlice AcquireUnboundTexelBufferDescriptor();
|
||||
|
||||
// Draw-time acquire for resident (device-storage) buffers: ensures the
|
||||
// resource exists and is fully uploaded, marks it used this frame.
|
||||
Bool AcquireResidentSlice(BufferKind kind, const SharedPtr<MG_State::GLState::BufferObject>& bufferObject,
|
||||
@@ -218,7 +138,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Immediate ops, dispatched from the frontend BufferBackendOps table.
|
||||
void OnRespecify(MG_State::GLState::BufferObject& bufferObject);
|
||||
void OnSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, SizeT size);
|
||||
void OnResidentSubData(MG_State::GLState::BufferObject& bufferObject, SizeT offset, DataPtr data);
|
||||
void OnFlushMappedRange(MG_State::GLState::BufferObject& bufferObject, Range1D range,
|
||||
Flags<BufferMappingAccessBit> appAccess);
|
||||
void OnResourceDestroyed(SharedPtr<MG_State::GLState::BackendBufferResource>&& resource);
|
||||
@@ -229,13 +148,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// back, and none was persistently mapped, in between - so a memo of resolved
|
||||
// slices needs no per-buffer re-check. See AcquirePersistentMap for the mapping half.
|
||||
Uint64 GetSliceEpochCounter() const { return m_sliceEpochCounter; }
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// Bumped every time this manager destroys a WIRE store's VkBuffer (see
|
||||
// m_wireStoreDestroyEpoch). Unchanged since a memo was taken means no VkBuffer handle
|
||||
// that memo names can have been freed and re-minted in between, which is the one
|
||||
// fact a handle-keyed memo of wire descriptors needs and cannot read off the handle.
|
||||
Uint64 GetWireStoreDestroyEpoch() const { return m_wireStoreDestroyEpoch; }
|
||||
#endif
|
||||
// Highest frame serial whose GPU work is known complete; serials at or
|
||||
// below it may be considered signaled. Drives IsResourceBusy and the
|
||||
// backend GL fence objects.
|
||||
@@ -243,135 +155,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Busy = potentially referenced by GPU work that has not been fenced yet
|
||||
// (including commands recorded for the current, unsubmitted frame).
|
||||
Bool IsResourceBusy(const VkBufferResource& resource) const;
|
||||
// Hand the manager a buffer to destroy once the frame that recorded commands
|
||||
// naming it has completed. The renderer's blit path needs a device-local
|
||||
// scratch store for one recorded operation, which cannot be a stack local:
|
||||
// the glBlitFramebuffer that records the commands returns long before the
|
||||
// command buffer is submitted.
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
|
||||
private:
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
struct WireBufferResource {
|
||||
VkBufferObject buffer;
|
||||
Uint64 size = 0;
|
||||
Uint64 lastUseSerial = 0;
|
||||
// P7 wave 2 package B3: the submission expected to carry the most recent GPU use
|
||||
// of this buffer - the busy predicate's DEFENCE term, not its guarantee (see
|
||||
// WriteWireBuffer). IsSubmitIndexComplete polls the real fence and reports an
|
||||
// unsubmitted index as incomplete, but the stamp is taken before the draw is
|
||||
// recorded and a mid-draw flush can submit it without the draw.
|
||||
Uint64 lastUseSubmitIndex = 0;
|
||||
Bool gpuWritesPending = false;
|
||||
// Only ranges actually submitted by resource_subdata are covered. No
|
||||
// shadow is retained: flush cannot replay stale bytes over GPU writes.
|
||||
Vector<Range1D> stagedCoverage;
|
||||
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
|
||||
// no bytes (the client reads its own pages once the wait below it is done).
|
||||
Bool imported = false;
|
||||
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
|
||||
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
|
||||
Bool indirectReadBarrierPending = false;
|
||||
};
|
||||
// P8-D: see GetWireHostWaitCount.
|
||||
Uint64 m_wireHostWaits = 0;
|
||||
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
|
||||
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
|
||||
}
|
||||
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
|
||||
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
|
||||
// P11 B2: the serial a T0 store's release waits behind (never "destroy at once").
|
||||
Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const;
|
||||
|
||||
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
|
||||
// BOUNDARY.
|
||||
//
|
||||
// RespecifyWireBuffer - which is every glBufferData that crosses the wire - orphans the
|
||||
// old store unconditionally, because unlike OnRespecify it has no shadow to upload in
|
||||
// place from and no cheap way to know the client is re-sending the same size. The orphan
|
||||
// is legitimate (M1 measured the conditional-orphan mirror: 6701 live against 6702).
|
||||
// What was missing is the RECLAIM. DeferRelease parks into m_deferredBufferReleases,
|
||||
// whose only sweep is CollectDeferredReleases from a frame boundary, and on a pbuffer
|
||||
// replay the server sees ONE present record for the whole run - so on
|
||||
// minecraft-1.21.4-fabric-iris-bsl-esc-menu-854 the buckets held 25,923 dead stores
|
||||
// against 28 live wire buffers, one memfd mapping each, and the server died in scudo's
|
||||
// secondary allocator.
|
||||
//
|
||||
// So a wire store is parked HERE instead, with the facts that say when it is dead, and
|
||||
// THE DEFER PATH ITSELF RECLAIMS - every park sweeps, and the frame boundary is only one
|
||||
// more sweep point (CollectDeferredReleases), never the one this depends on:
|
||||
//
|
||||
// * `lastUseSerial == 0` - read BEFORE RespecifyWireBuffer zeroes it. Zero means no
|
||||
// GPU command has named the store since it was minted or since the last host-access
|
||||
// wait proved every recorded command complete (WaitForWireBufferHostAccess); every
|
||||
// path that hands the store to the GPU stamps m_frameSerial, which starts at 1. Such
|
||||
// a store is destroyed at the park and never enters the list.
|
||||
// * `submitIndex` - the renderer's GetSyncPointSubmitIndex() at park time, which is by
|
||||
// construction the LAST submission that can name this store: every command that
|
||||
// names it was recorded before the park (the record left behind a bumped slice
|
||||
// epoch, so no memo can hand it out again), and a recorded command is either already
|
||||
// submitted (<= m_submitCounter) or in the batch that becomes m_submitCounter + 1.
|
||||
// IsSubmitIndexComplete(submitIndex) is a fence observation, so this is the gate
|
||||
// that empties the set MID-FRAME - the shape CollectWireObjects already uses for
|
||||
// the wire image/view tables.
|
||||
// * THE RENDERER IS IDLE - IsSubmitIndexComplete(GetSyncPointSubmitIndex()): nothing
|
||||
// is recorded and every submission has retired. Then every parked store is dead,
|
||||
// including one tagged for a batch that was abandoned rather than submitted (a
|
||||
// minimized Present drops its recording), whose own index may never complete.
|
||||
//
|
||||
// THE FRAME-SERIAL FLOOR IS DELIBERATELY NOT A PROOF HERE. It cannot move inside a
|
||||
// frame (NotifyFrameSerialComplete refuses the current serial), so it frees nothing in
|
||||
// the one-present replay this exists for; the submit index above is the fact that CAN
|
||||
// move mid-frame, and it is a fence observation rather than a count.
|
||||
//
|
||||
// AND EVERY DESTROY HERE HAPPENS MID-FRAME, which the memos above this manager were not
|
||||
// written for. UniformManager's descriptor memos are keyed on the VkBuffer HANDLE, and
|
||||
// before M2 a wire store only ever died at the same boundary that clears those memos
|
||||
// (UniformManager::BeginFrame, from Present or a drain). A store destroyed here can
|
||||
// have its handle value re-minted by the next Create - a heap pointer under lavapipe -
|
||||
// while a memo still maps that handle to a descriptor set baked to the dead store's
|
||||
// memory: silent wrong bytes, no Fatal (ID-P7-43). m_wireStoreDestroyEpoch is the
|
||||
// fact the memos fold in: every wire-store destroy bumps it, so a memo taken before
|
||||
// the destroy cannot match after it. Deliberately NOT m_sliceEpochCounter, which every
|
||||
// WriteWireBuffer bumps and which would defeat the memo on every glBufferSubData.
|
||||
struct DeferredWireRelease {
|
||||
VkBufferObject buffer;
|
||||
Uint64 lastUseSerial = 0;
|
||||
Uint64 submitIndex = 0;
|
||||
// Cached: GetSize() is gone once the object is destroyed, and the watermark's
|
||||
// running total has to stay exact as entries leave.
|
||||
Uint64 bytes = 0;
|
||||
};
|
||||
// Park a wire store, sweep, then hold the watermark. `lastUseSerial` is the releasing
|
||||
// resource's, captured before the caller resets it.
|
||||
void DeferWireRelease(VkBufferObject&& buffer, Uint64 lastUseSerial);
|
||||
// Destroy every parked store proven dead by the rules above. Returns how many.
|
||||
SizeT SweepDeferredWireReleases();
|
||||
// MOBILEGL_IPC_WIRE_DEFERRED_MB (Config.h has the semantics): when the parked bytes
|
||||
// still exceed the budget after a sweep, take the sync point WaitForWireBufferHostAccess
|
||||
// takes - flush what is recorded, wait for it - and sweep again, which retires every
|
||||
// parked store, because none can be tagged past the sync point it just waited out.
|
||||
void EnforceWireDeferredWatermark();
|
||||
// ...and the same sync point when more than this many stores are parked, whatever their
|
||||
// bytes (see the definition for the measurement). Not a knob: it bounds an object count
|
||||
// the byte budget cannot see, and MOBILEGL_IPC_WIRE_DEFERRED_MB=0 disables it too.
|
||||
static constexpr SizeT kWireDeferredCountCeiling = 1024;
|
||||
// Teardown: the caller has proven the device idle (Shutdown / RecreateTransientArenas).
|
||||
void DestroyAllDeferredWireReleases();
|
||||
// THE one Fatal{ResourceUnavailable, "buffer-write-sync"} site (rule I: no second abort
|
||||
// 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.
|
||||
void NoteWireStorePeaks();
|
||||
void PublishWireReclaimGauges();
|
||||
#endif
|
||||
Bool InitializeTransientArenas();
|
||||
static VkBufferUsageFlags GetVkBufferUsage(BufferKind kind);
|
||||
VkBufferResource* GetOrCreateResource(const SharedPtr<MG_State::GLState::BufferObject>& bufferObject);
|
||||
@@ -382,11 +167,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Bool SwapStorageAndUploadAll(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject);
|
||||
// Record a staging-slice copy into the resident storage, ordered against
|
||||
// in-flight and already-recorded GPU work.
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, const void* data,
|
||||
Bool StagedRangeCopy(VkBufferResource& resource, MG_State::GLState::BufferObject& bufferObject,
|
||||
SizeT offset, SizeT size);
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
Bool StagedWireRangeCopy(WireBufferResource& resource, const void* data, SizeT offset, SizeT size);
|
||||
#endif
|
||||
void DeferRelease(VkBufferObject&& buffer);
|
||||
void CollectDeferredReleases(Uint32 frameIndex);
|
||||
void DestroyAllDeferredReleases();
|
||||
void TrackLiveResource(const SharedPtr<VkBufferResource>& resource);
|
||||
@@ -396,35 +179,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkBufferManagerInitInfo m_initInfo{};
|
||||
BufferArena m_transientUploadArena;
|
||||
// See AcquireUnboundStorageDescriptor. Lazily created, never re-created, torn down
|
||||
// with the manager.
|
||||
VkBufferObject m_unboundStorageBuffer;
|
||||
// See AcquireUnboundTexelBufferDescriptor. Same lifetime rules.
|
||||
VkBufferObject m_unboundTexelBuffer;
|
||||
IBufferCopyCommandProvider* m_copyProvider = nullptr;
|
||||
Vector<Vector<VkBufferObject>> m_deferredBufferReleases;
|
||||
Vector<Vector<SharedPtr<VkBufferResource>>> m_deferredResourceReleases;
|
||||
Vector<WeakPtr<VkBufferResource>> m_liveResources;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
std::unordered_map<Uint64, WireBufferResource> m_wireBuffers;
|
||||
// See DeferredWireRelease. ONE FLAT LIST rather than the per-frame-slot buckets above:
|
||||
// the whole point is that these entries do not wait for a frame slot to come round.
|
||||
Vector<DeferredWireRelease> m_deferredWireReleases;
|
||||
// Bytes currently parked in that list, kept exact so the watermark needs no walk.
|
||||
Uint64 m_deferredWireBytes = 0;
|
||||
// Live VkBuffers this arm owns: the stores held by m_wireBuffers plus the parked ones.
|
||||
// Maintained rather than counted, because the publish runs on every park.
|
||||
Uint64 m_wireStoreCount = 0;
|
||||
// Run maxima of the two numbers above and the watermark's sync count, for the gauges.
|
||||
Uint64 m_wireStoreCountPeak = 0;
|
||||
Uint64 m_deferredWireBytesPeak = 0;
|
||||
Uint64 m_wireDeferredSyncs = 0;
|
||||
// See GetWireStoreDestroyEpoch and the DeferredWireRelease comment. Bumped on every
|
||||
// path that destroys a wire store's VkBuffer, and never reset (not even by Shutdown),
|
||||
// for m_sliceEpochCounter's reason: a memo taken before a re-initialize must not match
|
||||
// a handle minted after it.
|
||||
Uint64 m_wireStoreDestroyEpoch = 0;
|
||||
#endif
|
||||
// Size m_liveResources had just after the last sweep; the next sweep waits for it to double.
|
||||
SizeT m_liveResourcesLastPruned = 0;
|
||||
Uint32 m_currentFrameIndex = 0;
|
||||
|
||||
@@ -8,45 +8,13 @@
|
||||
|
||||
#include "VkBufferObject.h"
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
#include <MG_Remote/Transport/AdoptT0.h>
|
||||
#endif
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
|
||||
m_device = other.m_device;
|
||||
m_externalMemory = other.m_externalMemory;
|
||||
m_externalAhb = other.m_externalAhb;
|
||||
m_externalCoherent = other.m_externalCoherent;
|
||||
other.m_device = VK_NULL_HANDLE;
|
||||
other.m_externalMemory = VK_NULL_HANDLE;
|
||||
other.m_externalAhb = nullptr;
|
||||
other.m_externalCoherent = true;
|
||||
}
|
||||
|
||||
void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
|
||||
void* mapped, Bool coherent, void* ahb) {
|
||||
Destroy();
|
||||
m_device = device;
|
||||
m_buffer = buffer;
|
||||
m_externalMemory = memory;
|
||||
m_mappedData = mapped;
|
||||
m_size = size;
|
||||
m_externalCoherent = coherent;
|
||||
m_externalAhb = ahb;
|
||||
}
|
||||
#endif
|
||||
|
||||
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
|
||||
m_allocator = other.m_allocator;
|
||||
m_buffer = other.m_buffer;
|
||||
m_allocation = other.m_allocation;
|
||||
m_mappedData = other.m_mappedData;
|
||||
m_size = other.m_size;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MoveExternalFrom(other);
|
||||
#endif
|
||||
|
||||
other.m_allocator = nullptr;
|
||||
other.m_buffer = VK_NULL_HANDLE;
|
||||
@@ -67,9 +35,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_allocation = other.m_allocation;
|
||||
m_mappedData = other.m_mappedData;
|
||||
m_size = other.m_size;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
MoveExternalFrom(other);
|
||||
#endif
|
||||
|
||||
other.m_allocator = nullptr;
|
||||
other.m_buffer = VK_NULL_HANDLE;
|
||||
@@ -111,7 +76,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const VkResult result =
|
||||
vmaCreateBuffer(m_allocator, &bufferInfo, &allocationInfo, &m_buffer, &m_allocation, nullptr);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
MGLOG_E("VkBufferObject::Create failed: vmaCreateBuffer returned %d", result);
|
||||
m_allocator = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_allocation = nullptr;
|
||||
@@ -124,24 +89,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferObject::Destroy() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
// P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's
|
||||
// deferred release, i.e. once no submission can still name it.
|
||||
if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory);
|
||||
if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr);
|
||||
vkFreeMemory(m_device, m_externalMemory, nullptr);
|
||||
MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb);
|
||||
m_mappedData = nullptr;
|
||||
m_buffer = VK_NULL_HANDLE;
|
||||
m_externalMemory = VK_NULL_HANDLE;
|
||||
m_externalAhb = nullptr;
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_externalCoherent = true;
|
||||
m_size = 0;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
Unmap();
|
||||
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
|
||||
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
|
||||
@@ -161,7 +108,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkResult mapResult = vmaMapMemory(m_allocator, m_allocation, &m_mappedData);
|
||||
if (mapResult != VK_SUCCESS || m_mappedData == nullptr) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
MGLOG_E("VkBufferObject::Map failed: vmaMapMemory returned %d", mapResult);
|
||||
m_mappedData = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -170,10 +117,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
void VkBufferObject::Unmap() {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// An imported store stays mapped for its life (Destroy unmaps it).
|
||||
if (m_externalMemory != VK_NULL_HANDLE) return;
|
||||
#endif
|
||||
if (!IsValid() || m_mappedData == nullptr) {
|
||||
m_mappedData = nullptr;
|
||||
return;
|
||||
@@ -195,24 +138,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const Bool wasMapped = IsMapped();
|
||||
void* mapped = wasMapped ? m_mappedData : Map();
|
||||
if (mapped == nullptr) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: unable to map buffer");
|
||||
MGLOG_E("VkBufferObject::Upload failed: unable to map buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
if (m_externalCoherent) return true;
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = m_externalMemory;
|
||||
range.offset = 0;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
|
||||
if (flushResult != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
MGLOG_E("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
|
||||
if (!wasMapped) {
|
||||
Unmap();
|
||||
}
|
||||
@@ -234,20 +167,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (resolvedSize == 0) {
|
||||
return true;
|
||||
}
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) {
|
||||
if (m_externalCoherent) return true;
|
||||
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
|
||||
range.memory = m_externalMemory;
|
||||
range.offset = 0;
|
||||
range.size = VK_WHOLE_SIZE;
|
||||
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
|
||||
}
|
||||
#endif
|
||||
|
||||
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
|
||||
if (result != VK_SUCCESS) {
|
||||
MGLOG_E_ONCE("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
MGLOG_E("VkBufferObject::Invalidate failed: vmaInvalidateAllocation returned %d", result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -64,25 +64,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
void* GetMappedData() const { return m_mappedData; }
|
||||
Bool IsMapped() const { return m_mappedData != nullptr; }
|
||||
Bool IsValid() const {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
|
||||
#endif
|
||||
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
|
||||
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
|
||||
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
|
||||
// like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and
|
||||
// the memory and drops `ahb` - which is why the store goes through the wire arm's
|
||||
// serial-gated deferred release like every other: the AHardwareBuffer is let go only once
|
||||
// no submission can still name it. `coherent` = the memory type is HOST_COHERENT.
|
||||
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
|
||||
void* mapped, Bool coherent, void* ahb);
|
||||
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
|
||||
#endif
|
||||
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
|
||||
|
||||
private:
|
||||
VmaAllocator m_allocator = nullptr;
|
||||
@@ -90,12 +72,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VmaAllocation m_allocation = nullptr;
|
||||
void* m_mappedData = nullptr;
|
||||
VkDeviceSize m_size = 0;
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkDevice m_device = VK_NULL_HANDLE;
|
||||
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
|
||||
void* m_externalAhb = nullptr;
|
||||
Bool m_externalCoherent = true;
|
||||
void MoveExternalFrom(VkBufferObject& other);
|
||||
#endif
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -8,12 +8,7 @@
|
||||
|
||||
#include "VkClearManager.h"
|
||||
|
||||
// For the shared ResolveAttachmentLayerCount (and the ToVulkanLevelExtent it is built on): the
|
||||
// clear key's layer span has to be the same one the render pass builds its attachment view from.
|
||||
#include "VkTextureManager.h"
|
||||
|
||||
#include "MG_State/GLState/Core.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToStr/TextureEnumConverter.h"
|
||||
|
||||
@@ -55,7 +50,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (payload.colorEncoding != ClearColorEncoding::Float) return;
|
||||
// With GL_FRAMEBUFFER_SRGB enabled GL performs the encoding itself, so the driver doing it
|
||||
// is exactly right and there is nothing to undo.
|
||||
if (MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb)) return;
|
||||
if (ResolveSrgbAttachmentWriteFormat(destinationFormat, false) == destinationFormat) return;
|
||||
|
||||
// sRGB -> linear (GL 4.6 core 8.24), applied to the colour channels only: alpha is stored
|
||||
@@ -105,13 +100,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return ResolveAttachmentBaseArrayLayer(uploadTarget);
|
||||
}
|
||||
|
||||
// ResolveAttachmentLayerCount used to be duplicated here, reading attachment.GetSize().z()
|
||||
// raw - no ToVulkanLevelExtent remap for a 1D array, no six-faces arm for a cube map. That is
|
||||
// not a cosmetic difference: the count below is not key-only, it is written straight into
|
||||
// VkImageSubresourceRange::layerCount by MaterializePendingClearForTexture, which then POPS
|
||||
// the entry - so a layered cube map's glClear reached one face and the other five were lost
|
||||
// for good, while the very same queued clear cleared all six through the render pass's
|
||||
// LOAD_OP_CLEAR. The helper now lives once, in VkTextureManager.h beside ToVulkanLevelExtent.
|
||||
static Uint32 ResolveAttachmentLayerCount(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
|
||||
static const MG_State::GLState::FramebufferAttachmentObject* GetClearableAttachment(
|
||||
const MG_State::GLState::FramebufferObject& drawFbo, FramebufferAttachmentType attachmentType) {
|
||||
@@ -127,30 +122,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return &attachment;
|
||||
}
|
||||
|
||||
// The texture a pending clear is actually ABOUT. A clear issued through a GL texture view
|
||||
// (ARB_texture_view) targets the storage it views, so it must queue against - and be found
|
||||
// by - the storage texture; keying it on the view instead left the clear invisible to every
|
||||
// materialisation done through the parent's name (and vice versa), so the image stayed in
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED and the readback was dropped as unreadable.
|
||||
static MG_State::GLState::ITextureObject* ClearStorageTextureOf(MG_State::GLState::ITextureObject* texture) {
|
||||
if (texture == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
return storageOwner ? storageOwner.get() : texture;
|
||||
}
|
||||
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel,
|
||||
PendingClearKey VkClearManager::MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel,
|
||||
Uint32 baseArrayLayer, Uint32 layerCount) {
|
||||
MG_State::GLState::ITextureObject* texture = ClearStorageTextureOf(rawTexture);
|
||||
if (rawTexture != nullptr && texture != rawTexture) {
|
||||
// The caller named a level and a layer of the VIEW; the key describes the STORAGE, so
|
||||
// both have to be shifted into its numbering (GL 4.6 core 8.18). Without this a clear
|
||||
// of a view's level 0 would collide with a clear of the storage's level 0 even when
|
||||
// the view opened onto level 1.
|
||||
mipLevel += static_cast<Uint32>(rawTexture->GetViewMinLevel());
|
||||
baseArrayLayer += static_cast<Uint32>(rawTexture->GetViewMinLayer());
|
||||
}
|
||||
return PendingClearKey {
|
||||
.texture = texture,
|
||||
.textureLifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -184,15 +157,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
TextureIdentity VkClearManager::MakeTextureIdentity(MG_State::GLState::ITextureObject* texture) {
|
||||
// Same rule as VkTextureManager::MakeTextureIdentity: a GL texture view is identified by
|
||||
// the storage it views. A clear posted against a view and one posted against its parent
|
||||
// target the same image, so they have to coalesce rather than queue independently.
|
||||
if (texture != nullptr) {
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
if (storageOwner) {
|
||||
texture = storageOwner.get();
|
||||
}
|
||||
}
|
||||
return TextureIdentity {
|
||||
.texture = texture,
|
||||
.lifetimeId = texture ? texture->GetLifetimeId() : 0,
|
||||
@@ -202,15 +166,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void VkClearManager::MergeClearPayload(ClearAttachmentPayload& dst, const ClearAttachmentPayload& src) {
|
||||
dst.mask |= src.mask;
|
||||
if ((src.mask & GL_COLOR_BUFFER_BIT) != 0) {
|
||||
// The whole colour story travels together (same rule as
|
||||
// VkRenderPassManager::QueueRenderbufferClear): a glClearBufferiv/uiv
|
||||
// payload carries its value in colorInt/colorUint and its branch selector
|
||||
// in colorEncoding - dropping them here would leave the pending clear
|
||||
// reading as an all-zero float one.
|
||||
dst.color = src.color;
|
||||
dst.colorEncoding = src.colorEncoding;
|
||||
dst.colorInt = src.colorInt;
|
||||
dst.colorUint = src.colorUint;
|
||||
}
|
||||
if ((src.mask & GL_DEPTH_BUFFER_BIT) != 0) {
|
||||
dst.depth = src.depth;
|
||||
@@ -322,11 +278,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const PendingClearKey key = MakePendingClearKey(storageTexture.get());
|
||||
const PendingClearKey key = MakePendingClearKey(texture.get());
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -343,14 +297,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
const PendingClearKey key = MakePendingClearKey(attachment);
|
||||
// The alive entry must hold the STORAGE object, because the key names it:
|
||||
// LockTextureIdentityLocked cross-checks the two, and registering a view here under its
|
||||
// storage's identity made every lookup of this clear fail that check and silently report
|
||||
// "nothing pending" - which is how a clear issued through a view's framebuffer vanished.
|
||||
const auto& storageOwner = texture->GetViewStorageOwner();
|
||||
const SharedPtr<MG_State::GLState::ITextureObject>& storageTexture = storageOwner ? storageOwner : texture;
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_aliveObjects[MakeTextureIdentity(storageTexture.get())] = storageTexture;
|
||||
m_aliveObjects[MakeTextureIdentity(texture.get())] = texture;
|
||||
auto& pending = m_pendingClears[key];
|
||||
MergeClearPayload(pending, clearPayload);
|
||||
m_pendingCount.store(static_cast<Uint32>(m_pendingClears.size()), std::memory_order_relaxed);
|
||||
@@ -365,7 +313,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
for (auto it = m_pendingClears.begin(); it != m_pendingClears.end(); ++it) {
|
||||
@@ -456,7 +403,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return false; // per-draw hot path: nothing pending anywhere
|
||||
}
|
||||
|
||||
texture = ClearStorageTextureOf(texture);
|
||||
const Uint64 lifetimeId = texture->GetLifetimeId();
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
|
||||
@@ -114,8 +114,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
class VkClearManager {
|
||||
public:
|
||||
static PendingClearKey MakePendingClearKey(const MG_State::GLState::FramebufferAttachmentObject& attachment);
|
||||
// Resolves a GL texture view to the storage it views before keying; see the definition.
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* rawTexture, Uint32 mipLevel = 0,
|
||||
static PendingClearKey MakePendingClearKey(MG_State::GLState::ITextureObject* texture, Uint32 mipLevel = 0,
|
||||
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
|
||||
|
||||
Bool Initialize();
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include "MG_Util/Converters/MGToStr/FramebufferEnumConverter.h"
|
||||
#include "MG_Util/Converters/MGToVk/TextureEnumConverter.h"
|
||||
#include "MG_Util/Metrics/TextureMetrics.h"
|
||||
#include <MG_Pipe/PipeInputsSwitch.h>
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
static Bool TryResolveSampleCountFlagBits(Int requestedSamples, VkSampleCountFlagBits& outSampleCount) {
|
||||
@@ -68,58 +67,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
static Uint32 ResolveAttachmentBaseArrayLayer(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
// Every branch has to go through ToStorageArrayLayer, including the two that name layer 0
|
||||
// implicitly: a layered attachment of a texture VIEW starts at the view's first layer, not
|
||||
// at the image's, and a cube FACE index is a layer index like any other. Leaving either
|
||||
// unshifted made the render pass write layers [0, n) while the clear key, the blit, the
|
||||
// copy and the readback for the same attachment all addressed [minLayer, minLayer + n) -
|
||||
// they resolve the layer through their own copies of this helper, which do shift.
|
||||
const auto* texture = attachment.GetTexture().get();
|
||||
if (attachment.IsLayered()) {
|
||||
return ToStorageArrayLayer(texture, 0);
|
||||
return 0;
|
||||
}
|
||||
const TextureUploadTarget uploadTarget = attachment.GetTextureUploadTarget();
|
||||
if (!IsCubeMapFaceUploadTarget(uploadTarget)) {
|
||||
return ToStorageArrayLayer(texture, attachment.GetTextureLayer());
|
||||
return static_cast<Uint32>(std::max(attachment.GetTextureLayer(), 0));
|
||||
}
|
||||
const Int face =
|
||||
static_cast<Int>(uploadTarget) - static_cast<Int>(TextureUploadTarget::CubeMapPositiveX);
|
||||
return ToStorageArrayLayer(texture, face);
|
||||
return static_cast<Uint32>(uploadTarget) - static_cast<Uint32>(TextureUploadTarget::CubeMapPositiveX);
|
||||
}
|
||||
|
||||
// ResolveAttachmentLayerCount lives in VkTextureManager.h, beside ToVulkanLevelExtent, because
|
||||
// VkClearManager needs the SAME answer: its pending-clear key's layerCount becomes a real
|
||||
// VkImageSubresourceRange when a clear is materialised outside a render pass. See the header.
|
||||
static Uint32 ResolveAttachmentLayerCount(const MG_State::GLState::FramebufferAttachmentObject& attachment) {
|
||||
if (attachment.IsLayered()) {
|
||||
return static_cast<Uint32>(std::max(attachment.GetSize().z(), 1));
|
||||
}
|
||||
return 1u;
|
||||
}
|
||||
|
||||
// VUID-VkFramebufferCreateInfo-flags-04113: every view handed to vkCreateFramebuffer must have
|
||||
// been created as VK_IMAGE_VIEW_TYPE_2D or VK_IMAGE_VIEW_TYPE_2D_ARRAY. The image's OWN view
|
||||
// type is not a legal answer for several of the targets GL can attach, and returning it
|
||||
// unchanged is what took the process down on every layered 3D / cube-map-array attachment:
|
||||
// a 3D view is refused outright by the layer-span guard in GetOrCreateAttachmentViewAtMipLevel
|
||||
// (3D images have arrayLayers == 1) and a CUBE_ARRAY view is built happily and then rejected -
|
||||
// or dereferenced - by the driver inside vkCreateFramebuffer.
|
||||
//
|
||||
// A 2D_ARRAY view is the legal spelling of all three: over a 2D-array-compatible 3D image its
|
||||
// "layers" are the mip's z slices (VUID-VkImageViewCreateInfo-image-04970), and over a
|
||||
// CUBE_COMPATIBLE 2D image - which is what both cube targets are - its layers are the faces.
|
||||
//
|
||||
// Knowingly NOT remapped: VK_IMAGE_VIEW_TYPE_1D / _1D_ARRAY, which 04113 also forbids. There is
|
||||
// no legal alternative for them (a VK_IMAGE_TYPE_1D image admits no 2D-family view at all), so
|
||||
// the only honest answer would be to decline the attachment - and every driver this has run on,
|
||||
// lavapipe included, accepts them. Declining would turn working GL_TEXTURE_1D[_ARRAY] render
|
||||
// targets into skipped draws to satisfy a VU nothing enforces. Left as-is, deliberately.
|
||||
static VkImageViewType ResolveAttachmentViewType(
|
||||
const MG_State::GLState::FramebufferAttachmentObject& attachment,
|
||||
const VkTextureManager::TextureResource& resource) {
|
||||
if (attachment.IsLayered()) {
|
||||
switch (resource.viewType) {
|
||||
case VK_IMAGE_VIEW_TYPE_3D:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE:
|
||||
case VK_IMAGE_VIEW_TYPE_CUBE_ARRAY:
|
||||
return VK_IMAGE_VIEW_TYPE_2D_ARRAY;
|
||||
default:
|
||||
return resource.viewType;
|
||||
}
|
||||
return resource.viewType;
|
||||
}
|
||||
// A non-layered attachment names ONE layer, so the view over it is a plain 2D view whatever
|
||||
// the image's own view type is. The cube-face upload targets always meant this; a cube map
|
||||
@@ -127,15 +96,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// a single layer is not a legal attachment. The CUBE arm is inert today - no frontend path
|
||||
// produces a non-layered cube attachment without a face upload target - and is kept for
|
||||
// symmetry with CUBE_ARRAY.
|
||||
//
|
||||
// 3D belongs in the same list and was missing from it, which is why the "per-slice
|
||||
// attachment view is a 2D view whose array layer is the slice" branch in
|
||||
// GetOrCreateAttachmentViewAtMipLevel was unreachable: glFramebufferTextureLayer on a
|
||||
// GL_TEXTURE_3D asked for a 3D view (illegal as an attachment) whose span was then checked
|
||||
// against arrayLayers == 1, so every slice above z = 0 came back VK_NULL_HANDLE.
|
||||
if (IsCubeMapFaceUploadTarget(attachment.GetTextureUploadTarget()) ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE ||
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_3D) {
|
||||
resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY || resource.viewType == VK_IMAGE_VIEW_TYPE_CUBE) {
|
||||
return VK_IMAGE_VIEW_TYPE_2D;
|
||||
}
|
||||
return resource.viewType;
|
||||
@@ -161,7 +123,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
|
||||
if (!attachment.IsComplete()) {
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u has an incomplete texture attachment; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -170,7 +132,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
auto* texture = attachment.GetTexture().get();
|
||||
if (texture == nullptr) {
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
MGLOG_W("GetOrCreateRenderPass: draw buffer slot %u (%s) on FBO %u resolved to a null texture; using VK_ATTACHMENT_UNUSED",
|
||||
drawBufferIndex,
|
||||
MG_Util::ConvertFramebufferAttachmentTypeToString(attachmentType).c_str(),
|
||||
fbo.GetExternalIndex());
|
||||
@@ -235,11 +197,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.Get() = m_device;
|
||||
s_clearManager.Get() = &m_clearManager;
|
||||
s_textureManager.Get() = &m_textureManager;
|
||||
s_swapchainObject.Get() = &m_swapchainObject;
|
||||
s_renderPassManager.Get() = this;
|
||||
RenderPassEntry::s_device = m_device;
|
||||
s_clearManager = &m_clearManager;
|
||||
s_textureManager = &m_textureManager;
|
||||
s_swapchainObject = &m_swapchainObject;
|
||||
s_renderPassManager = this;
|
||||
}
|
||||
|
||||
VkRenderPassManager::~VkRenderPassManager() {}
|
||||
@@ -257,8 +219,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
|
||||
m_pendingRenderbufferClears.clear();
|
||||
RenderPassEntry::s_textureResourcesScratch.clear();
|
||||
s_activeRenderPass.Get() = {};
|
||||
s_hasActiveRenderPass.Get() = false;
|
||||
s_activeRenderPass = {};
|
||||
s_hasActiveRenderPass = false;
|
||||
m_rpFastValid = false;
|
||||
}
|
||||
|
||||
@@ -349,25 +311,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
|
||||
if (!TryResolveSampleCountFlagBits(renderbuffer->GetSamples(), sampleCount)) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer sample count %d for renderbuffer %u",
|
||||
renderbuffer->GetSamples(),
|
||||
renderbuffer->GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto internalFormat = renderbuffer->GetInternalFormat();
|
||||
// ONE resolver, shared with textures (VkTextureManager::ResolveTextureFormatInfo), so a
|
||||
// renderbuffer and a texture of the same GL format cannot disagree about their VkFormat.
|
||||
// `expandRgbToRgba` / `componentByteCount` / `alphaBytes` describe how to reshape a SHADOW
|
||||
// UPLOAD, and a renderbuffer has none, so only `.format` is taken.
|
||||
//
|
||||
// This used to be a hand-maintained second copy of that table, and it was missing exactly
|
||||
// four rows: RGBA2 and RGBA12 fell through to ConvertTextureInternalFormatToVkEnum's
|
||||
// VK_FORMAT_UNDEFINED (no image at all - bound as a draw buffer the attachment became
|
||||
// VK_ATTACHMENT_UNUSED and every draw into it was dropped), while RGBA4 and RGB5A1 fell
|
||||
// through to the 16-bit packed formats and then faced 32-bit R8G8B8A8_UNORM textures across
|
||||
// a size-incompatible vkCmdCopyImage.
|
||||
const VkFormat format = ResolveTextureFormatInfo(internalFormat).format;
|
||||
// Three-channel color formats widen to their RGBA twin exactly like textures do
|
||||
// (VkTextureManager::ResolveTextureFormatInfo): blits/resolves between a
|
||||
// renderbuffer and a texture of the same GL format then see one VkFormat.
|
||||
const VkFormat format = [&]() -> VkFormat {
|
||||
switch (internalFormat) {
|
||||
case TextureInternalFormat::RGB:
|
||||
case TextureInternalFormat::RGB8:
|
||||
case TextureInternalFormat::R3G3B2:
|
||||
case TextureInternalFormat::RGB4:
|
||||
case TextureInternalFormat::RGB5:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case TextureInternalFormat::SRGB8:
|
||||
return VK_FORMAT_R8G8B8A8_SRGB;
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
return VK_FORMAT_R8G8B8A8_SNORM;
|
||||
case TextureInternalFormat::RGB10:
|
||||
case TextureInternalFormat::RGB12:
|
||||
case TextureInternalFormat::RGB16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
return VK_FORMAT_R16G16B16A16_SNORM;
|
||||
case TextureInternalFormat::RGB16F:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case TextureInternalFormat::RGB32F:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case TextureInternalFormat::RGB8I:
|
||||
return VK_FORMAT_R8G8B8A8_SINT;
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case TextureInternalFormat::RGB16I:
|
||||
return VK_FORMAT_R16G16B16A16_SINT;
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case TextureInternalFormat::RGB32I:
|
||||
return VK_FORMAT_R32G32B32A32_SINT;
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default:
|
||||
return MG_Util::ConvertTextureInternalFormatToVkEnum(internalFormat);
|
||||
}
|
||||
}();
|
||||
const VkImageAspectFlags aspect = ResolveImageAspectMaskForFormat(format);
|
||||
// Renderbuffers are never sampled (GL has no way to bind one to a sampler), so the
|
||||
// usage set is attachment + transfer: transfer covers readback (vkCmdCopyImageToBuffer),
|
||||
@@ -466,7 +457,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_physicalDevice, format, imageInfo.imageType, imageInfo.tiling, imageInfo.usage, imageInfo.flags,
|
||||
&imageFormatProperties);
|
||||
if (imageFormatResult != VK_SUCCESS || (imageFormatProperties.sampleCounts & sampleCount) == 0) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
MGLOG_E("GetOrCreateRenderbufferResource: unsupported renderbuffer format=%d samples=%d for renderbuffer %u",
|
||||
static_cast<Int>(format),
|
||||
static_cast<Int>(sampleCount),
|
||||
renderbuffer->GetExternalIndex());
|
||||
@@ -607,12 +598,11 @@ 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.
|
||||
const Bool framebufferSrgbEnabled =
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &framebufferSrgbEnabled, sizeof(framebufferSrgbEnabled)));
|
||||
auto& drawBuffers = fbo.GetDrawBuffers();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, drawBuffers.data(), drawBuffers.size() * sizeof(drawBuffers[0])));
|
||||
@@ -643,13 +633,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (att.IsTexture()) {
|
||||
const Uint64 textureLifetimeId = att.GetTexture()->GetLifetimeId();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLifetimeId, sizeof(textureLifetimeId)));
|
||||
const Int textureLevel = static_cast<Int>(ToStorageMipLevel(att.GetTexture().get(),
|
||||
att.GetTextureLevel()));
|
||||
const Int textureLevel = att.GetTextureLevel();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLevel, sizeof(textureLevel)));
|
||||
const TextureUploadTarget textureUploadTarget = att.GetTextureUploadTarget();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureUploadTarget, sizeof(textureUploadTarget)));
|
||||
const Int textureLayer = static_cast<Int>(ToStorageArrayLayer(att.GetTexture().get(),
|
||||
att.GetTextureLayer()));
|
||||
const Int textureLayer = att.GetTextureLayer();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayer, sizeof(textureLayer)));
|
||||
const Bool textureLayered = att.IsLayered();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &textureLayered, sizeof(textureLayered)));
|
||||
@@ -766,7 +754,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
RenderPassEntry* VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
RenderPassEntry& VkRenderPassManager::GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil) {
|
||||
// Resolve the default-FBO depth flavor (see the header comment): keep the
|
||||
@@ -843,7 +831,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// recreated since (texture + renderbuffer image epochs), and no pending clear (which alters
|
||||
// load ops). Any of these differing forces the full recompute below. Portable to VK 1.1.
|
||||
if (activeRenderPass != nullptr && m_rpFastValid && m_rpFastFbo == &fbo &&
|
||||
m_rpFastFboLifetimeId == fbo.GetLifetimeId() &&
|
||||
m_rpFastFboVersion == fbo.GetObjectVersion() && m_rpFastSwapchainIndex == swapchainImageIndex &&
|
||||
m_rpFastTexEpoch == m_textureManager.GetTextureImageEpoch() &&
|
||||
m_rpFastRbEpoch == m_renderbufferImageEpoch &&
|
||||
@@ -852,7 +839,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
auto activeIt = m_renderPasses.find(activeRenderPass->hash);
|
||||
if (activeIt != m_renderPasses.end()) {
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return &activeIt->second;
|
||||
return activeIt->second;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,7 +855,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// epochs AFTER ComputeHash: its attachment SyncTexture can create an image (bump the epoch).
|
||||
m_rpFastValid = true;
|
||||
m_rpFastFbo = &fbo;
|
||||
m_rpFastFboLifetimeId = fbo.GetLifetimeId();
|
||||
m_rpFastFboVersion = fbo.GetObjectVersion();
|
||||
m_rpFastSwapchainIndex = swapchainImageIndex;
|
||||
m_rpFastTexEpoch = m_textureManager.GetTextureImageEpoch();
|
||||
@@ -876,13 +862,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_rpFastRenderPassHash = activeRenderPass->hash;
|
||||
m_rpFastHadDepthStencil = activeIt->second.hasDepthStencilAttachment;
|
||||
activeIt->second.lastUsedFrame = m_frameCounter;
|
||||
return &activeIt->second;
|
||||
return activeIt->second;
|
||||
}
|
||||
auto hash = ComputeHash(fbo, swapchainImageIndex, true, includeDefaultFboDepthStencil);
|
||||
auto it = m_renderPasses.find(hash);
|
||||
if (it != m_renderPasses.end()) {
|
||||
it->second.lastUsedFrame = m_frameCounter;
|
||||
return &it->second;
|
||||
return it->second;
|
||||
}
|
||||
|
||||
Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
|
||||
@@ -943,7 +929,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
const auto& renderbuffer = rbAtt.GetRenderbuffer();
|
||||
auto* rbResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (rbResource == nullptr || (rbResource->aspect & VK_IMAGE_ASPECT_COLOR_BIT) == 0) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
MGLOG_E("GetOrCreateRenderPass: draw buffer slot %u on FBO %u has an unsupported color "
|
||||
"renderbuffer %u; using VK_ATTACHMENT_UNUSED",
|
||||
i, fbo.GetExternalIndex(), renderbuffer->GetExternalIndex());
|
||||
continue;
|
||||
@@ -964,7 +950,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
const VkImageLayout trackedRbLayout = rbResource->layout;
|
||||
const Bool rbFramebufferSrgb =
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb);
|
||||
const VkFormat rbAttachmentFormat =
|
||||
ResolveSrgbAttachmentWriteFormat(rbResource->format, rbFramebufferSrgb);
|
||||
rbDesc.flags = 0;
|
||||
@@ -1005,12 +991,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(rbAttachmentFormat != rbResource->format ? rbResource->unormTwinView
|
||||
: rbResource->view);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for color attachment "
|
||||
"%u on FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: renderbuffer view missing at color attachment %d", i);
|
||||
|
||||
colorAttachmentRefs[i].attachment = rbAttachmentIndex;
|
||||
continue;
|
||||
@@ -1022,7 +1004,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
continue;
|
||||
|
||||
auto& att = fbo.GetAttachment(drawbuf);
|
||||
const Uint32 attachmentMipLevel = ToStorageMipLevel(att.GetTexture().get(), att.GetTextureLevel());
|
||||
const Uint32 attachmentMipLevel = static_cast<Uint32>(std::max(att.GetTextureLevel(), 0));
|
||||
const auto textureTarget = texture->GetTarget();
|
||||
const Uint32 attachmentIndex = static_cast<Uint32>(attachmentDescriptions.size());
|
||||
attachmentDescriptions.emplace_back();
|
||||
@@ -1065,12 +1047,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
.key = VkClearManager::MakePendingClearKey(att)
|
||||
});
|
||||
}
|
||||
// Same remap as ResolveAttachmentLayerCount, for the same reason: a
|
||||
// 1D-array attachment's GL height is its layer count, and using it as the
|
||||
// framebuffer height asks for a framebuffer taller than the VK_IMAGE_TYPE_1D
|
||||
// image it is built over.
|
||||
const IntVec2 attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo, ToVulkanLevelExtent(texture->GetTarget(), att.GetSize()), swapchainExtent);
|
||||
const IntVec2 attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, att.GetSize(), swapchainExtent);
|
||||
if (width == 0)
|
||||
width = attachmentExtent.x();
|
||||
if (height == 0)
|
||||
@@ -1098,19 +1076,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(swapchainViews[swapchainImageIndex]);
|
||||
} else {
|
||||
auto* textureResource = m_textureManager.SyncTextureAndGetDescriptor(*texture);
|
||||
if (textureResource == nullptr) {
|
||||
// SyncTextureResource legitimately declines - an unsupported format,
|
||||
// sample count or image-flag combination, or a vkCreateImage the driver
|
||||
// refused. There is no image to attach, so there is no render pass.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for color "
|
||||
"attachment %u on FBO %u; declining the render pass",
|
||||
texture->GetExternalIndex(), i, fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(textureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at color attachment %d", i);
|
||||
textureResources.emplace_back(textureResource);
|
||||
desc.format = ResolveSrgbAttachmentWriteFormat(
|
||||
textureResource->format,
|
||||
MGB_CTX->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
MG_State::pGLContext->IsCapabilityEnabled(MobileGL::CapabilityInput::FramebufferSrgb));
|
||||
attachmentSampleCount = textureResource->sampleCount;
|
||||
trackedColorLayout = textureResource->layout;
|
||||
trackedAttachmentLayouts.emplace_back(TrackedAttachmentLayoutInfo {
|
||||
@@ -1127,21 +1098,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
*texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers "
|
||||
"[%u, %u) viewType=%d at color attachment %u on FBO %u; declining the "
|
||||
"render pass",
|
||||
texture->GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType), i,
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at color attachment %d", i);
|
||||
}
|
||||
desc.samples = attachmentSampleCount;
|
||||
adoptRenderPassSampleCount(attachmentSampleCount, "color", texture->GetExternalIndex());
|
||||
|
||||
if (!hasClear && trackedColorLayout == VK_IMAGE_LAYOUT_UNDEFINED) {
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
MGLOG_W("GetOrCreateRenderPass: color attachment textureId=%d starts with undefined layout and no clear; "
|
||||
"using LOAD_OP_DONT_CARE",
|
||||
texture->GetExternalIndex());
|
||||
desc.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
|
||||
@@ -1178,8 +1142,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (a.IsTexture() && b.IsTexture()) {
|
||||
return a.GetTexture().get() == b.GetTexture().get() &&
|
||||
a.GetTextureUploadTarget() == b.GetTextureUploadTarget() &&
|
||||
ToStorageMipLevel(a.GetTexture().get(), a.GetTextureLevel()) ==
|
||||
ToStorageMipLevel(b.GetTexture().get(), b.GetTextureLevel());
|
||||
a.GetTextureLevel() == b.GetTextureLevel();
|
||||
}
|
||||
if (a.IsRenderbuffer() && b.IsRenderbuffer()) {
|
||||
return a.GetRenderbuffer().get() == b.GetRenderbuffer().get();
|
||||
@@ -1198,7 +1161,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
isUsableDepthStencilAttachment(depthAtt) && isUsableDepthStencilAttachment(stencilAtt) &&
|
||||
!sameDepthStencilAttachmentObject(depthAtt, stencilAtt);
|
||||
if (hasDistinctDepthAndStencilAttachments) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
MGLOG_E("GetOrCreateRenderPass: separate depth/stencil attachments are not supported yet; using the depth attachment and ignoring the standalone stencil attachment for framebuffer %u",
|
||||
fbo.GetExternalIndex());
|
||||
}
|
||||
if (selectedDepthStencilAttachment != nullptr) {
|
||||
@@ -1228,29 +1191,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
depthTextureResource = m_textureManager.SyncTextureAndGetDescriptor(texture);
|
||||
if (depthTextureResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: textureId=%d could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(depthTextureResource,
|
||||
"GetOrCreateRenderPass: SyncTextureAndGetDescriptor failed at depth attachment");
|
||||
trackedDepthLayout = depthTextureResource->layout;
|
||||
depthAttachmentDescription.format = depthTextureResource->format;
|
||||
depthAttachmentSampleCount = depthTextureResource->sampleCount;
|
||||
depthAttachmentId = static_cast<Int>(texture.GetExternalIndex());
|
||||
attachmentExtent = ResolveRenderPassFramebufferExtent(
|
||||
isDefaultFbo,
|
||||
ToVulkanLevelExtent(texture.GetTarget(), selectedDepthStencilAttachment->GetSize()),
|
||||
swapchainExtent);
|
||||
attachmentExtent =
|
||||
ResolveRenderPassFramebufferExtent(isDefaultFbo, selectedDepthStencilAttachment->GetSize(),
|
||||
swapchainExtent);
|
||||
} else {
|
||||
const auto& renderbuffer = selectedDepthStencilAttachment->GetRenderbuffer();
|
||||
depthRenderbufferResource = GetOrCreateRenderbufferResource(renderbuffer);
|
||||
if (depthRenderbufferResource == nullptr) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u could not be backed for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(depthRenderbufferResource,
|
||||
"GetOrCreateRenderPass: GetOrCreateRenderbufferResource failed at depth attachment");
|
||||
trackedDepthLayout = depthRenderbufferResource->layout;
|
||||
depthAttachmentDescription.format = depthRenderbufferResource->format;
|
||||
depthAttachmentSampleCount = depthRenderbufferResource->sampleCount;
|
||||
@@ -1269,7 +1223,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
depthAttachmentDescription.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
|
||||
depthAttachmentDescription.initialLayout = loadInfo.initialLayout;
|
||||
if (trackedDepthLayout == VK_IMAGE_LAYOUT_UNDEFINED && (!clearDepth || !clearStencil)) {
|
||||
MGLOG_W_ONCE("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
MGLOG_W("GetOrCreateRenderPass: depth/stencil attachment id=%d starts with undefined layout "
|
||||
"and partial/no clear; using DONT_CARE for uncleared aspects",
|
||||
depthAttachmentId);
|
||||
}
|
||||
@@ -1298,8 +1252,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
} else if (selectedDepthStencilAttachment->IsTexture()) {
|
||||
auto& texture = *selectedDepthStencilAttachment->GetTexture();
|
||||
const Uint32 attachmentMipLevel =
|
||||
ToStorageMipLevel(selectedDepthStencilAttachment->GetTexture().get(),
|
||||
selectedDepthStencilAttachment->GetTextureLevel());
|
||||
static_cast<Uint32>(std::max(selectedDepthStencilAttachment->GetTextureLevel(), 0));
|
||||
MOBILEGL_ASSERT(depthTextureResource->layout != VK_IMAGE_LAYOUT_UNDEFINED ||
|
||||
depthAttachmentDescription.loadOp != VK_ATTACHMENT_LOAD_OP_LOAD,
|
||||
"GetOrCreateRenderPass: depth attachment textureId=%d has undefined tracked layout with LOAD_OP_LOAD",
|
||||
@@ -1324,14 +1277,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
attachmentViews.emplace_back(
|
||||
m_textureManager.GetOrCreateAttachmentViewAtMipLevel(
|
||||
texture, attachmentMipLevel, baseArrayLayer, layerCount, attachmentViewType));
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: no attachment view for textureId=%d mip=%u layers [%u, %u) "
|
||||
"viewType=%d at the depth/stencil attachment of FBO %u; declining the render pass",
|
||||
texture.GetExternalIndex(), attachmentMipLevel, baseArrayLayer,
|
||||
baseArrayLayer + layerCount, static_cast<Int>(attachmentViewType),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
MOBILEGL_ASSERT(attachmentViews.back() != VK_NULL_HANDLE,
|
||||
"GetOrCreateRenderPass: GetOrCreateAttachmentView failed at depth attachment");
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1353,12 +1300,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
});
|
||||
textureResources.emplace_back(nullptr);
|
||||
attachmentViews.emplace_back(depthRenderbufferResource->view);
|
||||
if (attachmentViews.back() == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: renderbuffer %u has no usable view for the depth/stencil "
|
||||
"attachment of FBO %u; declining the render pass",
|
||||
renderbuffer->GetExternalIndex(), fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
if (width == 0 || height == 0) {
|
||||
width = attachmentExtent.x();
|
||||
height = attachmentExtent.y();
|
||||
@@ -1456,25 +1397,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
renderPassCreateInfo.dependencyCount = 2;
|
||||
renderPassCreateInfo.pDependencies = subpassDependencies;
|
||||
|
||||
// NOT VK_VERIFY. VkIncludes.h states the rule this function now lives by: VK_VERIFY is the
|
||||
// INVARIANT check - a should-never-happen state, fatal-logged unlatched and trapped in a
|
||||
// DEBUG build - and "a soft, recoverable failure must therefore NOT be routed through
|
||||
// VK_VERIFY. Check the VkResult directly and report it with MGLOG_E_ONCE". A decline here
|
||||
// is recoverable by construction: the caller drops the draw. Routing it through VK_VERIFY
|
||||
// would have made the recovery dead code in a DEBUG build (the TRAP fires inside the macro,
|
||||
// before the handle is ever examined) and, in an INFO build, printed an UNLATCHED fatal
|
||||
// line on every draw for the life of the process - a decline caches nothing, so every
|
||||
// later draw to the same framebuffer re-enters this path and fails again.
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
const VkResult renderPassResult =
|
||||
vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass);
|
||||
if (renderPassResult != VK_SUCCESS || renderPass == VK_NULL_HANDLE) {
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateRenderPass failed (%s, %d) for FBO %u; declining the "
|
||||
"render pass",
|
||||
VkResultToString(renderPassResult), static_cast<Int>(renderPassResult),
|
||||
fbo.GetExternalIndex());
|
||||
return nullptr;
|
||||
}
|
||||
VK_VERIFY(vkCreateRenderPass(m_device, &renderPassCreateInfo, nullptr, &renderPass));
|
||||
|
||||
// Framebuffer
|
||||
VkFramebufferCreateInfo framebufferCreateInfo;
|
||||
@@ -1487,21 +1411,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
framebufferCreateInfo.width = width;
|
||||
framebufferCreateInfo.height = height;
|
||||
framebufferCreateInfo.layers = framebufferLayers;
|
||||
// Direct VkResult check, for the same reason as vkCreateRenderPass above.
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
const VkResult framebufferResult =
|
||||
vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer);
|
||||
if (framebufferResult != VK_SUCCESS || framebuffer == VK_NULL_HANDLE) {
|
||||
// The render pass has no entry to own it yet, so it is destroyed here rather than
|
||||
// leaked - RenderPassEntry's destructor is the only other thing that would.
|
||||
MGLOG_E_ONCE("GetOrCreateRenderPass: vkCreateFramebuffer failed (%s, %d) for FBO %u (%dx%d, "
|
||||
"%u attachments, %u layers); declining the render pass",
|
||||
VkResultToString(framebufferResult), static_cast<Int>(framebufferResult),
|
||||
fbo.GetExternalIndex(), width, height,
|
||||
static_cast<Uint32>(attachmentViews.size()), framebufferLayers);
|
||||
vkDestroyRenderPass(m_device, renderPass, nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
VK_VERIFY(vkCreateFramebuffer(m_device, &framebufferCreateInfo, nullptr, &framebuffer));
|
||||
IntVec2 extent = {width, height};
|
||||
RenderPassEntry renderPassEntry {
|
||||
hash,
|
||||
@@ -1526,18 +1437,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
extent.y());
|
||||
auto [insertedIt, _] = m_renderPasses.emplace(hash, Move(renderPassEntry));
|
||||
insertedIt->second.lastUsedFrame = m_frameCounter;
|
||||
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);
|
||||
}
|
||||
return insertedIt->second;
|
||||
}
|
||||
|
||||
void VkRenderPassManager::OnPresent() {
|
||||
@@ -1566,9 +1466,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.Get() ? s_activeRenderPass.Get().hash : 0;
|
||||
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
|
||||
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
|
||||
const Bool isActive = s_hasActiveRenderPass.Get() && it->first == activeHash;
|
||||
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
|
||||
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
|
||||
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
|
||||
m_rpFastValid = false;
|
||||
@@ -1607,26 +1507,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
ClearAttachmentPayload clearPayload{};
|
||||
SharedPtr<MG_State::GLState::ITextureObject> liveTexture;
|
||||
if (pending.hasInlinePayload) {
|
||||
// The inline payload was snapshotted when the entry was CREATED, but the
|
||||
// clear VALUE is not part of the entry's hash - a cache hit with a newer
|
||||
// glClear would replay the creation-time value and drop the new one (the
|
||||
// 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.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) {
|
||||
// RGB renderbuffers are backed by an RGBA image; the missing alpha reads as 1.
|
||||
ForceOpaqueClearAlpha(clearPayload);
|
||||
}
|
||||
} else {
|
||||
clearPayload = pending.inlinePayload;
|
||||
}
|
||||
clearPayload = pending.inlinePayload;
|
||||
} else {
|
||||
if (pending.key.texture == nullptr ||
|
||||
!s_clearManager.Get()->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1648,29 +1532,29 @@ 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.Get() != nullptr) {
|
||||
if (s_textureManager != nullptr) {
|
||||
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
|
||||
if (tracked.target == TrackedAttachmentTarget::Texture) {
|
||||
if (const auto texture = tracked.texture.lock()) {
|
||||
s_textureManager.Get()->StampTextureRecordingUse(texture.get());
|
||||
s_textureManager->StampTextureRecordingUse(texture.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
|
||||
if (pending.hasInlinePayload) {
|
||||
if (s_renderPassManager.Get() != nullptr) {
|
||||
s_renderPassManager.Get()->PopPendingRenderbufferClear(pending.renderbuffer);
|
||||
if (s_renderPassManager != nullptr) {
|
||||
s_renderPassManager->PopPendingRenderbufferClear(pending.renderbuffer);
|
||||
}
|
||||
} else {
|
||||
s_clearManager.Get()->PopPendingClear(pending.key);
|
||||
s_clearManager->PopPendingClear(pending.key);
|
||||
}
|
||||
}
|
||||
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;
|
||||
s_activeRenderPass.hash = renderPassEntry.hash;
|
||||
s_activeRenderPass.compatibilityHash = renderPassEntry.compatibilityHash;
|
||||
s_activeRenderPass.trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
|
||||
s_activeRenderPass.extent = renderPassEntry.extent;
|
||||
s_hasActiveRenderPass = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1680,16 +1564,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
// 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.Get() != nullptr) {
|
||||
s_renderPassManager.Get()->m_rpFastValid = false;
|
||||
if (s_renderPassManager != nullptr) {
|
||||
s_renderPassManager->m_rpFastValid = false;
|
||||
}
|
||||
if (activeRenderPass != nullptr && !activeRenderPass->trackedAttachmentLayouts.empty()) {
|
||||
for (const auto& trackedAttachment : activeRenderPass->trackedAttachmentLayouts) {
|
||||
switch (trackedAttachment.target) {
|
||||
case TrackedAttachmentTarget::Texture:
|
||||
MOBILEGL_ASSERT(s_textureManager.Get() != nullptr, "EndRenderPass: texture manager is null");
|
||||
MOBILEGL_ASSERT(s_textureManager != nullptr, "EndRenderPass: texture manager is null");
|
||||
if (const auto texture = trackedAttachment.texture.lock()) {
|
||||
s_textureManager.Get()->UpdateTrackedImageLayoutAfterAttachmentWrite(
|
||||
s_textureManager->UpdateTrackedImageLayoutAfterAttachmentWrite(
|
||||
commandBuffer,
|
||||
texture.get(),
|
||||
trackedAttachment.textureMipLevel,
|
||||
@@ -1697,27 +1581,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
break;
|
||||
case TrackedAttachmentTarget::Renderbuffer:
|
||||
MOBILEGL_ASSERT(s_renderPassManager.Get() != nullptr, "EndRenderPass: render pass manager is null");
|
||||
MOBILEGL_ASSERT(s_renderPassManager != nullptr, "EndRenderPass: render pass manager is null");
|
||||
if (const auto renderbuffer = trackedAttachment.renderbuffer.lock()) {
|
||||
auto resourceIt =
|
||||
s_renderPassManager.Get()->m_renderbufferResources.find(renderbuffer.get());
|
||||
if (resourceIt != s_renderPassManager.Get()->m_renderbufferResources.end()) {
|
||||
s_renderPassManager->m_renderbufferResources.find(renderbuffer.get());
|
||||
if (resourceIt != s_renderPassManager->m_renderbufferResources.end()) {
|
||||
resourceIt->second.layout = trackedAttachment.finalLayout;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainColor:
|
||||
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject.Get()->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
|
||||
// The pass stored into the attachment: its content is defined
|
||||
// until the image is next presented.
|
||||
s_swapchainObject.Get()->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
case TrackedAttachmentTarget::SwapchainDepthStencil:
|
||||
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject.Get()->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
|
||||
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
|
||||
trackedAttachment.finalLayout);
|
||||
s_swapchainObject.Get()->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
|
||||
break;
|
||||
default:
|
||||
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
|
||||
@@ -1726,12 +1610,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
}
|
||||
s_activeRenderPass.Get() = {};
|
||||
s_hasActiveRenderPass.Get() = false;
|
||||
s_activeRenderPass = {};
|
||||
s_hasActiveRenderPass = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
ActiveRenderPassInfo* VkRenderPassManager::GetActiveRenderPass() {
|
||||
return s_hasActiveRenderPass.Get() ? &s_activeRenderPass.Get() : nullptr;
|
||||
return s_hasActiveRenderPass ? &s_activeRenderPass : nullptr;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -66,9 +66,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
VkExtent2D swapchainExtent);
|
||||
|
||||
struct RenderPassEntry {
|
||||
// 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;
|
||||
static inline VkDevice s_device;
|
||||
static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
|
||||
Uint64 hash = 0;
|
||||
VkRenderPass renderPass = VK_NULL_HANDLE;
|
||||
VkFramebuffer framebuffer = VK_NULL_HANDLE;
|
||||
@@ -102,42 +101,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
}
|
||||
// Move ASSIGNMENT, not just construction. The move constructor above and the
|
||||
// destructor below each independently suppress the implicit one, which left the
|
||||
// type move-constructible but not move-assignable - and therefore not swappable,
|
||||
// which std::swap(pair&, pair&) requires. That was invisible while UnorderedMap
|
||||
// only ever move-CONSTRUCTED an element into a fresh slot. ska::flat_hash_map
|
||||
// probes robin-hood: inserting swaps the entry being placed against the one
|
||||
// already sitting in the slot whenever it has travelled further from its desired
|
||||
// position, so the mapped type has to be swappable or the table fails to
|
||||
// instantiate at all.
|
||||
//
|
||||
// SWAP SEMANTICS, exactly like the move constructor: this does not release the
|
||||
// destination's handles, it parks them in `that`, which destroys them when it
|
||||
// dies. That is correct for the only caller - std::swap, whose temporary expires
|
||||
// immediately - and it is what keeps the three-move sequence from destroying a
|
||||
// live render pass. It is NOT correct for a hand-written `a = std::move(b)` where
|
||||
// `a` held live handles and `b` outlives the statement: those handles would then
|
||||
// survive until `b` dies. There is no such caller; add a destroy-then-steal
|
||||
// assignment before writing one.
|
||||
RenderPassEntry& operator=(RenderPassEntry&& that) noexcept {
|
||||
if (this != &that) {
|
||||
std::swap(hash, that.hash);
|
||||
std::swap(renderPass, that.renderPass);
|
||||
std::swap(framebuffer, that.framebuffer);
|
||||
std::swap(compatibilityHash, that.compatibilityHash);
|
||||
std::swap(pendingClearAttachments, that.pendingClearAttachments);
|
||||
std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
|
||||
std::swap(attachmentCount, that.attachmentCount);
|
||||
std::swap(colorAttachmentCount, that.colorAttachmentCount);
|
||||
std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
|
||||
std::swap(sampleCount, that.sampleCount);
|
||||
std::swap(extent, that.extent);
|
||||
std::swap(layers, that.layers);
|
||||
std::swap(lastUsedFrame, that.lastUsedFrame);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
RenderPassEntry(
|
||||
Uint64 hash,
|
||||
VkRenderPass renderpass,
|
||||
@@ -166,10 +129,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
|
||||
~RenderPassEntry() {
|
||||
if (renderPass != VK_NULL_HANDLE) {
|
||||
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
|
||||
vkDestroyRenderPass(s_device, renderPass, nullptr);
|
||||
}
|
||||
if (framebuffer != VK_NULL_HANDLE) {
|
||||
vkDestroyFramebuffer(s_device.Get(), framebuffer, nullptr);
|
||||
vkDestroyFramebuffer(s_device, framebuffer, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,15 +190,6 @@ 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,
|
||||
@@ -253,24 +207,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// draw against a depth-less active pass resolves to a new (incompatible)
|
||||
// entry, which the caller's compatibility check turns into a pass split;
|
||||
// the new pass's depth loads DONT_CARE (content was undefined all along).
|
||||
//
|
||||
// Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
|
||||
// all - a texture the texture manager declined to back (an unsupported format or sample
|
||||
// count), or an attachment view it cannot construct (a layer span the image has no room
|
||||
// for, a 3D image whose format was refused 2D-array compatibility). This used to be
|
||||
// unrepresentable: the function returned a reference, so the only thing the two fallible
|
||||
// calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
|
||||
// INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
|
||||
// vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
|
||||
// bodies, one runner restart each) where a declined draw is merely a wrong picture.
|
||||
//
|
||||
// EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
|
||||
// already drops a draw whose sampler descriptor could not be resolved
|
||||
// (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
|
||||
// themselves, so a caller needs no message of its own.
|
||||
[[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
RenderPassEntry& GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
|
||||
Uint32 swapchainImageIndex,
|
||||
Bool drawUsesDepthStencil = true);
|
||||
void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
|
||||
const MG_State::GLState::FramebufferObject& drawFbo);
|
||||
void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
|
||||
@@ -312,14 +251,8 @@ 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:
|
||||
// a deleted FBO reallocated at the same address whose fresh setup performed
|
||||
// the same number of version bumps would otherwise compare equal (both count
|
||||
// from 0), serving the dead framebuffer's pass to the new object.
|
||||
Uint64 m_rpFastFboLifetimeId = 0;
|
||||
Uint16 m_rpFastFboVersion = 0;
|
||||
Uint32 m_rpFastSwapchainIndex = 0;
|
||||
Uint64 m_rpFastTexEpoch = 0;
|
||||
@@ -382,30 +315,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
Uint64 deferredAtFrame = 0;
|
||||
};
|
||||
|
||||
// Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
|
||||
// Node-based std::unordered_map, deliberately not FastSTL's open-addressing UnorderedMap:
|
||||
// callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
|
||||
// calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
|
||||
// destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
|
||||
// materializes the source's pending clear, which looks that same resource up again. Growing
|
||||
// an open-addressed table relocates every element, so the cached pointer went on to name
|
||||
// freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
|
||||
// out at "source image layout is undefined", silently dropping the blit -
|
||||
// renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
|
||||
// iterations that grew the table.
|
||||
// materializes the source's pending clear, which looks that same resource up again. FastSTL's
|
||||
// operator[] runs its load-factor check before find_key and reallocates the whole bucket array
|
||||
// when occupancy crosses it, so even a plain lookup relocates every element; erase only
|
||||
// tombstones and never decrements the occupancy, so the doubling keeps firing. After a
|
||||
// relocation the cached pointer names freed storage still holding the pre-clear
|
||||
// VK_IMAGE_LAYOUT_UNDEFINED, and BlitFramebuffer bails out at "source image layout is
|
||||
// undefined", silently dropping the blit - renderbuffers_storage_multisample read back zero
|
||||
// instead of the clear colour on exactly the iterations that grew the table.
|
||||
//
|
||||
// Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
|
||||
// this: the destination resolve still runs after the source pointer is taken. The depth blit,
|
||||
// GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
|
||||
// kind of pointer, so the invariant belongs in the container rather than in a per-call-site
|
||||
// ordering rule. m_textureResources is node-based for the same reason.
|
||||
//
|
||||
// The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
|
||||
// the paragraph above as merely historical: ska erases by shifting the rest of the probe
|
||||
// cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
|
||||
// entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
|
||||
// no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
|
||||
// a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
|
||||
// That narrows the insert hazard; it does not touch the erase one.)
|
||||
// ordering rule. m_textureResources is node-based for the same reason. This buys stability
|
||||
// across rehash and insert only - erase still invalidates the erased element, which is safe
|
||||
// here because a renderbuffer that is an FBO attachment is held alive by that attachment.
|
||||
std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
|
||||
UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
|
||||
Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
|
||||
@@ -422,14 +351,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
|
||||
void CollectDeferredRenderbufferReleases(Bool destroyAll);
|
||||
|
||||
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;
|
||||
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;
|
||||
};
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
@@ -15,192 +15,12 @@
|
||||
|
||||
namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
namespace {
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
// An accessor view over the wire's POD values, not a GL object with constructor,
|
||||
// lifetime ID, allocator hooks or mutation callbacks. Both sources below use
|
||||
// one sampler policy, including format-sensitive border-color conversion.
|
||||
struct SamplerParametersSource {
|
||||
const SamplerParameters& Params;
|
||||
SamplerWrapMode GetWrapS() const { return Params.wrapS; }
|
||||
SamplerWrapMode GetWrapT() const { return Params.wrapT; }
|
||||
SamplerWrapMode GetWrapR() const { return Params.wrapR; }
|
||||
SamplerFilterMode GetMinFilter() const { return Params.minFilter; }
|
||||
SamplerFilterMode GetMagFilter() const { return Params.magFilter; }
|
||||
SamplerMipmapMode GetMipmapMode() const { return Params.mipmapMode; }
|
||||
Float GetMinLod() const { return Params.minLod; }
|
||||
Float GetMaxLod() const { return Params.maxLod; }
|
||||
Float GetLodBias() const { return Params.lodBias; }
|
||||
Float GetMaxAnisotropy() const { return Params.maxAnisotropy; }
|
||||
SamplerCompareMode GetCompareMode() const { return Params.compareMode; }
|
||||
SamplerCompareFunc GetSamplerCompareFunc() const { return Params.compareFunc; }
|
||||
const FloatVec4& GetBorderColor() const { return Params.borderColor; }
|
||||
const IntVec4& GetBorderColorI() const { return Params.borderColorI; }
|
||||
const UintVec4& GetBorderColorUI() const { return Params.borderColorUI; }
|
||||
// Cache identity is entirely content-based; these fields are diagnostics.
|
||||
Uint GetExternalIndex() const { return 0; }
|
||||
Uint16 GetVersion() const { return 0; }
|
||||
};
|
||||
|
||||
template <class SamplerSource>
|
||||
Bool UsesBorderColor(const SamplerSource& sampler) {
|
||||
#else
|
||||
Bool UsesBorderColor(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
return sampler.GetWrapS() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapT() == SamplerWrapMode::ClampToBorder ||
|
||||
sampler.GetWrapR() == SamplerWrapMode::ClampToBorder;
|
||||
}
|
||||
|
||||
// The numeric domain the texture is SAMPLED in. Vulkan splits VkBorderColor into a float
|
||||
// family and an integer family and requires the sampler's choice to match the image view's
|
||||
// format (a float border on an integer view, or the reverse, is undefined) - so the domain
|
||||
// comes from the TEXTURE, while the value comes from whichever GL entry point wrote it.
|
||||
enum class BorderColorDomain {
|
||||
Float,
|
||||
SignedInteger,
|
||||
UnsignedInteger
|
||||
};
|
||||
|
||||
BorderColorDomain ResolveBorderColorDomain(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::R32I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RG32I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGB32I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
case TextureInternalFormat::RGBA32I:
|
||||
return BorderColorDomain::SignedInteger;
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return BorderColorDomain::UnsignedInteger;
|
||||
default:
|
||||
return BorderColorDomain::Float;
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsSignedNormalizedFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8Snorm:
|
||||
case TextureInternalFormat::R16Snorm:
|
||||
case TextureInternalFormat::RG8Snorm:
|
||||
case TextureInternalFormat::RG16Snorm:
|
||||
case TextureInternalFormat::RGB8Snorm:
|
||||
case TextureInternalFormat::RGB16Snorm:
|
||||
case TextureInternalFormat::RGBA8Snorm:
|
||||
case TextureInternalFormat::RGBA16Snorm:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// GL 4.6 core 8.14.2: "The border values are clamped before they are used, according to the
|
||||
// format in which texture components are stored. For signed and unsigned normalized
|
||||
// fixed-point formats, border values are clamped to [-1,1] and [0,1] respectively. For
|
||||
// floating-point and integer formats, border values are clamped to the representable range of
|
||||
// the format." Every clause of that sentence is a real case here - the clamp is not just the
|
||||
// normalized one.
|
||||
//
|
||||
// Only the 32-bit float formats are genuinely unclamped: every finite float is representable
|
||||
// in them. Half-float has a finite maximum, and the two packed "float" formats are UNSIGNED,
|
||||
// so a negative border on them must come back as 0 rather than as a negative number the
|
||||
// driver delivers verbatim through VK_BORDER_COLOR_FLOAT_CUSTOM_EXT.
|
||||
struct FloatBorderRange {
|
||||
Bool clamped = true;
|
||||
Float minValue = 0.0f;
|
||||
Float maxValue = 1.0f;
|
||||
};
|
||||
|
||||
FloatBorderRange ResolveFloatBorderRange(TextureInternalFormat format, Bool isSignedNormalized) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R32F:
|
||||
case TextureInternalFormat::RG32F:
|
||||
case TextureInternalFormat::RGB32F:
|
||||
case TextureInternalFormat::RGBA32F:
|
||||
return {false, 0.0f, 0.0f};
|
||||
case TextureInternalFormat::R16F:
|
||||
case TextureInternalFormat::RG16F:
|
||||
case TextureInternalFormat::RGB16F:
|
||||
case TextureInternalFormat::RGBA16F:
|
||||
return {true, -65504.0f, 65504.0f};
|
||||
// Unsigned packed floats: no sign bit at all. 65024 is the largest 11-bit float; the
|
||||
// 10-bit blue channel tops out lower (64512) and RGB9E5 higher (65408), but the bound
|
||||
// that matters for correctness is the lower one, and a single conservative upper bound
|
||||
// costs nothing a real border colour will ever notice.
|
||||
case TextureInternalFormat::R11FG11FB10F:
|
||||
return {true, 0.0f, 64512.0f};
|
||||
case TextureInternalFormat::RGB9E5:
|
||||
return {true, 0.0f, 65408.0f};
|
||||
default:
|
||||
return {true, isSignedNormalized ? -1.0f : 0.0f, 1.0f};
|
||||
}
|
||||
}
|
||||
|
||||
// Per-component representable range of an integer texture format, as Int64 so that the whole
|
||||
// signed and unsigned 32-bit ranges are expressible in one type and the clamp can be written
|
||||
// once for both domains. Alpha is carried separately because RGB10_A2UI is the one format
|
||||
// whose alpha is narrower than its colour channels.
|
||||
struct IntegerBorderRange {
|
||||
Int64 rgbMin = 0;
|
||||
Int64 rgbMax = 0;
|
||||
Int64 alphaMin = 0;
|
||||
Int64 alphaMax = 0;
|
||||
};
|
||||
|
||||
IntegerBorderRange ResolveIntegerBorderRange(TextureInternalFormat format) {
|
||||
const auto uniform = [](Int64 low, Int64 high) { return IntegerBorderRange{low, high, low, high}; };
|
||||
switch (format) {
|
||||
case TextureInternalFormat::R8I:
|
||||
case TextureInternalFormat::RG8I:
|
||||
case TextureInternalFormat::RGB8I:
|
||||
case TextureInternalFormat::RGBA8I:
|
||||
return uniform(-128, 127);
|
||||
case TextureInternalFormat::R16I:
|
||||
case TextureInternalFormat::RG16I:
|
||||
case TextureInternalFormat::RGB16I:
|
||||
case TextureInternalFormat::RGBA16I:
|
||||
return uniform(-32768, 32767);
|
||||
case TextureInternalFormat::R8UI:
|
||||
case TextureInternalFormat::RG8UI:
|
||||
case TextureInternalFormat::RGB8UI:
|
||||
case TextureInternalFormat::RGBA8UI:
|
||||
return uniform(0, 255);
|
||||
case TextureInternalFormat::R16UI:
|
||||
case TextureInternalFormat::RG16UI:
|
||||
case TextureInternalFormat::RGB16UI:
|
||||
case TextureInternalFormat::RGBA16UI:
|
||||
return uniform(0, 65535);
|
||||
case TextureInternalFormat::R32UI:
|
||||
case TextureInternalFormat::RG32UI:
|
||||
case TextureInternalFormat::RGB32UI:
|
||||
case TextureInternalFormat::RGBA32UI:
|
||||
return uniform(0, 4294967295LL);
|
||||
case TextureInternalFormat::RGB10A2UI:
|
||||
return {0, 1023, 0, 3};
|
||||
default:
|
||||
// The signed 32-bit formats, and anything unexpected: the full int32 range, i.e. a
|
||||
// clamp that cannot alter a value the GL entry points could have carried.
|
||||
return uniform(-2147483648LL, 2147483647LL);
|
||||
}
|
||||
}
|
||||
|
||||
Bool IsDepthTextureFormat(TextureInternalFormat format) {
|
||||
switch (format) {
|
||||
case TextureInternalFormat::DepthComponent:
|
||||
@@ -221,24 +41,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
return std::fabs(lhs - rhs) <= 1e-6f;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMaxLod(const SamplerSource& sampler) {
|
||||
#else
|
||||
Float ResolveEffectiveMaxLod(const MG_State::GLState::SamplerObject& sampler) {
|
||||
#endif
|
||||
if (sampler.GetMipmapMode() == SamplerMipmapMode::None) {
|
||||
return 0.0f;
|
||||
}
|
||||
return sampler.GetMaxLod();
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveEffectiveMinLod(const SamplerSource& sampler, Float effectiveMaxLod) {
|
||||
#else
|
||||
Float ResolveEffectiveMinLod(const MG_State::GLState::SamplerObject& sampler, Float effectiveMaxLod) {
|
||||
#endif
|
||||
return std::min(sampler.GetMinLod(), effectiveMaxLod);
|
||||
}
|
||||
|
||||
@@ -249,12 +59,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// GL's non-mipmapped minification - large enough for lambda to stay positive, small enough
|
||||
// that a NEAREST mip mode still rounds down to level 0. Clamped rather than assigned, so a
|
||||
// texture whose GL_TEXTURE_MAX_LOD really is 0 keeps magnifying as GL says it must.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float ResolveSingleLevelMaxLod(const SamplerSource& sampler, Bool singleLevelView) {
|
||||
#else
|
||||
Float ResolveSingleLevelMaxLod(const MG_State::GLState::SamplerObject& sampler, Bool singleLevelView) {
|
||||
#endif
|
||||
const Float maxLod = ResolveEffectiveMaxLod(sampler);
|
||||
return singleLevelView ? std::min(maxLod, 0.25f) : maxLod;
|
||||
}
|
||||
@@ -267,20 +72,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_config = initInfo.config;
|
||||
m_samplerAnisotropySupported = initInfo.samplerAnisotropySupported;
|
||||
m_maxSamplerAnisotropy = std::max(initInfo.maxSamplerAnisotropy, 1.0f);
|
||||
m_customBorderColorSupported = initInfo.customBorderColorSupported;
|
||||
m_maxCustomBorderColorSamplers = initInfo.maxCustomBorderColorSamplers;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE && m_config != nullptr,
|
||||
"VkSamplerManager::Initialize failed: invalid initialization info");
|
||||
return true;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const SamplerSource& sampler,
|
||||
#else
|
||||
Float VkSamplerManager::ResolveEffectiveMaxAnisotropy(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering) const {
|
||||
if (!m_samplerAnisotropySupported) return 1.0f;
|
||||
if (forceNearestFiltering) return 1.0f;
|
||||
@@ -305,9 +102,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
m_device = VK_NULL_HANDLE;
|
||||
m_config = nullptr;
|
||||
m_frameBoundaryCounter = 0;
|
||||
m_customBorderColorSupported = false;
|
||||
m_maxCustomBorderColorSamplers = 0;
|
||||
m_customBorderColorSamplerCount = 0;
|
||||
}
|
||||
|
||||
void VkSamplerManager::OnFrameBoundary() {
|
||||
@@ -329,9 +123,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
if (m_device != VK_NULL_HANDLE && entry.handle != VK_NULL_HANDLE) {
|
||||
vkDestroySampler(m_device, entry.handle, nullptr);
|
||||
}
|
||||
if (entry.usesCustomBorderColor && m_customBorderColorSamplerCount > 0) {
|
||||
--m_customBorderColorSamplerCount;
|
||||
}
|
||||
it = m_samplers.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -339,14 +130,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const SamplerSource& sampler,
|
||||
#else
|
||||
Uint64 VkSamplerManager::BuildSamplerKey(const MG_State::GLState::SamplerObject& sampler,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Bool singleLevelView,
|
||||
const ResolvedBorderColor& borderColor) const {
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Bool singleLevelView) const {
|
||||
MOBILEGL_ASSERT(m_config != nullptr, "VkSamplerManager::BuildSamplerKey: m_config is null");
|
||||
XXHASH_VERIFY(XXH64_reset(m_hashState, m_config->CacheVersion));
|
||||
|
||||
@@ -380,24 +166,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareMode, sizeof(compareMode)));
|
||||
const auto compareFunc = sampler.GetSamplerCompareFunc();
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &compareFunc, sizeof(compareFunc)));
|
||||
// The resolved enum AND, when it is one of the *_CUSTOM_EXT values, the sixteen bytes of the
|
||||
// colour itself: two samplers that differ only in a custom border colour carry the same enum
|
||||
// and would otherwise collide onto whichever one was created first.
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.color, sizeof(borderColor.color)));
|
||||
if (borderColor.isCustom) {
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor.customValue, sizeof(borderColor.customValue)));
|
||||
}
|
||||
const auto borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
XXHASH_VERIFY(XXH64_update(m_hashState, &borderColor, sizeof(borderColor)));
|
||||
return XXH64_digest(m_hashState);
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerImpl(const SamplerSource& sampler,
|
||||
TextureInternalFormat format,
|
||||
#else
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
#endif
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
// A view that exposes a single mip level has no second level to blend with, so GL's
|
||||
// *_MIPMAP_* minification filters degenerate to plain filtering on the base level -
|
||||
@@ -408,13 +183,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// allocation for a genuinely single-level image) and faults the GPU - the same failure
|
||||
// the default-framebuffer blit shader had to work around with an explicit-LOD sample.
|
||||
const Bool singleLevelView = viewLevelCount == 1;
|
||||
// Resolved once and used for both the key and the create-info; see ResolvedBorderColor.
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, format);
|
||||
#else
|
||||
const ResolvedBorderColor borderColor = ResolveBorderColor(sampler, texture);
|
||||
#endif
|
||||
const Uint64 key = BuildSamplerKey(sampler, forceNearestFiltering, singleLevelView, borderColor);
|
||||
const Uint64 key = BuildSamplerKey(sampler, texture, forceNearestFiltering, singleLevelView);
|
||||
auto it = m_samplers.find(key);
|
||||
if (it != m_samplers.end()) {
|
||||
it->second.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
@@ -442,21 +211,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
// Must match BuildSamplerKey's resolution exactly.
|
||||
samplerInfo.maxLod = ResolveSingleLevelMaxLod(sampler, singleLevelView);
|
||||
samplerInfo.minLod = ResolveEffectiveMinLod(sampler, samplerInfo.maxLod);
|
||||
samplerInfo.borderColor = borderColor.color;
|
||||
samplerInfo.borderColor = ResolveVkBorderColor(sampler, texture);
|
||||
samplerInfo.unnormalizedCoordinates = VK_FALSE;
|
||||
|
||||
// VK_EXT_custom_border_color. `format` stays UNDEFINED, which is legal only because
|
||||
// customBorderColorWithoutFormat was required alongside customBorderColors at device
|
||||
// creation - a GL sampler object has no idea which texture it will be paired with.
|
||||
VkSamplerCustomBorderColorCreateInfoEXT customBorderColorInfo{};
|
||||
if (borderColor.isCustom) {
|
||||
customBorderColorInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CUSTOM_BORDER_COLOR_CREATE_INFO_EXT;
|
||||
customBorderColorInfo.customBorderColor = borderColor.customValue;
|
||||
customBorderColorInfo.format = VK_FORMAT_UNDEFINED;
|
||||
customBorderColorInfo.pNext = samplerInfo.pNext;
|
||||
samplerInfo.pNext = &customBorderColorInfo;
|
||||
}
|
||||
|
||||
VkSampler vkSampler = VK_NULL_HANDLE;
|
||||
VK_VERIFY(vkCreateSampler(m_device, &samplerInfo, nullptr, &vkSampler), "vkCreateSampler(texture)");
|
||||
|
||||
@@ -465,28 +222,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
entry.externalIndex = sampler.GetExternalIndex();
|
||||
entry.version = sampler.GetVersion();
|
||||
entry.lastUsedFrameBoundary = m_frameBoundaryCounter;
|
||||
entry.usesCustomBorderColor = borderColor.isCustom;
|
||||
if (entry.usesCustomBorderColor) {
|
||||
++m_customBorderColorSamplerCount;
|
||||
}
|
||||
m_samplers[key] = entry;
|
||||
return vkSampler;
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
VkSampler VkSamplerManager::GetOrCreateSampler(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture,
|
||||
Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(sampler, texture.GetFormat(), forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
|
||||
VkSampler VkSamplerManager::GetOrCreateSamplerFromParameters(const SamplerParameters& parameters,
|
||||
TextureInternalFormat format, Bool forceNearestFiltering, Uint32 viewLevelCount) {
|
||||
return GetOrCreateSamplerImpl(SamplerParametersSource{parameters}, format,
|
||||
forceNearestFiltering, viewLevelCount);
|
||||
}
|
||||
#endif
|
||||
|
||||
VkFilter VkSamplerManager::ToVkFilter(SamplerFilterMode mode) {
|
||||
return mode == SamplerFilterMode::Nearest ? VK_FILTER_NEAREST : VK_FILTER_LINEAR;
|
||||
}
|
||||
@@ -542,156 +281,39 @@ namespace MobileGL::MG_Backend::DirectVulkan {
|
||||
}
|
||||
}
|
||||
|
||||
#if MOBILEGL_BUILD_DISAGGREGATED
|
||||
template <class SamplerSource>
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const SamplerSource& sampler, TextureInternalFormat format) const {
|
||||
#else
|
||||
VkSamplerManager::ResolvedBorderColor VkSamplerManager::ResolveBorderColor(
|
||||
const MG_State::GLState::SamplerObject& sampler, const MG_State::GLState::ITextureObject& texture) const {
|
||||
#endif
|
||||
ResolvedBorderColor resolved{};
|
||||
VkBorderColor VkSamplerManager::ResolveVkBorderColor(const MG_State::GLState::SamplerObject& sampler,
|
||||
const MG_State::GLState::ITextureObject& texture) {
|
||||
if (!UsesBorderColor(sampler)) {
|
||||
return resolved; // FLOAT_TRANSPARENT_BLACK, never sampled
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
|
||||
// Border colour is sampler state: a bound sampler object supplies its own, and a texture
|
||||
// with none reaches the very same value through the sampler object it owns.
|
||||
#if !MOBILEGL_BUILD_DISAGGREGATED
|
||||
const auto format = texture.GetFormat();
|
||||
#endif
|
||||
const auto domain = ResolveBorderColorDomain(format);
|
||||
const Bool canUseCustom = m_customBorderColorSupported && m_maxCustomBorderColorSamplers > 0 &&
|
||||
m_customBorderColorSamplerCount < m_maxCustomBorderColorSamplers;
|
||||
const auto& borderColor = sampler.GetBorderColor();
|
||||
const Bool isDepthTexture = IsDepthTextureFormat(texture.GetFormat());
|
||||
|
||||
if (domain != BorderColorDomain::Float) {
|
||||
// An integer image view REQUIRES an integer border colour, whatever the value is - even
|
||||
// (0,0,0,1). The value itself is whichever integer form the application wrote; a float
|
||||
// border on an integer texture is nonsense GL leaves undefined, so the derived integer
|
||||
// representation (a plain cast) is as good an answer as any.
|
||||
//
|
||||
// Clamped to the format's representable range FIRST, per GL 4.6 core 8.14.2, and read
|
||||
// through Int64 so the whole signed and unsigned 32-bit ranges are expressible at once.
|
||||
//
|
||||
// Which representation to start from is the TEXTURE's domain, not the entry-point form
|
||||
// the application used. GL 4.6 core 8.10 stores an "I"-form border colour unmodified with
|
||||
// an integer internal data type and does not define a sign conversion between the two
|
||||
// integer forms, so the stored bits are reinterpreted in the sampled format's own
|
||||
// signedness. Measured, not assumed: a border of -1 written with glTexParameterIiv
|
||||
// against a GL_R8UI texture samples as 255 on the ES driver, i.e. as 0xFFFFFFFF clamped
|
||||
// to the format's maximum - see the IntegerBorderColorScenario case that pins it. Picking
|
||||
// the representation by the FORM instead would answer 0 here, which is a defensible
|
||||
// reading of the same spec text but puts DirectVulkan at odds with DirectGLES - and
|
||||
// DirectGLES cannot deviate, it forwards the value to the driver verbatim. Cross-backend
|
||||
// agreement decides it.
|
||||
const auto range = ResolveIntegerBorderRange(format);
|
||||
const auto& borderColorI = sampler.GetBorderColorI();
|
||||
const auto& borderColorUI = sampler.GetBorderColorUI();
|
||||
const Bool startFromUnsigned = domain == BorderColorDomain::UnsignedInteger;
|
||||
Int64 clamped[4];
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
const Int64 raw = startFromUnsigned ? static_cast<Int64>(borderColorUI[channel])
|
||||
: static_cast<Int64>(borderColorI[channel]);
|
||||
const Int64 low = channel == 3 ? range.alphaMin : range.rgbMin;
|
||||
const Int64 high = channel == 3 ? range.alphaMax : range.rgbMax;
|
||||
clamped[channel] = std::clamp(raw, low, high);
|
||||
}
|
||||
|
||||
// Matched against the CLAMPED value, so a border the format cannot hold still lands on
|
||||
// the palette entry it clamps to rather than missing every one of them.
|
||||
const Bool allZeroRgb = clamped[0] == 0 && clamped[1] == 0 && clamped[2] == 0;
|
||||
if (allZeroRgb && clamped[3] == 0) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (allZeroRgb && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
}
|
||||
if (clamped[0] == 1 && clamped[1] == 1 && clamped[2] == 1 && clamped[3] == 1) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
}
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_INT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
for (SizeT channel = 0; channel < 4; ++channel) {
|
||||
if (domain == BorderColorDomain::UnsignedInteger) {
|
||||
resolved.customValue.uint32[channel] = static_cast<Uint32>(clamped[channel]);
|
||||
} else {
|
||||
resolved.customValue.int32[channel] = static_cast<Int32>(clamped[channel]);
|
||||
}
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
// No custom colour available: pick the nearest of the three integer palette entries
|
||||
// rather than always answering transparent black, which is what turned an integer border
|
||||
// of (-1,-1,-1,-1) into 0 and broke the CTS's clamped-texel detection outright.
|
||||
const Bool opaque = clamped[3] != 0;
|
||||
const Bool bright = clamped[0] != 0 || clamped[1] != 0 || clamped[2] != 0;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_INT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_INT_OPAQUE_WHITE : VK_BORDER_COLOR_INT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Float domain. GL 4.6 core 8.14.2/8.23: the border colour is interpreted in the texture's
|
||||
// format, so it is clamped to that format's representable range first. Without the clamp the
|
||||
// CTS's border of (255,255,255,255) on a GL_RGBA8 texture matched none of the palette entries
|
||||
// and fell through to transparent black - every border texel sampled 0 where the test wanted
|
||||
// 255. The range is per format class, not just the normalized [0,1] / [-1,1] pair: only the
|
||||
// 32-bit float formats are unclamped.
|
||||
FloatVec4 borderColor = sampler.GetBorderColor();
|
||||
if (const auto range = ResolveFloatBorderRange(format, IsSignedNormalizedFormat(format)); range.clamped) {
|
||||
borderColor = FloatVec4(std::clamp(borderColor.x(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.y(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.z(), range.minValue, range.maxValue),
|
||||
std::clamp(borderColor.w(), range.minValue, range.maxValue));
|
||||
}
|
||||
|
||||
// A depth texture samples one component, so only x decides - and its alpha reads as 1.
|
||||
if (IsDepthTextureFormat(format)) {
|
||||
if (isDepthTexture) {
|
||||
if (NearlyEqual(borderColor.x(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 0.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
const Bool rgbZero = NearlyEqual(borderColor.x(), 0.0f) && NearlyEqual(borderColor.y(), 0.0f) &&
|
||||
NearlyEqual(borderColor.z(), 0.0f);
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 0.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
if (rgbZero && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK;
|
||||
}
|
||||
if (NearlyEqual(borderColor.x(), 1.0f) && NearlyEqual(borderColor.y(), 1.0f) &&
|
||||
NearlyEqual(borderColor.z(), 1.0f) && NearlyEqual(borderColor.w(), 1.0f)) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
|
||||
}
|
||||
|
||||
if (canUseCustom) {
|
||||
resolved.color = VK_BORDER_COLOR_FLOAT_CUSTOM_EXT;
|
||||
resolved.isCustom = true;
|
||||
resolved.customValue.float32[0] = borderColor.x();
|
||||
resolved.customValue.float32[1] = borderColor.y();
|
||||
resolved.customValue.float32[2] = borderColor.z();
|
||||
resolved.customValue.float32[3] = borderColor.w();
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Nearest of the three float palette entries. Transparent black stays the answer for a
|
||||
// transparent border, which is what the old unconditional fallback got right by accident.
|
||||
const Bool opaque = borderColor.w() >= 0.5f;
|
||||
const Bool bright = (borderColor.x() + borderColor.y() + borderColor.z()) >= 1.5f;
|
||||
resolved.color = !opaque ? VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK
|
||||
: (bright ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE : VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
|
||||
return resolved;
|
||||
return VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK;
|
||||
}
|
||||
} // namespace MobileGL::MG_Backend::DirectVulkan
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user