Skip to content

feat: pure Rust UltraHDR implementation (RIIR)#6

Open
Enter-tainer wants to merge 65 commits into
masterfrom
thread/riir-libultrahdr-rs-rust
Open

feat: pure Rust UltraHDR implementation (RIIR)#6
Enter-tainer wants to merge 65 commits into
masterfrom
thread/riir-libultrahdr-rs-rust

Conversation

@Enter-tainer

Copy link
Copy Markdown
Owner

Summary

Rewrite the UltraHDR gain-map JPEG library in pure Rust, eliminating all C/C++ FFI dependencies (cmake, nasm, pkg-config, bindgen).

What's new

  • Encoder: HDR+SDR → UltraHDR JPEG encoding with gain map generation, ICC profiles, and MPF assembly. Supports HDR-only auto tone-map (API-0 scenario).
  • Decoder: UltraHDR JPEG → packed pixel buffer (RGBA8888, RGBA1010102, RGBAF16). Gain map metadata probing without full decode.
  • Color: sRGB/PQ/HLG transfer functions, BT.709 ↔ Display P3 ↔ BT.2100 gamut conversion, ICC profile parsing & generation.
  • Gain Map: ISO 21496-1 binary metadata encode/decode, XMP metadata read/write, bilinear interpolation sampling, Reinhard tone mapping.
  • JPEG: Segment parser, codec wrappers (jpeg-decoder + jpeg-encoder).
  • MPF: Multi-Picture Format segment generation.
  • CLI: ultrahdr-bake migrated to pure Rust API (bake, motion photo, auto-detect preserved).

Key properties

  • Zero unsafe code in the entire crate
  • 3 runtime dependencies only: jpeg-decoder, jpeg-encoder, roxmltree
  • 73 tests (71 unit + 2 integration roundtrip), all passing
  • Zero clippy warnings (-D warnings)
  • Zero cargo doc warnings
  • Builder pattern API for both Encoder and Decoder

Modules

ultrahdr/src/
├── encoder.rs       — Builder API, gain map generation, JPEG assembly
├── decoder.rs       — Gain map extraction, HDR reconstruction
├── gainmap/
│   ├── math.rs      — Gain computation, bilinear sampling, tone mapping
│   └── metadata.rs  — ISO 21496-1 + XMP metadata
├── color/
│   ├── mod.rs       — Color type with arithmetic ops
│   ├── gamut.rs     — Gamut conversion matrices
│   ├── transfer.rs  — sRGB/PQ/HLG OETF/inv-OETF
│   └── icc.rs       — ICC profile parse & generate
├── jpeg/
│   ├── mod.rs       — JPEG segment parser
│   ├── decode.rs    — jpeg-decoder wrapper
│   └── encode.rs    — jpeg-encoder wrapper
├── mpf.rs           — Multi-Picture Format
├── types.rs         — Core types (PixelFormat, ColorGamut, etc.)
└── error.rs         — Error types

Enter-tainer and others added 30 commits March 3, 2026 04:26
Define goals, constraints, crate structure, API design principles,
dependency choices, testing strategy, and success criteria for
rewriting libultrahdr from C++ FFI bindings to native Rust.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…ndling

- Replace ultrahdr-sys FFI wrapper with pure Rust skeleton
- Add Error enum with Display/std::error::Error impl
- Add core types: PixelFormat, ColorGamut, ColorTransfer, GainMapMetadata
- Create module structure: color/, gainmap/, jpeg/, mpf
- Update ultrahdr-bake to remove C++ feature flags

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…and gamut conversion

