Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: DPI-Scaling on Windows  (Read 3936 times)

0 Members and 1 Guest are viewing this topic.

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
DPI-Scaling on Windows
« on: November 20, 2023, 10:46:46 pm »
I'm not sure if this is a help request, a feature request or actually providing help to others so it might be better in a different category...

Before we start, you should know that this is not intended to be just complaints. I'd really like to help improve this. First, though, we need to understand what the problems actually are and why they are problems.

One of the main things that has irritated me with using SFML is trying to get it to place nicely with already created windows. Note that my points here will about about Microsoft Windows only.

Creating a window with SFML is simple; it's great!
However, it lacks some features that require the ability to control the window yourself (adding a 'proper' window menu, for example). This can be ignored a lot of the time for most games but falls flat for other (more serious/professional) application types, forcing windows to be created without SFML.

So, instead we can create a window manually (this isn't really much of an issue but it's nicer when SFML does the work for you!). Then, we can pass the window's handle to an SFML window and it can take over control for that window. This is also a nice feature.

However, this starts to create some issues when faced with DPI-scaling. Newer operating systems use scaling and often start with a scale of not 100%. For example, I'm using Windows 11 and the default scale was 150%.

The issue is that things should be scaled by this value so that the user experiences scaled things automatically. If they don't, they are ignoring the user's request; this is an accessibility issue. When you create a window manually, it "cares" about the user's decision and it is scaled. The app itself doesn't see the scale and the accessibility is applied automatically.

If you pass control of a window already created (and scaled properly) to SFML, SFML removes scaling so that its size matches its expected number of pixels. This is understandable for when you are working with exact pixels. This means that window "shrinks" (assuming a larger scale in my examples) to fit SFML's preferences.
Firstly, I'd say that this in itself is a bit of an issue since the reason we create a window without SFML is access to window features that SFML doesn't provide and this removes the ability to do this.

If you've programmed directly in Windows already, you'll understand that you can create (child) windows within other windows (almost everything is a window!). So, creating a window as a child of the actual window as a destination for SFML graphics to be displayed is actually a good idea. This allows you to place SFML graphics in specified rectangles around the window.

Unfortunately, when you pass this child window to SFML, SFML will remove scaling - for everything! That includes its parent window! This means that you cannot create and control one window and give a child window to SFML to play with as it affects the parent window as well.

In addition to this, SFML's removal of the scaling actually affects all windows by the instance so all windows will be "unscaled" by SFML even if not at all related to the window used SFML. This is quite a disturbing situation.

It's worth noting too, that when the window is "unscaled" by SFML, the title bar is also "unscaled" and I've seen the window title disappearing as it's too small!

I should mention that I've looked around for previous info about this and a few things popped up that might interest someone experiencing issues with scaling:
https://en.sfml-dev.org/forums/index.php?topic=17092.0
https://en.sfml-dev.org/forums/index.php?topic=19617.msg141246#msg141246
https://en.sfml-dev.org/forums/index.php?topic=28770.msg178734#msg178734
https://github.com/SFML/SFML/pull/2268
All are many years old.

There doesn't seem to be anything in SFML 3's list of task related to this, unfortunately.

I will also provide a "simple" sample code you can use to see it in action:
#include <SFML/Graphics.hpp>
#include <Windows.h>

LRESULT CALLBACK MainWindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK ChildWindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK Main2WindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

