emoacht / Monitorian

A Windows desktop tool to adjust the brightness of multiple monitors with ease
https://www.microsoft.com/store/apps/9nw33j738bl0
MIT License
3.31k stars 155 forks source link

Brightness slider not working when Windows 11 HDR is on #411

Open Diti opened 1 year ago

Diti commented 1 year ago

The Monitorian slider for brightness does not change the brightness of my HDR-enabled monitors − tested on a Iiyama G-Master GB3467WQSU (400 cd/m² DisplayHDR through DisplayPort 1.4) and on an Atomos Ninja V+ (1000 cd/m² HDR through HDMI 2.0b).

(DisplayHDR 400 is not 10-bit HDR, but Windows 11 and Monitorian behave the same anyway.)

From my understanding of color science, I do believe Windows 11 must be using HLG, which enables SDR content (like the Windows UI itself) to be displayed with a relative base brightness; said base brightness can be changed in the Windows settings for displays, in the Use HDR section.

Currently, the brightness slider for Monitorian does not change brightness when the Use HDR option is On. At the very least, I believe the brightness slider for Monitorian should change brightness the exact same way the SDR content brightess slider of Windows 11 does. Optimally, Monitorian should be handling both regular brightness and SDR content brightness with different sliders (from my understanding, the latter slider only works when the monitor uses HLG, and should do nothing when the monitor uses PQ).

For screenshots and understanding of these Windows 11 menus, head over to Change HDR or SDR Content Brightness for HDR Display in Windows 11.

I would be happy to pay for a Monitorian subscription if HDR support was a paid option.

emoacht commented 1 year ago

Thanks for reporting. I tested with Dell S2721QS on Windows 11 and the brightness slider of this app works as always regardless of whether HDR is on or off. I have no idea what is wrong with your monitors.

fireph commented 1 year ago

@emoacht The original comment is extremely detailed and accurate on how HDR brightness works in windows. When HDR is enabled, monitor brightness should not be available by design. It would be great if Monitorian handled this case by having the brightness slider reflect what exists in the Windows HDR settings menu for SDR brightness (or at least have a different slider for SDR brightness when HDR is enabled).

I saw someone on stackoverflow already ask about how to do this programmatically: https://stackoverflow.com/questions/74594751/controlling-sdr-content-brightness-programmatically-in-windows-11

And twinkle tray already has a pinned issue about this exact thing as well where they have made some progress: https://github.com/xanderfrangos/twinkle-tray/issues/97

emoacht commented 1 year ago

@fireph It primarily depends on whether the necessary information is made publicly available and techinicall feasible. I have not seen any useful information on HDR and DDC/CI to date.

accurate on how HDR brightness works in windows. When HDR is enabled, monitor brightness should not be available by design

Could you give me a link to official documentation on this?

emoacht commented 1 year ago

The only available API related to SDR brightness which I could find is Windows.Graphics.Display.AdvancedColorInfo.SdrWhiteLevelInNits property which provides the current SDR luminance of a monitor where the window locates. However, it requires DisplayInformation instance which can only be obtained by DisplayInformation.GetForCurrentView method and thus it is only usable in an UWP app.

lulle2007200 commented 1 year ago

@emoacht That is not true anymore, winrt can be used in non packaged/non uwp apps just fine. You can also use DisplayConfigGetDeviceInfo to get DISPLAYCONFIG_SDR_WHITE_LEVEL. Brightness for SDR content can be set programmatically (see my answer on SO)

emoacht commented 1 year ago

@lulle2007200 Thanks for the information.

That is not true anymore, winrt can be used in non packaged/non uwp apps just fine.

Could you elaborate how to call DisplayInformation.GetForCurrentView in an app other than UWP?

Also, could you elaborate how to get DISPLAYCONFIG_SDR_WHITE_LEVEL? It seems a little complicated.

Regarding SO's answer, it is interesting. But that method is undocumented and unnamed. Could you elaborate how did you find the method and its signature?

lulle2007200 commented 1 year ago

Could you elaborate how did you find the method and its signature?

Display settings are implemented in a DLL, separate from the settings app. I ran the settings app under debugger to find the event handler in the display settings DLL that gets called when the SDR brightness slider changes value, from there, i stepped through that event handler until i found the call to dwmapi.dll. The function name comes from debug symbols, the signature i found by reverse engineering.

Also, could you elaborate how to get DISPLAYCONFIG_SDR_WHITE_LEVEL? It seems a little complicated.

This is not very useful as it only allows you to get the current white level, but not set it, but here is how you'd do it (error handling and cleanup omitted):

#include <Windows.h>
#include <iostream>

