SDL: wayland: Remove all window references from seats when destroying a window

From 83d4dce6979185c6be9c5af1183f2ef08c49e46c Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Mon, 12 May 2025 11:43:35 -0400
Subject: [PATCH] wayland: Remove all window references from seats when
 destroying a window

Compositors typically send keyboard, pointer, touch, and tablet leave events when a window is destroyed, however, this is not guaranteed behavior, and at least one compositor in widespread use doesn't always send pointer leave events immediately upon destroying a window. Ensure that all references held by seats to a focused window are removed before the underlying window surface and structs are destroyed to prevent potential segfaults if the seats are immediately destroyed after the window.
---
 src/video/wayland/SDL_waylandevents.c   | 135 +++++++++++++-----------
 src/video/wayland/SDL_waylandevents_c.h |   1 +
 src/video/wayland/SDL_waylandwindow.c   |   6 ++
 src/video/wayland/SDL_waylandwindow.h   |   3 +-
 4 files changed, 84 insertions(+), 61 deletions(-)

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 2ed845bcf84ec..6aa95a5bee8ca 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -90,7 +90,7 @@
 // Scoped function declarations
 static void Wayland_SeatUpdateKeyboardGrab(SDL_WaylandSeat *seat);
 
-struct SDL_WaylandTouchPoint
+typedef struct
 {
     SDL_TouchID id;
     wl_fixed_t fx;
@@ -98,11 +98,11 @@ struct SDL_WaylandTouchPoint
     struct wl_surface *surface;
 
     struct wl_list link;
-};
+} SDL_WaylandTouchPoint;
 
 static void Wayland_SeatAddTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fixed_t fx, wl_fixed_t fy, struct wl_surface *surface)
 {
-    struct SDL_WaylandTouchPoint *tp = SDL_malloc(sizeof(struct SDL_WaylandTouchPoint));
+    SDL_WaylandTouchPoint *tp = SDL_malloc(sizeof(SDL_WaylandTouchPoint));
 
     SDL_zerop(tp);
     tp->id = id;
@@ -113,9 +113,37 @@ static void Wayland_SeatAddTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fixed
     WAYLAND_wl_list_insert(&seat->touch.points, &tp->link);
 }
 
