SDL: video: Handle moving exclusive fullscreen windows between displays

From 3f5ef7dd422057edbcf3e736107e34be4b75d9ba Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Sat, 11 Feb 2023 15:03:45 -0500
Subject: [PATCH] video: Handle moving exclusive fullscreen windows between
 displays

Desktops can move windows, even exclusive fullscreen windows, from one display to another. To handle this, windows now hold two fullscreen modes: the desired mode, which is considered mutable only to the application, and the current mode. When a fullscreen request is made, the current mode is initially set to the desired mode for the initial fullscreen transition. If an exclusive fullscreen window is moved to a new display, the new display is checked to see if it has a mode compatible with the desired mode. If it does, the compatible mode is used so the windows will have the same properties on the new display. If no compatible mode is found, the window becomes desktop fullscreen. This occurs whenever the window is moved to ensure that an attempt will always be made to use the application's requested mode, if possible.

Exiting and reentering fullscreen results in the desired mode being restored on the display specified by it.
---
 src/video/SDL_sysvideo.h              |  5 +-
 src/video/SDL_video.c                 | 90 +++++++++++++++------------
 src/video/cocoa/SDL_cocoawindow.m     |  2 +-
 src/video/kmsdrm/SDL_kmsdrmvulkan.c   |  2 +-
 src/video/wayland/SDL_waylandvideo.c  |  8 +--
 src/video/wayland/SDL_waylandwindow.c | 22 +++----
 6 files changed, 71 insertions(+), 58 deletions(-)

diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index d444af0bd092..b16f04529a6c 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -107,13 +107,14 @@ struct SDL_Window
     int last_pixel_w, last_pixel_h;
     Uint32 flags;
     SDL_bool fullscreen_exclusive;  /* The window is currently fullscreen exclusive */
-    SDL_bool last_fullscreen_exclusive;  /* The last fullscreen_exclusive setting */
+    SDL_DisplayID last_fullscreen_exclusive_display;  /* The last fullscreen_exclusive display */
     SDL_DisplayID last_displayID;
 
     /* Stored position and size for windowed mode */
     SDL_Rect windowed;
 
-    SDL_DisplayMode fullscreen_mode;
+    SDL_DisplayMode requested_fullscreen_mode;
+    SDL_DisplayMode current_fullscreen_mode;
 
     float opacity;
 
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index e2e2673afa00..a1a60a939f5a 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -1264,7 +1264,7 @@ SDL_DisplayID SDL_GetDisplayForWindow(SDL_Window *window)
 
     /* An explicit fullscreen display overrides all */
     if (window->flags & SDL_WINDOW_FULLSCREEN) {
-        displayID = window->fullscreen_mode.displayID;
+        displayID = window->current_fullscreen_mode.displayID;
     }
 
     if (!displayID && _this->GetDisplayForWindow) {
@@ -1311,11 +1311,12 @@ void SDL_CheckWindowDisplayChanged(SDL_Window *window)
                         SDL_VideoDisplay *new_display = &_this->displays[display_index];
 
                         /* The window was moved to a different display */
-                        if (new_display->fullscreen_window != NULL) {
-                            /* Uh oh, there's already a fullscreen window here */
-                        } else {
-                            new_display->fullscreen_window = window;
+                        if (new_display->fullscreen_window &&
+                            new_display->fullscreen_window != window) {
+                            /* Uh oh, there's already a fullscreen window here; minimize it */
+                            SDL_MinimizeWindow(new_display->fullscreen_window);
                         }
+                        new_display->fullscreen_window = window;
                         display->fullscreen_window = NULL;
                     }
                 }