int main()
{
    UINT32 numPathArrayElems; 
    UINT32 numModeInfoArrayElems;
    GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &numPathArrayElems, &numModeInfoArrayElems);

    DISPLAYCONFIG_PATH_INFO *pathArray = new DISPLAYCONFIG_PATH_INFO[numPathArrayElems];
    DISPLAYCONFIG_MODE_INFO *modeInfoArray = new DISPLAYCONFIG_MODE_INFO[numModeInfoArrayElems];

    QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &numPathArrayElems, pathArray, &numModeInfoArrayElems, modeInfoArray, nullptr);

    // print white level for all displays
    for(size_t i = 0; i < numPathArrayElems; i++){
        DISPLAYCONFIG_SDR_WHITE_LEVEL request;
        request.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
        request.header.size = sizeof(request);
        request.header.adapterId = pathArray->targetInfo.adapterId;
        request.header.id = pathArray->targetInfo.id;

        DisplayConfigGetDeviceInfo(reinterpret_cast<DISPLAYCONFIG_DEVICE_INFO_HEADER*>(&request));

        std::cout << "White level: " << request.SDRWhiteLevel << std::endl;
    }
}

Could you elaborate how to call DisplayInformation.GetForCurrentView in an app other than UWP?

Basically exactly the same way as you would do it in an UWP app, e.g.

#include "winrt/base.h"
#include "winrt/Windows.Graphics.Display.h"

using namespace winrt;
using namespace Windows::Graphics::Display;

//...

auto displayInfo = DisplayInformation::GetForCurrentView();
auto white_level = displayInfo.GetAdvancedColorInfo().SdrWhiteLevelInNits();

The catch with this is that it is tied to a specific window. E.g. the thread you call it from has to be associated to a window. GetForCurrentView() then returns a DisplayInformation object that holds display information for that specific window (and it changes when you move the window from one screen to another, change scaling, etc.). Other than that, displayInfo.GetAdvancedColorInfo().SdrWhiteLevelInNits(); essentially does the same as DisplayConfigGetDeviceInfo to get DISPLAYCONFIG_SDR_WHITE_LEVEL

emoacht commented 1 year ago

@lulle2007200 Thank you for the information again.

I tried to call the function of dwmapi.dll by name but failed. Then I checked dwmapi.dll with Dumpin and found that the method is unnamed. So the only way to call it is by its index number 171.

Regarding DISPLAYCONFIG_SDR_WHITE_LEVEL with DisplayConfigGetDeviceInfo, I confirmed that the white level matches SdrWhiteLevelInNits obtained on UWP.

Regarding DisplayInformation.GetForCurrentView on other platform, if I call it on WPF, it causes COMException and I have no idea to avoid it. Thus it is unusable.

System.Runtime.InteropServices.COMException
  HResult=0x80070490
  Message=Element not found.(0x80070490)
  Source=WinRT.Runtime
lulle2007200 commented 1 year ago

Yeah, currently DwmpSDRToHDRBoost is only exported by ordinal 171 and not by name, so you have to load it by ordinal.

Maybe i put that badly. Generally you can use all of winrt in "normal" non UWP apps, however DisplayInformation.GetForCurrentView is only available when the calling thread is associated to a CoreApplicationView, CoreApplicationView is primarily used in UWP apps where there can only be a single window per thread (which is what CoreApplicationView models). In fact, usage of CoreApplicationView was deprecated in more recent versions of winrt.

The exception rises, because the thread doesnt have an associated CoreApplicationView.

emoacht commented 1 year ago

@lulle2007200 This app has been using some of WinRT APIs from early version. Some APIs that requires GetForCurrentView like method are exceptions. If we were able to use AdvancedColorInfo, we can make use of other useful APIs.

lulle2007200 commented 1 year ago

@emoacht There is a way to do that in non UWP apps/apps that don't use CoreApplicationView, but only if you are targeting Windows 11 build 22621 or later, otherwise you'd have to use the classic Win32 APIs.

You can obtain a DisplayInformation interface for a specific display instead of a specific window as follows:

#include "winrt/base.h"
#include "winrt/Windows.Graphics.Display.h"
#include <windows.graphics.display.interop.h>

#include <iostream>

using namespace winrt;
using namespace Windows::Graphics::Display;

HMONITOR getCurrentMonitor() {
    return MonitorFromWindow(nullptr, MONITOR_DEFAULTTOPRIMARY);
}

int main()
{
    auto display_info_factory = get_activation_factory<DisplayInformation, IDisplayInformationStaticsInterop>();

    HMONITOR h_monitor = getCurrentMonitor();
    DisplayInformation display_info = nullptr;
    display_info_factory->GetForMonitor(h_monitor, guid_of<DisplayInformation>(), put_abi(display_info));

    AdvancedColorInfo advanced_color_info = display_info.GetAdvancedColorInfo();

    std::cout << "max. nits: " << advanced_color_info.MaxLuminanceInNits() << std::endl;
}
emoacht commented 1 year ago

Thanks. I will wait for Microsoft to release C# equivalent.

lulle2007200 commented 1 year ago

I'm not too familiar with c#, but this should work pretty much the same in c#. As for the DwmpSDRToHDRBoost thing, someone posted an answer on the SO post with a c# version. I hope they make the HDR stuff more accessible eventually...

emoacht commented 1 year ago

I figured out the way to obtain DisplayInformation on WPF and incorporated the function to capture advanced color information in Ver 4.4.8. It can be enabled with /advancedcolor command-line option.

The next challenge is the maximum value. As far as I tested it with Dell U2720QM and S2721QS, the AdvancedColorInfo.SdrWhiteLevelInNits value when the slider is at 100 is always 480 whereas the AdvancedColorInfo.MaxLuminanceInNits value varies at 270, 350 or 400. I have no idea whether we can regard 480 as the common maximum value.

lulle2007200 commented 1 year ago

AdvancedColorInfo.MaxLuminanceInNits is defined in the color profile that is active for the monitor, it is supposed to report the monitors actual maximum brightness, but is basically meaningless if a wrong color profile is loaded. Also, the value that is reported for that is always for the primary monitor iirc. Same for AdvancedColorInfo.MaxAverageFullFrameLuminanceInNits. AdvancedColorInfo.SdrWhiteLevelInNits reports the absolute brightness level that pure white should be displayed at in SDR content. Windows defines the "reference" SDR white level to be 80 nits (or value 1.0f in FP16 HDR image data) and doesn't allow setting it any lower (therefore the minimum reported value for AdvancedColorInfo.SdrWhiteLevelInNits is always 80). Technically, it makes sense to allow setting AdvancedColorInfo.SdrWhiteLevelInNits up to AdvancedColorInfo.MaxAverageFullFrameLuminanceInNits, i don't know why windows limits it to 480 (6.0f in FP16 HDR) in the settings app. If the maximum is variable and depends on the monitor you use, i don't know how it calculates the maximum possible value. I've tried a few monitors and a bunch of (bogus) color profiles, it seems to always be 480.

When using DwmpSDRToHDRBoost, you can set the SDR white level from 80 nits up to whatever you want. However, if you set it higher than what your monitor actually can display, you get clipping.

emoacht commented 1 year ago

Also, the value that is reported for that is always for the primary monitor iirc. Same for AdvancedColorInfo.MaxAverageFullFrameLuminanceInNits.

What I have observed is different. The following is the values of each monitor when HDR is on for that monitor.

Monitor AdvancedColorInfo.MaxLuminanceInNits AdvancedColorInfo.MaxAverageFullFrameLuminanceInNits
U2720QM 400 400
S2721QS 351.2764 351.2764

Both monitors are connected to Surface Pro and Surface Pro's internal monitor is the primary. I confirmed each of them is associated with correct .icm file (Its file name includes each model name). This may depend on the devices.

I thought the actual brightness of a monitor could saturate at its AdvancedColorInfo.MaxLuminanceInNits but I noticed with my eyes that the brightness became higher even after it reached that value.

lulle2007200 commented 1 year ago

What I have observed is different. Yeah, i might be wrong on that. I just vaguely remember reading something like that on MSDN, might be in some other context.

AdvancedColorInfo.MaxLuminanceInNits is supposed to be the peak brightness of the monitor (for only a small area), AdvancedColorInfo.MaxAverageFullFrameLuminanceInNits is supposed to be the maximum brightness for full white frame (as the name suggests). In practice, they aren't always accurate, for one of the monitors i tested, the profile it loaded reports 10000 nits for both. So i can imagine that in practice in some cases the monitor might be more or less bright than what is reported in the profile.

I noticed with my eyes that the brightness became higher even after it reached that value.

If the monitor is actually a bit brighter than what is reported, that makes sense. I guess its simple to test. Just look at some black to white gradient while increasing SDR brightness (in settings or using DwmpSdrToHdrBoost when you want to go beyond 480 nits). Once you loose contrast on the white side of the gradient, you reached maximum supported brightness.

As for why the max. in settings is 480, maybe because minimum (vesa) HDR standard is HDR 400 which requires min. brightness of 400 nits?

emoacht commented 1 year ago

If the monitor is actually a bit brighter than what is reported, that makes sense.

Yes, I am sure.

As for why the max. in settings is 480, maybe because minimum (vesa) HDR standard is HDR 400 which requires min. brightness of 400 nits?

That is the question. I wish Microsoft could provide more information.

Draghmar commented 8 months ago

