SDL: wayland: Allow the creation of roleless window surfaces for custom application use

From f7dd0f9491ab52833801dce8b5ebc4694e8e7c39 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 6 Jan 2024 15:37:38 -0500
Subject: [PATCH] wayland: Allow the creation of roleless window surfaces for
 custom application use

Allow for the creation of SDL windows with a roleless surface that applications can use for their own purposes, such as with a windowing protocol other than XDG toplevel.

The property `wayland.surface_role_custom` will create a window with a surface that SDL can render to and handles input for, but is not associated with a toplevel window, so applications can use it for their own, custom purposes (e.g. wlr_layer_shell).

A test/minimal example is included in tests/testwaylandcustom.c
---
 docs/README-wayland.md                |  28 +++
 include/SDL3/SDL_video.h              |  65 ++---
 src/video/wayland/SDL_waylandwindow.c |  87 ++++---
 src/video/wayland/SDL_waylandwindow.h |   3 +-
 test/CMakeLists.txt                   |   9 +
 test/testwaylandcustom.c              | 333 ++++++++++++++++++++++++++
 6 files changed, 466 insertions(+), 59 deletions(-)
 create mode 100644 test/testwaylandcustom.c

diff --git a/docs/README-wayland.md b/docs/README-wayland.md
index a376b48ad01c..ffbadced1a24 100644
--- a/docs/README-wayland.md
+++ b/docs/README-wayland.md
@@ -43,3 +43,31 @@ encounter limitations or behavior that is different from other windowing systems
   on the format of this file. Note that if your application manually sets the application ID via the `SDL_APP_ID` hint
   string, the desktop entry file name should match the application ID. For example, if your application ID is set
   to `org.my_org.sdl_app`, the desktop entry file should be named `org.my_org.sdl_app.desktop`.
+
+## Using custom Wayland windowing protocols with SDL windows
+
+Under normal operation, an `SDL_Window` corresponds to an XDG toplevel window, which provides a standard desktop window.
+If an application wishes to use a different windowing protocol with an SDL window (e.g. wlr_layer_shell) while still
+having SDL handle input and rendering, it needs to create a custom, roleless surface and attach that surface to its own
+toplevel window.
+
+This is done by using `SDL_CreateWindowWithProperties()` and setting the
+`SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN` property to `SDL_TRUE`. Once the window has been
+successfully created, the `wl_display` and `wl_surface` objects can then be retrieved from the
+`SDL_PROPERTY_WINDOW_WAYLAND_DISPLAY_POINTER` and `SDL_PROPERTY_WINDOW_WAYLAND_SURFACE_POINTER` properties respectively.
+
+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.
+
+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.
\ No newline at end of file
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 0eefdec6d953..7c223fe40d2a 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -866,6 +866,13 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, in
  *   `(__unsafe_unretained)` NSView associated with the window, defaults to
  *   `[window contentView]`
  *
+ * These are additional supported properties on Wayland:
+ *
+ * - `SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN` - true
+ *   if the application wants to use the Wayland surface for a custom role and
+ *   does not want it attached to an XDG toplevel window. See
+ *   docs/README-wayland.md for more information on using custom surfaces.
+ *
  * These are additional supported properties on Windows:
  *
  * - `SDL_PROPERTY_WINDOW_CREATE_WIN32_HWND_POINTER`: the HWND associated with
