The 2024 Wheel Reinvention Jam just concluded. See the results.

Be Aware of High DPI

Timothy Barnes
I will show in this article why game programmers should be aware of High DPI monitors and the way that operating systems such as Windows work with them. I will show the quality increase you can get by making your application High DPI aware on a platform that supports desktop scaling, as well as the problems with text rendering that show up when the operating system scales non-aware applications. I have written about how this came to my attention, as well as some background on High DPI displays and things to think about when programming to support them. At the bottom of this article is the minimum source code needed to enable High DPI awareness when using the SDL library on Windows.




(It is easier to see the difference if you open the image in a new window to avoid the page from scaling the image.)

On the left is an image of a bird flying in the distance, rendered without the program being High DPI aware. This is running on Windows 10 with a 300% desktop scale on a 3840x2160 (4K) resolution laptop monitor. On the right is the exact same rendering, except rendered with High DPI awareness. There is a noticeable increase in resolution due to the application being able to utilize all of the pixels that the model is occupying on the display for graphics fragment processing.

Even worse than losing potential pixels is the fact that the output image of a non-aware application is scaled by Windows when the display scale is set to any value above 100%. This scaling might not seem like a problem, but due to the user being able to set the desktop scale in 25% increments, potentially unappealing bilinear scaling will result. This is especially problematic for font rendering, as it is critical for the image clarity of the font that it is not scaled either larger or smaller after initial rasterization before being written to the screen.





On the left is a non-aware rendering done with a desktop scale of 100%. On the right is also a non-aware rendering but with a desktop scale of 125%. It might be difficult to see depending on your monitor, but the image on the right contains an outline of blurred pixels all around the edges of the model.

I noticed this when I ran Seabird on a different computer than the one I had previously used for development. The rendered image quality on the new computer appeared to be significantly lower than it should have been given the resolution of screen I was using. I became especially suspicious discovering that other desktop application such as my browser and office suite did not suffer from this quality problem. This prompted further investigation.

On High DPI monitors the number of pixels per inch can far exceed 140, which is greater than the 96 pixels per inch that Windows assumes by default. The motivation for designing screens with a higher pixel density instead of larger physical dimensions I believe was the smartphones and tablet devices. It is not uncommon to see someone hold a smartphone or tablet 12 inches from their eyes. This necessitated screens which could convey the same visual information as a desktop monitor or TV screen, while being constrained to a screen with a surface area measured in tens of inches instead of hundreds of inches.

One thing to be aware of when writing code a High DPI aware application is that you might need to read the desktop scale from the operating system in order to correctly handle scaling in your application. High DPI aware applications are also supposed to be able to dynamically respond to desktop scale change events.

Here is a short example for the minimum code needed to enable High DPI awareness when using the SDL library on Windows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#pragma comment(lib, "Shcore.lib");

#include <windows.h>
#include <ShellScalingAPI.h>
#include <comdef.h>
#include "SDL.h"

int main(int argc, char *argv[])
{
    SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);

    SDL_Window *window = SDL_CreateWindow(
        "High DPI aware SDL window",
        SDL_WINDOWPOS_UNDEFINED,
        SDL_WINDOWPOS_UNDEFINED,
        1000, 1000,
        SDL_WINDOW_ALLOW_HIGHDPI /* | in other flags here */);
    
    return 0;
}


https://nlguillemot.wordpress.com/2016/12/11/high-dpi-rendering/
https://msdn.microsoft.com/en-us/...ary/windows/desktop/hh447398.aspx

Comments

I own a 4k on 15' monitor, and non dpi aware applications upset me.
I can't even look at the upscaled text when it's upscaled by 250%.

A number of common programs which are text based and not dpi aware:matlab, mathematica, lyx, anything java applet based. The solution i got was to disable the dpi scaling and enlarge the text but that makes the icons small, which makes the application hard to use.
I strongly recommend supporting high dpi monitors now, as 1080p monitors are now the common monitors for laptops. And they need about 125% or 150% dpi enlargement.
Good day,
The 8th mage
I'm pretty sure 1080+ is still not the most common resolution for laptops. 1366x768 is. Yeah I know...
I think that on new laptops it is 1080p, as i haven't seen 720p laptops when i baught my laptop last year.
I didn't talk about the old laptops that are still in use.
https://www.newegg.com/Product/Pr...Submit=ENE&N=100006740&IsNodeId=1
2553 items with 1920x1080
3265 items with 1366x768
For anyone else who comes across this, there's now a better way to do this with the DpiAwarenessContext API introduced in 1703:
1
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

Unlike SetProcessDpiAwareness, this will handle runtime DPI changes, including from dragging a window between monitors with different DPI, and will cause the window to be created at the specified resolution in virtual pixels as opposed to physical pixels. It's also in User32.dll, so linking against Shcore.dll and #includeing <ShellScalingAPI.h> is not necessary if you're only using this function.

