Explorar el Código

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.
Frank Praznik hace 4 meses
padre
commit
35cc58e027

+ 2 - 4
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);
     }

+ 3 - 0
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;

+ 233 - 18
src/video/wayland/SDL_waylandmouse.c

@@ -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

+ 3 - 1
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

+ 1 - 1
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) {