From dae759278803296181f9c369d0113e3034e6581e Mon Sep 17 00:00:00 2001 From: xXJSONDeruloXx Date: Mon, 28 Sep 2026 14:33:08 -0400 Subject: [PATCH] Desktop: hand labwc's cursor to the app, so full-screen games scan out The desktop forced a software cursor (WLR_NO_HARDWARE_CURSORS=1) because labwc's cursor never showed when handed to the app's compositor. With a software cursor wlroots never scans out: every frame of a full-screen game was composited by labwc first ("Direct scan-out disabled by software cursor", 2284 times in one Adreno 825 report). Why the cursor never showed: wlroots' Wayland backend attaches and commits its cursor surface before wl_pointer.set_cursor gives it the cursor role, and sets it again with nothing new committed each time the pointer enters. The compositor released a role-less surface's buffer on commit, so the cursor surface had no image by the time it became the cursor. - A role-less surface's last buffer (cursor-sized only) is held until the next commit; set_cursor publishes it, and a repeat set_cursor of the surface already shown unhides its last image. - A dma-buf cursor is read back after the client's implicit render fence, as the zero-copy path waits for it. - The desktop no longer forces a software cursor. Device-tested on the AYN Thor (Adreno 740, 1920x1080): labwc's own cursor is drawn, with its shapes (I-beam over a terminal); wlroots logs "Direct scan-out enabled" for a full-screen glxgears. At the same 60 fps on screen the GPU is 3.0 % busy against 4.6 % with the software cursor, both at 550 MHz. --- app/src/main/cpp/waylandcomp/src/compositor.c | 110 +++++++++++++++--- tools/linuxfs/desktop/droiddeck-desktop | 8 +- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index c206dfb2c..7dd9b7c73 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -247,6 +248,10 @@ struct surface { int releases_pending; /* FPS limiter: buffers of this surface still to be released */ enum surface_role role; + /* A role-less surface's last buffer, held unreleased: it may become the pointer's image + * (wl_pointer.set_cursor after the commit, which is wlroots' order). */ + struct wl_resource *idle_buffer; + struct wl_listener idle_buffer_destroy; struct wl_resource *xdg_surface, *xdg_toplevel; struct wl_resource *viewport; @@ -475,6 +480,7 @@ static int g_ntouches; #define CURSOR_MAX_PX (256 * 256) static pthread_mutex_t g_cursor_lock = PTHREAD_MUTEX_INITIALIZER; static struct surface *g_cursor_surface; /* compositor thread only */ +static struct surface *g_cursor_shown; /* compositor thread: whose image g_cursor_px holds */ static int g_cursor_hx, g_cursor_hy; /* hotspot, surface-local */ static uint32_t g_cursor_px[CURSOR_MAX_PX]; /* guarded by g_cursor_lock: ARGB8888 snapshot */ static uint32_t g_cursor_rb[CURSOR_MAX_PX]; /* compositor thread: GPU readback staging */ @@ -482,6 +488,14 @@ static int g_cursor_w, g_cursor_h; /* 0 = nothing to draw */ static int g_cursor_hidden = 1; /* the client asked for no pointer */ static int g_cursor_serial; /* bumped on every change; the app polls it */ +/* The last image again, after a hide: the same surface set as the pointer once more with nothing + * newly committed to it (wlroots re-sets its cursor surface each time the pointer enters). */ +static void cursor_publish_shown(void) { + pthread_mutex_lock(&g_cursor_lock); + if (g_cursor_w > 0) { g_cursor_hidden = 0; g_cursor_serial++; } /* serial: the hotspot may be new */ + pthread_mutex_unlock(&g_cursor_lock); +} + static void cursor_publish_hidden(void) { pthread_mutex_lock(&g_cursor_lock); if (!g_cursor_hidden) { g_cursor_hidden = 1; g_cursor_serial++; } @@ -931,6 +945,68 @@ static void surface_set_input(struct wl_client *c, struct wl_resource *r, static void send_toplevel_configure(struct surface *s); +/* The pointer image from a cursor surface's buffer: wl_shm is copied, a dma-buf (labwc on a GPU + * renderer) is read back once the client's render into it is done - its implicit fence, as the + * zero-copy path waits for it (ahb_swapchain_present). */ +static void cursor_publish_buffer(struct surface *s, struct wl_resource *buffer) { + struct dmabuf_buffer *db = get_dmabuf(buffer); + struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); + if (shm) { + cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy); + } else if (db && db->n_planes >= 1 && db->width > 0 && db->height > 0 && + (int64_t)db->width * db->height <= CURSOR_MAX_PX) { + if (!db->img && !db->import_failed) { + db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height, + db->stride[0], db->offset[0]); + if (!db->img) db->import_failed = 1; + } + if (!db->img) return; + struct pollfd p = {.fd = db->fd[0], .events = POLLIN}; + int r; + do { r = poll(&p, 1, 100); } while (r < 0 && errno == EINTR); + if (vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) != 0) return; + cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height, + (size_t)db->width * 4, g_cursor_hx, g_cursor_hy); + } else { + return; + } + g_cursor_shown = s; +} + +static void on_idle_buffer_destroyed(struct wl_listener *l, void *data) { + struct surface *s = wl_container_of(l, s, idle_buffer_destroy); + wl_list_remove(&s->idle_buffer_destroy.link); + wl_list_init(&s->idle_buffer_destroy.link); + s->idle_buffer = NULL; +} + +/* Only what could be a pointer image is held; anything larger goes straight back. */ +static void surface_hold_idle(struct surface *s, struct wl_resource *buffer) { + struct dmabuf_buffer *db = get_dmabuf(buffer); + struct wl_shm_buffer *shm = db ? NULL : wl_shm_buffer_get(buffer); + int64_t px = db ? (int64_t)db->width * db->height + : shm ? (int64_t)wl_shm_buffer_get_width(shm) * wl_shm_buffer_get_height(shm) : 0; + if (px <= 0 || px > CURSOR_MAX_PX) { wl_buffer_send_release(buffer); return; } + s->idle_buffer = buffer; + s->idle_buffer_destroy.notify = on_idle_buffer_destroyed; + wl_resource_add_destroy_listener(buffer, &s->idle_buffer_destroy); +} + +/* Lets go of the held buffer; release = give it back to the client (not while it is going away). */ +static struct wl_resource *surface_take_idle(struct surface *s) { + struct wl_resource *buffer = s->idle_buffer; + if (!buffer) return NULL; + wl_list_remove(&s->idle_buffer_destroy.link); + wl_list_init(&s->idle_buffer_destroy.link); + s->idle_buffer = NULL; + return buffer; +} + +static void surface_drop_idle(struct surface *s) { + struct wl_resource *buffer = surface_take_idle(s); + if (buffer) wl_buffer_send_release(buffer); +} + static void surface_commit(struct wl_client *c, struct wl_resource *r) { struct surface *s = wl_resource_get_user_data(r); struct surface *child; @@ -970,28 +1046,19 @@ static void surface_commit(struct wl_client *c, struct wl_resource *r) { s->pending_buffer = NULL; s->pending_attach = 0; + surface_drop_idle(s); /* replaced, whatever the surface is now */ if (s->role == ROLE_CURSOR) { /* The client's pointer image, copied out for the app's overlay (see cursor_publish_*) * rather than composited, so it survives the zero-copy and HDR layer paths. */ if (!buffer) { cursor_publish_hidden(); /* wlroots clears its cursor surface to hide the pointer */ - } else if (shm) { - cursor_publish_shm(shm, g_cursor_hx, g_cursor_hy); - } else if (db && db->n_planes >= 1 && db->width > 0 && db->height > 0 && - (int64_t)db->width * db->height <= CURSOR_MAX_PX) { - if (!db->img && !db->import_failed) { - db->img = vkp_image_from_dmabuf(db->fd[0], db->format, db->modifier, db->width, db->height, - db->stride[0], db->offset[0]); - if (!db->img) db->import_failed = 1; - } - if (db->img && vkp_image_readback(db->img, g_cursor_rb, CURSOR_MAX_PX) == 0) - cursor_publish_pixels((const uint8_t *)g_cursor_rb, db->width, db->height, - (size_t)db->width * 4, g_cursor_hx, g_cursor_hy); + } else { + cursor_publish_buffer(s, buffer); + wl_buffer_send_release(buffer); } - if (buffer) wl_buffer_send_release(buffer); } else if (s->role == ROLE_NONE) { - /* Role-less surface: never drawn. */ - if (buffer) wl_buffer_send_release(buffer); + /* Role-less surface: never drawn, but its buffer is kept for a set_cursor to come. */ + if (buffer) surface_hold_idle(s, buffer); } else if (db) { take_dmabuf(s, db, buffer); } else if (shm) { @@ -1063,6 +1130,8 @@ static void surface_resource_destroy(struct wl_resource *r) { for (int i = 0; i < g_nkbs; i++) if (g_kbs[i].focus == r) g_kbs[i].focus = NULL; touch_cancel_surface(s); if (g_cursor_surface == s) { g_cursor_surface = NULL; cursor_publish_hidden(); } + if (g_cursor_shown == s) g_cursor_shown = NULL; + surface_take_idle(s); if (g_grab == s) g_grab = NULL; if (g_key_target == s) g_key_target = NULL; if (g_ime_click == s) g_ime_click = NULL; @@ -1139,6 +1208,7 @@ static void compositor_create_surface(struct wl_client *c, struct wl_resource *r wl_list_init(&s->child_link); wl_list_init(&s->toplevel_link); wl_list_init(&s->pending_buffer_destroy.link); + wl_list_init(&s->idle_buffer_destroy.link); wl_list_insert(&g_surfaces, &s->link); wl_resource_set_implementation(s->resource, &surface_impl, s, surface_resource_destroy); } @@ -2143,6 +2213,16 @@ static void pointer_set_cursor(struct wl_client *c, struct wl_resource *r, uint3 g_cursor_surface = s; g_cursor_hx = hx; g_cursor_hy = hy; + /* Its content may already be committed: wlroots' Wayland backend attaches and commits the + * cursor surface first and sets it as the pointer after, and sets it again with nothing new + * committed each time the pointer re-enters. */ + struct wl_resource *buffer = surface_take_idle(s); + if (buffer) { + cursor_publish_buffer(s, buffer); + wl_buffer_send_release(buffer); + } else if (g_cursor_shown == s) { + cursor_publish_shown(); + } } static void pointer_release(struct wl_client *c, struct wl_resource *r) { wl_resource_destroy(r); } static const struct wl_pointer_interface pointer_impl = { diff --git a/tools/linuxfs/desktop/droiddeck-desktop b/tools/linuxfs/desktop/droiddeck-desktop index 6820a9dce..6e1d17595 100644 --- a/tools/linuxfs/desktop/droiddeck-desktop +++ b/tools/linuxfs/desktop/droiddeck-desktop @@ -55,10 +55,10 @@ if [ -n "${BL_DEBUG_DIR:-}" ]; then echo "== droiddeck-desktop $(date -u '+%Y-%m-%d %H:%M:%S UTC') renderer=$WLR_RENDERER" fi export WLR_BACKENDS=wayland -# The app's compositor draws no wl_pointer cursor surfaces, so a cursor wlroots hands to its -# parent ("hardware" cursor on the wayland backend) never shows - on the GPU renderers the whole -# cursor was missing. Draw it into the desktop's own frames instead. -export WLR_NO_HARDWARE_CURSORS=${WLR_NO_HARDWARE_CURSORS:-1} +# The cursor goes to the app's compositor as a wl_pointer cursor surface ("hardware" cursor on the +# wayland backend), which the app draws as its pointer. Drawn into the desktop's own frames instead +# (WLR_NO_HARDWARE_CURSORS=1), it cost every frame a full-screen composite: a software cursor +# rules out direct scan-out, so a full-screen game was never handed to the app as it is. mkdir -p "$HOME/.config/labwc" "$HOME/.config/lxqt" # Our defaults for labwc and the panel, refreshed every session. What a user changes in the # panel's own settings lands in panel.conf and is kept; only rc.xml and autostart are ours.