diff --git a/include/SDL_hints.h b/include/SDL_hints.h index 007a4bee0..fb8199f92 100644 --- a/include/SDL_hints.h +++ b/include/SDL_hints.h @@ -502,6 +502,26 @@ extern "C" { */ #define SDL_HINT_VIDEO_HIGHDPI_DISABLED "SDL_VIDEO_HIGHDPI_DISABLED" + /** + * \brief If set to 1, then allow high-DPI on all windows. + * + * If high-DPI is allowed, the framebuffer size (which can be queried by calling SDL_GL_GetDrawableSize, + * and returns a size in pixels) is decoupled from the SDL window size. SDL window sizes are in + * points, which are translated to pixels by multiplying by the scale factor of the monitor that + * the window is on. + * + * If high-DPI is not enabled with this hint, the framebuffer size will be equal to the window size + * returned by SDL_GetWindowSize, but the OS will scale up the framebuffer if the window is on a + * high-DPI monitor. + * + * An SDL_WINDOWEVENT_RESIZED event will be sent to the application when the framebuffer size changes, + * which could happen in response to the window moving to a monitor with a different scale factor + * (either from the user moving it, or SDL_SetWindowPosition), or the monitor's scale factor changing. + * + * High-DPI support is currently implemented on macOS and Windows. + */ +#define SDL_HINT_VIDEO_ALLOW_HIGHDPI "SDL_VIDEO_ALLOW_HIGHDPI" + /** * \brief A variable that determines whether ctrl+click should generate a right-click event on Mac * diff --git a/include/SDL_video.h b/include/SDL_video.h index 6f6cda8b1..74d011fa3 100644 --- a/include/SDL_video.h +++ b/include/SDL_video.h @@ -110,7 +110,7 @@ typedef enum SDL_WINDOW_MOUSE_FOCUS = 0x00000400, /**< window has mouse focus */ SDL_WINDOW_FULLSCREEN_DESKTOP = ( SDL_WINDOW_FULLSCREEN | 0x00001000 ), SDL_WINDOW_FOREIGN = 0x00000800, /**< window not created by SDL */ - SDL_WINDOW_ALLOW_HIGHDPI = 0x00002000, /**< window should be created in high-DPI mode if supported */ + SDL_WINDOW_ALLOW_HIGHDPI = 0x00002000, /**< window should be created in high-DPI mode if supported (deprecated, use SDL_HINT_VIDEO_ALLOW_HIGHDPI) */ SDL_WINDOW_MOUSE_CAPTURE = 0x00004000, /**< window has mouse captured (unrelated to INPUT_GRABBED) */ SDL_WINDOW_ALWAYS_ON_TOP = 0x00008000, /**< window should always be above others */ SDL_WINDOW_SKIP_TASKBAR = 0x00010000, /**< window should not be added to the taskbar */ diff --git a/src/render/direct3d/SDL_render_d3d.c b/src/render/direct3d/SDL_render_d3d.c index c40f699bb..5b094eea4 100644 --- a/src/render/direct3d/SDL_render_d3d.c +++ b/src/render/direct3d/SDL_render_d3d.c @@ -125,6 +125,7 @@ static void PrintShaderData(LPDWORD shader_data, DWORD shader_size) static SDL_Renderer *D3D_CreateRenderer(SDL_Window * window, Uint32 flags); static void D3D_WindowEvent(SDL_Renderer * renderer, const SDL_WindowEvent *event); +static int D3D_GetOutputSize(SDL_Renderer * renderer, int *w, int *h); static SDL_bool D3D_SupportsBlendMode(SDL_Renderer * renderer, SDL_BlendMode blendMode); static int D3D_CreateTexture(SDL_Renderer * renderer, SDL_Texture * texture); static int D3D_RecreateTexture(SDL_Renderer * renderer, SDL_Texture * texture); @@ -469,8 +470,7 @@ D3D_ActivateRenderer(SDL_Renderer * renderer) SDL_Window *window = renderer->window; int w, h; Uint32 window_flags = SDL_GetWindowFlags(window); - - SDL_GetWindowSize(window, &w, &h); + WIN_GetDrawableSize(window, &w, &h); data->pparams.BackBufferWidth = w; data->pparams.BackBufferHeight = h; if (window_flags & SDL_WINDOW_FULLSCREEN && (window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP) != SDL_WINDOW_FULLSCREEN_DESKTOP) { @@ -543,6 +543,7 @@ D3D_CreateRenderer(SDL_Window * window, Uint32 flags) } renderer->WindowEvent = D3D_WindowEvent; + renderer->GetOutputSize = D3D_GetOutputSize; renderer->SupportsBlendMode = D3D_SupportsBlendMode; renderer->CreateTexture = D3D_CreateTexture; renderer->UpdateTexture = D3D_UpdateTexture; @@ -570,7 +571,7 @@ D3D_CreateRenderer(SDL_Window * window, Uint32 flags) SDL_GetWindowWMInfo(window, &windowinfo); window_flags = SDL_GetWindowFlags(window); - SDL_GetWindowSize(window, &w, &h); + WIN_GetDrawableSize(window, &w, &h); SDL_GetWindowDisplayMode(window, &fullscreen_mode); SDL_zero(pparams); @@ -805,6 +806,13 @@ D3D_WindowEvent(SDL_Renderer * renderer, const SDL_WindowEvent *event) } } +static int +D3D_GetOutputSize(SDL_Renderer * renderer, int *w, int *h) +{ + WIN_GetDrawableSize(renderer->window, w, h); + return 0; +} + static D3DBLEND GetBlendFunc(SDL_BlendFactor factor) { switch (factor) { diff --git a/src/render/direct3d11/SDL_render_d3d11.c b/src/render/direct3d11/SDL_render_d3d11.c index 73cf68013..f903c4eaf 100644 --- a/src/render/direct3d11/SDL_render_d3d11.c +++ b/src/render/direct3d11/SDL_render_d3d11.c @@ -722,6 +722,7 @@ static const DWORD D3D11_VertexShader[] = { static SDL_Renderer *D3D11_CreateRenderer(SDL_Window * window, Uint32 flags); static void D3D11_WindowEvent(SDL_Renderer * renderer, const SDL_WindowEvent *event); +static int D3D11_GetOutputSize(SDL_Renderer * renderer, int *w, int *h); static SDL_bool D3D11_SupportsBlendMode(SDL_Renderer * renderer, SDL_BlendMode blendMode); static int D3D11_CreateTexture(SDL_Renderer * renderer, SDL_Texture * texture); static int D3D11_UpdateTexture(SDL_Renderer * renderer, SDL_Texture * texture, @@ -833,6 +834,7 @@ D3D11_CreateRenderer(SDL_Window * window, Uint32 flags) } renderer->WindowEvent = D3D11_WindowEvent; + renderer->GetOutputSize = D3D11_GetOutputSize; renderer->SupportsBlendMode = D3D11_SupportsBlendMode; renderer->CreateTexture = D3D11_CreateTexture; renderer->UpdateTexture = D3D11_UpdateTexture; @@ -1574,7 +1576,7 @@ D3D11_CreateWindowSizeDependentResources(SDL_Renderer * renderer) /* The width and height of the swap chain must be based on the display's * non-rotated size. */ - SDL_GetWindowSize(renderer->window, &w, &h); + WIN_GetDrawableSize(renderer->window, &w, &h); data->rotation = D3D11_GetCurrentRotation(); /* SDL_Log("%s: windowSize={%d,%d}, orientation=%d\n", __FUNCTION__, w, h, (int)data->rotation); */ if (D3D11_IsDisplayRotated90Degrees(data->rotation)) { @@ -1736,6 +1738,13 @@ D3D11_WindowEvent(SDL_Renderer * renderer, const SDL_WindowEvent *event) } } +static int +D3D11_GetOutputSize(SDL_Renderer * renderer, int *w, int *h) +{ + WIN_GetDrawableSize(renderer->window, w, h); + return 0; +} + static SDL_bool D3D11_SupportsBlendMode(SDL_Renderer * renderer, SDL_BlendMode blendMode) { diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c index d3101ea70..74fba5a1c 100644 --- a/src/test/SDL_test_common.c +++ b/src/test/SDL_test_common.c @@ -218,7 +218,7 @@ SDLTest_CommonArg(SDLTest_CommonState * state, int index) return 1; } if (SDL_strcasecmp(argv[index], "--allow-highdpi") == 0) { - state->window_flags |= SDL_WINDOW_ALLOW_HIGHDPI; + SDL_SetHint(SDL_HINT_VIDEO_ALLOW_HIGHDPI, "1"); return 1; } if (SDL_strcasecmp(argv[index], "--windows") == 0) { diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index 60b214718..aa6377588 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -1391,6 +1391,11 @@ SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags) } } + /* This hint adds SDL_WINDOW_ALLOW_HIGHDPI to all windows. */ + if (SDL_GetHintBoolean(SDL_HINT_VIDEO_ALLOW_HIGHDPI, SDL_FALSE)) { + flags |= SDL_WINDOW_ALLOW_HIGHDPI; + } + if (flags & SDL_WINDOW_VULKAN) { if (!_this->Vulkan_CreateSurface) { SDL_SetError("Vulkan support is either not configured in SDL " diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c index b73e9df8b..8709fe93a 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -34,6 +34,7 @@ #include "../../events/scancodes_windows.h" #include "SDL_assert.h" #include "SDL_hints.h" +#include "SDL_log.h" /* Dropfile support */ #include @@ -47,6 +48,8 @@ #include "wmmsg.h" #endif +/* #define HIGHDPI_DEBUG */ + /* For processing mouse WM_*BUTTON* and WM_MOUSEMOVE message-data from GetMessageExtraInfo() */ #define MOUSEEVENTF_FROMTOUCH 0xFF515700 @@ -81,6 +84,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 static SDL_Scancode VKeytoScancode(WPARAM vkey) @@ -381,6 +390,24 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) /* Get the window data for the window */ data = (SDL_WindowData *) GetProp(hwnd, TEXT("SDL_WindowData")); if (!data) { + /* Request that windows scale the non-client area when moving between monitors. + This is only for Windows 10 Anniversary Update. + In the Windows 10 Creators Update, this is the default behaviour with a per-monitor V2 context. + */ + switch (msg) { + case WM_NCCREATE: + { + SDL_VideoDevice *device = SDL_GetVideoDevice(); + if (device && device->driverdata) { + SDL_VideoData *data = SDL_static_cast(SDL_VideoData *, device->driverdata); + if (data->highdpi_enabled && data->EnableNonClientDpiScaling) { + data->EnableNonClientDpiScaling(hwnd); + } + } + } + break; + } + return CallWindowProc(DefWindowProc, hwnd, msg, wParam, lParam); } @@ -475,7 +502,12 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) SDL_Mouse *mouse = SDL_GetMouse(); if (!mouse->relative_mode || mouse->relative_mode_warp) { SDL_MouseID mouseID = (((GetMessageExtraInfo() & MOUSEEVENTF_FROMTOUCH) == MOUSEEVENTF_FROMTOUCH) ? SDL_TOUCH_MOUSEID : 0); - SDL_SendMouseMotion(data->window, mouseID, 0, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); + + WIN_ClientPointToSDL(data->window, &x, &y); + + SDL_SendMouseMotion(data->window, mouseID, 0, x, y); } } /* don't break here, fall through to check the wParam like the button presses */ @@ -681,8 +713,7 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) int w, h; int min_w, min_h; int max_w, max_h; - int style; - BOOL menu; + int unused_x, unused_y; BOOL constrain_max_size; if (SDL_IsShapedWindow(data->window)) @@ -703,6 +734,12 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) SDL_GetWindowMinimumSize(data->window, &min_w, &min_h); SDL_GetWindowMaximumSize(data->window, &max_w, &max_h); + /* Convert w, h, min_w, min_h, max_w, max_h to unscaled. + We can treat them as a point in the client area. */ + WIN_ClientPointFromSDL(data->window, &w, &h); + WIN_ClientPointFromSDL(data->window, &min_w, &min_h); + WIN_ClientPointFromSDL(data->window, &max_w, &max_h); + /* Store in min_w and min_h difference between current size and minimal size so we don't need to call AdjustWindowRectEx twice */ min_w -= w; @@ -715,21 +752,10 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) constrain_max_size = FALSE; } - size.top = 0; - size.left = 0; - size.bottom = h; - size.right = w; - - style = GetWindowLong(hwnd, GWL_STYLE); - /* DJM - according to the docs for GetMenu(), the - return value is undefined if hwnd is a child window. - Apparently it's too difficult for MS to check - inside their function, so I have to do it here. - */ - menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL); - AdjustWindowRectEx(&size, style, menu, 0); - w = size.right - size.left; - h = size.bottom - size.top; + /* Expand w/h to include the frame. */ + unused_x = 0; + unused_y = 0; + WIN_AdjustWindowRectWithRect(data->window, &unused_x, &unused_y, &w, &h); /* Fix our size to the current size */ info = (MINMAXINFO *) lParam; @@ -782,13 +808,28 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) x = rect.left; y = rect.top; + w = rect.right - rect.left; + h = rect.bottom - rect.top; + WIN_ScreenRectToSDL(&x, &y, &w, &h); + SDL_SendWindowEvent(data->window, SDL_WINDOWEVENT_MOVED, x, y); + /* NOTE: important to convert w/h from SDL (points) to Windows (pixels) using + WIN_ClientPointToSDL, which uses the window's actual + DPI value, rather than WIN_ScreenRectToSDL which guesses. */ w = rect.right - rect.left; h = rect.bottom - rect.top; + WIN_ClientPointToSDL(data->window, &w, &h); + 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 (points): (%d, %d) (%d x %d)", + rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, + x, y, w, h); +#endif + /* Forces a WM_PAINT event */ InvalidateRect(hwnd, NULL, FALSE); } @@ -963,7 +1004,10 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) if (window->hit_test) { POINT winpoint = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; if (ScreenToClient(hwnd, &winpoint)) { - const SDL_Point point = { (int) winpoint.x, (int) winpoint.y }; + int x = (int) winpoint.x; + int y = (int) winpoint.y; + WIN_ClientPointToSDL(data->window, &x, &y); + const SDL_Point point = { x, y }; const SDL_HitTestResult rc = window->hit_test(window, &point, window->hit_test_data); switch (rc) { #define POST_HIT_TEST(ret) { SDL_SendWindowEvent(data->window, SDL_WINDOWEVENT_HIT_TEST, 0, 0); return ret; } @@ -984,6 +1028,135 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) } } break; + + case WM_GETDPISCALEDSIZE: /* Windows 10 Creators Update+ */ + /* GetDpiForWindow and AdjustWindowRectExForDpi will be present, but check anyway. */ + if (data->videodata->highdpi_enabled + && 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 potentialDPI = (int)wParam; + const int currentDPI = (int)data->videodata->GetDpiForWindow(hwnd); + SIZE *sizeInOut = (SIZE *)lParam; + + int x, y, w, h; + int frame_w, frame_h; + int query_client_w_win, query_client_h_win; + +#ifdef HIGHDPI_DEBUG + SDL_Log("WM_GETDPISCALEDSIZE: current DPI: %d potential DPI: %d. input size: (%dx%d)", currentDPI, potentialDPI, sizeInOut->cx, sizeInOut->cy); +#endif + + WIN_AdjustWindowRect(data->window, &x, &y, &w, &h, SDL_TRUE); + + /* get the frame size */ + frame_w = w - MulDiv(data->window->w, currentDPI, 96); + frame_h = h - MulDiv(data->window->h, currentDPI, 96); + + query_client_w_win = sizeInOut->cx - frame_w; + query_client_h_win = sizeInOut->cy - frame_h; + + /* convert to new dpi */ + query_client_w_win = MulDiv(query_client_w_win, potentialDPI, currentDPI); + query_client_h_win = MulDiv(query_client_h_win, potentialDPI, currentDPI); + + /* re-add the window frame in the new DPI */ + { + DWORD style = GetWindowLong(hwnd, GWL_STYLE); + BOOL menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL); + RECT rect; + + rect.left = 0; + rect.top = 0; + rect.right = query_client_w_win; + rect.bottom = query_client_h_win; + + if (!(data->window->flags & SDL_WINDOW_BORDERLESS)) + data->videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, potentialDPI); + + /* 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+ */ + if (data->videodata->highdpi_enabled) { + const int newDPI = HIWORD(wParam); + RECT* const suggestedRect = (RECT*)lParam; + int x, y, w, h; + +#ifdef HIGHDPI_DEBUG + SDL_Log("WM_DPICHANGED: %d to %d\tsuggested rect: (%d, %d), (%dx%d)\n", + data->scaling_xdpi, newDPI, + suggestedRect->left, suggestedRect->top, suggestedRect->right - suggestedRect->left, suggestedRect->bottom - suggestedRect->top); +#endif + + /* update the cached DPI value for this window */ + data->scaling_xdpi = newDPI; + data->scaling_ydpi = newDPI; + + if (data->videodata->AreDpiAwarenessContextsEqual + && data->videodata->GetThreadDpiAwarenessContext + && data->videodata->AreDpiAwarenessContextsEqual(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, data->videodata->GetThreadDpiAwarenessContext())) + { + /* + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 means that + WM_GETDPISCALEDSIZE will have been called, so we can use suggestedRect. + */ + w = suggestedRect->right - suggestedRect->left; + h = suggestedRect->bottom - suggestedRect->top; + } else { + /* + The OS does not support WM_GETDPISCALEDSIZE, so we can't use suggestedRect. + + (suggestedRect is calculated by default to preserves the apparent size of the window rect, + whereas we want to preserve the apparent size of the client rect.) + */ + WIN_AdjustWindowRect(data->window, &x, &y, &w, &h, SDL_TRUE); + } + +#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 + + /* clear the window size, to cause us to send a SDL_WINDOWEVENT_RESIZED event in WM_WINDOWPOSCHANGED */ + data->window->w = 0; + data->window->h = 0; + + 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_windowsmodes.c b/src/video/windows/SDL_windowsmodes.c index d40479ef9..4edd961b1 100644 --- a/src/video/windows/SDL_windowsmodes.c +++ b/src/video/windows/SDL_windowsmodes.c @@ -51,6 +51,15 @@ WIN_UpdateDisplayMode(_THIS, LPCTSTR deviceName, DWORD index, SDL_DisplayMode * int logical_width = GetDeviceCaps( hdc, HORZRES ); int logical_height = GetDeviceCaps( hdc, VERTRES ); + /* + If DPI-unaware: + - GetDeviceCaps( hdc, HORZRES ) will return the monitor width in points. + - DeviceMode.dmPelsWidth is actual pixels (unlike almost all other Windows API's, + it's not virtualized when DPI unaware). + + If DPI-aware: + - GetDeviceCaps( hdc, HORZRES ) will return pixels, same as DeviceMode.dmPelsWidth + */ mode->w = logical_width; mode->h = logical_height; @@ -230,29 +239,6 @@ WIN_InitModes(_THIS) return 0; } -int -WIN_GetDisplayBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect) -{ - const SDL_DisplayData *data = (const SDL_DisplayData *)display->driverdata; - MONITORINFO minfo; - BOOL rc; - - SDL_zero(minfo); - minfo.cbSize = sizeof(MONITORINFO); - rc = GetMonitorInfo(data->MonitorHandle, &minfo); - - if (!rc) { - return SDL_SetError("Couldn't find monitor data"); - } - - rect->x = minfo.rcMonitor.left; - rect->y = minfo.rcMonitor.top; - rect->w = minfo.rcMonitor.right - minfo.rcMonitor.left; - rect->h = minfo.rcMonitor.bottom - minfo.rcMonitor.top; - - return 0; -} - int WIN_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi_out, float * hdpi_out, float * vdpi_out) { @@ -273,29 +259,21 @@ WIN_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi_out, float * h } } else { // Window 8.0 and below: same DPI for all monitors. - HDC hdc; - int hdpi_int, vdpi_int, hpoints, vpoints, hpix, vpix; + int hpoints, vpoints, hpix, vpix; float hinches, vinches; - hdc = GetDC(NULL); - if (hdc == NULL) { - return SDL_SetError("GetDC failed"); - } - hdpi_int = GetDeviceCaps(hdc, LOGPIXELSX); - vdpi_int = GetDeviceCaps(hdc, LOGPIXELSY); - ReleaseDC(NULL, hdc); - + /* NOTE: all of this is just to compute the diagonal DPI. */ hpoints = GetSystemMetrics(SM_CXVIRTUALSCREEN); vpoints = GetSystemMetrics(SM_CYVIRTUALSCREEN); - hpix = MulDiv(hpoints, hdpi_int, 96); - vpix = MulDiv(vpoints, vdpi_int, 96); + hpix = MulDiv(hpoints, videodata->system_xdpi, 96); + vpix = MulDiv(vpoints, videodata->system_ydpi, 96); hinches = (float)hpoints / 96.0f; vinches = (float)vpoints / 96.0f; - hdpi = (float)hdpi_int; - vdpi = (float)vdpi_int; + hdpi = (float)videodata->system_xdpi; + vdpi = (float)videodata->system_ydpi; ddpi = SDL_ComputeDiagonalDPI(hpix, vpix, hinches, vinches); } @@ -312,12 +290,16 @@ WIN_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi_out, float * h return ddpi != 0.0f ? 0 : SDL_SetError("Couldn't get DPI"); } -int -WIN_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect) +static int +WIN_GetDisplayBoundsInternal(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect, SDL_bool usable) { const SDL_DisplayData *data = (const SDL_DisplayData *)display->driverdata; + const SDL_VideoData *vid_data = (const SDL_VideoData *)_this->driverdata; MONITORINFO minfo; + const RECT *rect_win; BOOL rc; + int x, y; + int w, h; SDL_zero(minfo); minfo.cbSize = sizeof(MONITORINFO); @@ -327,14 +309,157 @@ WIN_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect) return SDL_SetError("Couldn't find monitor data"); } - rect->x = minfo.rcWork.left; - rect->y = minfo.rcWork.top; - rect->w = minfo.rcWork.right - minfo.rcWork.left; - rect->h = minfo.rcWork.bottom - minfo.rcWork.top; + rect_win = usable ? &minfo.rcWork : &minfo.rcMonitor; + + x = rect_win->left; + y = rect_win->top; + w = rect_win->right - rect_win->left; + h = rect_win->bottom - rect_win->top; + WIN_ScreenRectToSDL(&x, &y, &w, &h); + + rect->x = x; + rect->y = y; + rect->w = w; + rect->h = h; return 0; } +int +WIN_GetDisplayBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect) +{ + return WIN_GetDisplayBoundsInternal(_this, display, rect, SDL_FALSE); +} + +int +WIN_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect) +{ + return WIN_GetDisplayBoundsInternal(_this, display, rect, SDL_TRUE); +} + +static int +WIN_GetMonitorDPIAndRects(const SDL_VideoData *videodata, HMONITOR monitor, UINT *xdpi, UINT *ydpi, RECT *monitorrect_sdl, RECT *monitorrect_win) +{ + HRESULT result; + MONITORINFO moninfo = { 0 }; + UINT unused; + int mon_width, mon_height; + + /* Check for Windows < 8.1*/ + if (!videodata->GetDpiForMonitor) { + *xdpi = videodata->system_xdpi; + *ydpi = videodata->system_ydpi; + } else { + result = videodata->GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, xdpi, &unused); + *ydpi = *xdpi; + if (result != S_OK) { + /* Shouldn't happen? */ + return SDL_SetError("GetDpiForMonitor failed"); + } + } + + moninfo.cbSize = sizeof(MONITORINFO); + if (!GetMonitorInfo(monitor, &moninfo)) { + /* Shouldn't happen? */ + return SDL_SetError("GetMonitorInfo failed"); + } + + *monitorrect_win = moninfo.rcMonitor; + *monitorrect_sdl = moninfo.rcMonitor; + + /* fix up the right/bottom of monitorrect_sdl */ + mon_width = moninfo.rcMonitor.right - moninfo.rcMonitor.left; + mon_height = moninfo.rcMonitor.bottom - moninfo.rcMonitor.top; + mon_width = MulDiv(mon_width, 96, *xdpi); + mon_height = MulDiv(mon_height, 96, *ydpi); + + monitorrect_sdl->right = monitorrect_sdl->left + mon_width; + monitorrect_sdl->bottom = monitorrect_sdl->top + mon_height; + + return 0; +} + +/* Convert an SDL to a Windows screen rect. */ +void WIN_ScreenRectFromSDL(int *x, int *y, int *w, int *h) +{ + const SDL_VideoDevice *videodevice = SDL_GetVideoDevice(); + const SDL_VideoData *videodata; + RECT inputrect; + RECT monitorrect_sdl, monitorrect_win; + UINT xdpi, ydpi; + HMONITOR monitor; + + if (!videodevice || !videodevice->driverdata) + return; + + videodata = (SDL_VideoData *)videodevice->driverdata; + if (!videodata->highdpi_enabled) + return; + + /* + The trick here is passing SDL coordinates to MonitorFromRect, which expects Windows + coordinates (pixels). This is wrong, but there is no real alternative, and due to + the way we derive the SDL coordinate system, it works OK: + + - top-left corner of monitors in SDL coordinates are identical to the top-left corner in Windows coordinates. + - the widths/heights of monitors (and windows) in SDL coords are in scaled points, + which are equal or less than the corresponding sizes in pixels (because we only support scale factors >=100%) + - becuase of the above two points, a rect (in SDL coordinates) that is fully inside + a monitor's bounds (in SDL coordinates) will also be fully inside that monitor's bounds in Windows coordinates. + */ + inputrect.left = *x; + inputrect.top = *y; + inputrect.right = *x + *w; + inputrect.bottom = *y + *h; + monitor = MonitorFromRect(&inputrect, MONITOR_DEFAULTTONEAREST); + + if (WIN_GetMonitorDPIAndRects(videodata, monitor, &xdpi, &ydpi, &monitorrect_sdl, &monitorrect_win) == 0) { + *w = MulDiv(*w, xdpi, 96); + *h = MulDiv(*h, ydpi, 96); + + *x = monitorrect_sdl.left + MulDiv(*x - monitorrect_sdl.left, xdpi, 96); + *y = monitorrect_sdl.top + MulDiv(*y - monitorrect_sdl.top, ydpi, 96); + + /* ensure the result is not past the right/bottom of the monitor rect */ + if (*x >= monitorrect_win.right) + *x = monitorrect_win.right - 1; + if (*y >= monitorrect_win.bottom) + *y = monitorrect_win.bottom - 1; + } +} + +/* Converts a Windows screen rect to an SDL one. */ +void WIN_ScreenRectToSDL(int *x, int *y, int *w, int *h) +{ + const SDL_VideoDevice *videodevice = SDL_GetVideoDevice(); + const SDL_VideoData *videodata; + RECT inputrect; + RECT monitorrect_sdl, monitorrect_win; + UINT xdpi, ydpi; + HMONITOR monitor; + + if (!videodevice || !videodevice->driverdata) + return; + + videodata = (SDL_VideoData *)videodevice->driverdata; + if (!videodata->highdpi_enabled) + return; + + inputrect.left = *x; + inputrect.top = *y; + inputrect.right = *x + *w; + inputrect.bottom = *y + *h; + monitor = MonitorFromRect(&inputrect, MONITOR_DEFAULTTONEAREST); + + if (WIN_GetMonitorDPIAndRects(videodata, monitor, &xdpi, &ydpi, &monitorrect_sdl, &monitorrect_win) == 0) { + *w = MulDiv(*w, 96, xdpi); + *h = MulDiv(*h, 96, ydpi); + + *x = monitorrect_win.left + MulDiv(*x - monitorrect_win.left, 96, xdpi); + *y = monitorrect_win.top + MulDiv(*y - monitorrect_win.top, 96, ydpi); + } +} + void WIN_GetDisplayModes(_THIS, SDL_VideoDisplay * display) { @@ -361,6 +486,36 @@ WIN_GetDisplayModes(_THIS, SDL_VideoDisplay * display) } } +#ifdef DEBUG_MODES +static void +WIN_LogMonitor(_THIS, HMONITOR mon) +{ + const SDL_VideoData *vid_data = (const SDL_VideoData *)_this->driverdata; + MONITORINFOEX minfo; + UINT xdpi = 0, ydpi = 0; + char *name_utf8; + + if (vid_data->GetDpiForMonitor) + vid_data->GetDpiForMonitor(mon, MDT_EFFECTIVE_DPI, &xdpi, &ydpi); + + SDL_zero(minfo); + minfo.cbSize = sizeof(minfo); + GetMonitorInfo(mon, (LPMONITORINFO)&minfo); + + name_utf8 = WIN_StringToUTF8(minfo.szDevice); + + SDL_Log("WIN_LogMonitor: monitor \"%s\": dpi: %d. Windows virtual screen coordinates: (%d, %d), %dx%d", + name_utf8, + xdpi, + minfo.rcMonitor.left, + minfo.rcMonitor.top, + minfo.rcMonitor.right - minfo.rcMonitor.left, + minfo.rcMonitor.bottom - minfo.rcMonitor.top); + + SDL_free(name_utf8); +} +#endif + int WIN_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode) { @@ -368,9 +523,35 @@ WIN_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode) SDL_DisplayModeData *data = (SDL_DisplayModeData *) mode->driverdata; LONG status; +#ifdef DEBUG_MODES + SDL_Log("WIN_SetDisplayMode: monitor before mode change:"); + WIN_LogMonitor(_this, displaydata->MonitorHandle); +#endif + + /* + High-DPI notes: + + - ChangeDisplaySettingsEx always takes pixels. + - e.g. if the display is set to 2880x1800 with 200% scaling in the Control Panel, + - calling ChangeDisplaySettingsEx with a dmPelsWidth/Height other than 2880x1800 will + change the monitor DPI to 96. (100% scaling) + - calling ChangeDisplaySettingsEx with a dmPelsWidth/Height of 2880x1800 (or a NULL DEVMODE*) will + reset the monitor DPI to 192. (200% scaling) + NOTE: these are temporary changes in DPI, not modifications to the Control Panel setting. + + - Windows bug: windows do not get a WM_DPICHANGED message after a ChangeDisplaySettingsEx, even though the + monitor DPI changes + (as of Windows 10 Creator's Update, at least) + */ if (mode->driverdata == display->desktop_mode.driverdata) { +#ifdef DEBUG_MODES + SDL_Log("WIN_SetDisplayMode: resetting to original resolution"); +#endif status = ChangeDisplaySettingsEx(displaydata->DeviceName, NULL, NULL, CDS_FULLSCREEN, NULL); } else { +#ifdef DEBUG_MODES + SDL_Log("WIN_SetDisplayMode: changing to %dx%d pixels", data->DeviceMode.dmPelsWidth, data->DeviceMode.dmPelsHeight); +#endif status = ChangeDisplaySettingsEx(displaydata->DeviceName, &data->DeviceMode, NULL, CDS_FULLSCREEN, NULL); } if (status != DISP_CHANGE_SUCCESSFUL) { @@ -391,6 +572,12 @@ WIN_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode) } return SDL_SetError("ChangeDisplaySettingsEx() failed: %s", reason); } + +#ifdef DEBUG_MODES + SDL_Log("WIN_SetDisplayMode: monitor after mode change:"); + WIN_LogMonitor(_this, displaydata->MonitorHandle); +#endif + EnumDisplaySettings(displaydata->DeviceName, ENUM_CURRENT_SETTINGS, &data->DeviceMode); WIN_UpdateDisplayMode(_this, displaydata->DeviceName, ENUM_CURRENT_SETTINGS, mode); return 0; diff --git a/src/video/windows/SDL_windowsmodes.h b/src/video/windows/SDL_windowsmodes.h index b95350457..271546463 100644 --- a/src/video/windows/SDL_windowsmodes.h +++ b/src/video/windows/SDL_windowsmodes.h @@ -37,6 +37,8 @@ typedef struct extern int WIN_InitModes(_THIS); extern int WIN_GetDisplayBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect); extern int WIN_GetDisplayUsableBounds(_THIS, SDL_VideoDisplay * display, SDL_Rect * rect); +extern void WIN_ScreenRectFromSDL(int *x, int *y, int *w, int *h); +extern void WIN_ScreenRectToSDL(int *x, int *y, int *w, int *h); extern int WIN_GetDisplayDPI(_THIS, SDL_VideoDisplay * display, float * ddpi, float * hdpi, float * vdpi); extern void WIN_GetDisplayModes(_THIS, SDL_VideoDisplay * display); extern int WIN_SetDisplayMode(_THIS, SDL_VideoDisplay * display, SDL_DisplayMode * mode); diff --git a/src/video/windows/SDL_windowsopengl.c b/src/video/windows/SDL_windowsopengl.c index ae5e455d9..484334c05 100644 --- a/src/video/windows/SDL_windowsopengl.c +++ b/src/video/windows/SDL_windowsopengl.c @@ -823,6 +823,12 @@ WIN_GL_MakeCurrent(_THIS, SDL_Window * window, SDL_GLContext context) return 0; } +void +WIN_GL_GetDrawableSize(_THIS, SDL_Window *window, int *w, int *h) +{ + WIN_GetDrawableSize(window, w, h); +} + int WIN_GL_SetSwapInterval(_THIS, int interval) { diff --git a/src/video/windows/SDL_windowsopengl.h b/src/video/windows/SDL_windowsopengl.h index 4dc4d8faf..917f2dccd 100644 --- a/src/video/windows/SDL_windowsopengl.h +++ b/src/video/windows/SDL_windowsopengl.h @@ -71,6 +71,7 @@ extern int WIN_GL_SetupWindow(_THIS, SDL_Window * window); extern SDL_GLContext WIN_GL_CreateContext(_THIS, SDL_Window * window); extern int WIN_GL_MakeCurrent(_THIS, SDL_Window * window, SDL_GLContext context); +extern void WIN_GL_GetDrawableSize(_THIS, SDL_Window *window, int *w, int *h); extern int WIN_GL_SetSwapInterval(_THIS, int interval); extern int WIN_GL_GetSwapInterval(_THIS); extern int WIN_GL_SwapWindow(_THIS, SDL_Window * window); diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c index f4ab2a088..9f888014f 100644 --- a/src/video/windows/SDL_windowsvideo.c +++ b/src/video/windows/SDL_windowsvideo.c @@ -116,6 +116,15 @@ 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"); } else { SDL_ClearError(); } @@ -123,6 +132,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(); } @@ -174,6 +184,7 @@ WIN_CreateDevice(int devindex) device->GL_UnloadLibrary = WIN_GL_UnloadLibrary; device->GL_CreateContext = WIN_GL_CreateContext; device->GL_MakeCurrent = WIN_GL_MakeCurrent; + device->GL_GetDrawableSize = WIN_GL_GetDrawableSize; device->GL_SetSwapInterval = WIN_GL_SetSwapInterval; device->GL_GetSwapInterval = WIN_GL_GetSwapInterval; device->GL_SwapWindow = WIN_GL_SwapWindow; @@ -215,9 +226,62 @@ VideoBootStrap WINDOWS_bootstrap = { "windows", "SDL Windows video driver", WIN_Available, WIN_CreateDevice }; +static void +WIN_SetDPIAware(_THIS) +{ + SDL_VideoData *data = SDL_static_cast(SDL_VideoData *, _this->driverdata); + if (data->SetProcessDpiAwarenessContext) { + /* Windows 10 Anniversary Update+ */ + /* First, try the Windows 10 Creators Update version */ + BOOL result = data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (!result) { + result = data->SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + } + + /* + NOTE: The above calls will fail if the DPI awareness was already set outside of SDL. + It doesn't matter if they fail, we will just use whatever DPI awareness level was set externally. + */ + data->highdpi_enabled = SDL_TRUE; + } else if (data->SetProcessDpiAwareness) { + /* Windows 8.1-Windows 10 */ + HRESULT result = data->SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE); + + data->highdpi_enabled = SDL_TRUE; + } else if (data->SetProcessDPIAware) { + /* Vista-Windows 8.0 */ + BOOL success = data->SetProcessDPIAware(); + + data->highdpi_enabled = SDL_TRUE; + } + + /* NOTE: we won't set data->highdpi_enabled below Vista. + In theory we could still check LOGPIXELSX/Y on XP. */ +} + int WIN_VideoInit(_THIS) { + SDL_VideoData *data = SDL_static_cast(SDL_VideoData *, _this->driverdata); + + /* Set the process DPI awareness */ + if (SDL_GetHintBoolean(SDL_HINT_VIDEO_ALLOW_HIGHDPI, SDL_FALSE)) { + WIN_SetDPIAware(_this); + } + + /* Cache LOGPIXELSX/LOGPIXELSY. */ + { + HDC hdc = GetDC(NULL); + if (hdc) { + data->system_xdpi = GetDeviceCaps(hdc, LOGPIXELSX); + data->system_ydpi = GetDeviceCaps(hdc, LOGPIXELSY); + ReleaseDC(NULL, hdc); + } else { + data->system_xdpi = 96; + data->system_ydpi = 96; + } + } + if (WIN_InitModes(_this) < 0) { return -1; } diff --git a/src/video/windows/SDL_windowsvideo.h b/src/video/windows/SDL_windowsvideo.h index ed94abde8..9762ae1e8 100644 --- a/src/video/windows/SDL_windowsvideo.h +++ b/src/video/windows/SDL_windowsvideo.h @@ -85,8 +85,38 @@ 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; + #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*); @@ -134,13 +164,27 @@ 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); void* shcoreDLL; HRESULT (WINAPI *GetDpiForMonitor)( HMONITOR hmonitor, MONITOR_DPI_TYPE dpiType, UINT *dpiX, UINT *dpiY ); - + HRESULT (WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness); + + SDL_bool highdpi_enabled; + int system_xdpi; + int system_ydpi; + SDL_bool ime_com_initialized; struct ITfThreadMgr *ime_threadmgr; SDL_bool ime_initialized; diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index 32849c442..efe1f7c25 100644 --- a/src/video/windows/SDL_windowswindow.c +++ b/src/video/windows/SDL_windowswindow.c @@ -25,6 +25,7 @@ #include "../../core/windows/SDL_windows.h" #include "SDL_assert.h" +#include "SDL_log.h" #include "../SDL_sysvideo.h" #include "../SDL_pixels_c.h" #include "../../events/SDL_keyboard_c.h" @@ -45,6 +46,8 @@ #define SWP_NOCOPYBITS 0 #endif +/* #define HIGHDPI_DEBUG */ + /* Fake window to help with DirectInput events. */ HWND SDL_HelperWindow = NULL; static WCHAR *SDL_HelperWindowClassName = TEXT("SDLHelperWindowInputCatcher"); @@ -85,28 +88,94 @@ GetWindowStyle(SDL_Window * window) return style; } +/* +in: client rect (in Windows coordinates) +out: window rect, including frame (in Windows coordinates) +*/ static void -WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x, int *y, int *width, int *height, SDL_bool use_current) +WIN_AdjustWindowRectWithStyleAndRect(SDL_Window *window, DWORD style, BOOL menu, int *x, int *y, int *width, int *height) { + const SDL_WindowData *data = (SDL_WindowData *)window->driverdata; RECT rect; rect.left = 0; rect.top = 0; - rect.right = (use_current ? window->w : window->windowed.w); - rect.bottom = (use_current ? window->h : window->windowed.h); + rect.right = *width; + rect.bottom = *height; // 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 (data->videodata->highdpi_enabled && data->videodata->AdjustWindowRectExForDpi) { + data->videodata->AdjustWindowRectExForDpi(&rect, style, menu, 0, data->scaling_xdpi); + } 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; + *x += rect.left; + *y += rect.top; *width = (rect.right - rect.left); *height = (rect.bottom - rect.top); } +/* +out: window rect, including frame (in Windows coordinates) +*/ static void +WIN_AdjustWindowRectWithStyle(SDL_Window *window, DWORD style, BOOL menu, int *x, int *y, int *width, int *height, SDL_bool use_current) +{ + int x_win, y_win; + int w_win, h_win; + + const int x_sdl = (use_current ? window->x : window->windowed.x); + const int y_sdl = (use_current ? window->y : window->windowed.y); + const int w_sdl = (use_current ? window->w : window->windowed.w); + const int h_sdl = (use_current ? window->h : window->windowed.h); + + x_win = x_sdl; + y_win = y_sdl; + w_win = w_sdl; + h_win = h_sdl; + WIN_ScreenRectFromSDL(&x_win, &y_win, &w_win, &h_win); + + /* NOTE: we don't use the width/height returned by WIN_ScreenRectFromSDL, + (which is making a guess of which monitor the rect is considered to be on) + but instead calculate width/height using WIN_ClientPointFromSDL + which is using the DPI values that Windows considers the window to have. + */ + w_win = w_sdl; + h_win = h_sdl; + WIN_ClientPointFromSDL(window, &w_win, &h_win); + + WIN_AdjustWindowRectWithStyleAndRect(window, style, menu, &x_win, &y_win, &w_win, &h_win); + + *x = x_win; + *y = y_win; + *width = w_win; + *height = h_win; +} + +/* +in: client rect (in Windows coordinates) +out: window rect, including frame (in Windows coordinates) +*/ +void +WIN_AdjustWindowRectWithRect(SDL_Window *window, int *x, int *y, int *width, int *height) +{ + SDL_WindowData *data = (SDL_WindowData *)window->driverdata; + HWND hwnd = data->hwnd; + DWORD style; + BOOL menu; + + style = GetWindowLong(hwnd, GWL_STYLE); + menu = (style & WS_CHILDWINDOW) ? FALSE : (GetMenu(hwnd) != NULL); + WIN_AdjustWindowRectWithStyleAndRect(window, style, menu, x, y, width, height); +} + +/* +out: window rect, including frame (in Windows coordinates) +*/ +void WIN_AdjustWindowRect(SDL_Window *window, int *x, int *y, int *width, int *height, SDL_bool use_current) { SDL_WindowData *data = (SDL_WindowData *)window->driverdata; @@ -135,13 +204,56 @@ WIN_SetWindowPositionInternal(_THIS, SDL_Window * window, UINT flags) top = HWND_NOTOPMOST; } +#ifdef HIGHDPI_DEBUG + SDL_Log("WIN_SetWindowPositionInternal: SDL window rect: (%d, %d) (%d x %d)", window->x, window->y, window->w, window->h); +#endif + WIN_AdjustWindowRect(window, &x, &y, &w, &h, SDL_TRUE); +#ifdef HIGHDPI_DEBUG + SDL_Log("WIN_SetWindowPositionInternal: calling SetWindowPos (%d, %d) (%d x %d)", x, y, w, h); +#endif + data->expected_resize = SDL_TRUE; SetWindowPos(hwnd, top, x, y, w, h, flags); data->expected_resize = SDL_FALSE; } +static void +WIN_GetDPIForHWND(const SDL_VideoData *videodata, HWND hwnd, int *xdpi, int *ydpi) +{ + *xdpi = 96; + *ydpi = 96; + + /* highdpi not requested? */ + if (!videodata->highdpi_enabled) + return; + + /* Window 10+ */ + if (videodata->GetDpiForWindow) { + *xdpi = videodata->GetDpiForWindow(hwnd); + *ydpi = *xdpi; + return; + } + + /* window 8.1+ */ + if (videodata->GetDpiForMonitor) { + HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (monitor) { + UINT xdpi_uint, ydpi_uint; + if (S_OK == videodata->GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &xdpi_uint, &ydpi_uint)) { + *xdpi = xdpi_uint; + *ydpi = ydpi_uint; + } + } + return; + } + + /* Windows Vista-8.0 */ + *xdpi = videodata->system_xdpi; + *ydpi = videodata->system_ydpi; +} + static int SetupWindowData(_THIS, SDL_Window * window, HWND hwnd, HWND parent, SDL_bool created) { @@ -162,6 +274,7 @@ SetupWindowData(_THIS, SDL_Window * window, HWND hwnd, HWND parent, SDL_bool cre data->mouse_button_flags = 0; data->videodata = videodata; data->initializing = SDL_TRUE; + WIN_GetDPIForHWND(videodata, hwnd, &data->scaling_xdpi, &data->scaling_ydpi); window->driverdata = data; @@ -189,12 +302,16 @@ SetupWindowData(_THIS, SDL_Window * window, HWND hwnd, HWND parent, SDL_bool cre } #endif + /* Move window to the correct monitor and size it */ + WIN_SetWindowPositionInternal(_this, window, SWP_NOCOPYBITS | SWP_NOACTIVATE); + /* Fill in the SDL window with the window data */ { RECT rect; if (GetClientRect(hwnd, &rect)) { int w = rect.right; int h = rect.bottom; + WIN_ClientPointToSDL(window, &w, &h); if ((window->w && window->w != w) || (window->h && window->h != h)) { /* We tried to create a window larger than the desktop and Windows didn't allow it. Override! */ int x, y; @@ -210,12 +327,18 @@ SetupWindowData(_THIS, SDL_Window * window, HWND hwnd, HWND parent, SDL_bool cre } } { + RECT rect; POINT point; point.x = 0; point.y = 0; - if (ClientToScreen(hwnd, &point)) { - window->x = point.x; - window->y = point.y; + if (ClientToScreen(hwnd, &point) && GetClientRect(hwnd, &rect)) { + int x = point.x; + int y = point.y; + int w = rect.right; + int h = rect.bottom; + WIN_ScreenRectToSDL(&x, &y, &w, &h); + window->x = x; + window->y = y; } } { @@ -286,8 +409,6 @@ WIN_CreateWindow(_THIS, SDL_Window * window) { HWND hwnd, parent = NULL; DWORD style = STYLE_BASIC; - int x, y; - int w, h; if (window->flags & SDL_WINDOW_SKIP_TASKBAR) { parent = CreateWindow(SDL_Appname, TEXT(""), STYLE_BASIC, 0, 0, 32, 32, NULL, NULL, SDL_Instance, NULL); @@ -295,11 +416,15 @@ 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_TRUE); - + /* For high-DPI support, it's easier / more robust** to create the window + with a width/height of 0, then in SetupWindowData we will check the + DPI and adjust the position and size to match window->x,y,w,h. + + **The reason is, we can't know window DPI for sure until after the + window is created. + */ hwnd = - CreateWindow(SDL_Appname, TEXT(""), style, x, y, w, h, parent, NULL, + CreateWindow(SDL_Appname, TEXT(""), style, CW_USEDEFAULT, 0, 0, 0, parent, NULL, SDL_Instance, NULL); if (!hwnd) { return WIN_SetError("Couldn't create window"); @@ -561,14 +686,31 @@ WIN_RestoreWindow(_THIS, SDL_Window * window) void WIN_SetWindowFullscreen(_THIS, SDL_Window * window, SDL_VideoDisplay * display, SDL_bool fullscreen) { + SDL_DisplayData *displaydata = (SDL_DisplayData *) display->driverdata; SDL_WindowData *data = (SDL_WindowData *) window->driverdata; + SDL_VideoData *videodata = data->videodata; HWND hwnd = data->hwnd; - SDL_Rect bounds; + MONITORINFO minfo; DWORD style; HWND top; int x, y; int w, h; + /* BUG: windows don't receive a WM_DPICHANGED message after a ChangeDisplaySettingsEx, + so we must manually update the cached DPI (see WIN_SetDisplayMode). */ +#ifdef HIGHDPI_DEBUG + { + int xdpi, ydpi; + WIN_GetDPIForHWND(videodata, hwnd, &xdpi, &ydpi); + SDL_Log("WIN_SetWindowFullscreen: dpi: %d, stale cached dpi: %d", xdpi, data->scaling_xdpi); + } +#endif + WIN_GetDPIForHWND(videodata, hwnd, &data->scaling_xdpi, &data->scaling_ydpi); + + /* clear the window size, to cause us to send a SDL_WINDOWEVENT_RESIZED event in WM_WINDOWPOSCHANGED */ + data->window->w = 0; + data->window->h = 0; + if (SDL_ShouldAllowTopmost() && ((window->flags & (SDL_WINDOW_FULLSCREEN|SDL_WINDOW_INPUT_FOCUS)) == (SDL_WINDOW_FULLSCREEN|SDL_WINDOW_INPUT_FOCUS) || window->flags & SDL_WINDOW_ALWAYS_ON_TOP)) { top = HWND_TOPMOST; } else { @@ -579,13 +721,20 @@ WIN_SetWindowFullscreen(_THIS, SDL_Window * window, SDL_VideoDisplay * display, style &= ~STYLE_MASK; style |= GetWindowStyle(window); - WIN_GetDisplayBounds(_this, display, &bounds); + /* Use GetMonitorInfo instead of WIN_GetDisplayBounds because we want the + monitor bounds in Windows coordinates (pixels) rather than SDL coordinates (points). */ + SDL_zero(minfo); + minfo.cbSize = sizeof(MONITORINFO); + if (!GetMonitorInfo(displaydata->MonitorHandle, &minfo)) { + SDL_SetError("GetMonitorInfo failed"); + return; + } if (fullscreen) { - x = bounds.x; - y = bounds.y; - w = bounds.w; - h = bounds.h; + x = minfo.rcMonitor.left; + y = minfo.rcMonitor.top; + w = minfo.rcMonitor.right - minfo.rcMonitor.left; + h = minfo.rcMonitor.bottom - minfo.rcMonitor.top; /* Unset the maximized flag. This fixes https://bugzilla.libsdl.org/show_bug.cgi?id=3215 @@ -901,6 +1050,48 @@ WIN_SetWindowOpacity(_THIS, SDL_Window * window, float opacity) return 0; } +void +WIN_GetDrawableSize(const SDL_Window *window, int *w, int *h) +{ + const SDL_WindowData *data = ((SDL_WindowData *)window->driverdata); + HWND hwnd = data->hwnd; + RECT rect; + + if (GetClientRect(hwnd, &rect)) { + *w = rect.right; + *h = rect.bottom; + } else { + *w = 0; + *h = 0; + } +} + +void +WIN_ClientPointToSDL(const SDL_Window *window, int *x, int *y) +{ + const SDL_WindowData *data = ((SDL_WindowData *)window->driverdata); + const SDL_VideoData *videodata = data->videodata; + + if (!videodata->highdpi_enabled) + return; + + *x = MulDiv(*x, 96, data->scaling_xdpi); + *y = MulDiv(*y, 96, data->scaling_ydpi); +} + +void +WIN_ClientPointFromSDL(const SDL_Window *window, int *x, int *y) +{ + const SDL_WindowData *data = ((SDL_WindowData *)window->driverdata); + const SDL_VideoData *videodata = data->videodata; + + if (!videodata->highdpi_enabled) + return; + + *x = MulDiv(*x, data->scaling_xdpi, 96); + *y = MulDiv(*y, data->scaling_ydpi, 96); +} + #endif /* SDL_VIDEO_DRIVER_WINDOWS */ /* vi: set ts=4 sw=4 expandtab: */ diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h index 421469b29..7076b0d1b 100644 --- a/src/video/windows/SDL_windowswindow.h +++ b/src/video/windows/SDL_windowswindow.h @@ -50,8 +50,15 @@ typedef struct #if SDL_VIDEO_OPENGL_EGL EGLSurface egl_surface; #endif + /* NOTE: differing xdpi and ydpi is legacy-only, but we still support it. + On Windows 8.1+ it shouldn't happen (GetDpiForMonitor docs promise xdpi and ydpi are equal) + and on Windows 10+ it won't (GetDpiForWindow has a single return value).*/ + int scaling_xdpi; + int scaling_ydpi; } SDL_WindowData; +extern void WIN_AdjustWindowRectWithRect(SDL_Window *window, int *x, int *y, int *width, int *height); +extern void WIN_AdjustWindowRect(SDL_Window *window, int *x, int *y, int *width, int *height, SDL_bool use_current); extern int WIN_CreateWindow(_THIS, SDL_Window * window); extern int WIN_CreateWindowFrom(_THIS, SDL_Window * window, const void *data); extern void WIN_SetWindowTitle(_THIS, SDL_Window * window); @@ -77,6 +84,9 @@ extern SDL_bool WIN_GetWindowWMInfo(_THIS, SDL_Window * window, extern void WIN_OnWindowEnter(_THIS, SDL_Window * window); extern void WIN_UpdateClipCursor(SDL_Window *window); extern int WIN_SetWindowHitTest(SDL_Window *window, SDL_bool enabled); +extern void WIN_GetDrawableSize(const SDL_Window *window, int *w, int *h); +extern void WIN_ClientPointToSDL(const SDL_Window *window, int *w, int *h); +extern void WIN_ClientPointFromSDL(const SDL_Window *window, int *w, int *h); #endif /* SDL_windowswindow_h_ */