@@ -894,35 +901,35 @@ extern DECLSPEC SDL_Window *SDLCALL SDL_CreatePopupWindow(SDL_Window *parent, in
  */
 extern DECLSPEC SDL_Window *SDLCALL SDL_CreateWindowWithProperties(SDL_PropertiesID props);
 
-#define SDL_PROPERTY_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN            "always-on-top"
-#define SDL_PROPERTY_WINDOW_CREATE_BORDERLESS_BOOLEAN               "borderless"
-#define SDL_PROPERTY_WINDOW_CREATE_FOCUSABLE_BOOLEAN                "focusable"
-#define SDL_PROPERTY_WINDOW_CREATE_FULLSCREEN_BOOLEAN               "fullscreen"
-#define SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER                    "height"
-#define SDL_PROPERTY_WINDOW_CREATE_HIDDEN_BOOLEAN                   "hidden"
-#define SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN       "high-pixel-density"
-#define SDL_PROPERTY_WINDOW_CREATE_MAXIMIZED_BOOLEAN                "maximized"
-#define SDL_PROPERTY_WINDOW_CREATE_MENU_BOOLEAN                     "menu"
-#define SDL_PROPERTY_WINDOW_CREATE_METAL_BOOLEAN                    "metal"
-#define SDL_PROPERTY_WINDOW_CREATE_MINIMIZED_BOOLEAN                "minimized"
-#define SDL_PROPERTY_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN            "mouse-grabbed"
-#define SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN                   "opengl"
-#define SDL_PROPERTY_WINDOW_CREATE_PARENT_POINTER                   "parent"
-#define SDL_PROPERTY_WINDOW_CREATE_RESIZABLE_BOOLEAN                "resizable"
-#define SDL_PROPERTY_WINDOW_CREATE_TITLE_STRING                     "title"
-#define SDL_PROPERTY_WINDOW_CREATE_TRANSPARENT_BOOLEAN              "transparent"
-#define SDL_PROPERTY_WINDOW_CREATE_TOOLTIP_BOOLEAN                  "tooltip"
-#define SDL_PROPERTY_WINDOW_CREATE_UTILITY_BOOLEAN                  "utility"
-#define SDL_PROPERTY_WINDOW_CREATE_VULKAN_BOOLEAN                   "vulkan"
-#define SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER                     "width"
-#define SDL_PROPERTY_WINDOW_CREATE_X_NUMBER                         "x"
-#define SDL_PROPERTY_WINDOW_CREATE_Y_NUMBER                         "y"
-#define SDL_PROPERTY_WINDOW_CREATE_COCOA_WINDOW_POINTER             "cocoa.window"
-#define SDL_PROPERTY_WINDOW_CREATE_COCOA_VIEW_POINTER               "cocoa.view"
-#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"
-
+#define SDL_PROPERTY_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN               "always-on-top"
+#define SDL_PROPERTY_WINDOW_CREATE_BORDERLESS_BOOLEAN                  "borderless"
+#define SDL_PROPERTY_WINDOW_CREATE_FOCUSABLE_BOOLEAN                   "focusable"
+#define SDL_PROPERTY_WINDOW_CREATE_FULLSCREEN_BOOLEAN                  "fullscreen"
+#define SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER                       "height"
+#define SDL_PROPERTY_WINDOW_CREATE_HIDDEN_BOOLEAN                      "hidden"
+#define SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN          "high-pixel-density"
+#define SDL_PROPERTY_WINDOW_CREATE_MAXIMIZED_BOOLEAN                   "maximized"
+#define SDL_PROPERTY_WINDOW_CREATE_MENU_BOOLEAN                        "menu"
+#define SDL_PROPERTY_WINDOW_CREATE_METAL_BOOLEAN                       "metal"
+#define SDL_PROPERTY_WINDOW_CREATE_MINIMIZED_BOOLEAN                   "minimized"
+#define SDL_PROPERTY_WINDOW_CREATE_MOUSE_GRABBED_BOOLEAN               "mouse-grabbed"
+#define SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN                      "opengl"
+#define SDL_PROPERTY_WINDOW_CREATE_PARENT_POINTER                      "parent"
+#define SDL_PROPERTY_WINDOW_CREATE_RESIZABLE_BOOLEAN                   "resizable"
+#define SDL_PROPERTY_WINDOW_CREATE_TITLE_STRING                        "title"
+#define SDL_PROPERTY_WINDOW_CREATE_TRANSPARENT_BOOLEAN                 "transparent"
+#define SDL_PROPERTY_WINDOW_CREATE_TOOLTIP_BOOLEAN                     "tooltip"
+#define SDL_PROPERTY_WINDOW_CREATE_UTILITY_BOOLEAN                     "utility"
+#define SDL_PROPERTY_WINDOW_CREATE_VULKAN_BOOLEAN                      "vulkan"
+#define SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER                        "width"
+#define SDL_PROPERTY_WINDOW_CREATE_X_NUMBER                            "x"
+#define SDL_PROPERTY_WINDOW_CREATE_Y_NUMBER                            "y"
+#define SDL_PROPERTY_WINDOW_CREATE_COCOA_WINDOW_POINTER                "cocoa.window"
+#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_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"
 
 /**
  * Get the numeric ID of a window.
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 92ceb3ec1e83..ddc585d2e409 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -395,8 +395,10 @@ static void ConfigureWindowGeometry(SDL_Window *window)
                 wl_surface_set_buffer_scale(data->surface, 1);
                 SetDrawSurfaceViewport(window, data->drawable_width, data->drawable_height,
                                        window_width, window_height);
-            } else {
+            } else if (window->flags & SDL_WINDOW_HIGH_PIXEL_DENSITY) {
                 UnsetDrawSurfaceViewport(window);
+
+                /* Don't change this if DPI awareness flag is unset, as an application may have set this manually. */
                 wl_surface_set_buffer_scale(data->surface, (int32_t)data->windowed_scale_factor);
             }
 