I have PG49WCD and noticed the same issues with HDR mode set to on as others. Does the experimental changes from above should work in the 4.6.1.0 version? I tried to set brightness from cli with the /advancedcolor switch but it didn't do anything regardless if I have Brightness Adjustable turned on or off - this options allows to change brightness even though it shouldn't be allowed, which really saves my eyes when doing basically anything - I don't know how people can play games when things tends to be so bright it's stupid. XD

emoacht commented 8 months ago

@Draghmar Thank you for your interest. The /advancedcolor option has been there. It is only valid on Windows 22H2 (build 22621) or newer. If the conditions for HDR are met, the current value of HDR settings should be shown under the slider for a while after it changes.

Screenshot

Draghmar commented 8 months ago

Hm...I've updated to the 4.6.3.0. I'm using MS Store for that. How can I launch app with the switch present? And using this way:

%LocalAppData%\Microsoft\WindowsApps\Monitorian.exe /advancedcolor /set "DISPLAY\AUS4921\5&1a8dbdb9&0&UID4355" 25

still doesn't do anything. I'm getting this in response:

DISPLAY\AUS4921\5&1a8dbdb9&0&UID4355 "PG49WCD" 25 B *

Launching app only with:

%LocalAppData%\Microsoft\WindowsApps\Monitorian.exe /advancedcolor

doesn't show mi those additional informations. obraz

emoacht commented 8 months ago

/advancedcolor option is to show the current HDR settings. Nothing else. /set option has nothing to do with it. Microsoft has not released enough information on HDR to make use of this settings' information. Anyway, your monitor seems not compatible with this function.

arduinka55055 commented 3 months ago

Hello. I really like Monitorian app, I've got an HDR monitor, and I've noticed that the slider actually changes brightness (Samsung lu28r550) But I'd like to have SDR content brightness slider as was mentioned before.

I really want to have SDR content slider, but I don't have experience in XAML, here's what I've found: https://stackoverflow.com/a/76516363 the code does exactly that, 6 is max value, I'd be happy if you add this to Monitorian app.

emoacht commented 3 months ago

That SO question is alreadly discussed in this issue. To date, I have not found a reliable way to change SDR level in a manner consistend with the OS's SDR level settings.

arduinka55055 commented 3 months ago

I've checked 6 is max SDR brightness (around 480 nits), that stackoverflow code worked for me well.

emoacht commented 3 months ago

Does it match the OS's SDR settings?

arduinka55055 commented 3 months ago

Interestingly, it changes monitor brightness well, but not OS's SDR settings, slider is stuck at previous value. Need to debug Settings, I think it is cached somewhere

lulle2007200 commented 3 months ago

Does it match the OS's SDR settings?

It used to in some previous windows version. Now it is cached in multiple places and registry.

There is also DISPLAYCONFIG_DEVICE_INFO_SET_SDR_WHITE_LEVEL that can be used with DisplayConfigSetDeviceInfo, that seems to do exactly what the OS setting does, but it's also undocumented and therefore subject to change.

arduinka55055 commented 3 months ago

Yeah, that's the issue.

lulle2007200 commented 2 months ago

@emoacht I did some more REing and found how to set it properly, see https://github.com/xanderfrangos/twinkle-tray/issues/97#issuecomment-2218185314

API's are still undocumented, but this sets the brightness properly.

emoacht commented 1 month ago

@lulle2007200 So have you found the new member of DISPLAYCONFIG_DEVICE_INFO_TYPE enumeration?

The following is derived from wingdi.h of Windows SDK 10.0.26100.1.

typedef enum
{
    DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME                 = 1,
    DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME                 = 2,
    DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_PREFERRED_MODE       = 3,
    DISPLAYCONFIG_DEVICE_INFO_GET_ADAPTER_NAME                = 4,
    DISPLAYCONFIG_DEVICE_INFO_SET_TARGET_PERSISTENCE          = 5,
    DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_BASE_TYPE            = 6,
    DISPLAYCONFIG_DEVICE_INFO_GET_SUPPORT_VIRTUAL_RESOLUTION  = 7,
    DISPLAYCONFIG_DEVICE_INFO_SET_SUPPORT_VIRTUAL_RESOLUTION  = 8,
    DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO         = 9,
    DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE        = 10,
    DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL             = 11,
    DISPLAYCONFIG_DEVICE_INFO_GET_MONITOR_SPECIALIZATION      = 12,
    DISPLAYCONFIG_DEVICE_INFO_SET_MONITOR_SPECIALIZATION      = 13,
    DISPLAYCONFIG_DEVICE_INFO_SET_RESERVED1                   = 14,
    DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2       = 15,
    DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE                   = 16,
    DISPLAYCONFIG_DEVICE_INFO_SET_WCG_STATE                   = 17,

      DISPLAYCONFIG_DEVICE_INFO_FORCE_UINT32                = 0xFFFFFFFF
} DISPLAYCONFIG_DEVICE_INFO_TYPE;