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:
agsInit
to initialize the library.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:
NvAPI_Initialize
to initialize the library.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.
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:
IDXGISwapChain::SetFullscreenState(TRUE, NULL)
, but I don’t have to.IDXGISwapChain4::SetHDRMetaData
with various parameters - swapped red with green primary, changed luminances by orders of magnitude… No effect was observed.IDXGISwapChain3::SetColorSpace1
. With value DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020
the call failed, despite this is the value reported by the monitor. With value DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709
it succeeded, but it changed nothing, because that’s what we are using here anyway (RGB color space, G10 = gamma with exponent 1.0 = linear space, P709 = primaries similar to sRGB). I tried to use gamma 2.2, but call with DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709
also failed.agsSetDisplayMode
. Again, changing primaries or luminances has no effect, and so doesn’t setting mode to Mode_HDR10_scRGB
, Mode_FreeSync2_scRGB
, or Mode_FreeSync2_Gamma22
. The only thing that made a difference was using Mode_SDR
. My program then behaved in a strange way that it used SDR only, but it still expected range of values 0..6.2, in linear space.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.
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:
R8G8B8A8_UNORM
and no exclusive fullscreen, values 0..1 of course map to SDR, in sRGB (gamma) space.R16G16B16A16_FLOAT
, still in windowed mode, HDR starts working. Colors are expected in linear space, and the maximum brightness seems somewhere around value 12.5.R16G16B16A16_FLOAT
, but entered exclusive fullscreen mode using function IDXGISwapChain::SetFullscreenState(TRUE, NULL)
, HDR was activated, but maximum brightness was reached already at values around 0.6!R8G8B8A8_UNORM
, HDR was still active, but this time maximum brightness was reached at value 1.0, and sRGB gamma space was expected.IDXGISwapChain4::SetHDRMetaData
with any parameters had no effect.IDXGISwapChain3::SetColorSpace1
always failed on Intel regardless of parameter passed.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:
R16G16B16A16_FLOAT
format.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