This page looks best with JavaScript enabled

Windows DPI and Scale

 ·  ☕ 7 min read · 👀... views

Windows Scale

Nowadays, monitors generally have stronger performance and higher pixels, so most people tend to increase the scaling size of Windows in order to enhance the user experience. Scale refers to the Windows display scaling factor. You can adjust your scale in Desktop Right Click -> Setting -> System -> Display -> Scale. Today, I encountered a problem. I can use the following code to retrieve the Windows scaling size from a Win32 test console program, but this code does not work correctly in game applications.

 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
float GetScaling()
{

    HWND hWnd = GetDesktopWindow();
    HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);

    // Query monitor logical width and height
    MONITORINFOEX miex;
    miex.cbSize = sizeof(miex);
    GetMonitorInfoA(hMonitor, &miex);
    int cxLogical = (miex.rcMonitor.right - miex.rcMonitor.left);
    int cyLogical = (miex.rcMonitor.bottom - miex.rcMonitor.top);
    printf("%d %d\n", cxLogical, cyLogical);

    // Query monitor physical width and height
    DEVMODE dm;
    dm.dmSize = sizeof(dm);
    dm.dmDriverExtra = 0;
    EnumDisplaySettings(miex.szDevice, ENUM_CURRENT_SETTINGS, &dm);
    int cxPhysical = dm.dmPelsWidth;
    int cyPhysical = dm.dmPelsHeight;
    printf("%d %d\n", cxPhysical, cyPhysical);

    float horzScale = ((float)cxPhysical / (float)cxLogical);
    float vertScale = ((float)cyPhysical / (float)cyLogical);
    // assert(horzScale == vertScale);

    return horzScale;
}

My computer and monitor configuration is

The result of my Win32 test program:

cxLogical: 1536 
cyLogical: 864
cxPhysical: 3840 
cyPhysical: 2160
Scale: 2.5

The result is correct. However, when I injected this code into a game application, the result was different:

cxLogical: 3840 
cyLogical: 2160
cxPhysical: 3840 
cyPhysical: 2160
Scale: 1.0

I confirmed that the APIs were not hooked, but the result of the logical rectangle was different. I tried to analyze the implementation of GetMonitorInfoA. This API called GetMonitorworkRect, which retrieved information from TEB->Win32ClientInfo. I did not think the game process would modify this field, so there must be other reasons that led to this difference.

Logical vs Physical Inches

We have to study some other concepts to help us understand why the same code at the last section retrieves different results from different programs.

DPI(dots per inch), means the number of dots(pixels) per logical inch. The reason why I emphasize the word logical is that the logical inch is different from the physical inch. The concept of logical inch was proposed to solve the issue of pixel size being affected by monitor resolution and physical size. There is no fixed relationship between physical inches and pixels. Windows used the following conversion: One logical inch equals 96 pixels. Using this scaling factor, a 72-point font is rendered as 96 pixels tall, and DPI is 96.

This means that with the standard scaling factor, when the physical size of the monitor is fixed, the more pixels it has, the smaller the font appears. Conversely, when monitors have the same number of pixels but a larger physical size, the font appears larger. So, there is no fixed relationship between physical inches and pixels, because different monitors have different PPI(Pixels Per Inch).

Different monitors have different physical sizes and PPIs, so how can we achieve a similar visual experience when viewing the same point font? The answer is Logical Inches. The system adjusts the number of pixels in one logical inch so that a high PPI monitor has a higher logical DPI, maintaining a consistent user experience. The bridge of this mechanism is dynamic scaling adjustments. This may be difficult to understand, so let’s look at an example.

Parameter 24" 1080P (1920×1080) 14" 4K (3840×2160) Relationship
Diagonal Line 24 inches 14 inches 1.7× difference
Physical PPI √(1920² + 1080²)/24 ≈ 92 PPI √(3840² + 2160²)/14 ≈ 314 PPI 3.4× difference
Default Scaling 100% 250% 2.5× compensation
Logical DPI 96 DPI 240 DPI (96 × 2.5) 2.5× difference
1 Logical Inch in Pixels 96 pixels 240 pixels 2.5× difference
Physical Button Size 96px / 92 PPI ≈ 1.04 inches 240px / 314 PPI ≈ 0.76 inches 27% difference
Typical Viewing Distance 50-70 cm 30-40 cm 40-50% closer
Visual Angle ≈2.3° ≈2.3° Identical
Visual Perception Appears similar size Appears similar size Consistent UX