- Add sRGB/PQ/HLG transfer functions (OETF + inverse OETF)
- Add Color struct with arithmetic operators
- Add luminance calculations and gamut conversion matrices
- Add reference display peak luminance lookup

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Parse ICC rXYZ/gXYZ/bXYZ tags to extract chromaticity primaries and match
against known gamuts (BT.709, Display P3, BT.2100). Falls back to profile
description string matching. Includes s15Fixed16Number parsing and 3 tests.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add jpeg-decoder and jpeg-encoder dependencies. Implement:
- encode_rgb_to_jpeg / encode_grayscale_to_jpeg (Task 13)
- decode_jpeg returning JpegDecoded struct with pixels, dimensions, ICC (Task 12)
- parse_jpeg_segments walking JPEG markers to extract APP segments (Task 14)

All 32 tests pass (27 existing + 5 new).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…support

Add GainMapMetadataFrac struct for rational fraction representation and
implement binary encode/decode per ISO 21496-1 spec with common denominator
optimization, fraction-to-float conversion, and XMP metadata read/write
using roxmltree for parsing.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add core gain map computation functions ported from libultrahdr's
gainmapmath.cpp and jpegr.cpp:
- compute_gain: log2 ratio with dark pixel clamping
- encode_gain / affine_map_gain: normalized quantization to u8
- apply_gain_single / apply_gain_multi: HDR reconstruction from SDR + gain
- sample_map_bilinear: Shepard's IDW interpolation for gain map upsampling
- global_tonemap: Reinhard-style HDR to SDR tone mapping

8 new tests (43 total).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Port of multipictureformat.cpp - generates APP2/MPF marker data per
CIPA DC-007 spec for linking primary JPEG with secondary gain map JPEG.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
… HDR reconstruction

Add decoder module (Phase 8, Tasks 21-23) implementing:
- extract_gainmap_jpeg: parse MPF APP2 marker to locate secondary JPEG,
  extract gain map bytes and ISO 21496-1 / XMP metadata
- apply_gainmap_to_sdr: apply gain map to SDR RGBA pixels with configurable
  display boost, output transfer function (sRGB/PQ/HLG/Linear), and pixel
  format (RGBA8888/RGBA1010102/RgbaF16)
- Decoder builder API: Decoder::new().output_format().output_transfer()
  .max_display_boost().probe()/.decode()

All 49 tests pass (45 existing + 4 new).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…EG assembly, ICC profiles, and builder API

- generate_gainmap(): compute HDR/SDR ratio per pixel block, produce gain map image + metadata
- assemble_ultrahdr(): insert XMP/ISO metadata APP markers, MPF, and append gain map as secondary image
- write_icc_profile(): generate ICC v2 profiles for sRGB/PQ/HLG × BT.709/P3/BT.2100 gamuts
- Encoder builder: hdr_raw() -> sdr_compressed() -> encode() pipeline with quality/scale/multichannel options

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…I dependency

- Remove ultrahdr-sys from workspace members (no longer needed)
- Rewrite encode.rs to use ultrahdr::decoder::Decoder and ultrahdr::encoder::Encoder
  builder APIs instead of FFI CompressedImage/Decoder/Encoder/ImgLabel/sys
- Simplify color.rs by delegating ICC gamut detection to
  ultrahdr::color::icc::detect_color_gamut, removing ~120 lines of duplicated
  ICC parsing code
- Rewrite detect.rs probe_gainmap_metadata to use Decoder::new(buf).probe()
  instead of FFI Decoder with CompressedImage, changing signature from
  &mut [u8] to &[u8]

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
These workspace-level dependencies are no longer used by any active
workspace member (ultrahdr and ultrahdr-bake are pure Rust now).
The ultrahdr-sys directory is kept as deprecated but excluded from
the workspace.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Verifies the full UltraHDR pipeline: encode synthetic HDR+SDR images
into an UltraHDR JPEG, probe for gain map metadata, then decode back
to RGBA8888 and validate dimensions and pixel data.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Collapse nested if-let chains in decoder.rs (collapsible_if)
- Add #[allow(clippy::too_many_arguments)] for generate_gainmap and
  apply_gainmap_to_sdr (public API, restructuring not warranted)
