SDL: Support PMv2 DPI awareness, add SDL_HINT_WINDOWS_DPI_AWARENESS

From 51ebefeeee2228fe3a20e6985c4901d4e6a160d8 Mon Sep 17 00:00:00 2001
From: Eric Wasylishen <[EMAIL REDACTED]>
Date: Mon, 28 Feb 2022 00:43:43 -0700
Subject: [PATCH] Support PMv2 DPI awareness, add
 SDL_HINT_WINDOWS_DPI_AWARENESS

The hint allows setting a specific DPI awareness ("unaware", "system", "permonitor", "permonitorv2").

This is the first part of High-DPI support on Windows ( https://github.com/libsdl-org/SDL/issues/2119 ).
It doesn't implement a virtualized SDL coordinate system, which will be
addressed in a later commit. (This hint could be useful for SDL apps
that want 1 SDL unit = 1 pixel, though.)

Detecting and behaving correctly under per-monitor V2
(calling AdjustWindowRectExForDpi where needed) should fix the
following issues:

https://github.com/libsdl-org/SDL/issues/3286
https://github.com/libsdl-org/SDL/issues/4712
---
 include/SDL_hints.h                   |  30 ++++++
 src/video/windows/SDL_windowsevents.c | 136 +++++++++++++++++++++++++-
 src/video/windows/SDL_windowsvideo.c  | 116 ++++++++++++++++++++++
 src/video/windows/SDL_windowsvideo.h  |  45 ++++++++-
 src/video/windows/SDL_windowswindow.c |  57 +++++++++--
 5 files changed, 376 insertions(+), 8 deletions(-)

diff --git a/include/SDL_hints.h b/include/SDL_hints.h
index b5665b91d9f..57417292caa 100644
--- a/include/SDL_hints.h
+++ b/include/SDL_hints.h
@@ -1808,6 +1808,36 @@ extern "C" {
  */
 #define SDL_HINT_WINDOWS_USE_D3D9EX "SDL_WINDOWS_USE_D3D9EX"
 
+/**
+ * \brief Controls whether SDL will declare the process to be DPI aware.
+ *
+ *  This hint must be set before initializing the video subsystem.
+ *
+ *  The main purpose of declaring DPI awareness is to disable OS bitmap scaling of SDL windows on monitors with 
+ *  a DPI scale factor.
+ * 
+ *  This hint is equivalent to requesting DPI awareness via external means (e.g. calling SetProcessDpiAwarenessContext)
+ *  and does not cause SDL to use a virtualized coordinate system, so it will generally give you 1 SDL coordinate = 1 pixel
+ *  even on high-DPI displays.
+ * 
+ *  For more information, see:
+ *  https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
+ * 
+ *  This variable can be set to the following values:
+ *    ""             - Do not change the DPI awareness (default).
+ *    "unaware"      - Declare the process as DPI unaware. (Windows 8.1 and later).
+ *    "system"       - Request system DPI awareness. (Vista and later).
+ *    "permonitor"   - Request per-monitor DPI awareness. (Windows 8.1 and later).
+ *    "permonitorv2" - Request per-monitor V2 DPI awareness. (Windows 10, version 1607 and later).
+ *                     The most visible difference from "permonitor" is that window title bar will be scaled
+ *                     to the visually correct size when dragging between monitors with different scale factors.
+ *                     This is the preferred DPI awareness level.
+ *
+ * If the requested DPI awareness is not available on the currently running OS, SDL will try to request the best
+ * available match.
+ */
+#define SDL_HINT_WINDOWS_DPI_AWARENESS "SDL_WINDOWS_DPI_AWARENESS"
+
 /**
  *  \brief  A variable controlling whether the window frame and title bar are interactive when the cursor is hidden 
  *
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index 084bb9a961b..22780ad8489 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -34,6 +34,7 @@
 #include "../../events/SDL_touch_c.h"
 #include "../../events/scancodes_windows.h"
 #include "SDL_hints.h"
+#include "SDL_log.h"
 
 /* Dropfile support */
 #include <shellapi.h>
@@ -52,6 +53,8 @@
 #include "wmmsg.h"
 #endif
 
+/* #define HIGHDPI_DEBUG */
+
 /* Masks for processing the windows KEYDOWN and KEYUP messages */
 #define REPEATED_KEYMASK    (1<<30)
 #define EXTENDED_KEYMASK    (1<<24)
@@ -86,6 +89,12 @@
 #ifndef WM_UNICHAR
 #define WM_UNICHAR 0x0109
 #endif
+#ifndef WM_DPICHANGED
+#define WM_DPICHANGED 0x02E0
+#endif
+#ifndef WM_GETDPISCALEDSIZE
+#define WM_GETDPISCALEDSIZE 0x02E4
+#endif
 
 #ifndef IS_HIGH_SURROGATE
 #define IS_HIGH_SURROGATE(x)   (((x) >= 0xd800) && ((x) <= 0xdbff))
@@ -1084,7 +1093,12 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
                 size.bottom = h;
                 size.right = w;
 
-                AdjustWindowRectEx(&size, style, menu, 0);
+                if (WIN_IsPerMonitorV2DPIAware(SDL_GetVideoDevice())) {
+                    UINT 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;
             }
@@ -1154,6 +1168,12 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
             h = rect.bottom - rect.top;
             SDL_SendWindowEvent(data->window, SDL_WINDOWEVENT_RESIZED, w, h);
 
+#ifdef HIGHDPI_DEBUG
+            SDL_Log("WM_WINDOWPOSCHANGED: Windows client rect (pixels): (%d, %d) (%d x %d)\tSDL client rect: (%d, %d) (%d x %d)\tGetDpiForWindow: %d",
+                    rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
+                    x, y, w, h, data->videodata->GetDpiForWindow ? (int)data->videodata->GetDpiForWindow(data->hwnd) : 0);
+#endif
+
             /* Forces a WM_PAINT event */
             InvalidateRect(hwnd, NULL, FALSE);
 
@@ -1399,6 +1419,120 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
             }
         }
         break;
