wgpu/documentation/color/
hdr_surfaces.rs

1/*!
2# HDR Surfaces
3
4A surface can present in different color spaces to get HDR or wide-gamut
5output onto the screen. This is configured through
6[`SurfaceConfiguration::color_space`], and the rest of this section is a
7concept primer on how it works.
8
9### HDR output in a nutshell
10
11By default a surface is standard dynamic range (SDR) with the sRGB gamut: the
12value `1.0` is the brightest white, anything above it clips, and only colors
13within sRGB are expressible. Other [`SurfaceColorSpace`]s opt into a **wider
14gamut** (for example [`DisplayP3`](SurfaceColorSpace::DisplayP3), still SDR
15but with more saturated colors than sRGB), **high dynamic range** (values
16above `1.0` drive brighter-than-white output), or both (for example
17[`Bt2100Pq`](SurfaceColorSpace::Bt2100Pq), aka HDR10), on platforms that
18support it.
19
20Three ideas carry most of the weight:
21
22* **Reference white and headroom.** Brightness is measured in *nits*
23  (cd/m²). On HDR-capable monitors, SDR reference white (plain white, `(1.0, 1.0, 1.0)` in the
24  extended color spaces) sits *below* the display's peak output on purpose, so
25  highlights have room above it. That gap is the display's *headroom*.
26* **The transfer function is a round-trip.** Your shader applies an
27  encoding transfer function (the OETF) to turn the light it computed into a
28  stored signal, and the display applies the inverse decoding transfer
29  function (the EOTF) to turn it back into light. Choosing a color space
30  chooses which transfer functions both ends use.
31* **Who applies the encoding transfer function.** wgpu applies it for you
32  **only** when you render to an `*Srgb` texture view format, where the GPU
33  runs the sRGB OETF when a value is stored to a texture. For every other
34  color space (linear extended sRGB, encoded extended sRGB or P3, PQ, HLG)
35  **the values your shader writes to the surface texture must already be
36  encoded by you**, along with any gamut conversion; in a typical renderer
37  this happens in a final tone-mapping or post-processing pass. wgpu hands
38  the signal to the compositor unchanged; getting this wrong produces a
39  wrong image with no error.
40
41wgpu does **not** tonemap or gamut-map for you. It gives you the surface
42and, through [`DisplayHdrInfo`], the display's advisory capabilities;
43*choosing* and *applying* a tone curve is your application's job.
44
45### The practical path
46
471. **Query capabilities.** Call [`Surface::get_capabilities`]. To use HDR or
48   wide-gamut output, read [`SurfaceCapabilities::format_capabilities`] (each
49   format and the [`SurfaceColorSpaces`] it supports), **not**
50   [`SurfaceCapabilities::formats`]: the latter lists only formats usable
51   with [`Auto`](SurfaceColorSpace::Auto), which never selects HDR.
52   [`SurfaceCapabilities::color_spaces`] is a convenience lookup for one
53   format.
542. **Optionally query the display.** Call [`Surface::display_hdr_info`] for the
55   current [`DisplayHdrInfo`] (peak and SDR-white nits, EDR headroom,
56   primaries, and a coarse dynamic-range/gamut bucket). Use it to pick a
57   tone-map target ([`DisplayHdrInfo::tone_map_headroom`]); *whether* HDR is
58   worthwhile is the capability question from step 1, not this live value.
59   Every field is advisory and optional; `None` means "cannot tell here",
60   never "SDR".
613. **Choose a format and color space.** Intersect what you want with what
62   step 1 advertises, in your own preference order (for example HDR10, then
63   linear extended sRGB, then encoded extended sRGB, then SDR
64   [`Srgb`](SurfaceColorSpace::Srgb)). Keep an SDR fallback for when nothing
65   HDR is advertised, such as when OS HDR is off.
664. **Configure the surface.** Set [`SurfaceConfiguration::color_space`] and
67   `format`. [`Auto`](SurfaceColorSpace::Auto) (the default) reproduces
68   wgpu's historical behavior and never picks HDR; any other value must be in
69   that format's advertised set or configuration fails validation.
705. **Encode what you write to the surface texture.** For an `*Srgb` format,
71   output linear and the hardware encodes for you. Otherwise the values your
72   shader writes to the surface texture must already carry the encoding the
73   chosen color space expects (sRGB, extended sRGB, PQ, or HLG) **and** any
74   gamut conversion (for example outputting in the BT.2020 gamut for HDR10);
75   in a typical renderer you do this in a final tone-mapping or
76   post-processing pass. See the table below.
776. **Present** as usual. If OS HDR is toggled mid-run, you'll see it on the
78   next [`Surface::display_hdr_info`] poll; re-query [`Surface::get_capabilities`]
79   and re-run the steps above.
80
81The standalone [HDR surface example] implements every step, including the
82encoding transfer function for each color space.
83
84### What to output from your fragment shader
85
86What the values your shader writes to the surface texture must contain for
87each color space, and whether wgpu applies the transfer function for you:
88
89| Color space | Typical format | You write | wgpu encodes? |
90| ----------- | -------------- | --------- | ------------- |
91| `Srgb`, `*Srgb` format | `{Rgba,Bgra}8UnormSrgb` | linear | **yes** (hardware sRGB OETF on store) |
92| `Srgb`, non-srgb format | `{Rgba,Bgra}8Unorm` | sRGB-encoded | no; apply the sRGB OETF yourself or use `*Srgb` instead |
93| `ExtendedSrgbLinear` (scRGB) | `Rgba16Float` | linear, `1.0` = SDR white | no, but no encoding is necessary |
94| `ExtendedSrgb` | `Rgba16Float` | extended sRGB-encoded | no; apply the extended sRGB OETF yourself |
95| `DisplayP3` | `Bgra8Unorm` | sRGB-encoded, P3 primaries | no; apply the sRGB OETF (after gamut-mapping to P3) |
96| `ExtendedDisplayP3` | `Rgba16Float` | extended sRGB-encoded, P3 primaries | no; apply the extended sRGB OETF (after gamut-mapping to P3) |
97| `Bt2100Pq` (HDR10) | `Rgb10a2Unorm` | PQ-encoded, BT.2020 primaries | no; apply the PQ OETF (after gamut-mapping to BT.2020) |
98| `Bt2100Hlg` | `Rgb10a2Unorm` | HLG-encoded, BT.2020 primaries | no; apply the HLG OETF (after gamut-mapping to BT.2020) |
99
100In short, wgpu applies the transfer function for you only when you render to
101an `*Srgb` format. In every other case the values your shader writes to the
102surface texture must already carry both the transfer function and any gamut
103conversion. The [HDR surface example] implements every encoder in WGSL.
104
105### Glossary
106
107* **Chromaticity** --- a color's hue and saturation independent of its
108  brightness, given as an `(x, y)` coordinate on the CIE 1931 diagram.
109* **Primaries / gamut** --- the chromaticities of the red, green, and blue a
110  color space addresses, and so the range of colors it can express. [BT.709]
111  is the sRGB gamut, [Display P3] is wider, and [BT.2020] is wider still.
112* **White point** --- the chromaticity of `R = G = B` (what "white" looks
113  like). Every color space here uses [D65], standard daylight.
114* **Transfer function (OETF / EOTF)** --- how stored values map to light. The
115  *OETF* is the encoding transfer function your application applies; the
116  *EOTF* is the inverse decoding transfer function the display applies.
117* **SDR / HDR** --- standard dynamic range clips at `1.0` (reference white);
118  high dynamic range lets values above `1.0` drive brighter-than-white
119  output.
120* **Nits and reference white** --- a *nit* (cd/m²) is a unit of brightness;
121  *reference white* (also called *paper white*, especially on Windows) is the
122  brightness of SDR plain white, set below the panel's peak so highlights have
123  room above it.
124* **Headroom (EDR)** --- how much brighter than current SDR white the display
125  can go right now, as a multiplier (`1.0` means none). Dynamic; see
126  [`DisplayHdrInfo::tone_map_headroom`].
127* **PQ / HLG** --- the two HDR transfer functions: [PQ] (SMPTE ST 2084,
128  HDR10) encodes absolute luminance, [HLG] (BT.2100) encodes relative
129  luminance.
130* **scRGB / extended-range sRGB** --- sRGB extended past 0.0..=1.0 for
131  HDR: [scRGB] is *linear*
132  ([`ExtendedSrgbLinear`](SurfaceColorSpace::ExtendedSrgbLinear)), while
133  [`ExtendedSrgb`](SurfaceColorSpace::ExtendedSrgb) is the same range but
134  sRGB-*encoded* (gamma), the web's HDR path.
135
136[HDR surface example]: https://github.com/gfx-rs/wgpu/tree/v29/examples/standalone/03_hdr_surface
137[BT.709]: https://www.itu.int/rec/R-REC-BT.709
138[BT.2020]: https://www.itu.int/rec/R-REC-BT.2020
139[Display P3]: https://en.wikipedia.org/wiki/DCI-P3#Display_P3
140[D65]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
141[PQ]: https://en.wikipedia.org/wiki/Perceptual_quantizer
142[HLG]: https://www.itu.int/rec/R-REC-BT.2100
143[scRGB]: https://en.wikipedia.org/wiki/ScRGB
144*/
145
146use crate::{
147    DisplayHdrInfo, Surface, SurfaceCapabilities, SurfaceColorSpace, SurfaceColorSpaces,
148    SurfaceConfiguration,
149};