From 9961fb80a73e8766bf4a26c5b7ded1440efbc0f3 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 17 Apr 2026 08:56:35 -0400
Subject: [PATCH] wayland: Refactor the cursor event thread into a general
purpose event thread
No functional changes, just tidies the mouse code, and allows for future generalized use of the event handling thread, if required.
---
src/video/wayland/SDL_waylandeventthread.c | 210 +++++++++++++++++++++
src/video/wayland/SDL_waylandeventthread.h | 36 ++++
src/video/wayland/SDL_waylandmouse.c | 188 ++----------------
src/video/wayland/SDL_waylandvideo.c | 11 +-
src/video/wayland/SDL_waylandvideo.h | 2 +
5 files changed, 270 insertions(+), 177 deletions(-)
create mode 100644 src/video/wayland/SDL_waylandeventthread.c
create mode 100644 src/video/wayland/SDL_waylandeventthread.h
diff --git a/src/video/wayland/SDL_waylandeventthread.c b/src/video/wayland/SDL_waylandeventthread.c
new file mode 100644
index 0000000000000..ae8e124d04fe2
--- /dev/null
+++ b/src/video/wayland/SDL_waylandeventthread.c
@@ -0,0 +1,210 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+
+#include "../../core/unix/SDL_poll.h"
+#include "SDL_waylandeventthread.h"
+#include "SDL_waylandvideo.h"
+
+#include <errno.h>
+
+struct Wayland_EventThreadContext
+{
+ SDL_VideoData *wayland_data;
+ SDL_Thread *thread;
+ struct wl_event_queue *queue;
+ SDL_Mutex *dispatch_lock;
+ bool should_exit;
+};
+
+static void handle_event_thread_exit(void *data, struct wl_callback *wl_callback, uint32_t callback_data)
+{
+ Wayland_EventThreadContext *context = (Wayland_EventThreadContext *)data;
+ wl_callback_destroy(wl_callback);
+ context->should_exit = true;
+}
+
+static const struct wl_callback_listener event_thread_exit_listener = {
+ handle_event_thread_exit
+};
+
+static int SDLCALL Wayland_EventThreadFunc(void *data)
+{
+ Wayland_EventThreadContext *context = (Wayland_EventThreadContext *)data;
+ struct wl_display *display = context->wayland_data->display;
+ 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 adding
+ * or destroying objects using the queue.
+ *
+ * Any error other than EAGAIN is fatal and causes the thread to exit.
+ */
+ while (!context->should_exit) {
+ if (WAYLAND_wl_display_prepare_read_queue(display, 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(context->dispatch_lock);
+ ret = WAYLAND_wl_display_dispatch_queue_pending(display, context->queue);
+ SDL_UnlockMutex(context->dispatch_lock);
+
+ if (ret < 0) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+Wayland_EventThreadContext *Wayland_CreateEventThread(SDL_VideoData *data, const char *queue_name)
+{
+ Wayland_EventThreadContext *context = SDL_calloc(1, sizeof(Wayland_EventThreadContext));
+ if (!context) {
+ return NULL;
+ }
+
+ context->wayland_data = data;
+ context->queue = Wayland_DisplayCreateQueue(data->display, queue_name);
+ if (!context->queue) {
+ goto cleanup;
+ }
+
+ context->dispatch_lock = SDL_CreateMutex();
+ if (!context->dispatch_lock) {
+ goto cleanup;
+ }
+
+ context->thread = SDL_CreateThread(Wayland_EventThreadFunc, "wl_event_thread", context);
+ if (!context->thread) {
+ goto cleanup;
+ }
+
+ return context;
+
+cleanup:
+ if (context->dispatch_lock) {
+ SDL_DestroyMutex(context->dispatch_lock);
+ }
+
+ if (context->queue) {
+ WAYLAND_wl_event_queue_destroy(context->queue);
+ }
+
+ SDL_free(context);
+ return NULL;
+}
+
+void Wayland_DestroyEventThread(Wayland_EventThreadContext *context)
+{
+ if (context->thread) {
+ // Dispatch the exit event to unblock the thread and signal it to exit.
+ struct wl_display *display = context->wayland_data->display;
+ struct wl_proxy *display_wrapper = Wayland_CreateEventThreadProxyWrapper(context, display);
+
+ SDL_LockMutex(context->dispatch_lock);
+ struct wl_callback *cb = wl_display_sync((struct wl_display *)display_wrapper);
+ wl_callback_add_listener(cb, &event_thread_exit_listener, context);
+ SDL_UnlockMutex(context->dispatch_lock);
+
+ WAYLAND_wl_proxy_wrapper_destroy(display_wrapper);
+
+ int ret = WAYLAND_wl_display_flush(display);
+ while (ret == -1 && errno == EAGAIN) {
+ // Shutting down the thread requires a successful flush.
+ ret = SDL_IOReady(WAYLAND_wl_display_get_fd(display), SDL_IOR_WRITE, -1);
+ if (ret >= 0) {
+ ret = WAYLAND_wl_display_flush(display);
+ }
+ }
+
+ // Avoid a warning if the flush failed due to a broken connection.
+ if (ret < 0) {
+ wl_callback_destroy(cb);
+ }
+
+ // Wait for the thread to return; it will exit automatically on a broken connection.
+ SDL_WaitThread(context->thread, NULL);
+
+ WAYLAND_wl_event_queue_destroy(context->queue);
+ SDL_DestroyMutex(context->dispatch_lock);
+ SDL_free(context);
+ }
+}
+
+void *Wayland_CreateEventThreadProxyWrapper(Wayland_EventThreadContext *context, void *obj)
+{
+ struct wl_proxy *wrapper = WAYLAND_wl_proxy_create_wrapper(obj);
+ if (wrapper) {
+ WAYLAND_wl_proxy_set_queue(wrapper, context->queue);
+ return wrapper;
+ }
+
+ return NULL;
+}
+
+void Wayland_LockEventThread(Wayland_EventThreadContext *context)
+{
+ if (context) {
+ SDL_LockMutex(context->dispatch_lock);
+ }
+}
+
+void Wayland_UnlockEventThread(Wayland_EventThreadContext *context)
+{
+ if (context) {
+ SDL_UnlockMutex(context->dispatch_lock);
+ }
+}
+
+#endif // SDL_VIDEO_DRIVER_WAYLAND
diff --git a/src/video/wayland/SDL_waylandeventthread.h b/src/video/wayland/SDL_waylandeventthread.h
new file mode 100644
index 0000000000000..81f9f606b4005
--- /dev/null
+++ b/src/video/wayland/SDL_waylandeventthread.h
@@ -0,0 +1,36 @@
+/*
+ Simple DirectMedia Layer
+ Copyright (C) 1997-2026 Sam Lantinga <slouken@libsdl.org>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+#include "SDL_internal.h"
+
+#ifndef SDL_waylandeventthread_h_
+#define SDL_waylandeventthread_h_
+
+#include "../SDL_sysvideo.h"
+
+typedef struct Wayland_EventThreadContext Wayland_EventThreadContext;
+
+extern Wayland_EventThreadContext *Wayland_CreateEventThread(SDL_VideoData *data, const char *queue_name);
+extern void Wayland_DestroyEventThread(Wayland_EventThreadContext *context);
+extern void *Wayland_CreateEventThreadProxyWrapper(Wayland_EventThreadContext *context, void *obj);
+extern void Wayland_LockEventThread(Wayland_EventThreadContext *context);
+extern void Wayland_UnlockEventThread(Wayland_EventThreadContext *context);
+
+#endif // SDL_waylandeventthread_h_
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index 40c05495c98e8..5533ee7133f24 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -324,168 +324,6 @@ static struct wl_buffer *Wayland_CursorStateGetFrame(SDL_WaylandCursorState *sta
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_DisplayCreateQueue(data->display, "SDL Cursor Surface Queue");
- 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);
- while (ret == -1 && errno == EAGAIN) {
- // Shutting down the thread requires a successful flush.
- ret = SDL_IOReady(WAYLAND_wl_display_get_fd(data->display), SDL_IOR_WRITE, -1);
- if (ret >= 0) {
- ret = WAYLAND_wl_display_flush(data->display);
- }
- }
-
- // Avoid a warning if the flush failed due to a broken connection.
- if (ret < 0) {
- wl_callback_destroy(cb);
- }
-
- // Wait for the thread to return; it will exit automatically on a broken connection.
- 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_DestroyMutex(cursor_thread_context.lock);
- 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
@@ -545,24 +383,26 @@ static void cursor_frame_done(void *data, struct wl_callback *cb, uint32_t time)
void Wayland_CursorStateSetFrameCallback(SDL_WaylandCursorState *state, void *userdata)
{
- SDL_LockMutex(cursor_thread_context.lock);
+ SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
+ Wayland_LockEventThread(viddata->event_thread_context);
state->frame_callback = wl_surface_frame(state->surface);
wl_callback_add_listener(state->frame_callback, &cursor_frame_listener, userdata);
- SDL_UnlockMutex(cursor_thread_context.lock);
+ Wayland_UnlockEventThread(viddata->event_thread_context);
}
void Wayland_CursorStateDestroyFrameCallback(SDL_WaylandCursorState *state)
{
- SDL_LockMutex(cursor_thread_context.lock);
+ SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
+ Wayland_LockEventThread(viddata->event_thread_context);
if (state->frame_callback) {
wl_callback_destroy(state->frame_callback);
state->frame_callback = NULL;
}
- SDL_UnlockMutex(cursor_thread_context.lock);
+ Wayland_UnlockEventThread(viddata->event_thread_context);
}
static void Wayland_CursorStateResetAnimationState(SDL_WaylandCursorState *state)
@@ -575,9 +415,10 @@ static void Wayland_CursorStateResetAnimationState(SDL_WaylandCursorState *state
static void Wayland_CursorStateResetAnimation(SDL_WaylandCursorState *state, bool lock)
{
if (lock) {
- SDL_LockMutex(cursor_thread_context.lock);
+ SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
+ Wayland_LockEventThread(viddata->event_thread_context);
Wayland_CursorStateResetAnimationState(state);
- SDL_UnlockMutex(cursor_thread_context.lock);
+ Wayland_UnlockEventThread(viddata->event_thread_context);
} else {
Wayland_CursorStateResetAnimationState(state);
}
@@ -1175,8 +1016,10 @@ static void Wayland_CursorStateSetCursor(SDL_WaylandCursorState *state, const Wa
state->current_cursor = cursor_data;
if (!state->surface) {
- if (cursor_thread_context.compositor_wrapper) {
- state->surface = wl_compositor_create_surface((struct wl_compositor *)cursor_thread_context.compositor_wrapper);
+ if (viddata->event_thread_context) {
+ struct wl_compositor *compositor_wrapper = Wayland_CreateEventThreadProxyWrapper(viddata->event_thread_context, viddata->compositor);
+ state->surface = wl_compositor_create_surface(compositor_wrapper);
+ WAYLAND_wl_proxy_wrapper_destroy(compositor_wrapper);
} else {
state->surface = wl_compositor_create_surface(viddata->compositor);
}
@@ -1541,10 +1384,6 @@ void Wayland_InitMouse(SDL_VideoData *data)
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) {
@@ -1604,7 +1443,6 @@ void Wayland_FiniMouse(SDL_VideoData *data)
sys_cursors[i] = NULL;
}
- Wayland_DestroyCursorThread(data);
Wayland_FreeCursorThemes(data);
#ifdef SDL_USE_LIBDBUS
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 9404947f5ef3d..1c1fef5bcdbc1 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -1684,6 +1684,11 @@ bool Wayland_VideoInit(SDL_VideoDevice *_this)
{
SDL_VideoData *data = _this->internal;
+ data->event_thread_context = Wayland_CreateEventThread(data, "SDL Event Thread Queue");
+ if (!data->event_thread_context) {
+ SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "wayland: Failed to create event thread context");
+ }
+
data->xkb_context = WAYLAND_xkb_context_new(0);
if (!data->xkb_context) {
return SDL_SetError("Failed to create XKB context");
@@ -1787,6 +1792,10 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
}
Wayland_FiniMouse(data);
+ Wayland_QuitKeyboard(_this);
+
+ Wayland_DestroyEventThread(data->event_thread_context);
+ data->event_thread_context = NULL;
if (data->pointer_constraints) {
zwp_pointer_constraints_v1_destroy(data->pointer_constraints);
@@ -1813,8 +1822,6 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
data->key_inhibitor_manager = NULL;
}
- Wayland_QuitKeyboard(_this);
-
if (data->text_input_manager) {
zwp_text_input_manager_v3_destroy(data->text_input_manager);
data->text_input_manager = NULL;
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index f06895aa40a93..17a9ba24fa63a 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -30,6 +30,7 @@
#include "../SDL_sysvideo.h"
#include "../../core/linux/SDL_dbus.h"
#include "../../core/linux/SDL_ime.h"
+#include "SDL_waylandeventthread.h"
struct xkb_context;
struct SDL_WaylandSeat;
@@ -97,6 +98,7 @@ struct SDL_VideoData
struct SDL_WaylandSeat *last_implicit_grab_seat;
struct SDL_WaylandSeat *last_incoming_data_offer_seat;
struct SDL_WaylandSeat *last_incoming_primary_selection_seat;
+ Wayland_EventThreadContext *event_thread_context;
SDL_DisplayData **output_list;
int output_count;