SDL: wayland: Don't let multiple threads dispatch wayland events at once

From 25f9e32b0e923de18435772464a06abb82b1663c Mon Sep 17 00:00:00 2001
From: David Gow <[EMAIL REDACTED]>
Date: Sat, 2 Oct 2021 16:52:43 +0800
Subject: [PATCH] wayland: Don't let multiple threads dispatch wayland events
 at once

wl_display_dispatch() will block if there are no events available, and
while we try to avoid this by using SDL_IOReady() to verify there are
events before calling it, there is a race condition between
SDL_IOReady() and wl_display_dispatch() if multiple threads are
involved.

This is made more likely by the fact that SDL_GL_SwapWindow() calls
wl_display_dispatch() if vsync is enabled, in order to wait for frame
events. Therefore any program which pumps events on a different thread
from SDL_GL_SwapWindow() could end up blocking in one or other of them
until another event arrives.

This change fixes this by wrapping wl_display_dispatch() in a new mutex,
which ensures only one thread can compete for wayland events at a time,
and hence the SDL_IOReady() check should successfully prevent either
from blocking.
---
 src/video/wayland/SDL_waylandevents.c   | 10 ++++++++++
 src/video/wayland/SDL_waylandopengles.c | 11 ++++++++++-
 src/video/wayland/SDL_waylandvideo.c    |  3 +++
 src/video/wayland/SDL_waylandvideo.h    |  1 +
 4 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 4cfd0f1cdd..799e5fb9bb 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -231,11 +231,21 @@ Wayland_PumpEvents(_THIS)
         keyboard_repeat_handle(&input->keyboard_repeat, now);
     }
 
+    /* If we're trying to dispatch the display in another thread,
+     * we could trigger a race condition and end up blocking
+     * in wl_display_dispatch() */
+    if (SDL_TryLockMutex(d->display_dispatch_lock)) {
+        return;
+    }
+
     if (SDL_IOReady(WAYLAND_wl_display_get_fd(d->display), SDL_FALSE, 0)) {
         err = WAYLAND_wl_display_dispatch(d->display);
     } else {
         err = WAYLAND_wl_display_dispatch_pending(d->display);
     }
+
+    SDL_UnlockMutex(d->display_dispatch_lock);
+
     if (err == -1 && !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,
diff --git a/src/video/wayland/SDL_waylandopengles.c b/src/video/wayland/SDL_waylandopengles.c
index 23429dbbd3..a6cb5027fa 100644
--- a/src/video/wayland/SDL_waylandopengles.c
+++ b/src/video/wayland/SDL_waylandopengles.c
@@ -127,7 +127,8 @@ Wayland_GLES_SwapWindow(_THIS, SDL_Window *window)
 
     /* Control swap interval ourselves. See comments on Wayland_GLES_SetSwapInterval */
     if (swap_interval != 0) {
-        struct wl_display *display = ((SDL_VideoData *)_this->driverdata)->display;
+        SDL_VideoData *videodata = (SDL_VideoData *)_this->driverdata;
+        struct wl_display *display = videodata->display;
         SDL_VideoDisplay *sdldisplay = SDL_GetDisplayForWindow(window);
         /* ~10 frames (or 1 sec), so we'll progress even if throttled to zero. */
         const Uint32 max_wait = SDL_GetTicks() + (sdldisplay->current_mode.refresh_rate ?
@@ -148,12 +149,20 @@ Wayland_GLES_SwapWindow(_THIS, SDL_Window *window)
                 break;
             }
 
+            /* Make sure we're not competing with SDL_PumpEvents() for any new
+             * events, or one of us may end up blocking in wl_display_dispatch */
+            if (SDL_TryLockMutex(videodata->display_dispatch_lock)) {
+                continue;
+            }
+
             if (SDL_IOReady(WAYLAND_wl_display_get_fd(display), SDL_FALSE, max_wait - now) <= 0) {
                 /* Error or timeout expired without any events for us */
+                SDL_UnlockMutex(videodata->display_dispatch_lock);
                 break;
             }
 
             WAYLAND_wl_display_dispatch(display);
+            SDL_UnlockMutex(videodata->display_dispatch_lock);
         }
         SDL_AtomicSet(&data->swap_interval_ready, 0);
     }
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 03464d1938..df915eea68 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -169,6 +169,7 @@ Wayland_DeleteDevice(SDL_VideoDevice *device)
         WAYLAND_wl_display_flush(data->display);
         WAYLAND_wl_display_disconnect(data->display);
     }
+    SDL_DestroyMutex(data->display_dispatch_lock);
     SDL_free(data);
     SDL_free(device);
     SDL_WAYLAND_UnloadSymbols();
@@ -201,6 +202,8 @@ Wayland_CreateDevice(int devindex)
 
     data->display = display;
 
+    data->display_dispatch_lock = SDL_CreateMutex();
+
     /* Initialize all variables that we clean on shutdown */
     device = SDL_calloc(1, sizeof(SDL_VideoDevice));
     if (!device) {
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index 2d8b6323a1..42da52e0e4 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -86,6 +86,7 @@ typedef struct {
     char *classname;
 
     int relative_mouse_mode;
+    SDL_mutex *display_dispatch_lock;
 } SDL_VideoData;
 
 typedef struct {