SDL: wayland: Use viewports to scale custom cursors

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