winsiderss / systeminformer

A free, powerful, multi-purpose tool that helps you monitor system resources, debug software and detect malware. Brought to you by Winsider Seminars & Solutions, Inc. @ http://www.windows-internals.com
https://systeminformer.sourceforge.io
MIT License
10.55k stars 1.36k forks source link

DPI Awareness Support #1386

Open dmex opened 1 year ago

dmex commented 1 year ago

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

dmex commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

@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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

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 commented 1 year ago

Interesting comment about this api from Microsoft ;)

https://github.com/microsoft/terminal/blob/ed27737233714dea77877624d1beeb49e2ccd36e/src/interactivity/win32/windowproc.cpp#L104

As you see Microsoft itself using GetDpiForMonitor although Terminal is PerMonitor aware.

henrypp commented 1 year ago

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 commented 1 year ago

Per Monitor V1 is coming starts 8.1 when GetDpiForMonitor is added, it looks like official documentation required changes to should not be used if the calling thread is *not* per-monitor DPI aware.

dmex commented 1 year ago

@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:

henrypp commented 1 year ago

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 commented 1 year ago

@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:

https://github.com/winsiderss/systeminformer/blob/16f717c2053b4d9ddfd5b991491e20c05eeaf910/phlib/settings.c#L381-L389

And here: https://github.com/winsiderss/systeminformer/blob/16f717c2053b4d9ddfd5b991491e20c05eeaf910/phlib/settings.c#L1102-L1103

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 commented 1 year ago

@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 commented 1 year ago

All looks good on different monitors.

Image --- ![photo_2023-02-13_15-56-17](https://user-images.githubusercontent.com/3902025/218428140-f26c3345-3492-4ddd-a0f6-012157427f76.jpg)

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 commented 1 year ago

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

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

MagicAndre1981 commented 1 year ago

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 commented 1 year ago

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

MagicAndre1981 commented 1 year ago

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

MagicAndre1981 commented 1 year ago

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