DirectX HDR10 Tone Mapping for Wide-Gamut Buffers
This article provides an overview of how DirectX processes high-dynamic-range (HDR) imagery, detailing how wide-gamut, floating-point render buffers are translated into consumer-ready HDR10 signals. You will learn the mechanics behind color space transformations, tone-mapping operators tailored for display luminance boundaries, the application of the Perceptual Quantizer (PQ) curve, and how the DirectX Graphics Infrastructure (DXGI) negotiates final output to an HDR-capable display.
The Source: Floating-Point Render Targets
Modern DirectX rendering pipelines generate high-dynamic-range scenes
inside 64-bit floating-point surfaces, typically using the
DXGI_FORMAT_R16G16B16A16_FLOAT format. These FP16 buffers
store scene-referred lighting values linearly without arbitrary dynamic
range clipping.
In DirectX, these buffers operate primarily under one of two color representations:
- scRGB (Linear Rec.709 primaries): In this space, 1.0 represents nominal standard dynamic range (SDR) white (typically calibrated to 80 or 100 nits). Values exceeding 1.0 describe high-intensity specular highlights, while negative values represent wide-gamut colors outside Rec.709 primaries.
- Linear Rec.2020: In this space, pixel coordinates are directly mapped to the massive Rec.2020 wide color gamut, with light values expressed in absolute or normalized physical units.
Because displays cannot consume infinite dynamic range or arbitrary floating-point values, these buffers require transformation before transmission.
Display Calibration and Metadata Retrieval
Before mapping luminance and chrominance, DirectX must determine the
target display's physical boundaries. The engine queries the display’s
capabilities via DXGI using IDXGIOutput6::GetDesc1. This
structure returns:
- Minimum and maximum luminance values (expressed in nits).
- Display color primaries (usually conforming to DCI-P3 within a Rec.2020 container).
- MaxCLL (Maximum Content Light Level) and MaxFALL (Maximum Frame Average Light Level) limits.
With these parameters, the engine constructs a tone-mapping strategy adapted to the specific display hardware rather than a generic clipping threshold.
The Tone Mapping Stage
Tone mapping converts the high-dynamic-range scene values to match the peak luminance capabilities of the target display. Because HDR10 allows up to 10,000 nits, but consumer panels typically peak between 400 and 1,500 nits, unmapped values would blow out highlights into hard white clipping.
DirectX shaders execute luminance tone mapping via a compute or full-screen pixel shader using algorithms such as ACES (Academy Color Encoding System) or custom Hermite/spline curves. The operations generally follow this sequence:
- Luminance Extraction: The shader computes perceptual luminance (\(Y\)) from linear RGB values.
- Compressing Highlights: The tone mapper leaves midtones and values below nominal white linear to maintain visual fidelity, while applying a compression roll-off (shoulder) only to values approaching the display’s measured peak nit capability.
- Gamut Compression: Highly saturated colors outside the target monitor's capability are desaturated toward the display boundary rather than clipped abruptly, preventing color shifting in bright highlights.
Color Space Transformation to Rec.2020
HDR10 strictly mandates the Rec.2020 color primaries. If the engine renders in scRGB, it must convert those coordinates into Rec.2020 space.
This is accomplished in the shader using a \(3 \times 3\) color matrix multiplication:
\[\begin{bmatrix} R_{2020} \\ G_{2020} \\ B_{2020} \end{bmatrix} = M_{\text{scRGB} \to \text{Rec.2020}} \times \begin{bmatrix} R_{\text{scRGB}} \\ G_{\text{scRGB}} \\ B_{\text{scRGB}} \end{bmatrix}\]
Because scRGB can encode values outside the physical Rec.709 spectrum using negative coordinates, this matrix conversion correctly maps those extended values into positive coordinates within the wider Rec.2020 gamut.
Applying the SMPTE ST 2084 (PQ) Transfer Function
Unlike standard desktop pipelines that rely on standard gamma curves (such as Gamma 2.2 or sRGB), HDR10 requires the non-linear transfer function defined by SMPTE ST 2084, known as the Perceptual Quantizer (PQ).
The PQ curve is an absolute transfer function designed to cover luminance levels from 0.0001 nits up to 10,000 nits. The shader divides the linear nit values by 10,000 to normalize them into a \(0.0 \text{ to } 1.0\) range, then computes the non-linear PQ values using standard formula constants (\(m_1, m_2, c_1, c_2, c_3\)):
\[V = \left( \frac{c_1 + c_2 L^{m_1}}{1 + c_3 L^{m_1}} \right)^{m_2}\]
Where \(L\) is normalized absolute luminance. The resulting value encodes perceptual steps evenly across the entire dynamic range, mitigating visible color banding.
Swapchain Presentation and DXGI Encoding
DirectX provides two main paths to hand off this data to the Windows Desktop Window Manager (DWM) and the physical output:
1. In-Shader Encoding to 10-Bit Integer Surface
The application performs tone mapping, Rec.2020 conversion, and PQ encoding manually inside its post-processing shaders. The output is written directly into an integer render target:
- Format:
DXGI_FORMAT_R10G10B10A2_UNORM - Color Space:
DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020
DXGI recognizes that the data is already converted to PQ and
Rec.2020, passing the 10-bit signal straight through the swapchain to
the display controller with static HDR10 metadata attached via
IDXGISwapChain4::SetHDRMetaData.
2. Passing FP16 Directly to DWM
Alternatively, the engine can output linear scRGB directly into an FP16 swapchain:
- Format:
DXGI_FORMAT_R16G16B16A16_FLOAT - Color Space:
DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709
In this path, the application performs basic tone adjustments within its own linear pipeline, and the Desktop Window Manager handles the final hardware conversion to PQ and Rec.2020. While convenient, developer-driven in-shader tone mapping targeting the exact metadata limits often yields more precise artistic and luminance control.