@@ -1415,6 +1417,11 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
     SDL_WindowData *data = window->driverdata;
     SDL_PropertiesID props = SDL_GetWindowProperties(window);
 
+    /* Custom surfaces don't get toplevels and are always considered 'shown'; nothing to do here. */
+    if (data->shell_surface_type == WAYLAND_SURFACE_CUSTOM) {
+        return;
+    }
+
     /* If this is a child window, the parent *must* be in the final shown state,
      * meaning that it has received a configure event, followed by a frame callback.
      * If not, a race condition can result, with effects ranging from the child
@@ -1437,8 +1444,6 @@ void Wayland_ShowWindow(SDL_VideoDevice *_this, SDL_Window *window)
         WAYLAND_wl_display_roundtrip(c->display);
     }
 
-    data->surface_status = WAYLAND_SURFACE_STATUS_WAITING_FOR_CONFIGURE;
-
     /* Detach any previous buffers before resetting everything, otherwise when
      * calling this a second time you'll get an annoying protocol error!
      *
@@ -1683,6 +1688,11 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
     SDL_WindowData *wind = window->driverdata;
     SDL_PropertiesID props = SDL_GetWindowProperties(window);
 
+    /* Custom surfaces have nothing to destroy and are always considered to be 'shown'; nothing to do here. */
+    if (wind->shell_surface_type == WAYLAND_SURFACE_CUSTOM) {
+        return;
+    }
+
     /* The window was shown, but the sync point hasn't yet been reached.
      * Pump events to avoid a possible protocol violation.
      */
