← PIXL EngineDeep dive · HDR

The HDR pipeline

One master in linear light, with white at 203 cd/m². Every source enters it the same way, its peak is measured rather than assumed, and one flag decides whether the output carries headroom. PIXL Engine states every number it used, and refuses whatever it would have to guess.

One master

The engine has several HDR policies, and each one takes its numbers from the caller. Tone mapping a PQ file down to SDR needs an operator, a mode, a source peak and a target white; placing an SDR file into PQ needs an operator and a peak. None of them has a default. For hosts that want the engine’s own answer, there is one built-in path,ColorPolicy::Master, and every number it uses is fixed in the engine and stated back in the report’sMasterReport.

The master is linear light in PixlRGB, the engine’s working space, scaled so that1.0 is 203 cd/m². That is the graphics white of ITU-R BT.2408, the level a reference HDR display shows diffuse white at, so SDR content placed in the master sits where an HDR display expects it. Everything above 1.0 is headroom, carried unclipped through every stage: the grade, framing, the resize and overlays. Nothing is clipped until the output decides what it can hold.

Luminances in the masterA log scale of luminance from 100 to 10 000 cd/m². White sits at 203; the ceiling can be set anywhere from 203 to 10 000; HLG's nominal display and the effects scale are at 1000; PQ's top is 10 000, 5.6 stops above white.Luminances in the master, cd/m² (log scale)0 stops+1+2+3+4+5the ceiling: any value from 203 to 10 000, or the measured peak100100010 000white1.0 in the masterCR2 lifted a stopMaxCLL 368 · +0.9 stan iPhone HEIC’s peak575.7, measured · +1.5 stHLG display · effects scale1000 · +2.3 stPQ’s top10 000 · +5.6 st
White is 203 cd/m² (BT.2408). Stops are counted from white. The CR2 is the engine’s RAW fixture developed inScene and lifted a stop; the iPhone file is IMG_1318 from the test corpus, whose measured peak is 575.7 cd/m². HLG enters at a 1000 cd/m² display (BT.2100), and effects read tones on a scale to 1000 cd/m².

Every source in

Every source enters the master the same way, and each way in is a standard’s, not a guess:

  • SDR (JPEG, PNG, TIFF, WebP, AVIF, JPEG XL): through its own ICC profile or CICP code points into linear light, its white at 1.0.
  • PQ: through the SMPTE ST 2084 EOTF to absolute luminance, then divided by 203.
  • HLG: through the inverse OETF and the OOTF for a 1000 cd/m² display, as BT.2100 specifies.
  • Gain-map files: ISO 21496-1 (a HEIF tmap item or a JPEG’s APP2), Adobe’shdrgm XMP, and Apple’s legacy maps in HEIC and JPEG. The map is applied at the file’s own alternate headroom, so the base’s white is 1.0. A file whose map states no headroom is refused, not given one.
  • A RAW developed in Scene: the engine’s own develop delivers linear PixlRGB in 32-bit floats, unclamped, highlights above sensor white included. It enters the master as it stands.

Grey stays one channel throughout. The order of the stages is fixed: decode, gain map, orientation, lens correction, retouching, the grade, framing, resize, output sharpening and overlays, then the egress. Every stage sees headroom whatever the output will be.

The peak and the ceiling

HDR metadata in the wild rarely says what the pixels hold: most PQ files state no peak at all. So the master measures its own. At the egress, after framing, the resize and overlays, the peak is the 99.9th percentile of luminance, by nearest rank. The histogram has 1/1024-stop bins from 2⁻⁸ of white to the top, with integer counts, so the peak is the same at any thread count. Fully transparent pixels are left out. A peak in the first bin above white counts as white, so a white that arrives a rounding error high rolls nothing off.

A host can state the peak instead. A preview passes its measured figure back, so a region of the picture renders exactly as the whole does. A region asked to measure its own peak is refused, because its peak would differ from the picture’s.

With headroom on, the master is held to a ceiling: the measured peak, or any level from 203 to 10 000 cd/m². A peak above the ceiling rolls off with BT.2390’s Hermite curve, using the largest knee for which the curve stays monotone; a peak below it is clipped at it. The report states the peak, the limit and how many samples the limit changed.

The display tone map

Without headroom, everything above white has to fit under it. The first version, v0, was BT.2390 applied to each channel separately. Rolling each channel off on its own turns hue: a saturated highlight’s strongest channel flattens first, so orange drifts toward yellow and blue toward cyan. Version 1 rolled off luminance alone; that kept hue, but near white a colour of white’s luminance can only be white, so it lost chroma.

