Compare commits

..
Author SHA1 Message Date
BZLZHH 24dfbb41f9 [Fix] (Diligent): fix y inversion and front-face winding in viewport mapping
- Translate OpenGL bottom-left viewport/scissor rects to Diligent top-left origin
- Invert FrontCounterClockwise to compensate for the Y conversion
- Add DrawsTopHalfTriangleFromMobileGLState y-orientation test
2026-08-23 12:25:41 +08:00
BZLZHH ca7878bf3a [Feat] (AndroidPlugin): expose DiligentVulkan backend in plugin selector 2026-08-23 11:11:20 +08:00
BZLZHH e418063b08 [Feat] (Diligent, EGL): forward eglSwapInterval to swap chain Present
- Renderer stores the requested swap interval and passes it to ISwapChain::Present
- SetSwapInterval now updates the active renderer instead of being a no-op
2026-08-23 11:01:40 +08:00
BZLZHH b43ec25bd7 [Feat] (Diligent, EGL): validate and defer EGL surface activation
- CreateEGLWindowSurface/CreateEGLPbufferSurface register surfaces only; activation happens on eglMakeCurrent
- Reject unsupported native window backends
2026-08-23 10:57:09 +08:00
BZLZHH 525607bad6 [Feat] (Diligent, EGL): release swap chain on EGL surface release 2026-08-23 10:55:40 +08:00
BZLZHH b045024b6c [Feat] (Diligent, EGL): route EGL window resize to Diligent swap chain
- Add renderer->ResizeSwapChain and override BackendObject_Diligent::ResizeEGLWindowSurface
- Update handoff; 16 Diligent tests pass
2026-08-23 10:53:19 +08:00
BZLZHH 14dfbeeed9 [Feat] (Diligent, EGL): wire EGL window swapchain creation via Diligent ISwapChain
- Add CreateSwapChain to renderer using IEngineFactoryVk::CreateSwapChainVk
- InitWindowSurface creates the swapchain for native window surfaces
- Present now presents the active swapchain; ReleaseEGLResources releases it
- InitPbufferSurface keeps offscreen target for pbuffer EGL surfaces
- Update handoff; 16 Diligent tests pass
2026-08-23 10:46:06 +08:00
BZLZHH d7e79409b3 [Feat] (Diligent): wire SetSwapInterval no-op
- GlobalBackendFunctionsTable.SetSwapInterval is now present; offscreen renderer ignores it
- Update handoff; 16 Diligent tests pass
2026-08-23 10:16:56 +08:00
BZLZHH ca3b524396 [Feat] (Diligent): wire CPU timer query fallback
- Add steady_clock based BeginTimeElapsedQuery/EndTimeElapsedQuery/QueryCounterTimestamp
- Wire IsTimerQuerySupported/IsQueryResultAvailable/GetQueryResult64/DeleteBackendQuery
- Update handoff; 16 Diligent tests pass
2026-08-23 10:15:19 +08:00
BZLZHH 071c8eb673 [Feat] (Diligent): wire fence sync CPU fallback
- Provide always-signaled FenceSync/ClientWaitSync/WaitSync/DeleteSync/GetSyncStatus
- Update handoff; 16 Diligent tests pass
2026-08-23 10:12:58 +08:00
BZLZHH 51a43518ac [Feat] (Diligent, MG_Impl): wire BlitNamedFramebuffer color copy
- Explicit read/draw FBO color attachments resolve to Diligent textures/renderbuffers
- CopyTexture between them for same-size color blits
- Update handoff; 16 Diligent tests pass
2026-08-23 10:11:17 +08:00
BZLZHH 34ff95f6f5 [Feat] (Diligent, MG_Impl): wire GenerateMipmap via Diligent GPU mip generation
- Create state textures with MISC_TEXTURE_FLAG_GENERATE_MIPS
- GenerateMipmap resolves active GL_TEXTURE_2D and calls IDeviceContext::GenerateMips
- Update handoff; 16 Diligent tests pass
2026-08-23 10:08:54 +08:00
BZLZHH b9d1504cc5 [Feat] (Diligent, MG_Impl): wire CopyImageSubData whole-texture copy
- CopyImageSubData syncs both texture objects and issues a Diligent CopyTexture
- Update handoff; 16 Diligent tests pass
2026-08-23 10:05:50 +08:00
BZLZHH a223499143 [Feat] (Diligent, MG_Impl): wire ClearBufferfi and stencil clears through ClearBufferiv/uiv
- ClearBufferfi clears depth+stencil on the current draw framebuffer
- ClearBufferiv/uiv now support GL_STENCIL via ClearStencil
- Update handoff; 16 Diligent tests pass
2026-08-23 10:04:14 +08:00
BZLZHH 5dca617f01 [Feat] (Diligent, MG_Impl): honor DrawElementsBaseVertex baseVertex in CPU vertex packing
- Add baseVertex parameter through DrawFromState/UploadVertexDataFromState
- Apply baseVertex when resolving indexed vertex indices
- Pass baseVertex through DrawElementsBaseVertex, instanced, and indirect indexed draws
- Add DrawsIndexedBaseVertexFromMobileGLState; 16 Diligent tests pass
2026-08-23 10:02:44 +08:00
BZLZHH eb8ef893be [Feat] (Diligent): support multiple simultaneous color attachments
- PSO RTV count/formats now derive from the bound draw FBO color attachments
- Include RT layout in the last-PSO cache key
- Add DrawsToMultipleColorAttachmentsFromMobileGLState; 15 Diligent tests pass
2026-08-23 09:56:58 +08:00
BZLZHH 20567fba6d [Feat] (Diligent, MG_State): cache renderbuffer resources and support color readback
- SyncRenderbuffer now reuses the cached Diligent texture so Clear/Draw/ReadPixels target the same resource
- ReadPixels resolves renderbuffer color attachments from the read FBO
- Add DrawsToRenderbufferFramebufferFromMobileGLState; 14 Diligent tests pass
2026-08-23 09:54:08 +08:00
BZLZHH e3f44e8da1 [Feat] (Diligent, MG_Impl): add indirect draw CPU fallbacks
- Wire DrawArraysIndirect/DrawElementsIndirect
- Wire MultiDraw*Indirect and *IndirectCount using client memory or GL_DRAW_INDIRECT_BUFFER/GL_PARAMETER_BUFFER CPU reads
- Update handoff; 13 Diligent tests pass
2026-08-23 09:45:34 +08:00
BZLZHH 9f79a88af5 [Feat] (Diligent, MG_Impl): wire GetTexImage/GetTextureImage RGBA8 readback
- Copy texture to staging and map rows for GL_RGBA/GL_UNSIGNED_BYTE
- Wire both GLFunctionsTable entries; 13 Diligent tests pass
2026-08-23 09:41:17 +08:00
BZLZHH 6bf32acdef [Feat] (Diligent, MG_Impl): wire CopyTexImage2D/CopyTexSubImage2D readback copy
- Copy current read-FBO color attachment into the bound GL_TEXTURE_2D
- Uses whole-color CopyTexture fallback for now; 13 Diligent tests pass
2026-08-23 09:39:40 +08:00
BZLZHH 23b53eacce [Feat, Test] (Diligent, MG_Test): add stencil clear and stencil state test
- Use D24S8 for the default offscreen depth/stencil target
- Wire GL_STENCIL_BUFFER_BIT Clear through renderer->ClearStencil
- Add DrawsWithStencilTestFromMobileGLState; 13 Diligent tests pass
2026-08-23 09:37:49 +08:00
BZLZHH e829e70d8b [Feat] (Diligent, MG_State): bind named application uniform blocks from frontend buffers
- Resolve named UBOs through SPIRV-Reflect type names when block names are empty
- Read the bound GL buffer range at the frontend uniform-block binding point and upload it as a Diligent uniform buffer
- Add DrawsNamedUniformBlockFromMobileGLState test; 12 Diligent tests pass
2026-08-23 09:35:32 +08:00
BZLZHH 7e765e1535 [Test] (Diligent, MG_Test): add depth test and ensure default framebuffer isolation
- Verify nearer depth draw occludes farther draw
- Bind default framebuffer at test start so previous FBO state cannot leak
- Update handoff to 11 passing Diligent tests
2026-08-23 09:23:06 +08:00
BZLZHH bf6061811f [Test] (Diligent, MG_Test): add scissor and blend state tests
- Verify scissor clipping leaves outside pixels untouched
- Verify alpha blend combines source/destination colors
- Update handoff to 10 passing Diligent tests
2026-08-23 09:17:41 +08:00
BZLZHH 87750c3b21 [Feat] (Diligent): add instanced/clear-buffer/blit GL entry points
- Wire DrawElementsBaseVertex, instanced draw family, MultiDrawElementsBaseVertex
- Wire ClearBufferfv/iv/uiv to the Diligent clear path
- Add same-size color BlitFramebuffer between current read/draw framebuffers
- Update handoff with newly implemented GL 3.2 entry points
2026-08-23 09:15:21 +08:00
BZLZHH 45b309db37 [Feat] (Diligent, MG_Impl): wire GL ReadPixels entry to offscreen/user-FBO readback 2026-08-23 09:11:56 +08:00
BZLZHH 403c82ac4a [Feat, Docs] (Diligent): cache last PSO and document framebuffer/UBO progress
- Reuse the last state PSO when program/render-state/topology/VAO layout is unchanged
- Update handoff with completed texture/sampler, UBO, framebuffer, and multi-draw work
2026-08-23 09:10:53 +08:00
BZLZHH 827d46cad3 [Feat] (Diligent, MG_State): wire textures, samplers, global UBO, and user framebuffers
- Auto-sync ITextureObject to Diligent textures with dirty-level uploads
- Translate SamplerObject/unit sampler state into Diligent samplers
- Bind the synthesized MGL_GLOBAL_UBO for default-block glUniform data
- Resolve bound draw/read framebuffers to Diligent RTV/DSV for draws/clears/readback
- Wire DrawRangeElements, DrawRangeElementsBaseVertex, MultiDrawArrays, MultiDrawElements
- Add real-texture, uniform, and user-framebuffer tests; 8 Diligent tests pass
2026-08-23 09:09:03 +08:00
BZLZHH 1748da0443 [Docs] (Diligent): add Diligent GL3.2 backend handoff document 2026-08-23 08:36:57 +08:00
BZLZHH 7efee8e3e6 [Test] (Diligent, MG_Test): verify indexed DrawElements path from real frontend state
Add DrawsIndexedFromMobileGLState: creates a GL program, VBO, EBO and VAO
through the frontend, then draws via DrawFromState(GL_TRIANGLES, ...,
GL_UNSIGNED_INT, nullptr) and verifies the offscreen center is red.

All 5 Diligent local tests pass.
2026-08-18 14:22:20 +08:00
BZLZHH 08ca897a07 [Feat, Test] (Diligent, MG_Test): add basic texture binding and textured state-draw test
- Add CreateTestTexture(): creates an RGBA8 texture, SRV and default sampler,
  and attaches the sampler to the SRV.
- State PSOs now bind a static pixel-shader variable 'g_Texture' to the test
  texture and commit shader resources before drawing.
- Add DrawsTexturedFromMobileGLState test using a real GL program with
  sampler2D and interleaved position+UV attributes.
- All 4 Diligent local tests pass on Turnip Adreno 750.
2026-08-18 14:20:02 +08:00
BZLZHH 98f2a55214 [Feat] (Diligent, MG_Impl): clear depth in GL Clear when GL_DEPTH_BUFFER_BIT set 2026-08-18 13:38:44 +08:00
BZLZHH cedc257566 [Feat] (Diligent): add offscreen depth target and depth clear
The renderer now creates a D32_FLOAT depth target and binds it as DSV for all
render passes, so depth-test state wired earlier can actually work. Add
ClearDepth for depth clears. All Diligent local tests still pass.
2026-08-18 13:34:37 +08:00
BZLZHH 821c0e0d4e [Feat] (Diligent): wire stencil and color-mask state into state PSO
CreatePipelineFromState now applies GL_STENCIL_TEST state (front/back funcs,
ops, read/write masks) and GL color write mask. DrawFromState sets the stencil
reference before drawing. All Diligent local tests still pass.
2026-08-18 13:31:13 +08:00
BZLZHH bd9680ad67 [Feat] (Diligent): wire viewport/scissor state into state draws
DrawFromState now uses MG_State viewport (with full-target fallback when the
viewport is uninitialized) and applies the scissor test rect when enabled.
The state-driven test sets an explicit glViewport and passes again.
2026-08-18 13:26:20 +08:00
BZLZHH 6375e07030 [Feat] (Diligent): wire blend/depth/cull render state into state PSO
CreatePipelineFromState now reads MG_State render state:
- GL_BLEND enable, blend factors/equations
- GL_DEPTH_TEST enable, depth func, depth write mask
- GL_CULL_FACE enable, cull face mode, front-face winding

All Diligent local tests still pass.
2026-08-18 13:16:55 +08:00
BZLZHH 53cac39d4e [Test] (Diligent, MG_Test): verify state-driven draw with real MobileGL frontend state
Add DrawsFromMobileGLState test that creates a GL 3.2 program, buffer and VAO
through the real frontend, then draws through DiligentRenderer::DrawFromState
and verifies the offscreen pixels.

Also release PSO/vertex buffer before recreation to avoid Diligent debug
assertions about overwriting references.

All 3 Diligent local tests pass on Turnip Adreno 750.
2026-08-18 13:13:10 +08:00
BZLZHH f6b1ea635b [Feat] (Diligent, MG_State): add state-driven draw path (VAO/buffer/program to Diligent)
DiligentRenderer now has DrawFromState() that:
- reads the current MobileGL program SPIR-V and creates Diligent shaders
- reads the current VAO enabled attributes and packs bound buffer data into
  an interleaved vertex buffer
- creates a PSO with the matching input layout and primitive topology
- supports DrawArrays, DrawElements, triangle-fan and line-loop expansion

This is the first real front-end state wiring; it compiles and is used by
GLFunctionsTable DrawArrays/DrawElements, but is not yet covered by a
runtime state-driven test.
2026-08-18 13:08:23 +08:00
BZLZHH 8b2711e32a [Feat] (Diligent): add dynamic vertex buffer upload path
DiligentRenderer can now upload arbitrary vec2 vertex data into a dynamic
vertex buffer and draw it with the existing triangle PSO. The local sanity
test uses this path instead of the hardcoded triangle, verifying buffer
basics on Turnip Adreno 750.
2026-08-18 13:01:29 +08:00
BZLZHH 9776cc8047 [Feat] (Diligent, MG_Backend): wire Clear/Draw/Present into GLFunctionsTable
BackendObject_Diligent now exposes real function-table entries backed by the
DiligentRenderer: Clear reads the current GL clear color from MG_State, and
DrawArrays/DrawElements currently render the built-in triangle as a
placeholder until buffer/VAO/program state is connected. Present flushes the
immediate context.

Local Diligent tests still pass on Turnip Adreno 750.
2026-08-18 12:57:06 +08:00
BZLZHH 3b0591e0ba [Feat] (Diligent): add real offscreen renderer with clear and triangle draw
- Add DiligentRenderer: creates an offscreen RGBA8 render target, compiles a
  GLSL vertex/pixel shader through Diligent's glslang path, creates a triangle
  vertex buffer and pipeline, and supports clear/draw/readback.
- BackendObject_Diligent now owns a DiligentRenderer after device creation.
- Extend local sanity test to clear green, draw a red triangle, and verify
  center is red and corner stays green.
- All Diligent local tests pass on Turnip Adreno 750.
2026-08-18 12:54:10 +08:00
BZLZHH e62f158c22 [Feat] (Diligent, CMake): enable DiligentCore and add initial Diligent/Vulkan backend skeleton
- Add MOBILEGL_ENABLE_DILIGENT option; build DiligentCore Vulkan-only after
  MobileGL's existing 3rdparty targets so shared glslang/SPIRV-Cross/xxHash
  targets are reused instead of duplicated.
- Add BackendType::DiligentVulkan, config parsing, and backend-object switch.
- Add DiligentBackend::BackendObject_Diligent skeleton: creates a Diligent
  Vulkan device/context when an adapter is available, advertises GL 3.2 core,
  and returns an empty GL function table for now.
- Add local DiligentVulkanSanityTest that compiles/runs on the host (skips
  device creation gracefully when no Vulkan adapter is present).
- Fix GLXImpl EGLDisplay member shadowing the X11 Display typedef, exposed by
  GCC 16 + Diligent header include order.
2026-08-18 12:42:17 +08:00
1979 changed files with 11666 additions and 858526 deletions
@@ -1,3 +0,0 @@
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
* text=auto eol=lf
@@ -1,183 +0,0 @@
---
name: anland-mobilegl-plasma
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
---
# MobileGL on anland KDE Plasma
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
Human-readable runbook (Chinese, same content): `docs/Disaggregated/notes/anland/runbook-plasma.md`.
Background: `docs/Disaggregated/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
parameters are documented at the top of each script and in `scripts/env.sh`.
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
## Architecture (10 lines)
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl` from the first attached window on. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running" / action "Stop desktop"), so the server and the session outlive a hidden, locked or swiped-away window.
3. Backend = the app setting "Renderer backend" (Settings > Connection > MobileGL desktop; file `/data/data/<pkg>/files/mobilegl-backend`, default DirectGLES; also `am start ... --es mobilegl_backend X`). Espryt = DirectGLES, Magma = DirectVulkan. The server reads it when its process starts; the app publishes it as `/data/local/tmp/anland-mobilegl/backend` and the session copies that into the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend"). `debug.mobilegl.backend`, if someone sets it, still overrides the setting until the next reboot.
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker), started by the app (below). The module only starts the original one on `/data/local/tmp/display_daemon.sock` at boot; leave that alone.
5. Container `arch-kde-mgl` (Droidspaces 6.4.5, Arch Linux ARM aarch64 glibc, host net, `rootfs.img` mounted at `/mnt/Droidspaces/arch-kde-mgl` on the Android side). Bind mounts: `/data/local/tmp/anland-mobilegl` -> `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` -> `/run/display.sock`.
6. `desktop-session.service` (User=swung0x48, **enabled**: the container boots straight into it, as the original image does) has drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed + enabled by `sync-build-kwin.sh`): `ExecStartPre=mobilegl-startup.sh wait` (until `@anland-mobilegl` listens in `/proc/net/unix` and the daemon socket exists), `ExecStartPre=+... sync-backend`, `Restart=always`, then `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` session-wide (MobileGL never declines, so glvnd never falls through to another GL stack), writes a user drop-in for `plasma-kwin_wayland.service` (kwin_wayland_wrapper -> `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`) and runs `startplasma-wayland`.
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. No per-process environment is needed.
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
10. X11 (`docs/Disaggregated/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
## First-time prerequisites (Windows host)
- Git Bash, `adb` (SDK platform-tools), CMake >= 3.22 and Ninja on PATH (seen: CMake 3.27.6, Ninja 1.13.2), Python 3 + Pillow (only `rec.sh`).
- Android NDK **27.2.12479018** at `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (override `ANDROID_NDK`).
- WSL distro **archlinux** with clang, lld, llvm, cmake, ninja. **lld's major version must equal clang/llvm-libs'** (lld 23 on llvm 22 broke `ld.lld`; working set: all 22.1.6). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (-> `~/sysroots/arch-kde-mgl` + `~/sysroots/aarch64-arch.cmake`; re-run after container package upgrades, then `xbuild.sh --reconfigure`).
- MobileGL worktree (never build in the shared main checkout): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, and copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule leaves it empty).
- anland worktree on `legacy-mobilegl-unified` (KWin patch/backend, startup script, `mobilegl-tools/`).
- Phone side that already exists and is NOT recreated by this skill: the APK (build: anland `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug` with `MOBILEGL_DIST`, `-PmobileglApplicationId=com.anland.consumer.mobilegl -PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock`, from Git Bash with `MSYS_NO_PATHCONV=1`; signed with this machine's `~/.android/debug.keystore`, so `adb install -r` works; reinstalling with another key changes the UID and needs the KernelSU grant again - `mobilegl-tools/ksu_grant.c`), the anland KernelSU modules (`anland-daemon`, `anland-awl`), the container with KWin's configured build tree `/root/mobilegl-build/kwin-6.7.4/build` and pristine `/root/gbm-dev/kwin-a/src`.
- Install the helpers once (container must be running): `bash scripts/push-tools.sh` (device/*.sh -> `/data/local/tmp/anl`, `mgrun` -> container `/usr/local/bin`, `kwprep.sh` -> container `/root`).
Run host scripts from Git Bash as `bash scripts/<x>.sh`. Raw adb with device paths needs `MSYS_NO_PATHCONV=1` and the whole root command in one quoted string: `adb shell 'su -c "..."'`. A device script can also be run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
## Starting, idling, stopping - no manual steps
Nothing starts at phone boot (container `run_at_boot=0`). **Opening the app** (launcher icon = `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`, no extras; the APK's default socket is the experiment's, `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` - the single trigger - which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): display daemon if absent (the script leaves the app's cgroup first, so it survives the app) -> backend published -> container started if stopped -> `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server listens once the Surface is attached; the session's `wait` step then lets Plasma start. A reopen with everything up only reattaches the window. Log: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
Hidden window, screen off, lock screen or swiped-away task: server and session stay (foreground service); KWin turns the workspace's DPMS off (logs "viewer gone: output powered down"; windows get xdg_toplevel `suspended`, no frame callbacks) and back on ("viewer back"). Client swaps are paced by frame callbacks (`MG_Impl/EGLImpl/FrameThrottle.h`): interval >= 1 waits like any Wayland EGL; interval 0 (Chrome) drops to ~1 fps once a callback goes unanswered for 200 ms.
Measured 2026-10-03 (Espryt, Chrome on webglsamples aquarium; % of one core, phone = all of Android): open-to-desktop cold 7.5-12.5 s (daemon 0.5, server 1.0, container 1.5, kwin ~5, plasmashell ~6-7), after a phone reboot 8.5 s, reopen of a running desktop 0.2 s; visible phone 249% / kwin 13 / chrome 99 / server 57 / GPU 22%, hidden 54 / 0 / 6 / 2 / 0%, locked 38 / 0 / 8 / 3 / 0%. Stop desktop 1.7 s. Wireless adb does not come back by itself after a reboot (re-enable it on the phone). **Stop desktop** (notification action, or `am startservice -n <pkg>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (session + container stopped, daemon stays), closes the windows, ends `:mobilegl`.
```sh
adb connect <ip>:<port> # only for wireless debugging
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, dismiss keyguard, open the app, print the timeline
bash scripts/desktop-verify.sh push cold reopen idle # timed cold start, swipe-away + reopen, CPU/GPU visible/hidden/locked
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` has no `status` command; `droidspaces show` lists running containers. `device/run-plasma.sh` is a developer restart (session + logind user, then the app), needed after a rebuild because KWin does not reconnect to a new server process. `device/desktop-reset.sh` takes everything down to the post-reboot state. The old manual sequence (setprop, start container, `restart-daemon.sh`, `run-plasma.sh`) is gone; `restart-daemon.sh` remains for daemon debugging only.
## Build and deploy loops
**Server (Android, NDK) -> APK lib**
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so (RelWithDebInfo, unstripped)
bash scripts/cyc.sh s1 # clear sticky SW-renderer key, push, swap into APK, restart session, screenshot $ANL_DIR/s1.png, print fatals
```
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` keeping owner/mode/SELinux context (original saved as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`); no re-signing. To swap without the session restart: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, then `run-plasma.sh` anyway (KWin does not reconnect to a new server process).
**Client + GBM backend (container glibc) via WSL cross-build**
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; ~150 s full, seconds incremental
bash scripts/xdeploy.sh # rename-into-place install of libMobileGL.so + mobilegl_gbm.so, aliases, vendor JSONs, client.conf, GLX/GBM env drop-ins
```
Running clients keep the old inode; restart the client, or `run-plasma.sh` for KWin/plasmashell. Build server and client from the **same commit** (both stamp `git rev-parse HEAD`), else the logs WARN "compatible wire with different build". Fallback in-container build (slow): `scripts/sync-src.sh` + anland `mobilegl-tools/anland-build-client.sh`.
**KWin (anland backend / kwin.patch) - built inside the container**
```sh
bash scripts/kwin-stage.sh build # stage CR-stripped patch+backend+startup script, kwprep.sh, background sync-build-kwin.sh
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # wait for "== ALL DONE =="
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
```
`sync-build-kwin.sh` (anland `producers/kde/Arch_v5/`) copies only the backend sources, then `ninja -j4`, installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` by atomic rename. `kwprep.sh` is what applies changed `kwin.patch` hunks to core KWin files. Repo-root `libdisplay_producer/` is not synced by either. `kwin-stage.sh` is assembled from those two scripts and was not run as one unit yet.
**Unit tests (no device)** - in WSL (the MSVC build of this branch is broken):
```sh
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` builds and runs every unit test. Never run DirectVulkan integration tests; never two ctest runs at once.
## Switching backends
In the app: Settings > Connection > MobileGL desktop > Renderer backend, then Stop desktop and open the app. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same (saves the setting through the launch extra, STOPs the service, reopens; clears a stale `debug.mobilegl.backend`). No setprop, no file to edit in the container: the session copies the published backend into `/etc/mobilegl/backend` on its next start. `cyc.sh` keeps whatever is set.
## System-wide vendor and fallback
Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any process in the container gets MobileGL through the stock libglvnd/libgbm:
| File | Does |
|---|---|
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor, asked before the system's `50_*.json` |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | same JSON, for per-process forcing with `__EGL_VENDOR_LIBRARY_FILENAMES` |
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint); rewritten on deploy |
| `/etc/mobilegl/backend` | the backend (the session's `sync-backend` step copies the app's choice in); wins over a `MOBILEGL_BACKEND_TYPE` line in client.conf |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` for the systemd user manager (all Plasma services and what they start, terminals included) |
| `/etc/profile.d/mobilegl.sh` | the same two for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created and for `LIBGL_ALWAYS_SOFTWARE=1` - that is how a software Xwayland (`ANLAND_XWAYLAND_GLAMOR=0`, KWin's patch sets it then) keeps the software stack. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
```sh
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor = the other one, ~0.1 s
```
## Test clients and how to verify
X11 on the GPU has its own driver, `scripts/x11-verify.sh` (preflight, measure before/after with glxgears at `vblank_mode=0` on both GLX paths (presentation throughput only: glxgears is fixed-function GL, which MobileGL does not implement, so its window is black by design), deploy-client/-kwin/-server, check, apps, stop); `scripts/device/x11-measure.sh` refuses to load the phone below 40% battery.
Inside the container as root (`printf '...' | bash scripts/ct.sh <chan>`), `mgrun offscreen <cmd>` runs a client as swung0x48 in the live session through a login shell, with nothing but the surface mode and the log path set (log `/tmp/mgl-run.client.log`, override base with `MGLOG=/tmp/x.log`); a plain `env -i ... bash -l -c '<cmd>'` as swung0x48 gets MobileGL the same way:
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built: `docs/MobileVA/plan.md`.
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
A result counts only if ALL hold:
- client log says `presented through linux-dmabuf shared images` (not `wl_shm`; `could not be presented through a linux-dmabuf shared image` = fell back);
- `bash scripts/shot.sh g 2 1` gives two different hashes while the client animates (a "fast" score can mean frames are not shown);
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean (a frozen window is usually a dead server);
- for plasmashell verdicts, the sticky software renderer is off (below) and `/tmp/mgl-plasmashell.client.log` is freshly written.
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. Rerun any surprising pass.
## Logs
| What | Where |
|---|---|
| KWin client side | container `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`, `/tmp/mgl-ksplash.client.log` (user drop-ins `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`, `/tmp/mgl-chrome-<tag>.client.log` (+ `/tmp/chrome-<tag>-out.log`) |
| Server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| Native crashes | `adb logcat -b crash -d`; container `coredumpctl list` |
| Session / KWin stderr | container `journalctl -u desktop-session --no-pager`, `journalctl --no-pager --since -2min \| grep kwin_wayland` |
| Display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
| Desktop start/stop (app's su script) | `/data/local/tmp/anland-mobilegl/desktop.log`, `container-start.log`; logcat `-s AnlandMobileGL` |
| Session wait / backend sync | container `journalctl -u desktop-session --no-pager` ("waiting for the MobileGL server ...") |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` produces `/tmp/x.client.log` (and `.server.log` for forwarded lines); files are truncated on open, so one base per process. Client builds log at INFO. Symbolize server frames with `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <offset>`.
## Pitfalls
- **pkill -f self-match**: a `ct.sh`/`droidspaces run bash -lc "<script>"` process carries the script in its argv; `pkill -f chrome` kills the runner. Use `pkill -x`.
- **Root-owned /tmp files**: a log created by a root-run client cannot be reopened by swung0x48 later (no log appears). `rm -f` it as root first.
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "正在使用的软件渲染器"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
- **Lock screen**: autolock is off (`kscreenlockerrc [Daemon] Autolock=false`); a real lock can only be left by typing the password (logind lock/unlock is not supported for this session type - `device/unlock.sh` is unverified). Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and kill it.
- **CRLF**: `core.autocrlf=true` here; anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Strip CR before anything runs on Linux/Android (`tr -d '\r'`; `ct.sh`, `push-tools.sh`, `kwin-stage.sh` do). This skill has a `.gitattributes` forcing LF.
- **Display daemon**: the app starts it; the session's `wait` step keeps KWin from starting without it (a KWin that cannot reach it logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits in ~3 s). Anything started from the app's `su` must leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`) or a force-stop kills it.
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
## House rules
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
- Commit with `git commit --no-verify`, one short sentence, **no** `Co-Authored-By` trailer.
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
@@ -1,183 +0,0 @@
# Reproducing the MobileGL + anland + KDE Plasma setup from source
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
day-to-day loop once everything below exists. Paths and names that are specific to the original
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
## 0. What you need
**Phone.**
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
- About 20 GB free for the container image.
**Development host.**
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
- Tools:
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
- Android NDK **27.2.12479018**.
- JDK 17 and Android SDK platform 36 (for the APK).
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
## 1. Sources
```sh
# MobileGL: branch feat/disaggregated (everything here is on it)
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
git checkout feat/disaggregated
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
git checkout legacy-mobilegl-unified
```
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
## 2. Phone base: root, Droidspaces, display daemon
1. **Root** with KernelSU (or another `su` provider).
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
## 3. The container
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
```
enable_gpu_mode=1
net_mode=host
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
run_at_boot=0
```
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
## 4. MobileGL server and the anland APK
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
```
The script runs, with the NDK toolchain:
```
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
```
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
```sh
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
```
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
## 5. MobileGL client in the container
Pick one of these two:
- **Cross build** (fast, from WSL or Linux):
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
Either route installs the same files, renamed into place so running clients keep their old mapping:
| File | What it is |
|---|---|
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
## 6. KWin 6.7.4 with the anland backend, and Xwayland
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
2. **Configure:**
```sh
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
```
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
```ini
[Service]
ExecStart=
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
Environment=QT_LOGGING_RULES=kwin_*.info=true
```
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
## 7. Bring it up
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
That is the single trigger. The app starts its foreground service and runs its bundled
`mobilegl-desktop.sh up` as root, which:
1. starts the display daemon if it is not running;
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
`/data/local/tmp/anland-mobilegl/backend`;
3. starts the container if it is stopped;
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
`/etc/mobilegl/backend` and runs Plasma.
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
(the notification action) stops the session and the container.
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
that, open the app as above. See SKILL.md "Starting, idling, stopping".
## 8. Verify
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
## 9. Machine-specific values to change
| Value | Original | Where |
|---|---|---|
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
# Log level is compiled to INFO. android-30 = the APK's minSdk: thread_local is then the platform's
# own TLS rather than the emulated one (which cost 7-12% of an apply thread on the API 26 build).
set -euo pipefail
. "$(dirname "$0")/env.sh"
W=$MGL_WORKTREE
B=$W/${BUILD_DIR:-build-android}
STAMP=$(git -C "$W" rev-parse HEAD)
if [ ! -f "$B/build.ninja" ]; then
cmake -S "$W" -B "$B" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_STAMP="$STAMP"
else
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
ls -la "$B/libMobileGL.so"
@@ -1,25 +0,0 @@
#!/usr/bin/env bash
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL), client log
# /tmp/mgl-chrome-<tag>.client.log, then print its "presented through" line and save a screenshot
# to $ANL_DIR/ch-<tag>.png. Kills the running chrome first (pkill -x, never -f). MobileGL is the
# container's system-wide vendor (10_mobilegl.json + /etc/mobilegl/client.conf), so only the log is set here.
# Env: WAIT (s before checking, default 20), CHANNEL (ct.sh channel, default chrome),
# CHROME_GPU (process, the default: the GPU in its own process, which presents
# through GBM dma-bufs and is restarted by Chrome after a device loss; in-process opts out -
# needs the anland startup script that knows MOBILEGL_CHROME_GPU; in process mode the GPU process and the browser
# write the one client log, the GPU process last); CHROME_GPU_SANDBOX=on keeps its GPU sandbox.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
TAG=${1:?usage: chrome-launch.sh <tag> [chrome flags...]}; shift
bash "$here/ct.sh" "${CHANNEL:-chrome}" <<IN
pkill -x chrome; sleep 2
U=\$(id -u $MGL_DESKTOP_USER)
rm -f /tmp/mgl-chrome-$TAG.client.log /tmp/chrome-$TAG-out.log
sudo -u $MGL_DESKTOP_USER env XDG_RUNTIME_DIR=/run/user/\$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$U/bus MOBILEGL_LOG_FILE_PATH=/tmp/mgl-chrome-$TAG.log MOBILEGL_CHROME_GPU=${CHROME_GPU:-process} MOBILEGL_CHROME_GPU_SANDBOX=${CHROME_GPU_SANDBOX:-off} bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
sleep ${WAIT:-20}
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
IN
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
# chrome-recover.sh <in-process|process> [rounds] - does Chrome repaint after a GPU device loss,
# without a restart? Starts Chrome (chrome-launch.sh, CHROME_GPU=<mode>) on an animated WebGL page,
# then per round: screenshots (the page must animate), loses the session of Chrome's GPU client with
# the server's pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid; the pid as Chrome sees
# itself in the container), waits, and screenshots again. A round PASSES when the server latched
# that pid's session, the browser process is the same one (no restart), a GPU client is drawing on a
# NEW server session, and the screen still animates. Stops early below MIN_BATTERY percent.
#
# in-process the GPU in the browser process (the startup script's default today): the target is
# the browser itself; recovery needs ANGLE to make a new native context.
# process the GPU in its own process (MOBILEGL_CHROME_GPU=process): the target is the GPU
# process; Chrome restarts it after the loss, so the next round's target is a new pid.
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# STARTUP=<anland worktree>/producers/kde/Arch_v5/mobilegl-startup.sh bash scripts/chrome-recover.sh deploy-startup
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 8), MIN_BATTERY (default 40),
# CHANNEL (ct.sh channel, default crec), TAG (log tag, default crec-<mode>).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
MODE=${1:?usage: chrome-recover.sh <in-process|process|deploy-startup> [rounds]}
ROUNDS=${2:-${ROUNDS:-2}}
SETTLE=${SETTLE:-8}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-crec}
TAG=${TAG:-crec-$MODE}
PAGE=/tmp/mgl-spin.html
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; cleanup; exit 3
fi
}
cleanup() { adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true; }
if [ "$MODE" = deploy-startup ]; then
# The anland startup script that knows MOBILEGL_CHROME_GPU, installed by rename (a running
# session keeps its open copy).
src=${STARTUP:?STARTUP=<path to the anland mobilegl-startup.sh>}
tr -d '\r' < "$src" > "$ANL_DIR/mobilegl-startup.sh"
adb_ push "$ANL_DIR/mobilegl-startup.sh" "$DEV_SHARE/mobilegl-startup.sh.new" >/dev/null
ct <<'IN'
install -m 755 /run/anland-mobilegl/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh.new
mv -f /opt/mobilegl/bin/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh
grep -c MOBILEGL_CHROME_GPU /opt/mobilegl/bin/mobilegl-startup.sh
IN
exit 0
fi
case "$MODE" in in-process|process) ;; *) echo "mode must be in-process or process" >&2; exit 2;; esac
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
cleanup
# The page: WebGL clears that change colour every frame plus a moving block, so a frame that
# reaches the screen changes the screenshot.
ct <<IN
cat > $PAGE <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl');
let t = 0, lost = 0;
document.getElementById('c').addEventListener('webglcontextlost', e => { e.preventDefault(); lost++; });
function frame() {
t++;
if (gl && !gl.isContextLost()) {
gl.clearColor((t % 60) / 60, ((t + 20) % 60) / 60, ((t + 40) % 60) / 60, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST); gl.scissor((t * 7) % 600, 100, 40, 160);
gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.SCISSOR_TEST);
}
document.title = 'spin ' + t + ' lost ' + lost;
requestAnimationFrame(frame);
}
frame();
</script></body></html>
HTML
chmod 644 $PAGE
IN
WAIT=${WAIT:-20} CHROME_GPU=$MODE CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file://$PAGE"
# The target pid inside the container: the browser (in-process) or the GPU process.
target_pid() {
ct <<'IN' | tr -d '\r' | tail -1
for p in $(pgrep -x chrome); do
args=$(tr '\0' ' ' < /proc/$p/cmdline)
case "$args" in
*--type=gpu-process*) echo "gpu $p" ;;
*--type=*) ;;
*) echo "browser $p" ;;
esac
done | sort > /tmp/mgl-crec-pids
cat /tmp/mgl-crec-pids >&2
echo "$(awk '/^browser/ {print $2; exit}' /tmp/mgl-crec-pids) $(awk '/^gpu/ {print $2; exit}' /tmp/mgl-crec-pids)"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS ($MODE)"
check_battery
read -r browser gpu < <(target_pid)
if [ "$MODE" = process ]; then target=$gpu; else target=$browser; fi
echo "browser=$browser gpu=${gpu:-none} target=$target"
if [ -z "$target" ]; then echo "FAIL: no target process"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
adb_ logcat -c
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $target'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
latched=$(adb_ logcat -d | grep -c "injected:pid=$target" || true)
started=$(adb_ logcat -d | grep -c "session #[0-9]* started" || true)
adb_ logcat -d | grep -E "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)" | cut -c1-220 | head -12 || true
read -r browser2 gpu2 < <(target_pid)
echo "after: browser=$browser2 gpu=${gpu2:-none}"
ct <<IN || true
echo "--- client log ($TAG):"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|failed to make|session #" /tmp/mgl-chrome-$TAG.client.log | tail -8
echo "--- chrome stderr:"; grep -aiE "context lost|GPU process|make.*current|lost" /tmp/chrome-$TAG-out.log | tail -8
IN
adb_ logcat -b crash -d | tail -5
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched pid $target"; verdict=FAIL; }
[ "$browser2" = "$browser" ] || { echo " - the browser process changed ($browser -> $browser2): Chrome restarted"; verdict=FAIL; }
[ "$started" -ge 1 ] || { echo " - no new server session started after the loss"; verdict=FAIL; }
if [ "$MODE" = process ]; then
[ -n "$gpu2" ] && [ "$gpu2" != "$gpu" ] || { echo " - no new GPU process"; verdict=FAIL; }
fi
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
done
echo "=== $pass/$ROUNDS rounds passed ($MODE); screenshots in $ANL_DIR/$TAG-r*-*.png"
@@ -1,29 +0,0 @@
#!/bin/bash
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
#
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
set -e
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
P=/root/kwsync/Arch_v5/kwin.patch
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
cd /root/kp && patch -p1 -s < $P
LIVE=/root/mobilegl-build/kwin-6.7.4
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
for f in $FILES; do
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
done
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
done
echo PREP_DONE
@@ -1,21 +0,0 @@
#!/bin/bash
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
S=$1; shift
DU=${DESKTOP_USER:-swung0x48}
U=$(id -u "$DU")
cd /tmp
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
bash -lc 'exec "$@"' mgrun "$@"
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
#
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
# bash ct.sh mychan ./something.cmd
#
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
#
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
# script on its command line and kills itself. Use `pkill -x <name>`.
set -euo pipefail
. "$(dirname "$0")/env.sh"
ch=${1:?usage: ct.sh <channel> [script-file]}
src=${2:--}
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
shot=${1:?usage: cyc.sh <shot-name>}
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
adb_ logcat -c
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# desktop-verify.sh <phase>... - check that the MobileGL desktop starts, idles and stops by itself
# (anland app: foreground service + mobilegl-desktop.sh; container: desktop-session.service).
# Phases run in the order given; screenshots go to $ANL_DIR/dv-<phase>.png.
#
# push install device/*.sh into $DEV_TOOLS (push-tools.sh's device half)
# reset everything down: app force-stopped (its service with it), container stopped,
# experiment display daemon killed - the state after a phone reboot
# cold reset, then open the app like its icon does and time each piece (desktop-timeline.sh)
# open open the app without resetting (after a reboot: the reboot was the reset)
# chrome (re)start Chrome in the session on an animated WebGL page (kills a running Chrome)
# idle CPU/GPU for visible, hidden (HOME), locked (screen off), visible again (measure.sh)
# reopen swipe the app away (remove its task), check KWin survives, open it again, time it
# stop Stop desktop from the notification (the service's STOP action) and check it all ends
# rtest lock/unlock + window resize regression ($ANL_DIR/rtest.sh)
# Env: SECS (idle window, default 20), URL (chrome page).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
SECS=${SECS:-20}
URL=${URL:-https://webglsamples.org/aquarium/aquarium.html}
SOCK=$DEV_SHARE/display.sock
su_() { adb_ shell "su -c '$*'"; }
shot() {
su_ "screencap -p $DEV_SHARE/dv-$1.png"
adb_ pull "$DEV_SHARE/dv-$1.png" "$ANL_DIR/dv-$1.png" > /dev/null && echo "screenshot $ANL_DIR/dv-$1.png"
}
kwin_log() { # the anland backend's lines since <time>
printf '%s\n' "journalctl --no-pager --since '$1' 2>/dev/null | grep -a -E 'viewer (gone|back)|consumer (reconnected|disconnected)|failed to connect' | tail -${2:-8}" |
bash "$here/ct.sh" dv 2>/dev/null | grep -v pushed || true
}
task_id() {
su_ "am stack list" | tr -d '\r' | grep -m1 "$MGL_PKG/" | sed -n 's/.*taskId=\([0-9]*\).*/\1/p'
}
wake() {
su_ "input keyevent KEYCODE_WAKEUP"; sleep 1; su_ "wm dismiss-keyguard"; sleep 2
}
for phase in "$@"; do
echo "=== $phase"
case $phase in
push)
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" > /dev/null
done
rm -f "$ANL_DIR/push.tmp"; echo "device scripts in $DEV_TOOLS" ;;
reset)
su_ "sh $DEV_TOOLS/desktop-reset.sh" ;;
cold)
bash "$0" reset
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 240"
sleep 3; shot cold ;;
open)
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 300"
sleep 3; shot open ;;
chrome)
WAIT=15 bash "$here/chrome-launch.sh" dv "$URL" ;;
idle)
su_ "sh $DEV_TOOLS/measure.sh visible $SECS"
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
su_ "input keyevent KEYCODE_HOME"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh hidden $SECS"
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "input keyevent KEYCODE_SLEEP"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh locked $SECS"
wake
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "sh $DEV_TOOLS/measure.sh visible2 $SECS"
shot idle
kwin_log "$t" 12 ;;
reopen)
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
id=$(task_id); echo "removing task $id"
[ -n "$id" ] && su_ "am stack remove $id"
sleep 8
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG) kwin: \$(pidof kwin_wayland) plasmashell: \$(pidof plasmashell)"
su_ "sh $DEV_TOOLS/measure.sh closed 10"
su_ "sh $DEV_TOOLS/desktop-timeline.sh 120"
sleep 3; shot reopen
kwin_log "$t" ;;
stop)
su_ "am startservice -n $MGL_PKG/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP" || true
sleep 25
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG); $DS_BIN show"
su_ "tail -3 $DEV_SHARE/desktop.log" ;;
rtest)
bash "$ANL_DIR/rtest.sh" dv ;;
*)
echo "unknown phase $phase" >&2; exit 2 ;;
esac
done
@@ -1,7 +0,0 @@
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
input keyevent KEYCODE_WAKEUP
wm dismiss-keyguard
sh "$(dirname "$0")/desktop-timeline.sh" 300
@@ -1,17 +0,0 @@
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
# reboot: the app force-stopped (its foreground service and server with it), the container
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
am force-stop $P
$D --name=$C stop > /dev/null 2>&1
for p in $(pidof display_daemon); do
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
done
rm -f $SOCK
sleep 2
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
$D show
@@ -1,30 +0,0 @@
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
# Pieces that already run show 0.0. Read-only apart from the am start.
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
T=${1:-180}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
now() { awk '{print $1}' /proc/uptime; }
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
t0=$(now)
am start -n $P/com.anland.consumer.MainActivity > /dev/null
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
while :; do
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
fi
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
sleep 0.3
done
@@ -1,47 +0,0 @@
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
# /proc (Droidspaces only gives them their own pid namespace).
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
L=${1:-sample}
N=${2:-20}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
pids_named() { # exact process names (comm or argv0 basename)
for n in "$@"; do pidof "$n"; done
}
pids_matching() { # argv contains the word
for d in /proc/[0-9]*; do
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
done
}
ticks() { # utime+stime of the pids, in clock ticks
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
KW="$(pids_named kwin_wayland Xwayland)"
SH="$(pids_named plasmashell)"
CH="$(pids_matching /opt/google/chrome/)"
SV="$(pidof $P:mobilegl)"
UI="$(pidof $P)"
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
# CLK_TCK is 100: ticks per second == percent of one core.
pct() { echo $(( ($2 - $1) / N )); }
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
@@ -1,20 +0,0 @@
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
DIR=$(dirname $SOCK)
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
sleep 1
rm -f $SOCK
mkdir -p $DIR
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
sleep 1
chmod 777 $DIR
chmod 666 $SOCK
echo "daemon restarted:"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
@@ -1,27 +0,0 @@
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "
systemctl stop desktop-session.service 2>/dev/null
loginctl terminate-user $U 2>/dev/null; sleep 30
systemctl reset-failed desktop-session.service
rm -f /tmp/mobilegl-compositor.log
date +%s > /tmp/plasma-start
"
am force-stop $P
am start -n $P/com.anland.consumer.MainActivity > /dev/null
sleep 30
$D --name=$C run bash -lc "
systemctl --no-pager status desktop-session.service | head -8
UID_=\$(id -u $U)
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
"
@@ -1,13 +0,0 @@
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
$D show
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
@@ -1,17 +0,0 @@
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
set -e
P=${PKG:-com.anland.consumer.mobilegl}
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
ls -laZ $D
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
am force-stop $P
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
OWN=$(stat -c %u:%g $D/libMobileGL.so)
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
ls -laZ $D/libMobileGL.so
@@ -1,17 +0,0 @@
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
# the setting until the next reboot - this clears it.
# Env: PKG, SOCK.
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
P=${PKG:-com.anland.consumer.mobilegl}
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
if pidof $P:mobilegl > /dev/null; then
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
fi
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
@@ -1,8 +0,0 @@
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
# Env: DS, CONTAINER, DESKTOP_USER.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
@@ -1,47 +0,0 @@
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
# below 40% battery: the phone drains faster under load than USB charges it.
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
L=${1:-sample}
APP=${2:-glxgears}
N=${3:-10}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
exit 3
fi
ticks() {
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
n=0
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
echo $n
}
AP="$(pidof $APP)"
XW="$(pidof Xwayland)"
KW="$(pidof kwin_wayland)"
SV="$(pidof $P:mobilegl)"
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
pct() { echo $(( ($2 - $1) / N )); }
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
@@ -1,41 +0,0 @@
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
# Sourced, never run. Every value can be overridden from the environment before calling a script.
#
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
# (USB serial or a wireless "ip:port").
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
if [ -z "${MGL_WORKTREE:-}" ]; then
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
fi
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
: "${MGL_CONTAINER:=arch-kde-mgl}"
: "${MGL_DESKTOP_USER:=swung0x48}"
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
: "${DEV_TOOLS:=/data/local/tmp/anl}"
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
export MSYS_NO_PATHCONV=1
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
# C:/x/y -> /mnt/c/x/y (for WSL).
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
mkdir -p "$ANL_DIR"
@@ -1,146 +0,0 @@
#!/usr/bin/env bash
# kwin-recover.sh [rounds] - does the desktop keep working when KWin's OWN MobileGL session is lost?
# Starts animated clients (glmark2-es2-wayland: Wayland, zero-copy shared images; glxgears: X11 through
# Xwayland's GLX; with CHROME=1 also Chrome on an animated WebGL page), then per round loses the server
# session of KWin with the pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid, KWin's pid as
# it sees itself in the container), waits, and checks. A round PASSES when:
# - the server latched KWin's session (the server log file: "injected:pid=<kwin>"; logcat is too small);
# - KWin, plasmashell and every test client are the SAME processes (nothing restarted);
# - KWin restarted compositing on a fresh session (its journal: "compositing restarted on a fresh
# MobileGL session"; its client log: one more "RECOVERED");
# - the screen still animates (shot.sh hashes differ) and the clients keep presenting;
# - no native crash (logcat -b crash).
# Between rounds the knob is reset to 0 while the clients run, which re-arms it for the same pid.
# Stops early below MIN_BATTERY percent (the phone drains faster under load than USB charges it).
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# ANLAND_WORKTREE=<anland worktree> bash scripts/kwin-stage.sh build # kwin.patch + anland backend
# (wait for "== ALL DONE ==" in /root/kwin-build.log), then: adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 10), REARM (s with the knob at 0 between
# rounds, default 4), MIN_BATTERY (default 40), CHROME (1: add Chrome), CHANNEL (ct.sh channel,
# default krec), TAG (screenshot/log tag, default krec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
ROUNDS=${1:-${ROUNDS:-2}}
SETTLE=${SETTLE:-10}
REARM=${REARM:-4}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-krec}
TAG=${TAG:-krec}
COMPOSITOR_LOG=/tmp/mobilegl-compositor.client.log
SERVER_LOG=/data/data/$MGL_PKG/files/mobilegl-server.server.log
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
stop_clients() {
ct <<'IN' || true
pkill -x glmark2-es2-way 2>/dev/null; pkill -x glmark2-es2-wayland 2>/dev/null; pkill -x glxgears 2>/dev/null
true
IN
}
cleanup() {
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true
stop_clients
}
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; exit 3
fi
}
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
# The clients, each with its own log (mgrun's MGLOG), in the background for the whole run.
ct <<'IN'
rm -f /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log /tmp/krec-gm.out /tmp/krec-gx.out
MGLOG=/tmp/mgl-krec-gm.log nohup timeout 900 mgrun offscreen glmark2-es2-wayland -s 960x540 --run-forever \
> /tmp/krec-gm.out 2>&1 < /dev/null &
MGLOG=/tmp/mgl-krec-gx.log nohup timeout 900 mgrun offscreen glxgears -info > /tmp/krec-gx.out 2>&1 < /dev/null &
sleep 8
echo "glmark2: $(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland) glxgears: $(pgrep -x glxgears)"
grep -aE "presented through|could not be presented" /tmp/mgl-krec-gm.client.log | head -2
grep -aE "presented through|readback" /tmp/mgl-krec-gx.client.log | head -2
IN
if [ "${CHROME:-0}" = 1 ]; then
ct <<'IN'
cat > /tmp/mgl-krec-spin.html <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl'); let t = 0;
function frame() { t++; if (gl && !gl.isContextLost()) { gl.clearColor((t % 60) / 60, 0.2, 0.6, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
requestAnimationFrame(frame); }
frame();
</script></body></html>
HTML
chmod 644 /tmp/mgl-krec-spin.html
IN
WAIT=${WAIT:-20} CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file:///tmp/mgl-krec-spin.html"
fi
# KWin, plasmashell and the clients, as pids inside the container; how often KWin recovered so far.
snapshot() {
ct <<IN | tr -d '\r' | tail -1
k=\$(pgrep -x kwin_wayland | head -1); p=\$(pgrep -x plasmashell | head -1)
g=\$(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland); x=\$(pgrep -x glxgears | head -1)
c=\$(pgrep -x chrome | head -1)
r=\$(grep -ac "RECOVERED" $COMPOSITOR_LOG 2>/dev/null || true)
f=\$(grep -ac "FPS" /tmp/krec-gm.out 2>/dev/null || true)
echo "\${k:-none} \${p:-none} \${g:-none} \${x:-none} \${c:-none} \${r:-0} \${f:-0}"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS"
check_battery
read -r kwin plasma gm gx chrome recovered fps < <(snapshot)
echo "kwin=$kwin plasmashell=$plasma glmark2=$gm glxgears=$gx chrome=$chrome recoveries=$recovered fps-lines=$fps"
if [ "$kwin" = none ]; then echo "FAIL: KWin is not running"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
srvlines=$(adb_ shell "su -c 'wc -l < $SERVER_LOG'" | tr -dc '0-9')
adb_ logcat -b crash -c || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $kwin'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
read -r kwin2 plasma2 gm2 gx2 chrome2 recovered2 fps2 < <(snapshot)
echo "after: kwin=$kwin2 plasmashell=$plasma2 glmark2=$gm2 glxgears=$gx2 chrome=$chrome2 recoveries=$recovered2 fps-lines=$fps2"
adb_ shell "su -c 'tail -n +$((srvlines + 1)) $SERVER_LOG'" > "$ANL_DIR/$TAG-r$round-server.log"
latched=$(grep -c "injected:pid=$kwin" "$ANL_DIR/$TAG-r$round-server.log" || true)
grep -aE "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)|owner=server|lease" \
"$ANL_DIR/$TAG-r$round-server.log" | cut -c1-220 | head -14 || true
ct <<IN || true
echo "--- KWin's MobileGL client log:"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|re-created on the fresh|Refuse" $COMPOSITOR_LOG | tail -6
echo "--- KWin journal:"; journalctl --no-pager --since -40s 2>/dev/null | grep -aE "kwin_wayland" | grep -aiE "reset|restarted|MobileGL swap|context|fatal|abort" | tail -10
echo "--- clients:"; grep -aE "DEVICE LOST|lost|Fatal" /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log | tail -4
IN
crashes=$(adb_ logcat -b crash -d | grep -c "Fatal signal\|Abort message" || true)
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched KWin's session (pid $kwin)"; verdict=FAIL; }
[ "$kwin2" = "$kwin" ] || { echo " - KWin is a different process ($kwin -> $kwin2): it restarted"; verdict=FAIL; }
[ "$plasma2" = "$plasma" ] || { echo " - plasmashell restarted ($plasma -> $plasma2)"; verdict=FAIL; }
[ "$gm2" = "$gm" ] && [ "$gx2" = "$gx" ] || { echo " - a test client restarted or died"; verdict=FAIL; }
[ "${CHROME:-0}" != 1 ] || [ "$chrome2" = "$chrome" ] || { echo " - Chrome restarted"; verdict=FAIL; }
[ "$recovered2" -gt "$recovered" ] || { echo " - KWin's client log shows no new RECOVERED session"; verdict=FAIL; }
[ "$fps2" -gt "$fps" ] || { echo " - glmark2 stopped reporting frames"; verdict=FAIL; }
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
[ "$crashes" -eq 0 ] || { echo " - native crash(es):"; adb_ logcat -b crash -d | tail -8; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
# Re-arm the knob for the same pid: a session must observe 0 before the next value counts.
sleep "$REARM"
done
echo "=== $pass/$ROUNDS rounds passed; screenshots in $ANL_DIR/$TAG-r*-*.png"
[ "$pass" = "$ROUNDS" ]
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
#
# Two staging places, because the two container scripts read different ones:
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
# sync-build-kwin.sh (bash then fails on $'\r').
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
# CHANNEL (ct.sh channel, default kw).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
K=$ANLAND_WORKTREE/producers/kde
st=$ANL_DIR/kwstage
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
lf() { tr -d '\r' < "$1" > "$2"; }
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
lf "$f" "$st/kwin-sync/anland/$b"
done
for f in kwin.patch mobilegl-startup.sh desktop-session-mobilegl.conf; do
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
done
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
if [ "${1:-}" = build ]; then
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
bash /root/kwprep.sh || exit 1
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
IN
fi
@@ -1,94 +0,0 @@
#!/usr/bin/env python3
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
# module in the container): a minimal RFC 6455 client.
# python3 cdpmedia.py 9333 6
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
# they arrive, then the page's stats text.
import base64, json, os, socket, struct, sys, time, urllib.request
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
page = next(t for t in targets if t.get("type") == "page")
url = page["webSocketDebuggerUrl"]
path = url.split(f"127.0.0.1:{port}", 1)[1]
sock = socket.create_connection(("127.0.0.1", port))
key = base64.b64encode(os.urandom(16)).decode()
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
response = b""
while b"\r\n\r\n" not in response:
response += sock.recv(4096)
buffer = response.split(b"\r\n\r\n", 1)[1]
def send(message):
data = json.dumps(message).encode()
header = bytes([0x81])
mask = os.urandom(4)
if len(data) < 126:
header += bytes([0x80 | len(data)])
elif len(data) < 65536:
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
else:
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
global buffer
while len(buffer) < n:
chunk = sock.recv(65536)
if not chunk:
raise EOFError
buffer += chunk
out, buffer = buffer[:n], buffer[n:]
return out
def recv():
first, second = recv_exact(2)
length = second & 0x7F
if length == 126:
length = struct.unpack(">H", recv_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", recv_exact(8))[0]
payload = recv_exact(length)
return json.loads(payload) if first & 0x0F == 1 else None
send({"id": 1, "method": "Media.enable"})
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
if os.environ.get("CDP_NAVIGATE"):
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
elif os.environ.get("CDP_RELOAD", "1") == "1":
send({"id": 3, "method": "Page.reload"})
deadline = time.time() + seconds
sock.settimeout(0.5)
seen = set()
while time.time() < deadline:
try:
message = recv()
except socket.timeout:
continue
if not message or "method" not in message:
continue
if message["method"] == "Media.playerPropertiesChanged":
for prop in message["params"]["properties"]:
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
line = f"{prop['name']} = {prop['value']}"
if line not in seen:
seen.add(line)
print(line)
elif message["method"] in ("Media.playerErrorsRaised",):
print("media error:", json.dumps(message["params"])[:300])
send({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
sock.settimeout(3)
while True:
message = recv()
if message and message.get("id") == 2:
print("page:", message.get("result", {}).get("result", {}).get("value"))
break
@@ -1,34 +0,0 @@
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char** argv) {
char dir[64];
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
DIR* d = opendir(dir);
if (!d) { perror("opendir"); return 1; }
struct dirent* e;
while ((e = readdir(d))) {
char p[128], l[128] = {0};
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
int fd = open(p, O_RDONLY);
if (fd < 0) continue;
struct stat st; fstat(fd, &st);
if (st.st_size < (1 << 20)) { close(fd); continue; }
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
(unsigned long long)h, nz);
munmap((void*)m, st.st_size); close(fd);
}
return 0;
}
@@ -1,55 +0,0 @@
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
#endif
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
int main(int argc, char** argv) {
int seconds = argc > 1 ? atoi(argv[1]) : 20;
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
setvbuf(stdout, NULL, _IOLBF, 0);
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(d, NULL, NULL);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n;
eglChooseConfig(d, ca, &c, 1, &n);
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
if (x == EGL_NO_CONTEXT) { printf("no ctx 0x%x\n", eglGetError()); return 1; }
eglMakeCurrent(d, s, s, x);
EGLint got = 0; eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
const double t0 = now_ms(); double maxRt = 0; long iters = 0, slow = 0;
printf("[probe] start %.0f prio=0x%x\n", t0, got);
while (now_ms() - t0 < seconds * 1000.0) {
const double a = now_ms();
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
const double rt = now_ms() - a;
if (rt > maxRt) maxRt = rt;
if (rt > 50) { ++slow; printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms()); }
++iters;
usleep(10000);
}
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
return 0;
}
@@ -1,150 +0,0 @@
// hangclient - a Wayland xdg-toplevel, EGL + desktop GL 3.3 core client that animates and, on request,
// submits fragment work that never ends (the driver has to reset it). Prints per-frame gaps so the same
// binary measures "other app" frame pacing (frame callbacks come from the compositor) when run with -H -1.
//
// hangclient -T tag [-H hang_at_frame] [-N hang_repeat] [-I iters] [-S seconds] [-W w] [-X h] [-F]
// -H n frame at which the hanging draw is submitted (-1: never; default -1)
// -N k how many consecutive hanging frames (default 1)
// -E e re-arm a hang every e frames after the first (0: once)
// -I i loop count of the hanging shader (default 2^31-1)
// -S s run time in seconds (default 30)
// -F glFinish after every frame (puts the wait inside the client's own session)
#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-shell-client-protocol.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static struct wl_compositor* g_comp;
static struct xdg_wm_base* g_wm;
static int g_configured, g_closed;
static int g_w = 480, g_h = 360;
static void reg_global(void* d, struct wl_registry* r, uint32_t name, const char* iface, uint32_t ver) {
(void)d;
if (!strcmp(iface, "wl_compositor")) g_comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if (!strcmp(iface, "xdg_wm_base")) g_wm = wl_registry_bind(r, name, &xdg_wm_base_interface, 1);
}
static void reg_remove(void* d, struct wl_registry* r, uint32_t n) { (void)d; (void)r; (void)n; }
static const struct wl_registry_listener reg_l = {reg_global, reg_remove};
static void wm_ping(void* d, struct xdg_wm_base* wm, uint32_t s) { (void)d; xdg_wm_base_pong(wm, s); }
static const struct xdg_wm_base_listener wm_l = {wm_ping};
static void xs_conf(void* d, struct xdg_surface* s, uint32_t serial) { (void)d; xdg_surface_ack_configure(s, serial); g_configured = 1; }
static const struct xdg_surface_listener xs_l = {xs_conf};
static void tl_conf(void* d, struct xdg_toplevel* t, int32_t w, int32_t h, struct wl_array* st) { (void)d; (void)t; (void)st; (void)w; (void)h; }
static void tl_close(void* d, struct xdg_toplevel* t) { (void)d; (void)t; g_closed = 1; }
static const struct xdg_toplevel_listener tl_l = {tl_conf, tl_close};
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
static GLuint sh(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, NULL);
glCompileShader(s);
GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) { char log[2048]; glGetShaderInfoLog(s, sizeof log, NULL, log); fprintf(stderr, "shader: %s\n", log); exit(2); }
return s;
}
int main(int argc, char** argv) {
const char* tag = "hc"; int hangAt = -1, hangN = 1, every = 0, seconds = 30, finish = 0; long iters = 2147483647L;
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "-T") && i + 1 < argc) tag = argv[++i];
else if (!strcmp(argv[i], "-H") && i + 1 < argc) hangAt = atoi(argv[++i]);
else if (!strcmp(argv[i], "-N") && i + 1 < argc) hangN = atoi(argv[++i]);
else if (!strcmp(argv[i], "-E") && i + 1 < argc) every = atoi(argv[++i]);
else if (!strcmp(argv[i], "-I") && i + 1 < argc) iters = atol(argv[++i]);
else if (!strcmp(argv[i], "-S") && i + 1 < argc) seconds = atoi(argv[++i]);
else if (!strcmp(argv[i], "-W") && i + 1 < argc) g_w = atoi(argv[++i]);
else if (!strcmp(argv[i], "-X") && i + 1 < argc) g_h = atoi(argv[++i]);
else if (!strcmp(argv[i], "-F")) finish = 1;
}
setvbuf(stdout, NULL, _IOLBF, 0);
struct wl_display* dpy = wl_display_connect(NULL);
if (!dpy) { fprintf(stderr, "no wayland\n"); return 1; }
struct wl_registry* reg = wl_display_get_registry(dpy);
wl_registry_add_listener(reg, &reg_l, NULL);
wl_display_roundtrip(dpy);
if (!g_comp || !g_wm) { fprintf(stderr, "no compositor/xdg_wm_base\n"); return 1; }
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
xdg_surface_add_listener(xs, &xs_l, NULL);
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
xdg_toplevel_add_listener(tl, &tl_l, NULL);
xdg_toplevel_set_title(tl, tag);
wl_surface_commit(surf);
while (!g_configured) wl_display_dispatch(dpy);
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
if (!eglInitialize(ed, NULL, NULL)) { fprintf(stderr, "eglInitialize\n"); return 1; }
eglBindAPI(EGL_OPENGL_API);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig cfg; EGLint n = 0;
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) { fprintf(stderr, "no config\n"); return 1; }
const EGLint xa[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "no context 0x%x\n", eglGetError()); return 1; }
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
eglMakeCurrent(ed, es, es, ctx);
eglSwapInterval(ed, 1);
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
GLuint prog = glCreateProgram();
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
glLinkProgram(prog);
GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) { fprintf(stderr, "link failed\n"); return 2; }
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao);
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
const double t0 = now_ms(); double last = t0, maxGap = 0; long frames = 0, gaps100 = 0;
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt, hangN, every, iters);
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
wl_display_dispatch_pending(dpy);
const double t = (now_ms() - t0) / 1000.0;
int hang = 0;
if (hangAt >= 0 && frames >= hangAt) {
long rel = frames - hangAt;
if (every > 0) rel %= every;
hang = rel < hangN;
}
glViewport(0, 0, g_w, g_h);
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(prog);
glUniform1i(uIt, hang ? (int)iters : 0);
glUniform1f(uT, (float)t);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
if (finish) glFinish();
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
const GLenum rs = glGetError();
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
const double nowv = now_ms(), gap = nowv - last;
last = nowv; ++frames;
if (gap > maxGap) maxGap = gap;
if (gap > 100.0) { ++gaps100; printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames); }
if (frames % 120 == 0) printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
}
const double total = now_ms() - t0;
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap, gaps100);
return 0;
}
@@ -1,95 +0,0 @@
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
* left half red and the right half WHITE; a black right half means the alpha was lost. */
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define P(T, n) T n = (T)eglGetProcAddress(#n)
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
int main(void) {
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
GLint lm = glGetUniformLocation(pr, "mode");
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
};
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
while (glGetError()) {}
if (cases[i].storage) {
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
} else {
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
}
GLenum err = glGetError();
glUniform1i(lm, cases[i].mode);
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
};
glUniform1i(lm, 0);
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
GLenum err = glGetError();
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
int ok = 1;
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
return 0;
}
@@ -1,410 +0,0 @@
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
//
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
//
// What it does, in an ES 3.0 context on a pbuffer:
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
// again: the new order must show (the server refreshes a foreign buffer at each frame).
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
// check is that every call succeeds).
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
#define _GNU_SOURCE
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <fcntl.h>
#include <gbm.h>
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef DRM_FORMAT_NV12
#define DRM_FORMAT_NV12 0x3231564E
#endif
#ifndef DRM_FORMAT_P010
#define DRM_FORMAT_P010 0x30313050
#endif
static int g_failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("FAIL: " __VA_ARGS__); \
printf("\n"); \
++g_failures; \
} \
} while (0)
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
static PFNEGLCREATESYNCKHRPROC pCreateSync;
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
static EGLDisplay g_display;
enum { W = 256, H = 128, BARS = 4 };
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
const double y = kr * r + kg * g + kb * b;
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
const double scale = (double)(1 << (bits - 8));
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
}
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
// samples with the 10 significant bits high.
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
const int bits = bytes == 1 ? 8 : 10;
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
int bar = x * BARS / W;
if (reverse) bar = BARS - 1 - bar;
int code[3];
Encode709(kBars[bar], bits, code);
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
if (bytes == 1) py[0] = (uint8_t)code[0];
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
if ((x & 1) == 0 && (y & 1) == 0) {
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
if (bytes == 1) {
pc[0] = (uint8_t)code[1];
pc[1] = (uint8_t)code[2];
} else {
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
}
}
}
}
}
static GLuint Compile(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
printf("shader: %s\n", log);
}
return shader;
}
static GLuint g_program, g_fbo, g_rgba;
static void SetUpDraw(void) {
const char* vs = "#version 300 es\n"
"out vec2 uv;\n"
"void main() {\n"
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
" uv = p;\n"
"}\n";
const char* fs = "#version 300 es\n"
"#extension GL_OES_EGL_image_external_essl3 : require\n"
"precision mediump float;\n"
"uniform samplerExternalOES tex;\n"
"in vec2 uv;\n"
"out vec4 color;\n"
"void main() { color = texture(tex, uv); }\n";
g_program = glCreateProgram();
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
glLinkProgram(g_program);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
CHECK(linked, "the samplerExternalOES program did not link");
GLint type = 0, size = 0;
char name[64];
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
printf("uniform 0: %s type 0x%04x\n", name, type);
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
glGenTextures(1, &g_rgba);
glBindTexture(GL_TEXTURE_2D, g_rgba);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
glGenFramebuffers(1, &g_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
}
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glViewport(0, 0, W, H);
glUseProgram(g_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
static unsigned char pixels[W * H * 4];
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
if (tolerance < 0) {
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
pixels[(H / 2 * W + W / 2) * 4 + 3]);
return;
}
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
// so any row does; take two to catch a broken chroma row pitch.
const int rows[2] = {H / 4, (3 * H) / 4};
for (int r = 0; r < 2; ++r) {
for (int bar = 0; bar < BARS; ++bar) {
const int x = bar * (W / BARS) + W / (2 * BARS);
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
int worst = 0;
for (int c = 0; c < 3; ++c) {
int d = abs((int)p[c] - (int)want[c]);
if (d > worst) worst = d;
}
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
}
}
}
static int AllocateDmaHeap(size_t size) {
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
if (heap < 0) {
perror("open /dev/dma_heap/system");
return -1;
}
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
close(heap);
return -1;
}
close(heap);
return (int)data.fd;
}
static void Sync(int fd, int start) {
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
}
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
EGLint attribs[64];
int n = 0;
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
A(EGL_WIDTH, W);
A(EGL_HEIGHT, H);
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
if (modifier) {
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
}
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
attribs[n] = EGL_NONE;
#undef A
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
return image;
}
static GLuint ExternalTexture(EGLImageKHR image) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
return texture;
}
// A frame boundary as an offscreen producer makes one: a native fence.
static void EndFrame(void) {
EGLint attribs[] = {EGL_NONE};
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
}
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
const uint32_t offsetC = pitchY * (H + 16);
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
int fd = AllocateDmaHeap(size);
if (fd < 0) {
CHECK(0, "%s: no dma-buf to test with", what);
return;
}
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
if (map == MAP_FAILED) return;
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
Sync(fd, 0);
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
if (image == EGL_NO_IMAGE_KHR) return;
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, what, 0, tolerance);
EndFrame();
// New content, same buffer: shows after the frame boundary.
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
Sync(fd, 0);
EndFrame();
DrawAndCheck(texture, what, 1, tolerance);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
munmap(map, size);
close(fd);
}
static void GbmTest(void) {
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
struct gbm_device* gbm = gbm_create_device(node);
if (gbm == NULL) {
CHECK(0, "gbm_create_device failed");
return;
}
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
printf("gbm NV12 supported: %d, P010: %d\n", supported,
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
CHECK(supported == 1, "gbm does not support NV12");
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
if (bo != NULL) {
const int planes = gbm_bo_get_plane_count(bo);
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
int fd = gbm_bo_get_fd(bo);
EGLImageKHR image =
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
gbm_bo_get_stride_for_plane(bo, 1), 0);
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
if (image != EGL_NO_IMAGE_KHR) {
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, "gbm NV12", 0, -1);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
}
close(fd);
gbm_bo_destroy(bo);
}
gbm_device_destroy(gbm);
close(node);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major, minor;
if (!eglInitialize(g_display, &major, &minor)) {
printf("FAIL: eglInitialize\n");
return 2;
}
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig config;
EGLint count = 0;
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
if (count < 1) {
printf("FAIL: no config\n");
return 2;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
printf("FAIL: no ES 3 context\n");
return 2;
}
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
EGLint formats[32], formatCount = 0;
pQueryFormats(g_display, 32, formats, &formatCount);
int nv12 = 0;
for (int i = 0; i < formatCount; ++i) {
printf("dma-buf format %.4s", (const char*)&formats[i]);
EGLuint64KHR modifiers[4];
EGLBoolean externalOnly[4];
EGLint modifierCount = 0;
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
for (int m = 0; m < modifierCount; ++m)
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
printf("\n");
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
}
CHECK(nv12, "NV12 is not a listed dma-buf format");
SetUpDraw();
ForeignTest(DRM_FORMAT_NV12, 1, 12);
ForeignTest(DRM_FORMAT_P010, 2, 16);
GbmTest();
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglTerminate(g_display);
return g_failures ? 1 : 0;
}
@@ -1,22 +0,0 @@
#!/usr/bin/env bash
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
# container/mgrun -> <container>/usr/local/bin/mgrun
# container/kwprep.sh -> <container>/root/kwprep.sh
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
echo "pushed $DEV_TOOLS/$(basename "$f")"
done
for f in mgrun kwprep.sh; do
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
done
rm -f "$ANL_DIR/push.tmp"
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
@@ -1,29 +0,0 @@
#!/usr/bin/env bash
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
# `input mouse tap x y`. Env CHANNEL (default rec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
IN
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
import sys, os
from PIL import Image
d, out = sys.argv[1], sys.argv[2]
fs = sorted(os.listdir(d)); n = len(fs)
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
ims = [Image.open(os.path.join(d, f)) for f in pick]
w, h = ims[0].size; tw, th = w // 2, h // 2
g = Image.new('RGB', (tw * 3, th * 4))
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
g.save(out); print(n, pick)
P
@@ -1,15 +0,0 @@
#!/usr/bin/env bash
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
set -euo pipefail
. "$(dirname "$0")/env.sh"
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
cmd=""; i=1
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
adb_ shell "su -c '$cmd'"
i=1
while [ "$i" -le "$n" ]; do
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
done
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
# worktree changed since the container's source stamp into the container's MobileGL source tree
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
# then build/install inside the container with anland's anland-build-client.sh
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
set -euo pipefail
. "$(dirname "$0")/env.sh"
SRC=$CT_ROOT/root/mobilegl-unified-src
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
cd "$MGL_WORKTREE"
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
echo "$(echo "$FILES" | wc -l) files since $STAMP"
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
HEAD=$(git rev-parse HEAD)
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
@@ -1,9 +0,0 @@
#!/bin/bash
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
# (they need a live device + manual intervention); never run two ctest suites at once.
B=${B:-$HOME/mgl-anl-bld}
cd $B
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
@@ -1,13 +0,0 @@
#!/bin/bash
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
set -e
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
B=${B:-$HOME/mgl-anl-bld}
if [ ! -f $B/build.ninja ]; then
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
fi
cmake --build $B --target "$@" -j16
@@ -1,135 +0,0 @@
#!/usr/bin/env bash
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/Disaggregated/notes/anland/
# plan-x11-gpu.md): Xwayland drawing with glamor on MobileGL, and GLX windows presented through
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
#
# preflight read-only: battery, stack status, Xwayland package, render node, xcb libs,
# test apps, Xwayland's extensions and its GLX vendor
# measure <label> [s] glxgears 1280x720 (GEARS_ARGS; vblank_mode=0) for s seconds (10): its fps, then CPU/GPU/dma-buf
# counts (device/x11-measure.sh); also with MOBILEGL_GLX_PRESENT=readback
# deploy-client xbuild.sh + xdeploy.sh of this worktree's client (MGL_XBUILD_DIR)
# deploy-kwin kwin-stage.sh build from ANLAND_WORKTREE; waits for "== ALL DONE =="
# deploy-server build-android.sh + cyc.sh x11-server (swaps the APK lib, RESTARTS the session)
# check the evidence: Xwayland took glamor (journal), its MobileGL log, GLX's path
# line, dma-buf counts in Xwayland/KWin, the server's import lines
# apps X11 clients on screen: konsole and kate on Qt's xcb platform (glamor-drawn
# core X), glxgears (GLX), xterm/xeyes when installed; two screenshots
# stop kill the test apps
#
# Typical run: preflight; measure before; deploy-client; deploy-kwin; deploy-server; check; measure
# after; apps; stop. Battery: every load step refuses below MIN_BATTERY (40) %.
# Env: CHANNEL (ct.sh channel, default x11), ANLAND_WORKTREE (anland x11-gpu worktree, Windows
# form), MGL_XBUILD_DIR (default /home/swung/mgl-xbuild-x11), MIN_BATTERY.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
CH=${CHANNEL:-x11}
: "${MGL_XBUILD_DIR:=/home/swung/mgl-xbuild-x11}"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/x11-gpu}"
export MGL_XBUILD_DIR ANLAND_WORKTREE
battery_ok() {
local level
level=$(adb_ shell "dumpsys battery" | sed -n 's/^ *level: *//p' | tr -d '\r')
echo "battery: ${level}%"
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "below ${MIN_BATTERY:-40}%: stopping GPU-heavy work; charge the phone first" >&2
return 1
fi
}
push_measure() {
tr -d '\r' < "$here/device/x11-measure.sh" > "$ANL_DIR/x11-measure.sh"
adb_ push "$ANL_DIR/x11-measure.sh" "$DEV_TOOLS/x11-measure.sh" >/dev/null
}
step=${1:?usage: x11-verify.sh <preflight|measure|deploy-client|deploy-kwin|deploy-server|check|apps|stop>}
shift
case "$step" in
preflight)
battery_ok || true
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
bash "$here/ct.sh" "$CH" <<'IN'
echo "== packages"; pacman -Q xorg-xwayland libxcb mesa-utils xorg-xdpyinfo qt6-base 2>&1 | sed 's/^/ /'
echo "== render nodes"; ls -l /dev/dri/ 2>&1 | sed 's/^/ /'
U=swung0x48; for n in /dev/dri/renderD*; do sudo -u $U test -r "$n" -a -w "$n" && echo " $U can open $n"; done
echo "== KWin's thin node"; tr '\0' '\n' < /proc/$(pgrep -x kwin_wayland)/environ 2>/dev/null | grep -E "MOBILEGL_GBM_NODE|ANLAND_XWAYLAND_GLAMOR|GBM_BACKEND" | sed 's/^/ /'
echo "== xcb libraries"; for l in libxcb-dri3.so.0 libxcb-present.so.0 libxcb-xfixes.so.0 libxcb-shm.so.0; do ls /usr/lib/$l >/dev/null 2>&1 && echo " $l ok" || echo " $l MISSING"; done
echo "== Xwayland"; pgrep -a -x Xwayland | sed 's/^/ /'
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM|XFIXES|GLX)$" | sed 's/^/ ext/'
mgrun offscreen glxinfo -B 2>&1 | grep -E "vendor|renderer|direct rendering" | sed 's/^/ /'
IN
;;
measure)
label=${1:?usage: x11-verify.sh measure <label> [seconds]}
secs=${2:-10}
battery_ok
push_measure
for mode in auto readback; do
printf '%s\n' \
"pkill -x glxgears; rm -f /tmp/mgl-x11-gears.client.log /tmp/x11-gears.out" \
"MGLOG=/tmp/mgl-x11-gears.log timeout $((secs + 8)) mgrun offscreen env vblank_mode=${VBLANK_MODE:-0} MOBILEGL_GLX_PRESENT=$mode glxgears ${GEARS_ARGS:--geometry 1280x720} > /tmp/x11-gears.out 2>&1 &" \
"sleep 3" | bash "$here/ct.sh" "$CH" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/x11-measure.sh $label-$mode glxgears $secs'" || true
sleep 6
printf '%s\n' "grep -a 'frames in' /tmp/x11-gears.out | tail -3; grep -a 'presented through' /tmp/mgl-x11-gears.client.log | head -1; pkill -x glxgears; true" \
| bash "$here/ct.sh" "$CH"
done
;;
deploy-client)
bash "$here/xbuild.sh"
CHANNEL=$CH bash "$here/xdeploy.sh"
;;
deploy-kwin)
CHANNEL=$CH bash "$here/kwin-stage.sh" build
for _ in $(seq 1 120); do
out=$(printf 'tail -3 /root/kwin-build.log\n' | bash "$here/ct.sh" "$CH" 2>/dev/null || true)
echo "$out" | tail -1
echo "$out" | grep -q "== ALL DONE ==" && exit 0
echo "$out" | grep -qiE "error|failed" && { echo "KWin build failed" >&2; exit 1; }
sleep 30
done
echo "KWin build did not finish in an hour" >&2; exit 1
;;
deploy-server)
battery_ok
bash "$here/build-android.sh"
CHANNEL=$CH bash "$here/cyc.sh" x11-server
;;
check)
bash "$here/ct.sh" "$CH" <<'IN'
echo "== Xwayland's command line (no -shm with glamor)"; pgrep -a -x Xwayland | sed 's/^/ /'
echo "== Xwayland's glamor (journal)"
journalctl --no-pager --since -30min 2>/dev/null | grep -aiE "glamor|Xwayland.*(EGL|gbm|dri3|falling back)" | tail -10 | sed 's/^/ /'
echo "== Xwayland's MobileGL client log"; grep -aE "presented|shared image|declin|Fatal|ERROR" /tmp/mgl-xwayland.client.log /tmp/mgl-xwayland.client 2>/dev/null | tail -8 | sed 's/^/ /' || echo " (none)"
X=$(pgrep -x Xwayland); K=$(pgrep -x kwin_wayland)
echo "== dma-bufs held: Xwayland $(ls -l /proc/$X/fd 2>/dev/null | grep -c dmabuf), kwin $(ls -l /proc/$K/fd 2>/dev/null | grep -c dmabuf)"
echo "== Xwayland's GLX vendor and extensions"
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM)$" | sed 's/^/ ext/'
echo "== a GLX window's path"
rm -f /tmp/mgl-x11-check.client.log
MGLOG=/tmp/mgl-x11-check.log timeout 6 mgrun offscreen glxgears >/dev/null 2>&1
grep -a "presented through\|leaves DRI3" /tmp/mgl-x11-check.client.log | sed 's/^/ /'
IN
adb_ logcat -d | grep -aE "MobileGL.*(shared image|dma-buf import|refused)" | tail -8 || true
adb_ logcat -b crash -d | tail -5 || true
;;
apps)
battery_ok
printf '%s\n' \
"pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate" \
"MGLOG=/tmp/mgl-x11-konsole.log mgrun offscreen env QT_QPA_PLATFORM=xcb konsole >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-kate.log mgrun offscreen env QT_QPA_PLATFORM=xcb kate >/dev/null 2>&1 &" \
"command -v xterm >/dev/null && MGLOG=/tmp/mgl-x11-xterm.log mgrun offscreen xterm >/dev/null 2>&1 &" \
"command -v xeyes >/dev/null && MGLOG=/tmp/mgl-x11-xeyes.log mgrun offscreen xeyes >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-gears.log mgrun offscreen glxgears >/dev/null 2>&1 &" \
"sleep 6; pgrep -a -x konsole; pgrep -a -x kate; pgrep -a -x xterm; pgrep -a -x xeyes; pgrep -a -x glxgears; true" | bash "$here/ct.sh" "$CH"
bash "$here/shot.sh" x11-apps 2 1
;;
stop)
printf '%s\n' "pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate; true" | bash "$here/ct.sh" "$CH"
;;
*)
echo "unknown step $step" >&2; exit 2
;;
esac
@@ -1,42 +0,0 @@
#!/usr/bin/env bash
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
# MGL_JOBS (16), WSL_DISTRO (archlinux).
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
set -euo pipefail
. "$(dirname "$0")/env.sh"
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
RECONF=0
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
EXTRA="$*"
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -euo pipefail
OUT=${OUT:-$HOME/mgl-xbuild-a64}
TC=$HOME/sysroots/aarch64-arch.cmake
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
t0=$(date +%s)
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
t1=$(date +%s)
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
t2=$(date +%s)
echo "stamp: $STAMP"
echo "configure: $((t1-t0))s build: $((t2-t1))s"
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
done
IN
exit "${PIPESTATUS[0]}"
@@ -1,65 +0,0 @@
#!/usr/bin/env bash
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
# keep the old inode; restart the client (or the session) to pick up the new library.
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
mkdir -p "$ANL_DIR/xdist"
# 1. Copy the build outputs out of WSL.
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
IN
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
# 3. Install inside the container.
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
set -euo pipefail
in=/root/xdeploy
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
mkdir -p /usr/share/glvnd/egl_vendor.d
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
# Client settings every process reads when its environment does not say otherwise (the backend
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
mkdir -p /etc/mobilegl
cat >/etc/mobilegl/client.conf <<'CONF'
# MobileGL client configuration, read by every process that loads libMobileGL.so.
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
MOBILEGL_TRANSPORT=spawn
MOBILEGL_IPC_DATA=shm
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
CONF
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
mkdir -p /etc/environment.d /etc/profile.d
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
mkdir -p /usr/lib/gbm
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
echo DEPLOYED
IN
@@ -1,47 +0,0 @@
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(_triple aarch64-unknown-linux-gnu)
if (DEFINED ENV{MGL_SYSROOT})
set(_sysroot "$ENV{MGL_SYSROOT}")
else()
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
endif()
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
list(GET _gcc_dirs 0 _gcc_install_dir)
set(CMAKE_SYSROOT "${_sysroot}")
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${_triple})
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
set(CMAKE_AR llvm-ar)
set(CMAKE_RANLIB llvm-ranlib)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJCOPY llvm-objcopy)
set(CMAKE_STRIP llvm-strip)
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
set(CMAKE_C_FLAGS_INIT "${_xflags}")
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_LINKER_TYPE LLD)
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "")
@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/../env.sh"
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
set -e; cd /
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
echo SYSROOT_TAR_DONE
IN
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
IN
rm -f "$ANL_DIR/xsysroot.tar.gz"
+5 -30
View File
@@ -44,12 +44,12 @@ require 'key:MOBILEGL_BACKEND_TYPE' "$plugin_resource_text" 'V2 backend variable
require 'defaultValue:DirectGLES' "$plugin_resource_text" 'V2 DirectGLES default'
require 'DirectVulkan' "$plugin_resource_text" 'V2 DirectVulkan option'
require 'key:MOBILEGL_DISABLE_TIMERQUERY' "$plugin_resource_text" 'V2 timer-query toggle'
require 'key:MOBILEGL_MAGMA_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_DISABLE_SUBGROUP' "$plugin_resource_text" 'V2 Vulkan subgroup toggle'
require 'key:MOBILEGL_MAGMA_R11G11B10F_FALLBACK' "$plugin_resource_text" 'V2 Magma format fallback toggle'
require 'key:MOBILEGL_MAGMA_FRAMESINFLIGHT' "$plugin_resource_text" 'V2 Magma frames-in-flight setting'
require 'key:MOBILEGL_ESPRYT_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_AVOID_SAMPLER_MIPMAP_MIN_FILTER' "$plugin_resource_text" 'V2 sampler workaround toggle'
require 'key:MOBILEGL_COHERENT_AS_FLUSH' "$plugin_resource_text" 'V2 coherent-as-flush toggle'
require 'key:MOBILEGL_ESPRYT_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
require 'key:MOBILEGL_USE_ANGLE' "$plugin_resource_text" 'V2 ANGLE toggle'
if [[ $(grep -Fc 'fclPlugin_V2' <<<"$plugin_manifest") -ne 1 ]]; then
echo '::error::Plugin manifest must expose exactly one V2 descriptor' >&2
@@ -61,40 +61,15 @@ if ! grep -Eq '^lib/[^/]+/libMobileGL\.so$' <<<"$plugin_contents"; then
exit 1
fi
# The render server is not trace-only: both flavors carry the control screen, the offscreen
# service, the on-screen display activity, and (split native builds) the supervisor executable.
# The plugin APK ships to users, so its build keeps the three components private (manifest
# placeholder serverComponentsExported=false); only the debuggable trace APK exports them for adb.
for component in ServerControlActivity MobileGLServerService MobileGLDisplayActivity; do
require "$component" "$plugin_manifest" "plugin render server component ($component)"
require "$component" "$trace_manifest" "trace render server component ($component)"
done
if [[ "${MOBILEGL_CI_DISAGGREGATED:-OFF}" == "ON" ]]; then
trace_contents=$(unzip -Z1 "$trace_apk")
for entry in "plugin:${plugin_contents}" "trace:${trace_contents}"; do
if ! grep -Eq '^lib/[^/]+/libMobileGLServer\.so$' <<<"${entry#*:}"; then
echo "::error::${entry%%:*} APK does not contain libMobileGLServer.so (both flavors ship the render server)" >&2
exit 1
fi
done
fi
require 'top.mobilegl.plugin.trace' "$trace_manifest" 'trace package name'
require 'top.mobilegl.plugin.TRACE_REPLAY' "$trace_manifest" 'trace replay action'
if grep -Fq 'fclPlugin' <<<"$trace_manifest"; then
echo '::error::Trace APK must not advertise renderer-plugin metadata' >&2
exit 1
fi
# The trace APK IS a launchable app now: PostActivity (the POST self-test screen) is its
# entry icon and the bottom navigation bar switches to the render server screen. What is
# left of the old "no launcher" contract is the count - exactly ONE MAIN activity, so a
# screen that accidentally grows a launcher entry (ServerControlActivity was one for a day)
# reds here instead of shipping two icons.
main_count=$(grep -Fc 'android.intent.action.MAIN' <<<"$trace_manifest")
if [[ "$main_count" -ne 1 ]]; then
echo "::error::Trace APK must expose exactly one launcher activity (PostActivity); found ${main_count} android.intent.action.MAIN" >&2
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+28 -165
View File
@@ -6,28 +6,8 @@ on:
- dev
- Feat/Backend-Direct-GLES
- Feat/Backend-Direct-Vulkan
# TEMPORARY, remove before merging the MGPipe work into dev: the disaggregation
# branch runs the full lane on every push so a phase's landing is not gated on
# someone remembering to dispatch the workflow by hand.
- feat/disaggregated
- codex/fix-github-ci
# A docs-only push builds nothing this workflow tests (231 emulator legs per run). The Test
# workflow keeps running for docs: it hosts the doc citation lint.
paths-ignore:
- 'docs/**'
- '**/*.md'
workflow_dispatch:
env:
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' && github.ref != 'refs/heads/dev' }}
jobs:
build:
runs-on: ubuntu-latest
@@ -41,7 +21,6 @@ jobs:
CCACHE_MAXSIZE: 4G
CCACHE_NOHASHDIR: "true"
MOBILEGL_CMAKE_COMPILER_LAUNCHER: ccache
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -59,9 +38,6 @@ 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:
@@ -85,8 +61,6 @@ jobs:
uses: android-actions/setup-android@v4
with:
accept-android-sdk-licenses: false
packages: platform-tools
cmdline-tools-version: 13114758
- name: Accept Android SDK licenses
run: yes | sdkmanager --licenses >/dev/null
@@ -101,13 +75,7 @@ jobs:
run: python update_glslang_sources.py
- name: Build plugin APK
run: >-
gradle --no-daemon -p android-plugin :app:assemblePluginRelease
-Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO
-Pmobilegl.buildDisaggregated="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.buildDisaggregatedInproc="${MOBILEGL_CI_DISAGGREGATED}"
-Pmobilegl.pipePush="${MOBILEGL_CI_DISAGGREGATED}"
--parallel --max-workers "$(nproc)"
run: gradle --no-daemon -p android-plugin :app:assemblePluginRelease -Pmobilegl.apkSuffix="${GITHUB_SHA}" -Pmobilegl.logLevel=MOBILEGL_LOG_LEVEL_INFO --parallel --max-workers "$(nproc)"
env:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
@@ -149,14 +117,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 }}
@@ -238,8 +199,6 @@ jobs:
name: trace case matrix
runs-on: ubuntu-latest
needs: build
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
outputs:
android: ${{ steps.trace-cases.outputs.android }}
names: ${{ steps.trace-cases.outputs.names }}
@@ -250,12 +209,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-matrix)" >> "$GITHUB_OUTPUT"
echo "names=$(python3 tools/trace_replay/trace_cases.py --ci --format names)" >> "$GITHUB_OUTPUT"
trace-fixtures:
@@ -339,8 +293,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
@@ -368,7 +320,7 @@ jobs:
--avd-name "${AVD_NAME}"
retrace:
name: retrace (${{ matrix.backend.name }} / ${{ matrix.transport || 'monolith' }}, ${{ matrix.case.name }})
name: retrace (${{ matrix.backend.name }}, ${{ matrix.case.name }})
runs-on: ubuntu-latest
needs:
- build
@@ -378,8 +330,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
@@ -389,7 +339,7 @@ jobs:
max-parallel: 4
matrix: ${{ 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
@@ -418,8 +368,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
@@ -458,7 +406,7 @@ jobs:
--profile pixel_6 \
--avd-name "${AVD_NAME}"
- name: Launch emulator
- name: Launch Emulator
run: |
sh android-plugin/run-avd-ci.sh start \
--avd-name "${AVD_NAME}" \
@@ -469,41 +417,13 @@ jobs:
- name: Retrace and validate
env:
MOBILEGL_ESPRYT_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_USE_ANGLE: ${{ matrix.backend.name == 'DirectGLES' && '1' || '0' }}
MOBILEGL_TRACE_ANGLE_VARIANT: ${{ matrix.case.name == 'minecraft-1.21.4-fabric-iris-bliss-in-world' && '90a62123d794' || 'ec889e6ea831' }}
MOBILEGL_MAGMA_R11G11B10F_FALLBACK: ${{ matrix.backend.name == 'DirectVulkan' && '1' || '0' }}
MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: ${{ matrix.backend.name == 'DirectVulkan' && matrix.case.name == 'minecraft-1.21.4-fabric-iris-iterationrp-in-world' && '1' || '0' }}
run: |
apk_file="android-retrace-apks/MobileGL-plugin-trace-release-${GITHUB_SHA}.apk"
test -f "${apk_file}"
extra_retrace_args=()
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--require-inproc
)
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
# THE SAME CLIENT KNOBS AS inproc, so the two acceptance lanes differ in exactly one
# thing: which PROCESS the server role runs in. They are scrubbed out of the CHILD's
# environment by the launcher and read by the client, which is where they belong.
#
# MOBILEGL_IPC_SERVER_PATH IS DELIBERATELY ABSENT. nativeLibraryDir carries an
# install-time hash, so nothing off the device can spell it; TraceReplayActivity
# resolves it from getApplicationInfo() when the env asks for spawn, and an explicit
# value would override that. --use-pbuffer is NOT optional here: until P12 a spawned
# server cannot be handed a window at all, because an ANativeWindow* is a pointer
# into the CLIENT's process and SetWindowHandle is refused on the way across with
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--use-pbuffer
--require-spawn
)
else
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
fi
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
if [ "${{ matrix.backend.name }}" = "DirectGLES" ] && [ "${{ matrix.case.name }}" = "minecraft-1.21.4-fabric-iris-bliss-in-world" ]; then
@@ -585,12 +505,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
@@ -615,7 +529,7 @@ jobs:
free -h > android-retrace-result/diagnostics/host-memory.txt 2>&1 || true
sudo dmesg -T 2>/dev/null | tail -300 > android-retrace-result/diagnostics/host-dmesg.txt || true
- name: Stop emulator
- name: Stop Emulator
if: always()
run: |
sh android-plugin/run-avd-ci.sh stop \
@@ -627,7 +541,7 @@ jobs:
if: always()
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-retrace-result-${{ matrix.lane || 'monolith-control' }}-${{ matrix.transport || 'monolith' }}-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
name: MobileGL-android-retrace-result-${{ env.date_today }}-${{ github.sha }}-${{ matrix.backend.name }}-${{ matrix.case.name }}
path: android-retrace-result/**
if-no-files-found: warn
@@ -636,8 +550,6 @@ jobs:
runs-on: ubuntu-latest
needs: retrace
if: always()
env:
MOBILEGL_CI_DISAGGREGATED: ${{ (github.ref_name == 'feat/disaggregated' || github.ref_name == 'codex/fix-github-ci') && 'ON' || 'OFF' }}
steps:
- name: Checkout repo
@@ -652,73 +564,25 @@ jobs:
with:
node-version: '22'
- name: Download Android monolith control results
- name: Download Android retrace results
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-monolith-control-monolith-*
path: retrace-artifacts/monolith-control
pattern: MobileGL-android-retrace-result-*
path: retrace-artifacts
- name: Download Android inproc acceptance results
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-inproc-acceptance-inproc-*
path: retrace-artifacts/inproc-acceptance
- name: Download Android spawn acceptance results
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/download-artifact@v8
with:
pattern: MobileGL-android-retrace-result-spawn-acceptance-spawn-*
path: retrace-artifacts/spawn-acceptance
- name: Render monolith control summary
- name: Render retrace summary
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts/monolith-control \
--output-dir android-retrace-summary/monolith-control \
--title "MobileGL Android monolith control" \
--group-label "Android Emulator / monolith control" \
--html mobilegl-android-monolith-control.html
--input retrace-artifacts \
--output-dir android-retrace-summary \
--title "MobileGL Android retrace overview" \
--group-label "Android Emulator" \
--html mobilegl-android-retrace-overview.html
- name: Render inproc acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
run: |
node tools/trace_replay/render_retrace_summary.mjs \
--input retrace-artifacts/inproc-acceptance \
--output-dir android-retrace-summary/inproc-acceptance \
--title "MobileGL Android inproc acceptance" \
--group-label "Android Emulator / inproc acceptance" \
--html mobilegl-android-inproc-acceptance.html
- name: Render spawn acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
run: |
node tools/trace_replay/render_retrace_summary.mjs --input retrace-artifacts/spawn-acceptance --output-dir android-retrace-summary/spawn-acceptance --title "MobileGL Android spawn acceptance" --group-label "Android Emulator / spawn acceptance" --html mobilegl-android-spawn-acceptance.html
- name: Upload Android monolith control summary
- name: Upload Android retrace summary
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-monolith-control-summary-${{ github.sha }}
path: android-retrace-summary/monolith-control/mobilegl-android-monolith-control.html
archive: false
if-no-files-found: error
- name: Upload Android inproc acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-inproc-acceptance-summary-${{ github.sha }}
path: android-retrace-summary/inproc-acceptance/mobilegl-android-inproc-acceptance.html
archive: false
if-no-files-found: error
- name: Upload Android spawn acceptance summary
if: env.MOBILEGL_CI_DISAGGREGATED == 'ON'
uses: actions/upload-artifact@v7
with:
name: MobileGL-android-spawn-acceptance-summary-${{ github.sha }}
path: android-retrace-summary/spawn-acceptance/mobilegl-android-spawn-acceptance.html
path: android-retrace-summary/mobilegl-android-retrace-overview.html
archive: false
if-no-files-found: error
@@ -734,19 +598,15 @@ 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):"
@@ -789,6 +649,9 @@ jobs:
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)."
+38 -2961
View File
File diff suppressed because it is too large Load Diff
-31
View File
@@ -1,31 +0,0 @@
name: Arch WSL GPU smoke
on:
workflow_dispatch:
push:
branches:
- ci/arch-wsl-gpu-runner
paths:
- .github/workflows/wsl-gpu-smoke.yml
- docs/ci/arch-wsl-gpu-runner.md
permissions: {}
concurrency:
group: arch-wsl-gpu-smoke
cancel-in-progress: false
jobs:
gpu-smoke:
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
timeout-minutes: 5
steps:
- name: Report runner identity
shell: bash
run: |
printf 'Runner: %s\n' "$RUNNER_NAME"
uname -n
uname -a
- name: Verify hardware GLES and Vulkan
shell: bash
run: /usr/local/libexec/mobilegl-runner/gpu-smoke
+4 -3
View File
@@ -1,4 +1,4 @@
################################################################################
################################################################################
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
################################################################################
@@ -16,6 +16,9 @@ MobileGLCodeManager
MobileGL/MG_Test/build
/build_*
/cmake-build*
/build-*/
/local.properties
/.jspace/
.idea
MobileGL/MG*/build*
MobileGL/MG*/cmake-build*
@@ -27,5 +30,3 @@ MobileGL/MG*/cmake-build*
tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
-3
View File
@@ -34,6 +34,3 @@
[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/flatbuffers deleted from 7e163021e5
-11
View File
@@ -1,11 +0,0 @@
# glvnd vendor ABI headers
'libeglabi.h' and 'GLdispatchABI.h' from libglvnd (MIT), copied verbatim with their own licence
headers intact.
They are here because MobileGL implements the *vendor* half of glvnd's EGL ABI - the side a library
exposes so the system's 'libEGL.so.1', which IS glvnd, can dispatch to it. The distribution ships
the runtime but not these headers in the base image, and a vendored copy of two MIT headers is a
smaller dependency than a build-time download of a source tree.
Upstream ABI version implemented: 'EGL_VENDOR_ABI_MAJOR_VERSION' 0 (the header pins it).
-141
View File
@@ -1,141 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#include <GL/gl.h>
#if !defined(__GL_DISPATCH_ABI_H)
#define __GL_DISPATCH_ABI_H
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup gldispatchabi GL dispatching ABI
*
* This is not a complete ABI, but rather a fragment common to the libEGL and
* libGLX ABIs. Changes to this file should be accompanied by a version bump to
* these client ABIs.
*/
/*!
* Thread-local implementation used by libglvnd. This is passed into the patch
* function callback via the type parameter.
*
* For most architectures, the vendor library can ignore this parameter, since
* it will always be the same value. It's used for systems like ARM, where the
* stubs might be use the ARM or Thumb instruction sets.
*
* The stub type does not make any distinction between TLS and TSD stubs. The
* entire purpose of entrypoint rewriting is to skip the dispatch table in
* libGLdispatch.so, so it doesn't matter how that dispatch table is stored.
*/
enum {
/*!
* Indicates that the stubs aren't defined in assembly. For example, if the
* dispatch stubs are written in C. Vendor libraries generally won't see
* this value.
*/
__GLDISPATCH_STUB_UNKNOWN,
/*!
* Used for stubs on x86 systems.
*/
__GLDISPATCH_STUB_X86,
/*!
* Used for stubs on x86-64 systems.
*/
__GLDISPATCH_STUB_X86_64,
/*!
* Used for stubs on ARMv7, using the Thumb instruction set.
*/
__GLDISPATCH_STUB_ARMV7_THUMB,
/*!
* Used for stubs on ARMv7, using the normal ARM instruction set.
*/
__GLDISPATCH_STUB_ARMV7_ARM,
/*!
* Used for stubs on ARMv8/aarch64.
*/
__GLDISPATCH_STUB_AARCH64,
/*!
* Used for stubs on x32 builds (x86-64 with 32-bit pointers).
*/
__GLDISPATCH_STUB_X32,
/*!
* Used for stubs on PPC64 systems.
*/
__GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on PPC64LE systems. Same as PPC64, for compatibility.
*/
__GLDISPATCH_STUB_PPC64LE = __GLDISPATCH_STUB_PPC64,
/*!
* Used for stubs on LOONGARCH/loongarch64.
*/
__GLDISPATCH_STUB_LOONGARCH64,
};
/*!
* A callback function called by the vendor library to fetch the address of an
* entrypoint.
*
* The function returns two pointers, one writable and one executable. The two
* pointers may or may not be the same virtual address, but they will both be
* mappings of the same physical memory.
*
* The vendor library should write its entrypoint to the address returned by
* \p writePtr, but should use the address from \p execPtr for things like
* calculating PC-relative offsets.
*
* Note that if this function fails, then the vendor library can still try to
* patch other entrypoints.
*
* Note that on ARM, the low-order bit of both \c execPtr and \p writePtr will
* be zero, even if the stub uses the thumb instruction set. The vendor library
* should use the \c type parameter of \c initiatePatch to determine which
* instruction set to use.
*
* \param funcName The function name.
* \param[out] writePtr The pointer that the vendor library can write to.
* \param[out] execPtr The pointer to the executable code.
* \return GL_TRUE if the entrypoint exists, or GL_FALSE if it doesn't.
*/
typedef GLboolean (*DispatchPatchLookupStubOffset)(const char *funcName,
void **writePtr, const void **execPtr);
#if defined(__cplusplus)
}
#endif
#endif // __GL_DISPATCH_ABI_H
-464
View File
@@ -1,464 +0,0 @@
/*
* Copyright (c) 2013, NVIDIA CORPORATION.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and/or associated documentation files (the
* "Materials"), to deal in the Materials without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Materials, and to
* permit persons to whom the Materials are furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* unaltered in all copies or substantial portions of the Materials.
* Any additions, deletions, or changes to the original source files
* must be clearly indicated in accompanying documentation.
*
* THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
*/
#if !defined(__LIB_EGL_ABI_H)
#define __LIB_EGL_ABI_H
#include <stdint.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include "glvnd/GLdispatchABI.h"
#if defined(__cplusplus)
extern "C" {
#endif
/*!
* \defgroup eglvendorabi EGL Vendor ABI
*
* Definition of ABI exported by libEGL.so to libEGL_VENDOR.so libraries.
*
* Each vendor is associated with three distinct dispatch table types:
*
* - static EGL dispatch table: this is the fixed list of EGL 1.5 entrypoints
* provided by the vendor at load time during the initial handshake.
* - dynamic EGL dispatch table: this is a structure allocated by the API
* library at runtime used to manage EGL extension functions which are not
* present in the static table.
* - core GL dispatch table: this is a structure maintained by the API library
* which contains both GL core (static) and GL extension (dynamic) functions.
*
* @{
*/
/*
* Rendering API handling:
*
* libEGL only supports OpenGL and OpenGL ES, not OpenVG. If OpenVG or any
* other API is added, then the major version number will be incremented.
*
* When the application calls eglBindAPI, libEGL will forward the call to every
* vendor library. In addition, a vendor library can query the current API from
* libEGL using the getCurrentApi callback.
*
* Vendor libraries are not required to support both GL and GLES, but they must
* be able to deal with either one as the current rendering API. If a vendor
* doesn't support the current API, then it should return an error from
* eglCreateContext.
*/
/*!
* Current version of the ABI.
*
* This version number contains a major number in the high-order 16 bits, and
* a minor version number in the low-order 16 bits.
*
* The major version number is incremented when an interface change will break
* backwards compatibility with existing vendor libraries. The minor version
* number is incremented when there's a change but existing vendor libraries
* will still work.
*/
#define EGL_VENDOR_ABI_MAJOR_VERSION ((uint32_t) 0)
#define EGL_VENDOR_ABI_MINOR_VERSION ((uint32_t) 2)
#define EGL_VENDOR_ABI_VERSION ((EGL_VENDOR_ABI_MAJOR_VERSION << 16) | EGL_VENDOR_ABI_MINOR_VERSION)
static inline uint32_t EGL_VENDOR_ABI_GET_MAJOR_VERSION(uint32_t version)
{
return version >> 16;
}
static inline uint32_t EGL_VENDOR_ABI_GET_MINOR_VERSION(uint32_t version)
{
return version & 0xFFFF;
}
/*!
* This opaque structure stores function pointers for EGL extension functions.
* It is allocated at runtime by the API library. Vendor-provided dispatch
* functions retrieve and operate on this structure using the API below.
*/
typedef struct __EGLvendorInfoRec __EGLvendorInfo;
/****************************************************************************
* API library exports *
****************************************************************************/
typedef struct __EGLapiExportsRec {
/************************************************************************
* The following routines are used by vendor-implemented EGL dispatch
* functions to lookup and call into the right vendor.
************************************************************************/
/*!
* This function must be called at the start of every EGL dispatch stub. It
* performs any necessary per-call bookkeeping.
*/
void (* threadInit) (void);
EGLenum (* getCurrentApi)(void);
/*!
* This function retrieves the appropriate current dynamic dispatch table,
* if a GL context is current. Otherwise, this returns NULL.
*/
__EGLvendorInfo *(*getCurrentVendor)(void);
/*!
* This retrieves the current context for this thread.
*/
EGLContext (*getCurrentContext)(void);
/*!
* Returns the current display for this thread.
*/
EGLDisplay (*getCurrentDisplay)(void);
/*!
* Returns the current drawable for this thread.
*/
EGLSurface (*getCurrentSurface)(EGLint readDraw);
/*!
* This function retrieves an entry point from the dynamic dispatch table
* given an index into the table.
*/
__eglMustCastToProperFunctionPointerType (*fetchDispatchEntry) (__EGLvendorInfo *dynDispatch, int index);
/*!
* Sets the last error for the current thread. The error will be returned
* the next time the app calls eglGetError().
*
* This function will override a call to \c setLastVendor, and vice-versa.
*/
void (* setEGLError) (EGLint errorCode);
/*!
* Notifies libEGL about the vendor library that an EGL function is
* dispatched to. This is used to look up the last EGL error code from a
* vendor.
*
* A vendor library may generate an EGL error asynchronously, if it
* offloads some EGL functions onto a worker thread. In that case, calling
* \c setEGLError to set the last error would not work.
*
* When an EGL dispatch stubs finds the vendor library, it must call this
* function before it dispatches the function call.
*
* This function will override a call to \c setEGLError, and vice-versa.
*
* \param vendor The vendor library of the current EGL function call.
* \return EGL_TRUE on success, or EGL_FALSE on failure.
*/
EGLBoolean (* setLastVendor) (__EGLvendorInfo *vendor);
/************************************************************************
* These routines are used by vendor dispatch functions to look up
* and add mappings between various objects and vendors.
************************************************************************/
// As far as I can tell, all EGL functions that take a context or drawable
// also take an EGLDisplay. So, we don't need any extra functions for
// mapping a context/surface/whatever to a EGLDisplay or vendor.
/*!
* Returns the EGL vendor for an EGLDisplay handle.
*/
__EGLvendorInfo *(*getVendorFromDisplay)(EGLDisplay dpy);
/*!
* Returns the EGL vendor for an EGLDeviceEXT handle.
*/
__EGLvendorInfo *(*getVendorFromDevice)(EGLDeviceEXT dev);
/**
* Sets the EGL vendor for an EGLDeviceEXT handle. The dispatch stub for
* any function that returns an EGLDeviceEXT handle should call this.
*
* Supported since ABI version 1.2.
*/
EGLBoolean (* setVendorForDevice)(EGLDeviceEXT dev, __EGLvendorInfo *vendor);
} __EGLapiExports;
/*****************************************************************************
* API library imports *
*****************************************************************************/
/*!
* The enum values accepted by \c __EGLapiImports::getVendorString.
*/
enum
{
/*!
* The set of platform extensions that the vendor supports.
*
* Platform extensions typically just add enums to the existing
* eglGetPlatformDisplay function, so we don't need any additional logic
* or special handling from libEGL itself.
*
* As a result, vendor libraries can add these extensions without having
* to modify libEGL to match.
*/
__EGL_VENDOR_STRING_PLATFORM_EXTENSIONS,
};
/*!
* This structure stores required and optional vendor library callbacks.
*/
typedef struct __EGLapiImportsRec {
/**
* Creates an EGLDisplay. This function is used to handle both
* \c eglGetDisplay and \c eglGetPlatformDisplay.
*
* For some platform types, libEGL may be able to identify a specific
* vendor to use.
*
* If libEGL can't figure out which vendor to use on its own, then it will
* go through the list of available vendor libraries and call this function
* until one succeeds.
*
* If the application calls eglGetPlatformDisplay, then the parameters are
* passed through from the application.
*
* If the application calls eglGetDisplay, and the native display handle is
* not \c EGL_DEFAULT_DISPLAY, then libEGL will try to guess a platform
* type, and will pass that platform to the vendor library.
*
* Lastly, if eglGetDisplay is called with \c EGL_DEFAULT_DISPLAY, then
* libEGL will call into each vendor library with \p platform set to
* \c EGL_NONE. The vendor library can then select a default display to
* return.
*
* In all cases, if the vendor library can't find a matching EGLDisplay,
* then it should return \c EGL_NO_DISPLAY. Any errors should be reported
* through the vendor's \c eglGetError function.
*/
EGLDisplay (* getPlatformDisplay) (EGLenum platform, void *nativeDisplay,
const EGLAttrib *attrib_list);
/*!
* Checks if the vendor library supports a given client API (that is, the
* API value passed to \c eglBindAPI).
*/
EGLBoolean (* getSupportsAPI) (EGLenum api);
/*!
* (OPTIONAL) Returns a string from the vendor library. This is used for
* anything that isn't available from eglQueryString.
*/
const char * (* getVendorString) (int name);
/*
* NOTE: libEGL will load all of the vendor libraries up front, so it
* handles extension functions slightly differently than EGL.
*/
/*!
* This retrieves the pointer to the real EGL or core GL function.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function.
*/
void * (* getProcAddress) (const char *procName);
/*!
* This retrieves vendor-neutral functions which use the
* __EGLdispatchTableDynamic API above to dispatch to the correct vendor.
*
* A vendor library must provide a dispatch function for all EGL display
* extension functions that it supports.
*
* Client extension functions cannot be dispatched based on an EGDisplay,
* so they must be handled in libEGL itself.
*
* \param procName The name of the function.
* \return A pointer to a function, or \c NULL if the vendor does not
* support the function or \p procName is not a EGL display function.
*/
void * (*getDispatchAddress) (const char *procName);
/*!
* This notifies the vendor library which dispatch table index is
* assigned to a particular EGL extension function.
*/
void (*setDispatchIndex) (const char *procName, int index);
/*
* The vendor library may use the isPatchSupported, initiatePatch,
* releasePatch, and patchThreadAttach callbacks to re-write libglvnd's
* entrypoints at make current time, provided no other contexts are current
* and the TLS model supports this functionality. This is a performance
* optimization that may not be available at runtime; the vendor library
* must not depend on this functionality for correctness.
*
* To use this optimization, the vendor library must provide at least the
* isPatchSupported and initiatePatch entrypoints.
*
* Note that these functions are identical to the ones used in GLX.
*/
/*!
* (OPTIONAL) Checks to see if the vendor library supports patching the
* given stub type and size.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*/
GLboolean (* isPatchSupported)(int type, int stubSize);
/*!
* (OPTIONAL) Called by libglvnd to request that a vendor library patch its
* top-level entrypoints.
*
* The vendor library should use the \p lookupStubOffset callback to find
* the addresses of each entrypoint.
*
* This function may be called more than once to patch multiple sets of
* entrypoints. For example, depending on how they're built, libOpenGL.so
* or libGL.so may have their own entrypoints that are separate functions
* from the ones in libGLdispatch.
*
* Note that during this call is the only time that the entrypoints can be
* modified. After the call to \c initiatePatch returns, the vendor library
* should treat the entrypoints as read-only.
*
* \param type The type of entrypoints. This will be a one of the
* __GLDISPATCH_STUB_* values.
* \param stubSize The maximum size of the stub that the vendor library can
* write, in bytes.
* \param lookupStubOffset A callback into libglvnd to look up the address
* of each entrypoint.
*
* \return GL_TRUE if the vendor library supports patching with this type
* and size.
*/
GLboolean (*initiatePatch)(int type,
int stubSize,
DispatchPatchLookupStubOffset lookupStubOffset);
/*!
* (OPTIONAL) Called by libglvnd to notify the current vendor that it no
* longer owns the top-level entrypoints.
*
* Libglvnd will take care of the restoring the entrypoints back to their
* original state. The vendor library must not try to modify them.
*/
void (*releasePatch)(void);
/*!
* (OPTIONAL) Called at the start of window-system functions (GLX and EGL).
* This callback allows vendor libraries to perform any per-thread
* initialization.
*
* This is basically a workaround for broken applications. A lot of apps
* will make one or more invalid GLX/EGL calls on a thread (often including
* a MakeCurrent with invalid parameters), and then will try to call an
* OpenGL function.
*
* A non-libglvnd-based driver would be able to initialize any thread state
* even on a bogus GLX call, but with libglvnd, those calls wouldn't get
* past libGLX.
*
* This function is optional. If it's \c NULL, then libGLdispatch will
* simply ignore it.
*
* \note This function may be called concurrently from multiple threads.
*/
void (*patchThreadAttach)(void);
/*!
* (OPTIONAL) Tries to determine the platform type for a native display.
*
* If the vendor library provides this function, then libglvnd will call it
* to determine which platform to use for a native display handle in
* eglGetDisplay.
*
* If no vendor library identifies the platform, then libglvnd will fall
* back to its own platform detection logic.
*
* Libglvnd can call this function for any native display handle except
* \c EGL_DEFAULT_DISPLAY.
*
* No matter what the value of \p native_display, the vendor library must
* not crash, and must not return a false match. If the vendor library
* can't identify the display, then it must return \c EGL_NONE.
*
* In particular, that means that a vendor library must not return any sort
* of default or fallback platform.
*
* \param native_display The native display handle passed to eglGetDisplay.
* \return Either a platform type enum or EGL_NONE.
*/
EGLenum (* findNativeDisplayPlatform) (void *native_display);
} __EGLapiImports;
/*****************************************************************************/
#define __EGL_MAIN_PROTO_NAME "__egl_Main"
typedef EGLBoolean (* __PFNEGLMAINPROC) (uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* Vendor libraries must export a function called __egl_Main() with the
* following prototype.
*
* This function also performs a handshake based on the ABI version number.
* Vendor libraries can optionally use the version number to support older
* versions of the ABI.
*
* \param[in] version The ABI version. The upper 16 bits contains the major version
* number, and the lower 16 bits contains the minor version number.
*
* \param[in] exports The table of functions provided by libEGL. This pointer will
* remain valid for as long as the vendor is loaded.
*
* \param[in] vendor The opaque pointer used to identify this vendor library. This
* may be used in future versions to provide additional per-vendor information.
*
* \param[out] imports The function table that the vendor library should fill
* in. The vendor library must assign every non-optional function in the
* struct.
*
* \return True on success. If the vendor library does not support the
* requested ABI version or if some other error occurs, then it should return
* False.
*/
EGLBoolean __egl_Main(uint32_t version, const __EGLapiExports *exports,
__EGLvendorInfo *vendor, __EGLapiImports *imports);
/*!
* @}
*/
#if defined(__cplusplus)
}
#endif
#endif /* __LIB_EGL_ABI_H */
+40 -545
View File
@@ -14,35 +14,6 @@ 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")
@@ -187,61 +158,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
@@ -274,7 +199,6 @@ set(SPIRV_REFLECT_ENABLE_ASSERTS OFF CACHE BOOL "Enable asserts for debugging"
set(SPIRV_REFLECT_ENABLE_ASAN OFF CACHE BOOL "Use address sanitization" FORCE)
set(SPIRV_REFLECT_INSTALL OFF CACHE BOOL "Whether to install" FORCE)
# add_subdirectory(3rdparty/DiligentCore)
add_subdirectory(3rdparty/glslang)
add_subdirectory(3rdparty/SPIRV-Cross)
add_subdirectory(3rdparty/VulkanMemoryAllocator)
@@ -286,6 +210,23 @@ set(XXHASH_BUILD_XXHSUM OFF)
option(BUILD_SHARED_LIBS OFF)
add_subdirectory(3rdparty/xxHash/build/cmake xxhash_build EXCLUDE_FROM_ALL)
# Diligent-based backend. Enabled by default on local builds; only the Vulkan
# engine from DiligentCore is built. Added after the other 3rdparty projects so
# DiligentCore reuses the glslang / SPIRV-Cross / SPIRV-Tools / xxHash targets
# already defined by MobileGL instead of building its bundled copies.
option(MOBILEGL_ENABLE_DILIGENT "Enable the Diligent/Vulkan backend" ON)
if(MOBILEGL_ENABLE_DILIGENT)
set(DILIGENT_NO_DIRECT3D11 ON CACHE BOOL "Disable Direct3D11 backend" FORCE)
set(DILIGENT_NO_DIRECT3D12 ON CACHE BOOL "Disable Direct3D12 backend" FORCE)
set(DILIGENT_NO_OPENGL ON CACHE BOOL "Disable OpenGL backend" FORCE)
set(DILIGENT_NO_METAL ON CACHE BOOL "Disable Metal backend" FORCE)
set(DILIGENT_NO_WEBGPU ON CACHE BOOL "Disable WebGPU backend" FORCE)
set(DILIGENT_NO_ARCHIVER ON CACHE BOOL "Disable Archiver" FORCE)
set(DILIGENT_BUILD_TESTS OFF CACHE BOOL "Build Diligent tests" FORCE)
set(DILIGENT_INSTALL_CORE OFF CACHE BOOL "Install DiligentCore" FORCE)
add_subdirectory(3rdparty/DiligentCore)
endif()
set(TRACY_ENABLE ${MOBILEGL_ENABLE_TRACY} CACHE BOOL "Enable Tracy, this is an internal variable" FORCE)
if (TRACY_ENABLE)
@@ -304,7 +245,6 @@ set(SOURCE_FILES
MobileGL/ConfigLoader.cpp
MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/X11/DisplayGuard.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
@@ -314,8 +254,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
@@ -348,7 +286,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
@@ -357,61 +294,36 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecomposeWorkgroupVec3Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemoteFloat64Pass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenFloat64StorageBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerViewportIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PackDoubleVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenXfbInterfaceBlocksPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/UniquifyIoBlockNamesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripIoBlockLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/SplitArrayVertexInputsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ZeroBaseVertexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeriveNumSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPBarrierPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeFragmentOutputIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LegalizeResourceArrayIndexPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FlattenAtomicCounterBlockPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DemotePointSizePass.cpp
MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp
MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp
MobileGL/MG_Util/SelfTest/DriverBugProbes.cpp
MobileGL/MG_Util/SelfTest/PersistentBufferOrderingProbe.cpp
MobileGL/MG_Util/SelfTest/DriverPost.cpp
MobileGL/MG_Util/SelfTest/DriverPostIterationRPWitness.cpp
MobileGL/MG_Util/SelfTest/PrimitivesGeneratedNoXfbProbe.cpp
MobileGL/MG_Util/Texture/PixelStoreProcessor.cpp
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/EGLImpl/EGLImpl.cpp
MobileGL/MG_Impl/EGLImpl/WaylandWindow.cpp
# @INSERTION_POINT:SOURCE_FILE_GLIMPL@ #
MobileGL/MG_Impl/GLImpl/Sampler/Validators.cpp
@@ -424,7 +336,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
@@ -441,7 +352,6 @@ set(SOURCE_FILES
MobileGL/MG_Backend/Init.cpp
MobileGL/MG_Backend/BackendObject.cpp
MobileGL/MG_Backend/GpuHangWatch.cpp
MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp
MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp
@@ -451,7 +361,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
@@ -469,10 +378,8 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/GLState/ShareGroupState.cpp
MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
@@ -486,12 +393,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
@@ -507,202 +412,11 @@ 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")
if(MOBILEGL_ENABLE_DILIGENT)
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
MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.cpp
)
endif()
@@ -720,14 +434,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)
@@ -754,6 +460,21 @@ set(MOBILEGL_LINK_LIBRARIES
Threads::Threads
)
if(MOBILEGL_ENABLE_DILIGENT)
list(APPEND MOBILEGL_LINK_LIBRARIES
Diligent-GraphicsEngineVk-static
Diligent-GraphicsEngine
Diligent-GraphicsEngineNextGenBase
Diligent-GraphicsAccessories
Diligent-ShaderTools
Diligent-GraphicsTools
Diligent-Common
Diligent-Primitives
Diligent-TargetPlatform
Vulkan::Headers
)
endif()
set(MOBILEGL_COMPILE_DEF
-DVMA_STATIC_VULKAN_FUNCTIONS=0
-DVMA_DYNAMIC_VULKAN_FUNCTIONS=1
@@ -764,29 +485,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}
@@ -794,19 +497,9 @@ 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
)
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}
)
@@ -847,11 +540,12 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_TRACE_ANGLE_VARIANTS}>:MOBILEGL_TRACE_ANGLE_VARIANTS=1>
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
)
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}>"
@@ -905,6 +599,7 @@ if(NOT ANDROID)
PUBLIC
${MOBILEGL_COMPILE_DEF}
MOBILEGL_LOG_ACTIVE_LEVEL=${MOBILEGL_LOG_ACTIVE_LEVEL}
$<$<BOOL:${MOBILEGL_ENABLE_DILIGENT}>:MOBILEGL_ENABLE_DILIGENT=1>
)
endif()
@@ -915,28 +610,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
@@ -1010,17 +683,6 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
endif ()
endif ()
# Discover gtest cases when ctest starts, one binary after another, instead of in a post-build
# step. The post-build discovery of the CMake releases that never pass the target name down
# writes every binary's `--gtest_output=json` result to ONE per-directory file
# (cmake_test_discovery_<hash of an empty name>.json), so binaries linked in parallel in the same
# directory overwrite and delete each other's list: the registered count then differs between
# runs of the same tree and a binary ends up with none or only some of its cases. The pre-test
# mode reads the list from stdout, serially, and also works when the binary is cross-built.
if (NOT DEFINED CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE)
set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
endif()
if (NOT ANDROID)
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
# at the build-tree root. This lets `ctest` be invoked from the top-level
@@ -1049,170 +711,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()
+278
View File
@@ -0,0 +1,278 @@
# Handoff: Diligent/Vulkan GL3.2 Backend for MobileGL
Date: 2026-08-18
Branch: `feat/diligent-vulkan-backend`
Repo: `~/MobileGL-dev`
Status: **Active work-in-progress. Do not mark complete yet.**
---
## 1. Goal
Implement a complete OpenGL 3.2 front-end emulation on a new Diligent/Vulkan backend inside MobileGL, instead of the DirectVulkan / DirectGLES backends.
Target state:
- Fully wire MobileGL front-end `MG_State` (buffers, VAO, program, texture, sampler, framebuffer, render-state) into Diligent.
- Implement all GL 3.2 core entry points through the Diligent backend.
- Pass local non-Android GL3.2 tests on the Turnip Adreno 750 GPU.
---
## 2. Current Branch / Commits
Latest 12 commits on `feat/diligent-vulkan-backend`:
```
2f5abf83 test(diligent): verify indexed DrawElements path from real frontend state
c31b7381 feat(diligent): add basic texture binding and textured state-draw test
8945c507 feat(diligent): clear depth in GL Clear when GL_DEPTH_BUFFER_BIT set
558d3aea feat(diligent): add offscreen depth target and depth clear
7e2f0bc8 feat(diligent): wire stencil and color-mask state into state PSO
f99786f6 feat(diligent): wire viewport/scissor state into state draws
be4cc3ce feat(diligent): wire blend/depth/cull render state into state PSO
c855e6cf feat(diligent): verify state-driven draw with real MobileGL frontend state
02e60bfa feat(diligent): add state-driven draw path (VAO/buffer/program to Diligent)
a9515c92 feat(diligent): add dynamic vertex buffer upload path
2f57582a feat(diligent): wire Clear/Draw/Present into GLFunctionsTable
beb21123 feat(diligent): add real offscreen renderer with clear and triangle draw
```
Working tree is clean.
---
## 3. Key Files
### Backend core
- `MobileGL/MG_Backend/Diligent/BackendObject_Diligent.h/.cpp`
- `BackendObject_Diligent`
- Creates Diligent Vulkan device/context
- Owns `DiligentRenderer`
- Wires `GLFunctionsTable`:
- `Clear` (color + depth)
- `DrawArrays`
- `DrawElements`
- `Present`
- `MobileGL/MG_Backend/Diligent/DiligentVulkan.h/.cpp`
- Backend identity helper / translation unit
- `MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.h/.cpp`
- Offscreen RGBA8 + D32F targets
- Clear / ClearDepth / DrawTriangle / DrawVertices
- `CreateTestTexture` (RGBA8 texture + SRV + sampler)
- `DrawFromState` (main front-end emulation draw path)
- `CreatePipelineFromState`:
- SPIR-V → Diligent shaders via SPIRV-Reflect
- VAO attributes → input layout
- primitive topology from GL mode
- blend / depth / cull / stencil / color-mask state
- `UploadVertexDataFromState`:
- packs enabled VAO attributes from `BufferObject` into interleaved vertex buffer
- supports `DrawArrays`, `DrawElements`, triangle-fan and line-loop expansion
- Static texture binding to `g_Texture` through PSO static variables + SRB
### Integration changes
- `CMakeLists.txt`
- New option `MOBILEGL_ENABLE_DILIGENT` (default ON for local)
- DiligentCore added **after** glslang/SPIRV-Cross/xxHash/Vulkan-Headers so it reuses existing CMake targets
- Diligent static libraries linked into `MobileGL` / `MobileGL_s`
- New Diligent backend sources added
- `MobileGL/MG_Backend/BackendObject.h`
- New `BackendType::DiligentVulkan`
- `MobileGL/MG_Backend/Init.cpp`
- New backend switch case
- `MobileGL/ConfigLoader.cpp`
- `MOBILEGL_BACKEND_TYPE=DiligentVulkan` accepted
- `MobileGL/MG_Test/CMakeLists.txt`
- New `MobileGL/MG_Test/Backend/Diligent` subdirectory
- `MobileGL/MG_Test/Backend/Diligent/`
- `CMakeLists.txt`
- `SanityTest.cpp`
### Local test files
- `MobileGL/MG_Test/Backend/Diligent/SanityTest.cpp`
- `CreatesDiligentDeviceAndAdvertisesGL32`
- `ClearsAndDrawsTriangleOffscreen`
- `DrawsFromMobileGLState`
- `DrawsTexturedFromMobileGLState`
- `DrawsIndexedFromMobileGLState`
- `DrawsRealTexturedFromMobileGLState`
- `DrawsUniformFromMobileGLState`
- `DrawsToOffscreenFramebufferFromMobileGLState`
- `DrawsWithScissorFromMobileGLState`
- `DrawsWithBlendFromMobileGLState`
- `DrawsWithDepthTestFromMobileGLState`
- `DrawsNamedUniformBlockFromMobileGLState`
- `DrawsWithStencilTestFromMobileGLState`
- `DrawsToRenderbufferFramebufferFromMobileGLState`
- `DrawsToMultipleColorAttachmentsFromMobileGLState`
- `DrawsIndexedBaseVertexFromMobileGLState`
---
## 4. What Works Today
Verified locally on Turnip Adreno 750:
- Diligent device/context creation
- EGL window-surface swapchain creation path through Diligent `ISwapChain` (offscreen tests still use the offscreen target)
- GL 3.2 / GLSL 1.50 capability advertisement
- Offscreen color + depth rendering
- Clear color and depth
- Real mobilegl front-end state-driven drawing:
- Program SPIR-V → Diligent shaders
- VAO attributes + bound GL buffer → interleaved vertex buffer
- `DrawArrays` path
- `DrawElements` path (index buffer)
- Texture basics:
- Offscreen texture creation
- CPU → Diligent texture (`CreateTestTexture`)
- Static sampler2D binding to `g_Texture`
- Textured draw test passes
- Render state:
- Blend enable/factors/equations
- Stencil clear + test enabled on a D24S8 default depth/stencil target
- Depth test enable/func/write mask
- Cull face enable/mode/front-face winding
- Stencil test enable/masks/ops/func/ref
- Color write mask
- Viewport
- Scissor rect
- Texture/sampler full integration:
- `ITextureObject` → Diligent `ITexture` + SRV with automatic dirty upload
- `SamplerObject` / texture-object sampler → Diligent `ISampler`
- Real front-end `glTexImage2D` path (not only `CreateTestTexture`) verified
- Global UBO upload:
- Front-end `glUniform*` shadow → Diligent uniform buffer bound as `MGL_GLOBAL_UBO`
- User framebuffer mapping:
- Current draw/read FBO resolves texture attachments to Diligent RTV/DSV
- `ReadPixels` can read back from a user FBO color attachment
- More GL entry points wired:
- `DrawRangeElements` / `DrawRangeElementsBaseVertex`
- `DrawElementsBaseVertex` with real baseVertex selection
- `MultiDrawArrays` / `MultiDrawElements` / `MultiDrawElementsBaseVertex`
- `DrawArraysInstanced` / `DrawElementsInstanced` family
- Indirect draw CPU fallback: `DrawArraysIndirect`, `DrawElementsIndirect`, `MultiDraw*Indirect`, `*IndirectCount`
- `ClearBufferfv` / `ClearBufferfi` / `ClearBufferiv` / `ClearBufferuiv` (incl. stencil clear)
- `BlitFramebuffer` / `BlitNamedFramebuffer` (same-size color copy between read/draw FBOs)
- `CopyTexImage2D` / `CopyTexSubImage2D` (whole-color copy fallback)
- `CopyImageSubData` (whole-texture copy between two texture objects)
- `GenerateMipmap` (Diligent GPU mip generation on state textures)
- `GetTexImage` / `GetTextureImage` (RGBA8 readback)
- Fence sync entries (`FenceSync` / `ClientWaitSync` / `WaitSync` / `DeleteSync` / `GetSyncStatus`) as CPU always-signaled fallback
- Timer query entries (`BeginTimeElapsedQuery` / `EndTimeElapsedQuery` / `QueryCounterTimestamp` / `GetQueryResult64` etc.) as CPU `steady_clock` fallback
- `ReadPixels` from default and user color attachments
- Primitive expansion:
- `GL_TRIANGLE_FAN` expanded to triangle list
- `GL_LINE_LOOP` expanded to line strip
- Local test result:
```
[ PASSED ] 16 tests
```
---
## 5. How to Build and Run Locally
From repo root `~/MobileGL-dev`:
```bash
cmake -S . -B build-diligent -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DMOBILEGL_ENABLE_DILIGENT=ON \
-DMOBILEGL_BUILD_TEST=ON \
-DMOBILEGL_BUILD_BENCHMARK=OFF \
-DFETCHCONTENT_SOURCE_DIR_GOOGLETEST="$PWD/3rdparty/DiligentCore/ThirdParty/googletest"
cmake --build build-diligent --target DiligentVulkanSanityTest -j 4
./build-diligent/MobileGL/MG_Test/Backend/Diligent/DiligentVulkanSanityTest --gtest_color=no
```
Notes:
- `MOBILEGL_BUILD_BENCHMARK=OFF` avoids network fetch of google/benchmark in this environment.
- `FETCHCONTENT_SOURCE_DIR_GOOGLETEST` pins googletest to DiligentCore's bundled copy, avoiding flaky network clone.
- Max 4 cores is intentional: use `-j 4`.
---
## 6. Environment Notes
- Host: Linux `aarch64`, glibc 2.43 (Fedora container on Android/Droidspaces)
- GPU: Turnip Adreno 750, Vulkan API 1.4.354
- GPU nodes available:
- `/dev/dri/renderD128`
- `/dev/kgsl-3d0`
- Android SDK/NDK: `~/android-sdk` (aarch64 glibc)
- NDK `27.3.13750724`
- CMake `3.22.1`
- JDK/Gradle for APK builds:
- `~/android-build-tools/jdk17`
- `~/android-build-tools/gradle/gradle-8.10.2`
---
## 7. Known Limitations / Not Yet Implemented
- User framebuffers now support texture color attachments, renderbuffer color readback, multiple simultaneous color targets, and depth/stencil texture or renderbuffer attachments.
- Textures auto-sync `ITextureObject` → Diligent resources, including mip levels and sampler state; compressed textures and integer/3-channel formats that Diligent lacks are still skipped.
- Global UBO (default-block `glUniform*`) and named application UBO blocks (through `glBindBufferBase`/`glUniformBlockBinding`) now upload and bind; SSBOs are still not fed from frontend buffer bindings.
- Swapchain creation and resize are wired for native EGL window surfaces via `Diligent::ISwapChain`; `Present()` presents the active swap chain when present and otherwise flushes the offscreen target. Actual on-screen EGL presentation is still untested in this headless environment, and the X11 display/connection fields are not yet plumbed through `WindowHandle`. `SetSwapInterval` now forwards the requested sync interval to `ISwapChain::Present()`.
- No transform feedback / GPU-accelerated queries / non-color readback; fence sync and timer queries use CPU fallbacks.
- Draw range, multi-draw, instanced-draw wrappers, clear-buffer, blit, read-pixels, CopyTexImage*, CopyImageSubData, GenerateMipmap, GetTexImage/GetTextureImage and indirect draws are now wired; buffer subdata paths still remain.
- A last-PSO cache now avoids recreating the pipeline when program/render-state/topology/VAO layout is unchanged; texture/UBO resources are still rebound dynamically per draw.
- The `GLFunctionsTable` is only partially populated.
---
## 8. Recommended Next Steps
1. **Framebuffer / Renderbuffer mapping**
- [x] Map `MG_State::GLState::FramebufferObject` attachments to Diligent `ITextureView` / `ITexture`.
- [x] Support default framebuffer as current offscreen target.
- [x] Support `glBindFramebuffer`, `glFramebufferTexture2D`, renderbuffer color/depth attachments and renderbuffer color readback.
- [x] Multiple simultaneous color attachments.
2. **Texture / Sampler full integration**
- [x] Translate MobileGL `ITextureObject` to Diligent `ITexture` and cache by `GetLifetimeId()`.
- [x] Propagate texture unit bindings into the PSO SRB.
- [x] Translate `SamplerObject` state into Diligent `SamplerDesc`.
3. **Uniform / UBO support**
- [x] Create Diligent buffer for `ProgramObject::GetUBOData()` / `GetUBOSize()`.
- [x] Bind the global UBO as a dynamic shader resource.
- [x] Handle per-program uniform block bindings / named UBO blocks.
4. **PSO / resource caching**
- [~] Cache PSOs by program + VAO config + render state + topology (single last-PSO fast path).
- [~] Cache textures and samplers; buffers/SRBs can still be re-bound per draw.
5. **More GL 3.2 entry points**
- [x] `DrawRangeElements`
- [x] `MultiDraw*`
- [x] `BlitFramebuffer` (same-size color copy)
- [x] `ReadPixels` from non-default framebuffer
- [x] `CopyTexImage*` / `CopyImageSubData` wired as whole-resource copies
- [x] `GetTexImage` / `GetTextureImage` (RGBA8)
- [x] Indirect draws (CPU fallback)
6. **Expand local test suite**
- [x] Scissor test
- [x] Blend test
- [x] Texture filtering / sampler state test
- [x] framebuffer offscreen render-to-texture test
- [x] Depth test visual test
- [x] Stencil test
---
## 9. Handoff Notes for Next Agent
- Do **not** reference `origin/Deprecated/Feat/Diligent`; that old implementation is intentionally ignored.
- Work from this branch, keep tests green.
- The command `./build-diligent/.../DiligentVulkanSanityTest` runs all 5 Diligent tests.
- If a new test crashes during shader resource binding, remember Diligent texture SRVs need a sampler attached via `ITextureView::SetSampler()` before `InitializeStaticSRBResources()`.
- When re-creating a PSO or buffer, call `Release()` (or assign `nullptr`) before the create call to avoid Diligent debug “Overwriting reference” assertions.
+13 -589
View File
@@ -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;
@@ -69,57 +69,17 @@ namespace MobileGL::MG_Config {
struct FeaturesTable {
// MOBILEGL_DISABLE_TIMERQUERY: do not advertise or use GPU timer queries.
Bool DisableTimerQuery = false;
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW: advertise GL_ARB_texture_view on DirectGLES when
// the host ES driver has EXT/OES_texture_view. Off by default: the host extension is
// present on Adreno 830 and the functional half of KHR-GL4{2,3}.texture_view still fails
// there, because the view's ES internalformat is normalized independently of the storage
// it aliases (see BackendObject_DirectGLES::BuildAdvertisedExtensions). The flag exists
// so that work can be done without editing the gate.
Bool EsprytEnableTextureView = false;
// MOBILEGL_ENABLE_SPIRV_VALIDATION: validate generated and transformed SPIR-V.
// Disabled by default because validation is a diagnostics-only cost.
Bool EnableSpirvValidation = false;
// MOBILEGL_ESPRYT_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool EsprytUseAngle = false;
// MOBILEGL_USE_ANGLE: load ANGLE EGL/GLES libraries.
Bool UseAngle = false;
#if defined(MOBILEGL_TRACE_ANGLE_VARIANTS)
// MOBILEGL_TRACE_ANGLE_VARIANT: signed trace-APK ANGLE build short hash.
String TraceAngleVariant;
#endif
// MOBILEGL_MAGMA_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support,
// including the opt-in emulated compute path below.
Bool MagmaDisableSubgroup = false;
// MOBILEGL_MAGMA_EMULATE_SUBGROUP: implement GL_KHR_shader_subgroup's compute
// stage on a 32-lane VIRTUAL subgroup lowered to workgroup-shared memory
// (ShaderTranspiler::EmulateSubgroupsPass). Strictly a last resort: it only ever
// engages when this flag is set AND the device has no native subgroup support at
// all - a device with real subgroup operations always uses them natively,
// whatever their width (the known iterationRP defect is patched by
// MagmaFixIterationRPSubgroupScratch below instead). Off by default.
Bool MagmaEmulateSubgroup = false;
// MOBILEGL_MAGMA_FIX_ITERATIONRP_SUBGROUP_SCRATCH: patch iterationRP's own bug - the
// pack declares `shared vec2 prefixSumCache[32]` for a 512-invocation exposure
// reduction and indexes it by gl_SubgroupID, so any device with sub-16-lane
// subgroups (8-lane lavapipe -> 64 subgroups) writes shared memory out of
// bounds. The pass grows that one array to what the device's topology needs and
// touches nothing else; it only rewrites modules positively matching the pack's
// reduction fingerprint (ShaderTranspiler::FixIterationRPSubgroupScratchPass),
// so every other shader passes through byte-identical - as does iterationRP
// itself on >= 16-lane devices. Auto is ON; ForceOff replays the pack's bug
// verbatim.
QuirkOverride MagmaFixIterationRPSubgroupScratch = QuirkOverride::Auto;
// MOBILEGL_MAGMA_ITERATIONRP_FIX_BARRIER: repair Program 203's missing workgroup
// rendezvous between its two reductions over prefixSumCache. Off by default and
// fingerprint-gated by FixIterationRPBarrierPass when enabled.
Bool MagmaIterationRPFixBarrier = false;
// MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS: replace compute gl_NumSubgroups loads with
// ceil(workgroup invocations / gl_SubgroupSize) on the NATIVE subgroup path
// (ShaderTranspiler::DeriveNumSubgroupsPass). Auto is ON: GL requires
// gl_SubgroupID < gl_NumSubgroups, Adreno's builtin reports 1 while the same
// dispatch emits IDs 0..7, and the derived value is the one Vulkan guarantees
// whenever the pipeline can request REQUIRE_FULL_SUBGROUPS (which the renderer
// does whenever local_size_x is a multiple of the native width). ForceOff returns
// to the raw driver builtin.
QuirkOverride MagmaDeriveNumSubgroups = QuirkOverride::Auto;
// MOBILEGL_DISABLE_SUBGROUP: force-disable Vulkan shader subgroup support.
Bool DisableSubgroup = false;
// 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
@@ -130,58 +90,18 @@ namespace MobileGL::MG_Config {
Bool AdvertiseFp64 = false;
// MOBILEGL_MAGMA_R11G11B10F_FALLBACK: use fallback format for R11G11B10F on Vulkan.
Bool MagmaR11G11B10FFallback = false;
// MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV: hand a shader module that fails SPIR-V validation to
// the driver anyway. Off by default: Magma declines such a program (no shader module, its
// draws refused by name), because a driver that accepts invalid SPIR-V may fault the GPU
// into device loss. For diagnosing a driver that copes with a specific module only.
Bool MagmaAllowInvalidSpirv = false;
// MOBILEGL_MAGMA_FRAMESINFLIGHT: requested Magma frames in flight, defaulting to 3.
Uint32 MagmaFramesInFlight = 3;
// MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER: GL draws and dispatches Magma records into
// 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
Bool AvoidSamplerMipmapMinFilter = false;
// MOBILEGL_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 AvoidExplicitLodBias = false;
// MOBILEGL_COHERENT_AS_FLUSH: app-compat for engines (e.g. Flywheel) that write
// GPU-read data through persistent GL_MAP_FLUSH_EXPLICIT_BIT maps they never
// flush. Persistent FLUSH_EXPLICIT map requests are rewritten to coherent
@@ -191,41 +111,10 @@ namespace MobileGL::MG_Config {
Bool CoherentAsFlush = false;
// MOBILEGL_TRACE_SKIP_AUTODESTROY: skip teardown in the ELF destructor (Init.cpp).
Bool TraceSkipAutodestroy = false;
// MOBILEGL_ESPRYT_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// MOBILEGL_DISABLE_UBO_RING: force the DirectGLES global-UBO upload back to the
// per-draw glBufferSubData path instead of the persistent-mapped ring allocator
// (negative control / driver-bug escape hatch).
Bool EsprytDisableUboRing = false;
// MOBILEGL_ESPRYT_DISABLE_UNPACK_RING: force DirectGLES texture uploads back to
// glTexSubImage from the client pointer instead of staging them through the
// persistent-mapped unpack-PBO ring (negative control / driver-bug escape
// hatch).
Bool EsprytDisableUnpackRing = false;
// MOBILEGL_ESPRYT_DISABLE_UPLOAD_RING: force DirectGLES app buffer updates
// (glBufferSubData / map flushes) back to the immediate driver upload instead
// of queueing them for the staged-copy flush through the persistent-mapped
// upload ring (negative control / driver-bug escape hatch; the immediate
// upload stalls on drivers that resolve the WAR hazard on the CPU, e.g. Mali).
Bool EsprytDisableUploadRing = false;
// MOBILEGL_ESPRYT_DISABLE_INVALIDATE_FLUSH: skip the glMapBufferRange(WRITE |
// INVALIDATE_RANGE) tier of the DirectGLES pending-range flush and go straight
// to the upload ring's staged glCopyBufferSubData (negative control / escape
// hatch for a driver whose range-invalidating map misbehaves). The map tier is
// what keeps a partial write into a large in-flight buffer priced by the RANGE:
// on Mali both the immediate glBufferSubData and a staged copy into a busy
// mutable store ghost the whole destination on the CPU.
Bool EsprytDisableInvalidateFlush = false;
// MOBILEGL_DISABLE_LARGE_BUFFER_ADOPTION: keep mesh-arena-sized buffer stores
// (>= 16MiB) on the CPU-shadow model instead of backing them with the backend's
// persistently+coherently mapped storage at definition time (negative control /
// escape hatch). Frontend-scoped: it engages only where the active backend
// provides AcquirePersistentMap. With adoption on, an app SubData into a busy
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
Bool DisableLargeBufferAdoption = false;
Bool DisableUboRing = 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,
@@ -246,10 +135,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.
@@ -281,471 +170,6 @@ namespace MobileGL::MG_Config {
// 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_RECOVER (client): 1 (default) = after a device loss ends this client's
// session, the application's next eglCreateContext / eglCreate*Surface / eglInitialize
// dials the server for a FRESH session, and contexts created from then on work. Contexts
// (and their objects) from before the loss stay lost, as EGL's power-management event
// has them; surfaces survive and are re-created on the new session when next used.
// 0 = the loss is final for the process (every verb DECLINED for the rest of its life).
Uint32 RecoverAfterLoss = 1;
// MOBILEGL_IPC_ADOPT_TIER, THE ZERO-COPY SWITCH (ruling ID-P11-14, CONTRACT-P11 B2):
// UNSET (kAdoptTierUnset, the default since 2026-09-29, the user's decision) = T0 where the
// session can run it, T2 QUIETLY where it cannot (MGLOG_D only, still counted);
// 0 = T0 asked by name - a T0 this session cannot use (a stream plane, the server's allow
// switch, no AHardwareBuffer, a failed server POST) is T2 plus one named line;
// 1 = T1, closed - refused by name, then T2; 2 = emulate (T2: the client keeps the shadow
// and pushes). Never a Fatal (MG_Remote/Transport/AdoptTier.h).
Uint32 AdoptTier = kAdoptTierUnset;
// MOBILEGL_IPC_ALLOW_ADOPT_T0 (server): 1 (default) = grant a client's T0 ask when this
// platform and the backend's POST self-test allow it; 0 = never grant it (the session
// runs T2 and says so). Read by the server role only; kept through ServerSpawn.
Uint32 AllowAdoptT0 = 1;
// MOBILEGL_IPC_VERB_BARRIER: 1 = the client blocks at every verb boundary until
// appliedSeq reaches its emitSeq (R-1). 0 is the negative control: it is EXPECTED to
// be red, because 31 of the 63 PipeInputs fields are still pulled from a live
// GLContext by the client's residual fill and a free-running queue lets the server
// read a FUTURE value of them.
Uint32 VerbBarrier = 1;
// MOBILEGL_IPC_RUN_AHEAD (P5e, MG_Remote/CONTRACT-P5E.md §1): 1 = after publishing an
// UNBARRIERED record the client returns immediately instead of waiting for its apply.
// It is one half of a conjunction and never a switch on its own - the client arms
// run-ahead only when the server also publishes kCapRunAheadApply, so on Magma, and on
// Espryt before the P5e integration commit, 1 means exactly what 0 means and logs once
// saying so.
//
// 0 IS THE A/B CONTROL AND NOT A NEGATIVE ONE: the server code, the fill decision and
// every record are identical on both arms, and the only difference is whether the
// client waits. That is what makes "is the picture the same" a question about the wait
// rule alone. MOBILEGL_IPC_VERB_BARRIER=0 keeps its own meaning and stays the
// lockstep arm's negative control; under run-ahead it is the one that must go red, at
// the first barriered row's stale pull.
//
// Forced to 0 by MOBILEGL_PIPE_VERIFY, beside BatchWaits: the comparator needs a
// client-filled gPipeInputs block for every verb, and run-ahead is precisely the
// arm that stops filling it.
Uint32 RunAhead = 1;
// MOBILEGL_IPC_PRESENT_CREDIT (P5e, ruling 4 / ID-92): how many presents the client may
// have in flight before it waits for a swap to come back. 1 = the client publishes
// frame N+1's records while the server applies and swaps frame N - one frame of
// overlap, at most one frame of added latency - and the CREDIT, never the ring's bytes,
// is what paces a run-ahead client. 2 is a device MEASUREMENT arm: it buys no CPU on a
// client that is already CPU-bound and costs a frame of latency, which is why the
// default is 1 and not "as deep as the ring".
Uint32 PresentCredit = 1;
// MOBILEGL_IPC_CONTROL_TIMEOUT_MS (CONTRACT-P6 §5.4 D5b): how long a spawn/tcp client
// waits for the SurfaceReply to one surface-control op (eglCreate*Surface, MakeCurrent,
// ...) once the server's backend is up. Its expiry is NOT fatal: the doorbell's death
// latch decides dead (device lost) from alive-but-silent (a named diagnostic). The
// contract named this knob from P6 on; the client hard-coded its default until P7.
Uint32 ControlTimeoutMs = 5000;
// MOBILEGL_IPC_COLD_START_MS (P7): the same wait for the three ops a server may bring its
// NATIVE backend up inside - CreatePbufferSurface, CreateWindowSurface, MakeCurrent -
// until the session's first MakeCurrent is answered ok. The bring-up is lazy (Espryt's
// eglInitialize, Magma's Vulkan instance and device), ~100 ms on a workstation and more
// than the steady 5 s on a loaded CI runner (retrace-split spawn legs, runs 35671704873
// and 35706183230). Never shorter than CONTROL_TIMEOUT_MS; its expiry is the same
// non-fatal named answer. The default is the spawn connect budget, for the same reason:
// "the server has to create a backend, and a cold software rasteriser is not fast".
Uint32 ColdStartMs = 20000;
// MOBILEGL_IPC_STRICT_ERRORS: promote a BARRIER-PULLED field read - and, in a split
// build, the seven sticky forwards that are otherwise exempt - from "count it in
// rsp" to Fatal (R-7.3).
Bool StrictErrors = false;
// MOBILEGL_IPC_ROLE_SPLIT_STATE (P5f f1, P5F-WIRE-COMPLETENESS.md §4): the dual-block
// rehearsal. 1 = the client's residual fill writes a CLIENT-ROLE PipeInputs block and
// the backend/applier keep reading the SERVER-ROLE one, so every path that today works
// only because the two roles share one object turns into a named
// Fatal{UnmigratedPipeInput, "<field>@<verb>"} instead of a silent cross-role read.
// Meaningless under monolith transport (the two roles are one thread there, so the
// selection folds to the single shared block) and forced off by MOBILEGL_PIPE_VERIFY
// (the comparator owns the one fill block it compares against). 0 is not merely the
// default, it is the negative control: the lane's distinctness case must go red
// without it (P5F §6).
Bool RoleSplitState = false;
// MOBILEGL_IPC_AUDIT: after a record retires, the server fills the SEG_STAGE bytes
// it referenced with 0xDD (R-2.5). This is the ONLY mechanical control that an
// inproc implementation did not quietly keep using a pointer past its lifetime.
Bool Audit = false;
// MOBILEGL_IPC_SERVER_AFFINITY: `auto` (the default, big-core detection borrowed
// from ShaderCompilePool), `off`, or an explicit CPU mask. Kept as the raw string
// because the resolved mask is logged by whoever starts the apply thread, and the
// string is what an operator typed.
String ServerAffinity = "auto";
// MOBILEGL_IPC_SURFACE (P12, on-screen server window) = offscreen | server. `server` makes
// the client a HEADLESS one: eglCreateWindowSurface / eglCreatePlatformWindowSurface accept
// any native window including NULL, send ONE CreateWindowSurface naming
// WindowKind::ServerOwned (token 0, the size from EGL_WIDTH/EGL_HEIGHT, 0/0 = the server's
// own) and no SetWindowHandle, and take the surface's real geometry back from the server.
// Meaningful only when this client talks to a remote server (MOBILEGL_TRANSPORT=spawn, the
// fork or tcp:// control); under monolith / inproc it is parsed, logged as ignored, and
// changes nothing. `offscreen` (the default) is today's behaviour byte for byte.
// Pbuffers stay pbuffers in both modes (D4: one mode per session, decided by the first).
IpcSurface Surface = IpcSurface::Offscreen;
};
extern IpcTable Ipc;
// P12: the one predicate every client-side consumer of MOBILEGL_IPC_SURFACE asks - a window
// surface is the server's when the knob says so AND there is a remote server to own it.
inline Bool ServerOwnedWindowSurfaces() {
return Transport == TransportMode::Spawn && Ipc.Surface == IpcSurface::Server;
}
#else
// The whole point: in a build without MG_Remote this folds at compile time, so
// `if (MG_Config::Transport != MG_Config::TransportMode::Monolith)` in Init.cpp is a
// discarded statement and the pull build gains no symbol, no branch and no byte.
inline constexpr TransportMode Transport = TransportMode::Monolith;
#endif
} // namespace MobileGL::MG_Config
+8 -499
View File
@@ -6,22 +6,10 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
// constants beside it are the phase-by-phase controls, not the default). Push-only, so the
// pull build's translation unit is unchanged.
#include <MG_Pipe/MGPipe.h>
#endif
#include <cerrno>
#include <cstdio>
#include <cstdlib>
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#ifndef _WIN32
extern char** environ;
@@ -31,16 +19,6 @@ namespace MobileGL::MG_Config {
// Zero/default-initialized at static-init time (all fields have constexpr-friendly
// defaults), so it is safe to read even if MG_ConfigLoader::Init has not run yet.
FeaturesTable Features;
#if MOBILEGL_BUILD_DISAGGREGATED
// Same contract, and for the same reason: MG_Backend::Init() reads Transport, and a
// build order that put it before MG_ConfigLoader::Init() must see Monolith rather than
// a torn enum. Defined only here - in a pull build Config.h makes Transport a constexpr
// and there is nothing to define.
TransportMode Transport = TransportMode::Monolith;
String TransportEndpoint;
Bool SplitTransportRequestedByConfig = false;
IpcTable Ipc;
#endif
} // namespace MobileGL::MG_Config
namespace MobileGL::MG_ConfigLoader {
@@ -50,115 +28,6 @@ namespace MobileGL::MG_ConfigLoader {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
}
} // namespace
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
@@ -189,15 +58,6 @@ namespace MobileGL::MG_ConfigLoader {
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
@@ -299,140 +159,41 @@ 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.DisableSubgroup = QueryEnvFlag("MOBILEGL_DISABLE_SUBGROUP");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
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.AvoidExplicitLodBias = QueryEnvFlag("MOBILEGL_AVOID_EXPLICIT_LOD_BIAS");
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.DisableUboRing = QueryEnvFlag("MOBILEGL_DISABLE_UBO_RING");
features.EsprytForceDepthStencilReadbackEmulation =
QueryEnvFlag("MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION");
features.RelaxedSemantics = QueryEnvFlag("MOBILEGL_RELAXED_SEMANTICS");
features.MagmaDisableBlendedDepthWriteQuirk =
QueryEnvQuirkOverride("MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE");
features.MagmaDisableRobustBufferAccess = QueryEnvFlag("MOBILEGL_MAGMA_DISABLE_ROBUST_BUFFER_ACCESS");
features.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; \
@@ -441,270 +202,18 @@ namespace MobileGL::MG_ConfigLoader {
}
ENTRY(DirectGLES)
ENTRY(DirectVulkan)
ENTRY(DiligentVulkan)
ENTRY(Unknown)
MG_Config::ActiveBackendType = BackendType::Unknown;
#undef ENTRY
}
#if MOBILEGL_BUILD_DISAGGREGATED
// MOBILEGL_TRANSPORT = monolith | inproc | spawn | unix:<path> | pipe:<name>
// (ARCHITECTURE.md:583). Shaped after InitBackendType above: an exact-name table, then
// one fallback that names what it did instead. The two prefixed forms are the only
// reason this is not literally that function's ENTRY macro.
//
// spawn / unix: / pipe: PARSE AND THEN REFUSE. They are P6's, and the refusal is NAMED
// rather than silent, because the failure this avoids is a P6 lane that set
// MOBILEGL_TRANSPORT=spawn, fell back to monolith, and went green on the wrong arm.
// The mode is left at Monolith so nothing half-initializes.
inline void InitTransport() {
String value;
QueryEnvVariable("MOBILEGL_TRANSPORT", value, "monolith");
String lowered = value;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
MG_Config::TransportEndpoint.clear();
if (lowered.empty() || lowered == "monolith") {
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
if (lowered == "inproc") {
MG_Config::Transport = MG_Config::TransportMode::InProcess;
// P7 F1: see Config.h. Set beside the mode, on the two arms that actually resolve
// one, and NOT on the two that name a transport and then stay monolith - a process
// that was refused its transport brings no client half up either.
MG_Config::SplitTransportRequestedByConfig = true;
MGLOG_I("Config: MOBILEGL_TRANSPORT=inproc - the MGPipe record stream crosses a real "
"ring to an apply thread");
return;
}
// The three P6 forms. Recognised precisely, so the diagnostic can say "not yet"
// rather than "unknown", which are different bugs on the operator's side.
// P6 `sm`/`cp`: spawn is IMPLEMENTED. It launches a server process on a
// rendezvous of its own and connects to it - the two processes are
// independent, so this is `Dial = Connect` with a launcher attached
// rather than the fork-coupled shape ARCHITECTURE.md:488 described.
if (lowered == "spawn") {
MG_Config::Transport = MG_Config::TransportMode::Spawn;
MG_Config::SplitTransportRequestedByConfig = true; // P7 F1, as above.
// THE SAME SHAPE AS THE inproc LINE ABOVE, AND FOR THE SAME REASON.
// run_trace_case.cmake asserts on a distinctive sentence from this
// function rather than on `MOBILEGL_TRANSPORT=spawn` alone, because
// ConfigLoader logs an `Accepted env variable: KEY=VALUE` line for
// every MOBILEGL_* in EVERY build including the pull one - so the
// bare KEY=VALUE is satisfied by a monolith library that parsed
// nothing (review M-5). A spawn arm with no line of its own could
// only have been asserted with the inproc marker, which spawn never
// emits: the arm would have been red for a missing sentence rather
// than green for a working transport.
MGLOG_I("Config: MOBILEGL_TRANSPORT=spawn - the MGPipe record stream crosses a real "
"ring to an apply thread in ANOTHER PROCESS");
return;
}
if (lowered.compare(0, 5, "unix:") == 0 || lowered.compare(0, 5, "pipe:") == 0) {
// unix: and pipe: remain P6+ - connecting to an endpoint SOMEBODY
// ELSE is listening on needs the server lifecycle to be somebody
// else's too, which is P12's. Named, never a silent degrade.
MGLOG_E("Config: MOBILEGL_TRANSPORT='%s' names a transport P6 does not implement yet; "
"staying on monolith. This run is NOT a split run.",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
return;
}
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_TRANSPORT='%s'; expected "
"monolith|inproc|spawn|unix:<path>|pipe:<name>, using monolith",
value.c_str());
MG_Config::Transport = MG_Config::TransportMode::Monolith;
}
// The MOBILEGL_IPC_* family (Config.h IpcTable). Parsed unconditionally rather than only
// when Transport != Monolith: a knob that silently means nothing on one arm of an A/B is
// how an A/B stops being one, and the ranges below are the diagnostics.
inline void InitIpc() {
auto& ipc = MG_Config::Ipc;
QueryEnvVariable("MOBILEGL_IPC_SERVER_PATH", ipc.ServerPath, "");
QueryEnvVariable("MOBILEGL_IPC_CONTROL", ipc.Control, "fork");
QueryEnvVariable("MOBILEGL_IPC_DATA", ipc.Data, "auto");
// Both ring floors are 1 MiB, not 0: a ring caps ONE record at half its size, and
// the catalogue's largest fixed payload (MGPFramebufferState, 304 bytes) plus a
// create_shader_state archive already needs far more than a toy ring. The ceilings
// are sanity, not policy.
ipc.RingMb = QueryEnvUint32("MOBILEGL_IPC_RING_MB", 8, 1, 1024);
ipc.StageMb = QueryEnvUint32("MOBILEGL_IPC_STAGE_MB", 32, 1, 4096);
// P7 wave 4 M2 (Config.h has the semantics). 0 is admitted ON PURPOSE as the negative
// control: no forced sync, and MagmaWireReclaimScenario's watermark case must go red.
ipc.WireDeferredMb = QueryEnvUint32("MOBILEGL_IPC_WIRE_DEFERRED_MB", 64, 0, 65536);
ipc.SpinUs = QueryEnvUint32("MOBILEGL_IPC_SPIN_US", 50, 0, 1000000);
// PH-6 (ID-P7-2; Config.h has the semantics). 0 is NOT admitted: a server with no
// patience at all would forfeit a healthy run-ahead client the first time its ring
// filled between two of the client's drains, which is an ordinary backlog.
ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000);
// 0 is admitted ON PURPOSE and is the negative control of exit gate E3(a): it turns
// the persistent-map push OFF, and PersistentCoherentMapScenario must go red.
ipc.PersistentBlockKb = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_BLOCK_KB", 64, 0, 65536);
// Whole-range push is the 0 arm; with it on (default) only blocks whose
// xxHash64 changed since the last push are shipped.
ipc.PersistentHashSuppress = QueryEnvUint32("MOBILEGL_IPC_PERSISTENT_HASH_SUPPRESS", 1, 0, 1);
ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1);
// The create window. DEFAULT 6, and it works because a link that declares RetainsReplies
// keeps a declared answer until its reader takes it - Config.h carries the three reasons it
// was inert before, the device numbers, and the sweep that put the default at 6 rather
// than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing).
// Capped at 8: the retained set is bounded by the window's own depth, the store holds 24
// declared answers, and the device sweep showed the wall clock still falling at the old
// cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and
// StreamLink's crossing log are the checks on this number.
ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12);
// P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both).
ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1);
// P10: fence polls answered locally; 0 is the A/B arm (Config.h).
ipc.PollEscalate = QueryEnvUint32("MOBILEGL_IPC_POLL_ESCALATE", 64, 0, 1000000);
// A lost session is replaced by a fresh one on the application's next create (Config.h).
ipc.RecoverAfterLoss = QueryEnvUint32("MOBILEGL_IPC_RECOVER", 1, 0, 1);
// The verify harness compares the pushed block against the applier per verb; a
// batched queue lets the comparer read a supplied field mid-apply, which is a
// torn read rather than a divergence. The batch is therefore off whenever the
// shadow comparer is armed.
if (MG_Config::Features.PipeVerify) ipc.BatchWaits = 0;
// UNSET is T0 with quiet fallbacks (the default since 2026-09-29); 0 asks for T0 by name, 1 is
// refused by name, 2 is T2 - settled at the handshake from the data plane (P11 A1/B2,
// MG_Remote/Transport/AdoptTier.h). An unparseable value is warned about and reads as unset.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", MG_Config::kAdoptTierUnset, 0, 2);
// P11 B2: the server's own T0 allow switch (Config.h).
ipc.AllowAdoptT0 = QueryEnvUint32("MOBILEGL_IPC_ALLOW_ADOPT_T0", 1, 0, 1);
ipc.VerbBarrier = QueryEnvUint32("MOBILEGL_IPC_VERB_BARRIER", 1, 0, 1);
// P5e (MG_Remote/CONTRACT-P5E.md §1). The wait rule's A/B, parsed here like every
// other IPC knob and armed only where the server publishes kCapRunAheadApply.
ipc.RunAhead = QueryEnvUint32("MOBILEGL_IPC_RUN_AHEAD", 1, 0, 1);
// ... and forced OFF by the verify harness for BatchWaits' reason, one step further:
// run-ahead's whole point is that the client stops filling gPipeInputs for an
// unbarriered record, and the comparator has nothing left to compare when it does.
if (MG_Config::Features.PipeVerify) ipc.RunAhead = 0;
// The present credit (ruling 4). 1 is one frame of overlap; 8 is the ceiling because a
// deeper queue buys nothing on a CPU-bound client and pays for it in latency. 0 is NOT
// admitted: a credit of zero would mean "publish no present at all".
ipc.PresentCredit = QueryEnvUint32("MOBILEGL_IPC_PRESENT_CREDIT", 1, 1, 8);
// CONTRACT-P6 D5b's reply bound and P7's cold-start budget (Config.h has the semantics).
// Both are floored at 100 ms: a zero bound would declare every live server silent.
ipc.ControlTimeoutMs = QueryEnvUint32("MOBILEGL_IPC_CONTROL_TIMEOUT_MS", 5000, 100, 600000);
ipc.ColdStartMs = QueryEnvUint32("MOBILEGL_IPC_COLD_START_MS", 20000, 100, 600000);
ipc.StrictErrors = QueryEnvFlag("MOBILEGL_IPC_STRICT_ERRORS");
ipc.Audit = QueryEnvFlag("MOBILEGL_IPC_AUDIT");
QueryEnvVariable("MOBILEGL_IPC_SERVER_AFFINITY", ipc.ServerAffinity, "auto");
// P5f f1: the dual-block rehearsal (P5F-WIRE-COMPLETENESS.md §4). Forced OFF by the
// verify harness: the comparator's entry compare and compare-at-read hook are built on
// there being ONE filled block (the hook pins itself to &gPipeInputs,
// PipeFill.cpp's MGPipeVerifyReadHook), which is exactly what the rehearsal splits.
ipc.RoleSplitState = QueryEnvFlag("MOBILEGL_IPC_ROLE_SPLIT_STATE");
if (MG_Config::Features.PipeVerify && ipc.RoleSplitState) {
MGLOG_W("Config: MOBILEGL_IPC_ROLE_SPLIT_STATE=1 is incompatible with "
"MOBILEGL_PIPE_VERIFY (the comparator owns the single fill block); "
"the dual-block rehearsal is OFF for this run");
ipc.RoleSplitState = false;
}
// P6 `dl` (CONTRACT-P6 5.3): MOBILEGL_IPC_RESPAWN IS A NAMED REFUSAL, NOT A NO-OP.
//
// The device-lost latch is deliberately one-way - a session whose server died has
// nothing to recover into, because every handle the client minted names an object in a
// process that no longer exists. Re-pushing the world onto a fresh server is the work
// this knob would turn on, and no stage has written it.
//
// Refused BY NAME rather than parsed and ignored, for the reason Config.h gives about
// the whole IPC family: an environment variable nothing consumes is indistinguishable
// from one that is consumed and does nothing, and an operator who set this one would
// otherwise conclude that recovery had been tried and had not helped.
{
String respawn;
QueryEnvVariable("MOBILEGL_IPC_RESPAWN", respawn, "");
if (!respawn.empty() && respawn != "0") {
MGLOG_E("Config: MOBILEGL_IPC_RESPAWN='%s' names a recovery NO STAGE HAS "
"IMPLEMENTED. The device-lost latch is one-way on purpose: every handle "
"this client minted names an object inside the server process, so a new "
"server would have to be re-pushed the entire world before a single verb "
"could land. This run will latch device-lost and stay there.",
respawn.c_str());
}
}
// P12 (on-screen server window): MOBILEGL_IPC_SURFACE = offscreen | server (Config.h has
// the semantics). An unknown value is named and read as the default, like every knob here.
{
String surface;
QueryEnvVariable("MOBILEGL_IPC_SURFACE", surface, "offscreen");
std::transform(surface.begin(), surface.end(), surface.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (surface.empty() || surface == "offscreen") {
ipc.Surface = MG_Config::IpcSurface::Offscreen;
} else if (surface == "server") {
ipc.Surface = MG_Config::IpcSurface::Server;
} else {
MGLOG_W("Config: Ignoring invalid env variable MOBILEGL_IPC_SURFACE='%s'; expected "
"offscreen|server, using offscreen",
surface.c_str());
ipc.Surface = MG_Config::IpcSurface::Offscreen;
}
// (Not said by a server process: a spawned server inherits the client's environment,
// parses this before RunSession sets its own transport, and has no client half anyway.)
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
const Bool serverProcess = role != nullptr && std::strcmp(role, "server") == 0;
if (ipc.Surface == MG_Config::IpcSurface::Server && !serverProcess &&
MG_Config::Transport != MG_Config::TransportMode::Spawn) {
// Said, not silently honoured: there is no remote server here to own a window.
MGLOG_W("Config: MOBILEGL_IPC_SURFACE=server is IGNORED - it asks a remote server "
"(MOBILEGL_TRANSPORT=spawn, a fork or tcp:// control) to own the window surface, "
"and this run has none. Window surfaces stay the client's own");
}
}
if (MG_Config::Transport == MG_Config::TransportMode::Monolith) return;
// One line, on the arm where these numbers decide behaviour, because every one of
// them is a number a bug report has to quote.
// CREATE-WINDOW IS IN THIS LINE BECAUSE A RUN HAS TO SAY WHICH WINDOW IT USED. It was
// missing while the value was a number nobody varied, and the moment it became one that
// changes the wall clock by seconds (the P12 sweep: 28.3 s at 1 down to 10.8 s at 6) a
// log that does not name it makes every run ambiguous about its own configuration.
MGLOG_I("Config: IPC ring=%uMiB stage=%uMiB wire-deferred=%uMiB spin=%uus event-wait=%ums "
"persistent-block=%uKiB create-window=%u pbo-readback-sync=%u poll-escalate=%u "
"adopt-tier=%s allow-t0=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%ums "
"cold-start=%ums strict=%d audit=%d role-split-state=%d affinity='%s' surface=%s",
ipc.RingMb, ipc.StageMb, ipc.WireDeferredMb, ipc.SpinUs, ipc.EventWaitMs,
ipc.PersistentBlockKb, ipc.CreateWindow, ipc.PboReadbackSync, ipc.PollEscalate,
ipc.AdoptTier == 0 ? "0" : ipc.AdoptTier == 1 ? "1" : ipc.AdoptTier == 2 ? "2" : "unset(T0)",
ipc.AllowAdoptT0,
ipc.VerbBarrier, ipc.RunAhead, ipc.PresentCredit, ipc.ControlTimeoutMs, ipc.ColdStartMs,
static_cast<int>(ipc.StrictErrors), static_cast<int>(ipc.Audit),
static_cast<int>(ipc.RoleSplitState), ipc.ServerAffinity.c_str(),
MG_Config::ServerOwnedWindowSurfaces() ? "server" : "offscreen");
if (ipc.VerbBarrier == 0) {
MGLOG_W("Config: MOBILEGL_IPC_VERB_BARRIER=0 is the R-1 NEGATIVE CONTROL and is "
"expected to fail: the client still pulls 31 of 63 PipeInputs fields from a "
"live GLContext, so an unbarriered queue lets the server read future values");
}
if (ipc.WireDeferredMb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_WIRE_DEFERRED_MB=0 is the M2 NEGATIVE CONTROL: the "
"server never forces a sync for orphaned wire buffer stores, so a long frame "
"that respecifies and draws holds every one of them until the frame ends");
}
if (ipc.PersistentBlockKb == 0) {
MGLOG_W("Config: MOBILEGL_IPC_PERSISTENT_BLOCK_KB=0 is the E3(a) NEGATIVE CONTROL: "
"the persistent-map push is OFF and a coherent-map scenario must go red");
}
}
#endif
void Init() {
MGLOG_D("Loading configuration from environment variables...");
InitializeAcceptedEnvVariables();
InitBackendType();
InitFeatures();
#if MOBILEGL_BUILD_DISAGGREGATED
// After InitFeatures, so the one line InitIpc logs is the last word on this run's
// configuration, and before the accepted-env map is destroyed just below.
InitTransport();
InitIpc();
#endif
// Destroy the map since we won't need it anymore
acceptedEnvVariablesMap.reset();
-5
View File
@@ -34,11 +34,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
+1 -114
View File
@@ -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,25 +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 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
-16
View File
@@ -26,29 +26,13 @@ namespace MobileGL {
namespace MG_ConfigLoader {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // namespace MG_ConfigLoader
// Whether this process can be served right now. False when MobileGL is configured to reach a
// server that is not there (or a split session already failed to come up), or when the process
// asked for software rendering (LIBGL_ALWAYS_SOFTWARE, desktop Linux), so a
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
Bool ImplementationAvailable();
namespace MG_Backend {
void Init();
} // namespace MG_Backend
namespace MG_Impl {
void Init();
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
// Init) and installed as the one every later context starts with. Init's step, and the
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
void BuildDefaultFramebuffer();
} // namespace MG_Impl
} // namespace MobileGL
+11 -57
View File
@@ -97,8 +97,6 @@ namespace MobileGL::MG_Backend {
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
// An external texture is a 2D one wherever a format can go.
if (target == TextureTarget::External) return static_cast<SizeT>(TextureTarget::Texture2D);
return static_cast<SizeT>(target);
}
@@ -174,15 +172,11 @@ namespace MobileGL::MG_Backend {
return false;
}
// A DISPLAY IS A CONNECTION, NOT A RESOURCE THIS BACKEND OWNS, and refusing the second one
// refused a caller for a reason that is not there: the surface and context tables are keyed
// by the display each was created on, every operation carries its own display handle, and
// this field has no reader anywhere - it was the whole of the rule.
//
// What the rule cost was a compositor. One asks for the display it will draw into AFTER
// probing another, so the second eglInitialize is the one that matters, and a refusal there
// is reported one layer up as "no usable DRM render device" - the probe having succeeded
// and the real display having failed, with nothing in between that says which was refused.
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
return false;
}
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -202,15 +196,12 @@ namespace MobileGL::MG_Backend {
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
@@ -288,7 +279,6 @@ namespace MobileGL::MG_Backend {
return true;
}
m_activatingSurface = surface;
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
@@ -308,14 +298,8 @@ namespace MobileGL::MG_Backend {
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
// own (a multi-window client renders each window on its own thread), and wiping them made
// that thread's next eglSwapBuffers fail with "no current context attached".
auto activatedIt = m_eglSurfaces.find(surface);
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
activatedIt->second.Activated = true;
m_backendCapabilitiesInitialized = false;
}
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
return true;
}
@@ -382,24 +366,6 @@ namespace MobileGL::MG_Backend {
m_eglCurrentThreads.clear();
}
void BackendObject::ForgetActiveEGLSurface() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglSurfaceInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
}
namespace {
MG_Util::Damage::Region& PresentDamageSlot() {
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
return damage;
}
} // namespace
const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); }
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; }
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
@@ -416,16 +382,10 @@ namespace MobileGL::MG_Backend {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
// The calling thread's surface, which another thread's make-current may have moved the
// backend off: bring it back before presenting it.
if ((!m_eglSurfaceInitialized || draw != m_eglSurface) && !ActivateEGLSurface(draw)) {
MGLOG_E("SwapEGLBuffers failed: the draw surface could not be re-activated");
return false;
}
const auto& backendFunctions = GetBackendFunctions();
if (!backendFunctions.Present) {
@@ -466,9 +426,8 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ForgetActiveEGLSurface();
ResetEGLRuntimeState();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
@@ -499,9 +458,8 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ForgetActiveEGLSurface();
ResetEGLRuntimeState();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLResources() {
@@ -534,8 +492,4 @@ namespace MobileGL::MG_Backend {
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend
+7 -419
View File
@@ -9,54 +9,22 @@
#pragma once
#include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_Util/Damage/Damage.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeHandles.h>
#endif
namespace MobileGL {
namespace MG_State::GLState {
class FramebufferObject;
class ITextureObject;
class RenderbufferObject;
}
enum class BackendType {
DirectGLES,
DirectVulkan,
DiligentVulkan,
BackendTypeCount,
Unknown = -1
};
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 +161,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 +179,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 +237,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 +279,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 +319,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 +335,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 +384,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,256 +416,26 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
struct SharedImageView {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
void* NativeBuffer = nullptr;
};
// The client-side answer of an allocation: the image's id and its exported descriptor
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
struct SharedImageExport {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint64 Modifier = 0;
// A YUV image's Cb Cr plane: in the same descriptor, at this offset and pitch.
Uint32 Plane1Stride = 0;
Uint32 Plane1Offset = 0;
};
// An import of a two-plane (YUV) dma-buf: where its planes are in the one descriptor, and
// EGL_EXT_image_dma_buf_import's colour hints (0 = not given).
struct SharedImageImportLayout {
Uint32 PlaneCount = 1;
Uint32 Offset[2] = {0, 0};
Uint32 Pitch[2] = {0, 0};
Uint32 ColorSpace = 0;
Uint32 Range = 0;
Uint32 SitingX = 0;
Uint32 SitingY = 0;
// The DRM format modifier the import named; HasModifier false = none (the implicit one).
Bool HasModifier = false;
Uint64 Modifier = 0;
};
// THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it
// around the backend's Present (from the present record), and a backend that can hand damage
// to its native present reads it there. Full whenever nothing set it.
const MG_Util::Damage::Region& CurrentPresentDamage();
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage);
class BackendObject {
public:
virtual ~BackendObject() = default;
// ---- shared images, SERVER side (a backend implements these) ---------------------
//
// Copies the default framebuffer of the surface current on this (the apply) thread into
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
// differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy
// waits for the image's pending read fences before it writes, and its own completion
// is published with SharedImages::PublishWrite as a sync_file - the call returns once
// the copy is submitted, never waiting for it on the CPU (a backend with no fence export
// waits and publishes -1). A reader binding the image waits for that fence on its GPU
// when the write generation moved, and at its frame boundary publishes its frame's fence
// with PublishRead / ReadTracker. The application's GL state is untouched. False = not
// supported / failed (logged).
//
// `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy;
// Full copies all of it. The rest of the image is left as it is - the client sends a
// region only for an image that already holds the frame outside it - and a backend that
// cannot be sure the image's contents survived (a target it has never written) copies
// all of it regardless.
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
(void)image, (void)region;
return false;
}
// EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply)
// thread: how many presents ago the buffer the next frame draws into held the frame then
// presented; 0 = unknown (the application repaints everything). Asking says the client
// relies on the answer, so from then on the backend keeps what it presents.
//
// `damageRegionFollows`: the client declares the region it draws (SetCurrentDamageRegion)
// before drawing. A driver whose only buffer age is EGL_KHR_partial_update's keeps a
// buffer's content only outside that region, so without one coming it is not asked.
virtual Int32 QueryCurrentBufferAge(Bool damageRegionFollows) {
(void)damageRegionFollows;
return 0;
}
// eglSetDamageRegionKHR for that draw surface: the frame draws `region` (GL window
// coordinates; Full = all of it) and nothing outside it. A backend without the notion
// ignores it.
virtual Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return true;
}
// IMPLICIT SYNC BY FLUSH (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the session
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
// its last boundary is published as written and read by one fence of everything it
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
// use of an image in a frame waits for that image's pending reads (it may be about to
// overwrite what another session still samples). A backend without shared images has
// nothing to publish.
virtual Bool PublishSharedImageAccesses() { return true; }
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
// (the caller's to close) that signals once everything the session submitted so far has
// completed - and, for a session that writes images, what PublishSharedImageAccesses
// does. -1 when there is none to give: the backend cannot export one, or the work has
// already completed - in both cases it HAS completed when this returns.
virtual Bool ExportNativeFence(int* fence) {
*fence = -1;
return PublishSharedImageAccesses();
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
// A new image, allocated by the server; `out->Fd` is the caller's to close.
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
(void)width, (void)height, (void)fourcc, (void)out;
return false;
}
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
// server's images, or the size/format disagree.
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
return false;
}
// The same for a YUV fourcc, whose planes and hints `layout` gives. A YUV buffer the
// server did not export is taken too (its labelled CPU-copy fallback).
virtual Bool ImportSharedImagePlanes(int fd, Uint32 width, Uint32 height, Uint32 fourcc,
const SharedImageImportLayout& layout, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)layout, (void)outId;
return false;
}
// Drops this client's reference (an allocation's or an import's).
virtual Bool ReleaseSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the current context's default framebuffer, copied top row first into
// the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for
// all. Returns when it completed.
virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
(void)id, (void)region;
return false;
}
// In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge
// on the server). False: the backend has no answer (the caller reports 0).
virtual Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) {
(void)damageRegionFollows, (void)age;
return false;
}
// In the GL stream: eglSetDamageRegionKHR for the current draw surface
// (SetCurrentDamageRegion on the server).
virtual Bool SetDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return false;
}
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
(void)textureLifetimeId, (void)id;
return false;
}
// In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server,
// returning once the fence is published. Skipped (true) when nothing reached the stream
// since the last one.
virtual Bool FlushSharedImageAccesses() { return false; }
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
// caller's to close), or -1 when it had none - the fenced work has then completed.
virtual Bool CreateNativeFence(int* fence) {
*fence = -1;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0;
// A served session's apply thread - the one thread that runs this backend from here on -
// calls this first, in a process that may serve several sessions at once. A backend whose
// native state is process-global by construction gives this object its own copy and binds
// it to the calling thread. The default has nothing per process to separate.
virtual void BindSessionStateToThisThread() {}
// A served session's apply thread asks this each time it wakes, BEFORE it applies another
// record: true when this session's own GPU work faulted or hung the GPU and the backend has
// latched the session for it (MGPipeSessionLatch, family DeviceLost), so the next frame -
// which would very likely stop the GPU again - is never applied. Cheap by contract: no
// wait, at most a status read. The default knows of no fault.
virtual Bool LatchIfGpuFaulted() { return false; }
// A served session's ring moved to another of its client's contexts (bind_context). A
// backend whose surface follows the native context it binds needs nothing; one that keeps
// a single native surface binding re-binds the surface that context was last made current
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
virtual void OnClientContextBound(Uint64 token) { (void)token; }
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
virtual Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx);
virtual Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw);
// eglSwapBuffersWithDamage: `damage` (GL window coordinates) is a hint for the native
// present. A backend with nothing to hand it to swaps as SwapEGLBuffers does.
virtual Bool SwapEGLBuffersWithDamage(EGLDisplay dpy, EGLSurface draw, const MG_Util::Damage::Region& damage) {
(void)damage;
return SwapEGLBuffers(dpy, draw);
}
// Forwards the app-requested eglSwapInterval to the backend's native
// presentation path (no-op for backends without a SetSwapInterval hook).
virtual void SetEGLSwapInterval(Int interval);
virtual void ReleaseEGLSurface(EGLSurface surface);
virtual void ReleaseEGLResources();
// ---- the server's own display window going away and coming back -------------------
//
// `window` (a window surface's native window) is about to be destroyed - the display's
// screen went off. Release ONLY what is bound to it: every window surface on it keeps its
// client handle and draws into a placeholder from here on, and every context, object and
// device stays. Called on the thread that runs this backend; nothing may reference the
// window once it returns true. False: not supported (the caller ends the session instead).
virtual Bool SuspendServerWindow(void* window) {
(void)window;
return false;
}
// The surfaces SuspendServerWindow parked are rebuilt on `window` (a new native window,
// `width`x`height`), and their new extent is published as a creation publishes it. False:
// they could not be (the caller ends the session).
virtual Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// `window` changed size to `width`x`height` (the display's report - the one size to publish).
// Its surfaces publish that extent to the client now, without waiting for a present; a
// swapchain that has to follow is rebuilt when no frame is half-recorded, else at the
// present. False: nothing was published (no surface on it, or not supported).
virtual Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) {
(void)window, (void)width, (void)height;
return false;
}
// CLIENT side, from eglQuerySurface(EGL_WIDTH/EGL_HEIGHT): a backend whose surface extents
// change under the application (a remote server's window) applies what it has received.
virtual void RefreshSurfaceExtent(EGLSurface surface) { (void)surface; }
void SetWindowHandle(const WindowHandle& handle);
virtual const RendererInfo& GetRendererInfo() const = 0;
@@ -860,28 +462,18 @@ namespace MobileGL {
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
// Activated at least once: a re-activation (a multi-window client switching back)
// is a re-bind and does not re-run the backend's capability probe.
Bool Activated = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
// any more, but every OTHER thread's current context and surface stay as they were (a
// multi-window client closing one popup must not unbind its other windows' threads).
void ForgetActiveEGLSurface();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
// Every registered surface the base class drops, current or not, after it is gone from
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
@@ -889,10 +481,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;
-41
View File
@@ -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
@@ -0,0 +1,906 @@
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.cpp
// Copyright (c) 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
#include "BackendObject_Diligent.h"
#include "DiligentVulkan.h"
#include "Renderer/DiligentRenderer.h"
#include <MG_Backend/BackendObject.h>
#include <MG_Backend/BackendObjects.h>
#include <MG_State/GLState/Core.h>
#include <EngineFactoryVk.h>
#include <RenderDevice.h>
#include <DeviceContext.h>
#include <exception>
#include <chrono>
namespace MobileGL::MG_Backend::DiligentBackend {
namespace {
const RendererInfo BuildInitialRendererInfo() {
RendererInfo info;
info.RendererName = "MobileGL (Diligent/Vulkan)";
info.BackendName = "Diligent Vulkan";
info.RendererGLInfo.TargetGLVersion = {3, 2, 0};
info.RendererGLInfo.TargetGLSLVersion = {1, 50, 0};
info.RendererGLInfo.IsCompatibilityProfile = false;
return info;
}
DiligentRenderer* GetActiveRenderer() {
auto* backend = dynamic_cast<BackendObject_Diligent*>(pActiveBackendObject.get());
return backend != nullptr ? backend->GetRenderer() : nullptr;
}
struct DrawArraysIndirectCommand {
Uint32 Count = 0;
Uint32 InstanceCount = 0;
Uint32 First = 0;
Uint32 BaseInstance = 0;
};
struct DrawElementsIndirectCommand {
Uint32 Count = 0;
Uint32 InstanceCount = 0;
Uint32 FirstIndex = 0;
Int32 BaseVertex = 0;
Uint32 BaseInstance = 0;
};
struct CpuTimerQuery {
std::chrono::steady_clock::time_point Start;
Uint64 TimestampNs = 0;
Bool Available = false;
};
const Uint8* ResolveIndirectCommandBytes(const void* indirect, SizeT requiredBytes, const char* label) {
auto drawBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::DrawIndirect).GetBoundObject();
if (drawBuffer) {
drawBuffer->SyncPersistentMappedRange();
const SizeT commandOffset = reinterpret_cast<SizeT>(indirect);
if (drawBuffer->MappedData() == nullptr || commandOffset + requiredBytes > drawBuffer->GetSize()) {
MGLOG_E_ONCE("%s skipped: invalid GL_DRAW_INDIRECT_BUFFER binding or range", label);
return nullptr;
}
return drawBuffer->MappedData() + commandOffset;
}
if (indirect == nullptr) {
MGLOG_E_ONCE("%s skipped: indirect pointer is null", label);
return nullptr;
}
return reinterpret_cast<const Uint8*>(indirect);
}
void Clear(GLbitfield mask) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
return;
}
if ((mask & GL_COLOR_BUFFER_BIT) != 0) {
const auto& color = MG_State::pGLContext->GetClearColor();
renderer->Clear(color.x(), color.y(), color.z(), color.w());
}
if ((mask & GL_DEPTH_BUFFER_BIT) != 0) {
renderer->ClearDepth(MG_State::pGLContext->GetClearDepth());
}
if ((mask & GL_STENCIL_BUFFER_BIT) != 0) {
renderer->ClearStencil(MG_State::pGLContext->GetClearStencil());
}
}
void DrawArrays(GLenum mode, GLint first, GLsizei count) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->DrawFromState(mode, first, count, 0, nullptr);
}
}
void DrawElements(GLenum mode, GLsizei count, GLenum type, const void* indices) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->DrawFromState(mode, 0, count, type, indices);
}
}
void DrawRangeElements(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices) {
// The CPU-side UploadVertexDataFromState path already honors the selected index
// range. start/end only restrict which indices may be referenced; they do not
// change the vertex buffer layout for this backend.
(void)start;
(void)end;
DrawElements(mode, count, type, indices);
}
void DrawRangeElementsBaseVertex(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type,
const void* indices, GLint basevertex) {
(void)start;
(void)end;
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
}
}
void MultiDrawArrays(GLenum mode, const GLint* first, const GLsizei* count, GLsizei drawcount) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr) {
return;
}
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] > 0) {
renderer->DrawFromState(mode, first[i], count[i], 0, nullptr);
}
}
}
void MultiDrawElements(GLenum mode, const GLsizei* count, GLenum type, const GLvoid* const* indices,
GLsizei drawcount) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr) {
return;
}
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] > 0) {
renderer->DrawFromState(mode, 0, count[i], type, indices[i]);
}
}
}
void DrawElementsBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLint basevertex) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
}
}
void MultiDrawElementsBaseVertex(GLenum mode, const GLsizei* count, GLenum type,
const GLvoid* const* indices, GLsizei drawcount,
const GLint* basevertex) {
for (GLsizei i = 0; i < drawcount; ++i) {
if (count[i] > 0) {
DrawElementsBaseVertex(mode, count[i], type, indices[i],
basevertex != nullptr ? basevertex[i] : 0);
}
}
}
void DrawArraysIndirect(GLenum mode, const void* indirect) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
return;
}
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawArraysIndirectCommand),
"DrawArraysIndirect");
if (bytes == nullptr) {
return;
}
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, bytes, sizeof(cmd));
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
return;
}
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First), static_cast<GLsizei>(cmd.Count),
0, nullptr);
}
}
void DrawElementsIndirect(GLenum mode, GLenum type, const void* indirect) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr) {
return;
}
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
return;
}
const auto* bytes = ResolveIndirectCommandBytes(indirect, sizeof(DrawElementsIndirectCommand),
"DrawElementsIndirect");
if (bytes == nullptr) {
return;
}
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, bytes, sizeof(cmd));
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
return;
}
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
for (Uint32 i = 0; i < cmd.InstanceCount; ++i) {
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
static_cast<GLint>(cmd.BaseVertex));
}
}
void MultiDrawArraysIndirect(GLenum mode, const void* indirect, GLsizei drawcount, GLsizei stride) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
return;
}
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawArraysIndirectCommand)) : stride;
for (GLsizei i = 0; i < drawcount; ++i) {
const auto* bytes = ResolveIndirectCommandBytes(
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
sizeof(DrawArraysIndirectCommand), "MultiDrawArraysIndirect");
if (bytes == nullptr) {
continue;
}
DrawArraysIndirectCommand cmd{};
std::memcpy(&cmd, bytes, sizeof(cmd));
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
continue;
}
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
renderer->DrawFromState(mode, static_cast<GLint>(cmd.First),
static_cast<GLsizei>(cmd.Count), 0, nullptr);
}
}
}
void MultiDrawElementsIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || drawcount <= 0) {
return;
}
const SizeT indexSize = MG_Util::GetGLTypeSize(type);
if (indexSize == 0) {
return;
}
const GLsizei realStride = stride == 0 ? static_cast<GLsizei>(sizeof(DrawElementsIndirectCommand)) : stride;
for (GLsizei i = 0; i < drawcount; ++i) {
const auto* bytes = ResolveIndirectCommandBytes(
static_cast<const Uint8*>(indirect) + static_cast<SizeT>(i) * static_cast<SizeT>(realStride),
sizeof(DrawElementsIndirectCommand), "MultiDrawElementsIndirect");
if (bytes == nullptr) {
continue;
}
DrawElementsIndirectCommand cmd{};
std::memcpy(&cmd, bytes, sizeof(cmd));
if (cmd.Count == 0 || cmd.InstanceCount == 0) {
continue;
}
const void* indices = reinterpret_cast<const void*>(static_cast<SizeT>(cmd.FirstIndex) * indexSize);
for (Uint32 instance = 0; instance < cmd.InstanceCount; ++instance) {
renderer->DrawFromState(mode, 0, static_cast<GLsizei>(cmd.Count), type, indices,
static_cast<GLint>(cmd.BaseVertex));
}
}
}
void MultiDrawArraysIndirectCount(GLenum mode, const void* indirect, GLintptr drawcount,
GLsizei maxdrawcount, GLsizei stride) {
if (MG_State::pGLContext == nullptr) {
return;
}
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!paramBuffer) {
return;
}
paramBuffer->SyncPersistentMappedRange();
const Uint8* paramData = paramBuffer->MappedData();
if (paramData == nullptr) {
return;
}
Uint32 actualDrawCount = 0;
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
MultiDrawArraysIndirect(mode, indirect, static_cast<GLsizei>(actualDrawCount), stride);
}
void MultiDrawElementsIndirectCount(GLenum mode, GLenum type, const void* indirect,
GLintptr drawcount, GLsizei maxdrawcount, GLsizei stride) {
if (MG_State::pGLContext == nullptr) {
return;
}
auto paramBuffer = MG_State::pGLContext->GetBufferBindingSlot(BufferTarget::Parameter).GetBoundObject();
if (!paramBuffer) {
return;
}
paramBuffer->SyncPersistentMappedRange();
const Uint8* paramData = paramBuffer->MappedData();
if (paramData == nullptr) {
return;
}
Uint32 actualDrawCount = 0;
std::memcpy(&actualDrawCount, paramData + static_cast<SizeT>(drawcount), sizeof(actualDrawCount));
actualDrawCount = std::min<Uint32>(actualDrawCount, static_cast<Uint32>(maxdrawcount));
MultiDrawElementsIndirect(mode, type, indirect, static_cast<GLsizei>(actualDrawCount), stride);
}
void DrawArraysInstanced(GLenum mode, GLint first, GLsizei count, GLsizei instancecount) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || instancecount <= 0) {
return;
}
for (GLsizei i = 0; i < instancecount; ++i) {
renderer->DrawFromState(mode, first, count, 0, nullptr);
}
}
void DrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount,
GLuint baseinstance) {
(void)baseinstance;
DrawArraysInstanced(mode, first, count, instancecount);
}
void DrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || instancecount <= 0) {
return;
}
for (GLsizei i = 0; i < instancecount; ++i) {
renderer->DrawFromState(mode, 0, count, type, indices);
}
}
void DrawElementsInstancedBaseVertex(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLint basevertex) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || instancecount <= 0) {
return;
}
for (GLsizei i = 0; i < instancecount; ++i) {
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
}
}
void DrawElementsInstancedBaseInstance(GLenum mode, GLsizei count, GLenum type, const void* indices,
GLsizei instancecount, GLuint baseinstance) {
(void)baseinstance;
DrawElementsInstanced(mode, count, type, indices, instancecount);
}
void DrawElementsInstancedBaseVertexBaseInstance(GLenum mode, GLsizei count, GLenum type,
const void* indices, GLsizei instancecount,
GLint basevertex, GLuint baseinstance) {
(void)baseinstance;
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || instancecount <= 0) {
return;
}
for (GLsizei i = 0; i < instancecount; ++i) {
renderer->DrawFromState(mode, 0, count, type, indices, basevertex);
}
}
void ClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat* value) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || value == nullptr) {
return;
}
if (buffer == GL_COLOR && drawbuffer == 0) {
renderer->Clear(value[0], value[1], value[2], value[3]);
} else if (buffer == GL_DEPTH && drawbuffer == 0) {
renderer->ClearDepth(value[0]);
}
}
void ClearBufferiv(GLenum buffer, GLint drawbuffer, const GLint* value) {
if (value == nullptr) {
return;
}
if (buffer == GL_STENCIL) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->ClearStencil(static_cast<Uint32>(value[0]));
}
return;
}
Float color[4] = {
static_cast<Float>(value[0]) / 255.0f,
static_cast<Float>(value[1]) / 255.0f,
static_cast<Float>(value[2]) / 255.0f,
static_cast<Float>(value[3]) / 255.0f,
};
ClearBufferfv(buffer, drawbuffer, color);
}
void ClearBufferuiv(GLenum buffer, GLint drawbuffer, const GLuint* value) {
if (value == nullptr) {
return;
}
if (buffer == GL_STENCIL) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->ClearStencil(value[0]);
}
return;
}
Float color[4] = {
static_cast<Float>(value[0]) / 255.0f,
static_cast<Float>(value[1]) / 255.0f,
static_cast<Float>(value[2]) / 255.0f,
static_cast<Float>(value[3]) / 255.0f,
};
ClearBufferfv(buffer, drawbuffer, color);
}
void ClearBufferfi(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || buffer != GL_DEPTH_STENCIL) {
return;
}
(void)drawbuffer;
renderer->ClearDepth(depth);
renderer->ClearStencil(static_cast<Uint32>(stencil));
}
void ReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void* pixels) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || pixels == nullptr) {
return;
}
// The Diligent backend's offscreen targets are RGBA8; the frontend currently
// uses this entry for the common GL_RGBA/GL_UNSIGNED_BYTE readback path.
if (format != GL_RGBA || type != GL_UNSIGNED_BYTE) {
return;
}
renderer->ReadPixels(static_cast<Uint32>(x), static_cast<Uint32>(y),
static_cast<Uint32>(width), static_cast<Uint32>(height), pixels);
}
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->BlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1,
mask, filter);
}
}
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFramebuffer,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFramebuffer,
GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter) {
(void)srcX0;
(void)srcY0;
(void)srcX1;
(void)srcY1;
(void)dstX0;
(void)dstY0;
(void)dstX1;
(void)dstY1;
(void)filter;
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->BlitNamedFramebuffer(readFramebuffer, drawFramebuffer, mask);
}
}
void CopyTexImage2D(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y,
GLsizei width, GLsizei height, GLint border) {
(void)level;
(void)internalformat;
(void)x;
(void)y;
(void)width;
(void)height;
(void)border;
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
return;
}
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
if (texture) {
renderer->CopyReadFramebufferToTexture(*texture);
}
}
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y,
GLsizei width, GLsizei height) {
(void)level;
(void)xoffset;
(void)yoffset;
(void)x;
(void)y;
(void)width;
(void)height;
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
return;
}
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
if (texture) {
renderer->CopyReadFramebufferToTexture(*texture);
}
}
void GetTexImage(GLenum target, GLint level, GLenum format, GLenum type, GLvoid* pixels) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D ||
format != GL_RGBA || type != GL_UNSIGNED_BYTE || pixels == nullptr) {
return;
}
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
if (texture) {
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
}
}
void GetTextureImage(const SharedPtr<MG_State::GLState::ITextureObject>& texture,
TextureUploadTarget uploadTarget, GLint level, GLenum format, GLenum type,
GLsizei bufSize, GLvoid* pixels) {
(void)uploadTarget;
(void)bufSize;
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || !texture || format != GL_RGBA || type != GL_UNSIGNED_BYTE ||
pixels == nullptr) {
return;
}
renderer->ReadTextureImage(*texture, static_cast<Uint32>(level), pixels);
}
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth) {
(void)srcTarget;
(void)srcLevel;
(void)srcX;
(void)srcY;
(void)srcZ;
(void)dstTarget;
(void)dstLevel;
(void)dstX;
(void)dstY;
(void)dstZ;
(void)srcWidth;
(void)srcHeight;
(void)srcDepth;
auto* renderer = GetActiveRenderer();
if (renderer != nullptr && srcTexture && dstTexture) {
renderer->CopyTextureSubData(*srcTexture, *dstTexture);
}
}
void GenerateMipmap(GLenum target) {
auto* renderer = GetActiveRenderer();
if (renderer == nullptr || MG_State::pGLContext == nullptr || target != GL_TEXTURE_2D) {
return;
}
auto& unit = MG_State::pGLContext->GetTextureUnitObject(MG_State::pGLContext->GetActiveTextureUnit());
auto texture = unit.GetBindingSlot(TextureTarget::Texture2D).GetBoundObject();
if (texture) {
renderer->GenerateMipmap(*texture);
}
}
Bool IsTimerQuerySupported() {
return true;
}
BackendQueryHandle BeginTimeElapsedQuery() {
auto* query = new CpuTimerQuery;
query->Start = std::chrono::steady_clock::now();
query->Available = false;
return query;
}
void EndTimeElapsedQuery(BackendQueryHandle query) {
if (query == nullptr) {
return;
}
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
const auto now = std::chrono::steady_clock::now();
cpuQuery->TimestampNs = static_cast<Uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(now - cpuQuery->Start).count());
cpuQuery->Available = true;
}
BackendQueryHandle QueryCounterTimestamp() {
auto* query = new CpuTimerQuery;
query->TimestampNs = static_cast<Uint64>(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
query->Available = true;
return query;
}
Bool IsQueryResultAvailable(BackendQueryHandle query) {
return query != nullptr && static_cast<CpuTimerQuery*>(query)->Available;
}
Bool GetQueryResult64(BackendQueryHandle query, Bool wait, Uint64* outNanoseconds) {
if (query == nullptr || outNanoseconds == nullptr) {
return false;
}
auto* cpuQuery = static_cast<CpuTimerQuery*>(query);
if (!cpuQuery->Available && !wait) {
return false;
}
*outNanoseconds = cpuQuery->TimestampNs;
return true;
}
void DeleteBackendQuery(BackendQueryHandle query) {
delete static_cast<CpuTimerQuery*>(query);
}
BackendSyncHandle FenceSync() {
// CPU fallback fence: always signaled is a valid implementation for a
// backend without native sync primitives. The handle still round-trips
// through ClientWaitSync/DeleteSync so frontend state stays balanced.
return new int(0);
}
GLenum ClientWaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
(void)sync;
(void)flags;
(void)timeout;
return GL_ALREADY_SIGNALED;
}
void WaitSync(BackendSyncHandle sync, GLbitfield flags, GLuint64 timeout) {
(void)sync;
(void)flags;
(void)timeout;
}
void DeleteSync(BackendSyncHandle sync) {
delete static_cast<int*>(sync);
}
Bool GetSyncStatus(BackendSyncHandle sync) {
(void)sync;
return true;
}
void SetSwapInterval(Int interval) {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->SetSwapInterval(interval > 0 ? static_cast<Uint32>(interval) : 0);
}
}
void Present() {
auto* renderer = GetActiveRenderer();
if (renderer != nullptr) {
renderer->Present();
}
}
} // namespace
BackendObject_Diligent::BackendObject_Diligent()
: m_rendererInfo(BuildInitialRendererInfo()) {}
BackendObject_Diligent::~BackendObject_Diligent() {
m_pRenderer.reset();
m_pContext.Release();
m_pDevice.Release();
m_pFactoryVk = nullptr;
}
Bool BackendObject_Diligent::CreateDiligentDevice() {
if (m_pDevice && m_pContext) {
return true;
}
try {
if (m_pFactoryVk == nullptr) {
m_pFactoryVk = ::Diligent::GetEngineFactoryVk();
if (m_pFactoryVk == nullptr) {
MGLOG_E("Diligent: failed to load Vulkan engine factory");
return false;
}
m_pFactoryVk->SetBreakOnError(false);
}
::Diligent::Uint32 numAdapters = 0;
m_pFactoryVk->EnumerateAdapters(::Diligent::Version{}, numAdapters, nullptr);
if (numAdapters == 0) {
MGLOG_W("Diligent: no Vulkan adapters available; skipping device creation");
return false;
}
::Diligent::EngineVkCreateInfo engineCI;
::Diligent::ImmediateContextCreateInfo ctxCI;
ctxCI.Name = "MobileGL Diligent Main Context";
ctxCI.QueueId = 0;
ctxCI.Priority = ::Diligent::QUEUE_PRIORITY_MEDIUM;
engineCI.NumImmediateContexts = 1;
engineCI.pImmediateContextInfo = &ctxCI;
::Diligent::IRenderDevice* pDevice = nullptr;
::Diligent::IDeviceContext* pContext = nullptr;
m_pFactoryVk->CreateDeviceAndContextsVk(engineCI, &pDevice, &pContext);
if (pDevice == nullptr || pContext == nullptr) {
MGLOG_E("Diligent: failed to create Vulkan device/context");
return false;
}
m_pDevice.Attach(pDevice);
m_pContext.Attach(pContext);
MGLOG_I("Diligent: Vulkan device created");
return true;
} catch (const std::exception& e) {
MGLOG_W("Diligent: Vulkan device creation failed: %s", e.what());
return false;
} catch (...) {
MGLOG_W("Diligent: Vulkan device creation failed");
return false;
}
}
void BackendObject_Diligent::Initialize() {
if (m_initialized) {
return;
}
if (!CreateDiligentDevice()) {
MGLOG_W("Diligent: backend initialization failed");
return;
}
m_pRenderer = std::make_unique<DiligentRenderer>(m_pDevice, m_pContext);
if (!m_pRenderer->Initialize(256, 256)) {
MGLOG_W("Diligent: renderer initialization failed");
m_pRenderer.reset();
return;
}
m_functions.GL.Clear = Clear;
m_functions.GL.DrawArrays = DrawArrays;
m_functions.GL.DrawElements = DrawElements;
m_functions.GL.DrawElementsBaseVertex = DrawElementsBaseVertex;
m_functions.GL.DrawRangeElements = DrawRangeElements;
m_functions.GL.DrawRangeElementsBaseVertex = DrawRangeElementsBaseVertex;
m_functions.GL.MultiDrawArrays = MultiDrawArrays;
m_functions.GL.MultiDrawElements = MultiDrawElements;
m_functions.GL.MultiDrawElementsBaseVertex = MultiDrawElementsBaseVertex;
m_functions.GL.DrawArraysInstanced = DrawArraysInstanced;
m_functions.GL.DrawArraysInstancedBaseInstance = DrawArraysInstancedBaseInstance;
m_functions.GL.DrawElementsInstanced = DrawElementsInstanced;
m_functions.GL.DrawElementsInstancedBaseVertex = DrawElementsInstancedBaseVertex;
m_functions.GL.DrawElementsInstancedBaseInstance = DrawElementsInstancedBaseInstance;
m_functions.GL.DrawElementsInstancedBaseVertexBaseInstance = DrawElementsInstancedBaseVertexBaseInstance;
m_functions.GL.DrawArraysIndirect = DrawArraysIndirect;
m_functions.GL.DrawElementsIndirect = DrawElementsIndirect;
m_functions.GL.MultiDrawArraysIndirect = MultiDrawArraysIndirect;
m_functions.GL.MultiDrawElementsIndirect = MultiDrawElementsIndirect;
m_functions.GL.MultiDrawArraysIndirectCount = MultiDrawArraysIndirectCount;
m_functions.GL.MultiDrawElementsIndirectCount = MultiDrawElementsIndirectCount;
m_functions.GL.ClearBufferfv = ClearBufferfv;
m_functions.GL.ClearBufferfi = ClearBufferfi;
m_functions.GL.ClearBufferiv = ClearBufferiv;
m_functions.GL.ClearBufferuiv = ClearBufferuiv;
m_functions.GL.BlitFramebuffer = BlitFramebuffer;
m_functions.GL.BlitNamedFramebuffer = BlitNamedFramebuffer;
m_functions.GL.CopyTexImage2D = CopyTexImage2D;
m_functions.GL.CopyTexSubImage2D = CopyTexSubImage2D;
m_functions.GL.CopyImageSubData = CopyImageSubData;
m_functions.GL.GenerateMipmap = GenerateMipmap;
m_functions.GL.GetTexImage = GetTexImage;
m_functions.GL.GetTextureImage = GetTextureImage;
m_functions.GL.ReadPixels = ReadPixels;
m_functions.GL.FenceSync = FenceSync;
m_functions.GL.ClientWaitSync = ClientWaitSync;
m_functions.GL.WaitSync = WaitSync;
m_functions.GL.DeleteSync = DeleteSync;
m_functions.GL.GetSyncStatus = GetSyncStatus;
m_functions.GL.IsTimerQuerySupported = IsTimerQuerySupported;
m_functions.GL.BeginTimeElapsedQuery = BeginTimeElapsedQuery;
m_functions.GL.EndTimeElapsedQuery = EndTimeElapsedQuery;
m_functions.GL.QueryCounterTimestamp = QueryCounterTimestamp;
m_functions.GL.IsQueryResultAvailable = IsQueryResultAvailable;
m_functions.GL.GetQueryResult64 = GetQueryResult64;
m_functions.GL.DeleteBackendQuery = DeleteBackendQuery;
m_functions.Present = Present;
m_functions.SetSwapInterval = SetSwapInterval;
m_initialized = true;
}
DiligentRenderer* BackendObject_Diligent::GetRenderer() {
return m_pRenderer.get();
}
Bool BackendObject_Diligent::InitCapabilities() {
// Skeleton: no format probing yet. The backend advertises GL 3.2 core
// capability, and the capability tables will be filled as resource
// creation paths are ported.
m_backendCapabilitiesInitialized = true;
return true;
}
Bool BackendObject_Diligent::InitWindowSurface() {
if (!m_windowHandle.Handle) {
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: native window handle is null");
return false;
}
if (m_pRenderer == nullptr || m_pFactoryVk == nullptr) {
MGLOG_E("BackendObject_Diligent::InitWindowSurface failed: renderer/factory is not ready");
return false;
}
return m_pRenderer->CreateSwapChain(m_pFactoryVk, m_windowHandle,
m_windowHandle.Width, m_windowHandle.Height);
}
Bool BackendObject_Diligent::InitPbufferSurface(EGLint width, EGLint height) {
// The Diligent backend keeps its offscreen target for pbuffer EGL surfaces.
// A future enhancement can resize/recreate the offscreen target to match the
// pbuffer dimensions.
(void)width;
(void)height;
return m_pRenderer != nullptr;
}
void BackendObject_Diligent::ReleaseEGLResources() {
if (m_pRenderer != nullptr) {
m_pRenderer->ReleaseSwapChain();
}
BackendObject::ReleaseEGLResources();
}
void BackendObject_Diligent::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
if (m_pRenderer != nullptr) {
m_pRenderer->ReleaseSwapChain();
}
}
Bool BackendObject_Diligent::CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: backend not initialized");
return false;
}
if (!handle.Handle || (handle.Backend != WindowBackend::Android && handle.Backend != WindowBackend::X11 &&
handle.Backend != WindowBackend::MetalLayer && handle.Backend != WindowBackend::Win32)) {
MGLOG_E("BackendObject_Diligent::CreateEGLWindowSurface failed: unsupported native window backend");
return false;
}
return RegisterEGLWindowSurface(surface, handle);
}
Bool BackendObject_Diligent::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_initialized) {
MGLOG_E("BackendObject_Diligent::CreateEGLPbufferSurface failed: backend not initialized");
return false;
}
return RegisterEGLPbufferSurface(surface, width, height);
}
Bool BackendObject_Diligent::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
return false;
}
if (m_eglSurface == surface && m_pRenderer != nullptr) {
return m_pRenderer->ResizeSwapChain(width, height);
}
return true;
}
const RendererInfo& BackendObject_Diligent::GetRendererInfo() const {
return m_rendererInfo;
}
String BackendObject_Diligent::GetBackendAPIVersionString() const {
return "Diligent Vulkan 0.1 (GL 3.2 skeleton)";
}
const GlobalBackendFunctionsTable& BackendObject_Diligent::GetBackendFunctions() const {
return m_functions;
}
const DynamicBackendParameters& BackendObject_Diligent::GetDynamicParameters() const {
return m_dynamicParameters;
}
BackendType BackendObject_Diligent::GetBackendType() const {
return BackendType::DiligentVulkan;
}
} // namespace MobileGL::MG_Backend::DiligentBackend
@@ -0,0 +1,72 @@
// MobileGL - MobileGL/MG_Backend/Diligent/BackendObject_Diligent.h
// Copyright (c) 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
#pragma once
#include <Includes.h>
#include "../BackendObject.h"
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
#if defined(True)
#undef True
#endif
#if defined(False)
#undef False
#endif
#include <RefCntAutoPtr.hpp>
namespace Diligent {
struct IEngineFactoryVk;
struct IRenderDevice;
struct IDeviceContext;
}
namespace MobileGL::MG_Backend::DiligentBackend {
class DiligentRenderer;
// New Diligent/Vulkan backend, implemented from scratch on top of
// DiligentCore. The backend object owns the Diligent device/context and
// currently advertises OpenGL 3.2 core capability; the GL function table
// is intentionally empty until drawing/resource paths are ported.
class BackendObject_Diligent : public BackendObject {
public:
BackendObject_Diligent();
~BackendObject_Diligent() override;
void Initialize() override;
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitPbufferSurface(EGLint width, EGLint height) 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;
void OnEGLSurfaceReleased(EGLSurface surface) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
const GlobalBackendFunctionsTable& GetBackendFunctions() const override;
const DynamicBackendParameters& GetDynamicParameters() const override;
BackendType GetBackendType() const override;
void ReleaseEGLResources() override;
DiligentRenderer* GetRenderer();
private:
Bool CreateDiligentDevice();
RendererInfo m_rendererInfo;
DynamicBackendParameters m_dynamicParameters;
GlobalBackendFunctionsTable m_functions{};
::Diligent::IEngineFactoryVk* m_pFactoryVk = nullptr;
::Diligent::RefCntAutoPtr<::Diligent::IRenderDevice> m_pDevice;
::Diligent::RefCntAutoPtr<::Diligent::IDeviceContext> m_pContext;
std::unique_ptr<DiligentRenderer> m_pRenderer;
Bool m_initialized = false;
};
} // namespace MobileGL::MG_Backend::DiligentBackend
@@ -0,0 +1,8 @@
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.cpp
// Copyright (c) 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
#include "DiligentVulkan.h"
@@ -0,0 +1,17 @@
// MobileGL - MobileGL/MG_Backend/Diligent/DiligentVulkan.h
// Copyright (c) 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
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DiligentBackend {
// Backend identity string used by the backend object and local smoke tests.
inline String GetDiligentVulkanBackendName() {
return "DiligentVulkan";
}
} // namespace MobileGL::MG_Backend::DiligentBackend
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,153 @@
// MobileGL - MobileGL/MG_Backend/Diligent/Renderer/DiligentRenderer.h
// Copyright (c) 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
#pragma once
#include <Includes.h>
// X11 (pulled in by Includes.h through Vulkan-Headers) defines True/False as
// macros, which collide with Diligent's Bool constants in BasicTypes.h.
#if defined(True)
#undef True
#endif
#if defined(False)
#undef False
#endif
#include <RefCntAutoPtr.hpp>
namespace MobileGL::MG_Backend {
struct WindowHandle;
}
namespace Diligent {
struct IRenderDevice;
struct IDeviceContext;
struct ITexture;
struct ITextureView;
struct IPipelineState;
struct IBuffer;
struct ISampler;
struct IShaderResourceBinding;
struct ISwapChain;
struct IEngineFactoryVk;
}
namespace MobileGL::MG_State::GLState {
class ITextureObject;
class SamplerObject;
class ProgramObject;
class RenderbufferObject;
class FramebufferObject;
}
namespace MobileGL::MG_Backend::DiligentBackend {
// Minimal real Diligent renderer used to prove the GL 3.2 basic path:
// clear an offscreen color target, draw a hardcoded triangle, and read
// pixels back. This is the first concrete rendering layer on top of the
// Diligent device; it will be expanded into the full MobileGL backend.
class DiligentRenderer {
public:
DiligentRenderer(::Diligent::IRenderDevice* device, ::Diligent::IDeviceContext* context);
~DiligentRenderer();
Bool Initialize(Uint32 width, Uint32 height);
void Clear(Float r, Float g, Float b, Float a);
void ClearDepth(Float depth);
void ClearStencil(Uint32 stencil);
void DrawTriangle();
void DrawVertices(const Float* vertices, Uint32 vertexCount);
// Creates a real Diligent swap chain for a native EGL window surface.
Bool CreateSwapChain(::Diligent::IEngineFactoryVk* factory, const WindowHandle& handle,
Uint32 width, Uint32 height);
Bool ResizeSwapChain(Uint32 width, Uint32 height);
void SetSwapInterval(Uint32 interval);
// Creates a simple 2D RGBA8 texture from CPU data and makes it available
// to state PSOs under the shader variable name "g_Texture".
Bool CreateTestTexture(const void* data, Uint32 width, Uint32 height);
// Draws using the live MG_State GL context: current program, VAO and
// bound buffers. This is the front-end emulation entry point.
void DrawFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
GLint baseVertex = 0);
void ReadPixels(Uint32 x, Uint32 y, Uint32 width, Uint32 height, void* pixels);
void BlitFramebuffer(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1,
GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1,
GLbitfield mask, GLenum filter);
void BlitNamedFramebuffer(const SharedPtr<MG_State::GLState::FramebufferObject>& readFbo,
const SharedPtr<MG_State::GLState::FramebufferObject>& drawFbo,
GLbitfield mask);
void CopyReadFramebufferToTexture(MG_State::GLState::ITextureObject& dst);
void CopyTextureSubData(MG_State::GLState::ITextureObject& src, MG_State::GLState::ITextureObject& dst);
void GenerateMipmap(MG_State::GLState::ITextureObject& texture);
Bool ReadTextureImage(MG_State::GLState::ITextureObject& texture, Uint32 level, void* pixels);
void ReleaseSwapChain();
void Present();
::Diligent::IRenderDevice* GetDevice() const { return m_pDevice; }
::Diligent::IDeviceContext* GetContext() const { return m_pContext; }
private:
struct TextureResource {
::Diligent::RefCntAutoPtr<::Diligent::ITexture> Texture;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> SRV;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> RTV;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> DSV;
Uint64 ContentVersion = 0;
Uint16 ParamsVersion = 0;
Bool IsDepth = false;
};
struct SamplerResource {
::Diligent::RefCntAutoPtr<::Diligent::ISampler> Sampler;
Uint16 Version = 0;
};
Bool CreateOffscreenTargets();
Bool CreatePipeline();
Bool CreateVertexBuffer();
Bool CreatePipelineFromState(GLenum mode);
Bool UploadVertexDataFromState(GLenum mode, GLint first, GLsizei count, GLenum type, const void* indices,
GLint baseVertex = 0);
::Diligent::ITextureView* SyncTexture(MG_State::GLState::ITextureObject& texture);
::Diligent::ITextureView* SyncTextureForAttachment(MG_State::GLState::ITextureObject& texture, Bool depth);
::Diligent::ITextureView* SyncRenderbuffer(MG_State::GLState::RenderbufferObject& renderbuffer);
::Diligent::ISampler* SyncSampler(const MG_State::GLState::SamplerObject& sampler);
Bool BindShaderResourcesFromState(const MG_State::GLState::ProgramObject& program);
Bool UploadUBOFromState(const MG_State::GLState::ProgramObject& program);
Bool ResolveCurrentRenderTargets(Vector<::Diligent::ITextureView*>& rtvs,
::Diligent::ITextureView*& dsv);
::Diligent::IRenderDevice* m_pDevice = nullptr;
::Diligent::IDeviceContext* m_pContext = nullptr;
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pColorTarget;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pColorRTV;
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pDepthTarget;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pDepthDSV;
::Diligent::RefCntAutoPtr<::Diligent::ISwapChain> m_pSwapChain;
::Diligent::RefCntAutoPtr<::Diligent::ITexture> m_pTestTexture;
::Diligent::RefCntAutoPtr<::Diligent::ITextureView> m_pTestSRV;
::Diligent::RefCntAutoPtr<::Diligent::ISampler> m_pTestSampler;
::Diligent::RefCntAutoPtr<::Diligent::IShaderResourceBinding> m_pStateSRB;
::Diligent::RefCntAutoPtr<::Diligent::IPipelineState> m_pPSO;
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pVertexBuffer;
::Diligent::RefCntAutoPtr<::Diligent::IBuffer> m_pUBO;
Uint32 m_uboSize = 0;
Uint32 m_uboContentVersion = 0;
Uint64 m_uboProgramLifetimeId = 0;
UnorderedMap<Uint64, TextureResource> m_textureCache;
UnorderedMap<Uint64, SamplerResource> m_samplerCache;
UnorderedMap<Uint32, TextureResource> m_renderbufferCache;
UnorderedMap<Uint64, ::Diligent::RefCntAutoPtr<::Diligent::IBuffer>> m_namedUboCache;
Uint32 m_width = 256;
Uint32 m_height = 256;
Uint32 m_swapInterval = 0;
Uint32 m_lastDrawVertexCount = 0;
Uint64 m_lastPSOKey = 0;
Bool m_hasCachedPSO = false;
Bool m_initialized = false;
};
} // namespace MobileGL::MG_Backend::DiligentBackend
@@ -8,7 +8,6 @@
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
@@ -32,54 +31,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return draw == EGL_NO_SURFACE && read == EGL_NO_SURFACE && ctx == EGL_NO_CONTEXT;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / ID-67 (v1, under ID-52/ID-59's grant for this file). Which VIRTUAL (dpy, draw,
// read, ctx) the process's one native ES context is currently bound FOR, on the apply
// thread. DirectGLES has one native context and one native surface (g_Context, g_Surface)
// whatever virtual handles the client uses, so "is a native eglMakeCurrent needed" is never
// "is the native triple different" - it is "did the VIRTUAL context change":
// DirectGLES::MakeCurrent is also where the seven caches that describe the frontend context
// are invalidated, and a different virtual context needs them invalidated even though the
// driver binds the same triple. ID-67: a make-current with a DIFFERENT tuple is a real
// native bind (and a caps republish, ServerLoop's half); an IDENTICAL one is neither.
//
// Three states. NotBound: the next bind is native. FreshFromSurfaceCreation: the surface's
// own creation (InitPbufferSurface / InitWindowSurface) bound natively and invalidated, and
// no virtual tuple has claimed that bind yet - the first tuple adopts it, which is the
// "2 -> 1 native binds per process" of round 3. BoundForTuple: bound and invalidated for
// the recorded tuple; only that exact tuple may skip. Process-wide like the native state it
// mirrors; under split exactly one DirectGLES object exists (the server's), and only the
// apply thread reaches these.
enum class NativeBindState : Uint8 { NotBound, FreshFromSurfaceCreation, BoundForTuple };
NativeBindState g_nativeBindState = NativeBindState::NotBound;
EGLDisplay g_nativeBoundDpy = EGL_NO_DISPLAY;
EGLSurface g_nativeBoundDraw = EGL_NO_SURFACE;
EGLSurface g_nativeBoundRead = EGL_NO_SURFACE;
EGLContext g_nativeBoundCtx = EGL_NO_CONTEXT;
void NoteNativeContextGone() { g_nativeBindState = NativeBindState::NotBound; }
void NoteNativeContextFreshFromSurfaceCreation() {
g_nativeBindState = NativeBindState::FreshFromSurfaceCreation;
}
void NoteNativeBoundFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
g_nativeBindState = NativeBindState::BoundForTuple;
g_nativeBoundDpy = dpy;
g_nativeBoundDraw = draw;
g_nativeBoundRead = read;
g_nativeBoundCtx = ctx;
}
Bool NativeBindCanBeSkippedFor(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
switch (g_nativeBindState) {
case NativeBindState::NotBound: return false;
case NativeBindState::FreshFromSurfaceCreation: return true;
case NativeBindState::BoundForTuple:
return g_nativeBoundDpy == dpy && g_nativeBoundDraw == draw && g_nativeBoundRead == read &&
g_nativeBoundCtx == ctx;
}
return false;
}
#endif
void ClearGLErrors(const MG_External::GLESFunctionsTable& gl) {
if (!gl.glGetError) return;
while (gl.glGetError() != GL_NO_ERROR) {}
@@ -261,10 +212,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) {
reasons.push_back("no colour-renderable three-channel format on OpenGL ES");
}
// A format is either 8- or 16-bit signed normalized, so at most one of the two ever
// survives GetApplicablePixelFormatNormalizeOptions and the reason is not duplicated.
if ((options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) ||
(options & PixelFormatNormalizeOptionBit::NoSnorm8RenderTarget)) {
if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) {
reasons.push_back("EXT_render_snorm not supported");
}
@@ -355,23 +303,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return capabilities.MaxColorTextureSamples;
}
// The RENDERBUFFER twin, and it is a different set of pnames on purpose.
// GL_MAX_{COLOR,DEPTH}_TEXTURE_SAMPLES bound multisample TEXTURES; a renderbuffer is
// bounded by GL_MAX_SAMPLES (GL 4.6 core 9.2.4), with GL_MAX_INTEGER_SAMPLES for the
// integer formats. Using the texture ceilings here - which is what the renderbuffer probe
// did - is not merely untidy: the two texture pnames are ES 3.1 state, so on an ES 3.0
// context the loader's rejected-probe clamp leaves them at 1 (see the multisample clamps
// in the GLES loader) and the walk below would never run past one sample, recording {1}
// for EVERY colour format while GL_MAX_SAMPLES - ES 3.0 core, so genuinely answered -
// reports 4. Once the frontend validates against this list, that would reject every
// multisample renderbuffer on such a context.
Int GetGLESRenderbufferFormatMaxSamples(const MG_External::GLESCapabilities& capabilities,
GLenum imageFormat) {
const Bool isInteger = imageFormat == GL_RED_INTEGER || imageFormat == GL_RG_INTEGER ||
imageFormat == GL_RGB_INTEGER || imageFormat == GL_RGBA_INTEGER;
return isInteger ? capabilities.MaxIntegerSamples : capabilities.MaxSamples;
}
Bool ProbeFramebufferCompletenessForTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLuint texture, TextureInternalFormat format) {
GLuint framebuffer = 0;
@@ -475,12 +406,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
return complete;
}
// `samples` only reaches the multisample targets; every other target ignores it. The
// descending sample walk (ProbeTextureSampleCounts) reuses this whole routine rather than
// repeating the gen/bind/completeness/delete dance.
Bool ProbeTexture(const MG_External::GLESFunctionsTable& gl, TextureTarget target, GLenum internalFormat,
GLenum imageFormat, GLenum imageType, TextureInternalFormat logicalFormat,
Bool* outRenderable, Int samples = 1) {
Bool* outRenderable) {
if (!IsGLESProbeTextureTarget(target) || !gl.glGenTextures || !gl.glBindTexture || !gl.glDeleteTextures) {
return false;
}
@@ -500,11 +428,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
const Bool isMultisample = IsGLESProbeMultisampleTarget(target);
if (isMultisample) {
const auto probeSamples = static_cast<GLsizei>(std::max(samples, 1));
if (target == TextureTarget::Texture2DMultisample && gl.glTexStorage2DMultisample) {
gl.glTexStorage2DMultisample(glTarget, probeSamples, internalFormat, 1, 1, GL_TRUE);
gl.glTexStorage2DMultisample(glTarget, 1, internalFormat, 1, 1, GL_TRUE);
} else if (target == TextureTarget::Texture2DMultisampleArray && gl.glTexStorage3DMultisample) {
gl.glTexStorage3DMultisample(glTarget, probeSamples, internalFormat, 1, 1, 1, GL_TRUE);
gl.glTexStorage3DMultisample(glTarget, 1, internalFormat, 1, 1, 1, GL_TRUE);
} else {
gl.glBindTexture(glTarget, static_cast<GLuint>(previousBinding));
gl.glDeleteTextures(1, &texture);
@@ -600,29 +527,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return sampleCounts;
}
// The multisample TEXTURE twin of ProbeRenderbufferSampleCounts. It used to be a
// hardcoded {1}, which made glGetInternalformativ(GL_SAMPLES) claim a one-sample maximum
// for every format on the multisample targets even where glTexImage2DMultisample happily
// accepts four - GL 4.6 core 8.8 makes that query the definition of the maximum, so the
// two answers cannot both be right. Completeness is required at every count, exactly as
// the renderbuffer walk requires it; the caller only reaches here once the one-sample
// probe has already succeeded, so 1 terminates the list without being re-probed.
Vector<Int> ProbeTextureSampleCounts(const MG_External::GLESFunctionsTable& gl, TextureTarget target,
GLenum internalFormat, GLenum imageFormat, GLenum imageType,
TextureInternalFormat logicalFormat, Int maxSamples) {
Vector<Int> sampleCounts;
for (Int samples = std::max(maxSamples, 1); samples > 1; samples >>= 1) {
Bool renderable = false;
const Bool created = ProbeTexture(gl, target, internalFormat, imageFormat, imageType, logicalFormat,
&renderable, samples);
if (created && renderable) {
sampleCounts.push_back(samples);
}
}
sampleCounts.push_back(1);
return sampleCounts;
}
void PopulateFormatCapabilitiesImpl(const MG_External::GLESFunctionsTable& gl,
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache) {
@@ -723,11 +627,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, targetIndex, formatIndex,
BuildTextureCapsFromProbe(logicalFormat, target, nativeRenderable));
if (IsGLESProbeMultisampleTarget(target)) {
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, nativeInfo.InternalFormat, nativeInfo.ImageFormat,
nativeInfo.ImageType, logicalFormat, maxSamples);
cache.SampleCounts[targetIndex][formatIndex] = {1};
}
}
shouldProbeFallback = !nativeCreated || !nativeRenderable;
@@ -745,11 +645,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, targetIndex, fallbackInfo);
}
if (IsGLESProbeMultisampleTarget(target)) {
const Int maxSamples =
GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat);
cache.SampleCounts[targetIndex][formatIndex] = ProbeTextureSampleCounts(
gl, probeTarget, fallbackInfo.InternalFormat, fallbackInfo.ImageFormat,
fallbackInfo.ImageType, logicalFormat, maxSamples);
cache.SampleCounts[targetIndex][formatIndex] = {1};
}
}
}
@@ -782,7 +678,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
AddFullFormatCaps(cache, renderbufferTargetIndex, formatIndex,
GetRenderbufferFeatureCaps(logicalFormat));
const Int maxSamples =
GetGLESRenderbufferFormatMaxSamples(capabilities, nativeInfo.ImageFormat);
GetGLESFormatMaxSamples(capabilities, logicalFormat, nativeInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] =
ProbeRenderbufferSampleCounts(gl, nativeInfo.InternalFormat, logicalFormat, maxSamples);
} else {
@@ -796,7 +692,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, renderbufferFallbackInfo);
}
const Int maxSamples =
GetGLESRenderbufferFormatMaxSamples(capabilities, renderbufferFallbackInfo.ImageFormat);
GetGLESFormatMaxSamples(capabilities, logicalFormat, renderbufferFallbackInfo.ImageFormat);
cache.SampleCounts[renderbufferTargetIndex][formatIndex] = ProbeRenderbufferSampleCounts(
gl, renderbufferFallbackInfo.InternalFormat, logicalFormat, maxSamples);
}
@@ -814,11 +710,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
.ExtraVendor = Nullopt, // Extra vendor
.RendererGLInfo =
{
.TargetGLVersion = {4, 6, 0}, // GL target version
.TargetGLVersion = {4, 0, 0}, // GL target version
.TargetGLSLVersion = {4, 6, 0}, // Target Shading Language Version
// Baseline advertisement (no runtime capabilities yet); reconciled once
// the ES capabilities exist, see UpdateAdvertisedCapabilityExtensions.
.Extensions = BuildAdvertisedExtensions(false, false, false, false, false, false),
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.IsCompatibilityProfile = false // Is Compatibility Profile
},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false} // Backend Capability
@@ -842,8 +738,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
MutableRendererInfo().RendererGLInfo.Extensions = BuildAdvertisedExtensions(
AreTimerQueriesSupported(), capabilities.SupportsTextureFilterAnisotropy,
capabilities.SupportsDrawIndirect,
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance,
capabilities.SupportsTextureView, capabilities.SupportsTextureCubeMapArray);
capabilities.SupportsDrawIndirect && capabilities.SupportsBaseInstance);
}
} // namespace
@@ -852,123 +747,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
PopulateFormatCapabilitiesImpl(gl, capabilities, cache);
}
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples) {
if (samples <= 1) {
return samples;
}
Int maxSamples = 0;
const SizeT formatIndex = static_cast<SizeT>(logicalFormat);
#if MOBILEGL_BUILD_DISAGGREGATED
// C6 / ID-52: read the ROLE's own cache. Under split that is the server's private backend
// (ActiveBackendFormatCaps), not pActiveBackendObject, which holds the client's mirror.
const FormatCapabilityCache* activeCaps = ActiveBackendFormatCaps();
if (activeCaps != nullptr && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts = activeCaps->SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
#else
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& probedCounts =
pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex];
if (!probedCounts.empty()) {
maxSamples = probedCounts.front();
}
}
#endif
if (maxSamples <= 0) {
maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat);
}
return std::min(samples, std::max(maxSamples, 1));
}
BackendObject_DirectGLES::~BackendObject_DirectGLES() {
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
// P12: under a transport this object is the SERVER's backend and its destruction is the
// end of a session (ServerLoop's apply thread, after the final drain). The in-process
// display server runs the next session in this same process, so the twins this one built
// - naming ids of the context just destroyed - must not answer for the next client's
// handles (Managers.h, DropEveryTwinForEndedServerSession). Monolith keeps its twins.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
DropEveryTwinForEndedServerSession();
// Review fix: and the swap interval it asked for (DirectGLES.h).
ForgetRequestedSwapInterval();
}
#endif
}
Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) {
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return false;
if (DirectGLES::BindSessionSurface(it->second)) return true;
// The session no longer owns it (destroyed with a window loss): forget and recreate.
m_nativeSurfaces.erase(it);
return false;
}
void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) {
// The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to
// whichever context the session had bound (often another window's thread), and putting back
// "the surface activated last" instead re-attached THAT context to a third window's surface -
// a desktop drawing into and reading back from the panel's pbuffer.
if (previousNativeDraw == EGL_NO_SURFACE) return;
if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return;
for (const auto& [client, native] : m_nativeSurfaces) {
if (native != previousNativeDraw) continue;
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) break;
m_eglSurface = client;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = state->second.Kind;
if (state->second.Kind == SurfaceKind::Window) m_windowHandle = state->second.Window;
break;
}
}
Bool BackendObject_DirectGLES::InitWindowSurface() {
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
// A suspended surface whose placeholder went: another placeholder - never the window, which
// is gone.
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) !=
m_suspendedWindowSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1);
if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) {
MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p",
reinterpret_cast<void*>(m_activatingSurface));
return false;
}
m_nativeSurfaces[m_activatingSurface] = placeholder;
return true;
}
// Only use EGL for now
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
return false;
}
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
}
Bool BackendObject_DirectGLES::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return LatchIfGuiltyBeforeApply();
#else
return false;
#endif
}
void BackendObject_DirectGLES::Initialize() {
MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
@@ -1029,41 +821,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
//
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
// the native context - and with it every buffer, texture and program the application had
// built. The native context now belongs to the (session, context token) tuple the backend
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
// bookkeeping (which surface is active, which threads made it current) does describe the
// surface that is being replaced.
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / ID-67: the surface's creation bound natively (InitWindowSurface -> MakeCurrent);
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
// pull arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
if (created && previous != EGL_NO_SURFACE && previous != surface) {
// Another surface was current: the creation must not leave the context that held it
// drawing to the new one (P14 multi-window clients).
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLWindowSurface(surface, handle);
#endif
}
Bool BackendObject_DirectGLES::CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) {
@@ -1077,60 +840,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// P14 S4: as for the window surface above - the native context is the (session, context
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
// arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
if (created && previous != EGL_NO_SURFACE && previous != surface) {
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
}
return created;
#else
return BackendObject::CreateEGLPbufferSurface(surface, width, height);
#endif
}
Bool BackendObject_DirectGLES::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
const auto state = m_eglSurfaces.find(surface);
const auto native = m_nativeSurfaces.find(surface);
if (state == m_eglSurfaces.end() || state->second.Kind != SurfaceKind::Pbuffer || native == m_nativeSurfaces.end()) {
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
}
// A pbuffer has no resize: the client surface gets a new native pbuffer at the new size,
// and every context that drew to the old one draws to the new one.
const EGLint w = static_cast<EGLint>(std::max<Uint32>(width, 1));
const EGLint h = static_cast<EGLint>(std::max<Uint32>(height, 1));
const EGLSurface replacement = DirectGLES::CreateSessionPbuffer(w, h);
if (replacement == EGL_NO_SURFACE) {
MGLOG_E("ResizeEGLWindowSurface: no %dx%d pbuffer could replace the old drawable", w, h);
return false;
}
DirectGLES::ReplaceSessionSurface(native->second, replacement);
native->second = replacement;
return BackendObject::ResizeEGLWindowSurface(surface, width, height);
}
Bool BackendObject_DirectGLES::InitPbufferSurface(EGLint width, EGLint height) {
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
if (!DirectGLES::InitPbufferSurface(width, height)) return false;
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
return DirectGLES::InitPbufferSurface(width, height);
}
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
@@ -1139,9 +858,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (!DirectGLES::ReleaseCurrent()) {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
}
@@ -1162,99 +878,17 @@ namespace MobileGL::MG_Backend::DirectGLES {
return false;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// ID-54 / C7, the native half of "bind once per tuple" (v1, under ID-52/ID-59's grant for
// this file; #if-guarded so the pull build is byte-identical). Under an active transport
// this runs on the apply thread, which is the ONLY thread that ever binds the server's
// context, and the surface it is asked for was made natively current on this very thread
// by its own creation (InitPbufferSurface / InitWindowSurface -> DirectGLES::MakeCurrent).
// A second native eglMakeCurrent for the same surface is then a repeat: it rewrites the
// owner with the same thread, re-registers the same op table and invalidates seven caches
// that describe a context that did not change - the per-client-make-current storm the
// v2 review measured as "2 native binds per process, unchanged". So when the requested
// draw surface IS the active one and EGL itself says this thread holds the context
// (IsBackendContextCurrentOnThisThread re-verifies against eglGetCurrentContext), the
// native call is skipped and only the base class's bookkeeping below runs - which is
// still required: it is what InitCapabilities and SwapEGLBuffers' current-thread record
// hang off. Monolith transport in this build, and the pull build, bind exactly as before.
//
// AND ONLY FOR THE SAME VIRTUAL TUPLE (ID-67). The skip is keyed on NativeBindCanBeSkippedFor:
// the surface's own creation bind is adopted by the FIRST tuple, an identical tuple skips,
// and a DIFFERENT tuple - a second MobileGL context onto the same surface - runs the native
// call again even though the driver's triple is the same, because MakeCurrent's seven
// invalidations describe the frontend context that is changing. Red once, two ways:
// make this arm unconditional (ServerLoopTest's C7 control reads 2 native binds at the EGL
// function table instead of 1), or make NativeBindCanBeSkippedFor answer true for any tuple
// (the ID-67 control's different tuple stays at 1 native bind where 2 are required).
const Bool nativelyCurrentAlready = MG_Config::Transport != MG_Config::TransportMode::Monolith &&
m_eglSurfaceInitialized && m_eglSurface == draw &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
// The draw surface's own native surface (P14 multi-window): a known one is re-bound, so the
// bind goes to it and not to whichever surface the session drew to last.
// The real bind still runs whenever the skip above says so (ID-67: a different tuple onto
// the same surface needs MakeCurrent's invalidations even though the driver triple is equal).
const auto nativeDraw = m_nativeSurfaces.find(draw);
if (!nativelyCurrentAlready && nativeDraw != m_nativeSurfaces.end()) {
(void)DirectGLES::SelectSessionSurface(nativeDraw->second);
}
const Bool bound = nativelyCurrentAlready || DirectGLES::MakeCurrent();
if (!bound) {
NoteNativeContextGone();
return false;
}
NoteNativeBoundFor(dpy, draw, read, ctx);
#else
if (!DirectGLES::MakeCurrent()) {
return false;
}
#endif
if (!BackendObject::MakeEGLCurrent(dpy, draw, read, ctx)) {
(void)DirectGLES::ReleaseCurrent();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
return false;
}
return true;
}
Bool BackendObject_DirectGLES::BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::BlitDefaultFramebufferTo(image, region);
#else
(void)image, (void)region;
return false;
#endif
}
Int32 BackendObject_DirectGLES::QueryCurrentBufferAge(Bool damageRegionFollows) {
return DirectGLES::CurrentDrawBufferAge(damageRegionFollows);
}
Bool BackendObject_DirectGLES::SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
return DirectGLES::SetCurrentDrawDamageRegion(region);
}
Bool BackendObject_DirectGLES::PublishSharedImageAccesses() {
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::PublishPendingAccesses();
#else
return true;
#endif
}
Bool BackendObject_DirectGLES::ExportNativeFence(int* fence) {
*fence = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
return DirectGLES::SharedImageImpl::ExportNativeFence(fence);
#else
return true;
#endif
}
Bool BackendObject_DirectGLES::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
return BackendObject::SwapEGLBuffers(dpy, draw);
}
@@ -1267,119 +901,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::ReleaseEGLResources() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
DestroyEGLContext();
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
m_suspendedWindowSurfaces.clear();
BackendObject::ReleaseEGLResources();
}
Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> onWindow;
for (const auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
onWindow.push_back(client);
}
for (const EGLSurface client : onWindow) {
// A surface never activated has no native surface: only its record names the window.
const auto native = m_nativeSurfaces.find(client);
if (native != m_nativeSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second);
if (placeholder == EGL_NO_SURFACE) return false;
native->second = placeholder;
}
m_suspendedWindowSurfaces.push_back(client);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The native binding moved onto the placeholder: the next make-current binds for real.
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
"contexts and their objects stay",
onWindow.size(), window);
return true;
}
Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> suspended;
suspended.swap(m_suspendedWindowSurfaces);
for (const EGLSurface client : suspended) {
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) continue; // released while suspended
state->second.Window.Handle = window;
state->second.Window.Width = width;
state->second.Window.Height = height;
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state->second.Window;
// Never activated: its first activation creates it on the new window.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
const EGLSurface surface =
DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast<NativeWindowType>(window));
if (surface == EGL_NO_SURFACE) return false;
native->second = surface;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
return true;
}
Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Bool published = false;
for (auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
state.Window.Width = width;
state.Window.Height = height;
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state.Window;
// The window surface's buffers follow the window by themselves (from the next one the
// driver dequeues); only the client has to be told.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
published = DirectGLES::RepublishWindowSurfaceShape(native->second,
reinterpret_cast<NativeWindowType>(window)) ||
published;
}
return published;
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
// P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the
// contexts. This used to be DestroyEGLContext() - every native context of the session, with
// every object the client had built - which a multi-window client hit on its first closed
// popup.
(void)surface;
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
}
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
m_suspendedWindowSurfaces.erase(
std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface),
m_suspendedWindowSurfaces.end());
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return;
const EGLSurface native = it->second;
m_nativeSurfaces.erase(it);
DirectGLES::DestroySessionSurface(native);
DestroyEGLContext();
}
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
@@ -1401,23 +928,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
Vector<GLExtension> BuildAdvertisedExtensions(Bool timerQueriesSupported, Bool anisotropicFilteringSupported,
Bool drawIndirectSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool textureViewSupported, Bool cubeMapArraySupported) {
Bool nonZeroIndirectBaseInstanceSupported) {
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 on an otherwise capable
// implementation.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
E_GL_ARB_texture_non_power_of_two,
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_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
@@ -1442,77 +955,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// has had the same texture parameter since ES 3.1, which every device MobileGL
// runs on provides.
E_GL_ARB_stencil_texturing,
// 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 host GLsync here, a VkFence on DirectVulkan), 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 SyncAtomicCounterBuffers
// re-issues the counter buffer as an SSBO binding in the range reserved at the top of
// the ES driver's shader-storage points, so a counter dispatch reads and writes the
// buffer the application bound. DirectVulkan reaches the same place through its own
// descriptor resolution, so the string is symmetric.
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,
// The whole of KHR_debug lives in GLImpl - the message log, the group stack and the
// object-label table are MobileGL's own state, not the host driver's - so it is as
// available here as it is on DirectVulkan, which has advertised it all along.
E_GL_KHR_debug,
// 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.
//
// 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 this backend syncs through to the ES
// driver's identical parameters.
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 and the per-viewport routing emulation.
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.
@@ -1561,47 +1003,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
if (timerQueriesSupported && !MG_Config::Features.DisableTimerQuery) {
extensions.push_back(E_GL_ARB_timer_query);
}
// Cube map arrays are core from GL 4.0 and from ES 3.2, but on a pre-ES-3.2 driver without
// EXT/OES_texture_cube_map_array there is nothing underneath: the texture gets no storage
// and a samplerCubeArray shader does not even compile, which is exactly what the POST
// reports. So the string follows the host capability rather than the version.
//
// 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);
}
// Only advertised when the host ES driver has EXT/OES_texture_view. ES has no core
// texture views at any version and no honest emulation exists: a view is a SECOND NAME
// over the SAME storage, so that writes through either are visible through the other and
// the two carry independent per-texture parameters at the same time - which is exactly
// what applications use it for (Better Clouds samples one D24S8 through its own name with
// DEPTH_STENCIL_TEXTURE_MODE = STENCIL_INDEX and through a view with DEPTH_COMPONENT, in
// a single shading pass). A copy-based fallback satisfies neither half, and fails
// silently; withholding the string and answering glTextureView with INVALID_OPERATION is
// the only behaviour that cannot be mistaken for success.
//
// The host extension is necessary and NOT sufficient, which is why this second gate
// exists. Adreno 830 has EXT_texture_view, and on it the whole functional half of
// KHR-GL4{2,3}.texture_view fails: base_and_max_levels, reference_counting and
// view_sampling Fail and view_classes crashes, while only the two pure-API cases
// (errors, gettexparameter - neither of which touches the host view) pass. The cause is
// known and is MobileGL's, not the driver's: SyncTextureViewToBackend normalizes the
// VIEW's ES internalformat independently of the storage it aliases, so whenever the two
// land on different renderability carriers the host rejects the pair, the error is
// swallowed, and the view is left as a storage-less name that samples as zeros.
// DirectVulkan builds the view as a second VkImageView over one VkImage and has no such
// seam - it passes 5 of the 7 cases on the same device - so the string stays there.
//
// Until that reconciliation exists, advertising here would be the same lie the comment
// above refuses to tell, just with an extra prerequisite met. Set
// MOBILEGL_ESPRYT_ENABLE_TEXTURE_VIEW=1 to re-enable it for that work.
if (textureViewSupported && MG_Config::Features.EsprytEnableTextureView) {
extensions.push_back(E_GL_ARB_texture_view);
}
// Only advertised when the host ES driver actually filters anisotropically: the sampler
// state is accepted regardless, but forwarding it would be a no-op without the extension,
// and an app that trusts the string (LWJGL builds GLCapabilities from it) would silently
@@ -1655,6 +1056,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
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.Clear = Clear;
funcsTable.GL.ClearBufferfi = ClearBufferfi;
@@ -1704,12 +1107,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// geometry shader's amplification.
funcsTable.GL.BeginXfbPrimitivesQuery = BeginXfbPrimitivesQuery;
funcsTable.GL.EndXfbPrimitivesQuery = EndXfbPrimitivesQuery;
// ...but where it CAN see the whole capture - no geometry stage - the frontend's
// own count is the desktop-exact one and the ES driver's is only as good as the
// vendor made it (Adreno doubles PRIMITIVES_WRITTEN for a vertex-only capture that
// follows a large render pass). The query above stays installed: it is still what
// answers an amplifying span, and PRIMITIVES_GENERATED always.
funcsTable.GL.PrefersCpuXfbPrimitiveAccounting = true;
funcsTable.GL.IsQueryResultAvailable = IsQueryResultAvailable;
funcsTable.GL.GetQueryResult64 = GetQueryResult64;
funcsTable.GL.DeleteBackendQuery = DeleteBackendQuery;
@@ -1739,8 +1136,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
void BackendObject_DirectGLES::UpdateDynamicBackendParameters() {
m_dynamicParameters.UniformBufferOffsetAlignment = m_GLESCapabilities.UniformBufferOffsetAlignment;
m_dynamicParameters.ShaderStorageBufferOffsetAlignment =
m_GLESCapabilities.ShaderStorageBufferOffsetAlignment;
m_dynamicParameters.MaxTextureMaxAnisotropy = m_GLESCapabilities.MaxTextureMaxAnisotropy;
m_dynamicParameters.AliasedLineWidthRangeMin = m_GLESCapabilities.AliasedLineWidthRangeMin;
m_dynamicParameters.AliasedLineWidthRangeMax = m_GLESCapabilities.AliasedLineWidthRangeMax;
@@ -1791,38 +1186,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
static_cast<Int>(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS));
m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks;
m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks;
// Per-stage storage-block counts, forwarded from the host driver rather than invented.
// A stage the driver cannot serve reports 0, which is a legal answer everywhere these
// limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every
// graphics stage except fragment) and is the only answer that lets an application take
// its own fallback instead of building a program the driver will refuse to link. The
// stage limit cannot exceed the combined limit or the number of binding points there
// are to bind buffers to, so clamp to both.
const auto clampStageStorageBlocks = [this](Int stageLimit) {
return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0),
std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)});
};
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
m_dynamicParameters.MaxVertexShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks);
m_dynamicParameters.MaxTessControlShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks);
m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks);
m_dynamicParameters.MaxGeometryShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks);
m_dynamicParameters.MaxFragmentShaderStorageBlocks =
clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks);
m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks;
m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations;
// The six per-axis compute limits: the driver's raw glGetIntegeri_v answers, the same
// numbers GLFunctionsTable::GetIntegeri_v forwards live. Carried here so that MGPCaps has
// them once the table entry retires (plan B section 4.4.1); GL_Getter floors them.
for (SizeT axis = 0; axis < 3; ++axis) {
m_dynamicParameters.MaxComputeWorkGroupCount[axis] = m_GLESCapabilities.MaxComputeWorkGroupCount[axis];
m_dynamicParameters.MaxComputeWorkGroupSize[axis] = m_GLESCapabilities.MaxComputeWorkGroupSize[axis];
}
// (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.)
m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings;
// This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and
// on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather
// than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says
@@ -1875,61 +1241,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
DynParams::PerLayerFramebufferAttachmentBit(TextureTarget::TextureCubeMapArray);
}
}
// Not a driver question and never will be: 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 a
// module that still declared Float64 would never reach the driver at all. The demotion is
// mathematically mandatory here, on every device, forever - which is why this stays false
// regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsShaderFloat64 = false;
// Follows the line above, and must: OpenGL ES has no double-precision vertex format and no
// fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to land here.
// Not a driver question and never will be: OpenGL ES has no double-precision vertex format
// and ESSL has no fp64 type to consume one with, so a 64-bit vertex attribute has nowhere to
// land on this backend regardless of what the driver underneath happens to support.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
// Whether a tessellation / geometry stage's ESSL may name gl_PointSize at all: the two
// extension pairs the loader probed, independently, because they really do come
// separately. False arms the shared phase-B demotion
// (ShaderCompiler::DemoteTessellationGeometryPointSizeForProgram), whose ESSL then
// never names the built-in in those stages and needs no extension.
// MOBILEGL_POINT_SIZE_DEMOTION=1 pretends both are absent so the demotion can be
// exercised on a healthy driver (the pinned integration lane); =0 restores the
// detected answer's declines.
m_dynamicParameters.SupportsTessellationPointSize =
m_GLESCapabilities.TessellationPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
m_dynamicParameters.SupportsGeometryPointSize =
m_GLESCapabilities.GeometryPointSizeSupport !=
MG_External::GLESCapabilities::PointSizeTier::None;
switch (MG_Config::Features.PointSizeDemotion) {
case MG_Config::QuirkOverride::ForceOn:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=1 - treating tessellation/geometry "
"gl_PointSize as unhosted so the demotion runs on this driver");
m_dynamicParameters.SupportsTessellationPointSize = false;
m_dynamicParameters.SupportsGeometryPointSize = false;
break;
case MG_Config::QuirkOverride::ForceOff:
MGLOG_I("DirectGLES: MOBILEGL_POINT_SIZE_DEMOTION=0 - keeping the built-in and the "
"plain declines regardless of the driver's extensions");
m_dynamicParameters.SupportsTessellationPointSize = true;
m_dynamicParameters.SupportsGeometryPointSize = true;
break;
case MG_Config::QuirkOverride::Auto:
break;
}
m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers;
m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments;
m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances;
// The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which
// is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler
// would reject.
m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances;
m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances;
m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports;
// Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX
// for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test
// devices. That is not a shortfall being hidden: without the extension only viewport 0 is
// ever rasterized, so no vertex "selects" a viewport index and naming a convention would
// describe behaviour this backend does not implement.
m_dynamicParameters.LayerProvokingVertex = m_GLESCapabilities.LayerProvokingVertex;
m_dynamicParameters.ViewportIndexProvokingVertex = m_GLESCapabilities.ViewportIndexProvokingVertex;
m_dynamicParameters.MaxViewportWidth = m_GLESCapabilities.MaxViewportWidth;
m_dynamicParameters.MaxViewportHeight = m_GLESCapabilities.MaxViewportHeight;
m_dynamicParameters.ViewportBoundsRangeMin = m_GLESCapabilities.ViewportBoundsRangeMin;
@@ -18,52 +18,20 @@ namespace MobileGL::MG_Backend::DirectGLES {
const MG_External::GLESCapabilities& capabilities,
FormatCapabilityCache& cache);
// Clamps a requested sample count down to what the ES driver can really deliver for this
// format on this format-capability target: the probed per-format list when there is one, the
// driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against
// the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a
// driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES
// allocation call has to come through here. The shadow state keeps the requested count, so
// GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for.
Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat,
Int samples);
class BackendObject_DirectGLES : public BackendObject {
public:
~BackendObject_DirectGLES() override;
// The driver's verdict on this session's context (DirectGLES::LatchIfDeviceLost).
Bool LatchIfGpuFaulted() override;
void Initialize() override;
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
// the image's EGLImage, ordered after the image's pending reads and published as a native
// fence without a CPU wait (DirectGLES.h, SharedImageImpl).
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content,
// so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update.
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: each window surface on it swaps its native EGL
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -79,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;
@@ -114,12 +68,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
// non-zero indirect baseInstance semantics 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);
Bool nonZeroIndirectBaseInstanceSupported);
// Format: <OpenGL ES Renderer>, OpenGL ES <Major>.<Minor> — the exact string an
// initialized backend returns from GetBackendAPIVersionString (and that ends up
@@ -1,23 +0,0 @@
// MobileGL - DirectGLES context-owned static state identities (P5f fs).
#pragma once
#if MOBILEGL_BUILD_DISAGGREGATED
#include <Config.h>
#include <MG_Pipe/PipeApply.h>
namespace MobileGL::MG_Backend::DirectGLES {
extern Uint g_backendContextGeneration;
struct ContextEpoch {
Uint Native = 0;
Uint64 Served = 0;
Bool Server = false;
Bool operator==(const ContextEpoch&) const = default;
};
inline ContextEpoch CurrentContextEpoch() {
const Bool server = MG_Config::Transport != MG_Config::TransportMode::Monolith;
return {g_backendContextGeneration, server ? MG_Pipe::MGPipeApplierContextSerial() : 0, server};
}
}
#endif
File diff suppressed because it is too large Load Diff
+4 -349
View File
@@ -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);
@@ -92,9 +76,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
GLsizei height, GLint border);
void CopyTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width,
GLsizei height);
void CopyImageSubData(const CopyImageEndpoint& src,
void CopyImageSubData(const SharedPtr<MG_State::GLState::ITextureObject>& srcTexture,
GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ,
const CopyImageEndpoint& dst,
const SharedPtr<MG_State::GLState::ITextureObject>& dstTexture,
GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ,
GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth);
void GenerateMipmap(GLenum target);
@@ -108,75 +92,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), before a readback,
// and by the apply loop each time it wakes (LatchIfGpuFaulted), never per draw. The contexts ask
// for reset notification (NativeContextsNotifyResets) so the driver can name the GUILTY one;
// only GUILTY (or a reset of unknown cause) ends the session - an INNOCENT report, a context
// whose work merely waited behind another's hang (the compositor's among them), is only noted.
// `debugKnob` false: the knob is not consulted here (the on-screen Present, so an injected loss
// lands on an offscreen client's shared-image present or readback, never on the compositor's).
Bool LatchIfDeviceLost(const char* site, Bool debugKnob = true);
// The apply loop's form of it (BackendObject::LatchIfGpuFaulted), asked each time the apply thread
// wakes: the driver's report only, no debug knob, and no EGL ground-truth verification spent.
Bool LatchIfGuiltyBeforeApply();
#endif
// True once the process's native contexts were created with EGL_LOSE_CONTEXT_ON_RESET (decided
// by the first one; the driver must offer EGL_EXT_create_context_robustness).
Bool NativeContextsNotifyResets();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
@@ -255,210 +180,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
@@ -502,77 +226,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);
// YUV IMAGES (NV12/P010, MG_Remote/Server/SharedImageYuv.h) are never a name's level 0: the
// name keeps its own RGBA8 storage `target` (width x height), into which the image is
// converted at its first use in each of the calling session's frames - the image bound as
// an external texture, one triangle drawn - after the registry's CPU copy for a foreign
// buffer. Every touched binding is put back. Called after AcquireForSampling.
Bool IsYuv(const EglImageRef& image);
void ConvertYuvForSampling(const EglImageRef& image, GLuint target, Uint32 width, Uint32 height);
// The calling session's frame boundary for what it has read: hands a fence for everything
// the current context has submitted to every image noted since the last boundary. Called
// at Present and wherever the current native context stops being current (its reads can
// only be fenced from it). Nothing noted = nothing done.
void PublishPendingReads();
// IMPLICIT SYNC BY FLUSH (BackendObject::PublishSharedImageAccesses): everything the
// calling session used since its last boundary is published as written and read by one
// fence, and the session's first use of an image in each frame from now on also waits for
// the image's pending reads.
Bool PublishPendingAccesses();
// EGL_ANDROID_native_fence_sync's fence command (BackendObject::ExportNativeFence): a
// sync_file of everything the current context submitted in `*fence` (-1: none, and the work
// is waited out here), and - for a session that writes images - the boundary
// PublishPendingAccesses makes.
Bool ExportNativeFence(int* fence);
} // namespace SharedImageImpl
#endif
extern MG_External::EGLFunctionsTable g_EGLFuncs;
extern MG_External::GLESFunctionsTable g_GLESFuncs;
extern MG_External::GLESCapabilities g_GLESCapabilities;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+73 -340
View File
@@ -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;
@@ -495,20 +275,10 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// 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) {
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
@@ -564,18 +334,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;
@@ -618,7 +383,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
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 +402,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,18 +414,14 @@ 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);
@@ -681,10 +442,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
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);
@@ -709,30 +468,27 @@ 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 "
"buffer; skipping the batch",
@@ -745,8 +501,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;
}
@@ -760,10 +515,8 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
// 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);
@@ -914,18 +667,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 +704,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,17 +844,10 @@ 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
@@ -1132,9 +870,7 @@ 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;
}
@@ -1143,8 +879,7 @@ void main() {
// 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, feedBaseVertex, hasIndexBuffer);
Bool drawn = false;
switch (tier) {
@@ -1176,10 +911,8 @@ 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) {
// entry points can receive.
if (!drawn) {
drawn = RunBaseVertexLoop(mode, count, type, indices, drawcount, basevertex, feedDrawID, feedBaseVertex);
}
if (!drawn) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12 -340
View File
@@ -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
@@ -285,32 +154,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
@@ -325,115 +168,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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.
// SplitReadWriteImageUniforms); the suffix is the image's own 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).
@@ -445,74 +182,15 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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
// Restores a legal declaration:
// * loaded only -> add `readonly`
// * stored only -> add `writeonly`
// * 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.
// same type, `readonly <name>` and `writeonly
// <IMAGE_WRITE_ALIAS_PREFIX><name>`, and point every
// imageStore at the second one. 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.
//
// 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
@@ -522,14 +200,8 @@ namespace MobileGL::MG_Backend::DirectGLES {
//
// 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);
// site in Managers.cpp.
String SplitReadWriteImageUniforms(const String& glslCode);
// 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_";
@@ -545,7 +217,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// 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).
// into a constant LOD crashes the driver (MOBILEGL_AVOID_EXPLICIT_LOD_BIAS).
String EmulateTextureLodBias(const String& glslCode, Bool avoidExplicitLodBias = false);
} // namespace PrgramImpl
@@ -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,8 @@
#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 +328,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 +340,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,48 +353,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()));
}
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
#else
return false;
#endif
}
void BackendObject_DirectVulkan::Initialize() {
#if MOBILEGL_BUILD_DISAGGREGATED
VkBufferManager::RegisterWireResourceOps();
#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;
}
@@ -464,8 +384,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
PopulateFormatCapabilities(physicalDevice.handle, vkGetPhysicalDeviceFormatProperties, m_vulkanCaps,
MutableFormatCapabilities());
@@ -505,22 +425,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);
}
@@ -538,33 +442,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) {
@@ -576,56 +454,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
}
Bool BackendObject_DirectVulkan::PublishSharedImageAccesses() {
#if MOBILEGL_BUILD_DISAGGREGATED
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return pVulkanRenderer ? pVulkanRenderer->PublishSharedImageAccesses() : true;
#else
return true;
#endif
}
Bool BackendObject_DirectVulkan::ExportNativeFence(int* fence) {
*fence = -1;
#if MOBILEGL_BUILD_DISAGGREGATED
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return pVulkanRenderer ? pVulkanRenderer->ExportNativeFence(fence) : true;
#else
return true;
#endif
}
// A swapchain image keeps its content through a present whatever region is drawn next: the
// damage region the client may declare is not needed here.
Int32 BackendObject_DirectVulkan::QueryCurrentBufferAge(Bool damageRegionFollows) {
(void)damageRegionFollows;
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return pVulkanRenderer ? pVulkanRenderer->CurrentDrawBufferAge() : 0;
}
void BackendObject_DirectVulkan::SetEGLSwapInterval(Int interval) {
// Same lock as SwapEGLBuffers: the renderer reads the request in Present.
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::SetEGLSwapInterval(interval);
}
void BackendObject_DirectVulkan::ReleaseEGLSurface(EGLSurface surface) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
BackendObject::ReleaseEGLSurface(surface);
@@ -640,131 +468,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();
@@ -791,11 +499,11 @@ 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),
.Extensions = BuildAdvertisedExtensions(false, false, false, false),
.IsCompatibilityProfile = false},
.StaticBackendCapability = {.AllowVSOnlyPrograms = false}};
return rendererInfo;
@@ -803,22 +511,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<GLExtension> BuildAdvertisedExtensions(Bool shaderSubgroupSupported, Bool timerQueriesSupported,
Bool anisotropicFilteringSupported,
Bool nonZeroIndirectBaseInstanceSupported,
Bool cubeMapArraySupported) {
Bool nonZeroIndirectBaseInstanceSupported) {
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,
@@ -837,79 +532,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
@@ -921,7 +543,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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
@@ -939,13 +561,12 @@ 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.
// GL_ARB_gpu_shader_fp64 is opt-in (MOBILEGL_ADVERTISE_FP64). Every `double` in a
// shader compiles and runs already - it is narrowed to 32 bits before the module
// reaches this backend - so an application that simply uses doubles needs nothing
// advertised. What the extension additionally promises is 64-bit PRECISION, which no
// mobile GPU has and the narrowing cannot fake, so advertising it by default would
// make an application that checks the string take a path MobileGL cannot honour.
if (MG_Config::Features.AdvertiseFp64) {
extensions.push_back(E_GL_ARB_gpu_shader_fp64);
}
@@ -962,18 +583,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;
}
@@ -993,7 +602,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;
@@ -1038,6 +646,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;
@@ -1081,8 +691,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_vulkanCaps = capabilities;
UpdateDynamicBackendParameters();
UpdateAdvertisedExtensions();
if (MGB_CTX_LIVE) {
MGB_CTX->InvalidateCompileEnv();
if (MG_State::pGLContext) {
MG_State::pGLContext->InvalidateCompileEnv();
}
MutableFormatCapabilities().Clear();
}
@@ -1094,17 +704,10 @@ 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(),
m_vulkanCaps.SupportsShaderSubgroup, pVulkanRenderer && pVulkanRenderer->IsTimerQuerySupported(),
pVulkanRenderer && pVulkanRenderer->IsSamplerAnisotropySupported(),
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported(),
m_vulkanCaps.SupportsImageCubeArray);
pVulkanRenderer && pVulkanRenderer->IsNonZeroIndirectBaseInstanceSupported());
}
void BackendObject_DirectVulkan::UpdateDynamicBackendParameters() {
@@ -1152,7 +755,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)
@@ -1235,50 +837,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
clampLimit("GL_MAX_COMPUTE_UNIFORM_BLOCKS", m_vulkanCaps.MaxComputeUniformBlocks,
kMaxAdvertisedBufferBlocks);
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.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment;
@@ -1305,35 +866,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;
@@ -1378,59 +912,26 @@ 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.
// Never, on any device, and no longer for the reason it used to be. It used to track
// shaderFloat64 because a `dvec3` input needed the Float64 capability 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.
// The shader half of that is gone: every 64-bit float is narrowed before any module
// reaches a backend (ShaderTranspiler::DemoteFloat64Pass), so there is no `double` input
// left to bitcast INTO, and feeding a UINT-formatted attribute to what is now a `float`
// input would be silent garbage. Reconstructing the value would mean decoding the
// IEEE-754 double bit pattern in the shader - software fp64, which is precisely what the
// demotion exists to avoid - and on Espryt it would additionally need the ES driver to
// fetch 2N uint components where the application declared N doubles, which a dvec3 or
// dvec4 cannot even express within one attribute location.
//
// 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.
// So glVertexAttribLFormat / glVertexAttribLPointer are declined here exactly as they
// already were on Espryt and on every real mobile device (Adreno and Mali both report
// shaderFloat64 == VK_FALSE), and for the same visible reason. A `dvec3` INPUT still
// compiles and draws - it is a `vec3` after demotion - as long as the application feeds
// it with glVertexAttribPointer(GL_FLOAT) rather than 64-bit data.
m_dynamicParameters.SupportsFloat64VertexAttributes = false;
m_dynamicParameters.MaxShaderStorageBlockSize =
std::min(m_vulkanCaps.MaxShaderStorageBlockSize, kMaxAdvertisedShaderStorageBlockSize);
@@ -1440,18 +941,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,10 +26,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~BackendObject_DirectVulkan() override;
void Initialize() override;
void BindSessionStateToThisThread() override;
// The GPU hang watch's verdict on this session's device (VulkanRenderer::LatchIfGpuHung).
Bool LatchIfGpuFaulted() override;
void OnClientContextBound(Uint64 token) override;
Bool InitWindowSurface() override;
Bool InitCapabilities() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
@@ -39,22 +34,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
void SetEGLSwapInterval(Int interval) override;
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// The active surface target's swapchain image ages (VulkanRenderer::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// A glFlush that ends a shared-image producer's frame (VulkanRenderer::PublishSharedImageAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (VulkanRenderer::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: the window surface's target (VkSurfaceKHR,
// swapchain) is destroyed and a pbuffer target of the same extent stands in for it, and back.
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -66,11 +47,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void OnEGLSurfaceForgotten(EGLSurface surface) override;
// A served session keeps one renderer across all its client's surfaces and switches the
// renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget).
Bool UsesSurfaceTargets() const;
static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast<Uint64>(surface); }
void UpdateAdvertisedExtensions();
void UpdateDynamicBackendParameters();
@@ -78,21 +54,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
// A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its
// apply thread. Null in the monolith and every other single-session shape.
UniquePtr<MagmaSession> m_magmaSession;
// The binding each client context was last made current with, for OnClientContextBound.
UnorderedMap<EGLContext, EGLCurrentState> m_contextBindings;
// Window surfaces whose window is gone (SuspendServerWindow), with the extent their
// placeholder pbuffer target is built at - the window's, so the default framebuffer the
// client knows keeps its size.
struct SuspendedWindowSurface {
Uint32 Width = 1;
Uint32 Height = 1;
};
UnorderedMap<EGLSurface, SuspendedWindowSurface> m_suspendedWindowSurfaces;
// The placeholder pbuffer target of a suspended surface, made active. False: it could not be built.
Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent);
};
// Single-source-of-truth helpers shared with the driver POST
@@ -109,13 +70,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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 nonZeroIndirectBaseInstanceSupported);
// 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) {
@@ -128,14 +125,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) {
@@ -66,11 +63,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
if (!m_spareCommandBuffers.empty())
vkFreeCommandBuffers(device, commandPool, static_cast<Uint32>(m_spareCommandBuffers.size()),
m_spareCommandBuffers.data());
}
m_spareCommandBuffers.clear();
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
@@ -224,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;
@@ -242,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) {
@@ -304,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();
@@ -320,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,
@@ -366,16 +334,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
VkResult result = TakeCommandBuffer(replacement);
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
if (result != VK_SUCCESS) {
return result;
}
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = TakeCommandBuffer(preReplacement);
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
if (result != VK_SUCCESS) {
RecycleCommandBuffer(replacement);
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
@@ -410,7 +383,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
RecycleCommandBuffer(frame.retiredCommandBuffers[completedCount].commandBuffer);
vkFreeCommandBuffers(m_device, m_commandPool, 1,
&frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
@@ -420,31 +394,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkResult FrameContext::TakeCommandBuffer(VkCommandBuffer& out) {
if (!m_spareCommandBuffers.empty()) {
out = m_spareCommandBuffers.back();
m_spareCommandBuffers.pop_back();
return VK_SUCCESS;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
return vkAllocateCommandBuffers(m_device, &allocInfo, &out);
}
void FrameContext::RecycleCommandBuffer(VkCommandBuffer commandBuffer) {
// A handful covers the buffers in flight between two completions; beyond that, free.
constexpr SizeT kSpareCommandBuffers = 8;
if (commandBuffer == VK_NULL_HANDLE) return;
if (m_spareCommandBuffers.size() < kSpareCommandBuffers) {
m_spareCommandBuffers.push_back(commandBuffer);
return;
}
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
@@ -101,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);
@@ -150,10 +143,5 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
// Completed command buffers kept for the next retirement instead of being freed and
// reallocated: the pool resets a buffer implicitly at its next vkBeginCommandBuffer.
Vector<VkCommandBuffer> m_spareCommandBuffers;
VkResult TakeCommandBuffer(VkCommandBuffer& out);
void RecycleCommandBuffer(VkCommandBuffer commandBuffer);
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,202 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GpuProgressMarkers.h"
#include <MG_Util/Debug/Log.h>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace Watch = MG_Backend::GpuHangWatch;
void GpuProgressMarkers::Init(VkDevice device, Uint32 queueFamilyIndex) {
if (m_device != VK_NULL_HANDLE || device == VK_NULL_HANDLE) return;
if (Watch::BudgetMs() == 0) {
MGLOG_I("Magma: GPU hang watch off (MOBILEGL_GPU_HANG_BUDGET_MS=0)");
return;
}
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.queueFamilyIndex = queueFamilyIndex;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &m_pool) != VK_SUCCESS) {
MGLOG_W("Magma: GPU hang watch off - no command pool for its markers");
m_pool = VK_NULL_HANDLE;
return;
}
m_device = device;
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!AddSlot()) {
MGLOG_W("Magma: GPU hang watch off - the device cannot make its progress markers");
vkDestroyCommandPool(m_device, m_pool, nullptr);
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
return;
}
}
m_watch = Watch::Register(this, &Sample, &OnHung);
MGLOG_I("Magma: GPU hang watch on - a submission the GPU started and has not finished after %u ms ends "
"its session",
Watch::BudgetMs());
}
void GpuProgressMarkers::Shutdown() {
// The watch first, outside m_mutex: Unregister waits out a sample in progress, which takes it.
Watch::Unregister(m_watch);
m_watch = 0;
if (m_device == VK_NULL_HANDLE) return;
const std::lock_guard<std::mutex> lock(m_mutex);
for (Slot& slot : m_slots) {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
}
m_slots.clear();
if (m_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, m_pool, nullptr); // frees the buffers
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
}
Bool GpuProgressMarkers::AddSlot() {
if (m_slots.size() >= kMaxSlots) return false;
Slot slot;
const VkEventCreateInfo eventInfo{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO};
VkCommandBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocInfo.commandPool = m_pool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 2;
const auto fail = [&] {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
if (buffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(m_device, m_pool, 2, buffers);
return false;
};
if (vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Started) != VK_SUCCESS ||
vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Finished) != VK_SUCCESS ||
vkAllocateCommandBuffers(m_device, &allocInfo, buffers) != VK_SUCCESS)
return fail();
// Prerecorded once and resubmitted for every submission the slot brackets; SIMULTANEOUS_USE
// because the slot is reused once its `finished` event is set, which can be a moment before
// the submission that set it formally leaves the pending state.
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
if (vkBeginCommandBuffer(buffers[0], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[0], slot.Started, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
if (vkEndCommandBuffer(buffers[0]) != VK_SUCCESS) return fail();
if (vkBeginCommandBuffer(buffers[1], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[1], slot.Finished, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
if (vkEndCommandBuffer(buffers[1]) != VK_SUCCESS) return fail();
slot.Begin = buffers[0];
slot.End = buffers[1];
m_slots.push_back(slot);
return true;
}
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire() {
Bracket bracket;
if (m_device == VK_NULL_HANDLE) return bracket;
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_lost) return bracket;
Slot* free = nullptr;
for (Slot& slot : m_slots) {
if (slot.Claimed) continue;
// Retire what finished since the watch last looked: frames come faster than its polls.
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
if (!slot.InFlight && free == nullptr) free = &slot;
}
if (free == nullptr) {
if (!AddSlot()) return bracket;
free = &m_slots.back();
}
if (free->Dirty) {
if (vkResetEvent(m_device, free->Started) != VK_SUCCESS ||
vkResetEvent(m_device, free->Finished) != VK_SUCCESS)
return bracket;
free->Dirty = false;
}
free->Claimed = true;
bracket.Begin = free->Begin;
bracket.End = free->End;
bracket.Slot = static_cast<Uint32>(free - m_slots.data());
return bracket;
}
void GpuProgressMarkers::Submitted(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
Slot& slot = m_slots[bracket.Slot];
slot.Claimed = false;
slot.InFlight = true;
slot.Dirty = true;
slot.Id = m_nextId++;
}
void GpuProgressMarkers::Abandon(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
// A failed vkQueueSubmit may still have consumed nothing or something: reset before reuse.
m_slots[bracket.Slot].Claimed = false;
m_slots[bracket.Slot].Dirty = true;
}
Bool GpuProgressMarkers::Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
const std::lock_guard<std::mutex> lock(markers->m_mutex);
if (markers->m_lost || markers->m_device == VK_NULL_HANDLE) return false;
for (Slot& slot : markers->m_slots) {
if (!slot.InFlight) continue;
const VkResult finished = vkGetEventStatus(markers->m_device, slot.Finished);
const VkResult started = vkGetEventStatus(markers->m_device, slot.Started);
if ((finished != VK_EVENT_SET && finished != VK_EVENT_RESET) ||
(started != VK_EVENT_SET && started != VK_EVENT_RESET)) {
// VK_ERROR_DEVICE_LOST: the driver already reset the device and says so itself.
markers->m_lost = true;
return false;
}
if (finished == VK_EVENT_SET) {
slot.InFlight = false;
continue;
}
out.push_back({slot.Id, started == VK_EVENT_SET, false});
}
return true;
}
void GpuProgressMarkers::OnHung(void* self, Uint64 submissionId, Uint64 runningMs) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
markers->m_hungRunningMs.store(runningMs, std::memory_order_release);
markers->m_hung.store(true, std::memory_order_release);
MGLOG_E("Magma: submission %llu of this session's VkDevice has run %llu ms on the GPU and has not finished - "
"it is what stops the GPU for every process; the session ends at its next record and submits "
"nothing more (budget %u ms, MOBILEGL_GPU_HANG_BUDGET_MS)",
static_cast<unsigned long long>(submissionId), static_cast<unsigned long long>(runningMs),
MG_Backend::GpuHangWatch::BudgetMs());
}
VkResult GpuProgressMarkers::SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence,
const Bracket& bracket) {
constexpr Uint32 kMaxOwn = 6;
if (!bracket.Valid() || info.commandBufferCount == 0 || info.commandBufferCount > kMaxOwn)
return vkQueueSubmit(queue, 1, &info, fence);
VkCommandBuffer buffers[kMaxOwn + 2];
Uint32 count = 0;
buffers[count++] = bracket.Begin;
for (Uint32 i = 0; i < info.commandBufferCount; ++i) buffers[count++] = info.pCommandBuffers[i];
buffers[count++] = bracket.End;
const VkCommandBuffer* own = info.pCommandBuffers;
const Uint32 ownCount = info.commandBufferCount;
info.pCommandBuffers = buffers;
info.commandBufferCount = count;
const VkResult result = vkQueueSubmit(queue, 1, &info, fence);
info.pCommandBuffers = own;
info.commandBufferCount = ownCount;
return result;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,95 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "../VkIncludes.h"
#include <Includes.h>
#include <MG_Backend/GpuHangWatch.h>
#include <atomic>
#include <mutex>
namespace MobileGL::MG_Backend::DirectVulkan {
// WHICH OF THIS DEVICE'S SUBMISSIONS THE GPU HAS STARTED, AND WHICH IT HAS FINISHED
// (GpuHangWatch.h says why that is the whole attribution of a GPU hang).
//
// A frame's submission is bracketed by two prerecorded one-command buffers: the first sets a
// `started` event at TOP_OF_PIPE, the last sets a `finished` event once everything before it in
// the queue has completed. The host reads both with vkGetEventStatus from the watch thread - no
// fence is shared with the renderer's own waits and resets. On Adreno 750 a submission whose
// fragment shader never ends reads started=SET from its first poll until the kernel resets the
// GPU, and a submission queued behind it reads started=RESET for the whole stall.
//
// The renderer's thread brackets submissions (Acquire / Submitted / Abandon) and asks Hung(); the
// watch thread samples. Everything Vulkan the two share - the events - is touched under m_mutex.
class GpuProgressMarkers {
public:
struct Bracket {
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint32 Slot = ~0u;
Bool Valid() const { return Slot != ~0u; }
};
GpuProgressMarkers() = default;
~GpuProgressMarkers() { Shutdown(); }
GpuProgressMarkers(const GpuProgressMarkers&) = delete;
GpuProgressMarkers& operator=(const GpuProgressMarkers&) = delete;
// Off (no brackets, never hung) when the budget is 0 or the device cannot make events.
void Init(VkDevice device, Uint32 queueFamilyIndex);
// Before the device goes; its queue must be idle. Stops the watch first.
void Shutdown();
// The two buffers to put first and last in one submission (an invalid bracket when off or
// when every slot is in flight: the submission then goes unwatched).
Bracket Acquire();
// The submission carrying `bracket` was accepted by vkQueueSubmit / was not submitted.
void Submitted(const Bracket& bracket);
void Abandon(const Bracket& bracket);
// The watch named one of this device's submissions as hanging the GPU.
Bool Hung() const { return m_hung.load(std::memory_order_acquire); }
Uint64 HungRunningMs() const { return m_hungRunningMs.load(std::memory_order_acquire); }
Bool Active() const { return m_watch != 0; }
// vkQueueSubmit of `info` with `bracket` wrapped around its command buffers (which it puts
// back before returning). At most 6 buffers of the caller's.
static VkResult SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence, const Bracket& bracket);
private:
struct Slot {
VkEvent Started = VK_NULL_HANDLE;
VkEvent Finished = VK_NULL_HANDLE;
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint64 Id = 0;
Bool InFlight = false; // submitted and not yet seen finished
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
Bool Dirty = false; // its events were set by a past submission: reset before reuse
};
static constexpr Uint32 kMaxSlots = 16;
Bool AddSlot();
static Bool Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out);
static void OnHung(void* self, Uint64 submissionId, Uint64 runningMs);
std::mutex m_mutex;
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_pool = VK_NULL_HANDLE;
Vector<Slot> m_slots;
Uint64 m_nextId = 1;
Uint64 m_watch = 0;
Bool m_lost = false;
std::atomic<Bool> m_hung{false};
std::atomic<Uint64> m_hungRunningMs{0};
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,611 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/MagmaPipeArms.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include <Includes.h>
#include <Config.h>
#if MOBILEGL_PIPE_PUSH
// kMGPipeSubsystem* - the runtime bitmask's named bits - and MGPipeHandle itself. Both are
// header-only constant/POD declarations, and both are push-only, so the pull build's include
// graph is unchanged (G1).
#include <MG_Pipe/MGPipe.h>
#include <MG_Pipe/MGPipeHandles.h>
#endif
#include <cstdlib>
// Magma's arm selector for the P2 Track H / render-state re-keys (P2 brief D14), and the
// {slot, gen} mint the re-keyed sites are written against.
//
// Two switches decide which arm a re-keyed site runs, and they are NOT the same switch:
//
// MOBILEGL_PIPE_PUSH (compile) - is the pushed state there to be keyed on at all
// Features.PipePush (runtime bitmask) - is THIS subsystem migrated in THIS run
// MOBILEGL_PIPE_LEGACY_MEMOS (compile) - is the pre-handle arm compiled beside it
// Features.PipeLegacyMemos (runtime) - may the pre-handle arm be ENTERED in this run
//
// ARCHITECTURE.md 9.6's point: once a handle wave lands, a clear MOBILEGL_PIPE_PUSH bit is
// only a valid A/B while the legacy arm is still compiled, because with the bit clear the
// backend would otherwise still run the re-keyed code. So a clear bit selects the legacy
// arm, and a run that has explicitly disabled the legacy arm may not fall into it.
//
// D14 spends that last sentence at STARTUP, not per draw: "a Track-H subsystem whose bit is
// clear is a startup Fatal{PipeLegacyMemosDisabled}". Nothing in the draw path aborts, and
// nothing outside Track H consults the legacy-memo lever at all - see
// MagmaPipeValidateSubsystemConfiguration below for both halves of that rule.
//
// The whole header is inert in a pull build: MOBILEGL_PIPE_PUSH is 0 there, every helper
// below is behind it, and the pull build's translation units are byte-identical (G1).
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_PIPE_PUSH
// Is `subsystemBit` (MG_Pipe/MGPipe.h's kMGPipeSubsystem*) migrated in this run?
inline Bool MagmaPipeSubsystemOn(Uint64 subsystemBit) {
return (MG_Config::Features.PipePush & subsystemBit) != 0;
}
// ---------------------------------------------------------------------------------
// D14's startup gate
// ---------------------------------------------------------------------------------
//
// Called once from VulkanRenderer::Initialize(), i.e. only when Magma is the backend
// that is actually running. It answers exactly one question and it answers it before the
// first draw: is there an arm for Magma's Track-H subsystem in this configuration?
//
// Three deliberate boundaries, each of which the per-draw shape this replaces got wrong:
//
// * ONLY Magma's own Track-H bit is checked. Espryt's bit 5 is Espryt's business (a
// DirectVulkan run does not execute one line of DirectGLES' re-key), so
// MOBILEGL_PIPE_PUSH=0x20 must not kill a Magma run, and MOBILEGL_PIPE_PUSH=0x40 must
// not kill an Espryt one.
// * bit 0 (kMGPipeSubsystemRenderState) is NOT Track H and is NOT fatal. It is not a
// memo re-key at all: it decides where the pipeline memo's STATE KEY comes from, and
// a clear bit there simply means the client is not pushing render-state CSOs in this
// run, which GetOrCreatePipeline answers with its own state hash. D14 labels bits 5
// and 6 "Track H" and labels bit 0 nothing of the sort.
// * it is Fatal at STARTUP, once, not on a draw. A per-draw abort inside
// GetOrCreatePipeline turns a configuration mistake into a mid-frame crash and puts a
// branch nobody needs on the hottest path in the backend.
//
// [declared deviation from D14, review v2 minor 2] D14's runtime row reads "false: the
// legacy arm is never entered", and D14's compile-switch row names ComputePipelineStateHash
// as part of the pre-handle arm. Those two together would make MOBILEGL_PIPE_LEGACY_MEMOS=0
// with bit 0 CLEAR a contradiction: the pipeline memo has no CSO handle to key on, so it
// keys on a state hash, and in a build that compiles the pre-handle arm that hash IS
// ComputePipelineStateHash. Magma does not make that fatal - bit 0 is not Track H, and
// there is a correct answer (the state hash) where for bits 5/6 there is none - but it no
// longer does it SILENTLY: the combination is named once, at startup, right here.
inline void MagmaPipeValidateSubsystemConfiguration() {
if (!MG_Config::Features.PipeLegacyMemos &&
!MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemRenderState)) {
MGLOG_W("MGPipe: MOBILEGL_PIPE_LEGACY_MEMOS=0 with kMGPipeSubsystemRenderState (bit 0 "
"of MOBILEGL_PIPE_PUSH) clear - Magma's pipeline memo has no CSO handle to key "
"on, so every draw whose pipeline-state version moved runs the pre-handle STATE "
"HASH instead. That is not a Track-H subsystem and not fatal, but it is not the "
"handle arm either: set bit 0 (MOBILEGL_PIPE_PUSH=0x%llx) if this run was meant "
"to measure it.",
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemRenderState));
}
#if MOBILEGL_PIPE_LEGACY_MEMOS
// The pre-handle arm is compiled AND the operator has not forbidden entering it, so a
// clear bit is an ordinary, valid A/B: the site takes the legacy arm.
if (MG_Config::Features.PipeLegacyMemos) return;
#endif
if (MagmaPipeSubsystemOn(MG_Pipe::kMGPipeSubsystemMagmaVertexInput)) return;
#if MOBILEGL_PIPE_LEGACY_MEMOS
const char* const why = "this run has MOBILEGL_PIPE_LEGACY_MEMOS=0";
#else
const char* const why =
"this build has cmake -DMOBILEGL_PIPE_LEGACY_MEMOS=OFF, which compiles no such arm";
#endif
MGLOG_F("MGPipe: Fatal{PipeLegacyMemosDisabled} Magma's Track-H subsystem "
"(kMGPipeSubsystemMagmaVertexInput, bit 6 of MOBILEGL_PIPE_PUSH) is clear, so the "
"vertex-input cache and the VAO draw memo want the pre-handle arm - but %s. Set "
"bit 6 (MOBILEGL_PIPE_PUSH=0x%llx, or the default 0x%llx), or allow the legacy arm.",
why,
static_cast<unsigned long long>(MG_Config::Features.PipePush |
MG_Pipe::kMGPipeSubsystemMagmaVertexInput),
static_cast<unsigned long long>(MG_Pipe::kMGPipeSubsystemsMigratedAtP2));
std::abort();
}
// "Does this Track-H site run the handle arm?" - the ONE question every re-keyed Track-H
// site asks, so that they cannot disagree with each other or with the startup gate.
inline Bool MagmaPipeTrackHArmIsHandles(Uint64 trackHBit) {
#if MOBILEGL_PIPE_LEGACY_MEMOS
return MagmaPipeSubsystemOn(trackHBit);
#else
// No pre-handle arm exists in this build, and MagmaPipeValidateSubsystemConfiguration
// has already made a clear bit a startup Fatal, so the handle arm is the only arm a
// running process can be on.
(void)trackHBit;
return true;
#endif
}
// ---------------------------------------------------------------------------------
// Negative control C (P2 brief D18): MOBILEGL_PIPE_HANDLE_ABA_CONTROL
// ---------------------------------------------------------------------------------
//
// "Is the object-identity half of every vertex-input memo key deliberately defeated in
// this run?" - the ONE question the control's sites ask, for the same reason
// MagmaPipeTrackHArmIsHandles exists: three sites deciding separately could disagree,
// and a control that defeats two of three guards proves nothing.
//
// WHAT IT DEFEATS, AND WHY IT IS SPELLED AS "REPLACE THE IDENTITY WITH A CONSTANT"
// RATHER THAN "USE THE HEAP ADDRESS".
//
// D18 wrote the control as "hash attr.Buffer.get() instead of GetLifetimeId(), and skip
// the vaoLifetimeId compare", on the theory that a deleted object's replacement lands at
// the freed heap block and so reproduces the key. Measured, it does not: in
// HandleRecycleScenario the GL NAMES come back (glGen* hands the deleted name straight
// out) but the C++ heap blocks do not - a VertexArrayObject is 3920 bytes, too large for
// glibc's tcache, so its chunk goes to the unsorted bin and is split by the very next
// allocation the replacement path makes. Four create/delete cycles in one run produced
// four distinct addresses, ~1 MiB apart. With no address reuse there is nothing for
// "hash the address" to collide with: the replacement hashes differently, indexes a
// different memo slot, and inherits nothing - so the arm asserted stale pixels and saw
// fresh ones, which is a FAILING negative control that had stopped controlling anything.
//
// So the control no longer asks the allocator for the collision; it manufactures it. On
// both arms the object identity is replaced by a constant, which is the strongest form of
// "the allocator handed the block back" and is deterministic. That covers strictly more
// than D18's spelling, and in particular it reaches the arm P2 SHIPS: on the handle arm
// the constant defeats the OBJECT IDENTITY THAT SELECTS THE SLOT - the key the handle arm
// ships - so the replacement VAO is handed the dead one's memo entry and its content hash.
// Defeating only the retired lifetime-id/address guards would leave that key untested,
// which is exactly the vacuity this control exists to catch.
//
// WHAT IT DOES NOT COVER, AND WHY NO REPRODUCER OF THIS SHAPE CAN [fix-aba review v1,
// MAJOR 1]. It does NOT exercise the GENERATION half of {slot, gen}:
//
// * THIS MINT still has no death notification, and that is now a narrower statement than
// it was. P7 wave 2 package C gives DirectVulkan a StateObjectDeathOps table
// (DirectVulkan.cpp's g_magmaStateObjectDeathOps, CONTRACT-P7 §5.5), so
// NotifyStateObjectDestroyed DOES have a consumer on this backend - but that consumer's
// one job is to emit the `object_death` RECORD so the server drops its twin, and it
// never touches these per-renderer identity tables: they are the SERVER-side renderer's
// own, the notice is raised on the CLIENT thread, and an Acquire/retire from the wrong
// side is precisely the cross-role read CONTRACT-P5C §3.1 forbids. So a slot here still
// returns to the free list only through OnFrameBoundary's age sweep (kSweepInterval
// 256, kRetireAgeBoundaries 1024, below), and everything the two bullets below derive
// from that is unchanged;
// * HandleRecycleScenario issues five frame boundaries, so the free list is empty when
// the replacement VAO acquires and it gets a BRAND-NEW slot at Gen 1 (measured:
// redVao slot=2 gen=1, greenVao slot=3 gen=1). The knob-off FRESH verdict there is
// decided by the SLOT alone, and deleting the ++Gen below leaves all four arms green;
// * a genuine slot REUSE needs >= 1024 idle boundaries after the dead object's last
// draw, which necessarily puts the two draws in different frames - and the only memo
// that carries a GPU slice rather than a layout, ResolvedVertexBindings, declines
// across frames by design. The two requirements are mutually exclusive, so the
// generation is out of reach of any same-frame pixel reproducer for this memo.
//
// The generation is covered where it IS expressible, over this mint and the claim rule
// MagmaPipeClaimSlotMemos below: MG_Test/Pipe/MagmaPipeIdentityTest.cpp drives a real
// retire -> reuse and asserts that a memo stamped at {slot, gen=N} is not served at
// {slot, gen=N+1} with the knob off and IS served with it on. Deleting the ++Gen reds that
// suite; it is the only place in the tree where that deletion is caught.
//
// Everything the control does NOT defeat is as load-bearing as what it does. It never
// touches a guard that is not an IDENTITY guard: the resolved-bindings memo's frame
// serial, its slice-epoch compares and its host-map check all stay in force, so a green
// AbaControl arm still means "a replacement object was handed its dead predecessor's
// resolved vertex bindings because the identity halves of the keys were defeated", not
// "every safety net was switched off until something broke".
//
// Off by default (Config.h), set only by the HandleRecycle AbaControl ctest lanes, and
// #if MOBILEGL_PIPE_PUSH throughout, so no shipping pull build can even parse it.
// P4a (BRIEF-P4A.md D-I2, G8): WHICH KINDS THIS ANSWER COVERS, and it is not "all of them".
//
// P4a mints six more client-side kinds - Texture, Renderbuffer, Framebuffer, SamplerCso,
// SamplerViewCso and ShaderCso - and requires the ABA control to defeat "the identity half
// of P4a's memo keys as well", because a control that only defeats the guards a phase
// RETIRED says nothing about the key that phase SHIPS.
//
// On Magma there is no such key to defeat, and that is a fact about the roadmap rather than
// an omission here. MagmaPipeIdentityTables below mints exactly TWO kinds,
// VertexElementsCso and Buffer; a texture, a framebuffer, a sampler, a view and a program
// are all still reached from their frontend objects on this backend, and moving them onto
// handles is P7's work (ROADMAP.md:24 - "Magma anything"; P4a leaves MG_Backend/DirectVulkan
// untouched apart from this file). So the honest statement is per KIND, and it is spelled as
// code rather than as a comment so that a caller cannot read the blanket answer above and
// conclude the knob covers its kind:
//
// * for the two kinds this backend really keys on {slot, gen}, the knob defeats the
// identity exactly as it always has (MagmaPipeClaimSlotMemos);
// * for P4a's six there is nothing here to defeat, so the answer is FALSE - and
// MG_IntegrationTest's HandleRecycleScenario reads that through its own build probe and
// makes those cases' AbaControl arm assert the CORRECT pixels while SAYING that it is
// not controlling anything for that kind. It does not assert a corruption that no code
// on this tree can produce, which would be a permanently red always-on lane.
//
// WHAT MAKES IT TRUE LATER, in one sentence, so the next reader does not have to derive it:
// when a backend grows a Features.PipeHandleAbaControl consumer over its P4a object slot
// tables - one `if` in GetOrCreate / FindByHandle, the shape MagmaPipeClaimSlotMemos already
// has for vertex input - this function's per-kind answer becomes that consumer's, the
// integration probe finds the consumer, and the six cases flip to expecting the corruption.
inline Bool MagmaPipeAbaControlDefeatsIdentity() {
return MG_Config::Features.PipeHandleAbaControl;
}
// WHICH KINDS THIS BACKEND ACTUALLY KEYS ON {slot, gen}, and therefore which kinds the knob
// above has an identity to defeat at all. `kind` is MG_Pipe::MGPipeKind.
//
// EXHAUSTIVE, WITH NO `default:`, for MG_IntegrationTest/Harness/PipeSlotPeek.cpp's reason:
// a kind added to MGPipeKind without a decision here must be a -Wswitch warning in this
// file rather than a row that silently inherits somebody else's answer. Being wrong in the
// "covered" direction is the expensive one - a control asserting a corruption nobody can
// produce is a permanently red always-on lane - so an undecided kind must never read true,
// and with no `default:` there is no arm for it to read true from.
//
// constexpr AND PINNED BY static_assert BELOW, which is what stops it rotting the way a
// predicate with no caller does: MagmaPipeIdentityTables mints exactly two kinds, the
// asserts say so in both directions, and the file no longer compiles if the tables and this
// statement of them ever part company. (Review F-m5: the earlier form had no caller at all
// and could not make anything red or green.)
inline constexpr Bool MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind kind) {
switch (kind) {
// The two MagmaPipeIdentityTables really mints.
case MG_Pipe::MGPipeKind::VertexElementsCso:
case MG_Pipe::MGPipeKind::Buffer:
return true;
// P4a's six object classes: still reached from their frontend objects on this
// backend (Magma's object paths are P7, ROADMAP.md:24), so there is no key here for
// the knob to defeat.
case MG_Pipe::MGPipeKind::Texture:
case MG_Pipe::MGPipeKind::Renderbuffer:
case MG_Pipe::MGPipeKind::Framebuffer:
case MG_Pipe::MGPipeKind::SamplerCso:
case MG_Pipe::MGPipeKind::SamplerViewCso:
case MG_Pipe::MGPipeKind::ShaderCso:
// ...and everything else this backend does not mint a handle for.
case MG_Pipe::MGPipeKind::None:
case MG_Pipe::MGPipeKind::Xfb:
case MG_Pipe::MGPipeKind::RenderStateCso:
case MG_Pipe::MGPipeKind::Fence:
case MG_Pipe::MGPipeKind::Query:
case MG_Pipe::MGPipeKind::Context:
case MG_Pipe::MGPipeKind::KindCount:
return false;
}
return false;
}
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::VertexElementsCso),
"MagmaPipeIdentityTables mints VertexElementsCso: the knob has an identity to "
"defeat for it");
static_assert(MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Buffer),
"MagmaPipeIdentityTables mints Buffer: the knob has an identity to defeat for it");
static_assert(!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Texture) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Renderbuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::Framebuffer) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::SamplerViewCso) &&
!MagmaPipeAbaControlKindIsRekeyedHere(MG_Pipe::MGPipeKind::ShaderCso),
"P4a's six object classes are not keyed on {slot, gen} on this backend, so "
"HandleRecycleScenario's six AbaControl arms must NOT expect a corruption here. "
"Wiring one of them is what flips this assert, this predicate and that arm - and "
"MG_IntegrationTest's two-symbol probe over MG_Backend/DirectVulkan is what "
"carries the answer into the lane");
// THERE IS DELIBERATELY NO PER-KIND WRAPPER HERE, and review F-v2-m3 is why. An earlier
// round carried `MagmaPipeAbaControlCoversKind(kind)` - the conjunction of the two
// statements above - and it had no caller anywhere in the tree: the knob's only two
// consumers (VulkanRenderer.cpp's VAO draw memo and VertexInputStateFactory.cpp's pipeline
// key) each hold ONE kind, VertexElementsCso, by construction, so the kind is not a
// variable at either site. A conjunction no build ever evaluates cannot be pinned the way
// the predicate above is pinned - it is not constexpr, because it reads MG_Config::Features,
// so no static_assert can reach it - which makes it exactly the rot F-m5 was raised about,
// one level up: an `&&` whose operands could be inverted or dropped with nothing to say so.
//
// The two pieces stand alone instead, and each is pinned by something that runs:
// MagmaPipeAbaControlKindIsRekeyedHere is constexpr and asserted in BOTH directions by the
// three static_asserts above, which compile in every Magma build; MagmaPipeAbaControlDefeats
// Identity is the knob, and its two consumers are what make it true or false. A call site
// that ever does hold a variable kind writes the `&&` there, where a build will run it.
// The single consumer-table entry every VAO collapses onto while the control is on. Slot
// 0 is a real, ordinary entry of both tables (MagmaPipeSlotIndex maps the first allocatable
// handle onto it), so nothing about the tables changes shape for the control's sake.
inline constexpr Uint32 kMagmaPipeAbaControlSlotIndex = 0;
// ---------------------------------------------------------------------------------
// The {slot, gen} mint
// ---------------------------------------------------------------------------------
//
// Maps a frontend object's never-reused lifetime id to a dense {slot, gen}. Three
// properties, and the third is the one review v2 got wrong:
//
// 1. exact identity - Gen moves whenever a slot changes owner, so a stale handle can
// never match a live object even if the allocator hands back the same heap address
// (the ABA HandleRecycleScenario reproduces);
// 2. dense slots - the slot IS an index, so a consumer's per-slot table needs no hash,
// no probe and no mix;
// 3. NO CAPACITY CLIFF. A live object's handle never changes while the object is being
// drawn, whatever the working set size.
//
// Property 3 is why this is not the fixed 2-way set-associative LRU the previous round
// shipped. That structure evicted a LIVE object once the working set passed its capacity,
// and every consumer memo keyed on the handle died with it: measured on a verbatim
// transcription, 54% of uses lost their handle at 2500 live VAOs against 2048 entries, and
// 20% at 1024 live VAOs once the lifetime ids are sparse (an app that creates and destroys
// VAOs, which is the Minecraft chunk shape this exists for). Two of the three memos it
// fed - the content-hash memo and the resolved-state memo - had NO capacity before this
// package: they were unbounded mutable fields on VertexArrayObject. Introducing eviction
// there turns one ComputeHash per VAO reconfiguration into one per DRAW, and, once the
// buffer table thrashes too, makes the vertex-input content hash a per-draw value that
// inserts a fresh heap-allocated BackendVertexInputState into an unbounded map on every
// draw. That is a worse leak than the one it was introduced to avoid.
//
// So: grow on demand, and reclaim by AGE instead of by capacity.
//
// * Acquire hits an UnorderedMap<lifetimeId, slotIndex>, in front of which sits a
// one-entry memo. Every re-keyed site in a draw asks about the SAME VAO, so the memo
// turns the five-or-six acquisitions a draw makes into one map probe plus five Uint64
// compares - less than the address multiply plus two-way probe the pre-handle arm ran.
// * OnFrameBoundary retires slots whose object has not been drawn for
// kRetireAgeBoundaries boundaries and returns them to a free list, so the table's
// footprint tracks the LIVE DRAWN working set, not objects ever created. That is the
// property MG_Impl/Pipe/SlotAllocator cannot have here: nothing in P2 can call its
// Free (the tracker emits no object-class state, BufferBackendOps::OnDestroy is handed
// a BackendBufferResource rather than the BufferObject, and VertexArrayObject has no
// death hook at all - adding one is D13's explicit-destroy work, which covers Espryt's
// six kinds, not VertexElementsCso), so an allocator here would grow by one SlotState
// plus one map node per object EVER created, for the life of the process, on a
// platform with an LMK. Age-based reclamation is the stand-in for the death
// notification, and it is exactly as ABA-proof, because reuse bumps Gen.
// * A retire costs at most one memo recompute if the object is drawn again - the same
// price a cache miss costs - and it is charged only to objects that went idle for
// ~1024 frames, never to a hot one.
//
// Memory: one map node plus one 24-byte Entry per live object, i.e. tens of bytes against
// the kilobyte a VertexArrayObject or a BufferObject already costs the frontend. There is
// no capacity to size off a device measurement because there is no capacity; what the
// device run in D.4.2 can still want is the number itself, so the high-water mark is
// logged at MGLOG_D on the allocate-a-new-slot branch (once per new object, never on a
// draw - ROADMAP.md:7).
//
// Single-threaded, like the rest of the renderer. Owned per VulkanRenderer (see
// MagmaPipeIdentityTables): a process-global would share one table, and one reclamation
// clock, across two live contexts.
class MagmaPipeIdentityTable {
public:
explicit MagmaPipeIdentityTable(const char* kindName) : m_kindName(kindName) {}
// Slots ever minted. A consumer table indexed by MagmaPipeSlotIndex() needs this many
// entries; MagmaPipeSlotTable below grows itself, so nobody has to ask.
Uint32 Count() const { return static_cast<Uint32>(m_entries.size()); }
// Objects currently holding a slot - the live working set this table tracks.
Uint32 LiveCount() const { return static_cast<Uint32>(m_index.size()); }
MG_Pipe::MGPipeHandle Acquire(Uint64 lifetimeId) {
// Unreachable: MG_State hands out lifetime ids from 1 precisely so that a
// zero-initialised memo slot cannot carry a live object's id. Guarded anyway so
// that a zero can never be minted into a slot and then indexed with.
if (lifetimeId == 0) return MG_Pipe::kMGPipeNullHandle;
// The one-entry front memo. Cleared by any retire, so it can never serve a slot
// that has been handed back to the free list.
if (lifetimeId == m_lastLifetimeId) {
m_entries[m_lastIndex].LastUse = m_boundary;
return m_lastHandle;
}
Uint32 index = 0;
const auto it = m_index.find(lifetimeId);
if (it != m_index.end()) {
index = it->second;
} else {
index = ClaimSlot();
m_entries[index].LifetimeId = lifetimeId;
m_index.emplace(lifetimeId, index);
}
Entry& entry = m_entries[index];
entry.LastUse = m_boundary;
m_lastLifetimeId = lifetimeId;
m_lastIndex = index;
m_lastHandle = MG_Pipe::MGPipeHandle{index + MG_Pipe::kMGPipeFirstAllocatableSlot,
entry.Gen};
return m_lastHandle;
}
// Ages the table and returns idle slots to the free list. Same shape and the same
// self-gating as VertexInputStateFactory::OnFrameBoundary, which is what the reclaimed
// slots' consumers use.
void OnFrameBoundary() {
++m_boundary;
if ((m_boundary % kSweepInterval) != 0) return;
SizeT retired = 0;
for (auto it = m_index.begin(); it != m_index.end();) {
Entry& entry = m_entries[it->second];
if ((m_boundary - entry.LastUse) > kRetireAgeBoundaries) {
entry.LifetimeId = 0;
m_freeSlots.push_back(it->second);
it = m_index.erase(it);
++retired;
} else {
++it;
}
}
if (retired != 0) {
// A retired slot's Gen has not moved yet - it moves when the slot is reused -
// so a front memo pointing at one would still hand out a handle the consumer
// tables would accept. Drop it.
m_lastLifetimeId = 0;
m_lastHandle = MG_Pipe::kMGPipeNullHandle;
MGLOG_D("MagmaPipeIdentityTable(%s): retired %zu idle slots, %u live of %u minted",
m_kindName, retired, LiveCount(), Count());
}
}
private:
// Sweep cadence and retirement age, deliberately the same numbers
// VertexInputStateFactory::OnFrameBoundary uses for the entries these slots key: a slot
// retired earlier than its cache entry would mint a new handle for an object whose
// entry is still live and still correct, which is a pure waste.
static constexpr Uint64 kSweepInterval = 256;
static constexpr Uint64 kRetireAgeBoundaries = 1024;
struct Entry {
Uint64 LifetimeId = 0;
Uint64 LastUse = 0;
// Moves ONLY on slot reuse, never on respecify: an object that keeps its slot keeps
// its generation, which is what makes a memo survive a reconfiguration.
Uint32 Gen = 0;
};
Uint32 ClaimSlot() {
while (!m_freeSlots.empty()) {
const Uint32 index = m_freeSlots.back();
m_freeSlots.pop_back();
// MGPipeHandles.h:52-58 defends the Gen wrap only in a debug allocator, and
// MOBILEGL_ASSERT is compiled out of every build P2 runs (Defines.h: asserts are
// live only at MOBILEGL_LOG_ACTIVE_LEVEL == DEBUG). So the wrap is handled on the
// RELEASE path instead of asserted: a slot that has been reused 2^32 times is
// permanently retired rather than wrapped, because a wrapped Gen would let a
// stale handle match a live object. It costs one slot.
if (m_entries[index].Gen == ~Uint32{0}) {
MGLOG_W("MagmaPipeIdentityTable(%s): slot %u reached generation 2^32-1 and is "
"retired for good; {slot, gen} stays unique",
m_kindName, index + MG_Pipe::kMGPipeFirstAllocatableSlot);
continue;
}
++m_entries[index].Gen;
return index;
}
const Uint32 index = static_cast<Uint32>(m_entries.size());
m_entries.push_back(Entry{});
m_entries[index].Gen = 1;
// The high-water mark, at powers of two from 1024 up: at most a handful of lines
// for a whole session, emitted from the allocate-a-NEW-slot branch, i.e. once per
// object this backend has ever seen and never on a draw (ROADMAP.md:7).
//
// [narrow, declared deviation from D20's "MGLOG_D for anything non-critical"] This
// one is I, not D, because D is compiled out of every build that ships and of every
// build P2 measures, and this line IS the measurement review v2's MAJOR 1 asks for:
// the live-object high-water mark of minecraft-1.21.4-in-world and
// ...-sodium-in-world, which nothing on desktop reaches and no gate here can see.
// The structure no longer has a capacity to size off it, so the number is evidence
// rather than a tuning input - but D.4.2 should still read it out of the device log,
// and it cannot read a line that was compiled away.
const SizeT minted = m_entries.size();
if (minted >= 1024 && (minted & (minted - 1)) == 0) {
MGLOG_I("MagmaPipeIdentityTable(%s): high-water %zu slots minted, %u live",
m_kindName, minted, LiveCount());
}
return index;
}
const char* m_kindName = "";
Uint64 m_boundary = 0;
Vector<Entry> m_entries;
Vector<Uint32> m_freeSlots;
UnorderedMap<Uint64, Uint32> m_index;
// One-entry front memo (see Acquire). m_lastLifetimeId == 0 means "empty": a live
// object's lifetime id is never 0.
Uint64 m_lastLifetimeId = 0;
Uint32 m_lastIndex = 0;
MG_Pipe::MGPipeHandle m_lastHandle = MG_Pipe::kMGPipeNullHandle;
};
// The two mints one renderer owns. Per renderer, NOT process-global: two live contexts (or
// a context recreation, which destroys and rebuilds the renderer) would otherwise share one
// table and one reclamation clock, and both consumer tables are per-instance already.
class MagmaPipeIdentityTables {
public:
// A VAO is kind VertexElementsCso: that is the gallium-shaped CSO a vertex array
// resolves to, and the only kind in MGPipeKind that names vertex-input state.
MG_Pipe::MGPipeHandle HandleOf(MG_Pipe::MGPipeKind kind, Uint64 lifetimeId) {
return kind == MG_Pipe::MGPipeKind::Buffer ? m_buffers.Acquire(lifetimeId)
: m_vaos.Acquire(lifetimeId);
}
void OnFrameBoundary() {
m_vaos.OnFrameBoundary();
m_buffers.OnFrameBoundary();
}
const MagmaPipeIdentityTable& Vaos() const { return m_vaos; }
const MagmaPipeIdentityTable& Buffers() const { return m_buffers; }
private:
MagmaPipeIdentityTable m_vaos{"VertexElementsCso"};
MagmaPipeIdentityTable m_buffers{"Buffer"};
};
// The table entry a handle names. Every per-slot table Magma keeps is indexed by this.
//
// A null handle has no slot, and it is unreachable here: both lifetime-id sources start at
// 1 (VertexArrayObject.cpp, BufferObject.cpp), so Acquire's zero guard never fires. The
// ternary, not the assertion, is what has effect in a shipped build (Defines.h compiles
// MOBILEGL_ASSERT out at INFO), and slot 0 of a consumer table is a real entry that a null
// handle can never match, because MGPipeHandleIsNull is also what the consumers compare.
inline Uint32 MagmaPipeSlotIndex(const MG_Pipe::MGPipeHandle& handle) {
MOBILEGL_ASSERT(!MG_Pipe::MGPipeHandleIsNull(handle),
"a null MGPipeHandle has no slot to index a per-slot table with");
return MG_Pipe::MGPipeHandleIsNull(handle)
? 0u
: handle.Slot - MG_Pipe::kMGPipeFirstAllocatableSlot;
}
// A grow-on-demand per-slot table whose ENTRY ADDRESSES NEVER MOVE.
//
// D12.4 asks for a grow-on-demand Vector, and with an unbounded mint that is what a
// consumer needs - but a Vector that grows relocates its elements, and the draw path holds
// references into these entries across nested calls. Chunks of kChunkEntries are appended
// instead: the Vector of owning pointers reallocates, the chunks never do, so an entry
// reference is valid for the life of the table. That is the same guarantee the fixed table
// it replaces gave, without the fixed capacity.
template <typename T, Uint32 kChunkEntries = 256>
class MagmaPipeSlotTable {
public:
T& operator[](Uint32 index) {
const Uint32 chunk = index / kChunkEntries;
while (m_chunks.size() <= chunk) {
m_chunks.push_back(MakeUnique<Chunk>());
}
return m_chunks[chunk]->Entries[index % kChunkEntries];
}
SizeT Capacity() const { return m_chunks.size() * kChunkEntries; }
private:
struct Chunk {
T Entries[kChunkEntries] = {};
};
Vector<UniquePtr<Chunk>> m_chunks;
};
// The claim rule every per-slot memo table uses, in one place so that the rule and the
// negative control that defeats it cannot drift apart between consumers - and so that the
// unit suite which drives a REAL slot reuse (MG_Test/Pipe/MagmaPipeIdentityTest.cpp) tests
// this code rather than a copy of it.
//
// The SLOT picks the entry; the WHOLE handle - Gen included - decides whether the entry is
// this object's. A slot the mint recycled for a different object comes back with a moved
// Gen, so the compare fails and the entry is cleared rather than inherited. That is the
// half HandleRecycleScenario cannot reach (see MagmaPipeAbaControlDefeatsIdentity).
//
// With negative control C on, every object collapses onto one entry and the entry is handed
// back UNCLEARED and UNCLAIMED - at once "the replacement reproduced its predecessor's
// slot" and "the slot was reused and Gen did not move".
//
// `Memos` needs a MG_Pipe::MGPipeHandle member named Owner and a default constructor that
// means "empty"; VertexInputStateFactory::VaoBackendMemos is the one production instance.
template <typename Memos, Uint32 kChunkEntries>
inline Memos& MagmaPipeClaimSlotMemos(MagmaPipeSlotTable<Memos, kChunkEntries>& table,
const MG_Pipe::MGPipeHandle& handle) {
if (MagmaPipeAbaControlDefeatsIdentity()) {
return table[kMagmaPipeAbaControlSlotIndex];
}
Memos& memos = table[MagmaPipeSlotIndex(handle)];
if (!(memos.Owner == handle)) {
memos = Memos{};
memos.Owner = handle;
}
return memos;
}
#endif // MOBILEGL_PIPE_PUSH
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -1,224 +0,0 @@
// MobileGL - server-owned program input for Magma (P5f fm).
#pragma once
#include <MG_State/GLState/ProgramState/ProgramObject.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/PipeApply.h>
#include <MG_State/GLState/ProgramState/ProgramArtifactsCodec.h>
#include "../DirectVulkanResourceState.h"
// P7 wave 2 package C, OQ-8: spirv_reflect.h is GONE from this header, and its absence is the
// gate. Everything this class needed it for - the storage-block index space - now travels in
// the archive (LinkArtifacts::storageBlocks), so the SPIR-V reflector is off the Magma wire
// draw path entirely. A future edit that re-adds the include is re-adding a per-draw reflect,
// because a MagmaProgramSource is constructed once per draw and can memoise nothing.
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
// A borrowed view, valid only while its frontend object or server record lives.
// It never constructs a ProgramObject on the server. Post-link mutable values
// come from the record tails, not from the archive's link-time defaults.
class MagmaProgramSource {
public:
using Program = MG_State::GLState::ProgramObject;
MagmaProgramSource(const Program& program) : m_frontend(&program) {}
MagmaProgramSource(MG_Pipe::MGPipeHandle handle, const MG_Pipe::MGPipeShaderCsoRecord& record)
: m_handle(handle), m_record(&record) {
MOBILEGL_ASSERT(record.Archive != nullptr, "Magma program record has no archive");
}
Bool IsWire() const { return m_record != nullptr; }
MG_Pipe::MGPipeHandle Handle() const { return m_handle; }
const Program* Frontend() const { return m_frontend; }
Uint GetExternalIndex() const { return IsWire() ? m_handle.Slot : m_frontend->GetExternalIndex(); }
Uint64 GetLifetimeId() const {
return IsWire() ? (Uint64(m_handle.Gen) << 32) | m_handle.Slot : m_frontend->GetLifetimeId();
}
Bool GetLinkStatus() const { return IsWire() ? m_record->Desc.LinkStatus != 0 : m_frontend->GetLinkStatus(); }
Bool GetSpirvStatus() const { return IsWire() ? m_record->Desc.SpirvStatus != 0 : m_frontend->GetSpirvStatus(); }
Bool PointSizeDemoted() const { return IsWire() ? m_record->Desc.PointSizeDemoted != 0 : m_frontend->PointSizeDemoted(); }
Bool GetSpirvValidationEnabled() const {
return IsWire() ? m_record->Desc.EnableSpirvValidation != 0 : m_frontend->GetSpirvValidationEnabled();
}
const Vector<Vector<Uint>>& GetGeneratedSpirv() const {
return IsWire() ? m_record->Archive->Spirv.generatedSpirv : m_frontend->GetGeneratedSpirv();
}
Vector<ShaderStage> GetLinkedShaderStages() const {
if (!IsWire()) return m_frontend->GetLinkedShaderStages();
Vector<ShaderStage> result;
for (const auto stage : m_record->Archive->LinkedStages) result.push_back(static_cast<ShaderStage>(stage));
return result;
}
Bool HasLinkedShaderStage(ShaderStage stage) const {
if (!IsWire()) return m_frontend->HasLinkedShaderStage(stage);
const auto& stages = m_record->Archive->LinkedStages;
return std::find(stages.begin(), stages.end(), static_cast<Uint32>(stage)) != stages.end();
}
Bool GetBackendHashMemo(Uint flags, Uint64& hash) const {
// A view has no lifetime in which a memo can safely persist. The server
// factory's content cache still owns compiled programs; hash from current
// archive + binding tails rather than borrowing the client's mutable memo.
return !IsWire() && m_frontend->GetBackendHashMemo(flags, hash);
}
void SetBackendHashMemo(Uint flags, Uint64 hash) const {
if (!IsWire()) m_frontend->SetBackendHashMemo(flags, hash);
}
Uint64 GetBackendStateVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetBackendStateVersion(); }
Uint64 GetBlockBindingVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetBlockBindingVersion(); }
Uint64 GetImageUnitVersion() const { return IsWire() ? m_record->BindingsSerial : m_frontend->GetImageUnitVersion(); }
Uint64 GetLinkVersion() const { return IsWire() ? m_record->Serial : m_frontend->GetLinkVersion(); }
const void* GetUBOData() const { return IsWire() ? m_record->GlobalConstants.data() : m_frontend->GetUBOData(); }
Uint GetUBOSize() const { return IsWire() ? static_cast<Uint>(m_record->GlobalConstants.size()) : m_frontend->GetUBOSize(); }
Uint32 GetUBOContentVersion() const { return IsWire() ? m_record->GlobalConstantsVersion : m_frontend->GetUBOContentVersion(); }
Int GetActiveUniformBlocksCount() const {
return IsWire() ? static_cast<Int>(Link().glBlockIndexToTProgram.size()) : m_frontend->GetActiveUniformBlocksCount();
}
Uint GetUniformBlockIndex(const char* name) const {
if (!IsWire()) return m_frontend->GetUniformBlockIndex(name);
auto found = Link().uniformBlockIndexByName.find(name);
if (found == Link().uniformBlockIndexByName.end()) found = Link().uniformBlockIndexByName.find(String(name) + "[0]");
return found == Link().uniformBlockIndexByName.end() ? GL_INVALID_INDEX : found->second;
}
Uint GetUniformBlockBinding(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockBinding(index);
return index < m_record->BlockBindings.size() ? static_cast<Uint>(m_record->BlockBindings[index]) : 0;
}
const String& GetUniformBlockName(Uint index) const {
if (!IsWire()) return m_frontend->GetUniformBlockName(index);
const auto* block = Block(index);
static const String empty;
return block ? block->name : empty;
}
Uint GetUBOSizeAt(Uint index) const {
if (!IsWire()) return m_frontend->GetUBOSizeAt(index);
const auto* block = Block(index);
return block ? (static_cast<Uint>(block->size) + 15u) & ~15u : 0;
}
Bool IsValidUniformLocation(Int location) const {
return IsWire() ? Program::IsValidUniformLocation(Link(), location) : m_frontend->IsValidUniformLocation(location);
}
Bool UniformLocationsAliasSameUniform(Int a, Int b) const {
if (!IsWire()) return m_frontend->UniformLocationsAliasSameUniform(a, b);
return IsValidUniformLocation(a) && IsValidUniformLocation(b) &&
Link().uniformIndexInTProgram[a] == Link().uniformIndexInTProgram[b];
}
GLenum GetUniformType(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformType(location);
return IsValidUniformLocation(static_cast<Int>(location))
? Program::UniformAtIn(Link(), Link().uniformIndexInTProgram[location]).glDefineType : 0;
}
Int GetUniformSamplerOrImageUnitIndex(Uint location) const {
if (!IsWire()) return m_frontend->GetUniformSamplerOrImageUnitIndex(location);
const auto& units = m_record->SamplerUnits;
const auto found = std::lower_bound(units.begin(), units.end(), location,
[](const MG_Pipe::MGPProgramSamplerUnit& unit, Uint value) { return unit.Location < value; });
return found != units.end() && found->Location == location ? found->Unit : -1;
}
Int GetUniformLocation(const String& name) const {
if (!IsWire()) return m_frontend->GetUniformLocation(name);
const auto& locations = Link().uniformLocations;
auto found = locations.find(name);
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.empty()) return -1;
found = locations.find(name + "[0]");
if (found != locations.end()) return static_cast<Int>(found->second);
if (name.back() != ']' || name.size() < 4) return -1;
const SizeT bracket = name.rfind('[');
if (bracket == String::npos || bracket + 1 >= name.size() - 1) return -1;
Uint element = 0;
for (SizeT i = bracket + 1; i < name.size() - 1; ++i) {
if (name[i] < '0' || name[i] > '9') return -1;
element = element * 10 + static_cast<Uint>(name[i] - '0');
if (element > 0x0fffffffu) return -1;
}
found = locations.find(name.substr(0, bracket) + "[0]");
if (found == locations.end()) found = locations.find(name.substr(0, bracket));
if (found == locations.end()) return -1;
const Int base = static_cast<Int>(found->second);
if (!IsValidUniformLocation(base)) return -1;
const Int tIndex = Link().uniformIndexInTProgram[base];
if (!Program::UniformAtIn(Link(), tIndex).type.isArray ||
static_cast<GLint>(element) >= Program::GetUniformArraySizeByTIndex(Link(), tIndex)) return -1;
const Int location = base + static_cast<Int>(element);
return UniformLocationsAliasSameUniform(base, location) ? location : -1;
}
SizeT GetTransformFeedbackVaryingCount() const { return IsWire() ? Link().xfbVaryings.size() : m_frontend->GetTransformFeedbackVaryingCount(); }
const Vector<MG_State::GLState::XfbVarying>& GetTransformFeedbackVaryings() const {
return IsWire() ? Link().xfbVaryings : m_frontend->GetTransformFeedbackVaryings();
}
SizeT GetTransformFeedbackBufferCount() const { return IsWire() ? Link().xfbStrides.size() : m_frontend->GetTransformFeedbackBufferCount(); }
Uint32 GetTransformFeedbackStride(Uint index) const {
if (!IsWire()) return m_frontend->GetTransformFeedbackStride(index);
return index < Link().xfbStrides.size() ? Link().xfbStrides[index] : 0;
}
GLenum GetTransformFeedbackBufferMode() const {
return IsWire() ? Link().xfbBufferMode : m_frontend->GetTransformFeedbackBufferMode();
}
// P7 wave 2 package C, OQ-8 (CONTRACT-P7 §5.3): READ OUT OF THE ARCHIVE.
//
// What stood here re-ran spvReflectCreateShaderModule over EVERY stage module of the
// program, and it did so PER DRAW: a MagmaProgramSource is a borrowed view that
// WireDraw.inc's WireProgramSource constructs afresh for each wire draw, so the lazy
// `m_storageBlocksReady` latch could never outlive one. The list it rebuilt is a pure
// function of the linked program, so it is now built once at link time and travels in
// the archive (LinkArtifacts::storageBlocks), which takes the SPIR-V reflector off the
// wire draw path entirely - this header no longer includes spirv_reflect.h at all.
Uint GetShaderStorageBlockIndex(const String& name) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockIndex(*m_frontend, name);
const auto& blocks = Link().storageBlocks;
// The archive carries NORMALISED names (the array subscript stripped), because
// that is the spelling SPIRV-Reflect's type_name gives and the spelling
// ProgramFactory::ReflectLayout looks up. A caller that still holds a GL-style
// "Foo[2]" gets the same second chance it always had.
const auto find = [&blocks](const String& key) {
return std::find_if(blocks.begin(), blocks.end(),
[&](const MG_State::GLState::StorageBlockReflection& block) {
return block.name == key;
});
};
auto found = find(name);
if (found == blocks.end() && !name.empty() && name.back() == ']') {
const auto bracket = name.rfind('[');
if (bracket != String::npos) found = find(name.substr(0, bracket));
}
return found == blocks.end() ? GL_INVALID_INDEX
: static_cast<Uint>(found - blocks.begin());
}
Uint GetShaderStorageBlockBinding(Uint index) const {
if (!IsWire()) return DirectVulkan::GetShaderStorageBlockBinding(*m_frontend, index);
const auto& blocks = Link().storageBlocks;
if (index >= blocks.size()) return 0;
const auto& block = blocks[index];
// THE OVERRIDE STILL WINS, and it still comes off the RECORD rather than the
// archive: glShaderStorageBlockBinding is a post-link mutable and the archive
// carries link-time defaults only (rule on MagmaProgramSource, and the reason
// StorageBlockReflection::binding is documented as the DECLARED binding).
for (const auto& override : m_record->StorageOverrides) {
if (override.Name == block.name) return override.Binding;
}
return block.binding;
}
private:
const MG_State::GLState::LinkArtifacts& Link() const { return m_record->Archive->Link; }
const MG_State::GLState::BlockReflection* Block(Uint index) const {
if (index >= Link().glBlockIndexToTProgram.size()) return nullptr;
const Int tIndex = Link().glBlockIndexToTProgram[index];
return tIndex >= 0 && static_cast<SizeT>(tIndex) < Link().blockReflection.size()
? &Link().blockReflection[tIndex] : nullptr;
}
const Program* m_frontend = nullptr;
MG_Pipe::MGPipeHandle m_handle = MG_Pipe::kMGPipeNullHandle;
const MG_Pipe::MGPipeShaderCsoRecord* m_record = nullptr;
// P7 OQ-8: the two mutables that memoised the per-draw reflect are gone with it. They
// were `mutable` precisely because this class is a borrowed const view - and that is
// also why they never helped: a view lives for one draw, so the latch was cold every
// time it was read.
};
#else
using MagmaProgramSource = MG_State::GLState::ProgramObject;
#endif
}
@@ -1,37 +0,0 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the
// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled
// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client
// surface, or the placeholder a suspended server window draws into) must take the AImageReader
// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins
// it without a driver.
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class OffscreenSurfaceRoute : Uint8 {
Headless, // vkCreateHeadlessSurfaceEXT
AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays
Unavailable, // neither: the target cannot be built (the caller refuses, by name)
};
// `headlessEnabled`: the instance was created with VK_EXT_headless_surface.
// `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT.
// `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface.
constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint,
Bool androidSurfaceEnabled) {
if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless;
if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader;
return OffscreenSurfaceRoute::Unavailable;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -198,29 +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)));
@@ -285,14 +270,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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;
}
@@ -391,15 +369,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
// The assert compiles out of shipping builds, and a program declined for an invalid
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
if (payload.stages == nullptr || payload.stages->empty()) {
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
"(programHash=0x%llx); its draws are skipped",
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
@@ -466,14 +435,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;
@@ -35,46 +35,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
@@ -120,9 +87,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// 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.
// NOT hashed: it is a pure function of the program and of patchControlPoints, both
// of which ComputeHash already mixes in.
VkPipelineShaderStageCreateInfo passthroughTessControlStage{};
const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr;
// Diagnostic only; may be null. Read solely from the pipeline-creation failure path.
@@ -199,7 +165,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

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