- Replace manual div_ceil with .div_ceil() in encoder.rs
- Replace .filter().last() with .rfind() (double_ended_iterator_last)
- Remove unnecessary u32 cast in f16_to_f32 subnormal branch
- Add Default impl for Encoder
- Replace write!() with writeln!() in XMP metadata generation
- Fix unnecessary &mut in motion.rs probe_gainmap_metadata call
- Fix useless u8 <= 255 comparison in test
- Escape [0,1] as \[0,1\] in doc comments to fix rustdoc warnings

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
When no SDR JPEG is provided, the encoder now automatically generates
an SDR image from HDR input via global Reinhard tone mapping, then
proceeds with normal gain map computation and UltraHDR assembly. This
enables the API-0 scenario from the GOAL spec (HDR raw only input).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
These functions are ported from libultrahdr's gainmapmath.cpp for API
completeness but are not used in the current encoding/decoding paths.
Mark with #[allow(dead_code)] rather than removing to preserve parity.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
The decoder's apply_gainmap_to_sdr broadcasts a single sampled gain
value to all 3 channels even when multi_channel metadata is present.
This is correct for the default single-channel (grayscale) gain map
encoding mode. Added a comment clarifying this design limitation.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- icc.rs: change s15fixed16_to_f32() to return Option<f32> instead of
  panicking on non-4-byte input; update callers to propagate with ?
- encoder.rs: clamp negative values in to_n_u closure (metadata_to_frac)
  to prevent incorrect u32 saturation when hdr_capacity_min < 1.0
- encoder.rs: remove unused variables _insert_pos and _mpf_seg_start

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Remove unused thiserror dependency from ultrahdr/Cargo.toml
- Replace unreachable wildcard arm in gamut_convert() with unreachable!()
- Add u16 range validation for width/height in JPEG encode functions

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add integration tests comparing pure Rust encoder/decoder with C++
(ultrahdr-sys) golden reference data. Tests verify cross-implementation
decode, roundtrip, metadata preservation, and probe capabilities.

Fix MPF offset handling: use spec-compliant TIFF-header-relative
offsets in both encoder and decoder, with fallback for absolute
offsets for backwards compatibility.

Golden data generated via ultrahdr-sys FFI (RGBA1010102 HLG + SDR
JPEG, quality=95, decoded at SRGB boost=4.0).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Add sample_map_bilinear_rgb() for per-channel RGB gain map sampling
- Detect gain map channel count (grayscale vs RGB) automatically in decoder
- Add P010 1280x720 golden data from C++ encoder (real 10-bit HDR content)
- Add test_decode_p010_sys_encoded_jpeg and test_probe_p010_sys_encoded tests
- Rename existing tests to clarify minnie vs p010 variants
- PSNR: 20.71 dB (P010 HDR), 17.44 dB (minnie SDR), 35.74 dB (Rust roundtrip)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Two bugs fixed in the decoder:

1. Weight application order: the display boost weight was being
   multiplied into the raw gain value BEFORE gamma correction, but
   C++ libultrahdr applies it to the logBoost AFTER gamma correction:
   - Wrong (Rust):  gainFactor = exp2(logBoost)  where logBoost uses (gain*weight)^(1/gamma)
   - Correct (C++): gainFactor = exp2(logBoost * weight)  where logBoost uses gain^(1/gamma)

2. SRGB output should skip gain map entirely: C++ libultrahdr returns
   the SDR base image directly when output_ct=SRGB (no gain map applied).
   The Rust decoder was incorrectly applying the gain map for SRGB output,
   causing a 40 dB quality gap vs C++ reference.

After fix: Rust vs C++ PSNR = 64.54 dB (max_diff=3), matching within
expected JPEG decoder tolerance (jpeg-decoder vs libjpeg-turbo).

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add golden data and tests comparing Rust decoder output against C++
libultrahdr for three HDR output format/transfer combinations:
- LINEAR + RgbaF16 (PSNR 17.32 dB)
- HLG + Rgba1010102 (PSNR 19.62 dB)
- PQ + Rgba1010102 (PSNR 11.18 dB)

