12 KiB
Plan: X11 apps on the GPU, zero-copy (Xwayland glamor + GLX DRI3/Present) (2026-10-03)
Follows
plan-ahb-dmabuf.md(the Wayland half: server-allocated AHB "shared images" exported as dma-bufs). Same hard rules: vendor-neutral (AHB, dma-buf, sync_file, the libgbm backend ABI, linux-dmabuf, DRI3/Present), no layout assumptions (modifier INVALID).
Today
GLX app --glXSwapBuffers--> ReadPixels (server->client CPU) --xcb_put_image--> Xwayland (-shm, llvmpipe)
Xwayland --wl_shm--> KWin --glTexSubImage2D upload--> server
Two full-frame CPU trips per frame for every X11 GL app, and every ordinary X11 app is drawn by pixman/llvmpipe and uploaded again by KWin.
Target
GLX app --render--> pbuffer --GPU copy (PresentToSharedImage)--> [AHB image]
--DRI3 PixmapFromBuffer(s)(dma-buf, once per buffer)--> Xwayland pixmap (glamor: gbm_bo_import -> EGLImage)
--Present PresentPixmap--> Xwayland flips it (attaches the pixmap's dma-buf wl_buffer) or GPU-copies it
Xwayland (glamor on MobileGL EGL/GBM) --linux-dmabuf wl_buffer (window pixmaps are shared images)--> KWin
Every hop is a session of the same MobileGL server sampling or writing the same AHardwareBuffer. CPU copies: zero.
Stage 1 - Xwayland on MobileGL (glamor)
What Xwayland 24.1.13 (hw/xwayland/xwayland-glamor-gbm.c, glamor/glamor.c) needs, and where it is:
| Need | Status |
|---|---|
render node from linux-dmabuf v4 feedback main_device (drmGetDeviceFromDevId) |
KWin's thin node: must be a real DRM render node (MOBILEGL_GBM_NODE=/dev/dri/renderD128). /dev/null makes glamor fail -> shm fallback |
gbm_create_device(fd) with GBM_BACKEND=mobilegl |
mobilegl_gbm.so (exists) |
EGL client ext EGL_KHR/MESA_platform_gbm, eglGetPlatformDisplay(GBM, gbm) |
exists; the foreign-device decline keys on the backend name mobilegl |
eglCreateContext(EGL_NO_CONFIG_KHR, core 3.1) + surfaceless eglMakeCurrent |
cherry-picked from the surfaceless branch (6125f941), to be dropped on rebase |
GL >= 2.1 desktop, GL_ARB_vertex_array_object, GL_OES_EGL_image; renderer not llvmpipe |
exists on a split client |
EGL_EXT_image_dma_buf_import(_modifiers) -> dmabuf_capable |
exists (shared images) |
gbm_bo_create_with_modifiers2(XRGB8888/ARGB8888, {INVALID, LINEAR}), gbm_bo_get_fd_for_plane, _handle_for_plane, _stride_for_plane, _offset, _modifier, gbm_bo_import(FD / FD_MODIFIER) |
exists; now covered by a host test through the real libgbm loader |
eglCreateImageKHR(EGL_LINUX_DMA_BUF_EXT) of its own bos, glEGLImageTargetTexture2DOES |
exists (server identifies its image by inode) |
| KWin's tranche lists XRGB8888/ARGB8888 with modifier INVALID | KWin adds INVALID+LINEAR when the EGL reports no modifiers (ours) |
implicit sync: glamor renders into window pixmaps and commits after glFlush, assuming the kernel orders KWin's read after it |
new: see below |
| Xwayland must never dial its own display | new: see below |
Implicit sync by flush (new shared_image op Flush)
The server already orders an image between sessions: a writer publishes a sync_file per write
(PublishWrite), a reader waits on it at its first use and publishes its frame's read fence at
its frame boundary (swap). glamor has neither a swap nor an explicit write: it renders into the
image through an FBO and calls glFlush before committing.
- Client:
glFlush/glFinishin a process that has bound a shared image to a texture (glEGLImageTargetTexture2DOES) and does not own the server window (not KWin) send the new in-streamshared_image Flushop and wait for its reply. Policy is a pure function (SharedImageFlushPolicy), overridable withMOBILEGL_SHARED_IMAGE_FLUSH_SYNC=0/1. - Server:
BackendObject::PublishSharedImageAccesses()- every image the session used since its last boundary gets the flush's fence as a write (generation moves) and as a read. Espryt:ExportFence(EGL_ANDROID_native_fence_sync), Magma: release held images, submit with the exportable semaphore, export it. A session that has flushed this way also waits (GPU) for the pending read fences of an image before its first use in a frame (write-after-read). - The reply comes after the fence is published, so KWin (which samples after Xwayland's commit, which follows the reply) always finds the new generation.
X-connection safety (MG_Util/X11/DisplayGuard)
One rule, one function: MayDialX11Display(name) is false when this process serves that
display: it holds a listening unix socket named /tmp/.X11-unix/X<n> (path or abstract) per
/proc/net/unix + /proc/self/fd, or its executable is an X server (Xwayland, Xorg, Xvfb, ...).
MOBILEGL_X11_DIAL=0 turns all dialing off. Every place MobileGL opens an X connection asks it
first: EGLState's default-visual query (eglGetDisplay of a non-X native display has platform NONE,
so the old "only on X displays" test still dialed for eglGetDisplay(gbm_device)), DirectGLES's
visual query and window-surface display, Magma's Xlib surface. GLX never opens a connection (it
uses the application's). KWin's launcher also drops DISPLAY from Xwayland's environment.
KWin (anland kwin.patch, mobilegl-startup.sh)
ANLAND_MOBILEGL=1 and ANLAND_XWAYLAND_GLAMOR != 0: Xwayland starts without -shm, with
MOBILEGL_IPC_SURFACE=offscreen, its own MOBILEGL_LOG_FILE_PATH=/tmp/mgl-xwayland, GBM_BACKEND
kept, no LIBGL_ALWAYS_SOFTWARE/GALLIUM_DRIVER, no DISPLAY. Otherwise the old -shm +
software environment. If glamor cannot initialize, Xwayland itself falls back to shm rendering.
Stage 2 - GLX presentation through DRI3 + Present
Per GLX window drawable (GLXImpl/X11Present.{h,cpp}, logic behind two small interfaces so the
host tests drive it with fakes):
- Path selection (
SelectPresentPath): DRI3 >= 1.0 and Present >= 1.0 on the connection, shared images available, depth 24/32,MOBILEGL_GLX_PRESENTnotreadback->Dri3Present; else MIT-SHM >= 1.2 with fd passing ->ShmPutImage; elsePutImage(today's bands). - Buffers: up to 3 (4 when Xwayland holds all three past a 1 s wait) shared images of the
window's format (depth 24 -> XRGB8888, 32 -> ARGB8888: exactly what glamor imports them as), each
imported once as a pixmap (
PixmapFromBufferswith modifier INVALID on DRI3 >= 1.2, elsePixmapFromBuffer, checked). A buffer is busy from its PresentPixmap until itsIdleNotify. - Present:
PresentToSharedImage(GPU copy, rows top first, returns once submitted with the write fence published), thenPresentPixmapwith an XFixes update region from the swap's damage (flipped to X's top-left origin; none = whole), serial per frame. Events arrive on a special-event queue (xcb_register_for_special_xge), never in the application's queue. - Pacing:
FrameThrottle(the Wayland rules) onCompleteNotify: interval >= 1 waits for the previous frame's completion,target_msc = last_msc + interval; interval 0 usesPresentOptionAsync, unpaced while completions come, ~1 fps once one is 200 ms overdue. - Errors: a refused
PixmapFromBuffer, aPresentPixmaperror (checked lazily, no round trip), or the window's destruction (ConfigureNotifywithPresentWindowDestroyed) latch the drawable onto the readback path for good, logged once. - Resize:
ConfigureNotify(and the existing throttledXGetGeometry) resize the pbuffer; buffers of the old size are dropped as they come back idle. - Fallback (no DRI3/Present: Xwayland
-shm, a remote X server): readback into a MIT-SHM segment (memfd,xcb_shm_attach_fd) +xcb_shm_put_image; without MIT-SHM, today's bands. GLX_EXT_buffer_age(GLX_BACK_BUFFER_AGE_EXT) answers the EGL buffer age of the drawable.
Tests (host, WSL)
X11DisplayGuardTest (display names, /proc parsing, the Xwayland case), X11PresentTest (path
selection, rotation, idle/complete/configure handling, pacing, resize, error latching, fake xcb
and fake images), SharedImageSync additions (accesses publish as write + read),
SharedImageFlushPolicyTest, GbmGlamorTest (the real libgbm loader with GBM_BACKEND=mobilegl
over a fake shared-image library: glamor's create/export/import calls).
Device verification (phase B)
scripts/x11/ in the skill: deploy, check Xwayland's glamor log lines and its dma-buf wl_buffers in
KWin (WAYLAND_DEBUG-free: /proc/<kwin>/fdinfo dma-buf count and the server log's import lines),
glxgears/glxinfo/an X11 GL app before/after (fps, top CPU of app/Xwayland/KWin/server), and
ordinary X apps (xterm, xeyes) for glamor rendering.
Risks
drmGetDeviceFromDevIdon the borrowed render node inside the container (sysfs visibility).- Adreno: cross-context ordering relies on the Flush op's fence; a missed flush shows as tearing, not corruption of other windows.
- The extra round trip per Xwayland
glFlush(glamor flushes once per main-loop wakeup). - DRI3 clients that are not MobileGL (Mesa Vulkan WSI) hand Xwayland dma-bufs the server never
allocated:
PixmapFromBuffersfails with BadAlloc for them, as on any driver mismatch.
Device results (2026-10-03, TB321FU / Adreno 750, x11-gpu 95aa618d server + client)
glxgears 1280x720, % of one core, x11-verify.sh measure; "before" = the old stack (-shm
llvmpipe Xwayland, GLX readback + PutImage).
glxgears is fixed-function GL (glBegin/glEnd display lists, glLight), which MobileGL does not implement (Core Profile only), so its window stays black. These numbers measure presentation throughput of empty frames through each path, not rendering. Check X11 rendering with a core-profile or shader-based GLX client, and verify it from pixels (mgl-xgrab / XGetImage).
| run | fps | phone | glxgears | Xwayland | kwin | server | GPU |
|---|---|---|---|---|---|---|---|
| before, unpaced | 160 | 229% | 28% | 42% | 71% | 37% | 15% |
| Espryt DRI3+Present, interval 1 | 98-101 | 210% | 12% | 5% | 28% | 32% | 12% |
| Espryt DRI3+Present, vblank_mode=0 | 1440-1780 | 296% | 29% | 29% | 36% | 62% | 72% |
| Espryt readback + MIT-SHM on glamor | 46 | 168-188% | 5% | 78% | 11% | 35% | 5% |
| Magma DRI3+Present, interval 1 | 105-112 | 210% | 12% | 5% | 24% | 43% | 16% |
| Magma DRI3+Present, vblank_mode=0 | 940-970 | 282% | 24% | 18% | 33% | 60% | 77% |
| Magma readback + MIT-SHM on glamor | 27 | 161% | 2% | 50% | 7% | 70% | 5% |
- Glamor engages on both backends: Xwayland runs without
-shm, offers DRI3 1.2 + Present 1.2, its windows are linux-dmabuf shared images; GLX windows logpresented through DRI3+Present. - Found on the device and fixed: uploads into a texture bound to a shared image were dropped (Espryt) - an X server's PutImage into a window pixmap showed black windows. They are now drained at the producer's flush and written into the image (RGB-converted for X formats, which the driver holds as RGB8).
- The forced readback path is slower on a glamor Xwayland than on the old llvmpipe one (glamor uploads every frame through the server); it is only the fallback now.
- Fixed (
cd58f048): after a hide/show or lock/unlock round, X11 Qt windows kept stale or black regions in their window pixmap. Root cause: every glamor copy and fill drew nothing. glamor's fragment shaders name gl_FragColor only in the inactive arm of a version guard and write their ownfrag_color; the legacy lowering still declaredmg_FragColorahead of it, and the IO resolver gave that dead output location 0 andfrag_colorlocation 1, which no draw buffer reads. Uploads (PutImage) still landed, so windows looked right until Xwayland copied between window buffers (it swaps the window pixmap on every commit and reallocates buffers after a hide), and then showed old content or zeros. Inactive fragment outputs now take the top of the draw-buffer range. Host test: ProgramTest.InactiveLoweredFragColorDoesNotDisplaceTheActiveOutput. On the device, an X client checking its own XGetImage against a CPU model (PutImage, CopyArea and FillRectangle; MIT-SHM and plain; depth 24 and 32) sees 0 bad pixels through hide/show/lock on both backends. One run in about 20 on Magma (no SHM, depth 24) left a 2751-pixel rect mismatched; it did not come back in the repeats.