Version 2, the one the master runs, does two things:

  1. The roll-off runs on n = Y + 0.5 × (max(c, Y) − Y), half-way between luminance and the largest channel, and every channel is scaled by the same factor. A saturated highlight keeps its colour by ending a little below white; a grey one rolls off exactly as on luminance.
  2. The path to white. Near the ceiling, bright saturated colours are drawn gently toward the boundary of what the output can show, along their own hue line in ICtCp at their own intensity. A straight line to white in linear light turns hue as the eye sees it; ICtCp keeps it. The boundary is found by the Illinois method in about six evaluations a colour.
Hue shift of saturated highlights, by tone-map versionMedian hue shift in ICtCp across the corpus files with saturated highlights: version 0 between 4.3 and 19.8 degrees (p95 9.5 to 29.3), version 1 between 0.05 and 0.11, version 2 between 0.07 and 0.18 (p95 at most 0.28).Hue shift of saturated highlights, by tone-map version051015202530hue shift in ICtCp, degrees (range across files)v0BT.2390 per channelmedian4.3–19.8°p959.5–29.3°v1luminance alonemedian0.05–0.11°v2mix + path to whitemedian0.07–0.18°p95≤ 0.28°
Each bar is the range, across the iPhone corpus files with saturated highlights, of that file’s median (or 95th-percentile) hue shift in ICtCp. On the engine’s photograph lifted a stop: v0 3.0°, v1 0.03°, v2 0.04°.tests/tone_map_hue.rs.
Chroma kept in those highlightsMedian share of chroma kept in saturated highlights across the corpus files: version 0 0.19 to 0.65, version 1 0.13 to 0.48, version 2 0.50 to 0.83.Chroma kept in those highlights00.250.50.751median share of chroma kept (range across files)v00.19–0.65v10.13–0.48v20.50–0.83
The same files. On the photograph lifted a stop, v2 keeps 0.98 of the chroma.

A pixel below the knee with every channel inside keeps its exact bits, and with nothing above white no tone map runs at all. An unedited photograph through the master is its conversion to Display P3, sample for sample. On the photograph lifted a stop, the tone map leaves under a tenth of a plain clip’s pixels at pure white, keeps 7.5 times as many distinct colours where the clip flattens a highlight, and leaves every pixel below a quarter of white exactly as the clip does. The path to white costs about 1.3 s on a 24 MP frame at 8 threads, about 0.15 s at a 2048-pixel preview.

Two crops of the same forest road lifted one stop. On the left the sky and the brightest leaves clip to flat white; on the right the display tone map keeps the sky's tone and the leaves' colour.
The 24 MP road photograph lifted one stop, headroom off, in Display P3. Left: converted and clipped at white. Right: PIXL's display tone map v2, from the measured peak (381.7 cd/m²) to white. Rendered by the engine; a crop at a quarter size.

Colours past the primaries

An edit can push colours outside the output’s gamut: Display P3 without headroom, Rec.2020 for PQ. Version 2 of the gamut compression moves only those colours. Along each pixel’s ICtCp hue line, it finds where the line leaves the target’s primaries. The 99.9th percentile of how far past the boundary the picture reaches sets a soft knee, and colours above the knee are eased onto the boundary with hue kept. A picture that already fits moves no pixel and keeps its bytes. One step per output, never chained.

Measured on the P3 photograph at a quarter size with saturation doubled (reach 1.47, knee 0.82): over the 71 000 pixels past P3, the hue shift is 0.00° median and 0.01° at the 95th percentile, against a plain clip’s 0.23° and 0.70°. Bright colours inside P3’s sides keep the clip’s bits. A region rendered with the preview’s stated peak and reach is the full render, cut.

One flag, every sink

The master has one switch, headroom. What each sink receives follows from what it can hold:

SinkHeadroom offHeadroom on
JPEG XL, PNGDisplay P3, display tone mapRec.2100 PQ at the encode’s depth (8-bit PQ refused)
AVIF, JPEGDisplay P3, display tone mapthe same base, plus a full-size gain map
Float TIFF, raw floatsDisplay P3, display tone mapthe held master, unrounded, linear PixlRGB with its ICC profile
WebP, integer TIFFDisplay P3, display tone mapthe base, with a note in the report

With headroom on, a gain-map file’s base is the headroom-off rendition. Flipping the flag changes only the headroom; an SDR viewer sees the same picture either way. Every HDR output measures MaxCLL and MaxFALL on the pixels it produced and writes them where the format has a place: JPEG XL’s intensity_target, PNG’scLLI, HEIF’s clli. A source’s own light-level metadata never survives a transform: it described other pixels.

  • 4 msan unedited P3 photo, proven exact at plan time and copied
  • 0.88 slifted a stop, into 16-bit PQ JPEG XL
  • 2.98 slifted a stop, into JPEG with its gain map

