From ad3a4c677bf28f054482d03394bbd7b12ba33a0c Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Mon, 23 Sep 2024 12:50:28 -0400 Subject: [PATCH] wayland: Clean up cursor scaling Handle named and custom cursor scaling in a cleaner manner, and account for edge cases where named cursor sizes may not exactly match the required size. --- src/video/wayland/SDL_waylandmouse.c | 218 ++++++++++++++++----------- 1 file changed, 129 insertions(+), 89 deletions(-) diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c index 7b21a25491..f503e2de29 100644 --- a/src/video/wayland/SDL_waylandmouse.c +++ b/src/video/wayland/SDL_waylandmouse.c @@ -48,18 +48,16 @@ static bool Wayland_SetRelativeMouseMode(bool enabled); typedef struct { struct Wayland_SHMBuffer shmBuffer; - - int dst_width; - int dst_height; double scale; - struct wl_list node; -} Wayland_CachedCustomCursor; +} Wayland_ScaledCustomCursor; typedef struct { SDL_Surface *sdl_cursor_surface; - struct wl_list cursor_cache; + int hot_x; + int hot_y; + struct wl_list scaled_cursor_cache; } Wayland_CustomCursor; typedef struct @@ -91,8 +89,6 @@ struct SDL_CursorData struct wl_surface *surface; struct wp_viewport *viewport; - int hot_x, hot_y; - bool is_system_cursor; }; @@ -338,39 +334,35 @@ static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time) wl_surface_commit(c->surface); } -static bool wayland_get_system_cursor(SDL_VideoData *vdata, SDL_CursorData *cdata, double *scale) +static bool Wayland_GetSystemCursor(SDL_VideoData *vdata, SDL_CursorData *cdata, int *scale, int *dst_size, int *hot_x, int *hot_y) { struct wl_cursor_theme *theme = NULL; struct wl_cursor *cursor; const char *css_name = "default"; const char *fallback_name = NULL; - - int size = dbus_cursor_size; - - SDL_Window *focus; + double scale_factor = 1.0; + int theme_size = dbus_cursor_size; // Fallback envvar if the DBus properties don't exist - if (size <= 0) { + if (theme_size <= 0) { const char *xcursor_size = SDL_getenv("XCURSOR_SIZE"); if (xcursor_size) { - size = SDL_atoi(xcursor_size); + theme_size = SDL_atoi(xcursor_size); } } - if (size <= 0) { - size = 24; + if (theme_size <= 0) { + theme_size = 24; } // First, find the appropriate theme based on the current scale... - focus = SDL_GetMouse()->focus; + SDL_Window *focus = SDL_GetMouse()->focus; if (focus) { // TODO: Use the fractional scale once GNOME supports viewports on cursor surfaces. - *scale = SDL_ceil(focus->internal->scale_factor); - } else { - *scale = 1.0; + scale_factor = SDL_ceil(focus->internal->scale_factor); } - size *= (int)*scale; + const int scaled_size = (int)SDL_lround(theme_size * scale_factor); for (int i = 0; i < vdata->num_cursor_themes; ++i) { - if (vdata->cursor_themes[i].size == size) { + if (vdata->cursor_themes[i].size == scaled_size) { theme = vdata->cursor_themes[i].theme; break; } @@ -390,8 +382,8 @@ static bool wayland_get_system_cursor(SDL_VideoData *vdata, SDL_CursorData *cdat xcursor_theme = SDL_getenv("XCURSOR_THEME"); } - theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, size, vdata->shm); - vdata->cursor_themes[vdata->num_cursor_themes].size = size; + theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, scaled_size, vdata->shm); + vdata->cursor_themes[vdata->num_cursor_themes].size = scaled_size; vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme; } @@ -429,66 +421,116 @@ static bool wayland_get_system_cursor(SDL_VideoData *vdata, SDL_CursorData *cdat cdata->cursor_data.system.frames[i].duration = cursor->images[i]->delay; cdata->cursor_data.system.total_duration += cursor->images[i]->delay; } - cdata->hot_x = cursor->images[0]->hotspot_x; - cdata->hot_y = cursor->images[0]->hotspot_y; + + *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0; + *dst_size = theme_size; + + // Calculate the hotspot offset if the cursor is being scaled. + if (scaled_size == cursor->images[0]->width) { + // If the theme has an exact size match, just divide by the scale. + *hot_x = (int)SDL_lround(cursor->images[0]->hotspot_x / scale_factor); + *hot_y = (int)SDL_lround(cursor->images[0]->hotspot_y / scale_factor); + } else { + if (vdata->viewporter) { + // Use a viewport if no exact size match is found to avoid a potential "buffer size is not divisible by scale" protocol error. + *scale = 0; + + // Map the hotspot coordinates from the source to destination sizes. + const double hotspot_scale = (double)theme_size / (double)cursor->images[0]->width; + *hot_x = (int)SDL_lround(hotspot_scale * cursor->images[0]->hotspot_x); + *hot_y = (int)SDL_lround(hotspot_scale * cursor->images[0]->hotspot_y); + } else { + // No exact match, and viewports are unsupported. Find a safe integer scale. + for (; *scale > 1; --*scale) { + if (cursor->images[0]->width % *scale == 0) { + break; + } + } + *hot_x = cursor->images[0]->hotspot_x / *scale; + *hot_y = cursor->images[0]->hotspot_y / *scale; + } + } + return true; } -static Wayland_CachedCustomCursor *Wayland_GetCachedCustomCursor(SDL_Cursor *cursor) +static Wayland_ScaledCustomCursor *Wayland_CacheScaledCustomCursor(SDL_CursorData *cdata, double scale) +{ + Wayland_ScaledCustomCursor *cache = NULL; + + // Is this cursor already cached at the target scale? + if (!WAYLAND_wl_list_empty(&cdata->cursor_data.custom.scaled_cursor_cache)) { + Wayland_ScaledCustomCursor *c = NULL; + wl_list_for_each (c, &cdata->cursor_data.custom.scaled_cursor_cache, node) { + if (c->scale == scale) { + cache = c; + break; + } + } + } + + if (!cache) { + cache = SDL_calloc(1, sizeof(Wayland_ScaledCustomCursor)); + if (!cache) { + return NULL; + } + + SDL_Surface *surface = SDL_GetSurfaceImage(cdata->cursor_data.custom.sdl_cursor_surface, (float)scale); + if (!surface) { + SDL_free(cache); + return NULL; + } + + // Allocate the shared memory buffer for this cursor. + if (!Wayland_AllocSHMBuffer(surface->w, surface->h, &cache->shmBuffer)) { + SDL_free(cache); + SDL_DestroySurface(surface); + return NULL; + } + + // Wayland requires premultiplied alpha for its surfaces. + SDL_PremultiplyAlpha(surface->w, surface->h, + surface->format, surface->pixels, surface->pitch, + SDL_PIXELFORMAT_ARGB8888, cache->shmBuffer.shm_data, surface->w * 4, true); + + cache->scale = scale; + WAYLAND_wl_list_insert(&cdata->cursor_data.custom.scaled_cursor_cache, &cache->node); + SDL_DestroySurface(surface); + } + + return cache; +} + +static bool Wayland_GetCustomCursor(SDL_Cursor *cursor, struct wl_buffer **buffer, int *scale, int *dst_width, int *dst_height, int *hot_x, int *hot_y) { SDL_VideoDevice *vd = SDL_GetVideoDevice(); SDL_VideoData *wd = vd->internal; SDL_CursorData *data = cursor->internal; - Wayland_CachedCustomCursor *cache; SDL_Window *focus = SDL_GetMouseFocus(); - double scale = 1.0; + double scale_factor = 1.0; if (focus && SDL_SurfaceHasAlternateImages(data->cursor_data.custom.sdl_cursor_surface)) { - scale = focus->internal->scale_factor; + scale_factor = focus->internal->scale_factor; } // Only use fractional scale values if viewports are available. if (!wd->viewporter) { - scale = SDL_ceil(scale); + scale_factor = SDL_ceil(scale_factor); } - // Is this cursor already cached at the target scale? - wl_list_for_each(cache, &data->cursor_data.custom.cursor_cache, node) { - if (cache->scale == scale) { - return cache; - } + Wayland_ScaledCustomCursor *c = Wayland_CacheScaledCustomCursor(data, scale_factor); + if (!c) { + return false; } - cache = SDL_calloc(1, sizeof(Wayland_CachedCustomCursor)); - if (!cache) { - return NULL; - } + *buffer = c->shmBuffer.wl_buffer; + *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0; + *dst_width = data->cursor_data.custom.sdl_cursor_surface->w; + *dst_height = data->cursor_data.custom.sdl_cursor_surface->h; + *hot_x = data->cursor_data.custom.hot_x; + *hot_y = data->cursor_data.custom.hot_y; - SDL_Surface *surface = SDL_GetSurfaceImage(data->cursor_data.custom.sdl_cursor_surface, (float)scale); - if (!surface) { - SDL_free(cache); - return NULL; - } - - // Allocate shared memory buffer for this cursor - if (!Wayland_AllocSHMBuffer(surface->w, surface->h, &cache->shmBuffer)) { - SDL_free(cache); - SDL_DestroySurface(surface); - return NULL; - } - - // Wayland requires premultiplied alpha for its surfaces. - SDL_PremultiplyAlpha(surface->w, surface->h, - surface->format, surface->pixels, surface->pitch, - SDL_PIXELFORMAT_ARGB8888, cache->shmBuffer.shm_data, surface->w * 4, true); - - cache->dst_width = data->cursor_data.custom.sdl_cursor_surface->w; - cache->dst_height = data->cursor_data.custom.sdl_cursor_surface->h; - cache->scale = scale; - WAYLAND_wl_list_insert(&data->cursor_data.custom.cursor_cache, &cache->node); - SDL_DestroySurface(surface); - - return cache; + return true; } static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y) @@ -504,18 +546,17 @@ static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot return NULL; } cursor->internal = data; - WAYLAND_wl_list_init(&data->cursor_data.custom.cursor_cache); - data->hot_x = hot_x; - data->hot_y = hot_y; + WAYLAND_wl_list_init(&data->cursor_data.custom.scaled_cursor_cache); + data->cursor_data.custom.hot_x = hot_x; + data->cursor_data.custom.hot_y = hot_y; data->surface = wl_compositor_create_surface(wd->compositor); - wl_surface_set_user_data(data->surface, NULL); data->cursor_data.custom.sdl_cursor_surface = surface; ++surface->refcount; // If the cursor has only one size, just prepare it now. if (!SDL_SurfaceHasAlternateImages(surface)) { - Wayland_GetCachedCustomCursor(cursor); + Wayland_CacheScaledCustomCursor(data, 1.0); } } @@ -565,8 +606,8 @@ static void Wayland_FreeCursorData(SDL_CursorData *d) } SDL_free(d->cursor_data.system.frames); } else { - Wayland_CachedCustomCursor *c, *temp; - wl_list_for_each_safe(c, temp, &d->cursor_data.custom.cursor_cache, node) { + Wayland_ScaledCustomCursor *c, *temp; + wl_list_for_each_safe(c, temp, &d->cursor_data.custom.scaled_cursor_cache, node) { Wayland_ReleaseSHMBuffer(&c->shmBuffer); SDL_free(c); } @@ -681,9 +722,12 @@ static bool Wayland_ShowCursor(SDL_Cursor *cursor) SDL_VideoData *d = vd->internal; struct SDL_WaylandInput *input = d->input; struct wl_pointer *pointer = d->pointer; - double scale = 1.0; + struct wl_buffer *buffer = NULL; + int scale = 1; int dst_width = 0; int dst_height = 0; + int hot_x; + int hot_y; if (!pointer) { return false; @@ -700,19 +744,18 @@ static bool Wayland_ShowCursor(SDL_Cursor *cursor) SDL_CursorData *data = cursor->internal; if (data->is_system_cursor) { + // If the cursor shape protocol is supported, the compositor will draw nicely scaled cursors for us, so nothing more to do. if (input->cursor_shape) { Wayland_SetSystemCursorShape(input, data->cursor_data.system.id); input->current_cursor = data; - return true; } - if (!wayland_get_system_cursor(d, data, &scale)) { + if (!Wayland_GetSystemCursor(d, data, &scale, &dst_width, &hot_x, &hot_y)) { return false; } - } - if (data->is_system_cursor) { + dst_height = dst_width; wl_surface_attach(data->surface, data->cursor_data.system.frames[0].wl_buffer, 0, 0); // If more than one frame is available, create a frame callback to run the animation. @@ -724,34 +767,31 @@ static bool Wayland_ShowCursor(SDL_Cursor *cursor) wl_callback_add_listener(data->cursor_data.system.frame_callback, &cursor_frame_listener, data); } } else { - Wayland_CachedCustomCursor *cached = Wayland_GetCachedCustomCursor(cursor); - if (!cached) { + if (!Wayland_GetCustomCursor(cursor, &buffer, &scale, &dst_width, &dst_height, &hot_x, &hot_y)) { return false; } - dst_width = cached->dst_width; - dst_height = cached->dst_height; - scale = cached->scale; - wl_surface_attach(data->surface, cached->shmBuffer.wl_buffer, 0, 0); + + wl_surface_attach(data->surface, buffer, 0, 0); } - // TODO: Make the viewport path the default in all cases once GNOME finally supports viewports on cursor surfaces. - if (SDL_ceil(scale) != scale && d->viewporter) { + // A scale value of 0 indicates that a viewport with the returned destination size should be used. + if (!scale) { if (!data->viewport) { data->viewport = wp_viewporter_get_viewport(d->viewporter, data->surface); } wl_surface_set_buffer_scale(data->surface, 1); wp_viewport_set_source(data->viewport, wl_fixed_from_int(-1), wl_fixed_from_int(-1), wl_fixed_from_int(-1), wl_fixed_from_int(-1)); wp_viewport_set_destination(data->viewport, dst_width, dst_height); - wl_pointer_set_cursor(pointer, input->pointer_enter_serial, data->surface, data->hot_x, data->hot_y); } else { if (data->viewport) { wp_viewport_destroy(data->viewport); data->viewport = NULL; } - wl_surface_set_buffer_scale(data->surface, (int32_t)scale); - wl_pointer_set_cursor(pointer, input->pointer_enter_serial, data->surface, (int32_t)(data->hot_x / scale), (int32_t)(data->hot_y / scale)); + wl_surface_set_buffer_scale(data->surface, scale); } + wl_pointer_set_cursor(pointer, input->pointer_enter_serial, data->surface, hot_x, hot_y); + if (wl_surface_get_version(data->surface) >= WL_SURFACE_DAMAGE_BUFFER_SINCE_VERSION) { wl_surface_damage_buffer(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32); } else {