In this example, we find that using different scaling percentages can help us achieve a similar visual experience across different monitors.

Physical PPI Difference 
-> 
Windows Sets Scaling Percentages
-> 
Logical DPI = 96 x Scaling Percentages
->
Pixels per Logical Inch Adjusted
->
Visual Consistency Achieved

And we can retrieve DPI easily

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// First method
void GetPrimaryDisplayDPI() {
    HDC hdc = GetDC(nullptr);
    if (!hdc) {
        std::cerr << "Failed to get device context" << std::endl;
        return;
    }

    int dpiX = GetDeviceCaps(hdc, LOGPIXELSX);
    int dpiY = GetDeviceCaps(hdc, LOGPIXELSY);

    ReleaseDC(nullptr, hdc);

    std::cout << "X-DPI: " << dpiX << "\n";
    std::cout << "Y-DPI: " << dpiY << "\n";
}

// Second method
GetDpiForSystem();
GetDpiForWindow(GetDesktopWindow());

Now, we understand that the system introduced a concept called Logical Inches to help designers and developers ignore the physical differences between different monitors. They can ensure the different users receive the same visual experience by designing the UI with a fixed size of logical inches.

But in the programs and the processes, how do they specifically handle this issue?

DPI Awareness

If a program does not account for DPI, the DWM will scale the entire UI to match the DPI setting. This means Windows will bitmap-stretch the application’s windows. Some problems may occur when you increase your DPI settings:

1. Clipped UI elements.
2. Incorrect layout.
3. Pixelated bitmaps and icons.
4. Incorrect mouse coordinates, which can affect hit testing, drag and drop, and so forth.

To solve this problem, Windows introduced a mechanism called DPI Awareness to help programs adjust their UI dynamically. You can set this option in Visual Studio:

  • VS -> Configuration Properties -> Manifest Tool -> Input and Output -> DPI Awareness

If a program is DPI Aware, it can receive the scaled DPI of the primary monitor and lay out its UI appropriately using that system DPI value, rather than having Windows bitmap-stretch the UI. When the DPI changes, the system sends WM_DPICHANGED to the application window, giving the application an opportunity to handle resizing for the new DPI. You can retrieve more information from : https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows#per-monitor-v1-dpi-awareness

Logical resolution virtualization

Okay, we have learned enough information about scale and DPI. Let’s return to the original question: Why do I get different scale values from the Win32 test program and the game application? Maybe you have already guessed the answer: The Win32 test program is DPI Unaware and the game application is DPI Aware. So why does this happen?

When the application is DPI Unaware, MONITORINFOEX.rcMonitor is the virtualized logical resolution (scaled value), and DEVMODE.dmPelsWidth/Height is the physical resolution (actual pixel value). So we can get the correct scale percentage by dividing the two.

Physical resolution / Scale percentages = Logical resolution

# 3840x2160 Monitor
3840x2160 / 250% = 1536x864
3840x2160 / 1536x864 => 2.5

So for these applications, they can only render their UI in the logical resolution (such as 1536x864), and then the system will enlarge the drawn content by 250% to the physical resolution (such as 3840x2160).

If the application is DPI Aware, the system will provide the physical resolution to the application directly, and the application must handle scaling itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// retrieve scale factor
float GetDpiScale(HWND hWnd) {
    UINT dpi = GetDpiForWindow(hWnd);
    return dpi / 96.0f;
}

void CreateButton(HWND parent, int x, int y) {
    float scale = GetDpiScale(parent);
    
    CreateWindow(WC_BUTTON, L"Submit",
        WS_VISIBLE | WS_CHILD,
        (int)(x * scale),  // convert to physical pixel
        (int)(y * scale),
        (int)(BUTTON_WIDTH * scale),
        (int)(BUTTON_HEIGHT * scale),
        parent, NULL, NULL, NULL);
}

Reference

  1. https://learn.microsoft.com/en-us/windows/win32/learnwin32/dpi-and-device-independent-pixels
  2. https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
  3. https://chat.deepseek.com/
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer