GPU acceleration on Android — the venus deep dive
Haven runs guest Linux GL/Vulkan apps on the phone’s real Mali GPU from inside a proot cage, with no /dev/dri and no kernel virtio-gpu device. This is the detailed engineering record, extracted from VISION.md to keep the vision readable: the one-paragraph summary lives there, the full trail lives here.
The arc. Guest Mesa in a proot cage can’t reach Mali (the render node is denied to the app uid), so the only GPU path is Android’s own EGL/Vulkan loader. Haven host-brokers a renderer — virgl_test_server, and for modern GL a venus + Mesa-zink stack — holding a real GPU context in Haven’s Android process, with the guest’s Mesa pointed at it over a socket. What ships today: GL 2.1 over virgl, native-Vulkan windowed present, and (opt-in behind gpu_use_venus) windowed GL 3.2 via zink-on-venus, static and geometry-animating, flicker-free over the wl_shm cage. Below is every wall hit and cleared on the way — R1 through R8 — each device-verified on a Pixel 8 Pro (Mali-G715).
The record
- Brokered GPU acceleration into the proot cage. Guest Mesa in proot can’t reach the GPU —
/dev/driis permission-denied to the app uid and no Mali node is exposed — so nested-Wayland and cage GL fall back to llvmpipe (device-measured: glxgears ~140 FPS software under Sway+Xwayland in the cage on a Pixel 8 Pro). The app’s only GPU path is Android’s own EGL/Vulkan loader, so the unlock is the host-broker pattern (see “The host as a privileged peer”), not device-node passthrough: a Haven-launchedvirgl_test_server(virglrenderer holding a surfaceless EGL/GLES context on the GPU, running in Haven’s Android process wherelibEGLreaches Mali) with the guest’s Mesa pointed at it via thevirpipe/virglGallium driver over a socket in cacheDir (bound to the guest’s/tmp) — the same shape as the USB and wayvnc shims re-exposing an Android capability into the runtime. Licensing is clear: virglrenderer and Mesa are MIT, the Termux build glue is Apache-2.0 — all compatible with AGPL and F-Droid build-from-source. The guest half needs no submodule — the distro’s own Mesa already carries virgl (confirmed:virtio_gpu_dri.soin the Arch rootfs); we only setGALLIUM_DRIVER=virpipe. Only the host renderer is built — a submodule of upstream virglrenderer plus a small Haven Android-EGL patch (surfaceless context instead of a DRM node), cross-built with the NDK like wgbridge and the wayvnc shim. (No canonical Termux repo to submodule — their server is upstream virglrenderer + downstream Android patches, so Haven carries the patch itself.) The probe is the first integration slice, not a freebie: a drop-in of Termux’s prebuilt server can’t work — Android’s app sandbox stops a foreign process from sharing a socket into Haven’s proot, and the server must run outside proot to get Android EGL — so the minimal test is the NDK server launched by Haven, re-running glxgears to see whetherGL_RENDERERflips off llvmpipe. Device-specific: Mali rules out the Adrenozink+Turnip route, so virgl-over-Android-EGL is the path; full-pipeline accel additionally needs wlroots on a GPU renderer, so app-level GL accel is the first win. - GL acceleration — shipped virgl path, the GL-2.1 ceiling, and the venus deep dive (verified on a Pixel 8 Pro). The cage GL broker shipped (v5.59.63): app GL in a Sway cage runs on Mali (
GL_RENDERER = virgl (Mali-G715)) instead of llvmpipe. But virgl caps desktop GL at 2.1 — a hard ceiling, because Android’s EGL exposes only GLES and virgl’s GLES→desktop-GL compatibility profile is fixed at 2.1, so glxgears, older games, and most CAD viewers can’t exceed it. An ES2→ES3 host-context bump (vrend_winsys_egl.c, rebuilt + device-tested) was a verified dead-end — it didn’t raise the desktop-GL version (a higher GLES context only lifts GLES-native caps, which the software-Xwayland cage couldn’t even measure:glxinfo→X_GetImage BadMatch,eglinfohangs). The forward path is venus (pass the device’s real Mali Vulkan into the guest) + Mesa zink (GL-on-Vulkan → modern GL). Deep-dived over 2026-06-15; current state:- Built & wired with no JNI change. A venus-enabled
virgl_test_serverplus the separatevirgl_render_serverit requires (NDK cross-build,-Dvenus=true -Dvulkan-dload=true -Drender-server-worker=thread; venus forcesVIRGL_RENDERER_RENDER_SERVERwith no in-process path,virglrenderer.c:931). Two submodule patches make it run inside the app unchanged: defaultserver.venus=trueinvtest_server.c, and self-locate the render server via/proc/self/exedirname inproxy_server.c.apt install mesa-vulkan-driversships the guest venus ICD (libvulkan_virtio.so+virtio_icd.json); guest envGALLIUM_DRIVER=zink MESA_LOADER_DRIVER_OVERRIDE=zink VN_DEBUG=vtest VK_DRIVER_FILES=…/virtio_icd.json VTEST_SOCKET=/tmp/.virgl_test. - Transport works (device-verified).
vulkaninfoin the cage reports a realVirtio-GPU Venus (Mali-G715),DRIVER_ID_MESA_VENUS, apiVersion 1.4 — i.e. guest Vulkan → vtest socket → host venus → Androidlibvulkan→ Mali, with no/dev/dri/ virtio-gpu kernel device. - Allocation wall — found and cleared. Venus first aborted (
vkr: minigbm_allocation is not enabled→vkBindBufferMemory2 CS error→ a FORTIFY abort): virglrenderer’s venus (src/venus/vkr_device_memory.c) allocates exportable device memory only via minigbm, which needs a/dev/drirender node the sandbox lacks. Fixed with an AHardwareBuffer allocator (#elif defined(ENABLE_AHB_ALLOCATION): AHBBLOB→getNativeHandle→ dup’d dma-buf fd →VkImportMemoryFdInfoKHR{DMA_BUF_BIT_EXT}; Mali advertisesVK_EXT_external_memory_dma_buf, confirmed by an NDK probe). Memory now allocates + binds; the host processes the full venus command stream cleanly. - Rendering blocker — three stacked issues, all now root-caused, two fixed (deep dives, 2026-06-15). What looked like one “fence gap” was three distinct layers; peeling them in order got a real GL app from nothing to sustained on-Mali rendering, and pinned the last (present) to the Android-EGL + Mesa-WSI layer — not the compositor:
- Layer 1 — the fence hang: solved.
MESA-VIRTIO: stuck in fence waitwas root-caused to venus’s fence-feedback optimisation (venus signals a fence by having the GPU write a host-visible buffer the guest polls, instead of a real renderer fence wait) — unreliable over vtest.VN_PERF=no_fence_feedbackfixes it deterministically: a minimalvkQueueSubmit+vkWaitForFencesprobe (venusfence.c) hangs by default but passes 3/3 with the flag, verified on a clean no-hack build. This is a Mesa-venus behaviour; the earlier ring_idx-0 / CPU-roundtrip observation is just the mechanism feedback rides on (venus-over-vtest never creates a context fence at all). The crude “host-side blockingWaitForFences” idea from dive #2 was tried and reverted — it only perturbs feedback timing, non-deterministically. - Layer 2 — render-server crash: fixed (a genuine upstream virglrenderer bug). With feedback off, real GL apps got into rendering and then crashed the venus render server. Root cause:
vkr_device_destroy()(src/venus/vkr_device.c) destroysdev->object_mutexbefore the object-cleanup loop that locks it (vkr_device_remove_object), so a device torn down “with valid objects” (an app exiting/crashing without freeing every Vulkan object) locks a destroyed mutex — UB-but-silent on glibc, fatal under Android bionic FORTIFY (pthread_mutex_lock called on a destroyed mutex; confirmed by a device tombstone). Fix = destroy the mutex after the cleanup loop (one hunk). Present in upstream mainb7255563, introduced by7d81b255; device-verified (render-log FORTIFY count 0); patch + issue prepared and ready to submit upstream (scratch/0001-venus-destroy-object_mutex-after-cleanup.patch,scratch/virgl-fortify-issue.md). - What’s reachable today. With both fixes, real GL apps (glmark2 / es2gears via zink-on-venus) now render sustained frames on Mali — the render server stays alive and ~2 KB draw command buffers flow continuously — and the modern-GL context reaches
OpenGL core profile 3.2(zink Vulkan 1.4 (Venus Mali-G715)), past the virgl 2.1 ceiling. (zink is capped at 3.2 not 4.x by two missing Mali featuresfillModeNonSolid/shaderClipDistance.) - Layer 3 — root cause corrected (2026-06-15, #54 dive): GPU present is blocked by two stacked facts, NEITHER in the compositor — so a “dma-buf-import compositor” is impossible here. Apps still can’t present, but it is not a hang, not a venus bug, and not specific to the pixman cage. #53 had only tested the cage; #54 tested the in-process GLES2 labwc native compositor too and found the same wall. (1) Compositor side: Android’s libEGL exposes
EGL_ANDROID_image_native_bufferbut notEGL_EXT_image_dma_buf_import(the generic dma-buf import wlroots needs), so no wlroots compositor on Android can advertisezwp_linux_dmabuf_v1— the cage’s pixman Sway and the GLES2 labwc compositor are bothwl_shm-only (device-checked: labwc logsEGL_EXT_image_dma_buf_import: NO→unable to initialize dmabuf→ shm allocator). (2) Client side: Mesa’swsi_common_wayland.cintentionally ignoreswl_shmfor hardware-accelerated WSI devices, so a venus/zink swapchain returnsVK_ERROR_SURFACE_LOST_KHRat creation (vkcube-waylandasserts atdemo_init_vk_swapchain) while the softwarellvmpipedevice presents fine overwl_shmon the same compositor. The two ends are mutually incompatible at WSI — the GPU driver won’t emit awl_shmbuffer and the compositor can’t import a dma-buf. Mesa is current (ubuntu-noble 25.2.8), so it is not an old-driver miss. The unlock is therefore guest-Mesa, not a compositor: (a) patch Mesa WSI to allow awl_shmCPU-copy present for venus (rebuild only the venus ICD), or (b) a custom AHB-native-buffer WSI + Wayland protocol carrying AHardwareBuffer handles (which Android EGL can import viaEGL_ANDROID_image_native_buffer) for zero-copy. A DRM-render-node broker is likely dead on Mali. Shipped from this work:VN_PERF=no_fence_feedbackis now wired into both GPU cage launch paths (commit12c72648, device-verified). Status: the AHB allocator + the FORTIFY fix + fence/crash diagnostics remain uncommitted working-tree changes (recoverable from thewayland-androidsubmodule +/tmp/virgl-ahb+venusfence.c); releases build the clean shipped virgl.soand are unaffected. - Layer 3 — corrected again & resolved (2026-06-16, #54 close). Two updates to the entry above. (a) The
wl_shmCPU-copy present needs no Mesa patch — Mesa’s built-inMESA_VK_WSI_DEBUG=swforces the venus WSI onto thewl_shmpath, and native Vulkan now presents fully (vkcube renders a textured cube over both the cage and the labwc-native compositor). Shipped opt-in behindgpu_use_venus(commit15f0121c), so the line-282 “(2) client-sideVK_ERROR_SURFACE_LOST_KHR” wall is gone for Vulkan. (b) The residual gap is windowed GL via zink, and it is NOT the WSI. Traced down six Mesa layers with 10+ instrumented in-guest rebuilds: zink renders correctly (a surfaceless FBOglReadPixelsreturns the exact drawn geometry), but over awl_shm-only compositor EGL cannot route zink+venus onto the kopper (Vulkan-WSI-sw) present that vkcube uses — the GPU kopper screen builds (kopper_init_screenreturns OK,is_sw=0) but EGL’s config/display setup for it fails, soeglInitializefalls back to zink-on-swrast, whose drisw present (zink_flush_frontbufferno-op,is_swapchain=0) never copies the rendered pixels into the committedwl_shmbuffer → stale/rotating frames (visible flicker). Confirmed: the venus WSIwsi_common_queue_presentnever fires; the present is libgallium’s drisw path. Fix space = upstream Mesa (let zink+venus take the kopper present over awl_shm-only wayland compositor) or a dma-buf/GPU-capable compositor so the kopper screen’s EGL setup succeeds; the env leverLIBGL_ALWAYS_SOFTWAREis a dead end (it makes zink demand a CPU Vulkan device, excluding venus), and any guest-Mesa patch is per-distro and non-shippable. Net today: offscreen/compute zink-GL works; native-Vulkan windowed present works (Phase A); windowed GL is deferred (Phase C / upstream). Full 6-layer chain in theproject_virgl_cage_gpu_accelmemory note (★B6) and thegpuPassthroughEnvcode comment. - Layer 3 — Phase C probe settles the fix space (2026-06-16, #54 follow-up). The “dma-buf/GPU-capable compositor” half of the fix is closed on this platform. A device probe (
scratch/c0probe.c, NDK28 arm64 vs Mali-G715) confirms Android EGL has noEGL_EXT_image_dma_buf_import: a fullAHardwareBufferhandle imports fine in-process (EGL_ANDROID_image_native_buffer), but a bare dma-buf fd — all thatzwp_linux_dmabuf_v1carries cross-process — cannot be imported, and the gralloc handle (numFds=2, numInts=77) can’t be reconstructed from the protocol’s fields. The native labwc compositor already whitelistszwp_linux_dmabuf_v1+ has the AHB allocator/format/presenter scaffolding, but it’s gated off for exactly this reason. So the zero-copy route is dead (it would need a custom AHB-handle protocol + guest-Mesa WSI). This commits us to the CPU-copy/sw-WSI present (the path vkcube already presents over), whose only remaining blocker is Mesa EGL routing — zink+venus’s window present mis-routes to no-op drisw instead of the kopper sw-WSI path. That IS shippable, via Haven’s standard show-stopper pattern: fork guest Mesa as a first-party submodule, fix-and-ship to unblock now, upstream the MR in parallel (Phase D; the fix is not yet found — the open work is the kopper EGL config/display setup failure). The fix is being proven on-device first, then carried on aGlassHaven/mesahaven-patchesbranch and cross-built/LD_PRELOADed for ubuntu-noble/arm64 behindgpu_use_venus. Ships today regardless: GL 2.1 (virgl) + native-Vulkan windowed (vkcube). - Layer 3 — windowed GL geometry now PRESENTS; residual is a GPU-write flicker (2026-06-16, Phase D). The EGL-routing blocker is found and beaten in guest Mesa (“Fix B”,
platform_wayland.cdri2_initialize_wayland_swrast): when the env requests zink, pickdriver_name="zink"even aftereglInitialize’s ForceSoftware fallback clearedOptions.Zink, so the swrast init builds a kopper screen (it has thewl_shmconfigs the drm path lacks) and — becausekopper=1forcesswrast=0— zink still selects the venus GPU. Device-verified: the venus WSIwsi_common_queue_presentruns and grim captures the glmark2 build-scene geometry (zink+venus GL 3.2 presenting over thewl_shmcage) — the B6 “clear-colour only” wall is broken. One clean upstreamable hunk; LD_PRELOADed in test. Residual = flicker: ~⅔ of presented frames read black, bursty. Localized over a long renderer dive (full findings in theproject_virgl_cage_gpu_accelmemory note ★R2): (1) venus actually runs in the separatelibvirgl_render_server.soproxy process, not thevirgl_test_serverthat owns the socket — so renderer-side edits must rebuild+deploy both binaries (this silently no-op’d earlier fixes), and the render server’s only working debug channel is__android_log(adb logcat -s HVN_AHB); (2) the present-fence wait is avkGetFenceStatuspoll, notWaitForFences/sync-thread; (3) a host CPU write into the swapchain AHB at the fence-signal edge presents to the guest cleanly and stably (white-band test) ⇒ host↔guest CPU coherency works; the flicker is GPU-write-to-dma-buf visibility / swapchain ordering, not CPU coherency. A host-sideDeviceWaitIdle+AHardwareBuffer_lock(CPU_READ)fix at the fence edge gave one clean run (0/240 black) that did not reproduce (steady ~⅔ black after) — disproven as a reliable fix. The center-pixel flicker metric over-counts black (the rotating model leaves the centre pixel); a full-frame metric and a likely guest-WSI↔venus swapchain ordering bug are the open leads. Net: windowed zink+venus GL 3.2 geometry reaches the surface on-device (a real step past “deferred”), but a flicker-free present is still unsolved. Ships today regardless: GL 2.1 (virgl) + native-Vulkan windowed (vkcube). - Layer 3 — R3 (2026-06-18): the flicker is zink-on-venus-specific; the renderer/transport is exonerated and the entire guest-side env/sync space is exhausted. Built a proper full-frame black metric (replacing R2’s over-counting centre-pixel probe) with per-swapchain-image correlation. Five hypotheses fell to clean device experiments: swapchain ordering (uniform ~⅓ geom across all four images — not “2-of-3 buffers”), fence timing (a 0–50 ms spin before the memcpy doesn’t help), renderer/AHB/WSI/transport (the keystone: native-Vulkan
vkcubepresents 100% clean — 240/240 frames — through the identical venus→AHB→wl_shm→memcpy path, overturning R2’s “fix must be renderer-side”), present-boundary sync (zinkimplicit_syncis already active and insufficient), and threaded-present/reorder (ZINK_DEBUG=sync,flushsync,noreorder+mesa_glthread=false+fifoall unchanged). Vulkan synchronization-validation reports zero hazards → zink’s commands are spec-correct; the bug is venus/Mali executing correct commands. The metric is high-variance (52–72 % black run-to-run) — a timing race where the Mali render-pass store to the shared AHB image isn’t memory-domain-available to the guest CPU reader (no kernel dma-buf fence over vtest). FLAT frames are uniform clear-colour: theloadOp=CLEARlands every frame, the drawn geometry only ~⅓. No env lever touches it (VN_PERF=no_tiled_wsi_imagemade it worse); only the validation layer’s per-command serialisation nudges it (66 %→43 %). The remaining path is a renderer-side venus dive (why vkcube’s W-write is coherent and zink’s render-pass-store isn’t). Full chain in theproject_virgl_cage_gpu_accelmemory note ★R3. - Layer 3 — R4 (2026-06-18): the renderer dive is done and the render server is conclusively exonerated; the blit copies a stale swapchain image, so the bug is guest-side zink/kopper. Built the per-present host-side probe R3 called for, into the venus render server (
virglrenderervkr): itmmaps the WSI present-blit’s destination dma-buf right at the present fence and scans it for geometry, raw and after aDMA_BUFcache-invalidate. (Key arch finding: the WSI blit command buffers are pre-recorded once and resubmitted every frame, so the scan must be armed at submit-time, not record-time.) Three independent device-verified proofs all point the same way: host GEOM% == guest GEOM% to the percentage point (glmark2/zink 27 %≈27 %;vkcube100 %≈100 %, which validates the probe) ⇒ no transport loss; raw == synced on every sample ⇒ not a CPU-cache-coherency issue (the invalidate reveals nothing); and forcing a hostvkDeviceWaitIdleafter every submit gives ~30 % (barely above baseline) ⇒ not host ordering/timing — even with the host GPU provably drained before the blit, the swapchain image lacks the geometry. So the WSI blit faithfully copies a stale swapchain image: zink’s geometry render-pass-store isn’t landing in that image’s memory ~⅔ of the time on Mali-venus (theloadOpclear lands, the binned draws don’t). This overturns R2’s “fix must be renderer-side” with direct evidence and confirms R3’s re-localization: the fix venue is guest-side zink/kopper (Phase D / Haven-forkable guest Mesa), not virglrenderer. Next step is guest-side — instrument zink/kopper’s present (the zink-framebuffer→kopper-swapchain-image copy and its sync), not another renderer build. Throwaway instrumentation; committed binaries + releases untouched. Ships today regardless: GL 2.1 (virgl) + native-Vulkan windowed (vkcube). - Layer 3 — R5 (2026-06-18): root cause proven and a working fix demonstrated — the flicker is venus host-visible-buffer incoherence on the per-frame uniform. Following R4’s “go guest-side”, a frontend probe (
pipe_texture_mapof the just-rendered swapchain texture right afterdri_flush) showed render-time content == blit-time content frame-for-frame (0/240 mismatch) ⇒ the entire present/blit/WSI/transport path is also exonerated; the geometry is missing from the swapchain image the instant zink finishes rendering it. Further probes proved the loss is not render-pass structure (GEOM and FLAT frames are an identical singleloadOp=CLEAR/storeOp=STOREpass) and not a dropped draw (exactly one draw is recorded and submitted every frame) — on FLAT frames the recorded draw simply rasterises nothing, i.e. the model is transformed off-screen. The only per-frame-varying input is the transform uniform, which the state tracker pre-uploads into a host-visible buffer the vertex shader reads. Fix-test (decisive): re-issue that uniform through the coherent command stream — CPU-read the host-visible bytes (the guest sees its own write correctly),vkCmdUpdateBufferthem back, + a TRANSFER→UNIFORM barrier — and the flicker vanishes: 0 % FLAT reproduced across build (240/240, 222/222, 214/214) and shading (174/174), vs 31–38 % baseline. ⇒ proven root cause: the guest-CPU write to venus host-visible memory is intermittently not visible to Mali over vtest (the same coherency gap as the fence/semaphore-feedback bugs), so ~⅔ of frames the shader reads a stale/garbage MVP and draws off-screen. Caveats: the patch is a guest-Mesa(zink) proof-of-concept (heavy: re-uploads every small const buffer per set) → ships via the Phase-DGlassHaven/mesafork, per-distro; the complete fix is venus-side (make host-visible memory genuinely coherent over vtest), which would also cover other per-frame host-visible buffers (streamed vertices/SSBOs) this uniform-only workaround doesn’t. Full chain + the exact patch in theproject_virgl_cage_gpu_accelmemory note ★★★R5. Ships today regardless: GL 2.1 (virgl) + native-Vulkan windowed (vkcube). - Layer 3 — R6 (2026-06-18): the windowed-GL fix is PRODUCTIONIZED + device-verified on a release build, and the remaining gap is now exactly located. R5’s per-frame-uniform fix plus “Fix B” (the
platform_wayland.ckopper-routing hunk) ship together as a two-hunk Mesa patch built and cached in the guest on demand (apt-get source→ patch → meson → cachelibgallium+libEGL+ apreloadlist), LD_PRELOADed behindgpu_use_venus— no per-distro prebuilt and no fork checkout, just an APK-shipped patch + build script (commit66e98e45; the coherency hunk is staged as a clean upstream MR, Fix B carried in-tree). Verified end-to-end on the installed v5.59.66 APK through the real cage path (present_app→gpuPassthroughEnvLD_PRELOADs the cache): static-mesh GL — glmark2 build/shading/texture — presents 0 % flat (48/48 frames content) vs ~42 % without the fix, GL 3.2 on zink+venus; the README hero GIF is this, captured live from the cage. The remaining frontier is now exact: scenes that animate geometry (glmark2jellyfish) still flicker, and it is not a const-buffer-size limit (bumping the re-issue cap 4 KB → 64 KB changed nothing — 2/24 content either way). The split is purely the ICD: jellyfish is clean on the instrumented/tmp/vbvenus ICD (which carries R-dive coherency tweaks) and flat on the stock system ICD, so the animated geometry rides the same venus host-visible coherency gap one layer below zink’s const path, where no zink-const patch (any cap) can reach it. The complete fix is therefore venus-side (ICD) coherency — make host-visible memory genuinely GPU-visible over vtest — which/tmp/vbproves is achievable and which would cover jellyfish and every geometry-animating app at once (build.shwould then cache a patched venus ICD alongside libgallium/libEGL). That is the next round. Full chain in theproject_virgl_cage_gpu_accelmemory note ★★R6. Ships today: static-mesh windowed GL (zink+venus, GL 3.2) + native-Vulkan windowed (vkcube) + GL 2.1 (virgl). - Layer 3 — R7 (2026-06-18): the venus-side ICD coherency fix lands — geometry-animating windowed GL now presents cleanly, and zink+venus GL is no longer “static-mesh only”. R6’s closing claim was wrong: jellyfish was not actually clean on the instrumented
/tmp/vbICD — that was the ICD’s own in-probe metric lying. Measured externally withgrim, jellyfish flickered there too (2/24 content), the same as the stock ICD (5/24); the/tmp/vbWSI-invalidate was a red herring, so the real fix had to be built. Root cause (source-confirmed): venus’ vtest renderer maps app memory asmmap(MAP_SHARED)of a host3d dma-buf and advertises itHOST_COHERENT, butvtest_bo_flush/vtest_bo_invalidateare literal no-ops (the code comment admits “we know vtest can lie”) — on ARM the guest CPU’s cached writes to per-frame streamed data (the transform UBO, streamed vertices) aren’t written back to the dma-buf before the Mali GPU reads them. Fix (venus-vtest-coherency.patch,vn_renderer_vtest.c): track mapped host-visible bos and, before each vtest submit,dc cvac(clean-to-Point-of-Coherency) their ranges so the GPU sees the latest guest writes — default-on,HVN_VENUS_NO_FLUSHkill-switch. Proven on-device (external grim, same binary, env toggles the fix): jellyfish 5/24 → 24/24 content; build/shading/texture unaffected (20/20 each); native-Vulkanvkcubestill 240/240; FPS healthy (jellyfish ~40–69). Productionized + verified end-to-end:build.shnow builds the venus ICD too (-Dvulkan-drivers=virtio, one source tree) and cacheslibvulkan_virtio.so+ avirtio_icd.jsonmanifest;gpuPassthroughEnvpointsVK_DRIVER_FILESat the cached patched ICD when present. A from-scratch in-guestbuild.sh(pristineapt-get source+ both committed patches + combined build,BUILDSH_EXIT=0) produced the faithful cache, and the real cage path (present_app→gpuPassthroughEnv) presented jellyfish 24/24 content frames with the cached ICD selected — the README hero GIF is now this live jellyfish. Net: windowed zink+venus GL 3.2 — static and geometry-animating — presents flicker-free over the wl_shm cage, no/dev/dri. Being venus-side rather than zink-const-only, the coherency fix also covers streamed vertices/SSBOs, so it is general, not scene-specific. Full chain in theproject_virgl_cage_gpu_accelmemory note ★R7. Ships behindgpu_use_venus(opt-in); GL 2.1 (virgl) + native-Vulkan windowed remain the defaults. - Layer 3 — R8 (2026-06-22): the windowed/embedded-GL “Phase C gap” was a MISDIAGNOSIS — embedded GL already presents, and Haven runs interactive Qt CAD 3D on the GPU today; the real CAD-3D blocker is toolkit-side (wxWidgets GLX), not Haven. Two strands. (1) Embedded GL was never blocked.
scratch/subprobe.c(a minimalwl_subsurface+wl_egl_windowGL canvas inside a mapped parent) renders correctly over zink+venus+sw-WSI — overturning the earlier “embedded wxGLCanvas blank ⇒ needs a dmabuf/GPU-capable compositor (Phase C)” conclusion. The real CAD-3D split is the GL-context backend, device-verified: Qt apps work, wxWidgets apps don’t. OpenSCAD’s 3D preview renders and rotates interactively on the GPU (Qt → Wayland-EGL → zink → venus → Mali, withQT_QPA_PLATFORM=wayland+ qtwayland5). KiCad’s GAL canvas and PrusaSlicer’s wxGLCanvas both fall back to software (“Could not create the main OpenGL context”) because Ubuntu noble’s wxWidgets is built GLX-only (libwx_gtk3u_glexports onlywxGLCanvasX11, no EGL), so their GL routes through Xwayland → no DRI3 (Mali exposes no dmabuf render node — confirmed byscratch/c0probe.c:EGL_ANDROID_image_native_bufferpresent,EGL_EXT_image_dma_buf_importabsent). That blocker is app/toolkit-side — fixable only with a wx-EGL rebuild (per-distro) or by preferring Qt/native-Wayland CAD; not a Haven GPU/compositor/WSI limitation. (FreeCAD has no noble/arm64 package, but uses the same Qt-OpenGL path, so should work.) (2) Zero-copy AHB groundwork — proven, committed, gated off, but deferred. A keystone probe (scratch/c1probe.c) shows the in-process 2D-AHB GPU loopback works pixel-exact on Mali-G715 (producer renders into a 2D R8G8B8A8 AHB via FBO; an independent GL context imports the same AHB and samples it back, no tiling/swizzle loss). venus is patched host-side to back dedicated 2D colour swapchain images with sampleable AHBs (detect+enableVK_ANDROID_external_memory_android_hardware_buffer+VK_EXT_queue_family_foreign; create the image AHB-external + mutable-format — R8G8B8A8 primary with a {RGBA,BGRA} view list, since zink is BGRA-only and AHB has no BGRA; import viaVkImportAndroidHardwareBufferInfoANDROID, as the plain dma_buf-fd import binds only linear BLOBs not tiled 2D images). Device-verified: a BGRA swapchain image binds to an R8G8B8A8 AHB (vkBindImageMemory2 ret=0). All host-side/shippable, committed behind an off-by-default sentinel/env gate (virglrendererfe1a9fda→ Havend826c0cb). Deferred because the venus render server is a separate process from the labwc compositor — the “in-process AHB registry” can’t bridge them, so a real zero-copy present would need cross-process AHB transfer + a guest-WSI shim — and, per strand (1), it is not needed to enable embedded GL (which already works). It is zero-copy/perf groundwork (e.g. a future tear-free present; a residual OpenSCAD-drag shimmer is an app-side Qt interactive-render quirk — the GPU/compositor present and the VNC stream both verified clean under sustained heavy load via glmark2 + subprobe + grim). Probes committed inscratch/(7e44b4d7); full chain in theproject_3d_desktop_gl_prusaslicer_testmemory note ★R8. Net: interactive Qt CAD 3D on the GPU ships today (opt-ingpu_use_venus); wxWidgets CAD 3D needs a guest wx-EGL build.
- Built & wired with no JNI change. A venus-enabled
- Guest-Mesa WSI for GPU present (partly superseded 2026-06-16 — see above: native-Vulkan present already works via
MESA_VK_WSI_DEBUG=sw, no patch; this remains the route only for zero-copy and for windowed GL, where the real blocker is EGL routing, not the WSI) — patch Mesa’swsi_common_wayland.cto allow awl_shmCPU-copy present for the venus device (rebuild the venus ICD), or a custom AHB-native-buffer WSI+protocol for zero-copy viaEGL_ANDROID_image_native_buffer. AWLR_RENDERER=gles2compositor does not unlock present here: the in-process labwc native compositor already runs GLES2 on Mali and still can’t import client dma-bufs (Android EGL lacksEGL_EXT_image_dma_buf_import). The virgl broker is GLES-only and likewise can’t help the GLES-only nested compositors (Hyprland/aquamarine, niri/smithay still failCBackend::create()— they need a DRM render node, not a GL client driver).