SDL: wayland: Add the ability to import and wrap external surfaces

From 4f3d4bd110d7482c339f0e10b94b3fe9f35a010b Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Tue, 9 Jan 2024 11:38:38 -0500
Subject: [PATCH] wayland: Add the ability to import and wrap external surfaces

Add the ability to import and wrap external surfaces from external toolkits such as Qt and GTK.

Wayland surfaces and windows are more intrinsically tied to the client library than other windowing systems, so it is necessary to provide a way to initialize SDL with an existing wl_display object, which needs to be set prior to video system initialization, or export the internal SDL wl_display object for use by external applications or toolkits. For this, the global property SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER is used.

A Wayland example was added to testnative, and a basic example of Qt 6 interoperation is provided in the Wayland readme to demonstrate the use of external windows with both SDL owning the wl_display, and an external toolkit owning it.
---
 docs/README-wayland.md                | 168 ++++++++++++++++++-
 include/SDL3/SDL_video.h              |  20 +++
 src/video/wayland/SDL_waylandevents.c |  61 +++----
 src/video/wayland/SDL_waylandvideo.c  |  54 +++++-
 src/video/wayland/SDL_waylandvideo.h  |   5 +
 src/video/wayland/SDL_waylandwindow.c |  46 ++++--
 src/video/wayland/SDL_waylandwindow.h |   2 +
 test/CMakeLists.txt                   |  17 +-
 test/testnative.c                     |   8 +-
 test/testnative.h                     |   5 +
 test/testnativewayland.c              | 226 ++++++++++++++++++++++++++
 11 files changed, 548 insertions(+), 64 deletions(-)
 create mode 100644 test/testnativewayland.c

diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index c145230f8a72..383ee3b3f120 100644
--- a/docs/README-wayland.md
+++ b/docs/README-wayland.md
@@ -59,15 +59,165 @@ successfully created, the `wl_display` and `wl_surface` objects can then be retr
 Surfaces don't receive any size change notifications, so if an application changes the window size, it must inform SDL
 that the surface size has changed by calling SDL_SetWindowSize() with the new dimensions.
 
-Custom surfaces will automatically handle scaling internally if the window was created with the `high-pixel-density`
-property set to `SDL_TRUE`. In this case, applications should not manually attach viewports or change the surface scale
-value, as SDL will handle this internally. Calls to `SDL_SetWindowSize()` should use the logical size of the window, and
-`SDL_GetWindowSizeInPixels()` should be used to query the size of the backbuffer surface in pixels. If this property is
-not set or is `SDL_FALSE`, applications can attach their own viewports or change the surface scale manually, and the SDL
-backend will not interfere or change any values internally. In this case, calls to `SDL_SetWindowSize()` should pass the
-requested surface size in pixels, not the logical window size, as no scaling calculations will be done internally.
+Custom surfaces will automatically handle scaling internally if the window was created with the
+`SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN` property set to `SDL_TRUE`. In this case, applications should
+not manually attach viewports or change the surface scale value, as SDL will handle this internally. Calls
+to `SDL_SetWindowSize()` should use the logical size of the window, and `SDL_GetWindowSizeInPixels()` should be used to
+query the size of the backbuffer surface in pixels. If this property is not set or is `SDL_FALSE`, applications can
+attach their own viewports or change the surface scale manually, and the SDL backend will not interfere or change any
+values internally. In this case, calls to `SDL_SetWindowSize()` should pass the requested surface size in pixels, not
+the logical window size, as no scaling calculations will be done internally.
 
 All window functions that control window state aside from `SDL_SetWindowSize()` are no-ops with custom surfaces.
 
