wayland: Run cursor animations on a thread

If the main event handling thread runs slowly, so will cursor animations. Use a dedicated thread for cursor surface events, so that animations will always run at a consistent rate.
This commit is contained in:
Frank Praznik
2025-10-16 14:56:52 -04:00
parent 776d11a9c8
commit 35cc58e027
5 changed files with 242 additions and 24 deletions

View File

@@ -2356,6 +2356,8 @@ static const struct wl_keyboard_listener keyboard_listener = {
static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
{
Wayland_SeatDestroyCursorFrameCallback(seat);
// End any active gestures.
if (seat->pointer.gesture_focus) {
SDL_SendPinch(SDL_EVENT_PINCH_END, 0, seat->pointer.gesture_focus->sdlwindow, 0.0f);
@@ -2388,10 +2390,6 @@ static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat, bool send_event)
zwp_pointer_gesture_pinch_v1_destroy(seat->pointer.gesture_pinch);
}
if (seat->pointer.cursor_state.frame_callback) {
wl_callback_destroy(seat->pointer.cursor_state.frame_callback);
}
if (seat->pointer.cursor_state.surface) {
wl_surface_destroy(seat->pointer.cursor_state.surface);
}

View File

@@ -184,7 +184,10 @@ typedef struct SDL_WaylandSeat
// Animation state for cursors
void *cursor_handle;
// The cursor animation thread lock must be held when modifying this.
struct wl_callback *frame_callback;
Uint64 last_frame_callback_time_ms;
Uint32 current_frame_time_ms;
int current_frame;

View File

@@ -23,9 +23,12 @@
#ifdef SDL_VIDEO_DRIVER_WAYLAND
#include <errno.h>
#include "../SDL_sysvideo.h"
#include "../SDL_video_c.h"
#include "../../core/unix/SDL_poll.h"
#include "../../events/SDL_mouse_c.h"
#include "SDL_waylandvideo.h"
#include "../SDL_pixels_c.h"
@@ -305,6 +308,164 @@ static struct wl_buffer *Wayland_SeatGetCursorFrame(SDL_WaylandSeat *seat, int f
return NULL;
}
static struct CursorThreadContext
{
SDL_Thread *thread;
struct wl_event_queue *queue;
struct wl_proxy *compositor_wrapper;
SDL_Mutex *lock;
bool should_exit;
} cursor_thread_context;
static void handle_cursor_thread_exit(void *data, struct wl_callback *wl_callback, uint32_t callback_data)
{
wl_callback_destroy(wl_callback);
cursor_thread_context.should_exit = true;
}
static const struct wl_callback_listener cursor_thread_exit_listener = {
handle_cursor_thread_exit
};
static int SDLCALL Wayland_CursorThreadFunc(void *data)
{
struct wl_display *display = data;
const int display_fd = WAYLAND_wl_display_get_fd(display);
int ret;
/* The lock must be held whenever dispatching to avoid a race condition when setting
* or destroying cursor frame callbacks, as adding the callback followed by setting
* the listener is not an atomic operation, and the callback proxy must not be
* destroyed while in the callback handler.
*
* Any error other than EAGAIN is fatal and causes the thread to exit.
*/
while (!cursor_thread_context.should_exit) {
if (WAYLAND_wl_display_prepare_read_queue(display, cursor_thread_context.queue) == 0) {
Sint64 timeoutNS = -1;
ret = WAYLAND_wl_display_flush(display);
if (ret < 0) {
if (errno == EAGAIN) {
// If the flush failed with EAGAIN, don't block as not to inhibit other threads from reading events.
timeoutNS = SDL_MS_TO_NS(1);
} else {
WAYLAND_wl_display_cancel_read(display);
return -1;
}
}
// Wait for a read/write operation to become possible.
ret = SDL_IOReady(display_fd, SDL_IOR_READ, timeoutNS);
if (ret <= 0) {
WAYLAND_wl_display_cancel_read(display);
if (ret < 0) {
return -1;
}
// Nothing to read, and woke to flush; try again.
continue;
}
ret = WAYLAND_wl_display_read_events(display);
if (ret == -1) {
return -1;
}
}
SDL_LockMutex(cursor_thread_context.lock);
ret = WAYLAND_wl_display_dispatch_queue_pending(display, cursor_thread_context.queue);
SDL_UnlockMutex(cursor_thread_context.lock);
if (ret < 0) {
return -1;
}
}
return 0;
}
static bool Wayland_StartCursorThread(SDL_VideoData *data)
{
if (!cursor_thread_context.thread) {
cursor_thread_context.queue = WAYLAND_wl_display_create_queue(data->display);
if (!cursor_thread_context.queue) {
goto cleanup;
}
cursor_thread_context.compositor_wrapper = WAYLAND_wl_proxy_create_wrapper(data->compositor);
if (!cursor_thread_context.compositor_wrapper) {
goto cleanup;
}
WAYLAND_wl_proxy_set_queue(cursor_thread_context.compositor_wrapper, cursor_thread_context.queue);
cursor_thread_context.lock = SDL_CreateMutex();
if (!cursor_thread_context.lock) {
goto cleanup;
}
cursor_thread_context.thread = SDL_CreateThread(Wayland_CursorThreadFunc, "wl_cursor_surface", data->display);
if (!cursor_thread_context.thread) {
goto cleanup;
}
return true;
}
cleanup:
if (cursor_thread_context.lock) {
SDL_DestroyMutex(cursor_thread_context.lock);
}
if (cursor_thread_context.compositor_wrapper) {
WAYLAND_wl_proxy_wrapper_destroy(cursor_thread_context.compositor_wrapper);
}
if (cursor_thread_context.queue) {
WAYLAND_wl_event_queue_destroy(cursor_thread_context.queue);
}
SDL_zero(cursor_thread_context);
return false;
}
static void Wayland_DestroyCursorThread(SDL_VideoData *data)
{
if (cursor_thread_context.thread) {
// Dispatch the exit event to unblock the animation thread and signal it to exit.
struct wl_proxy *display_wrapper = WAYLAND_wl_proxy_create_wrapper(data->display);
WAYLAND_wl_proxy_set_queue(display_wrapper, cursor_thread_context.queue);
SDL_LockMutex(cursor_thread_context.lock);
struct wl_callback *cb = wl_display_sync((struct wl_display *)display_wrapper);
wl_callback_add_listener(cb, &cursor_thread_exit_listener, NULL);
SDL_UnlockMutex(cursor_thread_context.lock);
WAYLAND_wl_proxy_wrapper_destroy(display_wrapper);
int ret = WAYLAND_wl_display_flush(data->display);
if (ret == -1 && errno == EAGAIN) {
// The timeout is long, but shutting down the thread requires a successful flush.
ret = SDL_IOReady(WAYLAND_wl_display_get_fd(data->display), SDL_IOR_WRITE, SDL_MS_TO_NS(1000));
if (ret >= 0) {
ret = WAYLAND_wl_display_flush(data->display);
}
}
// Wait for the thread to return. Don't wait if the flush failed, or this can hang.
if (ret >= 0) {
SDL_WaitThread(cursor_thread_context.thread, NULL);
}
WAYLAND_wl_proxy_wrapper_destroy(cursor_thread_context.compositor_wrapper);
WAYLAND_wl_event_queue_destroy(cursor_thread_context.queue);
SDL_zero(cursor_thread_context);
}
}
static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time);
static const struct wl_callback_listener cursor_frame_listener = {
cursor_frame_done
@@ -362,6 +523,51 @@ static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
wl_surface_commit(seat->pointer.cursor_state.surface);
}
void Wayland_SeatSetCursorFrameCallback(SDL_WaylandSeat *seat)
{
if (cursor_thread_context.lock) {
SDL_LockMutex(cursor_thread_context.lock);
}
seat->pointer.cursor_state.frame_callback = wl_surface_frame(seat->pointer.cursor_state.surface);
wl_callback_add_listener(seat->pointer.cursor_state.frame_callback, &cursor_frame_listener, seat);
if (cursor_thread_context.lock) {
SDL_UnlockMutex(cursor_thread_context.lock);
}
}
void Wayland_SeatDestroyCursorFrameCallback(SDL_WaylandSeat *seat)
{
if (cursor_thread_context.lock) {
SDL_LockMutex(cursor_thread_context.lock);
}
if (seat->pointer.cursor_state.frame_callback) {
wl_callback_destroy(seat->pointer.cursor_state.frame_callback);
seat->pointer.cursor_state.frame_callback = NULL;
}
if (cursor_thread_context.lock) {
SDL_UnlockMutex(cursor_thread_context.lock);
}
}
static void Wayland_SeatResetCursorAnimation(SDL_WaylandSeat *seat, bool lock)
{
if (lock && cursor_thread_context.lock) {
SDL_LockMutex(cursor_thread_context.lock);
}
seat->pointer.cursor_state.last_frame_callback_time_ms = SDL_GetTicks();
seat->pointer.cursor_state.current_frame_time_ms = 0;
seat->pointer.cursor_state.current_frame = 0;
if (lock && cursor_thread_context.lock) {
SDL_UnlockMutex(cursor_thread_context.lock);
}
}
static Wayland_CachedSystemCursor *Wayland_CacheSystemCursor(SDL_CursorData *cdata, struct wl_cursor *cursor, int size)
{
Wayland_CachedSystemCursor *cache = NULL;
@@ -718,10 +924,8 @@ static void Wayland_FreeCursorData(SDL_CursorData *d)
wl_list_for_each (seat, &video_data->seat_list, link)
{
if (seat->pointer.current_cursor == d) {
if (seat->pointer.cursor_state.frame_callback) {
wl_callback_destroy(seat->pointer.cursor_state.frame_callback);
seat->pointer.cursor_state.frame_callback = NULL;
}
Wayland_SeatDestroyCursorFrameCallback(seat);
if (seat->pointer.cursor_state.surface) {
wl_surface_attach(seat->pointer.cursor_state.surface, NULL, 0, 0);
}
@@ -851,13 +1055,17 @@ static void Wayland_SeatSetCursor(SDL_WaylandSeat *seat, SDL_Cursor *cursor)
int hot_y;
// Stop the frame callback for old animated cursors.
if (seat->pointer.cursor_state.frame_callback && cursor_data != seat->pointer.current_cursor) {
wl_callback_destroy(seat->pointer.cursor_state.frame_callback);
seat->pointer.cursor_state.frame_callback = NULL;
if (cursor_data != seat->pointer.current_cursor) {
Wayland_SeatDestroyCursorFrameCallback(seat);
}
if (cursor) {
if (cursor_data == seat->pointer.current_cursor) {
// Restart the animation sequence if the cursor didn't change.
if (cursor_data->num_frames > 1) {
Wayland_SeatResetCursorAnimation(seat, true);
}
return;
}
@@ -895,21 +1103,16 @@ static void Wayland_SeatSetCursor(SDL_WaylandSeat *seat, SDL_Cursor *cursor)
seat->pointer.current_cursor = cursor_data;
if (!seat->pointer.cursor_state.surface) {
seat->pointer.cursor_state.surface = wl_compositor_create_surface(seat->display->compositor);
if (cursor_thread_context.compositor_wrapper) {
seat->pointer.cursor_state.surface = wl_compositor_create_surface((struct wl_compositor *)cursor_thread_context.compositor_wrapper);
} else {
seat->pointer.cursor_state.surface = wl_compositor_create_surface(seat->display->compositor);
}
}
struct wl_buffer *buffer = Wayland_SeatGetCursorFrame(seat, 0);
wl_surface_attach(seat->pointer.cursor_state.surface, buffer, 0, 0);
// If more than one frame is available, create a frame callback to run the animation.
if (cursor_data->num_frames > 1) {
seat->pointer.cursor_state.last_frame_callback_time_ms = SDL_GetTicks();
seat->pointer.cursor_state.current_frame_time_ms = 0;
seat->pointer.cursor_state.current_frame = 0;
seat->pointer.cursor_state.frame_callback = wl_surface_frame(seat->pointer.cursor_state.surface);
wl_callback_add_listener(seat->pointer.cursor_state.frame_callback, &cursor_frame_listener, seat);
}
// A scale value of 0 indicates that a viewport with the returned destination size should be used.
if (!scale) {
if (!seat->pointer.cursor_state.viewport) {
@@ -934,8 +1137,15 @@ static void Wayland_SeatSetCursor(SDL_WaylandSeat *seat, SDL_Cursor *cursor)
wl_surface_damage(seat->pointer.cursor_state.surface, 0, 0, SDL_MAX_SINT32, SDL_MAX_SINT32);
}
// If more than one frame is available, create a frame callback to run the animation.
if (cursor_data->num_frames > 1) {
Wayland_SeatResetCursorAnimation(seat, false);
Wayland_SeatSetCursorFrameCallback(seat);
}
wl_surface_commit(seat->pointer.cursor_state.surface);
} else {
Wayland_SeatDestroyCursorFrameCallback(seat);
seat->pointer.current_cursor = NULL;
wl_pointer_set_cursor(seat->pointer.wl_pointer, seat->pointer.enter_serial, NULL, 0, 0);
}
@@ -1180,7 +1390,7 @@ void Wayland_RecreateCursors(void)
}
#endif // 0
void Wayland_InitMouse(void)
void Wayland_InitMouse(SDL_VideoData *data)
{
SDL_Mouse *mouse = SDL_GetMouse();
@@ -1194,6 +1404,10 @@ void Wayland_InitMouse(void)
mouse->SetRelativeMouseMode = Wayland_SetRelativeMouseMode;
mouse->GetGlobalMouseState = Wayland_GetGlobalMouseState;
if (!Wayland_StartCursorThread(data)) {
SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "wayland: Failed to start cursor animation event thread");
}
SDL_HitTestResult r = SDL_HITTEST_NORMAL;
while (r <= SDL_HITTEST_RESIZE_LEFT) {
switch (r) {
@@ -1248,6 +1462,7 @@ void Wayland_InitMouse(void)
void Wayland_FiniMouse(SDL_VideoData *data)
{
Wayland_DestroyCursorThread(data);
Wayland_FreeCursorThemes(data);
#ifdef SDL_USE_LIBDBUS

View File

@@ -24,10 +24,12 @@
#ifndef SDL_waylandmouse_h_
#define SDL_waylandmouse_h_
extern void Wayland_InitMouse(void);
extern void Wayland_InitMouse(SDL_VideoData *data);
extern void Wayland_FiniMouse(SDL_VideoData *data);
extern void Wayland_SeatUpdateCursor(SDL_WaylandSeat *seat);
extern void Wayland_SeatWarpMouse(SDL_WaylandSeat *seat, SDL_WindowData *window, float x, float y);
extern void Wayland_SeatSetCursorFrameCallback(SDL_WaylandSeat *seat);
extern void Wayland_SeatDestroyCursorFrameCallback(SDL_WaylandSeat *seat);
#if 0 // TODO RECONNECT: See waylandvideo.c for more information!
extern void Wayland_RecreateCursors(void);
#endif // 0

View File

@@ -1452,7 +1452,7 @@ bool Wayland_VideoInit(SDL_VideoDevice *_this)
Wayland_FinalizeDisplays(data);
Wayland_InitMouse();
Wayland_InitMouse(data);
Wayland_InitKeyboard(_this);
if (data->primary_selection_device_manager) {