Epic: M×N HDR pipeline — gain-map/SDR → true HDR (JXL/AVIF/PNG) via an absolute-luminance hub
@lilith is already working on this.
Since Jun 14, 2026.
- Dominant language
- Rust
- Stars
- 1
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Design of record for converting gain-map / SDR sources into true HDR (JXL / AVIF / PNG-3) via an absolute-luminance hub. Follows the prior-art survey (docs/hdr-design-survey-2026-06-13.md) and a verified 4-leg audit of the current matrix.
Headline: the HDR science is done — the gaps are four wiring points
A read across the stack (file:line below) found the hard parts already built and mutually consistent:
- Luminance anchor is universal:
1.0 = 203 nits(BT.2408 diffuse white), SDR primaries — identical acrossultrahdr-core(apply.rs:124,types.rs:93), heic, zenavif, and zenjxl's reconstruct path.peak = 203 × capacity_max. - Transfer ops: all present, SIMD — linear↔{sRGB,709,PQ,HLG,γ2.2}, u8/u16/f32 (
zenpixels-convert/src/convert.rs:90-142). - Gamut ops: present and general — BT.709/P3/BT.2020/AdobeRGB, Bradford-adapted, chromaticity-derived 3×3 (
registry.rs:203-226). SDR→BT.2020 works today. - Tone-mapping: exists in
zentone(BT.2390 EETF, BT.2446 A/B/C, BT.2408 Hermite, ACES/Hable/AgX, HLG-OOTF, gamut soft-clip, gain-map split — all SIMD).zenpixels-convertdoes not depend on it yet. - JXL HDR engine (
jxl-encoder): fully capable — float PQ/HLG input (api.rs:425-458),intensity_targetgenuinely plumbed PQ→10000/HLG→1000 → metadata → request (api.rs:9918-9984), nativejhgmgain-map writer (hdr/from_sdr.rs). - M×N orchestrator (
zenpipe/zencodecs):transcode()already does decode→adapt→re-encode withMetadataroundtrip (transcode.rs:197). - Carrier types: no duplication —
zencodecre-exports zenpixels'Cicp/ContentLightLevel/MasteringDisplay.Metadatais#[non_exhaustive](metadata.rs:46).
The four gaps (all plumbing, not new math)
zencodecs::transcodedoes pixel-format adaptation only — no transfer/primaries/tonemap, and it drops the gain map (standing TODOtranscode.rs:168).zenpixels-converthas no anchored tonemap step; PQ domain is hard-pinned to 1.0=10000 (every caller hand-prescales, e.g.quantize_to).- The anchor isn't in any carrier — neither
zencodec::MetadatanorColorContexthas aDiffuseWhitefield. - zenjxl's encode wrapper exposes only sRGB/linear descriptors and never forwards
intensity_target— so via the trait you can decode HDR but not encode true-HDR JXL, though the engine can.
Confirmed architecture decisions
- Anchor home:
ColorContext+zencodec::Metadata(both, additive). Adddiffuse_white: Option<DiffuseWhite>to the existingPixelBuffer.color: Arc<ColorContext>sidecar (propagates through every clone already) AND toMetadata/SourceColor(survives the codec boundary, feedsintensity_target). No newPixelDescriptorfield (it'sCopy/≤8B/frozen presets), no wrapper type. Caveat:ColorContext: Eqandf32isn'tEq— bit-compare newtype or drop the derive (tolerated 0.2.x). - Gain-map → JXL: both paths, caller-selected, default reconstruct. Default = apply gain map → single linear HDR → native float/PQ JXL with
intensity_target. Knob = preserve as nativejhgm(SDR base + gain map) for backward-compat.
Slices (each ends in a demoable artifact)
- S1 — Carrier:
diffuse_whiteontoColorContext(zenpixels) +Metadata/SourceColor(zencodec); populate it in the gain-map decode adapters (they already compute203 × capacity_max). Demo: decode an UltraHDR JPEG, assert buffer + metadata carry the anchor. - S2 — Anchored hub ops: make PQ/HLG
ConvertSteps read the anchor fromColorContext(scale byanchor/10000internally, soconvert_bufferneeds no manual pre-scale); depend onzentone(verify no dep cycle) for EETF/BT.2446/HLG-OOTF/soft-clip; add a peak-adaptation step taking(content_nits, display_nits).quantize_todissolves intoconvert_buffer; migrate~/work/hdr-corpus-convertto it. Demo:convert_bufferlinear@203→PQ16 matches the oldencode_pq16±1; PQ→HLG peak-adapt round-trips. - S3 — JXL HDR encode wrapper: map PQ/HLG/float descriptors in zenjxl's encode wrapper; forward
intensity_targetfrom the anchor/peak; expose thejhgmpath behind the knob. Demo: UltraHDR JPEG → true-HDR JXL; decode back, assertintensity_target+ CICP. - S4 — M×N orchestration: insert the HDR hub step in
zencodecs::transcode(reconstruct-or-carry gain maps per the knob, gamut + tonemap to the target's primaries/peak, set the anchor); wire the gain-map re-embed (the TODO). Demo:zcimgconverts gain-map JPEG → JXL HDR and → AVIF HDR with correct signaling.
Relationship to PR #41
#41 lands the DiffuseWhite type + ContentLightLevel::measure + the HdrMetadata deprecation — the foundation S1 builds on. quantize_to stays the pub(crate) tested oracle until S2's anchored convert_buffer supersedes it (then deprecate + migrate hdr-corpus-convert). S1 depends on #41 landing first.
Cross-repo: zenpixels, zencodec, zenpixels-convert, zentone, zenjxl, zenpipe/zencodecs.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.