int main()
{
        HINSTANCE hInstance{ GetModuleHandle(NULL) };



        // main window
        const wchar_t mainClassName[] = L"Main Window Class";
        WNDCLASS wc = { };
        wc.lpfnWndProc = MainWindowProcedure;
        wc.hInstance = hInstance;
        wc.lpszClassName = mainClassName;
        RegisterClass(&wc);

        HWND hWnd = CreateWindowEx(
                0, mainClassName, L"SFML in child window test", WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT, // position
                1000, 1000, // size
                NULL, NULL, hInstance, NULL
        );
        if (!hWnd)
                return EXIT_FAILURE;

        ShowWindow(hWnd, SW_SHOWNORMAL);



        // reference window (window 2: identical to - but independent from - main window)
        const wchar_t main2ClassName[] = L"Main 2 Window Class";
        WNDCLASS wc2 = { };
        wc2.lpfnWndProc = Main2WindowProcedure;
        wc2.hInstance = hInstance;
        wc2.lpszClassName = main2ClassName;
        RegisterClass(&wc2);

        HWND hWnd2 = CreateWindowEx(
                0, main2ClassName, L"SFML in child window test 2 (for reference size)", WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT, // position
                1000, 1000, // size
                NULL, NULL, hInstance, NULL
        );
        if (!hWnd2)
                return EXIT_FAILURE;

        ShowWindow(hWnd2, SW_SHOWNORMAL);



        // child window (child of main window, not window 2)
        const wchar_t childClassName[] = L"Child Window Class";
        WNDCLASS wcChild = { };
        wcChild.lpfnWndProc = ChildWindowProcedure;
        wcChild.hInstance = hInstance;
        wcChild.lpszClassName = childClassName;
        RegisterClass(&wcChild);

        HWND hWndChild = CreateWindowEx(
                0, childClassName, L"Child Window", WS_CHILD | WS_BORDER,
                250, 250, // position
                500, 500, // size
                hWnd, // parent
                NULL, hInstance, NULL
        );
        if (!hWndChild)
                return EXIT_FAILURE;

        ShowWindow(hWndChild, SW_SHOWNORMAL);



       
        sf::RenderWindow sfmlWindow(hWndChild); // sfml window is created from the child window (of main window)



        sfmlWindow.clear(sf::Color(255u, 160u, 64u)); // fill the SFML window (child) with a solid colour
        sfmlWindow.display();



        MSG msg{};
        while (GetMessage(&msg, NULL, 0, 0) > 0)
        {
                TranslateMessage(&msg);
                DispatchMessage(&msg);
        }
}

LRESULT CALLBACK MainWindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
        switch (msg)
        {
        case WM_DESTROY:
                PostQuitMessage(0);
                return 0;
        default:
                return DefWindowProc(hWnd, msg, wParam, lParam);
        }
}

LRESULT CALLBACK ChildWindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
        switch (msg)
        {
        case WM_DESTROY:
                PostQuitMessage(0);
                return 0;
        default:
                return DefWindowProc(hWnd, msg, wParam, lParam);
        }
}

LRESULT CALLBACK Main2WindowProcedure(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
        switch (msg)
        {
        case WM_DESTROY:
                PostQuitMessage(0);
                return 0;
        default:
                return DefWindowProc(hWnd, msg, wParam, lParam);
        }
}
This is complete and should compile (you will need to link SFML Graphics as normal). Note that if you are using "Multi-byte Character Set" instead of "Unicode Character Set", you'll need to either change it to the Unicode one or replace the "wide" characters with standard ones.

