Rendering HDR content is well-understood in games and has existed since at least 2005 with Half-Life 2: Lost Coast created by Valve specifically to showcase HDR rendering.

HDR displays, however, have only started reaching the consumer market quite recently so HDR output is still not straightforward and, in some cases, requires using multiple OS-specific and vendor-specific APIs in the same codebase.

This post will cover the different methods of presenting HDR content to the user on desktop platforms.

HDR formats

Unlike films, game titles have to render HDR content locally and in real-time. Color grading and tonemapping have to be applied to all frames in the scene and cannot be done per-frame, with the exception of cutscenes.

However, rendering locally means that the tonemapping can be tailored to the user’s display and OS resulting in more control over the final image.

The recommended pipeline for rendering HDR content is to use compositor-specific formats. These will ensure that the final image is displayed correctly and will prevent the compositor from having to perform additional conversions.

On all platforms, the recommended backbuffer format is 16-bit floating point (not UNORM which limits all channels to 0-1).

Windows

Desktop HDR

Windows 10 Creators Update (version 1703) introduced support for rendering the entire desktop using HDR. This is mostly useful for DirectX 12 titles that use flip model swap chains and do not use exclusive fullscreen mode.

DWM (the compositor) uses scRGB in HDR mode. This is a linear format with the same color primaries as sRGB / Rec. 709 but a linear brightness scale.

In scRGB, 80 nits of brightness are mapped to 1.0 (for example, 1000 nits of brightness will be produced from the value of 12.5).

Detecting HDR support

To detect whether the display supports HDR content, the title has to:

  1. Obtain the IDXGIOutput6 interface
  2. Use IDXGIOutput6::GetDesc1 to obtain the DXGI_OUTPUT_DESC1 structure

The DXGI_OUTPUT_DESC1’s ColorSpace member will be:

  • DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 if the display supports HDR and HDR mode is enabled in Settings
  • DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709 if the display does not support HDR or HDR mode is not enabled in Settings

Sample code:

IDXGIOutput6 *output6 = nullptr;
output->QueryInterface(IID_PPV_ARGS(&output6));
DXGI_OUTPUT_DESC1 oDesc;
output6->GetDesc1(&oDesc);

bool supportsHDR = (oDesc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020);

When support is detected, the title can present the HDR option to the user and proceed with outputting HDR content.

Obtaining display capabilities

DXGI_OUTPUT_DESC1 contains several fields that are relevant for tonemapping:

  • RedPrimary, GreenPrimary, BluePrimary are float[2] that contain the X and Y coordinates of color primaries
  • WhitePoint is a float[2] that contains the X and Y coordinates of the white point
  • MinLuminance and MaxLuminance are the minimum and maximum luminance of the display in nits
  • MaxFullFrameLuminance is the maximum luminance of the display that is valid for the entire surface

These values should be used for tonemapping the image. Remember to properly map the luminance values to component values (as mentioned above, 80 nits of brightness are mapped to 1.0 in scRGB).

To detect any display changes, the title should call IDXGIFactory1::IsCurrent every frame and re-enumerate the outputs if the result is FALSE.

Presenting HDR content

To present HDR content, the title should:

  • use DXGI_FORMAT_R16G16B16A16_FLOAT as the backbuffer format
  • use a flip model swap chain (created with DXGI_SWAP_EFFECT_FLIP_DISCARD or DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL)
  • obtain IDXGISwapChain3
  • verify support for the color space using IDXGISwapChain3::CheckColorSpaceSupport
  • set the color space to DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 using IDXGISwapChain3::SetColorSpace1

Exclusive fullscreen with AMD GPUs

Use desktop HDR if possible, it is easier to implement and provides better user experience.

If the title runs in exclusive fullscreen mode or the Windows version is older than 1703, the title has to use AMD-specific APIs to output HDR content. AMD GPUs support HDR with FreeSync HDR-compatible displays and titles that link and use AMD GPU Services (AGS).

To check whether GPU is manufactured by AMD, the title can obtain the DXGI_ADAPTER_DESC or any of its newer versions and check the VendorId member. AMD’s vendor IDs are 0x1002 and 0x1022.

AMD provides detailed instructions and code samples for using AGS to output HDR content here: https://gpuopen.com/learn/using-amd-freesync-premium-pro-hdr-code-samples/

Exclusive fullscreen with NVIDIA GPUs

Use desktop HDR if possible, it is easier to implement and provides better user experience.

If the title runs in exclusive fullscreen mode or the Windows version is older than 1703, the title has to use NVIDIA-specific APIs to output HDR content. NVIDIA GPUs support HDR with HDR10-compatible displays and titles that link and use NVAPI.

To check whether GPU is manufactured by NVIDIA, the title can obtain the DXGI_ADAPTER_DESC or any of its newer versions and check the VendorId member. NVIDIA’s vendor ID is 0x10DE.

NVIDIA provides detailed instructions and code samples for using NVAPI to output HDR content here: https://developer.nvidia.com/displaying-hdr-nuts-and-bolts

macOS

macOS supports HDR output natively in any application from version 10.15 (Catalina) as long as the display supports HDR and the preset applied to the display has “Enable HDR content” checked.

macOS’s Quartz Compositor supports linear extended sRGB for high-performance direct rendering. This is a linear format with the same color primaries as sRGB / Rec. 709 but a linear brightness scale. 100 nits of brightness are mapped to 1.0 in this format (for example, 1000 nits of brightness will be produced from the value of 10.0).

Detecting HDR support

CAMetalLayer always outputs in the display’s native color space. To detect whether the display supports HDR content, the title has to:

  1. Obtain the NSScreen object that represents the target display
  2. Check the value of maximumPotentialExtendedDynamicRangeColorComponentValue is greater than 1.0. If it is, the display supports HDR content

Sample code:

NSScreen *screen = [NSScreen mainScreen];
CGFloat maxEDR = [screen maximumPotentialExtendedDynamicRangeColorComponentValue];
bool supportsHDR = maxEDR > 1.0;

When support is detected, the title can present the HDR option to the user and proceed with outputting HDR content.

Obtaining display capabilities

NSScreen contains maximumExtendedDynamicRangeColorComponentValue that is the maximum luminance of the display in nits. This value should be used for tonemapping the image.

To detect any display changes, the title should subscribe to NSApplicationDidChangeScreenParametersNotification and re-enumerate the screens if the notification is received.

Presenting HDR content

To present HDR content, the title should:

  • use MTLPixelFormatRGBA16Float as the backbuffer format
  • set wantsExtendedDynamicRangeContent to YES on the CAMetalLayer object
  • set the color space to linear extended sRGB on the CAMetalLayer object

Sample code:

CAMetalLayer *metalLayer = [CAMetalLayer new];
metalLayer.wantsExtendedDynamicRangeContent = YES;
metalLayer.pixelFormat = MTLPixelFormatRGBA16Float;
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceExtendedLinearSRGB);
metalLayer.colorspace = colorspace;
CGColorSpaceRelease(colorspace);

Further reading

https://learn.microsoft.com/en-us/windows/win32/direct3darticles/high-dynamic-range

https://developer.nvidia.com/downloads/hdr-gdc-2018

https://www.asawicki.info/news_1703_programming_hdr_monitor_support_in_direct3d

https://developer.apple.com/documentation/metal/hdr_content/displaying_hdr_content_in_a_metal_layer?language=objc

https://developer.apple.com/documentation/metal/hdr_content/performing_your_own_tone_mapping?language=objc