+static void Wayland_SeatCancelTouch(SDL_WaylandSeat *seat, SDL_WaylandTouchPoint *tp)
+{
+    if (tp->surface) {
+        SDL_WindowData *window_data = (SDL_WindowData *)wl_surface_get_user_data(tp->surface);
+
+        if (window_data) {
+            const float x = (float)(wl_fixed_to_double(tp->fx) / window_data->current.logical_width);
+            const float y = (float)(wl_fixed_to_double(tp->fy) / window_data->current.logical_height);
+
+            SDL_SendTouch(0, (SDL_TouchID)(uintptr_t)seat->touch.wl_touch,
+                          (SDL_FingerID)(tp->id + 1), window_data->sdlwindow, SDL_EVENT_FINGER_CANCELED, x, y, 0.0f);
+
+            --window_data->active_touch_count;
+
+            /* If the window currently has mouse focus and has no currently active keyboards, pointers,
+             * or touch events, then consider mouse focus to be lost.
+             */
+            if (SDL_GetMouseFocus() == window_data->sdlwindow && !window_data->keyboard_focus_count &&
+                !window_data->pointer_focus_count && !window_data->active_touch_count) {
+                SDL_SetMouseFocus(NULL);
+            }
+        }
+    }
+
+    WAYLAND_wl_list_remove(&tp->link);
+    SDL_free(tp);
+}
+
 static void Wayland_SeatUpdateTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fixed_t fx, wl_fixed_t fy, struct wl_surface **surface)
 {
-    struct SDL_WaylandTouchPoint *tp;
+    SDL_WaylandTouchPoint *tp;
 
     wl_list_for_each (tp, &seat->touch.points, link) {
         if (tp->id == id) {
@@ -131,7 +159,7 @@ static void Wayland_SeatUpdateTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fi
 
 static void Wayland_SeatRemoveTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fixed_t *fx, wl_fixed_t *fy, struct wl_surface **surface)
 {
-    struct SDL_WaylandTouchPoint *tp;
+    SDL_WaylandTouchPoint *tp;
 
     wl_list_for_each (tp, &seat->touch.points, link) {
         if (tp->id == id) {
@@ -152,23 +180,6 @@ static void Wayland_SeatRemoveTouch(SDL_WaylandSeat *seat, SDL_TouchID id, wl_fi
     }
 }
 
-static bool Wayland_SurfaceHasActiveTouches(SDL_VideoData *display, struct wl_surface *surface)
-{
-    struct SDL_WaylandTouchPoint *tp;
-    SDL_WaylandSeat *seat;
-
-    // Check all seats for active touches on the surface.
-    wl_list_for_each (seat, &display->seat_list, link) {
-        wl_list_for_each (tp, &seat->touch.points, link) {
-            if (tp->surface == surface) {
-                return true;
-            }
-        }
-    }
-
-    return false;
-}
-
 static void Wayland_GetScaledMouseRect(SDL_Window *window, SDL_Rect *scaled_rect)
 {
     SDL_WindowData *window_data = window->internal;
@@ -751,7 +762,7 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
      */
     SDL_Window *mouse_focus = SDL_GetMouseFocus();
     const bool had_focus = mouse_focus && window->sdlwindow == mouse_focus;
-    if (!--window->pointer_focus_count && had_focus && !Wayland_SurfaceHasActiveTouches(seat->display, surface)) {
+    if (!--window->pointer_focus_count && had_focus && !window->active_touch_count) {
         SDL_SetMouseFocus(NULL);
     }
 
@@ -1243,6 +1254,7 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri
             y = (float)wl_fixed_to_double(fy) / (window_data->current.logical_height - 1);
         }
 
+        ++window_data->active_touch_count;
         SDL_SetMouseFocus(window_data->sdlwindow);
 
         SDL_SendTouch(Wayland_GetTouchTimestamp(seat, timestamp), (SDL_TouchID)(uintptr_t)touch,
@@ -1269,11 +1281,13 @@ static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial
             SDL_SendTouch(Wayland_GetTouchTimestamp(seat, timestamp), (SDL_TouchID)(uintptr_t)touch,
                           (SDL_FingerID)(id + 1), window_data->sdlwindow, SDL_EVENT_FINGER_UP, x, y, 0.0f);
 
-            /* If the window currently has mouse focus, the keyboard focus is another window or NULL, the window has no
-             * pointers active on it, and the surface has no active touch events, then consider mouse focus to be lost.
+            --window_data->active_touch_count;
+
+            /* If the window currently has mouse focus and has no currently active keyboards, pointers,
+             * or touch events, then consider mouse focus to be lost.
              */
-            if (SDL_GetMouseFocus() == window_data->sdlwindow && seat->keyboard.focus != window_data &&
-                !window_data->pointer_focus_count && !Wayland_SurfaceHasActiveTouches(seat->display, surface)) {
+            if (SDL_GetMouseFocus() == window_data->sdlwindow && !window_data->keyboard_focus_count &&
+                !window_data->pointer_focus_count && !window_data->active_touch_count) {
                 SDL_SetMouseFocus(NULL);
             }
         }
@@ -1308,39 +1322,11 @@ static void touch_handler_frame(void *data, struct wl_touch *touch)
 static void touch_handler_cancel(void *data, struct wl_touch *touch)
 {
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
-    struct SDL_WaylandTouchPoint *tp, *temp;
+    SDL_WaylandTouchPoint *tp, *temp;
 
+    // Need the safe loop variant here as cancelling a touch point removes it from the list.
     wl_list_for_each_safe (tp, temp, &seat->touch.points, link) {
-        bool removed = false;
-
-        if (tp->surface) {
-            SDL_WindowData *window_data = (SDL_WindowData *)wl_surface_get_user_data(tp->surface);
-
-            if (window_data) {
-                const float x = (float)(wl_fixed_to_double(tp->fx) / window_data->current.logical_width);
-                const float y = (float)(wl_fixed_to_double(tp->fy) / window_data->current.logical_height);
-
-                SDL_SendTouch(0, (SDL_TouchID)(uintptr_t)touch,
-                              (SDL_FingerID)(tp->id + 1), window_data->sdlwindow, SDL_EVENT_FINGER_CANCELED, x, y, 0.0f);
-
-                // Remove the touch from the list before checking for still-active touches on the surface.
-                WAYLAND_wl_list_remove(&tp->link);
-                removed = true;
-
-                /* If the window currently has mouse focus, the keyboard focus is another window or NULL, the window has no
-                 * pointers active on it, and the surface has no active touch events, then consider mouse focus to be lost.
-                 */
-                if (SDL_GetMouseFocus() == window_data->sdlwindow && seat->keyboard.focus != window_data &&
-                    !window_data->pointer_focus_count && !Wayland_SurfaceHasActiveTouches(seat->display, tp->surface)) {
-                    SDL_SetMouseFocus(NULL);
-                }
-            }
-        }
-
-        if (!removed) {
-            WAYLAND_wl_list_remove(&tp->link);
-        }
-        SDL_free(tp);
+        Wayland_SeatCancelTouch(seat, tp);
     }
 }
 
@@ -1924,8 +1910,7 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *keyboard,
     /* If the window has mouse focus, has no pointers within it, and no active touches, consider
      * mouse focus to be lost.
      */
-    if (SDL_GetMouseFocus() == window->sdlwindow && !window->pointer_focus_count &&
-        !Wayland_SurfaceHasActiveTouches(seat->display, surface)) {
+    if (SDL_GetMouseFocus() == window->sdlwindow && !window->pointer_focus_count && !window->active_touch_count) {
         SDL_SetMouseFocus(NULL);
     }
 }
@@ -3416,6 +3401,36 @@ void Wayland_DisplayCreateSeat(SDL_VideoData *display, struct wl_seat *wl_seat,
     WAYLAND_wl_display_flush(display->display);
 }
 
+void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_WindowData *window)
+{
+    SDL_WaylandSeat *seat;
+    wl_list_for_each (seat, &display->seat_list, link)
+    {
+        if (seat->keyboard.focus == window) {
+            keyboard_handle_leave(seat, seat->keyboard.wl_keyboard, 0, window->surface);
+        }
+
+        if (seat->pointer.focus == window) {
+            pointer_handle_leave(seat, seat->pointer.wl_pointer, 0, window->surface);
+        }
+
+        // Need the safe loop variant here as cancelling a touch point removes it from the list.
+        SDL_WaylandTouchPoint *tp, *temp;
+        wl_list_for_each_safe (tp, temp, &seat->touch.points, link) {
+            if (tp->surface == window->surface) {
+                Wayland_SeatCancelTouch(seat, tp);
+            }
+        }
+
+        SDL_WaylandPenTool *tool;
+        wl_list_for_each (tool, &seat->tablet.tool_list, link) {
+            if (tool->tool_focus == window->sdlwindow) {
+                tablet_tool_handle_proximity_out(tool, tool->wltool);
+            }
+        }
+    }
+}
+
 void Wayland_SeatDestroy(SDL_WaylandSeat *seat, bool send_events)
 {
     if (!seat) {
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 3c3aed69d5c07..a6c1eed589652 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -205,6 +205,7 @@ extern bool Wayland_SeatHasRelativePointerFocus(SDL_WaylandSeat *seat);
 extern void Wayland_SeatUpdatePointerGrab(SDL_WaylandSeat *seat);
 extern void Wayland_DisplayUpdatePointerGrabs(SDL_VideoData *display, SDL_WindowData *window);
 extern void Wayland_DisplayUpdateKeyboardGrabs(SDL_VideoData *display, SDL_WindowData *window);
+extern void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_WindowData *window);
 
 /* The implicit grab serial needs to be updated on:
  * - Keyboard key down/up
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index b019eff2f92f0..7a06d37dbfbc8 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -3093,6 +3093,12 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
             WAYLAND_wl_display_roundtrip(data->display);
         }
 
+        /* The compositor should have relinquished keyboard, pointer, touch, and tablet tool focus when the toplevel
+         * window was destroyed upon being hidden, but there is no guarantee of this, so ensure that all references
+         * to the window held by seats are released before destroying the underlying surface and struct.
+         */
+        Wayland_DisplayRemoveWindowReferencesFromSeats(data, wind);
+
 #ifdef SDL_VIDEO_OPENGL_EGL
         if (wind->egl_surface) {
             SDL_EGL_DestroySurface(_this, wind->egl_surface);
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index 343a0ed251514..7099462abd006 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -132,9 +132,10 @@ struct SDL_WindowData
     struct Wayland_SHMBuffer *icon_buffers;
     int icon_buffer_count;
 
-    // Keyboard and pointer focus refcount.
+    // Keyboard, pointer, and touch focus refcount.
     int keyboard_focus_count;
     int pointer_focus_count;
+    int active_touch_count;
 
     struct
     {