-Please see the minimal example in tests/testwaylandcustom.c for an example of how to use a custom, roleless surface and
-attach it to an application-managed toplevel window.
+Please see the minimal example in `tests/testwaylandcustom.c` for an example of how to use a custom, roleless surface
+and attach it to an application-managed toplevel window.
+
+## Importing external surfaces into SDL windows
+
+Wayland windows and surfaces are more intrinsically tied to the client library than other windowing systems, therefore,
+when importing surfaces, it is necessary for both SDL and the application or toolkit to use the same `wl_display`
+object. This can be set/queried via the global `SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER` property. To
+import an external `wl_display`, set this property before initializing the SDL video subsystem, and read the value to
+export the internal `wl_display` after the video subsystem has been initialized. Setting this property after the video
+subsystem has been initialized has no effect, and reading it when the video subsystem is uninitialized will either
+return the user provided value, if one was set while in the uninitialized state, or NULL.
+
+Once this is done, and the application has created or obtained the `wl_surface` to be wrapped in an `SDL_Window`, the
+window is created with `SDL_CreateWindowWithProperties()` with the
+`SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` property to set to the `wl_surface` object that is to be
+imported by SDL.
+
+SDL receives no notification regarding size changes on external surfaces or toplevel windows, so if the external surface
+needs to be resized, SDL must be informed by calling SDL_SetWindowSize() with the new dimensions.
+
+If desired, SDL can automatically handle the scaling for the surface by setting the
+`SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN` property to `SDL_TRUE`, however, if the surface being imported
+already has, or will have, a viewport/fractional scale manager attached to it by the application or an external toolkit,
+a protocol violation will result. Avoid setting this property if importing surfaces from toolkits such as Qt or GTK.
+
+If the window is flagged as high pixel density, calls to `SDL_SetWindowSize()` should pass the logical size of the
+window and `SDL_GetWindowSizeInPixels()` should be used to retrieve the backbuffer size in pixels. Otherwise, calls to
+`SDL_SetWindowSize()` should pass the requested surface size in pixels, not the logical window size, as no scaling
+calculations will be done internally.
+
+All window functions that control window state aside from `SDL_SetWindowSize()` are no-ops with external surfaces.
+
+An example of how to use external surfaces with a `wl_display` owned by SDL can be seen in `tests/testnativewayland.c`,
+and the following is a minimal example of interoperation with Qt 6, with Qt owning the `wl_display`:
+
+```c++
+#include <QApplication>
+#include <QWindow>
+#include <qpa/qplatformnativeinterface.h>
+
+#include <SDL3/SDL.h>
+
+int main(int argc, char *argv[])
+{
+    int ret = -1;
+    int done = 0;
+    SDL_PropertiesID props;
+    SDL_Event e;
+    SDL_Window *sdlWindow = NULL;
+    SDL_Renderer *sdlRenderer = NULL;
+    struct wl_display *display = NULL;
+    struct wl_surface *surface = NULL;
+
+    /* Initialize Qt */
+    QApplication qtApp(argc, argv);
+    QWindow qtWindow;
+
+    /* The windowing system must be Wayland. */
+    if (QApplication::platformName() != "wayland") {
+        goto exit;
+    }
+
+    {
+        /* Get the wl_display object from Qt */
+        QNativeInterface::QWaylandApplication *qtWlApp = qtApp.nativeInterface<QNativeInterface::QWaylandApplication>();
+        display = qtWlApp->display();
+
+        if (!display) {
+            goto exit;
+        }
+    }
+
+    /* Set SDL to use the existing wl_display object from Qt and initialize. */
+    SDL_SetProperty(SDL_GetGlobalProperties(), SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, display);
+    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
+
+    /* Create a basic, frameless QWindow */
+    qtWindow.setFlags(Qt::FramelessWindowHint);
+    qtWindow.setGeometry(0, 0, 640, 480);
+    qtWindow.show();
+
+    {
+        /* Get the native wl_surface backing resource for the window */
+        QPlatformNativeInterface *qtNative = qtApp.platformNativeInterface();
+        surface = (struct wl_surface *)qtNative->nativeResourceForWindow("surface", &qtWindow);
+
+        if (!surface) {
+            goto exit;
+        }
+    }
+
+    /* Create a window that wraps the wl_surface from the QWindow.
+     * Qt objects should not be flagged as DPI-aware or protocol violations will result.
+     */
+    props = SDL_CreateProperties();
+    SDL_SetProperty(props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER, surface);
+    SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN, SDL_TRUE);
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER, 640);
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER, 480);
+    sdlWindow = SDL_CreateWindowWithProperties(props);
+    SDL_DestroyProperties(props);
+    if (!sdlWindow) {
+        goto exit;
+    }
+
+    /* Create a renderer */
+    sdlRenderer = SDL_CreateRenderer(sdlWindow, NULL, 0);
+    if (!sdlRenderer) {
+        goto exit;
+    }
+
+    /* Draw a blue screen for the window until ESC is pressed or the window is no longer visible. */
+    while (!done) {
+        while (SDL_PollEvent(&e)) {
+            if (e.type == SDL_EVENT_KEY_DOWN && e.key.keysym.sym == SDLK_ESCAPE) {
+                done = 1;
+            }
+        }
+
+        qtApp.processEvents();
+        
+        /* Update the backbuffer size if the window scale changed. */
+        qreal scale = qtWindow.devicePixelRatio();
+        SDL_SetWindowSize(sdlWindow, SDL_lround(640. * scale), SDL_lround(480. * scale));
+
+        if (qtWindow.isVisible()) {
+            SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 255, 255);
+            SDL_RenderClear(sdlRenderer);
+            SDL_RenderPresent(sdlRenderer);
+        } else {
+            done = 1;
+        }
+    }
+
+    ret = 0;
+
+exit:
+    /* Cleanup */
+    if (sdlRenderer) {
+        SDL_DestroyRenderer(sdlRenderer);
+    }
+    if (sdlWindow) {
+        SDL_DestroyWindow(sdlWindow);
+    }
+
+    SDL_Quit();
+    return ret;
+}
+```
+
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index d64f015a5eb7..da87c9aed346 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -44,6 +44,22 @@ extern "C" {
 typedef Uint32 SDL_DisplayID;
 typedef Uint32 SDL_WindowID;
 
+/**
+ *  Global video properties
+ *
+ *  - `SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER`: the pointer to
+ *    the global `wl_display` object used by the Wayland video backend. Can be
+ *    set before the video subsystem is initialized to import an external
+ *    `wl_display` object from an application or toolkit for use in SDL, or
+ *    read after initialization to export the `wl_display` used by the
+ *    Wayland video backend. Setting this property after the video subsystem
+ *    has been initialized has no effect, and reading it when the video
+ *    subsystem is uninitialized will either return the user provided value,
+ *    if one was set prior to initialization, or NULL. See
+ *    docs/README-wayland.md for more information.
+ */
+#define SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER "video.wayland.wl_display"
+
 /**
  *  System theme
  */
@@ -875,6 +891,9 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, in
  * - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN - true if
  *   the application wants an associated `wl_egl_window` object to be created,
  *   even if the window does not have the OpenGL property or flag set.
+ * - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER` - the wl_surface
+ *   associated with the window, if you want to wrap an existing window. See
+*    docs/README-wayland.md for more information.
  *
  * These are additional supported properties on Windows:
  *
@@ -931,6 +950,7 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindowWithProperties(SDL_Propertie
 #define SDL_PROPERTY_WINDOW_CREATE_COCOA_VIEW_POINTER                  "cocoa.view"
 #define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN "wayland.surface_role_custom"
 #define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN   "wayland.create_egl_window"
+#define SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER          "wayland.wl_surface"
 #define SDL_PROPERTY_WINDOW_CREATE_WIN32_HWND_POINTER                  "win32.hwnd"
 #define SDL_PROPERTY_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER     "win32.pixel_format_hwnd"
 #define SDL_PROPERTY_WINDOW_CREATE_X11_WINDOW_NUMBER                   "x11.window"
diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index 0dbc8555470b..42297290e916 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -539,18 +539,12 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
         return;
     }
 
-    /* check that this surface belongs to one of the SDL windows */
-    if (!SDL_WAYLAND_own_surface(surface)) {
-        return;
-    }
-
     /* This handler will be called twice in Wayland 1.4
      * Once for the window surface which has valid user data
      * and again for the mouse cursor surface which does not have valid user data
      * We ignore the later
      */
-
-    window = (SDL_WindowData *)wl_surface_get_user_data(surface);
+    window = Wayland_GetWindowDataForOwnedSurface(surface);
 
     if (window) {
         input->pointer_focus = window;
@@ -576,12 +570,12 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
 {
     struct SDL_WaylandInput *input = data;
 
-    if (!surface || !SDL_WAYLAND_own_surface(surface)) {
+    if (!surface) {
         return;
     }
 
     if (input->pointer_focus) {
-        SDL_WindowData *wind = (SDL_WindowData *)wl_surface_get_user_data(surface);
+        SDL_WindowData *wind = Wayland_GetWindowDataForOwnedSurface(surface);
 
         if (wind) {
             /* Clear the capture flag and raise all buttons */
@@ -980,14 +974,14 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri
     struct SDL_WaylandInput *input = (struct SDL_WaylandInput *)data;
     SDL_WindowData *window_data;
 
-    /* Check that this surface belongs to one of the SDL windows */
-    if (!SDL_WAYLAND_own_surface(surface)) {
+    /* Check that this surface is valid. */
+    if (!surface) {
         return;
     }
 
     touch_add(id, fx, fy, surface);
     Wayland_UpdateImplicitGrabSerial(input, serial);
-    window_data = (SDL_WindowData *)wl_surface_get_user_data(surface);
+    window_data = Wayland_GetWindowDataForOwnedSurface(surface);
 
     if (window_data) {
         float x, y;
@@ -1430,19 +1424,18 @@ static void keyboard_handle_enter(void *data, struct wl_keyboard *keyboard,
         return;
     }
 
-    if (!SDL_WAYLAND_own_surface(surface)) {
+    window = Wayland_GetWindowDataForOwnedSurface(surface);
+
+    if (!window) {
         return;
     }
 
-    window = wl_surface_get_user_data(surface);
+    input->keyboard_focus = window;
+    window->keyboard_device = input;
 
-    if (window) {
-        input->keyboard_focus = window;
-        window->keyboard_device = input;
+    /* Restore the keyboard focus to the child popup that was holding it */
+    SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow);
 
-        /* Restore the keyboard focus to the child popup that was holding it */
-        SDL_SetKeyboardFocus(window->keyboard_focus ? window->keyboard_focus : window->sdlwindow);
-    }
 #ifdef SDL_USE_IME
     if (!input->text_input) {
         SDL_IME_SetFocus(SDL_TRUE);
@@ -1479,17 +1472,19 @@ static void keyboard_handle_leave(void *data, struct wl_keyboard *keyboard,
     SDL_WindowData *wind;
     SDL_Window *window = NULL;
 
-    if (!surface || !SDL_WAYLAND_own_surface(surface)) {
+    if (!surface) {
         return;
     }
 
-    wind = wl_surface_get_user_data(surface);
-    if (wind) {
-        wind->keyboard_device = NULL;
-        window = wind->sdlwindow;
-        window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
+    wind = Wayland_GetWindowDataForOwnedSurface(surface);
+    if (!wind) {
+        return;
     }
 
+    wind->keyboard_device = NULL;
+    window = wind->sdlwindow;
+    window->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
+
     /* Stop key repeat before clearing keyboard focus */
     keyboard_repeat_clear(&input->keyboard_repeat);
 
@@ -1935,11 +1930,9 @@ static void data_device_handle_enter(void *data, struct wl_data_device *wl_data_
 
         /* find the current window */
         if (surface) {
-            if (SDL_WAYLAND_own_surface(surface)) {
-                SDL_WindowData *window = (SDL_WindowData *)wl_surface_get_user_data(surface);
-                if (window) {
-                    data_device->dnd_window = window->sdlwindow;
-                }
+            SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface);
+            if (window) {
+                data_device->dnd_window = window->sdlwindow;
             } else {
                 data_device->dnd_window = NULL;
             }
@@ -2609,11 +2602,7 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v
         return;
     }
 
-    if (!SDL_WAYLAND_own_surface(surface)) {
-        return;
-    }
-
-    window = (SDL_WindowData *)wl_surface_get_user_data(surface);
+    window = Wayland_GetWindowDataForOwnedSurface(surface);
 
     if (window) {
         input->tool_focus = window;
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index b29cd3754f90..728d7b987922 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -106,12 +106,45 @@ SDL_bool SDL_WAYLAND_own_output(struct wl_output *output)
     return wl_proxy_get_tag((struct wl_proxy *)output) == &SDL_WAYLAND_output_tag;
 }
 
+/* External surfaces may have their own user data attached, the modification of which
+ * can cause problems with external toolkits. Instead, external windows are kept in
+ * their own list, and a search is conducted to find a matching surface.
+ */
+static struct wl_list external_window_list;
+
+void Wayland_AddWindowDataToExternalList(SDL_WindowData *data)
+{
+    WAYLAND_wl_list_insert(&external_window_list, &data->external_window_list_link);
+}
+
+void Wayland_RemoveWindowDataFromExternalList(SDL_WindowData *data)
+{
+    WAYLAND_wl_list_remove(&data->external_window_list_link);
+}
+
+SDL_WindowData *Wayland_GetWindowDataForOwnedSurface(struct wl_surface *surface)
+{
+    if (SDL_WAYLAND_own_surface(surface)) {
+        return (SDL_WindowData *)wl_surface_get_user_data(surface);
+    } else if (!WAYLAND_wl_list_empty(&external_window_list)) {
+        SDL_WindowData *p;
+        wl_list_for_each (p, &external_window_list, external_window_list_link) {
+            if (p->surface == surface) {
+                return p;
+            }
+        }
+    }
+
+    return NULL;
+}
+
 static void Wayland_DeleteDevice(SDL_VideoDevice *device)
 {
     SDL_VideoData *data = device->driverdata;
-    if (data->display) {
+    if (data->display && !data->display_externally_owned) {
         WAYLAND_wl_display_flush(data->display);
         WAYLAND_wl_display_disconnect(data->display);
+        SDL_ClearProperty(SDL_GetGlobalProperties(), SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER);
     }
     if (device->wakeup_lock) {
         SDL_DestroyMutex(device->wakeup_lock);
@@ -125,7 +158,9 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
 {
     SDL_VideoDevice *device;
     SDL_VideoData *data;
-    struct wl_display *display;
+    struct wl_display *display = SDL_GetProperty(SDL_GetGlobalProperties(),
+                                                 SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, NULL);
+    SDL_bool display_is_external = !!display;
 
     /* Are we trying to connect to or are currently in a Wayland session? */
     if (!SDL_getenv("WAYLAND_DISPLAY")) {
@@ -139,10 +174,12 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
         return NULL;
     }
 
-    display = WAYLAND_wl_display_connect(NULL);
     if (!display) {
-        SDL_WAYLAND_UnloadSymbols();
-        return NULL;
+        display = WAYLAND_wl_display_connect(NULL);
+        if (!display) {
+            SDL_WAYLAND_UnloadSymbols();
+            return NULL;
+        }
     }
 
     data = SDL_calloc(1, sizeof(*data));
@@ -154,7 +191,9 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
 
     data->initializing = SDL_TRUE;
     data->display = display;
+    data->display_externally_owned = display_is_external;
     WAYLAND_wl_list_init(&data->output_list);
+    WAYLAND_wl_list_init(&external_window_list);
 
     /* Initialize all variables that we clean on shutdown */
     device = SDL_calloc(1, sizeof(SDL_VideoDevice));
@@ -165,6 +204,11 @@ static SDL_VideoDevice *Wayland_CreateDevice(void)
         return NULL;
     }
 
+    if (!display_is_external) {
+        SDL_SetProperty(SDL_GetGlobalProperties(),
+                        SDL_PROPERTY_GLOBAL_VIDEO_WAYLAND_WL_DISPLAY_POINTER, display);
+    }
+
     device->driverdata = data;
     device->wakeup_lock = SDL_CreateMutex();
 
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index 4691ae6e9cc7..25a4b3c0809b 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -79,6 +79,7 @@ struct SDL_VideoData
     struct wl_list output_list;
 
     int relative_mouse_mode;
+    SDL_bool display_externally_owned;
 };
 
 struct SDL_DisplayData
@@ -107,6 +108,10 @@ extern void SDL_WAYLAND_register_output(struct wl_output *output);
 extern SDL_bool SDL_WAYLAND_own_surface(struct wl_surface *surface);
 extern SDL_bool SDL_WAYLAND_own_output(struct wl_output *output);
 
+extern SDL_WindowData *Wayland_GetWindowDataForOwnedSurface(struct wl_surface *surface);
+void Wayland_AddWindowDataToExternalList(SDL_WindowData *data);
+void Wayland_RemoveWindowDataFromExternalList(SDL_WindowData *data);
+
 extern SDL_bool Wayland_LoadLibdecor(SDL_VideoData *data, SDL_bool ignore_xdg);
 
 extern SDL_bool Wayland_VideoReconnect(SDL_VideoDevice *_this);
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 7c77f1e04466..5637ea3cd070 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -2057,10 +2057,13 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
 {
     SDL_WindowData *data;
     SDL_VideoData *c;
-    const SDL_bool custom_surface_role = SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, SDL_FALSE);
+    struct wl_surface *external_surface = (struct wl_surface *)SDL_GetProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_WL_SURFACE_POINTER,
+                                                                               (struct wl_surface *)SDL_GetProperty(create_props, "sdl2-compat.external_window", NULL));
+    const SDL_bool custom_surface_role = (external_surface != NULL) || SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, SDL_FALSE);
     const SDL_bool create_egl_window = !!(window->flags & SDL_WINDOW_OPENGL) ||
                                        SDL_GetBooleanProperty(create_props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_CREATE_EGL_WINDOW_BOOLEAN, SDL_FALSE);
 
+
     data = SDL_calloc(1, sizeof(*data));
     if (!data) {
         return -1;
@@ -2102,10 +2105,20 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
     data->requested_window_width = window->w;
     data->requested_window_height = window->h;
 
-    data->surface = wl_compositor_create_surface(c->compositor);
-    wl_surface_add_listener(data->surface, &surface_listener, data);
+    if (!external_surface) {
+        data->surface = wl_compositor_create_surface(c->compositor);
+        wl_surface_add_listener(data->surface, &surface_listener, data);
+        wl_surface_set_user_data(data->surface, data);
+        SDL_WAYLAND_register_surface(data->surface);
+    } else {
+        window->flags |= SDL_WINDOW_EXTERNAL;
+        data->surface = external_surface;
 
-    SDL_WAYLAND_register_surface(data->surface);
+        /* External surfaces are registered by being put in a list, as changing tags or userdata
+         * can cause problems with external toolkits.
+         */
+        Wayland_AddWindowDataToExternalList(data);
+    }
 
     /* Must be called before EGL configuration to set the drawable backbuffer size. */
     ConfigureWindowGeometry(window);
@@ -2123,9 +2136,12 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
         wl_callback_add_listener(data->gles_swap_frame_callback, &gles_swap_frame_listener, data);
     }
 
-    /* Fire a callback when the compositor wants a new frame to set the surface damage region. */
-    data->surface_frame_callback = wl_surface_frame(data->surface);
-    wl_callback_add_listener(data->surface_frame_callback, &surface_frame_listener, data);
+    /* No frame callback on external surfaces as it may already have one attached. */
+    if (!external_surface) {
+        /* Fire a callback when the compositor wants a new frame to set the surface damage region. */
+        data->surface_frame_callback = wl_surface_frame(data->surface);
+        wl_callback_add_listener(data->surface_frame_callback, &surface_frame_listener, data);
+    }
 
     if (window->flags & SDL_WINDOW_TRANSPARENT) {
         if (_this->gl_config.alpha_size == 0) {
@@ -2152,7 +2168,13 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
         Wayland_input_lock_pointer(c->input);
     }
 
-    if (c->fractional_scale_manager) {
+    /* Don't attach a fractional scale manager to surfaces unless they are
+     * flagged as DPI-aware. Under non-scaled operation, the scale will
+     * always be 1.0, and external/custom surfaces may already have, or
+     * will try to attach, their own fractional scale manager, which will
+     * result in a protocol violation.
+     */
+    if (c->fractional_scale_manager && (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY)) {
         data->fractional_scale = wp_fractional_scale_manager_v1_get_fractional_scale(c->fractional_scale_manager, data->surface);
         wp_fractional_scale_v1_add_listener(data->fractional_scale,
                                             &fractional_scale_listener, data);
@@ -2175,7 +2197,7 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
             }
         } /* All other cases will be WAYLAND_SURFACE_UNKNOWN */
     } else {
-        /* Roleless surfaces are always considered to be in the shown state by the backend. */
+        /* Roleless and external surfaces are always considered to be in the shown state by the backend. */
         data->shell_surface_type = WAYLAND_SURFACE_CUSTOM;
         data->surface_status = WAYLAND_SURFACE_STATUS_SHOWN;
     }
@@ -2433,7 +2455,11 @@ void Wayland_DestroyWindow(SDL_VideoDevice *_this, SDL_Window *window)
             wl_callback_destroy(wind->surface_frame_callback);
         }
 
-        wl_surface_destroy(wind->surface);
+        if (!(window->flags & SDL_WINDOW_EXTERNAL)) {
+            wl_surface_destroy(wind->surface);
+        } else {
+            Wayland_RemoveWindowDataFromExternalList(wind);
+        }
 
         SDL_free(wind);
         WAYLAND_wl_display_flush(data->display);
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index 96c7a0eab739..0925ac6d6cdb 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -130,6 +130,8 @@ struct SDL_WindowData
     SDL_bool show_hide_sync_required;
 
     SDL_HitTestResult hit_test_result;
+
+    struct wl_list external_window_list_link;
 };
 
 extern void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 7604e0e65eb7..014e29d1c099 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -288,9 +288,20 @@ if(APPLE)
     endif()
 elseif(WINDOWS)
     add_sdl_test_executable(testnative BUILD_DEPENDENT NEEDS_RESOURCES TESTUTILS SOURCES testnative.c testnativew32.c)
-elseif(HAVE_X11)
-    add_sdl_test_executable(testnative BUILD_DEPENDENT NEEDS_RESOURCES TESTUTILS SOURCES testnative.c testnativex11.c)
-    target_link_libraries(testnative PRIVATE X11)
+elseif(HAVE_X11 OR HAVE_WAYLAND)
+    add_sdl_test_executable(testnative BUILD_DEPENDENT NO_C90 NEEDS_RESOURCES TESTUTILS SOURCES testnative.c)
+    if(HAVE_X11)
+        target_sources(testnative PRIVATE testnativex11.c)
+        target_link_libraries(testnative PRIVATE X11)
+    endif()
+    if(HAVE_WAYLAND)
+        set_property(SOURCE ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c PROPERTY GENERATED 1)
+        target_sources(testnative PRIVATE testnativewayland.c ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c)
+
+        # Needed to silence the documentation warning in the generated header file
+        target_compile_options(testnative PRIVATE -Wno-documentation-unknown-command)
+        target_link_libraries(testnative PRIVATE wayland-client)
+    endif ()
 endif()
 
 find_package(Python3)
diff --git a/test/testnative.c b/test/testnative.c
index 32af19b37c7b..4ac30de658d7 100644
--- a/test/testnative.c
+++ b/test/testnative.c
@@ -30,6 +30,9 @@ static NativeWindowFactory *factories[] = {
 #ifdef TEST_NATIVE_WINDOWS
     &WindowsWindowFactory,
 #endif
+#ifdef TEST_NATIVE_WAYLAND
+    &WaylandWindowFactory,
+#endif
 #ifdef TEST_NATIVE_X11
     &X11WindowFactory,
 #endif
@@ -47,10 +50,10 @@ static SDLTest_CommonState *state;
 static void
 quit(int rc)
 {
-    SDL_Quit();
     if (native_window && factory) {
         factory->DestroyNativeWindow(native_window);
     }
+    SDL_Quit();
     SDLTest_CommonDestroyState(state);
     /* Let 'main()' return normally */
     if (rc != 0) {
@@ -149,6 +152,9 @@ int main(int argc, char *argv[])
     }
     props = SDL_CreateProperties();
     SDL_SetProperty(props, "sdl2-compat.external_window", native_window);
+    SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN, SDL_TRUE);
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER, WINDOW_W);
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER, WINDOW_H);
     window = SDL_CreateWindowWithProperties(props);
     SDL_DestroyProperties(props);
     if (!window) {
diff --git a/test/testnative.h b/test/testnative.h
index a1f2d4842601..7a1c934d0209 100644
--- a/test/testnative.h
+++ b/test/testnative.h
@@ -36,6 +36,11 @@ typedef struct
 extern NativeWindowFactory WindowsWindowFactory;
 #endif
 
+#ifdef SDL_VIDEO_DRIVER_WAYLAND
+#define TEST_NATIVE_WAYLAND
+extern NativeWindowFactory WaylandWindowFactory;
+#endif
+
 #ifdef SDL_VIDEO_DRIVER_X11
 #define TEST_NATIVE_X11
 extern NativeWindowFactory X11WindowFactory;
diff --git a/test/testnativewayland.c b/test/testnativewayland.c
new file mode 100644
index 000000000000..f5025ca4d958
--- /dev/null
+++ b/test/testnativewayland.c
@@ -0,0 +1,226 @@
+/*

(Patch may be truncated, please check the link at the top of this post.)