SDL: HighDPI: remove SWP_NOSIZE in WIN_SetWindowPosition

From d3b970d4d5d6d640e51f422894c3dea8daf8e6c7 Mon Sep 17 00:00:00 2001
From: Eric Wasylishen <[EMAIL REDACTED]>
Date: Sun, 29 May 2022 21:56:37 -0600
Subject: [PATCH] HighDPI: remove SWP_NOSIZE in WIN_SetWindowPosition

If the move results in a DPI change, we need to allow the window to resize (e.g. AdjustWindowRectExForDpi frame sizes are different).

- WM_DPICHANGED: Don't assume WM_GETDPISCALEDSIZE is always called for PMv2 awareness - it's only called during interactive dragging.

- WIN_AdjustWindowRectWithStyle: always calculate final window size including frame based on the destination rect,
not based on the current window DPI.

- Update wmmsg.h to include WM_GETDPISCALEDSIZE (for WMMSG_DEBUG)

- WIN_AdjustWindowRectWithStyle: add optional logging

- WM_GETMINMAXINFO: add optional HIGHDPI_DEBUG logging

- WM_DPICHANGED: fix potentially clobbering data->expected_resize

Together these changes fix the following scenario:
- launch testwm2 with the SDL_WINDOWS_DPI_AWARENESS=permonitorv2 environment variable
- Windows 10 21H2 (OS Build 19044.1706)
- Left (primary) monitor: 3840x2160, 125% scaling
- Right (secondary) monitor: 2560x1440, 100% scaling

- Alt+Enter, Alt+Enter (to enter + leave desktop fullscreen), Alt+Right (to move window to right monitor). Ensure the window client area stays 640x480. Drag the window back to the 125% monitor, ensure client area stays 640x480.
---
 src/video/windows/SDL_windowsevents.c | 49 ++++++++++++++---
 src/video/windows/SDL_windowswindow.c | 75 +++++++++++++--------------
 src/video/windows/wmmsg.h             |  2 +-
 3 files changed, 80 insertions(+), 46 deletions(-)

diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 22780ad8489..4130f97068e 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -1088,19 +1088,25 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
                    inside their function, so I have to do it here.
                  */
                 BOOL menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
+                UINT dpi;
+                
+                dpi = 96;
                 size.top = 0;
                 size.left = 0;
                 size.bottom = h;
                 size.right = w;
 
                 if (WIN_IsPerMonitorV2DPIAware(SDL_GetVideoDevice())) {
-                    UINT dpi = data->videodata->GetDpiForWindow(hwnd);
+                    dpi = data->videodata->GetDpiForWindow(hwnd);
                     data->videodata->AdjustWindowRectExForDpi(&size, style, menu, 0, dpi);
                 } else {
                     AdjustWindowRectEx(&size, style, menu, 0);
                 }
                 w = size.right - size.left;
                 h = size.bottom - size.top;
+#ifdef HIGHDPI_DEBUG
+                SDL_Log("WM_GETMINMAXINFO: max window size: %dx%d using dpi: %u", w, h, dpi);
+#endif
             }
 
             /* Fix our size to the current size */
@@ -1422,7 +1428,10 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
 
     case WM_GETDPISCALEDSIZE:
         /* Windows 10 Creators Update+ */