@@ -1343,7 +1344,7 @@ static void SDL_RestoreMousePosition(SDL_Window *window)
 
 static int SDL_UpdateFullscreenMode(SDL_Window *window, SDL_bool fullscreen)
 {
-    SDL_VideoDisplay *display;
+    SDL_VideoDisplay *display = NULL;
     SDL_DisplayMode *mode = NULL;
 
     CHECK_WINDOW_MAGIC(window, -1);
@@ -1369,17 +1370,17 @@ static int SDL_UpdateFullscreenMode(SDL_Window *window, SDL_bool fullscreen)
        do nothing, or else we may trigger an ugly double-transition
      */
     if (SDL_strcmp(_this->name, "cocoa") == 0) { /* don't do this for X11, etc */
-        if (window->is_destroying && !window->last_fullscreen_exclusive) {
+        if (window->is_destroying && !window->last_fullscreen_exclusive_display) {
             window->fullscreen_exclusive = SDL_FALSE;
             goto done;
         }
 
         /* If we're switching between a fullscreen Space and exclusive fullscreen, we need to get back to normal first. */
-        if (fullscreen && !window->last_fullscreen_exclusive && window->fullscreen_exclusive) {
+        if (fullscreen && !window->last_fullscreen_exclusive_display && window->fullscreen_exclusive) {
             if (!Cocoa_SetWindowFullscreenSpace(window, SDL_FALSE)) {
                 goto error;
             }
-        } else if (fullscreen && window->last_fullscreen_exclusive && !window->fullscreen_exclusive) {
+        } else if (fullscreen && window->last_fullscreen_exclusive_display && !window->fullscreen_exclusive) {
             display = SDL_GetVideoDisplayForWindow(window);
             SDL_SetDisplayModeForDisplay(display, NULL);
             if (_this->SetWindowFullscreen) {
@@ -1421,12 +1422,12 @@ static int SDL_UpdateFullscreenMode(SDL_Window *window, SDL_bool fullscreen)
 #endif
 
     /* Restore the video mode on other displays if needed */
-    if (window->last_fullscreen_exclusive) {
+    if (window->last_fullscreen_exclusive_display) {
         int i;
 
         for (i = 0; i < _this->num_displays; ++i) {
             SDL_VideoDisplay *other = &_this->displays[i];
-            if (display != other && other->fullscreen_window == window) {
+            if (display != other && other->id == window->last_fullscreen_exclusive_display) {
                 SDL_SetDisplayModeForDisplay(other, NULL);
             }
         }
@@ -1501,7 +1502,7 @@ static int SDL_UpdateFullscreenMode(SDL_Window *window, SDL_bool fullscreen)
     }
 
 done:
-    window->last_fullscreen_exclusive = window->fullscreen_exclusive;
+    window->last_fullscreen_exclusive_display = display && window->fullscreen_exclusive ? display->id : 0;
     return 0;
 
 error:
@@ -1523,13 +1524,16 @@ int SDL_SetWindowFullscreenMode(SDL_Window *window, const SDL_DisplayMode *mode)
         }
 
         /* Save the mode so we can look up the closest match later */
-        SDL_memcpy(&window->fullscreen_mode, mode, sizeof(window->fullscreen_mode));
+        SDL_memcpy(&window->requested_fullscreen_mode, mode, sizeof(window->requested_fullscreen_mode));
     } else {
-        SDL_zero(window->fullscreen_mode);
+        SDL_zero(window->requested_fullscreen_mode);
     }
 
-    if (SDL_WINDOW_FULLSCREEN_VISIBLE(window)) {
-        SDL_UpdateFullscreenMode(window, SDL_TRUE);
+    if (window->flags & SDL_WINDOW_FULLSCREEN) {
+        SDL_memcpy(&window->current_fullscreen_mode, &window->requested_fullscreen_mode, sizeof(window->current_fullscreen_mode));
+        if (SDL_WINDOW_FULLSCREEN_VISIBLE(window)) {
+            SDL_UpdateFullscreenMode(window, SDL_TRUE);
+        }
     }
 
     return 0;
@@ -1539,7 +1543,11 @@ const SDL_DisplayMode *SDL_GetWindowFullscreenMode(SDL_Window *window)
 {
     CHECK_WINDOW_MAGIC(window, NULL);
 
-    return SDL_GetFullscreenModeMatch(&window->fullscreen_mode);
+    if (window->flags & SDL_WINDOW_FULLSCREEN) {
+        return SDL_GetFullscreenModeMatch(&window->current_fullscreen_mode);
+    } else {
+        return SDL_GetFullscreenModeMatch(&window->requested_fullscreen_mode);
+    }
 }
 
 void *SDL_GetWindowICCProfile(SDL_Window *window, size_t *size)
@@ -2613,6 +2621,7 @@ int SDL_RestoreWindow(SDL_Window *window)
 
 int SDL_SetWindowFullscreen(SDL_Window *window, SDL_bool fullscreen)
 {
+    int ret = 0;
     Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN : 0;
 
     CHECK_WINDOW_MAGIC(window, -1);
@@ -2624,7 +2633,19 @@ int SDL_SetWindowFullscreen(SDL_Window *window, SDL_bool fullscreen)
     /* Clear the previous flags and OR in the new ones */
     window->flags = (window->flags & ~SDL_WINDOW_FULLSCREEN) | flags;
 
-    return SDL_UpdateFullscreenMode(window, SDL_WINDOW_FULLSCREEN_VISIBLE(window));
+    if (fullscreen) {
+        /* Set the current fullscreen mode to the desired mode */
+        SDL_memcpy(&window->current_fullscreen_mode, &window->requested_fullscreen_mode, sizeof(window->current_fullscreen_mode));
+    }
+
+    ret = SDL_UpdateFullscreenMode(window, SDL_WINDOW_FULLSCREEN_VISIBLE(window));
+
+    if (!fullscreen || ret != 0) {
+        /* Clear the current fullscreen mode. */
+        SDL_zero(window->current_fullscreen_mode);
+    }
+
+    return ret;
 }
 
 static SDL_Surface *SDL_CreateWindowFramebuffer(SDL_Window *window)
@@ -2983,30 +3004,21 @@ void SDL_OnWindowHidden(SDL_Window *window)
 void SDL_OnWindowDisplayChanged(SDL_Window *window)
 {
     if (window->flags & SDL_WINDOW_FULLSCREEN) {
-        SDL_Rect rect;
+        SDL_DisplayID displayID = SDL_GetDisplayForWindowPosition(window);
+        const SDL_DisplayMode *new_mode = NULL;
 
-        if (SDL_WINDOW_FULLSCREEN_VISIBLE(window) && window->fullscreen_exclusive) {
-            SDL_UpdateFullscreenMode(window, SDL_TRUE);
+        if (window->current_fullscreen_mode.pixel_w != 0 || window->current_fullscreen_mode.pixel_h != 0) {
+            new_mode = SDL_GetClosestFullscreenDisplayMode(displayID, window->current_fullscreen_mode.pixel_w, window->current_fullscreen_mode.pixel_h, window->current_fullscreen_mode.refresh_rate);
         }
 
-        /*
-         * If mode switching is being emulated, the display bounds don't necessarily reflect the
-         * emulated mode dimensions since the window is just being scaled.
-         */
-        if (!ModeSwitchingEmulated(_this) &&
-            SDL_GetDisplayBounds(SDL_GetDisplayForWindow(window), &rect) == 0) {
-            int old_w = window->w;
-            int old_h = window->h;
-            window->x = rect.x;
-            window->y = rect.y;
-            window->w = rect.w;
-            window->h = rect.h;
-            if (_this->SetWindowSize) {
-                _this->SetWindowSize(_this, window);
-            }
-            if (window->w != old_w || window->h != old_h) {
-                SDL_OnWindowResized(window);
-            }
+        if (new_mode) {
+            SDL_memcpy(&window->current_fullscreen_mode, new_mode, sizeof(window->current_fullscreen_mode));
+        } else {
+            SDL_zero(window->current_fullscreen_mode);
+        }
+
+        if (SDL_WINDOW_FULLSCREEN_VISIBLE(window)) {
+            SDL_UpdateFullscreenMode(window, SDL_TRUE);
         }
     }
 
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 1b7cdd12e535..6cd9af61a6c0 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -570,7 +570,7 @@ - (BOOL)setFullscreenSpace:(BOOL)state
         return NO; /* Spaces are forcibly disabled. */
     } else if (state && window->fullscreen_exclusive) {
         return NO; /* we only allow you to make a Space on fullscreen desktop windows. */
-    } else if (!state && window->last_fullscreen_exclusive) {
+    } else if (!state && window->last_fullscreen_exclusive_display) {
         return NO; /* we only handle leaving the Space on windows that were previously fullscreen desktop. */
     } else if (state == isFullscreenSpace) {
         return YES; /* already there. */
diff --git a/src/video/kmsdrm/SDL_kmsdrmvulkan.c b/src/video/kmsdrm/SDL_kmsdrmvulkan.c
index 889e72c5486a..da524de4c440 100644
--- a/src/video/kmsdrm/SDL_kmsdrmvulkan.c
+++ b/src/video/kmsdrm/SDL_kmsdrmvulkan.c
@@ -371,7 +371,7 @@ SDL_bool KMSDRM_Vulkan_CreateSurface(_THIS,
         new_mode_parameters.visibleRegion.height = window->h;
         /* SDL (and DRM, if we look at drmModeModeInfo vrefresh) uses plain integer Hz for
            display mode refresh rate, but Vulkan expects higher precision. */
-        new_mode_parameters.refreshRate = window->fullscreen_mode.refresh_rate * 1000;
+        new_mode_parameters.refreshRate = window->current_fullscreen_mode.refresh_rate * 1000;
 
         SDL_zero(display_mode_create_info);
         display_mode_create_info.sType = VK_STRUCTURE_TYPE_DISPLAY_MODE_CREATE_INFO_KHR;
diff --git a/src/video/wayland/SDL_waylandvideo.c b/src/video/wayland/SDL_waylandvideo.c
index 18038061b503..d706303053da 100644
--- a/src/video/wayland/SDL_waylandvideo.c
+++ b/src/video/wayland/SDL_waylandvideo.c
@@ -957,10 +957,10 @@ static int Wayland_GetDisplayBounds(_THIS, SDL_VideoDisplay *display, SDL_Rect *
     if (display->fullscreen_window &&
         display->fullscreen_window->fullscreen_exclusive &&
         display->fullscreen_window == SDL_GetFocusWindow() &&
-        display->fullscreen_window->fullscreen_mode.screen_w != 0 &&
-        display->fullscreen_window->fullscreen_mode.screen_h != 0) {
-        rect->w = display->fullscreen_window->fullscreen_mode.screen_w;
-        rect->h = display->fullscreen_window->fullscreen_mode.screen_h;
+        display->fullscreen_window->current_fullscreen_mode.screen_w != 0 &&
+        display->fullscreen_window->current_fullscreen_mode.screen_h != 0) {
+        rect->w = display->fullscreen_window->current_fullscreen_mode.screen_w;
+        rect->h = display->fullscreen_window->current_fullscreen_mode.screen_h;
     } else {
         rect->w = display->current_mode->screen_w;
         rect->h = display->current_mode->screen_h;
diff --git a/src/video/wayland/SDL_waylandwindow.c b/src/video/wayland/SDL_waylandwindow.c
index 1b98e7c8f88b..f77feb7ecd9f 100644
--- a/src/video/wayland/SDL_waylandwindow.c
+++ b/src/video/wayland/SDL_waylandwindow.c
@@ -56,7 +56,7 @@ SDL_FORCE_INLINE SDL_bool FloatEqual(float a, float b)
 static SDL_bool SurfaceScaleIsFractional(SDL_Window *window)
 {
     SDL_WindowData *data = window->driverdata;
-    const float scale_value = !(window->fullscreen_exclusive) ? data->windowed_scale_factor : window->fullscreen_mode.display_scale;
+    const float scale_value = !(window->fullscreen_exclusive) ? data->windowed_scale_factor : window->current_fullscreen_mode.display_scale;
     return !FloatEqual(SDL_roundf(scale_value), scale_value);
 }
 
@@ -77,7 +77,7 @@ static SDL_bool WindowNeedsViewport(SDL_Window *window)
         if (SurfaceScaleIsFractional(window)) {
             return SDL_TRUE;
         } else if (window->fullscreen_exclusive) {
-            if (window->fullscreen_mode.screen_w != output_width || window->fullscreen_mode.screen_h != output_height) {
+            if (window->current_fullscreen_mode.screen_w != output_width || window->current_fullscreen_mode.screen_h != output_height) {
                 return SDL_TRUE;
             }
         }
@@ -93,8 +93,8 @@ static void GetBufferSize(SDL_Window *window, int *width, int *height)
     int buf_height;
 
     if (window->fullscreen_exclusive) {
-        buf_width = window->fullscreen_mode.pixel_w;
-        buf_height = window->fullscreen_mode.pixel_h;
+        buf_width = window->current_fullscreen_mode.pixel_w;
+        buf_height = window->current_fullscreen_mode.pixel_h;
     } else {
         /* Round fractional backbuffer sizes halfway away from zero. */
         buf_width = (int)SDL_lroundf((float)data->requested_window_width * data->windowed_scale_factor);
@@ -160,8 +160,8 @@ static void ConfigureWindowGeometry(SDL_Window *window)
         /* If the compositor supplied fullscreen dimensions, use them, otherwise fall back to the display dimensions. */
         const int output_width = data->requested_window_width ? data->requested_window_width : output->screen_width;
         const int output_height = data->requested_window_height ? data->requested_window_height : output->screen_height;
-        window_width = window->fullscreen_mode.screen_w;
-        window_height = window->fullscreen_mode.screen_h;
+        window_width = window->current_fullscreen_mode.screen_w;
+        window_height = window->current_fullscreen_mode.screen_h;
 
         window_size_changed = window_width != window->w || window_height != window->h ||
             data->wl_window_width != output_width || data->wl_window_height != output_height;
@@ -178,10 +178,10 @@ static void ConfigureWindowGeometry(SDL_Window *window)
             } else {
                 /* Always use the mode dimensions for integer scaling. */
                 UnsetDrawSurfaceViewport(window);
-                wl_surface_set_buffer_scale(data->surface, (int32_t)window->fullscreen_mode.display_scale);
+                wl_surface_set_buffer_scale(data->surface, (int32_t)window->current_fullscreen_mode.display_scale);
 
-                data->wl_window_width = window->fullscreen_mode.screen_w;
-                data->wl_window_height = window->fullscreen_mode.screen_h;
+                data->wl_window_width = window->current_fullscreen_mode.screen_w;
+                data->wl_window_height = window->current_fullscreen_mode.screen_h;
             }
 
             data->pointer_scale_x = (float)window_width / (float)data->wl_window_width;
@@ -554,7 +554,7 @@ static void handle_configure_xdg_toplevel(void *data,
          * place the fullscreen window is unknown.
          */
         if (window->fullscreen_exclusive && !wind->fullscreen_was_positioned) {
-            SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->fullscreen_mode.displayID);
+            SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->current_fullscreen_mode.displayID);
             if (disp) {
                 wind->fullscreen_was_positioned = SDL_TRUE;
                 xdg_toplevel_set_fullscreen(xdg_toplevel, disp->driverdata->output);
@@ -768,7 +768,7 @@ static void decoration_frame_configure(struct libdecor_frame *frame,
          * place the fullscreen window is unknown.
          */
         if (window->fullscreen_exclusive && !wind->fullscreen_was_positioned) {
-            SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->fullscreen_mode.displayID);
+            SDL_VideoDisplay *disp = SDL_GetVideoDisplay(window->current_fullscreen_mode.displayID);
             if (disp) {
                 wind->fullscreen_was_positioned = SDL_TRUE;
                 libdecor_frame_set_fullscreen(frame, disp->driverdata->output);