+
+    case WM_GETDPISCALEDSIZE:
+        /* Windows 10 Creators Update+ */
+        /* Documented as only being sent to windows that are per-monitor V2 DPI aware. */
+        if (data->videodata->GetDpiForWindow && data->videodata->AdjustWindowRectExForDpi) {
+            /* Windows expects applications to scale their window rects linearly
+               when dragging between monitors with different DPI's.
+               e.g. a 100x100 window dragged to a 200% scaled monitor
+               becomes 200x200.
+
+               For SDL, we instead want the client size to scale linearly.
+               This is not the same as the window rect scaling linearly,
+               because Windows doesn't scale the non-client area (titlebar etc.)
+               linearly. So, we need to handle this message to request custom
+               scaling. */
+            
+            const int nextDPI = (int)wParam;
+            const int prevDPI = (int)data->videodata->GetDpiForWindow(hwnd);
+            SIZE *sizeInOut = (SIZE *)lParam;
+
+            int frame_w, frame_h;
+            int query_client_w_win, query_client_h_win;
+
+            const DWORD style = GetWindowLong(hwnd, GWL_STYLE);
+            const BOOL menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
+
+#ifdef HIGHDPI_DEBUG
+            SDL_Log("WM_GETDPISCALEDSIZE: current DPI: %d potential DPI: %d input size: (%dx%d)",
+                    prevDPI, nextDPI, sizeInOut->cx, sizeInOut->cy);
+#endif
+
+            /* Subtract the window frame size that would have been used at prevDPI */
+            {
+                RECT rect = {0};
+
+                if (!(data->window->flags & SDL_WINDOW_BORDERLESS)) {
+                    data->videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, prevDPI);
+                }
+
+                frame_w = -rect.left + rect.right;
+                frame_h = -rect.top + rect.bottom;
+
+                query_client_w_win = sizeInOut->cx - frame_w;
+                query_client_h_win = sizeInOut->cy - frame_h;
+            }
+
+            /* Add the window frame size that would be used at nextDPI */
+            {
+                RECT rect = { 0, 0, query_client_w_win, query_client_h_win };
+
+                if (!(data->window->flags & SDL_WINDOW_BORDERLESS)) {
+                    data->videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, nextDPI);
+                }
+
+                /* This is supposed to control the suggested rect param of WM_DPICHANGED */
+                sizeInOut->cx = rect.right - rect.left;
+                sizeInOut->cy = rect.bottom - rect.top;
+            }
+
+#ifdef HIGHDPI_DEBUG
+            SDL_Log("WM_GETDPISCALEDSIZE: output size: (%dx%d)", sizeInOut->cx, sizeInOut->cy);
+#endif
+            return TRUE;
+        }
+        break;
+
+    case WM_DPICHANGED:
+        /* Windows 8.1+ */
+        {
+            const int newDPI = HIWORD(wParam);
+            RECT* const suggestedRect = (RECT*)lParam;
+            int w, h;
+
+#ifdef HIGHDPI_DEBUG
+            SDL_Log("WM_DPICHANGED: to %d\tsuggested rect: (%d, %d), (%dx%d)\n", newDPI,
+                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 (WIN_IsPerMonitorV2DPIAware(SDL_GetVideoDevice())) {
+                w = suggestedRect->right - suggestedRect->left;
+                h = suggestedRect->bottom - suggestedRect->top;
+            } 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);
+
+                if (!(data->window->flags & SDL_WINDOW_BORDERLESS)) {
+                    AdjustWindowRectEx(&rect, style, menu, 0);
+                }
+
+                w = rect.right - rect.left;
+                h = rect.bottom - rect.top;
+            }
+            
+#ifdef HIGHDPI_DEBUG
+            SDL_Log("WM_DPICHANGED: current SDL window size: (%dx%d)\tcalling SetWindowPos: (%d, %d), (%dx%d)\n",
+                data->window->w, data->window->h,
+                suggestedRect->left, suggestedRect->top, w, h);
+#endif
+
+            data->expected_resize = SDL_TRUE;
+            SetWindowPos(hwnd,
+                NULL,
+                suggestedRect->left,
+                suggestedRect->top,
+                w,
+                h,
+                SWP_NOZORDER | SWP_NOACTIVATE);
+            data->expected_resize = SDL_FALSE;
+            return 0;
+        }
+        break;
     }
 
     /* If there's a window proc, assume it's going to handle messages */
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index 62ad1c96450..7d916bd7a65 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -122,6 +122,16 @@ WIN_CreateDevice(int devindex)
         data->CloseTouchInputHandle = (BOOL (WINAPI *)(HTOUCHINPUT)) SDL_LoadFunction(data->userDLL, "CloseTouchInputHandle");
         data->GetTouchInputInfo = (BOOL (WINAPI *)(HTOUCHINPUT, UINT, PTOUCHINPUT, int)) SDL_LoadFunction(data->userDLL, "GetTouchInputInfo");
         data->RegisterTouchWindow = (BOOL (WINAPI *)(HWND, ULONG)) SDL_LoadFunction(data->userDLL, "RegisterTouchWindow");
