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.
This commit is contained in:
Frank Praznik
2024-09-23 12:50:28 -04:00
parent e5d3a1b6f5
commit ad3a4c677b

View File

@@ -48,18 +48,16 @@ static bool Wayland_SetRelativeMouseMode(bool enabled);
typedef struct typedef struct
{ {
struct Wayland_SHMBuffer shmBuffer; struct Wayland_SHMBuffer shmBuffer;
int dst_width;
int dst_height;
double scale; double scale;
struct wl_list node; struct wl_list node;
} Wayland_CachedCustomCursor; } Wayland_ScaledCustomCursor;
typedef struct typedef struct
{ {
SDL_Surface *sdl_cursor_surface; SDL_Surface *sdl_cursor_surface;
struct wl_list cursor_cache; int hot_x;
int hot_y;
struct wl_list scaled_cursor_cache;
} Wayland_CustomCursor; } Wayland_CustomCursor;
typedef struct typedef struct
@@ -91,8 +89,6 @@ struct SDL_CursorData
struct wl_surface *surface; struct wl_surface *surface;
struct wp_viewport *viewport; struct wp_viewport *viewport;
int hot_x, hot_y;
bool is_system_cursor; 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); 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_theme *theme = NULL;
struct wl_cursor *cursor; struct wl_cursor *cursor;
const char *css_name = "default"; const char *css_name = "default";
const char *fallback_name = NULL; const char *fallback_name = NULL;
double scale_factor = 1.0;
int size = dbus_cursor_size; int theme_size = dbus_cursor_size;
SDL_Window *focus;
// Fallback envvar if the DBus properties don't exist // Fallback envvar if the DBus properties don't exist
if (size <= 0) { if (theme_size <= 0) {
const char *xcursor_size = SDL_getenv("XCURSOR_SIZE"); const char *xcursor_size = SDL_getenv("XCURSOR_SIZE");
if (xcursor_size) { if (xcursor_size) {
size = SDL_atoi(xcursor_size); theme_size = SDL_atoi(xcursor_size);
} }
} }
if (size <= 0) { if (theme_size <= 0) {
size = 24; theme_size = 24;
} }
// First, find the appropriate theme based on the current scale... // First, find the appropriate theme based on the current scale...
focus = SDL_GetMouse()->focus; SDL_Window *focus = SDL_GetMouse()->focus;
if (focus) { if (focus) {
// TODO: Use the fractional scale once GNOME supports viewports on cursor surfaces. // TODO: Use the fractional scale once GNOME supports viewports on cursor surfaces.
*scale = SDL_ceil(focus->internal->scale_factor); scale_factor = SDL_ceil(focus->internal->scale_factor);
} else {
*scale = 1.0;
} }
size *= (int)*scale; const int scaled_size = (int)SDL_lround(theme_size * scale_factor);
for (int i = 0; i < vdata->num_cursor_themes; ++i) { 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; theme = vdata->cursor_themes[i].theme;
break; break;
} }
@@ -390,8 +382,8 @@ static bool wayland_get_system_cursor(SDL_VideoData *vdata, SDL_CursorData *cdat
xcursor_theme = SDL_getenv("XCURSOR_THEME"); xcursor_theme = SDL_getenv("XCURSOR_THEME");
} }
theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, size, vdata->shm); theme = WAYLAND_wl_cursor_theme_load(xcursor_theme, scaled_size, vdata->shm);
vdata->cursor_themes[vdata->num_cursor_themes].size = size; vdata->cursor_themes[vdata->num_cursor_themes].size = scaled_size;
vdata->cursor_themes[vdata->num_cursor_themes++].theme = theme; 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.frames[i].duration = cursor->images[i]->delay;
cdata->cursor_data.system.total_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; 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_VideoDevice *vd = SDL_GetVideoDevice();
SDL_VideoData *wd = vd->internal; SDL_VideoData *wd = vd->internal;
SDL_CursorData *data = cursor->internal; SDL_CursorData *data = cursor->internal;
Wayland_CachedCustomCursor *cache;
SDL_Window *focus = SDL_GetMouseFocus(); 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)) { 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. // Only use fractional scale values if viewports are available.
if (!wd->viewporter) { if (!wd->viewporter) {
scale = SDL_ceil(scale); scale_factor = SDL_ceil(scale_factor);
} }
// Is this cursor already cached at the target scale? Wayland_ScaledCustomCursor *c = Wayland_CacheScaledCustomCursor(data, scale_factor);
wl_list_for_each(cache, &data->cursor_data.custom.cursor_cache, node) { if (!c) {
if (cache->scale == scale) { return false;
return cache;
}
} }
cache = SDL_calloc(1, sizeof(Wayland_CachedCustomCursor)); *buffer = c->shmBuffer.wl_buffer;
if (!cache) { *scale = SDL_ceil(scale_factor) == scale_factor ? (int)scale_factor : 0;
return NULL; *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); return true;
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;
} }
static SDL_Cursor *Wayland_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y) 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; return NULL;
} }
cursor->internal = data; cursor->internal = data;
WAYLAND_wl_list_init(&data->cursor_data.custom.cursor_cache); WAYLAND_wl_list_init(&data->cursor_data.custom.scaled_cursor_cache);
data->hot_x = hot_x; data->cursor_data.custom.hot_x = hot_x;
data->hot_y = hot_y; data->cursor_data.custom.hot_y = hot_y;
data->surface = wl_compositor_create_surface(wd->compositor); data->surface = wl_compositor_create_surface(wd->compositor);
wl_surface_set_user_data(data->surface, NULL);
data->cursor_data.custom.sdl_cursor_surface = surface; data->cursor_data.custom.sdl_cursor_surface = surface;
++surface->refcount; ++surface->refcount;
// If the cursor has only one size, just prepare it now. // If the cursor has only one size, just prepare it now.
if (!SDL_SurfaceHasAlternateImages(surface)) { 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); SDL_free(d->cursor_data.system.frames);
} else { } else {
Wayland_CachedCustomCursor *c, *temp; Wayland_ScaledCustomCursor *c, *temp;
wl_list_for_each_safe(c, temp, &d->cursor_data.custom.cursor_cache, node) { wl_list_for_each_safe(c, temp, &d->cursor_data.custom.scaled_cursor_cache, node) {
Wayland_ReleaseSHMBuffer(&c->shmBuffer); Wayland_ReleaseSHMBuffer(&c->shmBuffer);
SDL_free(c); SDL_free(c);
} }
@@ -681,9 +722,12 @@ static bool Wayland_ShowCursor(SDL_Cursor *cursor)
SDL_VideoData *d = vd->internal; SDL_VideoData *d = vd->internal;
struct SDL_WaylandInput *input = d->input; struct SDL_WaylandInput *input = d->input;
struct wl_pointer *pointer = d->pointer; struct wl_pointer *pointer = d->pointer;
double scale = 1.0; struct wl_buffer *buffer = NULL;
int scale = 1;
int dst_width = 0; int dst_width = 0;
int dst_height = 0; int dst_height = 0;
int hot_x;
int hot_y;
if (!pointer) { if (!pointer) {
return false; return false;
@@ -700,19 +744,18 @@ static bool Wayland_ShowCursor(SDL_Cursor *cursor)
SDL_CursorData *data = cursor->internal; SDL_CursorData *data = cursor->internal;
if (data->is_system_cursor) { 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) { if (input->cursor_shape) {
Wayland_SetSystemCursorShape(input, data->cursor_data.system.id); Wayland_SetSystemCursorShape(input, data->cursor_data.system.id);
input->current_cursor = data; input->current_cursor = data;
return true; return true;
} }
if (!wayland_get_system_cursor(d, data, &scale)) { if (!Wayland_GetSystemCursor(d, data, &scale, &dst_width, &hot_x, &hot_y)) {
return false; 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); 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. // 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); wl_callback_add_listener(data->cursor_data.system.frame_callback, &cursor_frame_listener, data);
} }
} else { } else {
Wayland_CachedCustomCursor *cached = Wayland_GetCachedCustomCursor(cursor); if (!Wayland_GetCustomCursor(cursor, &buffer, &scale, &dst_width, &dst_height, &hot_x, &hot_y)) {
if (!cached) {
return false; return false;
} }
dst_width = cached->dst_width;
dst_height = cached->dst_height; wl_surface_attach(data->surface, buffer, 0, 0);
scale = cached->scale;
wl_surface_attach(data->surface, cached->shmBuffer.wl_buffer, 0, 0);
} }
// TODO: Make the viewport path the default in all cases once GNOME finally supports viewports on cursor surfaces. // A scale value of 0 indicates that a viewport with the returned destination size should be used.
if (SDL_ceil(scale) != scale && d->viewporter) { if (!scale) {
if (!data->viewport) { if (!data->viewport) {
data->viewport = wp_viewporter_get_viewport(d->viewporter, data->surface); data->viewport = wp_viewporter_get_viewport(d->viewporter, data->surface);
} }
wl_surface_set_buffer_scale(data->surface, 1); 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_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); 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 { } else {
if (data->viewport) { if (data->viewport) {
wp_viewport_destroy(data->viewport); wp_viewport_destroy(data->viewport);
data->viewport = NULL; data->viewport = NULL;
} }
wl_surface_set_buffer_scale(data->surface, (int32_t)scale); wl_surface_set_buffer_scale(data->surface, 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_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) { 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); wl_surface_damage_buffer(data->surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
} else { } else {