Note:
this code also outputs an error (that I must admit that I don't understand):
An internal OpenGL call failed in RenderWindow.cpp(125).
Expression:
   glGetIntegerv(GLEXT_GL_FRAMEBUFFER_BINDING, reinterpret_cast<GLint*>(&m_defaultFrameBuffer))
Error description:
   GL_INVALID_OPERATION
   The specified operation is not allowed in the current state.



And finally...
As mentioned at the beginning of this post, I will also be giving some help/advice.

As a work-around, you can fix this issue yourself but it's on a "per exe file" basis.

To do this, you can modify the properties of the executable manually (right-click file and click on Properties).
Then, go to Compatibility tab, click on Change high DPI settings, activate Override high DPI scaling behaviour and choose Application or System from the drop-down menu.
Application lets SFML scale the window but the title bar still works.
System disallows SFML from changing the scaling of the windows.



In conclusion, having to "override" settings per file is a bit of a dirty "hack" and it would be cleaner if we could at minimum let SFML know we don't want it to be in charge of scaling.
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

eXpl0it3r

  • SFML Team
  • Hero Member
  • *****
  • Posts: 11030
    • View Profile
    • development blog
    • Email
Re: DPI-Scaling on Windows
« Reply #1 on: November 25, 2023, 09:10:24 pm »
I moved it to the SFML development sub-forum, because I think we do need to tackle this in some way.

I see four "focus areas":
  • Scaling behavior of the window content
  • Scaling behavior of the window decoration
  • API to get the scaled and unscaled sizes
  • Interaction/integration with window-handle created windows / parent windows

We should provide an "intuitive" way to handle the window scaling. Meaning it should act as any other window on the system, while however giving the possibility to render unscaled as well (use the pixels available!).

Yet the window decoration (i.e. title bar) should match the scaling of other windows at all times. Just because we may want to not scale the content, doesn't mean we would also not want to scale the title bar.

One possibility would be to have an API that provides us with the two sizes of the window content. One with and one without scaling. That way users would be able to calculate the differences on their own.

While you used number 4 as an example of why the current setup is problematic, I think the use case of parent-child combination etc. is at a lower priority, than the other three points.

Goal

Maybe to take one step back and formulate what's the overall goal of handling DPI scaling.
DPI scaling itself primarily solves the problem of having too many pixel on a given display panel (DPI = dots per inch), which then would give you tiny user interfaces if 1 dot was mapped to 1 pixel. By defining a scaling factor one can turn the tiny UI into something readable on the same "high DPI" display.

But now when it comes to rendering things in games, we may want to make use of the more detailed resolution that these "high DPI" displays can give us, instead of just scaling everything up. Rendering a simple diagonal line at a higher resolution, will give a much smoother line, even before enabling AA.

Of course each OS handles things a bit differently:

Windows

Seems like Windows for the longest time only support setting the DPI scaling per process and allowing it to be set only once. Which is why when you change it in a parent window, it applies to all the sub window, as they're part of the same process and which is also why you can override it in "explorer".

It looks like from Windows 10 on there's a thread-based function as well, which MSDN actually recommends to use instead.


With texus' PR we switched from PROCESS_SYSTEM_DPI_AWARE to DPI_AWARENESS_PER_MONITOR_AWARE, which might make things slightly better, however I also then read on the documentation:
Quote from: MSDN
It is important to note that if your application has a DPI_AWARENESS_PER_MONITOR_AWARE window, you are responsible for keeping track of the DPI by responding to WM_DPICHANGED messages.
Which we currently aren't doing, but texus proposed an implementation overall to get a consistent behavior: https://github.com/texus/SFML/commit/e813c9575fa7a46edacb472b6c5e79d22f170a94

Glossing over what texus wrote, I believe we could more or less take up their solution and make it the default behavior. I believe there's no need to have a flag to enable or disable high DPI support. But then on top of that, provide a function to retrieve the current "actual" window/rendering size, so if anyone wants to render at a high resolution, they can calculate the factor themselves.
Official FAQ: https://www.sfml-dev.org/faq.php
Official Discord Server: https://discord.gg/nr4X7Fh
——————————————————————
Dev Blog: https://duerrenberger.dev/blog/

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: DPI-Scaling on Windows
« Reply #2 on: November 25, 2023, 09:51:17 pm »
Thanks for moving it and managing to read through it all! ;D

I should mention I linked to texus' post in my original post and did wonder why there was no discussion there.

In Windows, it's actually recommended to not automatically scale. Newer apps 'should' be marking themselves as "DPI aware" and deal with scaling themselves. Automatic scaling is for apps that cannot deal with it manually, where it just scales everything (including blurriness).
Creating a window that is "DPI aware" removes automatic scaling that is the issue when adding SFML (it removes it - but also affects the title bar for some reason).
With that said, I believe it should be possible to create an app that is not DPI-aware (where it scales automatically) and have SFML accept this and not scale the already-created windows.

Just for clarity, by the way, giving a child window to SFML causes its parent window (as well as other non-related windows) to descale. It's not just if you give a parent with to SFML.

Anyway, I should mention that creating an app that marks itself as DPI-aware involves extra steps, making everything just that little bit less simple.

The reason I mention it with an external window ("number 4") is that it's the most obvious situation, albeit probably less used. Creating SFML windows directly never show bad title bars, for example. The main issue is that it looks like SFML can't actually do the thing it pretends it can: take over an already-created window (not my opinion, by the way). It looks that way to anyone immediately trying it so it would easily put people off. Obviously, though, most of it can be overcome with some work.

I believe texus' implementation/proposal is intended to keep scaling matching across multiple monitors and it seems to do this by affecting scaling to make it seem the same size. This is still interfering with scaling (even if you don't want it to). Also, I'm unsure as to how it handles multiple windows: if you move one window to another monitor, does it rescale that window (affecting the other window not on that monitor)?
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

texus

  • Hero Member
  • *****
  • Posts: 505
    • View Profile
    • TGUI
    • Email
Re: DPI-Scaling on Windows
« Reply #3 on: November 27, 2023, 08:06:30 pm »
I have no idea how things will work with child windows or windows that weren't created with SFML, so I won't comment on that.

Quote
Scaling behavior of the window content

The thing that needs to be discussed is how much options are left to the user. Will SFML force a certain scaling or will it provide many different options. I'll list the possible options that I'm aware of below.

1) DPI-Unaware

When using this, Windows will scale the entire window, including the title bar. Windows does everything for you and the developer doesn't has to do anything. The big downside is that the whole window will be blurry, making text less nice to read.

This mode is really intended for legacy applications that were written without DPI in mind.

2) Don't scaling anything (Per Monitor Aware V1 without handling WM_DPICHANGED)

This is what we currently do in SFML 2.6 and what my PR does if you don't set the highDpi flag.

Both the window contents and the title bar are unscaled and show up as if the monitor had 100% scaling. The window will look too small for people that have DPI scaling on their monitor. While that clearly isn't an ideal thing to do, it gives all the control to the developer. The window has exactly the amount of pixels that the developer wanted, without SFML manipulating anything.

The problem with this method is mainly that SFML doesn't provide enough tools to the developer to actually do anything themself. They would need to know the DPI scaling of each monitor, and get an event when the window switches monitor. Only then would they be able to manually decide what the wanted window size should be.

3) Scaling everything automatically (Per Monitor Aware V2)

This is what my PR does with the highDpi flag.
The advantage is that SFML takes care of all the DPI changes for the user, the downside is that it takes all control away from the user.

I'm sure there are alternative ways to implement this, but this was the easiest that I could think of: only manipulating the window size on window creation and on DPI changes (e.g. when switching monitor). When a window of 800x600 is requested on a monitor with 200% scaling, a window of size 1600x1200 will be created instead.

While SFML doesn't has to do the same as SDL or GLFW, it might be useful to know how they handle things:
- SDL uses option 1 (DPI-Unaware) as default. When a high-DPI flag is set, they act similar to option 3 (scaling contents and title bar automatically). The big difference with my implementation is that SDL chose to keep the window size and mouse events unscaled. So even with the high-DPI flag set, the real window size and rendering area are larger than what SDL_GetWindowSize reports, and you have to manually scale mouse events to figure out where on the render target the mouse is located.
- GLFW uses option 2 (not scaling anything) and option 3 (scaling contents and title bar automatically) based on a global flag. My PR code was heavily influenced by this design, because I liked how it kept the window and rendering area the same unlike with SDL.

Quote
Scaling behavior of the window decoration

With scaling options 1 and 3 above, the contents and decoration are scaled the same. For option 2, you could provide an alternative that would not scale the contents but would scale the title bar. So that gives a 4th scaling option:

4) Scaling only title bar (Per Monitor Aware V2 while keeping the window at a fixed size)

