Skip to content
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

DPI Awareness Support #1386

Open
10 of 11 tasks
dmex opened this issue Sep 23, 2022 · 23 comments
Open
10 of 11 tasks

DPI Awareness Support #1386

dmex opened this issue Sep 23, 2022 · 23 comments
Assignees

Comments

@dmex
Copy link
Member

dmex commented Sep 23, 2022

The recent addition of V2 DPI awareness introduced a few issues:

  • Toolbar (fixed)
  • Searchbox (fixed)
  • Menu Icons (fixed)
  • Tray Icons (fixed)
  • Process tree icons corrupted after DPI changes (fixed)
  • System information window fonts/icons need to be reloaded after DPI change
  • Child window fonts/icons need to be reloaded after DPI change
  • The GetDpiForSystem function is not V2 aware and always returns the same value even after changing the system DPI.
  • The GetDpiForMonitor function is also not V2 aware and always returns the same value even after changing the system DPI.
  • Our workaround of using GetShellWindow() to query the current system DPI without a window no longer returns the actual DPI and is now static until reboot. (Fixed by caching)
  • If the System information window is minimized and the display DPI changes it becomes miniaturized and unusable.
@dmex dmex added the bug label Sep 23, 2022
@dmex dmex self-assigned this Sep 23, 2022
@dmex
Copy link
Member Author

dmex commented Sep 23, 2022

Original PR reference #1347