Golden data generated by C program using C++ libultrahdr decoder with
max_display_boost=4.0. Helper functions added for F16↔f32 conversion,
1010102 unpacking, and typed PSNR computation.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…parison

Regenerate all golden data using C++ libultrahdr library. Decoder tests
compare Rust vs C++ decode of the same UltraHDR JPEG; encoder tests
compare Rust enc→dec vs C++ enc→dec pipeline outputs.

- 4 decoder tests (SRGB/LINEAR/HLG/PQ) with PSNR thresholds
- 4 encoder tests (minnie SDR round-trip) with PSNR thresholds
- 2 metadata tests (probe + roundtrip)
- Remove old reference-based tests and unused golden data

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Replace per-pixel pow/log2/exp2 calls with pre-computed lookup tables:
- sRGB inverse OETF: 256-entry LUT (exact for u8 inputs)
- PQ OETF: 65536-entry LUT
- HLG OETF + inverse OOTF: 65536-entry LUTs
- GainLut: 1024-entry per-channel gain factor table

Performance improvement (1280x720, 20 iterations):
  LINEAR: 101.2 → 61.7 ms (1.64x faster)
  HLG:    112.1 → 79.4 ms (1.41x faster)
  PQ:     170.5 → 71.8 ms (2.37x faster)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Add optional `rayon` feature flag for parallel gain map processing
- Refactor apply_gainmap_to_sdr to process rows independently via par_chunks_mut
- Add criterion microbenchmarks comparing scalar/LUT/polynomial transfer functions
- ~2.4x decode speedup on 8 cores (LINEAR 52→25ms, HLG 71→27ms, PQ 62→25ms)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Enter-tainer and others added 28 commits March 3, 2026 13:27
- Move ISO 21496-1 metadata from primary to secondary (gain map) JPEG,
  matching C++ appendGainMap() which puts full metadata in the gain map
  image and only a version stub in the primary image
- Write hdrgm:BaseColorSpace="0" in XMP when use_base_cg is false
- Parse BaseColorSpace attribute in XMP to correctly set use_base_cg
  instead of hardcoding true

These fixes resolve Rust→C++ cross-decode failure (UHDR_CODEC_INVALID_PARAM)
where C++ decoder expected ISO metadata in the secondary image.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…max channel)

Three fixes to align Rust encoder gain map computation with C++ libultrahdr:

1. OOTF direction: HLG decode now uses forward OOTF (hlg_ootf_approx,
   pow(x, 1.2)) instead of inverse (hlg_inv_ootf_approx, pow(x, 1/1.2))
   matching C++ hlgOotfApprox scene-to-display transform.

2. Nits scaling: Both SDR and HDR values are now multiplied by
   SDR_WHITE_NITS (203) before computing gain, matching C++ behavior
   where sdr_y_nits = sdr_y * kSdrWhiteNits and
   hdr_y_nits = hdr_y * hdrSampleToNitsFactor.

3. Max channel: Single-channel mode now uses fmax(r,g,b) instead of
   weighted luminance, matching C++ use_luminance=false default.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…exact

- Add Encoder::sdr_raw() method for raw RGBA8888 SDR input (bypasses
  JPEG decode losses in gain map computation)
- Add gamut conversion in generate_gainmap: when sdr_gamut != hdr_gamut,
  convert HDR pixels to SDR gamut via 3x3 matrix (reuses color::gamut)
- Add clipNegatives: clip both SDR and HDR channels to max(0, val) after
  gamut conversion and before gain computation, matching C++ pipeline
- Update generate_gainmap signature: separate sdr_gamut and hdr_gamut
  parameters (was single _gamut)
- Update debug_encode test to use sdr_raw() instead of sdr_compressed()