+        data->SetProcessDPIAware = (BOOL (WINAPI *)(void)) SDL_LoadFunction(data->userDLL, "SetProcessDPIAware");
+        data->SetProcessDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT)) SDL_LoadFunction(data->userDLL, "SetProcessDpiAwarenessContext");
+        data->SetThreadDpiAwarenessContext = (DPI_AWARENESS_CONTEXT (WINAPI *)(DPI_AWARENESS_CONTEXT)) SDL_LoadFunction(data->userDLL, "SetThreadDpiAwarenessContext");
+        data->GetThreadDpiAwarenessContext = (DPI_AWARENESS_CONTEXT (WINAPI *)(void)) SDL_LoadFunction(data->userDLL, "GetThreadDpiAwarenessContext");
+        data->GetAwarenessFromDpiAwarenessContext = (DPI_AWARENESS (WINAPI *)(DPI_AWARENESS_CONTEXT)) SDL_LoadFunction(data->userDLL, "GetAwarenessFromDpiAwarenessContext");
+        data->EnableNonClientDpiScaling = (BOOL (WINAPI *)(HWND)) SDL_LoadFunction(data->userDLL, "EnableNonClientDpiScaling");
+        data->AdjustWindowRectExForDpi = (BOOL (WINAPI *)(LPRECT, DWORD, BOOL, DWORD, UINT)) SDL_LoadFunction(data->userDLL, "AdjustWindowRectExForDpi");
+        data->GetDpiForWindow = (UINT (WINAPI *)(HWND)) SDL_LoadFunction(data->userDLL, "GetDpiForWindow");
+        data->AreDpiAwarenessContextsEqual = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT)) SDL_LoadFunction(data->userDLL, "AreDpiAwarenessContextsEqual");
+        data->IsValidDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT)) SDL_LoadFunction(data->userDLL, "IsValidDpiAwarenessContext");
     } else {
         SDL_ClearError();
     }