@henrypp I've been making changes to the DPI implementation. Do you have any ideas for avoiding GetDpiForSystem and GetDpiForMonitor since they don't change at runtime (GetDpiForWindow doesn't have this issue)?

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

@dmex

I just checked GetDpiForMonitor and it returns correct DPI for my monitors (same as GetDpiForWindow).
To avoid using GetDpiForSystem need passing WindowHandle to functions where GetDpiForSystem is used.

@dmex
Copy link
Member Author

dmex commented Sep 24, 2022

GetDpiForMonitor and it returns correct DPI for my monitors

I fixed this case by using GetShellWindow() with GetDpiForWindow().

To avoid using GetDpiForSystem

We can use GetShellWindow()+GetDpiForWindow or GetScaleFactorForMonitor (with or without GetShellWindow)

GetScaleFactorForMonitor returns the correct DPI for the monitor but not sure if it should use GetShellWindow or just NULL?

if (SUCCEEDED(GetScaleFactorForMonitor(MonitorFromWindow(NULL, MONITOR_DEFAULTTOPRIMARY), &value)))
{
    UINT dpi = PhMultiplyDivideSigned(value, USER_DEFAULT_SCREEN_DPI, 100);
}

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

From here:

GetScaleFactorForMonitor() returns the correct result if the calling application has set its
dpi awareness to DPI_AWARENESS_UNAWARE.

If any of the others (DPI_AWARENESS_SYSTEM_AWARE, DPI_AWARENESS_PER_MONITOR_AWARE) is set, then
the results are incorrect.

System Informer now is under DPI_AWARENESS_PER_MONITOR_AWARE.

@dmex
Copy link
Member Author

dmex commented Sep 24, 2022

@henrypp

It works for me and GetScaleFactorForMonitor changes at runtime when changing the display properties.

He might be right about the UWP aspect.... The IDisplayPropertiesStatics_get_ResolutionScale method returns the correct scaling at 150% and so does the DPI. Can you compile this and compare the output?

#include <roapi.h>
#include <windows.graphics.display.h>
#pragma comment(lib, "runtimeobject.lib")

DEFINE_GUID(IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation, 0xBED112AE, 0xADC3, 0x4DC9, 0xAE, 0x65, 0x85, 0x1F, 0x4D, 0x7D, 0x47, 0x99);
DEFINE_GUID(IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics, 0xc6a02a6c, 0xd452, 0x44dc, 0xba, 0x07, 0x96, 0xf3, 0xc6, 0xad, 0xf9, 0xd1);
DEFINE_GUID(IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics, 0x6937ed8d, 0x30ea, 0x4ded, 0x82, 0x71, 0x45, 0x53, 0xff, 0x02, 0xf6, 0x8a);


VOID CheckUWP()
{        
        __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics* displayInformation;
        __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics* displayProperties;
        HSTRING stringHandle;
        HRESULT hr;

        WindowsCreateString(RuntimeClass_Windows_Graphics_Display_DisplayInformation, (UINT)wcslen(RuntimeClass_Windows_Graphics_Display_DisplayInformation), &stringHandle);
        hr = RoGetActivationFactory(stringHandle, &IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics, (IInspectable**)&displayInformation);

        if (SUCCEEDED(hr))
        {
            __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation* display_information;
            __x_ABI_CWindows_CGraphics_CDisplay_CResolutionScale resolution_scale;

            hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics_GetForCurrentView(displayInformation, &display_information);

            if (SUCCEEDED(hr))
            {
                hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation_get_ResolutionScale(display_information, &resolution_scale);

                if (SUCCEEDED(hr))
                {
                    float value = (float)(resolution_scale / 100.0f);
                    dprintf("");
                }
            }
        }

        WindowsCreateString(RuntimeClass_Windows_Graphics_Display_DisplayProperties, (UINT)wcslen(RuntimeClass_Windows_Graphics_Display_DisplayProperties), &stringHandle);
        hr = RoGetActivationFactory(stringHandle, &IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics, (IInspectable**)&displayProperties);

        if (SUCCEEDED(hr))
        {
            __x_ABI_CWindows_CGraphics_CDisplay_CResolutionScale scale;
            FLOAT dpi;

            hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics_get_ResolutionScale(displayProperties, &scale);
            if (SUCCEEDED(hr))
            {
                dprintf("");
            }

            hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics_get_LogicalDpi(displayProperties, &dpi);
            if (SUCCEEDED(hr))
            {
                FLOAT value = (FLOAT)dpi / 96.0f;
                dprintf("");
            }
        }
}

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

100 dpi:

GetDpiForMonitor: 96
GetDpiForWindow: 96
GetScaleFactorForMonitor: 100
GetDeviceCaps: 96
__x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics_get_ResolutionScale: 100

125 dpi:

GetDpiForMonitor: 120
GetDpiForWindow: 120
GetScaleFactorForMonitor: 125
GetDeviceCaps: 120
__x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics_get_ResolutionScale: 125

Looks like it returns correct results, but GetScaleFactorForMonitor and winrt return values not for MulDiv, and just says dpi value in human readable format. Thats why i did not understand why you are going to use GetScaleFactorForMonitor.

Again, GetDpiForMonitor is V2 aware and return correct result when dpi changed for monitor. Maybe your test builds not enabled PerMonitorv2 support?

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

Here is working binary with PerMonitorv2 enabled. On start waiting 2 seconds. Just move cursor to monitor to display it's DPI.

test_binary.zip

@dmex
Copy link
Member Author

dmex commented Sep 24, 2022

Maybe your test builds not enabled PerMonitorv2 support?

The documentation says it's not V2 aware?
https://learn.microsoft.com/en-us/windows/win32/api/shellscalingapi/nf-shellscalingapi-getdpiformonitor#remarks

should not be used if the calling thread is per-monitor DPI aware.

@henrypp

This comment was marked as off-topic.

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

When you call GetDpiForMonitor, you will receive different DPI values depending on the DPI awareness of the calling application.

It exactly says GetDpiForMonitor is not dpi aware (ONLY!) when you are running not under PROCESS_PER_MONITOR_DPI_AWARE.

And how should not be used if the calling thread is per-monitor DPI aware appears in official documentation i did not understand.

Where is here "not PerMonitor aware":
PROCESS_PER_MONITOR_DPI_AWARE returns The actual DPI value set by the user for that display.

As you can see, official documentation is contradicts itself.

@henrypp

This comment was marked as off-topic.

@dmex
Copy link
Member Author

dmex commented Sep 24, 2022

@henrypp

Are we able to eliminate DPI from application settings? PhLoadWindowPlacementFromSetting/PhSaveWindowPlacementToSetting for example are both saving the DPI but this should be redundant. Application settings should be changed to logical units which don't require persisting DPI and then we convert to physical units at runtime so those settings are portable across all machines?

For example:

  • https://github.com/microsoft/Windows-task-snippets/blob/master/tasks/Convert-DPI-rectangles.md
  • https://bitbucket.org/chromiumembedded/cef/commits/a5a5e7f
  • https://github.com/raysan5/raylib/issues/1982#issuecomment-916075719

@henrypp
Copy link
Contributor

henrypp commented Sep 24, 2022

Are we able to eliminate DPI from application settings?

Already!

PhGetSizeDpiValue is used to avoid saving dpi value in configuration, how it works you can see in layout manager, saving the packed size value and on resizing unpack it's size to current dpi value and doing the job. It can be implemented in PhLoadWindowPlacementFromSetting/PhSaveWindowPlacementToSetting without saving actual dpi value.

But, i think some changes is required for PhGetSizeDpiValue, like do not change left/right values, change only width and height, some maths is required to transit RECT into PH_RECTANGLE. My misstake ;)

