SDL: wayland: Refactor event processing

From bfa89b3d5f262d240ddfa0a788b41452b6a678c4 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Thu, 16 Oct 2025 21:31:28 -0400
Subject: [PATCH] wayland: Refactor event processing

Clean up and implement some best practices for event polling and handling:

- Ensure that events are still pumped, even if existing events are in the queue, to ensure that the most recent data is always processed.
- Handle wl_display_flush returning EAGAIN by trying to poll the socket until it can be written to.
- Ensure that events are always pumped on interrupts and broken pipe errors to handle quit events.
- Minimize double-pumping of events, since returning a success code from a wait will implicitly pump events again.
- Wake to poll when using a DBus IME, but the text input protocol is not enabled, to ensure that text events are delivered.
---
 src/video/wayland/SDL_waylandevents.c | 208 ++++++++++++++------------
 1 file changed, 112 insertions(+), 96 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 695af204a8258..5d4bf4b04bb9d 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -495,117 +495,102 @@ void Wayland_SendWakeupEvent(SDL_VideoDevice *_this, SDL_Window *window)
     WAYLAND_wl_display_flush(d->display);
 }
 
-static int dispatch_queued_events(SDL_VideoData *viddata)
-{
-    int rc;
-
-    /*
-     * NOTE: When reconnection is implemented, check if libdecor needs to be
-     *       involved in the reconnection process.
-     */
-#ifdef HAVE_LIBDECOR_H
-    if (viddata->shell.libdecor) {
-        libdecor_dispatch(viddata->shell.libdecor, 0);
-    }
-#endif
-
-    rc = WAYLAND_wl_display_dispatch_pending(viddata->display);
-    return rc >= 0 ? 1 : rc;
-}
-
 int Wayland_WaitEventTimeout(SDL_VideoDevice *_this, Sint64 timeoutNS)
 {
     SDL_VideoData *d = _this->internal;
     SDL_WaylandSeat *seat;
-    bool key_repeat_active = false;
-
-    WAYLAND_wl_display_flush(d->display);
+    Uint64 start = SDL_GetTicksNS();
+    const int display_fd = WAYLAND_wl_display_get_fd(d->display);
+    int ret;
+    bool poll_alarm_set = false;
 
 #ifdef SDL_USE_IME
     SDL_Window *keyboard_focus = SDL_GetKeyboardFocus();
     if (!d->text_input_manager && keyboard_focus && SDL_TextInputActive(keyboard_focus)) {
-        SDL_IME_PumpEvents();
+        // If a DBus IME is active with no text input protocol, periodically wake to poll it.
+        if (timeoutNS < 0 || SDL_MS_TO_NS(200) <= timeoutNS) {
+            timeoutNS = SDL_MS_TO_NS(200);
+            poll_alarm_set = true;
+        }
     }
 #endif
 
-#ifdef SDL_USE_LIBDBUS
-    SDL_DBus_PumpEvents();
-#endif
-
     // If key repeat is active, we'll need to cap our maximum wait time to handle repeats
     wl_list_for_each (seat, &d->seat_list, link) {
         if (keyboard_repeat_is_set(&seat->keyboard.repeat)) {
-            Wayland_SeatSetKeymap(seat);
-
-            const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns;
-            if (keyboard_repeat_handle(&seat->keyboard.repeat, elapsed)) {
-                // A repeat key event was already due
-                return 1;
-            } else {
-                const Uint64 next_repeat_wait_time = (seat->keyboard.repeat.next_repeat_ns - elapsed) + 1;
-                if (timeoutNS >= 0) {
-                    timeoutNS = SDL_min(timeoutNS, next_repeat_wait_time);
-                } else {
-                    timeoutNS = next_repeat_wait_time;
-                }
-                key_repeat_active = true;
+            const Uint64 elapsed = start - seat->keyboard.repeat.sdl_press_time_ns;
+            const Uint64 next_repeat_wait_time = (seat->keyboard.repeat.next_repeat_ns - elapsed) + 1;
+            if (timeoutNS < 0 || next_repeat_wait_time <= timeoutNS) {
+                timeoutNS = next_repeat_wait_time;
+                poll_alarm_set = true;
             }
         }
     }
 
-    /* wl_display_prepare_read() will return -1 if the default queue is not empty.
-     * If the default queue is empty, it will prepare us for our SDL_IOReady() call. */
     if (WAYLAND_wl_display_prepare_read(d->display) == 0) {
-        // Use SDL_IOR_NO_RETRY to ensure SIGINT will break us out of our wait
-        int err = SDL_IOReady(WAYLAND_wl_display_get_fd(d->display), SDL_IOR_READ | SDL_IOR_NO_RETRY, timeoutNS);
-        if (err > 0) {
-            // There are new events available to read
-            WAYLAND_wl_display_read_events(d->display);
-            return dispatch_queued_events(d);
-        } else if (err == 0) {
-            int ret = 0;
-
-            // No events available within the timeout
-            WAYLAND_wl_display_cancel_read(d->display);
+        if (timeoutNS > 0) {
+            const Uint64 now = SDL_GetTicksNS();
+            const Uint64 elapsed = now - start;
+            start = now;
+            timeoutNS = elapsed <= timeoutNS ? timeoutNS - elapsed : 0;
+        }
 
-            // If key repeat is active, we might have woken up to generate a key event
-            if (key_repeat_active) {
-                wl_list_for_each (seat, &d->seat_list, link) {
-                    if (keyboard_repeat_is_set(&seat->keyboard.repeat)) {
-                        Wayland_SeatSetKeymap(seat);
+        ret = WAYLAND_wl_display_flush(d->display);
 
-                        const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns;
-                        if (keyboard_repeat_handle(&seat->keyboard.repeat, elapsed)) {
-                            ++ret;
-                        }
-                    }
-                }
+        if (ret == -1 && errno == EAGAIN) {
+            // Unable to write to the socket; poll until the socket can be written to, it times out, or is interrupted.
+            ret = SDL_IOReady(display_fd, SDL_IOR_WRITE | SDL_IOR_NO_RETRY, timeoutNS);
+
+            if (ret <= 0) {
+                // The poll operation timed out or experienced an error, so see if there are any events to read without waiting.
+                timeoutNS = 0;
             }
+        }
 
-            return ret;
-        } else {
-            // Error returned from poll()/select()
+        if (ret < 0) {
+            // Pump events on an interrupt or broken pipe to handle the error.
+            WAYLAND_wl_display_cancel_read(d->display);
+            return errno == EINTR || errno == EPIPE ? 1 : ret;
+        }
+
+        if (timeoutNS > 0) {
+            const Uint64 now = SDL_GetTicksNS();
+            const Uint64 elapsed = now - start;
+            start = now;
+            timeoutNS = elapsed <= timeoutNS ? timeoutNS - elapsed : 0;
+        }
+
+        // Use SDL_IOR_NO_RETRY to catch EINTR.
+        ret = SDL_IOReady(display_fd, SDL_IOR_READ | SDL_IOR_NO_RETRY, timeoutNS);
+        if (ret <= 0) {
+            // Timeout or error, cancel the read.
             WAYLAND_wl_display_cancel_read(d->display);
 
-            if (errno == EINTR) {
-                /* If the wait was interrupted by a signal, we may have generated a
-                 * SDL_EVENT_QUIT event. Let the caller know to call SDL_PumpEvents(). */
-                return 1;
+            // The poll timed out with no data to read, but signal the caller to pump events if polling is required.
+            if (ret == 0) {
+                return poll_alarm_set ? 1 : 0;
             } else {
-                return err;
+                // Pump events on an interrupt or broken pipe to handle the error.
+                return errno == EINTR || errno == EPIPE ? 1 : ret;
             }
         }
-    } else {
-        // We already had pending events
-        return dispatch_queued_events(d);
+
+        ret = WAYLAND_wl_display_read_events(d->display);
+        if (ret == -1) {
+            return ret;
+        }
     }
+
+    // Signal to the caller that there might be an event available.
+    return 1;
 }
 
 void Wayland_PumpEvents(SDL_VideoDevice *_this)
 {
     SDL_VideoData *d = _this->internal;
     SDL_WaylandSeat *seat;
-    int err;
+    const int display_fd = WAYLAND_wl_display_get_fd(d->display);
+    int ret = 0;
 
 #ifdef SDL_USE_IME
     SDL_Window *keyboard_focus = SDL_GetKeyboardFocus();
@@ -618,37 +603,70 @@ void Wayland_PumpEvents(SDL_VideoDevice *_this)
     SDL_DBus_PumpEvents();
 #endif
 
+    // Synthesize key repeat events.
+    wl_list_for_each (seat, &d->seat_list, link) {
+        if (keyboard_repeat_is_set(&seat->keyboard.repeat)) {
+            Wayland_SeatSetKeymap(seat);
+
+            const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns;
+            keyboard_repeat_handle(&seat->keyboard.repeat, elapsed);
+        }
+    }
+
 #ifdef HAVE_LIBDECOR_H
     if (d->shell.libdecor) {
         libdecor_dispatch(d->shell.libdecor, 0);
     }
 #endif
 
-    WAYLAND_wl_display_flush(d->display);
-
-    /* wl_display_prepare_read() will return -1 if the default queue is not empty.
-     * If the default queue is empty, it will prepare us for our SDL_IOReady() call. */
-    if (WAYLAND_wl_display_prepare_read(d->display) == 0) {
-        if (SDL_IOReady(WAYLAND_wl_display_get_fd(d->display), SDL_IOR_READ, 0) > 0) {
-            WAYLAND_wl_display_read_events(d->display);
-        } else {
-            WAYLAND_wl_display_cancel_read(d->display);
+    /* If the queue isn't empty, dispatch any old events, and try to prepare for reading again.
+     * If preparing to read returns -1 on the second try, wl_display_read_events() enqueued new
+     * events at some point between dispatching the old events and preparing for the read,
+     * probably from another thread, which means that the events in the queue are current.
+     */
+    ret = WAYLAND_wl_display_prepare_read(d->display);
+    if (ret == -1) {
+        ret = WAYLAND_wl_display_dispatch_pending(d->display);
+        if (ret < 0) {
+            goto connection_error;
         }
+
+        ret = WAYLAND_wl_display_prepare_read(d->display);
     }
 
-    // Dispatch any pre-existing pending events or new events we may have read
-    err = WAYLAND_wl_display_dispatch_pending(d->display);
+    if (ret == 0) {
+        ret = WAYLAND_wl_display_flush(d->display);
 
-    wl_list_for_each (seat, &d->seat_list, link) {
-        if (keyboard_repeat_is_set(&seat->keyboard.repeat)) {
-            Wayland_SeatSetKeymap(seat);
+        if (ret == -1 && errno == EAGAIN) {
+            // Unable to write to the socket; wait a brief time to see if it becomes writable.
+            ret = SDL_IOReady(display_fd, SDL_IOR_WRITE, SDL_MS_TO_NS(4));
+            if (ret > 0) {
+                ret = WAYLAND_wl_display_flush(d->display);
+            }
+        }
 
-            const Uint64 elapsed = SDL_GetTicksNS() - seat->keyboard.repeat.sdl_press_time_ns;
-            keyboard_repeat_handle(&seat->keyboard.repeat, elapsed);
+        // If the compositor closed the socket, just jump to the error handler.
+        if (ret < 0 && errno == EPIPE) {
+            WAYLAND_wl_display_cancel_read(d->display);
+            goto connection_error;
         }
+
+        ret = SDL_IOReady(display_fd, SDL_IOR_READ, 0);
+        if (ret > 0) {
+            ret = WAYLAND_wl_display_read_events(d->display);
+            if (ret == 0) {
+                ret = WAYLAND_wl_display_dispatch_pending(d->display);
+            }
+        } else {
+            WAYLAND_wl_display_cancel_read(d->display);
+        }
+
+    } else {
+        ret = WAYLAND_wl_display_dispatch_pending(d->display);
     }
 
-    if (err < 0 && !d->display_disconnected) {
+connection_error:
+    if (ret < 0 && !d->display_disconnected) {
         /* Something has failed with the Wayland connection -- for example,
          * the compositor may have shut down and closed its end of the socket,
          * or there is a library-specific error.
@@ -659,9 +677,7 @@ void Wayland_PumpEvents(SDL_VideoDevice *_this)
             d->display_disconnected = 1;
             SDL_LogError(SDL_LOG_CATEGORY_VIDEO, "Wayland display connection closed by server (fatal)");
 
-            /* Only send a single quit message, as application shutdown might call
-             * SDL_PumpEvents
-             */
+            // Only send a single quit message, as application shutdown might call SDL_PumpEvents().
             SDL_SendQuit();
         }
     }