The colour stage on the 24 MP JPEG, 8 threads, i7-14700F.

Gain maps, full size

A gain map is computed where both renditions exist: the master after its ceiling, and the SDR base after it has been rounded. Gains are taken per channel in the base’s primaries (one channel for grey), with gamma 1 and offsets of 1/64. Each channel’s range is what its pixels need over the colours the base can hold.

The map is written at full size. The base is tone mapped pixel by pixel, so the gain changes as sharply as the picture’s edges. At half size, a reader’s rebuild of even a lossless file missed the held master by up to 1.3 stops at fine detail. At full size the rebuild is within 0.04 stops, and files are 23–39% larger.

The rebuild never passes the ceiling. Lossy coding moves both base and map: without a cap, a white base’s map code pushed to the top of a widened range rebuilt 0.49 stops past the ceiling. A safety margin of that size would have dimmed every highlight by 30%. Instead, each channel’s top gain is capped at the gain that takes a white base to the ceiling, so no code a lossy codec produces can rebuild above it.

Measured on four pictures, JPEG at quality 50–100 and AVIF Iq at quality 40–100 and lossless: rebuilt MaxCLL within 0.4 cd/m² of the stated ceiling, no pixel past it by more than 1%. libultrahdr 2.0.2 and libavif 1.3.0 rebuild the files the same way.

Read back against the PQ export of the same master, in 10-bit PQ codes (mean / p99): a lossless AVIF is within 0.13 / 0.38, a JPEG at quality 100, 4:4:4, within 0.93 / 5.07, and MaxCLL within 0.3%. The JPEG is an UltraHDR file (ISO 21496-1 with hdrgm XMP and an MPF index); the AVIF carries an ISO tmap item.

iPhone HDR, read as Apple reads it

An iPhone 17 writes an ISO 21496-1 map beside Apple’s older one; an iPhone 13 Pro writes only the older one. When both are present the engine reads the ISO map. The older map is applied the way Core Image applies it. The headroom comes from MakerNote tags 33 and 48 by Apple’s published formula, and the gain is1 + (min(H, h) − 1) · map^2.2 — a pure 2.2 gamma on the map, even where it is tagged linear, rather than the Rec.709 curve Apple’s document gives. The map’s headroom tag is a ratio, not stops; the file’s three statements of its headroom agree only read that way.

The test corpus is eleven HEICs from two iPhones: iOS 17.5 to 27.0, 12 and 48 MP, ultra wide, tele, the front camera (rotated and mirrored), a Live Photo and an edited crop. Against Apple’s own renders on macOS 27.0, within the bounds set before the comparison:

MeasureBoundAll eleven files
SDR base, largest difference (8-bit codes)20.69–1.27
HDR rendition, luminance mean (10-bit PQ codes)1.00.31–0.85
HDR rendition, luminance p99 (10-bit PQ codes)4.01.17–3.80

HDR is compared over the pixels where Apple’s render has no negative channel. Core Image keeps a float YCbCr conversion’s out-of-range values; the engine clips at the integer decode. On files carrying both maps, the ISO and legacy readings agree to a median of 0.032 stops at most. The base matches partly because the engine also upsamples HEIC chroma as Core Image does (see Codecs).

Effects above white

Grain, clarity, the colour wheels and a vignette’s highlight protection all weigh their effect by tone. In SDR the tone is clamped at white, so a highlight lifted into headroom would lose its grain the moment it passed 1.0. In an HDR pipeline with headroom the tone is read on PQ’s perceptual scale, up to 1000 cd/m² in the master (BT.2408’s reference display), where white sits at 0.77. The weight is a function of the pixel alone, so a region is still the full render cut, and the headroom flag never changes it.

Grain and clarity weight by toneThe weight grain and clarity give a pixel, by its tone as a multiple of white: in an SDR stage 0.36, 0.99, 0.78, 0, 0, 0 at 1%, 18%, 50%, white, twice and four times white; in the master 0.75, 0.99, 0.87, 0.70, 0.45, 0.12.Grain and clarity weight by tone00.250.50.7511%18%50%white2×4×tone, multiple of white (log)weightSDR stagethe master
The tonal weight of grain and clarity at six tones, in an sRGB stage of an SDR pipeline and in the master. Lines join the six stated points. Measured: grain in an sRGB stage of the master on the PQ fixture lifted 1.5 stops raises the pixels between 1.2× and 2× white by 3.93 ten-bit PQ codes, where before it added none.

The master cache

