-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Windows high-dpi / DPI scaling support #5778
Conversation
The hint allows setting a specific DPI awareness ("unaware", "system", "permonitor", "permonitorv2"). This is the first part of High-DPI support on Windows ( libsdl-org#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: libsdl-org#3286 libsdl-org#4712
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.
…tartup if defined document some additional quirks
Adds hint "SDL_WINDOWS_DPI_SCALING" which can be set to "1" to change the SDL coordinate system units to be DPI-scaled points, rather than pixels everywhere. This means windows will be appropriately sized, even when created on high-DPI displays with scaling. e.g. requesting a 640x480 window from SDL, on a display with 125% scaling in Windows display settings, will create a window with an 800x600 client area (in pixels). Setting this to "1" implicitly requests process DPI awareness (setting SDL_WINDOWS_DPI_AWARENESS is unnecessary), and forces SDL_WINDOW_ALLOW_HIGHDPI on all windows.
…NT_SIZE_CHANGED, not SDL_WINDOWEVENT_RESIZED Fixes bug with viewport not updating when moving window between monitors with different scale factors on Windows (this should also fix the same issue on other OS'es, though untested)
So For the sake of having a consistent API across all supported platforms, would it be worth adding a new function to SDL's API to set high-DPI support program-wide, to somewhat replace |
Yeah, exactly. Unifying the ways of declaring High-DPI support across platforms was my initial goal as well, e.g. instead of |
Sure, yeah. I'm in UTC-7/Mountain. Pretty open Saturday or Sunday. |
I can be around whenever, with a little advance notice. |
Sam Lantinga is inviting you to a scheduled Zoom meeting. Topic: Sam Lantinga's Zoom Meeting Join Zoom Meeting Meeting ID: 963 3721 2424 Dial by your location |
Sure, that works! |
Ok, it's on my calendar! |
Works for me |
Does SDL_render_d3d12.c not need the same touch as d3d and d3d11 renderers? |
Looks like it will need the same change, yeah |
Is the following good then? (not tested) diff --git a/src/render/direct3d12/SDL_render_d3d12.c b/src/render/direct3d12/SDL_render_d3d12.c
index a358a098d..b263e882e 100644
--- a/src/render/direct3d12/SDL_render_d3d12.c
+++ b/src/render/direct3d12/SDL_render_d3d12.c
@@ -240,7 +240,6 @@ static const GUID SDL_IID_ID3D12InfoQueue = { 0x0742a90b, 0xc387, 0x483f, { 0xb9
#endif
-
UINT
D3D12_Align(UINT location, UINT alignment)
{
@@ -485,6 +484,13 @@ D3D12_DestroyRenderer(SDL_Renderer * renderer)
SDL_free(renderer);
}
+static int
+D3D12_GetOutputSize(SDL_Renderer *renderer, int *w, int *h)
+{
+ WIN_GetDrawableSize(renderer->window, w, h);
+ return 0;
+}
+
static D3D12_BLEND
GetBlendFunc(SDL_BlendFactor factor)
{
@@ -1211,7 +1217,7 @@ D3D12_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 = D3D12_GetCurrentRotation();
if (D3D12_IsDisplayRotated90Degrees(data->rotation)) {
int tmp = w;
@@ -2936,6 +2942,7 @@ D3D12_CreateRenderer(SDL_Window * window, Uint32 flags)
data->identity = MatrixIdentity();
renderer->WindowEvent = D3D12_WindowEvent;
+ renderer->GetOutputSize = D3D12_GetOutputSize;
renderer->SupportsBlendMode = D3D12_SupportsBlendMode;
renderer->CreateTexture = D3D12_CreateTexture;
renderer->UpdateTexture = D3D12_UpdateTexture; |
@ericwa I'm trying to implement SDL2 support into my game now that this PR is merged and I'm running into some issues. I'm not sure whether I'm just missing something or what, or whether I should file a PR for this instead:
|
Cool, thanks for trying it out. I think the best approach currently is to call There's this previous feature request for adding a convenience API: #3580 BTW which specific functions are you referring to with
The intended way of doing this is to listen for I should note that I haven't seen cases where calling |
Alright, though I hope this isn't a permanent solution due to integer rounding errors and such.
I'm pretty sure this isn't the problem but I'm not sure what you mean with this. Is it not just virtual scaled pixels (i.e. not physical drawable pixels)? |
There shouldn't be any integer rounding errors if you do appropriate casting before the division. With GetDisplayDPI, keep in mind that it might not match the user's expected scaling factor (especially across different monitors with slightly different DPI and viewing distances). |
The problem is that SDL2's coordinate space is still integers right now. If you have something like a 2px wide window, this kind of test would give you a 2px drawable = 100% scale, even when the scale is actually 125%.
I found the problem. I'll send a PR with it fixed and also look into the By the way, isn't base DPI value on macOS 72 instead of 96? If so that might make it annoying to derive scale factor from for cross-plat apps since the base is different between macOS/Windows. |
Yeah, that's the problem. I had hoped that it'd work, since the docs say it's the monitor containing the central pixel of the window (which IIRC is also what Windows uses as metric to decide DPI) but alas. |
It's not meant to be used for determining DPI scale factors (whereas GetWindowSize and GetDrawableSize are) - I'm curious if your integer issue manifests in non-contrived situations though, since those APIs are basically the same as what half the operating systems provide to native app developers. (It'd be good to know the most useful approach to take for SDL3 or whatever.) |
Windows provides an exact integer DPI value, so this is not a problem there. Obviously it's a contrived example but it still feels important to report the actual data the OS returns instead of guessing. I don't even think Windows has a DPI-scaled |
What I mean is: it's not actually possible to create a 2x2 window (on many operating systems at least, maybe depending on window flags). I wonder if the possible-and-used-in-practice window sizes would ever run into that problem or not. Also Windows is just one backend of many, SDL has to work across several different implementation choices from different OS authors (as Eric discovered when trying to fit SDL's existing APIs with Windows' DPI scaling model). |
Probably not, but providing exact values is still nice. For example with this inaccurate system for fetching DPI I would need to manually do slight rounding on SDL2's values to account for this error, because otherwise my engine would constantly see changing content scales during window resize (which means re-rendering fonts and all that noise). With the exact value returned from the OS this is not an issue at all.
So obviously SDL2 covers more platforms here, but I checked GLFW's source for how they get their values and Windows, macOS, and Wayland all get exact integral scaling values (not int scaling factor but without float error) from their respective API to work with. I think it's worth providing for the platforms that do have it, both for accuracy and convenience. (I didn't even bother to check X11 since I know that platform is a broken joke for DPI scaling). On this note, I cross-checked both SDL2 and GLFW for how SDL2's display DPI values are reported on macOS and... am I right they're exact DPI values? On Windows, This makes |
I don't think it was ever intended for that use case (as I've said above). That being said I've personally never felt like it had a lot of legit use at all, aside from confusing people who did actually want DPI scale factors. |
One good use of exact DPI values is something like a 100% zoom scale on a PDF reader, so the user can have an idea of the exact size of the A4 page or whatever. Otherwise nothing much comes up in my mind.
I guess it doesn't help that on Windows it returns a value that looks like it's intended for that right now 😅 |
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Yeah - I was just commenting that the size included with |
Yeah, I agree, it's questionable how useful the physical DPI is, but I guess we can wrap what the OS provides. (Maybe it's accurate on Apple hardware?) However, there's an unfortunate disagreement between SDL backends - on Windows, I just double checked on macOS - SDL is returning an actual "pixels per inch" value based on the physical screen size returned by |
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Analogous to glfwGetWindowContentScale(). See comments on libsdl-org#5778
Description
This PR builds on top of #5393 to finish adding high DPI support on Windows. In this PR, our own "SDL" coordinate system (in DPI-scaled points) is established - so SDL window positions, sizes, mouse events, etc. are all in this new coordinate system.
Behaviour
Assuming the new hint
SDL_WINDOWS_DPI_SCALING
is set to1
:Creating a window that's 640x480 (as the size passed to SDL_CreateWindow), on a display with 125% scaling set in Windows display settings, will create a window with an 800x600 client area (in pixels). Dragging it to a second monitor that uses 100% scaling will resize the client area to 640x480 pixels.
SDL_WINDOWEVENT_SIZE_CHANGED
is emitted when a window's DPI changes. It always sends the window size in points, even as the client area size in pixels is changing due to DPI changes. This matches SDL's behaviour on macOS, I believe.SDL displays have their bounds position specified in pixels, but size in DPI-scaled points.
Display modes returned by SDL are currently always in pixels (technically violating the SDL documentation, which says they're points)
This is something @slime73 and I have discussed a bit in the past. To expand on what this means, my 4K monitor set to use 125% scaling (in Windows settings), which translates to a point size of 3072x1728
Currently, this PR will return SDL display modes for 3840x2160 (pixels) but not 3072x1728 (points).
The way Windows (ChangeDisplaySettingsExW) treats the issue is, any 3840x2160 pixel modes will return 120 DPI (=125% scale) as the monitor DPI when you change to those modes (given that I'm using 125% scaling on the desktop), and any other modes will return 96 DPI (=100% scale).
So, if we followed the docs and returned modes in points, 3840x2160 would not be listed in the mode list but rather 3072x1728. The rest of the modes would be the expected ones 2560x1440, 1920x1080, etc.
We could follow the docs strictly and return points - though not having a 3840x2160 mode on a 4K monitor seems strange to me, and we currently lack an API to query the pixel size of a display mode (if they're truly given as points).
SDL_WINDOWS_DPI_SCALING hint
The new DPI scaling is opt-in with a hint
SDL_WINDOWS_DPI_SCALING=1
. Setting this hint should be all that's needed for high DPI support if a game is already written with SDL's macOS/Wayland DPI awareness support in mind (i.e., as long as it usesSDL_GL_GetDrawableSize
/SDL_GetRendererOutputSize()
and doesn't assumeSDL_GetWindowSize()
is the drawable size in pixels).Without setting
SDL_WINDOWS_DPI_SCALING=1
, behaviour is as before (games that don't declare any DPI awareness will get Windows-provided "bitmap scaling" on a >100% scaled monitor. Games that declare DPI awareness somehow (manifest, callingSetProcessDpiAwareness
, etc.) will continue to get 1 SDL unit = 1 pixel.)Setting the hint
SDL_WINDOWS_DPI_SCALING=1
should cooperate with the DPI awareness being already set (or not) - if the process is not already DPI aware, SDL will attempt to enable "per-monitor V2", and if that's not available on the current version of Windows, fall back to older methods (the implementation used for this is from my earlier PR #5393). If the game is already DPI aware, andSDL_WINDOWS_DPI_SCALING=1
is requested, we'll use whatever the awareness was - theSDL_WINDOWS_DPI_SCALING=1
codepaths handle any of the dpi awareness levels (system, permonitor, permonitorv2).The window flag
SDL_WINDOW_ALLOW_HIGHDPI
is not used by this PR, although I did make theSDL_WINDOWS_DPI_SCALING=1
hint also force set theALLOW_HIGHDPI
flag on windows, in case application code is checking for that hint. It would be ideal if we could just use the window flag and not need a new hint but it doesn't look like that's an option. (In a previous discussion of my WIP patch there was a suggestion to try always enabling DPI awareness, then if an SDL window is created without theALLOW_HIGHDPI
flag, we could use the windows API's for setting DPI awareness per window to opt out of DPI awareness on those windows and use OS scaling. I tried this and couldn't get it to work, at least on Windows 10/Nvidia drivers - for OpenGL at least, the drivers seem to only care about the process DPI awareness setting and not respect the per-window settings. I couldn't find any bug reports or indication that anyone else has ever tried this approach either.)Implementation Comments
The changes fall into the following categories:
SDL_GetWindowSize
returns the backbuffer size in pixels (instead callingWIN_GetDrawableSize
, a wrapper aroundGetClientRect
) - this part is pretty straightforward.WIN_ClientPointToSDL
(converts pixels -> points),WIN_ScreenPointFromSDL
converts a SDL screen space coordinate like a window position, or a global mouse position to a Windows screen coordinate.#ifdef HIGHDPI_DEBUG
. As I've been maintaining / working on this patch on and off for several years now, I found it important to be able to turn on detailed logging with a #define. Also some things are more or less impossible to debug traditionally with breakpoints (e.g. exclusive fullscreen mode), so having detailed logging was imporatant.#ifdef HIGHDPI_DEBUG
logging if it seems excessiveWIN_ClientPointToSDL
,WIN_ClientPointFromSDL
) because we have a cached DPI value for the window.Known bugs
SDL_SetWindowPosition
when a window is overlapping 2 screens with different DPI scales can cause it to grow/shrinktestwm2
by placing the window on the seam between two monitors, then using the "Shift+Up/Down" arrow key bindings.WIN_AdjustWindowRectWithStyle
->WIN_ScreenPointFromSDL
is guessing a DPI based on the upper-left corner of the window's client area, which is different than the DPI Windows considers the window to have.Testing
To give ideas of how to test this, or areas to test that I've missed, here's what I've typically been trying:
Windows 10, 21H2
Nvidia 512.77
My monitor setup:
SDL test apps with different renderer / graphics API's:
SDL_WINDOWS_DPI_SCALING=1
testwm2 --renderer direct3d11
testwm2 --renderer direct3d
testwm2 --renderer opengl
testwm2 --min-geometry 640x480 --max-geometry 800x600
testgl2
testvulkan
testgles2
testautomation
testautomation --filter video_setWindowCenteredOnDisplay
Use cases I test:
Existing Issue(s)
#4908
#2119