From ca569bb83777b24aac28e118ab9a61098c5388ca Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 25 Oct 2025 12:38:22 -0400
Subject: [PATCH] wayland: Use viewports to scale custom cursors
Cache the cursor image data at creation time, and use a viewport to render scaled custom cursors, instead of generating new cursor images for every scale.
---
src/video/wayland/SDL_waylandmouse.c | 307 ++++++++++++-----------
src/video/wayland/SDL_waylandshmbuffer.c | 67 ++---
src/video/wayland/SDL_waylandshmbuffer.h | 15 +-
3 files changed, 195 insertions(+), 194 deletions(-)
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index 19d53ab1caa7f..bae09f9ff6035 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -51,19 +51,22 @@ static bool Wayland_SetRelativeMouseMode(bool enabled);
typedef struct
{
- Wayland_SHMPool *shmPool;
- double scale;
- struct wl_list node;
-} Wayland_ScaledCustomCursor;
+ int width;
+ int height;
+ struct wl_buffer *buffer;
+} CustomCursorImage;
typedef struct
{
+ // The base dimensions of the cursor.
int width;
int height;
int hot_x;
int hot_y;
- struct wl_list scaled_cursor_cache;
- SDL_Surface *sdl_cursor_surfaces[];
+
+ Wayland_SHMPool *shmPool;
+ int images_per_frame;
+ CustomCursorImage images[];
} Wayland_CustomCursor;
typedef struct
@@ -293,13 +296,46 @@ static void Wayland_DBusFinishCursorProperties(void)
#endif
+static CustomCursorImage *Wayland_GetScaledCustomCursorImage(SDL_CursorData *data, int frame_index, double scale)
+{
+ const int offset = data->cursor_data.custom.images_per_frame * frame_index;
+
+ /* Find the closest image. Images that are larger than the
+ * desired size are preferred over images that are smaller.
+ */
+ CustomCursorImage *closest = NULL;
+ int desired_w = (int)SDL_round(data->cursor_data.custom.width * scale);
+ int desired_h = (int)SDL_round(data->cursor_data.custom.height * scale);
+ int desired_size = desired_w * desired_h;
+ int closest_distance = -1;
+ int closest_size = -1;
+ for (int i = 0; i < data->cursor_data.custom.images_per_frame && closest_distance && data->cursor_data.custom.images[offset + i].buffer; ++i) {
+ CustomCursorImage *candidate = &data->cursor_data.custom.images[offset + i];
+ int size = candidate->width * candidate->height;
+ int delta_w = candidate->width - desired_w;
+ int delta_h = candidate->height - desired_h;
+ int distance = (delta_w * delta_w) + (delta_h * delta_h);
+ if (closest_distance < 0 || distance < closest_distance ||
+ (size > desired_size && closest_size < desired_size)) {
+ closest = candidate;
+ closest_distance = distance;
+ closest_size = size;
+ }
+ }
+
+ return closest;
+}
+
static struct wl_buffer *Wayland_SeatGetCursorFrame(SDL_WaylandSeat *seat, int frame_index)
{
SDL_CursorData *data = seat->pointer.current_cursor;
if (data) {
if (!data->is_system_cursor) {
- return ((Wayland_ScaledCustomCursor *)(seat->pointer.cursor_state.cursor_handle))->shmPool->buffers[frame_index].wl_buffer;
+ const double scale = seat->pointer.focus ? seat->pointer.focus->scale_factor : 1.0;
+ CustomCursorImage *image = Wayland_GetScaledCustomCursorImage(data, frame_index, scale);
+
+ return image ? image->buffer : NULL;
} else {
return ((Wayland_CachedSystemCursor *)(seat->pointer.cursor_state.cursor_handle))->buffers[frame_index];
}
@@ -713,170 +749,132 @@ static bool Wayland_GetSystemCursor(SDL_CursorData *cdata, SDL_WaylandSeat *seat
return true;
}
-static Wayland_ScaledCustomCursor *Wayland_CacheScaledCustomCursor(SDL_CursorData *cursor, double scale)
+static SDL_Cursor *Wayland_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
{
- Wayland_ScaledCustomCursor *cache = NULL;
-
- // Is this cursor already cached at the target scale?
- if (!WAYLAND_wl_list_empty(&cursor->cursor_data.custom.scaled_cursor_cache)) {
- Wayland_ScaledCustomCursor *c = NULL;
- wl_list_for_each (c, &cursor->cursor_data.custom.scaled_cursor_cache, node) {
- if (c->scale == scale) {
- cache = c;
- break;
- }
- }
+ SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
+ if (!cursor) {
+ return NULL;
}
- if (!cache) {
- cache = SDL_calloc(1, sizeof(Wayland_ScaledCustomCursor));
- if (!cache) {
- return NULL;
+ SDL_CursorData *data = NULL;
+ int pool_size = 0;
+ int max_images = 0;
+ bool is_stack = false;
+ struct SurfaceArray
+ {
+ SDL_Surface **surfaces;
+ int count;
+ } *surfaces = SDL_small_alloc(struct SurfaceArray, frame_count, &is_stack);
+ if (!surfaces) {
+ goto failed;
+ }
+ SDL_memset(surfaces, 0, sizeof(struct SurfaceArray) * frame_count);
+
+ // Calculate the total allocation size.
+ for (int i = 0; i < frame_count; ++i) {
+ surfaces[i].surfaces = SDL_GetSurfaceImages(frames[i].surface, &surfaces[i].count);
+ if (!surfaces[i].surfaces) {
+ goto failed;
}
- SDL_Surface *surface = SDL_GetSurfaceImage(cursor->cursor_data.custom.sdl_cursor_surfaces[0], (float)scale);
- if (!surface) {
- SDL_free(cache);
- return NULL;
+ max_images = SDL_max(max_images, surfaces[i].count);
+ for (int j = 0; j < surfaces[i].count; ++j) {
+ pool_size += surfaces[i].surfaces[j]->w * surfaces[i].surfaces[j]->h * 4;
}
+ }
- // Allocate the shared memory buffer for this cursor.
- cache->shmPool = Wayland_AllocSHMPool(surface->w, surface->h, cursor->num_frames);
- if (!cache->shmPool) {
- SDL_free(cache);
- SDL_DestroySurface(surface);
- return NULL;
- }
+ data = SDL_calloc(1, sizeof(*data) + (sizeof(CustomCursorImage) * max_images * frame_count));
+ if (!data) {
+ goto failed;
+ }
- for (int i = 0; i < cursor->num_frames; ++i) {
- if (!surface) {
- surface = SDL_GetSurfaceImage(cursor->cursor_data.custom.sdl_cursor_surfaces[i], (float)scale);
- if (!surface) {
- Wayland_ReleaseSHMPool(cache->shmPool);
- SDL_free(cache);
- return NULL;
- }
+ data->frame_durations_ms = SDL_calloc(frame_count, sizeof(Uint32));
+ if (!data->frame_durations_ms) {
+ goto failed;
+ }
+
+ data->cursor_data.custom.shmPool = Wayland_AllocSHMPool(pool_size);
+ if (!data->cursor_data.custom.shmPool) {
+ goto failed;
+ }
+
+ cursor->internal = data;
+ data->cursor_data.custom.width = frames[0].surface->w;
+ data->cursor_data.custom.height = frames[0].surface->h;
+ data->cursor_data.custom.hot_x = hot_x;
+ data->cursor_data.custom.hot_y = hot_y;
+ data->cursor_data.custom.images_per_frame = max_images;
+ data->num_frames = frame_count;
+
+ for (int i = 0; i < frame_count; ++i) {
+ data->frame_durations_ms[i] = frames[i].duration;
+ if (data->total_duration_ms < SDL_MAX_UINT32) {
+ if (data->frame_durations_ms[i] > 0) {
+ data->total_duration_ms += data->frame_durations_ms[i];
+ } else {
+ data->total_duration_ms = SDL_MAX_UINT32;
}
+ }
+
+ const int offset = i * max_images;
+ for (int j = 0; j < surfaces[i].count; ++j) {
+ SDL_Surface *surface = surfaces[i].surfaces[j];
+ // Convert the surface format, if required.
if (surface->format != SDL_PIXELFORMAT_ARGB8888) {
- SDL_Surface *temp = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
- if (temp) {
- SDL_DestroySurface(surface);
- surface = temp;
- } else {
- Wayland_ReleaseSHMPool(cache->shmPool);
- SDL_free(cache);
- return NULL;
+ surface = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_ARGB8888);
+ if (!surface) {
+ goto failed;
}
}
+ data->cursor_data.custom.images[offset + j].width = surface->w;
+ data->cursor_data.custom.images[offset + j].height = surface->h;
+
+ void *buf_data;
+ data->cursor_data.custom.images[offset + j].buffer = Wayland_AllocBufferFromPool(data->cursor_data.custom.shmPool, surface->w, surface->h, &buf_data);
// Wayland requires premultiplied alpha for its surfaces.
SDL_PremultiplyAlpha(surface->w, surface->h,
surface->format, surface->pixels, surface->pitch,
- SDL_PIXELFORMAT_ARGB8888, cache->shmPool->buffers[i].shm_data, surface->w * 4, true);
+ SDL_PIXELFORMAT_ARGB8888, buf_data, surface->w * 4, true);
- SDL_DestroySurface(surface);
- surface = NULL;
+ if (surface != surfaces[i].surfaces[j]) {
+ SDL_DestroySurface(surface);
+ }
}
- cache->scale = scale;
- WAYLAND_wl_list_insert(&cursor->cursor_data.custom.scaled_cursor_cache, &cache->node);
- }
-
- return cache;
-}
-
-static bool Wayland_GetCustomCursor(SDL_CursorData *cursor, SDL_WaylandSeat *seat, int *scale, int *dst_width, int *dst_height, int *hot_x, int *hot_y)
-{
- SDL_VideoDevice *vd = SDL_GetVideoDevice();
- SDL_VideoData *wd = vd->internal;
- Wayland_CustomCursor *custom_cursor = &cursor->cursor_data.custom;
- SDL_Window *focus = SDL_GetMouseFocus();
- double scale_factor = 1.0;
-
- // If the surfaces were released, there are no scaled images.
- if (focus && custom_cursor->sdl_cursor_surfaces[0]) {
- scale_factor = focus->internal->scale_factor;
- }
-
- // Only use fractional scale values if viewports are available.
- if (!wd->viewporter) {
- scale_factor = SDL_ceil(scale_factor);
- }
-
- Wayland_ScaledCustomCursor *c = Wayland_CacheScaledCustomCursor(cursor, scale_factor);
- if (!c) {
- return false;
+ // Free the memory returned by SDL_GetSurfaceImages().
+ SDL_free(surfaces[i].surfaces);
}
- seat->pointer.cursor_state.cursor_handle = c;
- *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0;
- *dst_width = custom_cursor->width;
- *dst_height = custom_cursor->height;
- *hot_x = custom_cursor->hot_x;
- *hot_y = custom_cursor->hot_y;
-
- return true;
-}
-
-static SDL_Cursor *Wayland_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
-{
- SDL_Cursor *cursor = SDL_calloc(1, sizeof(*cursor));
-
- if (cursor) {
- SDL_CursorData *data = SDL_calloc(1, sizeof(*data) + (sizeof(SDL_Surface *) * frame_count));
- if (!data) {
- SDL_free(cursor);
- return NULL;
- }
+ SDL_small_free(surfaces, is_stack);
- data->frame_durations_ms = SDL_calloc(frame_count, sizeof(Uint32));
- if (!data->frame_durations_ms) {
- SDL_free(data);
- SDL_free(cursor);
- return NULL;
- }
+ return cursor;
- cursor->internal = data;
- WAYLAND_wl_list_init(&data->cursor_data.custom.scaled_cursor_cache);
- data->cursor_data.custom.width = frames[0].surface->w;
- data->cursor_data.custom.height = frames[0].surface->h;
- data->cursor_data.custom.hot_x = hot_x;
- data->cursor_data.custom.hot_y = hot_y;
- data->num_frames = frame_count;
+failed:
+ Wayland_ReleaseSHMPool(data->cursor_data.custom.shmPool);
- for (int i = 0; i < frame_count; ++i) {
- data->frame_durations_ms[i] = frames[i].duration;
- if (data->total_duration_ms < SDL_MAX_UINT32) {
- if (data->frame_durations_ms[i] > 0) {
- data->total_duration_ms += data->frame_durations_ms[i];
- } else {
- data->total_duration_ms = SDL_MAX_UINT32;
- }
+ if (data) {
+ SDL_free(data->frame_durations_ms);
+ for (int i = 0; i < data->cursor_data.custom.images_per_frame * frame_count; ++i) {
+ if (data->cursor_data.custom.images[i].buffer) {
+ wl_buffer_destroy(data->cursor_data.custom.images[i].buffer);
}
- data->cursor_data.custom.sdl_cursor_surfaces[i] = frames[i].surface;
- ++frames[i].surface->refcount;
}
+ }
- // If the cursor has only one size, just prepare it now.
- if (!SDL_SurfaceHasAlternateImages(frames[0].surface)) {
- bool success = !!Wayland_CacheScaledCustomCursor(data, 1.0);
-
- // Done with the surfaces.
- for (int i = 0; i < frame_count; ++i) {
- SDL_DestroySurface(data->cursor_data.custom.sdl_cursor_surfaces[i]);
- data->cursor_data.custom.sdl_cursor_surfaces[i] = NULL;
- }
+ SDL_free(data);
- if (!success) {
- SDL_free(data);
- SDL_free(cursor);
- return NULL;
- }
+ if (surfaces) {
+ for (int i = 0; i < frame_count; ++i) {
+ SDL_free(surfaces[i].surfaces);
}
+ SDL_small_free(surfaces, is_stack);
}
- return cursor;
+ SDL_free(cursor);
+
+ return NULL;
}
static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
@@ -940,15 +938,13 @@ static void Wayland_FreeCursorData(SDL_CursorData *d)
SDL_free(c);
}
} else {
- Wayland_ScaledCustomCursor *c, *temp;
- wl_list_for_each_safe(c, temp, &d->cursor_data.custom.scaled_cursor_cache, node) {
- Wayland_ReleaseSHMPool(c->shmPool);
- SDL_free(c);
+ for (int i = 0; i < d->num_frames * d->cursor_data.custom.images_per_frame; ++i) {
+ if (d->cursor_data.custom.images[i].buffer) {
+ wl_buffer_destroy(d->cursor_data.custom.images[i].buffer);
+ }
}
- for (int i = 0; i < d->num_frames; ++i) {
- SDL_DestroySurface(d->cursor_data.custom.sdl_cursor_surfaces[i]);
- }
+ Wayland_ReleaseSHMPool(d->cursor_data.custom.shmPool);
}
SDL_free(d->frame_durations_ms);
@@ -1048,7 +1044,7 @@ static void Wayland_SeatSetCursor(SDL_WaylandSeat *seat, SDL_Cursor *cursor)
{
if (seat->pointer.wl_pointer) {
SDL_CursorData *cursor_data = cursor ? cursor->internal : NULL;
- int scale = 1;
+ int scale = 0;
int dst_width = 0;
int dst_height = 0;
int hot_x;
@@ -1095,8 +1091,21 @@ static void Wayland_SeatSetCursor(SDL_WaylandSeat *seat, SDL_Cursor *cursor)
dst_height = dst_width;
} else {
- if (!Wayland_GetCustomCursor(cursor_data, seat, &scale, &dst_width, &dst_height, &hot_x, &hot_y)) {
- return;
+ dst_width = cursor_data->cursor_data.custom.width;
+ dst_height = cursor_data->cursor_data.custom.height;
+ hot_x = cursor_data->cursor_data.custom.hot_x;
+ hot_y = cursor_data->cursor_data.custom.hot_y;
+
+ // If viewports aren't available, figure out the integer scale.
+ if (!seat->display->viewporter) {
+ scale = 1;
+
+ double image_scale = seat->pointer.focus ? seat->pointer.focus->scale_factor : 1.0;
+ CustomCursorImage *image = Wayland_GetScaledCustomCursorImage(cursor_data, 0, image_scale);
+ if (image) {
+ image_scale = (double)image->width / (double)cursor_data->cursor_data.custom.width;
+ scale= SDL_lround(image_scale);
+ }
}
}
diff --git a/src/video/wayland/SDL_waylandshmbuffer.c b/src/video/wayland/SDL_waylandshmbuffer.c
index 77b3c8429147f..709491dc54df6 100644
--- a/src/video/wayland/SDL_waylandshmbuffer.c
+++ b/src/video/wayland/SDL_waylandshmbuffer.c
@@ -166,73 +166,74 @@ void Wayland_ReleaseSHMBuffer(Wayland_SHMBuffer *shmBuffer)
}
}
-Wayland_SHMPool *Wayland_AllocSHMPool(int width, int height, int buffer_count)
+struct Wayland_SHMPool
+{
+ struct wl_shm_pool *shm_pool;
+ void *shm_pool_memory;
+ int shm_pool_size;
+ int offset;
+};
+
+Wayland_SHMPool *Wayland_AllocSHMPool(int size)
{
SDL_VideoDevice *vd = SDL_GetVideoDevice();
SDL_VideoData *data = vd->internal;
- const Uint32 SHM_FMT = WL_SHM_FORMAT_ARGB8888;
- if (buffer_count <= 0) {
- SDL_InvalidParamError("count");
+ if (size <= 0) {
return NULL;
}
- Wayland_SHMPool *shmPool = SDL_calloc(buffer_count, sizeof(Wayland_SHMPool) + (sizeof(Wayland_SHMBuffer) * buffer_count));
+ Wayland_SHMPool *shmPool = SDL_calloc(1, sizeof(Wayland_SHMPool));
if (!shmPool) {
return NULL;
}
- const int stride = width * 4;
- const int element_size = stride * height;
- const int element_offset = (element_size + 15) & (~15);
- shmPool->internal.shm_pool_size = element_offset * buffer_count;
- shmPool->buffer_count = buffer_count;
+ shmPool->shm_pool_size = (size + 15) & (~15);
- const int shm_fd = CreateTempFD(shmPool->internal.shm_pool_size);
+ const int shm_fd = CreateTempFD(shmPool->shm_pool_size);
if (shm_fd < 0) {
SDL_free(shmPool);
SDL_SetError("Creating SHM buffer failed.");
return NULL;
}
- shmPool->internal.shm_pool_handle = mmap(NULL, shmPool->internal.shm_pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
- if (shmPool->internal.shm_pool_handle == MAP_FAILED) {
- shmPool->internal.shm_pool_handle = NULL;
+ shmPool->shm_pool_memory = mmap(NULL, shmPool->shm_pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
+ if (shmPool->shm_pool_memory == MAP_FAILED) {
+ shmPool->shm_pool_memory = NULL;
close(shm_fd);
SDL_free(shmPool);
SDL_SetError("mmap() failed.");
return NULL;
}
- SDL_assert(shmPool->internal.shm_pool_handle != NULL);
+ shmPool->shm_pool = wl_shm_create_pool(data->shm, shm_fd, shmPool->shm_pool_size);
+ close(shm_fd);
- struct wl_shm_pool *shm_pool = wl_shm_create_pool(data->shm, shm_fd, shmPool->internal.shm_pool_size);
+ return shmPool;
+}
+
+struct wl_buffer *Wayland_AllocBufferFromPool(Wayland_SHMPool *shmPool, int width, int height, void **data)
+{
+ const Uint32 SHM_FMT = WL_SHM_FORMAT_ARGB8888;
- for (size_t i = 0; i < buffer_count; i++) {
- shmPool->buffers[i].shm_data = (Uint8 *)shmPool->internal.shm_pool_handle + (element_offset * i);
- shmPool->buffers[i].wl_buffer = wl_shm_pool_create_buffer(shm_pool, element_offset * i, width, height, stride, SHM_FMT);
- wl_buffer_add_listener(shmPool->buffers[i].wl_buffer, &buffer_listener, shmPool);
+ if (!shmPool || !width || !height || !data) {
+ return NULL;
}
- wl_shm_pool_destroy(shm_pool);
- close(shm_fd);
+ *data = (Uint8 *)shmPool->shm_pool_memory + shmPool->offset;
+ struct wl_buffer *buffer = wl_shm_pool_create_buffer(shmPool->shm_pool, shmPool->offset, width, height, width * 4, SHM_FMT);
+ wl_buffer_add_listener(buffer, &buffer_listener, shmPool);
- return shmPool;
+ shmPool->offset += width * height * 4;
+
+ return buffer;
}
void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool)
{
if (shmPool) {
- for (int i = 0; i < shmPool->buffer_count; ++i) {
- if (shmPool->buffers[i].wl_buffer) {
- wl_buffer_destroy(shmPool->buffers[i].wl_buffer);
- }
- }
-
- if (shmPool->internal.shm_pool_handle) {
- munmap(shmPool->internal.shm_pool_handle, shmPool->internal.shm_pool_size);
- }
-
+ wl_shm_pool_destroy(shmPool->shm_pool);
+ munmap(shmPool->shm_pool_memory, shmPool->shm_pool_size);
SDL_free(shmPool);
}
}
diff --git a/src/video/wayland/SDL_waylandshmbuffer.h b/src/video/wayland/SDL_waylandshmbuffer.h
index 4410fd6e56975..5355412f3abd6 100644
--- a/src/video/wayland/SDL_waylandshmbuffer.h
+++ b/src/video/wayland/SDL_waylandshmbuffer.h
@@ -31,23 +31,14 @@ typedef struct
int shm_data_size;
} Wayland_SHMBuffer;
-typedef struct
-{
- struct
- {
- void *shm_pool_handle;
- int shm_pool_size;
- } internal;
-
- int buffer_count;
- Wayland_SHMBuffer buffers[];
-} Wayland_SHMPool;
+typedef struct Wayland_SHMPool Wayland_SHMPool;
// Allocates an SHM buffer with the format WL_SHM_FORMAT_ARGB8888
extern bool Wayland_AllocSHMBuffer(int width, int height, Wayland_SHMBuffer *shmBuffer);
extern void Wayland_ReleaseSHMBuffer(Wayland_SHMBuffer *shmBuffer);
-extern Wayland_SHMPool *Wayland_AllocSHMPool(int width, int height, int buffer_count);
+extern Wayland_SHMPool *Wayland_AllocSHMPool(int size);
+extern struct wl_buffer *Wayland_AllocBufferFromPool(Wayland_SHMPool *shmPool, int width, int height, void **data);
extern void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool);
#endif