@dmex
Copy link
Member Author

dmex commented Sep 26, 2022

@henrypp

We're still saving the DPI and scaling on startup? There's some weird issue where hidden treelist window columns are incorrectly scaled when their window is hidden and starting/closing/restarting the application with different DPI values:

if (ScaleToCurrent)
{
if (value.Scale != dpiValue && value.Scale != 0)
{
value.X = PhMultiplyDivideSigned(value.X, dpiValue, value.Scale);
value.Y = PhMultiplyDivideSigned(value.Y, dpiValue, value.Scale);
value.Scale = dpiValue;
}
}

And here:

if (scale != dpiValue && scale != 0)
width = PhMultiplyDivideSigned(width, dpiValue, scale);

Although it's probably caused by GetDpiForSystem returning the wrong DPI until the application is restarted?

GetScaleFactorForMonitor and winrt return values not for MulDiv, and just says dpi value in human readable format. Thats why i did not understand why you are going to use GetScaleFactorForMonitor.

The enums/values returned by winrt can be used with muldiv by casting those values to float. I'm looking for alternatives to GetDpiForSystem since it has a major problem when you change the scaling at runtime from the display properties.

If the current display properties are using 100% then GetDpiForSystem returns 96 but continues returning 96 even after changing the scaling and doesn't update unless you restart the application. GetDpiForWindow will instantly return the newer scaling but parts of the application (i.e settings) using GetDpiForSystem end up saving the 96 DPI but the application is using 120 DPI and with RECTs from 120 - When you restart the application it sees 96 != 120 and scales those values but they were already scaled for 120.

System: 96, Window: 96, Monitor: 120, Monitor2: 120, DeviceCaps: 96
<User changed display settings>
System: 96, Window: 120, Monitor: 120, Monitor2: 120, DeviceCaps: 96

You can call this in a loop and see GetDpiForSystem return the wrong values:

VOID PrintDPI(VOID)
{
    static PH_INITONCE initOnce = PH_INITONCE_INIT;
    static HRESULT (WINAPI *GetDpiForMonitor_I)(
        _In_ HMONITOR hmonitor,
        _In_ MONITOR_DPI_TYPE dpiType,
        _Out_ PUINT dpiX,
        _Out_ PUINT dpiY
        ) = NULL; // win81+
    static UINT (WINAPI *GetDpiForWindow_I)(
        _In_ HWND hwnd
        ) = NULL; // win10rs1+
    static UINT (WINAPI *GetDpiForSystem_I)(
        VOID
        ) = NULL; // win10rs1+
    static UINT (WINAPI *GetDpiForSession_I)(
        VOID
        ) = NULL; // ordinal 2713

    if (PhBeginInitOnce(&initOnce))
    {
        PVOID baseAddress;

        if (!(baseAddress = PhGetLoaderEntryDllBase(L"shcore.dll")))
            baseAddress = PhLoadLibrary(L"shcore.dll");

        if (baseAddress)
        {
            GetDpiForMonitor_I = PhGetProcedureAddress(baseAddress, "GetDpiForMonitor", 0);
        }


        if (!(baseAddress = PhGetLoaderEntryDllBase(L"user32.dll")))
            baseAddress = PhLoadLibrary(L"user32.dll");

        if (baseAddress)
        {
            GetDpiForWindow_I = PhGetProcedureAddress(baseAddress, "GetDpiForWindow", 0);
            GetDpiForSystem_I = PhGetProcedureAddress(baseAddress, "GetDpiForSystem", 0);
        }

        PhEndInitOnce(&initOnce);
    }

    UINT systemDpi = 0;
    UINT shellWindowDpi = 0;
    UINT monitorWindowDpi = 0;
    UINT monitorRectWindowDpi = 0;
    UINT deviceCapsDpi = 0;

    {
        systemDpi = GetDpiForSystem_I();
        shellWindowDpi = GetDpiForWindow_I(GetShellWindow());
    }

    {
        UINT dpi_x;
        UINT dpi_y;

        GetDpiForMonitor_I(MonitorFromWindow(GetShellWindow(), MONITOR_DEFAULTTONEAREST), MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y);

        monitorWindowDpi = dpi_x;
    }

    {
        UINT dpi_x;
        UINT dpi_y;

        GetDpiForMonitor_I(MonitorFromRect(&(RECT){ 1, 1, 1, 1 }, MONITOR_DEFAULTTONEAREST), MDT_EFFECTIVE_DPI, &dpi_x, &dpi_y);

        monitorRectWindowDpi = dpi_x;
    }

    {
        HDC screenHdc;

        if (screenHdc = GetDC(NULL))
        {          
            deviceCapsDpi = GetDeviceCaps(screenHdc, LOGPIXELSX);
            ReleaseDC(NULL, screenHdc);
        }
    }

    dprintf("System: %lu, Window: %lu, Monitor: %lu, Monitor2: %lu, DeviceCaps: %lu\n", systemDpi, shellWindowDpi, monitorWindowDpi, monitorRectWindowDpi, deviceCapsDpi);
}

BTW the above code with IDisplayInformationStatics wasn't working because the runtime wasn't initialized. IDisplayProperties is deprecated and probably doesn't return the correct values. This code should work:

#include <roapi.h>
#include <windows.graphics.display.h>
#pragma comment(lib, "runtimeobject.lib")

DEFINE_GUID(IID___x_ABI_CWindows_CUI_CXaml_CHosting_CIWindowsXamlManagerStatics, 0x28258A12, 0x7D82, 0x505B, 0xB2, 0x10, 0x71, 0x2B, 0x04, 0xA5, 0x88, 0x82);
DEFINE_GUID(IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics, 0xc6a02a6c, 0xd452, 0x44dc, 0xba, 0x07, 0x96, 0xf3, 0xc6, 0xad, 0xf9, 0xd1);

