Expand description
HDR → SDR tone mapping curves in safe Rust.
zentone is a library of tone-mapping curves — the math that compresses
HDR luminance into an SDR display range. It is not a full color
management pipeline: linearization, primary conversion, OETF encoding, and
ICC handling live in linear-srgb and
zenpixels-convert. Use zentone
when you have linear-light HDR samples and need to choose a curve.
no_std + alloc, zero allocation in hot paths, SIMD-accelerated on
x86-64 (AVX2+FMA) with scalar fallback everywhere else.
§API tiers
The public surface splits into three tiers; pick by workload, not by name recognition.
- Hot path — strip / row SIMD. Use these for any non-trivial
workload (a row, a strip, a whole image). Inputs are packed
&[[f32; 3]]/&[[f32; 4]]/&[[u8; 3]]slices; kernels dispatch througharchmageat runtime. Examples:pipeline::tonemap_pq_row_simd,pipeline::tonemap_pq_rgba_row_simd,pipeline::tonemap_hlg_row_simd,pipeline::tonemap_pq_to_srgb8_row_simd,gamut::apply_matrix_row_simd,gamut::soft_clip_row_simd,hlg::hlg_ootf_row_simd, and theToneMap::map_strip_simdtrait method (with SIMD overrides onBt2408Tonemapper,Bt2446A,Bt2446B,Bt2446C, andCompiledFilmicSpline). - Reference / per-pixel.
ToneMap::map_rgb,gamut::apply_matrix,gamut::soft_clip,hlg::hlg_ootf, and the named-curve scalar functions incurves(reinhard_simple,bt2390_tonemap,narkowicz_aces, etc.). These are the parity surface — suitable for one-off use, doctests, and cross-checks against external reference implementations. Calls inside an inner loop should usually go through the row form instead. - Experimental. Behind the
experimentalfeature, semver-unstable. Covers the streaming tonemapper, the adaptive LUT fitter, and the DNGProfileToneCurve. APIs may change without semver bumps until stabilized.
As of 0.2.0, the pipeline ships SIMD strip-form APIs only — the old
&[f32] + channels: u8 forms are not present. See CHANGELOG.md
for the removal record.
§What’s in the box
Curves come in four families. Each implements the ToneMap trait;
pick by use case, not by name.
- Classical, stateless —
ToneMapCurve: simple Reinhard (x/(1+x)), extended Reinhard with white point, Reinhard-Jodie, tuned Reinhard (display-aware nits), Narkowicz, Hable, ACES AP1, AgX (Blender) withDefault/Punchy/Goldenlooks, BT.2390 EETF, andClamp. No state, no allocation, SIMD-accelerated row paths. - ITU broadcast standards —
Bt2408Tonemapper(BT.2408 Annex 5 PQ-domain Hermite spline, YRGB or MaxRGB),Bt2446A/Bt2446B/Bt2446C(BT.2446 Methods A, B, C). Constructed once with(content_max_nits, display_max_nits). - Filmic spline —
CompiledFilmicSpline/FilmicSplineConfig: darktable / Blender-style rational spline with toe / linear / shoulder regions and per-pixel highlight desaturation. Heavy parameter surface for calibrated workflows. - Gain map splitter (
gainmap) — round-trippable HDR ↔ (SDR, log2 gain) splitter for ISO 21496-1 / Apple Ultra HDR encoders, plus companionLumaToneMapcurve impls (Bt2408Yrgb,ExtendedReinhardLuma,HableFilmic) and PQ/HLG row helpers. - Experimental (behind the
experimentalfeature) — adaptive LUT fitting, single-pass streaming tonemap with local adaptation, DNG ProfileToneCurve.
§Quick start
Every tonemapper takes a linear-light RGB triple and returns linear SDR RGB. Wire format decoding (PQ / HLG / sRGB) and primary conversion happen before / after.
use zentone::{Bt2446C, ToneMap};
// 1000 cd/m² HDR content → 203 cd/m² SDR (HDR Reference White).
// Input scale: 1.0 = hdr_peak_nits; output scale: 1.0 = sdr_peak_nits.
let curve = Bt2446C::new(1000.0, 203.0);
let sdr = curve.map_rgb([2.0, 1.0, 0.5]);
assert!(sdr.iter().all(|&c| c.is_finite() && c >= 0.0));For an entire row, use map_row (in place) or
map_into (separate dst). Both dispatch on
channels (3 = RGB, 4 = RGBA, alpha preserved):
use zentone::{ToneMap, ToneMapCurve};
let mut row = [0.5_f32, 1.2, 0.3, 0.8, 2.0, 0.1];
ToneMapCurve::Narkowicz.map_row(&mut row, 3);For a fused PQ→tone-map→sRGB-gamut pipeline on a packed strip (the canonical hot-path shape):
use zentone::{Bt2446C, TonemapScratch, pipeline::tonemap_pq_row_simd};
// 1024 PQ-encoded BT.2020 RGB pixels — 0.58 ≈ HDR Reference White.
let pq = vec![[0.58_f32, 0.58, 0.58]; 1024];
let mut out = vec![[0.0_f32; 3]; 1024];
let curve = Bt2446C::new(1000.0, 203.0);
let mut scratch = TonemapScratch::new();
tonemap_pq_row_simd(&mut scratch, &pq, &mut out, &curve);§Choosing a curve
| Need | Pick |
|---|---|
| “Just give me something cheap and decent” | ToneMapCurve::Narkowicz or HableFilmic |
| Game engine / shader port | ToneMapCurve::AcesAp1 or Agx |
| Broadcast-grade HDR10 / HLG → SDR with display peak nits | Bt2408Tonemapper (PQ-domain) or Bt2446A |
| Live HLG → SDR, conservative on clipped highlights | Bt2446B |
| HDR → SDR with mathematical inverse (round-trip / detection) | Bt2446C |
| Calibrated photo workflow with toe / shoulder control | CompiledFilmicSpline |
| ISO 21496-1 / Ultra HDR gain map encoder | LumaGainMapSplitter |
| Re-derive a curve from an HDR/SDR reference pair | experimental::AdaptiveTonemapper |
Curves that need RGB→Y weights take them at construction time via
LUMA_BT709, LUMA_BT2020, or LUMA_P3. Pick the one that
matches the input primaries — passing BT.709 weights for BT.2020 input
over-desaturates greens.
§Tone mapping vs gain map splitting
Two adjacent but distinct contracts live in this crate.
- Tone mapping (
ToneMaptrait,pipelinestrip kernels): one-way, lossy. Takes linear-light HDR RGB and returns linear-light SDR RGB. Information above SDR peak is compressed and not recoverable. Used to render HDR content on SDR displays. Seepipeline::tonemap_pq_row_simd,pipeline::tonemap_hlg_row_simd,ToneMap::map_strip_simd. - Gain map splitting (
LumaToneMaptrait,LumaGainMapSplitteringainmap): two-way, reversible. Takes linear-light HDR RGB and returns(SDR RGB, log2 gain)per pixel per the ISO 21496-1 / Adobe Gain Map / Apple Ultra HDR contract. Apply the gain map back to the SDR base to recover the HDR within float precision (modulo SDR clipping; tracked inSplitStats::clipped_sdr_pixels).
Most curves implement both traits — Bt2408Yrgb, Bt2446A/B/C,
ExtendedReinhardLuma, CompiledFilmicSpline, and HableFilmic
all derive LumaToneMap from their luma response. The splitter’s
LumaToneMap is luma-only because chromaticity preservation
requires applying a scalar curve to luma and then rescaling the RGB
triple — per-channel and matrix-based ToneMap curves do not have a
coherent luma-only interpretation, so they are intentionally not
adapted.
See src/gainmap.rs for the splitter; see
src/pipeline.rs for the tone-map row kernels.
§Utility modules
gamut— 6 gamut conversion matrices (BT.709 ↔ BT.2020 ↔ Display P3) plus a hue-preservingsoft_clip.hlg— HLG system gamma, OOTF, inverse OOTF, full HLG → display.sdr_hdr— reference-white scaling (100 ↔ 203 nits), OOTF gamma adjustments per BT.2408 §5.1.pipeline— fused linearization + tone map + gamut conversion + soft clip via SIMD strip-form entry pointstonemap_pq_row_simd/tonemap_pq_to_srgb8_row_simd/tonemap_hlg_row_simd(plus RGBA variants).
§Experimental (experimental feature)
Behind a feature flag because the APIs are still in flux:
experimental::AdaptiveTonemapper— fits a LUT from an HDR/SDR pair.experimental::StreamingTonemapper— single-pass spatially-local tonemap with bounded-memory pull API.experimental::ProfileToneCurve— DNG camera-profile tone curve; per-channel or luminance-preserving views viaToneMap.experimental::detect::detect_standard— identifies which standard curve was applied to a fitted LUT.
Lightly tested; API may change without semver bumps until stabilized.
See the experimental module docs when the feature is enabled.
Re-exports§
pub use curves::AgxLook;pub use curves::ToneMapCurve;pub use gainmap::Bt2408Yrgb;pub use gainmap::ExtendedReinhardLuma;pub use gainmap::HableFilmic;pub use gainmap::LumaFn;pub use gainmap::LumaGainMapSplitter;pub use gainmap::LumaToneMap;pub use gainmap::SplitConfig;pub use gainmap::SplitStats;
Modules§
- curves
- Classical tone mapping curves and unified
ToneMapCurvedispatch. - gainmap
- Luma gain map splitter — round-trippable HDR ↔ (SDR, log2 gain).
- gamut
- Color gamut conversion and soft clipping.
- hlg
- HLG system gamma, OOTF, and display adaptation.
- pipeline
- HDR → SDR pipeline helpers.
- sdr_hdr
- Display-referred SDR↔HDR signal-level conversion utilities.
Structs§
- Bt2408
Tonemapper - BT.2408 tone mapper operating in PQ perceptual domain.
- Bt2446A
- BT.2446 Method A tonemapper.
- Bt2446B
- BT.2446 Method B simplified HLG tonemapper.
- Bt2446C
- BT.2446 Method C tonemapper.
- Compiled
Filmic Spline - Compiled filmic spline (precomputed from
FilmicSplineConfig). - Filmic
Spline Config - Filmic spline configuration parameters.
- Tonemap
Scratch - Reusable scratch buffers + chunk-size policy for the SIMD tone-map pipelines.
Enums§
Constants§
- LUMA_
BT709 - BT.709 / sRGB luminance coefficients
[0.2126, 0.7152, 0.0722]. - LUMA_
BT2020 - BT.2020 luminance coefficients
[0.2627, 0.6780, 0.0593]. - LUMA_P3
- Display-P3 / DCI-P3 luminance coefficients
[0.2289746, 0.6917385, 0.0792869].
Traits§
- ToneMap
- A linear-light RGB tonemapper.
Type Aliases§
- Result
- Result alias for zentone.