I'm not sure how easy or difficult this is to implement, I do know it requires handling WM_DPICHANGED. When moving monitor, Windows won't resize the window. So when it changes the title bar size, it also changes the client size of the window (which you wouldn't want). So you would need to implement WM_DPICHANGED in such a way that it reverts the change that Windows automatically makes (this will likely be similar to the code from option 3).

Quote
API to get the scaled and unscaled sizes

With all 4 scaling modes mentioned above (at least when implementing it like I did in the PR), the window size and rendering area are the exact same, so no additional functions are needed. The size of the window on screen is also the size reported when requesting the window size in SFML.
The only case where the scaled and unscaled sizes differ is with "DPI-Unaware", but there Windows is intentionally providing SFML with the wrong window size, so as far as SFML can tell the rendering size is still the same as the window size (and Windows will just stretch it when displaying).

One thing that needs to be done is make people aware that the window size will no longer be what they specify in the API. That's why I had the highDpi flag: to keep thing as expected until the user made the choice to activate the new behavior. But if the other scaling modes are deemed as unneeded, then you could just force the behavior on everyone in SFML 3 without the flag. Although I can think of at least one case where you might want to have such flag (to turn scaling off): what if the user queries the monitor size and tries to create a window that matches it? SFML shouldn't change the window scale at that moment.
« Last Edit: November 28, 2023, 04:57:24 pm by texus »
TGUI: C++ SFML GUI

Hapax

  • Hero Member
  • *****
  • Posts: 3379
  • My number of posts is shown in hexadecimal.
    • View Profile
    • Links
Re: DPI-Scaling on Windows
« Reply #4 on: November 29, 2023, 03:48:51 pm »
Even though new apps should be DPI-aware, I think the default for SFML should be to assume that they aren't. This simplifies things for people unaware of how to work with scaling (as you said: "Windows does everything for you and the developer doesn't has to do anything.")

Of course, being DPI-aware is what we should all be aiming for so it should be possible with SFML but it should be "opt-in" because it's basically presumed at the moment. This is "not scaling" as you mentioned although it technically removes scaling if there is any already present.

The good thing about being opt-in is that it makes sure that the developer is DPI-aware so everything to do with scaling should be expected. It also means that the title bar shouldn't be an issue if externally created (if they have set it to be DPI-aware already).

Of course, we can mention in documentation and tutorials that windows may be automatically scaled and may produce slightly blurry visuals unless the windows are DPI-aware (and then have a tutorial on DPI-aware, preferably) so they know what to expect and how to learn how to avoid it if it isn't what they want.

Also, I'm curious as to how "monitor-aware" windows act when there are multiple windows on different windows and also how they act when they are across 2 (or more) windows e.g. part of the window on one monitor and part of it on another.


tldr;
To summarise the above, (I think that) DPI-unaware should be the default and DPI-aware should be opt-in.
Selba Ward -SFML drawables
Cheese Map -Drawable Layered Tile Map
Kairos -Timing Library
Grambol
 *Hapaxia Links*

texus

  • Hero Member
  • *****
  • Posts: 505
    • View Profile
    • TGUI
    • Email
Re: DPI-Scaling on Windows
« Reply #5 on: December 02, 2023, 05:57:59 pm »
Quote from: eXpl0it3r
It looks like from Windows 10 on there's a thread-based function as well, which MSDN actually recommends to use instead.
SetThreadDpiAwarenessContext does look like the best option (compared to SetProcessDpiAwareness which I was still using in my changes).

I learned today that it can be used to set a different DPI per window (not just per thread): https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-improvements-for-desktop-applications

So SFML would no longer need to change the global DPI setting (on Windows 10 1607 and newer) and can make the change only for the windows that it controls.

Quote from: Hapax
SFML's removal of the scaling actually affects all windows by the instance so all windows will be "unscaled" by SFML even if not at all related to the window used SFML
By using SetThreadDpiAwarenessContext and restoring the old value after creating the window in SFML, we can at least make sure the non-related window (and according to a quick test also the parent window) won't lose their scaling.

Quote from: eXpl0it3r
Scaling behavior of the window decoration
Is there any case where someone wants an unscaled title bar?
My 2nd scaling option "Don't scaling anything" is a bit of a hack, and all other options do scale the title bar according to the monitor DPI. So maybe we should only focus on the window contents and let Windows always handle the decoration (i.e. always use Per Monitor V2 scaling)?

Quote from: Hapax
DPI-unaware should be the default
SFML hasn't supported DPI-Unaware for almost 10 years already (since this commit), and apparently earlier it didn't work correctly (which is why that commit was made). If SFML did support it and people were relying on it, then I would probably agree that it should be kept as an option. However, since nobody has been able to use it, I don't see why SFML should do more effort now to allow people to start programming without taking DPI into account.

Personally I dislike blurry DPI-Unaware apps a lot, so my opinion on DPI-Unaware support in SFML might be a bit biased and you shouldn't put too much value in my opinion.

Quote from: eXpl0it3r
I believe there's no need to have a flag to enable or disable high DPI support
I think we probably do need some flag. Both DPI-Unaware or my "Scaling everything automatically" implementation would create a window at a different size as what the user requests. There should probably be some method for the user to request a window with an exact size (only the title bar would be scaled automatically).

Quote from: Hapax
Also, I'm curious as to how "monitor-aware" windows act when there are multiple windows on different windows and also how they act when they are across 2 (or more) windows e.g. part of the window on one monitor and part of it on another.
A normal window will switch scale when it's center is located on a different monitor. For child windows, according to a quick test, they seem to just scale based on the parent window (instead of having their own scaling). If I create the parent with PER_MONITOR_AWARE_V2 and the child with UNAWARE, then the child window never changes size (when I don't handle WM_DPICHANGED to resize the parent). If I create the parent with UNAWARE and the child with PER_MONITOR_AWARE_V2, the child is scaled with the parent window when switching monitor.
TGUI: C++ SFML GUI