VOID CheckUWP()
{
    __x_ABI_CWindows_CUI_CXaml_CHosting_CIWindowsXamlManagerStatics* manager;
    __x_ABI_CWindows_CUI_CXaml_CHosting_CIWindowsXamlManager* result;
    __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics* displayInformation;
    __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayPropertiesStatics* displayProperties;
    HSTRING stringHandle;
    HRESULT hr;

    WindowsCreateString(RuntimeClass_Windows_UI_Xaml_Hosting_WindowsXamlManager, (UINT)wcslen(RuntimeClass_Windows_UI_Xaml_Hosting_WindowsXamlManager), &stringHandle);
    if (stringHandle)
    {
        hr = RoGetActivationFactory(stringHandle, &IID___x_ABI_CWindows_CUI_CXaml_CHosting_CIWindowsXamlManagerStatics, (IInspectable**)&manager);

        if (SUCCEEDED(hr))
        {
            hr = __x_ABI_CWindows_CUI_CXaml_CHosting_CIWindowsXamlManagerStatics_InitializeForCurrentThread(manager, &result);
        }
    }

    WindowsCreateString(RuntimeClass_Windows_Graphics_Display_DisplayInformation, (UINT)wcslen(RuntimeClass_Windows_Graphics_Display_DisplayInformation), &stringHandle);
    hr = RoGetActivationFactory(stringHandle, &IID___x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics, (IInspectable**)&displayInformation);

    if (SUCCEEDED(hr))
    {
        __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation* display_information;

        hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformationStatics_GetForCurrentView(displayInformation, &display_information);

        if (SUCCEEDED(hr))
        {
            __x_ABI_CWindows_CGraphics_CDisplay_CResolutionScale scale;
            FLOAT dpi;

            hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation_get_ResolutionScale(display_information, &scale);
            if (SUCCEEDED(hr))
            {
                dprintf("");
            }

            hr = __x_ABI_CWindows_CGraphics_CDisplay_CIDisplayInformation_get_LogicalDpi(display_information, &dpi);
            if (SUCCEEDED(hr))
            {
                FLOAT value = (FLOAT)dpi / 96.0f;
                dprintf("");
            }
        }
    }
}

@dmex
Copy link
Member Author

dmex commented Feb 6, 2023

@henrypp

All the remaining DPI issues have been fixed and support now includes the application and plugins. Are you able to try out the latest nightly and let me know if there's any remaining DPI issues?

On a separate topic I've also found a major Windows 11 bug with the CreateDialog API and PerMonitorV2... If the user minimizes windows/dialogs that were created using the CreateDialog API and then changes the DPI settings for the display while the window is minimized the internal state of the window becomes deadlocked/corrupted. The message loop immediately stops processing messages, various window functions return incorrect data and if you attempt to active the window it'll trip the message loop then resize to 0,0 and become unusable.

I'm not sure if we can workaround the issue with CreateDialog by just changing some window styles?

@henrypp
Copy link
Contributor

henrypp commented Feb 13, 2023

All looks good on different monitors.

Image

photo_2023-02-13_15-56-17

> I'm not sure if we can workaround the issue with CreateDialog by just changing some window styles?

You can send bug to micro$oft, i think.

@dmex
Copy link
Member Author

dmex commented Feb 14, 2023

You can send bug to micro$oft, i think.

I have and they marked it "needs more details" 🤦

dmex added a commit that referenced this issue May 26, 2023
dmex added a commit that referenced this issue May 31, 2023
dmex added a commit that referenced this issue Jun 8, 2023
@MagicAndre1981
Copy link
Contributor

What about scaling columns sizes depending on DPI? On 100% display the sizes can be larger compared to displays with for example 125%. When moving the Main Window between such 2 displays I have to change the sizes each time.

@henrypp
Copy link
Contributor

henrypp commented Jun 22, 2023

@MagicAndre1981 in my project haved listview resize on dpi change, do not know how it realized here.

@MagicAndre1981
Copy link
Contributor

nice, maybe create a PR to add it here, too?

@MagicAndre1981
Copy link
Contributor

ok, there is no scaling on DPI change. When you start SI on 100% and move it to 125% display it still uses 100% scaling

@rstarkov
Copy link

rstarkov commented Nov 6, 2024

This works really well, but there are a couple of per-monitor-DPI corner cases not listed, affecting window size:

  • put main window on Secondary monitor (different DPI), open System Information whose last position is on Primary monitor. Window size grows. Close, open, close open - it grows and grows with each open. This is particularly unfortunate. All windows are affected - Options, Find, etc.
  • drag a window between monitors. Content DPI changes correctly, but the window size is also supposed to change to accommodate bigger content. It doesn't. This only affects the main window - Options, Find etc resize correctly.

(this is Win10, not sure if Win11 is affected)

@MagicAndre1981
Copy link
Contributor

My issue #2251 is also related to DPI scaling.

@jxy-s jxy-s removed the release 3.0 label Dec 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants