SDL: wayland: Implement WaitEventTimeout() and SendWakeupEvent()

From 2bf36bfac4aeccfa38cf57b3c7ee0b9a963b5220 Mon Sep 17 00:00:00 2001
From: Cameron Gutman <[EMAIL REDACTED]>
Date: Sun, 24 Oct 2021 21:28:04 -0500
Subject: [PATCH] wayland: Implement WaitEventTimeout() and SendWakeupEvent()

We can have spurious wakeups in WaitEventTimeout() due to Wayland events
that don't end up causing us to generate an SDL event. Fortunately for us,
SDL_WaitEventTimeout_Device() handles this situation properly by calling
WaitEventTimeout() again with an adjusted timeout.
---
 src/video/wayland/SDL_waylandevents.c   | 80 ++++++++++++++++++++++++-
 src/video/wayland/SDL_waylandevents_c.h |  2 +
 src/video/wayland/SDL_waylandvideo.c    |  6 ++
 3 files changed, 85 insertions(+), 3 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 7a03039f29..9a17b0ae83 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -169,12 +169,13 @@ touch_surface(SDL_TouchID id)
     return NULL;
 }
 
-/* Returns the time till next repeat, or 0 if no key is down. */
-static void
+/* Returns SDL_TRUE if a key repeat event was due */
+static SDL_bool
 keyboard_repeat_handle(SDL_WaylandKeyboardRepeat* repeat_info, uint32_t now)
 {
+    SDL_bool ret = SDL_FALSE;
     if (!repeat_info->is_key_down || !repeat_info->is_initialized) {
-        return;
+        return ret;
     }
     while (repeat_info->next_repeat_ms <= now) {
         if (repeat_info->scancode != SDL_SCANCODE_UNKNOWN) {
@@ -184,7 +185,9 @@ keyboard_repeat_handle(SDL_WaylandKeyboardRepeat* repeat_info, uint32_t now)
             SDL_SendKeyboardText(repeat_info->text);
         }
         repeat_info->next_repeat_ms += 1000 / repeat_info->repeat_rate;
+        ret = SDL_TRUE;
     }
+    return ret;
 }
 
 static void
@@ -211,6 +214,77 @@ keyboard_repeat_set(SDL_WaylandKeyboardRepeat* repeat_info,
     }
 }
 
+void
+Wayland_SendWakeupEvent(_THIS, SDL_Window *window)
+{
+    SDL_VideoData *d = _this->driverdata;
+
+    /* TODO: Maybe use a pipe to avoid the compositor round trip? */
+    wl_display_sync(d->display);
+    WAYLAND_wl_display_flush(d->display);
+}
+
+int
+Wayland_WaitEventTimeout(_THIS, int timeout)
+{
+    SDL_VideoData *d = _this->driverdata;
+    struct SDL_WaylandInput *input = d->input;
+    SDL_bool key_repeat_active = SDL_FALSE;
+
+    WAYLAND_wl_display_flush(d->display);
+
+#ifdef SDL_USE_IME
+    if (d->text_input_manager == NULL && SDL_GetEventState(SDL_TEXTINPUT) == SDL_ENABLE) {
+        SDL_IME_PumpEvents();
+    }
+#endif
+
+    /* If key repeat is active, we'll need to cap our maximum wait time to handle repeats */
+    if (input && input->keyboard_repeat.is_initialized && input->keyboard_repeat.is_key_down) {
+        uint32_t now = SDL_GetTicks();
+        if (keyboard_repeat_handle(&input->keyboard_repeat, now)) {
+            /* A repeat key event was already due */
+            return 1;
+        } else {
+            uint32_t next_repeat_wait_time = (input->keyboard_repeat.next_repeat_ms - now) + 1;
+            if (timeout >= 0) {
+                timeout = SDL_min(timeout, next_repeat_wait_time);
+            } else {
+                timeout = next_repeat_wait_time;
+            }
+            key_repeat_active = SDL_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) {
+        if (SDL_IOReady(WAYLAND_wl_display_get_fd(d->display), SDL_FALSE, timeout) > 0) {
+            /* There are new events available to read */
+            WAYLAND_wl_display_read_events(d->display);
+            WAYLAND_wl_display_dispatch_pending(d->display);
+            return 1;
+        } else {
+            /* No events available within the timeout */
+            WAYLAND_wl_display_cancel_read(d->display);
+
+            /* If key repeat is active, we might have woken up to generate a key event */
+            if (key_repeat_active) {
+                uint32_t now = SDL_GetTicks();
+                if (keyboard_repeat_handle(&input->keyboard_repeat, now)) {
+                    return 1;
+                }
+            }
+
+            return 0;
+        }
+    } else {
+        /* We already had pending events */
+        WAYLAND_wl_display_dispatch_pending(d->display);
+        return 1;
+    }
+}
+
 void
 Wayland_PumpEvents(_THIS)
 {
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index e4b76d830e..699d2b3779 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -83,6 +83,8 @@ struct SDL_WaylandInput {
 };
 
 extern void Wayland_PumpEvents(_THIS);
+extern void Wayland_SendWakeupEvent(_THIS, SDL_Window *window);
+extern int Wayland_WaitEventTimeout(_THIS, int timeout);
 
 extern void Wayland_add_data_device_manager(SDL_VideoData *d, uint32_t id, uint32_t version);
 extern void Wayland_add_text_input_manager(SDL_VideoData *d, uint32_t id, uint32_t version);
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 03464d1938..71f6fa8866 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -169,6 +169,9 @@ Wayland_DeleteDevice(SDL_VideoDevice *device)
         WAYLAND_wl_display_flush(data->display);
         WAYLAND_wl_display_disconnect(data->display);
     }
+    if (device->wakeup_lock) {
+        SDL_DestroyMutex(device->wakeup_lock);
+    }
     SDL_free(data);
     SDL_free(device);
     SDL_WAYLAND_UnloadSymbols();
@@ -212,6 +215,7 @@ Wayland_CreateDevice(int devindex)
     }
 
     device->driverdata = data;
+    device->wakeup_lock = SDL_CreateMutex();
 
     /* Set the function pointers */
     device->VideoInit = Wayland_VideoInit;
@@ -222,6 +226,8 @@ Wayland_CreateDevice(int devindex)
     device->SuspendScreenSaver = Wayland_SuspendScreenSaver;
 
     device->PumpEvents = Wayland_PumpEvents;
+    device->WaitEventTimeout = Wayland_WaitEventTimeout;
+    device->SendWakeupEvent = Wayland_SendWakeupEvent;
 
     device->GL_SwapWindow = Wayland_GLES_SwapWindow;
     device->GL_GetSwapInterval = Wayland_GLES_GetSwapInterval;