Programming HDR monitor support in Direct3D

Wed
27
Feb 2019

I got an HDR supporting monitor (LG 32GK850F), so I started learning how I can use its capabilities programatically. I still have much to learn, as there is a lot of theory to be ingested about color spaces etc., but in this blog post I’d like to go straight to the point: How to enable HDR in your C++ DirectX program? To test this, I used 3 graphics chips from 3 different PC GPU vendors. Below you can see results of my experiments.

You need to know that there are two different ways to query for display capabilities, as well as to control its working parameters.

1. First is standard Windows, GPU-independent API called DirectX Graphics Infrastructure (DXGI). It’s available together with Windows SDK, in form of a COM API. It’s a common layer for Direct3D 11, as well as 12 (you can even use it with Vulkan). Functions related to HDR are added in recent updates of the API, which take form of new interfaces with a number at the end. To access them, you have to query your base object for them first. Example:

IDXGIOutput* output = /* initialize output */;
IDXGIOutput6* output6;
HRESULT hr = output->QueryInterface(__uuidof(IDXGIOutput6), (void**)&output6);
if(SUCCEEDED(hr)) {
    // Use output6...
    output6->Release();
} else {
    // Error!
}

You will be able to successfully compile this code only if you have sufficiently new version of Windows SDK installed. The code will execute successfully (as opposed to failing with an error code) only if the user has sufficiently new version of Windows 10.

You can then query for monitor capabilities by calling function IDXGIOutput6::GetDesc1. You get structure DXGI_OUTPUT_DESC1 filled, which describes available color space, bits per component, red/green/blue primaries, white point, and the range of luminances available on the device.

To inform the system about intended HDR usage, you may call function IDXGISwapChain4::SetHDRMetaData with structure DXGI_HDR_METADATA_HDR10 filled, where you pass your parameters - again red/green/blue, white point, and luminance ranges. There is also IDXGISwapChain3::SetColorSpace1 function that lets you pass enum DXGI_COLOR_SPACE_TYPE, telling what color space do you use (RGB or YCbCr, linear or gamma 2.2, P709 or P2020 etc.)

2. There are custom APIs from different GPU vendors.

2.1. In case of AMD, we are talking about the library called AMD AGS. To use it in your C++ code, you need to:

To query for monitor capabilities, inspect structure AGSGPUInfo filled by agsInit. Contained structure AGSDisplayInfo provides information about a connected monitor, including displayFlags (telling what HDR modes are supported), color primaries, white point, and min/max/avg luminance.

To enable HDR mode, call agsSetDisplayMode function. Specify requested parameters in AGSDisplaySettings structure.

2.2. In case of Nvidia, there is a library called NVAPI (free registration is required to download it). To use it in your C++ code, you need to:

To query for monitor capabilities, first enumerate available DisplayIds. Then call NvAPI_Disp_GetHdrCapabilities. Returned structure NV_HDR_CAPABILITIES provides information about HDR parameters of a monitor - color primaries, white point, min/max luminance etc. Displays supporting HDR set the isST2084EotfSupported member of the structure to 1.

To enable HDR mode, call NvAPI_Disp_HdrColorControl function. Use structure NV_HDR_COLOR_DATA to specify requested parameters.

Update 2019-03-05: As @DirectX12 responded on Twitter, "Vendor-specific APIs shouldn't be necessary these days."

That’s the theory. Below are the results of my experiment on 3 different GPUs. All use the same HDR monitor (LG 32GK850F), connected as the only active display via HDMI or DisplayPort cable, with monitor driver installed. I used Windows 10 64-bit version 1809 (OS Build 17763.316) in each case.

1. GPU = AMD Radeon RX Vega 56, driver = 19.2.3

IDXGIOutput6::GetDesc1 returned DXGI_OUTPUT_DESC1 members:

BitsPerColor = 10
ColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020
RedPrimary = 0.669921875, 0.3095703125
GreenPrimary = 0.2548828125, 0.6796875
BluePrimary = 0.150390625, 0.0751953125
WhitePoint = 0.3134765625, 0.3291015625
MinLuminance = 0 nits
MaxLuminance = 496 nits
MaxFullFrameLuminance = 496 nits

AGSDisplayInfo has members:

displayFlags = AGS_DISPLAYFLAG_PRIMARY_DISPLAY | AGS_DISPLAYFLAG_HDR10 | AGS_DISPLAYFLAG_FREESYNC | AGS_DISPLAYFLAG_FREESYNC_2
chromaticityRed = 0.6699, 0.3096
chromaticityGreen = 0.2549, 0.6797
chromaticityBlue = 0.1504, 0.0752
chromaticityWhitePoint = 0.3135, 0.3291
screenDiffuseReflectance = 0
screenSpecularReflectance = 0
minLuminance: 0.1687 nits
maxLuminance = 496 nits
avgLuminance = 496 nits