The downside is the function doesn't exist prior to Windows 10 version 1703 (mid-2017 update), so if you run the code above on older systems, it will crash at startup. To avoid crashing it can be loaded from the DLL manually:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
HMODULE user32_dll = LoadLibraryA("User32.dll");
if (user32_dll) {
    DPI_AWARENESS_CONTEXT (WINAPI * Loaded_SetProcessDpiAwarenessContext) (DPI_AWARENESS_CONTEXT) =
        (DPI_AWARENESS_CONTEXT (WINAPI *) (DPI_AWARENESS_CONTEXT))
            GetProcAddress(user32_dll, "SetProcessDpiAwarenessContext");
    if (Loaded_SetProcessDpiAwarenessContext) {
        if (!Loaded_SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
            printf("WARNING: SetProcessDpiAwarenessContext failed!\n");
        }
    } else {
        printf("WARNING: Could not load SetProcessDpiAwarenessContext!\n");
        //fallback path for win8.1 and later can try loading Shcore.dll and calling SetProcessDpiAwareness() here
    }
    FreeLibrary(user32_dll);
} else {
    printf("WARNING: Could not load User32.dll!\n");
}
Update: here is an updated function which handles DPI awareness with whatever is the most modern API available, going as far back as possible. It should run correctly on any version of Windows, and should compile at least on relatively modern versions of MSVC (you may need to put a version check around #include <ShellScalingAPI.h> to get it to compile even further back).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#ifdef _WIN32
#include <ShellScalingAPI.h>

static void handle_dpi_awareness() {
    //NOTE: SetProcessDpiAwarenessContext isn't available on targets older than win10-1703
    //      and will therefore crash at startup on those systems if we call it normally,
    //      so we load the function pointer manually to make it fail silently instead
    HMODULE user32_dll = LoadLibraryA("User32.dll");
    if (user32_dll) {
        #if _MSC_VER >= 1920
        DPI_AWARENESS_CONTEXT (WINAPI * Loaded_SetProcessDpiAwarenessContext) (DPI_AWARENESS_CONTEXT) =
            (DPI_AWARENESS_CONTEXT (WINAPI *) (DPI_AWARENESS_CONTEXT))
                GetProcAddress(user32_dll, "SetProcessDpiAwarenessContext");
        if (Loaded_SetProcessDpiAwarenessContext) {
            if (!Loaded_SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) {
                printf("WARNING: SetProcessDpiAwarenessContext failed!\n");
            }
        } else {
            printf("WARNING: Could not load SetProcessDpiAwarenessContext!\n");
        #endif // _MSC_VER >= 1920

            bool should_try_setprocessdpiaware = false;
            //OK, let's try to load `SetProcessDpiAwareness` then,
            //that way we get at least *some* DPI awareness as far back as win8.1
            HMODULE shcore_dll = LoadLibraryA("Shcore.dll");
            if (shcore_dll) {
                HRESULT (WINAPI * Loaded_SetProcessDpiAwareness) (PROCESS_DPI_AWARENESS) =
                    (HRESULT (WINAPI *) (PROCESS_DPI_AWARENESS)) GetProcAddress(shcore_dll, "SetProcessDpiAwareness");
                if (Loaded_SetProcessDpiAwareness) {
                    if (Loaded_SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) != S_OK) {
                        printf("WARNING: SetProcessDpiAwareness failed! (fallback)\n");
                    }
                } else {
                    printf("WARNING: Could not load SetProcessDpiAwareness! (fallback)\n");
                    should_try_setprocessdpiaware = true;
                }
            } else {
                printf("WARNING: Could not load Shcore.dll! (fallback)\n");
                should_try_setprocessdpiaware = true;
            }
            if (should_try_setprocessdpiaware) {
                BOOL (WINAPI * Loaded_SetProcessDPIAware)(void) =
                    (BOOL (WINAPI *) (void)) GetProcAddress(user32_dll, "SetProcessDPIAware");
                if (Loaded_SetProcessDPIAware) {
                    if (!Loaded_SetProcessDPIAware()) {
                        printf("WARNING: SetProcessDPIAware failed! (fallback 2)");
                    }
                } else {
                    printf("WARNING: Could not load SetProcessDPIAware! (fallback 2)\n");
                }
            }
            FreeLibrary(shcore_dll);
        #if _MSC_VER >= 1920
        }
        #endif // _MSC_VER >= 1920

        FreeLibrary(user32_dll);
    } else {
        printf("WARNING: Could not load User32.dll!\n");
    }
}
#endif
Why SetProcessDpiAwarenessContext is called only for MSVC specific compiler versions (newer than 1920)?
Doesn't condition to call this function depend on what Windows version application is running, not with what compiler are you compiling source code?
The compile-time version check is there to hopefully prevent code that relies on new versions of headers (specifically the DPI_AWARENESS_CONTEXT type) from being compiled by compilers that don't have them. It was necessary in my case because someone working on the game this code was written for is using a slightly older version of the visual studio dev tools. It's probably possible to call these functions without relying on those definitions, but I haven't put in the time to figure out how to do that.
But compiler version is not reliable way to detect what Windows SDK version of headers/libraries you have installed. You can always get newer SDK headers for older VS versions. Or use older SDK headers in newer MSVC project (it's a setting in project properties).

DPI_AWARENESS_CONTEXT is just an enum. So pass whatever value you need (DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 is -4) to it as integer, and it'll work just fine:

1
2
3
4
5
6
7
       int (WINAPI * Loaded_SetProcessDpiAwarenessContext) (int) =
            (int (WINAPI *) (int))
                GetProcAddress(user32_dll, "SetProcessDpiAwarenessContext");
        if (Loaded_SetProcessDpiAwarenessContext) {
            if (!Loaded_SetProcessDpiAwarenessContext(-4)) {
                printf("WARNING: SetProcessDpiAwarenessContext failed!\n");
            }
I'm well aware that it is possible to mix and match. There is, however, not a straightforward way to detect the SDK version as far as I know, so unfortunately detecting the corresponding compiler version is the best I can do.