White scenario now produces bit-exact metadata match with C++.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
C++ libultrahdr averages pixel blocks in gamma (OETF) space before
linearizing, while Rust was pre-linearizing all pixels then averaging
in linear space. This matters because transfer functions (sRGB, HLG)
are nonlinear: avg(inv_oetf(a), inv_oetf(b)) != inv_oetf(avg(a,b)).

Also fixes HDR nits factor: C++ uses hdr_white_nits (1000 for HLG,
10000 for PQ) while Rust was using SDR_WHITE_NITS (203). With the
old pre-scaling approach this happened to be equivalent, but the new
per-block linearization needs the correct factor.

Changes:
- Add decode_pixels_to_normalized: pixel format unpacking without transfer fn
- Add linearize_normalized: apply transfer fn to normalized buffer
- generate_gainmap: now takes gamma-space inputs + hdr_transfer param,
  averages in gamma space, then linearizes per block
- Encoder::encode: pass gamma-space buffers to generate_gainmap

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct srgb_inv_oetf/hlg_inv_oetf/pq_inv_oetf calls in
generate_gainmap with LUT-based versions matching C++ libultrahdr's
exact table sizes (sRGB=1024, HLG=4096, PQ=4096 entries).

This eliminates the metadata mismatch caused by quantization differences:
- max_content_boost: 4.4533067 → 4.453096 (matches C++)
- min_content_boost: 0.09103692 → 0.09114423 (matches C++)

Only the encoder's gain map path is changed; decoder paths (decode_pixels_to_linear,
linearize_normalized) retain direct computation for accuracy.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
11 tests comparing encoder and decoder output pixel-by-pixel:
- 5 encoder scenarios (gradient/white/black/color_ramp/mixed): metadata exact, gain map pixels identical
- 6 decoder scenarios (HLG/PQ/Linear × C++/Rust encoded): small diffs noted (max_diff ≤5)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Three fixes to reduce Rust vs C++ decoder pixel differences:

1. SDR linearization: use 1024-entry srgb_inv_oetf_lut_1024 (matching C++
   srgbInvOetfLUT) instead of 256-entry srgb_inv_oetf_lut. The C++ decoder
   operates on float RGB from YUV conversion, not integer u8 values.

2. HLG inverse OOTF: use direct powf(1/1.2) instead of 65536-entry LUT,
   matching C++ hlgInverseOotfApprox which uses std::pow directly.

3. f32_to_f16: implement round-to-nearest (wrapping_add 0x1000) matching
   C++ floatToHalf, instead of truncation. Also handles denormals and
   saturation correctly.

Results (gradient 64x64 test image):
- HLG 1010102: 1351→400 differing pixels, max_diff 2→2
- PQ 1010102:  830→367 differing pixels, max_diff 5→1
- Linear F16:  10182→1488 differing channels, max_diff 0.003→0.002

Remaining differences are due to JPEG decoder divergence: C++ decodes
to YUV420 then converts to float RGB via p3YuvToRgb, while Rust's
jpeg_decoder crate outputs u8 RGB directly with different rounding.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Performance optimizations for decode pipeline:
- Add zune-jpeg/zune-core as optional feature flag for faster JPEG decode
- Eliminate rgb_to_rgba allocation (apply_gainmap_to_sdr_rgb direct RGB path)
- Pre-compute ShepardsIDW weight table (eliminates per-pixel sqrt)
- Merge HLG inverse OOTF + OETF into single combined LUT
- Pre-initialize TransferLuts outside hot loop (avoids LazyLock atomic checks)
- Use 256-entry exact u8 sRGB LUT in decoder (inputs are u8, no quantization loss)
- Inline LUT lookup with unsafe get_unchecked for bounds-check elimination

Results at 512x512 (release mode, avg of 10 runs):
  Encode: Rust 8.0ms vs C++ 11.4ms = 0.70x (Rust faster)
  Decode: Rust 14.2ms vs C++ 10.1ms = 1.41x (target was <1.5x)