As you can see, the parameters pretty much match between DXGI and AGS, except minLuminance.

Now to the test of enabling HDR: As it turns out, the only thing needed to make it working is to create swap chain in half-float format R16G16B16A16_FLOAT instead of traditional one with 8 bits per component, like R8G8B8A8_UNORM.

Then values 0..1 map to SDR, while values above that make things brighter. The value that gets maximum HDR brightness seems to be around 6.2. It makes sense, because we use scRGB standard here (designed to be backward-compatible with SDR), which defines value 1 as 80 nits, so value 6.2 gives 496 nits - exactly the maximum luminance as reported by my monitor.

It is also important to note that you are expected to output color values in linear space, not gamma-corrrected!

I tried many different things that may be related to display mode, but it seems they are neither required nor changing anything in the HDR behavior:

2. GPU = Nvidia GeForce GTX 1070, driver = 419.17

IDXGIOutput6::GetDesc1 returned DXGI_OUTPUT_DESC1 members:

BitsPerColor = 8
ColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020
RedPrimary, GreenPrimary, BluePrimary, WhitePoint same as on AMD
MinLuminance = 0 nits
MaxLuminance = 1499 nits
MaxFullFrameLuminance = 799 nits

Wow, that’s much different! Is it because I use different cable and connector type, or because of different graphics card? I don’t know.

NVAPI function NvAPI_Disp_GetHdrCapabilities returns following NV_HDR_CAPABILITIES structure members:

isST2084EotfSupported = true
isTraditionalHdrGammaSupported = false
isEdrSupported = true
driverExpandDefaultHdrParameters = false
isTraditionalSdrGammaSupported = true
isDolbyVisionSupported = false
display_data.displayPrimary_x0 = 33496, which means red X = 0.66992
display_data.displayPrimary_y0 = 15478, which means red Y = 0.30956
display_data.displayPrimary_x1 = 12744, which means green X = 0.25488
display_data.displayPrimary_y1 = 33984, which means green Y = 0.67968
display_data.displayPrimary_x2 = 7519, which means blue X = 0.15038
display_data.displayPrimary_y2 = 3759, which means blue Y = 0.07518
display_data.displayWhitePoint_x = 15673, which means 0.31346
display_data.displayWhitePoint_y = 16455, which means 0.3291
display_data.desired_content_max_luminance = 0
display_data.desired_content_min_luminance = 0
display_data.desired_content_max_frame_average_luminance = 0
dv_static_metadata = all zeros

As you can see, we have same red/green/blue primaries and white point as before, but we don’t have information about luminance.

Just like on AMD, to enable HDR mode, it is enough to create swap chain in R16G16B16A16_FLOAT format. We can do it fullscreen or in a window, it doesn’t matter. Colors are also expected in linear space, but the maximum seems to be around value 12.5 (which corresponds to exactly 1000 nits).

Just as before, calling IDXGISwapChain3::SetColorSpace1 with DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 does nothing, while other values make this function fail. I also tried to call NvAPI_Disp_HdrColorControl, but no values set in NV_HDR_COLOR_DATA structure made any difference, even changing hdrMode from NV_HDR_MODE_UHDA (“This is the only supported production HDR mode.”) to NV_HDR_MODE_OFF. HDR was still active in this case.

3. GPU = Intel Iris Plus Graphics 655, driver = 25.20.100.6519

IDXGIOutput6::GetDesc1 returned same members of DXGI_OUTPUT_DESC1 structure as on Nvidia:

BitsPerColor = 8
ColorSpace = DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020
RedPrimary, GreenPrimary, BluePrimary, WhitePoint same as on AMD
MinLuminance = 0 nits
MaxLuminance = 1499 nits
MaxFullFrameLuminance = 799 nits

Experiments with various display modes gave very interesting results:

Conclusions

What a mess! It is possible that I still don’t understand something here. I may update this blog post or write a new one later. For now it seems to me that that you can robustly support HDR monitors in your game (which is a good news), but you need to disregard all these advanced parameters and just follow simple steps:

  1. Use swapchain in R16G16B16A16_FLOAT format.
  2. Give the player control over brightness and gamma exponent in graphics options, because valid parameters may vary depending on graphics card (maximum HDR brightness may be reached at 0.6 or at 12.5) and there is no good way to query them.

Update 2019-03-02: Please see also my next post: Programming FreeSync 2 support in Direct3D

Update 2019-03-07: OK, I was wrong, setting different values of min/max luminance for IDXGISwapChain4::SetHDRMetaData does change something in the appearance of dark colors, to be seen when looking at a gradient. I recommend D3D12HDR in Microsoft/DirectX-Graphics-Samples GitHub - a great sample app.

Comments | #graphics #windows #directx #gpu Share

Comments

[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2024