1 · Light units
Lux, candela and cd/m², exposure as EV100, in a new ADR. glTF lights import unchanged and light ratios are real.
Or U2, relative units: fewer stops, a scale at glTF import, hard to convert later.
Chapter 2A field guide to the HDR survey: how bright light is measured, exposed, stored, squeezed onto a screen and dithered, and what each of PR #219's seven decisions chooses between. The demos run the same formulas as the survey's probe, so what you see here is what the probe measured.
Provenance. PR #219 merged on 2026-10-20 as f15a35f. The project's owner took all seven of its recommendations, as the project's map issue records, and U1 became ADR 0023 in the PR itself. #200 then built the chosen pipeline in seven PRs, #250 to #256, all merged on 2026-10-22: photometric lights with an EV100 per Reference scene, an Rgba16Float target with the weighted resolve, and a tonemapping pass that applies GT7, encodes sRGB and dithers into an 8-bit UNORM target. Tonemapping Pass Explained follows that pipeline.
When the survey was written, the forward pass wrote linear radiance straight into an 8-bit sRGB target. There was no exposure and no tonemapping operator, so anything brighter than 1.0 clipped, channel by channel. The lights were relative: a sun of about 1, unitless point lights, a constant ambient term.
That worked while scenes stayed dim. The five scenes the probe captured peaked at 855 (helmet) and 499.9 (pbr-sweep), yet no more than 0.56% of any one's pixels passed 1.0. Real light spans about 30 stops, each stop a doubling, from a moonless night to direct sun. A display shows a small slice of that. Getting from one to the other takes a chain of steps, and the survey has one decision for each link.
Physics measures visible light with four photometric units. glTF, the format Praxis imports, already uses three of them:
Option U1 puts Praxis's lights in these units, exactly as glTF defines them: directional lights in lux, point lights in candela, emissive and background in cd/m², ambient in lux, exposure as EV100. glTF lights then import unchanged, and the ratios between sun, sky and lamps are real, which is what ray-traced shadows and probe GI (ADR 0020) will expect. Option U2 keeps relative numbers: fewer stops to handle and an exposure near 1, but glTF lights need a scale at import. Frostbite moved to physical units mid-life and called converting its relative lighting rigs "extremely hard". Units are hard to change once worlds are authored, which is why U1 is recorded in an ADR, ADR 0023.
A camera does not record light as it is. It picks a slice: aperture, shutter time and sensor sensitivity set how much light becomes a mid-grey photo. Engines copy this with one number, EV100, the exposure value at ISO 100:
EV100 = log2(N² / t) − log2(S / 100) for f-number N, shutter time t in seconds and ISO S. A pixel's exposed value is its luminance times 1 / (1.2 · 2^EV100), so 1.0 lands near display white. The 1.2 comes from how camera sensors are rated. The photographer's Sunny 16 rule lands near EV100 15.
One EV cannot fit everything. Move the slider and watch two 18% grey cards, one in a living room and one in direct sun.
Three ways to choose the EV:
Exposed values can be 0.001 or 3,000, so the lit pass needs a target that stores floats. A 16-bit float (fp16, Rgba16Float) has 1 sign bit, 5 exponent bits and 10 mantissa bits. Its largest value is 65504, its smallest normal value 6.1×10⁻⁵, and from there up its steps are at most 0.1% of the value. Rg11b10Ufloat packs three unsigned floats into 32 bits, half the memory, with 6-, 6- and 5-bit mantissas and no alpha. The coarse blue is where Frostbite saw a small hue shift.
Bright values still overflow. A glossy non-metal's highlight of the sun at 100,000 lx, at roughness 0.04 and with the sun as a directional light, peaks at 1.24×10⁸ cd/m², 10.9 stops past fp16's maximum. Engines therefore pre-expose: shading runs in f32 and only the exposed result is stored. At EV100 15 that highlight becomes 3,162. A white metal's highlight becomes 79,053, still too big, so every option clamps to 65504 before the write. That clamp matters across backends: the probe measured that the M4 Pro saturates overflow to 65504, a case Metal's specification leaves open, while Vulkan's rules allow infinity, and an infinite sample can turn into black patches.
Type a positive number, such as 0.5, 1000 or 1.24e8.
| Format | Stores | Step at this size | Note |
|---|
Rg11b10Ufloat as a render target needs the RG11B10UFLOAT_RENDERABLE feature, a new Capability-floor entry. At 2560 × 1440 one target is 29.5 MB in fp16 against 14.7 MB in 11/11/10.A tonemapping operator is a curve from exposed values, 0 to infinity, onto the display's 0 to 1. A good one keeps mid-tones close to what the scene had, rolls highlights off smoothly instead of clipping them, and decides how bright colours behave near white.
That last point separates the families. Apply the curve to each channel and a bright saturated colour shifts hue: the brightest channel tops out while the others keep climbing, so a bright blue turns cyan or magenta, or stays a saturated block that never whitens. Apply it to luminance only and the hue holds, but saturated highlights leave the display's range. Film and cameras do something in between: very bright coloured light desaturates toward white. Engines call that the path to white.
Pick an operator below. The sweep is a small copy of the probe's: fully saturated hues across, a neutral strip at the left, and exposure from 0.18 × 2¹⁰ at the top down to 0.18 × 2⁻⁶ at the bottom.
Move over the chart to read a value.
The probe ran all nine operators over the full sweep and over the scenes it captured, as they were then, at the same exposure. Brightness differences between tiles come from where each operator puts mid-grey; look at how colours behave as they get bright.
The sweep, through every operator
pbr-sweep
helmetPR #219's short list for O, all analytic, permissively licensed and taking saturated light to white (the survey note itself lists five options, Tony McMapface and an ACES 1 fit among them, and recommends none):
Not recommended: the ACES 1 fits (Hill's whitens blue through magenta, and Narkowicz's over-saturates brights), Tony McMapface (a lookup table with no HDR variant), and Reinhard and Hable (saturated colours never reach white).
With 4x MSAA every pixel has four samples, and the hardware averages them into one value: the resolve. ADR 0019 keeps those samples in fast on-chip memory and never writes them out (the attachment is memoryless), so the hardware average is the only resolve on offer.
Averaging is right for light but wrong for what you see. Say a thin wire of brightness 100 covers one of a pixel's four samples and the background is 0.05. The average is 25: still far above white, so the edge pixel turns fully white, as if the wire covered all of it. Bright sub-pixel detail becomes thick white stair steps.
The fix (Karis, 2013) is reversible weighting. Each shader in the lit pass writes c / (1 + luminance), which keeps every sample's luminance under 1, though a saturated colour's channels can stay above 1: pure blue of 100 is stored as 12.2. The hardware averages those, and the final pass undoes the squeeze with c / (1 − luminance). The result behaves like averaging after tonemapping: the same pixel resolves to 0.40.
Full frame, 2×
Zoom, 4×The options for R:
Rgba16Float with the weighting. It measured 0.009 ms extra in the lit pass and 0.018 ms in the final pass, and keeps ADR 0019's memoryless 4x. Two costs: sub-pixel highlights lose energy, and M6's transparent passes would blend in the weighted space, which a fog line records. fp16 is required, since the inverse of the largest value under 1 caps at 2047 in fp16 but at 127 in 11-bit floats.Rgba16Float with the plain box resolve. Physically averaged, stair-stepped at bright edges.An 8-bit surface has 256 levels per channel. A smooth, dim gradient covers only a few of them, so it shows as bands. Dither adds a little noise before rounding, so neighbouring pixels round up or down in the right proportion and the average comes out right. The eye sees fine grain instead of bands.
The details matter. Triangular noise spanning 2 steps peak to peak is the least noise that keeps the error's strength the same whatever the signal, so the grain does not itself vary with brightness (Lipshitz et al.). It must be added after the sRGB encode, in 8-bit steps: added in linear light it is 12.92 times too strong near black. And the pattern must stay fixed on screen. ADR 0008 rules out averaging frames, and a fixed pattern keeps Goldens exact.
Blue noise is noise without low frequencies: its dots are spread evenly, so it reads as finer grain than white noise. Ordered (Bayer) patterns are cheap but show a regular grid. The recommended mask is Christoph Peters's free 64 × 64 blue-noise texture (CC0), the same one used below.
The probe's gradient through PBR Neutral, levels stretched about 8×The options for D: D1 (recommended), static triangular dither from Peters's mask, added in the encoded domain through a non-sRGB view of the surface; the mask joins assets/manifest.json. An integer hash gives white noise without adding an asset. D2: none, and dark gradients band. The probe measured the dither at a few hundredths of a millisecond on top of the operator.
A colour space is three things: which red, green and blue its primaries are, which white they balance to, and a transfer function between stored numbers and light. sRGB uses the Rec. 709 primaries, the D65 white, and a curve that spends more codes on dark tones, where eyes are sensitive.
Shading and blending must happen in linear light, where numbers add like light does. Encoding happens once, at the end. The …Srgb texture formats do the encode in hardware on write, which is why the dither step must write to a format that does not: it encodes itself, adds noise in 8-bit steps, then writes the codes as they are. The survey proposed a non-sRGB view of the sRGB surface; since #255 the final pass writes a Bgra8Unorm or Rgba8Unorm surface instead, because that view needs a wgpu downlevel flag outside the Capability floor.
SDR output is relative: 1.0 is the display's white, whatever its brightness. HDR output adds room above that white, and each platform names it differently:
wgpu 30.0.1 can already configure all of these, through SurfaceConfiguration::color_space, and reports the display's headroom. It writes no HDR10 metadata. The M4 Pro's built-in display is an XDR panel, so HDR output could be tried on the reference machine. But an HDR path needs an operator that takes a display peak, as GT7 does, and its own testing.
The options for H: H1 (recommended), SDR only in M3, with HDR output a fog line. H2: EDR on Metal now, a second output path, with Goldens staying SDR.
Bright light seems to glow, because lenses and eyes scatter a little of it into its surroundings. A display cannot show a light brighter than white, so bloom adds that glow to say "brighter than white". The usual form (Call of Duty: Advanced Warfare, 2014) blurs the image down a chain of ever smaller copies and adds the result back. It uses no brightness threshold: the eye's scattering has none, and a threshold breaks energy conservation. The probe measured that chain at 0.27 ms at 1440p.
The options for B: B1 (recommended), none in M3, with the measured cost kept in a fog line. #200's size ceiling, about 400 changed lines, would make bloom a ticket of its own anyway. B2, the Call of Duty chain, and B3, physically based glare by FFT convolution, are later polish.
PR #219 put these seven questions to the project's owner, who took every recommendation on 2026-10-20. The answers are recorded on the project's map, U1 became the light-units ADR, ADR 0023, before the PR merged, and #200 carried out the rest, with one change to D1: the final pass writes an 8-bit UNORM surface in place of a non-sRGB view (chapter 8). Each card gives PR #219's recommendation first, which is also what was chosen.
Lux, candela and cd/m², exposure as EV100, in a new ADR. glTF lights import unchanged and light ratios are real.
Or U2, relative units: fewer stops, a scale at glTF import, hard to convert later.
Chapter 2Exact every frame, Goldens exact, free. Histogram adaptation becomes a fog line, revived by the first scene that crosses interiors and sun or changes time of day.
Or E2, histogram adaptation (0.27 ms), or E3, local tone mapping.
Chapter 3Linear mid-tones, hue held, white near the peak, and one path for SDR and any HDR peak. 0.29 ms, bakeable into a lookup table later.
Or AgX, softer and desaturated, or PBR Neutral, most faithful base colours. Judge them on the probe's images.
Chapter 5The only fix for bright-edge aliasing that keeps 4x MSAA memoryless, for 0.009 ms plus 0.018 ms. Sub-pixel highlights lose energy.
Or R1, the box resolve and its white stair steps, or R3, stored samples (118 MB, reopens ADR 0019).
Chapter 6Added after the sRGB encode through a non-sRGB view; the mask joins the asset manifest. A hash gives white noise without an asset.
Or D2, no dither: dark gradients band.
Chapter 7A fog line keeps the measured 0.27 ms chain for later; it would be a ticket of its own.
Or B2, the Call of Duty mip chain now, or B3, FFT glare.
Chapter 9HDR output becomes a fog line: wgpu 30 can already configure EDR and PQ, and the M4 Pro has an XDR display.
Or H2, EDR on Metal now, as a second output path.
Chapter 8Grouped by topic. The survey itself checked every external link below except Wyman, Sloan and Shirley's paper, which this guide adds for its chromaticity diagram; page numbers point at the parts it relies on.