@@ -129,6 +139,7 @@ WIN_CreateDevice(int devindex)
     data->shcoreDLL = SDL_LoadObject("SHCORE.DLL");
     if (data->shcoreDLL) {
         data->GetDpiForMonitor = (HRESULT (WINAPI *)(HMONITOR, MONITOR_DPI_TYPE, UINT *, UINT *)) SDL_LoadFunction(data->shcoreDLL, "GetDpiForMonitor");
+        data->SetProcessDpiAwareness = (HRESULT (WINAPI *)(PROCESS_DPI_AWARENESS)) SDL_LoadFunction(data->shcoreDLL, "SetProcessDpiAwareness");
     } else {
         SDL_ClearError();
     }
@@ -233,11 +244,103 @@ VideoBootStrap WINDOWS_bootstrap = {
     "windows", "SDL Windows video driver", WIN_CreateDevice
 };
 
+static BOOL
+WIN_DeclareDPIAwareUnaware(_THIS)
+{
+    SDL_VideoData* data = (SDL_VideoData*)_this->driverdata;
+
+    if (data->SetProcessDpiAwarenessContext) {
+        return data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
+    } else if (data->SetProcessDpiAwareness) {
+        /* Windows 8.1 */
+        return SUCCEEDED(data->SetProcessDpiAwareness(PROCESS_DPI_UNAWARE));
+    }
+    return FALSE;
+}
+
+static BOOL
+WIN_DeclareDPIAwareSystem(_THIS)
+{
+    SDL_VideoData* data = (SDL_VideoData*)_this->driverdata;
+
+    if (data->SetProcessDpiAwarenessContext) {
+        /* Windows 10, version 1607 */
+        return data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE);
+    } else if (data->SetProcessDpiAwareness) {
+        /* Windows 8.1 */
+        return SUCCEEDED(data->SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE));
+    } else if (data->SetProcessDPIAware) {
+        /* Windows Vista */
+        return data->SetProcessDPIAware();
+    }
+    return FALSE;
+}
+
+static BOOL
+WIN_DeclareDPIAwarePerMonitor(_THIS)
+{
+    SDL_VideoData *data = (SDL_VideoData *) _this->driverdata;
+
+    if (data->SetProcessDpiAwarenessContext) {
+        /* Windows 10, version 1607 */
+        return data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
+    } else if (data->SetProcessDpiAwareness) {
+        /* Windows 8.1 */
+        return SUCCEEDED(data->SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE));
+    } else {
+        /* Older OS: fall back to system DPI aware */
+        return WIN_DeclareDPIAwareSystem(_this);
+    }
+    return FALSE;
+}
+
+static BOOL
+WIN_DeclareDPIAwarePerMonitorV2(_THIS)
+{
+    SDL_VideoData* data = (SDL_VideoData*)_this->driverdata;
+
+    /* Declare DPI aware(may have been done in external code or a manifest, as well) */
+    if (data->SetProcessDpiAwarenessContext) {
+        /* Windows 10, version 1607 */
+
+        /* NOTE: SetThreadDpiAwarenessContext doesn't work here with OpenGL - the OpenGL contents
+           end up still getting OS scaled. (tested on Windows 10 21H1 19043.1348, NVIDIA 496.49) */
+        if (data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
+            return TRUE;
+        } else {
+            return WIN_DeclareDPIAwarePerMonitor(_this);
+        }
+    } else {
+        /* Older OS: fall back to per-monitor (or system) */
+        return WIN_DeclareDPIAwarePerMonitor(_this);
+    }
+}
+
+static void
+WIN_InitDPIAwareness(_THIS)
+{
+    const char* hint = SDL_GetHint(SDL_HINT_WINDOWS_DPI_AWARENESS);
+
+    if (hint != NULL) {
+        if (SDL_strcmp(hint, "permonitorv2") == 0) {
+            WIN_DeclareDPIAwarePerMonitorV2(_this);
+        } else if (SDL_strcmp(hint, "permonitor") == 0) {
+            WIN_DeclareDPIAwarePerMonitor(_this);
+        } else if (SDL_strcmp(hint, "system") == 0) {
+            WIN_DeclareDPIAwareSystem(_this);
+        } else if (SDL_strcmp(hint, "unaware") == 0) {
+            WIN_DeclareDPIAwareUnaware(_this);
+        }
+    }
+}
+
 int
 WIN_VideoInit(_THIS)
 {
     SDL_VideoData *data = (SDL_VideoData *) _this->driverdata;
 
+    WIN_InitDPIAwareness(_this);
+
     if (WIN_InitModes(_this) < 0) {
         return -1;
     }
@@ -473,6 +576,19 @@ SDL_DXGIGetOutputInfo(int displayIndex, int *adapterIndex, int *outputIndex)
 #endif
 }
 
+SDL_bool
+WIN_IsPerMonitorV2DPIAware(_THIS)
+{
+    SDL_VideoData* data = (SDL_VideoData*) _this->driverdata;
+    
+    if (data->AreDpiAwarenessContextsEqual && data->GetThreadDpiAwarenessContext) {
+        /* Windows 10, version 1607 */
+        return (SDL_bool)data->AreDpiAwarenessContextsEqual(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
+                                                            data->GetThreadDpiAwarenessContext());
+    }
+    return SDL_FALSE;
+}
+
 #endif /* SDL_VIDEO_DRIVER_WINDOWS */
 
 /* vim: set ts=4 sw=4 expandtab: */
diff --git a/src/video/windows/SDL_windowsvideo.h b/src/video/windows/SDL_windowsvideo.h
index d4208c40500..1a42b8010ac 100644
--- a/src/video/windows/SDL_windowsvideo.h
+++ b/src/video/windows/SDL_windowsvideo.h
@@ -86,10 +86,40 @@ typedef enum MONITOR_DPI_TYPE {
     MDT_DEFAULT = MDT_EFFECTIVE_DPI
 } MONITOR_DPI_TYPE;
 
+typedef enum PROCESS_DPI_AWARENESS {
+    PROCESS_DPI_UNAWARE = 0,
+    PROCESS_SYSTEM_DPI_AWARE = 1,
+    PROCESS_PER_MONITOR_DPI_AWARE = 2
+} PROCESS_DPI_AWARENESS;
+
 #else
 #include <shellscalingapi.h>
 #endif /* WINVER < 0x0603 */
 
+#ifndef _DPI_AWARENESS_CONTEXTS_
+
+typedef enum DPI_AWARENESS {
+    DPI_AWARENESS_INVALID = -1,
+    DPI_AWARENESS_UNAWARE = 0,
+    DPI_AWARENESS_SYSTEM_AWARE = 1,
+    DPI_AWARENESS_PER_MONITOR_AWARE = 2
+} DPI_AWARENESS;
+
+DECLARE_HANDLE(DPI_AWARENESS_CONTEXT);
+
+#define DPI_AWARENESS_CONTEXT_UNAWARE           ((DPI_AWARENESS_CONTEXT)-1)
+#define DPI_AWARENESS_CONTEXT_SYSTEM_AWARE      ((DPI_AWARENESS_CONTEXT)-2)
+#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE ((DPI_AWARENESS_CONTEXT)-3)
+
+#endif /* _DPI_AWARENESS_CONTEXTS_ */
+
+/* Windows 10 Creators Update */
+#if NTDDI_VERSION < 0x0A000003
+
+#define DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 ((DPI_AWARENESS_CONTEXT)-4)
+
+#endif /* NTDDI_VERSION < 0x0A000003 */
+
 typedef BOOL  (*PFNSHFullScreen)(HWND, DWORD);
 typedef void  (*PFCoordTransform)(SDL_Window*, POINT*);
 
@@ -137,13 +167,24 @@ typedef struct SDL_VideoData
     BOOL (WINAPI *CloseTouchInputHandle)( HTOUCHINPUT );
     BOOL (WINAPI *GetTouchInputInfo)( HTOUCHINPUT, UINT, PTOUCHINPUT, int );
     BOOL (WINAPI *RegisterTouchWindow)( HWND, ULONG );