-        /* Documented as only being sent to windows that are per-monitor V2 DPI aware. */
+        /* Documented as only being sent to windows that are per-monitor V2 DPI aware.
+           
+           Experimentation shows it's only sent during interactive dragging, not in response to
+           SetWindowPos. */
         if (data->videodata->GetDpiForWindow && data->videodata->AdjustWindowRectExForDpi) {
             /* Windows expects applications to scale their window rects linearly
                when dragging between monitors with different DPI's.
@@ -1490,6 +1499,7 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
         {
             const int newDPI = HIWORD(wParam);
             RECT* const suggestedRect = (RECT*)lParam;
+            SDL_bool setExpectedResize = SDL_FALSE;
             int w, h;
 
 #ifdef HIGHDPI_DEBUG
@@ -1497,12 +1507,28 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
                 suggestedRect->left, suggestedRect->top, suggestedRect->right - suggestedRect->left, suggestedRect->bottom - suggestedRect->top);
 #endif
 
-            /* DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 means that
-               WM_GETDPISCALEDSIZE will have been called, so we can use suggestedRect. */
+            if (data->expected_resize) {
+                /* This DPI change is coming from an explicit SetWindowPos call within SDL.
+                   Assume all call sites are calculating the DPI-aware frame correctly, so
+                   we don't need to do any further adjustment. */
+#ifdef HIGHDPI_DEBUG
+                SDL_Log("WM_DPICHANGED: Doing nothing, assuming window is already sized correctly");
+#endif
+                return 0;
+            }
+
+            /* Interactive user-initiated resizing/movement */
+
             if (WIN_IsPerMonitorV2DPIAware(SDL_GetVideoDevice())) {
+                /* WM_GETDPISCALEDSIZE should have been called prior, so we can trust the given
+                   suggestedRect. */
                 w = suggestedRect->right - suggestedRect->left;
                 h = suggestedRect->bottom - suggestedRect->top;
-            } else {
+
+#ifdef HIGHDPI_DEBUG
+                SDL_Log("WM_DPICHANGED: using suggestedRect");
+#endif
+            } else {               
                 RECT rect = { 0, 0, data->window->w, data->window->h };
                 const DWORD style = GetWindowLong(hwnd, GWL_STYLE);
                 const BOOL menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
@@ -1521,7 +1547,10 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
                 suggestedRect->left, suggestedRect->top, w, h);
 #endif
 
-            data->expected_resize = SDL_TRUE;
+            if (!data->expected_resize) {
+                setExpectedResize = SDL_TRUE;
+                data->expected_resize = SDL_TRUE;
+            }            
             SetWindowPos(hwnd,
                 NULL,
                 suggestedRect->left,
@@ -1529,7 +1558,13 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
                 w,
                 h,
                 SWP_NOZORDER | SWP_NOACTIVATE);
-            data->expected_resize = SDL_FALSE;
+            if (setExpectedResize) {
+                /* Only unset data->expected_resize if we set it above.
+                   WM_DPICHANGED can happen inside a block of code that sets data->expected_resize,
+                   e.g. WIN_SetWindowPositionInternal.
+                */
+                data->expected_resize = SDL_FALSE;
+            }
             return 0;
         }
         break;
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index e2289f3adeb..50420dcbff5 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -46,6 +46,8 @@
 #define SWP_NOCOPYBITS 0
 #endif
 
+/* #define HIGHDPI_DEBUG */
+
 /* Fake window to help with DirectInput events. */
 HWND SDL_HelperWindow = NULL;
 static const TCHAR *SDL_HelperWindowClassName = TEXT("SDLHelperWindowInputCatcher");
@@ -114,13 +116,14 @@ GetWindowStyle(SDL_Window * window)
 }
 
 static void
-WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x, int *y, int *width, int *height, SDL_bool use_current, 
-                              SDL_bool force_ignore_window_dpi)
+WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x, int *y, int *width, int *height, SDL_bool use_current)
 {
     SDL_WindowData *data = (SDL_WindowData *)window->driverdata;
     SDL_VideoData* videodata = SDL_GetVideoDevice() ? SDL_GetVideoDevice()->driverdata : NULL;
     RECT rect;
+    UINT dpi;
 
+    dpi = 96;
     rect.left = 0;
     rect.top = 0;
     rect.right = (use_current ? window->w : window->windowed.w);
@@ -133,33 +136,21 @@ WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x
         if (WIN_IsPerMonitorV2DPIAware(SDL_GetVideoDevice())) {
             /* With per-monitor v2, the window border/titlebar size depend on the DPI, so we need to call AdjustWindowRectExForDpi instead of 
                AdjustWindowRectEx. */
-            UINT dpi;
+            
+            UINT unused;
+            RECT screen_rect;
+            HMONITOR mon;
 
-            if (data && !force_ignore_window_dpi) {
-                /* The usual case - we have a HWND, so we can look up the DPI to use. */
-                dpi = videodata->GetDpiForWindow(data->hwnd);
-            } else {
-                /* In this case we guess the window DPI based on its rectangle on the screen.
-                 
-                   This happens at creation time of an SDL window, before we have a HWND, 
-                   and also in a bug workaround (when force_ignore_window_dpi is SDL_TRUE
-                   - see WIN_SetWindowFullscreen).
-                */
-                UINT unused;
-                RECT screen_rect;
-                HMONITOR mon;
-
-                screen_rect.left = (use_current ? window->x : window->windowed.x);
-                screen_rect.top = (use_current ? window->y : window->windowed.y);
-                screen_rect.right = screen_rect.left + (use_current ? window->w : window->windowed.w);
-                screen_rect.bottom  = screen_rect.top + (use_current ? window->h : window->windowed.h);
-
-                mon = MonitorFromRect(&screen_rect, MONITOR_DEFAULTTONEAREST);
-
-                /* GetDpiForMonitor docs promise to return the same hdpi / vdpi */
-                if (videodata->GetDpiForMonitor(mon, MDT_EFFECTIVE_DPI, &dpi, &unused) != S_OK) {
-                    dpi = 96;
-                }
+            screen_rect.left = (use_current ? window->x : window->windowed.x);
+            screen_rect.top = (use_current ? window->y : window->windowed.y);
+            screen_rect.right = screen_rect.left + (use_current ? window->w : window->windowed.w);
+            screen_rect.bottom  = screen_rect.top + (use_current ? window->h : window->windowed.h);
+
+            mon = MonitorFromRect(&screen_rect, MONITOR_DEFAULTTONEAREST);
+
+            /* GetDpiForMonitor docs promise to return the same hdpi / vdpi */
+            if (videodata->GetDpiForMonitor(mon, MDT_EFFECTIVE_DPI, &dpi, &unused) != S_OK) {
+                dpi = 96;
             }
 
             videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, dpi);
@@ -172,6 +163,15 @@ WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x
     *y = (use_current ? window->y : window->windowed.y) + rect.top;
     *width = (rect.right - rect.left);
     *height = (rect.bottom - rect.top);
+
+#ifdef HIGHDPI_DEBUG
+    SDL_Log("WIN_AdjustWindowRectWithStyle: in: %d, %d, %dx%d, returning: %d, %d, %dx%d, used dpi %d for frame calculation", 
+        (use_current ? window->x : window->windowed.x),
+        (use_current ? window->y : window->windowed.y),
+        (use_current ? window->w : window->windowed.w),
+        (use_current ? window->h : window->windowed.h),
+        *x, *y, *width, *height, dpi);
+#endif
 }
 
 static void
@@ -184,7 +184,7 @@ WIN_AdjustWindowRect(SDL_Window *window, int *x, int *y, int *width, int *height
 
     style = GetWindowLong(hwnd, GWL_STYLE);
     menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
-    WIN_AdjustWindowRectWithStyle(window, style, menu, x, y, width, height, use_current, SDL_FALSE);
+    WIN_AdjustWindowRectWithStyle(window, style, menu, x, y, width, height, use_current);
 }
 
 static void
@@ -279,7 +279,9 @@ SetupWindowData(_THIS, SDL_Window * window, HWND hwnd, HWND parent, SDL_bool cre
                 int x, y;
                 /* Figure out what the window area will be */
                 WIN_AdjustWindowRect(window, &x, &y, &w, &h, SDL_FALSE);
+                data->expected_resize = SDL_TRUE;
                 SetWindowPos(hwnd, HWND_NOTOPMOST, x, y, w, h, SWP_NOCOPYBITS | SWP_NOZORDER | SWP_NOACTIVATE);
+                data->expected_resize = SDL_FALSE;
             } else {
                 window->w = w;
                 window->h = h;
@@ -395,7 +397,7 @@ WIN_CreateWindow(_THIS, SDL_Window * window)
     style |= GetWindowStyle(window);
 
     /* Figure out what the window area will be */
-    WIN_AdjustWindowRectWithStyle(window, style, FALSE, &x, &y, &w, &h, SDL_FALSE, SDL_FALSE);
+    WIN_AdjustWindowRectWithStyle(window, style, FALSE, &x, &y, &w, &h, SDL_FALSE);
 
     hwnd =
         CreateWindow(SDL_Appname, TEXT(""), style, x, y, w, h, parent, NULL,
@@ -580,7 +582,10 @@ WIN_SetWindowIcon(_THIS, SDL_Window * window, SDL_Surface * icon)
 void
 WIN_SetWindowPosition(_THIS, SDL_Window * window)
 {
-    WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOSIZE | SWP_NOACTIVATE);
+    /* HighDPI support: removed SWP_NOSIZE. If the move results in a DPI change, we need to allow
+     * the window to resize (e.g. AdjustWindowRectExForDpi frame sizes are different).
+     */
+    WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOACTIVATE);
 }
 
 void
@@ -820,13 +825,7 @@ WIN_SetWindowFullscreen(_THIS, SDL_Window * window, SDL_VideoDisplay * display,
         }
 
         menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
-        /* HighDPI bug workaround - when leaving exclusive fullscreen, the window DPI reported
-           by GetDpiForWindow will be wrong. Pass SDL_TRUE for `force_ignore_window_dpi`
-           makes us recompute the DPI based on the monitor we are restoring onto.
-           Fixes windows shrinking slightly when going from exclusive fullscreen to windowed
-           on a HighDPI monitor with scaling.
-        */
-        WIN_AdjustWindowRectWithStyle(window, style, menu, &x, &y, &w, &h, SDL_FALSE, SDL_TRUE);
+        WIN_AdjustWindowRectWithStyle(window, style, menu, &x, &y, &w, &h, SDL_FALSE);
     }
     SetWindowLong(hwnd, GWL_STYLE, style);
     data->expected_resize = SDL_TRUE;
diff --git a/src/video/windows/wmmsg.h b/src/video/windows/wmmsg.h
index bd416eb8023..86cae1df959 100644
--- a/src/video/windows/wmmsg.h
+++ b/src/video/windows/wmmsg.h
@@ -762,7 +762,7 @@ const char *wmtab[] = {
     "UNKNOWN (737)",
     "UNKNOWN (738)",
     "UNKNOWN (739)",
-    "UNKNOWN (740)",
+    "WM_GETDPISCALEDSIZE",
     "UNKNOWN (741)",
     "UNKNOWN (742)",
     "UNKNOWN (743)",