From 35cc58e0277f06d414f407f0b747178ba4ede548 Mon Sep 17 00:00:00 2001 From: Frank Praznik Date: Thu, 16 Oct 2025 14:56:52 -0400 Subject: [PATCH] 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. --- src/video/wayland/SDL_waylandevents.c | 6 +- src/video/wayland/SDL_waylandevents_c.h | 3 + src/video/wayland/SDL_waylandmouse.c | 251 ++++++++++++++++++++++-- src/video/wayland/SDL_waylandmouse.h | 4 +- src/video/wayland/SDL_waylandvideo.c | 2 +- 5 files changed, 242 insertions(+), 24 deletions(-) diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c index 08c012a4f3..5378ddf279 100644 --- a/src/video/wayland/SDL_waylandevents.c +++ b/src/video/wayland/SDL_waylandevents.c @@ -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); } diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h index 077c2659f4..a178d129f4 100644 --- a/src/video/wayland/SDL_waylandevents_c.h +++ b/src/video/wayland/SDL_waylandevents_c.h @@ -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; diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c index b40058cbe7..eedfc5d9e8 100644 --- a/src/video/wayland/SDL_waylandmouse.c +++ b/src/video/wayland/SDL_waylandmouse.c @@ -23,9 +23,12 @@ #ifdef SDL_VIDEO_DRIVER_WAYLAND +#include + #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 diff --git a/src/video/wayland/SDL_waylandmouse.h b/src/video/wayland/SDL_waylandmouse.h index 82f877c1e5..537b4bd33f 100644 --- a/src/video/wayland/SDL_waylandmouse.h +++ b/src/video/wayland/SDL_waylandmouse.h @@ -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 diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c index cd81e327b6..4d37c497bb 100644 --- a/src/video/wayland/SDL_waylandvideo.c +++ b/src/video/wayland/SDL_waylandvideo.c @@ -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) {