+    BOOL (WINAPI *SetProcessDPIAware)( void );
+    BOOL (WINAPI *SetProcessDpiAwarenessContext)( DPI_AWARENESS_CONTEXT );
+    DPI_AWARENESS_CONTEXT (WINAPI *SetThreadDpiAwarenessContext)( DPI_AWARENESS_CONTEXT );
+    DPI_AWARENESS_CONTEXT (WINAPI *GetThreadDpiAwarenessContext)( void );
+    DPI_AWARENESS (WINAPI *GetAwarenessFromDpiAwarenessContext)( DPI_AWARENESS_CONTEXT );
+    BOOL (WINAPI *EnableNonClientDpiScaling)( HWND );
+    BOOL (WINAPI *AdjustWindowRectExForDpi)( LPRECT, DWORD, BOOL, DWORD, UINT );
+    UINT (WINAPI *GetDpiForWindow)( HWND );
+    BOOL (WINAPI *AreDpiAwarenessContextsEqual)(DPI_AWARENESS_CONTEXT, DPI_AWARENESS_CONTEXT);
+    BOOL (WINAPI *IsValidDpiAwarenessContext)(DPI_AWARENESS_CONTEXT);
 
     void* shcoreDLL;
     HRESULT (WINAPI *GetDpiForMonitor)( HMONITOR         hmonitor,
                                         MONITOR_DPI_TYPE dpiType,
                                         UINT             *dpiX,
                                         UINT             *dpiY );
-    
+    HRESULT (WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness);
+
     SDL_bool ime_com_initialized;
     struct ITfThreadMgr *ime_threadmgr;
     SDL_bool ime_initialized;
@@ -203,6 +244,8 @@ extern SDL_bool g_WindowFrameUsableWhileCursorHidden;
 typedef struct IDirect3D9 IDirect3D9;
 extern SDL_bool D3D_LoadDLL( void **pD3DDLL, IDirect3D9 **pDirect3D9Interface );
 
+extern SDL_bool WIN_IsPerMonitorV2DPIAware(_THIS);
+
 #endif /* SDL_windowsvideo_h_ */
 
 /* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index 6608a5f9838..e2289f3adeb 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -114,8 +114,11 @@ 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)
+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)
 {
+    SDL_WindowData *data = (SDL_WindowData *)window->driverdata;
+    SDL_VideoData* videodata = SDL_GetVideoDevice() ? SDL_GetVideoDevice()->driverdata : NULL;
     RECT rect;
 
     rect.left = 0;
@@ -126,8 +129,44 @@ WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x
     /* borderless windows will have WM_NCCALCSIZE return 0 for the non-client area. When this happens, it looks like windows will send a resize message
        expanding the window client area to the previous window + chrome size, so shouldn't need to adjust the window size for the set styles.
      */
-    if (!(window->flags & SDL_WINDOW_BORDERLESS))
-        AdjustWindowRectEx(&rect, style, menu, 0);
+    if (!(window->flags & SDL_WINDOW_BORDERLESS)) {
+        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;
+
+            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;
+                }
+            }
+
+            videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, dpi);
+        } else {
+            AdjustWindowRectEx(&rect, style, menu, 0);
+        }
+    }
 
     *x = (use_current ? window->x : window->windowed.x) + rect.left;
     *y = (use_current ? window->y : window->windowed.y) + rect.top;
@@ -145,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);
+    WIN_AdjustWindowRectWithStyle(window, style, menu, x, y, width, height, use_current, SDL_FALSE);
 }
 
 static void
@@ -356,7 +395,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);
+    WIN_AdjustWindowRectWithStyle(window, style, FALSE, &x, &y, &w, &h, SDL_FALSE, SDL_FALSE);
 
     hwnd =
         CreateWindow(SDL_Appname, TEXT(""), style, x, y, w, h, parent, NULL,
@@ -781,7 +820,13 @@ WIN_SetWindowFullscreen(_THIS, SDL_Window * window, SDL_VideoDisplay * display,
         }
 
         menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL);
-        WIN_AdjustWindowRectWithStyle(window, style, menu, &x, &y, &w, &h, SDL_FALSE);
+        /* 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);
     }
     SetWindowLong(hwnd, GWL_STYLE, style);
     data->expected_resize = SDL_TRUE;