All 78 unit + 10 bit-exact + 2 roundtrip tests pass.
clippy + fmt clean.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Replace unsafe get_unchecked with safe bounds-checked indexing in
  lut_lookup (LLVM optimizes away the check for idx.min(max_idx))
- Remove unused pq_oetf_lookup and hlg_combined_lookup methods from
  TransferLuts (decoder uses inline lut_lookup instead)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
…fsets

- Pass pulp::Arch as parameter to apply_gain_simd/clamp_simd instead of
  creating it per-call (avoids redundant CPUID checks per row)
- Fix single-channel gain mode to use offset_sdr[0]/offset_hdr[0] for
  all channels, matching the scalar apply_single() behavior

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add comprehensive JPEG segment parser and metadata comparison test that
extracts and compares XMP APP1, ISO APP2, MPF APP2, and ICC APP2 segments
between Rust and C++ encoder outputs. Diagnostic output reveals specific
byte differences to guide metadata fixes.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
- Add continued fractions algorithm (float_to_unsigned_fraction_impl)
  ported from C++ floatToUnsignedFractionImpl() for accurate float-to-
  rational conversion, replacing fixed denom=10000 approach
- Add float_to_signed_fraction() and float_to_unsigned_fraction() public API
- Replace metadata_to_frac() to use continued fractions per field
- Add XmlWriter struct matching C++ XmlWriter output format exactly
- Add write_xmp_primary_container() for container directory XMP
- Rewrite write_xmp_gainmap_metadata() using XmlWriter for format match
- Add cpp_float_to_string() matching C++ stringstream defaultfloat
- Restructure assemble_ultrahdr(): primary gets container XMP,
  secondary gets gainmap metadata XMP + ISO APP2

Results from metadata_bytes_diagnostic:
- ISO-21496-1 segments now MATCH byte-exactly (both primary and secondary)
- XMP structure correct (container in primary, gainmap in secondary)
- All 78 unit tests + 11 bitexact tests pass (no regression)

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
C++ encoder only defines UHDR_WRITE_ISO (kWriteXmpMetadata=false),
so no XMP is written. Simplify assemble_ultrahdr() to match:
- Primary: SOI → ISO stub APP2 → MPF APP2 → rest of SDR JPEG
- Secondary: SOI → ISO full APP2 → rest of gain map JPEG
- Remove parse_jpeg_segments dependency and selective segment copying
- Update unit test to verify XMP absence

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…coder segments

Only assert on metadata we control (ISO-21496-1, XMP, EXIF). Treat MPF
and ICC as informational — they come from the underlying JPEG encoder
(mozjpeg vs libjpeg-turbo) and naturally differ.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add 5 parameterized multi-channel (RGB) gainmap bitexact tests comparing
Rust encoder output against C++ reference. Three encoder bugs fixed:
- Per-channel min/max gain tracking (was single global scalar)
- Gamut conversion direction: SDR→HDR when use_base_cg=false (was HDR→SDR)
- JPEG 4:4:4 chroma subsampling for RGB images (was defaulting to 4:2:0)

All 17 bitexact tests pass with 0 pixel differences.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Add 5 parameterized encoder bitexact tests:
- scale1/scale2: test gainmap_scale=1,2 with gradient scene
- nits1000/nits4000/nits10000: test target_display_peak_nits variants

All tests verify metadata and gain map pixel exact match with C++.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Adds a proper criterion-based benchmark comparing Rust and C++ encoder/decoder
performance at 512x512 resolution. Benchmarks encode (rust vs cpp) and decode
with multiple output format/transfer combinations (HLG 1010102, PQ 1010102,
Linear F16) plus C++ baseline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SIMD acceleration for gain map application is stable and provides
measurable performance improvement with no correctness impact.

Slack-Thread: https://metabit-trading.slack.com/archives/D0AC2E6SESV/p1772504256906069
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant