SDL: wayland: Handle min/max sizes in fixed-size windows with viewports

From 0a45525242516772f8dfa6e07b123dedf1439372 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 7 Nov 2025 12:32:24 -0500
Subject: [PATCH] wayland: Handle min/max sizes in fixed-size windows with
 viewports

Wayland is sometimes at-odds with clients that want to enforce an aspect ratio or min/max window size, as certain window states have dimensions that either must be obeyed (maximized), or will give terrible results if they aren't (tiled). Use a viewport and a masking subsurface to handle cases where surfaces are unable to match the exact window size.

The changes made to accommodate this also catches some additional windowing related edge-cases, simplifies synchronization, and prevents commits before a buffer has been attached to the surface.
---
 src/video/wayland/SDL_waylandevents.c        | 114 ++--
 src/video/wayland/SDL_waylandevents_c.h      |   4 +-
 src/video/wayland/SDL_waylandmouse.c         |  17 +
 src/video/wayland/SDL_waylandmouse.h         |   2 +
 src/video/wayland/SDL_waylandshmbuffer.c     |  25 +
 src/video/wayland/SDL_waylandshmbuffer.h     |   2 +
 src/video/wayland/SDL_waylandvideo.c         |  16 +
 src/video/wayland/SDL_waylandvideo.h         |   2 +
 src/video/wayland/SDL_waylandwindow.c        | 655 +++++++++++++------
 src/video/wayland/SDL_waylandwindow.h        |  24 +-
 wayland-protocols/single-pixel-buffer-v1.xml |  76 +++
 11 files changed, 666 insertions(+), 271 deletions(-)
 create mode 100644 wayland-protocols/single-pixel-buffer-v1.xml

diff --git a/src/video/wayland/SDL_waylandevents.c b/src/video/wayland/SDL_waylandevents.c
index b8fb8e0a42472..30b4002701f0d 100644
--- a/src/video/wayland/SDL_waylandevents.c
+++ b/src/video/wayland/SDL_waylandevents.c
@@ -802,7 +802,20 @@ static void pointer_handle_motion(void *data, struct wl_pointer *pointer,
 
 static void pointer_dispatch_enter(SDL_WaylandSeat *seat)
 {
-    SDL_WindowData *window = seat->pointer.pending_frame.enter_window;
+    SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(seat->pointer.pending_frame.enter_surface);
+    if (!window) {
+        // Entering a surface not managed by SDL; just set the cursor reset flag.
+        Wayland_SeatResetCursor(seat);
+        return;
+    }
+
+    if (window->surface != seat->pointer.pending_frame.enter_surface) {
+        /* This surface is part of the window managed by SDL, but it is not the main content
+         * surface and doesn't get focus. Just set the default cursor and leave.
+         */
+        Wayland_SeatSetDefaultCursor(seat);
+        return;
+    }
 
     seat->pointer.focus = window;
     ++window->pointer_focus_count;
@@ -834,14 +847,8 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
         return;
     }
 
-    SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface);
-    if (!window) {
-        // Not a surface owned by SDL.
-        return;
-    }
-
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
-    seat->pointer.pending_frame.enter_window = window;
+    seat->pointer.pending_frame.enter_surface = surface;
     seat->pointer.enter_serial = serial;
 
     /* In the case of e.g. a pointer confine warp, we may receive an enter
@@ -860,32 +867,39 @@ static void pointer_handle_enter(void *data, struct wl_pointer *pointer,
 
 static void pointer_dispatch_leave(SDL_WaylandSeat *seat, bool update_pointer)
 {
-    SDL_WindowData *window = seat->pointer.pending_frame.leave_window;
+    SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(seat->pointer.pending_frame.leave_surface);
 
     if (window) {
-        // Clear the capture flag and raise all buttons
-        window->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
-
-        seat->pointer.focus = NULL;
-        for (Uint8 i = 1; seat->pointer.buttons_pressed; ++i) {
-            if (seat->pointer.buttons_pressed & SDL_BUTTON_MASK(i)) {
-                SDL_SendMouseButton(0, window->sdlwindow, seat->pointer.sdl_id, i, false);
-                seat->pointer.buttons_pressed &= ~SDL_BUTTON_MASK(i);
-            }
-        }
+        if (seat->pointer.focus) {
+            if (seat->pointer.focus->surface == seat->pointer.pending_frame.leave_surface) {
+                // Clear the capture flag and raise all buttons
+                window->sdlwindow->flags &= ~SDL_WINDOW_MOUSE_CAPTURE;
 
-        /* A pointer leave event may be emitted if the compositor hides the pointer in response to receiving a touch event.
-         * Don't relinquish focus if the surface has active touches, as the compositor is just transitioning from mouse to touch mode.
-         */
-        SDL_Window *mouse_focus = SDL_GetMouseFocus();
-        const bool had_focus = mouse_focus && window->sdlwindow == mouse_focus;
-        if (!--window->pointer_focus_count && had_focus && !window->active_touch_count) {
-            SDL_SetMouseFocus(NULL);
-        }
+                seat->pointer.focus = NULL;
+                for (Uint8 i = 1; seat->pointer.buttons_pressed; ++i) {
+                    if (seat->pointer.buttons_pressed & SDL_BUTTON_MASK(i)) {
+                        SDL_SendMouseButton(0, window->sdlwindow, seat->pointer.sdl_id, i, false);
+                        seat->pointer.buttons_pressed &= ~SDL_BUTTON_MASK(i);
+                    }
+                }
 
-        if (update_pointer) {
-            Wayland_SeatUpdatePointerGrab(seat);
-            Wayland_SeatUpdatePointerCursor(seat);
+                /* A pointer leave event may be emitted if the compositor hides the pointer in response to receiving a touch event.
+                 * Don't relinquish focus if the surface has active touches, as the compositor is just transitioning from mouse to touch mode.
+                 */
+                SDL_Window *mouse_focus = SDL_GetMouseFocus();
+                const bool had_focus = mouse_focus && window->sdlwindow == mouse_focus;
+                if (!--window->pointer_focus_count && had_focus && !window->active_touch_count) {
+                    SDL_SetMouseFocus(NULL);
+                }
+
+                if (update_pointer) {
+                    Wayland_SeatUpdatePointerGrab(seat);
+                    Wayland_SeatUpdatePointerCursor(seat);
+                }
+            }
+        } else if (update_pointer) {
+            // Leaving a non-content surface managed by SDL; just set the cursor reset flag.
+            Wayland_SeatResetCursor(seat);
         }
     }
 }
@@ -898,15 +912,9 @@ static void pointer_handle_leave(void *data, struct wl_pointer *pointer,
         return;
     }
 
-    SDL_WindowData *window = Wayland_GetWindowDataForOwnedSurface(surface);
-    if (!window) {
-        // Not a surface owned by SDL.
-        return;
-    }
-
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
-    seat->pointer.pending_frame.leave_window = window;
-    if (wl_pointer_get_version(seat->pointer.wl_pointer) < WL_POINTER_FRAME_SINCE_VERSION && window == seat->pointer.focus) {
+    seat->pointer.pending_frame.leave_surface = surface;
+    if (wl_pointer_get_version(seat->pointer.wl_pointer) < WL_POINTER_FRAME_SINCE_VERSION) {
         pointer_dispatch_leave(seat, true);
     }
 }
@@ -1277,11 +1285,13 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer)
 {
     SDL_WaylandSeat *seat = data;
 
-    if (seat->pointer.pending_frame.enter_window) {
-        if (seat->pointer.focus && seat->pointer.pending_frame.leave_window == seat->pointer.focus) {
+    if (seat->pointer.pending_frame.enter_surface) {
+        if (seat->pointer.pending_frame.leave_surface) {
             // Leaving the previous surface before entering a new surface.
             pointer_dispatch_leave(seat, false);
+            seat->pointer.pending_frame.leave_surface = NULL;
         }
+
         pointer_dispatch_enter(seat);
     }
 
@@ -1309,7 +1319,7 @@ static void pointer_handle_frame(void *data, struct wl_pointer *pointer)
         pointer_dispatch_axis(seat);
     }
 
-    if (seat->pointer.focus && seat->pointer.pending_frame.leave_window == seat->pointer.focus) {
+    if (seat->pointer.pending_frame.leave_surface) {
         pointer_dispatch_leave(seat, true);
     }
 
@@ -1435,7 +1445,7 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri
     Wayland_UpdateImplicitGrabSerial(seat, serial);
     window_data = Wayland_GetWindowDataForOwnedSurface(surface);
 
-    if (window_data) {
+    if (window_data && window_data->surface == surface) {
         float x, y;
 
         if (window_data->current.logical_width <= 1) {
@@ -1457,8 +1467,7 @@ static void touch_handler_down(void *data, struct wl_touch *touch, uint32_t seri
     }
 }
 
-static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial,
-                             uint32_t timestamp, int id)
+static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial, uint32_t timestamp, int id)
 {
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
     wl_fixed_t fx = 0, fy = 0;
@@ -1469,7 +1478,7 @@ static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial
     if (surface) {
         SDL_WindowData *window_data = Wayland_GetWindowDataForOwnedSurface(surface);
 
-        if (window_data) {
+        if (window_data && window_data->surface == surface) {
             const float x = (float)wl_fixed_to_double(fx) / window_data->current.logical_width;
             const float y = (float)wl_fixed_to_double(fy) / window_data->current.logical_height;
 
@@ -1489,8 +1498,7 @@ static void touch_handler_up(void *data, struct wl_touch *touch, uint32_t serial
     }
 }
 
-static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t timestamp,
-                                 int id, wl_fixed_t fx, wl_fixed_t fy)
+static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t timestamp, int id, wl_fixed_t fx, wl_fixed_t fy)
 {
     SDL_WaylandSeat *seat = (SDL_WaylandSeat *)data;
     struct wl_surface *surface = NULL;
@@ -1500,7 +1508,7 @@ static void touch_handler_motion(void *data, struct wl_touch *touch, uint32_t ti
     if (surface) {
         SDL_WindowData *window_data = Wayland_GetWindowDataForOwnedSurface(surface);
 
-        if (window_data) {
+        if (window_data && window_data->surface == surface) {
             const float x = (float)wl_fixed_to_double(fx) / window_data->current.logical_width;
             const float y = (float)wl_fixed_to_double(fy) / window_data->current.logical_height;
 
@@ -2395,9 +2403,9 @@ static void Wayland_SeatDestroyPointer(SDL_WaylandSeat *seat)
 
     // Make sure focus is removed from a surface before the pointer is destroyed.
     if (seat->pointer.focus) {
-        seat->pointer.pending_frame.leave_window = seat->pointer.focus;
+        seat->pointer.pending_frame.leave_surface = seat->pointer.focus->surface;
         pointer_dispatch_leave(seat, false);
-        seat->pointer.pending_frame.leave_window = NULL;
+        seat->pointer.pending_frame.leave_surface = NULL;
     }
 
     SDL_RemoveMouse(seat->pointer.sdl_id);
@@ -3349,7 +3357,7 @@ static void tablet_tool_handle_proximity_in(void *data, struct zwp_tablet_tool_v
 {
     SDL_WaylandPenTool *sdltool = (SDL_WaylandPenTool *) data;
     SDL_WindowData *windowdata = surface ? Wayland_GetWindowDataForOwnedSurface(surface) : NULL;
-    sdltool->focus = windowdata;
+    sdltool->focus = windowdata && windowdata->surface == surface ? windowdata : NULL;
     sdltool->proximity_serial = serial;
     sdltool->frame.have_proximity = true;
     sdltool->frame.in_proximity = true;
@@ -3664,9 +3672,9 @@ void Wayland_DisplayRemoveWindowReferencesFromSeats(SDL_VideoData *display, SDL_
         }
 
         if (seat->pointer.focus == window) {
-            seat->pointer.pending_frame.leave_window = seat->pointer.focus;
+            seat->pointer.pending_frame.leave_surface = seat->pointer.focus->surface;
             pointer_dispatch_leave(seat, true);
-            seat->pointer.pending_frame.leave_window = NULL;
+            seat->pointer.pending_frame.leave_surface = NULL;
         }
 
         // Need the safe loop variant here as cancelling a touch point removes it from the list.
diff --git a/src/video/wayland/SDL_waylandevents_c.h b/src/video/wayland/SDL_waylandevents_c.h
index 4e98e6f69f7be..1a597a35f8b10 100644
--- a/src/video/wayland/SDL_waylandevents_c.h
+++ b/src/video/wayland/SDL_waylandevents_c.h
@@ -236,8 +236,8 @@ typedef struct SDL_WaylandSeat
                 SDL_MouseWheelDirection direction;
             } axis;
 
-            SDL_WindowData *enter_window;
-            SDL_WindowData *leave_window;
+            struct wl_surface *enter_surface;
+            struct wl_surface *leave_surface;
 
             // Event timestamp in nanoseconds
             Uint64 timestamp_ns;
diff --git a/src/video/wayland/SDL_waylandmouse.c b/src/video/wayland/SDL_waylandmouse.c
index ec54994d87c2d..72b9d14d5e372 100644
--- a/src/video/wayland/SDL_waylandmouse.c
+++ b/src/video/wayland/SDL_waylandmouse.c
@@ -1523,6 +1523,23 @@ void Wayland_FiniMouse(SDL_VideoData *data)
 #endif
 }
 
+void Wayland_SeatResetCursor(SDL_WaylandSeat *seat)
+{
+    Wayland_CursorStateResetCursor(&seat->pointer.cursor_state);
+}
+
+void Wayland_SeatSetDefaultCursor(SDL_WaylandSeat *seat)
+{
+    SDL_Mouse *mouse = SDL_GetMouse();
+    SDL_WindowData *pointer_focus = seat->pointer.focus;
+    const Wayland_PointerObject obj = {
+        .wl_pointer = seat->pointer.wl_pointer,
+        .is_pointer = true
+    };
+
+    Wayland_CursorStateSetCursor(&seat->pointer.cursor_state, &obj, pointer_focus, seat->pointer.enter_serial, mouse->def_cursor);
+}
+
 void Wayland_SeatUpdatePointerCursor(SDL_WaylandSeat *seat)
 {
     SDL_Mouse *mouse = SDL_GetMouse();
diff --git a/src/video/wayland/SDL_waylandmouse.h b/src/video/wayland/SDL_waylandmouse.h
index 481cae10739eb..85ba5eef24d40 100644
--- a/src/video/wayland/SDL_waylandmouse.h
+++ b/src/video/wayland/SDL_waylandmouse.h
@@ -27,6 +27,8 @@
 extern void Wayland_InitMouse(SDL_VideoData *data);
 extern void Wayland_FiniMouse(SDL_VideoData *data);
 extern void Wayland_SeatUpdatePointerCursor(SDL_WaylandSeat *seat);
+extern void Wayland_SeatSetDefaultCursor(SDL_WaylandSeat *seat);
+extern void Wayland_SeatResetCursor(SDL_WaylandSeat *seat);
 extern void Wayland_TabletToolUpdateCursor(SDL_WaylandPenTool *tool);
 extern void Wayland_SeatWarpMouse(SDL_WaylandSeat *seat, SDL_WindowData *window, float x, float y);
 extern void Wayland_CursorStateSetFrameCallback(SDL_WaylandCursorState *state, void *userdata);
diff --git a/src/video/wayland/SDL_waylandshmbuffer.c b/src/video/wayland/SDL_waylandshmbuffer.c
index 88da2e9324185..9e355fd77a302 100644
--- a/src/video/wayland/SDL_waylandshmbuffer.c
+++ b/src/video/wayland/SDL_waylandshmbuffer.c
@@ -32,6 +32,7 @@
 
 #include "SDL_waylandshmbuffer.h"
 #include "SDL_waylandvideo.h"
+#include "single-pixel-buffer-v1-client-protocol.h"
 
 static bool SetTempFileSize(int fd, off_t size)
 {
@@ -186,4 +187,28 @@ void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool)
     }
 }
 
+struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a)
+{
+    SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
+
+    // The single-pixel buffer protocol is preferred, as the compositor can choose an optimal format.
+    if (viddata->single_pixel_buffer_manager) {
+        return wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer(viddata->single_pixel_buffer_manager, r, g, b, a);
+    } else {
+        Wayland_SHMPool *pool = Wayland_AllocSHMPool(4);
+        if (!pool) {
+            return NULL;
+        }
+
+        void *mem;
+        struct wl_buffer *wl_buffer = Wayland_AllocBufferFromPool(pool, 1, 1, &mem);
+
+        const Uint8 pixel[4] = { r >> 24, g >> 24, b >> 24, a >> 24 };
+        SDL_memcpy(mem, pixel, sizeof(pixel));
+
+        Wayland_ReleaseSHMPool(pool);
+        return wl_buffer;
+    }
+}
+
 #endif
diff --git a/src/video/wayland/SDL_waylandshmbuffer.h b/src/video/wayland/SDL_waylandshmbuffer.h
index 019793e7edc33..44caa6ed09c4a 100644
--- a/src/video/wayland/SDL_waylandshmbuffer.h
+++ b/src/video/wayland/SDL_waylandshmbuffer.h
@@ -30,4 +30,6 @@ extern Wayland_SHMPool *Wayland_AllocSHMPool(int size);
 extern struct wl_buffer *Wayland_AllocBufferFromPool(Wayland_SHMPool *shmPool, int width, int height, void **data);
 extern void Wayland_ReleaseSHMPool(Wayland_SHMPool *shmPool);
 
+extern struct wl_buffer *Wayland_CreateSinglePixelBuffer(Uint32 r, Uint32 g, Uint32 b, Uint32 a);
+
 #endif
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 2b0cfad3d3c9c..bcefeb74dd98a 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -69,6 +69,7 @@
 #include "color-management-v1-client-protocol.h"
 #include "pointer-warp-v1-client-protocol.h"
 #include "pointer-gestures-unstable-v1-client-protocol.h"
+#include "single-pixel-buffer-v1-client-protocol.h"
 
 #ifdef HAVE_LIBDECOR_H
 #include <libdecor.h>
@@ -653,6 +654,7 @@ static SDL_VideoDevice *Wayland_CreateDevice(bool require_preferred_protocols)
     device->SetWindowResizable = Wayland_SetWindowResizable;
     device->SetWindowPosition = Wayland_SetWindowPosition;
     device->SetWindowSize = Wayland_SetWindowSize;
+    device->SetWindowAspectRatio = Wayland_SetWindowAspectRatio;
     device->SetWindowMinimumSize = Wayland_SetWindowMinimumSize;
     device->SetWindowMaximumSize = Wayland_SetWindowMaximumSize;
     device->SetWindowParent = Wayland_SetWindowParent;
@@ -1278,6 +1280,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin
 
     if (SDL_strcmp(interface, "wl_compositor") == 0) {
         d->compositor = wl_registry_bind(d->registry, id, &wl_compositor_interface, SDL_min(SDL_WL_COMPOSITOR_VERSION, version));
+    } else if (SDL_strcmp(interface, "wl_subcompositor") == 0) {
+        d->subcompositor = wl_registry_bind(d->registry, id, &wl_subcompositor_interface, 1);
     } else if (SDL_strcmp(interface, "wl_output") == 0) {
         Wayland_add_display(d, id, SDL_min(version, SDL_WL_OUTPUT_VERSION));
     } else if (SDL_strcmp(interface, "wl_seat") == 0) {
@@ -1344,6 +1348,8 @@ static void handle_registry_global(void *data, struct wl_registry *registry, uin
     } else if (SDL_strcmp(interface, "zwp_pointer_gestures_v1") == 0) {
         d->zwp_pointer_gestures = wl_registry_bind(d->registry, id, &zwp_pointer_gestures_v1_interface, SDL_min(version, 3));
         Wayland_DisplayInitPointerGestureManager(d);
+    } else if (SDL_strcmp(interface, "wp_single_pixel_buffer_manager_v1") == 0) {
+        d->single_pixel_buffer_manager = wl_registry_bind(d->registry, id, &wp_single_pixel_buffer_manager_v1_interface, 1);
     }
 #ifdef SDL_WL_FIXES_VERSION
     else if (SDL_strcmp(interface, "wl_fixes") == 0) {
@@ -1692,6 +1698,16 @@ static void Wayland_VideoCleanup(SDL_VideoDevice *_this)
         data->zwp_pointer_gestures = NULL;
     }
 
+    if (data->single_pixel_buffer_manager) {
+        wp_single_pixel_buffer_manager_v1_destroy(data->single_pixel_buffer_manager);
+        data->single_pixel_buffer_manager = NULL;
+    }
+
+    if (data->subcompositor) {
+        wl_subcompositor_destroy(data->subcompositor);
+        data->subcompositor = NULL;
+    }
+
     if (data->compositor) {
         wl_compositor_destroy(data->compositor);
         data->compositor = NULL;
diff --git a/src/video/wayland/SDL_waylandvideo.h b/src/video/wayland/SDL_waylandvideo.h
index 3542b48afa04a..8578aa7650a67 100644
--- a/src/video/wayland/SDL_waylandvideo.h
+++ b/src/video/wayland/SDL_waylandvideo.h
@@ -61,6 +61,7 @@ struct SDL_VideoData
         struct libdecor *libdecor;
 #endif
     } shell;
+    struct wl_subcompositor *subcompositor;
     struct zwp_relative_pointer_manager_v1 *relative_pointer_manager;
     struct zwp_pointer_constraints_v1 *pointer_constraints;
     struct wp_pointer_warp_v1 *wp_pointer_warp_v1;
@@ -85,6 +86,7 @@ struct SDL_VideoData
     struct zwp_tablet_manager_v2 *tablet_manager;
     struct wl_fixes *wl_fixes;
     struct zwp_pointer_gestures_v1 *zwp_pointer_gestures;
+    struct wp_single_pixel_buffer_manager_v1 *single_pixel_buffer_manager;
 
     struct xkb_context *xkb_context;
 
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 5dac6f22a84c5..7fdaa52b83a01 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -117,39 +117,15 @@ static enum WaylandModeScale GetModeScaleMethod(void)
     return scale_mode;
 }
 
-static void GetBufferSize(SDL_Window *window, int *width, int *height)
-{
-    SDL_WindowData *data = window->internal;
-    int buf_width;
-    int buf_height;
-
-    // Exclusive fullscreen modes always have a pixel density of 1
-    if (data->is_fullscreen && window->fullscreen_exclusive) {
-        buf_width = window->current_fullscreen_mode.w;
-        buf_height = window->current_fullscreen_mode.h;
-    } else if (!data->scale_to_display) {
-        // Round fractional backbuffer sizes halfway away from zero.
-        buf_width = PointToPixel(window, data->requested.logical_width);
-        buf_height = PointToPixel(window, data->requested.logical_height);
-    } else {
-        buf_width = data->requested.pixel_width;
-        buf_height = data->requested.pixel_height;
-    }
-
-    if (width) {
-        *width = buf_width;
-    }
-    if (height) {
-        *height = buf_height;
-    }
-}
-
 static void SetMinMaxDimensions(SDL_Window *window)
 {
     SDL_WindowData *wind = window->internal;
     int min_width, min_height, max_width, max_height;
 
-    if ((window->flags & SDL_WINDOW_FULLSCREEN) || wind->fullscreen_deadline_count) {
+    /* Keep the limits off while the window is in a fixed-size state, or the controls
+     * to exit that state may be disabled.
+     */
+    if (window->flags & (SDL_WINDOW_FULLSCREEN | SDL_WINDOW_MAXIMIZED)) {
         min_width = 0;
         min_height = 0;
         max_width = 0;
@@ -184,6 +160,13 @@ static void SetMinMaxDimensions(SDL_Window *window)
         if (!wind->shell_surface.libdecor.frame) {
             return; // Can't do anything yet, wait for ShowWindow
         }
+
+        if (min_width && min_height && min_width == max_width && min_height == max_height) {
+            libdecor_frame_unset_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE);
+        } else {
+            libdecor_frame_set_capabilities(wind->shell_surface.libdecor.frame, LIBDECOR_ACTION_RESIZE);
+        }
+
         /* No need to change these values if the window is non-resizable,
          * as libdecor will just overwrite them internally.
          */
@@ -286,18 +269,17 @@ static void RepositionPopup(SDL_Window *window, bool use_current_position)
     }
 }
 
-static void SetSurfaceOpaqueRegion(SDL_WindowData *wind, bool is_opaque)
+static void SetSurfaceOpaqueRegion(struct wl_surface *surface, int width, int height)
 {
-    SDL_VideoData *viddata = wind->waylandData;
+    SDL_VideoData *viddata = SDL_GetVideoDevice()->internal;
 
-    if (is_opaque) {
+    if (width && height) {
         struct wl_region *region = wl_compositor_create_region(viddata->compositor);
-        wl_region_add(region, 0, 0,
-                      wind->current.logical_width, wind->current.logical_height);
-        wl_surface_set_opaque_region(wind->surface, region);
+        wl_region_add(region, 0, 0, width, height);
+        wl_surface_set_opaque_region(surface, region);
         wl_region_destroy(region);
     } else {
-        wl_surface_set_opaque_region(wind->surface, NULL);
+        wl_surface_set_opaque_region(surface, NULL);
     }
 }
 
@@ -307,29 +289,19 @@ static void ConfigureWindowGeometry(SDL_Window *window)
     const double scale_factor = GetWindowScale(window);
     const int old_pixel_width = data->current.pixel_width;
     const int old_pixel_height = data->current.pixel_height;
-    int window_width, window_height;
+    int window_width = 0;
+    int window_height = 0;
+    int viewport_width, viewport_height;
     bool window_size_changed;
-
-    // Set the drawable backbuffer size.
-    GetBufferSize(window, &data->current.pixel_width, &data->current.pixel_height);
-    const bool buffer_size_changed = data->current.pixel_width != old_pixel_width ||
-                                         data->current.pixel_height != old_pixel_height;
-
-    if (data->egl_window && buffer_size_changed) {
-        WAYLAND_wl_egl_window_resize(data->egl_window,
-                                     data->current.pixel_width,
-                                     data->current.pixel_height,
-                                     0, 0);
-    }
+    bool buffer_size_changed;
+    const bool is_opaque = !(window->flags & SDL_WINDOW_TRANSPARENT) && window->opacity == 1.0f;
 
     if (data->is_fullscreen && window->fullscreen_exclusive) {
-        int output_width;
-        int output_height;
         window_width = window->current_fullscreen_mode.w;
         window_height = window->current_fullscreen_mode.h;
 
-        output_width = data->requested.logical_width;
-        output_height = data->requested.logical_height;
+        viewport_width = data->requested.logical_width;
+        viewport_height = data->requested.logical_height;
 
         switch (GetModeScaleMethod()) {
         case WAYLAND_MODE_SCALE_NONE:
@@ -337,40 +309,50 @@ static void ConfigureWindowGeometry(SDL_Window *window)
              * Windows can request a smaller size, but exceeding these dimensions is a protocol violation,
              * thus, modes that exceed the output size still need to be scaled with a viewport.
              */
-            if (window_width <= output_width && window_height <= output_height) {
-                output_width = window_width;
-                output_height = window_height;
+            if (window_width <= viewport_width && window_height <= viewport_height) {
+                viewport_width = window_width;
+                viewport_height = window_height;
 
                 break;
             }
             SDL_FALLTHROUGH;
         case WAYLAND_MODE_SCALE_ASPECT:
         {
-            const float output_ratio = (float)output_width / (float)output_height;
+            const float output_ratio = (float)viewport_width / (float)viewport_height;
             const float mode_ratio = (float)window_width / (float)window_height;
 
             if (output_ratio > mode_ratio) {
-                output_width = SDL_lroundf((float)window_width * ((float)output_height / (float)window_height));
+                viewport_width = SDL_lroundf((float)window_width * ((float)viewport_height / (float)window_height));
             } else if (output_ratio < mode_ratio) {
-                output_height = SDL_lroundf((float)window_height * ((float)output_width / (float)window_width));
+                viewport_height = SDL_lroundf((float)window_height * ((float)viewport_width / (float)window_width));
             }
         } break;
         default:
             break;
         }
 
-        window_size_changed = window_width != window->w || window_height != window->h ||
-            data->current.logical_width != output_width || data->current.logical_height != output_height;
+        window_size_changed = window_width != window->w ||
+                              window_height != window->h ||
+                              data->current.viewport_width != viewport_width ||
+                              data->current.viewport_height != viewport_height;
+
+        // Exclusive fullscreen window sizes are always in pixel units.
+        data->current.pixel_width = window_width;
+        data->current.pixel_height = window_height;
+        buffer_size_changed = data->current.pixel_width != old_pixel_width ||
+                              data->current.pixel_height != old_pixel_height;
 
         if (window_size_changed || buffer_size_changed) {
             if (data->viewport) {
-                wp_viewport_set_destination(data->viewport, output_width, output_height);
+                wp_viewport_set_destination(data->viewport, viewport_width, viewport_height);
 
-                data->current.logical_width = output_width;
-                data->current.logical_height = output_height;
+                data->current.logical_width = data->requested.logical_width;
+                data->current.logical_height = data->requested.logical_height;
+                data->current.viewport_width = viewport_width;
+                data->current.viewport_height = viewport_height;
             } else {
                 // Calculate the integer scale from the mode and output.
-                const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / output_width, 1);
+                const int32_t int_scale = SDL_max(window->current_fullscreen_mode.w / viewport_width, 1);
 
                 wl_surface_set_buffer_scale(data->surface, int_scale);
                 data->current.logical_width = window->current_fullscreen_mode.w;
@@ -381,33 +363,184 @@ static void ConfigureWindowGeometry(SDL_Window *window)
             data->pointer_scale.y = (double)window_height / (double)data->current.logical_height;
         }
     } else {
-        window_width = data->requested.logical_width;
-        window_height = data->requested.logical_height;
+        if (!data->scale_to_display) {
+            viewport_width = data->requested.logical_width;
+            viewport_height = data->requested.logical_height;
+        } else {
+            viewport_width = data->requested.pixel_width;
+            viewport_height = data->requested.pixel_height;
+        }
+
+        if (data->viewport && data->waylandData->subcompositor && !data->is_fullscreen) {
+            if (window->min_w) {
+                window_width = viewport_width = SDL_max(viewport_width, window->min_w);
+            }
+            if (window->min_h) {
+                window_height = viewport_height = SDL_max(viewport_height, window->min_h);
+            }
+            if (window->max_w) {
+                window_width = viewport_width = SDL_min(viewport_width, window->max_w);
+            }
+            if (window->max_h) {
+                window_height = viewport_height = SDL_min(viewport_height, window->max_h);
+            }
+
+            float aspect = (float)viewport_width / (float)viewport_height;
+            if (window->min_aspect != 0.f && aspect < window->min_aspect) {
+                viewport_height = SDL_lroundf((float)viewport_width / window->min_aspect);
+            } else if (window->max_aspect != 0.f && aspect > window->max_aspect) {
+                viewport_width = SDL_lroundf((float)viewport_height * window->max_aspect);
+            }
+
+            // At this point, the viewport matches the window dimensions, but the viewport might be clamped to window dimensions beyond here.
+            window_width = viewport_width;
+            window_height = viewport_height;
+
+            // If the viewport bounds exceed the window size, scale them while maintaining the aspect ratio.
+            if (!data->scale_to_display) {
+                if (viewport_width > data->requested.logical_width || viewport_height > data->requested.logical_height) {
+                    aspect = (float)viewport_width / (float)viewport_height;
+                    const float window_ratio = (float)data->requested.logical_width / (float)data->requested.logical_height;
+                    if (aspect >= window_ratio) {
+                        viewport_width = data->requested.logical_wid

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