@@ -1715,17 +1725,16 @@ void Wayland_HideWindow(SDL_VideoDevice *_this, SDL_Window *window)
     } else
 #endif
         if (wind->shell_surface_type == WAYLAND_SURFACE_XDG_POPUP) {
-            Wayland_ReleasePopup(_this, window);
-        } else if (wind->shell_surface.xdg.roleobj.toplevel) {
-            xdg_toplevel_destroy(wind->shell_surface.xdg.roleobj.toplevel);
-            wind->shell_surface.xdg.roleobj.toplevel = NULL;
-            SDL_SetProperty(props, SDL_PROPERTY_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL);
-        }
-        if (wind->shell_surface.xdg.surface) {
-            xdg_surface_destroy(wind->shell_surface.xdg.surface);
-            wind->shell_surface.xdg.surface = NULL;
-            SDL_SetProperty(props, SDL_PROPERTY_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL);
-        }
+        Wayland_ReleasePopup(_this, window);
+    } else if (wind->shell_surface.xdg.roleobj.toplevel) {
+        xdg_toplevel_destroy(wind->shell_surface.xdg.roleobj.toplevel);
+        wind->shell_surface.xdg.roleobj.toplevel = NULL;
+        SDL_SetProperty(props, SDL_PROPERTY_WINDOW_WAYLAND_XDG_TOPLEVEL_POINTER, NULL);
+    }
+    if (wind->shell_surface.xdg.surface) {
+        xdg_surface_destroy(wind->shell_surface.xdg.surface);
+        wind->shell_surface.xdg.surface = NULL;
+        SDL_SetProperty(props, SDL_PROPERTY_WINDOW_WAYLAND_XDG_SURFACE_POINTER, NULL);
     }
 
     wind->show_hide_sync_required = SDL_TRUE;
