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.
This commit is contained in:
xXJSONDeruloXx committed 2026-09-28 14:33:08 -04:00
1 parent 435f7a1b03
commit dae7592788
2 files changed
+99 -19

No files matched your search

+95 -15
View File
@@ -30,6 +30,7 @@
#include <time.h>
#include <stdarg.h>
#include <errno.h>
#include <poll.h>
#include <sys/timerfd.h>
#include <sys/syscall.h>
#include <sys/sysmacros.h>
@@ -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 = {
+4 -4
View File
@@ -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.