A host renders an edited photo many times, and everything before the grade — the decode, a gain map applied, orientation, enhancement, the lens correction — is the same every time. On a RAW or a 48 MP HEIC it is also the expensive part. The master cache keeps the master at that point, the seam, as a lossless float JPEG XL with apixl box, and a render from it is the render from the original, byte for byte.

The box records the engine version, the source’s SHA-256, and a digest of every request field that acts before the seam: the RAW develop, enhancement, the lens, orientation, the channel count. A render whose fields differ, or a cache made by another engine version, is refused with StaleCache naming the field, and the host makes the cache again. A plain JPEG XL reader sees the master’s floats as linear PixlRGB, alpha included.

SourceCacheEncodeDecode
24 MP road JPEG (288 MB of floats)187 MB0.64 s0.13 s
48 MP HEIC (585 MB of floats)346 MB1.29 s0.28 s

Effort 1, 8 threads. A 2048-pixel PQ preview of the road photograph takes 0.49 s from the cache against 0.68 s from the original; a RAW or a 48 MP HEIC also saves its decode. Tested byte for byte, both ways, on an RGBA PNG through a lens, the PQ HEIC, an UltraHDR JPEG, an iPhone HEIC with Apple’s map, and a CR2 in Scene, each rendered as a graded, straightened, cropped, resized PQ PNG, the master’s floats, a region, an SDR JPEG and a gain-map JPEG.

Verification

The claims on this page are held by tests in the engine’s repository, and by other readers:

  • Identity where nothing happens. An unedited photograph through the master is its conversion to Display P3, sample for sample, and the 24 MP JPEG at quality 90 through the command line, byte for byte.
  • Headroom into PQ equals a plain linear expansion at 203 cd/m² on over 99% of pixels (all below the ceiling in every channel); MaxCLL is the ceiling within 0.5 cd/m², and a ceiling of 400 is never passed.
  • Every source kind — JPEG, PNG, JPEG XL, HEIC, TIFF, WebP, PQ, HLG, gain-map files and a Scene CR2 — comes out as PQ JPEG XL and as a base with a three-channel map in AVIF and JPEG.
  • Threads. 1 thread and 7 give the same bytes on every HDR path.
  • SDR previews. An SDR rendition of an HDR master equals a separate tone map of its PQ export to one code at most, mean ≤ 0.005.
  • Other readers. libavif tone maps the engine’s gain maps as the engine does (MaxCLL 795 against 797 cd/m² at 2 stops). On a Mac, Preview shows the PQ and HLG files in HDR, and Core Image renders them as the engine reads them (0.017 stops apart for PQ, 0.055 for HLG) and applies the ISO gain maps as the engine does (0.017 stops).

Sources and method

Times are on an Intel i7-14700F under Linux at 8 threads unless stated. The Apple comparisons ran once on the owner’s MacBook Pro (M2 Pro, macOS 27.0); the renders are pinned by SHA-256 and the tests that read them run by hand.

  • The master: tests/master.rs — an_unedited_photo_is_its_conversion_to_display_p3, a_rebuilt_lossy_file_never_passes_the_ceiling, with_headroom_a_float_sink_gets_the_master_in_linear_pixlrgb, grain_carries_on_above_white_in_the_master.
  • Hue and chroma: tests/tone_map_hue.rs (ICtCp; the photograph held, the corpus by hand); the_boundary_is_where_a_bisection_puts_it.
  • Why hue is measured in ICtCp: in psychophysical hue matching on HDR stimuli across nine colour spaces, “the hue linearity of ICTCP color space was the best” (Wang, Wei and Qu, Optics Express 30(25):44896, 2022, 10.1364/OE.475433). That per-channel tone curves turn hue is also shown by Burke, Smith and Zink, “Color Volume and Hue-preservation in HDR Tone Mapping”, SMPTE Motion Imaging Journal 129(4), 2020, 10.5594/JMI.2020.2984046.
  • Gamut compression: tests/gamut.rs.
  • HDR paths and threads: tests/hdr.rs — every_hdr_path_is_byte_identical_whatever_the_thread_count; RAW into headroom: tests/raw_hdr.rs, a_scene_raw_graded_above_white_reaches_headroom_in_pq.
  • Gain maps: tests/gainmap.rs; the iPhone corpus and Apple’s renders: tests/heic.rs, tests/heic/pixl-heic-reference.swift.
  • The cache: tests/master_cache.rs; timings from the_cache_codec_measured and preview_times_at_2048_measured (by hand).
  • Interoperability: ci/avif-interop/ (libavif 1.3.0, libheif 1.23.5), and the device check on macOS 27.