@@ -1844,6 +1853,11 @@ int Wayland_SetWindowFullscreen(SDL_VideoDevice *_this, SDL_Window *window,
     SDL_WindowData *wind = window->driverdata;
     struct wl_output *output = display->driverdata->output;
 
+    /* Custom surfaces have no toplevel to make fullscreen. */
+    if (wind->shell_surface_type == WAYLAND_SURFACE_CUSTOM) {
+        return -1;
+    }
+
     if (wind->show_hide_sync_required) {
         WAYLAND_wl_display_roundtrip(_this->driverdata->display);
     }
@@ -2041,6 +2055,7 @@ 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);
 
     data = SDL_calloc(1, sizeof(*data));
     if (!data) {
@@ -2140,18 +2155,24 @@ int Wayland_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Propert
     /* We may need to create an idle inhibitor for this new window */
     Wayland_SuspendScreenSaver(_this);
 
+    if (!custom_surface_role) {
 #ifdef HAVE_LIBDECOR_H
-    if (c->shell.libdecor && !SDL_WINDOW_IS_POPUP(window)) {
-        data->shell_surface_type = WAYLAND_SURFACE_LIBDECOR;
-    } else
+        if (c->shell.libdecor && !SDL_WINDOW_IS_POPUP(window)) {
+            data->shell_surface_type = WAYLAND_SURFACE_LIBDECOR;
+        } else
 #endif
-        if (c->shell.xdg) {
-        if (SDL_WINDOW_IS_POPUP(window)) {
-            data->shell_surface_type = WAYLAND_SURFACE_XDG_POPUP;
-        } else {
-            data->shell_surface_type = WAYLAND_SURFACE_XDG_TOPLEVEL;
-        }
-    } /* All other cases will be WAYLAND_SURFACE_UNKNOWN */
+            if (c->shell.xdg) {
+            if (SDL_WINDOW_IS_POPUP(window)) {
+                data->shell_surface_type = WAYLAND_SURFACE_XDG_POPUP;
+            } else {
+                data->shell_surface_type = WAYLAND_SURFACE_XDG_TOPLEVEL;
+            }
+        } /* All other cases will be WAYLAND_SURFACE_UNKNOWN */
+    } else {
+        /* Roleless 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;
+    }
 
     SDL_PropertiesID props = SDL_GetWindowProperties(window);
     SDL_SetProperty(props, SDL_PROPERTY_WINDOW_WAYLAND_DISPLAY_POINTER, data->waylandData->display);
@@ -2259,12 +2280,20 @@ void Wayland_SetWindowSize(SDL_VideoDevice *_this, SDL_Window *window)
 {
     SDL_WindowData *wind = window->driverdata;
 
-    /* Queue an event to send the window size. */
-    struct wl_callback *cb = wl_display_sync(_this->driverdata->display);
+    if (wind->shell_surface_type != WAYLAND_SURFACE_CUSTOM) {
+        /* Queue an event to send the window size. */
+        struct wl_callback *cb = wl_display_sync(_this->driverdata->display);
 
-    wind->pending_size_event.width = window->floating.w;
-    wind->pending_size_event.height = window->floating.h;
-    wl_callback_add_listener(cb, &size_event_listener, (void*)((uintptr_t)window->id));
+        wind->pending_size_event.width = window->floating.w;
+        wind->pending_size_event.height = window->floating.h;
+        wl_callback_add_listener(cb, &size_event_listener, (void *)((uintptr_t)window->id));
+    } else {
+        /* We are being informed of a size change on a custom surface, just configure. */
+        wind->requested_window_width = window->floating.w;
+        wind->requested_window_height = window->floating.h;
+
+        ConfigureWindowGeometry(window);
+    }
 }
 
 void Wayland_GetWindowSizeInPixels(SDL_VideoDevice *_this, SDL_Window *window, int *w, int *h)
diff --git a/src/video/wayland/SDL_waylandwindow.h b/src/video/wayland/SDL_waylandwindow.h
index f7f5037175b2..96c7a0eab739 100644
--- a/src/video/wayland/SDL_waylandwindow.h
+++ b/src/video/wayland/SDL_waylandwindow.h
@@ -70,7 +70,8 @@ struct SDL_WindowData
         WAYLAND_SURFACE_UNKNOWN = 0,
         WAYLAND_SURFACE_XDG_TOPLEVEL,
         WAYLAND_SURFACE_XDG_POPUP,
-        WAYLAND_SURFACE_LIBDECOR
+        WAYLAND_SURFACE_LIBDECOR,
+        WAYLAND_SURFACE_CUSTOM
     } shell_surface_type;
     enum
     {
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 98fc28c95cd6..7604e0e65eb7 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -398,6 +398,15 @@ add_sdl_test_executable(testvulkan NO_C90 SOURCES testvulkan.c)
 add_sdl_test_executable(testoffscreen SOURCES testoffscreen.c)
 add_sdl_test_executable(testpopup SOURCES testpopup.c)
 
+if (HAVE_WAYLAND)
+    # Set the GENERATED property on the protocol file, since it is first created at build time
+    set_property(SOURCE ${SDL3_BINARY_DIR}/wayland-generated-protocols/xdg-shell-protocol.c PROPERTY GENERATED 1)
+    add_sdl_test_executable(testwaylandcustom NO_C90 NEEDS_RESOURCES SOURCES testwaylandcustom.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(testwaylandcustom PRIVATE -Wno-documentation-unknown-command)
+    target_link_libraries(testwaylandcustom PRIVATE wayland-client)
+endif()
+
 check_c_compiler_flag(-Wformat-overflow HAVE_WFORMAT_OVERFLOW)
 if(HAVE_WFORMAT_OVERFLOW)
     target_compile_definitions(testautomation PRIVATE HAVE_WFORMAT_OVERFLOW)
diff --git a/test/testwaylandcustom.c b/test/testwaylandcustom.c
new file mode 100644
index 000000000000..3e1042bbbe7d
--- /dev/null
+++ b/test/testwaylandcustom.c
@@ -0,0 +1,333 @@
+/*
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+
+#include <stdlib.h>
+#include <time.h>
+
+#include <SDL3/SDL.h>
+#include <wayland-client.h>
+#include <xdg-shell-client-protocol.h>
+
+#include "icon.h"
+
+#define WINDOW_WIDTH  640
+#define WINDOW_HEIGHT 480
+#define NUM_SPRITES   100
+#define MAX_SPEED     1
+
+static SDL_Window *window;
+static SDL_Renderer *renderer;
+static SDL_Texture *sprite;
+static SDL_FRect positions[NUM_SPRITES];
+static SDL_FRect velocities[NUM_SPRITES];
+static int sprite_w, sprite_h;
+static int done;
+
+static SDL_Texture *CreateTexture(SDL_Renderer *r, unsigned char *data, unsigned int len, int *w, int *h)
+{
+    SDL_Texture *texture = NULL;
+    SDL_Surface *surface;
+    SDL_RWops *src = SDL_RWFromConstMem(data, len);
+    if (src) {
+        surface = SDL_LoadBMP_RW(src, SDL_TRUE);
+        if (surface) {
+            /* Treat white as transparent */
+            SDL_SetSurfaceColorKey(surface, SDL_TRUE, SDL_MapRGB(surface->format, 255, 255, 255));
+
+            texture = SDL_CreateTextureFromSurface(r, surface);
+            *w = surface->w;
+            *h = surface->h;
+            SDL_DestroySurface(surface);
+        }
+    }
+    return texture;
+}
+
+static void MoveSprites(void)
+{
+    int i;
+    int window_w;
+    int window_h;
+    SDL_FRect *position, *velocity;
+
+    /* Get the window size */
+    SDL_GetWindowSizeInPixels(window, &window_w, &window_h);
+
+    /* Draw a gray background */
+    SDL_SetRenderDrawColor(renderer, 0xA0, 0xA0, 0xA0, 0xFF);
+    SDL_RenderClear(renderer);
+
+    /* Move the sprite, bounce at the wall, and draw */
+    for (i = 0; i < NUM_SPRITES; ++i) {
+        position = &positions[i];
+        velocity = &velocities[i];
+        position->x += velocity->x;
+        if ((position->x < 0) || (position->x >= (window_w - sprite_w))) {
+            velocity->x = -velocity->x;
+            position->x += velocity->x;
+        }
+        position->y += velocity->y;
+        if ((position->y < 0) || (position->y >= (window_h - sprite_h))) {
+            velocity->y = -velocity->y;
+            position->y += velocity->y;
+        }
+
+        /* Blit the sprite onto the screen */
+        SDL_RenderTexture(renderer, sprite, NULL, position);
+    }
+
+    /* Update the screen! */
+    SDL_RenderPresent(renderer);
+}
+
+static int InitSprites(void)
+{
+    /* Create the sprite texture and initialize the sprite positions */
+    sprite = CreateTexture(renderer, icon_bmp, icon_bmp_len, &sprite_w, &sprite_h);
+
+    if (!sprite) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to create sprite texture");
+        return -1;
+    }
+
+    srand((unsigned int)time(NULL));
+    for (int i = 0; i < NUM_SPRITES; ++i) {
+        positions[i].x = (float)(rand() % (WINDOW_WIDTH - sprite_w));
+        positions[i].y = (float)(rand() % (WINDOW_HEIGHT - sprite_h));
+        positions[i].w = (float)sprite_w;
+        positions[i].h = (float)sprite_h;
+        velocities[i].x = 0.0f;
+        velocities[i].y = 0.0f;
+        while (!velocities[i].x && !velocities[i].y) {
+            velocities[i].x = (float)((rand() % (MAX_SPEED * 2 + 1)) - MAX_SPEED);
+            velocities[i].y = (float)((rand() % (MAX_SPEED * 2 + 1)) - MAX_SPEED);
+        }
+    }
+
+    return 0;
+}
+
+/* Encapsulated in a struct to silence shadow variable warnings */
+static struct _state
+{
+    /* These are owned by SDL and must not be destroyed! */
+    struct wl_display *wl_display;
+    struct wl_surface *wl_surface;
+
+    /* These are owned by the application and need to be cleaned up on exit. */
+    struct wl_registry *wl_registry;
+    struct xdg_wm_base *xdg_wm_base;
+    struct xdg_surface *xdg_surface;
+    struct xdg_toplevel *xdg_toplevel;
+} state;
+
+static void xdg_surface_configure(void *data, struct xdg_surface *xdg_surface, uint32_t serial)
+{
+    xdg_surface_ack_configure(state.xdg_surface, serial);
+}
+
+static const struct xdg_surface_listener xdg_surface_listener = {
+    .configure = xdg_surface_configure,
+};
+
+static void xdg_toplevel_configure(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height, struct wl_array *states)
+{
+    /* NOP */
+}
+
+static void xdg_toplevel_close(void *data, struct xdg_toplevel *xdg_toplevel)
+{
+    done = 1;
+}
+
+static void xdg_toplevel_configure_bounds(void *data, struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height)
+{
+    /* NOP */
+}
+
+static void xdg_toplevel_wm_capabilities(void *data, struct xdg_toplevel *xdg_toplevel, struct wl_array *capabilities)
+{
+    /* NOP */
+}
+
+static const struct xdg_toplevel_listener xdg_toplevel_listener = {
+    .configure = xdg_toplevel_configure,
+    .close = xdg_toplevel_close,
+    .configure_bounds = xdg_toplevel_configure_bounds,
+    .wm_capabilities = xdg_toplevel_wm_capabilities
+};
+
+static void xdg_wm_base_ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial)
+{
+    xdg_wm_base_pong(state.xdg_wm_base, serial);
+}
+
+static const struct xdg_wm_base_listener xdg_wm_base_listener = {
+    .ping = xdg_wm_base_ping,
+};
+
+static void registry_global(void *data, struct wl_registry *wl_registry, uint32_t name, const char *interface, uint32_t version)
+{
+    if (SDL_strcmp(interface, xdg_wm_base_interface.name) == 0) {
+        state.xdg_wm_base = wl_registry_bind(state.wl_registry, name, &xdg_wm_base_interface, 1);
+        xdg_wm_base_add_listener(state.xdg_wm_base, &xdg_wm_base_listener, NULL);
+    }
+}
+
+static void registry_global_remove(void *data, struct wl_registry *wl_registry, uint32_t name)
+{
+    /* NOP */
+}
+
+static const struct wl_registry_listener wl_registry_listener = {
+    .global = registry_global,
+    .global_remove = registry_global_remove,
+};
+
+int main(int argc, char **argv)
+{
+    int ret = -1;
+    SDL_PropertiesID props;
+
+    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
+        return -1;
+    }
+
+    if (SDL_strcmp(SDL_GetCurrentVideoDriver(), "wayland") != 0) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Video driver must be 'wayland', not '%s'", SDL_GetCurrentVideoDriver());
+        goto exit;
+    }
+
+    /* Create a window with the custom surface role property set. */
+    props = SDL_CreateProperties();
+    SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_WAYLAND_SURFACE_ROLE_CUSTOM_BOOLEAN, SDL_TRUE);   /* Roleless surface */
+    SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_OPENGL_BOOLEAN, SDL_TRUE);                        /* OpenGL enabled */
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_WIDTH_NUMBER, WINDOW_WIDTH);                       /* Default width */
+    SDL_SetNumberProperty(props, SDL_PROPERTY_WINDOW_CREATE_HEIGHT_NUMBER, WINDOW_HEIGHT);                     /* Default height */
+    SDL_SetBooleanProperty(props, SDL_PROPERTY_WINDOW_CREATE_HIGH_PIXEL_DENSITY_BOOLEAN, SDL_TRUE);            /* Handle DPI scaling internally */
+    SDL_SetStringProperty(props, SDL_PROPERTY_WINDOW_CREATE_TITLE_STRING, "Wayland custom surface role test"); /* Default title */
+
+    window = SDL_CreateWindowWithProperties(props);
+    if (!window) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Window creation failed");
+        goto exit;
+    }
+
+    /* Create the renderer */
+    renderer = SDL_CreateRenderer(window, NULL, 0);
+    if (!renderer) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Renderer creation failed");
+        goto exit;
+    }
+
+    /* Get the display object and use it to create a registry object, which will enumerate the xdg_wm_base protocol. */
+    state.wl_display = SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROPERTY_WINDOW_WAYLAND_DISPLAY_POINTER, NULL);
+    state.wl_registry = wl_display_get_registry(state.wl_display);
+    wl_registry_add_listener(state.wl_registry, &wl_registry_listener, NULL);
+
+    /* Roundtrip to enumerate registry objects. */
+    wl_display_roundtrip(state.wl_display);
+
+    if (!state.xdg_wm_base) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "'xdg_wm_base' protocol not found!");
+        goto exit;
+    }
+
+    /* Get the wl_surface object from the SDL_Window, and create a toplevel window with it. */
+    state.wl_surface = SDL_GetProperty(SDL_GetWindowProperties(window), SDL_PROPERTY_WINDOW_WAYLAND_SURFACE_POINTER, NULL);
+
+    /* Create the xdg_surface from the wl_surface. */
+    state.xdg_surface = xdg_wm_base_get_xdg_surface(state.xdg_wm_base, state.wl_surface);
+    xdg_surface_add_listener(state.xdg_surface, &xdg_surface_listener, NULL);
+
+    /* Create the xdg_toplevel from the xdg_surface. */
+    state.xdg_toplevel = xdg_surface_get_toplevel(state.xdg_surface);
+    xdg_toplevel_add_listener(state.xdg_toplevel, &xdg_toplevel_listener, NULL);
+    xdg_toplevel_set_title(state.xdg_toplevel, SDL_GetWindowTitle(window));
+
+    /* Initialize the sprites. */
+    if (InitSprites() < 0) {
+        goto exit;
+    }
+
+    while (!done) {
+        SDL_Event event;
+        while (SDL_PollEvent(&event)) {
+            if (event.type == SDL_EVENT_KEY_DOWN) {
+                switch (event.key.keysym.sym) {
+                case SDLK_ESCAPE:
+                    done = 1;
+                    break;
+                case SDLK_EQUALS:
+                    /* Ctrl+ enlarges the window */
+                    if (event.key.keysym.mod & SDL_KMOD_CTRL) {
+                        int w, h;
+                        SDL_GetWindowSize(window, &w, &h);
+                        SDL_SetWindowSize(window, w * 2, h * 2);
+                    }
+                    break;
+                case SDLK_MINUS:
+                    /* Ctrl- shrinks the window */
+                    if (event.key.keysym.mod & SDL_KMOD_CTRL) {
+                        int w, h;
+                        SDL_GetWindowSize(window, &w, &h);
+                        SDL_SetWindowSize(window, w / 2, h / 2);
+                    }
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+
+        /* Draw the sprites */
+        MoveSprites();
+    }
+
+    ret = 0;
+
+exit:
+    /* The display and surface handles obtained from SDL are owned by SDL and must *NOT* be destroyed here! */
+    if (state.xdg_toplevel) {
+        xdg_toplevel_destroy(state.xdg_toplevel);
+        state.xdg_toplevel = NULL;
+    }
+    if (state.xdg_surface) {
+        xdg_surface_destroy(state.xdg_surface);
+        state.xdg_surface = NULL;
+    }
+    if (state.xdg_wm_base) {
+        xdg_wm_base_destroy(state.xdg_wm_base);
+        state.xdg_wm_base = NULL;
+    }
+    if (state.wl_registry) {
+        wl_registry_destroy(state.wl_registry);
+        state.wl_registry = NULL;
+    }
+
+    /* Destroy the SDL resources */
+    if (sprite) {
+        SDL_DestroyTexture(sprite);
+        sprite = NULL;
+    }
+    if (renderer) {
+        SDL_DestroyRenderer(renderer);
+        renderer = NULL;
+    }
+    if (window) {
+        SDL_DestroyWindow(window);
+        window = NULL;
+    }
+
+    SDL_Quit();
+    return ret;
+}