Compare commits

..
Author SHA1 Message Date
swung0x48 5331106e90 [CI] Use uname for Arch runner hostname 2026-10-05 08:57:17 -04:00
swung0x48 ca981a8fc1 [CI] Add Arch WSL GPU runner smoke workflow and configuration 2026-10-05 08:56:27 -04:00
swung0x48 c90383db16 [Fix] (MG_Remote): bound socket buffers in log flood tests 2026-10-05 07:13:39 -04:00
swung0x48 698f04574b [Fix] (MG_Remote): wait for records emitted while draining return events 2026-10-05 06:21:42 -04:00
swung0x48 f497ff9741 [Fix] (DirectVulkan): detach ended render passes before attachment layout reconciliation 2026-10-05 05:12:42 -04:00
swung0x48 946e63d810 [Fix] (MG_IntegrationTest): check pre-finish work completion in present credit tests 2026-10-05 04:22:18 -04:00
swung0x48 918d40dd4f [Fix] (Init): allow MobileGL initialization with LIBGL_ALWAYS_SOFTWARE 2026-10-05 02:55:35 -04:00
swung0x48 f5b8fe48aa [Fix] (MGPipe, MG_Remote): verify server pack state and reject records after guilty GPU resets 2026-10-05 02:51:19 -04:00
swung0x48 c5d25868cf [Fix] (ShaderTranspiler): preserve block identity when deduplicating image types 2026-10-05 01:27:50 -04:00
swung0x48 c464947bdd [Fix] (TraceReplay): share Android window surfaces and release current bindings before resize 2026-10-05 00:47:56 -04:00
swung0x48 4fb75f4e00 [Fix] (DirectVulkan): prepare image copy recording after uploads and share texture usage checks 2026-10-05 00:19:21 -04:00
swung0x48 4d6904184d [Fix] (ShaderTranspiler): deduplicate image types after Vulkan image rewrites 2026-10-05 00:08:32 -04:00
swung0x48 2f1a673513 [Fix] (BackendLoader, DirectGLES): check EGL extensions before resolving damage swap entry points 2026-10-05 00:08:31 -04:00
swung0x48 88bc35b711 [Refactor] (BackendLoader): share extension name matching between GLES and EGL probes 2026-10-04 20:54:28 -04:00
swung0x48 880be57bb6 [CI] (Watchdog): disable GPU hang detection in all CI runs 2026-10-04 10:39:39 -04:00
swung0x48 70e4691b3a [Docs] (Anland): refresh the handoff to the 2026-10-04 state, with the perf, CI, YUV and VA-API streams 2026-10-04 07:25:23 -04:00
Swung0x48 2d42414667 [CI] (Test): annotate failed ctest entries with their failure lines, since the run logs need a signed-in reader 2026-10-04 07:20:04 -04:00
Swung0x48 32d27b1491 [Fix] (DirectGLES): a session's backend registration no longer trips the strict apply-thread pin while another session is applying 2026-10-04 07:20:04 -04:00
Swung0x48 78c5f8dae7 [CI] (Census): record Refuse{Recover} as a local refusal; the client declines to redial and there is no peer to tell 2026-10-04 07:20:04 -04:00
swung0x48 96bd540ba1 [Docs] (Performance): record the device state at the glmark2 handoff 2026-10-04 06:46:51 -04:00
swung0x48 710beefd02 [Docs] (Performance): glmark2 optimization handoff with the landed fixes, current numbers, remaining work and the bench scripts 2026-10-04 06:44:49 -04:00
Swung0x48 c1e957cb98 [CI] (Gates): retire the P3a/P4a untouched-region gates; those phases closed and later phases legitimately changed the regions 2026-10-04 06:30:24 -04:00
Swung0x48 eb71b669b3 [Fix] (Build): make the pull and verify builds compile again 2026-10-04 06:30:24 -04:00
swung0x48 bb31f374a3 [Perf] (DirectVulkan): keep the wire draw's render pass open across draws to the same attachments, closing it only for work that cannot sit inside one 2026-10-04 06:18:35 -04:00
swung0x48 e5ee31b022 [Build] (Anland): build the anland server for android-30, the APK's minSdk, so thread_local uses native TLS 2026-10-04 06:02:45 -04:00
swung0x48 915882b293 [Perf] (Server): time records only when the per-frame statistics are on 2026-10-04 06:02:44 -04:00
swung0x48 963f4d8177 [Perf] (DirectVulkan): reuse completed command buffers instead of freeing and allocating one per submission 2026-10-04 06:02:43 -04:00
swung0x48 d627fca387 [Perf] (Transport): the server rings a parked client only once the sequence it waits for is applied 2026-10-04 05:40:14 -04:00
swung0x48 2de0d6dd25 [Perf] (Pipe): reuse client-array buffers instead of a blocking create per draw, and copy contiguous ranges in one go 2026-10-04 04:54:54 -04:00
swung0x48 66b47a101b [Fix] (Remote): glFinish waits for the GPU, Magma bounds unsubmitted clear objects, ES contexts advertise OES_mapbuffer/rgb8_rgba8/depth24 2026-10-04 04:54:53 -04:00
swung0x48 4722fa38bb [Docs] (Performance): glmark2 baseline of Espryt and Magma against the stock kgsl stack on anland, with profiles and ranked bottlenecks 2026-10-04 04:33:24 -04:00
swung0x48 3a289222af [Docs] (MobileVA): plan a vendor-agnostic VA-API driver on Android MediaCodec, H.264 first 2026-10-03 23:46:13 -04:00
swung0x48 18a5dba0e4 [Docs] (Anland): Chrome decodes video in hardware through the YUV shared images, with probes to check it 2026-10-03 22:46:01 -04:00
swung0x48 7218bacb49 [Feature] (SharedImage): sample NV12/P010 dma-bufs through GL_OES_EGL_image_external on both backends, copying foreign buffers into server YUV images 2026-10-03 22:46:00 -04:00
swung0x48 4633ca223c [Fix] (Server): log the per-frame P65ServerFrame line only when MOBILEGL_SERVER_FRAME_STATS asks for it 2026-10-03 14:20:11 -04:00
swung0x48 0ef13ab0a6 [Docs] (Anland): Chrome decodes video in software because MobileGL cannot import the container VA-API driver's NV12 frames. 2026-10-03 14:16:04 -04:00
swung0x48 287e1df8dc [Fix] (Server, Magma, Espryt): end the session whose GPU work hangs the GPU so it cannot stall the desktop again. 2026-10-03 12:28:39 -04:00
swung0x48 4f6f4125ac [Fix] (EGL, SharedImage): real EGL native fences publish a dma-buf producer's writes, so Chrome's GPU process frames reach the compositor. 2026-10-03 10:59:02 -04:00
swung0x48 2a1889b817 Discover gtest cases at ctest start so parallel links stop sharing one discovery JSON file. 2026-10-03 09:38:51 -04:00
swung0x48 07e7730085 [Docs] (Anland): kwin-recover.sh reads the latch from the server log file, not the too-small logcat buffer. 2026-10-03 09:05:55 -04:00
swung0x48 03648c2783 [Docs] (Anland): add a device test that loses KWin's own session and checks the desktop keeps compositing. 2026-10-03 09:05:54 -04:00
swung0x48 f22ae625cb [Fix] (Server): a lost session's window lease is waited for by the fresh session, and a window wait holds no backend turn. 2026-10-03 09:05:54 -04:00
swung0x48 f52a546707 [Fix] (EGL, Remote): a server-owned surface is re-created at the size it asked for, and a lost session's image reference is never released on the fresh one. 2026-10-03 09:05:53 -04:00
swung0x48 77c604061e [Docs] (Anland): glxgears is fixed-function, so it measures presentation only 2026-10-03 07:53:11 -04:00
swung0x48 36190b7cc0 [Fix] (EGL, Wayland): a swap that runs into a device loss reports it and still commits the window, instead of showing an empty frame. 2026-10-03 07:17:08 -04:00
swung0x48 cf763b4081 [Fix] (EGL, Remote): a thread that released a lost context keeps reporting the reset until it binds another. 2026-10-03 07:17:07 -04:00
swung0x48 2acc6c41bb [Fix] (Pipe): an evicted sampler CSO is deleted from every share group of its session. 2026-10-03 05:37:53 -04:00
swung0x48 a84104a40f [Fix] (Pipe): the default framebuffer's record names no placeholder surface that only one share group holds. 2026-10-03 05:37:53 -04:00
swung0x48 1dd888cb21 [Fix] (EGL, DirectGLES): keep a lost context's release off the fresh session, bind no surfaceless context natively, and keep the display while surfaces remain. 2026-10-03 05:37:52 -04:00
swung0x48 393002a46e [Fix] (Pipe): give each context's applier its own serial range so backend memos never match another context. 2026-10-03 05:37:51 -04:00
swung0x48 25a76c1b20 [Fix] (Pipe): drain a texture's pending texels only under a context of the share group that wrote them. 2026-10-03 05:37:51 -04:00
swung0x48 a9f22ebb63 [Fix] (EGL): create and end a context's objects under its own binding and the stream lock, and defer destroying a current context. 2026-10-03 05:37:50 -04:00
swung0x48 2fe011adf6 [Test] (Wire): enable the concurrent-context repro and add release, sibling, deferred-destroy and read-back-race shapes. 2026-10-03 05:37:49 -04:00
swung0x48 11668b02f2 [Docs] (Anland): Chrome runs its GPU in its own process by default and recovers from a device loss. 2026-10-03 05:21:53 -04:00
swung0x48 30977c181f [Feature] (EGL+GL): an opt-in OpenGL ES identity for ES contexts with BGRA textures, and contexts take the dispatcher's API, so Chrome's GPU process runs on ANGLE. 2026-10-03 05:21:52 -04:00
swung0x48 e5f097f9e1 [Docs] (Anland): add a Chrome device-loss recovery test and the GPU-process launch mode. 2026-10-03 04:37:34 -04:00
swung0x48 90e63c9e2a [Feature] (EGL+GL): advertise EGL_EXT_create_context_robustness and GL robustness, with per-context reset notification and a pid-targeted device-loss knob. 2026-10-03 04:37:33 -04:00
swung0x48 af2b9f4c1c [Docs] (Anland): root cause of the X11 stale-region issue 2026-10-03 04:36:24 -04:00
swung0x48 cd58f048b6 [Fix] (Shader): an inactive fragment output no longer takes location 0 from the active one 2026-10-03 04:16:41 -04:00
swung0x48 b0dad54cf1 [Docs] (Anland): device numbers and the open stale-region issue for X11 on the GPU 2026-10-03 03:20:12 -04:00
swung0x48 95aa618d22 [Docs] (Anland): x11-verify measures 1280x720 glxgears and runs Qt apps on xcb 2026-10-03 02:58:03 -04:00
swung0x48 5de6ecd4e1 [Fix] (SharedImage): uploads into a texture bound to a shared image are written into the image, and a producer's flush sends them first 2026-10-03 02:58:02 -04:00
swung0x48 7fd965ceda [Fix] (SharedImage): a session that writes images publishes every frame boundary as a write 2026-10-03 01:51:25 -04:00
swung0x48 9e7305b885 [Docs] (Anland): plan and device scripts for X11 apps on the GPU 2026-10-03 01:51:24 -04:00
swung0x48 bd4c5b5770 [Test] (Gbm): drive mobilegl_gbm through the system loader the way Xwayland's glamor does 2026-10-03 01:51:23 -04:00
swung0x48 198d6feaf3 [Feature] (GLX): present windows as shared images through DRI3 and Present, with MIT-SHM readback as the fallback 2026-10-03 01:51:22 -04:00
swung0x48 08f23543aa [Feature] (SharedImage): a glFlush publishes an image producer's accesses, so Xwayland's glamor gets implicit sync 2026-10-03 01:51:20 -04:00
swung0x48 316ffe02d0 [Fix] (X11): never open an X connection to a display this process serves 2026-10-03 01:50:58 -04:00
swung0x48 fdf9252b53 [Test] (Wire): add a disabled repro for read-backs lost when threads with their own contexts tear down. 2026-10-03 00:42:35 -04:00
swung0x48 8024b645cb [Fix] (EGL): contexts recreated naming a lost share context share one new group instead of none. 2026-10-03 00:42:34 -04:00
swung0x48 9f30fc500f [Fix] (Magma): log how many SPIR-V modules the driver screen validated at renderer shutdown. 2026-10-03 00:10:58 -04:00
swung0x48 e3120af6a9 [Fix] (EGL): name a lost context's refused bind and a refused share in the client log once. 2026-10-03 00:08:49 -04:00
swung0x48 c60fcc17c5 [Fix] (Pipe): the device-loss debug knob re-arms when set to a new value. 2026-10-02 23:59:55 -04:00
swung0x48 b06c2a9bf2 [Fix] (Remote): a surfaceless make-current's hidden pbuffer no longer latches the session offscreen. 2026-10-02 23:59:54 -04:00
swung0x48 8b5295401f [Docs] (Anland): reproduce.md brings the desktop up by opening the app. 2026-10-02 23:41:20 -04:00
swung0x48 22266c47cf Merge client-recover: open a fresh server session after a device loss. 2026-10-02 23:38:56 -04:00
swung0x48 4a4e53d9a9 Merge magma-validate: Magma SPIR-V validation declines and surfaceless EGL. 2026-10-02 23:38:19 -04:00
swung0x48 b7cc56dd44 [Docs] (Anland): record the app-driven desktop's timings, idle numbers and frame pacing 2026-10-02 23:36:47 -04:00
swung0x48 7823bcd987 [Fix] (Client): recover a fresh server session after a device loss so applications can recreate their contexts 2026-10-02 23:34:21 -04:00
swung0x48 caa00995e7 [Fix] (EGL): slow an unpaced Wayland window to one frame a second while the compositor shows none of it 2026-10-02 23:26:03 -04:00
swung0x48 1ff7ca5331 [Fix] (EGL): pace Wayland window swaps by frame callbacks so clients idle while the compositor shows nothing 2026-10-02 23:10:33 -04:00
swung0x48 6125f941d1 [Fix] (EGL): accept surfaceless make-current and config-less contexts so eglinfo prints the GL renderer lines 2026-10-02 23:09:59 -04:00
swung0x48 bfa7987ca6 [Fix] (Magma): decline programs whose final SPIR-V fails validation instead of handing the module to the driver 2026-10-02 23:09:58 -04:00
swung0x48 b1405e17ad [Docs] (Anland): the desktop starts from the app and idles while hidden, with no manual bring-up 2026-10-02 22:46:04 -04:00
swung0x48 b64c2de3c8 [Docs] (Anland): point the reproduce guide at the anland fork 2026-10-02 22:26:24 -04:00
swung0x48 2b95a9697a [Docs] (Anland): reproduce the MobileGL Plasma container setup from source 2026-10-02 22:26:11 -04:00
swung0x48 228bd24c67 [Fix] (Espryt): turn window buffer age on by default 2026-10-02 21:00:20 -04:00
swung0x48 c822a9fef7 [Fix] (Espryt): copy only the damaged rectangles into shared images again 2026-10-02 21:00:10 -04:00
swung0x48 815217cc07 [Fix] (Espryt): turn the scissor off around backend copies by what the driver has, not another session's render-state shadow 2026-10-02 20:57:18 -04:00
swung0x48 7a5b0755b3 [Docs] (Anland): install MobileGL as the container's system-wide vendor and document the fallback 2026-10-02 20:13:33 -04:00
swung0x48 3d69c37504 [Fix] (EGL): keep a process the vendor does not serve from opening its log 2026-10-02 20:07:15 -04:00
swung0x48 4f679d669d [Fix] (EGL): answer client queries and proc lookups without bringing a session up 2026-10-02 20:03:45 -04:00
swung0x48 4095624f87 [Fix] (Client): step aside for processes that ask for software rendering 2026-10-02 19:57:49 -04:00
swung0x48 321e61ca0b [Docs] (README): the target is OpenGL 4.6 Core, with the current modules and platforms 2026-10-02 19:53:29 -04:00
swung0x48 552dd3d39b [Fix] (EGL): decline another backend's GBM device and ask X for a visual only on X displays 2026-10-02 19:49:32 -04:00
swung0x48 6c742eba8a [Build] (Config): bump the version to 26.10 2026-10-02 19:43:47 -04:00
swung0x48 69e96f5b8d [Feature] (Client): read a system config file and decline EGL/GLX/GBM when no server answers 2026-10-02 19:36:38 -04:00
swung0x48 d24abe9287 [Fix] (Espryt): make window buffer age opt-in 2026-10-02 18:34:23 -04:00
swung0x48 d6d75c9951 [Fix] (Espryt): copy whole frames into shared images 2026-10-02 17:38:55 -04:00
swung0x48 bb4e3e0ec6 [Fix] (Espryt): query buffer age only after a damage region is declared 2026-10-02 17:11:31 -04:00
swung0x48 233e2f8ced [Fix] (Espryt): swap a pbuffer without damage rectangles 2026-10-02 16:27:16 -04:00
swung0x48 74919a3bc1 [Feature] (Present): buffer age and swap damage end to end on Espryt and Magma 2026-10-02 15:19:39 -04:00
swung0x48 7090e31dee [Feature] (Damage): damage-region and buffer-age bookkeeping with host tests 2026-10-02 14:51:48 -04:00
swung0x48 5accb0985c [Fix] (glslang): keep samplers of anonymous uniform structs out of the default uniform block, which made Adreno page-fault into device loss on Shadertoy 2026-10-02 14:20:36 -04:00
swung0x48 58685bc116 [Fix] (Server): wait out an apply thread that is busy in a GPU stall at Stop() instead of aborting every session 2026-10-02 13:54:26 -04:00
swung0x48 34090097a1 [Fix] (Client): make a readback on a lost device a no-op instead of a Fatal that kills the client 2026-10-02 13:43:51 -04:00
swung0x48 58c3d9bde5 [Fix] (P14): latch a lost GPU device per session on Magma and Espryt instead of aborting the whole server 2026-10-02 13:37:58 -04:00
swung0x48 d03fe4c9dd [Fix] (Client): drop the late reply of a timed-out surface op instead of reading it as the next op's 2026-10-02 13:34:17 -04:00
swung0x48 239fc228fc [Fix] (DirectVulkan): retype gl_FragCoord access chains to Private when the y-flip pass redirects them to its copy 2026-10-02 13:34:16 -04:00
swung0x48 8da082f805 [Fix] (State): start the clear colour at GL's (0, 0, 0, 0), not opaque black 2026-10-02 12:50:36 -04:00
swung0x48 258b946086 [Fix] (P12): publish the server window's extent from the display on every resize, at once, and let eglQuerySurface take it in 2026-10-02 12:03:52 -04:00
swung0x48 16b07cf4b5 [Fix] (DirectVulkan): build offscreen targets of a window-created renderer through AImageReader instead of a null headless entry point 2026-10-02 11:42:14 -04:00
swung0x48 51c37bcbc9 [Fix] (Sampler): convert sampler parameters set through the other scalar type 2026-10-02 11:22:00 -04:00
swung0x48 5a7b23f661 [Feature] (P12): suspend the on-screen session when the server window goes, resume it on the next, and republish a resized window's extent 2026-10-02 11:14:27 -04:00
swung0x48 15f9b27a42 [Fix] (P14): publish a cached sampler CSO into each share group that names it 2026-10-02 10:35:44 -04:00
swung0x48 82d96a63c9 [Docs] (Anland): runbook and skill for bringing up MobileGL with anland and KDE Plasma 2026-10-02 10:31:35 -04:00
swung0x48 7520e53b5e [Feature] (SharedImage): GPU-side fences between sessions on Espryt and Magma, no CPU wait at present 2026-10-02 10:31:34 -04:00
swung0x48 b5cab44100 [Feature] (SharedImage): sync_file write/read fences per image, so sessions order their access without CPU waits 2026-10-02 09:40:12 -04:00
swung0x48 672261764e [Fix] (SharedImage): allocate and import ARGB8888/XRGB8888 images so KWin's offscreen Quick views get buffers 2026-10-02 09:37:17 -04:00
swung0x48 0e314f645e Remove a stray build log 2026-10-02 09:18:20 -04:00
swung0x48 350fc699a1 [Fix] (SharedImage): create dma-buf wl_buffers without create_immed, drain the ring before teardown ops, and decline Magma records after the renderer is gone 2026-10-02 09:18:05 -04:00
swung0x48 3d17e8c10b [Feature] (SharedImage): zero-copy Wayland windows via linux-dmabuf, EGL dma-buf import, GBM backend, and Espryt/Magma AHB present and texture binding 2026-10-02 08:39:59 -04:00
swung0x48 38842985f8 [Feature] (SharedImage): server-allocated AHB images over a shared_image pipe verb, with dma-buf fds on the aux socket 2026-10-02 07:52:25 -04:00
swung0x48 e06aabe812 [Docs] (Anland): plan for zero-copy windows via server-allocated AHB dma-bufs 2026-10-02 07:34:45 -04:00
swung0x48 67a97065b8 [Fix] (DirectVulkan): transpose quarter-turn viewports and scissors, rebuild a resized pbuffer's target, and leave out-of-range default-framebuffer reads unfilled 2026-10-02 07:15:47 -04:00
swung0x48 ec084959fa [Docs] (Anland): record the Magma bring-up to Espryt parity 2026-10-02 06:36:20 -04:00
swung0x48 4b388950e3 [Fix] (DirectVulkan): decline draws from storeless buffers and sample with defaults when a texture's sampler CSO is gone, as DirectGLES does 2026-10-02 06:33:53 -04:00
swung0x48 7e76a98d36 [Fix] (DirectVulkan): keep one renderer per session across its surfaces, follow bind_context to each context's surface, and keep the wire resource ops registered while any session lives 2026-10-02 06:24:13 -04:00
swung0x48 15dbe78ce6 [Fix] (DirectVulkan): give each served session its own renderer and Magma state 2026-10-02 05:52:38 -04:00
swung0x48 168808d1a7 [Fix] (DirectVulkan): advertise the GLSL 1.00-era ARB strings KWin gates on, and let a debug property pick the Android backend 2026-10-02 05:42:14 -04:00
swung0x48 75e1da4d1b [Docs] (Anland): record the lock-screen corruption root cause 2026-10-02 05:07:59 -04:00
swung0x48 cd610934d7 [Fix] (DirectGLES): scope object deaths to their session, and keep container objects and staging rings per native context 2026-10-02 05:07:59 -04:00
swung0x48 335637af38 [Docs] (Anland): record the GLX, Chrome and server-config fixes 2026-10-02 03:09:18 -04:00
swung0x48 4236b42a57 [Fix] (Server+Transpiler): configure the server once per process, and enable blend_func_extended for index-1 outputs 2026-10-02 03:09:17 -04:00
swung0x48 0b6842ac8b [Fix] (EGL+GL): let ANGLE's GL backend run on MobileGL (GBM displays, foreign wl_display, precision query, ES-free GL_VERSION) 2026-10-02 02:58:55 -04:00
swung0x48 47db68d557 [Feature] (GLX): serve as a glvnd GLX vendor and present remote X windows 2026-10-02 02:11:36 -04:00
swung0x48 ce11c9accd [Docs] (Anland): record the Chrome/dma-buf feedback fix 2026-10-02 01:45:03 -04:00
swung0x48 7bb9d9829f [Docs] (Anland): record the Plasma desktop milestone and the remaining issues 2026-10-02 01:38:57 -04:00
swung0x48 ccf8eb6571 [Fix] (Client): serialize multi-threaded GL streams, follow Wayland window resizes, export the KHR image aliases 2026-10-02 01:38:21 -04:00
swung0x48 0b314ddae7 [Fix] (P14): serialize sessions on one backend turn and give each client surface its own native surface 2026-10-02 01:38:20 -04:00
swung0x48 d5acece005 [Docs] (Anland): record the Plasma bring-up state after the P14 server-state fixes 2026-10-02 00:04:01 -04:00
swung0x48 30fef23d52 [Fix] (P14): scope the server staged stores and the context-live flag to the calling session 2026-10-02 00:04:00 -04:00
swung0x48 cbb87b968a [Docs] (Anland): drop the stale-glslang note and record d554c87c and the landed tooling 2026-10-01 23:36:27 -04:00
swung0x48 d554c87c45 [Fix] (Loader+Texture): tolerate drivers without EXT swap-with-damage, and pair stencil-only storage correctly
- eglSwapBuffersWithDamageEXT is an extension entry point that Android's
  framework libEGL does not export through dlsym; requiring it was a
  bring-up FATAL on TB321FU. Resolve EXT, fall back to the KHR spelling
  (same function, interchangeable signature), then to plain
  eglSwapBuffers (a zero-rectangle damage call is defined to be exactly
  that). The GLES dlsym fallback gains a self-reference guard for
  MobileGL installed under a GLES soname, where dlsym would otherwise
  hand the backend the frontend's own entry points and recurse forever.
- NormalizePixelFormat maps GL_STENCIL_INDEX8/GL_STENCIL_INDEX to
  GL_STENCIL_INDEX/GL_UNSIGNED_BYTE instead of falling through to
  RGBA/UBYTE, which no driver can pair with stencil-only storage.
- Tests pin the EXT->KHR->none preference order and the stencil pair.
- The anland handoff is rewritten to the post-P14 landed state: device
  evidence (win-smoke green screen, Konsole on screen, eglinfo/glxinfo),
  the remote-only recovery procedure, and the known gaps (Plasma session
  bring-up, cursor scale, the no-CPU-blit requirement, resize/surface
  loss, AVC audit).
2026-10-01 23:16:33 -04:00
swung0x48 7a48521d37 [Fix] (DirectGLES): destroy stale window surfaces before creating a new one
A session that dies without its teardown (crash, SIGKILLed client) leaks
its native window surface; the server-owned display window is one
ANativeWindow for the process's lifetime, and Adreno refuses a second
eglCreateWindowSurface on it with EGL_BAD_NATIVE_WINDOW - on TB321FU this
made every session after the first die with 'MobileGL could not acquire
the app Surface 300b'. InitWindowSurface now finds and destroys any
surface still bound to the window it is about to use, and the session
registry records the window with each surface to make that possible.
2026-10-01 22:32:53 -04:00
swung0x48 6946ff3a5c [Docs] (Anland): refresh the handoff to the post-P14 landed state
The document still described the state before the state-ownership work
landed: it called the unified server design unfinished, listed the
KernelSU grant as unsolved, said the directory bind had not taken effect,
and presented MobileGLAppsService plus the per-app fork worker as pending
drafts. None of that is true now.

Rewritten against the current tree and the device:

- header: architecture landed per the user's correction, acceptance still
  open; the rejected fork-worker/AppsService path is removed and the
  unified server lives in the MobileGL library (design 11-state-ownership,
  commits d52c031c / 4ed1c9ce / 5c6399e2 / ff212061 / 5a0f13fd, plus the
  landed 6fb880cd / ebc552a0 that used to be listed as uncommitted)
- MobileGL: HEAD moved to 5a0f13fd; the state-ownership section now records
  what S1-S6 landed and the debts that remain (process-level unit shadows,
  g_backendContextGeneration, per-record ContextSlot, the glIsBuffer probe,
  the window half of pbuffer->window)
- anland: 2ffa1f5 and 33e496c pushed; the draft inventory is now a landed
  inventory plus the three uncommitted KWin-acceptance fixes
- device: KernelSU grant resolved (reinstall changed the uid to 10270), the
  clone restarted with the directory bind effective, the 5a0f13fd APK
  serving @anland-mobilegl with six completed client sessions, and the
  client library installed with a stale build stamp
- new: concurrency warning (parallel sessions have overwritten the dist and
  the source tar), the current desktop-session.service failure state, and a
  rewritten continuation order

Verified: doc citations resolve (scripts/check_doc_citations.py), device
facts checked against HA27Q3LQ at refresh time.
2026-10-01 20:41:54 -04:00
swung0x48 5a0f13fdfa [Fix] (DirectGLES): advertise the GLSL 1.00-era ARB strings KWin gates on
GL_ARB_shader_objects/vertex_shader/fragment_shader and
GL_ARB_texture_non_power_of_two are core since GL 2.0 and trivially true
for the 4.6 this backend claims, but KWin 6's EglContext::checkSupported()
requires exactly these strings before it brings up compositing. Found on
TB321FU: kwin_wayland --anland failed with 'Could not initialize rendering
context' after a successful context create + makeCurrent on the
server-owned window surface.
2026-10-01 20:21:35 -04:00
swung0x48 ff2120616d [Feature] (P14): key the backend twin tables by session and share group
The last piece of state ownership this phase owed: the eight DirectGLES twin
tables resolved a handle process-wide, so two contexts of one session that do
NOT share a share group - which have independent name spaces, so the same
{slot, gen} in both is legal - were handed the SAME twin, driver ids,
persistent map and pooled store included. Two contexts that DO share a group
were served by whichever of them twinned first. Design and per-slice shape:
docs/Disaggregated/design/11-state-ownership.md (S6).

Key: DirectGLES.h's TwinKey{SessionKey, ShareGroupKey} - the same two words
the applier's object records are filed under, with the group derived by
MGPipeApplierShareGroupKeyFor so a record and the twin describing it can never
land in different buckets. Session is in the key because a twin names a driver
id and no driver id crosses a connection.

Resolution: SessionRuntime.cpp's ResolveThreadTwinKey, installed by
InstallSessionRuntimeHooks beside the S2 PipeInputs probe, the S4 native
tuple key and the S5 applier key. It reads the ThreadSessionScope's session
and CurrentContextToken() - apply-thread state only, never MG_State, so the
frontend-keyed-lookup prohibition on the apply thread still holds. A thread
that belongs to no session answers {0, 0}, which is the process singleton this
replaces; the monolith and every unit case are unchanged by construction.

Shape: SlotTables.h's SlotTableRegistry holds one SharedPtr bucket per key,
with a thread-local last-key memo. It forwards every handle-keyed entry -
GetOrCreate, FindByHandle, LiveGenAt, HandleOf, StateForHandle,
NoteStateForHandle, ReleaseByHandle, ForEachLive, the counters - to the
CALLING THREAD's bucket, so the ~150 call sites in Managers.cpp and
DirectGLES.cpp are literally unchanged. The holder list stays process-wide: a
death notice is about an object, not a bucket, and each shared holder is one
bucket. ForEachLive now walks the calling thread's bucket (what a context can
see is its own group); the death and diagnostic walks take the new
ForEachBucket.

DropEveryTwinForEndedServerSession is session-scoped: it runs on the apply
thread of the session that is ending, while S2 lets neighbour sessions keep
rendering in the same process. No session on the thread keeps the old
process-wide drop verbatim. Managers.h's "a texture is owned by exactly one
context (share groups are not implemented)" is corrected: a texture belongs to
a share group and its sibling contexts legitimately mutate it.

Tests: MG_Test/Wire/TwinOwnershipTest.cpp, five cases driving the real twin
tables with no EGL and no wire - the same handle in two nonshared groups is
two twins and neither overwrites the other; two contexts of one group resolve
ONE twin; a per-session drop takes only that session's; a thread with no
session still gets the process-wide table; and the six-registry template
repeats the first three. Negative control run and reverted: making the
registry answer a single {0, 0} bucket turns four of the five red.

Full host ctest against a stashed baseline, compared by test name: 0 new
failures. The one name in the difference set
(LogForwardChannel.APeerThatStopsReadingCostsWarnLinesNotTheCallersTime) is a
pre-existing flake, failing on the baseline too with the identical assertion.
The 103-case acceptance set plus the five new cases is 108/108 green.

Remaining debts (documented): the unit shadows (g_activeTextureUnit,
g_boundTexturesCache, g_boundSamplersCache) are still process-global; the
driver-level glIsBuffer probe is still not usable on this host, so sharing is
proven at the twin layer (the twin IS the object holding the driver id); the
backend context/sync generations are still process-wide; the per-record
ContextSlot is still S1's edge declaration.
2026-10-01 20:11:53 -04:00
swung0x48 5c6399e240 [Docs] (Design): record S5 landed shape and remaining twin-key debt 2026-10-01 19:23:57 -04:00
swung0x48 4ed1c9cef2 [Feature] (P14): state ownership - multi-session, multi-context, share groups
Breaks the one process = one server = one session = one context equation
that the whole stack's correctness used to rest on. Design and per-slice
shape: docs/Disaggregated/design/11-state-ownership.md.

S1 wire: bind_context ring record (opcode 84) announces the current
context at the make-current edge; CreateContext/DestroyContext join the
SurfaceOp control frames; control revision 5 -> 6; the server keeps a
per-session token table and refuses unknown tokens by name.

S2 server: SessionRuntime per session with thread-scoped resolution
(singletons become fallbacks), per-thread gPipeInputs, per-session latch
domains, per-thread segment resolvers; the in-process unix supervisor
serves up to MOBILEGL_IPC_INPROC_MAX_SESSIONS (16) concurrent sessions;
surface mode is declared per session and the display lease refuses a
second on-screen session by name.

S3 frontend: ShareGroupState owns the shared object stores and name
generators (buffer/texture/renderbuffer/sampler/program), GLContext keeps
per-context bindings/errors/container objects; pGLContext becomes
thread-local with a process default context preserving the single-context
world verbatim; eglCreateContext builds a real GLContext and joins the
sharing context's group.

S4 backend: native EGL tuples keyed by (session, context token);
InitDisplayAndContext's unconditional teardown is gone - creating a
second context or surface no longer destroys the first; eglCreateContext
sharing is wired to the native driver; bind_context is the native switch
point.

S5 applier: MGPipeApplierState splits into MGPipeObjectRecords (per
share group) and MGPipeWorkingState (per context), resolved through a
{session, token} registry with a thread-local cached fast path.

Tests: MultiSessionTest (incl. two sessions x two contexts interleaved
with real clear/readback witnesses), ApplierOwnershipTest (with a
null-resolver negative control), ContextStateOwnershipTest,
ServerLoopEglTest native-context liveness; 99/99 of the acceptance set
green on the host build.

Known debts (documented in the design doc): twin registries still keyed
by handle alone, no per-record ContextSlot, process-global backend
generations, MakeCurrent dedup per current binding.
2026-10-01 19:16:41 -04:00
swung0x48 ebc552a048 [Fix] (WSI): report Wayland present failure and keep GLX drawables off XIDs
Wayland Present() failure now returns EGL_BAD_SURFACE instead of being
dropped. In spawn mode a GLX X drawable is backed by a remote pbuffer so
the XID is never mistaken for an Android ANativeWindow; X11 pixel present
and remote resize are still unimplemented.
2026-10-01 13:45:52 -04:00
swung0x48 6fb880cdf2 [Build] (Linux): alias libGL.so and libGL.so.1 to libMobileGL
GLX clients link against libGL; give the Linux build the same alias
treatment libEGL already had so glvnd-less clients can load MobileGL.
2026-10-01 13:45:51 -04:00
swung0x48 d52c031c50 [Docs] (Design): state ownership for multi-session/context/share-group (P14)
The whole stack assumes one process = one server = one session = one
context = one share group. The anland unified server needs that equation
broken in the library instead of fork-per-app workers. This document fixes
the four ownership layers (session, context, share group, thread current),
the wire changes (per-record context slot, CreateContext/DestroyContext
SurfaceOps, context-tagged reverse events), and the staged plan P14a-d
with acceptance tests.
2026-10-01 13:45:41 -04:00
swung0x48 4156ca6e36 [Docs] (Anland): hand off unified runtime work and record corrected state ownership requirements 2026-10-01 13:18:23 -04:00
swung0x48 2f64ede8c6 [Feature] (Remote): serve concurrent shared-memory clients in isolated workers 2026-10-01 12:53:27 -04:00
swung0x48 a5b463a5e8 [Feature] (Remote): expose a package-neutral embedded display server API 2026-10-01 12:31:57 -04:00
BZLZHH 32c79f4135 [Feature] (EGL): Wayland window surfaces, presented through wl_shm
A Wayland client's window is its own wl_surface, and what the compositor shows is the wl_buffer
attached to it - but the pixels of a MobileGL frame are drawn where the backend is, on a split
build in another process on another OS.  So far a wl_egl_window was taken for an X11 window and
its client-side address sent to the Android server (EGL_BAD_NATIVE_WINDOW at best), and under
glvnd a Wayland display never reached this vendor at all.

A window on a Wayland display - asked for as EGL_PLATFORM_WAYLAND, or a wl_display recognised by
its first word being wl_display_interface - is now a window surface in the EGL state at the
wl_egl_window's size with a pbuffer of that size behind it in the backend, whatever
MOBILEGL_IPC_SURFACE says.  At every swap the frame is read back out of the default framebuffer
through this library's own glReadPixels (the application's read framebuffer, pack buffer and pack
state put back as they were), its rows flipped and swizzled into a wl_shm buffer, and attached,
damaged and committed on the application's wl_surface.  Three buffers rotate on their release
events, dispatched on an event queue of the surface's own.  libwayland-client is loaded at run
time, so neither the build nor the library depends on it.  EGL_KHR_platform_wayland and
EGL_EXT_platform_wayland are announced on Linux.

Measured: glmark2-wayland runs its build scene at 13 FPS (400x300) and the window shows the frame
under kwin.  A window resize is not followed yet; the surface keeps its creation size.
2026-10-01 12:25:20 -04:00
gold f458924360 [Fix] (EGL): a second display is served, and glvnd gets its answers from one table
Two defects, both found by asking the client directly rather than by reading its log.

The backend refused to initialize any EGLDisplay but the first - "already bound to a different
EGLDisplay" - and the field it compared against has no reader anywhere else in the tree: the domain
rule was the whole of the rule.  What it cost was a compositor, which asks for the display it will
draw into AFTER probing another one.  The probe succeeded, the real display was refused, and the
caller reported the refusal one layer up as "no usable DRM render device" - a message that names
where it stopped, not why.

The second: glvnd asks a vendor for entry points from two places.  getProcAddress is how it builds
its own static EGL table, and getDispatchAddress is how it answers eglGetProcAddress for every name
it does not know itself - which is every extension entry point, the device queries among them.  This
side answered the second from a GL-only table, so an application asking for eglQueryDeviceStringEXT
got a null for a function the vendor implements.
2026-10-01 12:22:55 -04:00
gold 0dad3fcaa1 [Feature] (EGL): MobileGL serves EGL through libglvnd
On a system with libglvnd the libEGL.so.1 an application links is a dispatcher, not an
implementation, and the supported way to be the implementation it talks to is to be a vendor
library it loads and routes to.  On this machine it is also the ONLY way that reaches a caller which
does not link EGL directly: kwin goes through libepoxy, which dlopen()s libEGL.so.1 and dlsyms its
entry points at startup, so a library injected with LD_PRELOAD is invisible to it however loudly it
is named - measured, with the client in kwin's maps and not one call into it.

So this is the vendor half of that ABI: __egl_Main answers glvnd with the entry points by name, the
platforms this implementation serves, and the device it is associated with - the render node of the
machine, because that is what lets a compositor match its own node to the display it is about to
draw into, and because glvnd drops a vendor that does not answer at all.

Two details are the difference between a vendor that is loaded and a vendor that is used.  glvnd
requires setDispatchIndex to exist even when a vendor has nothing to record, which this one has -
its entry points are the implementation, they never re-enter glvnd.  And a vendor must hand over ITS
OWN entry points: with default visibility a reference inside this library resolves against the whole
process, where another libEGL that loaded first wins.  That is what -Bsymbolic-functions is for;
declaring the entry points protected says the same thing but does not compile against the platform
headers, which declare them default.
2026-10-01 12:22:55 -04:00
gold 75f83b20fc [Feature] (EGL): the platform and device extensions are one list, spelled once
A vendor string and an eglQueryString answer have to agree, and they are read by two different
parties: the system's libglvnd reads the first to decide which vendor a platform request should be
routed to, and an application reads the second before it decides to make that request at all.  Kept
apart they drift, and the drift has a signature - a library that is loaded, initialized, and never
called, while the caller reports that it found no provider for a function it never reached.

The list gains the device family for the same reason: a compositor asks which devices exist and
reads the DRM node off one before it asks for a display on it, so an implementation that answers
"no devices" is one it cannot use no matter how well the implementation draws.
2026-10-01 12:22:54 -04:00
gold 57781688ce [Chore] (glvnd): vendor the EGL vendor ABI headers
Two MIT headers from libglvnd, verbatim, because MobileGL is about to implement the vendor half of
glvnd's EGL ABI and the distribution ships the runtime without them.
2026-10-01 12:22:54 -04:00
swung0x48 7308668488 [Docs] (Anland): kgsl-as-server.md - read with the kernel as the server, routes A and B are about who owns the buffer (6.x + Mesa is A, 5.x is B), plus the split-level analogy to virtio-gpu native context vs virgl / venus; linked from the README 2026-09-30 13:00:11 -04:00
swung0x48 134791b4b9 [Docs] (Anland): compare today's container Mesa (freedreno / turnip over kgsl) with routes A and B - it is route A's no-change dma-buf tier with the renderer in the app process, Anland's dma-buf ingress was shaped around it, and 5.x's private protocol sat between A and B 2026-09-30 12:45:16 -04:00
swung0x48 c24926cafe [Docs] (Anland): add route B - Anland hands the window's Surface to the MobileGL server (the FCL shape: SC mode leaves the parent Surface's queue free, waylandbridge itself receives it over binder), with its MobileGL items B1-B4, the Anland extension, its limits, and the phasing reordered around it 2026-09-30 12:24:24 -04:00
swung0x48 c5fa722be7 [Docs] (Anland): correct the window model - a toplevel attaches on demand to at most one Android window and several can be attached at once (popups and subsurfaces composite into the parent); P12 does not fit because the Android window and its input belong to Anland, not because of the window count 2026-09-30 12:17:56 -04:00
swung0x48 b7aa8d2011 [Docs] (Anland): research note - what MobileGL and Anland would each need for MobileGL to render Anland's Linux apps (glibc split client in the container, bionic render server, native AHB ingress into waylandbridge), with the two source-audit reports, their briefs and the notes index entry 2026-09-30 11:57:57 -04:00
swung0x48 378020adf3 [Fix] (MG_Remote, P11 T0): the T0 POST also checks what the CPU writes through the held lock after the import and its first GPU use, in both directions - Mali-G1-Ultra passed the old single round (the import cleans the CPU cache) while its GPU saw 0/1024 post-import words, so T0 was granted and create-indirect-in-world-align1024 x Magma lost every Flywheel object on inproc/spawn (0.867/0.848); it now fails there and runs T2 (0.999984), and the Redmi passes both rounds and keeps T0 2026-09-30 10:21:46 -04:00
swung0x48 028cf4eb62 [Merge] (trace): take dev's create-indirect-in-world-align1024 fixture (a derived trace replacing the original create-indirect capture) and register its documentation-only derived_from key 2026-09-30 10:21:46 -04:00
swung0x48 08124c99f1 [Test] (trace): replace the create-indirect fixture with minecraft-1.21.1-neoforge-create-indirect-in-world-align1024, a derived trace and not the original capture - the desktop capture bound Flywheel's scatter lists at 16-byte SSBO offsets that Adreno 830 and Mali's Vulkan driver (64) reject, and fixture_tools/realign_buffer_ranges.cpp moves the 1,270 binds off a 1024-byte boundary onto 1024-aligned slots (3,812 calls inserted, target call 504631 -> 508443, host SSIM unchanged to 9 digits on both backends) 2026-09-30 10:00:26 -04:00
swung0x48 648800a2da [Docs] (P8): close the phase - ruling ID-P8-19 (the exit device check and the P12 cross-host rerun), the P8 README and PLAN, the roadmap row and header, MEASUREMENTS row 19, the status page and the last debt 2026-09-30 08:25:38 -04:00
swung0x48 b4bee9dd38 [Docs] (P8): CONTRACT-P8 (the rules P8 landed, each with its code anchor and its test), rulings ID-P8-14..18 (MD + MF and the create-indirect fixture's alignment, SE, SV, the dev merge, the wave 2 integration gate) and the debts wave 2 hands on 2026-09-30 07:55:50 -04:00
swung0x48 494305381e [Merge] (P8): integrate wave 2 package MF - the push flush ladder gets dev's Adreno 830 queued-ring-copy guard (FlushPendingRangesFrom, create-instancing x Espryt x monolith 0.870 -> 0.99998 on the Redmi), and both G5 scripts pin the two reviewed ladder bodies under ID-P8-14 2026-09-30 07:12:50 -04:00
swung0x48 fb106a682e [Docs] (P8-MF): the package note - the push-ladder mirror of dev f973008c, create-instancing Espryt monolith 0.870 -> 0.99998 on the Redmi with spawn unchanged, why spawn was unaffected (the same two partial maps behind queued ring copies, the copy already submitted by the server's intervening whole-buffer maps), the red-onces and the gate 2026-09-30 07:09:28 -04:00
swung0x48 c8c4397475 [Merge] (P8): integrate wave 2 package SV - Magma's 3D native mip blit uses one layer, the depth/stencil mip decline gets its tally row, 1D depth mips decline by name, and the wire arm issues glDispatchComputeIndirect natively
# Conflicts:
#	MobileGL/MG_IntegrationTest/CMakeLists.txt
2026-09-30 06:11:04 -04:00
swung0x48 9c24a14d2b [CI] (scripts, P8-MF): pin G5's tenth row as well and re-pin both flush ladders on the reviewed post-fix bodies (ID-P8-14) - dev f973008c moved FlushPendingRangesNow past every base ref CI passes (the Adreno 830 queued ring copy) and ac9ab078 mirrors it into FlushPendingRangesFrom; both scripts' self-tests now run the pin-precedence and one-token controls on both ladders 2026-09-30 05:45:10 -04:00
swung0x48 ac9ab078cc [Fix] (Espryt, P8-MF): the push arm's flush ladder keeps a partial range-invalidating map behind an upload-ring copy still queued on the store, as dev f973008c's pull arm does - FlushPendingRangesFrom's ring tier stamps ringCopyRetireSerial and InvalidateFlushAccessFor refuses tier 1 for a partial range while that copy is unretired (the whole-buffer orphan-map stays exempt); Adreno 830 loses the queued copy and create-instancing on the monolith arm drew terrain sections empty 2026-09-30 05:43:55 -04:00
swung0x48 377d90d9f1 [Merge] (P8): integrate wave 2 package SE - the spawn/tcp default framebuffer publishes its surface to the client, the CPU mip arm declines on a driver-written base, D's case 3 runs on the Espryt split arms, the unmigrated-emulation name list is linted against its call sites, and the P8 notes' citations point at their content again 2026-09-30 05:24:11 -04:00
swung0x48 4cfb29a553 [Docs] (P8-SV): the note's gate table (G1 0/0, wire-declines 61/66, split_coverage, every lane, retrace 230/236) and the dirty-surface scan finding 2026-09-30 05:02:37 -04:00
swung0x48 51e95c9260 [Docs] (P8-SE): the note's gate and retrace readings 2026-09-30 04:47:27 -04:00
swung0x48 982958ebb1 [Test] (P8-SV): rename WireIndirectDispatchScenario's dispatch store member m_parameters -> m_groupCounts - gen_pipe_dirty_surface.py reads writes to shutter member names across all of MobileGL, and the name made NEW_PATCH_STATE's shutter look written outside its roots (self-test negative control 16 stopped tripping) 2026-09-30 04:42:06 -04:00
swung0x48 3717424229 [Merge] (P8): merge dev f973008c - the Espryt partial-flush fix for a queued ring copy on Adreno 830 (wave 2 MD), and dev's empty-binding uniform block fix deduplicated against P11's 7784d6bb (one code path), UnboundUniformBlockScenario registered on the split arms 2026-09-30 04:34:15 -04:00
swung0x48 f973008c4d [Fix] (Espryt): a partial buffer flush no longer takes the range-invalidating map while an upload-ring copy into the same store may still be queued - Adreno 830 loses the queued copy, and create-instancing drew whole Embeddium terrain sections empty 2026-09-30 04:07:41 -04:00
swung0x48 edcb9328e2 [Docs] (P8-SE): cite the integration CMakeLists by path - the bare basename resolves to the top-level file 2026-09-30 03:55:20 -04:00
swung0x48 e61060fbdf [Docs] (P8-SE): the package note, and the P8 package notes' line citations re-anchored by content at HEAD (A 14, B 29, C 10, D 12, E 18 plus the copy-image-shadow-mirror line, F 4) 2026-09-30 03:55:01 -04:00
swung0x48 3ce8ce3cb1 [Test] (P8-SE): the ForcedDs knob lanes run the default-framebuffer depth/stencil case again on all three arms (it was subtracted while red on spawn / tcp) 2026-09-30 03:55:01 -04:00
swung0x48 394d255487 [Docs] (P8-SV): the package note - the four items with their before/after, the design, the gates, ten red-once rows, the monolith observation and what was found but not fixed 2026-09-30 03:45:40 -04:00
swung0x48 0222852b94 [Fix] (Espryt, P8-SE): a spawn / tcp server publishes the default framebuffer's depth/stencil format (it has no frontend placeholder; the case leaves pending-fix for the three arms); the server CPU mip arm declines by name (generate-mipmap-store-declined) instead of filtering a store the driver wrote over, and E's refused-readback knob reaches it (CpuMipRefused. entries); WireIndirectDrawScenario case 3 runs on the Espryt split arms; copy-image-shadow-mirror goes and unmigrated_emulation_sites.py holds PipeCatalogueTest's list to the call sites 2026-09-30 03:44:29 -04:00
swung0x48 2edb2aa82a [Test] (P8-SV): WireMipShapeScenario (3D chain on both backends x every arm, 1D depth decline on Magma split+spawn, the 3D chain under the validation layer inproc) and WireIndirectDispatchScenario (both backends x every arm, Magma gated .IndirectDispatch.), the MipmapDepthStencilAspect tally in F1's depth/stencil decline case, WireDeclinePeek, and the two parity rows 2026-09-30 03:44:03 -04:00
swung0x48 fc415aee76 [Fix] (Magma, P8-SV): the wire arm's 3D native mip blit uses one layer with the depth in its z offsets, the depth/stencil mip decline gets its WireDeclineTally row (MipmapDepthStencilAspect), the depth shader mip refuses 1D/1D-array/3D view types before the chain grows, and glDispatchComputeIndirect is issued as vkCmdDispatchIndirect from the wire store (P8-D's barrier rule, wdsp counter, two named declines) 2026-09-30 03:44:03 -04:00
swung0x48 307267c188 [Docs] (P8): the monolith defect count is 12 (13 reports, one duplicate between C and D), 9 of them left to P13, not 14 and 11 2026-09-30 03:24:47 -04:00
swung0x48 cbbaf77f6d [Docs] (P8): wave 1 integrated - rulings ID-P8-5..13 (the survey, packages A-F, the integration gate, the wave 2 scope and the monolith defects left to P13), the survey note, open questions 9/11/15 answered, the debts and the P8 contract amendments, the P8 README table, PLAN-P8 wave 2 and the status page 2026-09-30 03:09:09 -04:00
swung0x48 39cd8fb73b [Merge] (P8): integrate package E - a texture readback the Espryt driver refuses is answered from the split server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline texture-readback-store-declined); CopyImageStoreReadbackScenario on both backends x every arm plus the extent-scoped StoreRead knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-30 02:17:51 -04:00
swung0x48 92eab9f3b7 [Merge] (P8): integrate package D - the Magma wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] natively from the wire store (vkCmdDraw*Indirect[Count]) instead of a CPU expansion behind a GPU-idle wait, with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters and WireIndirectDrawScenario on both backends x every arm 2026-09-30 01:28:20 -04:00
swung0x48 9441071567 [Merge] (P8): integrate package C - the Espryt split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it (storage/atomic/buffer-image marks, XFB captures into the copy; restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead); GpuWrittenDrawInputScenario (12 cases) on the Espryt split arms 2026-09-30 00:45:16 -04:00
swung0x48 2f530ad8a2 [Merge] (P8): integrate package B - Espryt glGenerateMipmap on the server: the record's generated window (BASE/MAX clip, B1) and RGB16F/RGB32F natively or on the server's CPU from the driver's level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-30 00:01:29 -04:00
swung0x48 c00f5eabae [Docs] (P8-E): the note's gate and retrace readings and two residuals (the extent-scoped tcp knob, the ERROR line before a store answer) 2026-09-29 23:51:49 -04:00
swung0x48 24b8aef616 [Fix] (P8 A): exempt F's Magma-only F1WireScenario.GenerateMipmapWithoutAShaderArmShapeDeclinesAndKeepsTheSession on DirectGLES as single-backend - it landed with P8-F after A's exemption table was cut, skips on Espryt like its F1Wire decline siblings, and split_coverage.py named it missing on the merged head 2026-09-29 23:41:10 -04:00
swung0x48 8133cd52ae [Merge] (P8): integrate package A - register the monolith-only cases on split/spawn/tcp (P8Coverage.cmake, five server-knob tcp supervisors), gate them with split_coverage.py and its exemption table, pin the tcp supervisor's Vulkan env 2026-09-29 23:37:12 -04:00
swung0x48 06f28b0a1b [Docs] (P8-A): the note says what the census measured about the audit's 396 and DoublePrecision on tcp, and the knob-port comment names the split/verify one-port overlap 2026-09-29 23:18:08 -04:00
swung0x48 b9feb9d7be [Docs] (P8-B): the package note - the generated window and the server CPU arm, the coverage matrix, seven red-onces and monolith defects M1-M3 recorded for dev 2026-09-29 23:16:05 -04:00
swung0x48 2b4ae539da [Merge] (P8): integrate package F - reclassify what our client cannot send: the multi-draw latches and Magma client-vertex-array become ProtocolCorruption, Magma mip shader-shape becomes a pre-growth named decline, unpublished cap bits and the index-mirror sentinel marked reserved 2026-09-29 23:07:03 -04:00
swung0x48 e815ce8d33 [Docs] (P8-C): the package note - readers and writers of the server's staged copy, the twelve GPU-written draw-input cases, each red-once, the retrace subset and the five monolith defects recorded for dev 2026-09-29 22:53:18 -04:00
swung0x48 00d6c2480e [Docs] (P8-E): the package note - the split read never touched a shadow, the store route and FollowCopy, the red-once table, the monolith defect for dev 2026-09-29 22:43:02 -04:00
swung0x48 f823422bfa [Fix] (Espryt, P8-E): a texture readback the driver refuses is answered from the server's staged store, which now follows glCopyImageSubData (FollowCopy, client-box merge, sticky driver-written mark, named decline); CopyImageStoreReadbackScenario on both backends x four arms plus the extent-scoped store-read knob; the dead CopyImageSubData+RENDERBUFFER latch goes (census 79 -> 78) 2026-09-29 22:43:02 -04:00
swung0x48 e52a997f34 [Fix] (Espryt, P8-C): the split server refreshes its staged copy of a buffer a shader wrote before a CPU reader consumes it - storage/atomic/buffer-image writes mark the server twin, XFB captures land in the copy, the restart rewrite, multi-draw rebase, *IndirectCount, CPU-fed indirect fields, XFB scatter and fp64 narrowing read through SplitHostBytesForCpuRead; GpuWrittenDrawInputScenario (12 cases) on the split arms 2026-09-29 22:38:33 -04:00
swung0x48 c7a14ed790 [Test] (P8-F): give the mip shader-shape decline its own .ShaderMipDecline. tail - spawn_lane_parity.py holds each server-knob tail to exactly one case, and sharing .ShaderMip1. named two 2026-09-29 22:37:49 -04:00
swung0x48 d639e54616 [Perf] (P8-D, Magma): the wire arm issues glDraw*Indirect / glMultiDraw*Indirect[Count] as vkCmdDraw*Indirect[Count] from the wire store instead of expanding them on the CPU behind a GPU-idle wait (create-indirect split: 321 waits / ~8 s -> 0), with an INDIRECT_COMMAND_READ barrier once per shader write; windr[] PipeStats counters, WireIndirectDrawScenario on both backends x every arm, notes/p8/D.md 2026-09-29 22:32:49 -04:00
swung0x48 32f222dbc2 [Docs] (P8-A): the package note - the 304-case census per arm, the server-knob lanes and their supervisors, the exemption classes, the red-once record, lane wall times and the findings 2026-09-29 22:30:20 -04:00
swung0x48 a2c81a2f64 [Fix] (P8-A): the tcp lane's shared supervisor carries the Vulkan ICD and the three lavapipe iterationRP repairs, which Magma reads in the server - DirectVulkan.Tcp IterationRPProgram203 missed its golden output by one texel without them 2026-09-29 22:30:20 -04:00
swung0x48 8dac1fd8a4 [Fix] (Espryt, P8-B): glGenerateMipmap on the server - the record's plan is the generated window (B1) and RGB16F/RGB32F generate natively or on the server's CPU from its own level (B2), retiring both split generate-mipmap names; GenerateMipmapServerScenario on both backends x monolith/Split/Spawn/Tcp plus the forced-CPU arm 2026-09-29 22:22:55 -04:00
swung0x48 b71cb9cc2c [Docs] (P8-F): cite MG_Backend/Init.cpp by path - the bare basename is ambiguous 2026-09-29 22:15:45 -04:00
swung0x48 efd3533bd1 [Docs] (P8-F): the package note - the four items, their evidence, the red-once record and what was found but not fixed 2026-09-29 22:15:29 -04:00
swung0x48 67890f523a [Refactor] (P8-F): reclassify what our client cannot send - the multi-draw client-indices/instanced latches and Magma's client-vertex-array become ProtocolCorruption, Magma's mipmap-shader-format-or-shape becomes a named decline decided before the chain grows (with a knob-forced 3D case on split and spawn), and the three unpublished cap bits and the index-mirror sentinel are marked reserved 2026-09-29 22:15:29 -04:00
swung0x48 976fbe4cd4 [Test] (P8-A): register the monolith-only integration cases on the split, spawn and tcp arms (P8Coverage.cmake: 246 ambient cases, five server-knob lanes each on its own tcp supervisor), retire the ClientSideIndices and GuiBatch exclusions, and gate the rest with split_coverage.py and its exemption table 2026-09-29 22:14:31 -04:00
swung0x48 8d3e2317a6 [Docs] (P8): re-scope the phase - strike the original scope in the roadmap and the P8 README, add PLAN-P8 (six items closed, packages P8-0 and A-G, the rewritten gates) and rulings ID-P8-1..4, and mark P8 in progress on the status page 2026-09-29 21:31:15 -04:00
swung0x48 0b55b55d4d [Docs] (P11): the status page lists the decisions waiting on the user - the P8 start and the six debt rulings 2026-09-29 21:17:21 -04:00
swung0x48 642ac891af [Docs] (P11): ruling ID-P11-20 - the closing head's gate, lanes and retrace numbers 2026-09-29 21:13:12 -04:00
swung0x48 592558f5fc [Docs] (Debts): the debt review - drop the closed E1, DualBlock and GetCaps-blobref rows (open question 24 closed, the blobref rule now a CONTRACT-P5 row-18 invariant), move the unmeasured M2 note to MEASUREMENTS, rewrite HyperOS / P11 RSS / named refusals, split the P12 row, merge the two CI rows, add three B2 rows, and mark each destination as needing a ruling or ready 2026-09-29 21:12:28 -04:00
swung0x48 0a117243d5 [Docs] (P11): close the phase - rulings ID-P11-17..19 (B2 T0 zero-copy, the stale indexed binding and Magma empty-uniform fixes with the skip-hole, T0 on by default), B2-DEVICE, the P11 README, the roadmap row and header, the status page and the debts P11 hands on 2026-09-29 20:33:00 -04:00
swung0x48 344e2f8439 [Docs] (P11 B2): T0 is the default (the user, 2026-09-29) - CONTRACT-P11 B2's tier table, unset versus 0, the revision-5 fix-up; the knob rows in design/09 2026-09-29 20:15:29 -04:00
swung0x48 91900bdc5e [Test] (P11 B2): the unset knob on a stream and on a no-AHardwareBuffer shared-segment session - T2, no W/I fallback line, the fallback counted; the 0x80 ask accepted and 0x81 refused 2026-09-29 20:15:29 -04:00
swung0x48 b32140e614 [Feat] (MG_Remote, P11 B2): T0 is the default - an unset MOBILEGL_IPC_ADOPT_TIER asks T0 (ask 0x80) and falls back to T2 quietly (MGLOG_D, still counted: client T0Fallbacks, server T0Refusals), while an explicit 0 keeps every named line; revision 5's schema comment and pin fixed up 2026-09-29 20:15:29 -04:00
swung0x48 7030d307be [Test] (Magma): UnboundUniformBlockScenario - a dispatch and a draw whose program declares a uniform block at an empty binding point (never bound, or its buffer deleted while bound) run and produce their fallback, and a buffer bound there afterwards is read, on both backends 2026-09-29 19:58:41 -04:00
swung0x48 853c5f1264 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block instead of dereferencing a null buffer 2026-09-29 19:55:53 -04:00
swung0x48 e44d6ac7da [Docs] (P11 B2): CONTRACT-P11 - a buffer deleted while bound to an indexed point: the defect, the fix, Magma's behaviour before it, the tests with their red-once, and the G1 delta 2026-09-29 19:44:08 -04:00
swung0x48 8571e0c001 [Test] (P11 B2): DeletedBoundBufferScenario - a buffer deleted while bound to a storage, uniform, atomic-counter or transform-feedback point, its slot reused, then a dispatch and a draw, on monolith and Split/Spawn/Tcp for both backends; and the one-process arena pair as a regression 2026-09-29 19:32:19 -04:00
swung0x48 7784d6bbf7 [Fix] (Magma): a uniform block declared at an empty binding point reads a zeroed block on the monolith arm instead of dereferencing a null buffer, as the wire arm already does 2026-09-29 19:32:19 -04:00
swung0x48 4aec5621a8 [Fix] (MG_State, P11 B2): glDeleteBuffers moves the bind-point generation of every indexed target it unbinds, and a transform-feedback object swap moves it too - the server's window no longer names a deleted buffer's handle after its slot is reused 2026-09-29 19:32:19 -04:00
swung0x48 4c02279f22 [Docs] (P11 B2): CONTRACT-P11 B2 - the Espryt split/spawn itest entries now pin MOBILEGL_ITEST_REQUIRE_GPU themselves 2026-09-29 19:12:46 -04:00
swung0x48 e869d18ce8 [Test] (P11 B2): pin MOBILEGL_ITEST_REQUIRE_GPU=1 on every Espryt split/spawn itest environment, as the Magma and Espryt TCP lists do - a handshake-time Fatal fails there instead of skipping as a ctest pass 2026-09-29 18:57:04 -04:00
swung0x48 9d1e788597 [Docs] (P11 B2): CONTRACT-P11 B2 - M2 staying green on Espryt is consistent with the driver holding its own reference, not verified 2026-09-29 18:44:25 -04:00
swung0x48 ce5753c883 [Docs] (P11 B2): CONTRACT-P11 B2 - the tier table that replaces A1's shared-segment Fatal, revision 5, the Offer and its thread, POST, the staged-copy audit, lifetime, CPU reads, host and device gates with their red-once 2026-09-29 18:24:53 -04:00
swung0x48 5e65907d63 [Test] (P11 B2): RemoteClientAdoptT0.KnobTwoIsTodaysT2WithNoT0Line - the default asks 2 and neither side settles, imports or logs T0 even where the stand-in platform could run it 2026-09-29 18:20:25 -04:00
swung0x48 98bf1925aa [Test] (P11 B2): LargeArenaAdoptionScenario - a draw still queued when the arena is rewritten or respecified keeps the bytes it was issued against (no readback between the two draws, so an in-place write or an early free shows) 2026-09-29 18:06:48 -04:00
swung0x48 17f1c0b59b [Tool] (P11 B2): extmem probe --hold-ms - the T0S child keeps its AHardwareBuffer locked while the parent holds both imports, for the dmabuf memory calibration 2026-09-29 17:50:50 -04:00
swung0x48 2522f1803c [Test] (P11 B2): T0 protocol on the host - the Offer inbox (late, out of order, stale, missing) over both aux channels, the whole T0 path on a stand-in platform, A1's two handshake-Fatal cases rewritten as named fallbacks, revision 4 refused by name, AdoptTier0 lanes on the shared-segment arms and the allow switch through spawn 2026-09-29 17:50:50 -04:00
swung0x48 7cde55b1c1 [Feat] (Magma, P11 B2): import a T0 store through VK_ANDROID_external_memory_android_hardware_buffer as a wire buffer (external VkBufferObject), serial-gated release, done-no-bytes readback, and the sustained-lock POST on the renderer's device 2026-09-29 17:50:50 -04:00
swung0x48 2a69f61c88 [Feat] (Espryt, P11 B2): import a T0 store with glBufferStorageExternalEXT as the adopted twin - no staged copy, server writes queued GPU-ordered, CPU readers on the import's coherent map, fence-gated release, re-import after a context loss, and the sustained-lock POST in a throwaway context 2026-09-29 17:50:50 -04:00
swung0x48 590d74525d [Feat] (MG_Remote, P11 B2): T0 behind MOBILEGL_IPC_ADOPT_TIER - control revision 5 (LinkTerms.adoptTier ask), kCapAdoptT0 granted at the first native bind after the backend's POST, the AHardwareBuffer Offer on the aux socket matched to its map_persistent record by seq, named T2 fallbacks instead of A1's handshake Fatal, the server allow switch, done-no-bytes CPU reads 2026-09-29 17:40:56 -04:00
swung0x48 b57276b88a [Tool] (extmem_probe, P11 B2): route mode - the probe serves in the render server's place (-Pmobilegl.extmemProbeAsServer) and --route-client sends a held-lock AHB over B1's fd: pair, directly on aux and through a socketpair hop 2026-09-29 16:44:37 -04:00
swung0x48 ac25e3323a [Tool] (extmem_probe): build inside the API-26 trace APK - memfd_create through syscall() below API 30, the two Vulkan 1.1 calls through vkGetInstanceProcAddr below API 28 2026-09-29 16:31:52 -04:00
swung0x48 96881f639c [Build] (P11 A3): MOBILEGL_BUILD_EXTMEM_PROBE / -Pmobilegl.buildExtmemProbe packages tools/spikes/extmem_probe as libMobileGLExtmemProbe.so in the trace flavour 2026-09-29 16:31:52 -04:00
swung0x48 16ea23c927 [Tool] (extmem_probe): marker/args-file mode for the trace APK exec hook, --regions-at-end, and the T0S sustained-lock rows (Vulkan copy/fill + GL compute on a never-unlocked client AHB) 2026-09-29 16:31:52 -04:00
swung0x48 b9b14c4775 [Docs] (P11): status page - the two handed-on dev bugs are merged, the gate row points at the B1 and M2 rulings 2026-09-29 16:14:34 -04:00
swung0x48 37c7aede31 [Docs] (P11): ruling ID-P11-16 on B1 (fd: endpoint, token broker pairing at hand-off, app_process helper, per-peer apply affinity, the HyperOS battery requirement, the device pass), the B1 debts, the status page, the phase README and the roadmap row 2026-09-29 16:14:18 -04:00
swung0x48 64b5794bc9 [Docs] (P11 B1): CONTRACT-P11 B1 - under the EOF Hello wait a second client slower than 2 s sees a send failure rather than Refuse{Busy}, and the unix-reach argument is the device's 2026-09-29 16:10:45 -04:00
swung0x48 ce4dca2835 [Docs] (P11 B1): CONTRACT-P11 B1 - the rebased head's device pass: rd12 shm vs tcp on both routes, Magma openra re-measured clean, the 10 s slow start and the plugin flavour 2026-09-29 16:06:30 -04:00
swung0x48 df826f426f [Docs] (P11 B1): CONTRACT-P11 B1 - the broker pairs at hand-off, a paired unix session waits for its Hello until EOF (the decision and its file:line), the lone-prime guard, the supervisor's announced core as the one source, and the follow-up gates 2026-09-29 15:03:52 -04:00
swung0x48 7ccb920ad2 [Feat] (Plugin, P11 B1): the broker presents the PairBind pair at hand-off (PairBindFrames, the encoder's bytes) and answers v2 with that flag and the apply core its supervisor announced; the helper passes MOBILEGL_IPC_FD_PAIRED=1 on, and Java no longer re-derives the reserved core 2026-09-29 15:03:52 -04:00
swung0x48 582adb0dd6 [Test] (P11 B1): the broker's PairBind templates against the encoder, a pair presented at hand-off whose Hello comes 5 s later, a pair presented only at program start refused by name, and the reserved core on lone-prime, symmetric and unreadable topologies 2026-09-29 15:00:30 -04:00
swung0x48 578795c84d [Feat] (MG_Remote, P11 B1): pairing at hand-off - the fd: endpoint presents no PairBind when MOBILEGL_IPC_FD_PAIRED=1 says the broker did, names the server's refusal when its own PairBinds arrive too late, and a paired unix session waits for its Hello until EOF instead of 10 s 2026-09-29 15:00:30 -04:00
swung0x48 9af4c8aefd [Feat] (MG_Remote, P11 B1): the reserved apply core never takes a lone prime core - several prime cores give the lowest, one gives a big core beside it or none, and the log names which - and the supervisor announces the core it reserves on stdout at start-up, the one source the broker relays 2026-09-29 15:00:30 -04:00
swung0x48 5a97eb8d1b [Docs] (P11 B1): CONTRACT-P11 B1 - anchor the evidence paths to ~/w7/notes/p11/evidence/b1 and the report 2026-09-29 14:47:31 -04:00
swung0x48 fc427a14d6 [Docs] (P11 B1): CONTRACT-P11 - the external-client route (helper, token broker, fd: endpoint), the token rule and the named refusals, the apply-thread policy by peer and plane with its selection measurements, the keep-alive setting users must make, the ROM dependence and the gates 2026-09-29 14:47:31 -04:00
swung0x48 54e7c92cf4 [Feat] (Plugin, P11 B1): the broker hands the helper the core the server reserves for the session's apply thread and the helper execs the program through taskset on every other core (--keep-affinity opts out), so a client the scheduler would put there is not starved 2026-09-29 14:47:31 -04:00
swung0x48 1abddf0d00 [Test] (P11 B1): ApplyThreadPolicy - every row of the auto table by peer and plane, the Redmi topology, the shapes with no prime pair to split, explicit masks, spins and unrecognised text 2026-09-29 14:47:31 -04:00
swung0x48 0819f9d410 [Feat] (MG_Remote, P11 B1): the apply thread's auto affinity and spin chosen per session from the peer's dial mode and data plane - a dialled-in shared-segment client leaves the apply thread only the lowest prime core, stream, forked and in-process peers keep every big core 2026-09-29 14:47:31 -04:00
swung0x48 952a1ca4a7 [Tool] (P11 B1): trace flavour gate tooling - package the retrace CLI as libMobileGLRetrace.so (packageRetraceExe) and run an external client as a child of the app kept in front (mobilegl_exec_argv) 2026-09-29 14:47:31 -04:00
swung0x48 bd05e061fc [Feat] (Plugin, P11 B1): same-device external clients - the exported token-checked broker that connects twice to the server's own endpoint and answers every request, the app_process helper that execs the program on the pair, a private broker listener beside a TCP-only supervisor, and the copyable launch command on the server screen 2026-09-29 14:47:31 -04:00
swung0x48 23a9470a28 [Test] (P11 B1): the fd: endpoint end to end against a real server process after two bare connects, its PairBind pair read off the server side, and its named refusals that write nothing 2026-09-29 14:47:31 -04:00
swung0x48 53098bc57f [Feat] (MG_Remote, P11 B1): the fd:<control>,<aux> client endpoint - adopt two connected unix sockets a broker opened for this process, refuse by name anything that is not a distinct connected AF_UNIX stream pair, and present the client's own PairBind pair before the Hello, over shared segments 2026-09-29 14:47:31 -04:00
swung0x48 76f75f3a0c [Docs] (P11): ruling ID-P11-15 on the M2 dev merge - the scattered-upload fix completed on the split arms, the trim fence re-audited on the split server, the census and ratchet notes; DEBTS closes the two merged dev bugs and takes M2 leftovers 2026-09-29 13:46:37 -04:00
swung0x48 d587d394fe [Docs] (P11): zero-copy stays switchable (ruling ID-P11-14) - MOBILEGL_IPC_ADOPT_TIER 2 (default) keeps today shared-memory push, 0 takes the T0 import, and an unusable T0 falls back to T2 with a named line; the server gets its own allow switch 2026-09-29 13:45:57 -04:00
swung0x48 de996d539f [Test] (P11 M2): DescriptorPoolGrowthScenario waits for the inproc server to apply every record it emitted before each census read - under run-ahead a census right after the burst's swap read the pools before the burst, or a trim a frame late 2026-09-29 13:29:38 -04:00
swung0x48 b4b1825951 [Docs] (P11 M2): TextureEmit.h and the applier's accumulation name the one box-only record that is not a whole level - the oversize fallback's withheld cover, a partial box spanning gaps 2026-09-29 12:36:41 -04:00
swung0x48 ec5f6ead3f [Test] (P11 M2): TextureEmit.MoreThanKMaxDirtyRectsCollapsesToTheBoxWithRegionCountZero keeps its name and pins the one collapse left - a footprint larger than a record carries falls back to the storage's rect list 2026-09-29 12:26:10 -04:00
swung0x48 f636a4cdae [Test] (P11 M2): the link ratchet records TextureObjectWithOneMipmap::GetStorageDirtyFootprint - dev 9524c688's new virtual on the placeholder vtable UniformManager already reaches (171 -> 172) 2026-09-29 12:26:10 -04:00
swung0x48 bd2e43ede3 [Test] (P11 M2): dev's two Magma scenarios on the split arms - both on Espryt's three arms, OversizedRecordingSplitScenario on Magma's three (and pinned to a 64-draw budget on split and spawn, where the server reads it), DescriptorPoolGrowthScenario's census cases on Magma's split arm only; the split-recording arming reads both role logs and dev's pinned lane holds its log's RESOURCE_LOCK, and the census cases count the sets the wire arm holds between its descriptor rewinds 2026-09-29 12:20:15 -04:00
swung0x48 e2333d98a8 [Fix] (Magma, P11 M2): DirectVulkan::GetDescriptorPoolCensus reads the server renderer's pools on the apply thread under inproc (the test probe, between drain batches) instead of racing its growth, rewinds and trims, and answers unavailable on spawn and tcp 2026-09-29 12:20:15 -04:00
swung0x48 5c9ad5b499 [Test] (P11 M2): RemoteGuards pins GetStorageDirtyFootprint on the apply thread as Fatal{RoleViolation, "texture-legacy-arm"}, beside the rest of the dirty model's rows 2026-09-29 12:16:55 -04:00
swung0x48 9145433890 [Fix] (P11 M2, Espryt, Magma): the texture emitter carries a level's exact dirty footprint as its regions (dev 9524c688) - a single partial box as one region, only a whole-level write as none - so both servers upload exactly the texels written over GPU content; the applier keeps it exact up to 4096 regions, and Espryt's handle arm applies the storage's box-or-rects cut-offs itself while its texture holds no GPU writes 2026-09-29 12:16:55 -04:00
swung0x48 c371599811 [Merge] (P11): dev@c14283ee - scattered sub-image writes upload exactly their texels, Magma submits a frame's recording every 16384 draws with backpressure, and Magma grows descriptor pools geometrically and trims a quiet slot whole; the handle arm notes driver-side writes by handle and the wire arm splits recordings and keeps the trim's epoch peak across descriptor rewinds 2026-09-29 12:11:37 -04:00
swung0x48 b34eed580d [Docs] (P11): HSPIKE - an app_process helper with no Context gets connected sockets to the server app over Binder under the shell uid and another app uid, token-checked; ruling ID-P11-13 on the single broker mechanism and the ROM caveat 2026-09-29 11:42:59 -04:00
swung0x48 c14283ee2d [Docs] (Magma): the trim threshold's comment states the bound its argument proves - a growth adds at most twice what the slot held, so the slot stays over a third full 2026-09-29 10:58:33 -04:00
swung0x48 9dccabf03c [Test] (Magma): descriptor pool growth and trim - the pure sizing policy adds O(log N) pools for an N-set frame, and DescriptorPoolGrowthScenario's 4096-distinct-set frame lands whole on both backends, grows at most 7 pools, and is given back after quiet frames and re-grown while they are still in flight 2026-09-29 10:34:16 -04:00
swung0x48 53fa16c2c2 [Fix] (Magma): a frame slot whose descriptor pools stay under a quarter full for 120 frames is handed back whole - its set cache cleared, grown pools destroyed, base pool reset and every memo holding a set handle dropped - so rd12's loading burst no longer lives for the whole process (live sets 15,042 -> 39, pools 65 -> 6); MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES (0 = never) and a test-visible pool census 2026-09-29 10:34:16 -04:00
swung0x48 c2de6576ad [Fix] (Magma): a frame slot's next descriptor pool doubles its LARGEST pool of the same update-after-bind flavour, capped at 4096 sets - doubling the first (oldest, smallest) one made every grown pool the same size, so rd12's 15,016-set loading frame grew one slot to 60 pools (1x128 + 59x256); now 9 (64 ... 4096, 4096, 4096) 2026-09-29 10:34:15 -04:00
swung0x48 35e1c861a3 [Docs] (P11): ruling ID-P11-12 on pairing by PairBind nonce and revision 3 -> 4; B0 F2 corrected - the product TCP path already paired by identity, the defect was unix endpoints and the one-shot TCP server; the TCP fixture comment no longer names AcceptPair 2026-09-29 10:20:53 -04:00
swung0x48 9ebcf5a73d [Docs] (P11 PAIR): CONTRACT-P11 - the PairBind wire change and revision 3 -> 4, the acceptor's rules, cross-revision behaviour, the gates and the red-once 2026-09-29 10:19:59 -04:00
swung0x48 05dc94fa38 [Test] (P11 PAIR): PairAcceptorTest - a stray before a client, four interleaved connections, an orphaned aux past the budget and a Hello-first client, each on unix and tcp - and the device's probe-then-client case end to end in ServerSpawnTest 2026-09-29 10:19:59 -04:00
swung0x48 abe29eae2d [Fix] (MG_Remote, P11 PAIR): a socket client's control and aux connections pair by the nonce each presents first, not by arrival order - a readiness probe or a second client can no longer take half a session, and a client that sends its Hello first is refused by name 2026-09-29 10:19:59 -04:00
swung0x48 0a6af63655 [Feat] (P11 PAIR): the PairBind control message - the client's nonce and which of its two connections this is, the first frame on each - with the control revision 3 -> 4 and its pin 2026-09-29 10:19:59 -04:00
swung0x48 32b81dae42 [Docs] (P11): the two dev bugs P11 handed on are fixed on dev (Espryt scattered uploads 9524c688; Magma rd12 command-buffer growth d20c2d01) and arrive with the next dev merge 2026-09-29 09:52:37 -04:00
swung0x48 9c32d9fa68 [Test] (Magma): OversizedRecordingSplitScenario carries state across the split - a depth occluder, a stencil mark, an additive blend destination, a 4x multisample target resolved after it, occlusion and primitives-generated queries and a transform-feedback capture left open across it, each checked on both backends and in the budget-64 lane 2026-09-29 09:44:58 -04:00
swung0x48 4d3d677a7f [Test] (Magma): OversizedRecordingSplitScenario - a 1024-draw frame, one pixel and one uniform value per draw, lands whole on both backends; the DirectVulkan.SplitRecording lane pins the draw budget to 64 and asserts the split fired 2026-09-29 09:44:58 -04:00
swung0x48 d20c2d01ae [Fix] (Magma): a frame's recording is submitted every 16384 draws/dispatches and continues on a fresh command buffer, with at most frames-in-flight + 1 submissions outstanding - rd12's 981k-draw loading frame no longer grows one command buffer until Adreno's 16 KiB driver mappings exhaust vm.max_map_count (MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER, 0 = unbounded) 2026-09-29 09:44:58 -04:00
swung0x48 2fcde6eb9b [Docs] (P11): B0-CROSS-APP - SELinux refuses a foreign context the connect to the server app unix endpoint, past a brokered connect the shared segments work unchanged, default placement makes shm slower than tcp; ruling ID-P11-11 on what B1 must build, and the HyperOS uid freeze as a debt 2026-09-29 09:34:26 -04:00
swung0x48 21c7e8cd63 [Test] (MG_IntegrationTest, MG_Test): scattered writes over a GPU clear on both backends - two distant 2x2 boxes, a hundred boxes, boxes filling 3/4 of their union and three quadrants spanning the level, the 3/4 and level-spanning cases again at the image-bindable re-mint, and a MipmapStorage footprint fuzz against a reference bitmap 2026-09-29 09:06:07 -04:00
swung0x48 9524c688ca [Fix] (Espryt, Magma): scattered glTexSubImage writes upload exactly their texels - MipmapStorage keeps an exact dirty footprint beside its rect list (a write log once a merge or a fold would widen the list past the writes), Magma always uploads it, Espryt uploads it box by box (through the unpack ring too) once the driver texture may hold GPU writes and merges it at the image-bindable re-mint; the union box, the 3/4 cut-off, the 96-rect fold and a level-spanning union no longer put the stale shadow over GPU-written texels (two 2x2 writes over a GPU clear: 41 of 64 texels red -> 0) 2026-09-29 09:06:07 -04:00
swung0x48 6481691d1e [Docs] (P11): the status page, the roadmap row and the phase README for P11 in progress, and ruling ID-P11-10 on absorbing origin@86dc38a0 2026-09-29 08:06:38 -04:00
swung0x48 9176dcff3d [Merge] (P11): origin feat/disaggregated@86dc38a0 - its eglSwapInterval port is dev c60639db (already merged) plus ForgetRequestedSwapInterval, so an ended Magma server session no longer hands its interval to the next 2026-09-29 08:05:57 -04:00
swung0x48 26de2b60fc [Docs] (P11): rulings ID-P11-1..9 with the integration gate, A-DEVICE (T0 is GO from the app domain on both backends; adoption buys RSS not frame time on the Adreno; split T2 costs RSS and p99), the debts the dev merge closes and the ones P11 hands on, and W2-REMINT marking the monolith re-mint fixed 2026-09-29 08:03:09 -04:00
swung0x48 82467823ef [Docs] (P11 C): the contract's A1/A2 sections, Config.h's GPU-ordered landing, Init.cpp's DirectVulkan resource table, and the stream refusal CONTRACT-P6 / design 06 / 07 claimed now cites its code 2026-09-29 07:23:41 -04:00
swung0x48 2a902cbd79 [Test] (P11 A1/A2): LargeArenaAdoptionScenario asserts the tier its arena landed in and reads the tcp AdoptTier0 refusal; registered on Split/Spawn/Tcp for both backends (Espryt through the split-arms macro, Magma's Arena. block and Buffers lane), the monolith lanes declare adopted 2026-09-29 07:23:41 -04:00
swung0x48 3c72e5c637 [Fix] (P11 A1): settle MOBILEGL_IPC_ADOPT_TIER at the handshake from the data plane - a stream refuses T0/T1 by name and runs T2 on either side, a client over shared segments dies by name before any record, the at-use check trusts the settlement; unit gates on a real tcp session and a real inproc handshake 2026-09-29 07:23:41 -04:00
swung0x48 1797db0070 [Docs] (P11): B0 drives the cross-app client first as a retrace executable run from adb shell, then re-checks the connection and fd passing from a retrace client inside an app, the domain a Termux-style client really runs in 2026-09-29 07:23:41 -04:00
swung0x48 01a33dfc58 [Docs] (P11): B0 tests the cross-app client as a retrace client in one app against a render server in another - no Termux route; the fallback channel is a bound Service in the server app handing over a socketpair end 2026-09-29 07:23:41 -04:00
swung0x48 93a72678ca [Docs] (P11): B is scheduled - the consumer is a GL program started from another app on the same device (Termux and the like); B0 probes a cross-app fd channel, B1 moves such a client onto shared-memory segments, B2 adds the T0 import 2026-09-29 07:23:41 -04:00
swung0x48 3cb3e32b87 [Test] (P11 M): ImageBindableRemintScenario pins a barrier before its imageLoad copy - the kept-allocation case read the pre-clear upload once on push-monolith under load, the ID-P9-11 shape 2026-09-29 06:47:11 -04:00
swung0x48 89f1e6cfba [Test] (P11 M): P4aFinalFixScenario expects no re-mint count on the monolith arm either - dev's 568090f0 keeps an immutable, unwidened allocation there as the server already did 2026-09-29 06:47:10 -04:00
swung0x48 2c7d99d5e8 [Test] (P11 M): the server half of a split eglSwapInterval - Magma's backend object carries the request into its table's SetSwapInterval (null before dev@c60639db) 2026-09-29 06:33:36 -04:00
swung0x48 9cde08064c [Test] (P11 M): dev's ImageBindableRemint and PartialUploadOverGpuContent scenarios on the Split/Spawn/Tcp arms of both backends, three scattered-upload cases for the handle arm's carried layout, and TextureRemintPullScenario's monolith Espryt skip retired 2026-09-29 06:30:54 -04:00
swung0x48 48a306025d [Fix] (Espryt, P11 M): the push builds' monolith re-mint merges the applier's pending boxes over the GPU readback - the client clears its dirty flags at emission there, so a pending glTexSubImage was read back over and lost 2026-09-29 06:30:54 -04:00
swung0x48 e0102bad8c [Merge] (P11): dev@c60639db - the Espryt image-bindable re-mint reads GPU levels back on the monolith arm too, partial writes into converted or reshaped levels upload only their region on every arm, Magma orders an upload after the recording's writes and queued clears, and Magma's eglSwapInterval reaches the swapchain 2026-09-29 06:15:56 -04:00
swung0x48 ffeed0da3a [Docs] (P11): the re-scoped plan - each roadmap item against the tree at 74ae652e; A makes the adopt tier honest (client-side verdict, the Stream refusal, a tier assertion, three arms) and re-runs open question 3 and the baseline on the device, B (T0) is conditional, C closes by docs 2026-09-29 05:52:51 -04:00
swung0x48 86dc38a07e [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes, and an ended server session forgets its interval (MC 26.3 inproc vsync on: flat 120) 2026-09-29 05:52:22 -04:00
swung0x48 c60639db1d [Fix] (Magma): eglSwapInterval reaches the swapchain - 0 picks IMMEDIATE, else MAILBOX, else FIFO; >= 1 FIFO; < 0 FIFO_RELAXED, else FIFO; rebuilt after the next present when the mode changes (MC 26.3 vsync on: 190-290 fps -> 120) 2026-09-29 05:51:18 -04:00
swung0x48 74ae652ecd [Docs] (P10): close the phase - CONTRACT-P10 sections 2-3, rulings ID-P10-1..14 with the integration gate, C-MEASUREMENTS (device fence bench, loopback credit 1/2/3, the retrace round-trip census), the roadmap row, the debts P10 hands on, design/09, the phase README and the status page 2026-09-29 05:29:00 -04:00
swung0x48 414dd09bda [Test] (P10): the fence bench comment - the replay corpus does carry timed fence waits (improved-transparency-minecraft-26.3), the bench isolates the shape 2026-09-29 05:29:00 -04:00
swung0x48 47bbf5e6fd [Docs] (P10 A): CONTRACT-P10 section 1 - the fence-report protocol, the present-time report, the gates and red-once - with the re-scoped plan and A-FENCE-POLL, the paired host bench and the llvmpipe finding 2026-09-29 04:34:16 -04:00
swung0x48 73089cf2b3 [Test] (P10 B): the caps mirror's parallel-shader-compile entry is the client's switch in both directions - the extension-list read-back case pins compilation inline, and a new case holds the reconcile (red once with it removed) 2026-09-29 04:34:02 -04:00
swung0x48 80d205c401 [Refactor] (Remote, P10 B): the emit table's SetSwapInterval slot is the server forwarder - SetSwapInterval_Unmigrated and the BackendObject_Remote override deleted, class C empty (69 emitted / 0 unmigrated), and a unit control that the slot crosses as one control frame 2026-09-29 04:34:02 -04:00
swung0x48 4373964443 [Test] (P10 B): AsyncCompileScenario (AsyncOn/AsyncOff/OptimisticShaderStatus), XfbPrimitiveQueryScenario and PrimitivesGeneratedNoXfbScenario on the split arms of both backends - Espryt Split/Spawn/Tcp, Magma Split/Spawn/Tcp gated, PrimGenReroute split+spawn only 2026-09-29 04:34:02 -04:00
swung0x48 2271f6c1ec [Fix] (Remote, P10 B): GL_KHR_parallel_shader_compile follows the client's compile switch - the caps mirror re-derives it on adoption, so a tcp server started with another MOBILEGL_ASYNC_SHADER_COMPILE no longer decides the client's string 2026-09-29 04:34:02 -04:00
swung0x48 8d0a1b6ca5 [Test] (P10 A): PacedFrameFenceWaitsBench - the per-frame timed wait on an older frame's fence as an opt-in measurement (MGITEST_FENCE_PACE_BENCH, _LAG) on both backends 2026-09-29 04:29:49 -04:00
swung0x48 625f4db47d [Feat] (MG_Remote, P10 A): a present reports the fences finished so far before it returns the credit, so the client the credit releases finds the fence of a frame ago already known 2026-09-29 04:29:49 -04:00
swung0x48 b1d60523e4 [Fix] (Magma): an upload into an image the open recording already wrote, or that still has a queued clear, is submitted after that work - a glTexSubImage after a clear or a draw into the level no longer ends up under it 2026-09-29 04:28:55 -04:00
swung0x48 8966d5c294 [Fix] (Espryt, Magma): a partial write into a level whose upload is converted (widened RGB formats, Magma's RGB->RGBA expansion) or reshaped (Espryt 1D arrays) uploads only its dirty region - the rest of the stale shadow no longer lands over GPU-drawn texels 2026-09-29 04:28:22 -04:00
swung0x48 bcff9b00cc [Feat] (MG_Remote): fence polls are answered from the server's reports - the apply thread posts kEventFenceSignaled as the GPU finishes each client fence (and submits and re-checks while idle), glClientWaitSync(0) / glGetSynciv answer locally, and only every MOBILEGL_IPC_POLL_ESCALATE-th unanswered poll crosses 2026-09-29 03:59:44 -04:00
swung0x48 de33d40173 [Docs] (P9): close the phase - the contract's remint section and invariants, rulings ID-P9-11..12 with the integration gate, the roadmap row, the debts P9 hands on, the phase README and the status page 2026-09-29 03:36:45 -04:00
swung0x48 7aa00761f8 [Docs] (P9 W2): W2-REMINT - no pull protocol, the census, the coverage matrix and the red-once record; design/04 8.4, the P5c/P5e contract amendments, open question 2 and the P7 census attribution 2026-09-29 03:24:03 -04:00
swung0x48 aa0c242b51 [Test] (P9 W2): TextureRemintPullScenario - fourteen re-mint cases on both backends x monolith/Split/Spawn/Tcp, and the forced-readback-failure fallback entries 2026-09-29 03:23:54 -04:00
swung0x48 6c2f8a560c [Fix] (Espryt, P9 W2): a texture re-mint never needs the client - read back only levels the driver holds, replay the staged level when the readback is refused, count trp on the server path, and retire the texture-remint-pull markers and OnTexturePullRequest (callbacks 9 -> 8) 2026-09-29 03:23:54 -04:00
swung0x48 568090f09f [Fix] (Espryt): the image-bindable re-mint reads every level back from the driver texture before replaying it, so GPU-written levels survive the first glBindImageTexture - iris-derivative SSIM 0.996196552 -> 0.996376198 2026-09-29 03:06:45 -04:00
swung0x48 738b29f3e4 [Docs] (P9): the contract's first three sections (pack-buffer readback, graded log forwarding, the glGetError observation point), both fault injections, the W1 note with its red-once evidence, and rulings ID-P9-1..10 2026-09-29 03:03:03 -04:00
swung0x48 a7a82ffee5 [Test] (MG_Remote): P9 W3 - LogForwardTest leaves no role logs in /tmp on a green run 2026-09-29 02:42:20 -04:00
swung0x48 24560ffe81 [Fix] (MG_Remote): P9 W3 - a FATAL forward waits once per stall rather than once per line (MGLOG_F also marks survivable conditions), and F2 proves the flood got through on a live connection, inside TCP_USER_TIMEOUT 2026-09-29 02:42:20 -04:00
swung0x48 6bcfb1fba1 [Feat] (MG_Remote): P9 W3 - the server's log forward carries each line's level and never blocks a logging thread (<=WARN dropped and counted, >=ERROR lossless and paced, FATAL a bounded flush), fault injection F2, and the producerless OnLog callback deleted (9 -> 8) 2026-09-29 02:42:20 -04:00
swung0x48 362daafeed [Fix] (MG_Remote): the pack-buffer readbacks check their whole shape before the read and refuse it at one latch site per sink, each with a raw-peer control, and a verb counts as statically barriered only when every op that stamps it waits 2026-09-29 02:21:36 -04:00
swung0x48 6485ae1dc0 [Feat] (MG_Remote): a read into a pixel pack buffer crosses as read_pixels_to_buffer / get_texture_image_to_buffer and waits for nothing - the server lands the rows through the buffer-write gate on both backends, the client marks the buffer GPU-written, and a device lost with the write still owed ends in Fatal{ReadbackDeclined, "buffer-writeback"} 2026-09-29 02:02:29 -04:00
swung0x48 ad705a7392 [Docs] (P9): the implementation hand-off - the roadmap row checked against the tree (half of it is done, one design premise changed), five work packages and two fault injections 2026-09-29 01:23:58 -04:00
swung0x48 acfd367852 [Docs] (P12): the FCL fork's keep-running setting is committed and pushed 2026-09-29 01:10:19 -04:00
swung0x48 743f4e8e6e [Docs] (P12): close the phase - the contract, the disposition of every leftover item, a G1 line that only compares same-machine builds, and the doc citation the docs reshuffle broke 2026-09-29 01:05:45 -04:00
swung0x48 6e35ea2c02 [Docs] (P12): the portrait-window clipping was the 0x0 window request, not Magma - root cause, fix and evidence 2026-09-29 00:54:46 -04:00
swung0x48 c932af7f3c [Fix] (EGLImpl): a server-owned window surface with no EGL_WIDTH/HEIGHT asks for the native window's size, so a landscape game is no longer clipped in a portrait server window 2026-09-29 00:54:46 -04:00
swung0x48 8a71ea5954 [Docs] (P12): exit gate (a) passes - FCL's Minecraft renders into the render server window on both backends and killing the server latches device-lost in tens of milliseconds 2026-09-29 00:27:29 -04:00
swung0x48 984e2b69f4 [Feat] (tools): the FCL exit-gate driver - Minecraft renders into the render server window, then the server is killed 2026-09-29 00:27:28 -04:00
swung0x48 f2ad2fae9b [Fix] (MG_Remote): once the device-lost latch has spoken, declined verbs no longer log one ERROR line each - an FCL game wrote 113,080 of them in 48 s 2026-09-29 00:27:28 -04:00
swung0x48 27cbc53241 [Docs] (P12): exit gate (b) passes - a WSL client replays the in-world trace over Wi-Fi TCP at ssim 0.999883; the load frame is bandwidth-bound, the steady state is latency-bound 2026-09-28 23:22:06 -04:00
swung0x48 b60da6729d [Feat] (tools): the cross-host acceptance driver and its report reducer, and a note that netbench's nc sink understates the link 2026-09-28 23:22:05 -04:00
swung0x48 b4f0c53426 [Docs] (Disaggregated): the cross-host eglMakeCurrent hang was the host's xray TUN proxy stalling its relay, not MobileGL; direct Wi-Fi runs pass and are bandwidth-bound 2026-09-28 22:57:20 -04:00
swung0x48 429b4472bb [Fix] (tools): cross-host runs first prove the WSL -> phone TCP path is end to end, and netbench's pkill no longer kills its own shell 2026-09-28 22:57:20 -04:00
swung0x48 221a8144c2 [Docs] (Disaggregated): sync stage progress and roadmap with the five-arm perf comparison and the cross-host hang blocker 2026-09-28 22:33:53 -04:00
swung0x48 1987bdff5c [Feat] (Android): the render server screen and binary ship in the plugin APK too, not just trace 2026-09-28 22:10:56 -04:00
swung0x48 2fa58940e4 [Fix] (ci): the trace APK is a launchable app; assert exactly one launcher activity
The "Trace APK must not expose a launcher activity" check predates the render-server
screen: 1f72597dd gave the trace APK a launcher on purpose and 884f6aac9 moved the entry
icon back to PostActivity with a bottom navigation bar between the two screens, so the
apk.yml build job has been red ever since - taking the whole retrace matrix down with it
(skipped, not failed), which hid the distinction.

What is left of the old isolation contract is the count: exactly one
android.intent.action.MAIN in the merged manifest (a second one means a screen grew a
launcher entry by accident), and it belongs to PostActivity. The fclPlugin metadata ban is
unchanged.
2026-09-28 21:54:16 -04:00
swung0x48 35dddc0c89 [Fix] (MG_Remote): the socket-io stats are LinkMetrics free functions, not StreamLink members
6bccf98d7 put TakeSendStats/TakeReadStats on the concrete StreamLink class, so
ClientSession.cpp and ServerLoop.cpp had to include <MG_Remote/Transport/StreamLink.h> -
which the P6.5 seam gate (scripts/ci/link_seam_purity.py, "concrete link included above
Transport") rejects, and the MGPipe generators and hygiene gates job has been red since.

The counters stay in StreamLink.cpp beside the recv/sendmsg loops that feed them; only the
struct and the two Take functions move, to LinkMetrics.h as LinkIoStats +
LinkMetricsTakeReadStats/TakeSendStats. The readers drop the concrete include; the hot
loops are untouched.
2026-09-28 21:54:08 -04:00
swung0x48 3bdd565dc6 [Chore] (docs): Remove orphaned CS_Refactor notes accidentally committed in 4e48702f2 2026-09-28 21:16:35 -04:00
swung0x48 884f6aac96 [Feat] (Android): trace APK boots to POST; bottom nav bar switches to the render server
The trace flavor manifest removed PostActivity and made ServerControlActivity
the launcher, so the APK opened straight into the render server screen. Keep
both screens: PostActivity is the launcher again, and a bottom navigation bar
(NavBar, plugin flavor unaffected) switches between them via
FLAG_ACTIVITY_REORDER_TO_FRONT with transitions suppressed, so each screen
keeps its state. ServerControlActivity is singleTop now and handles extras in
onNewIntent so adb-driven starts still work. The POST screen also shows an
indeterminate progress bar while the self-test runs.
2026-09-28 21:06:44 -04:00
swung0x48 b9a89967b0 [Docs] (Disaggregated): five-arm transport perf comparison and bottleneck attribution
rd12/openra on-device matrix (monolith/inproc/spawn+shm/spawn+tcp),
tcp-localhost transport_wait decomposition (present credit=1 ~74% +
futex wakeup tax; SPIN_US=2000 alone 2.9x), cross-host control-plane
hang evidence chain blocking P12 acceptance B, Wi-Fi bandwidth ceiling,
and the prioritized knob/code optimization list.
2026-09-28 20:55:45 -04:00
swung0x48 141227e8ba [Feat] (tools): add the disaggregated-mode device perf matrix and profiling harness
run_matrix.sh drives interleaved monolith/inproc/spawn+shm/spawn+tcp
benchmark rounds on an adb device with per-run arm-marker verification;
profile_matrix.sh layers simpleperf, ftrace wakeup capture and a /proc
sampler on top; make_report.py aggregates runs into REPORT.md. The wifi
kit carries the cross-host control-plane hang repro and phone-side
strace/fd-reconcile helpers. .trace-work/ (run output) is now ignored.
2026-09-28 19:36:30 -04:00
swung0x48 930c29e9e0 [Feat] (Android): the render server screen takes intent extras for prefill and autostart
listen/token/env/onscreen prefill the form, --ez start true starts the
server, --ez stop true stops it - adb drives the screen without the IME,
which is how the on-screen unix path (@name + shm, ServerOwned window)
was verified end to end from the UI.
2026-09-27 08:36:57 -04:00
swung0x48 c16fb5a8ac [Feat] (MG_Remote): the in-process display server serves unix endpoints too
mobilegl_server_serve_inprocess refused anything but tcp:// because its
only supervisor was the TCP one's thread hand-off. A unix endpoint
(@abstract name or filesystem path) now runs a supervisor of the same
shape minus the TCP exposure machinery: AcceptPair pairs a client's
control and SCM_RIGHTS connections by arrival order, there is no
DataBind to route and no pre-auth gate, sessions run on threads one at
a time with Busy for a second client, the latch resets between sessions
and g_inProcessStop ends it. That gives the on-screen server the
on-device fast path: ServerOwned windows over a unix socket with shm
data, no TCP copies.

The control screen's tcp-only guard for the on-screen mode goes with
the refusal it mirrored.
2026-09-27 07:43:56 -04:00
swung0x48 e3e890ed5d [Feat] (Trace): the replay runner builds as an adb-shell executable for Android, and unix endpoints carry a headless window
MOBILEGL_BUILD_TRACE_REPLAY now also produces mobilegl_trace_replay on
ANDROID: the same CLI as desktop, linked against the APK runner's
glws/glproc glue. On the device it dials a render server with no
Activity at all - MOBILEGL_IPC_CONTROL=unix:@name with shm data to the
offscreen supervisor (a cross-uid abstract connect needs root or an
SELinux waiver; loopback TCP works from plain adb shell), or
tcp:// with MOBILEGL_IPC_SURFACE=server to the on-screen display server.

glws_android grows the desktop glws_egl's server-owned null-window
createWindowSurface, gated on MOBILEGL_TRACE_SURFACE=window so the APK's
pbuffer mode keeps its old answer, and the config carries EGL_WINDOW_BIT
for it. The control screen refuses unix endpoints in on-screen mode:
the in-process display server serves tcp:// only.

Verified on-device with an OpenRA retrace, ssim=1.0 both ways:
offscreen service on @mgl-cli with the CLI as root, and the display
Activity on tcp://127.0.0.1:40613 with the replay's frames on the
phone's screen.
2026-09-27 06:28:00 -04:00
swung0x48 bf6feebb22 [Perf] (MG_Remote): The producer's transport park is timed as its own per-frame number, and a reducer pairs the two role logs into a frame-time breakdown
transport_wait_ns measures wall time in Ring::Wait's flush/spin/sleep, so
the client's parked time is read directly instead of inferred by
subtracting CPU clocks that run concurrently with the server.
frame_stats.py matches client and server present serials into per-frame
avg/p50/p99 splits; the README documents the MOBILEGL_PIPE_STATS=1
workflow.
2026-09-27 04:45:07 -04:00
swung0x48 f10db410e8 [Feat] (Android): the render server screen also starts the on-screen display server
An On-screen window checkbox switches Start from the offscreen service
to MobileGLDisplayActivity (process :mglwin), which serves in its own
process and renders a client's window surface onto its SurfaceView.
Stop ends either shape: stopService for the supervisor, killProcess for
:mglwin, and the running check covers both. Verified end to end with an
OpenRA retrace: the client dialed tcp://127.0.0.1:40613 with
MOBILEGL_IPC_SURFACE=server from a freeform window while the display
Activity held the screen, frames landed on the SurfaceView and the run
passed with ssim=1.0.
2026-09-27 04:39:01 -04:00
swung0x48 aaa50c88e1 [Feat] (Android): the render server screen takes unix endpoints, and is titled for what it serves
The endpoint field accepts @abstract names and filesystem paths beside
tcp://, matching what mobilegl_server_main's Listen already binds - on
Android the abstract namespace is the on-device rendezvous that always
works, and a relative path lands in the app's files dir, the service's
working directory. Unix endpoints skip the token rule (same-device
reach by construction) and are probed with LocalSocket instead of a
TCP connect. The screen is now titled MobileGL Render Server.
2026-09-27 04:39:00 -04:00
swung0x48 1f72597dd8 [Feat] (Android): a launcher screen starts and stops the offscreen TCP server
ServerControlActivity (trace flavor) collects the listen endpoint, auth
token and extra env, then drives MobileGLServerService with the same
extras adb used. Non-loopback listens are held to the 16-byte token the
server enforces, and a connect probe confirms the supervisor bound its
socket. The screen is the APK's launcher icon, which the trace flavor
lacked since PostActivity is removed there.
2026-09-27 04:39:00 -04:00
582 changed files with 72748 additions and 3931 deletions
@@ -0,0 +1,3 @@
# Everything here is executed by bash/sh on Linux, Android or WSL: keep LF even on a Windows checkout
# (core.autocrlf=true would otherwise hand sh a script full of $'\r').
* text=auto eol=lf
@@ -0,0 +1,183 @@
---
name: anland-mobilegl-plasma
description: Bring up, rebuild, deploy, switch backends on, test and debug the "MobileGL + anland + KDE Plasma" stack - a rooted Android tablet whose anland APK embeds the MobileGL server, plus a Droidspaces Arch Linux ARM container running KWin/plasmashell/Chrome through the MobileGL client library and GBM backend - driven from this Windows machine (Git Bash, WSL archlinux, adb). Use it after a phone reboot, when changing server (NDK) or client (container glibc) MobileGL code or anland's KWin backend/patch, when switching Espryt/Magma, when validating zero-copy dma-buf presentation, and when chasing crashes, black windows, noise or software-rendering fallbacks in the Plasma session.
---
# MobileGL on anland KDE Plasma
**Setting this up on a new machine or phone from source: `reproduce.md` next to this file.**
Human-readable runbook (Chinese, same content): `docs/Disaggregated/notes/anland/runbook-plasma.md`.
Background: `docs/Disaggregated/notes/anland/handoff-legacy-mobilegl-unified.md` (state, commits, known
issues) and `plan-ahb-dmabuf.md` (zero-copy design). All scripts are in `scripts/` next to this file;
parameters are documented at the top of each script and in `scripts/env.sh`.
**The phone may be shared with another session.** Before anything that restarts the app, the daemon,
the container session or replaces a library, run the read-only `scripts/device/status.sh` and make sure
nobody else is mid-test. Use your own `ct.sh` channel names (others use c, c2, c3, xd, xr, gbm).
## Architecture (10 lines)
1. Phone: Lenovo TB321FU (Y700, Adreno 750, Android 15, kernel 6.1), rooted with **KernelSU** (`/data/adb/ksu`, `ksud`; `su -c` works). USB serial `HA27Q3LQ`, or wireless adb `ip:port`.
2. anland APK `com.anland.consumer.mobilegl` (debuggable build of anland branch `legacy-mobilegl-unified`): UI process + private service process `:mobilegl` (`MobileGLWorker`), which loads the **server** `libMobileGL.so` (NDK, from MobileGL `feat/disaggregated`) and listens on abstract socket `@anland-mobilegl` from the first attached window on. One process serves every client (KWin, plasmashell, ksplash, Chrome...), one backend per process. `MobileGLWorker` is a **foreground service** (notification "Linux desktop is running" / action "Stop desktop"), so the server and the session outlive a hidden, locked or swiped-away window.
3. Backend = the app setting "Renderer backend" (Settings > Connection > MobileGL desktop; file `/data/data/<pkg>/files/mobilegl-backend`, default DirectGLES; also `am start ... --es mobilegl_backend X`). Espryt = DirectGLES, Magma = DirectVulkan. The server reads it when its process starts; the app publishes it as `/data/local/tmp/anland-mobilegl/backend` and the session copies that into the container's `/etc/mobilegl/backend` (what every client reports; mismatch = "Hello.backendType disagrees with pinned backend"). `debug.mobilegl.backend`, if someone sets it, still overrides the setting until the next reboot.
4. Experiment display daemon `/data/adb/modules/anland-daemon/display_daemon /data/local/tmp/anland-mobilegl/display.sock` (display/input broker), started by the app (below). The module only starts the original one on `/data/local/tmp/display_daemon.sock` at boot; leave that alone.
5. Container `arch-kde-mgl` (Droidspaces 6.4.5, Arch Linux ARM aarch64 glibc, host net, `rootfs.img` mounted at `/mnt/Droidspaces/arch-kde-mgl` on the Android side). Bind mounts: `/data/local/tmp/anland-mobilegl` -> `/run/anland-mobilegl`, `/data/local/tmp/display_daemon.sock` -> `/run/display.sock`.
6. `desktop-session.service` (User=swung0x48, **enabled**: the container boots straight into it, as the original image does) has drop-in `/etc/systemd/system/desktop-session.service.d/mobilegl.conf` (anland `producers/kde/Arch_v5/desktop-session-mobilegl.conf`, installed + enabled by `sync-build-kwin.sh`): `ExecStartPre=mobilegl-startup.sh wait` (until `@anland-mobilegl` listens in `/proc/net/unix` and the daemon socket exists), `ExecStartPre=+... sync-backend`, `Restart=always`, then `ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma`, which deletes the sticky software-renderer key, sets `MOBILEGL_IPC_PROBE_TIMEOUT_MS=0` session-wide (MobileGL never declines, so glvnd never falls through to another GL stack), writes a user drop-in for `plasma-kwin_wayland.service` (kwin_wayland_wrapper -> `/opt/mobilegl/kwin/bin/kwin_wayland` + `/opt/mobilegl/kwin/lib/libkwin.so.6.7.4`, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`) and runs `startplasma-wayland`.
7. KWin 6.7.4 with anland's `anland` backend renders straight into the anland Android Surface through the server. While the window is not on screen (consumer disconnected) the backend powers its output down (DPMS Off + RenderLoop inhibited: no compositing, no frame callbacks) and back up when the window returns. Every other GL process is `MOBILEGL_IPC_SURFACE=offscreen` and presents through KWin.
8. Client = `/opt/mobilegl/lib/libMobileGL.so` (glibc aarch64), the container's **system-wide** GL vendor (section below): glvnd EGL vendor `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` (sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines), GLX vendor `/usr/lib/libGLX_mobilegl.so.0`, settings from `/etc/mobilegl/client.conf` (`MOBILEGL_TRANSPORT=spawn MOBILEGL_IPC_DATA=shm MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl`) + `/etc/mobilegl/backend`. No per-process environment is needed.
9. Zero-copy windows: client `wl_egl_window` frames are server-allocated AHardwareBuffer "shared images" exported as dma-buf fds over `zwp_linux_dmabuf_v1`; KWin imports them with EGL and the server recognises its own buffer by inode. KWin gets a DRM device via `GBM_BACKEND=mobilegl` -> `/usr/lib/gbm/mobilegl_gbm.so` (node is only an identity, never ioctl'd). Without linux-dmabuf the client falls back to wl_shm readback.
10. X11 (`docs/Disaggregated/notes/anland/plan-x11-gpu.md`): Xwayland draws with **glamor on MobileGL** (an offscreen session; `GBM_BACKEND=mobilegl` on KWin's render node; window pixmaps are shared images handed to KWin as linux-dmabuf buffers; its `glFlush` publishes them through the server, `/tmp/mgl-xwayland.client.log`); `ANLAND_XWAYLAND_GLAMOR=0` in KWin's drop-in brings back the software `-shm` Xwayland. GLX windows present their frames as shared images through **DRI3 + Present** (log `presented through DRI3+Present shared images`), falling back to readback + MIT-SHM / `xcb_put_image` when the X server has no DRI3 (`MOBILEGL_GLX_PRESENT=readback` forces it per app). Chrome runs ANGLE-GLES over MobileGL EGL with the GPU in its own process (`mobilegl-startup.sh chrome`; `MOBILEGL_CHROME_GPU=in-process` opts out), presenting GBM dma-bufs over linux-dmabuf; after a device loss Chrome restarts that process and repaints.
## First-time prerequisites (Windows host)
- Git Bash, `adb` (SDK platform-tools), CMake >= 3.22 and Ninja on PATH (seen: CMake 3.27.6, Ninja 1.13.2), Python 3 + Pillow (only `rec.sh`).
- Android NDK **27.2.12479018** at `%LOCALAPPDATA%/Android/Sdk/ndk/27.2.12479018` (override `ANDROID_NDK`).
- WSL distro **archlinux** with clang, lld, llvm, cmake, ninja. **lld's major version must equal clang/llvm-libs'** (lld 23 on llvm 22 broke `ld.lld`; working set: all 22.1.6). Then export the sysroot once: `bash scripts/xsysroot/refresh-sysroot.sh` (-> `~/sysroots/arch-kde-mgl` + `~/sysroots/aarch64-arch.cmake`; re-run after container package upgrades, then `xbuild.sh --reconfigure`).
- MobileGL worktree (never build in the shared main checkout): `GIT_LFS_SKIP_SMUDGE=1 git worktree add ...`, `git submodule update --init --recursive`, and copy `3rdparty/glslang/External/spirv-tools` from the main checkout (the submodule leaves it empty).
- anland worktree on `legacy-mobilegl-unified` (KWin patch/backend, startup script, `mobilegl-tools/`).
- Phone side that already exists and is NOT recreated by this skill: the APK (build: anland `consumers/anland_v5/android_consumer/MOBILEGL.md`, gradle `:app:assemblePlainDebug` with `MOBILEGL_DIST`, `-PmobileglApplicationId=com.anland.consumer.mobilegl -PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock`, from Git Bash with `MSYS_NO_PATHCONV=1`; signed with this machine's `~/.android/debug.keystore`, so `adb install -r` works; reinstalling with another key changes the UID and needs the KernelSU grant again - `mobilegl-tools/ksu_grant.c`), the anland KernelSU modules (`anland-daemon`, `anland-awl`), the container with KWin's configured build tree `/root/mobilegl-build/kwin-6.7.4/build` and pristine `/root/gbm-dev/kwin-a/src`.
- Install the helpers once (container must be running): `bash scripts/push-tools.sh` (device/*.sh -> `/data/local/tmp/anl`, `mgrun` -> container `/usr/local/bin`, `kwprep.sh` -> container `/root`).
Run host scripts from Git Bash as `bash scripts/<x>.sh`. Raw adb with device paths needs `MSYS_NO_PATHCONV=1` and the whole root command in one quoted string: `adb shell 'su -c "..."'`. A device script can also be run without pushing: `MSYS_NO_PATHCONV=1 adb shell 'su -c sh' < scripts/device/status.sh`.
## Starting, idling, stopping - no manual steps
Nothing starts at phone boot (container `run_at_boot=0`). **Opening the app** (launcher icon = `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity`, no extras; the APK's default socket is the experiment's, `-PanlandDefaultSocket`) calls `MobileGLDesktop.ensureStarted()` - the single trigger - which starts the foreground service and runs the APK's `assets/mobilegl-desktop.sh up` as root (`su`): display daemon if absent (the script leaves the app's cgroup first, so it survives the app) -> backend published -> container started if stopped -> `systemctl start --no-block desktop-session`. The window waits for the daemon socket; the server listens once the Surface is attached; the session's `wait` step then lets Plasma start. A reopen with everything up only reattaches the window. Log: `/data/local/tmp/anland-mobilegl/desktop.log`, logcat `AnlandMobileGL` ("MobileGL desktop up done in N ms").
Hidden window, screen off, lock screen or swiped-away task: server and session stay (foreground service); KWin turns the workspace's DPMS off (logs "viewer gone: output powered down"; windows get xdg_toplevel `suspended`, no frame callbacks) and back on ("viewer back"). Client swaps are paced by frame callbacks (`MG_Impl/EGLImpl/FrameThrottle.h`): interval >= 1 waits like any Wayland EGL; interval 0 (Chrome) drops to ~1 fps once a callback goes unanswered for 200 ms.
Measured 2026-10-03 (Espryt, Chrome on webglsamples aquarium; % of one core, phone = all of Android): open-to-desktop cold 7.5-12.5 s (daemon 0.5, server 1.0, container 1.5, kwin ~5, plasmashell ~6-7), after a phone reboot 8.5 s, reopen of a running desktop 0.2 s; visible phone 249% / kwin 13 / chrome 99 / server 57 / GPU 22%, hidden 54 / 0 / 6 / 2 / 0%, locked 38 / 0 / 8 / 3 / 0%. Stop desktop 1.7 s. Wireless adb does not come back by itself after a reboot (re-enable it on the phone). **Stop desktop** (notification action, or `am startservice -n <pkg>/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP`) runs `mobilegl-desktop.sh down` (session + container stopped, daemon stays), closes the windows, ends `:mobilegl`.
```sh
adb connect <ip>:<port> # only for wireless debugging
export MSYS_NO_PATHCONV=1
adb shell 'su -c "sh /data/local/tmp/anl/bringup.sh"' # after a reboot: wake, dismiss keyguard, open the app, print the timeline
bash scripts/desktop-verify.sh push cold reopen idle # timed cold start, swipe-away + reopen, CPU/GPU visible/hidden/locked
adb shell 'su -c "sh /data/local/tmp/anl/status.sh"'
```
`droidspaces` has no `status` command; `droidspaces show` lists running containers. `device/run-plasma.sh` is a developer restart (session + logind user, then the app), needed after a rebuild because KWin does not reconnect to a new server process. `device/desktop-reset.sh` takes everything down to the post-reboot state. The old manual sequence (setprop, start container, `restart-daemon.sh`, `run-plasma.sh`) is gone; `restart-daemon.sh` remains for daemon debugging only.
## Build and deploy loops
**Server (Android, NDK) -> APK lib**
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so (RelWithDebInfo, unstripped)
bash scripts/cyc.sh s1 # clear sticky SW-renderer key, push, swap into APK, restart session, screenshot $ANL_DIR/s1.png, print fatals
```
`device/swap.sh` overwrites `/data/app/.../lib/arm64/libMobileGL.so` keeping owner/mode/SELinux context (original saved as `/data/local/tmp/anland-mobilegl/libMobileGL.so.orig`); no re-signing. To swap without the session restart: push to `/data/local/tmp/anland-mobilegl/libMobileGL.new.so`, `su -c "sh /data/local/tmp/anl/swap.sh"`, then `run-plasma.sh` anyway (KWin does not reconnect to a new server process).
**Client + GBM backend (container glibc) via WSL cross-build**
```sh
bash scripts/xbuild.sh [--reconfigure] # WSL ~/mgl-xbuild-a64; ~150 s full, seconds incremental
bash scripts/xdeploy.sh # rename-into-place install of libMobileGL.so + mobilegl_gbm.so, aliases, vendor JSONs, client.conf, GLX/GBM env drop-ins
```
Running clients keep the old inode; restart the client, or `run-plasma.sh` for KWin/plasmashell. Build server and client from the **same commit** (both stamp `git rev-parse HEAD`), else the logs WARN "compatible wire with different build". Fallback in-container build (slow): `scripts/sync-src.sh` + anland `mobilegl-tools/anland-build-client.sh`.
**KWin (anland backend / kwin.patch) - built inside the container**
```sh
bash scripts/kwin-stage.sh build # stage CR-stripped patch+backend+startup script, kwprep.sh, background sync-build-kwin.sh
printf 'tail -5 /root/kwin-build.log\n' | bash scripts/ct.sh kw # wait for "== ALL DONE =="
adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
```
`sync-build-kwin.sh` (anland `producers/kde/Arch_v5/`) copies only the backend sources, then `ninja -j4`, installs `kwin_wayland`, `libkwin.so.6.7.4`, the patched screencast plugin and `/opt/mobilegl/bin/mobilegl-startup.sh` by atomic rename. `kwprep.sh` is what applies changed `kwin.patch` hunks to core KWin files. Repo-root `libdisplay_producer/` is not synced by either. `kwin-stage.sh` is assembled from those two scripts and was not run as one unit yet.
**Unit tests (no device)** - in WSL (the MSVC build of this branch is broken):
```sh
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest TwinOwnershipTest
MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash -lc 'cd ~/mgl-anl-bld && ctest -R "MultiSession|TwinOwnership" -E DirectVulkan --output-on-failure'
```
`wsl-all.sh` builds and runs every unit test. Never run DirectVulkan integration tests; never two ctest runs at once.
## Switching backends
In the app: Settings > Connection > MobileGL desktop > Renderer backend, then Stop desktop and open the app. Or `adb shell 'su -c "sh /data/local/tmp/anl/switch.sh DirectGLES"'`, which does the same (saves the setting through the launch extra, STOPs the service, reopens; clears a stale `debug.mobilegl.backend`). No setprop, no file to edit in the container: the session copies the published backend into `/etc/mobilegl/backend` on its next start. `cyc.sh` keeps whatever is set.
## System-wide vendor and fallback
Installed by `xdeploy.sh` (and anland's `anland-build-client.sh`), so any process in the container gets MobileGL through the stock libglvnd/libgbm:
| File | Does |
|---|---|
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | EGL vendor, asked before the system's `50_*.json` |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | same JSON, for per-process forcing with `__EGL_VENDOR_LIBRARY_FILENAMES` |
| `/etc/mobilegl/client.conf` | `KEY=value` defaults for every client (transport, data, endpoint); rewritten on deploy |
| `/etc/mobilegl/backend` | the backend (the session's `sync-backend` step copies the app's choice in); wins over a `MOBILEGL_BACKEND_TYPE` line in client.conf |
| `/etc/environment.d/10-mobilegl.conf` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` for the systemd user manager (all Plasma services and what they start, terminals included) |
| `/etc/profile.d/mobilegl.sh` | the same two for login shells (`bash -l`, `su -l`, ssh, `droidspaces run bash -l`) |
Precedence is environment > `/etc/mobilegl/backend` (backend only) > client.conf > built-in; `MOBILEGL_CONFIG_FILE` / `MOBILEGL_BACKEND_FILE` name other files (empty = off); a server process never reads them.
**Fallback.** Before bringing anything up the client probes the configured endpoint (one non-blocking connect, `MOBILEGL_IPC_PROBE_TIMEOUT_MS`, default 250, 0 = off; a refused abstract socket answers in ~0.2 ms). With no server: the EGL vendor returns `EGL_NO_DISPLAY` and no devices (glvnd moves on to the other vendor), the GLX vendor fails `__glx_Main` (libGLX falls back to the vendor Xwayland names), and `mobilegl_gbm` fails `create_device` (libgbm falls back to the device's own backend). Client queries (`eglQueryString(EGL_NO_DISPLAY)`, `eglGetProcAddress`) never bring a session up. MobileGL also steps aside for a GBM device another backend created and for `LIBGL_ALWAYS_SOFTWARE=1` - that is how a software Xwayland (`ANLAND_XWAYLAND_GLAMOR=0`, KWin's patch sets it then) keeps the software stack. MobileGL never opens an X connection to a display its own process serves (`MG_Util/X11/DisplayGuard.h`; `MOBILEGL_X11_DIAL=0` forbids all), which is what made a glamor Xwayland deadlock on itself before. Show it without stopping the real server:
```sh
sed 's|^MOBILEGL_IPC_CONTROL=.*|MOBILEGL_IPC_CONTROL=unix:@nobody|' /etc/mobilegl/client.conf > /tmp/down.conf
sudo -u swung0x48 env -i HOME=/home/swung0x48 PATH=/usr/bin XDG_RUNTIME_DIR=/run/user/1000 WAYLAND_DISPLAY=wayland-0 \
MOBILEGL_CONFIG_FILE=/tmp/down.conf eglinfo -B -p wayland # vendor = the other one, ~0.1 s
```
## Test clients and how to verify
X11 on the GPU has its own driver, `scripts/x11-verify.sh` (preflight, measure before/after with glxgears at `vblank_mode=0` on both GLX paths (presentation throughput only: glxgears is fixed-function GL, which MobileGL does not implement, so its window is black by design), deploy-client/-kwin/-server, check, apps, stop); `scripts/device/x11-measure.sh` refuses to load the phone below 40% battery.
Inside the container as root (`printf '...' | bash scripts/ct.sh <chan>`), `mgrun offscreen <cmd>` runs a client as swung0x48 in the live session through a login shell, with nothing but the surface mode and the log path set (log `/tmp/mgl-run.client.log`, override base with `MGLOG=/tmp/x.log`); a plain `env -i ... bash -l -c '<cmd>'` as swung0x48 gets MobileGL the same way:
```sh
mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B
timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720 --run-forever > /tmp/gm.out 2>&1 &
sleep 8; grep -a "presented through" /tmp/mgl-run.client.log
```
Chrome: `bash scripts/chrome-launch.sh <tag> [flags]` (log `/tmp/mgl-chrome-<tag>.client.log`, screenshot `ch-<tag>.png`; `CHROME_GPU=in-process` puts the GPU back in the browser, which cannot recover from a device loss).
Video: Chrome decodes in hardware. The GPU process's render node has a VA-API driver in the container (`/usr/lib/dri/msm_drm_drv_video.so`, DroidSpaces' V4L2 driver): Chrome allocates its frames with `vaCreateSurfaces` and exports them (`vaExportSurfaceHandle`, one fd, R8 + GR88 layers) - the driver's own buffers, never GBM ones - and imports each as an NV12 (P010 for 10-bit) EGLImage bound to `GL_TEXTURE_EXTERNAL_OES`. MobileGL takes such a FOREIGN YUV dma-buf through its labelled CPU-copy fallback: the server maps it and copies it into a YUV AHardwareBuffer of its own (`AHardwareBuffer_lockPlanes`) at the reader's first use in each frame (any present, publishing flush or native fence), then converts that into the texture's RGBA level on the GPU (Espryt: `GL_EXT_YUV_target` with the import's colour hints, `MOBILEGL_YUV_DRIVER_CONVERSION=1` for the driver's external sampler; Magma: `VkSamplerYcbcrConversion` on the AHB's external format). A server-allocated NV12/P010 buffer (GBM `gbm_bo_create(NV12)`) needs no copy. `MOBILEGL_CHROME_VIDEO_DECODE=software` in `mobilegl-startup.sh chrome` brings back `--disable-accelerated-video-decode`. Check a video with `scripts/probes/cdpmedia.py <port> <s>` in the container (CDP `Media.enable`: `kVideoDecoderName = VaapiVideoDecoder`, `kIsPlatformVideoDecoder = true`; `CDP_NAVIGATE=<url>` / `CDP_EXPR=<js>`), which needs Chrome started with `--remote-debugging-port` and a non-default `--user-data-dir` (a fresh profile also needs `--password-store=basic`, else page loads wait forever on KWallet), and grep Chrome's log for `exit_code` (a GPU-process restart). `scripts/probes/yuvprobe.c` pixel-checks foreign NV12/P010 imports, their per-frame refresh and a GBM NV12 buffer (`mgrun offscreen mgl-yuvprobe`, exit 0 = pass). KWin's own NV12 path (it splits a YUV buffer into R8 + GR88 imports) is not served: MobileGL imports no single-channel dma-bufs, so KWin advertises no YUV format over linux-dmabuf. A vendor-agnostic replacement driver (MobileVA: VA-API on Android MediaCodec, decoding into server YUV images with no CPU copy, the current driver kept as an A/B fallback) is planned, not built: `docs/MobileVA/plan.md`.
Device loss: `setprop debug.mobilegl.inject_device_lost_pid <pid>` (as root) loses the server session of the client whose pid - as it sees itself in the container - is `<pid>`, once per value, on any backend and whether or not it presents; `debug.mobilegl.inject_device_lost_at N` instead loses whichever session makes the N-th device check (Espryt presents and read-backs only). `bash scripts/chrome-recover.sh <in-process|process> [rounds]` checks that Chrome repaints after such a loss without a restart (battery-guarded, stops below 40%). `bash scripts/kwin-recover.sh [rounds]` loses KWin's own session (default twice) and checks that KWin restarts compositing on a fresh session - same KWin, plasmashell and client processes, the screen still animating (glmark2 on Wayland, glxgears on X11, `CHROME=1` adds Chrome) - which needs the anland backend + kwin.patch reset handling deployed (`kwin-stage.sh build`).
Probes: `scripts/probes/texfmt.c` (texture formats keep alpha?), `dmahash.c` (hash a process's dma-bufs), `yuvprobe.c` (YUV dma-buf sampling, `gcc -O1 /root/yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm -lm`), `cdpmedia.py` (which decoder a Chrome page uses); build in the container, e.g. `gcc -O1 /root/texfmt.c -o /usr/local/bin/mgl-texfmt -lEGL`, run with `mgrun offscreen`.
GPU hangs: a shader that never ends stops the WHOLE GPU (every process, MobileGL or not) until the kernel resets it - ~2 s, or ~10 s ("Preemption Fault" in dmesg) when a higher-priority context waits; nothing in user space shortens one stall. What the server does is end the guilty session so it cannot hang the GPU again (Magma: event-bracketed submissions + `mgl-gpu-watch`, budget `MOBILEGL_GPU_HANG_BUDGET_MS` / `debug.mobilegl.gpu_hang_budget_ms`, default 2000; Espryt: the driver's GUILTY reset status, which it reports after the ~2 s reset only). Repro: `scripts/probes/hangclient.c` (Wayland + GL 3.3 core; `-H 60 -N 1 -E 60` hangs every 60 frames, `-H -1` is a frame-gap observer; build with wayland-scanner's xdg-shell files, `-lwayland-client -lwayland-egl -lEGL -lOpenGL -lm`), and `gpuprobe.c` (NDK, plain adb shell; `gpuprobe 30 high` is a high-priority GLES context outside MobileGL - it both measures the device-wide stall and forces the 10 s preemption path).
A result counts only if ALL hold:
- client log says `presented through linux-dmabuf shared images` (not `wl_shm`; `could not be presented through a linux-dmabuf shared image` = fell back);
- `bash scripts/shot.sh g 2 1` gives two different hashes while the client animates (a "fast" score can mean frames are not shown);
- `adb logcat -b crash -d` and `adb logcat -d | grep -E "Fatal signal|Abort message|E MobileGL"` are clean (a frozen window is usually a dead server);
- for plasmashell verdicts, the sticky software renderer is off (below) and `/tmp/mgl-plasmashell.client.log` is freshly written.
Reference (glmark2-es2-wayland 1280x720, zero-copy vs wl_shm): Espryt 187 vs 49, Magma 158 vs 34. Rerun any surprising pass.
## Logs
| What | Where |
|---|---|
| KWin client side | container `/tmp/mobilegl-compositor.client.log` |
| plasmashell / ksplash | `/tmp/mgl-plasmashell.client.log`, `/tmp/mgl-ksplash.client.log` (user drop-ins `~/.config/systemd/user/plasma-{plasmashell,ksplash}.service.d/mobilegl-log.conf`) |
| mgrun / Chrome | `/tmp/mgl-run.client.log`, `/tmp/mgl-chrome-<tag>.client.log` (+ `/tmp/chrome-<tag>-out.log`) |
| Server | `adb logcat -s MobileGL`; file `/data/data/com.anland.consumer.mobilegl/files/mobilegl-server.server.log` |
| Native crashes | `adb logcat -b crash -d`; container `coredumpctl list` |
| Session / KWin stderr | container `journalctl -u desktop-session --no-pager`, `journalctl --no-pager --since -2min \| grep kwin_wayland` |
| Display daemon | `/data/local/tmp/anland-mobilegl/display-daemon.log` |
| Desktop start/stop (app's su script) | `/data/local/tmp/anland-mobilegl/desktop.log`, `container-start.log`; logcat `-s AnlandMobileGL` |
| Session wait / backend sync | container `journalctl -u desktop-session --no-pager` ("waiting for the MobileGL server ...") |
`MOBILEGL_LOG_FILE_PATH=/tmp/x.log` produces `/tmp/x.client.log` (and `.server.log` for forwarded lines); files are truncated on open, so one base per process. Client builds log at INFO. Symbolize server frames with `$ANDROID_NDK/toolchains/llvm/prebuilt/windows-x86_64/bin/llvm-addr2line.exe -Cfe build-android/libMobileGL.so <offset>`.
## Pitfalls
- **pkill -f self-match**: a `ct.sh`/`droidspaces run bash -lc "<script>"` process carries the script in its argv; `pkill -f chrome` kills the runner. Use `pkill -x`.
- **Root-owned /tmp files**: a log created by a root-run client cannot be reopened by swung0x48 later (no log appears). `rm -f` it as root first.
- **Sticky Plasma software renderer**: after any server crash Plasma writes `[QtQuickRendererSettings] SceneGraphBackend=software` into `~swung0x48/.config/kdeglobals`; plasmashell then renders on the CPU and screenshots still look fine (tray icon "正在使用的软件渲染器"). `mobilegl-startup.sh plasma` deletes it at every session start; within a running session that hit a crash, restart the session (`run-plasma.sh`) before any verdict.
- **Lock screen**: autolock is off (`kscreenlockerrc [Daemon] Autolock=false`); a real lock can only be left by typing the password (logind lock/unlock is not supported for this session type - `device/unlock.sh` is unverified). Test the greeter with `/usr/lib/kscreenlocker_greet --testing` and kill it.
- **CRLF**: `core.autocrlf=true` here; anland's backend sources and `sync-build-kwin.sh` are CRLF in the Windows checkout. Strip CR before anything runs on Linux/Android (`tr -d '\r'`; `ct.sh`, `push-tools.sh`, `kwin-stage.sh` do). This skill has a `.gitattributes` forcing LF.
- **Display daemon**: the app starts it; the session's `wait` step keeps KWin from starting without it (a KWin that cannot reach it logs "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and exits in ~3 s). Anything started from the app's `su` must leave the app's cgroup (`echo $$ > /sys/fs/cgroup/cgroup.procs`) or a force-stop kills it.
- **Device power/connection**: low battery or the screen turning off (30 s default) can kill the SurfaceView - `svc power stayon true`; wireless adb `ip:port` changes per pairing - never pin `-s HA27Q3LQ`, use `ANDROID_SERIAL` if several devices are attached.
- **Container vs phone paths**: container `/tmp` is a different place from the phone's `/data/local/tmp`; move files via `/mnt/Droidspaces/arch-kde-mgl/root` (container `/root`) or `/data/local/tmp/anland-mobilegl` (= container `/run/anland-mobilegl`).
- **Never overwrite a mapped .so in place** (container or APK while running): copy to a new inode and `mv -f`; the APK swap force-stops the app first.
- **Input**: plain `adb shell input` is blocked - use `su -c input`; anland grabs the touchscreen, so if `input tap` does nothing use `input mouse tap x y`. Never `debuggerd -b` a container process (signal 35 kills glibc processes).
- **linux-dmabuf**: the client must use `create` + roundtrip, never `create_immed` (KWin answers a failed immediate import with a protocol error that kills the client).
- **Cross-session bugs**: noise / another window's pixels after some client starts or exits = process-global state in the multi-session server (handles are per client, FBO/VAO per context). Repro with `kscreenlocker_greet --testing`.
- `/etc/environment` leaks the baseline session's driver variables through pam_env (and, via `/usr/lib/environment.d/99-environment.conf`, into the user manager); `mobilegl-startup.sh` unsets them - keep that when editing it.
- **Shared log path**: Xwayland inherits KWin's `MOBILEGL_LOG_FILE_PATH`; any line a process logs opens (truncates) that file. MobileGL stays silent in processes it does not serve, but give any extra client its own `MOBILEGL_LOG_FILE_PATH`.
- **Stale per-process overrides**: an `__EGL_VENDOR_LIBRARY_FILENAMES` / `MOBILEGL_*` left in a unit drop-in or the user manager's environment (`systemctl --user show-environment`) still wins over the system files; `run-plasma.sh` (terminate-user) clears the manager's.
- `handoff` mentions a `kwin_wayland` logging shim and `/tmp/mgl-kwin-<pid>.log`: stale - `/opt/mobilegl/kwin/bin/kwin_wayland` is now the real binary and logs to `/tmp/mobilegl-compositor.client.log`.
## House rules
- Work in a git worktree, never in the shared main checkout or its build dirs; worktrees need `GIT_LFS_SKIP_SMUDGE=1`.
- Commit with `git commit --no-verify`, one short sentence, **no** `Co-Authored-By` trailer.
- Never run the DirectVulkan integration tests (`ctest -E DirectVulkan`).
- No external project names ("Mesa", "Zink", ...) in source code or comments; describe the technique.
- Vendor-agnostic only on the zero-copy path: AHB, EGL_ANDROID_*, VK_ANDROID AHB import, sync_file, dma-buf, libgbm ABI - no kgsl/gralloc layouts/QCOM modifiers.
- Identity caches key on monotonic lifetime ids, never GL names or pointers.
@@ -0,0 +1,183 @@
# Reproducing the MobileGL + anland + KDE Plasma setup from source
This is the from-scratch path for a NEW development machine and/or a NEW phone. `SKILL.md` covers the
day-to-day loop once everything below exists. Paths and names that are specific to the original
machine are listed in step 9: set them in `scripts/env.sh` (or the environment) before running anything.
## 0. What you need
**Phone.**
- arm64 Android 11+ with **root**. KernelSU is what this was built on; the scripts only need `su -c`.
- A Vulkan 1.1 driver with `VK_ANDROID_external_memory_android_hardware_buffer` for Magma, and/or GLES 3.2 + EGL_ANDROID_native_fence_sync for Espryt. Nothing vendor-specific is used: no kgsl and no vendor ioctls, only AHardwareBuffer, sync_file and the vendor's public GLES/Vulkan.
- About 20 GB free for the container image.
**Development host.**
- The scripts are written for **Windows + Git Bash** with WSL (`archlinux` distro) for the unit tests and the cross build.
- A Linux host works too: drop the `cygpath`/`MSYS_NO_PATHCONV` handling in `env.sh`, and run the WSL parts natively.
- Tools:
- `adb`, CMake >= 3.22, Ninja, Python 3 (+ Pillow for screenshots/recording).
- Android NDK **27.2.12479018**.
- JDK 17 and Android SDK platform 36 (for the APK).
- In the Linux environment: clang/lld/llvm of ONE major version (a mismatched lld breaks `ld.lld`), cmake, ninja.
## 1. Sources
```sh
# MobileGL: branch feat/disaggregated (everything here is on it)
git clone https://github.com/MobileGL-Dev/MobileGL.git && cd MobileGL
git checkout feat/disaggregated
GIT_LFS_SKIP_SMUDGE=1 git submodule update --init --recursive
# 3rdparty/glslang is the MobileGL-Dev fork (main); its External/spirv-tools must be populated
# (glslang's own update_glslang_sources.py, or copy it from another checkout).
# Keep builds out of a shared checkout: work in a worktree (GIT_LFS_SKIP_SMUDGE=1 git worktree add ...).
# anland: branch legacy-mobilegl-unified (KWin backend + patch, startup script, APK, daemon module)
git clone https://github.com/MobileGL-Dev/anland.git && cd anland # the fork that carries this branch
git checkout legacy-mobilegl-unified
```
Build the server (APK) and the client (container) from the **same MobileGL commit**: both stamp
`git rev-parse HEAD`, and a mismatch logs "compatible wire with different build".
## 2. Phone base: root, Droidspaces, display daemon
1. **Root** with KernelSU (or another `su` provider).
2. **Droidspaces:** install the module and its app. Containers live under `/data/local/Droidspaces/Containers/<name>/`, and the CLI is `/data/local/Droidspaces/bin/droidspaces`.
3. **anland display daemon** (KernelSU/Magisk module `anland-daemon`):
- Build it from the anland repo with `build_daemon_android.sh`, then package it with `build_magisk_module.sh`, which uses `magisk_module/`. Flash it and reboot.
- The module's `service.sh` starts the original daemon on `/data/local/tmp/display_daemon.sock`.
- The MobileGL session uses a SECOND instance of the same binary on `/data/local/tmp/anland-mobilegl/display.sock`: see `scripts/device/restart-daemon.sh`, which also chmods the directory and socket.
## 3. The container
Use an **Arch Linux ARM (aarch64, glibc) KDE Plasma Wayland rootfs** that has a systemd `desktop-session.service` running the desktop as a normal user with `PAMName=login`. A ready-made one is the Arch KDE "Wayland" image from Droidspaces RootFS Desktop Builder (see anland `doc/UserManual/anland_guide.md`, "Use prebuilt packages or a RootFS"). Or install `plasma-desktop`, `plasma-workspace`, `libglvnd`, `xorg-xwayland`, `base-devel`, `cmake` and `ninja` on plain Arch Linux ARM and write the service yourself.
Import it into Droidspaces as `arch-kde-mgl`. The default name is in `env.sh`. Reference `container.config` keys:
```
enable_gpu_mode=1
net_mode=host
bind_mounts=/data/local/tmp/display_daemon.sock:/run/display.sock,/data/local/tmp/anland-mobilegl:/run/anland-mobilegl
run_at_boot=0
```
- **Host networking:** it is required. The MobileGL server listens on the ABSTRACT socket `@anland-mobilegl`, which crosses into the container only when the network namespace is shared.
- **The anland-mobilegl directory:** create `/data/local/tmp/anland-mobilegl` on the phone before the first start, mode 777.
- **Desktop user:** the default is `swung0x48` (`MGL_DESKTOP_USER`). Create your own and point `desktop-session.service` at it.
- **Starting it:** `droidspaces -C .../container.config start`. Android root sees the rootfs at `/mnt/Droidspaces/<name>`, which `CT_ROOT` in `env.sh` relies on.
- **Container tools:** install them with `bash scripts/push-tools.sh` once the container runs.
## 4. MobileGL server and the anland APK
```sh
bash scripts/build-android.sh # -> <worktree>/build-android/libMobileGL.so
```
The script runs, with the NDK toolchain:
```
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF
```
APK, from anland `consumers/anland_v5/android_consumer` (details in its `MOBILEGL.md`):
```sh
mkdir -p dist/arm64-v8a && cp <worktree>/build-android/libMobileGL.so dist/arm64-v8a/
MOBILEGL_DIST=$PWD/dist ./gradlew :app:assemblePlainDebug \
-PanlandNdkVersion=27.2.12479018 -PmobileglApplicationId=com.anland.consumer.mobilegl \
-PanlandDefaultSocket=/data/local/tmp/anland-mobilegl/display.sock
adb install -r app/build/outputs/apk/plain/debug/app-plain-debug.apk
```
- **Grant root:** the app needs root for its helper; approve it in KernelSU.
- **Reinstall with another signing key:** that changes the UID, so grant root again (anland `mobilegl-tools/ksu_grant.c` does it from a shell). Don't uninstall to fix a signature clash: that wipes the app data.
- **Later server-only changes:** swap just the library into the installed APK (`scripts/cyc.sh` / `device/swap.sh`) instead of rebuilding the APK.
## 5. MobileGL client in the container
Pick one of these two:
- **Cross build** (fast, from WSL or Linux):
1. `bash scripts/xsysroot/refresh-sysroot.sh` exports the container's headers and libraries to `~/sysroots/arch-kde-mgl` and installs `~/sysroots/aarch64-arch.cmake`. Re-run it after container package upgrades.
2. `bash scripts/xbuild.sh`, then `bash scripts/xdeploy.sh`.
- **Native build in the container** (slow, no WSL): `scripts/sync-src.sh` copies the worktree to `/root/mobilegl-unified-src`, then run anland `producers/kde/Arch_v5/mobilegl-tools/anland-build-client.sh` inside (g++, `-DMOBILEGL_BUILD_DISAGGREGATED=ON`, Release).
Either route installs the same files, renamed into place so running clients keep their old mapping:
| File | What it is |
|---|---|
| `/opt/mobilegl/lib/libMobileGL.so` (+ `libEGL`/`libGL` aliases) | the client library |
| `/usr/share/glvnd/egl_vendor.d/10_mobilegl.json` | system-wide EGL vendor; sorts before the distribution's `50_*.json`, which serves whenever MobileGL declines (no server reachable) |
| `/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json` | per-process forcing |
| `/usr/lib/libGLX_mobilegl.so.0` | GLX vendor |
| `/usr/lib/gbm/mobilegl_gbm.so` | GBM backend (zero-copy window buffers) |
| `/etc/mobilegl/client.conf` | `MOBILEGL_TRANSPORT=spawn`, `MOBILEGL_IPC_DATA=shm`, `MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl` |
| `/etc/mobilegl/backend` | `DirectVulkan` (Magma) or `DirectGLES` (Espryt) |
| `/etc/environment.d/10-mobilegl.conf`, `/etc/profile.d/mobilegl.sh` | `__GLX_VENDOR_LIBRARY_NAME=mobilegl`, `GBM_BACKEND=mobilegl` |
## 6. KWin 6.7.4 with the anland backend, and Xwayland
The MobileGL session runs a rebuilt KWin from `/opt/mobilegl/kwin`, so the packaged one stays untouched.
1. **Source tree:** in the container, unpack KWin 6.7.4 to `/root/mobilegl-build/kwin-6.7.4`, apply anland `producers/kde/Arch_v5/kwin.patch`, and overlay `producers/kde/anland_backend_Arch_v5/src/backends/anland` into `src/backends/anland`. Also take the current `common/` and `libdisplay_producer/`, as `mobilegl.md` there says.
2. **Configure:**
```sh
cmake -S kwin-6.7.4 -B kwin-6.7.4/build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_LIBDIR=lib -DBUILD_TESTING=OFF
```
3. **Build and install:** run `producers/kde/Arch_v5/sync-build-kwin.sh` (`ninja -j4`). It installs `kwin_wayland` + `libkwin.so.6.7.4` into `/opt/mobilegl/kwin/{bin,lib}`, the patched screencast plugin in place (the original is kept beside it), and `/opt/mobilegl/bin/mobilegl-startup.sh`.
- From the dev host, `bash scripts/kwin-stage.sh build` does steps 1–3 incrementally: `kwprep.sh` applies changed patch hunks.
4. **Xwayland:** the anland-patched Xwayland 24.1.13 comes from `producers/kde/Arch_v5/build.sh` (makepkg, as a normal user; it builds the anland KWin and Xwayland packages). A prebuilt anland rootfs already has it.
5. **Session hook:** add `/etc/systemd/system/desktop-session.service.d/mobilegl.conf`:
```ini
[Service]
ExecStart=
ExecStart=/opt/mobilegl/bin/mobilegl-startup.sh plasma
Environment=ANLAND_SOCKET=/run/anland-mobilegl/display.sock
Environment=QT_LOGGING_RULES=kwin_*.info=true
```
`mobilegl-startup.sh plasma` writes the user drop-in for `plasma-kwin_wayland.service` (the `/opt/mobilegl/kwin` binary, `ANLAND_MOBILEGL=1`, `MOBILEGL_IPC_SURFACE=server`, `GBM_BACKEND=mobilegl`) and runs `startplasma-wayland`.
## 7. Bring it up
Nothing has to be started by hand, and the order no longer matters: **open the app** (its launcher
icon, or `am start -n com.anland.consumer.mobilegl/com.anland.consumer.MainActivity` with no extras).
That is the single trigger. The app starts its foreground service and runs its bundled
`mobilegl-desktop.sh up` as root, which:
1. starts the display daemon if it is not running;
2. publishes the backend (the app setting "Renderer backend", default DirectGLES) to
`/data/local/tmp/anland-mobilegl/backend`;
3. starts the container if it is stopped;
4. starts `desktop-session`, which waits for the server and the daemon socket, copies the backend into
`/etc/mobilegl/backend` and runs Plasma.
Cold start to a desktop takes about 8-12 s. `scripts/device/bringup.sh` wakes the phone, dismisses the
keyguard, opens the app and prints the timeline. To change the backend, use the app setting and then
Stop desktop and reopen, or run `scripts/device/switch.sh DirectGLES|DirectVulkan`. Do not use
`setprop debug.mobilegl.backend`: it overrides the setting until the next reboot. **Stop desktop**
(the notification action) stops the session and the container.
**After a phone reboot:** wireless adb does not come back by itself. Re-enable it on the phone
(Developer options > Wireless debugging), then `adb connect <ip>:<port>`; the port changes. After
that, open the app as above. See SKILL.md "Starting, idling, stopping".
## 8. Verify
- **Plasma:** it appears in the app. The tray must NOT show the "software renderer" icon; if it does, delete `SceneGraphBackend=software` from the user's `kdeglobals` and restart.
- **Plain shell:** in a plain container shell as the desktop user, `eglinfo -B` names MobileGL, and `glxinfo -B` from a login shell does too.
- **Wayland client:** `timeout 25 mgrun offscreen glmark2-es2-wayland -s 1280x720`. The log must say `presented through linux-dmabuf shared images`.
- **Chrome:** `bash scripts/chrome-launch.sh t1` gives windows with correct icons.
- **Resize and lock:** BACK toggles the extra-keys bar (resize); lock/unlock resumes the desktop. `rtest.sh` in the scratch dir checks both.
- **Unit tests:** `bash scripts/wsl-all.sh` in WSL. NEVER run the DirectVulkan integration tests.
## 9. Machine-specific values to change
| Value | Original | Where |
|---|---|---|
| MobileGL worktree | this skill's worktree | `MGL_WORKTREE` (auto) |
| scratch dir | `%LOCALAPPDATA%/Temp/anl` | `ANL_DIR` |
| NDK | `<SDK>/ndk/27.2.12479018` | `ANDROID_NDK` |
| anland worktree | `%USERPROFILE%/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified` | `ANLAND_WORKTREE` (`kwin-stage.sh`) |
| APK id | `com.anland.consumer.mobilegl` | `MGL_PKG` |
| container | `arch-kde-mgl` | `MGL_CONTAINER` |
| desktop user | `swung0x48` | `MGL_DESKTOP_USER` + `desktop-session.service` |
| WSL distro | `archlinux` | `WSL_DISTRO` (`xbuild.sh`, `wsl-*.sh`) |
| cross build dir | WSL `~/mgl-xbuild-a64` | `MGL_XBUILD_DIR` |
| phone | Lenovo TB321FU, Adreno 750 | nothing: no serial is pinned; with several devices set `ANDROID_SERIAL` |
**Thermals:** a phone vendor's thermal daemon may throttle charging while the screen is on during long sessions. That is not part of this setup.
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# build-android.sh - build the Android SERVER libMobileGL.so (NDK, arm64-v8a) that the anland APK embeds.
# Output: $MGL_WORKTREE/build-android/libMobileGL.so (RelWithDebInfo, unstripped, ~320 MB; keep it for
# llvm-addr2line, it is what cyc.sh pushes). Stamp = worktree HEAD, so the container client
# (xbuild.sh, same stamp) and the server agree on the build ("compatible wire with different build"
# WARN otherwise). Env: MGL_WORKTREE, ANDROID_NDK, BUILD_DIR (default build-android), JOBS (12).
# Log level is compiled to INFO. android-30 = the APK's minSdk: thread_local is then the platform's
# own TLS rather than the emulated one (which cost 7-12% of an apply thread on the API 26 build).
set -euo pipefail
. "$(dirname "$0")/env.sh"
W=$MGL_WORKTREE
B=$W/${BUILD_DIR:-build-android}
STAMP=$(git -C "$W" rev-parse HEAD)
if [ ! -f "$B/build.ninja" ]; then
cmake -S "$W" -B "$B" -G Ninja -DCMAKE_TOOLCHAIN_FILE="$ANDROID_NDK/build/cmake/android.toolchain.cmake" \
-DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-30 -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DMOBILEGL_PIPE_PUSH=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON \
-DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DCMAKE_CXX_FLAGS=-DMOBILEGL_LOG_ACTIVE_LEVEL=MOBILEGL_LOG_LEVEL_INFO \
-DMOBILEGL_BUILD_STAMP="$STAMP"
else
cmake "$B" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
cmake --build "$B" --target MobileGL -j"${JOBS:-12}"
ls -la "$B/libMobileGL.so"
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# chrome-launch.sh <tag> [extra chrome flags...] - (re)start Chrome inside the running Plasma session
# through mobilegl-startup.sh's `chrome` mode (ANGLE GLES over MobileGL EGL), client log
# /tmp/mgl-chrome-<tag>.client.log, then print its "presented through" line and save a screenshot
# to $ANL_DIR/ch-<tag>.png. Kills the running chrome first (pkill -x, never -f). MobileGL is the
# container's system-wide vendor (10_mobilegl.json + /etc/mobilegl/client.conf), so only the log is set here.
# Env: WAIT (s before checking, default 20), CHANNEL (ct.sh channel, default chrome),
# CHROME_GPU (process, the default: the GPU in its own process, which presents
# through GBM dma-bufs and is restarted by Chrome after a device loss; in-process opts out -
# needs the anland startup script that knows MOBILEGL_CHROME_GPU; in process mode the GPU process and the browser
# write the one client log, the GPU process last); CHROME_GPU_SANDBOX=on keeps its GPU sandbox.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
TAG=${1:?usage: chrome-launch.sh <tag> [chrome flags...]}; shift
bash "$here/ct.sh" "${CHANNEL:-chrome}" <<IN
pkill -x chrome; sleep 2
U=\$(id -u $MGL_DESKTOP_USER)
rm -f /tmp/mgl-chrome-$TAG.client.log /tmp/chrome-$TAG-out.log
sudo -u $MGL_DESKTOP_USER env XDG_RUNTIME_DIR=/run/user/\$U WAYLAND_DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/\$U/bus MOBILEGL_LOG_FILE_PATH=/tmp/mgl-chrome-$TAG.log MOBILEGL_CHROME_GPU=${CHROME_GPU:-process} MOBILEGL_CHROME_GPU_SANDBOX=${CHROME_GPU_SANDBOX:-off} bash -c 'nohup /opt/mobilegl/bin/mobilegl-startup.sh chrome --restore-last-session $* > /tmp/chrome-$TAG-out.log 2>&1 &'
sleep ${WAIT:-20}
grep -ah 'presented through' /tmp/mgl-chrome-$TAG.client.log | head -3
IN
adb_ shell "su -c 'screencap -p $DEV_SHARE/ch-$TAG.png'"
adb_ pull "$DEV_SHARE/ch-$TAG.png" "$ANL_DIR/ch-$TAG.png" >/dev/null && echo "screenshot: $ANL_DIR/ch-$TAG.png"
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# chrome-recover.sh <in-process|process> [rounds] - does Chrome repaint after a GPU device loss,
# without a restart? Starts Chrome (chrome-launch.sh, CHROME_GPU=<mode>) on an animated WebGL page,
# then per round: screenshots (the page must animate), loses the session of Chrome's GPU client with
# the server's pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid; the pid as Chrome sees
# itself in the container), waits, and screenshots again. A round PASSES when the server latched
# that pid's session, the browser process is the same one (no restart), a GPU client is drawing on a
# NEW server session, and the screen still animates. Stops early below MIN_BATTERY percent.
#
# in-process the GPU in the browser process (the startup script's default today): the target is
# the browser itself; recovery needs ANGLE to make a new native context.
# process the GPU in its own process (MOBILEGL_CHROME_GPU=process): the target is the GPU
# process; Chrome restarts it after the loss, so the next round's target is a new pid.
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# STARTUP=<anland worktree>/producers/kde/Arch_v5/mobilegl-startup.sh bash scripts/chrome-recover.sh deploy-startup
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 8), MIN_BATTERY (default 40),
# CHANNEL (ct.sh channel, default crec), TAG (log tag, default crec-<mode>).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
MODE=${1:?usage: chrome-recover.sh <in-process|process|deploy-startup> [rounds]}
ROUNDS=${2:-${ROUNDS:-2}}
SETTLE=${SETTLE:-8}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-crec}
TAG=${TAG:-crec-$MODE}
PAGE=/tmp/mgl-spin.html
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; cleanup; exit 3
fi
}
cleanup() { adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true; }
if [ "$MODE" = deploy-startup ]; then
# The anland startup script that knows MOBILEGL_CHROME_GPU, installed by rename (a running
# session keeps its open copy).
src=${STARTUP:?STARTUP=<path to the anland mobilegl-startup.sh>}
tr -d '\r' < "$src" > "$ANL_DIR/mobilegl-startup.sh"
adb_ push "$ANL_DIR/mobilegl-startup.sh" "$DEV_SHARE/mobilegl-startup.sh.new" >/dev/null
ct <<'IN'
install -m 755 /run/anland-mobilegl/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh.new
mv -f /opt/mobilegl/bin/mobilegl-startup.sh.new /opt/mobilegl/bin/mobilegl-startup.sh
grep -c MOBILEGL_CHROME_GPU /opt/mobilegl/bin/mobilegl-startup.sh
IN
exit 0
fi
case "$MODE" in in-process|process) ;; *) echo "mode must be in-process or process" >&2; exit 2;; esac
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
cleanup
# The page: WebGL clears that change colour every frame plus a moving block, so a frame that
# reaches the screen changes the screenshot.
ct <<IN
cat > $PAGE <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl');
let t = 0, lost = 0;
document.getElementById('c').addEventListener('webglcontextlost', e => { e.preventDefault(); lost++; });
function frame() {
t++;
if (gl && !gl.isContextLost()) {
gl.clearColor((t % 60) / 60, ((t + 20) % 60) / 60, ((t + 40) % 60) / 60, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.SCISSOR_TEST); gl.scissor((t * 7) % 600, 100, 40, 160);
gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT); gl.disable(gl.SCISSOR_TEST);
}
document.title = 'spin ' + t + ' lost ' + lost;
requestAnimationFrame(frame);
}
frame();
</script></body></html>
HTML
chmod 644 $PAGE
IN
WAIT=${WAIT:-20} CHROME_GPU=$MODE CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file://$PAGE"
# The target pid inside the container: the browser (in-process) or the GPU process.
target_pid() {
ct <<'IN' | tr -d '\r' | tail -1
for p in $(pgrep -x chrome); do
args=$(tr '\0' ' ' < /proc/$p/cmdline)
case "$args" in
*--type=gpu-process*) echo "gpu $p" ;;
*--type=*) ;;
*) echo "browser $p" ;;
esac
done | sort > /tmp/mgl-crec-pids
cat /tmp/mgl-crec-pids >&2
echo "$(awk '/^browser/ {print $2; exit}' /tmp/mgl-crec-pids) $(awk '/^gpu/ {print $2; exit}' /tmp/mgl-crec-pids)"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS ($MODE)"
check_battery
read -r browser gpu < <(target_pid)
if [ "$MODE" = process ]; then target=$gpu; else target=$browser; fi
echo "browser=$browser gpu=${gpu:-none} target=$target"
if [ -z "$target" ]; then echo "FAIL: no target process"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
adb_ logcat -c
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $target'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
latched=$(adb_ logcat -d | grep -c "injected:pid=$target" || true)
started=$(adb_ logcat -d | grep -c "session #[0-9]* started" || true)
adb_ logcat -d | grep -E "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)" | cut -c1-220 | head -12 || true
read -r browser2 gpu2 < <(target_pid)
echo "after: browser=$browser2 gpu=${gpu2:-none}"
ct <<IN || true
echo "--- client log ($TAG):"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|failed to make|session #" /tmp/mgl-chrome-$TAG.client.log | tail -8
echo "--- chrome stderr:"; grep -aiE "context lost|GPU process|make.*current|lost" /tmp/chrome-$TAG-out.log | tail -8
IN
adb_ logcat -b crash -d | tail -5
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched pid $target"; verdict=FAIL; }
[ "$browser2" = "$browser" ] || { echo " - the browser process changed ($browser -> $browser2): Chrome restarted"; verdict=FAIL; }
[ "$started" -ge 1 ] || { echo " - no new server session started after the loss"; verdict=FAIL; }
if [ "$MODE" = process ]; then
[ -n "$gpu2" ] && [ "$gpu2" != "$gpu" ] || { echo " - no new GPU process"; verdict=FAIL; }
fi
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
done
echo "=== $pass/$ROUNDS rounds passed ($MODE); screenshots in $ANL_DIR/$TAG-r*-*.png"
@@ -0,0 +1,29 @@
#!/bin/bash
# kwprep.sh - (container, as root) bring the live KWin tree /root/mobilegl-build/kwin-6.7.4 in line with
# anland's kwin.patch + anland backend sources, touching ONLY files whose content changed (keeps the
# ninja build incremental). It re-applies the whole patch to a pristine copy of KWin's src/
# (/root/gbm-dev/kwin-a/src) in /root/kp and copies every patched file that differs into the live tree,
# then copies changed backend files. Build + install afterwards with anland's sync-build-kwin.sh.
#
# Input: /root/kwsync.tgz, a tar made in the anland worktree's producers/kde directory:
# tar czf kwsync.tgz Arch_v5/kwin.patch Arch_v5/mobilegl-startup.sh anland_backend_Arch_v5/src/backends/anland/
# and pushed to the container's /root (e.g. via /mnt/Droidspaces/arch-kde-mgl/root/ on the Android side).
# The pristine tree /root/gbm-dev/kwin-a/src was set up by hand earlier (unverified that it is exactly
# upstream 6.7.4); patch hunks that fail are reported by `patch`.
set -e
rm -rf /root/kwsync && mkdir -p /root/kwsync && tar xzf /root/kwsync.tgz -C /root/kwsync
P=/root/kwsync/Arch_v5/kwin.patch
rm -rf /root/kp && mkdir -p /root/kp && cp -a /root/gbm-dev/kwin-a/src /root/kp/
cd /root/kp && patch -p1 -s < $P
LIVE=/root/mobilegl-build/kwin-6.7.4
FILES=$(grep -E '^\+\+\+ b/' $P | sed 's|^+++ b/||' | awk '{print $1}' | sort -u)
for f in $FILES; do
if [ -f "/root/kp/$f" ] && ! cmp -s "/root/kp/$f" "$LIVE/$f"; then cp "/root/kp/$f" "$LIVE/$f"; echo "updated $f"; fi
done
for f in /root/kwsync/anland_backend_Arch_v5/src/backends/anland/*; do
# libdisplay_producer is a symlink in git; a Windows checkout turns it into a small text file. The live
# tree has the real directory (repo-root libdisplay_producer/), which neither script syncs.
[ -f "$f" ] && [ "$(basename $f)" != libdisplay_producer ] || continue
b=$(basename $f); if ! cmp -s "$f" "$LIVE/src/backends/anland/$b"; then cp "$f" "$LIVE/src/backends/anland/$b"; echo "updated anland/$b"; fi
done
echo PREP_DONE
@@ -0,0 +1,21 @@
#!/bin/bash
# mgrun <surface: offscreen|server> <command...> - (container, as root) run one MobileGL client as the
# desktop user inside the running Plasma session. Installed as /usr/local/bin/mgrun (push-tools.sh).
# mgrun offscreen glmark2-es2-wayland -s 1280x720 # Wayland client, presents through KWin
# mgrun offscreen eglinfo -B ; mgrun offscreen glxinfo -B # glxinfo goes through Xwayland + GLX vendor
# "server" is only for a client that owns the Android Surface itself (KWin does); everything else is
# offscreen. MobileGL is the container's system-wide vendor, so nothing selects it here: EGL comes from
# /usr/share/glvnd/egl_vendor.d/10_mobilegl.json, transport/endpoint from /etc/mobilegl/client.conf,
# the backend from /etc/mobilegl/backend (must match the server), and the GLX vendor name / GBM backend
# from /etc/profile.d/mobilegl.sh (sourced by the login shell below). Client log:
# ${MGLOG:-/tmp/mgl-run.log} -> the library writes /tmp/mgl-run.client.log (truncated per run; give
# concurrent clients different MGLOG values). Env: MGLOG, DESKTOP_USER (swung0x48).
S=$1; shift
DU=${DESKTOP_USER:-swung0x48}
U=$(id -u "$DU")
cd /tmp
exec sudo -u "$DU" env XDG_RUNTIME_DIR=/run/user/$U WAYLAND_DISPLAY=wayland-0 DISPLAY=:0 \
XAUTHORITY=$(ls /run/user/$U/xauth_* 2>/dev/null | head -1) \
DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$U/bus QT_QPA_PLATFORM=wayland \
MOBILEGL_IPC_SURFACE=$S MOBILEGL_LOG_FILE_PATH=${MGLOG:-/tmp/mgl-run.log} \
bash -lc 'exec "$@"' mgrun "$@"
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# ct.sh <channel> [script-file] - run a bash script as ROOT inside the container (login shell),
# reading the script from the file or from stdin. Generic form of the old c.sh/c2.sh/c3.sh.
#
# printf '%s\n' 'ls -la /tmp/*.log' | bash ct.sh mychan
# bash ct.sh mychan ./something.cmd
#
# How: the script is written to $ANL_DIR/<channel>.cmd, pushed to $DEV_TOOLS/<channel>.cmd together with
# a one-line <channel>.sh that does `droidspaces --name=<container> run bash -lc "$(cat <channel>.cmd)"`,
# and that runs under `su`. Quoting through adb + su + droidspaces is thereby avoided entirely.
#
# Pick a channel name of your own: other sessions on the same phone use c, c2, c3, xd, xr, gbm...;
# writing to their channel files races with them. CR characters are stripped (Windows checkouts).
# Inside, run user-side commands with `sudo -u <desktop user> env XDG_RUNTIME_DIR=/run/user/<uid> ...`.
# Never `pkill -f <word>` in a channel script: the `bash -lc "<script>"` process carries the whole
# script on its command line and kills itself. Use `pkill -x <name>`.
set -euo pipefail
. "$(dirname "$0")/env.sh"
ch=${1:?usage: ct.sh <channel> [script-file]}
src=${2:--}
if [ "$src" = - ]; then tr -d '\r' > "$ANL_DIR/$ch.cmd"; else tr -d '\r' < "$src" > "$ANL_DIR/$ch.cmd"; fi
printf '%s\n' "$DS_BIN --name=$MGL_CONTAINER run bash -lc \"\$(cat $DEV_TOOLS/$ch.cmd)\"" > "$ANL_DIR/$ch.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
adb_ push "$ANL_DIR/$ch.cmd" "$DEV_TOOLS/$ch.cmd" >/dev/null
adb_ push "$ANL_DIR/$ch.sh" "$DEV_TOOLS/$ch.sh" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/$ch.sh'"
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# cyc.sh <shot-name> - one server iteration: clear Plasma's sticky software-renderer key, push the
# NDK-built server lib, swap it into the installed APK (device/swap.sh), restart app + Plasma session
# (device/run-plasma.sh, ~90 s), screenshot to $ANL_DIR/<shot-name>.png, print fatal logcat lines.
# Env: SERVER_LIB (default $MGL_WORKTREE/build-android/libMobileGL.so), WAIT (s after the session
# restart, default 15), N (logcat lines, default 20), CHANNEL (ct.sh channel, default cyc).
# Needs device/swap.sh + device/run-plasma.sh in $DEV_TOOLS (push-tools.sh). RESTARTS THE SESSION.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
shot=${1:?usage: cyc.sh <shot-name>}
LIB=${SERVER_LIB:-$MGL_WORKTREE/build-android/libMobileGL.so}
printf '%s\n' "sudo -u $MGL_DESKTOP_USER kwriteconfig6 --file kdeglobals --group QtQuickRendererSettings --key SceneGraphBackend --delete; rm -f /tmp/mgl-plasmashell.client.log" \
| bash "$here/ct.sh" "${CHANNEL:-cyc}"
adb_ push "$LIB" "$DEV_SHARE/libMobileGL.new.so" >/dev/null
adb_ logcat -c
adb_ shell "su -c 'sh $DEV_TOOLS/swap.sh >/dev/null && sh $DEV_TOOLS/run-plasma.sh >/dev/null 2>&1; sleep ${WAIT:-15}; screencap -p $DEV_SHARE/$shot.png'"
adb_ pull "$DEV_SHARE/$shot.png" "$ANL_DIR/$shot.png" >/dev/null && echo "screenshot: $ANL_DIR/$shot.png"
adb_ logcat -d | grep -E "Fatal signal|FATAL|Abort message|Magma wire buffer|E MobileGL" | cut -c1-260 | head -"${N:-20}" || true
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# desktop-verify.sh <phase>... - check that the MobileGL desktop starts, idles and stops by itself
# (anland app: foreground service + mobilegl-desktop.sh; container: desktop-session.service).
# Phases run in the order given; screenshots go to $ANL_DIR/dv-<phase>.png.
#
# push install device/*.sh into $DEV_TOOLS (push-tools.sh's device half)
# reset everything down: app force-stopped (its service with it), container stopped,
# experiment display daemon killed - the state after a phone reboot
# cold reset, then open the app like its icon does and time each piece (desktop-timeline.sh)
# open open the app without resetting (after a reboot: the reboot was the reset)
# chrome (re)start Chrome in the session on an animated WebGL page (kills a running Chrome)
# idle CPU/GPU for visible, hidden (HOME), locked (screen off), visible again (measure.sh)
# reopen swipe the app away (remove its task), check KWin survives, open it again, time it
# stop Stop desktop from the notification (the service's STOP action) and check it all ends
# rtest lock/unlock + window resize regression ($ANL_DIR/rtest.sh)
# Env: SECS (idle window, default 20), URL (chrome page).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
SECS=${SECS:-20}
URL=${URL:-https://webglsamples.org/aquarium/aquarium.html}
SOCK=$DEV_SHARE/display.sock
su_() { adb_ shell "su -c '$*'"; }
shot() {
su_ "screencap -p $DEV_SHARE/dv-$1.png"
adb_ pull "$DEV_SHARE/dv-$1.png" "$ANL_DIR/dv-$1.png" > /dev/null && echo "screenshot $ANL_DIR/dv-$1.png"
}
kwin_log() { # the anland backend's lines since <time>
printf '%s\n' "journalctl --no-pager --since '$1' 2>/dev/null | grep -a -E 'viewer (gone|back)|consumer (reconnected|disconnected)|failed to connect' | tail -${2:-8}" |
bash "$here/ct.sh" dv 2>/dev/null | grep -v pushed || true
}
task_id() {
su_ "am stack list" | tr -d '\r' | grep -m1 "$MGL_PKG/" | sed -n 's/.*taskId=\([0-9]*\).*/\1/p'
}
wake() {
su_ "input keyevent KEYCODE_WAKEUP"; sleep 1; su_ "wm dismiss-keyguard"; sleep 2
}
for phase in "$@"; do
echo "=== $phase"
case $phase in
push)
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" > /dev/null
done
rm -f "$ANL_DIR/push.tmp"; echo "device scripts in $DEV_TOOLS" ;;
reset)
su_ "sh $DEV_TOOLS/desktop-reset.sh" ;;
cold)
bash "$0" reset
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 240"
sleep 3; shot cold ;;
open)
wake
su_ "sh $DEV_TOOLS/desktop-timeline.sh 300"
sleep 3; shot open ;;
chrome)
WAIT=15 bash "$here/chrome-launch.sh" dv "$URL" ;;
idle)
su_ "sh $DEV_TOOLS/measure.sh visible $SECS"
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
su_ "input keyevent KEYCODE_HOME"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh hidden $SECS"
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "input keyevent KEYCODE_SLEEP"; sleep 4
su_ "sh $DEV_TOOLS/measure.sh locked $SECS"
wake
su_ "am start -n $MGL_PKG/com.anland.consumer.MainActivity >/dev/null"; sleep 5
su_ "sh $DEV_TOOLS/measure.sh visible2 $SECS"
shot idle
kwin_log "$t" 12 ;;
reopen)
t=$(date -u '+%Y-%m-%d %H:%M:%S UTC')
id=$(task_id); echo "removing task $id"
[ -n "$id" ] && su_ "am stack remove $id"
sleep 8
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG) kwin: \$(pidof kwin_wayland) plasmashell: \$(pidof plasmashell)"
su_ "sh $DEV_TOOLS/measure.sh closed 10"
su_ "sh $DEV_TOOLS/desktop-timeline.sh 120"
sleep 3; shot reopen
kwin_log "$t" ;;
stop)
su_ "am startservice -n $MGL_PKG/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP" || true
sleep 25
su_ "echo server: \$(pidof $MGL_PKG:mobilegl) ui: \$(pidof $MGL_PKG); $DS_BIN show"
su_ "tail -3 $DEV_SHARE/desktop.log" ;;
rtest)
bash "$ANL_DIR/rtest.sh" dv ;;
*)
echo "unknown phase $phase" >&2; exit 2 ;;
esac
done
@@ -0,0 +1,7 @@
# bringup.sh - (Android root shell) bring the MobileGL desktop up after a phone reboot. There is
# nothing left to do by hand: opening the anland app (its launcher icon, or this am start) starts
# the display daemon, the MobileGL server, the container and its Plasma session. Prints the timeline.
# Run: su -c "sh /data/local/tmp/anl/bringup.sh" Env: as desktop-timeline.sh.
input keyevent KEYCODE_WAKEUP
wm dismiss-keyguard
sh "$(dirname "$0")/desktop-timeline.sh" 300
@@ -0,0 +1,17 @@
# desktop-reset.sh - (Android root shell) take the MobileGL desktop fully down, as after a phone
# reboot: the app force-stopped (its foreground service and server with it), the container
# stopped, the experiment display daemon killed. Leaves the original anland daemon alone.
# Run: su -c "sh /data/local/tmp/anl/desktop-reset.sh" Env: PKG, CONTAINER, SOCK, DS.
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
am force-stop $P
$D --name=$C stop > /dev/null 2>&1
for p in $(pidof display_daemon); do
tr '\0' ' ' < /proc/$p/cmdline | grep -qF " $SOCK " && kill $p
done
rm -f $SOCK
sleep 2
echo "app: $(pidof $P) server: $(pidof $P:mobilegl) daemon: $(ls $SOCK 2>/dev/null)"
$D show
@@ -0,0 +1,30 @@
# desktop-timeline.sh [timeout-s] - (Android root shell) open the anland app the way its launcher
# icon does (am start, no extras) and print when each piece of the MobileGL desktop comes up,
# in seconds after the open: display daemon socket, the app's MobileGL server listening, the
# container, kwin_wayland, plasmashell, and the splash (ksplashqml) gone = desktop on screen.
# Pieces that already run show 0.0. Read-only apart from the am start.
# Run: su -c "sh /data/local/tmp/anl/desktop-timeline.sh 180" Env: PKG, CONTAINER, SOCK, DS.
T=${1:-180}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
now() { awk '{print $1}' /proc/uptime; }
since() { awk -v a="$1" -v b="$(now)" 'BEGIN{printf "%.1f", b - a}'; }
listening() { grep -q '00010000 0001 01 [0-9]* @anland-mobilegl$' /proc/net/unix; }
t0=$(now)
am start -n $P/com.anland.consumer.MainActivity > /dev/null
for k in daemon server container kwin shell splash_seen shown; do eval "t_$k="; done
while :; do
[ -z "$t_daemon" ] && [ -S "$SOCK" ] && t_daemon=$(since $t0) && echo "daemon socket +${t_daemon}s"
[ -z "$t_server" ] && listening && t_server=$(since $t0) && echo "server listening +${t_server}s"
[ -z "$t_container" ] && [ -n "$($D --name=$C pid 2>/dev/null | tr -dc 0-9)" ] && t_container=$(since $t0) && echo "container up +${t_container}s"
[ -z "$t_kwin" ] && pidof kwin_wayland > /dev/null && t_kwin=$(since $t0) && echo "kwin_wayland +${t_kwin}s"
[ -z "$t_shell" ] && pidof plasmashell > /dev/null && t_shell=$(since $t0) && echo "plasmashell +${t_shell}s"
[ -z "$t_splash_seen" ] && pidof ksplashqml > /dev/null && t_splash_seen=1
if [ -n "$t_shell" ] && [ -z "$t_shown" ] && ! pidof ksplashqml > /dev/null; then
t_shown=$(since $t0); echo "splash gone +${t_shown}s (desktop on screen)"; break
fi
[ "$(since $t0 | cut -d. -f1)" -ge "$T" ] && { echo "TIMEOUT after ${T}s"; break; }
sleep 0.3
done
@@ -0,0 +1,47 @@
# measure.sh <label> [seconds] - (Android root shell) CPU and GPU load of the MobileGL desktop over a
# window: % of ONE core (so 800 = all 8 cores) for the whole phone, KWin (+Xwayland), plasmashell,
# Chrome (all its processes), the app's :mobilegl server and its UI process, plus the mean of the
# kgsl gpu_busy_percentage samples (1/s). The container's processes are visible from Android's
# /proc (Droidspaces only gives them their own pid namespace).
# Run: su -c "sh /data/local/tmp/anl/measure.sh visible 20" Env: PKG.
L=${1:-sample}
N=${2:-20}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
pids_named() { # exact process names (comm or argv0 basename)
for n in "$@"; do pidof "$n"; done
}
pids_matching() { # argv contains the word
for d in /proc/[0-9]*; do
{ tr '\0' ' ' < $d/cmdline; } 2>/dev/null | grep -q "$1" && echo ${d#/proc/}
done
}
ticks() { # utime+stime of the pids, in clock ticks
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
KW="$(pids_named kwin_wayland Xwayland)"
SH="$(pids_named plasmashell)"
CH="$(pids_matching /opt/google/chrome/)"
SV="$(pidof $P:mobilegl)"
UI="$(pidof $P)"
a0=$(total); k0=$(ticks $KW); s0=$(ticks $SH); c0=$(ticks $CH); v0=$(ticks $SV); u0=$(ticks $UI)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); k1=$(ticks $KW); s1=$(ticks $SH); c1=$(ticks $CH); v1=$(ticks $SV); u1=$(ticks $UI)
# CLK_TCK is 100: ticks per second == percent of one core.
pct() { echo $(( ($2 - $1) / N )); }
printf '%-10s %3ss phone %4s%% kwin %3s%% plasmashell %3s%% chrome %3s%% server %3s%% app-ui %3s%% gpu %3s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$(pct $k0 $k1)" "$(pct $s0 $s1)" "$(pct $c0 $c1)" "$(pct $v0 $v1)" "$(pct $u0 $u1)" "$((g / N))"
@@ -0,0 +1,20 @@
# restart-daemon.sh - (Android root shell) (re)start the EXPERIMENT's anland display daemon on $SOCK.
# It is not started at boot (the anland-daemon module's service.sh only starts the original daemon on
# /data/local/tmp/display_daemon.sock, which must be left alone). Without it KWin logs
# "failed to connect to display daemon at /run/anland-mobilegl/display.sock" and the session dies in ~3 s.
# The container sees $SOCK as /run/anland-mobilegl/display.sock (bind mount in container.config);
# uid 1000 in the container must connect, hence 777 dir / 666 socket.
# Env: SOCK, DAEMON (/data/adb/modules/anland-daemon/display_daemon).
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
DAEMON=${DAEMON:-/data/adb/modules/anland-daemon/display_daemon}
DIR=$(dirname $SOCK)
for p in $(ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep | awk '{print $1}'); do kill $p; done
sleep 1
rm -f $SOCK
mkdir -p $DIR
nohup $DAEMON $SOCK > $DIR/display-daemon.log 2>&1 &
sleep 1
chmod 777 $DIR
chmod 666 $SOCK
echo "daemon restarted:"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep
@@ -0,0 +1,27 @@
# run-plasma.sh - (Android root shell) DEVELOPER restart of the whole MobileGL desktop, e.g. after a
# server, client or KWin rebuild: ends the Plasma session (and the user's logind session, which takes
# a while to wind down), then restarts the anland app; opening it brings up the server, starts
# desktop-session.service again and the session waits for the server. Normal use needs none of this:
# opening the app is enough. ~60 s. Prints the unit status, plasma user units and recent coredumps.
# Run: su -c "sh /data/local/tmp/anl/run-plasma.sh"
# Env: PKG, CONTAINER, DESKTOP_USER (swung0x48), DS (droidspaces binary).
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "
systemctl stop desktop-session.service 2>/dev/null
loginctl terminate-user $U 2>/dev/null; sleep 30
systemctl reset-failed desktop-session.service
rm -f /tmp/mobilegl-compositor.log
date +%s > /tmp/plasma-start
"
am force-stop $P
am start -n $P/com.anland.consumer.MainActivity > /dev/null
sleep 30
$D --name=$C run bash -lc "
systemctl --no-pager status desktop-session.service | head -8
UID_=\$(id -u $U)
sudo -u $U XDG_RUNTIME_DIR=/run/user/\$UID_ systemctl --user --no-pager list-units --all 'plasma*' 2>&1 | head -30
coredumpctl --no-pager list --since '-2min' 2>&1 | tail -5
"
@@ -0,0 +1,13 @@
# status.sh - (Android root shell) READ-ONLY health check of the whole stack.
# Run: su -c "sh /data/local/tmp/anl/status.sh" Env: PKG, CONTAINER, DS, SOCK.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
P=${PKG:-com.anland.consumer.mobilegl}
C=${CONTAINER:-arch-kde-mgl}
SOCK=${SOCK:-/data/local/tmp/anland-mobilegl/display.sock}
echo "battery: $(dumpsys battery | grep ' level:' | tr -d ' ') $(dumpsys power | grep -o 'mStayOn=[a-z]*')"
echo "backend: app setting=$(cat /data/data/$P/files/mobilegl-backend 2>/dev/null || echo 'DirectGLES (default)') published=$(cat ${SOCK%/*}/backend 2>/dev/null) container /etc/mobilegl/backend=$(cat /mnt/Droidspaces/$C/etc/mobilegl/backend 2>/dev/null) prop override=$(getprop debug.mobilegl.backend)"
echo "desktop service log: $(tail -1 ${SOCK%/*}/desktop.log 2>/dev/null)"
echo "app pid: $(pidof $P) server (:mobilegl) pid: $(pidof $P:mobilegl)"
ps -A -o PID,ARGS | grep "display_daemon $SOCK" | grep -v grep || echo "experiment display daemon NOT running"
$D show
$D --name=$C run bash -lc 'echo "desktop-session: $(systemctl is-active desktop-session.service)"; pgrep -a -x kwin_wayland; pgrep -a -x plasmashell; grep -A1 QtQuickRendererSettings /home/*/.config/kdeglobals 2>/dev/null || echo "kdeglobals: no sticky software renderer"'
@@ -0,0 +1,17 @@
# swap.sh - (Android root shell) replace the server libMobileGL.so inside the INSTALLED anland APK with
# $SHARE/libMobileGL.new.so, keeping owner, mode and SELinux context; force-stops the app. The first
# run backs the APK's original lib up to $SHARE/libMobileGL.so.orig. No re-signing/reinstall (that
# would change the app UID and lose its root grant). Run: su -c "sh /data/local/tmp/anl/swap.sh"
# Env: PKG (com.anland.consumer.mobilegl), SHARE (/data/local/tmp/anland-mobilegl).
set -e
P=${PKG:-com.anland.consumer.mobilegl}
SHARE=${SHARE:-/data/local/tmp/anland-mobilegl}
D=$(dirname $(pm path $P | sed 's/package://'))/lib/arm64
ls -laZ $D
[ -f $SHARE/libMobileGL.so.orig ] || cp $D/libMobileGL.so $SHARE/libMobileGL.so.orig
am force-stop $P
CTX=$(ls -Z $D/libMobileGL.so | awk '{print $1}')
OWN=$(stat -c %u:%g $D/libMobileGL.so)
cp $SHARE/libMobileGL.new.so $D/libMobileGL.so
chown $OWN $D/libMobileGL.so; chmod 755 $D/libMobileGL.so; chcon $CTX $D/libMobileGL.so
ls -laZ $D/libMobileGL.so
@@ -0,0 +1,17 @@
# switch.sh <DirectGLES|DirectVulkan> - (Android root shell) change the MobileGL backend the anland
# app's server runs (Espryt = DirectGLES, Magma = DirectVulkan). The choice is the app's setting
# (Settings > Connection > MobileGL desktop, saved in the app's files/mobilegl-backend); this does
# what a user does there: save it, Stop desktop, open the app again. The new server publishes the
# backend and the container's session copies it to /etc/mobilegl/backend before it starts, so
# server and clients agree. ~30-60 s. No setprop: debug.mobilegl.backend, if set, still overrides
# the setting until the next reboot - this clears it.
# Env: PKG, SOCK.
B=${1:?usage: switch.sh DirectGLES|DirectVulkan}
P=${PKG:-com.anland.consumer.mobilegl}
[ -n "$(getprop debug.mobilegl.backend)" ] && setprop debug.mobilegl.backend ""
if pidof $P:mobilegl > /dev/null; then
am startservice -n $P/com.anland.consumer.MobileGLWorker -a com.anland.consumer.mobilegl.STOP > /dev/null
i=0; while pidof $P:mobilegl > /dev/null && [ $i -lt 60 ]; do sleep 1; i=$((i + 1)); done
fi
am start -n $P/com.anland.consumer.MainActivity --es mobilegl_backend $B > /dev/null
echo "backend $B saved; the desktop is starting (watch: sh $(dirname $0)/status.sh)"
@@ -0,0 +1,8 @@
# unlock.sh - (Android root shell) ask logind to unlock the desktop user's session. UNVERIFIED that this
# helps here: the handoff notes this logind session type does not support lock-session (a real lock
# screen then needs the password). Prevention: [Daemon] Autolock=false in ~/.config/kscreenlockerrc.
# Env: DS, CONTAINER, DESKTOP_USER.
D=${DS:-/data/local/Droidspaces/bin/droidspaces}
C=${CONTAINER:-arch-kde-mgl}
U=${DESKTOP_USER:-swung0x48}
$D --name=$C run bash -lc "S=\$(loginctl list-sessions --no-legend | awk '\$3==\"$U\"{print \$1}' | head -1); loginctl unlock-session \$S; echo unlocked \$S"
@@ -0,0 +1,47 @@
# x11-measure.sh <label> <app-process-name> [seconds] - (Android root shell) CPU of the X11 path while
# an X11 app runs: % of ONE core (800 = all 8 cores) for the whole phone, the app, Xwayland,
# kwin_wayland, the app's :mobilegl server; mean kgsl gpu_busy_percentage; and the number of dma-buf
# descriptors Xwayland and KWin hold (window pixmaps / client buffers as dma-bufs). Refuses to run
# below 40% battery: the phone drains faster under load than USB charges it.
# Run: su -c "sh /data/local/tmp/anl/x11-measure.sh glamor glxgears 10" Env: PKG, MIN_BATTERY.
L=${1:-sample}
APP=${2:-glxgears}
N=${3:-10}
P=${PKG:-com.anland.consumer.mobilegl}
GPU=/sys/class/kgsl/kgsl-3d0/gpu_busy_percentage
level=$(dumpsys battery | sed -n 's/^ *level: *//p')
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "battery ${level}% < ${MIN_BATTERY:-40}%: not loading the phone; charge first" >&2
exit 3
fi
ticks() {
t=0
for p in "$@"; do
[ -r /proc/$p/stat ] || continue
v=$(sed 's/^.*) //' /proc/$p/stat | awk '{print $12 + $13}')
t=$((t + v))
done
echo $t
}
total() { awk '/^cpu /{print $2+$3+$4+$6+$7+$8}' /proc/stat; }
dmabufs() { # dma-buf descriptors held by the pids (Android kernels link them to /dmabuf:...)
n=0
for p in "$@"; do n=$((n + $(ls -l /proc/$p/fd 2>/dev/null | grep -c dmabuf))); done
echo $n
}
AP="$(pidof $APP)"
XW="$(pidof Xwayland)"
KW="$(pidof kwin_wayland)"
SV="$(pidof $P:mobilegl)"
[ -n "$AP" ] || { echo "no running process named $APP" >&2; exit 2; }
a0=$(total); p0=$(ticks $AP); x0=$(ticks $XW); k0=$(ticks $KW); v0=$(ticks $SV)
g=0; i=0
while [ $i -lt $N ]; do
sleep 1
b=$(cat $GPU 2>/dev/null | tr -dc '0-9'); g=$((g + ${b:-0})); i=$((i + 1))
done
a1=$(total); p1=$(ticks $AP); x1=$(ticks $XW); k1=$(ticks $KW); v1=$(ticks $SV)
pct() { echo $(( ($2 - $1) / N )); }
printf '%-12s %3ss phone %4s%% %s %3s%% Xwayland %3s%% kwin %3s%% server %3s%% gpu %3s%% dmabufs Xwayland %s kwin %s battery %s%%\n' \
"$L" "$N" "$(pct $a0 $a1)" "$APP" "$(pct $p0 $p1)" "$(pct $x0 $x1)" "$(pct $k0 $k1)" "$(pct $v0 $v1)" "$((g / N))" \
"$(dmabufs $XW)" "$(dmabufs $KW)" "$level"
@@ -0,0 +1,41 @@
# env.sh - shared parameters for the host-side (Git Bash) scripts of the anland-mobilegl-plasma skill.
# Sourced, never run. Every value can be overridden from the environment before calling a script.
#
# MGL_WORKTREE MobileGL worktree (Windows form C:/...). Default: the worktree this skill lives in.
# ANL_DIR Local scratch dir: channel files, screenshots, xdist/. Default: %LOCALAPPDATA%/Temp/anl
# ANDROID_NDK NDK used for the Android server lib. Default: <SDK>/ndk/27.2.12479018
# MGL_PKG Anland APK that embeds the server. Default: com.anland.consumer.mobilegl
# MGL_CONTAINER Droidspaces container. Default: arch-kde-mgl
# MGL_DESKTOP_USER Desktop user inside the container. Default: swung0x48
# DS_BIN droidspaces binary on the phone. Default: /data/local/Droidspaces/bin/droidspaces
# DEV_TOOLS Phone dir for device/*.sh and channel files. Default: /data/local/tmp/anl
# DEV_SHARE Phone dir bind-mounted into the container as /run/anland-mobilegl (screenshots,
# server lib staging, display daemon socket). Default: /data/local/tmp/anland-mobilegl
# ANDROID_SERIAL Honoured by adb itself; leave unset when only one device is attached
# (USB serial or a wireless "ip:port").
_skill_scripts=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
if [ -z "${MGL_WORKTREE:-}" ]; then
MGL_WORKTREE=$(cd "$_skill_scripts/../../../.." && pwd -W 2>/dev/null || pwd)
fi
: "${ANL_DIR:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null || echo /tmp)/Temp/anl}"
: "${ANDROID_NDK:=$(cygpath -m "${LOCALAPPDATA:-C:/Users/$USERNAME/AppData/Local}" 2>/dev/null)/Android/Sdk/ndk/27.2.12479018}"
: "${MGL_PKG:=com.anland.consumer.mobilegl}"
: "${MGL_CONTAINER:=arch-kde-mgl}"
: "${MGL_DESKTOP_USER:=swung0x48}"
: "${DS_BIN:=/data/local/Droidspaces/bin/droidspaces}"
: "${DEV_TOOLS:=/data/local/tmp/anl}"
: "${DEV_SHARE:=/data/local/tmp/anland-mobilegl}"
# The container rootfs as Android root sees it (the container's /tmp is NOT visible here).
CT_ROOT=/mnt/Droidspaces/$MGL_CONTAINER
export MGL_WORKTREE ANL_DIR ANDROID_NDK MGL_PKG MGL_CONTAINER MGL_DESKTOP_USER DS_BIN DEV_TOOLS DEV_SHARE CT_ROOT
# Git Bash would rewrite /data/... arguments into C:/Program Files/Git/data/...; keep them literal.
# Consequence: pass LOCAL paths to Windows tools (adb, cmake, git) in C:/ form, never /c/.
export MSYS_NO_PATHCONV=1
adb_() { MSYS2_ARG_CONV_EXCL='*' adb "$@"; }
# C:/x/y -> /mnt/c/x/y (for WSL).
win2wsl() { local p=$1 d; d=$(printf '%s' "${p:0:1}" | tr 'A-Z' 'a-z'); printf '/mnt/%s%s' "$d" "${p:2}"; }
mkdir -p "$ANL_DIR"
@@ -0,0 +1,146 @@
#!/usr/bin/env bash
# kwin-recover.sh [rounds] - does the desktop keep working when KWin's OWN MobileGL session is lost?
# Starts animated clients (glmark2-es2-wayland: Wayland, zero-copy shared images; glxgears: X11 through
# Xwayland's GLX; with CHROME=1 also Chrome on an animated WebGL page), then per round loses the server
# session of KWin with the pid-targeted debug knob (debug.mobilegl.inject_device_lost_pid, KWin's pid as
# it sees itself in the container), waits, and checks. A round PASSES when:
# - the server latched KWin's session (the server log file: "injected:pid=<kwin>"; logcat is too small);
# - KWin, plasmashell and every test client are the SAME processes (nothing restarted);
# - KWin restarted compositing on a fresh session (its journal: "compositing restarted on a fresh
# MobileGL session"; its client log: one more "RECOVERED");
# - the screen still animates (shot.sh hashes differ) and the clients keep presenting;
# - no native crash (logcat -b crash).
# Between rounds the knob is reset to 0 while the clients run, which re-arms it for the same pid.
# Stops early below MIN_BATTERY percent (the phone drains faster under load than USB charges it).
#
# Deploy first (same MobileGL commit on both sides; see SKILL.md):
# bash scripts/build-android.sh && bash scripts/cyc.sh pre # server -> APK, restarts the session
# bash scripts/xbuild.sh && bash scripts/xdeploy.sh # client -> container
# ANLAND_WORKTREE=<anland worktree> bash scripts/kwin-stage.sh build # kwin.patch + anland backend
# (wait for "== ALL DONE ==" in /root/kwin-build.log), then: adb shell 'su -c "sh /data/local/tmp/anl/run-plasma.sh"'
# Env: ROUNDS (default 2), SETTLE (s after the loss, default 10), REARM (s with the knob at 0 between
# rounds, default 4), MIN_BATTERY (default 40), CHROME (1: add Chrome), CHANNEL (ct.sh channel,
# default krec), TAG (screenshot/log tag, default krec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
ROUNDS=${1:-${ROUNDS:-2}}
SETTLE=${SETTLE:-10}
REARM=${REARM:-4}
MIN_BATTERY=${MIN_BATTERY:-40}
CH=${CHANNEL:-krec}
TAG=${TAG:-krec}
COMPOSITOR_LOG=/tmp/mobilegl-compositor.client.log
SERVER_LOG=/data/data/$MGL_PKG/files/mobilegl-server.server.log
ct() { bash "$here/ct.sh" "$CH"; }
battery() { adb_ shell dumpsys battery | tr -d '\r' | awk '/ level:/ {print $2}'; }
stop_clients() {
ct <<'IN' || true
pkill -x glmark2-es2-way 2>/dev/null; pkill -x glmark2-es2-wayland 2>/dev/null; pkill -x glxgears 2>/dev/null
true
IN
}
cleanup() {
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'" || true
stop_clients
}
check_battery() {
local level; level=$(battery)
echo "battery: $level%"
if [ -n "$level" ] && [ "$level" -lt "$MIN_BATTERY" ]; then
echo "STOP: battery below $MIN_BATTERY%"; exit 3
fi
}
trap cleanup EXIT
check_battery
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
# The clients, each with its own log (mgrun's MGLOG), in the background for the whole run.
ct <<'IN'
rm -f /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log /tmp/krec-gm.out /tmp/krec-gx.out
MGLOG=/tmp/mgl-krec-gm.log nohup timeout 900 mgrun offscreen glmark2-es2-wayland -s 960x540 --run-forever \
> /tmp/krec-gm.out 2>&1 < /dev/null &
MGLOG=/tmp/mgl-krec-gx.log nohup timeout 900 mgrun offscreen glxgears -info > /tmp/krec-gx.out 2>&1 < /dev/null &
sleep 8
echo "glmark2: $(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland) glxgears: $(pgrep -x glxgears)"
grep -aE "presented through|could not be presented" /tmp/mgl-krec-gm.client.log | head -2
grep -aE "presented through|readback" /tmp/mgl-krec-gx.client.log | head -2
IN
if [ "${CHROME:-0}" = 1 ]; then
ct <<'IN'
cat > /tmp/mgl-krec-spin.html <<'HTML'
<!doctype html><html><body style="margin:0;background:#000">
<canvas id=c width=640 height=360 style="width:100vw;height:100vh"></canvas>
<script>
const gl = document.getElementById('c').getContext('webgl'); let t = 0;
function frame() { t++; if (gl && !gl.isContextLost()) { gl.clearColor((t % 60) / 60, 0.2, 0.6, 1); gl.clear(gl.COLOR_BUFFER_BIT); }
requestAnimationFrame(frame); }
frame();
</script></body></html>
HTML
chmod 644 /tmp/mgl-krec-spin.html
IN
WAIT=${WAIT:-20} CHANNEL=$CH bash "$here/chrome-launch.sh" "$TAG" --new-window "file:///tmp/mgl-krec-spin.html"
fi
# KWin, plasmashell and the clients, as pids inside the container; how often KWin recovered so far.
snapshot() {
ct <<IN | tr -d '\r' | tail -1
k=\$(pgrep -x kwin_wayland | head -1); p=\$(pgrep -x plasmashell | head -1)
g=\$(pgrep -x glmark2-es2-way || pgrep -x glmark2-es2-wayland); x=\$(pgrep -x glxgears | head -1)
c=\$(pgrep -x chrome | head -1)
r=\$(grep -ac "RECOVERED" $COMPOSITOR_LOG 2>/dev/null || true)
f=\$(grep -ac "FPS" /tmp/krec-gm.out 2>/dev/null || true)
echo "\${k:-none} \${p:-none} \${g:-none} \${x:-none} \${c:-none} \${r:-0} \${f:-0}"
IN
}
pass=0
for round in $(seq 1 "$ROUNDS"); do
echo "=== round $round/$ROUNDS"
check_battery
read -r kwin plasma gm gx chrome recovered fps < <(snapshot)
echo "kwin=$kwin plasmashell=$plasma glmark2=$gm glxgears=$gx chrome=$chrome recoveries=$recovered fps-lines=$fps"
if [ "$kwin" = none ]; then echo "FAIL: KWin is not running"; break; fi
bash "$here/shot.sh" "$TAG-r$round-pre" 2 1
srvlines=$(adb_ shell "su -c 'wc -l < $SERVER_LOG'" | tr -dc '0-9')
adb_ logcat -b crash -c || true
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid $kwin'"
sleep "$SETTLE"
adb_ shell "su -c 'setprop debug.mobilegl.inject_device_lost_pid 0'"
bash "$here/shot.sh" "$TAG-r$round-post" 3 1 | tee "$ANL_DIR/$TAG-r$round-post.md5"
read -r kwin2 plasma2 gm2 gx2 chrome2 recovered2 fps2 < <(snapshot)
echo "after: kwin=$kwin2 plasmashell=$plasma2 glmark2=$gm2 glxgears=$gx2 chrome=$chrome2 recoveries=$recovered2 fps-lines=$fps2"
adb_ shell "su -c 'tail -n +$((srvlines + 1)) $SERVER_LOG'" > "$ANL_DIR/$TAG-r$round-server.log"
latched=$(grep -c "injected:pid=$kwin" "$ANL_DIR/$TAG-r$round-server.log" || true)
grep -aE "inject_device_lost_pid|SessionLatch|session #[0-9]+ (started|pid)|owner=server|lease" \
"$ANL_DIR/$TAG-r$round-server.log" | cut -c1-220 | head -14 || true
ct <<IN || true
echo "--- KWin's MobileGL client log:"; grep -aE "DEVICE LOST|RECOVERED|GL_UNKNOWN_CONTEXT_RESET|re-created on the fresh|Refuse" $COMPOSITOR_LOG | tail -6
echo "--- KWin journal:"; journalctl --no-pager --since -40s 2>/dev/null | grep -aE "kwin_wayland" | grep -aiE "reset|restarted|MobileGL swap|context|fatal|abort" | tail -10
echo "--- clients:"; grep -aE "DEVICE LOST|lost|Fatal" /tmp/mgl-krec-gm.client.log /tmp/mgl-krec-gx.client.log | tail -4
IN
crashes=$(adb_ logcat -b crash -d | grep -c "Fatal signal\|Abort message" || true)
hashes=$(awk '{print $1}' "$ANL_DIR/$TAG-r$round-post.md5" | sort -u | wc -l)
verdict=PASS
[ "$latched" -ge 1 ] || { echo " - the server never latched KWin's session (pid $kwin)"; verdict=FAIL; }
[ "$kwin2" = "$kwin" ] || { echo " - KWin is a different process ($kwin -> $kwin2): it restarted"; verdict=FAIL; }
[ "$plasma2" = "$plasma" ] || { echo " - plasmashell restarted ($plasma -> $plasma2)"; verdict=FAIL; }
[ "$gm2" = "$gm" ] && [ "$gx2" = "$gx" ] || { echo " - a test client restarted or died"; verdict=FAIL; }
[ "${CHROME:-0}" != 1 ] || [ "$chrome2" = "$chrome" ] || { echo " - Chrome restarted"; verdict=FAIL; }
[ "$recovered2" -gt "$recovered" ] || { echo " - KWin's client log shows no new RECOVERED session"; verdict=FAIL; }
[ "$fps2" -gt "$fps" ] || { echo " - glmark2 stopped reporting frames"; verdict=FAIL; }
[ "$hashes" -ge 2 ] || { echo " - the screen does not change after the loss (frozen)"; verdict=FAIL; }
[ "$crashes" -eq 0 ] || { echo " - native crash(es):"; adb_ logcat -b crash -d | tail -8; verdict=FAIL; }
echo "round $round: $verdict"
[ "$verdict" = PASS ] && pass=$((pass + 1))
[ "$verdict" = PASS ] || break
# Re-arm the knob for the same pid: a session must observe 0 before the next value counts.
sleep "$REARM"
done
echo "=== $pass/$ROUNDS rounds passed; screenshots in $ANL_DIR/$TAG-r*-*.png"
[ "$pass" = "$ROUNDS" ]
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# kwin-stage.sh [build] - stage anland's KWin changes (kwin.patch, anland backend sources,
# mobilegl-startup.sh, sync-build-kwin.sh) for the in-container KWin rebuild; with "build", also run
# kwprep.sh (apply changed patch hunks / backend files to the live tree) and start sync-build-kwin.sh
# (ninja -j4 + atomic install into /opt/mobilegl/kwin, screencast plugin, startup script) in the
# background, log /root/kwin-build.log. KWin is NOT restarted: run device/run-plasma.sh afterwards.
# Poll: printf 'tail -5 /root/kwin-build.log\n' | bash ct.sh kw
#
# Two staging places, because the two container scripts read different ones:
# /root/kwsync.tgz <- kwprep.sh (installed by push-tools.sh)
# $DEV_SHARE/kwin-sync = /run/anland-mobilegl/kwin-sync <- sync-build-kwin.sh (as kwin-sync/sync-build.sh)
# All files are CR-stripped: the Windows checkout of anland has CRLF backend sources and a CRLF
# sync-build-kwin.sh (bash then fails on $'\r').
# Assembled from kwprep.sh + sync-build-kwin.sh; not exercised as one unit yet.
# Env: ANLAND_WORKTREE (anland checkout on branch legacy-mobilegl-unified, Windows form),
# CHANNEL (ct.sh channel, default kw).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/mgl-unified}"
K=$ANLAND_WORKTREE/producers/kde
st=$ANL_DIR/kwstage
rm -rf "$st"; mkdir -p "$st/kwsync/Arch_v5" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland" "$st/kwin-sync/anland" "$st/kwin-sync/misc"
lf() { tr -d '\r' < "$1" > "$2"; }
for f in "$K"/anland_backend_Arch_v5/src/backends/anland/*; do
b=$(basename "$f"); [ "$b" = libdisplay_producer ] && continue
lf "$f" "$st/kwsync/anland_backend_Arch_v5/src/backends/anland/$b"
lf "$f" "$st/kwin-sync/anland/$b"
done
for f in kwin.patch mobilegl-startup.sh desktop-session-mobilegl.conf; do
lf "$K/Arch_v5/$f" "$st/kwsync/Arch_v5/$f"
lf "$K/Arch_v5/$f" "$st/kwin-sync/misc/$f"
done
lf "$K/Arch_v5/sync-build-kwin.sh" "$st/kwin-sync/sync-build.sh"
(cd "$st/kwsync" && tar --force-local -czf ../kwsync.tgz Arch_v5 anland_backend_Arch_v5)
tar --force-local -C "$st" -czf "$st/kwin-sync.tgz" kwin-sync
adb_ push "$st/kwsync.tgz" "$DEV_TOOLS/kwsync.tgz" >/dev/null
adb_ push "$st/kwin-sync.tgz" "$DEV_TOOLS/kwin-sync.tgz" >/dev/null
adb_ shell "su -c 'cp $DEV_TOOLS/kwsync.tgz $CT_ROOT/root/kwsync.tgz && tar xzf $DEV_TOOLS/kwin-sync.tgz -C $DEV_SHARE && chmod -R a+rX $DEV_SHARE/kwin-sync && rm -f $DEV_TOOLS/kwsync.tgz $DEV_TOOLS/kwin-sync.tgz && echo staged'"
if [ "${1:-}" = build ]; then
bash "$here/ct.sh" "${CHANNEL:-kw}" <<'IN'
bash /root/kwprep.sh || exit 1
nohup bash /run/anland-mobilegl/kwin-sync/sync-build.sh > /root/kwin-build.log 2>&1 < /dev/null &
echo "kwin build started (pid $!), log /root/kwin-build.log; it ends with '== ALL DONE =='"
IN
fi
@@ -0,0 +1,94 @@
#!/usr/bin/env python3
# cdpmedia.py [port] [seconds] - ask a Chrome started with --remote-debugging-port=<port> (and a
# non-default --user-data-dir) which video decoder its first page's players use, and evaluate
# `window.__mglStats` / the element `#t` text of the page. Standard library only (no websocket
# module in the container): a minimal RFC 6455 client.
# python3 cdpmedia.py 9333 6
# Prints kVideoDecoderName / kIsPlatformVideoDecoder / kVideoTracks properties and media errors as
# they arrive, then the page's stats text.
import base64, json, os, socket, struct, sys, time, urllib.request
port = int(sys.argv[1]) if len(sys.argv) > 1 else 9333
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0
targets = json.load(urllib.request.urlopen(f"http://127.0.0.1:{port}/json"))
page = next(t for t in targets if t.get("type") == "page")
url = page["webSocketDebuggerUrl"]
path = url.split(f"127.0.0.1:{port}", 1)[1]
sock = socket.create_connection(("127.0.0.1", port))
key = base64.b64encode(os.urandom(16)).decode()
sock.sendall((f"GET {path} HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\nSec-WebSocket-Version: 13\r\n\r\n").encode())
response = b""
while b"\r\n\r\n" not in response:
response += sock.recv(4096)
buffer = response.split(b"\r\n\r\n", 1)[1]
def send(message):
data = json.dumps(message).encode()
header = bytes([0x81])
mask = os.urandom(4)
if len(data) < 126:
header += bytes([0x80 | len(data)])
elif len(data) < 65536:
header += bytes([0x80 | 126]) + struct.pack(">H", len(data))
else:
header += bytes([0x80 | 127]) + struct.pack(">Q", len(data))
sock.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_exact(n):
global buffer
while len(buffer) < n:
chunk = sock.recv(65536)
if not chunk:
raise EOFError
buffer += chunk
out, buffer = buffer[:n], buffer[n:]
return out
def recv():
first, second = recv_exact(2)
length = second & 0x7F
if length == 126:
length = struct.unpack(">H", recv_exact(2))[0]
elif length == 127:
length = struct.unpack(">Q", recv_exact(8))[0]
payload = recv_exact(length)
return json.loads(payload) if first & 0x0F == 1 else None
send({"id": 1, "method": "Media.enable"})
# Players created before the domain was enabled report nothing: reload so the page makes new ones.
if os.environ.get("CDP_NAVIGATE"):
send({"id": 3, "method": "Page.navigate", "params": {"url": os.environ["CDP_NAVIGATE"]}})
elif os.environ.get("CDP_RELOAD", "1") == "1":
send({"id": 3, "method": "Page.reload"})
deadline = time.time() + seconds
sock.settimeout(0.5)
seen = set()
while time.time() < deadline:
try:
message = recv()
except socket.timeout:
continue
if not message or "method" not in message:
continue
if message["method"] == "Media.playerPropertiesChanged":
for prop in message["params"]["properties"]:
if prop["name"] in ("kVideoDecoderName", "kIsPlatformVideoDecoder", "kVideoTracks", "kResolution"):
line = f"{prop['name']} = {prop['value']}"
if line not in seen:
seen.add(line)
print(line)
elif message["method"] in ("Media.playerErrorsRaised",):
print("media error:", json.dumps(message["params"])[:300])
send({"id": 2, "method": "Runtime.evaluate",
"params": {"expression": os.environ.get("CDP_EXPR", "(document.getElementById('t')||{}).textContent"), "returnByValue": True}})
sock.settimeout(3)
while True:
message = recv()
if message and message.get("id") == 2:
print("page:", message.get("result", {}).get("result", {}).get("value"))
break
@@ -0,0 +1,34 @@
// dmahash <pid> : for each dma-buf fd of <pid> at least 1 MB, print inode, size and a sampled checksum.
#include <dirent.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char** argv) {
char dir[64];
snprintf(dir, sizeof dir, "/proc/%s/fd", argv[1]);
DIR* d = opendir(dir);
if (!d) { perror("opendir"); return 1; }
struct dirent* e;
while ((e = readdir(d))) {
char p[128], l[128] = {0};
snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
if (readlink(p, l, sizeof l - 1) <= 0 || strncmp(l, "/dmabuf", 7) != 0) continue;
int fd = open(p, O_RDONLY);
if (fd < 0) continue;
struct stat st; fstat(fd, &st);
if (st.st_size < (1 << 20)) { close(fd); continue; }
const uint8_t* m = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (m == MAP_FAILED) { printf("fd %s ino %lu mmap failed\n", e->d_name, (unsigned long)st.st_ino); close(fd); continue; }
uint64_t h = 1469598103934665603ull; unsigned nz = 0;
for (off_t i = 0; i < st.st_size; i += 64) { h = (h ^ m[i]) * 1099511628211ull; nz += m[i] != 0; }
printf("fd %s ino %lu size %ld hash %016llx nz %u\n", e->d_name, (unsigned long)st.st_ino, (long)st.st_size,
(unsigned long long)h, nz);
munmap((void*)m, st.st_size); close(fd);
}
return 0;
}
@@ -0,0 +1,55 @@
// gpuprobe - a plain Android process (outside MobileGL) that measures whether the GPU keeps executing
// other contexts' work: a pbuffer GLES context clears + draws and glFinish()es every ~10 ms, printing any
// round trip over 50 ms with a CLOCK_REALTIME timestamp. Usage: gpuprobe <seconds> [priority: low|med|high]
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#ifndef EGL_CONTEXT_PRIORITY_LEVEL_IMG
#define EGL_CONTEXT_PRIORITY_LEVEL_IMG 0x3100
#define EGL_CONTEXT_PRIORITY_HIGH_IMG 0x3101
#define EGL_CONTEXT_PRIORITY_MEDIUM_IMG 0x3102
#define EGL_CONTEXT_PRIORITY_LOW_IMG 0x3103
#endif
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
int main(int argc, char** argv) {
int seconds = argc > 1 ? atoi(argv[1]) : 20;
EGLint prio = EGL_CONTEXT_PRIORITY_MEDIUM_IMG;
if (argc > 2 && !strcmp(argv[2], "high")) prio = EGL_CONTEXT_PRIORITY_HIGH_IMG;
if (argc > 2 && !strcmp(argv[2], "low")) prio = EGL_CONTEXT_PRIORITY_LOW_IMG;
setvbuf(stdout, NULL, _IOLBF, 0);
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(d, NULL, NULL);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n;
eglChooseConfig(d, ca, &c, 1, &n);
const EGLint pa[] = {EGL_WIDTH, 256, EGL_HEIGHT, 256, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa);
const EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_CONTEXT_PRIORITY_LEVEL_IMG, prio, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
if (x == EGL_NO_CONTEXT) { printf("no ctx 0x%x\n", eglGetError()); return 1; }
eglMakeCurrent(d, s, s, x);
EGLint got = 0; eglQueryContext(d, x, EGL_CONTEXT_PRIORITY_LEVEL_IMG, &got);
const double t0 = now_ms(); double maxRt = 0; long iters = 0, slow = 0;
printf("[probe] start %.0f prio=0x%x\n", t0, got);
while (now_ms() - t0 < seconds * 1000.0) {
const double a = now_ms();
glClearColor((iters & 1) ? 1.f : 0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glFinish();
const double rt = now_ms() - a;
if (rt > maxRt) maxRt = rt;
if (rt > 50) { ++slow; printf("[probe] glFinish %.0f ms ending at %.0f\n", rt, now_ms()); }
++iters;
usleep(10000);
}
printf("[probe] END iters=%ld max=%.0f ms slow=%ld\n", iters, maxRt, slow);
return 0;
}
@@ -0,0 +1,150 @@
// hangclient - a Wayland xdg-toplevel, EGL + desktop GL 3.3 core client that animates and, on request,
// submits fragment work that never ends (the driver has to reset it). Prints per-frame gaps so the same
// binary measures "other app" frame pacing (frame callbacks come from the compositor) when run with -H -1.
//
// hangclient -T tag [-H hang_at_frame] [-N hang_repeat] [-I iters] [-S seconds] [-W w] [-X h] [-F]
// -H n frame at which the hanging draw is submitted (-1: never; default -1)
// -N k how many consecutive hanging frames (default 1)
// -E e re-arm a hang every e frames after the first (0: once)
// -I i loop count of the hanging shader (default 2^31-1)
// -S s run time in seconds (default 30)
// -F glFinish after every frame (puts the wait inside the client's own session)
#define GL_GLEXT_PROTOTYPES 1
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GL/glcorearb.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#include "xdg-shell-client-protocol.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static struct wl_compositor* g_comp;
static struct xdg_wm_base* g_wm;
static int g_configured, g_closed;
static int g_w = 480, g_h = 360;
static void reg_global(void* d, struct wl_registry* r, uint32_t name, const char* iface, uint32_t ver) {
(void)d;
if (!strcmp(iface, "wl_compositor")) g_comp = wl_registry_bind(r, name, &wl_compositor_interface, 4);
else if (!strcmp(iface, "xdg_wm_base")) g_wm = wl_registry_bind(r, name, &xdg_wm_base_interface, 1);
}
static void reg_remove(void* d, struct wl_registry* r, uint32_t n) { (void)d; (void)r; (void)n; }
static const struct wl_registry_listener reg_l = {reg_global, reg_remove};
static void wm_ping(void* d, struct xdg_wm_base* wm, uint32_t s) { (void)d; xdg_wm_base_pong(wm, s); }
static const struct xdg_wm_base_listener wm_l = {wm_ping};
static void xs_conf(void* d, struct xdg_surface* s, uint32_t serial) { (void)d; xdg_surface_ack_configure(s, serial); g_configured = 1; }
static const struct xdg_surface_listener xs_l = {xs_conf};
static void tl_conf(void* d, struct xdg_toplevel* t, int32_t w, int32_t h, struct wl_array* st) { (void)d; (void)t; (void)st; (void)w; (void)h; }
static void tl_close(void* d, struct xdg_toplevel* t) { (void)d; (void)t; g_closed = 1; }
static const struct xdg_toplevel_listener tl_l = {tl_conf, tl_close};
static double now_ms(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; }
static GLuint sh(GLenum type, const char* src) {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, NULL);
glCompileShader(s);
GLint ok = 0; glGetShaderiv(s, GL_COMPILE_STATUS, &ok);
if (!ok) { char log[2048]; glGetShaderInfoLog(s, sizeof log, NULL, log); fprintf(stderr, "shader: %s\n", log); exit(2); }
return s;
}
int main(int argc, char** argv) {
const char* tag = "hc"; int hangAt = -1, hangN = 1, every = 0, seconds = 30, finish = 0; long iters = 2147483647L;
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "-T") && i + 1 < argc) tag = argv[++i];
else if (!strcmp(argv[i], "-H") && i + 1 < argc) hangAt = atoi(argv[++i]);
else if (!strcmp(argv[i], "-N") && i + 1 < argc) hangN = atoi(argv[++i]);
else if (!strcmp(argv[i], "-E") && i + 1 < argc) every = atoi(argv[++i]);
else if (!strcmp(argv[i], "-I") && i + 1 < argc) iters = atol(argv[++i]);
else if (!strcmp(argv[i], "-S") && i + 1 < argc) seconds = atoi(argv[++i]);
else if (!strcmp(argv[i], "-W") && i + 1 < argc) g_w = atoi(argv[++i]);
else if (!strcmp(argv[i], "-X") && i + 1 < argc) g_h = atoi(argv[++i]);
else if (!strcmp(argv[i], "-F")) finish = 1;
}
setvbuf(stdout, NULL, _IOLBF, 0);
struct wl_display* dpy = wl_display_connect(NULL);
if (!dpy) { fprintf(stderr, "no wayland\n"); return 1; }
struct wl_registry* reg = wl_display_get_registry(dpy);
wl_registry_add_listener(reg, &reg_l, NULL);
wl_display_roundtrip(dpy);
if (!g_comp || !g_wm) { fprintf(stderr, "no compositor/xdg_wm_base\n"); return 1; }
xdg_wm_base_add_listener(g_wm, &wm_l, NULL);
struct wl_surface* surf = wl_compositor_create_surface(g_comp);
struct xdg_surface* xs = xdg_wm_base_get_xdg_surface(g_wm, surf);
xdg_surface_add_listener(xs, &xs_l, NULL);
struct xdg_toplevel* tl = xdg_surface_get_toplevel(xs);
xdg_toplevel_add_listener(tl, &tl_l, NULL);
xdg_toplevel_set_title(tl, tag);
wl_surface_commit(surf);
while (!g_configured) wl_display_dispatch(dpy);
EGLDisplay ed = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, dpy, NULL);
if (!eglInitialize(ed, NULL, NULL)) { fprintf(stderr, "eglInitialize\n"); return 1; }
eglBindAPI(EGL_OPENGL_API);
const EGLint ca[] = {EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig cfg; EGLint n = 0;
if (!eglChooseConfig(ed, ca, &cfg, 1, &n) || n < 1) { fprintf(stderr, "no config\n"); return 1; }
const EGLint xa[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_CONTEXT_MINOR_VERSION, 3, EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, EGL_NONE};
EGLContext ctx = eglCreateContext(ed, cfg, EGL_NO_CONTEXT, xa);
if (ctx == EGL_NO_CONTEXT) { fprintf(stderr, "no context 0x%x\n", eglGetError()); return 1; }
struct wl_egl_window* ew = wl_egl_window_create(surf, g_w, g_h);
EGLSurface es = eglCreatePlatformWindowSurface(ed, cfg, ew, NULL);
eglMakeCurrent(ed, es, es, ctx);
eglSwapInterval(ed, 1);
const char* vs = "#version 330 core\nlayout(location=0) in vec2 p; uniform float t; out vec2 uv;\n"
"void main(){ uv=p*0.5+0.5; gl_Position=vec4(p*0.4+vec2(0.5*sin(t),0.0),0.0,1.0);}";
const char* fs = "#version 330 core\nin vec2 uv; uniform int iters; uniform float t; out vec4 c;\n"
"void main(){ float a=uv.x; for(int i=0;i<iters;++i){ a=fract(a*1.0001+0.37+uv.y); }\n"
" c=vec4(uv, 0.5+0.5*sin(t)+a*1e-6, 1.0);}";
GLuint prog = glCreateProgram();
glAttachShader(prog, sh(GL_VERTEX_SHADER, vs));
glAttachShader(prog, sh(GL_FRAGMENT_SHADER, fs));
glLinkProgram(prog);
GLint ok = 0; glGetProgramiv(prog, GL_LINK_STATUS, &ok);
if (!ok) { fprintf(stderr, "link failed\n"); return 2; }
GLint uIt = glGetUniformLocation(prog, "iters"), uT = glGetUniformLocation(prog, "t");
GLuint vao, vbo; glGenVertexArrays(1, &vao); glBindVertexArray(vao);
const float q[] = {-1, -1, 1, -1, -1, 1, 1, 1};
glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW);
glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
const double t0 = now_ms(); double last = t0, maxGap = 0; long frames = 0, gaps100 = 0;
printf("[%s] start %.0f renderer=%s hangAt=%d N=%d every=%d iters=%ld\n", tag, t0, glGetString(GL_RENDERER), hangAt, hangN, every, iters);
while (!g_closed && now_ms() - t0 < seconds * 1000.0) {
wl_display_dispatch_pending(dpy);
const double t = (now_ms() - t0) / 1000.0;
int hang = 0;
if (hangAt >= 0 && frames >= hangAt) {
long rel = frames - hangAt;
if (every > 0) rel %= every;
hang = rel < hangN;
}
glViewport(0, 0, g_w, g_h);
glClearColor(0.5f + 0.5f * (float)sin(t * 3.0), 0.2f, 0.4f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(prog);
glUniform1i(uIt, hang ? (int)iters : 0);
glUniform1f(uT, (float)t);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
if (hang) printf("[%s] HANG submitted frame %ld at %.0f\n", tag, frames, now_ms());
if (finish) glFinish();
if (!eglSwapBuffers(ed, es)) printf("[%s] swap failed 0x%x at %.0f\n", tag, eglGetError(), now_ms());
const GLenum rs = glGetError();
if (rs != GL_NO_ERROR) printf("[%s] glGetError 0x%x at %.0f\n", tag, rs, now_ms());
const double nowv = now_ms(), gap = nowv - last;
last = nowv; ++frames;
if (gap > maxGap) maxGap = gap;
if (gap > 100.0) { ++gaps100; printf("[%s] GAP %.0f ms ending at %.0f (frame %ld)\n", tag, gap, nowv, frames); }
if (frames % 120 == 0) printf("[%s] frame %ld t=%.1fs fps=%.1f\n", tag, frames, (nowv - t0) / 1e3, frames * 1e3 / (nowv - t0));
}
const double total = now_ms() - t0;
printf("[%s] END frames=%ld fps=%.1f maxGap=%.0f ms gaps>100ms=%ld\n", tag, frames, frames * 1e3 / total, maxGap, gaps100);
return 0;
}
@@ -0,0 +1,95 @@
/* texfmt: does a texture keep its alpha? One opaque red texel and one transparent texel per
* format, sampled across a 64x64 ES3 pbuffer cleared white with SRC_ALPHA blending. Expect the
* left half red and the right half WHITE; a black right half means the alpha was lost. */
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define P(T, n) T n = (T)eglGetProcAddress(#n)
static const char* vs = "#version 300 es\nlayout(location=0) in vec2 p; out vec2 uv; void main(){uv=p*0.5+0.5; gl_Position=vec4(p,0,1);}";
static const char* fs = "#version 300 es\nprecision mediump float; uniform sampler2D t; uniform int mode; in vec2 uv; out vec4 o;"
"void main(){vec4 c=texture(t,uv); o = mode==1 ? vec4(1.0,0.0,0.0,c.a) : (mode==2 ? vec4(c.r,0.0,0.0,c.r) : c);}";
int main(void) {
EGLDisplay d = eglGetDisplay(EGL_DEFAULT_DISPLAY); eglInitialize(d, 0, 0); eglBindAPI(EGL_OPENGL_ES_API);
EGLint ca[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE};
EGLConfig c; EGLint n; eglChooseConfig(d, ca, &c, 1, &n);
EGLint xa[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
EGLContext x = eglCreateContext(d, c, EGL_NO_CONTEXT, xa);
EGLint pa[] = {EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE};
EGLSurface s = eglCreatePbufferSurface(d, c, pa); eglMakeCurrent(d, s, s, x);
P(PFNGLCREATESHADERPROC, glCreateShader); P(PFNGLSHADERSOURCEPROC, glShaderSource); P(PFNGLCOMPILESHADERPROC, glCompileShader);
P(PFNGLCREATEPROGRAMPROC, glCreateProgram); P(PFNGLATTACHSHADERPROC, glAttachShader); P(PFNGLLINKPROGRAMPROC, glLinkProgram);
P(PFNGLUSEPROGRAMPROC, glUseProgram); P(PFNGLGETUNIFORMLOCATIONPROC, glGetUniformLocation); P(PFNGLUNIFORM1IPROC, glUniform1i);
P(PFNGLGENBUFFERSPROC, glGenBuffers); P(PFNGLBINDBUFFERPROC, glBindBuffer); P(PFNGLBUFFERDATAPROC, glBufferData);
P(PFNGLVERTEXATTRIBPOINTERPROC, glVertexAttribPointer); P(PFNGLENABLEVERTEXATTRIBARRAYPROC, glEnableVertexAttribArray);
P(PFNGLDRAWARRAYSPROC, glDrawArrays); P(PFNGLCLEARCOLORPROC, glClearColor); P(PFNGLCLEARPROC, glClear);
P(PFNGLREADPIXELSPROC, glReadPixels); P(PFNGLVIEWPORTPROC, glViewport); P(PFNGLGENTEXTURESPROC, glGenTextures);
P(PFNGLBINDTEXTUREPROC, glBindTexture); P(PFNGLTEXIMAGE2DPROC, glTexImage2D); P(PFNGLTEXPARAMETERIPROC, glTexParameteri);
P(PFNGLENABLEPROC, glEnable); P(PFNGLBLENDFUNCPROC, glBlendFunc); P(PFNGLGETERRORPROC, glGetError);
P(PFNGLTEXSTORAGE2DPROC, glTexStorage2D); P(PFNGLTEXSUBIMAGE2DPROC, glTexSubImage2D); P(PFNGLGETSTRINGPROC, glGetString);
printf("GL_VERSION %s\n", glGetString(GL_VERSION));
printf("BGRA ext: %s\n", strstr((const char*)glGetString(GL_EXTENSIONS), "GL_EXT_texture_format_BGRA8888") ? "yes" : "no");
GLuint v = glCreateShader(GL_VERTEX_SHADER), f = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(v, 1, &vs, 0); glCompileShader(v); glShaderSource(f, 1, &fs, 0); glCompileShader(f);
GLuint pr = glCreateProgram(); glAttachShader(pr, v); glAttachShader(pr, f); glLinkProgram(pr); glUseProgram(pr);
float q[] = {-1,-1, 1,-1, -1,1, 1,1};
GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof q, q, GL_STATIC_DRAW); glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0); glEnableVertexAttribArray(0);
GLint lm = glGetUniformLocation(pr, "mode");
glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
struct { const char* name; GLint ifmt; GLenum fmt; int bpp; int mode; int storage; GLenum sized; unsigned char px[8]; } cases[] = {
{"RGBA/TexImage", GL_RGBA, GL_RGBA, 4, 0, 0, 0, {255,0,0,255, 0,0,0,0}},
{"BGRA_EXT/TexImage", GL_BGRA_EXT, GL_BGRA_EXT, 4, 0, 0, 0, {0,0,255,255, 0,0,0,0}},
{"BGRA8_EXT/TexStorage+Sub", 0, GL_BGRA_EXT, 4, 0, 1, GL_BGRA8_EXT, {0,0,255,255, 0,0,0,0}},
{"RGBA8/TexStorage+Sub", 0, GL_RGBA, 4, 0, 1, GL_RGBA8, {255,0,0,255, 0,0,0,0}},
{"ALPHA", GL_ALPHA, GL_ALPHA, 1, 1, 0, 0, {255, 0}},
{"LUMINANCE_ALPHA", GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA, 2, 1, 0, 0, {255,255, 0,0}},
{"R8/TexStorage+Sub", 0, GL_RED, 1, 2, 1, GL_R8, {255, 0}},
};
for (unsigned i = 0; i < sizeof cases / sizeof cases[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
while (glGetError()) {}
if (cases[i].storage) {
glTexStorage2D(GL_TEXTURE_2D, 1, cases[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
} else {
glTexImage2D(GL_TEXTURE_2D, 0, cases[i].ifmt, 2, 1, 0, cases[i].fmt, GL_UNSIGNED_BYTE, cases[i].px);
}
GLenum err = glGetError();
glUniform1i(lm, cases[i].mode);
glViewport(0, 0, 64, 64); glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
const int ok = l[0] > 200 && l[1] < 60 && l[2] < 60 && r[0] > 200 && r[1] > 200 && r[2] > 200;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", cases[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
/* ANGLE's emulation of ALPHA8 / LUMINANCE_ALPHA8 on a desktop-GL driver: R8 / RG8 + swizzle. */
P(PFNGLTEXPARAMETERIPROC, glTexParameteri2);
struct { const char* name; GLenum sized, fmt; int bpp; GLint sw[4]; unsigned char px[4]; unsigned char wantL[3], wantR[3]; } sz[] = {
{"R8 swizzled as ALPHA8", GL_R8, GL_RED, 1, {GL_ZERO, GL_ZERO, GL_ZERO, GL_RED}, {255, 0}, {0,0,0}, {255,255,255}},
{"RG8 swizzled as LUM_ALPHA", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {255,255, 0,0}, {255,255,255}, {255,255,255}},
{"RG8 as LUM_ALPHA (lum 0)", GL_RG8, GL_RG, 2, {GL_RED, GL_RED, GL_RED, GL_GREEN}, {0,255, 0,0}, {0,0,0}, {255,255,255}},
};
glUniform1i(lm, 0);
for (unsigned i = 0; i < sizeof sz / sizeof sz[0]; ++i) {
GLuint t; glGenTextures(1, &t); glBindTexture(GL_TEXTURE_2D, t);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexStorage2D(GL_TEXTURE_2D, 1, sz[i].sized, 2, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 2, 1, sz[i].fmt, GL_UNSIGNED_BYTE, sz[i].px);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_R, sz[i].sw[0]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_G, sz[i].sw[1]);
glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_B, sz[i].sw[2]); glTexParameteri2(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, sz[i].sw[3]);
GLenum err = glGetError();
glClearColor(1, 1, 1, 1); glClear(GL_COLOR_BUFFER_BIT); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
unsigned char l[4], r[4];
glReadPixels(16, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, l); glReadPixels(48, 32, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, r);
int ok = 1;
for (int k = 0; k < 3; ++k) ok = ok && abs(l[k] - sz[i].wantL[k]) < 40 && abs(r[k] - sz[i].wantR[k]) < 40;
printf("%-26s err=0x%04x left=%3d,%3d,%3d right=%3d,%3d,%3d %s\n", sz[i].name, err, l[0], l[1], l[2], r[0], r[1], r[2], ok ? "ok" : "BAD");
}
return 0;
}
@@ -0,0 +1,410 @@
// yuvprobe.c - pixel check of YUV dma-buf sampling through MobileGL (GL_OES_EGL_image_external).
//
// gcc -O1 yuvprobe.c -o /usr/local/bin/mgl-yuvprobe -lEGL -lGLESv2 -lgbm
// mgrun offscreen mgl-yuvprobe # exit 0 = every check passed
//
// What it does, in an ES 3.0 context on a pbuffer:
// 1. FOREIGN NV12 (and P010): a dma-buf from /dev/dma_heap/system - a buffer the MobileGL server
// did not allocate, as a video decoder's would be - with padded pitches and a gap before the
// chroma plane, filled with four vertical bars (red, green, blue, white) in BT.709 narrow
// range. Imported with eglCreateImage(EGL_LINUX_DMA_BUF_EXT) and its colour hints, bound to
// GL_TEXTURE_EXTERNAL_OES, drawn through a samplerExternalOES shader into an RGBA8 FBO and read
// back: each bar must come out its colour. The buffer is then rewritten with the bars in
// reverse order and, after a native fence (what ends a frame for an offscreen producer), drawn
// again: the new order must show (the server refreshes a foreign buffer at each frame).
// 2. GBM NV12: gbm_bo_create(NV12) through the mobilegl GBM backend (a server-allocated YUV
// buffer, zero-copy), its planes printed, imported and sampled once (content undefined; the
// check is that every call succeeds).
// Also prints the dma-buf formats/modifiers EGL lists for NV12/P010.
#define _GNU_SOURCE
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <GLES2/gl2ext.h>
#include <fcntl.h>
#include <gbm.h>
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#ifndef DRM_FORMAT_NV12
#define DRM_FORMAT_NV12 0x3231564E
#endif
#ifndef DRM_FORMAT_P010
#define DRM_FORMAT_P010 0x30313050
#endif
static int g_failures = 0;
#define CHECK(cond, ...) \
do { \
if (!(cond)) { \
printf("FAIL: " __VA_ARGS__); \
printf("\n"); \
++g_failures; \
} \
} while (0)
static PFNEGLCREATEIMAGEKHRPROC pCreateImage;
static PFNEGLDESTROYIMAGEKHRPROC pDestroyImage;
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC pTargetTexture;
static PFNEGLQUERYDMABUFFORMATSEXTPROC pQueryFormats;
static PFNEGLQUERYDMABUFMODIFIERSEXTPROC pQueryModifiers;
static PFNEGLCREATESYNCKHRPROC pCreateSync;
static PFNEGLDESTROYSYNCKHRPROC pDestroySync;
static EGLDisplay g_display;
enum { W = 256, H = 128, BARS = 4 };
static const unsigned char kBars[BARS][3] = {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {255, 255, 255}};
// RGB -> BT.709 narrow-range codes at `bits` (8 or 10).
static void Encode709(const unsigned char rgb[3], int bits, int out[3]) {
const double kr = 0.2126, kb = 0.0722, kg = 1.0 - kr - kb;
const double r = rgb[0] / 255.0, g = rgb[1] / 255.0, b = rgb[2] / 255.0;
const double y = kr * r + kg * g + kb * b;
const double cb = (b - y) / (2.0 * (1.0 - kb)), cr = (r - y) / (2.0 * (1.0 - kr));
const double scale = (double)(1 << (bits - 8));
out[0] = (int)lround((16.0 + 219.0 * y) * scale);
out[1] = (int)lround((128.0 + 224.0 * cb) * scale);
out[2] = (int)lround((128.0 + 224.0 * cr) * scale);
}
// Writes the bars (`reverse`d or not) into a semi-planar buffer: 1 byte samples, or P010's 16-bit
// samples with the 10 significant bits high.
static void FillBars(uint8_t* base, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int bytes, int reverse) {
const int bits = bytes == 1 ? 8 : 10;
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
int bar = x * BARS / W;
if (reverse) bar = BARS - 1 - bar;
int code[3];
Encode709(kBars[bar], bits, code);
uint8_t* py = base + (size_t)y * pitchY + (size_t)x * bytes;
if (bytes == 1) py[0] = (uint8_t)code[0];
else *(uint16_t*)py = (uint16_t)(code[0] << 6);
if ((x & 1) == 0 && (y & 1) == 0) {
uint8_t* pc = base + offsetC + (size_t)(y / 2) * pitchC + (size_t)(x / 2) * 2 * bytes;
if (bytes == 1) {
pc[0] = (uint8_t)code[1];
pc[1] = (uint8_t)code[2];
} else {
((uint16_t*)pc)[0] = (uint16_t)(code[1] << 6);
((uint16_t*)pc)[1] = (uint16_t)(code[2] << 6);
}
}
}
}
}
static GLuint Compile(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint ok = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &ok);
if (!ok) {
char log[2048];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
printf("shader: %s\n", log);
}
return shader;
}
static GLuint g_program, g_fbo, g_rgba;
static void SetUpDraw(void) {
const char* vs = "#version 300 es\n"
"out vec2 uv;\n"
"void main() {\n"
" vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n"
" gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n"
" uv = p;\n"
"}\n";
const char* fs = "#version 300 es\n"
"#extension GL_OES_EGL_image_external_essl3 : require\n"
"precision mediump float;\n"
"uniform samplerExternalOES tex;\n"
"in vec2 uv;\n"
"out vec4 color;\n"
"void main() { color = texture(tex, uv); }\n";
g_program = glCreateProgram();
glAttachShader(g_program, Compile(GL_VERTEX_SHADER, vs));
glAttachShader(g_program, Compile(GL_FRAGMENT_SHADER, fs));
glLinkProgram(g_program);
GLint linked = 0;
glGetProgramiv(g_program, GL_LINK_STATUS, &linked);
CHECK(linked, "the samplerExternalOES program did not link");
GLint type = 0, size = 0;
char name[64];
glGetActiveUniform(g_program, 0, sizeof(name), NULL, &size, (GLenum*)&type, name);
printf("uniform 0: %s type 0x%04x\n", name, type);
CHECK(type == GL_SAMPLER_EXTERNAL_OES, "the sampler reports type 0x%04x, not GL_SAMPLER_EXTERNAL_OES", type);
glGenTextures(1, &g_rgba);
glBindTexture(GL_TEXTURE_2D, g_rgba);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, W, H);
glGenFramebuffers(1, &g_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_rgba, 0);
CHECK(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE, "the readback FBO is incomplete");
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
}
// Draws the external texture `texture` and checks each bar's center (row `y` of the image).
static void DrawAndCheck(GLuint texture, const char* what, int reverse, int tolerance) {
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
glViewport(0, 0, W, H);
glUseProgram(g_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
glUniform1i(glGetUniformLocation(g_program, "tex"), 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
static unsigned char pixels[W * H * 4];
glReadPixels(0, 0, W, H, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "%s: GL error 0x%04x", what, error);
if (tolerance < 0) {
printf("%s: center pixel %d %d %d %d (content undefined)\n", what, pixels[(H / 2 * W + W / 2) * 4],
pixels[(H / 2 * W + W / 2) * 4 + 1], pixels[(H / 2 * W + W / 2) * 4 + 2],
pixels[(H / 2 * W + W / 2) * 4 + 3]);
return;
}
// Rows: GL row 0 is the image's first row (the uv mapping above), and the bars are vertical,
// so any row does; take two to catch a broken chroma row pitch.
const int rows[2] = {H / 4, (3 * H) / 4};
for (int r = 0; r < 2; ++r) {
for (int bar = 0; bar < BARS; ++bar) {
const int x = bar * (W / BARS) + W / (2 * BARS);
const unsigned char* p = &pixels[(rows[r] * W + x) * 4];
const unsigned char* want = kBars[reverse ? BARS - 1 - bar : bar];
int worst = 0;
for (int c = 0; c < 3; ++c) {
int d = abs((int)p[c] - (int)want[c]);
if (d > worst) worst = d;
}
printf("%s: row %d bar %d -> %3d %3d %3d %3d (want %3d %3d %3d)%s\n", what, rows[r], bar, p[0], p[1], p[2],
p[3], want[0], want[1], want[2], worst > tolerance ? " <-- off" : "");
CHECK(worst <= tolerance, "%s: bar %d is off by %d", what, bar, worst);
}
}
}
static int AllocateDmaHeap(size_t size) {
int heap = open("/dev/dma_heap/system", O_RDWR | O_CLOEXEC);
if (heap < 0) {
perror("open /dev/dma_heap/system");
return -1;
}
struct dma_heap_allocation_data data = {.len = size, .fd_flags = O_RDWR | O_CLOEXEC};
if (ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &data) != 0) {
perror("DMA_HEAP_IOCTL_ALLOC");
close(heap);
return -1;
}
close(heap);
return (int)data.fd;
}
static void Sync(int fd, int start) {
struct dma_buf_sync sync = {.flags = DMA_BUF_SYNC_WRITE | (start ? DMA_BUF_SYNC_START : DMA_BUF_SYNC_END)};
ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync);
}
static EGLImageKHR Import(int fd, uint32_t fourcc, uint32_t pitchY, uint32_t offsetC, uint32_t pitchC, int modifier) {
EGLint attribs[64];
int n = 0;
#define A(k, v) (attribs[n++] = (k), attribs[n++] = (EGLint)(v))
A(EGL_WIDTH, W);
A(EGL_HEIGHT, H);
A(EGL_LINUX_DRM_FOURCC_EXT, fourcc);
A(EGL_DMA_BUF_PLANE0_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE0_OFFSET_EXT, 0);
A(EGL_DMA_BUF_PLANE0_PITCH_EXT, pitchY);
A(EGL_DMA_BUF_PLANE1_FD_EXT, fd);
A(EGL_DMA_BUF_PLANE1_OFFSET_EXT, offsetC);
A(EGL_DMA_BUF_PLANE1_PITCH_EXT, pitchC);
if (modifier) {
A(EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_LO_EXT, 0);
A(EGL_DMA_BUF_PLANE1_MODIFIER_HI_EXT, 0);
}
A(EGL_YUV_COLOR_SPACE_HINT_EXT, EGL_ITU_REC709_EXT);
A(EGL_SAMPLE_RANGE_HINT_EXT, EGL_YUV_NARROW_RANGE_EXT);
attribs[n] = EGL_NONE;
#undef A
EGLImageKHR image = pCreateImage(g_display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, NULL, attribs);
if (image == EGL_NO_IMAGE_KHR) printf("eglCreateImage failed: 0x%04x\n", eglGetError());
return image;
}
static GLuint ExternalTexture(EGLImageKHR image) {
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
pTargetTexture(GL_TEXTURE_EXTERNAL_OES, image);
GLenum error = glGetError();
CHECK(error == GL_NO_ERROR, "glEGLImageTargetTexture2DOES(GL_TEXTURE_EXTERNAL_OES) -> 0x%04x", error);
GLint bound = 0;
glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &bound);
CHECK((GLuint)bound == texture, "GL_TEXTURE_BINDING_EXTERNAL_OES reads %d", bound);
return texture;
}
// A frame boundary as an offscreen producer makes one: a native fence.
static void EndFrame(void) {
EGLint attribs[] = {EGL_NONE};
EGLSyncKHR sync = pCreateSync(g_display, EGL_SYNC_NATIVE_FENCE_ANDROID, attribs);
CHECK(sync != EGL_NO_SYNC_KHR, "eglCreateSync(native fence) failed");
if (sync != EGL_NO_SYNC_KHR) pDestroySync(g_display, sync);
}
static void ForeignTest(uint32_t fourcc, int bytes, int tolerance) {
const char* what = fourcc == DRM_FORMAT_NV12 ? "foreign NV12" : "foreign P010";
// Padded pitches and a gap before the chroma plane, as decoders lay buffers out.
const uint32_t pitchY = W * bytes + 64, pitchC = W * bytes + 128;
const uint32_t offsetC = pitchY * (H + 16);
const size_t size = offsetC + (size_t)pitchC * (H / 2) + 4096;
int fd = AllocateDmaHeap(size);
if (fd < 0) {
CHECK(0, "%s: no dma-buf to test with", what);
return;
}
uint8_t* map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
CHECK(map != MAP_FAILED, "%s: mmap failed", what);
if (map == MAP_FAILED) return;
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 0);
Sync(fd, 0);
EGLImageKHR image = Import(fd, fourcc, pitchY, offsetC, pitchC, fourcc == DRM_FORMAT_NV12);
CHECK(image != EGL_NO_IMAGE_KHR, "%s: import refused", what);
if (image == EGL_NO_IMAGE_KHR) return;
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, what, 0, tolerance);
EndFrame();
// New content, same buffer: shows after the frame boundary.
Sync(fd, 1);
FillBars(map, pitchY, offsetC, pitchC, bytes, 1);
Sync(fd, 0);
EndFrame();
DrawAndCheck(texture, what, 1, tolerance);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
munmap(map, size);
close(fd);
}
static void GbmTest(void) {
int node = open("/dev/dri/renderD128", O_RDWR | O_CLOEXEC);
if (node < 0) node = open("/dev/null", O_RDWR | O_CLOEXEC);
struct gbm_device* gbm = gbm_create_device(node);
if (gbm == NULL) {
CHECK(0, "gbm_create_device failed");
return;
}
printf("gbm backend: %s\n", gbm_device_get_backend_name(gbm));
const int supported = gbm_device_is_format_supported(gbm, DRM_FORMAT_NV12, 0);
printf("gbm NV12 supported: %d, P010: %d\n", supported,
gbm_device_is_format_supported(gbm, DRM_FORMAT_P010, 0));
CHECK(supported == 1, "gbm does not support NV12");
struct gbm_bo* bo = gbm_bo_create(gbm, W, H, DRM_FORMAT_NV12, 0);
CHECK(bo != NULL, "gbm_bo_create(NV12) failed");
if (bo != NULL) {
const int planes = gbm_bo_get_plane_count(bo);
printf("gbm NV12 bo: planes %d, stride %u/%u, offset %u/%u, modifier 0x%llx\n", planes,
gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_stride_for_plane(bo, 1), gbm_bo_get_offset(bo, 0),
gbm_bo_get_offset(bo, 1), (unsigned long long)gbm_bo_get_modifier(bo));
CHECK(planes == 2, "the NV12 bo has %d planes", planes);
int fd = gbm_bo_get_fd(bo);
EGLImageKHR image =
Import(fd, DRM_FORMAT_NV12, gbm_bo_get_stride_for_plane(bo, 0), gbm_bo_get_offset(bo, 1),
gbm_bo_get_stride_for_plane(bo, 1), 0);
CHECK(image != EGL_NO_IMAGE_KHR, "the server's own NV12 buffer was not imported");
if (image != EGL_NO_IMAGE_KHR) {
GLuint texture = ExternalTexture(image);
DrawAndCheck(texture, "gbm NV12", 0, -1);
glDeleteTextures(1, &texture);
pDestroyImage(g_display, image);
}
close(fd);
gbm_bo_destroy(bo);
}
gbm_device_destroy(gbm);
close(node);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
EGLint major, minor;
if (!eglInitialize(g_display, &major, &minor)) {
printf("FAIL: eglInitialize\n");
return 2;
}
const char* eglExtensions = eglQueryString(g_display, EGL_EXTENSIONS);
printf("EGL %d.%d dma_buf_import: %s\n", major, minor,
strstr(eglExtensions, "EGL_EXT_image_dma_buf_import") ? "yes" : "no");
eglBindAPI(EGL_OPENGL_ES_API);
const EGLint configAttribs[] = {EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_NONE};
EGLConfig config;
EGLint count = 0;
eglChooseConfig(g_display, configAttribs, &config, 1, &count);
if (count < 1) {
printf("FAIL: no config\n");
return 2;
}
const EGLint pbufferAttribs[] = {EGL_WIDTH, 16, EGL_HEIGHT, 16, EGL_NONE};
EGLSurface surface = eglCreatePbufferSurface(g_display, config, pbufferAttribs);
const EGLint contextAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 3, EGL_NONE};
EGLContext context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, contextAttribs);
if (context == EGL_NO_CONTEXT || !eglMakeCurrent(g_display, surface, surface, context)) {
printf("FAIL: no ES 3 context\n");
return 2;
}
pCreateImage = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
pDestroyImage = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
pTargetTexture = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
pQueryFormats = (PFNEGLQUERYDMABUFFORMATSEXTPROC)eglGetProcAddress("eglQueryDmaBufFormatsEXT");
pQueryModifiers = (PFNEGLQUERYDMABUFMODIFIERSEXTPROC)eglGetProcAddress("eglQueryDmaBufModifiersEXT");
pCreateSync = (PFNEGLCREATESYNCKHRPROC)eglGetProcAddress("eglCreateSyncKHR");
pDestroySync = (PFNEGLDESTROYSYNCKHRPROC)eglGetProcAddress("eglDestroySyncKHR");
printf("GL_RENDERER: %s\n", glGetString(GL_RENDERER));
const char* glExtensions = (const char*)glGetString(GL_EXTENSIONS);
const int external = strstr(glExtensions, "GL_OES_EGL_image_external_essl3") != NULL;
printf("GL_OES_EGL_image_external_essl3: %s\n", external ? "yes" : "no");
CHECK(external, "GL_OES_EGL_image_external_essl3 is not advertised");
EGLint formats[32], formatCount = 0;
pQueryFormats(g_display, 32, formats, &formatCount);
int nv12 = 0;
for (int i = 0; i < formatCount; ++i) {
printf("dma-buf format %.4s", (const char*)&formats[i]);
EGLuint64KHR modifiers[4];
EGLBoolean externalOnly[4];
EGLint modifierCount = 0;
pQueryModifiers(g_display, formats[i], 4, modifiers, externalOnly, &modifierCount);
for (int m = 0; m < modifierCount; ++m)
printf(" [modifier 0x%llx%s]", (unsigned long long)modifiers[m], externalOnly[m] ? " external-only" : "");
printf("\n");
nv12 |= formats[i] == (EGLint)DRM_FORMAT_NV12;
}
CHECK(nv12, "NV12 is not a listed dma-buf format");
SetUpDraw();
ForeignTest(DRM_FORMAT_NV12, 1, 12);
ForeignTest(DRM_FORMAT_P010, 2, 16);
GbmTest();
printf("%s (%d failure(s))\n", g_failures ? "FAILED" : "PASSED", g_failures);
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglTerminate(g_display);
return g_failures ? 1 : 0;
}
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# push-tools.sh - install this skill's device- and container-side helpers on the phone.
# device/*.sh -> $DEV_TOOLS (run as root: adb shell 'su -c "sh $DEV_TOOLS/<x>.sh"')
# container/mgrun -> <container>/usr/local/bin/mgrun
# container/kwprep.sh -> <container>/root/kwprep.sh
# Needs the container running (its rootfs is mounted at $CT_ROOT only then).
# Overwrites same-named files other sessions may be using in $DEV_TOOLS; check first on a shared phone.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
adb_ shell "mkdir -p $DEV_TOOLS"
for f in "$here"/device/*.sh; do
tr -d '\r' < "$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$(basename "$f")" >/dev/null
echo "pushed $DEV_TOOLS/$(basename "$f")"
done
for f in mgrun kwprep.sh; do
tr -d '\r' < "$here/container/$f" > "$ANL_DIR/push.tmp"
adb_ push "$ANL_DIR/push.tmp" "$DEV_TOOLS/$f" >/dev/null
done
rm -f "$ANL_DIR/push.tmp"
adb_ shell "su -c 'cp $DEV_TOOLS/mgrun $CT_ROOT/usr/local/bin/mgrun && chmod 755 $CT_ROOT/usr/local/bin/mgrun && cp $DEV_TOOLS/kwprep.sh $CT_ROOT/root/kwprep.sh && echo installed mgrun kwprep.sh'"
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# rec.sh <name> - record 3 s of the screen while opening Kickoff, extract frames inside the container
# with ffmpeg, pull them to $ANL_DIR/af_<name>/ and build a 3x4 contact sheet $ANL_DIR/grid_<name>.png
# (needs Python + Pillow on Windows). Use it to catch flicker/black frames a single screenshot misses.
# Env: TAP="x y" Kickoff button position (default "69 1349" = bottom-left on the 2560x1600 tablet;
# device specific). Note: anland grabs the touchscreen (EVIOCGRAB); if `input tap` does nothing, use
# `input mouse tap x y`. Env CHANNEL (default rec).
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
N=${1:?usage: rec.sh <name>}; TAP=${TAP:-69 1349}
adb_ shell "su -c 'input keyevent 111; sleep 1.5; (screenrecord --time-limit 3 --bit-rate 20000000 $DEV_SHARE/$N.mp4 &); sleep 0.7; input tap $TAP; sleep 3; cp $DEV_SHARE/$N.mp4 $CT_ROOT/root/$N.mp4'"
bash "$here/ct.sh" "${CHANNEL:-rec}" <<IN
rm -rf /root/af_$N; mkdir -p /root/af_$N; ffmpeg -loglevel quiet -i /root/$N.mp4 -vf scale=1000:-1 -fps_mode passthrough /root/af_$N/f%03d.png; ls /root/af_$N | wc -l
IN
adb_ shell "su -c 'rm -rf $DEV_SHARE/af_$N; cp -r $CT_ROOT/root/af_$N $DEV_SHARE/ && chmod -R 777 $DEV_SHARE/af_$N'"
rm -rf "$ANL_DIR/af_$N"; adb_ pull "$DEV_SHARE/af_$N" "$ANL_DIR/" >/dev/null
python - "$ANL_DIR/af_$N" "$ANL_DIR/grid_$N.png" <<'P'
import sys, os
from PIL import Image
d, out = sys.argv[1], sys.argv[2]
fs = sorted(os.listdir(d)); n = len(fs)
pick = [fs[min(n-1, int(i * n / 12))] for i in range(12)]
ims = [Image.open(os.path.join(d, f)) for f in pick]
w, h = ims[0].size; tw, th = w // 2, h // 2
g = Image.new('RGB', (tw * 3, th * 4))
for i, im in enumerate(ims): g.paste(im.resize((tw, th)), ((i % 3) * tw, (i // 3) * th))
g.save(out); print(n, pick)
P
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# shot.sh <name> [count] [interval-s] - take <count> (default 2) phone screenshots <interval> s apart
# (default 1), pull them to $ANL_DIR/<name>-<i>.png and print their md5. Identical hashes while an
# animated client (glmark2, a video) runs mean the frames are NOT reaching the screen.
set -euo pipefail
. "$(dirname "$0")/env.sh"
name=${1:?usage: shot.sh <name> [count] [interval]}; n=${2:-2}; dt=${3:-1}
cmd=""; i=1
while [ "$i" -le "$n" ]; do cmd="$cmd screencap -p $DEV_SHARE/$name-$i.png; sleep $dt;"; i=$((i+1)); done
adb_ shell "su -c '$cmd'"
i=1
while [ "$i" -le "$n" ]; do
adb_ pull "$DEV_SHARE/$name-$i.png" "$ANL_DIR/$name-$i.png" >/dev/null
md5sum "$ANL_DIR/$name-$i.png"; i=$((i+1))
done
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# sync-src.sh - FALLBACK path only (the WSL cross build, xbuild.sh, replaced it). Pushes the files the
# worktree changed since the container's source stamp into the container's MobileGL source tree
# (/root/mobilegl-unified-src), keeping mtimes so the in-container ninja build stays incremental;
# then build/install inside the container with anland's anland-build-client.sh
# (producers/kde/Arch_v5/mobilegl-tools/, gcc, -j4, slow on the phone).
# A full re-sync of the tree is anland's anland-sync-source.sh (MOBILEGL_REPO=<worktree>).
set -euo pipefail
. "$(dirname "$0")/env.sh"
SRC=$CT_ROOT/root/mobilegl-unified-src
STAMP=$(adb_ shell "su -c 'cat $SRC/.anland-build-stamp'" | tr -d '\r\n')
cd "$MGL_WORKTREE"
FILES=$( (git diff --name-only --diff-filter=d "$STAMP"; git ls-files --others --exclude-standard) \
| grep -v -E '^(3rdparty/|build-android|\.anland-build-stamp|\.claude/)' | sort -u)
echo "$(echo "$FILES" | wc -l) files since $STAMP"
tar --force-local -czf "$ANL_DIR/src-delta.tgz" $FILES
adb_ push "$ANL_DIR/src-delta.tgz" /data/local/tmp/src-delta.tgz >/dev/null
HEAD=$(git rev-parse HEAD)
adb_ shell "su -c 'tar xzf /data/local/tmp/src-delta.tgz -C $SRC && echo $HEAD > $SRC/.anland-build-stamp && echo synced'"
@@ -0,0 +1,9 @@
#!/bin/bash
# wsl-all.sh - run INSIDE WSL archlinux after wsl-test.sh configured $B: build every *Test target except
# DirectVulkan*/Benchmark* and run the unit label. NEVER run the DirectVulkan integration tests
# (they need a live device + manual intervention); never run two ctest suites at once.
B=${B:-$HOME/mgl-anl-bld}
cd $B
T=$(ninja -t targets all 2>/dev/null | grep -oE "^[A-Za-z]+Test:" | tr -d : | sort -u | grep -v -E "^(DirectVulkan|Benchmark)" | tr "\n" " ")
ninja -j14 MobileGLServer $T 2>&1 | grep -E "error:|FAILED" | head -10
ctest -L unit -j10 --timeout 300 -E "DirectVulkan" 2>&1 | tail -25
@@ -0,0 +1,13 @@
#!/bin/bash
# wsl-test.sh <targets...> - run INSIDE WSL archlinux: configure (once) and build MobileGL unit-test
# targets for the host (clang, disaggregated + in-process server ON). From Git Bash:
# MSYS2_ARG_CONV_EXCL='*' wsl -d archlinux -- bash /mnt/c/<worktree>/.claude/skills/anland-mobilegl-plasma/scripts/wsl-test.sh MultiSessionTest
# then ctest in $B (see wsl-all.sh). Env: W (worktree, WSL path; default: derived from this file),
# B (build dir, default ~/mgl-anl-bld). The Windows MSVC build of feat/disaggregated is broken; use this.
set -e
W=${W:-$(cd "$(dirname "$0")/../../../.." && pwd)}
B=${B:-$HOME/mgl-anl-bld}
if [ ! -f $B/build.ninja ]; then
cmake -S $W -B $B -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DMOBILEGL_BUILD_TEST=ON -DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_BUILD_DISAGGREGATED_INPROC=ON -DMOBILEGL_BUILD_STAMP=wsl
fi
cmake --build $B --target "$@" -j16
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
# x11-verify.sh <step> [...] - device verification of X11 on the GPU (docs/Disaggregated/notes/anland/
# plan-x11-gpu.md): Xwayland drawing with glamor on MobileGL, and GLX windows presented through
# DRI3 + Present from shared images. Steps, in the order a full run takes them:
#
# preflight read-only: battery, stack status, Xwayland package, render node, xcb libs,
# test apps, Xwayland's extensions and its GLX vendor
# measure <label> [s] glxgears 1280x720 (GEARS_ARGS; vblank_mode=0) for s seconds (10): its fps, then CPU/GPU/dma-buf
# counts (device/x11-measure.sh); also with MOBILEGL_GLX_PRESENT=readback
# deploy-client xbuild.sh + xdeploy.sh of this worktree's client (MGL_XBUILD_DIR)
# deploy-kwin kwin-stage.sh build from ANLAND_WORKTREE; waits for "== ALL DONE =="
# deploy-server build-android.sh + cyc.sh x11-server (swaps the APK lib, RESTARTS the session)
# check the evidence: Xwayland took glamor (journal), its MobileGL log, GLX's path
# line, dma-buf counts in Xwayland/KWin, the server's import lines
# apps X11 clients on screen: konsole and kate on Qt's xcb platform (glamor-drawn
# core X), glxgears (GLX), xterm/xeyes when installed; two screenshots
# stop kill the test apps
#
# Typical run: preflight; measure before; deploy-client; deploy-kwin; deploy-server; check; measure
# after; apps; stop. Battery: every load step refuses below MIN_BATTERY (40) %.
# Env: CHANNEL (ct.sh channel, default x11), ANLAND_WORKTREE (anland x11-gpu worktree, Windows
# form), MGL_XBUILD_DIR (default /home/swung/mgl-xbuild-x11), MIN_BATTERY.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
CH=${CHANNEL:-x11}
: "${MGL_XBUILD_DIR:=/home/swung/mgl-xbuild-x11}"
: "${ANLAND_WORKTREE:=$(cygpath -m "$USERPROFILE" 2>/dev/null)/AndroidStudioProjects/anland/.claude/worktrees/x11-gpu}"
export MGL_XBUILD_DIR ANLAND_WORKTREE
battery_ok() {
local level
level=$(adb_ shell "dumpsys battery" | sed -n 's/^ *level: *//p' | tr -d '\r')
echo "battery: ${level}%"
if [ "${level:-0}" -lt "${MIN_BATTERY:-40}" ]; then
echo "below ${MIN_BATTERY:-40}%: stopping GPU-heavy work; charge the phone first" >&2
return 1
fi
}
push_measure() {
tr -d '\r' < "$here/device/x11-measure.sh" > "$ANL_DIR/x11-measure.sh"
adb_ push "$ANL_DIR/x11-measure.sh" "$DEV_TOOLS/x11-measure.sh" >/dev/null
}
step=${1:?usage: x11-verify.sh <preflight|measure|deploy-client|deploy-kwin|deploy-server|check|apps|stop>}
shift
case "$step" in
preflight)
battery_ok || true
adb_ shell "su -c 'sh $DEV_TOOLS/status.sh'" || true
bash "$here/ct.sh" "$CH" <<'IN'
echo "== packages"; pacman -Q xorg-xwayland libxcb mesa-utils xorg-xdpyinfo qt6-base 2>&1 | sed 's/^/ /'
echo "== render nodes"; ls -l /dev/dri/ 2>&1 | sed 's/^/ /'
U=swung0x48; for n in /dev/dri/renderD*; do sudo -u $U test -r "$n" -a -w "$n" && echo " $U can open $n"; done
echo "== KWin's thin node"; tr '\0' '\n' < /proc/$(pgrep -x kwin_wayland)/environ 2>/dev/null | grep -E "MOBILEGL_GBM_NODE|ANLAND_XWAYLAND_GLAMOR|GBM_BACKEND" | sed 's/^/ /'
echo "== xcb libraries"; for l in libxcb-dri3.so.0 libxcb-present.so.0 libxcb-xfixes.so.0 libxcb-shm.so.0; do ls /usr/lib/$l >/dev/null 2>&1 && echo " $l ok" || echo " $l MISSING"; done
echo "== Xwayland"; pgrep -a -x Xwayland | sed 's/^/ /'
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM|XFIXES|GLX)$" | sed 's/^/ ext/'
mgrun offscreen glxinfo -B 2>&1 | grep -E "vendor|renderer|direct rendering" | sed 's/^/ /'
IN
;;
measure)
label=${1:?usage: x11-verify.sh measure <label> [seconds]}
secs=${2:-10}
battery_ok
push_measure
for mode in auto readback; do
printf '%s\n' \
"pkill -x glxgears; rm -f /tmp/mgl-x11-gears.client.log /tmp/x11-gears.out" \
"MGLOG=/tmp/mgl-x11-gears.log timeout $((secs + 8)) mgrun offscreen env vblank_mode=${VBLANK_MODE:-0} MOBILEGL_GLX_PRESENT=$mode glxgears ${GEARS_ARGS:--geometry 1280x720} > /tmp/x11-gears.out 2>&1 &" \
"sleep 3" | bash "$here/ct.sh" "$CH" >/dev/null
adb_ shell "su -c 'sh $DEV_TOOLS/x11-measure.sh $label-$mode glxgears $secs'" || true
sleep 6
printf '%s\n' "grep -a 'frames in' /tmp/x11-gears.out | tail -3; grep -a 'presented through' /tmp/mgl-x11-gears.client.log | head -1; pkill -x glxgears; true" \
| bash "$here/ct.sh" "$CH"
done
;;
deploy-client)
bash "$here/xbuild.sh"
CHANNEL=$CH bash "$here/xdeploy.sh"
;;
deploy-kwin)
CHANNEL=$CH bash "$here/kwin-stage.sh" build
for _ in $(seq 1 120); do
out=$(printf 'tail -3 /root/kwin-build.log\n' | bash "$here/ct.sh" "$CH" 2>/dev/null || true)
echo "$out" | tail -1
echo "$out" | grep -q "== ALL DONE ==" && exit 0
echo "$out" | grep -qiE "error|failed" && { echo "KWin build failed" >&2; exit 1; }
sleep 30
done
echo "KWin build did not finish in an hour" >&2; exit 1
;;
deploy-server)
battery_ok
bash "$here/build-android.sh"
CHANNEL=$CH bash "$here/cyc.sh" x11-server
;;
check)
bash "$here/ct.sh" "$CH" <<'IN'
echo "== Xwayland's command line (no -shm with glamor)"; pgrep -a -x Xwayland | sed 's/^/ /'
echo "== Xwayland's glamor (journal)"
journalctl --no-pager --since -30min 2>/dev/null | grep -aiE "glamor|Xwayland.*(EGL|gbm|dri3|falling back)" | tail -10 | sed 's/^/ /'
echo "== Xwayland's MobileGL client log"; grep -aE "presented|shared image|declin|Fatal|ERROR" /tmp/mgl-xwayland.client.log /tmp/mgl-xwayland.client 2>/dev/null | tail -8 | sed 's/^/ /' || echo " (none)"
X=$(pgrep -x Xwayland); K=$(pgrep -x kwin_wayland)
echo "== dma-bufs held: Xwayland $(ls -l /proc/$X/fd 2>/dev/null | grep -c dmabuf), kwin $(ls -l /proc/$K/fd 2>/dev/null | grep -c dmabuf)"
echo "== Xwayland's GLX vendor and extensions"
mgrun offscreen xdpyinfo 2>/dev/null | grep -E "^ +(DRI3|Present|MIT-SHM)$" | sed 's/^/ ext/'
echo "== a GLX window's path"
rm -f /tmp/mgl-x11-check.client.log
MGLOG=/tmp/mgl-x11-check.log timeout 6 mgrun offscreen glxgears >/dev/null 2>&1
grep -a "presented through\|leaves DRI3" /tmp/mgl-x11-check.client.log | sed 's/^/ /'
IN
adb_ logcat -d | grep -aE "MobileGL.*(shared image|dma-buf import|refused)" | tail -8 || true
adb_ logcat -b crash -d | tail -5 || true
;;
apps)
battery_ok
printf '%s\n' \
"pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate" \
"MGLOG=/tmp/mgl-x11-konsole.log mgrun offscreen env QT_QPA_PLATFORM=xcb konsole >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-kate.log mgrun offscreen env QT_QPA_PLATFORM=xcb kate >/dev/null 2>&1 &" \
"command -v xterm >/dev/null && MGLOG=/tmp/mgl-x11-xterm.log mgrun offscreen xterm >/dev/null 2>&1 &" \
"command -v xeyes >/dev/null && MGLOG=/tmp/mgl-x11-xeyes.log mgrun offscreen xeyes >/dev/null 2>&1 &" \
"MGLOG=/tmp/mgl-x11-gears.log mgrun offscreen glxgears >/dev/null 2>&1 &" \
"sleep 6; pgrep -a -x konsole; pgrep -a -x kate; pgrep -a -x xterm; pgrep -a -x xeyes; pgrep -a -x glxgears; true" | bash "$here/ct.sh" "$CH"
bash "$here/shot.sh" x11-apps 2 1
;;
stop)
printf '%s\n' "pkill -x xterm; pkill -x xeyes; pkill -x glxgears; pkill -x konsole; pkill -x kate; true" | bash "$here/ct.sh" "$CH"
;;
*)
echo "unknown step $step" >&2; exit 2
;;
esac
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# xbuild.sh [--reconfigure] [extra ninja targets...] - cross-build the container CLIENT: libMobileGL.so
# (glibc aarch64, glvnd EGL/GLX vendor) + mobilegl_gbm.so (GBM backend), in WSL archlinux with
# clang + lld against a sysroot exported from the container (xsysroot/refresh-sysroot.sh).
# Full build ~150 s, incremental seconds. Deploy with xdeploy.sh.
# Env: MGL_WORKTREE (source, Windows form), MGL_XBUILD_DIR (WSL build dir, default ~/mgl-xbuild-a64),
# MGL_JOBS (16), WSL_DISTRO (archlinux).
# Prereq in WSL: clang, lld, llvm (lld major MUST equal clang/llvm-libs major - lld 23 on llvm 22
# broke ld.lld), cmake, ninja; ~/sysroots/aarch64-arch.cmake + ~/sysroots/arch-kde-mgl.
set -euo pipefail
. "$(dirname "$0")/env.sh"
STAMP=$(git -C "$MGL_WORKTREE" rev-parse HEAD)
RECONF=0
if [ "${1:-}" = "--reconfigure" ]; then RECONF=1; shift; fi
EXTRA="$*"
SRC_WSL=$(win2wsl "$MGL_WORKTREE")
wsl -d "${WSL_DISTRO:-archlinux}" -- env SRC="$SRC_WSL" STAMP="$STAMP" RECONF="$RECONF" EXTRA="$EXTRA" \
OUT="${MGL_XBUILD_DIR:-}" JOBS="${MGL_JOBS:-16}" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -euo pipefail
OUT=${OUT:-$HOME/mgl-xbuild-a64}
TC=$HOME/sysroots/aarch64-arch.cmake
[ -f "$TC" ] || { echo "missing toolchain $TC (run xsysroot/refresh-sysroot.sh)"; exit 1; }
ld.lld --version >/dev/null || { echo "ld.lld is not runnable in WSL (lld/llvm version mismatch?)"; exit 1; }
t0=$(date +%s)
if [ "$RECONF" = 1 ] || [ ! -f "$OUT/build.ninja" ]; then
rm -rf "$OUT/CMakeCache.txt" "$OUT/CMakeFiles"
cmake -S "$SRC" -B "$OUT" -G Ninja --toolchain "$TC" -DMOBILEGL_BUILD_STAMP="$STAMP" \
-DCMAKE_BUILD_TYPE=Release -DMOBILEGL_BUILD_TEST=OFF -DMOBILEGL_BUILD_BENCHMARK=OFF \
-DMOBILEGL_BUILD_DISAGGREGATED=ON -DMOBILEGL_ENABLE_LTO=OFF
elif ! grep -qx "MOBILEGL_BUILD_STAMP:STRING=$STAMP" "$OUT/CMakeCache.txt"; then
cmake -S "$SRC" -B "$OUT" -DMOBILEGL_BUILD_STAMP="$STAMP" >/dev/null
fi
t1=$(date +%s)
cmake --build "$OUT" --target MobileGL mobilegl_gbm $EXTRA -j"$JOBS"
t2=$(date +%s)
echo "stamp: $STAMP"
echo "configure: $((t1-t0))s build: $((t2-t1))s"
for f in "$OUT/libMobileGL.so" "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so"; do
ls -la "$f"; echo " windows: $(wslpath -w "$f")"
done
IN
exit "${PIPESTATUS[0]}"
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# xdeploy.sh - install the WSL cross-built libMobileGL.so + mobilegl_gbm.so into the container, exactly
# like anland's anland-build-client.sh: copy to a NEW inode and rename into place (overwriting a mapped
# .so in place crashes every running client, KWin included), refresh the libEGL/libGL aliases, the
# glvnd GLX vendor link, the EGL vendor JSON (system-wide 10_mobilegl.json + the /opt copy), the client
# config /etc/mobilegl/client.conf and the GLX/GBM selection drop-ins. Does not restart anything: already-running clients
# keep the old inode; restart the client (or the session) to pick up the new library.
# Env: MGL_XBUILD_DIR (WSL build dir), CHANNEL (ct.sh channel, default xd), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/env.sh"
mkdir -p "$ANL_DIR/xdist"
# 1. Copy the build outputs out of WSL.
wsl -d "${WSL_DISTRO:-archlinux}" -- env OUT="${MGL_XBUILD_DIR:-}" D="$(win2wsl "$ANL_DIR")/xdist" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; OUT=${OUT:-$HOME/mgl-xbuild-a64}
cp -f "$OUT/libMobileGL.so" "$D/libMobileGL.so"; cp -f "$OUT/MobileGL/MG_Gbm/mobilegl_gbm.so" "$D/mobilegl_gbm.so"
sha256sum "$D/libMobileGL.so" "$D/mobilegl_gbm.so"
IN
# 2. Push to the phone and stage them in the container's /root/xdeploy (the container's /tmp and the
# phone's /data/local/tmp are different places; the rootfs is reachable at $CT_ROOT).
adb_ shell "mkdir -p $DEV_TOOLS/xdist"
adb_ push "$ANL_DIR/xdist/libMobileGL.so" "$DEV_TOOLS/xdist/libMobileGL.so"
adb_ push "$ANL_DIR/xdist/mobilegl_gbm.so" "$DEV_TOOLS/xdist/mobilegl_gbm.so"
adb_ shell "su -c 'mkdir -p $CT_ROOT/root/xdeploy && cp $DEV_TOOLS/xdist/libMobileGL.so $DEV_TOOLS/xdist/mobilegl_gbm.so $CT_ROOT/root/xdeploy/ && rm -f $DEV_TOOLS/xdist/*.so'"
# 3. Install inside the container.
bash "$here/ct.sh" "${CHANNEL:-xd}" <<'IN'
set -euo pipefail
in=/root/xdeploy
mkdir -p /opt/mobilegl/lib /opt/mobilegl/share/glvnd/egl_vendor.d
cp "$in/libMobileGL.so" /opt/mobilegl/lib/libMobileGL.so.new
mv -f /opt/mobilegl/lib/libMobileGL.so.new /opt/mobilegl/lib/libMobileGL.so
for lib in libEGL.so libEGL.so.1 libGL.so libGL.so.1; do ln -sf libMobileGL.so "/opt/mobilegl/lib/$lib"; done
ln -sf /opt/mobilegl/lib/libMobileGL.so /usr/lib/libGLX_mobilegl.so.0
vendor='{"file_format_version":"1.0.0","ICD":{"library_path":"/opt/mobilegl/lib/libMobileGL.so"}}'
# Per-process forcing (__EGL_VENDOR_LIBRARY_FILENAMES=<this file>) keeps working off the /opt copy.
printf '%s\n' "$vendor" >/opt/mobilegl/share/glvnd/egl_vendor.d/50_mobilegl.json
# System-wide vendor: 10_ sorts ahead of the system's 50_mesa.json, so stock libglvnd asks MobileGL
# first and moves on to the other vendor whenever MobileGL declines (no server reachable).
mkdir -p /usr/share/glvnd/egl_vendor.d
printf '%s\n' "$vendor" >/usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new
mv -f /usr/share/glvnd/egl_vendor.d/10_mobilegl.json.new /usr/share/glvnd/egl_vendor.d/10_mobilegl.json
# Client settings every process reads when its environment does not say otherwise (the backend
# comes from /etc/mobilegl/backend, which switch.sh rewrites).
mkdir -p /etc/mobilegl
cat >/etc/mobilegl/client.conf <<'CONF'
# MobileGL client configuration, read by every process that loads libMobileGL.so.
# KEY=value lines; a MOBILEGL_* variable in a process's environment overrides the line here.
# The backend is /etc/mobilegl/backend. Written by xdeploy.sh / anland-build-client.sh.
MOBILEGL_TRANSPORT=spawn
MOBILEGL_IPC_DATA=shm
MOBILEGL_IPC_CONTROL=unix:@anland-mobilegl
CONF
# What glvnd's libGLX and libgbm cannot infer: environment.d reaches the systemd user manager (every
# Plasma service and what it launches, terminals included), profile.d reaches login shells.
mkdir -p /etc/environment.d /etc/profile.d
printf '%s\n' '__GLX_VENDOR_LIBRARY_NAME=mobilegl' 'GBM_BACKEND=mobilegl' >/etc/environment.d/10-mobilegl.conf
printf '%s\n' 'export __GLX_VENDOR_LIBRARY_NAME=mobilegl' 'export GBM_BACKEND=mobilegl' >/etc/profile.d/mobilegl.sh
mkdir -p /usr/lib/gbm
cp "$in/mobilegl_gbm.so" /usr/lib/gbm/mobilegl_gbm.so.new
mv -f /usr/lib/gbm/mobilegl_gbm.so.new /usr/lib/gbm/mobilegl_gbm.so
rm -f "$in/libMobileGL.so" "$in/mobilegl_gbm.so"
sha256sum /opt/mobilegl/lib/libMobileGL.so /usr/lib/gbm/mobilegl_gbm.so
ls /usr/share/glvnd/egl_vendor.d/ /etc/mobilegl/ /etc/environment.d/
echo DEPLOYED
IN
@@ -0,0 +1,47 @@
# Cross toolchain: WSL clang -> aarch64 Arch Linux ARM (glibc) container "arch-kde-mgl".
# Sysroot exported from the container by refresh-sysroot.sh (default $HOME/sysroots/arch-kde-mgl).
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(_triple aarch64-unknown-linux-gnu)
if (DEFINED ENV{MGL_SYSROOT})
set(_sysroot "$ENV{MGL_SYSROOT}")
else()
set(_sysroot "$ENV{HOME}/sysroots/arch-kde-mgl")
endif()
# The container's GCC install dir (crtbegin/crtend, libgcc, libstdc++ via /usr/include/c++/<ver>).
file(GLOB _gcc_dirs LIST_DIRECTORIES true "${_sysroot}/usr/lib/gcc/${_triple}/*")
list(SORT _gcc_dirs COMPARE NATURAL ORDER DESCENDING)
list(GET _gcc_dirs 0 _gcc_install_dir)
set(CMAKE_SYSROOT "${_sysroot}")
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
set(CMAKE_ASM_COMPILER clang)
set(CMAKE_C_COMPILER_TARGET ${_triple})
set(CMAKE_CXX_COMPILER_TARGET ${_triple})
set(CMAKE_ASM_COMPILER_TARGET ${_triple})
set(CMAKE_AR llvm-ar)
set(CMAKE_RANLIB llvm-ranlib)
set(CMAKE_NM llvm-nm)
set(CMAKE_OBJCOPY llvm-objcopy)
set(CMAKE_STRIP llvm-strip)
set(_xflags "--gcc-install-dir=${_gcc_install_dir}")
set(CMAKE_C_FLAGS_INIT "${_xflags}")
set(CMAKE_CXX_FLAGS_INIT "${_xflags}")
set(CMAKE_ASM_FLAGS_INIT "${_xflags}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_MODULE_LINKER_FLAGS_INIT "-fuse-ld=lld")
set(CMAKE_LINKER_TYPE LLD)
set(CMAKE_FIND_ROOT_PATH "${_sysroot}")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
set(ENV{PKG_CONFIG_SYSROOT_DIR} "${_sysroot}")
set(ENV{PKG_CONFIG_LIBDIR} "${_sysroot}/usr/lib/pkgconfig:${_sysroot}/usr/share/pkgconfig")
set(ENV{PKG_CONFIG_PATH} "")
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# xsysroot/refresh-sysroot.sh - (re)export the cross sysroot from the container into WSL
# ~/sysroots/arch-kde-mgl and install the toolchain file as ~/sysroots/aarch64-arch.cmake.
# Read-only on the container side (it only tars headers/libs into /root and moves the tarball out).
# Run once on a new machine, and again after the container upgrades glibc/gcc/vulkan/libdrm/gbm;
# then `bash xbuild.sh --reconfigure`. Env: CHANNEL (default xr), WSL_DISTRO.
set -euo pipefail
here=$(cd "$(dirname "$0")" && pwd)
. "$here/../env.sh"
cp -f "$here/aarch64-arch.cmake" "$ANL_DIR/aarch64-arch.cmake"
bash "$here/../ct.sh" "${CHANNEL:-xr}" <<'IN'
set -e; cd /
L=$(ls -d usr/lib/*crt*.o usr/lib/lib{c,m,mvec,pthread,dl,rt,util,resolv,anl,stdc++,stdc++fs,stdc++exp,supc++,gcc_s,atomic,vulkan,drm,drm_*,gbm,z}.* usr/lib/lib{c,m}_nonshared* usr/lib/ld-linux* usr/lib/pkgconfig usr/share/pkgconfig usr/share/cmake/VulkanHeaders usr/lib/cmake/Vulkan* 2>/dev/null)
tar czf /root/xsysroot.tar.gz --exclude='usr/lib/gcc/*/*/cc1' --exclude='usr/lib/gcc/*/*/cc1plus' --exclude='usr/lib/gcc/*/*/lto1' --exclude='usr/lib/gcc/*/*/lto-wrapper' --exclude='usr/lib/gcc/*/*/collect2' --exclude='usr/lib/gcc/*/*/plugin' --exclude='usr/lib/gcc/*/*/install-tools' --exclude='usr/lib/gcc/*/*/liblto_plugin.so' lib usr/include usr/lib/gcc $L
echo SYSROOT_TAR_DONE
IN
adb_ shell "su -c 'mv $CT_ROOT/root/xsysroot.tar.gz $DEV_TOOLS/xsysroot.tar.gz && chmod 644 $DEV_TOOLS/xsysroot.tar.gz'"
adb_ pull "$DEV_TOOLS/xsysroot.tar.gz" "$ANL_DIR/xsysroot.tar.gz"
adb_ shell "su -c 'rm -f $DEV_TOOLS/xsysroot.tar.gz'"
wsl -d "${WSL_DISTRO:-archlinux}" -- env A="$(win2wsl "$ANL_DIR")" bash -s <<'IN' 2>&1 | tr -d '\0' | grep -av '^wsl:'
set -e; S=$HOME/sysroots/arch-kde-mgl; rm -rf "$S.new"; mkdir -p "$S.new"
tar xzf "$A/xsysroot.tar.gz" -C "$S.new"
rm -rf "$S"; mv "$S.new" "$S"; cp -f "$A/aarch64-arch.cmake" "$HOME/sysroots/aarch64-arch.cmake"
du -sh "$S"; ls "$S/usr/lib/gcc/aarch64-unknown-linux-gnu/"
IN
rm -f "$ANL_DIR/xsysroot.tar.gz"
+27 -2
View File
@@ -61,15 +61,40 @@ 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
if grep -Fq 'android.intent.action.MAIN' <<<"$trace_manifest"; then
echo '::error::Trace APK must not expose a launcher activity' >&2
# 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
exit 1
fi
require 'PostActivity' "$trace_manifest" 'trace launcher activity (PostActivity)'
echo 'Validated unified MobileGL plugin APK and isolated trace APK.'
+7 -3
View File
@@ -18,6 +18,10 @@ on:
- '**/*.md'
workflow_dispatch:
env:
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
# ONE LIVE RUN PER BRANCH, as in test.yml: a superseded push is cancelled rather than left queued
# behind its successor; a manual dispatch gets a group of its own; dev queues rather than cancels.
concurrency:
@@ -477,7 +481,7 @@ jobs:
extra_retrace_args=()
if [ "${MOBILEGL_CI_TRANSPORT}" = "inproc" ]; then
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
--env "MOBILEGL_TRANSPORT=inproc;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--require-inproc
)
elif [ "${MOBILEGL_CI_TRANSPORT}" = "spawn" ]; then
@@ -493,12 +497,12 @@ jobs:
# into the CLIENT's process and SetWindowHandle is refused on the way across with
# Fatal{UnmigratedSurface, "AndroidNativeWindow@P12"} (Rule H).
extra_retrace_args+=(
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1"
--env "MOBILEGL_TRANSPORT=spawn;MOBILEGL_IPC_ROLE_SPLIT_STATE=1;MOBILEGL_IPC_STRICT_ERRORS=1;MOBILEGL_IPC_RUN_AHEAD=1;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}"
--use-pbuffer
--require-spawn
)
else
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith")
extra_retrace_args+=(--env "MOBILEGL_TRANSPORT=monolith;MOBILEGL_GPU_HANG_BUDGET_MS=${MOBILEGL_GPU_HANG_BUDGET_MS}")
fi
# Bliss needs the newer signed ANGLE variant plus sampler mipmap
# min-filter downgrading on ANGLE llvmpipe.
+64 -90
View File
@@ -21,6 +21,10 @@ on:
required: false
default: "087685d1"
env:
# Software-rendered CI submissions may exceed the interactive GPU hang budget.
MOBILEGL_GPU_HANG_BUDGET_MS: "0"
# ONE LIVE RUN PER BRANCH. The disaggregation branch pushes (often docs-only) commits minutes apart
# and each push fans out to ~330 retrace legs here and ~275 jobs in apk.yml; with no group, runs
# sat queued for 10+ hours behind their own successors. A manual dispatch gets a group of its own,
@@ -413,6 +417,12 @@ jobs:
--discovery "${RUNNER_TEMP}/primary-inproc-lane.json" \
--junit "${RUNNER_TEMP}/primary-inproc.xml" --output "${RUNNER_TEMP}/primary-inproc-proof.json"
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload integration lane evidence
if: always()
uses: actions/upload-artifact@v7
@@ -1169,6 +1179,12 @@ jobs:
# arm proof read; verify-split-logs/ is whatever the control steps left behind, which for
# twelve entries is a deliberately red re-run under the same name. Uploading only the
# second - which is what this step did before the copy above existed - loses the first.
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload verify-split lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -1461,11 +1477,27 @@ jobs:
- name: Spawn/split lane parity (9.2)
run: python3 scripts/ci/spawn_lane_parity.py build-split
# P8-A (ID-P8-2): every monolith case, tails normalised, is on a gated split arm or is named
# with a reason class in scripts/data/split_coverage_exemptions.txt. The same `ctest -N`
# cost as the parity step above, and the question it cannot answer: parity compares the
# split arms with EACH OTHER, so a case missing from all three passes it.
- name: Split coverage against monolith (P8-A)
run: |
python3 scripts/ci/split_coverage.py --self-test
python3 scripts/ci/split_coverage.py build-split
# P8-SE: PipeCatalogueTest's unmigrated-emulation list equals the MGPipeUnmigratedEmulation
# call sites in the tree, one site per name. A source lint: no build needed.
- name: Unmigrated-emulation list against its call sites (P8-SE)
run: |
python3 scripts/ci/unmigrated_emulation_sites.py --self-test
python3 scripts/ci/unmigrated_emulation_sites.py
- name: Unit tests on the split runtime
working-directory: build-split
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-split.xml"
# P7 wave 2-F, PH-7 (1). THE TCP SUPERVISOR LANE NEEDS flatc, AND CI HAD NONE.
#
@@ -1632,6 +1664,12 @@ jobs:
CONTROL_TMPDIR: ${{ runner.temp }}
run: bash "${GITHUB_WORKSPACE}/scripts/ci/split_negative_controls.sh"
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload split lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -1721,7 +1759,7 @@ jobs:
env:
MOBILEGL_ITEST_REQUIRE_GPU: "1"
MOBILEGL_IPC_STRICT_ERRORS: "1"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)"
run: ctest --output-on-failure -L unit --no-tests=error -j "$(nproc)" --output-junit "${RUNNER_TEMP}/unit-strict.xml"
# ---- P5e (ra), MG_Remote/CONTRACT-P5E.md §7: ONE STEP, TWO ARMS, AND THE TREE PICKS ----
#
@@ -2042,6 +2080,13 @@ jobs:
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
# P11 A2: the >= 16 MiB arena and its tier assertion (emulated).
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
]
slp.require_green(json.load(open(sys.argv[2])), sys.argv[3],
["DirectVulkan.Split.Buffers." + name for name in cases])
@@ -2104,6 +2149,12 @@ jobs:
"PersistentCoherentMapScenario.TwoWritesThroughTheCoherentPointerEachReachTheirOwnDraw",
"PersistentCoherentMapScenario.AWriteAfterAFrameBoundaryReachesTheNextFramesDraw",
"StorageBufferRegrowScenario.AGrownStoreIsVisibleThroughItsExistingIndexedBinding",
"LargeArenaAdoptionScenario.SubDataAfterAnInFlightDrawReachesTheNextDraw",
"LargeArenaAdoptionScenario.RespecifiedVertexArenaKeepsVaoBindings",
"LargeArenaAdoptionScenario.RespecifiedIndexArenaKeepsVaoBinding",
"LargeArenaAdoptionScenario.ReadbackSeesTheLatestCpuWrite",
"LargeArenaAdoptionScenario.GpuWriteIntoTheArenaIsReadBack",
"LargeArenaAdoptionScenario.TheArenaLandsInTheTierItsLaneDeclares",
]
required = [sys.argv[4] + name for name in cases]
if sys.argv[5] == "tcp":
@@ -2217,6 +2268,12 @@ jobs:
slp.require_green(lane, sys.argv[3], names)
PY
# The run log needs a signed-in reader; annotations do not. Name every failed ctest entry,
# with the lines of its output that say why, where anyone reading the run can see them.
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py "${RUNNER_TEMP}"
- name: Upload strict lane logs
if: always()
uses: actions/upload-artifact@v7
@@ -2722,7 +2779,11 @@ jobs:
&& [ '${{ matrix.case }}' = 'improved-transparency-minecraft-26.3' ]; then
export MOBILEGL_MAGMA_DISABLE_BLENDED_DEPTH_WRITE=1
fi
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$'
ctest -V --no-tests=error -R '^MobileGLTraceReplay\.${{ matrix.case }}\.${{ matrix.backend }}$' --output-junit "${RUNNER_TEMP}/retrace.xml"
- name: Annotate failed entries
if: failure()
run: python3 scripts/ci/junit_annotate.py --excerpts 1 "${RUNNER_TEMP}"
- name: Upload core dumps
if: failure()
@@ -3477,27 +3538,10 @@ jobs:
runs-on: ubuntu-latest
# Deliberately independent of build-linux: these are source-level gates, they take
# seconds, and a broken build must not hide a drifted interface.
env:
# THE CURRENT PHASE's base ref, for the two G5 region gates below. It is 37da3c3a - P4a's
# base ref, INTEGRATOR-DECISIONS ID-1 - and NOT the workflow's baseline_sha input: that input
# is the SYMBOL baseline (087685d1, P1's G1 reading) and it is empty on a push, whereas these
# gates ask "did the do-not-touch list move since the phase started".
#
# IT MOVED FROM P3a's 44c2b5cf TO P4a's 37da3c3a WITH THE PHASE, and that is a deliberate
# narrowing rather than a loss: P3a's eleven functions were compared against 44c2b5cf at P3a's
# own exit and were byte-identical there, so 37da3c3a carries the same bodies (measured: the
# eleven shas at 37da3c3a are the eleven shas at 44c2b5cf, and FlushPendingRangesFrom's is
# still the sha pinned in the script at 3e298c9a). What the two gates now both answer is "did
# anything on the list move during P4a", which is the question this phase can act on.
BASELINE: "37da3c3a"
steps:
- name: Checkout repo
uses: actions/checkout@v6
with:
# The G5 gate reads Managers.cpp at BASELINE with `git show`, which a depth-1 checkout
# does not have. Nothing else in this job needs history.
fetch-depth: 0
# The seven generators all read MG_Pipe/*.def, so regenerating and diffing is what
# keeps the two interface tables, the wire records, the verify comparators, the
@@ -3626,76 +3670,6 @@ jobs:
python3 tools/trace_replay/test_run_tcp_matrix.py
python3 tools/trace_replay/test_compare_actuals.py
# A GATE as of P3a (G5). "pool 与延迟释放原样搬" (ROADMAP.md:19) is meant literally: the
# buffer pool, the deferred-release drain and the three persistently mapped rings move
# VERBATIM, and ARCHITECTURE.md:515 says why - their retire happens only inside Present, so
# a batching or ordering change there starves them, and nothing else in this workflow can
# see it. P3a rewrites the rest of Managers.cpp by design, so a file diff says nothing; the
# script extracts the ELEVEN named bodies and compares their hashes on their own.
#
# Eleven and not ten (ID-15): Managers.cpp carries the three-tier flush drain TWICE, once
# per preprocessor arm, and a push build compiles only FlushPendingRangesFrom while the
# untouched FlushPendingRangesNow lives in the `#else`. Hashing the pull name alone would
# protect text no shipping build compiles, so both are hashed - the pull ladder against
# BASELINE, the push ladder against a sha pinned in the script at 3dadd4c1 (ID-41), because
# that one was born in P3a and is compared against the reviewed body rather than against the
# base ref.
#
# Scoped to the disaggregation branch and to a manual dispatch, deliberately: the question
# is "did these eleven move since P3a started", and BASELINE is P3a's base ref. On dev,
# where unrelated buffer fixes land on their own schedule, the same comparison would be
# asking a question nobody posed - it belongs with the TEMPORARY trigger lines at the top
# of this file and retires with them.
#
# --self-test is the half that keeps it honest, and it is not optional: a comparison that
# silently stopped comparing produces exactly the same green as eleven untouched bodies. It
# runs eight canned controls - eleven bodies extracted, an untouched copy compared equal, an
# edit OUTSIDE them ignored, each of the three perturbation targets (ClearBufferPool and BOTH
# flush ladders) reported BY NAME, plus the pin-precedence control and a one-token edit to the
# pinned ladder compared against the pin - and fails if any of them does not answer.
# Same shape as gen_pipe.py --self-test above.
- name: P3a regions unchanged (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p3a_untouched_regions.sh "${BASELINE}" HEAD
- name: P3a untouched-region self-test (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p3a_untouched_regions.sh --self-test
# A GATE AS OF P4a (G5), and a SECOND script rather than an edit to the one above. P4a extends
# the same claim to the rest of ARCHITECTURE.md:318's do-not-touch list - the unpack PBO ring's
# staging repack and its two helpers, the attachment permutation, the D24S8 sampling-emulation
# core and the format-caveat handler - which is SEVENTEEN regions across THREE files
# (BRIEF-P4A.md D-N: P3a's eleven, which P4a must not touch either, plus P4a's six). The
# parent's SOURCE_PATH is a single file, so the extension needed a per-region source path and
# a region KIND (DepthStencilSamplingReadImpl is a namespace, not a function); everything else
# about the extraction is its parent's, verbatim.
#
# Both scripts run. The parent keeps answering its own question against its own eleven, so a
# regression in either half names itself, and neither gate can be silenced by editing the
# other's list.
#
# Same feat/disaggregated-or-dispatch guard as the P3a step, for the same reason: the question
# is "did these move since the phase started", and on dev - where unrelated buffer and texture
# fixes land on their own schedule - it would be a question nobody posed. It belongs with the
# TEMPORARY trigger lines at the top of this file and retires with them.
#
# --self-test is the half that keeps it honest and is not optional: a comparison that silently
# stopped comparing produces exactly the same green as seventeen untouched regions. It runs
# three positive controls (seventeen regions extracted, an untouched copy compared equal, an
# edit OUTSIDE them invisible in all three files) and NINE negative ones - ClearBufferPool,
# FlushPendingRangesNow, RecomputeBackendColorSlots and StageBlocksIntoUnpackRing, each
# perturbed at its HEAD and at its TAIL and each required to be named BY NAME, plus a
# one-token edit to the pinned FlushPendingRangesFrom body compared against the pin - and
# fails if any of them does not answer.
- name: P4a regions unchanged (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p4a_untouched_regions.sh "${BASELINE}" HEAD
- name: P4a untouched-region self-test (G5)
if: ${{ github.ref == 'refs/heads/feat/disaggregated' || github.event_name == 'workflow_dispatch' }}
run: bash scripts/p4a_untouched_regions.sh --self-test
# Warning only for now: the disaggregation documents are still being written, and a
# lint that fails a rewrite in progress teaches people to ignore it. It becomes
# --strict when the documents settle.
+31
View File
@@ -0,0 +1,31 @@
name: Arch WSL GPU smoke
on:
workflow_dispatch:
push:
branches:
- ci/arch-wsl-gpu-runner
paths:
- .github/workflows/wsl-gpu-smoke.yml
- docs/ci/arch-wsl-gpu-runner.md
permissions: {}
concurrency:
group: arch-wsl-gpu-smoke
cancel-in-progress: false
jobs:
gpu-smoke:
runs-on: [self-hosted, Linux, X64, arch, wsl, gpu, gles, vulkan]
timeout-minutes: 5
steps:
- name: Report runner identity
shell: bash
run: |
printf 'Runner: %s\n' "$RUNNER_NAME"
uname -n
uname -a
- name: Verify hardware GLES and Vulkan
shell: bash
run: /usr/local/libexec/mobilegl-runner/gpu-smoke
+1
View File
@@ -28,3 +28,4 @@ tools/trace_replay/work/
__pycache__/
*.py[cod]
/.gradle
/.trace-work/
+11
View File
@@ -0,0 +1,11 @@
# 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
@@ -0,0 +1,141 @@
/*
* 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
@@ -0,0 +1,464 @@
/*
* 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 */
+128 -2
View File
@@ -29,6 +29,8 @@ option(MOBILEGL_BUILD_DISAGGREGATED "Build the MG_Remote transport layer (two-pr
# 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).
@@ -302,6 +304,7 @@ set(SOURCE_FILES
MobileGL/ConfigLoader.cpp
MobileGL/MG_Util/Debug/Log.cpp
MobileGL/MG_Util/X11/DisplayGuard.cpp
MobileGL/MG_Util/Async/JobNode.cpp
MobileGL/MG_Util/Async/ShaderCompilePool.cpp
@@ -369,6 +372,7 @@ set(SOURCE_FILES
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/FixIterationRPSubgroupScratchPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateSubgroupsPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DeduplicateImageTypesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DSampledImagesPass.cpp
MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp
@@ -397,11 +401,17 @@ set(SOURCE_FILES
MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/Definitions.cpp
MobileGL/MG_Impl/GLXImpl/Exporting/GlvndVendor.cpp
MobileGL/MG_Impl/GLXImpl/GLXImpl.cpp
MobileGL/MG_Impl/GLXImpl/X11Present.cpp
MobileGL/MG_Impl/GLXImpl/X11PresentXcb.cpp
MobileGL/MG_Impl/GLXImpl/LookUp/LookUp.cpp
MobileGL/MG_Impl/EGLImpl/Exporting/Definitions.cpp
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
@@ -431,6 +441,7 @@ 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
@@ -440,6 +451,7 @@ 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
@@ -457,8 +469,10 @@ set(SOURCE_FILES
MobileGL/MG_Backend/DirectVulkan/Renderer/VkSamplerManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkClearManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.cpp
MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
MobileGL/MG_State/GLState/Core.cpp
MobileGL/MG_State/GLState/ShareGroupState.cpp
MobileGL/MG_State/EGLState/Core.cpp
MobileGL/MG_State/GLState/ErrorState/Error.cpp
MobileGL/MG_State/GLState/BufferState/BufferState.cpp
@@ -607,9 +621,17 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
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]
@@ -630,6 +652,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
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
@@ -649,7 +674,19 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
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).
@@ -659,6 +696,8 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# (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 -
@@ -682,7 +721,7 @@ if (ANDROID)
MobileGL/MG_Util/SelfTest/DriverBenchJni.cpp
)
if (MOBILEGL_BUILD_DISAGGREGATED)
# P12 (on-screen server window): the trace APK's MobileGLDisplayActivity - the in-process
# 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
@@ -763,6 +802,9 @@ if (MOBILEGL_BUILD_DISAGGREGATED)
# 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
@@ -809,7 +851,7 @@ target_compile_definitions(${CMAKE_PROJECT_NAME}
if(UNIX AND NOT APPLE AND NOT ANDROID)
foreach(MOBILEGL_LOADER_ALIAS
libEGL.so libEGL.so.1)
libEGL.so libEGL.so.1 libGL.so libGL.so.1 libGLX_mobilegl.so.0)
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
"$<TARGET_FILE_NAME:${CMAKE_PROJECT_NAME}>"
@@ -873,6 +915,28 @@ if (TRACY_ENABLE)
target_compile_definitions(${CMAKE_PROJECT_NAME}_s PUBLIC -DTRACY_ENABLE)
endif ()
# GLVND, AND WHY THIS IS A LINKER FLAG RATHER THAN A VISIBILITY ONE.
#
# MobileGL can serve as a glvnd EGL vendor - the library the system's libEGL.so.1 dispatches to -
# and a vendor must hand glvnd ITS OWN entry points: glvnd calls whatever address it is given, so
# an address that resolves to another libEGL in the process is a call back into the dispatcher.
# With default visibility every reference inside this library resolves against the whole process,
# and a process that loaded the system libEGL first wins. Marking the entry points protected says
# the same thing in the source, except the platform's own GL/EGL headers declare them default and
# the two visibilities do not mix; this flag is that promise made by the linker instead.
if (UNIX AND NOT APPLE)
target_link_options(${CMAKE_PROJECT_NAME} PRIVATE "LINKER:-Bsymbolic-functions")
endif()
# ---- Shared images outside GL: the C ABI and the GBM backend (Linux client, split builds) ----
# docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md. libMobileGL.so exports the shared-image C ABI
# (MG_Impl/SharedImageApi.h); mobilegl_gbm.so (MG_Gbm/) is a separate GBM loader module that reaches
# it through dlsym. Self-contained: nothing outside this block refers to either.
if (MOBILEGL_BUILD_DISAGGREGATED AND UNIX AND NOT APPLE AND NOT ANDROID)
target_sources(${CMAKE_PROJECT_NAME} PRIVATE MobileGL/MG_Impl/SharedImageApi.cpp)
add_subdirectory(MobileGL/MG_Gbm)
endif()
if (ANDROID)
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
android
@@ -946,6 +1010,17 @@ if (NOT ANDROID AND NOT MOBILEGL_IOS)
endif ()
endif ()
# Discover gtest cases when ctest starts, one binary after another, instead of in a post-build
# step. The post-build discovery of the CMake releases that never pass the target name down
# writes every binary's `--gtest_output=json` result to ONE per-directory file
# (cmake_test_discovery_<hash of an empty name>.json), so binaries linked in parallel in the same
# directory overwrite and delete each other's list: the registered count then differs between
# runs of the same tree and a binary ends up with none or only some of its cases. The pre-test
# mode reads the list from stdout, serially, and also works when the binary is cross-built.
if (NOT DEFINED CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE)
set(CMAKE_GTEST_DISCOVER_TESTS_DISCOVERY_MODE PRE_TEST)
endif()
if (NOT ANDROID)
# Enable testing in the top-level scope so a CTestTestfile.cmake is emitted
# at the build-tree root. This lets `ctest` be invoked from the top-level
@@ -982,6 +1057,12 @@ 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.
#
@@ -1090,3 +1171,48 @@ if (ANDROID AND MOBILEGL_BUILD_SERVER_SPIKE)
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()
+64 -11
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, 9, 0, "-dev", VersionType::Development};
inline const Version CoreVersion = {26, 10, 0, "-dev", VersionType::Development};
inline const VersionStringFormatAttrib DefaultVersionStringFormatAttrib = {2, 2, 0, true, true};
inline const Uint64 CacheVersion = 0;
@@ -130,8 +130,25 @@ 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,
// resolves certain rendering bugs on ANGLE + llvmpipe.
Bool EsprytAvoidSamplerMipmapMinFilter = false;
@@ -202,9 +219,12 @@ namespace MobileGL::MG_Config {
// 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 is a plain memcpy into GPU-visible memory; every driver-mediated
// route for the same write stalls the thread or ghost-copies the whole arena on
// this class of Mali driver, and the arena stops costing its size again in RAM.
// 128MB arena lands GPU-ordered through the backend's resident-subdata op
// (BufferObject::LandBytesIntoResidentStore, since 0ee3384b: an in-place write
// tore the frames still reading the old bytes) - still no driver-mediated write
// into the busy mutable store, which on this class of Mali driver stalls the
// thread or ghost-copies the whole arena, and the arena stops costing its size
// again in RAM.
Bool DisableLargeBufferAdoption = false;
// MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION: make DirectGLES skip the native ES
// depth/stencil reads and always go through the shader-sampling emulation. Core GL
@@ -408,8 +428,9 @@ namespace MobileGL::MG_Config {
Uint32 PipeIndexMirrorMb = 64;
// MOBILEGL_PIPE_STATS_PERIOD: frames per boundary-counter summary line. 120 is the
// steady-state cadence; the device retrace harness never reaches the teardown dump
// and a trimmed fixture (create-indirect) is shorter than 120 frames, so a run that
// needs its numbers at all sets this low enough to land at least one window.
// and a trimmed fixture (minecraft-1.21.1-neoforge-create-indirect-in-world-align1024)
// is shorter than 120 frames, so a run that needs its numbers at all sets this low
// enough to land at least one window.
Uint32 PipeStatsPeriod = 120;
// MOBILEGL_PIPE_STATS_FILE: where the boundary counters' teardown JSON dump goes.
// Empty (the default) means no dump; the per-120-frame summary line still goes to
@@ -485,6 +506,9 @@ namespace MobileGL::MG_Config {
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
@@ -595,11 +619,40 @@ namespace MobileGL::MG_Config {
// 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_ADOPT_TIER: 2 = emulate (client keeps the shadow and pushes), which
// is the only tier P5 implements and the reason persistent-map-push can be non-zero
// at all (R-6). 0 and 1 parse and are Fatal at use with "P11"; they exist now so the
// negative control has a spelling the day P11 writes it.
Uint32 AdoptTier = 2;
// 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
+155 -10
View File
@@ -6,6 +6,9 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#if defined(__ANDROID__)
#include <sys/system_properties.h>
#endif
#include "Config.h"
#if MOBILEGL_PIPE_PUSH
// For kMGPipeSubsystemsMigratedAtP5e, the push build's PipePush default (the P2, P3a and P4a
@@ -15,12 +18,10 @@
#endif
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#if MOBILEGL_BUILD_DISAGGREGATED
// std::strcmp, for InitIpc's server-role check (P12). Split-only, so the pull build's TU is
// unchanged (G1).
// std::strcmp: InitIpc's server-role check (P12) and the system configuration's.
#include <cstring>
#endif
#ifndef _WIN32
extern char** environ;
@@ -49,6 +50,115 @@ namespace MobileGL::MG_ConfigLoader {
return (key.compare(0, 6, "LIBGL_") == 0 || key.compare(0, 9, "MOBILEGL_") == 0);
}
// ---- the system configuration -----------------------------------------------------------
//
// A library that is every process's GL implementation (installed as the system's EGL/GLX
// vendor) cannot rely on each process having been started with MOBILEGL_* in its environment:
// most are started by something that has never heard of it. So the settings a whole machine
// shares live in files, and the environment only overrides them:
//
// environment > /etc/mobilegl/backend (MOBILEGL_BACKEND_TYPE only) > client.conf > built-in
//
// client.conf is `KEY=value` lines; blank lines and lines starting with '#' are skipped, the
// key and the value are trimmed, and one pair of matching quotes around the value is removed.
// Only MOBILEGL_* / LIBGL_* keys are taken, exactly as from the environment. The backend file
// holds just the backend's name (the file the backend switch rewrites), and names it over a
// MOBILEGL_BACKEND_TYPE line in client.conf so that switch cannot be silently shadowed.
//
// MOBILEGL_CONFIG_FILE / MOBILEGL_BACKEND_FILE name other files; set to empty they turn the
// file off. The defaults exist only on desktop Linux: an Android app configures its process
// itself, and a server process (MOBILEGL_IPC_ROLE=server) never reads the clients' file.
namespace {
#if defined(__linux__) && !defined(__ANDROID__)
constexpr const char* kDefaultConfigFile = "/etc/mobilegl/client.conf";
constexpr const char* kDefaultBackendFile = "/etc/mobilegl/backend";
#else
constexpr const char* kDefaultConfigFile = "";
constexpr const char* kDefaultBackendFile = "";
#endif
String FilePathSetting(const char* variable, const char* fallback) {
const char* value = std::getenv(variable);
return value != nullptr ? String(value) : String(fallback);
}
String Trimmed(const String& text) {
const auto first = text.find_first_not_of(" \t\r\n");
if (first == String::npos) return String();
const auto last = text.find_last_not_of(" \t\r\n");
return text.substr(first, last - first + 1);
}
Bool IsServerProcess() {
const char* role = std::getenv("MOBILEGL_IPC_ROLE");
return role != nullptr && std::strcmp(role, "server") == 0;
}
// Reads `path` into `out` without replacing what `out` already holds. A missing file is
// not an error: the defaults are the absence of one.
void ReadConfigFile(const String& path, UnorderedMap<String, String>& out, Bool log) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[4096];
Uint32 lineNumber = 0;
while (std::fgets(buffer, sizeof(buffer), file) != nullptr) {
++lineNumber;
const String line = Trimmed(buffer);
if (line.empty() || line[0] == '#') continue;
const auto equals = line.find('=');
const String key = equals == String::npos ? String() : Trimmed(line.substr(0, equals));
if (key.empty() || !IsAcceptedPrefix(key)) {
if (log) {
MGLOG_W("Config: %s:%u is not a MOBILEGL_*/LIBGL_* KEY=value line; ignored", path.c_str(),
lineNumber);
}
continue;
}
String value = Trimmed(line.substr(equals + 1));
if (value.size() >= 2 && (value.front() == '"' || value.front() == '\'') && value.back() == value.front()) {
value = value.substr(1, value.size() - 2);
}
out.emplace(key, value);
}
std::fclose(file);
}
void ReadBackendFile(const String& path, UnorderedMap<String, String>& out) {
if (path.empty()) return;
std::FILE* file = std::fopen(path.c_str(), "r");
if (file == nullptr) return;
char buffer[256];
const Bool read = std::fgets(buffer, sizeof(buffer), file) != nullptr;
std::fclose(file);
if (!read) return;
const String name = Trimmed(buffer);
if (!name.empty()) out.emplace("MOBILEGL_BACKEND_TYPE", name);
}
// Everything the system files say, the backend file ahead of client.conf (first emplace
// wins). Re-read on every call: callers are few (Init, and a handful of lookups before it),
// and a file that changed between two processes' starts must not be served stale.
void ReadSystemConfig(UnorderedMap<String, String>& out, Bool log) {
if (IsServerProcess()) return;
ReadBackendFile(FilePathSetting("MOBILEGL_BACKEND_FILE", kDefaultBackendFile), out);
ReadConfigFile(FilePathSetting("MOBILEGL_CONFIG_FILE", kDefaultConfigFile), out, log);
}
} // namespace
Bool LookupSetting(const char* key, String& outValue) {
if (const char* value = std::getenv(key)) {
outValue = value;
return true;
}
UnorderedMap<String, String> system;
ReadSystemConfig(system, false);
const auto it = system.find(key);
if (it == system.end()) return false;
outValue = it->second;
return true;
}
inline void InitializeAcceptedEnvVariables() {
if (!acceptedEnvVariablesMap) {
acceptedEnvVariablesMap = MakeUnique<UnorderedMap<String, String>>();
@@ -79,6 +189,15 @@ namespace MobileGL::MG_ConfigLoader {
}
}
}
// Below the environment: a key the environment set is never replaced.
UnorderedMap<String, String> system;
ReadSystemConfig(system, true);
for (const auto& [key, value] : system) {
if (acceptedEnvVariablesMap->emplace(key, value).second) {
MGLOG_D("Config: Accepted system setting: %s=%s", key.c_str(), value.c_str());
}
}
}
inline void QueryEnvVariable(const String& key, String& outValue, const String& defaultValue) {
@@ -229,7 +348,11 @@ namespace MobileGL::MG_ConfigLoader {
features.MagmaDeriveNumSubgroups = QueryEnvQuirkOverride("MOBILEGL_MAGMA_DERIVE_NUM_SUBGROUPS");
features.AdvertiseFp64 = QueryEnvFlag("MOBILEGL_ADVERTISE_FP64");
features.MagmaR11G11B10FFallback = QueryEnvFlag("MOBILEGL_MAGMA_R11G11B10F_FALLBACK");
features.MagmaAllowInvalidSpirv = QueryEnvFlag("MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV");
features.MagmaFramesInFlight = QueryEnvUint32("MOBILEGL_MAGMA_FRAMESINFLIGHT", 3, 1, 64);
features.MagmaMaxDrawsPerCommandBuffer =
QueryEnvUint32("MOBILEGL_MAGMA_MAX_DRAWS_PER_COMMAND_BUFFER", 16384, 0, 1u << 24);
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");
@@ -299,6 +422,17 @@ namespace MobileGL::MG_ConfigLoader {
inline void InitBackendType() {
String backendTypeStr;
QueryEnvVariable("MOBILEGL_BACKEND_TYPE", backendTypeStr, "DirectGLES");
#if defined(__ANDROID__)
// An Android app that hosts the library sets the environment itself, before the library
// loads; the debug property is how a developer picks the backend for such a process
// without rebuilding it (`adb shell setprop debug.mobilegl.backend DirectVulkan`).
char property[PROP_VALUE_MAX] = {};
if (__system_property_get("debug.mobilegl.backend", property) > 0) {
MGLOG_I("Config: debug.mobilegl.backend=%s overrides MOBILEGL_BACKEND_TYPE=%s", property,
backendTypeStr.c_str());
backendTypeStr = property;
}
#endif
#define ENTRY(backendType) \
if (backendTypeStr == #backendType) { \
MG_Config::ActiveBackendType = BackendType::backendType; \
@@ -421,14 +555,23 @@ namespace MobileGL::MG_ConfigLoader {
// 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;
// 2 is the only tier P5 implements (R-6). 0 and 1 parse here and are refused at the
// point of use, which is where the "P11" in the message belongs.
ipc.AdoptTier = QueryEnvUint32("MOBILEGL_IPC_ADOPT_TIER", 2, 0, 2);
// 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.
@@ -522,11 +665,13 @@ namespace MobileGL::MG_ConfigLoader {
// 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 "
"adopt-tier=%u verb-barrier=%u run-ahead=%u present-credit=%u control-timeout=%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.AdoptTier,
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(),
+5
View File
@@ -34,6 +34,11 @@
#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
+26
View File
@@ -24,6 +24,9 @@
#if MOBILEGL_PIPE_VERIFY
#include <MG_Backend/MGPipe/PipeInputs.h> // MGPipeVerifyFlushSummary, before Debug::Close in DestroyImpl
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Client/ServerProbe.h>
#endif
#include <atomic>
#include <mutex>
@@ -102,6 +105,10 @@ namespace MobileGL {
MG_State::GLState::ClearProgramTranslationCache();
MG_Backend::gBackendFunctionsTable = {};
g_isInitialized = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// A later re-initialization asks whether the server is there again.
MG_Remote::Client::ResetServerProbeCache();
#endif
#if MOBILEGL_PIPE_VERIFY
// BEFORE Close, and from here rather than from a static destructor (V1 fix round 2):
// Close() nulls the role's sink and Log.cpp's next write reopens it with "w", so the
@@ -220,6 +227,25 @@ namespace MobileGL {
DestroyImpl(true);
}
Bool ImplementationAvailable() {
if (g_isInitialized.load(std::memory_order_acquire)) {
#if MOBILEGL_BUILD_DISAGGREGATED
// Initialized: under a split transport the session either came up (a backend object
// exists) or it did not, and a display on a session that never came up cannot draw.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
return MG_Backend::pActiveBackendObject != nullptr;
}
#endif
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// Not yet: the bring-up would dial the configured server and wait for it. Ask first.
return MG_Remote::Client::ConfiguredServerAvailable();
#else
return true;
#endif
}
// MobileGL's lifecycle is owned entirely by the host-API layers
// (EGL/WGL/CGL): initialization happens lazily on the first entry point
// via EnsureInitialized(), and full teardown happens deterministically
+16
View File
@@ -26,13 +26,29 @@ namespace MobileGL {
namespace MG_ConfigLoader {
void Init();
// One setting resolved the way Init() resolves it - the environment, then the system
// configuration files (ConfigLoader.cpp) - for the few readers that must decide something
// before MobileGL initializes. False when neither names it.
Bool LookupSetting(const char* key, String& outValue);
} // namespace MG_ConfigLoader
// Whether this process can be served right now. False when MobileGL is configured to reach a
// server that is not there (or a split session already failed to come up), or when the process
// asked for software rendering (LIBGL_ALWAYS_SOFTWARE, desktop Linux), so a
// caller that can choose another implementation - the system's EGL/GLX/GBM loaders - should
// decline rather than hand out something that cannot draw. Cheap: once true it stays true
// until the library is torn down, and a probe is one non-blocking connect with a short bound.
Bool ImplementationAvailable();
namespace MG_Backend {
void Init();
} // namespace MG_Backend
namespace MG_Impl {
void Init();
// GL framebuffer 0, built into the calling thread's GLContext (the process default at
// Init) and installed as the one every later context starts with. Init's step, and the
// rebuild after a recovered device loss, whose fresh session holds none of the old one's.
void BuildDefaultFramebuffer();
} // namespace MG_Impl
} // namespace MobileGL
+57 -11
View File
@@ -97,6 +97,8 @@ namespace MobileGL::MG_Backend {
static_cast<SizeT>(target) >= kFormatCapabilityTextureTargetCount) {
return kFormatCapabilityTargetCount;
}
// An external texture is a 2D one wherever a format can go.
if (target == TextureTarget::External) return static_cast<SizeT>(TextureTarget::Texture2D);
return static_cast<SizeT>(target);
}
@@ -172,11 +174,15 @@ namespace MobileGL::MG_Backend {
return false;
}
if (m_eglDisplayInitialized && m_eglDisplay != dpy) {
MGLOG_E("InitializeEGLDisplay failed: backend already bound to a different EGLDisplay");
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.
m_eglDisplay = dpy;
m_eglDisplayInitialized = true;
if (major) {
@@ -196,12 +202,15 @@ namespace MobileGL::MG_Backend {
Bool BackendObject::ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
auto surfaceIt = m_eglSurfaces.find(surface);
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind != SurfaceKind::Window) {
if (surfaceIt == m_eglSurfaces.end() || surfaceIt->second.Kind == SurfaceKind::None) {
MGLOG_E("ResizeEGLWindowSurface failed: no window surface is initialized");
return false;
}
// A pbuffer-backed window (a Wayland window presented through wl_shm) resizes too.
surfaceIt->second.Window.Width = width;
surfaceIt->second.Window.Height = height;
surfaceIt->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
surfaceIt->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == surface) {
m_windowHandle.Width = width;
m_windowHandle.Height = height;
@@ -279,6 +288,7 @@ namespace MobileGL::MG_Backend {
return true;
}
m_activatingSurface = surface;
if (surfaceState->Kind == SurfaceKind::Window) {
SetWindowHandle(surfaceState->Window);
if (!InitWindowSurface()) {
@@ -298,8 +308,14 @@ namespace MobileGL::MG_Backend {
m_eglSurface = surface;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = surfaceState->Kind;
m_eglCurrentThreads.clear();
m_backendCapabilitiesInitialized = false;
// NOT m_eglCurrentThreads.clear(): another thread's current context and surface are its
// own (a multi-window client renders each window on its own thread), and wiping them made
// that thread's next eglSwapBuffers fail with "no current context attached".
auto activatedIt = m_eglSurfaces.find(surface);
if (activatedIt != m_eglSurfaces.end() && !activatedIt->second.Activated) {
activatedIt->second.Activated = true;
m_backendCapabilitiesInitialized = false;
}
return true;
}
@@ -366,6 +382,24 @@ namespace MobileGL::MG_Backend {
m_eglCurrentThreads.clear();
}
void BackendObject::ForgetActiveEGLSurface() {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
m_eglSurfaceInitialized = false;
m_eglSurfaceKind = SurfaceKind::None;
m_eglSurface = EGL_NO_SURFACE;
m_windowHandle = {};
}
namespace {
MG_Util::Damage::Region& PresentDamageSlot() {
thread_local MG_Util::Damage::Region damage = MG_Util::Damage::Region::Full();
return damage;
}
} // namespace
const MG_Util::Damage::Region& CurrentPresentDamage() { return PresentDamageSlot(); }
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage) { PresentDamageSlot() = damage; }
Bool BackendObject::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!m_eglDisplayInitialized || m_eglDisplay != dpy) {
@@ -382,10 +416,16 @@ namespace MobileGL::MG_Backend {
MGLOG_E("SwapEGLBuffers failed: draw surface is not current on this thread");
return false;
}
if (!m_eglSurfaceInitialized || draw == EGL_NO_SURFACE || draw != m_eglSurface) {
if (draw == EGL_NO_SURFACE || !GetRegisteredEGLSurface(draw)) {
MGLOG_E("SwapEGLBuffers failed: invalid draw surface");
return false;
}
// 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) {
@@ -426,8 +466,9 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
ForgetActiveEGLSurface();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLCurrentThread(const std::thread::id& threadKey) {
@@ -458,8 +499,9 @@ namespace MobileGL::MG_Backend {
m_eglSurfaces.erase(surfaceIt);
if (m_eglSurface == surface) {
OnEGLSurfaceReleased(surface);
ResetEGLRuntimeState();
ForgetActiveEGLSurface();
}
OnEGLSurfaceForgotten(surface);
}
void BackendObject::ReleaseEGLResources() {
@@ -492,4 +534,8 @@ namespace MobileGL::MG_Backend {
void BackendObject::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
}
void BackendObject::OnEGLSurfaceForgotten(EGLSurface surface) {
(void)surface;
}
} // namespace MobileGL::MG_Backend
+245
View File
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include "MG_State/GLState/TextureState/TextureEnum.h"
#include <MG_Util/Damage/Damage.h>
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Pipe/MGPipeHandles.h>
#endif
@@ -583,26 +584,256 @@ namespace MobileGL {
Uint32 Height = 0;
};
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): server-allocated colour
// buffers exported to clients as dma-buf descriptors.
//
// The server-side view of one image, as a backend sees it: `NativeBuffer` is the image's
// AHardwareBuffer (null where there is none), `Fourcc` a DRM fourcc - ABGR8888 (R, G, B, A in
// memory, GL RGBA8) or XBGR8888 (alpha ignored); ARGB8888 / XRGB8888 images are stored the
// same way (SharedImageRegistry.h says why), so a backend binds every one as RGBA8.
struct SharedImageView {
Uint64 Id = 0;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
void* NativeBuffer = nullptr;
};
// The client-side answer of an allocation: the image's id and its exported descriptor
// (`Fd`, owned by the caller), with the plane layout a linux-dmabuf wl_buffer is created with.
struct SharedImageExport {
Uint64 Id = 0;
int Fd = -1;
Uint32 Width = 0;
Uint32 Height = 0;
Uint32 Fourcc = 0;
Uint32 Stride = 0;
Uint32 Offset = 0;
Uint64 Modifier = 0;
// A YUV image's Cb Cr plane: in the same descriptor, at this offset and pitch.
Uint32 Plane1Stride = 0;
Uint32 Plane1Offset = 0;
};
// An import of a two-plane (YUV) dma-buf: where its planes are in the one descriptor, and
// EGL_EXT_image_dma_buf_import's colour hints (0 = not given).
struct SharedImageImportLayout {
Uint32 PlaneCount = 1;
Uint32 Offset[2] = {0, 0};
Uint32 Pitch[2] = {0, 0};
Uint32 ColorSpace = 0;
Uint32 Range = 0;
Uint32 SitingX = 0;
Uint32 SitingY = 0;
// The DRM format modifier the import named; HasModifier false = none (the implicit one).
Bool HasModifier = false;
Uint64 Modifier = 0;
};
// THE DAMAGE OF THE PRESENT BEING APPLIED on this thread: the server's apply thread sets it
// around the backend's Present (from the present record), and a backend that can hand damage
// to its native present reads it there. Full whenever nothing set it.
const MG_Util::Damage::Region& CurrentPresentDamage();
void SetCurrentPresentDamage(const MG_Util::Damage::Region& damage);
class BackendObject {
public:
virtual ~BackendObject() = default;
// ---- shared images, SERVER side (a backend implements these) ---------------------
//
// Copies the default framebuffer of the surface current on this (the apply) thread into
// `image`, rows TOP FIRST - GL's top row (y = height-1) lands in the image's first memory
// row, which is what a wl_buffer's first row is - scaled to the image's extent if the two
// differ. The image is ORDERED THROUGH ITS SYNC STATE (SharedImageRegistry.h): the copy
// waits for the image's pending read fences before it writes, and its own completion
// is published with SharedImages::PublishWrite as a sync_file - the call returns once
// the copy is submitted, never waiting for it on the CPU (a backend with no fence export
// waits and publishes -1). A reader binding the image waits for that fence on its GPU
// when the write generation moved, and at its frame boundary publishes its frame's fence
// with PublishRead / ReadTracker. The application's GL state is untouched. False = not
// supported / failed (logged).
//
// `region` (GL window coordinates, origin bottom-left) is the part of the frame to copy;
// Full copies all of it. The rest of the image is left as it is - the client sends a
// region only for an image that already holds the frame outside it - and a backend that
// cannot be sure the image's contents survived (a target it has never written) copies
// all of it regardless.
virtual Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) {
(void)image, (void)region;
return false;
}
// EGL_BUFFER_AGE_EXT of the draw surface of the context current on this (the apply)
// thread: how many presents ago the buffer the next frame draws into held the frame then
// presented; 0 = unknown (the application repaints everything). Asking says the client
// relies on the answer, so from then on the backend keeps what it presents.
//
// `damageRegionFollows`: the client declares the region it draws (SetCurrentDamageRegion)
// before drawing. A driver whose only buffer age is EGL_KHR_partial_update's keeps a
// buffer's content only outside that region, so without one coming it is not asked.
virtual Int32 QueryCurrentBufferAge(Bool damageRegionFollows) {
(void)damageRegionFollows;
return 0;
}
// eglSetDamageRegionKHR for that draw surface: the frame draws `region` (GL window
// coordinates; Full = all of it) and nothing outside it. A backend without the notion
// ignores it.
virtual Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return true;
}
// IMPLICIT SYNC BY FLUSH (docs/Disaggregated/notes/anland/plan-x11-gpu.md): the session
// on this (the apply) thread ends a frame at a glFlush. Every shared image it used since
// its last boundary is published as written and read by one fence of everything it
// submitted (SharedImages::ReadTracker::PublishFrameAsWrite), and from now on its first
// use of an image in a frame waits for that image's pending reads (it may be about to
// overwrite what another session still samples). A backend without shared images has
// nothing to publish.
virtual Bool PublishSharedImageAccesses() { return true; }
// EGL_ANDROID_native_fence_sync's fence command, on the server: in `*fence`, a sync_file
// (the caller's to close) that signals once everything the session submitted so far has
// completed - and, for a session that writes images, what PublishSharedImageAccesses
// does. -1 when there is none to give: the backend cannot export one, or the work has
// already completed - in both cases it HAS completed when this returns.
virtual Bool ExportNativeFence(int* fence) {
*fence = -1;
return PublishSharedImageAccesses();
}
// ---- shared images, CLIENT side (BackendObject_Remote implements these) ----------
//
// A new image, allocated by the server; `out->Fd` is the caller's to close.
virtual Bool AllocateSharedImage(Uint32 width, Uint32 height, Uint32 fourcc, SharedImageExport* out) {
(void)width, (void)height, (void)fourcc, (void)out;
return false;
}
// Names the image `fd` was exported from (the caller keeps `fd`). False: not one of this
// server's images, or the size/format disagree.
virtual Bool ImportSharedImage(int fd, Uint32 width, Uint32 height, Uint32 fourcc, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)outId;
return false;
}
// The same for a YUV fourcc, whose planes and hints `layout` gives. A YUV buffer the
// server did not export is taken too (its labelled CPU-copy fallback).
virtual Bool ImportSharedImagePlanes(int fd, Uint32 width, Uint32 height, Uint32 fourcc,
const SharedImageImportLayout& layout, Uint64* outId) {
(void)fd, (void)width, (void)height, (void)fourcc, (void)layout, (void)outId;
return false;
}
// Drops this client's reference (an allocation's or an import's).
virtual Bool ReleaseSharedImage(Uint64 id) {
(void)id;
return false;
}
// In the GL stream: the current context's default framebuffer, copied top row first into
// the image (BlitDefaultFramebufferToSharedImage on the server) - `region` of it, Full for
// all. Returns when it completed.
virtual Bool PresentToSharedImage(Uint64 id, const MG_Util::Damage::Region& region) {
(void)id, (void)region;
return false;
}
// In the GL stream: EGL_BUFFER_AGE_EXT of the current draw surface (QueryCurrentBufferAge
// on the server). False: the backend has no answer (the caller reports 0).
virtual Bool QueryBufferAge(Bool damageRegionFollows, EGLint* age) {
(void)damageRegionFollows, (void)age;
return false;
}
// In the GL stream: eglSetDamageRegionKHR for the current draw surface
// (SetCurrentDamageRegion on the server).
virtual Bool SetDamageRegion(const MG_Util::Damage::Region& region) {
(void)region;
return false;
}
// In the GL stream: the texture whose lifetime id is `textureLifetimeId` takes the image as
// its level-0 storage (glEGLImageTargetTexture2DOES), until its next storage definition.
// The caller has already defined level 0 as Width x Height RGBA8 with no data.
virtual Bool AttachSharedImageToTexture(Uint64 textureLifetimeId, Uint64 id) {
(void)textureLifetimeId, (void)id;
return false;
}
// In the GL stream, from glFlush/glFinish: PublishSharedImageAccesses on the server,
// returning once the fence is published. Skipped (true) when nothing reached the stream
// since the last one.
virtual Bool FlushSharedImageAccesses() { return false; }
// In the GL stream, from eglCreateSync(EGL_SYNC_NATIVE_FENCE_ANDROID) with no descriptor:
// ExportNativeFence on the server. `*fence` is the sync_file it handed back (the
// caller's to close), or -1 when it had none - the fenced work has then completed.
virtual Bool CreateNativeFence(int* fence) {
*fence = -1;
return false;
}
virtual void Initialize() = 0;
virtual Bool InitCapabilities() = 0;
virtual Bool InitWindowSurface() = 0;
// A served session's apply thread - the one thread that runs this backend from here on -
// calls this first, in a process that may serve several sessions at once. A backend whose
// native state is process-global by construction gives this object its own copy and binds
// it to the calling thread. The default has nothing per process to separate.
virtual void BindSessionStateToThisThread() {}
// A served session's apply thread asks this each time it wakes, BEFORE it applies another
// record: true when this session's own GPU work faulted or hung the GPU and the backend has
// latched the session for it (MGPipeSessionLatch, family DeviceLost), so the next frame -
// which would very likely stop the GPU again - is never applied. Cheap by contract: no
// wait, at most a status read. The default knows of no fault.
virtual Bool LatchIfGpuFaulted() { return false; }
// A served session's ring moved to another of its client's contexts (bind_context). A
// backend whose surface follows the native context it binds needs nothing; one that keeps
// a single native surface binding re-binds the surface that context was last made current
// with. `token` is the client's context token (the EGLContext MakeEGLCurrent was given).
virtual void OnClientContextBound(Uint64 token) { (void)token; }
virtual Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor);
virtual Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
virtual Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height);
virtual Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
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;
@@ -629,18 +860,28 @@ namespace MobileGL {
struct EGLSurfaceState {
SurfaceKind Kind = SurfaceKind::None;
Bool DestroyPending = false;
// Activated at least once: a re-activation (a multi-window client switching back)
// is a re-bind and does not re-run the backend's capability probe.
Bool Activated = false;
WindowHandle Window;
EGLint Width = 1;
EGLint Height = 1;
};
void ResetEGLRuntimeState();
// ResetEGLRuntimeState for a released ACTIVE surface: the backend has no active surface
// any more, but every OTHER thread's current context and surface stay as they were (a
// multi-window client closing one popup must not unbind its other windows' threads).
void ForgetActiveEGLSurface();
Bool RegisterEGLWindowSurface(EGLSurface surface, const WindowHandle& handle);
Bool RegisterEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height);
const EGLSurfaceState* GetRegisteredEGLSurface(EGLSurface surface) const;
Bool ActivateEGLSurface(EGLSurface surface);
virtual Bool InitPbufferSurface(EGLint width, EGLint height);
virtual void OnEGLSurfaceReleased(EGLSurface surface);
// Every registered surface the base class drops, current or not, after it is gone from
// m_eglSurfaces. A backend that keeps a native surface per client surface frees it here.
virtual void OnEGLSurfaceForgotten(EGLSurface surface);
FormatCapabilityCache& MutableFormatCapabilities();
mutable std::recursive_mutex m_eglStateMutex;
@@ -648,6 +889,10 @@ namespace MobileGL {
WindowHandle m_windowHandle;
EGLDisplay m_eglDisplay = EGL_NO_DISPLAY;
EGLSurface m_eglSurface = EGL_NO_SURFACE;
// The client surface ActivateEGLSurface is bringing up while it calls InitWindowSurface /
// InitPbufferSurface, which take no surface argument: a backend that maps client surfaces
// to native ones reads it to re-bind an existing native surface instead of making another.
EGLSurface m_activatingSurface = EGL_NO_SURFACE;
Bool m_eglDisplayInitialized = false;
Bool m_eglSurfaceInitialized = false;
Bool m_backendCapabilitiesInitialized = false;
@@ -906,16 +906,69 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
}
Bool BackendObject_DirectGLES::RebindNativeSurfaceFor(EGLSurface surface) {
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return false;
if (DirectGLES::BindSessionSurface(it->second)) return true;
// The session no longer owns it (destroyed with a window loss): forget and recreate.
m_nativeSurfaces.erase(it);
return false;
}
void BackendObject_DirectGLES::RestoreSurfaceAfterCreation(EGLSurface previousNativeDraw) {
// The ACTIVE CONTEXT's own surface, exactly: the creation's bind attached the new surface to
// whichever context the session had bound (often another window's thread), and putting back
// "the surface activated last" instead re-attached THAT context to a third window's surface -
// a desktop drawing into and reading back from the panel's pbuffer.
if (previousNativeDraw == EGL_NO_SURFACE) return;
if (!DirectGLES::BindSessionSurface(previousNativeDraw)) return;
for (const auto& [client, native] : m_nativeSurfaces) {
if (native != previousNativeDraw) continue;
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) break;
m_eglSurface = client;
m_eglSurfaceInitialized = true;
m_eglSurfaceKind = state->second.Kind;
if (state->second.Kind == SurfaceKind::Window) m_windowHandle = state->second.Window;
break;
}
}
Bool BackendObject_DirectGLES::InitWindowSurface() {
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
// A suspended surface whose placeholder went: another placeholder - never the window, which
// is gone.
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), m_activatingSurface) !=
m_suspendedWindowSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::CreateSessionPbuffer(1, 1);
if (placeholder == EGL_NO_SURFACE || !DirectGLES::BindSessionSurface(placeholder)) {
MGLOG_E("DirectGLES: no placeholder for the suspended window surface %p",
reinterpret_cast<void*>(m_activatingSurface));
return false;
}
m_nativeSurfaces[m_activatingSurface] = placeholder;
return true;
}
// Only use EGL for now
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
if (!DirectGLES::InitWindowSurface(nativeWindow)) {
MGLOG_E("Failed to initialize window surface for DirectGLES backend");
return false;
}
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
}
Bool BackendObject_DirectGLES::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return LatchIfGuiltyBeforeApply();
#else
return false;
#endif
}
void BackendObject_DirectGLES::Initialize() {
MG_Util::BackendLoader::AcquireEGLFunctions(m_EGLFunctions);
MG_Util::BackendLoader::AcquireGLESFunctions(m_GLESFunctions, m_EGLFunctions.eglGetProcAddress);
@@ -976,8 +1029,19 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// ---- P14 S4: A SECOND SURFACE NO LONGER TEARS THE CONTEXT DOWN. ----
//
// This used to be `DestroyEGLContext(); ResetEGLRuntimeState();` whenever a surface was
// created while another was active, i.e. every pbuffer -> window -> pbuffer switch destroyed
// the native context - and with it every buffer, texture and program the application had
// built. The native context now belongs to the (session, context token) tuple the backend
// resolves, and creating a surface for it only adds a surface: the tuple keeps its context
// and its objects. ResetEGLRuntimeState still runs because the base class's per-surface
// bookkeeping (which surface is active, which threads made it current) does describe the
// surface that is being replaced.
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
@@ -986,7 +1050,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the first virtual tuple adopts that bind. On failure nothing is known to be bound. The
// pull arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLWindowSurface(surface, handle);
if (created) {
if (created && previous != EGL_NO_SURFACE && previous != surface) {
// Another surface was current: the creation must not leave the context that held it
// drawing to the new one (P14 multi-window clients).
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
@@ -1008,8 +1077,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
return true;
}
// P14 S4: as for the window surface above - the native context is the (session, context
// token) tuple's and survives a surface switch. This used to be DestroyEGLContext().
const EGLSurface previous = m_eglSurfaceInitialized ? m_eglSurface : EGL_NO_SURFACE;
const EGLSurface previousNativeDraw = DirectGLES::CurrentContextDrawSurface();
if (m_eglSurfaceInitialized) {
DestroyEGLContext();
ResetEGLRuntimeState();
}
@@ -1017,7 +1089,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ID-54 / ID-67: as for the window surface - InitPbufferSurface bound natively. The pull
// arm below is the original statement, byte for byte (G1).
const Bool created = BackendObject::CreateEGLPbufferSurface(surface, width, height);
if (created) {
if (created && previous != EGL_NO_SURFACE && previous != surface) {
RestoreSurfaceAfterCreation(previousNativeDraw);
NoteNativeContextGone();
} else if (created) {
NoteNativeContextFreshFromSurfaceCreation();
} else {
NoteNativeContextGone();
@@ -1028,8 +1103,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
#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) {
return DirectGLES::InitPbufferSurface(width, height);
if (RebindNativeSurfaceFor(m_activatingSurface)) return true;
if (!DirectGLES::InitPbufferSurface(width, height)) return false;
if (m_activatingSurface != EGL_NO_SURFACE) {
m_nativeSurfaces[m_activatingSurface] = DirectGLES::CurrentSessionDrawSurface();
}
return true;
}
Bool BackendObject_DirectGLES::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
@@ -1089,7 +1190,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
m_eglSurfaceInitialized && m_eglSurface == draw &&
DirectGLES::IsBackendContextCurrentOnThisThread() &&
NativeBindCanBeSkippedFor(dpy, draw, read, ctx);
if (!nativelyCurrentAlready && !DirectGLES::MakeCurrent()) {
// 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;
}
@@ -1110,6 +1220,41 @@ namespace MobileGL::MG_Backend::DirectGLES {
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);
}
@@ -1125,15 +1270,116 @@ namespace MobileGL::MG_Backend::DirectGLES {
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
m_suspendedWindowSurfaces.clear();
BackendObject::ReleaseEGLResources();
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
DestroyEGLContext();
Bool BackendObject_DirectGLES::SuspendServerWindow(void* window) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> onWindow;
for (const auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
onWindow.push_back(client);
}
for (const EGLSurface client : onWindow) {
// A surface never activated has no native surface: only its record names the window.
const auto native = m_nativeSurfaces.find(client);
if (native != m_nativeSurfaces.end()) {
const EGLSurface placeholder = DirectGLES::SuspendSessionWindowSurface(native->second);
if (placeholder == EGL_NO_SURFACE) return false;
native->second = placeholder;
}
m_suspendedWindowSurfaces.push_back(client);
}
#if MOBILEGL_BUILD_DISAGGREGATED
// The native binding moved onto the placeholder: the next make-current binds for real.
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu window surface(s) on server window %p now draw into placeholder pbuffers; the "
"contexts and their objects stay",
onWindow.size(), window);
return true;
}
Bool BackendObject_DirectGLES::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Vector<EGLSurface> suspended;
suspended.swap(m_suspendedWindowSurfaces);
for (const EGLSurface client : suspended) {
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) continue; // released while suspended
state->second.Window.Handle = window;
state->second.Window.Width = width;
state->second.Window.Height = height;
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state->second.Window;
// Never activated: its first activation creates it on the new window.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
const EGLSurface surface =
DirectGLES::ResumeSessionWindowSurface(native->second, reinterpret_cast<NativeWindowType>(window));
if (surface == EGL_NO_SURFACE) return false;
native->second = surface;
}
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
MGLOG_I("DirectGLES: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
return true;
}
Bool BackendObject_DirectGLES::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr) return false;
Bool published = false;
for (auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (std::find(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), client) !=
m_suspendedWindowSurfaces.end())
continue;
state.Window.Width = width;
state.Window.Height = height;
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state.Window;
// The window surface's buffers follow the window by themselves (from the next one the
// driver dequeues); only the client has to be told.
const auto native = m_nativeSurfaces.find(client);
if (native == m_nativeSurfaces.end()) continue;
published = DirectGLES::RepublishWindowSurfaceShape(native->second,
reinterpret_cast<NativeWindowType>(window)) ||
published;
}
return published;
}
void BackendObject_DirectGLES::OnEGLSurfaceReleased(EGLSurface surface) {
// P14: releasing a surface releases THAT SURFACE (OnEGLSurfaceForgotten), never the
// contexts. This used to be DestroyEGLContext() - every native context of the session, with
// every object the client had built - which a multi-window client hit on its first closed
// popup.
(void)surface;
#if MOBILEGL_BUILD_DISAGGREGATED
NoteNativeContextGone();
#endif
}
void BackendObject_DirectGLES::OnEGLSurfaceForgotten(EGLSurface surface) {
m_suspendedWindowSurfaces.erase(
std::remove(m_suspendedWindowSurfaces.begin(), m_suspendedWindowSurfaces.end(), surface),
m_suspendedWindowSurfaces.end());
const auto it = m_nativeSurfaces.find(surface);
if (it == m_nativeSurfaces.end()) return;
const EGLSurface native = it->second;
m_nativeSurfaces.erase(it);
DirectGLES::DestroySessionSurface(native);
}
const RendererInfo& BackendObject_DirectGLES::GetRendererInfo() const {
@@ -1165,6 +1411,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
// trivially true for every context this backend claims (4.6). KWin 6's
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
// strings, so withholding them fails its renderer bring-up on an otherwise capable
// implementation.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
E_GL_ARB_texture_non_power_of_two,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_EXT_framebuffer_object,
E_GL_ARB_depth_texture, E_GL_ARB_buffer_storage, E_GL_ARB_texture_storage,
@@ -32,16 +32,38 @@ namespace MobileGL::MG_Backend::DirectGLES {
public:
~BackendObject_DirectGLES() override;
// The driver's verdict on this session's context (DirectGLES::LatchIfDeviceLost).
Bool LatchIfGpuFaulted() override;
void Initialize() override;
Bool InitCapabilities() override;
Bool InitWindowSurface() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
Bool CreateEGLWindowSurface(EGLSurface surface, const WindowHandle& handle) override;
Bool CreateEGLPbufferSurface(EGLSurface surface, EGLint width, EGLint height) override;
Bool ResizeEGLWindowSurface(EGLSurface surface, Uint32 width, Uint32 height) override;
Bool MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) override;
Bool SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) override;
// Shared images: a GL blit of default framebuffer 0 into a renderbuffer whose storage is
// the image's EGLImage, ordered after the image's pending reads and published as a native
// fence without a CPU wait (DirectGLES.h, SharedImageImpl).
Bool BlitDefaultFramebufferToSharedImage(const SharedImageView& image,
const MG_Util::Damage::Region& region) override;
// A window surface's age is the driver's (EGL_EXT_buffer_age); a pbuffer keeps its content,
// so it is 1 once it has been presented (DirectGLES::CurrentDrawBufferAge).
Int32 QueryCurrentBufferAge(Bool damageRegionFollows) override;
// eglSetDamageRegionKHR on a window surface whose driver has EGL_KHR_partial_update.
Bool SetCurrentDamageRegion(const MG_Util::Damage::Region& region) override;
// A glFlush that ends a shared-image producer's frame (SharedImageImpl::PublishPendingAccesses).
Bool PublishSharedImageAccesses() override;
// An EGL native fence of the session's work (SharedImageImpl::ExportNativeFence).
Bool ExportNativeFence(int* fence) override;
void ReleaseEGLSurface(EGLSurface surface) override;
void ReleaseEGLResources() override;
// The server window going and coming back: each window surface on it swaps its native EGL
// window surface for a placeholder pbuffer and back (DirectGLES::Suspend/ResumeSessionWindowSurface).
Bool SuspendServerWindow(void* window) override;
Bool ResumeServerWindow(void* window, Uint32 width, Uint32 height) override;
Bool ServerWindowResized(void* window, Uint32 width, Uint32 height) override;
const RendererInfo& GetRendererInfo() const override;
String GetBackendAPIVersionString() const override;
@@ -57,8 +79,22 @@ 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;
File diff suppressed because it is too large Load Diff
+335 -5
View File
@@ -23,15 +23,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Content uploads use scratch bindings, so draws and dispatches call this after
// texture synchronization.
void BindCurrentTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
Bool ReadTextureImageWire(const MG_Pipe::MGPReadbackInfo& info, Vector<Uint8>& bytes);
// Native/server-only color level readback. Returns tightly packed, owned
// 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 resolves identity/extent from its resource record and supplies
// whether the SOURCE allocation already uses an image carrier.
// 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);
@@ -105,11 +111,72 @@ namespace MobileGL::MG_Backend::DirectGLES {
void ShaderStorageBlockBinding(GLuint program, const GLchar* storageBlockName, GLuint storageBlockBinding);
Bool InitWindowSurface(NativeWindowType window);
Bool InitPbufferSurface(EGLint width, EGLint height);
// The calling thread's session's current native draw surface (the one the last Init*Surface
// created, or the last BindSessionSurface named).
EGLSurface CurrentSessionDrawSurface();
// The surface the calling thread's ACTIVE context last drew to (EGL_NO_SURFACE for none).
EGLSurface CurrentContextDrawSurface();
// EGL_BUFFER_AGE_EXT of the session's current draw surface for the next frame: the driver's
// answer for a window surface, 1 for a pbuffer that has been presented (a pbuffer's content stays
// across swaps), 0 otherwise. A driver whose buffer age is only EGL_KHR_partial_update's keeps
// nothing inside the frame's damage region - the whole surface unless one is set - so it is asked
// only when `damageRegionFollows` (SetCurrentDrawDamageRegion before the first draw).
Int32 CurrentDrawBufferAge(Bool damageRegionFollows);
// eglSetDamageRegionKHR(region) on the session's current window surface (a pbuffer, or a driver
// without EGL_KHR_partial_update, ignores it). The driver wants the age asked first in the frame;
// when the client was answered without asking it, it is asked here.
Bool SetCurrentDrawDamageRegion(const MG_Util::Damage::Region& region);
// Makes an EXISTING native surface of the calling thread's session the one it draws to, and
// binds it to the active context. False for a surface the session does not own.
Bool BindSessionSurface(EGLSurface surface);
// The same selection without the bind (the caller binds - MakeCurrent - unconditionally).
Bool SelectSessionSurface(EGLSurface surface);
// Destroys one native surface of the calling thread's session - and only it; the session's
// contexts and other surfaces are untouched.
void DestroySessionSurface(EGLSurface surface);
// A new pbuffer of the calling thread's session, created with the active context's config and
// bound to nothing (EGL: creating a surface changes no binding).
EGLSurface CreateSessionPbuffer(EGLint width, EGLint height);
// Every context of the session that drew to `previous` draws to `replacement` instead, and
// `previous` is destroyed (a pbuffer-backed window's resize).
void ReplaceSessionSurface(EGLSurface previous, EGLSurface replacement);
// The server window going away (screen off): every context of the session that drew to the
// window surface `windowSurface` draws to a new 1x1 placeholder pbuffer instead, and the window
// surface is destroyed - no longer current anywhere once this returns. The contexts and their
// objects stay. Returns the placeholder, or EGL_NO_SURFACE (nothing changed) when none could be
// made.
EGLSurface SuspendSessionWindowSurface(EGLSurface windowSurface);
// The way back: a window surface on `window` replaces `placeholder` (EGL_NO_SURFACE: none to
// replace) for every context that drew to it, is bound, and its extent is published as a
// creation publishes it. Returns the new surface, or EGL_NO_SURFACE.
EGLSurface ResumeSessionWindowSurface(EGLSurface placeholder, NativeWindowType window);
// The window under `surface` changed size: its extent (the display's) is published again, at once.
Bool RepublishWindowSurfaceShape(EGLSurface surface, NativeWindowType window);
Bool MakeCurrent();
Bool ReleaseCurrent();
// True when the backend ES context is current on the calling thread, i.e.
// immediate buffer ops may issue GL calls right now.
Bool IsBackendContextCurrentOnThisThread();
#if MOBILEGL_BUILD_DISAGGREGATED
// DEVICE LOSS, Espryt's half (Magma's is VulkanRenderer::LatchWireDeviceLoss). Asks the
// driver whether the calling thread's context was reset (glGetGraphicsResetStatus); a reset -
// or the debug knob MGPipeDebugDeviceLossDue - latches THIS session through
// MGPipeSessionLatch and answers true, and the caller returns from its verb without issuing
// more GL. Asked at the frame boundaries (Present, a shared-image present), before a readback,
// and by the apply loop each time it wakes (LatchIfGpuFaulted), never per draw. The contexts ask
// for reset notification (NativeContextsNotifyResets) so the driver can name the GUILTY one;
// only GUILTY (or a reset of unknown cause) ends the session - an INNOCENT report, a context
// whose work merely waited behind another's hang (the compositor's among them), is only noted.
// `debugKnob` false: the knob is not consulted here (the on-screen Present, so an injected loss
// lands on an offscreen client's shared-image present or readback, never on the compositor's).
Bool LatchIfDeviceLost(const char* site, Bool debugKnob = true);
// The apply loop's form of it (BackendObject::LatchIfGpuFaulted), asked each time the apply thread
// wakes: the driver's report only, no debug knob, and no EGL ground-truth verification spent.
Bool LatchIfGuiltyBeforeApply();
#endif
// True once the process's native contexts were created with EGL_LOSE_CONTEXT_ON_RESET (decided
// by the first one; the driver must offer EGL_EXT_create_context_robustness).
Bool NativeContextsNotifyResets();
// GL fence sync objects, backed by native ES fences. FenceSync returns null
// (the frontend then falls back to an always-signaled sync) when the calling
// thread does not own the ES context. Waits/queries degrade to "signaled" in
@@ -198,6 +265,200 @@ namespace MobileGL::MG_Backend::DirectGLES {
void SetEGLFuncsTable(const MG_External::EGLFunctionsTable& eglFuncs);
void SetGLESFuncsTable(const MG_External::GLESFunctionsTable& glesFuncs);
void SetGLESCapabilities(const MG_External::GLESCapabilities& capabilities);
// ---------------------------------------------------------------------------------
// P14 S4 (docs/Disaggregated/design/11-state-ownership.md): THE NATIVE TUPLE IS PER CONTEXT
// ---------------------------------------------------------------------------------
//
// The display is process state and stays one object (eglInitialize once per process, terminated
// when the last native context leaves). The native EGLContext, the config it was created with
// and the native surfaces are keyed by (session, client context token) - the identity S1 put on
// the wire and S2 gave a per-thread owner. Creating a second context therefore no longer tears
// the first one's native context down, which is what lets two contexts (and two share groups)
// be live at once in one process.
// Answers, for the CALLING THREAD, which session it belongs to and which client context token
// that session has bound. Installed once by the server layer; with no resolver installed -
// monolith, a frontend-only process, a unit case - the answer is the single key {0, 0}, which is
// exactly the process singleton this replaces. `false` also means {0,0}.
using NativeContextKeyResolver = Bool (*)(Uint64* outSessionKey, Uint64* outContextToken);
void SetNativeContextKeyResolver(NativeContextKeyResolver resolver);
#if MOBILEGL_PIPE_PUSH
// ---------------------------------------------------------------------------------
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLES' KEY, AND ITS
// SHAPE IS S4's, ONE LEVEL DOWN. A twin is a DRIVER object, so it lives in one native
// context's object namespace; the identity a twin table must be keyed on is therefore
// {session, share group} - the SAME two words the applier's OBJECT RECORDS are filed under
// (PipeApply.h's MGPipeApplierKey, derived by the same probe and the same `0 means its own
// group` rule, so the two agree by construction rather than by two conventions).
//
// Answers, for the CALLING THREAD, which session it belongs to and which share group that
// session's current context is in. Installed once by the server layer beside the native and
// applier probes; with no probe installed - or on a thread that is no session's own, which
// is the client process, the in-process client+server shape and every unit case - the
// answer is the single key {0, 0}, exactly the process-wide table this replaces.
struct TwinKey {
Uint64 SessionKey = 0;
Uint64 ShareGroupKey = 0;
Bool operator==(const TwinKey& other) const {
return SessionKey == other.SessionKey && ShareGroupKey == other.ShareGroupKey;
}
Bool operator!=(const TwinKey& other) const { return !(*this == other); }
};
struct TwinKeyHash {
SizeT operator()(const TwinKey& key) const {
// The 64-bit mix SlotAllocator and the applier's registry already use for their own
// pair keys, so every keyed table in this tree hashes a session the same way.
Uint64 mixed = key.SessionKey ^ (key.ShareGroupKey + 0x9E3779B97F4A7C15ull +
(key.SessionKey << 6) + (key.SessionKey >> 2));
mixed ^= mixed >> 33;
mixed *= 0xFF51AFD7ED558CCDull;
mixed ^= mixed >> 33;
return static_cast<SizeT>(mixed);
}
};
using TwinKeyResolver = Bool (*)(TwinKey* outKey);
void SetTwinKeyResolver(TwinKeyResolver resolver);
// The calling thread's key. Answers false for {0, 0} - the process-wide group - which is
// also the answer when no probe has ever been installed.
Bool CurrentTwinKey(TwinKey* outKey);
#endif
// Builds the native context for `contextToken` under the calling thread's session. A
// `shareGroupToken` whose group already has a live native context in this session hands that
// context to eglCreateContext as the share argument - which is what makes two contexts see one
// set of buffer/texture objects. Idempotent: a token that already has one answers true.
Bool CreateNativeContextFor(Uint64 contextToken, Uint64 shareGroupToken);
// Destroys ONE context's native tuple (the DestroyContext control frame). The session's other
// contexts are untouched; the display is terminated only when the last context in the process
// has gone.
void DestroyNativeContextFor(Uint64 contextToken);
// ---- objects that belong to ONE native context ----
//
// Framebuffers, vertex arrays, transform feedbacks and queries are container objects: GL never
// shares them, not even between contexts of one share group, and every context numbers its own
// from 1. A process-wide scratch FBO id minted in one context and bound in another therefore
// names THAT context's own framebuffer - often an application's - and the scratch path's
// attach/scrub then rewires it onto staging textures. With one process serving several
// sessions (each with its own native contexts) that silently corrupted other clients' frames.
//
// The calling thread's current native context as a lifetime serial: assigned when the context
// is created and never reused (a recycled EGLContext address is a new serial). 0 when none was
// made current through MakeCurrent.
Uint64 CurrentNativeContextSerial();
class PerNativeContextBase {
public:
PerNativeContextBase();
PerNativeContextBase(const PerNativeContextBase&) = delete;
PerNativeContextBase& operator=(const PerNativeContextBase&) = delete;
// The context died, and its names with it: forget them (no GL call - there is no context
// left to make one in).
virtual void ForgetContext(Uint64 serial) = 0;
protected:
~PerNativeContextBase();
};
// Called where a native context is destroyed.
void ForgetNativeContextObjects(Uint64 serial);
// Moves whenever any per-context entry is dropped; a thread's cached entry is valid while
// it has not moved.
Uint64 PerNativeContextEpoch();
void BumpPerNativeContextEpoch();
// One T per native context, created on first use from that context.
template <typename T>
class PerNativeContext final : public PerNativeContextBase {
public:
T& Current() {
const Uint64 serial = CurrentNativeContextSerial();
// The draw path asks for the same entry over and over from one thread: a one-entry
// thread-local cache, invalidated by any drop anywhere (rare).
thread_local CurrentCache cache;
const Uint64 epoch = PerNativeContextEpoch();
if (cache.Owner == this && cache.Serial == serial && cache.Epoch == epoch) return *cache.Entry;
const std::lock_guard<std::mutex> lock(m_mutex);
T& entry = m_bySerial[serial]; // node-based: the reference survives other contexts' inserts
cache = {this, serial, epoch, &entry};
return entry;
}
template <typename F>
void ForEach(F&& visit) {
const std::lock_guard<std::mutex> lock(m_mutex);
for (auto& entry : m_bySerial) visit(entry.second);
}
void Clear() {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.clear();
}
void ForgetContext(Uint64 serial) override {
const std::lock_guard<std::mutex> lock(m_mutex);
BumpPerNativeContextEpoch();
m_bySerial.erase(serial);
}
private:
struct CurrentCache {
const PerNativeContext* Owner = nullptr;
Uint64 Serial = 0;
Uint64 Epoch = 0;
T* Entry = nullptr;
};
std::mutex m_mutex;
std::unordered_map<Uint64, T> m_bySerial;
};
// The GL name of one per-context object, spelled like the plain `GLuint` it replaces: it reads
// as the CURRENT context's name, `= 0` resets that one, and `&name` is where glGen* writes it.
class NativeContextName {
public:
operator GLuint() const { return m_names.Current(); }
NativeContextName& operator=(GLuint name) {
m_names.Current() = name;
return *this;
}
GLuint* operator&() { return &m_names.Current(); }
private:
mutable PerNativeContext<GLuint> m_names;
};
// P14 S4. THE NATIVE HALF OF A CONTEXT SWITCH. `bind_context` moves which context the session's
// records belong to; this makes the calling thread's native tuple follow it, binding the
// session's current surface to the newly named context's native EGLContext. A no-op when the
// named context has no native context yet (the session never sent CreateContext) or when the
// native context already current on this thread IS that one - so the single-context world pays
// nothing for it. Called from the bind_context path on the apply thread.
Bool MakeNativeContextCurrentForBoundToken();
#if MOBILEGL_BUILD_DISAGGREGATED
// P14: the server's backend turn moved to the calling apply thread (ServerLoop.cpp's
// BackendTurnLock). Takes the backend's GL ownership for this thread and drops every
// binding shadow, which still describes the previous holder's native context.
void OnBackendTurnHandoff();
#endif
// How many native contexts this process holds, and how many of them belong to one session. The
// test observables for "a second context did not tear the first one down".
Uint64 NativeContextCount();
Uint64 NativeContextCountForSession(Uint64 sessionKey);
// The calling thread's session, as the resolver above reports it (0 with no resolver).
Uint64 CurrentNativeSessionKey();
// ---- Test observables (P14 S4). EGL exposes no "which EGLContext does this client context
// hold" query and no way to ask a context for its share group, so a case that must tell "the
// same native context still serves" from "a fresh one was built", or a sharing pair from two
// unrelated contexts, reads it here. These are pure reads plus one key override; neither is on
// a production path and neither changes behaviour.
EGLContext NativeContextHandleFor(Uint64 sessionKey, Uint64 contextToken);
// Overrides the key the calling thread resolves to, for the life of the call. The backend's
// ordinary answer comes from the server layer's probe; this is how a case drives exactly the
// tuple it means when no session runtime is installed.
void ForceNativeContextKeyForTesting(Uint64 sessionKey, Uint64 contextToken);
void DestroyEGLContext();
// Transform feedback capture spans, performed by the real ES driver. The
@@ -241,8 +502,77 @@ namespace MobileGL::MG_Backend::DirectGLES {
// answers from the frontend state, which this function only reads.
void SyncRenderState(Bool forColorClear = false);
void InvalidateSyncedRenderState();
// GL_SCISSOR_TEST as the driver has it in the native context current on this thread -
// what a guard that turns the test off around its own blit, and puts it back, needs to
// know. The shadow answers only while it describes this context (synced since the last
// invalidation, in this context epoch); otherwise the driver is asked. The shadow is one
// per process: once another session's context synced into it, or a context switch
// invalidated it, its bits are some other context's.
Bool DriverScissorTestEnabled();
// TESTING: the shadow as SyncRenderState leaves it in the current context epoch, with
// GL_SCISSOR_TEST `enabled`.
void SetSyncedScissorTestForTesting(Bool enabled);
// TESTING: `inside` runs within the guard the shared-image present and the readbacks
// copy under (ScopedScissorDisable).
void RunScissorDisabledForTesting(void (*inside)(void*), void* data);
} // namespace RenderStateImpl
#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
+242 -19
View File
@@ -74,6 +74,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// flag while the process is very much alive.
Bool InProcessTeardown();
void EnsureProcessTeardownSentinel();
// The exit() half of InProcessTeardown alone, for an object whose release needs only the
// display, not a current context (an EGLImage): a server session's twin teardown has no
// context current but a live display, and skipping the release there would leak the image.
Bool InProcessExit();
// Generation of the backend ES context that owns the driver ids currently handed
// out. Bumped exactly once per DestroyEGLContext. Every backend twin that owns a
@@ -332,6 +336,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
using const_iterator = typename BackendMap::const_iterator;
#if MOBILEGL_PIPE_PUSH
using SlotTable = BackendSlotTable<StateObject, BackendObject, kKind>;
// P14 S6: the ONE bucket of this kind the calling thread is in. Every handle-keyed entry
// below resolves through it, so the six twin registries answer per {session, share
// group} without a single call site naming a bucket.
using SlotTables = SlotTableRegistry<StateObject, BackendObject, kKind>;
using TwinKey = DirectGLES::TwinKey;
#endif
BackendPtr& GetOrCreate(const StatePtr& stateObj) {
@@ -348,7 +357,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// insertion (D13; SlotTables.h) - so a table used outside a registry arms
// it too, which is right: it is the twin, not the registry, that owns the
// driver id a guarded destructor exists for.
return m_slotTable.GetOrCreate(stateObj);
return m_slotTables.ForCallingThread().GetOrCreate(stateObj);
}
#endif
// Twin creation is the moment a driver-owned id starts needing a guarded
@@ -407,7 +416,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
#endif
#if MOBILEGL_PIPE_PUSH
if (EsprytSlotTablesEnabled()) {
return m_slotTable.Find(stateObj);
return m_slotTables.ForCallingThread().Find(stateObj);
}
#endif
const auto entryIt = m_entries.find(stateObj);
@@ -445,7 +454,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// backend memo stores instead of a raw pointer, a GL name or a bare lifetime id.
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
if (EsprytSlotTablesEnabled()) {
return m_slotTable.HandleOf(stateObj);
return m_slotTables.HandleOf(stateObj);
}
return MG_Pipe::kMGPipeNullHandle;
}
@@ -454,7 +463,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the lookup a backend memo that already holds a handle wants: no lifetime-id probe.
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
if (EsprytSlotTablesEnabled()) {
return m_slotTable.FindByHandle(handle);
return m_slotTables.FindByHandle(handle);
}
return nullptr;
}
@@ -490,7 +499,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return nullptr;
#endif
}
const Uint32 liveGen = m_slotTable.LiveGenAt(handle.Slot);
const Uint32 liveGen = m_slotTables.LiveGenAt(handle.Slot);
if (liveGen != 0 && liveGen > handle.Gen) {
#if MOBILEGL_BUILD_DISAGGREGATED
MG_Pipe::MGPipeSessionFail( // @Ph-declined (ID-P7-1): PH-2 stays Fatal, CONTRACT-P7 §12
@@ -502,7 +511,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
return nullptr;
#endif
}
return &m_slotTable.GetOrCreate(handle);
return &m_slotTables.GetOrCreate(handle);
}
// The generation of the LIVE entry at this slot, or 0. It exists so a caller can
@@ -510,7 +519,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// performs silently.
Uint32 LiveGenAt(Uint32 slot) const {
if (!EsprytSlotTablesEnabled()) return 0;
return m_slotTable.LiveGenAt(slot);
return m_slotTables.LiveGenAt(slot);
}
// P5c (hd): the two halves of SlotTables.h's state note, forwarded. A caller holding
@@ -524,12 +533,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
// which is the answer the legacy walk's own weak_ptr test already gives.
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
if (EsprytSlotTablesEnabled()) {
m_slotTable.NoteStateForHandle(handle, stateObj);
m_slotTables.NoteStateForHandle(handle, stateObj);
}
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
if (EsprytSlotTablesEnabled()) {
return m_slotTable.StateForHandle(handle);
return m_slotTables.StateForHandle(handle);
}
return nullptr;
}
@@ -591,9 +600,52 @@ namespace MobileGL::MG_Backend::DirectGLES {
template <typename Fn>
void ForEachLive(Fn&& fn) const {
if (EsprytSlotTablesEnabled()) {
m_slotTable.ForEachLive(fn);
m_slotTables.ForEachLive(fn);
}
}
// ---- P14 S6: the KEYED shape's own controls ----------------------------------------
//
// Everything above is the shape a CALL SITE sees, and it is deliberately the shape it
// saw before the rekey. These four are the shape only a caller that has to reason about
// buckets needs: the teardown of a session, a test asking whether two contexts landed in
// two namespaces, and a diagnostic.
//
// THE ONE BUCKET THIS THREAD RESOLVES TO. A by-value copy of it is exactly what a
// fixture's saved registry was, and it is what makes `= {}` and "put it back" both mean
// something under a key.
SlotTable& BucketForCallingThread() {
if (EsprytSlotTablesEnabled()) return m_slotTables.ForCallingThread();
// The legacy arm has no buckets; the empty table's Reset() is the only honest
// answer, and no shipping path asks (the arm is compiled out or the bit is clear).
return m_legacyEmptyTable;
}
Vector<SharedPtr<SlotTable>> BucketSnapshot() const {
if (EsprytSlotTablesEnabled()) return m_slotTables.Snapshot();
return {};
}
SizeT BucketCount() const {
return EsprytSlotTablesEnabled() ? m_slotTables.BucketCount() : 0;
}
// Drop one {session, share group}. Answers whether such a bucket existed.
Bool DropBucket(const TwinKey& key) {
return EsprytSlotTablesEnabled() && m_slotTables.Drop(key);
}
// Drop every bucket of one session - the difference between "this session's twins go"
// and "the process's do" - and answer how many groups that took with it.
Uint32 DropBucketsOfSession(Uint64 sessionKey) {
return EsprytSlotTablesEnabled() ? m_slotTables.DropSession(sessionKey) : 0;
}
// Empty every bucket. The teardown of a process-shaped world, where the key is {0, 0}.
void ResetBuckets() {
if (EsprytSlotTablesEnabled()) m_slotTables.Reset();
}
// Every bucket, walked with its key.
template <typename Fn>
void ForEachBucket(Fn&& fn) const {
if (EsprytSlotTablesEnabled()) m_slotTables.ForEachBucket(fn);
}
#endif
// The seven DirectGLES.cpp call sites drive the LEGACY arm and nothing else. On the
@@ -664,7 +716,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
Uint32 m_creationTick = 0;
Bool m_isCollecting = false;
#if MOBILEGL_PIPE_PUSH
BackendSlotTable<StateObject, BackendObject, kKind> m_slotTable;
// P14 S6: the ONE bucket per {session, share group}, hidden behind the same member names
// the single table had, so every handle-keyed call site above and every caller below is
// unchanged. See SlotTables.h's SlotTableRegistry for the resolution chain.
SlotTables m_slotTables;
// The legacy arm's stand-in for BucketForCallingThread(). Never read after a Reset and
// never populated: the two entries that could write it are the handle-keyed ones, which
// return early on that arm.
SlotTable m_legacyEmptyTable;
#endif
};
@@ -1004,6 +1063,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// (the UBO binding walk) skip the probe while their pre-pass epoch read
// matches, exactly like the per-VAO memo stamps.
Uint64 drawCleanEpoch = 0;
// Frame serial whose completion retires the last glCopyBufferSubData the upload
// ring staged INTO this store (the flush ladder's ring tier: FlushPendingRangesNow
// on the pull arm, FlushPendingRangesFrom on the push arm); 0 = none. Until it
// retires, both keep every partial range off the range-invalidating map: on
// Adreno 830 that map, on a store with a queued copy, loses the copy (see
// FlushPendingRangesNow and InvalidateFlushAccessFor). Draw thread only, like the
// flush.
Uint64 ringCopyRetireSerial = 0;
// Zero-copy coherent persistent map (EXT_buffer_storage): the GL store is
// immutable, persistently+coherently mapped, and persistentPtr is what the app
// (and the frontend PipeResource) write into directly. While set, draw-time
@@ -1042,6 +1109,21 @@ namespace MobileGL::MG_Backend::DirectGLES {
// the legacy arm did; this member exists for the drains that have no object, which
// in P3a is the readback flush and the fp64 narrowing.
const Uint8* hostBytes = nullptr;
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): the client's AHardwareBuffer this store IS - imported with
// glBufferStorageExternalEXT, so `id` is immutable, persistentMapped and
// persistentPtr is this server's own coherent map of the client's pages. The
// reference is this twin's own, released only after a fence that follows the
// store's last use has signalled (RetireT0Import). Survives an ES context loss:
// the next ensure re-imports it. Null for every store T0 did not adopt.
void* externalAhb = nullptr;
// P8-C: A SHADER WROTE THIS STORE ON THIS SIDE since the R-11 staged copy (or, for
// T0, the coherent map) was last known to match it - set by the draw / dispatch that
// bound it writable (storage block, atomic counter, writable buffer image), cleared
// by SplitHostBytesForCpuRead's refresh and by a respecify. Only the server's own
// CPU readers consult it; the client keeps its own set (GpuWritePending.h).
Bool serverGpuWritten = false;
#endif
};
@@ -1058,8 +1140,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// HandleOf, no ForEachLive), and the handle overloads never look at it. Death is
// announced by the family's own ResourceDestroy call, not by the shared death notice
// (D-L), and the slot is freed by the CLIENT after that call returns.
//
// P14 S6: A BUCKET REGISTRY, like the six TwinRegistry tables. A buffer twin owns a
// driver buffer id, so the SAME {slot, gen} in two contexts that do not share a group
// must be two resources - and it was one process-wide table until this slice. The
// handle-keyed member-for-member forwarding in SlotTables.h is what leaves every
// `g_backendBufferResources.X(...)` call site below untouched.
using BackendBufferResourceTable =
BackendSlotTable<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
SlotTableRegistry<MG_State::GLState::BufferObject, GLESBufferResource, MG_Pipe::MGPipeKind::Buffer>;
extern BackendBufferResourceTable g_backendBufferResources;
// Resolved once per process and latched, exactly like EsprytSlotTablesEnabled() and
@@ -1104,6 +1192,34 @@ namespace MobileGL::MG_Backend::DirectGLES {
void RequireStagedCoverage(GLESBufferResource& resource, const Uint8* hostBase, SizeT start,
SizeT end, const char* site);
// P11 B2: THE CPU-READABLE BYTES OF A SPLIT SERVER'S BUFFER TWIN, for the handle-arm
// readers that have no frontend object (indirect commands, primitive-restart
// substitution, the multi-draw index rebase): the R-11 staged copy, or - for a T0 store,
// which has none - this server's own coherent map of the client's AHardwareBuffer, read
// exactly as the monolith arm reads the adopted map through MappedData().
inline const Uint8* SplitHostBytes(const GLESBufferResource& resource) {
if (resource.hostBytes != nullptr) return resource.hostBytes;
return resource.externalAhb != nullptr ? static_cast<const Uint8*>(resource.persistentPtr) : nullptr;
}
// P8-C: THE SAME BYTES, MADE CURRENT FIRST - for a reader that CONSUMES them (the restart
// rewrite, the multi-draw rebase, the *IndirectCount count, an indirect command's
// CPU-fed fields, the XFB scatter's pre-capture bytes). SplitHostBytes above is only
// "what uploads staged": a store a shader wrote on this side (serverGpuWritten) is
// refreshed from this server's own GL buffer - queued uploads flushed into it first,
// then the whole store read back into the staged copy, which also gives an orphaned
// store its first copy - and a T0 store lands its queued resident writes and finishes
// before its coherent map is read. A clean staged store costs one flag test.
// `res` names the record whose descriptor width the copy takes; `site` names the
// reader in the one-time diagnostic when the refresh cannot run.
const Uint8* SplitHostBytesForCpuRead(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res,
const char* site);
// P8-C: bytes the SERVER itself put into a store's GL buffer and already holds on the CPU
// (an XFB capture it mapped for the client's writeback, a scatter it composed) go into
// the staged copy as well, so no refresh is owed for them. A T0 store has no copy.
void StageServerWrittenRange(GLESBufferResource& resource, MG_Pipe::MGPipeHandle res, const void* bytes,
SizeT offset, SizeT size);
// M-3's WHOLE-STORE predicate, and the GUARD every caller of RequireStagedCoverage over
// a whole-store read has to carry: true when the application DECLARED the store's
// content (glBufferData(size, data)), so a coverage gap is a missing record; false when
@@ -1143,10 +1259,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ("do not take tier 1"), which drops the range onto the staging ring and costs a copy
// rather than a corruption.
//
// `ringCopyInFlight`: the store still has an upload-ring glCopyBufferSubData queued that the
// GPU may not have run (GLESBufferResource::ringCopyRetireSerial not yet retired).
//
// Returns the glMapBufferRange access bits, or 0 when tier 1 must not be taken.
inline constexpr SizeT kEsprytInvalidateRangeMinBytes = 128u * 1024u;
constexpr GLbitfield InvalidateFlushAccessFor(SizeT queuedStart, SizeT queuedEnd, SizeT mapStart,
SizeT mapEnd, SizeT limit, SizeT storageSize) {
SizeT mapEnd, SizeT limit, SizeT storageSize,
Bool ringCopyInFlight) {
if (mapEnd <= mapStart) return 0u;
// THE WIDENING REFUSAL. Not >=, not "covers": exactly, in both directions. A map
// narrower than the queued range leaves bytes unwritten inside a range it has just
@@ -1158,6 +1278,13 @@ namespace MobileGL::MG_Backend::DirectGLES {
// page-substitution fast path needs a page-coverable range to engage, and below it
// the driver falls back to waiting out the WAR hazard on the CPU.
if (!wholeBuffer && size < kEsprytInvalidateRangeMinBytes) return 0u;
// A partial range-invalidating map must not overtake a ring copy into this store the GPU
// has not run yet. Adreno 830 answers such a map on a store with a copy still queued by
// losing the copy (dev f973008c, the pull arm's FlushPendingRangesNow: create-instancing
// drew whole Embeddium terrain sections empty, SSIM 0.870). The range goes to the ring
// instead, GPU-ordered behind the copies. The whole-buffer orphan-map is exempt: it
// rewrites every byte from the shadow the queued copies were taken from.
if (!wholeBuffer && ringCopyInFlight) return 0u;
return GL_MAP_WRITE_BIT |
(wholeBuffer ? GL_MAP_INVALIDATE_BUFFER_BIT : GL_MAP_INVALIDATE_RANGE_BIT);
}
@@ -1243,6 +1370,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
void UnregisterBufferBackendOps();
// The ES context died: unregister ops, invalidate all outstanding GL ids
// (they belonged to the dead context) and drop deferred deletes.
// P14: a server session ended while another session's native context is still alive (one
// process-wide share group): drops only the ending session's own staged bytes.
void OnServerSessionEndedWithOthersLive();
void OnBackendContextDestroyed();
// Get-or-create the backend resource and bring its storage up to date
@@ -1267,6 +1397,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Deletes GL buffers whose owning frontend objects died (possibly on a
// thread without a current ES context). Called from draw-time sync.
void ProcessDeferredBufferReleases();
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2: the T0 half of that drain - run right after it (retired AHardwareBuffers).
void ProcessDeferredT0Retires();
#endif
// glBindBuffer with a redundant-bind cache for GL_ARRAY_BUFFER.
void BindBufferId(GLenum target, Uint id);
@@ -1810,6 +1944,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
switch (target) {
case TextureTarget::Texture1D:
case TextureTarget::TextureRectangle:
case TextureTarget::External:
return TextureTarget::Texture2D;
case TextureTarget::Texture1DArray:
return TextureTarget::Texture2DArray;
@@ -1963,15 +2098,33 @@ namespace MobileGL::MG_Backend::DirectGLES {
// allocation, or capture the old native contents, merge pending
// upload regions and re-arm the server's whole-level replay set.
// First allocations use staged bytes directly. No frontend dirty
// flag, allocator or pixel-store object participates.
// flag, allocator or pixel-store object participates. P9 W2: a level
// the driver does not hold (defined after the last sync) is never read
// back, and a readback the driver refuses falls back to the staged
// level when the store covers it (notes/p9/W2-REMINT.md).
void RequireImageBindableStorageByHandle(MG_Pipe::MGPipeHandle res,
const MG_Pipe::MGPipeResourceRecord& record);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// SHARED IMAGES (MGPipeResourceRecord::SharedImageId). True when this call settled the
// storage - the ES name's level 0 IS the image (glEGLImageTargetTexture2DOES), or the
// image could not be bound and the name is left empty - and SyncMipmapsToBackend must
// not go on to allocate or upload. False when the record names no image: a twin that
// was bound to one has been told to re-mint, and the normal storage path takes over.
Bool SyncSharedImageStorage(const MG_Pipe::MGPipeResourceRecord& record);
// Pending sub-data written into the attached shared image (Managers.cpp).
void ReplaySharedImageUploads(const MG_Pipe::MGPipeResourceRecord& record);
// The attached image takes RGB texels only (an X format): uploads are converted.
Bool m_sharedImageUploadsRgb = false;
#endif
// Whether this texture's ES storage was minted in an image carrier rather than in the
// frontend format's own layout - the readback has to ask, because for a NORMALIZED
// carrier the storage is an integer texture holding codes and glGetTexImage still owes
// the application floats.
Bool RequiresImageBindableStorage() const { return m_imageBindableStorageRequired; }
// See m_driverMayHoldGpuWrites. Callers holding the STATE texture use
// NoteDriverSideTextureWrite, which also reaches the storage behind a view.
void NoteDriverSideWrite() { m_driverMayHoldGpuWrites = true; }
void Bind(GLenum target, Uint unit = TempTextureUnit);
Uint GetBackendTextureId() const;
@@ -1997,6 +2150,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool IsDrawSyncClean(const MG_State::GLState::ITextureObject* t, Uint64 contextId,
Uint64 samplingGeneration) const {
#if MOBILEGL_BUILD_DISAGGREGATED
// A shared image's texels move with its producer's presents, which nothing here
// sees: every use goes through SyncMipmapsToBackend's shared-image arm, which waits
// for a write that landed since the last one (SharedImageImpl::AcquireForSampling).
if (m_sharedImage != nullptr) return false;
#endif
if (!m_isInitialized || m_syncedShapeContextId == 0 || m_syncedShapeContextId != contextId ||
m_syncedShapeGeneration != samplingGeneration) {
return false;
@@ -2088,6 +2247,23 @@ namespace MobileGL::MG_Backend::DirectGLES {
Bool m_isInitialized = false;
Bool m_imageBindableStorageRequired = false;
Bool m_backendStorageImmutable = false;
#if MOBILEGL_BUILD_DISAGGREGATED
// The shared image the ES name's level 0 was last bound to (0 = none; image ids are
// never reused, so this is a lifetime key), and the EGLImage that binding went through,
// which holds the image for as long as the name samples it. Null with a nonzero id when
// the image could not be bound: the name is left empty until the record moves.
Uint64 m_sharedImageId = 0;
SharedImageImpl::EglImageRef m_sharedImage;
#endif
// Whether the driver texture may hold texels the shadow never saw. Set the first time
// anything other than a texel upload can write it - an application framebuffer
// attachment, a writable image binding, a copy or blit into it, a driver-side mipmap
// generation - and never cleared. Until then the driver holds, outside a level's
// pending writes, exactly the shadow bytes it was last sent, so those writes may go up
// as their union box (one unpack-ring job, which is what Mali needs); from then on
// only the texels the client wrote may go up, since the shadow is stale everywhere
// else. See SyncMipmapsToBackend.
Bool m_driverMayHoldGpuWrites = false;
// Latches the "this driver has no buffer textures" report to once per texture. The
// report is emitted from the respecify path, which bails before recording the state
// it was asked to apply - so without the latch the texture stays permanently dirty
@@ -2105,10 +2281,16 @@ namespace MobileGL::MG_Backend::DirectGLES {
// unconditionally bumps it), the content version on every CPU pixel mutation, and
// the params version covers SetSamples/SetFixedSampleLocations, which bump neither
// of the other two but feed the shape probe. The context id pins the generation to
// the context that produced it - generations restart at 0 with a new context, and a
// texture is owned by exactly one context (share groups are not implemented), so a
// mutation can never happen under a context this key does not name. 0 = never
// stamped (real context ids start at 1). Backend-side invalidation rides on
// the context that produced it - generations restart at 0 with a new context, so the
// id is part of the key. IT IS NOT AN OWNERSHIP CLAIM: a texture belongs to a SHARE
// GROUP (P14 S3/S6 - share groups ARE implemented, `ShareGroupState` holds the object
// tables and the twin tables are keyed by {session, share group}), so two contexts of
// one group both legitimately mutate this texture. What the id buys is only the
// cheaper half of the gate: a mutation made under a context sharing this texture
// stamps a different id and re-derives the shape, which is a spurious re-derivation
// (safe, and only on a shape mutation) rather than a missed one - the per-mutation
// versions below are what actually catch a change, and they are group-independent.
// 0 = never stamped (real context ids start at 1). Backend-side invalidation rides on
// m_isInitialized: RequireImageBindableStorage and RecreateBackendTexture clear it.
Uint64 m_syncedShapeContextId = 0;
Uint64 m_syncedShapeGeneration = 0;
@@ -2257,6 +2439,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
SharedPtr<BackendTextureObject>& SyncTextureObjectToBackend(
const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
Bool imageBindableStorageRequired = false);
// Every path that lets the DRIVER write a texture's texels calls this (see
// BackendTextureObject::m_driverMayHoldGpuWrites): an application framebuffer attachment,
// a writable image binding, a copy, blit or mipmap generation into it. A view writes its
// storage texture's image, so the storage texture is the one marked.
void NoteDriverSideTextureWrite(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject);
#if MOBILEGL_PIPE_PUSH
// ---- P5e SEAM (MG_Remote/CONTRACT-P5E.md §4.2, §5.2; declared by c0e, bodied by
// id/tx2) --------------------------------------------------------------------------
@@ -2273,11 +2460,40 @@ namespace MobileGL::MG_Backend::DirectGLES {
SharedPtr<BackendTextureObject>& SyncTextureToBackendByHandle(
MG_Pipe::MGPipeHandle texture, Bool imageBindableStorageRequired = false);
BackendTextureObject* ResolveTextureTwin(MG_Pipe::MGPipeHandle texture);
// NoteDriverSideTextureWrite's HANDLE ARM, for the same paths when the caller holds a
// Texture handle (a framebuffer surface's Res, an image unit's Res, VerbMipRes,
// VerbCopyTexDst, a copy-image endpoint): the twin the handle's record names is marked, or
// its storage owner's (Desc.ViewOf) for a view. Nothing on the apply thread reads a
// frontend object for it.
//
// P8-E: under a transport the same storage owner's staged store is marked driver-written
// too (StagedTextureStore::MarkDriverWritten), so a refused readback does not answer from
// bytes the driver has since overwritten - unless `storeFollowsTheWrite`, which only
// glCopyImageSubData passes: it tries FollowCopyImageInStagedStore first and marks the store
// itself when that fails.
void NoteDriverSideTextureWriteByHandle(MG_Pipe::MGPipeHandle texture, Bool storeFollowsTheWrite = false);
#endif
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-E (notes/p8/E.md): make glCopyImageSubData's texel-block move in the server's staged
// store too (StagedTextureStore::FollowCopy), so the destination level's bytes stay its
// content. Box in each image's own texel grid. False when the store cannot follow: a cube
// map or 1D array endpoint (copy-image puts faces / layers on z, the store does not), a view,
// two different internal formats, a level whose bytes did not all cross, a source the
// driver has written, or a monolith arm.
Bool FollowCopyImageInStagedStore(MG_Pipe::MGPipeHandle source, Uint32 sourceLevel, const IntVec3& sourceOrigin,
MG_Pipe::MGPipeHandle destination, Uint32 destinationLevel,
const IntVec3& destinationOrigin, const IntVec3& size);
#endif
// Brings every texture the next draw reads - the touched units' bindings and the draw
// FBO's texture attachments - onto the backend, through the two borrowed-pair memos
// documented at their definitions. Declared here so tests can drive those memos directly.
void SyncNeccessaryTextures();
#if MOBILEGL_BUILD_DISAGGREGATED
// Every texture of this session bound to a shared image with sub-data still pending is
// synced now, so the uploads are in the images before a flush publishes them (an X
// server's PutImage into a window pixmap no later draw of its own samples).
void WritePendingSharedImageUploads();
#endif
extern Array<Array<BackendTextureObject*, (SizeT)TextureTarget::TextureTargetCount>,
MG_State::GLState::TextureState::MAX_TEXTURE_IMAGE_UNITS>
g_boundTexturesCache;
@@ -3030,6 +3246,11 @@ namespace MobileGL::MG_Backend::DirectGLES {
// Same for gl_BaseVertex: only a program that reads it pays for the per-draw
// uniform write, and only such a program needs the reset after one.
Bool ReadsBaseVertex() const { return m_baseVertexUniformLocation >= 0; }
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-C: same for the gl_BaseInstance uniform - which an indirect draw feeds from the
// command on the CPU only when the mg_IndirectParams view is absent.
Bool ReadsBaseInstance() const { return m_baseInstanceUniformLocation >= 0; }
#endif
// Which viewport indices the next draw's fragments may keep, one bit each. Written
// once per replay pass; see ForEachViewportRoutingPass.
void SetViewportPassMask(Uint32 indexMask) const;
@@ -3525,7 +3746,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
// what makes HandleOf below resolve at all. Not one member that would dereference it is
// instantiated - no Find(StateObject*), no ForEachLive - and the handle overloads never
// look at it.
using BackendSamplerViewTable = BackendSlotTable<MG_State::GLState::ITextureObject,
// P14 S6: and a bucket registry for the same reason the buffer one above is - a sampler
// view's twin is a driver object of the texture's group.
using BackendSamplerViewTable = SlotTableRegistry<MG_State::GLState::ITextureObject,
BackendSamplerViewObject,
MG_Pipe::MGPipeKind::SamplerViewCso>;
extern BackendSamplerViewTable g_backendSamplerViews;
+7 -6
View File
@@ -191,15 +191,16 @@ namespace MobileGL::MG_Backend::DirectGLES::MultiDrawImpl {
if (question == IndexBufferQuestion::Presence) return view;
if (question == IndexBufferQuestion::HostBytes) {
// No Sync* pair here and none is missing: on this side the applier has
// already consumed the persistent-map blocks and the shader writebacks for
// this resource before the draw verb replayed, so the server's staged shadow
// IS the synced copy. That is also why this arm cannot be expressed as "find
// the object and run the monolith body".
// 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 = resource->hostBytes;
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");
+290 -1
View File
@@ -11,6 +11,12 @@
#include <MG_Pipe/MGPipeHandles.h>
// P14 S6: the key this table is filed under includes the calling thread's {session, share
// group}, and the probe that answers it lives on the backend's public surface beside S4's
// native-tuple key (DirectGLES.h). Included here so this header is self-contained for the two
// translation units that include it without copying Managers.h's include order.
#include "DirectGLES.h"
#if MOBILEGL_PIPE_PUSH
#include <MG_Impl/Pipe/SlotAllocator.h>
#endif
@@ -155,6 +161,250 @@ namespace MobileGL::MG_Backend::DirectGLES {
return enabled;
}
// =====================================================================================
// P14 S6 (docs/Disaggregated/design/11-state-ownership.md): THE TWIN TABLE IS PER
// {session, share group}, NOT PER PROCESS.
// =====================================================================================
//
// WHAT WAS WRONG, in one sentence: a twin is a DRIVER object - a GL buffer id, a program, a
// texture, the storage behind them - so it belongs to one native context's object namespace,
// and a table keyed by {slot, gen} ALONE resolves the same handle value to the same twin
// whichever context asked. The frontend gives two contexts that do NOT share a group two
// independent name spaces, so the same {slot, gen} in both is legal and means two different
// objects; one process-wide table made the second context adopt the first's twin (including
// its driver ids, its persistent map and its pooled store). At the same time two contexts of
// ONE group - which must see one set of objects - were served by whichever of them twinned
// first, i.e. by luck rather than by the group the two agree on.
//
// THE KEY IS the same pair the applier files its OBJECT RECORDS under: {session, share
// group}. Same probe, same `0 means this context started its own group` rule (see
// DirectGLES.h's TwinKey and SessionRuntime.cpp's ResolveThreadTwinKey), so a record and the
// twin it describes are always in the same bucket. Session is in the key because a twin
// names a driver id and no driver id crosses a connection - two connections in one process
// are two clients, and nothing may be shared with either from the other.
//
// THE HOLDER LIST STAYS PROCESS-WIDE AND THAT IS NOT A COMPROMISE. A notice is about an
// OBJECT, not about a bucket: OnFrontendObjectDestroyed and ReleaseTwinByHandle must reach
// the by-value copies a fixture or a context reset is holding, whichever bucket they sit in,
// and "which bucket holds it" is exactly the question that produced the two-holder leak the
// list exists to fix. Each SHARED holder of the list is ONE bucket; the walk therefore covers
// every bucket of every table.
//
// THE SAME TABLE OBJECT IS REUSED, NOT REPLACED, and that is what keeps ~200 call sites
// alive: `g_backendTextureObjects.Find(...)` still answers from the bucket the calling thread
// is in, because the object the global names multiplexes on the key at each lookup. A caller
// that must address a bucket OTHER than the calling thread's - the death walks, the
// session-teardown drop - takes Shared() or calls the static entries, which walk buckets;
// and a caller that wants the whole process's view (the teardown, the tests) takes Snapshot().
// FORWARD-DECLARED, because the registry below is DEFINED before the table it holds; the
// members that name the table are defined out of line at the bottom of this header.
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable;
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class SlotTableRegistry {
public:
using Table = BackendSlotTable<StateObject, BackendObject, kKind>;
using Key = TwinKey;
using StatePtr = SharedPtr<StateObject>;
using BackendPtr = SharedPtr<BackendObject>;
// The bound the two tables that ARE a bucket registry have always spelled by name; the
// six registries reach it through their own `SlotTable` alias.
static constexpr Uint32 kMaxHandleSlot = 1u << 20;
// One bucket. SHARED, so a caller may keep it across other lookups: the map owns the
// pointee and never moves it.
SharedPtr<Table> Shared(const Key& key) const {
// The common case on the hot path: the same key as last time. One compare, no probe.
if (m_memoTable != nullptr && m_memoKey == key) return m_memoTable;
const auto found = m_buckets.find(key);
if (found != m_buckets.end()) {
m_memoKey = key;
m_memoTable = found->second;
return found->second;
}
SharedPtr<Table> fresh = MakeShared<Table>();
fresh->SetOwnerSession(key.SessionKey);
m_buckets.emplace(key, fresh);
m_memoKey = key;
m_memoTable = fresh;
return fresh;
}
// The calling thread's bucket - the ONE entry every in-place call site below resolves
// through, and the process-wide answer on a thread that belongs to no session.
//
// CONST, AND IT ANSWERS A MUTABLE BUCKET: the index is `mutable` because a LOOKUP may
// have to create the bucket it is about to miss in (a handle with no twin yet is the
// ordinary case), and the bucket's own entries are the table's business rather than the
// registry's. The alternative - a const overload returning "no table" - would make every
// read-only caller handle a case that cannot arise.
Table& ForCallingThread() const {
Key key;
CurrentTwinKey(&key);
return *Shared(key);
}
// Every bucket, in no particular order.
Vector<SharedPtr<Table>> Snapshot() const {
Vector<SharedPtr<Table>> all;
all.reserve(m_buckets.size());
for (const auto& bucket : m_buckets) all.push_back(bucket.second);
return all;
}
SizeT BucketCount() const { return m_buckets.size(); }
// THE PER-BUCKET ONE-SHOT, because a bucket held by SharedPtr is never dropped by an
// assignment: a caller that means "this context/group is gone" has to be able to say so,
// and the key is how it says which one. The bucket LEAVES the index before its twins are
// released, so a twin destructor that re-entered Shared() cannot find a half-erased one.
Bool Drop(const Key& key) {
const auto found = m_buckets.find(key);
if (found == m_buckets.end()) return false;
SharedPtr<Table> dropped = found->second;
m_buckets.erase(found);
if (m_memoTable == dropped) {
m_memoKey = Key{};
m_memoTable = nullptr;
}
dropped->Reset();
return true;
}
// EVERY bucket of one session, for the teardown of a session that owned several groups.
// The session is the key's high half, so this is one pass and no per-group bookkeeping -
// and it is the difference between "this session's twins go" and "the process's do".
Uint32 DropSession(Uint64 sessionKey) {
Uint32 dropped = 0;
for (auto bucket = m_buckets.begin(); bucket != m_buckets.end();) {
if (bucket->first.SessionKey != sessionKey) {
++bucket;
continue;
}
SharedPtr<Table> table = bucket->second;
if (m_memoTable == table) {
m_memoKey = Key{};
m_memoTable = nullptr;
}
bucket = m_buckets.erase(bucket);
table->Reset();
++dropped;
}
return dropped;
}
// Replace this registry with an empty one, releasing every bucket's twins. This is what
// `registry = {}` meant when the registry WAS one table; a caller that must keep the
// tables alive past the call takes a Snapshot() first.
void Reset() {
const Vector<SharedPtr<Table>> all = Snapshot();
m_buckets.clear();
m_memoKey = Key{};
m_memoTable = nullptr;
for (const SharedPtr<Table>& table : all) table->Reset();
}
// ---- the calling thread's bucket, member for member ---------------------------------
//
// THIS IS WHY THE SHAPE COST THE CALL SITES NOTHING. Every handle-keyed entry the table
// has is repeated here and forwarded to the bucket the calling thread resolves to, so
// `g_backendTextureObjects.FindByHandle(h)` and `g_backendBufferResources.GetOrCreate(h)`
// - unchanged source - answer from the right context's object namespace. A caller that
// must name a bucket OTHER than its own (a death walk, a case) takes Shared()/Snapshot()
// and calls the table directly.
BackendPtr& GetOrCreate(MG_Pipe::MGPipeHandle handle) {
return ForCallingThread().GetOrCreate(handle);
}
BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) {
return ForCallingThread().FindByHandle(handle);
}
const BackendPtr* FindByHandle(MG_Pipe::MGPipeHandle handle) const {
return ForCallingThread().FindByHandle(handle);
}
// Resolve-or-create in a bucket the caller NAMES rather than the one it is in. The
// monolith-glue frontend-keyed entry (StateBackendObjectRegistry::GetOrCreate below)
// needs it: a state object handed over on the client side of a sync is described by a
// record in the CALLING thread's group, and a twin built for it must land in the same
// bucket as that record.
BackendPtr& GetOrCreateIn(const Key& key, MG_Pipe::MGPipeHandle handle) {
return Shared(key)->GetOrCreate(handle);
}
Uint32 LiveGenAt(Uint32 slot) const { return ForCallingThread().LiveGenAt(slot); }
MG_Pipe::MGPipeHandle HandleOf(const StateObject* stateObj) const {
return ForCallingThread().HandleOf(stateObj);
}
void NoteStateForHandle(MG_Pipe::MGPipeHandle handle, const StatePtr& stateObj) {
ForCallingThread().NoteStateForHandle(handle, stateObj);
}
StatePtr StateForHandle(MG_Pipe::MGPipeHandle handle) const {
return ForCallingThread().StateForHandle(handle);
}
template <typename Fn>
void ForEachLive(Fn&& fn) const {
// THE CALLING THREAD'S BUCKET, not every bucket, and that is the semantic answer
// rather than a shortcut: a caller walking twins is asking "which of the objects I
// can see need this", and what it can see is its own share group. Every bucket in
// one process would walk a neighbour session's objects.
ForCallingThread().ForEachLive(fn);
}
// Every bucket, in one pass. The caller receives the key with the handle so it can tell
// which context a twin belongs to - the only walks that need the whole process's view
// are a diagnostic and a teardown.
template <typename Fn>
void ForEachBucket(Fn&& fn) const {
for (const auto& bucket : m_buckets) {
bucket.second->ForEachLive(
[&](MG_Pipe::MGPipeHandle handle, const BackendPtr& twin) { fn(bucket.first, handle, twin); });
}
}
// The process-wide answer for the two statics: a notice is about an OBJECT, so it must
// reach the twin in EVERY bucket of EVERY holder. They forward to the table's own
// statics, which walk the holder list - each SHARED holder of it is one bucket.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
return Table::ReleaseTwinByHandle(handle);
}
static Bool OnFrontendObjectDestroyed(Uint64 lifetimeId) {
return Table::OnFrontendObjectDestroyed(lifetimeId);
}
// The instance spelling, and it HANDS THE TWIN OUT rather than answering a Bool: the two
// tables that ARE a bucket registry (buffers, sampler views) have always been called as
// `g_backendBufferResources.ReleaseByHandle(res)` and their callers still have to decide
// what happens to the driver id the twin owns. The bucket is the CALLING THREAD'S, which
// is the bucket that twin was created in.
BackendPtr ReleaseByHandle(MG_Pipe::MGPipeHandle handle) {
return ForCallingThread().ReleaseByHandle(handle);
}
static Uint32 HolderCount() { return Table::HolderCount(); }
Uint32 LiveCount() const { return ForCallingThread().LiveCount(); }
Uint32 CompositeLiveCount() const { return ForCallingThread().CompositeLiveCount(); }
SizeT OrdinaryCapacityForTest() const { return ForCallingThread().OrdinaryCapacityForTest(); }
SizeT CompositeCapacityForTest() const { return ForCallingThread().CompositeCapacityForTest(); }
// ---- the bucket index's own controls, for the two tables that ARE a registry --------
//
// The six TwinRegistry registries carry the same three under the names in Managers.h
// (DropBucket / DropBucketsOfSession / ResetBuckets); they are repeated here because
// SlotTableRegistry IS the public type of the buffer and sampler-view tables, so a caller
// of those has no registry object in between.
Bool DropBucket(const Key& key) { return Drop(key); }
// Drop every bucket of one session; answers how many groups that took with it.
Uint32 DropBucketsOfSession(Uint64 sessionKey) { return DropSession(sessionKey); }
// Empty every bucket (the process-shaped world's teardown, key {0, 0}).
void ResetBuckets() { Reset(); }
private:
// ONE BUCKET PER LIVE {session, share group}, held by SHARED pointer so a caller may
// snapshot one across other lookups and so `Drop` can take a bucket's twins away
// explicitly - an embedded table could do neither. `std::unordered_map` rather than the
// tree's own flat Vector: this map is probed once per twin lookup on the draw path.
mutable std::unordered_map<Key, SharedPtr<Table>, TwinKeyHash> m_buckets;
// Hot-path memo. A registry is per KIND and each kind is touched several times per draw;
// a thread only ever moves between buckets when its session binds another context.
mutable SharedPtr<Table> m_memoTable;
mutable Key m_memoKey;
};
template <typename StateObject, typename BackendObject, MG_Pipe::MGPipeKind kKind>
class BackendSlotTable {
public:
@@ -209,6 +459,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
// ENTRIES and the memo; the links are the table's own and are never copied.
BackendSlotTable() { LinkHolder(); }
BackendSlotTable(const BackendSlotTable& other):
m_ownerSession(other.m_ownerSession),
m_slots(other.m_slots),
m_band(other.m_band),
m_nullTwin(other.m_nullTwin),
@@ -217,6 +468,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
LinkHolder();
}
BackendSlotTable(BackendSlotTable&& other) noexcept:
m_ownerSession(other.m_ownerSession),
m_slots(std::move(other.m_slots)),
m_band(std::move(other.m_band)),
m_nullTwin(std::move(other.m_nullTwin)),
@@ -252,6 +504,12 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
~BackendSlotTable() { UnlinkHolder(); }
// The served session whose bucket this table is (SlotTableRegistry::Shared), 0 for a
// table that belongs to none (the process-wide registry on a sessionless thread, a
// fixture's copy of one).
void SetOwnerSession(Uint64 session) { m_ownerSession = session; }
Uint64 OwnerSession() const { return m_ownerSession; }
// Resolve-or-create. The handle comes from the client allocator keyed on the frontend
// object's lifetime id, so two calls for the same live object always land on the same
// slot, and a successor object at the same heap address never does.
@@ -553,12 +811,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
// double Free would be refused by generation anyway. Idempotent against the kind's
// own delete opcode, exactly as the notice arm is: a twin the delete already released
// fails ReleaseTwinAt's generation check and the walk moves on.
//
// ONE SESSION'S HOLDERS, WHEN THE DEATH IS ONE SESSION'S. A handle is numbered by its own
// client's allocator, so with several clients served by one process the same {slot, gen}
// names a live, unrelated object in every other session: walking every holder released
// THOSE twins too - a client deleting a texture (or exiting) deleted the compositor's
// driver texture with the same handle, and its windows sampled freed memory. On a thread
// that belongs to a session only that session's buckets answer; a sessionless thread
// (the in-library backend, a unit fixture) keeps the process-wide walk.
static Bool ReleaseTwinByHandle(MG_Pipe::MGPipeHandle handle) {
if (MG_Pipe::MGPipeHandleIsNull(handle)) return false;
TwinKey key;
const Bool sessionScoped = CurrentTwinKey(&key) && key.SessionKey != 0;
Bool released = false;
for (BackendSlotTable* holder = s_firstHolder; holder != nullptr;) {
BackendSlotTable* const next = holder->m_nextHolder;
released = holder->ReleaseTwinAt(handle) || released;
if (!sessionScoped || holder->m_ownerSession == key.SessionKey) {
released = holder->ReleaseTwinAt(handle) || released;
}
holder = next;
}
return released;
@@ -616,6 +886,24 @@ namespace MobileGL::MG_Backend::DirectGLES {
}
}
// Empty this table, releasing every twin it holds. THE SPACES ARE MOVED OUT FIRST and
// that is the whole of the care taken here: a twin's destructor is a driver call that
// could re-enter GetOrCreate on this table (SlotTables.h's ReleaseTwinAt says why), and
// destroying the entries in place would leave that re-entry resizing the vector the
// destructors are being walked out of. Here the table is already empty when the entries
// die, so a re-entrant insert grows a fresh vector.
//
// This is what `table = {}` used to mean, spelled so a caller cannot miss the ordering.
void Reset() {
Vector<Entry> slots = std::move(m_slots);
Vector<Entry> band = std::move(m_band);
BackendPtr nullTwin = std::move(m_nullTwin);
ForgetHandle();
slots.clear();
band.clear();
nullTwin.reset();
}
Uint32 LiveCount() const {
Uint32 count = 0;
for (const Entry& entry : m_slots) {
@@ -745,6 +1033,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
static inline BackendSlotTable* s_firstHolder = nullptr;
BackendSlotTable* m_prevHolder = nullptr;
BackendSlotTable* m_nextHolder = nullptr;
Uint64 m_ownerSession = 0;
// Indexed by MGPipeHandle::Slot; [0] is the reserved slot and is never live.
Vector<Entry> m_slots;
+4 -1
View File
@@ -572,7 +572,10 @@ namespace MobileGL::MG_Backend::DirectGLES {
ZoneScopedC(TRACY_ZONECOLOR_BACKEND);
#endif
// The name is the marker: ShaderSourceProcessor only emits it when the source
// wrote gl_FragColor, and such a shader can have no other output.
// names gl_FragColor, and a shader that writes it can have no other output. One that
// names it only in an inactive preprocessor arm keeps the declaration unwritten, and
// the IO resolver moves that inactive output off location 0, so the match below
// (which wants location 0) does not take it.
static const char* const kLoweredName = "mg_FragColor";
if (shaderType != GL_FRAGMENT_SHADER || drawBufferCount <= 1) {
return glslCode;
@@ -1,6 +1,7 @@
// Included in DirectGLES.cpp after the native packed-word readback helper.
// The server needs owned tight rows for a storage promotion, not client PACK
// layout or a fallback into a frontend texture's potentially stale shadow.
// 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)
@@ -280,6 +281,144 @@
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;
@@ -293,17 +432,32 @@
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 (!ReadTextureLevelTight(twin->GetBackendTextureId(), target, uploadTarget,
static_cast<TextureInternalFormat>(record->Desc.InternalFormat), info.Level, extent,
twin->RequiresImageBindableStorage(), info.Format, info.Type, slice)) return false;
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
@@ -332,6 +332,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
BackendObject_DirectVulkan::~BackendObject_DirectVulkan() {
// Everything below reaches this session's renderer slot, whichever thread destroys it.
MagmaSession* const ownSession = m_magmaSession.get();
MagmaSession* const previousSession = BoundMagmaSession();
if (ownSession != nullptr) BindMagmaSessionToThisThread(ownSession);
// P12 review fix: A SERVER SESSION THAT ENDS WITHOUT A CLEAN TEARDOWN LEAVES NO RENDERER ON
// THE WINDOW. The renderer - swapchain and VkSurfaceKHR on the window - is a process global
// that only ReleaseEGLResources / a released surface drops, and a client that went away
@@ -345,6 +349,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
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;
@@ -353,6 +366,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
BackendObject_DirectVulkan::BackendObject_DirectVulkan() : m_rendererInfo{GetRendererIdentity()} {}
Bool BackendObject_DirectVulkan::InitWindowSurface() {
// A suspended surface activates its placeholder - never the window, which is gone.
const auto suspended = m_suspendedWindowSurfaces.find(m_activatingSurface);
if (suspended != m_suspendedWindowSurfaces.end()) {
if (UsesSurfaceTargets() && pVulkanRenderer) {
return ActivateSuspendedPlaceholder(m_activatingSurface, suspended->second);
}
return InitPbufferSurface(static_cast<EGLint>(suspended->second.Width),
static_cast<EGLint>(suspended->second.Height));
}
if (!m_windowHandle.Handle) {
MGLOG_E("Cannot initialize DirectVulkan window surface: native window handle is null");
return false;
@@ -360,12 +382,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto nativeWindow = reinterpret_cast<NativeWindowType>(m_windowHandle.Handle);
VulkanRendererConfig config;
config.SwapInterval = GetRequestedSwapInterval();
if (UsesSurfaceTargets() && pVulkanRenderer) {
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), nativeWindow, config);
}
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow);
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(nativeWindow, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitWindowSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
return true;
}
@@ -373,15 +401,35 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VulkanRendererConfig config;
config.SurfaceWidth = static_cast<Uint32>(std::max<EGLint>(width, 1));
config.SurfaceHeight = static_cast<Uint32>(std::max<EGLint>(height, 1));
if (UsesSurfaceTargets() && pVulkanRenderer) {
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(m_activatingSurface), NativeWindowType{},
config);
}
// Any renderer instance this assignment replaces is destroyed here;
// fence/timer-query handles stamped with the old generation go stale.
BumpRendererGeneration();
pVulkanRenderer = MakeUnique<MG_Backend::DirectVulkan::VulkanRenderer>(NativeWindowType{}, config);
MOBILEGL_ASSERT(pVulkanRenderer != nullptr, "InitPbufferSurface: VulkanRenderer creation failed");
pVulkanRenderer->Initialize();
pVulkanRenderer->SetActiveSurfaceTargetKey(SurfaceTargetKey(m_activatingSurface));
return true;
}
void BackendObject_DirectVulkan::BindSessionStateToThisThread() {
if (m_magmaSession == nullptr) m_magmaSession = MakeUnique<MagmaSession>();
BindMagmaSessionToThisThread(m_magmaSession.get());
MGLOG_I("DirectVulkan: session state %llu bound to this apply thread",
static_cast<unsigned long long>(m_magmaSession->Id()));
}
Bool BackendObject_DirectVulkan::LatchIfGpuFaulted() {
#if MOBILEGL_BUILD_DISAGGREGATED
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchIfGpuHung("apply");
#else
return false;
#endif
}
void BackendObject_DirectVulkan::Initialize() {
#if MOBILEGL_BUILD_DISAGGREGATED
VkBufferManager::RegisterWireResourceOps();
@@ -457,6 +505,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!BackendObject::ResizeEGLWindowSurface(surface, width, height)) {
return false;
}
const EGLSurfaceState* state = GetRegisteredEGLSurface(surface);
if (UsesSurfaceTargets() && pVulkanRenderer && state != nullptr && state->Kind == SurfaceKind::Pbuffer) {
// A pbuffer-backed window (a Wayland window presented through wl_shm) whose target is an
// image reader of FIXED size: a swapchain rebuild cannot follow the resize, so the target
// is rebuilt at the new size - as DirectGLES replaces its native pbuffer. A parked one is
// rebuilt when it is next activated, from the size registered above.
const Bool active = m_eglSurfaceInitialized && m_eglSurface == surface;
pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
if (active) {
VulkanRendererConfig config;
config.SurfaceWidth = static_cast<Uint32>(state->Width);
config.SurfaceHeight = static_cast<Uint32>(state->Height);
(void)pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
}
return true;
}
if (pVulkanRenderer && m_eglSurface == surface) {
pVulkanRenderer->RequestSwapchainResize(width, height);
}
@@ -474,7 +538,33 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool BackendObject_DirectVulkan::MakeEGLCurrent(EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext ctx) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
return BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
const Bool ok = BackendObject::MakeEGLCurrent(dpy, draw, read, ctx);
if (ok && UsesSurfaceTargets() && ctx != EGL_NO_CONTEXT && draw != EGL_NO_SURFACE) {
m_contextBindings[ctx] = EGLCurrentState{.Display = dpy, .DrawSurface = draw, .ReadSurface = read,
.Context = ctx};
}
return ok;
}
void BackendObject_DirectVulkan::OnClientContextBound(Uint64 token) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (!UsesSurfaceTargets() || token == 0) return;
// Each of a multi-window client's contexts draws to the surface IT was made current with
// (Qt renders every window on its own thread and context); the ring interleaves them with
// a bind_context between, and no make-current. Follow the context back to its surface.
const auto found = m_contextBindings.find(reinterpret_cast<EGLContext>(token));
if (found == m_contextBindings.end()) return;
const EGLCurrentState binding = found->second;
if (!GetRegisteredEGLSurface(binding.DrawSurface)) {
m_contextBindings.erase(found);
return;
}
const auto current = m_eglCurrentThreads.find(std::this_thread::get_id());
if (current != m_eglCurrentThreads.end() && current->second.Context == binding.Context &&
current->second.DrawSurface == binding.DrawSurface && m_eglSurface == binding.DrawSurface) {
return;
}
(void)BackendObject::MakeEGLCurrent(binding.Display, binding.DrawSurface, binding.ReadSurface, binding.Context);
}
Bool BackendObject_DirectVulkan::SwapEGLBuffers(EGLDisplay dpy, EGLSurface draw) {
@@ -486,6 +576,56 @@ 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);
@@ -500,11 +640,131 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// The reflection cache is file-scope, not renderer-owned; without this the
// deleted programs' reflection strings survive full context teardown.
ClearProgramResourceCaches();
m_suspendedWindowSurfaces.clear();
BackendObject::ReleaseEGLResources();
}
Bool BackendObject_DirectVulkan::ActivateSuspendedPlaceholder(EGLSurface surface,
const SuspendedWindowSurface& extent) {
// A pbuffer target at the window's extent: every default-framebuffer path (render passes,
// read-back, the shared-image blit, the acquire and the present) keeps a live swapchain to
// work on, and the extent it publishes is the one the client already has.
VulkanRendererConfig config;
config.SurfaceWidth = std::max<Uint32>(extent.Width, 1);
config.SurfaceHeight = std::max<Uint32>(extent.Height, 1);
return pVulkanRenderer->ActivateSurfaceTarget(SurfaceTargetKey(surface), NativeWindowType{}, config);
}
Bool BackendObject_DirectVulkan::SuspendServerWindow(void* window) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
// The monolith's one renderer IS its window; only surface targets can be swapped under it.
if (window == nullptr || !UsesSurfaceTargets()) return false;
Vector<EGLSurface> onWindow;
for (const auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
onWindow.push_back(client);
}
for (const EGLSurface client : onWindow) {
const auto state = m_eglSurfaces.find(client);
SuspendedWindowSurface extent;
extent.Width = static_cast<Uint32>(std::max<EGLint>(state->second.Width, 1));
extent.Height = static_cast<Uint32>(std::max<EGLint>(state->second.Height, 1));
if (pVulkanRenderer) {
const Uint64 key = SurfaceTargetKey(client);
const VkExtent2D built = pVulkanRenderer->SurfaceTargetExtent(key);
if (built.width != 0 && built.height != 0) {
extent.Width = built.width;
extent.Height = built.height;
}
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
// Flushes, waits the device idle, purges the render passes on the target and drops its
// swapchain and VkSurfaceKHR - nothing references the window after this.
pVulkanRenderer->DestroySurfaceTarget(key);
m_suspendedWindowSurfaces[client] = extent;
// A placeholder that cannot be built is a suspend that failed: the window is already let
// go of, and the caller ends the session by name instead (its EGL resources go).
if (active && !ActivateSuspendedPlaceholder(client, extent)) return false;
} else {
m_suspendedWindowSurfaces[client] = extent;
}
}
MGLOG_I("DirectVulkan: %zu window surface(s) on server window %p now draw into placeholder pbuffer targets; "
"the renderer, the device and every object stay",
onWindow.size(), window);
return true;
}
Bool BackendObject_DirectVulkan::ResumeServerWindow(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr || !UsesSurfaceTargets()) return false;
UnorderedMap<EGLSurface, SuspendedWindowSurface> suspended;
suspended.swap(m_suspendedWindowSurfaces);
for (const auto& [client, extent] : suspended) {
(void)extent;
const auto state = m_eglSurfaces.find(client);
if (state == m_eglSurfaces.end()) continue; // released while suspended
state->second.Window.Handle = window;
state->second.Window.Width = width;
state->second.Window.Height = height;
state->second.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state->second.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state->second.Window;
if (!pVulkanRenderer) continue;
const Uint64 key = SurfaceTargetKey(client);
const Bool active = pVulkanRenderer->IsActiveSurfaceTarget(key);
pVulkanRenderer->DestroySurfaceTarget(key); // the placeholder
// A parked surface is built on the new window when it is next activated (InitWindowSurface).
if (!active) continue;
// The creation's own path: a new VkSurfaceKHR and swapchain with the surface's current
// pre-transform, a new target serial (render passes rebuilt), and the extent published.
VulkanRendererConfig config;
config.SurfaceWidth = std::max<Uint32>(width, 1);
config.SurfaceHeight = std::max<Uint32>(height, 1);
config.SwapInterval = GetRequestedSwapInterval();
if (!pVulkanRenderer->ActivateSurfaceTarget(key, reinterpret_cast<NativeWindowType>(window), config)) {
return false;
}
}
MGLOG_I("DirectVulkan: %zu suspended window surface(s) rebuilt on server window %p (%ux%u)", suspended.size(),
window, width, height);
return true;
}
Bool BackendObject_DirectVulkan::ServerWindowResized(void* window, Uint32 width, Uint32 height) {
const std::lock_guard<std::recursive_mutex> lock(m_eglStateMutex);
if (window == nullptr || !UsesSurfaceTargets()) return false;
Bool followed = false;
for (auto& [client, state] : m_eglSurfaces) {
if (state.Kind != SurfaceKind::Window || state.Window.Handle != window) continue;
if (m_suspendedWindowSurfaces.find(client) != m_suspendedWindowSurfaces.end()) continue;
state.Window.Width = width;
state.Window.Height = height;
state.Width = static_cast<EGLint>(std::max<Uint32>(width, 1));
state.Height = static_cast<EGLint>(std::max<Uint32>(height, 1));
if (m_eglSurface == client) m_windowHandle = state.Window;
// Magma's extent is its swapchain's, published when it is built: the active target follows
// the window now (or at the half-recorded frame's present); a parked one at its next present.
if (pVulkanRenderer && pVulkanRenderer->IsActiveSurfaceTarget(SurfaceTargetKey(client))) {
followed = pVulkanRenderer->FollowActiveWindowResize() || followed;
}
}
return followed;
}
Bool BackendObject_DirectVulkan::UsesSurfaceTargets() const {
return MG_Config::Transport != MG_Config::TransportMode::Monolith;
}
void BackendObject_DirectVulkan::OnEGLSurfaceForgotten(EGLSurface surface) {
m_suspendedWindowSurfaces.erase(surface);
if (UsesSurfaceTargets() && pVulkanRenderer) pVulkanRenderer->DestroySurfaceTarget(SurfaceTargetKey(surface));
}
void BackendObject_DirectVulkan::OnEGLSurfaceReleased(EGLSurface surface) {
(void)surface;
// The renderer outlives its surfaces here: only this surface's target goes (in
// OnEGLSurfaceForgotten, which follows), and every fence stays valid.
if (UsesSurfaceTargets() && pVulkanRenderer) return;
// Outstanding fence/timer-query handles now refer to a dead renderer;
// treat them as signaled/available with zero results from here on.
BumpRendererGeneration();
@@ -553,6 +813,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
V_OpenGL30, V_OpenGL31, V_OpenGL32, V_OpenGL33, V_OpenGL40, V_OpenGL41, V_OpenGL42, V_OpenGL43,
V_OpenGL44, V_OpenGL45, V_OpenGL46,
E_GL_ARB_draw_buffers_blend,
// The GLSL 1.00/1.10-era ARB trio plus NPOT textures: all core since GL 2.0 and
// trivially true for every context this backend claims (4.6). KWin 6's
// EglContext::checkSupported() gates desktop-GL compositing on exactly these four
// strings, so withholding them fails its renderer bring-up. Same block as DirectGLES.
E_GL_ARB_shader_objects, E_GL_ARB_vertex_shader, E_GL_ARB_fragment_shader,
E_GL_ARB_texture_non_power_of_two,
E_GL_ARB_compute_shader, E_GL_ARB_shader_storage_buffer_object, E_GL_ARB_shader_image_load_store,
E_GL_ARB_clear_buffer_object, E_GL_ARB_program_interface_query, E_GL_ARB_framebuffer_object, E_GL_ARB_draw_indirect,
E_GL_ARB_multi_draw_indirect,
@@ -727,6 +993,7 @@ 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;
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include "../BackendObject.h"
#include "MagmaSession.h"
#include <MG_Util/BackendLoaders/Vulkan/Loader.h>
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -26,6 +27,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~BackendObject_DirectVulkan() override;
void Initialize() override;
void BindSessionStateToThisThread() override;
// The GPU hang watch's verdict on this session's device (VulkanRenderer::LatchIfGpuHung).
Bool LatchIfGpuFaulted() override;
void OnClientContextBound(Uint64 token) override;
Bool InitWindowSurface() override;
Bool InitCapabilities() override;
Bool InitializeEGLDisplay(EGLDisplay dpy, EGLint* major, EGLint* minor) override;
@@ -34,8 +39,22 @@ 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;
@@ -47,6 +66,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
Bool InitPbufferSurface(EGLint width, EGLint height) override;
void OnEGLSurfaceReleased(EGLSurface surface) override;
void OnEGLSurfaceForgotten(EGLSurface surface) override;
// A served session keeps one renderer across all its client's surfaces and switches the
// renderer's surface target instead (VulkanRenderer::ActivateSurfaceTarget).
Bool UsesSurfaceTargets() const;
static Uint64 SurfaceTargetKey(EGLSurface surface) { return reinterpret_cast<Uint64>(surface); }
void UpdateAdvertisedExtensions();
void UpdateDynamicBackendParameters();
@@ -54,6 +78,21 @@ namespace MobileGL::MG_Backend::DirectVulkan {
DynamicBackendParameters m_dynamicParameters;
MG_External::VulkanCapabilities m_vulkanCaps;
RendererInfo m_rendererInfo;
// A served session's own copy of Magma's per-process state (MagmaSession.h), bound to its
// apply thread. Null in the monolith and every other single-session shape.
UniquePtr<MagmaSession> m_magmaSession;
// The binding each client context was last made current with, for OnClientContextBound.
UnorderedMap<EGLContext, EGLCurrentState> m_contextBindings;
// Window surfaces whose window is gone (SuspendServerWindow), with the extent their
// placeholder pbuffer target is built at - the window's, so the default framebuffer the
// client knows keeps its size.
struct SuspendedWindowSurface {
Uint32 Width = 1;
Uint32 Height = 1;
};
UnorderedMap<EGLSurface, SuspendedWindowSurface> m_suspendedWindowSurfaces;
// The placeholder pbuffer target of a suspended surface, made active. False: it could not be built.
Bool ActivateSuspendedPlaceholder(EGLSurface surface, const SuspendedWindowSurface& extent);
};
// Single-source-of-truth helpers shared with the driver POST
@@ -0,0 +1,29 @@
// 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
+100 -17
View File
@@ -35,8 +35,9 @@
#include <spirv_reflect.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Leak-at-exit storage; see GlobalObjects.cpp.
UniquePtr<VulkanRenderer>& pVulkanRenderer = *new UniquePtr<VulkanRenderer>();
// Per Magma session (MagmaSession.h); the process-wide session is leak-at-exit storage, see
// GlobalObjects.cpp.
RendererSlot pVulkanRenderer;
namespace {
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -59,7 +60,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// current one (a new renderer restarts its frame-serial counter and
// reuses pool indices). Atomic because handles may be polled from a
// thread other than the EGL thread that recreates the renderer.
std::atomic<Uint64> g_rendererGeneration{1};
//
// One per Magma session, drawn from one process-wide source: a handle stamped by one
// session's renderer must read as stale against every other session's, never equal.
std::atomic<Uint64> g_rendererGenerationSource{1};
struct RendererGeneration {
std::atomic<Uint64> value{g_rendererGenerationSource.fetch_add(1, std::memory_order_acq_rel)};
};
SessionLocal<RendererGeneration> g_rendererGeneration;
} // namespace
#if MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
@@ -123,11 +131,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif // MOBILEGL_BUILD_DISAGGREGATED && MOBILEGL_PIPE_PUSH
Uint64 GetRendererGeneration() {
return g_rendererGeneration.load(std::memory_order_acquire);
return g_rendererGeneration->value.load(std::memory_order_acquire);
}
void BumpRendererGeneration() {
g_rendererGeneration.fetch_add(1, std::memory_order_acq_rel);
g_rendererGeneration->value.store(g_rendererGenerationSource.fetch_add(1, std::memory_order_acq_rel),
std::memory_order_release);
}
namespace {
@@ -370,16 +379,36 @@ namespace MobileGL::MG_Backend::DirectVulkan {
#endif // !MOBILEGL_BUILD_DISAGGREGATED
#if MOBILEGL_BUILD_DISAGGREGATED
// The verb's handles identify server stores. Readback orders GPU-produced
// command/count bytes before CPU tier expansion; never inspect a client binding.
// The verb's handles identify server stores; never inspect a client binding.
//
// P8-D: THE GPU READS THE WORDS (VulkanRenderer::DrawWireIndirectNative, WireDraw.inc), as
// on the monolith arm. The CPU expansion below is kept for the one shape the monolith arm
// also expands - a COUNT form the device cannot issue natively (no VK_KHR_draw_indirect_count,
// or maxdrawcount > 1 without multiDrawIndirect: VulkanRenderer.cpp's
// MultiDrawElementsIndirectCount) - where ReadWireBuffer orders GPU-produced command/count
// bytes before the expansion.
void DrawWireIndirect(GLenum mode, GLenum type, const void* indirect, GLsizei drawcount,
GLsizei stride, Bool indexed, Bool counted = false, GLintptr countOffset = 0) {
if (drawcount <= 0) return;
if (drawcount <= 0 || countOffset < 0) return;
if (pVulkanRenderer->DrawWireIndirectNative(mode, type, reinterpret_cast<Uint64>(indirect), drawcount,
stride, indexed, counted, static_cast<Uint64>(countOffset)))
return;
auto& buffers = pVulkanRenderer->GetWireBufferManager();
const auto& state = MG_Pipe::MGPipeApplier();
// P8-D: this call reads its words on the CPU; the waits those reads take are its own.
struct ExpansionCount {
VkBufferManager& buffers;
const Uint64 waitsBefore = buffers.GetWireHostWaitCount();
~ExpansionCount() {
if (!MG_Util::PipeStats::Enabled()) return;
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireIndirectCpuExpansions, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitsIndirect,
buffers.GetWireHostWaitCount() - waitsBefore);
}
} expansionCount{buffers};
if (counted) {
Uint32 count = 0;
if (countOffset < 0 || !buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
if (!buffers.ReadWireBuffer(state.VerbIndirectParameterBuffer,
static_cast<Uint64>(countOffset), sizeof(count), &count)) return;
drawcount = static_cast<GLsizei>(std::min<Uint32>(count, static_cast<Uint32>(drawcount)));
if (!drawcount) return;
@@ -917,12 +946,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DispatchComputeIndirect(GLintptr indirect) {
#if MOBILEGL_BUILD_DISAGGREGATED
// P8-SV: THE GPU READS THE GROUP COUNTS on the wire arm too - VulkanRenderer's
// DispatchComputeIndirect takes its transport branch into DispatchWireComputeIndirect
// (WireDraw.inc), which issues vkCmdDispatchIndirect from the verb's store. The CPU read
// that was here (ReadWireBuffer, a whole-GPU wait once a shader had written the store) is gone.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith) {
Uint32 groups[3]{};
if (indirect >= 0 && pVulkanRenderer->GetWireBufferManager().ReadWireBuffer(
MG_Pipe::MGPipeApplier().VerbDispatchIndirectBuffer,
static_cast<Uint64>(indirect), sizeof(groups), groups))
pVulkanRenderer->DispatchCompute(groups[0], groups[1], groups[2]);
pVulkanRenderer->DispatchComputeIndirect(indirect);
return;
}
#endif
@@ -1254,7 +1283,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
payload.mode = mode;
// TODO: allocate draw cmd buf elsewhere
static Vector<DrawCmdParam> params;
static thread_local Vector<DrawCmdParam> params;
params.clear();
params.resize(drawcount);
@@ -1327,7 +1356,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Uint32 indexSizeShift = static_cast<Uint32>(std::countr_zero(indexSize));
// TODO: allocate draw cmd buf elsewhere
static Vector<DrawIndexedCmdParam> params;
static thread_local Vector<DrawIndexedCmdParam> params;
params.clear();
params.resize(drawcount);
@@ -1731,7 +1760,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void Present() {
MOBILEGL_ASSERT(pVulkanRenderer, "DirectVulkan::Present called with null VulkanRenderer");
// A served session's renderer is gone once its client released its resources; a present
// that still reaches it has nothing to show and is dropped rather than dereferenced.
if (!pVulkanRenderer) {
MGLOG_E_ONCE("DirectVulkan: a present reached a session whose renderer is already released; dropped");
return;
}
pVulkanRenderer->Present();
// THE frame boundary for the MGPipe counters, at the backend entry point rather
// than inside VulkanRenderer::Present: that function has an early return for the
@@ -1741,4 +1775,53 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Util::PipeStats::OnPresent();
}
}
namespace {
// Per session: each client asks for its own interval.
SessionLocal<Optional<Int>> g_requestedSwapInterval;
} // namespace
void SetSwapInterval(Int interval) {
*g_requestedSwapInterval = interval;
if (pVulkanRenderer) {
pVulkanRenderer->SetSwapInterval(interval);
}
}
Optional<Int> GetRequestedSwapInterval() {
return *g_requestedSwapInterval;
}
#if MOBILEGL_BUILD_DISAGGREGATED
void ForgetRequestedSwapInterval() {
g_requestedSwapInterval->reset();
}
#endif
DescriptorPoolCensus GetDescriptorPoolCensus() {
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 M2: UNDER A TRANSPORT THE POOLS ARE THE SERVER RENDERER'S, and its apply thread
// grows, rewinds and trims them while the test thread would be reading - a census taken
// after a swap the client ran ahead of, or mid-burst, walks a set cache BeginFrame may be
// clearing. Inproc, the server is a thread of this process: the read is posted to it as
// the test probe, which runs between drain batches, so it sees every record published
// before it and none half-applied - without waiting on the GPU, which is the point of the
// trim case (it gives a burst back with other slots' work in flight). Spawn and tcp keep
// their pools in another process; the client has no renderer and answers unavailable.
if (MG_Config::Transport != MG_Config::TransportMode::Monolith &&
!MG_Remote::Server::ServerLoop::OnApplyThread()) {
if (MG_Config::Transport != MG_Config::TransportMode::InProcess) return DescriptorPoolCensus{};
DescriptorPoolCensus census{};
const MobileGLResult result = MG_Remote::Server::ServerLoopInstance().RunProbeOnApplyThreadForTesting(
+[](void* user) -> MobileGLResult {
*static_cast<DescriptorPoolCensus*>(user) =
pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
return MOBILEGL_OK;
},
&census);
return result == MOBILEGL_OK ? census : DescriptorPoolCensus{};
}
#endif
return pVulkanRenderer ? pVulkanRenderer->GetDescriptorPoolCensus() : DescriptorPoolCensus{};
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -9,10 +9,32 @@
#pragma once
#include <Includes.h>
#include <MG_Backend/BackendObject.h>
#include "MagmaSession.h"
#include "Renderer/VulkanRenderer.h"
namespace MobileGL::MG_Backend::DirectVulkan {
extern UniquePtr<VulkanRenderer>& pVulkanRenderer;
// The renderer of the calling thread's Magma session (MagmaSession.h): a served session's own on
// its apply thread, the process-wide one everywhere else. Spelled like the UniquePtr it used to
// be so the call sites read the same.
class RendererSlot {
public:
UniquePtr<VulkanRenderer>& Slot() const { return m_slot.Get(); }
VulkanRenderer* get() const { return Slot().get(); }
VulkanRenderer* operator->() const { return get(); }
VulkanRenderer& operator*() const { return *get(); }
explicit operator bool() const { return get() != nullptr; }
RendererSlot& operator=(UniquePtr<VulkanRenderer>&& renderer) {
Slot() = std::move(renderer);
return *this;
}
void reset() { Slot().reset(); }
friend Bool operator==(const RendererSlot& slot, std::nullptr_t) { return slot.get() == nullptr; }
friend Bool operator!=(const RendererSlot& slot, std::nullptr_t) { return slot.get() != nullptr; }
private:
SessionLocal<UniquePtr<VulkanRenderer>> m_slot;
};
extern RendererSlot pVulkanRenderer;
// Generation of the live VulkanRenderer instance, mirroring DirectGLES's
// g_syncContextGeneration. BackendObject_DirectVulkan bumps it wherever
@@ -147,4 +169,14 @@ 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
@@ -0,0 +1,87 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "MagmaSession.h"
#include <atomic>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace {
struct SessionLocalEntry {
Detail::SessionLocalConstruct construct;
Detail::SessionLocalDestroy destroy;
};
// Construct-on-first-use: SessionLocals are namespace-scope objects in several translation
// units and register during static initialization, in no particular order.
Vector<SessionLocalEntry>& Registry() {
static auto* registry = new Vector<SessionLocalEntry>();
return *registry;
}
std::atomic<Uint64> g_nextSessionId{1};
thread_local MagmaSession* t_boundSession = nullptr;
// Leak-at-exit, like the renderer it holds (GlobalObjects.cpp): nothing may tear the
// monolith's renderer down from a static destructor after the driver is gone.
MagmaSession& ProcessSession() {
static auto* session = new MagmaSession();
return *session;
}
} // namespace
Uint32 Detail::RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy) {
auto& registry = Registry();
registry.push_back({construct, destroy});
return static_cast<Uint32>(registry.size() - 1);
}
MagmaSession& Detail::CurrentMagmaSession() {
MagmaSession* bound = t_boundSession;
return bound != nullptr ? *bound : ProcessSession();
}
void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index) {
if (index >= session.m_slots.size()) session.m_slots.resize(Registry().size(), nullptr);
void*& slot = session.m_slots[index];
if (slot == nullptr) slot = Registry()[index].construct();
return slot;
}
MagmaSession::MagmaSession() : m_id(g_nextSessionId.fetch_add(1, std::memory_order_relaxed)) {
// Every slot is built up front: the process-wide session is reached from whichever thread
// the app renders on, and a lazily built slot would be a first-touch race between two.
const auto& registry = Registry();
m_slots.resize(registry.size(), nullptr);
for (SizeT i = 0; i < registry.size(); ++i) m_slots[i] = registry[i].construct();
}
MagmaSession::~MagmaSession() {
MagmaSession* const previous = t_boundSession;
t_boundSession = this;
const auto& registry = Registry();
for (SizeT i = m_slots.size(); i-- > 0;) {
if (m_slots[i] != nullptr) registry[i].destroy(m_slots[i]);
m_slots[i] = nullptr;
}
// Never back to this session: it is gone.
t_boundSession = previous == this ? nullptr : previous;
}
MagmaSessionScope::MagmaSessionScope(MagmaSession* session) : m_previous(t_boundSession) {
t_boundSession = session;
}
MagmaSessionScope::~MagmaSessionScope() { t_boundSession = m_previous; }
void BindMagmaSessionToThisThread(MagmaSession* session) { t_boundSession = session; }
MagmaSession* BoundMagmaSession() { return t_boundSession; }
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,100 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/MagmaSession.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// MAGMA'S PER-SESSION STATE. Magma was written for one renderer per process: the renderer slot,
// the active buffer manager, the dynamic-state shadow, the render-pass manager's statics and the
// renderer generation are all file-scope. A multi-session server (several clients served from one
// process, each on its own apply thread with its own BackendObject_DirectVulkan) needs one copy of
// each per session, or one session's work lands in another's tables.
//
// SessionLocal<T> is that copy. It resolves through the CALLING THREAD's bound MagmaSession, and to
// the process-wide default session on every thread nothing bound - which is the monolith, the client
// role and every unit case, i.e. exactly the old behaviour, on whatever thread the app renders from.
// A served session's backend owns its MagmaSession and binds it on its apply thread (the only thread
// that runs that session's backend), so the two never mix.
#pragma once
#include <Includes.h>
#include <cstddef>
#include <new>
#include <utility>
namespace MobileGL::MG_Backend::DirectVulkan {
class MagmaSession;
namespace Detail {
using SessionLocalConstruct = void* (*)();
using SessionLocalDestroy = void (*)(void*);
// Registers one SessionLocal<T> and returns its slot. Called from static initialization.
Uint32 RegisterSessionLocal(SessionLocalConstruct construct, SessionLocalDestroy destroy);
// The calling thread's session, or the process-wide default.
MagmaSession& CurrentMagmaSession();
void* SessionLocalSlot(MagmaSession& session, Uint32 index);
} // namespace Detail
// One served session's copy of every SessionLocal. Slots are built lazily on first use and
// destroyed in reverse registration order with the session still bound, so a destructor that
// reaches another SessionLocal finds this session's copy and not the binding thread's.
class MagmaSession {
public:
MagmaSession();
~MagmaSession();
MagmaSession(const MagmaSession&) = delete;
MagmaSession& operator=(const MagmaSession&) = delete;
// A monotonic id, unique for the process lifetime. Never an address: a later session can
// be allocated where an ended one lived.
Uint64 Id() const { return m_id; }
private:
friend void* Detail::SessionLocalSlot(MagmaSession& session, Uint32 index);
Vector<void*> m_slots;
Uint64 m_id = 0;
};
// Makes `session` the calling thread's Magma session for the scope's lifetime, restoring the
// previous binding (usually none) afterwards.
class MagmaSessionScope {
public:
explicit MagmaSessionScope(MagmaSession* session);
~MagmaSessionScope();
MagmaSessionScope(const MagmaSessionScope&) = delete;
MagmaSessionScope& operator=(const MagmaSessionScope&) = delete;
private:
MagmaSession* m_previous = nullptr;
};
// Binds `session` to the calling thread until the next call (nullptr unbinds). For the apply
// thread, which binds its backend once and runs it for the rest of its life.
void BindMagmaSessionToThisThread(MagmaSession* session);
// The calling thread's bound session, or nullptr (the process-wide default answers).
MagmaSession* BoundMagmaSession();
template <class T>
class SessionLocal {
public:
SessionLocal() : m_index(Detail::RegisterSessionLocal(&Construct, &Destroy)) {}
SessionLocal(const SessionLocal&) = delete;
SessionLocal& operator=(const SessionLocal&) = delete;
T& Get() const {
return *static_cast<T*>(Detail::SessionLocalSlot(Detail::CurrentMagmaSession(), m_index));
}
T& operator*() const { return Get(); }
T* operator->() const { return &Get(); }
private:
static void* Construct() { return new T(); }
static void Destroy(void* object) { delete static_cast<T*>(object); }
const Uint32 m_index;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -66,7 +66,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
FreeRetiredCommandBuffers(frame);
}
vkFreeCommandBuffers(device, commandPool, frameCount * 2, commandBuffers.data());
if (!m_spareCommandBuffers.empty())
vkFreeCommandBuffers(device, commandPool, static_cast<Uint32>(m_spareCommandBuffers.size()),
m_spareCommandBuffers.data());
}
m_spareCommandBuffers.clear();
m_frames.clear();
currentFrameIndex = 0;
m_device = VK_NULL_HANDLE;
@@ -300,6 +304,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return packet;
}
VkResult FrameContext::AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain,
Uint32& outImageIndex) {
auto& frame = GetCurrent();
const VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore,
VK_NULL_HANDLE, &outImageIndex);
if (result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR) frame.imageAvailableSemaphoreConsumed = false;
return result;
}
VkResult FrameContext::WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout, VkFence acquireFence) {
auto& frame = GetCurrent();
@@ -353,21 +366,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(!frame.isPreCommandRecording,
"RetireCurrentCommandBuffer called while the pre-pass stream is still recording");
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
VkCommandBuffer replacement = VK_NULL_HANDLE;
VkResult result = vkAllocateCommandBuffers(m_device, &allocInfo, &replacement);
VkResult result = TakeCommandBuffer(replacement);
if (result != VK_SUCCESS) {
return result;
}
if (retirePreCommandBuffer) {
VkCommandBuffer preReplacement = VK_NULL_HANDLE;
result = vkAllocateCommandBuffers(m_device, &allocInfo, &preReplacement);
result = TakeCommandBuffer(preReplacement);
if (result != VK_SUCCESS) {
vkFreeCommandBuffers(m_device, m_commandPool, 1, &replacement);
RecycleCommandBuffer(replacement);
return result;
}
frame.retiredCommandBuffers.push_back({frame.preCommandBuffer, frame.lastSubmitIndex});
@@ -402,8 +410,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
SizeT completedCount = 0;
while (completedCount < frame.retiredCommandBuffers.size() &&
frame.retiredCommandBuffers[completedCount].submitIndex <= completedSubmitIndex) {
vkFreeCommandBuffers(m_device, m_commandPool, 1,
&frame.retiredCommandBuffers[completedCount].commandBuffer);
RecycleCommandBuffer(frame.retiredCommandBuffers[completedCount].commandBuffer);
++completedCount;
}
if (completedCount > 0) {
@@ -413,6 +420,31 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkResult FrameContext::TakeCommandBuffer(VkCommandBuffer& out) {
if (!m_spareCommandBuffers.empty()) {
out = m_spareCommandBuffers.back();
m_spareCommandBuffers.pop_back();
return VK_SUCCESS;
}
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = m_commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;
return vkAllocateCommandBuffers(m_device, &allocInfo, &out);
}
void FrameContext::RecycleCommandBuffer(VkCommandBuffer commandBuffer) {
// A handful covers the buffers in flight between two completions; beyond that, free.
constexpr SizeT kSpareCommandBuffers = 8;
if (commandBuffer == VK_NULL_HANDLE) return;
if (m_spareCommandBuffers.size() < kSpareCommandBuffers) {
m_spareCommandBuffers.push_back(commandBuffer);
return;
}
vkFreeCommandBuffers(m_device, m_commandPool, 1, &commandBuffer);
}
void FrameContext::FreeAllRetiredCommandBuffers() {
for (auto& frame : m_frames) {
FreeRetiredCommandBuffers(frame);
@@ -101,6 +101,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkImageLayout presentLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR);
SubmitInfoPacket GetSubmitInfo(Bool shouldSubmitCommandBuffer, Uint32 swapchainImageIndex) const;
PresentInfoPacket GetPresentInfo(VkSwapchainKHR swapchain, Uint32 imageIndex) const;
// Acquires into the current slot's semaphore WITHOUT the slot-fence wait: for a surface
// brought up mid-frame, whose slot fence this frame has already waited and reset. The
// slot's semaphore must be unsignaled with no pending signal (consumed).
VkResult AcquireNextImageInCurrentSlot(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex);
// The per-swapchain-image render-finished semaphores belong to one swapchain; a renderer
// that parks a surface parks them with it.
void ExchangeSwapchainSemaphores(Vector<VkSemaphore>& other) { std::swap(m_swapchainImageRenderFinishedSemaphores, other); }
VkResult WaitAndAcquireNextImage(VkDevice device, VkSwapchainKHR swapchain, Uint32& outImageIndex,
Uint64 timeout = UINT64_MAX, VkFence acquireFence = VK_NULL_HANDLE);
@@ -143,5 +150,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Stored at Initialize for retired-command-buffer management.
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_commandPool = VK_NULL_HANDLE;
// Completed command buffers kept for the next retirement instead of being freed and
// reallocated: the pool resets a buffer implicitly at its next vkBeginCommandBuffer.
Vector<VkCommandBuffer> m_spareCommandBuffers;
VkResult TakeCommandBuffer(VkCommandBuffer& out);
void RecycleCommandBuffer(VkCommandBuffer commandBuffer);
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,202 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "GpuProgressMarkers.h"
#include <MG_Util/Debug/Log.h>
namespace MobileGL::MG_Backend::DirectVulkan {
namespace Watch = MG_Backend::GpuHangWatch;
void GpuProgressMarkers::Init(VkDevice device, Uint32 queueFamilyIndex) {
if (m_device != VK_NULL_HANDLE || device == VK_NULL_HANDLE) return;
if (Watch::BudgetMs() == 0) {
MGLOG_I("Magma: GPU hang watch off (MOBILEGL_GPU_HANG_BUDGET_MS=0)");
return;
}
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.queueFamilyIndex = queueFamilyIndex;
if (vkCreateCommandPool(device, &poolInfo, nullptr, &m_pool) != VK_SUCCESS) {
MGLOG_W("Magma: GPU hang watch off - no command pool for its markers");
m_pool = VK_NULL_HANDLE;
return;
}
m_device = device;
{
const std::lock_guard<std::mutex> lock(m_mutex);
if (!AddSlot()) {
MGLOG_W("Magma: GPU hang watch off - the device cannot make its progress markers");
vkDestroyCommandPool(m_device, m_pool, nullptr);
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
return;
}
}
m_watch = Watch::Register(this, &Sample, &OnHung);
MGLOG_I("Magma: GPU hang watch on - a submission the GPU started and has not finished after %u ms ends "
"its session",
Watch::BudgetMs());
}
void GpuProgressMarkers::Shutdown() {
// The watch first, outside m_mutex: Unregister waits out a sample in progress, which takes it.
Watch::Unregister(m_watch);
m_watch = 0;
if (m_device == VK_NULL_HANDLE) return;
const std::lock_guard<std::mutex> lock(m_mutex);
for (Slot& slot : m_slots) {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
}
m_slots.clear();
if (m_pool != VK_NULL_HANDLE) vkDestroyCommandPool(m_device, m_pool, nullptr); // frees the buffers
m_pool = VK_NULL_HANDLE;
m_device = VK_NULL_HANDLE;
}
Bool GpuProgressMarkers::AddSlot() {
if (m_slots.size() >= kMaxSlots) return false;
Slot slot;
const VkEventCreateInfo eventInfo{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO};
VkCommandBuffer buffers[2] = {VK_NULL_HANDLE, VK_NULL_HANDLE};
VkCommandBufferAllocateInfo allocInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
allocInfo.commandPool = m_pool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 2;
const auto fail = [&] {
if (slot.Started != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Started, nullptr);
if (slot.Finished != VK_NULL_HANDLE) vkDestroyEvent(m_device, slot.Finished, nullptr);
if (buffers[0] != VK_NULL_HANDLE) vkFreeCommandBuffers(m_device, m_pool, 2, buffers);
return false;
};
if (vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Started) != VK_SUCCESS ||
vkCreateEvent(m_device, &eventInfo, nullptr, &slot.Finished) != VK_SUCCESS ||
vkAllocateCommandBuffers(m_device, &allocInfo, buffers) != VK_SUCCESS)
return fail();
// Prerecorded once and resubmitted for every submission the slot brackets; SIMULTANEOUS_USE
// because the slot is reused once its `finished` event is set, which can be a moment before
// the submission that set it formally leaves the pending state.
VkCommandBufferBeginInfo beginInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
if (vkBeginCommandBuffer(buffers[0], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[0], slot.Started, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT);
if (vkEndCommandBuffer(buffers[0]) != VK_SUCCESS) return fail();
if (vkBeginCommandBuffer(buffers[1], &beginInfo) != VK_SUCCESS) return fail();
vkCmdSetEvent(buffers[1], slot.Finished, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT);
if (vkEndCommandBuffer(buffers[1]) != VK_SUCCESS) return fail();
slot.Begin = buffers[0];
slot.End = buffers[1];
m_slots.push_back(slot);
return true;
}
GpuProgressMarkers::Bracket GpuProgressMarkers::Acquire() {
Bracket bracket;
if (m_device == VK_NULL_HANDLE) return bracket;
const std::lock_guard<std::mutex> lock(m_mutex);
if (m_lost) return bracket;
Slot* free = nullptr;
for (Slot& slot : m_slots) {
if (slot.Claimed) continue;
// Retire what finished since the watch last looked: frames come faster than its polls.
if (slot.InFlight && vkGetEventStatus(m_device, slot.Finished) == VK_EVENT_SET) slot.InFlight = false;
if (!slot.InFlight && free == nullptr) free = &slot;
}
if (free == nullptr) {
if (!AddSlot()) return bracket;
free = &m_slots.back();
}
if (free->Dirty) {
if (vkResetEvent(m_device, free->Started) != VK_SUCCESS ||
vkResetEvent(m_device, free->Finished) != VK_SUCCESS)
return bracket;
free->Dirty = false;
}
free->Claimed = true;
bracket.Begin = free->Begin;
bracket.End = free->End;
bracket.Slot = static_cast<Uint32>(free - m_slots.data());
return bracket;
}
void GpuProgressMarkers::Submitted(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
Slot& slot = m_slots[bracket.Slot];
slot.Claimed = false;
slot.InFlight = true;
slot.Dirty = true;
slot.Id = m_nextId++;
}
void GpuProgressMarkers::Abandon(const Bracket& bracket) {
if (!bracket.Valid()) return;
const std::lock_guard<std::mutex> lock(m_mutex);
if (bracket.Slot >= m_slots.size()) return;
// A failed vkQueueSubmit may still have consumed nothing or something: reset before reuse.
m_slots[bracket.Slot].Claimed = false;
m_slots[bracket.Slot].Dirty = true;
}
Bool GpuProgressMarkers::Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
const std::lock_guard<std::mutex> lock(markers->m_mutex);
if (markers->m_lost || markers->m_device == VK_NULL_HANDLE) return false;
for (Slot& slot : markers->m_slots) {
if (!slot.InFlight) continue;
const VkResult finished = vkGetEventStatus(markers->m_device, slot.Finished);
const VkResult started = vkGetEventStatus(markers->m_device, slot.Started);
if ((finished != VK_EVENT_SET && finished != VK_EVENT_RESET) ||
(started != VK_EVENT_SET && started != VK_EVENT_RESET)) {
// VK_ERROR_DEVICE_LOST: the driver already reset the device and says so itself.
markers->m_lost = true;
return false;
}
if (finished == VK_EVENT_SET) {
slot.InFlight = false;
continue;
}
out.push_back({slot.Id, started == VK_EVENT_SET, false});
}
return true;
}
void GpuProgressMarkers::OnHung(void* self, Uint64 submissionId, Uint64 runningMs) {
auto* markers = static_cast<GpuProgressMarkers*>(self);
markers->m_hungRunningMs.store(runningMs, std::memory_order_release);
markers->m_hung.store(true, std::memory_order_release);
MGLOG_E("Magma: submission %llu of this session's VkDevice has run %llu ms on the GPU and has not finished - "
"it is what stops the GPU for every process; the session ends at its next record and submits "
"nothing more (budget %u ms, MOBILEGL_GPU_HANG_BUDGET_MS)",
static_cast<unsigned long long>(submissionId), static_cast<unsigned long long>(runningMs),
MG_Backend::GpuHangWatch::BudgetMs());
}
VkResult GpuProgressMarkers::SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence,
const Bracket& bracket) {
constexpr Uint32 kMaxOwn = 6;
if (!bracket.Valid() || info.commandBufferCount == 0 || info.commandBufferCount > kMaxOwn)
return vkQueueSubmit(queue, 1, &info, fence);
VkCommandBuffer buffers[kMaxOwn + 2];
Uint32 count = 0;
buffers[count++] = bracket.Begin;
for (Uint32 i = 0; i < info.commandBufferCount; ++i) buffers[count++] = info.pCommandBuffers[i];
buffers[count++] = bracket.End;
const VkCommandBuffer* own = info.pCommandBuffers;
const Uint32 ownCount = info.commandBufferCount;
info.pCommandBuffers = buffers;
info.commandBufferCount = count;
const VkResult result = vkQueueSubmit(queue, 1, &info, fence);
info.pCommandBuffers = own;
info.commandBufferCount = ownCount;
return result;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,95 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/GpuProgressMarkers.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "../VkIncludes.h"
#include <Includes.h>
#include <MG_Backend/GpuHangWatch.h>
#include <atomic>
#include <mutex>
namespace MobileGL::MG_Backend::DirectVulkan {
// WHICH OF THIS DEVICE'S SUBMISSIONS THE GPU HAS STARTED, AND WHICH IT HAS FINISHED
// (GpuHangWatch.h says why that is the whole attribution of a GPU hang).
//
// A frame's submission is bracketed by two prerecorded one-command buffers: the first sets a
// `started` event at TOP_OF_PIPE, the last sets a `finished` event once everything before it in
// the queue has completed. The host reads both with vkGetEventStatus from the watch thread - no
// fence is shared with the renderer's own waits and resets. On Adreno 750 a submission whose
// fragment shader never ends reads started=SET from its first poll until the kernel resets the
// GPU, and a submission queued behind it reads started=RESET for the whole stall.
//
// The renderer's thread brackets submissions (Acquire / Submitted / Abandon) and asks Hung(); the
// watch thread samples. Everything Vulkan the two share - the events - is touched under m_mutex.
class GpuProgressMarkers {
public:
struct Bracket {
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint32 Slot = ~0u;
Bool Valid() const { return Slot != ~0u; }
};
GpuProgressMarkers() = default;
~GpuProgressMarkers() { Shutdown(); }
GpuProgressMarkers(const GpuProgressMarkers&) = delete;
GpuProgressMarkers& operator=(const GpuProgressMarkers&) = delete;
// Off (no brackets, never hung) when the budget is 0 or the device cannot make events.
void Init(VkDevice device, Uint32 queueFamilyIndex);
// Before the device goes; its queue must be idle. Stops the watch first.
void Shutdown();
// The two buffers to put first and last in one submission (an invalid bracket when off or
// when every slot is in flight: the submission then goes unwatched).
Bracket Acquire();
// The submission carrying `bracket` was accepted by vkQueueSubmit / was not submitted.
void Submitted(const Bracket& bracket);
void Abandon(const Bracket& bracket);
// The watch named one of this device's submissions as hanging the GPU.
Bool Hung() const { return m_hung.load(std::memory_order_acquire); }
Uint64 HungRunningMs() const { return m_hungRunningMs.load(std::memory_order_acquire); }
Bool Active() const { return m_watch != 0; }
// vkQueueSubmit of `info` with `bracket` wrapped around its command buffers (which it puts
// back before returning). At most 6 buffers of the caller's.
static VkResult SubmitBracketed(VkQueue queue, VkSubmitInfo& info, VkFence fence, const Bracket& bracket);
private:
struct Slot {
VkEvent Started = VK_NULL_HANDLE;
VkEvent Finished = VK_NULL_HANDLE;
VkCommandBuffer Begin = VK_NULL_HANDLE;
VkCommandBuffer End = VK_NULL_HANDLE;
Uint64 Id = 0;
Bool InFlight = false; // submitted and not yet seen finished
Bool Claimed = false; // handed out by Acquire, not yet submitted or abandoned
Bool Dirty = false; // its events were set by a past submission: reset before reuse
};
static constexpr Uint32 kMaxSlots = 16;
Bool AddSlot();
static Bool Sample(void* self, Vector<MG_Backend::GpuHangWatch::SubmissionProgress>& out);
static void OnHung(void* self, Uint64 submissionId, Uint64 runningMs);
std::mutex m_mutex;
VkDevice m_device = VK_NULL_HANDLE;
VkCommandPool m_pool = VK_NULL_HANDLE;
Vector<Slot> m_slots;
Uint64 m_nextId = 1;
Uint64 m_watch = 0;
Bool m_lost = false;
std::atomic<Bool> m_hung{false};
std::atomic<Uint64> m_hungRunningMs{0};
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,37 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/OffscreenSurfaceRoute.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// How an offscreen (pbuffer) surface target gets its VkSurfaceKHR on Android, decided from what the
// INSTANCE has, not from what the loader could offer. A renderer created for a window enabled
// VK_KHR_android_surface and nothing headless, so its later offscreen targets (a second client
// surface, or the placeholder a suspended server window draws into) must take the AImageReader
// route; asking that instance for vkCreateHeadlessSurfaceEXT answers null. Pure, so a host case pins
// it without a driver.
#pragma once
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
enum class OffscreenSurfaceRoute : Uint8 {
Headless, // vkCreateHeadlessSurfaceEXT
AndroidImageReader, // vkCreateAndroidSurfaceKHR on an AImageReader's window nobody displays
Unavailable, // neither: the target cannot be built (the caller refuses, by name)
};
// `headlessEnabled`: the instance was created with VK_EXT_headless_surface.
// `headlessEntryPoint`: vkGetInstanceProcAddr answered vkCreateHeadlessSurfaceEXT.
// `androidSurfaceEnabled`: the instance was created with VK_KHR_android_surface.
constexpr OffscreenSurfaceRoute ChooseOffscreenSurfaceRoute(Bool headlessEnabled, Bool headlessEntryPoint,
Bool androidSurfaceEnabled) {
if (headlessEnabled && headlessEntryPoint) return OffscreenSurfaceRoute::Headless;
if (androidSurfaceEnabled) return OffscreenSurfaceRoute::AndroidImageReader;
return OffscreenSurfaceRoute::Unavailable;
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -391,6 +391,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const {
MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty");
// The assert compiles out of shipping builds, and a program declined for an invalid
// module (VkProgramObject::invalidSpirvDeclined) has no stages at all: a caller that
// forgot to refuse it must get a skipped draw, not a stageCount=0 pipeline.
if (payload.stages == nullptr || payload.stages->empty()) {
MGLOG_E_ONCE("PipelineFactory::CreatePipeline: refusing a pipeline with no shader stages "
"(programHash=0x%llx); its draws are skipped",
static_cast<unsigned long long>(payload.programHash));
return VK_NULL_HANDLE;
}
MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null");
MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null");
MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null");
@@ -199,7 +199,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
UnorderedMap<HashType, PipelineCacheEntry> m_cache;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
static inline Bool s_suppressBlendedDepthWrite = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -12,7 +12,9 @@
#include "MG_Util/ShaderTranspiler/ShaderCompiler.h"
#include "MG_Util/ShaderTranspiler/SpvcSession.h"
#include "MG_Util/ShaderTranspiler/Types.h"
#include <Config.h>
#include <algorithm>
#include <atomic>
#include <bit>
#include <cmath>
#include <cstring>
@@ -1313,6 +1315,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
});
if (!ok) return false;
// An access chain into the builtin (gl_FragCoord.y is one) carries an Input
// pointer type. Based on the Private copy it has to carry a Private one, and so
// does every chain built on top of it - otherwise the module hands the driver a
// pointer whose storage class disagrees with its base, which is invalid SPIR-V
// and reads an undefined value. Plan every retype before touching anything, so a
// shape this pass does not model declines the whole rewrite.
Vector<Pair<Instruction*, Uint32>> retypes;
Vector<Instruction*> chains;
for (Instruction* user : users) {
if (IsAccessChain(user->opcode())) chains.push_back(user);
}
while (!chains.empty()) {
Instruction* chain = chains.back();
chains.pop_back();
const Uint32 privateType = PrivatePointerTypeFor(chain->type_id());
if (privateType == 0) return false;
retypes.emplace_back(chain, privateType);
context()->get_def_use_mgr()->ForEachUser(chain, [&](Instruction* next) {
if (IsAccessChain(next->opcode()) && next->NumInOperands() > 0 &&
next->GetSingleWordInOperand(0) == chain->result_id()) {
chains.push_back(next);
}
});
}
for (Instruction* user : users) {
for (Uint32 i = 0; i < user->NumInOperands(); ++i) {
auto& operand = user->GetInOperand(i);
@@ -1323,9 +1349,32 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
context()->AnalyzeUses(user);
}
for (auto& [chain, privateType] : retypes) {
chain->SetResultType(privateType);
context()->AnalyzeUses(chain);
}
return true;
}
static Bool IsAccessChain(spv::Op opcode) {
return opcode == spv::Op::OpAccessChain || opcode == spv::Op::OpInBoundsAccessChain ||
opcode == spv::Op::OpPtrAccessChain || opcode == spv::Op::OpInBoundsPtrAccessChain;
}
// The Private-storage twin of an Input pointer type, or 0 when `pointerTypeId` is not
// an Input pointer (then the chain is not the shape this pass rewrites).
Uint32 PrivatePointerTypeFor(Uint32 pointerTypeId) {
using namespace spvtools::opt;
Instruction* pointerType = context()->get_def_use_mgr()->GetDef(pointerTypeId);
if (!pointerType || pointerType->opcode() != spv::Op::OpTypePointer) return 0;
if (static_cast<spv::StorageClass>(pointerType->GetSingleWordInOperand(0)) !=
spv::StorageClass::Input) {
return 0;
}
return context()->get_type_mgr()->FindPointerToType(pointerType->GetSingleWordInOperand(1),
spv::StorageClass::Private);
}
Uint32 m_framebufferHeight = 0;
};
@@ -1861,35 +1910,38 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return spvtools::Optimizer::PassToken(MakeUnique<GlToVulkanPositionFixPass>(transformFlags));
}
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
Uint32 framebufferHeight) {
if (input.empty()) {
output.clear();
return true;
}
if (framebufferHeight == 0) {
output = input;
return true;
}
} // namespace
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
output = input;
}
return success;
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output,
Uint32 framebufferHeight) {
if (input.empty()) {
output.clear();
return true;
}
if (framebufferHeight == 0) {
output = input;
return true;
}
spvtools::Optimizer optimizer(SPV_ENV_VULKAN_1_3);
spvtools::OptimizerOptions options;
options.set_run_validator(false); // see TransformSpirvForExplicitLod0Sampling
optimizer.SetMessageConsumer([](spv_message_level_t, const char*, const spv_position_t&,
const char* message) {
MGLOG_E("Vulkan: fragcoord y-flip pass: %s", message != nullptr ? message : "");
});
optimizer.RegisterPass(
spvtools::Optimizer::PassToken(MakeUnique<GlFragCoordYFlipPass>(framebufferHeight)));
const Bool success = optimizer.Run(input.data(), input.size(), &output, options);
if (!success) {
MGLOG_E("Vulkan: failed to run the gl_FragCoord y-flip pass; keeping the original module");
output = input;
}
return success;
}
namespace {
Bool TransformSpirvForXfbCapture(const Vector<Uint>& input, Vector<Uint>& output,
const MagmaProgramSource& program) {
if (input.empty()) {
@@ -2223,8 +2275,84 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return false;
}
}
// ScreenModuleForDriver's memory. Keyed by content, never by program: the same bytes get
// the same answer whichever program, variant or session carries them.
struct DriverModuleVerdictCache {
struct Entry {
Uint64 wordCount = 0;
Bool valid = true;
String reason;
};
// Generous for any one application's distinct stage modules; past it the whole table
// is dropped rather than tracked per entry - a re-validation is the only cost.
static constexpr SizeT kMaxEntries = 8192;
std::mutex mutex;
UnorderedMap<Uint64, Entry> entries;
std::atomic<Uint64> validatorRuns{0};
};
DriverModuleVerdictCache& GetDriverModuleVerdictCache() {
// Leaked on purpose: screened from session threads that may outlive static destruction.
static auto* cache = new DriverModuleVerdictCache();
return *cache;
}
} // namespace
ProgramFactory::DriverModuleVerdict ProgramFactory::ScreenModuleForDriver(const Vector<Uint>& spirv,
Bool allowInvalid, String* outReason) {
if (outReason != nullptr) outReason->clear();
if (spirv.empty()) return DriverModuleVerdict::Valid;
auto& cache = GetDriverModuleVerdictCache();
const Uint64 key = XXH64(spirv.data(), spirv.size() * sizeof(Uint), 0);
Bool valid = true;
String reason;
Bool known = false;
{
const std::lock_guard<std::mutex> lock(cache.mutex);
const auto it = cache.entries.find(key);
if (it != cache.entries.end() && it->second.wordCount == spirv.size()) {
known = true;
valid = it->second.valid;
reason = it->second.reason;
}
}
if (!known) {
// Outside the lock: two threads screening the same new module both validate, and
// agree; nothing waits on another session's spirv-val.
MG_Util::ShaderTranspiler::ShaderCompiler::PrepareSpirvValidation();
cache.validatorRuns.fetch_add(1, std::memory_order_relaxed);
spv_context context = spvContextCreate(GetSpirvTargetEnv(spirv));
spv_validator_options options = spvValidatorOptionsCreate();
spvValidatorOptionsSetFriendlyNames(options, true);
spv_const_binary_t binary = {spirv.data(), spirv.size()};
spv_diagnostic diagnostic = nullptr;
const spv_result_t result = spvValidateWithOptions(context, options, &binary, &diagnostic);
valid = result == SPV_SUCCESS;
if (!valid) {
reason = diagnostic != nullptr && diagnostic->error != nullptr ? diagnostic->error : "<no diagnostic>";
// The process-wide counter the validating test lanes assert on.
MG_Util::ShaderTranspiler::ShaderCompiler::NoteSpirvValidationFailure();
}
spvDiagnosticDestroy(diagnostic);
spvValidatorOptionsDestroy(options);
spvContextDestroy(context);
const std::lock_guard<std::mutex> lock(cache.mutex);
if (cache.entries.size() >= DriverModuleVerdictCache::kMaxEntries) cache.entries.clear();
cache.entries[key] = DriverModuleVerdictCache::Entry{spirv.size(), valid, reason};
}
if (valid) return DriverModuleVerdict::Valid;
if (outReason != nullptr) *outReason = Move(reason);
return allowInvalid ? DriverModuleVerdict::AdmittedInvalid : DriverModuleVerdict::DeclinedInvalid;
}
Uint64 ProgramFactory::DriverModuleValidatorRuns() {
return GetDriverModuleVerdictCache().validatorRuns.load(std::memory_order_relaxed);
}
// A shader that assigns gl_FragDepth (SPIR-V DepthReplacing) supplies depth itself
// instead of taking the pipeline's interpolated Z, so a driver that varies the vertex
// position math between pipelines cannot desynchronize it; the blended depth-write
@@ -2401,6 +2529,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return SamplerNumericDomain::UnsignedInteger;
case GL_SAMPLER_1D:
case GL_SAMPLER_2D:
case GL_SAMPLER_EXTERNAL_OES:
case GL_SAMPLER_3D:
case GL_SAMPLER_CUBE:
case GL_SAMPLER_2D_RECT:
@@ -2591,6 +2720,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY:
return TextureTarget::TextureCubeMapArray;
case GL_SAMPLER_2D:
case GL_SAMPLER_EXTERNAL_OES: // a 2D texture below the frontend
case GL_SAMPLER_2D_SHADOW:
case GL_INT_SAMPLER_2D:
case GL_UNSIGNED_INT_SAMPLER_2D:
@@ -3764,27 +3894,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool remapOk = RemapDescriptorBindingsForVulkan(moduleSpirvs, m_maxBindings, moduleSpirvs);
MOBILEGL_ASSERT(remapOk, "ProgramFactory::GetOrCreateProgram: descriptor binding remap failed");
// Last look at the exact bytes the driver receives, in EVERY build rather than only in
// DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed, and for every stage BEFORE any
// module is created. By here the descriptor bindings have been remapped and the layout
// about to be reflected describes the remapped module, so there is no module left that
// is both valid and consistent with it to fall back to (the recovery that exists lives
// one step earlier, at the clip/XFB fixups). What is left is to keep the bytes away from
// the driver: a driver is not a validating entry point, and an invalid module it accepts
// can fault the GPU into device loss (a frontend translation once put a sampler inside
// the default uniform block, and Adreno page-faulted reading its "descriptor" out of
// uniform-buffer bytes). One invalid stage declines the whole program: no shader module
// of it is created, and every draw or dispatch of it is refused by name
// (VkProgramObject::invalidSpirvDeclined). MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV hands the
// bytes over anyway, for diagnosing a driver that copes.
for (SizeT i = 0; i < moduleCount; ++i) {
String reason;
const DriverModuleVerdict verdict = ScreenModuleForDriver(
moduleSpirvs[i], MG_Config::Features.MagmaAllowInvalidSpirv, &reason);
if (verdict == DriverModuleVerdict::DeclinedInvalid) {
MGLOG_E_ONCE("ProgramFactory: declining program %u - its stage %d module fails SPIR-V validation (%s); "
"no shader module is created and its draws are refused. "
"MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV=1 hands such modules to the driver anyway.",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
entry.invalidSpirvDeclined = true;
break;
}
if (verdict == DriverModuleVerdict::AdmittedInvalid) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d "
"because MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV is set (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]), reason.c_str());
}
}
for (SizeT i = 0; i < moduleCount && !entry.invalidSpirvDeclined; ++i) {
auto& moduleSpv = moduleSpirvs[i];
if (moduleSpv.empty()) continue;
// Last look at the exact bytes the driver receives, in EVERY build rather than only
// in DEBUG or with MOBILEGL_ENABLE_SPIRV_VALIDATION armed. This one only reports:
// by here the descriptor bindings have been remapped and the layout about to be
// reflected describes the remapped module, so there is no module left that is both
// valid and consistent with it to fall back to. The recovery lives one step earlier,
// at the clip/XFB fixups (see the revert there) - which is where a transform can
// introduce a reference to a delisted interface variable, the failure this whole
// guard exists for. Anything that reaches this line names itself in the log of a
// shipping build instead of dying anonymously inside the driver.
SpirvValidationFailure finalFailure{};
if (!ValidateTransformedSpirv(moduleSpv, stages[i], program.GetExternalIndex(), &finalFailure)) {
MGLOG_E_ONCE("ProgramFactory: handing vkCreateShaderModule an INVALID module for program %u stage %d - "
"a backend transform after the clip/XFB fixups broke it (%s)",
program.GetExternalIndex(), static_cast<Int>(stages[i]),
finalFailure.message.c_str());
}
// Does the stage the driver will treat as the last pre-rasterization one actually
// carry Xfb? Asked of the FINAL bytes, so it answers for whatever the whole transform
// chain produced - a rewound clip/XFB backstop, a capture pass that resolved no
@@ -9,6 +9,7 @@
#pragma once
#include "../VkIncludes.h"
#include "../MagmaSession.h"
#include "PipelineFactory.h"
#include "MagmaProgramSource.h"
#include "MG_State/GLState/ProgramState/ShaderObject.h"
@@ -228,6 +229,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// the cache entry, so every later draw of the same program variant reads the same
// answer instead of re-deciding it.
Bool pointSizeCapabilityUnsupported = false;
// One of the program's final modules fails SPIR-V validation, so none of its modules
// was created and every draw or dispatch of it is refused by name (the ProgramInvalidSpirv /
// ComputeProgramInvalidSpirv wire declines) - see ProgramFactory::ScreenModuleForDriver.
// Sticky like the flag above: it lives on the cache entry, so the one log line is honest.
Bool invalidSpirvDeclined = false;
Bool needsPassthroughTessControl = false;
// ...and the pass-through this renderer can synthesize carries gl_Position and
// nothing else, so it is only correct when the evaluation stage's inputs are
@@ -251,7 +257,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// entry pointer re-stamps use through a const reference (StampProgramUse).
mutable Uint64 lastUsedFrame = 0;
static inline VkDevice s_device = VK_NULL_HANDLE;
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
VkProgramObject() = default;
VkProgramObject(const VkProgramObject&) = delete;
@@ -288,6 +294,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -309,6 +320,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -352,6 +366,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
storageBlockIndexByBinding = std::move(other.storageBlockIndexByBinding);
hasStorageImages = other.hasStorageImages;
declinedDescriptors = other.declinedDescriptors;
// The three refusal flags travel like every other field: a robin-hood insert swaps
// entries, and a flag left behind would refuse (or admit) the wrong program.
invalidSpirvDeclined = other.invalidSpirvDeclined;
xfbCaptureDeclined = other.xfbCaptureDeclined;
pointSizeCapabilityUnsupported = other.pointSizeCapabilityUnsupported;
globalUboBinding = other.globalUboBinding;
activeVertexInputLocationMask = other.activeVertexInputLocationMask;
vertexInputTypes = other.vertexInputTypes;
@@ -373,6 +392,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
other.pipelineLayout = VK_NULL_HANDLE;
other.hasStorageImages = false;
other.declinedDescriptors = false;
other.invalidSpirvDeclined = false;
other.xfbCaptureDeclined = false;
other.pointSizeCapabilityUnsupported = false;
other.globalUboBinding = -1;
other.activeVertexInputLocationMask = 0;
other.activeFragmentOutputLocationMask = 0;
@@ -395,18 +417,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
private:
void Destroy() {
if (s_device != VK_NULL_HANDLE) {
if (s_device.Get() != VK_NULL_HANDLE) {
if (pipelineLayout != VK_NULL_HANDLE) {
vkDestroyPipelineLayout(s_device, pipelineLayout, nullptr);
vkDestroyPipelineLayout(s_device.Get(), pipelineLayout, nullptr);
pipelineLayout = VK_NULL_HANDLE;
}
if (descriptorSetLayout != VK_NULL_HANDLE) {
vkDestroyDescriptorSetLayout(s_device, descriptorSetLayout, nullptr);
vkDestroyDescriptorSetLayout(s_device.Get(), descriptorSetLayout, nullptr);
descriptorSetLayout = VK_NULL_HANDLE;
}
for (auto module : modules) {
if (module != VK_NULL_HANDLE) {
vkDestroyShaderModule(s_device, module, nullptr);
vkDestroyShaderModule(s_device.Get(), module, nullptr);
}
}
}
@@ -463,7 +485,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_enableSpirvValidation(enableSpirvValidation),
m_updateAfterBindLimits(updateAfterBindLimits),
m_subgroupPolicy(subgroupPolicy) {
VkProgramObject::s_device = device;
VkProgramObject::s_device.Get() = device;
}
// Destroys the pass-through tessellation control modules. Runs while the device is
// still alive for the same reason ~VkProgramObject's does: this factory outlives
@@ -503,6 +525,29 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// VkRenderPassManager::OnPresent's sweep.
void OnFrameBoundary();
// What the last look at a module's final bytes - the exact words vkCreateShaderModule
// would receive - decided.
enum class DriverModuleVerdict : Uint8 {
Valid, // validates: handed to the driver
DeclinedInvalid, // fails validation: never handed over, the program's work is declined
AdmittedInvalid, // fails validation, handed over anyway (MOBILEGL_MAGMA_ALLOW_INVALID_SPIRV)
};
// Validates `spirv` for the Vulkan environment its version word names and turns the
// result into a verdict. A driver is not a validating entry point: Adreno page-faulted
// into device loss on a module that carried a sampler inside a uniform block, Mali
// SIGSEGVs inside pipeline creation. So an invalid module is declined unless
// `allowInvalid` (the escape hatch) says to hand it over regardless.
//
// The validator's answer is remembered per module content (64-bit hash plus word count),
// process-wide: a program variant, an evicted-then-rebuilt entry and two programs that
// share a stage all re-screen the same bytes, and only the first pays for spirv-val.
// `outReason` receives the validator's message for an invalid module.
static DriverModuleVerdict ScreenModuleForDriver(const Vector<Uint>& spirv, Bool allowInvalid,
String* outReason = nullptr);
// How many times ScreenModuleForDriver actually ran the validator (cache misses), for
// the tests that pin the cache.
static Uint64 DriverModuleValidatorRuns();
static VkShaderStageFlagBits ToVkStage(ShaderStage stage);
static VkFormat ConvertSpirvImageFormatToVkFormat(SpvImageFormat format);
static SamplerNumericDomain UniformTypeToSamplerNumericDomain(GLenum glType);
@@ -655,6 +700,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// and adds only a handful of values, so it does not move the cap in practice.
static constexpr SizeT kMaxPassthroughTessControlStages = 64;
UnorderedMap<Uint64, VkPipelineShaderStageCreateInfo> m_passthroughTessControlStages;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
// The FragCoordYFlip variant's module transform: every read of gl_FragCoord goes through a
// Private copy whose y is (framebufferHeight - y). Fails open (output = input, false). Exposed
// so a unit case can validate the bytes it hands the driver.
Bool TransformSpirvForFragCoordYFlip(const Vector<Uint>& input, Vector<Uint>& output, Uint32 framebufferHeight);
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -0,0 +1,27 @@
// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/RenderPassGuard.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#pragma once
#include "../VkIncludes.h"
#include <Includes.h>
namespace MobileGL::MG_Backend::DirectVulkan {
// Ends the render pass that is active on `commandBuffer`, if there is one. Called right before
// recording a command a render pass may not contain (a barrier, a copy, a blit, a clear of an
// image) into a command buffer that may hold an open pass: the wire draw path keeps its pass
// open across consecutive draws to the same attachments, so the resource work that falls
// between two draws must close it first. A pass on another command buffer is left alone.
void EndActiveRenderPassOn(VkCommandBuffer commandBuffer);
// A counter that moves whenever images may have been written since it was last read: a render
// pass ended (its attachments), resource work recorded outside a pass (copies, clears, blits,
// dispatches all close the pass first), or a draw with storage images. A sampled image needs
// the memory barrier that makes earlier writes visible only when this moved since the last one.
Uint64 WireImageWriteEpoch();
void BumpWireImageWriteEpoch();
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -8,6 +8,8 @@
#include "SwapchainObject.h"
#include <span>
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -140,12 +142,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkPresentModeKHR SwapchainObject::ChooseSwapchainPresentMode(
const Vector<VkPresentModeKHR>& availablePresentModes) {
for (auto desiredPresentMode : s_desiredPresentModes) {
for (const auto& presentMode : availablePresentModes) {
if (presentMode == desiredPresentMode) {
return presentMode;
}
const Vector<VkPresentModeKHR>& availablePresentModes, Optional<Int> swapInterval) {
static constexpr VkPresentModeKHR s_unthrottledPresentModes[]{
VK_PRESENT_MODE_IMMEDIATE_KHR, VK_PRESENT_MODE_MAILBOX_KHR, VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_vsyncPresentModes[]{VK_PRESENT_MODE_FIFO_KHR};
static constexpr VkPresentModeKHR s_adaptiveVsyncPresentModes[]{VK_PRESENT_MODE_FIFO_RELAXED_KHR,
VK_PRESENT_MODE_FIFO_KHR};
std::span<const VkPresentModeKHR> ranked = s_desiredPresentModes;
if (swapInterval) {
if (*swapInterval == 0) {
ranked = s_unthrottledPresentModes;
} else if (*swapInterval > 0) {
ranked = s_vsyncPresentModes;
} else {
ranked = s_adaptiveVsyncPresentModes;
}
}
for (auto desiredPresentMode : ranked) {
if (std::find(availablePresentModes.begin(), availablePresentModes.end(), desiredPresentMode) !=
availablePresentModes.end()) {
return desiredPresentMode;
}
}
@@ -153,9 +169,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return availablePresentModes[0];
}
const char* SwapchainObject::GetPresentModeName(VkPresentModeKHR presentMode) {
return string_VkPresentModeKHR(presentMode);
}
void SwapchainObject::Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface,
Uint32 graphicsQueueFamily, Uint32 presentQueueFamily, Uint32 minImageCountHint,
VkExtent2D desiredExtent) {
VkExtent2D desiredExtent, Optional<Int> swapInterval) {
const auto swapchainCapabilities = GetSwapchainCapabilities(physicalDevice, surface);
MOBILEGL_ASSERT(swapchainCapabilities.IsComplete(),
"SwapchainObject::Create failed: incomplete swapchain capabilities");
@@ -174,8 +194,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_D(" %s", string_VkPresentModeKHR(pm));
}
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes);
const auto presentMode = ChooseSwapchainPresentMode(swapchainCapabilities.presentModes, swapInterval);
MGLOG_I("Picked present mode: %s", string_VkPresentModeKHR(presentMode));
m_presentMode = presentMode;
m_supportedPresentModes = swapchainCapabilities.presentModes;
const auto& swapchainCaps = swapchainCapabilities.capabilities;
Uint32 targetImageCount = std::max<Uint32>(minImageCountHint, swapchainCaps.minImageCount);
@@ -270,6 +292,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Fresh swapchain images hold garbage until a render pass stores into them.
m_imageContentDefined.assign(imageCount, false);
m_depthStencilContentDefined.assign(imageCount, false);
// Nothing presented from this chain yet: every buffer age is unknown.
m_ages.Reset(imageCount);
CreateImageViews(device);
CreateDepthStencilResources(device, physicalDevice);
@@ -277,6 +301,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MGLOG_I("Swapchain created, extent = %dx%d, swapchain imageCount = %d", m_extent.width, m_extent.height,
imageCount);
PublishDefaultFramebufferInfo();
}
void SwapchainObject::PublishDefaultFramebufferInfo() const {
const VkExtent2D defaultFramebufferExtent = m_surfaceExtent;
// Properly initialize Default FBO here
auto& defaultFBOInfo = MG_Impl::GLImpl::FramebufferImpl::pDefaultFramebufferInfo;
const Int extentWidth = static_cast<Int>(defaultFramebufferExtent.width);
@@ -466,6 +495,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_imageLayouts.clear();
m_imageContentDefined.clear();
m_depthStencilContentDefined.clear();
m_ages.Reset(0);
m_preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
}
@@ -9,6 +9,7 @@
#pragma once
#include <Includes.h>
#include <MG_Util/Damage/Damage.h>
#include "../VkIncludes.h"
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -26,13 +27,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static SwapchainCapabilities GetSwapchainCapabilities(VkPhysicalDevice physicalDevice, VkSurfaceKHR surface);
static VkSurfaceFormatKHR ChooseSwapchainSurfaceFormat(const Vector<VkSurfaceFormatKHR>& availableFormats);
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes);
// swapInterval is the app's eglSwapInterval, empty until it asks (s_desiredPresentModes
// ranks the modes then). 0 wants IMMEDIATE, else MAILBOX, else FIFO; >= 1 wants FIFO,
// which cannot skip vblanks, so 2 and up pace like 1; a negative interval (adaptive
// vsync) wants FIFO_RELAXED, else FIFO.
static VkPresentModeKHR ChooseSwapchainPresentMode(const Vector<VkPresentModeKHR>& availablePresentModes,
Optional<Int> swapInterval = Nullopt);
static const char* GetPresentModeName(VkPresentModeKHR presentMode);
void Create(VkDevice device, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, Uint32 graphicsQueueFamily,
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent);
Uint32 presentQueueFamily, Uint32 minImageCountHint, VkExtent2D desiredExtent,
Optional<Int> swapInterval);
void Shutdown(VkDevice device);
// Tells the frontend (or, under a transport, the client) the default framebuffer's size
// and formats. Create does this; a renderer switching between surfaces repeats it.
void PublishDefaultFramebufferInfo() const;
VkSwapchainKHR GetHandle() const { return m_swapchain; }
VkPresentModeKHR GetPresentMode() const { return m_presentMode; }
// The surface's present modes as queried when this swapchain was created.
const Vector<VkPresentModeKHR>& GetSupportedPresentModes() const { return m_supportedPresentModes; }
const VkSurfaceFormatKHR& GetSurfaceFormat() const { return m_surfaceFormat; }
VkExtent2D GetExtent() const { return m_extent; }
// Surface-space extent (before the pre-rotation quarter-turn swap) this swapchain was
@@ -67,6 +81,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void SetDepthStencilContentDefined(Uint32 index, Bool defined);
void SetAllDepthStencilContentUndefined();
// EGL_EXT_buffer_age over this chain's images. Ages start unknown with the chain (Create).
// Until a client asks for an age (NoteBufferAgeAsked) a presented image's content is
// treated as undefined, as before; once one has, a presented image keeps it - its age is
// counted and its content stays defined, so the next frame loads it.
void NoteBufferAgeAsked() { m_bufferAgeAsked = true; }
Bool BufferAgeAsked() const { return m_bufferAgeAsked; }
Int32 BufferAgeOf(Uint32 index) const { return m_ages.AgeOf(index); }
// Image `index` was presented.
void OnImagePresented(Uint32 index) { m_ages.OnPresented(index, m_bufferAgeAsked); }
// A swapped frame reached no image: every age is unknown.
void OnFrameDropped() { m_ages.OnFrameDropped(); }
private:
void CreateImageViews(VkDevice device);
void CreateDepthStencilResources(VkDevice device, VkPhysicalDevice physicalDevice);
@@ -79,6 +105,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
VkSwapchainKHR m_swapchain = VK_NULL_HANDLE;
VkPresentModeKHR m_presentMode = VK_PRESENT_MODE_FIFO_KHR;
Vector<VkPresentModeKHR> m_supportedPresentModes;
VkSurfaceFormatKHR m_surfaceFormat{};
VkExtent2D m_extent{};
VkExtent2D m_surfaceExtent{};
@@ -94,5 +122,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Vector<VkImageLayout> m_depthStencilImageLayouts;
Vector<Bool> m_imageContentDefined;
Vector<Bool> m_depthStencilContentDefined;
MG_Util::Damage::SwapchainAgeTracker m_ages;
// Kept across a rebuild: it is the application's habit, not the chain's.
Bool m_bufferAgeAsked = false;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "RenderPassGuard.h"
#include "UniformManager.h"
#include "MG_Backend/DirectVulkan/DirectVulkanResourceState.h"
@@ -251,18 +252,26 @@ namespace MobileGL::MG_Backend::DirectVulkan {
WireDescriptorFatal("image-descriptor-transition");
// A second descriptor/draw can keep GENERAL after a shader write. A layout
// equality fast return is not a memory dependency for that prior write.
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &memory, 0, nullptr, 0, nullptr);
// Only when something may have written an image since the last such barrier
// (WireImageWriteEpoch): with nothing written in between, consecutive draws keep their
// render pass open instead of closing it for a barrier that orders nothing.
if (WireImageWriteEpoch() != m_wireImageBarrierEpoch) {
VkMemoryBarrier memory{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
memory.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
memory.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &memory, 0, nullptr, 0, nullptr);
m_wireImageBarrierEpoch = WireImageWriteEpoch();
}
VkImageViewCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
info.image = resource->image;
info.viewType = type;
info.format = format;
info.subresourceRange = {aspect, level, levels, layer, layers};
if (!storage) {
const Bool alphaIsOne = ResolveTextureFormatInfo(
// An X-format shared image carries undefined alpha bytes, exactly like an expanded RGB.
const Bool alphaIsOne = resource->sharedImageAlphaOne || ResolveTextureFormatInfo(
static_cast<TextureInternalFormat>(record.Desc.InternalFormat)).expandRgbToRgba;
const auto swizzle = [&](Uint32 channel) {
const auto value = record.Params.Swizzle[channel];
@@ -293,11 +302,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
} else {
auto samplerHandle = state.BoundSamplerStates[unit];
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle)) samplerHandle = record.Params.BuiltinSampler;
if (MG_Pipe::MGPipeHandleIsNull(samplerHandle) || samplerHandle.Slot >= state.SamplerCsos.size())
WireDescriptorFatal("sampler-record");
const auto& sampler = state.SamplerCsos[samplerHandle.Slot];
if (!sampler.Live || sampler.Gen != samplerHandle.Gen) WireDescriptorFatal("sampler-record-generation");
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(sampler.Params,
// A texture's built-in sampler CSO can be missing or gone while the texture still names
// it: sampler CSOs are content-addressed and recycled at capacity, and the texture's
// record is only re-pointed by its next set_texture_params (a texture whose params never
// crossed names none at all). Sample with GL's default parameters rather than end the
// session - the DirectGLES arm declines the push in both cases.
using SamplerCsoRecord = std::decay_t<decltype(state.SamplerCsos[0])>;
static const decltype(SamplerCsoRecord::Params) kDefaultSamplerParams{};
const Bool known = !MG_Pipe::MGPipeHandleIsNull(samplerHandle) &&
samplerHandle.Slot < state.SamplerCsos.size();
const SamplerCsoRecord* sampler = known ? &state.SamplerCsos[samplerHandle.Slot] : nullptr;
const Bool stale = sampler == nullptr || !sampler->Live || sampler->Gen != samplerHandle.Gen;
if (stale) {
MGLOG_E_ONCE("Magma wire: sampler CSO {%u, %u} named by texture unit %u is missing or no longer "
"live; sampling with default parameters", samplerHandle.Slot, samplerHandle.Gen, unit);
}
out.sampler = m_samplerManager->GetOrCreateSamplerFromParameters(stale ? kDefaultSamplerParams : sampler->Params,
static_cast<TextureInternalFormat>(record.Desc.InternalFormat),
domain == SamplerNumericDomain::SignedInteger || domain == SamplerNumericDomain::UnsignedInteger,
levels);
@@ -591,6 +611,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_samplerResolveMemoHighWater = 0;
m_setsPerFrame = setsPerFrame;
m_peakDescriptorSetsObserved = 0;
m_trimQuietEpochs = MG_Config::Features.MagmaDescriptorTrimFrames;
// The renderer records its first frame into slot 0 before any BeginFrame runs.
m_epochSlot = 0;
m_epochOpen = true;
m_descriptorSlotTrims = 0;
m_textureManager = textureManager;
m_samplerManager = samplerManager;
@@ -600,6 +625,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.capacitySets = m_setsPerFrame;
frame.quietEpochs = 0;
frame.descriptorPools.clear();
VkDescriptorPool initialPool = VK_NULL_HANDLE;
@@ -654,6 +681,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
frame.capacitySets = 0;
frame.quietEpochs = 0;
}
m_frames.clear();
#if MOBILEGL_BUILD_DISAGGREGATED
@@ -662,6 +691,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_wirePrivateStoragePlaceholders = 0;
m_wireInvalidStorageImagesBindNull = false;
#endif
m_epochSlot = 0;
m_epochOpen = false;
m_bufferManager = nullptr;
m_programFactory = nullptr;
@@ -701,6 +732,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"UniformDescriptorBinder: new descriptor set peak observed=%u (base setsPerFrame=%u, frame=%u, pools=%zu)",
m_peakDescriptorSetsObserved, m_setsPerFrame, frameIndex, frame.descriptorPools.size());
}
// The epoch that just ended is the one of the slot begun last - this slot again on the
// drain path, the previous frame's slot on the Present path. Its peak is still unreset.
if (m_epochOpen && m_epochSlot < m_frames.size()) {
NoteEndedEpochForTrim(m_frames[m_epochSlot].peakAllocatedSetsThisFrame);
}
m_epochSlot = frameIndex;
m_epochOpen = true;
// No pending command buffer references this slot's sets here (see TrimFrameDescriptorPools
// for why that holds at both of BeginFrame's call sites), so a quiet grown slot can be
// handed back whole.
if (m_trimQuietEpochs != 0 && frame.descriptorPools.size() > 1 &&
frame.capacitySets >= kDescriptorTrimMinCapacity && frame.quietEpochs >= m_trimQuietEpochs) {
TrimFrameDescriptorPools(frame, frameIndex);
}
frame.activeDescriptorPoolIndex = 0;
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
@@ -709,11 +754,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
// The frame's descriptor sets are recycled above, so last frame's reuse targets
// are gone: start the per-draw descriptor-reuse cache fresh this frame.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
InvalidateDescriptorSetMemos();
// Re-fingerprint the bound sampler set fresh this frame so any GL object address
// reuse cannot outlive a single frame (see SamplerResolveMemo). Only the entries a
// resolve has actually written can be valid, so the high-water mark bounds the
@@ -747,10 +788,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
entry.second.cursor = 0;
}
frame.allocatedSetsThisFrame = 0;
frame.peakAllocatedSetsThisFrame = 0;
for (auto& entry : m_descriptorReuseMemo) entry.valid = false;
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
// The PEAK is not reset: a rewind ends a window of the epoch, not the epoch. The trim scores
// an epoch by the most sets the slot held at once in it (NoteEndedEpochForTrim at the next
// BeginFrame), and on this arm that is the largest window - resetting here would score a
// burst frame by its last window alone and trim a slot that re-grows every frame.
InvalidateDescriptorSetMemos();
const Uint32 touchedBindings =
std::min<Uint32>(m_samplerResolveMemoHighWater, static_cast<Uint32>(m_samplerResolveMemo.size()));
for (Uint32 binding = 0; binding < touchedBindings; ++binding) {
@@ -761,6 +803,68 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
#endif
void UniformManager::InvalidateDescriptorSetMemos() {
// Every VkDescriptorSet handle held outside the per-layout caches - in this backend they all
// live in this class. A trim can hand a destroyed set's handle value to a fresh allocation,
// so the bind-dedup shadow must drop too, not only the two reuse memos.
for (auto& entry : m_descriptorReuseMemo) {
entry.valid = false;
}
m_fastRebindMemo.valid = false;
m_lastBindValid = false;
}
void UniformManager::NoteEndedEpochForTrim(Uint32 epochPeakSets) {
for (auto& slot : m_frames) {
if (static_cast<Uint64>(epochPeakSets) * kDescriptorTrimHeadroom > slot.capacitySets) {
slot.quietEpochs = 0;
} else if (slot.quietEpochs < std::numeric_limits<Uint32>::max()) {
++slot.quietEpochs;
}
}
}
void UniformManager::TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex) {
const SizeT poolsBefore = frame.descriptorPools.size();
const Uint64 capacityBefore = frame.capacitySets;
// Holders first: the per-layout cache and every memo that may name one of these sets.
frame.descriptorSetCacheByLayout.clear();
InvalidateDescriptorSetMemos();
for (SizeT i = 1; i < frame.descriptorPools.size(); ++i) {
if (frame.descriptorPools[i].handle != VK_NULL_HANDLE) {
vkDestroyDescriptorPool(m_device, frame.descriptorPools[i].handle, nullptr);
}
}
frame.descriptorPools.resize(1);
auto& base = frame.descriptorPools[0];
// Frees every set allocated from it; the spec defines no failure for vkResetDescriptorPool.
vkResetDescriptorPool(m_device, base.handle, 0);
base.allocatedSets = 0;
frame.activeDescriptorPoolIndex = 0;
frame.capacitySets = base.maxSets;
frame.quietEpochs = 0;
++m_descriptorSlotTrims;
MGLOG_D("UniformDescriptorBinder: frame %u trimmed its descriptor pools (%zu pools / %llu sets -> 1 / %u)",
frameIndex, poolsBefore, static_cast<unsigned long long>(capacityBefore), base.maxSets);
}
DescriptorPoolCensus UniformManager::GetDescriptorPoolCensus() const {
DescriptorPoolCensus census;
census.available = true;
census.frameSlots = static_cast<uint32_t>(m_frames.size());
for (const auto& frame : m_frames) {
const auto poolCount = static_cast<uint32_t>(frame.descriptorPools.size());
census.pools += poolCount;
census.maxPoolsInOneSlot = std::max(census.maxPoolsInOneSlot, poolCount);
census.capacitySets += frame.capacitySets;
for (const auto& cacheEntryPair : frame.descriptorSetCacheByLayout) {
census.cachedSets += cacheEntryPair.second.sets.size();
}
}
census.slotTrims = m_descriptorSlotTrims;
return census;
}
void UniformManager::OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout) {
SizeT purgedSets = 0;
for (auto& frame : m_frames) {
@@ -2700,9 +2804,20 @@ namespace MobileGL::MG_Backend::DirectVulkan {
auto& bindingPoint = MGB_CTX->GetBufferBindingPoint(BufferTarget::Uniform, frontendBinding);
const auto& bufferObject = bindingPoint.GetBoundObject();
MOBILEGL_ASSERT(bufferObject != nullptr,
"ResolveUniformBufferPayload: no UBO bound at frontend binding %u for block '%s'",
frontendBinding, program.GetUniformBlockName(static_cast<Uint32>(blockIndex)).c_str());
static thread_local Vector<Uint8> paddedUbo;
if (bufferObject == nullptr) {
// A block the program declares at a point with nothing bound - never bound, or its
// buffer was deleted while bound, which unbinds it (UnboundUniformBlockScenario,
// DeletedBoundBufferScenario). GL leaves what the shader READS undefined and raises no
// error; this used to dereference the null object. Answer it with a zeroed block of the
// reflected size, as the wire arm does (ResolveWireUniformBufferPayload).
const VkDeviceSize blockSize =
static_cast<VkDeviceSize>(program.GetUBOSizeAt(static_cast<Uint32>(blockIndex)));
paddedUbo.assign(static_cast<SizeT>(blockSize > 0 ? blockSize : 16), 0);
out.payload = paddedUbo.data();
out.payloadSize = static_cast<VkDeviceSize>(paddedUbo.size());
return true;
}
bufferObject->SyncPersistentMappedRange();
MOBILEGL_ASSERT(bufferObject->MappedData() != nullptr && bufferObject->GetSize() != 0,
@@ -2735,7 +2850,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
outSize = blockSize;
outData = bufferObject->MappedData() + static_cast<SizeT>(rangeStart);
if (available < blockSize) {
static thread_local Vector<Uint8> paddedUbo;
paddedUbo.assign(static_cast<SizeT>(blockSize), 0);
Memcpy(paddedUbo.data(), outData, static_cast<SizeT>(available));
outData = paddedUbo.data();
@@ -2831,32 +2945,49 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return true;
}
Uint32 UniformManager::NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets) {
const Uint32 minSets = std::max<Uint32>(1, initialMaxSets);
const Uint32 maxSets = std::max(minSets, kMaxDescriptorPoolSets);
Uint32 largest = 0;
for (const auto& pool : pools) {
if (pool.updateAfterBind == updateAfterBind) {
largest = std::max(largest, pool.maxSets);
}
}
if (largest == 0) {
return minSets;
}
const Uint64 doubled = static_cast<Uint64>(largest) * 2;
return static_cast<Uint32>(std::clamp<Uint64>(doubled, minSets, maxSets));
}
Bool UniformManager::GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind) {
if (frame.descriptorPools.empty()) {
return false;
}
const auto matchingBucket = std::find_if(
frame.descriptorPools.begin(), frame.descriptorPools.end(),
[updateAfterBind](const DescriptorPoolBucket& candidate) { return candidate.updateAfterBind == updateAfterBind; });
const Uint32 currentMaxSets = matchingBucket != frame.descriptorPools.end()
? std::max<Uint32>(1, matchingBucket->maxSets)
: m_setsPerFrame;
const Uint32 grownMaxSets = currentMaxSets <= (std::numeric_limits<Uint32>::max() / 2) ? (currentMaxSets * 2)
: currentMaxSets;
Uint32 grownMaxSets = NextDescriptorPoolMaxSets(frame.descriptorPools, updateAfterBind, m_setsPerFrame);
VkDescriptorPool grownPool = VK_NULL_HANDLE;
if (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u -> %u sets)",
currentMaxSets, grownMaxSets);
return false;
// A large pool is a large driver allocation; if the device refuses one, settle for less
// rather than failing the draw - halving down to the base size, where the old policy lived.
while (!CreateDescriptorPool(grownMaxSets, updateAfterBind, grownPool)) {
if (grownMaxSets <= m_setsPerFrame) {
MGLOG_E_ONCE("UniformDescriptorBinder::GrowFrameDescriptorPool failed: cannot create grown pool (%u sets)",
grownMaxSets);
return false;
}
grownMaxSets = std::max(m_setsPerFrame, grownMaxSets / 2);
}
frame.descriptorPools.push_back({grownPool, grownMaxSets, 0, updateAfterBind});
frame.activeDescriptorPoolIndex = static_cast<Uint32>(frame.descriptorPools.size() - 1);
MGLOG_D(
"UniformDescriptorBinder: frame %u descriptor pool exhausted, grew pool (%u -> %u sets), poolCount=%zu",
frameIndex, currentMaxSets, grownMaxSets, frame.descriptorPools.size());
frame.capacitySets += grownMaxSets;
// Growing is the opposite of quiet: a trim must wait a full quiet run from here.
frame.quietEpochs = 0;
MGLOG_D("UniformDescriptorBinder: frame %u descriptor pool exhausted, grew a %u-set pool (updateAfterBind=%d), "
"poolCount=%zu",
frameIndex, grownMaxSets, updateAfterBind ? 1 : 0, frame.descriptorPools.size());
return true;
}
@@ -3072,7 +3203,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// preparing the texture would decline a storage-less one and lose the pass.
if (storage && WireShaderImageNamesNoTexel(*m_textureManager, state.BoundShaderImages[unit]))
continue;
if (!m_textureManager->SyncTextureResourceByHandle(handle, false, storage)) return false;
const auto* resource = m_textureManager->SyncTextureResourceByHandle(handle, false, storage);
if (resource == nullptr) return false;
// Another session writes it: the renderer acquires it before this use.
if (resource->sharedImageId != 0 || resource->yuvImageId != 0)
m_textureManager->NoteSharedImageUse(*resource);
}
}
}
@@ -14,6 +14,7 @@
#include "VkSamplerManager.h"
#include "VkTextureManager.h"
#include "WirePlaceholderKey.h"
#include "../DescriptorPoolCensus.h"
#include "../VkIncludes.h"
#include <Includes.h>
@@ -84,9 +85,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// FREE_DESCRIPTOR_SET_BIT) and the pool accounting is credited, so program
// churn recycles pool capacity instead of abandoning it. GPU-safe: the layout
// only dies after >1024 idle frame boundaries, so no in-flight command buffer
// references its sets. This is the only eviction path for the per-layout
// caches - a live layout's entry must never be purged (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here.
// references its sets. This is the only PER-LAYOUT eviction path for the
// caches - a live layout's entry must never be purged on its own (its sets would be
// unreachable pool slots), so there is deliberately no age-based sweep here. The
// other path, TrimFrameDescriptorPools, drops a slot's whole cache together with
// the pools behind it.
void OnDescriptorSetLayoutDestroyed(VkDescriptorSetLayout descriptorSetLayout);
// One record per visited CombinedImageSampler DESCRIPTOR (post fallback substitution,
// in binding order, and within a binding in array-element order): the resolved texture
@@ -158,13 +161,46 @@ namespace MobileGL::MG_Backend::DirectVulkan {
static Bool ProgramSamplesOnlySingleLevelTextures(const MagmaProgramSource& program,
const ProgramFactory::VkProgramObject& programObj);
private:
// ---- per-frame descriptor pools ----------------------------------------------------
// Each frame slot owns a list of pools. The first is the base pool (non-update-after-bind,
// setsPerFrame sets) and is never destroyed while the manager lives; every later one was
// added by GrowFrameDescriptorPool when an allocation found no room. A pool serves one
// updateAfterBind flavour, because a set can only come from a pool created with the flag
// its layout was.
struct DescriptorPoolBucket {
VkDescriptorPool handle = VK_NULL_HANDLE;
Uint32 maxSets = 0;
Uint32 allocatedSets = 0;
Bool updateAfterBind = false;
};
// One pool's maxSets never exceeds this. It bounds one pool's driver memory: CreateDescriptorPool
// declares 8 descriptors of each of 6 types per set, so a 4096-set pool asks for 196,608
// descriptors - about 12 MiB at 64 bytes each on a driver that backs every declared one.
static constexpr Uint32 kMaxDescriptorPoolSets = 4096;
// The growth policy, pure so the host tests can drive it: the size of the next pool a slot
// adds for `updateAfterBind` layouts, given the slot's existing pools. Twice the LARGEST pool
// of that flavour, capped at kMaxDescriptorPoolSets; `initialMaxSets` when the slot has no
// pool of that flavour yet. So a frame that needs N sets adds O(log N) pools while N stays
// under the cap, and one more pool per kMaxDescriptorPoolSets sets beyond it. Taking the
// largest (not the first) pool is the point: the first same-flavour pool is the oldest,
// smallest one, and doubling IT made every grown pool the same size.
static Uint32 NextDescriptorPoolMaxSets(const Vector<DescriptorPoolBucket>& pools, Bool updateAfterBind,
Uint32 initialMaxSets);
// Trim policy (see TrimFrameDescriptorPools). A slot is "quiet" in an epoch whose peak set
// count times kDescriptorTrimHeadroom still fits its capacity; a slot that has been quiet for
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES consecutive epochs, and holds at least
// kDescriptorTrimMinCapacity sets, is trimmed. The headroom is what keeps the trim from
// thrashing: geometric growth leaves a re-grown slot at least a third full (the pool a growth
// adds is at most twice the largest before it, so at most twice everything the slot held, and
// the epoch that grew it needed more than that), so the epoch that grew a slot is never quiet
// for it. The floor keeps a slot sized for ordinary
// frames from being trimmed at all - the trim exists to give back a burst.
static constexpr Uint32 kDescriptorTrimHeadroom = 4;
static constexpr Uint32 kDescriptorTrimMinCapacity = 1024;
// For tests (DescriptorPoolCensus.h): pools, capacity, cached sets and trims over every slot.
DescriptorPoolCensus GetDescriptorPoolCensus() const;
private:
// A cached descriptor set together with the pool it was allocated from, so a
// layout-destroyed purge can vkFreeDescriptorSets it back and credit the
@@ -239,6 +275,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DestroyWirePlaceholderImage(WirePlaceholderImage& image) const;
#endif
// descriptorSetCacheByLayout is the per-frame set cache: BeginFrame rewinds every cursor, so
// a slot re-writes the sets it allocated in earlier frames instead of allocating new ones, and
// the cache only ever grows - except through TrimFrameDescriptorPools, which gives a slot's
// whole burst back at once (and OnDescriptorSetLayoutDestroyed, one dead layout at a time).
struct FrameResources {
Vector<DescriptorPoolBucket> descriptorPools;
UnorderedMap<VkDescriptorSetLayout, DescriptorSetCacheEntry> descriptorSetCacheByLayout;
@@ -252,6 +292,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 activeDescriptorPoolIndex = 0;
Uint32 allocatedSetsThisFrame = 0;
Uint32 peakAllocatedSetsThisFrame = 0;
// Sum of maxSets over descriptorPools, kept in step by Initialize, Grow and Trim.
Uint64 capacitySets = 0;
// Consecutive epochs (see NoteEndedEpochForTrim) that were quiet for THIS slot's capacity.
Uint32 quietEpochs = 0;
};
static Bool ResolveSamplerTexture(const MagmaProgramSource& program,
@@ -395,6 +439,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Vector<Uint32>& dynamicOffsets);
Bool CreateDescriptorPool(Uint32 maxSets, Bool updateAfterBind, VkDescriptorPool& outPool) const;
Bool GrowFrameDescriptorPool(FrameResources& frame, Uint32 frameIndex, Bool updateAfterBind);
// An epoch is the stretch between two BeginFrame calls: one frame, or the part of one between
// two drains (VulkanRenderer::TryDrainFrameTransients). Called by BeginFrame with the epoch
// that just ended, it updates every slot's quiet count - a burst in any slot counts against
// all of them, so bursts that land in the other slots still hold a slot's trim off.
void NoteEndedEpochForTrim(Uint32 epochPeakSets);
// Gives a slot's whole descriptor burst back: clears its per-layout cache, destroys every
// grown pool and resets the base pool, so the slot re-grows geometrically from setsPerFrame.
// Whole-slot rather than per-set: vkFreeDescriptorSets fragments a pool and can never empty
// a grown one. Called only from BeginFrame, which runs only when no pending command buffer
// references the slot's sets:
// - Present path (VulkanRenderer::Present, after WaitAndAcquireNextImage): the slot's
// imageInFlightFence has been waited. That fence rides the slot's LAST submission - the
// Present submit - and every mid-frame submission of the slot (FlushPendingCommands,
// SplitOversizedRecording, readbacks) went to the same queue earlier, so the fence's
// signal covers them (vkQueueSubmit's fence scope includes all earlier submissions).
// - Drain path (VulkanRenderer::TryDrainFrameTransients): every submission has been
// observed complete and no recording is open.
// Sets are per slot: a slot's sets are only ever bound into recordings made while it is the
// current slot, so trimming one slot never touches another slot's in-flight work.
void TrimFrameDescriptorPools(FrameResources& frame, Uint32 frameIndex);
// Drops every handle this class holds to a descriptor set outside the per-layout caches.
void InvalidateDescriptorSetMemos();
VkResult AllocateDescriptorSetsFromActivePool(
Uint32 frameIndex, const ProgramFactory::VkProgramObject& programObj, VkDescriptorSet& outDescriptorSet);
VkResult AcquireDescriptorSet(Uint32 frameIndex,
@@ -412,6 +478,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Uint32 m_maxBindings = 0;
Uint32 m_setsPerFrame = 0;
Uint32 m_peakDescriptorSetsObserved = 0;
// MOBILEGL_MAGMA_DESCRIPTOR_TRIM_FRAMES, latched at Initialize (0 = never trim).
Uint32 m_trimQuietEpochs = 0;
// The slot whose epoch is open (the last one BeginFrame was called for); valid once
// m_epochOpen is set. Its peak is the ended epoch's peak at the next BeginFrame.
Uint32 m_epochSlot = 0;
Bool m_epochOpen = false;
Uint64 m_descriptorSlotTrims = 0;
VkTextureManager* m_textureManager = nullptr;
VkSamplerManager* m_samplerManager = nullptr;
mutable SharedPtr<MG_State::GLState::ITextureObject> m_fallbackTexture2D;
@@ -587,5 +660,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_samplerResolveMemoHighWater = binding + 1;
}
}
// WireImageWriteEpoch() when ResolveWireImageDescriptor last made image writes visible.
mutable Uint64 m_wireImageBarrierEpoch = 0;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -19,6 +19,7 @@
#include "VertexInputStateBuilder.h"
#include "MG_State/GLState/VertexArrayState/VertexArrayObject.h"
#include <Includes.h>
#include <atomic>
#include "../VkIncludes.h"
namespace MobileGL::MG_Backend::DirectVulkan {
@@ -217,11 +218,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// memo table is owned by this factory and dies with it, so a per-instance counter is
// enough there and the epoch shrinks back to what it looks like it should be.
#if MOBILEGL_PIPE_LEGACY_MEMOS
static inline Uint64 s_evictionEpochSource = 0;
static inline std::atomic<Uint64> s_evictionEpochSource{0};
Uint64 m_evictionEpoch = ++s_evictionEpochSource;
#else
Uint64 m_evictionEpoch = 1;
#endif
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "RenderPassGuard.h"
#include "VkBufferManager.h"
#include "../DirectVulkan.h"
#include "VulkanRenderer.h"
@@ -15,7 +16,14 @@
#include "MG_Pipe/MGPipeCallbacks.h"
#include "MG_Pipe/PipeApply.h"
#include "MG_Remote/Server/StagedShadow.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include "MG_Remote/Transport/AdoptT0.h"
#endif
#if MOBILEGL_BUILD_DISAGGREGATED && defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#include <Config.h>
#include <chrono>
#include <cstdlib>
#endif
@@ -106,30 +114,30 @@ namespace MobileGL::MG_Backend::DirectVulkan {
using MG_State::GLState::BufferObject;
// The manager owned by the active VulkanRenderer; immediate ops route here.
VkBufferManager* g_activeBufferManager = nullptr;
SessionLocal<VkBufferManager*> g_activeBufferManager; // per Magma session (MagmaSession.h)
void Ops_Respecify(BufferObject& bufferObject) {
if (g_activeBufferManager) {
g_activeBufferManager->OnRespecify(bufferObject);
if (g_activeBufferManager.Get()) {
g_activeBufferManager.Get()->OnRespecify(bufferObject);
}
}
void Ops_SubData(BufferObject& bufferObject, SizeT offset, SizeT size) {
if (g_activeBufferManager) {
g_activeBufferManager->OnSubData(bufferObject, offset, size);
if (g_activeBufferManager.Get()) {
g_activeBufferManager.Get()->OnSubData(bufferObject, offset, size);
}
}
void Ops_ResidentSubData(BufferObject& bufferObject, SizeT offset, DataPtr data) {
if (g_activeBufferManager) {
g_activeBufferManager->OnResidentSubData(bufferObject, offset, data);
if (g_activeBufferManager.Get()) {
g_activeBufferManager.Get()->OnResidentSubData(bufferObject, offset, data);
}
}
void Ops_FlushMappedRange(BufferObject& bufferObject, Range1D range,
Flags<BufferMappingAccessBit> appAccess) {
if (g_activeBufferManager) {
g_activeBufferManager->OnFlushMappedRange(bufferObject, range, appAccess);
if (g_activeBufferManager.Get()) {
g_activeBufferManager.Get()->OnFlushMappedRange(bufferObject, range, appAccess);
}
}
@@ -147,15 +155,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void* Ops_AcquirePersistentMap(BufferObject& bufferObject) {
if (g_activeBufferManager) {
return g_activeBufferManager->AcquirePersistentMap(bufferObject);
if (g_activeBufferManager.Get()) {
return g_activeBufferManager.Get()->AcquirePersistentMap(bufferObject);
}
return nullptr;
}
void Ops_OnDestroy(SharedPtr<BackendBufferResource>&& resource) {
if (g_activeBufferManager) {
g_activeBufferManager->OnResourceDestroyed(std::move(resource));
if (g_activeBufferManager.Get()) {
g_activeBufferManager.Get()->OnResourceDestroyed(std::move(resource));
}
// No active manager: the device/allocator is gone or going away and
// Shutdown() already destroyed the storage; dropping the handle here
@@ -174,38 +182,54 @@ namespace MobileGL::MG_Backend::DirectVulkan {
};
#if MOBILEGL_BUILD_DISAGGREGATED
VkBufferManager& WireManager() {
if (g_activeBufferManager == nullptr) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"wire-buffer-manager\"}");
std::abort();
// The session's manager, or null once its renderer is gone - a client's records can still
// arrive after its eglTerminate released it (or, surfaceless, before its first surface built
// one). Such a record is declined with one line rather than taking the whole server down.
VkBufferManager* WireManager(const char* op) {
VkBufferManager* manager = g_activeBufferManager.Get();
if (manager == nullptr) {
MGLOG_E_ONCE("Magma: a wire buffer %s reached a session with no renderer; declined", op);
}
return *g_activeBufferManager;
return manager;
}
const MG_Pipe::MGPipeResourceOps g_vulkanWireResourceOps = {
.Create = [](auto res, const auto& desc) { WireManager().CreateWireBuffer(res, desc); },
.Create = [](auto res, const auto& desc) { if (auto* m = WireManager("create")) m->CreateWireBuffer(res, desc); },
.Respecify = [](auto res, const auto& desc, const void* bytes) {
WireManager().RespecifyWireBuffer(res, desc, bytes);
if (auto* m = WireManager("respecify")) m->RespecifyWireBuffer(res, desc, bytes);
},
.SubData = [](auto res, const auto& record, const void* bytes) {
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
if (auto* m = WireManager("write"))
m->WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
},
.SubDataResident = [](auto res, const auto& record, const void* bytes) {
WireManager().WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
if (auto* m = WireManager("write"))
m->WriteWireBuffer(res, MG_Pipe::MGPipeSubDataBufferOffset(record),
MG_Pipe::MGPipeSubDataBufferSize(record), bytes);
},
.FlushRange = [](auto res, const auto& record, const void* bytes) {
WireManager().FlushWireBuffer(res, record.Offset, record.Size, bytes);
if (auto* m = WireManager("flush")) m->FlushWireBuffer(res, record.Offset, record.Size, bytes);
},
.Readback = [](auto res, const auto& record) {
WireManager().ReadbackWireBuffer(res, record.Offset, record.Size);
if (auto* m = WireManager("readback")) m->ReadbackWireBuffer(res, record.Offset, record.Size);
},
.Destroy = [](auto res) { WireManager().DestroyWireBuffer(res); },
.Destroy = [](auto res) { if (auto* m = WireManager("destroy")) m->DestroyWireBuffer(res); },
// Split mapping remains T2: the client owns its map and pushes exact
// modified ranges. A server pointer is never donated across the wire.
.MapPersistent = [](MG_Pipe::MGPipeHandle, Uint64, const void*) -> void* { return nullptr; },
.UnmapPersistent = [](MG_Pipe::MGPipeHandle) {},
#if defined(__ANDROID__)
// P11 B2 (T0): the one door a server pointer is still never donated through stays shut;
// what T0 adds is the other direction - the CLIENT's pages imported as the store.
.ImportExternal = [](MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) -> Bool {
auto* m = WireManager("import");
return m != nullptr && m->ImportWireBuffer(res, ahb, size);
},
.SelfTestExternal = [](char* why, Uint64 whyBytes) -> Bool {
return VkBufferManager::SelfTestWireImport(why, whyBytes);
},
#endif
};
#endif
} // namespace
@@ -217,6 +241,18 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
namespace {
// Diagnostic memory for DescribeWireBufferHistory: the last handles created and destroyed.
struct WireBufferHistory {
static constexpr SizeT kDepth = 256;
MG_Pipe::MGPipeHandle created[kDepth]{};
MG_Pipe::MGPipeHandle destroyed[kDepth]{};
SizeT createdCount = 0;
SizeT destroyedCount = 0;
};
SessionLocal<WireBufferHistory> g_wireBufferHistory;
} // namespace
VkBufferManager::WireBufferResource* VkBufferManager::FindWireBuffer(MG_Pipe::MGPipeHandle res) {
const auto found = m_wireBuffers.find(WireBufferKey(res));
return found == m_wireBuffers.end() ? nullptr : &found->second;
@@ -229,6 +265,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::abort();
}
m_wireBuffers.try_emplace(WireBufferKey(res));
{
auto& history = *g_wireBufferHistory;
history.created[history.createdCount++ % 256] = res;
}
RespecifyWireBuffer(res, desc, nullptr);
}
@@ -245,11 +285,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// to wait for a submission (DeferredWireRelease), and three lines from now the record
// no longer carries it.
const Uint64 orphanedUseSerial = resource->lastUseSerial;
DeferWireRelease(std::move(resource->buffer), orphanedUseSerial);
// P11 B2: a T0 store is ALWAYS released through the serial-gated list (never "at once"),
// so the client's AHardwareBuffer outlives every submission that can still name it however
// the client's own release (at this respecify) raced the frames in flight.
DeferWireRelease(std::move(resource->buffer),
resource->imported ? T0ReleaseSerial(orphanedUseSerial) : orphanedUseSerial);
resource->imported = false;
resource->size = desc.Width;
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0; // M2 r2: the new store carries no submission yet (B3's stamp is per store)
resource->gpuWritesPending = false;
resource->indirectReadBarrierPending = false;
resource->stagedCoverage.clear();
++m_sliceEpochCounter;
if (resource->size == 0) return;
@@ -292,9 +338,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkMemoryBarrier barrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
barrier.dstAccessMask = VK_ACCESS_HOST_READ_BIT | VK_ACCESS_HOST_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_HOST_BIT,
0, 1, &barrier, 0, nullptr, 0, nullptr);
if (!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
// P8-D: counted here, past the idle early-out, so `whw` is waits TAKEN, not calls.
++m_wireHostWaits;
const Bool timed = MG_Util::PipeStats::Enabled();
const auto waitStart = timed ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
const Bool waited =
pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true);
if (timed) {
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaits, 1);
MG_Util::PipeStats::AddCalls(MG_Util::PipeStats::CallClass::WireHostWaitMicros,
static_cast<Uint64>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now() - waitStart).count()));
}
if (!waited) {
return false;
}
resource.lastUseSerial = 0;
@@ -368,7 +427,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploaded = StagedWireRangeCopy(*resource, bytes, static_cast<SizeT>(offset),
static_cast<SizeT>(size));
}
if (!uploaded && !WaitForWireBufferHostAccess(*resource)) WireBufferSyncFatal("host-write");
if (!uploaded && !WaitForWireBufferHostAccess(*resource)) {
if (LatchedOnDeviceLoss("buffer-host-write")) return;
WireBufferSyncFatal("host-write");
}
}
if (!uploaded) {
uploaded = resource->buffer.Upload(bytes, size, offset);
@@ -377,6 +439,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
if (!uploaded) {
if (LatchedOnDeviceLoss("buffer-upload")) return;
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-upload\"}");
std::abort();
}
@@ -470,13 +533,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const VkBufferCopy copy{offset, dst.offset, size};
EndActiveRenderPassOn(commands);
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), dst.buffer, 1, &copy);
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &after, 0, nullptr, 0, nullptr);
resource->lastUseSerial = m_frameSerial;
@@ -514,22 +580,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
before.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT | VK_ACCESS_HOST_WRITE_BIT;
before.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1, &before, 0, nullptr, 0, nullptr);
const VkBufferCopy widen{alignedOffset, staging.offset, stagedSize};
EndActiveRenderPassOn(commands);
vkCmdCopyBuffer(commands, resource->buffer.GetHandle(), staging.buffer, 1, &widen);
// The shift reads what the widening copy just wrote, so it needs its own edge even
// though both halves are transfers on one command buffer.
VkMemoryBarrier staged{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
staged.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
staged.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT,
0, 1, &staged, 0, nullptr, 0, nullptr);
const VkBufferCopy shift{staging.offset + headPad, dst.offset + dstSkip, size};
EndActiveRenderPassOn(commands);
vkCmdCopyBuffer(commands, staging.buffer, dst.buffer, 1, &shift);
VkMemoryBarrier after{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
after.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
after.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
0, 1, &after, 0, nullptr, 0, nullptr);
resource->lastUseSerial = m_frameSerial;
@@ -537,11 +608,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferManager::ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size) {
Vector<Uint8> bytes(static_cast<SizeT>(size));
if (!ReadWireBuffer(res, offset, size, bytes.data())) {
// P11 B2: A T0 STORE'S READBACK IS "DONE, NO BYTES". Its pages are the client's
// AHardwareBuffer, so what the client needs before it reads them is this server's GPU
// having finished every write to them - the resident copies WriteWireBuffer recorded and
// any shader write - and nothing crossing SEG_EVENT. The wait is the store's own host-access
// wait (a barrier to host, then the sync point's submission); the reply then says "done".
// One death for both shapes: the store could not be made readable.
auto* resource = FindWireBuffer(res);
const Bool t0 = resource != nullptr && resource->imported;
Vector<Uint8> bytes(t0 ? 0 : static_cast<SizeT>(size));
if (t0 ? !WaitForWireBufferHostAccess(*resource) : !ReadWireBuffer(res, offset, size, bytes.data())) {
// A lost device: the session is latched and closing; its client reads a lost context.
if (LatchedOnDeviceLoss("buffer-readback")) return;
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-readback\"} {slot=%u, gen=%u}", res.Slot, res.Gen);
std::abort();
}
if (t0) {
++m_sliceEpochCounter;
return;
}
if (MG_Pipe::gMGPipeCallbacks.OnBufferWriteback == nullptr) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-writeback-callback\"}");
std::abort();
@@ -561,6 +646,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->lastUseSerial = m_frameSerial;
resource->stagedCoverage.clear();
resource->gpuWritesPending = true;
resource->indirectReadBarrierPending = true;
++m_sliceEpochCounter;
// The existing client dirty-state protocol accepts one whole-store
// notification. The store remains on the GPU and exact later SubData
@@ -573,10 +659,393 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Pipe::gMGPipeCallbacks.OnGpuWritten(res, 1, &range);
}
Bool VkBufferManager::TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res) {
auto* resource = FindWireBuffer(res);
if (!resource || !resource->indirectReadBarrierPending) return false;
resource->indirectReadBarrierPending = false;
return true;
}
// ---- P11 B2: T0 - THE CLIENT'S AHardwareBuffer AS A WIRE STORE -----------------------------
// The serial a T0 store is released under: its last use, or - when no GPU command has named it
// since a host-access wait - the current frame's, so the release still waits for the sync
// point rather than taking DeferWireRelease's "never named, destroy now" arm. Conservative by
// at most one submission.
Uint64 VkBufferManager::T0ReleaseSerial(Uint64 lastUseSerial) const {
if (lastUseSerial != 0) return lastUseSerial;
return m_frameSerial != 0 ? m_frameSerial : 1;
}
#if defined(__ANDROID__)
namespace {
Int32 T0PickMemoryType(const VkPhysicalDeviceMemoryProperties& props, Uint32 bits,
VkMemoryPropertyFlags want) {
for (Uint32 i = 0; i < props.memoryTypeCount; ++i) {
if ((bits & (1u << i)) != 0 && (props.memoryTypes[i].propertyFlags & want) == want)
return static_cast<Int32>(i);
}
return -1;
}
struct T0ImportedBuffer {
VkBuffer buffer = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
void* mapped = nullptr;
Bool coherent = true;
};
void T0DestroyImported(VkDevice device, T0ImportedBuffer& imported) {
if (imported.mapped != nullptr) vkUnmapMemory(device, imported.memory);
if (imported.buffer != VK_NULL_HANDLE) vkDestroyBuffer(device, imported.buffer, nullptr);
if (imported.memory != VK_NULL_HANDLE) vkFreeMemory(device, imported.memory, nullptr);
imported = T0ImportedBuffer{};
}
// A VkBuffer on a dedicated allocation IMPORTED from `ahb`, in a host-visible memory type
// (coherent preferred), mapped whole. The server's own host map is what WriteWireBuffer's
// idle path and every CPU reader of a wire store use, so a type without HOST_VISIBLE is a
// refusal, not a degraded import. Nothing is left behind on failure.
Bool T0ImportAhb(const VulkanRenderer::WireAhbImport& ctx, void* ahb, Uint64 size, VkBufferUsageFlags usage,
T0ImportedBuffer& out, String& why) {
out = T0ImportedBuffer{};
const auto getProperties =
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
VkAndroidHardwareBufferPropertiesANDROID properties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
VkResult result = getProperties(ctx.device, static_cast<const AHardwareBuffer*>(ahb), &properties);
if (result != VK_SUCCESS) {
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
return false;
}
VkExternalMemoryBufferCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_BUFFER_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkBufferCreateInfo bufferInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
bufferInfo.pNext = &external;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
result = vkCreateBuffer(ctx.device, &bufferInfo, nullptr, &out.buffer);
if (result != VK_SUCCESS) {
out.buffer = VK_NULL_HANDLE;
why = "vkCreateBuffer(external AHB) -> " + std::to_string(static_cast<int>(result));
return false;
}
Int32 type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
out.coherent = type >= 0;
if (type < 0)
type = T0PickMemoryType(ctx.memory, properties.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT);
if (type < 0) {
why = "no host-visible memory type can import it (memoryTypeBits 0x" +
std::to_string(properties.memoryTypeBits) + ")";
T0DestroyImported(ctx.device, out);
return false;
}
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.buffer = out.buffer;
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import.buffer = static_cast<AHardwareBuffer*>(ahb);
import.pNext = &dedicated;
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocateInfo.pNext = &import;
allocateInfo.allocationSize = properties.allocationSize;
allocateInfo.memoryTypeIndex = static_cast<Uint32>(type);
result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory);
if (result != VK_SUCCESS) {
out.memory = VK_NULL_HANDLE;
why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast<int>(result));
T0DestroyImported(ctx.device, out);
return false;
}
result = vkBindBufferMemory(ctx.device, out.buffer, out.memory, 0);
if (result == VK_SUCCESS) result = vkMapMemory(ctx.device, out.memory, 0, VK_WHOLE_SIZE, 0, &out.mapped);
if (result != VK_SUCCESS || out.mapped == nullptr) {
out.mapped = nullptr;
why = "vkBindBufferMemory/vkMapMemory -> " + std::to_string(static_cast<int>(result));
T0DestroyImported(ctx.device, out);
return false;
}
return true;
}
} // namespace
#endif
Bool VkBufferManager::ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size) {
#if defined(__ANDROID__)
auto* resource = FindWireBuffer(res);
if (resource == nullptr || ahb == nullptr || size == 0 || size != resource->size) return false;
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
MGLOG_W_ONCE("Magma T0: this device did not take VK_ANDROID_external_memory_android_hardware_buffer");
return false;
}
VkBufferUsageFlags usage = kPersistentBackedUsage | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
if (m_initInfo.transformFeedbackUsageEnabled) usage |= VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT;
T0ImportedBuffer imported;
String why;
if (!T0ImportAhb(ctx, ahb, size, usage, imported, why)) {
MGLOG_W("Magma T0: store {slot=%u, gen=%u} (%llu bytes) not imported - %s", res.Slot, res.Gen,
static_cast<unsigned long long>(size), why.c_str());
return false;
}
// The T2 store it replaces goes the way a respecify's does (and any draw still naming it
// keeps it until its submission completes).
DeferWireRelease(std::move(resource->buffer), resource->lastUseSerial);
MG_Remote::Transport::AdoptT0::AcquireImported(ahb);
resource->buffer.AdoptExternal(ctx.device, imported.buffer, imported.memory, size, imported.mapped,
imported.coherent, ahb);
resource->imported = true;
resource->lastUseSerial = 0;
resource->lastUseSubmitIndex = 0;
resource->gpuWritesPending = false;
// The whole store holds bytes the client supplied (its AHB, seeded from its shadow).
resource->stagedCoverage.clear();
MG_Remote::Server::StagedShadowStore::CoverageAdd(resource->stagedCoverage, 0, static_cast<SizeT>(size));
++m_sliceEpochCounter;
++m_wireStoreCount;
NoteWireStorePeaks();
PublishWireReclaimGauges();
MGLOG_D("Magma T0: imported {slot=%u, gen=%u} (%llu bytes, %s memory)", res.Slot, res.Gen,
static_cast<unsigned long long>(size), imported.coherent ? "coherent" : "non-coherent");
return true;
#else
(void)res;
(void)ahb;
(void)size;
return false;
#endif
}
// THE POST (B2; decided by probe, never by driver name). The AHB API promises CPU coherence only
// across an unlock/lock, and T0 holds the client's lock for the store's life, so this exercises
// that pattern on THIS device before kCapAdoptT0 may be published:
// hold - a 64 KiB BLOB allocated and locked by the test, never unlocked while it runs;
// read - this server's own host map of the import reads what the CPU wrote through the lock;
// gpu - one submission copies that pattern out of the import (the GPU read) and fills another
// region of it (the GPU write), then a transfer->host barrier and a fence wait;
// back - the fill is read through the HELD pointer, the copy from a host-visible staging buffer;
// steady - the same again with bytes the CPU wrote AFTER the import and its first GPU use (the
// block at the end says why the first round alone is not enough).
Bool VkBufferManager::SelfTestWireImport(char* whyOut, Uint64 whyBytes) {
const auto answer = [&](Bool ok, const String& text) {
std::snprintf(whyOut, static_cast<SizeT>(whyBytes), "%s", text.c_str());
return ok;
};
#if defined(__ANDROID__)
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx))
return answer(false, "the device did not take VK_ANDROID_external_memory_android_hardware_buffer");
constexpr Uint64 kBytes = 64 * 1024;
constexpr Uint32 kWords = 1024; // one 4 KiB region
constexpr Uint64 kFillAt = 4 * 4096; // region 4
constexpr Uint32 kFillWord = 0x6C0FFEE0u;
Vector<Uint32> pattern(kBytes / 4);
for (SizeT i = 0; i < pattern.size(); ++i) pattern[i] = 0x5A000000u ^ static_cast<Uint32>(i * 2654435761u);
MG_Remote::Transport::AdoptT0::HeldStore held;
String why;
if (!MG_Remote::Transport::AdoptT0::AllocateHeld(kBytes, pattern.data(), held, why))
return answer(false, "hold: " + why);
T0ImportedBuffer imported, staging;
VkCommandPool pool = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
String verdict;
Bool ok = false;
do {
if (!T0ImportAhb(ctx, held.ahb, kBytes,
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT |
VK_BUFFER_USAGE_STORAGE_BUFFER_BIT,
imported, why)) {
verdict = "import: " + why;
break;
}
if (!imported.coherent) {
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = imported.memory;
range.size = VK_WHOLE_SIZE;
vkInvalidateMappedMemoryRanges(ctx.device, 1, &range);
}
if (std::memcmp(imported.mapped, pattern.data(), kWords * 4) != 0) {
verdict = "read: the server's host map of the import does not hold the CPU's pattern";
break;
}
// A host-visible staging buffer for the GPU read's destination.
VkBufferCreateInfo stagingInfo{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
stagingInfo.size = kWords * 4;
stagingInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
stagingInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
if (vkCreateBuffer(ctx.device, &stagingInfo, nullptr, &staging.buffer) != VK_SUCCESS) {
staging.buffer = VK_NULL_HANDLE;
verdict = "staging buffer";
break;
}
VkMemoryRequirements requirements{};
vkGetBufferMemoryRequirements(ctx.device, staging.buffer, &requirements);
const Int32 stagingType = T0PickMemoryType(ctx.memory, requirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VkMemoryAllocateInfo stagingAlloc{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
stagingAlloc.allocationSize = requirements.size;
stagingAlloc.memoryTypeIndex = static_cast<Uint32>(stagingType < 0 ? 0 : stagingType);
if (stagingType < 0 || vkAllocateMemory(ctx.device, &stagingAlloc, nullptr, &staging.memory) != VK_SUCCESS ||
vkBindBufferMemory(ctx.device, staging.buffer, staging.memory, 0) != VK_SUCCESS ||
vkMapMemory(ctx.device, staging.memory, 0, VK_WHOLE_SIZE, 0, &staging.mapped) != VK_SUCCESS) {
verdict = "staging memory";
break;
}
std::memset(staging.mapped, 0, kWords * 4);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.queueFamilyIndex = ctx.queueFamily;
poolInfo.flags = VK_COMMAND_POOL_CREATE_TRANSIENT_BIT;
VkCommandBuffer commands = VK_NULL_HANDLE;
VkCommandBufferAllocateInfo commandInfo{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
commandInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
commandInfo.commandBufferCount = 1;
VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
if (vkCreateCommandPool(ctx.device, &poolInfo, nullptr, &pool) != VK_SUCCESS) {
pool = VK_NULL_HANDLE;
verdict = "command pool";
break;
}
commandInfo.commandPool = pool;
if (vkAllocateCommandBuffers(ctx.device, &commandInfo, &commands) != VK_SUCCESS ||
vkCreateFence(ctx.device, &fenceInfo, nullptr, &fence) != VK_SUCCESS) {
verdict = "command buffer / fence";
break;
}
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commands, &begin);
VkMemoryBarrier hostWrites{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
hostWrites.srcAccessMask = VK_ACCESS_HOST_WRITE_BIT;
hostWrites.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&hostWrites, 0, nullptr, 0, nullptr);
VkBufferCopy region{0, 0, kWords * 4};
EndActiveRenderPassOn(commands);
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, &region);
EndActiveRenderPassOn(commands);
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kFillWord);
VkMemoryBarrier toHost{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
toHost.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toHost.dstAccessMask = VK_ACCESS_HOST_READ_BIT;
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
0, nullptr, 0, nullptr);
vkEndCommandBuffer(commands);
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
submit.commandBufferCount = 1;
submit.pCommandBuffers = &commands;
if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS ||
vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) {
verdict = "gpu: the submission did not complete within 2 s";
break;
}
Uint32 gpuRead = 0, heldSaw = 0;
const auto* copied = static_cast<const Uint32*>(staging.mapped);
const auto* heldWords = static_cast<const Uint32*>(held.ptr);
for (Uint32 i = 0; i < kWords; ++i) {
if (copied[i] == pattern[i]) ++gpuRead;
if (heldWords[kFillAt / 4 + i] == kFillWord) ++heldSaw;
}
ok = gpuRead == kWords && heldSaw == kWords;
verdict = "64 KiB AHB, lock held throughout: host map read ok (" +
String(imported.coherent ? "coherent" : "non-coherent") + " memory), GPU read " +
std::to_string(gpuRead) + "/1024 words of the CPU's pattern, GPU write seen through the "
"held lock " + std::to_string(heldSaw) + "/1024 words";
if (!ok) break;
// STEADY STATE. The round above reads bytes the CPU wrote BEFORE the import, and an import
// can clean the CPU's caches on its way in, so it passes on a device whose GPU never sees a
// write made through the held lock afterwards - which is all of T0's real traffic (Mali-G1
// on a Dimensity: the first round passed, then Flywheel's staging scatter read stale bytes
// and the create-indirect trace lost every instanced object). So the CPU now writes a fresh
// pattern into region 1 and reads region 4 back into its cache, and one more submission
// copies region 1 out and overwrites region 4.
constexpr Uint64 kSteadyAt = 1 * 4096; // region 1
constexpr Uint32 kSteadyFillWord = 0x3D5EA11Eu;
auto* heldWrite = static_cast<volatile Uint32*>(held.ptr);
Uint32 cached = 0;
for (Uint32 i = 0; i < kWords; ++i) {
heldWrite[kSteadyAt / 4 + i] = 0xA5000000u ^ (i * 40503u);
cached ^= heldWrite[kFillAt / 4 + i];
}
(void)cached;
std::memset(staging.mapped, 0, kWords * 4);
if (vkResetFences(ctx.device, 1, &fence) != VK_SUCCESS ||
vkResetCommandPool(ctx.device, pool, 0) != VK_SUCCESS ||
vkBeginCommandBuffer(commands, &begin) != VK_SUCCESS) {
ok = false;
verdict = "steady: the second submission could not be recorded";
break;
}
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&hostWrites, 0, nullptr, 0, nullptr);
VkBufferCopy steadyRegion{kSteadyAt, 0, kWords * 4};
EndActiveRenderPassOn(commands);
vkCmdCopyBuffer(commands, imported.buffer, staging.buffer, 1, &steadyRegion);
EndActiveRenderPassOn(commands);
vkCmdFillBuffer(commands, imported.buffer, kFillAt, kWords * 4, kSteadyFillWord);
EndActiveRenderPassOn(commands);
vkCmdPipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, 0, 1, &toHost,
0, nullptr, 0, nullptr);
vkEndCommandBuffer(commands);
if (vkQueueSubmit(ctx.queue, 1, &submit, fence) != VK_SUCCESS ||
vkWaitForFences(ctx.device, 1, &fence, VK_TRUE, 2'000'000'000ull) != VK_SUCCESS) {
ok = false;
verdict = "steady: the second submission did not complete within 2 s";
break;
}
Uint32 steadyRead = 0, steadySaw = 0;
for (Uint32 i = 0; i < kWords; ++i) {
if (copied[i] == (0xA5000000u ^ (i * 40503u))) ++steadyRead;
if (heldWords[kFillAt / 4 + i] == kSteadyFillWord) ++steadySaw;
}
ok = steadyRead == kWords && steadySaw == kWords;
verdict += "; with the import in use, GPU read " + std::to_string(steadyRead) +
"/1024 words the CPU wrote after it, GPU write seen through the held lock " +
std::to_string(steadySaw) + "/1024 words";
} while (false);
if (fence != VK_NULL_HANDLE) vkDestroyFence(ctx.device, fence, nullptr);
if (pool != VK_NULL_HANDLE) vkDestroyCommandPool(ctx.device, pool, nullptr);
T0DestroyImported(ctx.device, staging);
T0DestroyImported(ctx.device, imported);
MG_Remote::Transport::AdoptT0::ReleaseHeld(held);
return answer(ok, verdict);
#else
return answer(false, "no AHardwareBuffer on this platform");
#endif
}
void VkBufferManager::DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const {
const auto& history = *g_wireBufferHistory;
Int createdAgo = -1, destroyedAgo = -1;
for (SizeT i = 0; i < std::min(history.createdCount, WireBufferHistory::kDepth); ++i) {
const auto& h = history.created[(history.createdCount - 1 - i) % WireBufferHistory::kDepth];
if (h.Slot == res.Slot && h.Gen == res.Gen) { createdAgo = static_cast<Int>(i); break; }
}
for (SizeT i = 0; i < std::min(history.destroyedCount, WireBufferHistory::kDepth); ++i) {
const auto& h = history.destroyed[(history.destroyedCount - 1 - i) % WireBufferHistory::kDepth];
if (h.Slot == res.Slot && h.Gen == res.Gen) { destroyedAgo = static_cast<Int>(i); break; }
}
MGLOG_E("Magma wire buffer {slot=%u, gen=%u}: live stores %zu; created %d creates ago, destroyed %d "
"destroys ago (-1 = not in the last %zu); %zu creates, %zu destroys in this session",
res.Slot, res.Gen, m_wireBuffers.size(), createdAgo, destroyedAgo, WireBufferHistory::kDepth,
history.createdCount, history.destroyedCount);
}
void VkBufferManager::DestroyWireBuffer(MG_Pipe::MGPipeHandle res) {
{
auto& history = *g_wireBufferHistory;
history.destroyed[history.destroyedCount++ % WireBufferHistory::kDepth] = res;
}
const auto found = m_wireBuffers.find(WireBufferKey(res));
if (found == m_wireBuffers.end()) return;
DeferWireRelease(std::move(found->second.buffer), found->second.lastUseSerial);
DeferWireRelease(std::move(found->second.buffer),
found->second.imported ? T0ReleaseSerial(found->second.lastUseSerial)
: found->second.lastUseSerial); // P11 B2, as Respecify
m_wireBuffers.erase(found);
++m_sliceEpochCounter;
PublishWireReclaimGauges();
@@ -629,6 +1098,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// budget's worth of orphans, not one per glBufferData.
if (pVulkanRenderer == nullptr ||
!pVulkanRenderer->WaitForSubmitIndex(pVulkanRenderer->GetSyncPointSubmitIndex(), UINT64_MAX, true)) {
if (LatchedOnDeviceLoss("buffer-deferred-watermark")) return;
WireBufferSyncFatal("deferred-watermark");
}
++m_wireDeferredSyncs;
@@ -649,6 +1119,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MG_Util::PipeStats::PublishGauge(Gauge::WireDeferredSyncs, m_wireDeferredSyncs);
}
Bool VkBufferManager::LatchedOnDeviceLoss(const char* site) {
return pVulkanRenderer != nullptr && pVulkanRenderer->LatchWireDeviceLoss(site);
}
void VkBufferManager::WireBufferSyncFatal(const char* site) {
MGLOG_F("Magma: Fatal{ResourceUnavailable, \"buffer-write-sync\"} {site=%s}", site);
std::abort();
@@ -722,7 +1196,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (!InitializeTransientArenas()) {
return false;
}
g_activeBufferManager = this;
g_activeBufferManager.Get() = this;
MG_State::GLState::SetBufferBackendOps(&g_vulkanBufferBackendOps);
#if MOBILEGL_BUILD_DISAGGREGATED
RegisterWireResourceOps();
@@ -731,10 +1205,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferManager::Shutdown() {
if (g_activeBufferManager == this) {
g_activeBufferManager = nullptr;
if (g_activeBufferManager.Get() == this) {
g_activeBufferManager.Get() = nullptr;
#if MOBILEGL_BUILD_DISAGGREGATED
if (MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) {
// Under a transport the table is the PROCESS's and serves every session: it dispatches
// to the calling session's own manager (WireManager), so one session's renderer going
// away must not take it from the others - their buffer creates would be dropped from
// then on, and every buffer born after that would be a handle the server never saw.
if (MG_Config::Transport == MG_Config::TransportMode::Monolith &&
MG_Pipe::MGPipeGetResourceOps() == &g_vulkanWireResourceOps) {
MG_Pipe::MGPipeSetResourceOps(nullptr);
}
#endif
@@ -1027,6 +1506,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&beforeBarrier, 0, nullptr, 0, nullptr);
@@ -1034,11 +1514,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
region.srcOffset = staging.offset;
region.dstOffset = static_cast<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
&afterBarrier, 0, nullptr, 0, nullptr);
@@ -1073,6 +1555,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkMemoryBarrier beforeBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
beforeBarrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
beforeBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 1,
&beforeBarrier, 0, nullptr, 0, nullptr);
@@ -1080,11 +1563,13 @@ namespace MobileGL::MG_Backend::DirectVulkan {
region.srcOffset = staging.offset;
region.dstOffset = static_cast<VkDeviceSize>(offset);
region.size = static_cast<VkDeviceSize>(size);
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBuffer(commandBuffer, staging.buffer, resource.buffer.GetHandle(), 1, &region);
VkMemoryBarrier afterBarrier{VK_STRUCTURE_TYPE_MEMORY_BARRIER};
afterBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
afterBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 1,
&afterBarrier, 0, nullptr, 0, nullptr);
@@ -110,6 +110,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// frontend BufferObject. Acquires expose the full GPU store, without a CPU
// pointer: CPU consumers must use ReadWireBuffer for ordered, current bytes.
Bool AcquireWireSlice(BufferKind kind, MG_Pipe::MGPipeHandle res, BufferSlice& outSlice);
// The handle names a store this session created (whatever its size, zero included).
Bool IsKnownWireBuffer(MG_Pipe::MGPipeHandle res) { return FindWireBuffer(res) != nullptr; }
// Names, for a Fatal about an unknown handle, whether this manager ever saw it.
void DescribeWireBufferHistory(MG_Pipe::MGPipeHandle res) const;
Bool ReadWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, void* dst);
Bool CopyWireBufferRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
const BufferSlice& dst);
@@ -125,6 +129,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool CopyWireBufferSubWordRangeToSlice(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size,
Uint32 frameIndex, const BufferSlice& dst, Uint64 dstSkip);
void MarkWireBufferGpuWritten(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
// P8-D: whether a shader write was marked on this store since the last indirect draw
// read it natively, clearing the mark. The caller records the INDIRECT_COMMAND_READ
// barrier before the draw when this answers true. Transfer writes need no mark: their own
// after-barriers already make them visible to MEMORY_READ at ALL_COMMANDS.
Bool TakeWireIndirectReadBarrier(MG_Pipe::MGPipeHandle res);
// P8-D: every WaitForWireBufferHostAccess that actually waited, over this manager's life -
// the attribution base a caller reads before and after its own ReadWireBuffer.
Uint64 GetWireHostWaitCount() const { return m_wireHostWaits; }
// Resource-op entry points. All run on the server apply owner.
void CreateWireBuffer(MG_Pipe::MGPipeHandle res, const MG_Pipe::MGPResourceDesc& desc);
@@ -134,6 +146,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void FlushWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size, const void* bytes);
void ReadbackWireBuffer(MG_Pipe::MGPipeHandle res, Uint64 offset, Uint64 size);
void DestroyWireBuffer(MG_Pipe::MGPipeHandle res);
// P11 B2 (T0): make the client's AHardwareBuffer `ahb` (a BLOB of `size` bytes) buffer
// `res`'s store - a VkBuffer on memory imported through
// VK_ANDROID_external_memory_android_hardware_buffer, host-visible and mapped like every
// wire store, so every wire path (draw binds, WriteWireBuffer's ordered copy, CPU readers)
// applies unchanged. The T2 store it replaces goes through DeferWireRelease. False = not
// imported (the T2 store is untouched and the client is DECLINED).
Bool ImportWireBuffer(MG_Pipe::MGPipeHandle res, void* ahb, Uint64 size);
// The POST self-test of the sustained-lock pattern on this device (see the definition).
static Bool SelfTestWireImport(char* why, Uint64 whyBytes);
#endif
// Recreate all per-frame transient arenas
@@ -245,12 +266,22 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Only ranges actually submitted by resource_subdata are covered. No
// shadow is retained: flush cannot replay stale bytes over GPU writes.
Vector<Range1D> stagedCoverage;
// P11 B2 (T0): `buffer` is the client's imported AHardwareBuffer. Its readback posts
// no bytes (the client reads its own pages once the wait below it is done).
Bool imported = false;
// P8-D: a shader write was marked (MarkWireBufferGpuWritten) and no indirect draw has
// recorded its INDIRECT_COMMAND_READ barrier since (TakeWireIndirectReadBarrier).
Bool indirectReadBarrierPending = false;
};
// P8-D: see GetWireHostWaitCount.
Uint64 m_wireHostWaits = 0;
static Uint64 WireBufferKey(MG_Pipe::MGPipeHandle res) {
return (static_cast<Uint64>(res.Gen) << 32) | res.Slot;
}
WireBufferResource* FindWireBuffer(MG_Pipe::MGPipeHandle res);
Bool WaitForWireBufferHostAccess(WireBufferResource& resource);
// P11 B2: the serial a T0 store's release waits behind (never "destroy at once").
Uint64 T0ReleaseSerial(Uint64 lastUseSerial) const;
// P7 wave 4 (M2), ID-P7-27: THE WIRE ARM'S ORPHANS NEED A RECLAIM THAT IS NOT A FRAME
// BOUNDARY.
@@ -331,6 +362,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// for the census to count), shared by the host write that cannot wait for the GPU and
// the watermark that cannot. `site` says which.
[[noreturn]] static void WireBufferSyncFatal(const char* site);
// True when the wait or copy that just failed failed because the device is lost: the
// session has been latched (VulkanRenderer::LatchWireDeviceLoss) and the caller returns
// instead of taking its Fatal.
static Bool LatchedOnDeviceLoss(const char* site);
// The wbuf[] gauges (PipeStats.h, Gauge::WireBuffers..WireDeferredSyncs). The peaks are
// taken at the two points the numbers can rise - a park and a mint - and published when
// the stats channel is on; MagmaWireReclaimScenario reads them off the server's line.
@@ -8,13 +8,45 @@
#include "VkBufferObject.h"
#if MOBILEGL_BUILD_DISAGGREGATED
#include <MG_Remote/Transport/AdoptT0.h>
#endif
namespace MobileGL::MG_Backend::DirectVulkan {
#if MOBILEGL_BUILD_DISAGGREGATED
void VkBufferObject::MoveExternalFrom(VkBufferObject& other) {
m_device = other.m_device;
m_externalMemory = other.m_externalMemory;
m_externalAhb = other.m_externalAhb;
m_externalCoherent = other.m_externalCoherent;
other.m_device = VK_NULL_HANDLE;
other.m_externalMemory = VK_NULL_HANDLE;
other.m_externalAhb = nullptr;
other.m_externalCoherent = true;
}
void VkBufferObject::AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb) {
Destroy();
m_device = device;
m_buffer = buffer;
m_externalMemory = memory;
m_mappedData = mapped;
m_size = size;
m_externalCoherent = coherent;
m_externalAhb = ahb;
}
#endif
VkBufferObject::VkBufferObject(VkBufferObject&& other) noexcept {
m_allocator = other.m_allocator;
m_buffer = other.m_buffer;
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -35,6 +67,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_allocation = other.m_allocation;
m_mappedData = other.m_mappedData;
m_size = other.m_size;
#if MOBILEGL_BUILD_DISAGGREGATED
MoveExternalFrom(other);
#endif
other.m_allocator = nullptr;
other.m_buffer = VK_NULL_HANDLE;
@@ -89,6 +124,24 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Destroy() {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
// P11 B2 (T0): an imported AHardwareBuffer store. Reached through the wire arm's
// deferred release, i.e. once no submission can still name it.
if (m_mappedData != nullptr) vkUnmapMemory(m_device, m_externalMemory);
if (m_buffer != VK_NULL_HANDLE) vkDestroyBuffer(m_device, m_buffer, nullptr);
vkFreeMemory(m_device, m_externalMemory, nullptr);
MG_Remote::Transport::AdoptT0::ReleaseImported(m_externalAhb);
m_mappedData = nullptr;
m_buffer = VK_NULL_HANDLE;
m_externalMemory = VK_NULL_HANDLE;
m_externalAhb = nullptr;
m_device = VK_NULL_HANDLE;
m_externalCoherent = true;
m_size = 0;
return;
}
#endif
Unmap();
if (m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr) {
vmaDestroyBuffer(m_allocator, m_buffer, m_allocation);
@@ -117,6 +170,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void VkBufferObject::Unmap() {
#if MOBILEGL_BUILD_DISAGGREGATED
// An imported store stays mapped for its life (Destroy unmaps it).
if (m_externalMemory != VK_NULL_HANDLE) return;
#endif
if (!IsValid() || m_mappedData == nullptr) {
m_mappedData = nullptr;
return;
@@ -143,6 +200,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
Memcpy(static_cast<Uint8*>(mapped) + offset, data, static_cast<SizeT>(size));
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkFlushMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult flushResult = vmaFlushAllocation(m_allocator, m_allocation, offset, size);
if (flushResult != VK_SUCCESS) {
MGLOG_E_ONCE("VkBufferObject::Upload failed: vmaFlushAllocation returned %d", flushResult);
@@ -167,6 +234,16 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if (resolvedSize == 0) {
return true;
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) {
if (m_externalCoherent) return true;
VkMappedMemoryRange range{VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE};
range.memory = m_externalMemory;
range.offset = 0;
range.size = VK_WHOLE_SIZE;
return vkInvalidateMappedMemoryRanges(m_device, 1, &range) == VK_SUCCESS;
}
#endif
const VkResult result = vmaInvalidateAllocation(m_allocator, m_allocation, offset, resolvedSize);
if (result != VK_SUCCESS) {
@@ -64,7 +64,25 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
void* GetMappedData() const { return m_mappedData; }
Bool IsMapped() const { return m_mappedData != nullptr; }
Bool IsValid() const { return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr; }
Bool IsValid() const {
#if MOBILEGL_BUILD_DISAGGREGATED
if (m_externalMemory != VK_NULL_HANDLE) return m_buffer != VK_NULL_HANDLE;
#endif
return m_allocator != nullptr && m_buffer != VK_NULL_HANDLE && m_allocation != nullptr;
}
#if MOBILEGL_BUILD_DISAGGREGATED
// P11 B2 (T0): a store whose memory is NOT this allocator's - a client's AHardwareBuffer
// imported with VK_ANDROID_external_memory_android_hardware_buffer into `memory` (a
// dedicated allocation bound to `buffer`, mapped persistently at `mapped`). It behaves
// like any mapped store (GetSlice, Upload, Invalidate), and Destroy frees the buffer and
// the memory and drops `ahb` - which is why the store goes through the wire arm's
// serial-gated deferred release like every other: the AHardwareBuffer is let go only once
// no submission can still name it. `coherent` = the memory type is HOST_COHERENT.
void AdoptExternal(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize size,
void* mapped, Bool coherent, void* ahb);
Bool IsExternal() const { return m_externalMemory != VK_NULL_HANDLE; }
#endif
private:
VmaAllocator m_allocator = nullptr;
@@ -72,5 +90,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VmaAllocation m_allocation = nullptr;
void* m_mappedData = nullptr;
VkDeviceSize m_size = 0;
#if MOBILEGL_BUILD_DISAGGREGATED
VkDevice m_device = VK_NULL_HANDLE;
VkDeviceMemory m_externalMemory = VK_NULL_HANDLE;
void* m_externalAhb = nullptr;
Bool m_externalCoherent = true;
void MoveExternalFrom(VkBufferObject& other);
#endif
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -7,6 +7,8 @@
// End of Source File Header
#include "VkRenderPassManager.h"
#include "RenderPassGuard.h"
#include <atomic>
#include "MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.h"
#include "MG_State/GLState/TextureState/TextureObject2D.h"
@@ -235,11 +237,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject):
m_device(device), m_physicalDevice(physicalDevice), m_allocator(allocator), m_config(config),
m_clearManager(clearManager), m_textureManager(textureManager), m_swapchainObject(swapchainObject) {
RenderPassEntry::s_device = m_device;
s_clearManager = &m_clearManager;
s_textureManager = &m_textureManager;
s_swapchainObject = &m_swapchainObject;
s_renderPassManager = this;
RenderPassEntry::s_device.Get() = m_device;
s_clearManager.Get() = &m_clearManager;
s_textureManager.Get() = &m_textureManager;
s_swapchainObject.Get() = &m_swapchainObject;
s_renderPassManager.Get() = this;
}
VkRenderPassManager::~VkRenderPassManager() {}
@@ -257,8 +259,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
CollectDeferredRenderbufferReleases(/*destroyAll=*/true); // caller guarantees device idle
m_pendingRenderbufferClears.clear();
RenderPassEntry::s_textureResourcesScratch.clear();
s_activeRenderPass = {};
s_hasActiveRenderPass = false;
s_activeRenderPass.Get() = {};
s_hasActiveRenderPass.Get() = false;
m_rpFastValid = false;
}
@@ -607,6 +609,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool isDefaultFbo = fbo.IsDefaultFramebuffer();
if (isDefaultFbo) {
XXHASH_VERIFY(XXH64_update(m_hashState, &swapchainImageIndex, sizeof(swapchainImageIndex)));
XXHASH_VERIFY(XXH64_update(m_hashState, &m_defaultFramebufferTarget, sizeof(m_defaultFramebufferTarget)));
}
// sRGB attachments switch between their sRGB and UNORM-twin views with this
// capability (ResolveSrgbAttachmentWriteFormat), changing the render pass formats.
@@ -1528,6 +1531,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return &insertedIt->second;
}
void VkRenderPassManager::PurgeRenderPasses() {
Vector<VkRenderPass> destroyedRenderPasses;
destroyedRenderPasses.reserve(m_renderPasses.size());
for (auto& [_, entry] : m_renderPasses) destroyedRenderPasses.push_back(entry.renderPass);
m_renderPasses.clear();
m_rpFastValid = false;
if (!destroyedRenderPasses.empty() && m_evictionObserver != nullptr) {
m_evictionObserver->OnRenderPassesDestroyed(destroyedRenderPasses);
}
}
void VkRenderPassManager::OnPresent() {
++m_frameCounter;
@@ -1554,9 +1568,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// immediately - and a single batched notification costs one pipeline-cache
// scan instead of one per evicted pass.
Vector<VkRenderPass> destroyedRenderPasses;
const Uint64 activeHash = s_hasActiveRenderPass ? s_activeRenderPass.hash : 0;
const Uint64 activeHash = s_hasActiveRenderPass.Get() ? s_activeRenderPass.Get().hash : 0;
for (auto it = m_renderPasses.begin(); it != m_renderPasses.end();) {
const Bool isActive = s_hasActiveRenderPass && it->first == activeHash;
const Bool isActive = s_hasActiveRenderPass.Get() && it->first == activeHash;
if (!isActive && m_frameCounter - it->second.lastUsedFrame > kRetireAgeFrames) {
if (m_rpFastValid && m_rpFastRenderPassHash == it->first) {
m_rpFastValid = false;
@@ -1601,8 +1615,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// texture path below is immune because it re-reads the live payload).
// Same defense as ClearAttachmentsOnActiveRenderPass: prefer the live
// pending clear, fall back to the snapshot only when none is queued.
if (s_renderPassManager != nullptr &&
s_renderPassManager->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
if (s_renderPassManager.Get() != nullptr &&
s_renderPassManager.Get()->GetPendingRenderbufferClear(pending.renderbuffer, clearPayload)) {
if ((clearPayload.mask & GL_COLOR_BUFFER_BIT) != 0 && pending.renderbuffer != nullptr &&
MG_Util::GetBaseInternalFormatComponentCount(pending.renderbuffer->GetInternalFormat()) ==
3) {
@@ -1614,7 +1628,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
} else {
if (pending.key.texture == nullptr ||
!s_clearManager->GetPendingClear(pending.key, clearPayload, liveTexture)) {
!s_clearManager.Get()->GetPendingClear(pending.key, clearPayload, liveTexture)) {
continue;
}
}
@@ -1636,48 +1650,58 @@ namespace MobileGL::MG_Backend::DirectVulkan {
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
// Pre-pass stream bookkeeping: this pass's attachment images are now
// referenced by the open frame recording.
if (s_textureManager != nullptr) {
if (s_textureManager.Get() != nullptr) {
for (const auto& tracked : renderPassEntry.trackedAttachmentLayouts) {
if (tracked.target == TrackedAttachmentTarget::Texture) {
if (const auto texture = tracked.texture.lock()) {
s_textureManager->StampTextureRecordingUse(texture.get());
s_textureManager.Get()->StampTextureRecordingUse(texture.get());
}
}
}
}
for (const auto& pending: renderPassEntry.pendingClearAttachments) {
if (pending.hasInlinePayload) {
if (s_renderPassManager != nullptr) {
s_renderPassManager->PopPendingRenderbufferClear(pending.renderbuffer);
if (s_renderPassManager.Get() != nullptr) {
s_renderPassManager.Get()->PopPendingRenderbufferClear(pending.renderbuffer);
}
} else {
s_clearManager->PopPendingClear(pending.key);
s_clearManager.Get()->PopPendingClear(pending.key);
}
}
s_activeRenderPass.hash = renderPassEntry.hash;
s_activeRenderPass.compatibilityHash = renderPassEntry.compatibilityHash;
s_activeRenderPass.trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
s_activeRenderPass.extent = renderPassEntry.extent;
s_hasActiveRenderPass = true;
s_activeRenderPass.Get().commandBuffer = commandBuffer;
s_activeRenderPass.Get().hash = renderPassEntry.hash;
s_activeRenderPass.Get().compatibilityHash = renderPassEntry.compatibilityHash;
s_activeRenderPass.Get().trackedAttachmentLayouts = renderPassEntry.trackedAttachmentLayouts;
s_activeRenderPass.Get().extent = renderPassEntry.extent;
s_hasActiveRenderPass.Get() = true;
return true;
}
Bool VkRenderPassManager::EndRenderPass(VkCommandBuffer commandBuffer) {
BumpWireImageWriteEpoch();
auto* activeRenderPass = GetActiveRenderPass();
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
if (activeRenderPass != nullptr) {
trackedAttachmentLayouts = Move(activeRenderPass->trackedAttachmentLayouts);
}
vkCmdEndRenderPass(commandBuffer);
// Layout reconciliation below may record barriers, whose guard closes an active
// pass. Detach the ended pass first so that guard cannot recursively end it again.
s_activeRenderPass.Get() = {};
s_hasActiveRenderPass.Get() = false;
// The fast-path memo reuses the ACTIVE render pass; once the pass ends it must not carry
// over (the next span may be a different FBO resolved before its render pass is begun).
if (s_renderPassManager != nullptr) {
s_renderPassManager->m_rpFastValid = false;
if (s_renderPassManager.Get() != nullptr) {
s_renderPassManager.Get()->m_rpFastValid = false;
}
if (activeRenderPass != nullptr && !activeRenderPass->trackedAttachmentLayouts.empty()) {
for (const auto& trackedAttachment : activeRenderPass->trackedAttachmentLayouts) {
if (!trackedAttachmentLayouts.empty()) {
for (const auto& trackedAttachment : trackedAttachmentLayouts) {
switch (trackedAttachment.target) {
case TrackedAttachmentTarget::Texture:
MOBILEGL_ASSERT(s_textureManager != nullptr, "EndRenderPass: texture manager is null");
MOBILEGL_ASSERT(s_textureManager.Get() != nullptr, "EndRenderPass: texture manager is null");
if (const auto texture = trackedAttachment.texture.lock()) {
s_textureManager->UpdateTrackedImageLayoutAfterAttachmentWrite(
s_textureManager.Get()->UpdateTrackedImageLayoutAfterAttachmentWrite(
commandBuffer,
texture.get(),
trackedAttachment.textureMipLevel,
@@ -1685,27 +1709,27 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
break;
case TrackedAttachmentTarget::Renderbuffer:
MOBILEGL_ASSERT(s_renderPassManager != nullptr, "EndRenderPass: render pass manager is null");
MOBILEGL_ASSERT(s_renderPassManager.Get() != nullptr, "EndRenderPass: render pass manager is null");
if (const auto renderbuffer = trackedAttachment.renderbuffer.lock()) {
auto resourceIt =
s_renderPassManager->m_renderbufferResources.find(renderbuffer.get());
if (resourceIt != s_renderPassManager->m_renderbufferResources.end()) {
s_renderPassManager.Get()->m_renderbufferResources.find(renderbuffer.get());
if (resourceIt != s_renderPassManager.Get()->m_renderbufferResources.end()) {
resourceIt->second.layout = trackedAttachment.finalLayout;
}
}
break;
case TrackedAttachmentTarget::SwapchainColor:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject.Get()->SetImageLayout(trackedAttachment.swapchainImageIndex, trackedAttachment.finalLayout);
// The pass stored into the attachment: its content is defined
// until the image is next presented.
s_swapchainObject->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
s_swapchainObject.Get()->SetImageContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
case TrackedAttachmentTarget::SwapchainDepthStencil:
MOBILEGL_ASSERT(s_swapchainObject != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
MOBILEGL_ASSERT(s_swapchainObject.Get() != nullptr, "EndRenderPass: swapchain object is null");
s_swapchainObject.Get()->SetDepthStencilImageLayout(trackedAttachment.swapchainImageIndex,
trackedAttachment.finalLayout);
s_swapchainObject->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
s_swapchainObject.Get()->SetDepthStencilContentDefined(trackedAttachment.swapchainImageIndex, true);
break;
default:
MOBILEGL_ASSERT(false, "EndRenderPass: unsupported tracked attachment target=%d",
@@ -1714,12 +1738,23 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
s_activeRenderPass = {};
s_hasActiveRenderPass = false;
return true;
}
ActiveRenderPassInfo* VkRenderPassManager::GetActiveRenderPass() {
return s_hasActiveRenderPass ? &s_activeRenderPass : nullptr;
return s_hasActiveRenderPass.Get() ? &s_activeRenderPass.Get() : nullptr;
}
namespace {
std::atomic<Uint64> g_wireImageWriteEpoch{1};
}
Uint64 WireImageWriteEpoch() { return g_wireImageWriteEpoch.load(std::memory_order_relaxed); }
void BumpWireImageWriteEpoch() { g_wireImageWriteEpoch.fetch_add(1, std::memory_order_relaxed); }
void EndActiveRenderPassOn(VkCommandBuffer commandBuffer) {
BumpWireImageWriteEpoch();
const auto* active = VkRenderPassManager::GetActiveRenderPass();
if (active != nullptr && active->commandBuffer == commandBuffer && commandBuffer != VK_NULL_HANDLE)
VkRenderPassManager::EndRenderPass(commandBuffer);
}
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -66,8 +66,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkExtent2D swapchainExtent);
struct RenderPassEntry {
static inline VkDevice s_device;
static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
// Per Magma session (MagmaSession.h): each session's renderer is its own device.
static inline SessionLocal<VkDevice> s_device;
static inline thread_local Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
Uint64 hash = 0;
VkRenderPass renderPass = VK_NULL_HANDLE;
VkFramebuffer framebuffer = VK_NULL_HANDLE;
@@ -165,10 +166,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
~RenderPassEntry() {
if (renderPass != VK_NULL_HANDLE) {
vkDestroyRenderPass(s_device, renderPass, nullptr);
vkDestroyRenderPass(s_device.Get(), renderPass, nullptr);
}
if (framebuffer != VK_NULL_HANDLE) {
vkDestroyFramebuffer(s_device, framebuffer, nullptr);
vkDestroyFramebuffer(s_device.Get(), framebuffer, nullptr);
}
}
@@ -184,6 +185,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
struct ActiveRenderPassInfo {
Uint64 hash = 0;
Uint64 compatibilityHash = 0;
VkCommandBuffer commandBuffer = VK_NULL_HANDLE;
Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
IntVec2 extent = {0, 0};
@@ -226,6 +228,15 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool Initialize();
void Shutdown();
// The surface target the default framebuffer currently names (VulkanRenderer's surface
// targets): a default-framebuffer pass of one surface is not another's.
void SetDefaultFramebufferTarget(Uint64 serial) {
m_defaultFramebufferTarget = serial;
m_rpFastValid = false;
}
// Destroys every cached pass and framebuffer (not the renderbuffers). The device must be
// idle and no pass may be open: a surface target's image views are going away.
void PurgeRenderPasses();
HashType ComputeHash(
const MG_State::GLState::FramebufferObject& fbo,
@@ -302,6 +313,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
// version change, swapchain rotation, any attachment image recreation (the two epochs),
// or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
Uint64 m_defaultFramebufferTarget = 0;
Bool m_rpFastValid = false;
const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
// The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
@@ -411,12 +423,14 @@ namespace MobileGL::MG_Backend::DirectVulkan {
void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
void CollectDeferredRenderbufferReleases(Bool destroyAll);
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline ActiveRenderPassInfo s_activeRenderPass{};
static inline Bool s_hasActiveRenderPass = false;
static inline VkClearManager* s_clearManager = nullptr;
static inline VkTextureManager* s_textureManager = nullptr;
static inline SwapchainObject* s_swapchainObject = nullptr;
static inline VkRenderPassManager* s_renderPassManager = nullptr;
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
// Per Magma session (MagmaSession.h): one session's open render pass and managers are not
// another's.
static inline SessionLocal<ActiveRenderPassInfo> s_activeRenderPass;
static inline SessionLocal<Bool> s_hasActiveRenderPass;
static inline SessionLocal<VkClearManager*> s_clearManager;
static inline SessionLocal<VkTextureManager*> s_textureManager;
static inline SessionLocal<SwapchainObject*> s_swapchainObject;
static inline SessionLocal<VkRenderPassManager*> s_renderPassManager;
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -138,6 +138,6 @@ private:
UnorderedMap<Uint64, SamplerCacheEntry> m_samplers;
// Monotonic frame-boundary counter (bumped in OnFrameBoundary) for cache aging.
Uint64 m_frameBoundaryCounter = 0;
static inline XXH64_state_t* m_hashState = XXH64_createState();
static inline thread_local XXH64_state_t* m_hashState = XXH64_createState();
};
} // namespace MobileGL::MG_Backend::DirectVulkan
@@ -6,6 +6,7 @@
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
#include "RenderPassGuard.h"
#include "VkTextureManager.h"
#include "ProgramFactory.h"
@@ -22,10 +23,16 @@
// (shape) and the server's staged-texture store (texels).
#include <MG_Pipe/PipeApply.h>
#include <MG_Remote/Server/StagedTextureStore.h>
// Shared images: a texture whose level 0 is a server-allocated AHardwareBuffer.
#include <MG_Remote/Server/SharedImageRegistry.h>
#include "../DirectVulkan.h"
// P7 wave 2 package B3: rule I's tally for the silent exits on this file's wire arm.
#include "WireDeclineTally.h"
#if defined(__ANDROID__)
#include <android/hardware_buffer.h>
#endif
#endif
#include <string>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
@@ -42,9 +49,28 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// their feature is off (VUID-vkCmdPipelineBarrier-srcStageMask-04090/-04091), and ALL_GRAPHICS
// would also serialize against non-shader stages. The default only matters before a device
// exists, when nothing records barriers.
static VkPipelineStageFlags s_sampledReadStages =
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
// Per Magma session: each session's renderer is its own device.
struct SampledReadStages {
VkPipelineStageFlags value = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT |
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT;
};
static SessionLocal<SampledReadStages> s_sampledReadStagesSlot;
static VkImageUsageFlags ResolveTextureUsage(VkImageAspectFlags aspect,
VkFormatFeatureFlags formatFeatures,
Bool supportsStorageImage) {
VkImageUsageFlags usage = VK_IMAGE_USAGE_SAMPLED_BIT;
if (supportsStorageImage) usage |= VK_IMAGE_USAGE_STORAGE_BIT;
// A color aspect does not imply renderability (for example, RGB9_E5).
if ((aspect & VK_IMAGE_ASPECT_COLOR_BIT) &&
(formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)) {
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
}
if (aspect & (VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT)) {
usage |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
}
return usage;
}
static Uint32 ComputeFullMipLevelCount(const IntVec3& baseTexelSize) {
Int maxDimension = std::max<Int>(baseTexelSize.x(),
@@ -203,7 +229,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outSrcStageMask = s_sampledReadStages;
outSrcStageMask = s_sampledReadStagesSlot->value;
outSrcAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -267,7 +293,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
case VK_IMAGE_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_OPTIMAL:
case VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_OPTIMAL:
case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:
outDstStageMask = s_sampledReadStages;
outDstStageMask = s_sampledReadStagesSlot->value;
outDstAccessMask = VK_ACCESS_SHADER_READ_BIT;
return;
case VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:
@@ -367,6 +393,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
copyRegions.push_back(copy);
}
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyImage(commandBuffer,
oldResource.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
newResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
@@ -646,7 +673,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
m_commandPool = initInfo.commandPool;
m_graphicsQueue = initInfo.graphicsQueue;
m_imageFormatListSupported = initInfo.imageFormatListSupported;
s_sampledReadStages = initInfo.sampledReadStageMask;
s_sampledReadStagesSlot->value = initInfo.sampledReadStageMask;
m_currentFrameIndex = 0;
m_deferredReleases.clear();
m_deferredReleases.resize(initInfo.frameCount);
@@ -659,8 +686,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
MOBILEGL_ASSERT(initInfo.frameCount > 0,
"VkTextureManager::Initialize failed: frameCount must be > 0");
TextureResource::s_device = m_device;
TextureResource::s_allocator = m_allocator;
TextureResource::s_device.Get() = m_device;
TextureResource::s_allocator.Get() = m_allocator;
// Own pool for the recycled upload-batch command buffers. Parking a
// dozen reset-but-alive command buffers in the renderer's shared pool
@@ -698,6 +725,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// Destroy images while the device/allocator still exist, including after context death.
m_wireTextureResources.clear();
m_wireRenderbufferResources.clear();
m_sharedImageUses.clear();
#endif
m_aliveObjects.clear();
m_storageImageTextures.clear();
@@ -1333,6 +1361,42 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
VkTextureManager::TextureResource* VkTextureManager::FindTextureResource(
MG_State::GLState::ITextureObject& textureOrView) {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() ? &it->second : nullptr;
}
Bool VkTextureManager::HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.image != VK_NULL_HANDLE;
}
Bool VkTextureManager::HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const {
auto& storage = StorageTextureOf(textureOrView);
auto it = m_textureResources.find(MakeTextureIdentity(&storage));
if (it == m_textureResources.end() || it->second.image == VK_NULL_HANDLE) {
return false;
}
return it->second.syncedShapeVersion == storage.GetShapeVersion() &&
it->second.syncedContentVersion != storage.GetContentVersion();
}
Bool VkTextureManager::WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const {
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(textureOrView)));
return it != m_textureResources.end() && it->second.lastRecordingWriteGeneration == m_recordingGeneration;
}
void VkTextureManager::StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture) {
if (texture == nullptr) {
return;
}
auto it = m_textureResources.find(MakeTextureIdentity(&StorageTextureOf(*texture)));
if (it != m_textureResources.end()) {
StampResourceRecordingWrite(it->second);
}
}
void VkTextureManager::UpdateTrackedImageLayout(MG_State::GLState::ITextureObject* texture, VkImageLayout newLayout) {
MOBILEGL_ASSERT(texture != nullptr, "UpdateTrackedImageLayout: texture is null");
auto it = m_textureResources.find(MakeTextureIdentity(texture));
@@ -1361,7 +1425,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
"UpdateTrackedImageLayoutAfterAttachmentWrite: textureId=%d mipLevel=%u out of range %u",
texture->GetExternalIndex(), writtenMipLevel, resource.mipLevels);
// Pre-pass stream bookkeeping: the render pass that just ended wrote this image.
StampResourceRecordingUse(resource);
StampResourceRecordingWrite(resource);
if (resource.layout != newLayout && resource.mipLevels > 1) {
VkPipelineStageFlags srcStageMask = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
@@ -1441,7 +1505,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
const Bool ok = TransitionImageLayout(commandBuffer, resource->image, resource->layout, targetLayout, srcStageMask,
s_sampledReadStages, srcAccessMask,
s_sampledReadStagesSlot->value, srcAccessMask,
VK_ACCESS_SHADER_READ_BIT, resource->aspect, 0, resource->mipLevels);
MOBILEGL_ASSERT(ok, "TransitionTextureForSampling: transition failed for textureId=%d", texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
@@ -1475,8 +1539,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource->aspect, 0, resource->mipLevels);
MOBILEGL_ASSERT(ok, "TransitionTextureForStorageImage: transition failed for textureId=%d",
texture.GetExternalIndex());
// Pre-pass stream bookkeeping: a command referencing the image was recorded.
StampResourceRecordingUse(*resource);
// Pre-pass stream bookkeeping: a command referencing the image was recorded, and an
// image unit may store to it.
StampResourceRecordingWrite(*resource);
return ok;
}
@@ -1611,6 +1676,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
std::max(source->depth >> level, 1u)};
copyRegions.push_back(copy);
}
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyImage(commandBuffer, source->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, snapshot.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, static_cast<Uint32>(copyRegions.size()), copyRegions.data());
@@ -1725,6 +1791,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// slices" but will read as "depth slice 0" once VK_KHR_maintenance9 is enabled. The
// validation layer warns about that literal 1 by name.
barrier.subresourceRange.layerCount = VK_REMAINING_ARRAY_LAYERS;
EndActiveRenderPassOn(commandBuffer);
vkCmdPipelineBarrier(commandBuffer, srcStageMask, dstStageMask, 0, 0, nullptr, 0, nullptr, 1, &barrier);
trackedLayout = newLayout;
@@ -2068,6 +2135,44 @@ namespace MobileGL::MG_Backend::DirectVulkan {
Bool VkTextureManager::SyncWireTextureShape(const MG_Pipe::MGPipeResourceRecord& record,
TextureResource& resource, Bool requireStorage) {
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
// A YUV shared image is converted INTO the texture's own RGBA8 storage, which the rest of
// this function defines as for any texture; the image is only named here, and the
// renderer converts it at its first use in each frame (WireYuvImage.inc).
if (record.SharedImageId != resource.yuvImageId && !m_syncingYuvTextureStorage) {
// Respecified away from (or to another) YUV image: its source goes with it, once the
// GPU is done with it (a released resource's Reset is deferred).
if (resource.yuvSource != nullptr) {
TextureResource retired;
retired.yuvSource = std::move(resource.yuvSource);
DeferResourceRelease(Move(retired));
}
resource.yuvImageId = 0;
resource.yuvOwner.reset();
resource.yuvSource.reset();
resource.yuvConvertedFrame = 0;
}
if (record.SharedImageId != 0 && !m_syncingYuvTextureStorage) {
const auto yuv = MG_Remote::Server::SharedImages::Find(record.SharedImageId);
if (yuv != nullptr && MG_Remote::Server::SharedImages::FourccIsYuv(yuv->Fourcc)) {
m_syncingYuvTextureStorage = true;
const Bool defined = SyncWireTextureShape(record, resource, requireStorage);
m_syncingYuvTextureStorage = false;
if (!defined) return false;
if (resource.yuvImageId != record.SharedImageId) {
resource.yuvImageId = record.SharedImageId;
resource.yuvOwner = yuv;
resource.yuvSource.reset();
resource.yuvConvertedFrame = 0;
}
return true;
}
}
// Level 0 is a shared image: bind its buffer. An image that cannot be bound here leaves
// the texture the storage its NULL-data definition asked for, below.
if (record.SharedImageId != 0 && !m_syncingYuvTextureStorage) {
const SharedImageBind bound = SyncWireSharedImage(record, resource, requireStorage);
if (bound != SharedImageBind::Unresolved) return bound == SharedImageBind::Bound;
}
if (desc.StorageKind != static_cast<Uint8>(TextureStorageType::Mipmap)) {
MGLOG_W_ONCE("Magma wire texture {slot=%u, gen=%u}: buffer-backed textures are not migrated; declined",
desc.Resource.Slot, desc.Resource.Gen);
@@ -2167,13 +2272,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkImageUsageFlags desiredUsage =
VK_IMAGE_USAGE_SAMPLED_BIT | (supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) &&
(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT)
? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT))
? VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
: 0);
ResolveTextureUsage(aspect, formatProperties.optimalTilingFeatures, supportsStorageImage);
// Multisample attachments are also transfer sources for vkCmdResolveImage.
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
@@ -2236,7 +2335,10 @@ namespace MobileGL::MG_Backend::DirectVulkan {
: (desc.Levels > 1 ? std::min(std::max<Uint32>(desc.Levels, fullMipLevels), fullMipLevels)
: 1u);
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.format == format &&
// A shared image never stands in for allocated storage: the attach is over (or never
// resolved), so the texture gets storage of its own, and none of the image's texels.
const Bool compatible = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
resource.format == format &&
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
@@ -2251,7 +2353,8 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// A named-level respecify changes ONLY that level; a BindMask update changes no
// storage at all. Preserve every compatible old level when growing/upgrading the
// allocation. A whole-store redefinition permits (but does not require) old texels.
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.format == format &&
const Bool preserve = resource.image != VK_NULL_HANDLE && resource.sharedImageId == 0 &&
resource.format == format &&
resource.extent.width == desc.Width && resource.extent.height == imageHeight &&
resource.depth == shapeInfo.depth && resource.arrayLayers == shapeInfo.arrayLayers &&
resource.viewType == shapeInfo.viewType &&
@@ -2645,9 +2748,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
stencilCopy.bufferOffset = stagingBase + item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
} else {
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, resource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
}
@@ -2656,7 +2761,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const VkImageLayout finalLayout = ResolveSampledReadOnlyLayout(aspectMask);
VkImageLayout uploadLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
ok = TransitionImageLayout(commandBuffer, resource.image, uploadLayout, finalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStages,
VK_PIPELINE_STAGE_TRANSFER_BIT, s_sampledReadStagesSlot->value,
VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, aspectMask, 0,
resource.mipLevels);
MOBILEGL_ASSERT(ok, "UploadPendingWireLevels: transition to the sampled layout failed");
@@ -2729,6 +2834,201 @@ namespace MobileGL::MG_Backend::DirectVulkan {
return MG_Pipe::kMGPipeNullHandle;
}
Bool VkTextureManager::ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height,
ImportedSharedImage& out, String& why) {
out = ImportedSharedImage{};
#if defined(__ANDROID__)
auto* ahb = static_cast<AHardwareBuffer*>(nativeBuffer);
if (ahb == nullptr) {
why = "the image has no AHardwareBuffer";
return false;
}
VulkanRenderer::WireAhbImport ctx;
if (pVulkanRenderer == nullptr || !pVulkanRenderer->GetWireAhbImport(ctx)) {
why = "the device did not take VK_ANDROID_external_memory_android_hardware_buffer";
return false;
}
AHardwareBuffer_Desc desc{};
AHardwareBuffer_describe(ahb, &desc);
if (desc.width != width || desc.height != height || desc.layers != 1) {
why = "the buffer is " + std::to_string(desc.width) + "x" + std::to_string(desc.height) + "x" +
std::to_string(desc.layers) + ", not " + std::to_string(width) + "x" + std::to_string(height);
return false;
}
const auto getProperties =
reinterpret_cast<PFN_vkGetAndroidHardwareBufferPropertiesANDROID>(ctx.getAhbProperties);
VkAndroidHardwareBufferFormatPropertiesANDROID formatProperties{
VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_FORMAT_PROPERTIES_ANDROID};
VkAndroidHardwareBufferPropertiesANDROID properties{VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID};
properties.pNext = &formatProperties;
VkResult result = getProperties(ctx.device, ahb, &properties);
if (result != VK_SUCCESS) {
why = "vkGetAndroidHardwareBufferPropertiesANDROID -> " + std::to_string(static_cast<int>(result));
return false;
}
// The RGBA8/RGBX8 buffers the registry allocates have a Vulkan format; an external-only
// format would need a YCbCr conversion and is not a colour buffer this path can write.
if (formatProperties.format == VK_FORMAT_UNDEFINED) {
why = "the buffer has no Vulkan format (external format only)";
return false;
}
// Usage is bounded by the buffer's own usage (the AHardwareBuffer usage equivalence);
// transfers need no buffer usage bit.
VkImageUsageFlags usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE) != 0 &&
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0)
usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
if ((desc.usage & AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT) != 0 &&
(formatProperties.formatFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0)
usage |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
VkExternalMemoryImageCreateInfo external{VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO};
external.handleTypes = VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
imageInfo.pNext = &external;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.format = formatProperties.format;
imageInfo.extent = {width, height, 1};
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.usage = usage;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
result = vkCreateImage(ctx.device, &imageInfo, nullptr, &out.image);
if (result != VK_SUCCESS) {
out.image = VK_NULL_HANDLE;
why = "vkCreateImage(external AHB) -> " + std::to_string(static_cast<int>(result));
return false;
}
Int32 type = -1;
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
if ((properties.memoryTypeBits & (1u << i)) != 0 &&
(ctx.memory.memoryTypes[i].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0)
type = static_cast<Int32>(i);
}
for (Uint32 i = 0; i < ctx.memory.memoryTypeCount && type < 0; ++i) {
if ((properties.memoryTypeBits & (1u << i)) != 0) type = static_cast<Int32>(i);
}
if (type < 0) {
why = "no memory type can import it";
DestroyImportedSharedImage(ctx.device, out);
return false;
}
VkMemoryDedicatedAllocateInfo dedicated{VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO};
dedicated.image = out.image;
VkImportAndroidHardwareBufferInfoANDROID import{VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID};
import.pNext = &dedicated;
import.buffer = ahb;
VkMemoryAllocateInfo allocateInfo{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocateInfo.pNext = &import;
allocateInfo.allocationSize = properties.allocationSize;
allocateInfo.memoryTypeIndex = static_cast<Uint32>(type);
result = vkAllocateMemory(ctx.device, &allocateInfo, nullptr, &out.memory);
if (result != VK_SUCCESS) {
out.memory = VK_NULL_HANDLE;
why = "vkAllocateMemory(import AHB) -> " + std::to_string(static_cast<int>(result));
DestroyImportedSharedImage(ctx.device, out);
return false;
}
result = vkBindImageMemory(ctx.device, out.image, out.memory, 0);
if (result != VK_SUCCESS) {
why = "vkBindImageMemory -> " + std::to_string(static_cast<int>(result));
DestroyImportedSharedImage(ctx.device, out);
return false;
}
out.format = formatProperties.format;
out.usage = usage;
out.features = formatProperties.formatFeatures;
out.queueFamily = ctx.queueFamily;
return true;
#else
(void)nativeBuffer, (void)width, (void)height;
why = "shared images are imported on Android only";
return false;
#endif
}
void VkTextureManager::DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image) {
if (image.image != VK_NULL_HANDLE) vkDestroyImage(device, image.image, nullptr);
if (image.memory != VK_NULL_HANDLE) vkFreeMemory(device, image.memory, nullptr);
image = ImportedSharedImage{};
}
VkTextureManager::SharedImageBind VkTextureManager::SyncWireSharedImage(
const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource, Bool requireStorage) {
const MG_Pipe::MGPResourceDesc& desc = record.Desc;
const Uint64 id = record.SharedImageId;
if (requireStorage) {
// The buffer's usage has no storage equivalent; a reallocation would drop the image.
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is bound as a shader image, "
"which it cannot back; declined",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
return SharedImageBind::Failed;
}
// Already this image: the serial moved for something else (params, the bind mask).
if (resource.sharedImageId == id && resource.image != VK_NULL_HANDLE) return SharedImageBind::Bound;
const auto image = MG_Remote::Server::SharedImages::Find(id);
if (image == nullptr) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu is no longer live; the texture "
"keeps its own (undefined) level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id));
return SharedImageBind::Unresolved;
}
const Bool flat2D = desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::Tex2D) ||
desc.Target == static_cast<Uint8>(MG_Pipe::MGPipeResourceTarget::TexRect);
if (!flat2D || desc.Width != image->Width || desc.Height != image->Height ||
!MG_Pipe::MGPipeHandleIsNull(desc.ViewOf)) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: pipe target %u at %ux%u cannot take shared image "
"%llu (%ux%u); the texture keeps its own level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<Uint32>(desc.Target), desc.Width,
desc.Height, static_cast<unsigned long long>(id), image->Width, image->Height);
return SharedImageBind::Unresolved;
}
ImportedSharedImage imported;
String why;
if (!ImportSharedImage(image->Native, image->Width, image->Height, imported, why)) {
MGLOG_E_ONCE("Magma wire texture {slot=%u, gen=%u}: shared image %llu not imported - %s; the texture "
"keeps its own level 0",
desc.Resource.Slot, desc.Resource.Gen, static_cast<unsigned long long>(id), why.c_str());
return SharedImageBind::Unresolved;
}
// NOT ACQUIRED HERE. The writer (a client session's present, on its own device) releases
// the image to VK_QUEUE_FAMILY_FOREIGN_EXT in GENERAL - also the layout every wire
// descriptor samples in - once per frame it writes. Each use acquires it again when the
// write generation moved, waiting for that write's fence on the GPU, and the frame boundary
// releases it back (VulkanRenderer::AcquireSharedImage, WireSharedImage.inc).
TextureResource replacement;
replacement.image = imported.image;
replacement.importedMemory = imported.memory;
replacement.layout = VK_IMAGE_LAYOUT_GENERAL;
replacement.extent = {image->Width, image->Height};
replacement.depth = 1;
replacement.arrayLayers = 1;
replacement.mipLevels = 1;
replacement.sampledBaseMipLevel = 0;
replacement.sampledLevelCount = 1;
replacement.format = imported.format;
replacement.aspect = VK_IMAGE_ASPECT_COLOR_BIT;
replacement.viewType = VK_IMAGE_VIEW_TYPE_2D;
replacement.sampleCount = VK_SAMPLE_COUNT_1_BIT;
replacement.imageCreateFlags = 0;
replacement.usageFlags = imported.usage;
replacement.sharedImageId = id;
replacement.sharedImageOwner = image;
replacement.sharedImageAlphaOne = MG_Remote::Server::SharedImages::FourccIgnoresAlpha(image->Fourcc);
++m_textureImageEpoch; // a new attachment image invalidates cached render passes
DeferResourceRelease(Move(resource));
std::destroy_at(&resource);
std::construct_at(&resource, Move(replacement));
MGLOG_D("Magma wire texture {slot=%u, gen=%u}: level 0 is shared image %llu (%ux%u)", desc.Resource.Slot,
desc.Resource.Gen, static_cast<unsigned long long>(id), image->Width, image->Height);
return SharedImageBind::Bound;
}
VkTextureManager::TextureResource* VkTextureManager::SyncTextureResourceByHandle(
MG_Pipe::MGPipeHandle handle, Bool renderbuffer, Bool requireStorage) {
MOBILEGL_ASSERT(m_device != VK_NULL_HANDLE, "SyncTextureResourceByHandle: m_device == VK_NULL_HANDLE");
@@ -2805,6 +3105,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
handle.Slot, handle.Gen);
return nullptr;
}
resource.sharedImageKey = resource.sharedImageId != 0 || resource.yuvImageId != 0 ? key : 0;
resource.syncedWireSerial = record.Serial;
return &resource;
}
@@ -2926,6 +3227,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
if ((aspect & copyAspect) == 0) continue;
copy.srcSubresource.aspectMask = copyAspect;
copy.dstSubresource.aspectMask = copyAspect;
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyImage(commandBuffer, old.image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, grown.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copy);
}
@@ -3221,12 +3523,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
VkImageUsageFlags desiredUsage =
VK_IMAGE_USAGE_SAMPLED_BIT |
(supportsStorageImage ? VK_IMAGE_USAGE_STORAGE_BIT : 0) |
((aspect & VK_IMAGE_ASPECT_COLOR_BIT) ? VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT : 0) |
(((aspect & VK_IMAGE_ASPECT_DEPTH_BIT) || (aspect & VK_IMAGE_ASPECT_STENCIL_BIT)) ?
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :
0);
ResolveTextureUsage(aspect, formatProperties.optimalTilingFeatures, supportsStorageImage);
if (!isMultisampleTexture) {
desiredUsage |= VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
}
@@ -3521,7 +3818,11 @@ namespace MobileGL::MG_Backend::DirectVulkan {
resource.sampledView == VK_NULL_HANDLE &&
resource.perMipViews.empty() && resource.perMipSampledViews.empty() &&
resource.attachmentViews.empty() && resource.alternateSampledViews.empty() &&
resource.storageImageViews.empty()) {
resource.storageImageViews.empty()
#if MOBILEGL_BUILD_DISAGGREGATED
&& resource.yuvSource == nullptr
#endif
) {
return;
}
@@ -4077,18 +4378,17 @@ namespace MobileGL::MG_Backend::DirectVulkan {
VkDeviceSize offset = 0;
// Sub-region upload (a small sprite in a big atlas): only the dirty box
// is staged and copied. texelSize keeps the LEVEL extent - the staging
// row copy needs it for the shadow's stride. Plain color formats only;
// the RGB-expand and depth(+stencil) conversion passes rewrite whole
// levels and stay full-size.
// row copy needs it for the stride. Color formats only (an RGB-expanded
// level is cut from its expanded bytes); the depth(+stencil) conversion
// passes rewrite whole levels and stay full-size.
Bool subRegion = false;
IntVec3 regionLo = {0, 0, 0};
IntVec3 regionSize = {0, 0, 0};
SizeT texelBytes = 0;
// Scatter refinement of the single dirty box: when the storage's rect
// list reports the writes' true footprint (~100 sprites whose union box
// spans the whole atlas), each rect is staged tightly and copied with
// its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage. Empty means
// "stage the one box above". Only set while subRegion.
// The writes' exact footprint when it is more than one box (~100 sprites
// whose union box spans the whole atlas): each rect is staged tightly and
// copied with its own VkBufferImageCopy in ONE vkCmdCopyBufferToImage.
// Empty means "stage the one box above". Only set while subRegion.
Vector<MG_State::GLState::MipmapDirtyRegion> rects;
};
@@ -4100,6 +4400,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
targets.push_back(uploadTarget);
}
const TextureFormatInfo formatInfo = ResolveTextureFormatInfo(mipmapTexture.GetFormat());
Vector<MG_State::GLState::MipmapDirtyRegion> dirtyFootprint;
VkDeviceSize stagingSize = 0;
for (const TextureUploadTarget target : targets) {
@@ -4142,34 +4443,48 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadItem.source = source;
uploadItem.offset = stagingSize;
uploadItem.uploadByteSize = byteSize;
if (!formatInfo.expandRgbToRgba &&
GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
// The RGB -> RGBA expansion rewrites the level texel for texel into a tight buffer
// of its own, so it runs first and the dirty region below is taken from whichever
// bytes are staged. Uploading an expanded level WHOLE put the shadow's stale texels
// over whatever the GPU had drawn into the rest of it.
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
const SizeT levelBytes = uploadItem.uploadByteSize;
if (GetAspectMaskForFormat(outResource.format) == VK_IMAGE_ASPECT_COLOR_BIT) {
const auto region = mipmapTexture.GetStorageDirtyRegion(target, level);
const SizeT texelCount = static_cast<SizeT>(texelSize.x()) *
static_cast<SizeT>(texelSize.y()) *
static_cast<SizeT>(std::max(texelSize.z(), 1));
if (!region.Empty() && !region.CoversWholeLevel(texelSize) && texelCount > 0 &&
byteSize % texelCount == 0) {
// Exactly the texels the client wrote, never their union box or the storage's
// rect list (which merges touching writes into their bounding box, folds when
// full and is withheld past 3/4 of the box): the image may hold what the GPU
// drew or cleared anywhere else, and the shadow is stale there. Each box is one
// more VkBufferImageCopy in the same vkCmdCopyBufferToImage, which costs next
// to nothing, so there is no cheaper shape worth being wrong for. A union box
// that spans the level with gaps in it is not a whole-level write either.
mipmapTexture.GetStorageDirtyFootprint(target, level, dirtyFootprint);
if (!region.Empty() && (dirtyFootprint.size() >= 2 || !region.CoversWholeLevel(texelSize)) &&
texelCount > 0 && levelBytes % texelCount == 0) {
uploadItem.subRegion = true;
uploadItem.regionLo = region.lo;
uploadItem.regionSize = {region.hi.x() - region.lo.x(), region.hi.y() - region.lo.y(),
region.hi.z() - region.lo.z()};
uploadItem.texelBytes = byteSize / texelCount;
uploadItem.texelBytes = levelBytes / texelCount;
uploadItem.uploadByteSize = static_cast<SizeT>(uploadItem.regionSize.x()) *
static_cast<SizeT>(uploadItem.regionSize.y()) *
static_cast<SizeT>(uploadItem.regionSize.z()) *
uploadItem.texelBytes;
// Scatter refinement: the storage only hands out its rect list
// when the rects' summed area is materially smaller than the
// union box (0 otherwise), so taking it always stages fewer
// bytes than the box - the very amplification this path exists
// to avoid paying twice.
MG_State::GLState::MipmapDirtyRegion
dirtyRects[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
const SizeT dirtyRectCount = mipmapTexture.GetStorageDirtyRects(
target, level, dirtyRects, MG_State::GLState::MipmapStorage::kMaxDirtyRects);
if (dirtyRectCount >= 2) {
uploadItem.rects.assign(dirtyRects, dirtyRects + dirtyRectCount);
// Several boxes: each is staged tightly and copied on its own. One box is
// the union box above.
if (dirtyFootprint.size() >= 2) {
uploadItem.rects.assign(dirtyFootprint.begin(), dirtyFootprint.end());
SizeT rectTexels = 0;
for (const auto& rect : uploadItem.rects) {
rectTexels += rect.TexelCount();
@@ -4195,15 +4510,6 @@ namespace MobileGL::MG_Backend::DirectVulkan {
}
}
}
if (formatInfo.expandRgbToRgba) {
const Bool expanded = ExpandRgbSourceToRgba(source, byteSize, texelSize, formatInfo,
uploadItem.expandedData);
MOBILEGL_ASSERT(expanded,
"UploadDirtyMipLevels: failed to expand RGB textureId=%d target=%s level=%u to RGBA staging data",
mipmapTexture.GetExternalIndex(),
MG_Util::ConvertTextureUploadTargetToString(target).c_str(), level);
uploadItem.uploadByteSize = uploadItem.expandedData.size();
}
uploadItems.push_back(Move(uploadItem));
if (!uploadItems.back().expandedData.empty()) {
uploadItems.back().source = uploadItems.back().expandedData.data();
@@ -4438,6 +4744,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
const Bool depthSelectsArrayLayer = outResource.viewType == VK_IMAGE_VIEW_TYPE_1D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_2D_ARRAY ||
outResource.viewType == VK_IMAGE_VIEW_TYPE_CUBE_ARRAY;
Vector<VkBufferImageCopy> rectCopies;
for (const auto& item : uploadItems) {
if (!item.rects.empty()) {
// Multi-rect item: one VkBufferImageCopy per rect, all submitted in a
@@ -4445,7 +4752,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
// construction, so no two copies write the same texels. Multi-rect
// implies subRegion, which implies a plain color aspect - the combined
// depth-stencil split below can never see one of these.
VkBufferImageCopy rectCopies[MG_State::GLState::MipmapStorage::kMaxDirtyRects];
rectCopies.resize(item.rects.size());
Uint32 rectCopyCount = 0;
VkDeviceSize runningOffset = item.offset;
for (const auto& rect : item.rects) {
@@ -4475,8 +4782,9 @@ namespace MobileGL::MG_Backend::DirectVulkan {
rectCopies[rectCopyCount++] = rectCopy;
runningOffset += static_cast<VkDeviceSize>(rect.TexelCount() * item.texelBytes);
}
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies);
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, rectCopyCount, rectCopies.data());
continue;
}
const Uint32 depthOrLayers = item.texelSize.z() > 0 ? static_cast<Uint32>(item.texelSize.z()) : 1u;
@@ -4516,10 +4824,12 @@ namespace MobileGL::MG_Backend::DirectVulkan {
stencilCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_STENCIL_BIT;
stencilCopy.bufferOffset = stagingBase + item.offset + static_cast<VkDeviceSize>(texelCount) * 4;
const VkBufferImageCopy copies[2] = {depthCopy, stencilCopy};
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 2, copies);
continue;
}
EndActiveRenderPassOn(commandBuffer);
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, outResource.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &copy);
}
@@ -4530,7 +4840,7 @@ namespace MobileGL::MG_Backend::DirectVulkan {
uploadLayout,
finalLayout,
VK_PIPELINE_STAGE_TRANSFER_BIT,
s_sampledReadStages,
s_sampledReadStagesSlot->value,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
aspectMask, 0, outResource.mipLevels);
@@ -9,6 +9,7 @@
#pragma once
#include "../VkIncludes.h"
#include "../MagmaSession.h"
#include <Includes.h>
#include <MG_State/GLState/FramebufferState/FramebufferObject.h>
#include <MG_State/GLState/TextureState/TextureObject.h>
@@ -332,6 +333,11 @@ public:
// into the frame's PRE command buffer - which executes strictly before the
// frame's commands - instead of splitting the active render pass.
Uint64 lastRecordingGeneration = 0;
// The same, for the last recorded command that WRITES the image (a render pass it is an
// attachment of, a clear, a blit/copy into it, a mip generation, a storage-image binding).
// An upload into an image written this recording has to be ordered after that write
// (VulkanRenderer::OrderPendingUploadAfterRecording).
Uint64 lastRecordingWriteGeneration = 0;
// Snapshot of ITextureObject::GetContentVersion() at the last successful sync;
// lets SyncTexture skip the whole re-check/re-upload when content is unchanged.
Uint64 syncedContentVersion = 0;
@@ -351,6 +357,30 @@ public:
// on every mutation, so the first handle sync of a fresh image always re-checks.
#if MOBILEGL_BUILD_DISAGGREGATED
Uint64 syncedWireSerial = 0;
// SHARED IMAGES: level 0 is a server shared image (an imported AHardwareBuffer), not a
// VMA allocation. The resource owns the image and its dedicated imported memory, and
// holds the registry's reference so the buffer outlives every use (deferred releases
// included). `sharedImageAlphaOne`: an X-format image, whose alpha bytes are undefined.
VkDeviceMemory importedMemory = VK_NULL_HANDLE;
Uint64 sharedImageId = 0;
SharedPtr<const void> sharedImageOwner;
Bool sharedImageAlphaOne = false;
// The reading side's ownership of the image (VulkanRenderer::AcquireSharedImage): whether
// this device holds it now - acquired from the foreign family and not yet released back at
// the frame boundary - the writer's generation it last acquired, and the
// m_wireTextureResources key it lives under, by which the frame boundary finds it again.
Bool sharedImageHeld = false;
Uint64 sharedImageGeneration = 0;
Uint64 sharedImageKey = 0;
// A YUV shared image (NV12/P010) is never the texture's image: the texture keeps its own
// RGBA8 level 0, into which the renderer converts the image at its first use in each frame
// (WireYuvImage.inc) - `yuvSource` being that conversion's import of the buffer, made on
// first use, and `yuvConvertedFrame` the frame it last ran in. `yuvOwner` holds the
// registry's image for as long as the texture names it.
Uint64 yuvImageId = 0;
SharedPtr<const void> yuvOwner;
SharedPtr<void> yuvSource;
Uint64 yuvConvertedFrame = 0;
#endif
TextureResource() = default;
@@ -381,49 +411,78 @@ public:
std::swap(this->storageUsageResolved, that.storageUsageResolved);
std::swap(this->syncedTextureParamsVersion, that.syncedTextureParamsVersion);
std::swap(this->lastRecordingGeneration, that.lastRecordingGeneration);
std::swap(this->lastRecordingWriteGeneration, that.lastRecordingWriteGeneration);
std::swap(this->syncedContentVersion, that.syncedContentVersion);
std::swap(this->syncedMipLevelCount, that.syncedMipLevelCount);
std::swap(this->syncedShapeVersion, that.syncedShapeVersion);
#if MOBILEGL_BUILD_DISAGGREGATED
std::swap(this->syncedWireSerial, that.syncedWireSerial);
std::swap(this->importedMemory, that.importedMemory);
std::swap(this->sharedImageId, that.sharedImageId);
std::swap(this->sharedImageOwner, that.sharedImageOwner);
std::swap(this->sharedImageAlphaOne, that.sharedImageAlphaOne);
std::swap(this->sharedImageHeld, that.sharedImageHeld);
std::swap(this->sharedImageGeneration, that.sharedImageGeneration);
std::swap(this->sharedImageKey, that.sharedImageKey);
std::swap(this->yuvImageId, that.yuvImageId);
std::swap(this->yuvOwner, that.yuvOwner);
std::swap(this->yuvSource, that.yuvSource);
std::swap(this->yuvConvertedFrame, that.yuvConvertedFrame);
#endif
}
void Reset() {
if (fullView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, fullView, nullptr);
vkDestroyImageView(s_device.Get(), fullView, nullptr);
}
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
}
for (const auto attachmentView : perMipViews) {
if (attachmentView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, attachmentView, nullptr);
vkDestroyImageView(s_device.Get(), attachmentView, nullptr);
}
}
for (const auto sampledView : perMipSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
}
}
for (const auto& [_, attachmentView] : attachmentViews) {
if (attachmentView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, attachmentView, nullptr);
vkDestroyImageView(s_device.Get(), attachmentView, nullptr);
}
}
for (const auto& [_, sampledView] : alternateSampledViews) {
if (sampledView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, sampledView, nullptr);
vkDestroyImageView(s_device.Get(), sampledView, nullptr);
}
}
for (const auto& [_, storageImageView] : storageImageViews) {
if (storageImageView != VK_NULL_HANDLE) {
vkDestroyImageView(s_device, storageImageView, nullptr);
vkDestroyImageView(s_device.Get(), storageImageView, nullptr);
}
}
if (image != VK_NULL_HANDLE && allocation != nullptr) {
vmaDestroyImage(s_allocator, image, allocation);
vmaDestroyImage(s_allocator.Get(), image, allocation);
}
#if MOBILEGL_BUILD_DISAGGREGATED
if (importedMemory != VK_NULL_HANDLE) {
if (image != VK_NULL_HANDLE) vkDestroyImage(s_device.Get(), image, nullptr);
vkFreeMemory(s_device.Get(), importedMemory, nullptr);
}
importedMemory = VK_NULL_HANDLE;
sharedImageId = 0;
sharedImageOwner.reset();
sharedImageAlphaOne = false;
sharedImageHeld = false;
sharedImageGeneration = 0;
sharedImageKey = 0;
yuvImageId = 0;
yuvOwner.reset();
yuvSource.reset();
yuvConvertedFrame = 0;
#endif
fullView = VK_NULL_HANDLE;
sampledView = VK_NULL_HANDLE;
perMipViews.clear();
@@ -460,8 +519,8 @@ public:
Reset();
}
static inline VkDevice s_device = VK_NULL_HANDLE;
static inline VmaAllocator s_allocator = VK_NULL_HANDLE;
static inline SessionLocal<VkDevice> s_device; // per Magma session (MagmaSession.h)
static inline SessionLocal<VmaAllocator> s_allocator;
};
struct SampledTextureSnapshot {
@@ -585,6 +644,41 @@ public:
// ranges select cube faces; array and 3D levels share one staged level shadow.
void MarkWireTextureGpuWritten(MG_Pipe::MGPipeHandle handle, Uint32 mipLevel,
Uint32 baseArrayLayer = 0, Uint32 layerCount = 1);
// SHARED IMAGES (docs/Disaggregated/notes/anland/plan-ahb-dmabuf.md): a server shared image's
// AHardwareBuffer bound as a single-level 2D colour image on THIS session's device, on dedicated
// memory imported from the buffer. Android only (false with `why` elsewhere, or when the device
// did not take the AHardwareBuffer extension). The image is born UNDEFINED; whoever uses it
// acquires it from the foreign queue family first, and a writer releases it back after.
struct ImportedSharedImage {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkImageUsageFlags usage = 0;
VkFormatFeatureFlags features = 0;
Uint32 queueFamily = 0;
};
static Bool ImportSharedImage(void* nativeBuffer, Uint32 width, Uint32 height, ImportedSharedImage& out,
String& why);
static void DestroyImportedSharedImage(VkDevice device, ImportedSharedImage& image);
// The reading side's per-use hook: PrepareWireTextureResources notes every shared-image
// texture a draw or dispatch samples, and the renderer takes the list right after
// (VulkanRenderer::AcquireNotedSharedImages) to acquire each one ahead of the sampling.
void NoteSharedImageUse(const TextureResource& resource) {
if (std::find(m_sharedImageUses.begin(), m_sharedImageUses.end(), resource.sharedImageKey) ==
m_sharedImageUses.end())
m_sharedImageUses.push_back(resource.sharedImageKey);
}
Bool HasNotedSharedImageUses() const { return !m_sharedImageUses.empty(); }
void TakeNotedSharedImageUses(Vector<Uint64>& out) {
out.clear();
std::swap(out, m_sharedImageUses);
}
// The live handle-keyed texture resource under `key`, or null.
TextureResource* FindWireTextureResource(Uint64 key) {
const auto it = m_wireTextureResources.find(key);
return it != m_wireTextureResources.end() && it->second.image != VK_NULL_HANDLE ? &it->second : nullptr;
}
#endif
VkImageView GetOrCreateViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel);
VkImageView GetOrCreateAttachmentViewAtMipLevel(MG_State::GLState::ITextureObject& texture, Uint32 mipLevel,
@@ -623,6 +717,26 @@ public:
Bool WasTouchedThisRecording(const TextureResource& resource) const {
return resource.lastRecordingGeneration == m_recordingGeneration;
}
// A write is a use too. See TextureResource::lastRecordingWriteGeneration.
void StampResourceRecordingWrite(TextureResource& resource) const {
resource.lastRecordingGeneration = m_recordingGeneration;
resource.lastRecordingWriteGeneration = m_recordingGeneration;
}
void StampTextureRecordingWrite(MG_State::GLState::ITextureObject* texture);
// Upload-ordering queries (VulkanRenderer::OrderPendingUploadAfterRecording). None of them
// syncs, so nothing pending is uploaded by asking. All answer for the STORAGE texture.
//
// Uploads are recorded into a batch submitted AHEAD of the frame's recording
// (FlushPendingUploads), so an upload into an image the open recording already WROTE lands
// before that write, which then overwrites the newer texels. (An image the recording only
// read keeps the old trade: its earlier reads see the newer texels.)
Bool HasLiveImage(MG_State::GLState::ITextureObject& textureOrView) const;
// The next sync would record client texels into the image that is live now: same shape (a
// shape change mints a new image instead) and content moved since the last sync.
Bool HasPendingTexelUpload(MG_State::GLState::ITextureObject& textureOrView) const;
Bool WasTextureWrittenThisRecording(MG_State::GLState::ITextureObject& textureOrView) const;
// The live resource, or null.
TextureResource* FindTextureResource(MG_State::GLState::ITextureObject& textureOrView);
// Records that this texture is bound to a GL image unit, so its image must carry
// VK_IMAGE_USAGE_STORAGE_BIT. Must be called before NeedsStorageImagePreparation, and
// therefore before the render pass is committed: an image that has to be upgraded is
@@ -751,10 +865,18 @@ private:
Bool requireStorage);
Bool UploadPendingWireLevels(MG_Pipe::MGPipeHandle handle, MG_Pipe::MGPipeResourceRecord& record,
TextureResource& resource);
// SyncWireTextureShape's arm for a record whose level 0 IS a shared image (SharedImageId):
// binds the imported buffer instead of allocating. Unresolved = the image is gone or cannot be
// imported here; `resource` is then untouched and the caller allocates as for any texture.
enum class SharedImageBind : Uint8 { Bound, Failed, Unresolved };
SharedImageBind SyncWireSharedImage(const MG_Pipe::MGPipeResourceRecord& record, TextureResource& resource,
Bool requireStorage);
// The handle-keyed twin of m_textureResources, keyed by StagedTextureStore::KeyForHandle.
// Node-based for the same reason m_renderbufferResources is: callers hold TextureResource*
// across map-touching calls.
std::unordered_map<Uint64, TextureResource> m_wireTextureResources;
// Shared-image textures sampled since the renderer last took the list (NoteSharedImageUse).
Vector<Uint64> m_sharedImageUses;
// Handles are per-kind; a renderbuffer and texture may have identical slot/gen.
std::unordered_map<Uint64, TextureResource> m_wireRenderbufferResources;
#endif
@@ -777,6 +899,8 @@ private:
// Active only between BeginDrawSyncScope/EndDrawSyncScope; identities of
// textures already fully synced in the current draw (small N -> flat scan).
Bool m_drawSyncScopeActive = false;
// SyncWireTextureShape defining the RGBA8 storage a YUV shared image is converted into.
Bool m_syncingYuvTextureStorage = false;
// Per-draw sync memo: the identity plus the resolved resource pointer. The pointer is stable
// across rehash in the node-based m_textureResources and stays valid for the draw (a texture
// synced this draw is alive and is not erased mid-draw), so a repeat sync of the same texture

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