open-standards/wai/wai-rs/src/jpegai_bitstream.rs
Transaction Science f96ce04da3
Some checks failed
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1m48s
standards workspaces build + test / aac-rs (pull_request) Successful in 36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 47s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 39s
standards workspaces build + test / map-rs (pull_request) Successful in 40s
standards workspaces build + test / mesh-rs (pull_request) Successful in 32s
standards workspaces build + test / openpay (pull_request) Successful in 1m50s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 21s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 1m0s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 14s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m52s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m21s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m30s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 16s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 1m13s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 14s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 40s
wai formal verification (integer core) / kani (pull_request) Successful in 3m59s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Failing after 1m8s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 27s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m54s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m19s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m45s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 2m26s
standards workspaces build + test / sense-rs (pull_request) Successful in 2m15s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Failing after 2m23s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 13s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 2m30s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 35s
wai cross-architecture byte-equality / windows-x86_64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · windows (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (pull_request) Has been cancelled
wai-rs: the JPEG AI exact decoder calls no platform math library, and its output is pinned
The exact decoder's only math-library calls were `exp` in the gain scaler
and, on some targets, the float-to-integer roundings. Those results could
differ between platform math libraries, so the decoded bytes could differ
between machines.

- `float_repro` provides `exp` and the roundings built only from IEEE-754
  basic operations, so their bits are the same on every conforming target.
  Its unit tests pin their bits over a fixed grid of inputs and compare them
  with the host's own functions.
- Two guards keep the decode path free of math-library calls. Clippy's
  `disallowed_methods` (wai-rs/clippy.toml) is denied in each module on the
  path, however the call is spelled. tests/jpegai_decode_symbols.rs fails if
  the linked decoder imports a C math-library function.
- tests/jpegai_cross_platform.rs pins the decoded bytes of the `jpegai/v1`
  inputs and the bits of the float values reported after each stage. They
  were measured identical on macOS arm64, wasm32-wasip2, Linux arm64 (glibc)
  and Linux x86_64 (glibc, under user-mode emulation). Not measured: macOS
  x86_64, Windows, x86_64 hardware, 32-bit targets. The test skips unless its
  inputs are supplied.
- The gain-scaler margin tool now models the reference software's float32
  arithmetic and corrects `log_k`.
- determinism-tiers' evidence table records the measurement and its limits.
  The registry class of `wai.image.jpegai` is unchanged (NeuralEntropyExact).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 18:30:35 -04:00

611 lines
25 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! JPEG-AI **bitstream container + header bit reader** — the decoder's entry point.
//!
//! A codestream is a sequence of marker segments. Each (except the marker-only SOC/EOC)
//! is `marker(2 B, big-endian) + expgolomb_k0(payload_size) + byte-align + payload`.
//! The header substreams (PIH, TON) are **plain bit-packed**, read MSB-first — the
//! headers are *not* arithmetic-coded; only the latent substreams (SORP/SORS/SOZ) are,
//! and those are handed to [`crate::jpegai_mans`].
//!
//! [`BitReader`] is a faithful port of the reference `EcLibDirect::read_bits`: bits are
//! taken MSB-first within each byte and accumulated MSB-first into the value, with a
//! bit-granular cursor. Reads past the end return the bits available (zero-extended),
//! matching the reference's bounds check.
// On the JPEG AI exact decode path: no platform math-library call (see clippy.toml).
#![deny(clippy::disallowed_methods)]
/// Bits needed to carry `max_value`, per the reference `HeaderCoder::calc_bits_count`:
/// `ceil(log2(max_value + 1))`. `max_value = 0` needs no bits.
pub fn bits_for_max(max_value: u64) -> u32 {
if max_value == 0 { 0 } else { u64::BITS - max_value.leading_zeros() }
}
/// Marker identifiers (big-endian 2-byte prefixes).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Marker {
Soc, // 0xFF80 start of codestream (no payload)
Eoc, // 0xFF81 end of codestream (no payload)
Pih, // 0xFF82 picture header
Ton, // 0xFF83 tools header
Rdi, // 0xFF84
Soz, // 0xFF88 hyper-latent substream
Sorp, // 0xFF89 primary residual substream
Sors, // 0xFF8A secondary residual substream
Soq, // 0xFF8B
Udi, // 0xFF8C
}
impl Marker {
pub fn from_u16(v: u16) -> Option<Marker> {
Some(match v {
0xFF80 => Marker::Soc,
0xFF81 => Marker::Eoc,
0xFF82 => Marker::Pih,
0xFF83 => Marker::Ton,
0xFF84 => Marker::Rdi,
0xFF88 => Marker::Soz,
0xFF89 => Marker::Sorp,
0xFF8A => Marker::Sors,
0xFF8B => Marker::Soq,
0xFF8C => Marker::Udi,
_ => return None,
})
}
/// SOC/EOC carry no length or payload.
pub fn has_payload(self) -> bool {
!matches!(self, Marker::Soc | Marker::Eoc)
}
}
/// One parsed marker segment: where its payload lives in the codestream.
#[derive(Clone, Copy, Debug)]
pub struct Segment {
pub marker: Marker,
pub start: usize, // payload start (byte offset)
pub len: usize, // payload length in bytes
}
impl Segment {
/// Bit offset of the payload start — the cursor for a [`BitReader`].
pub fn start_bit(&self) -> usize { self.start * 8 }
}
/// MSB-first bit reader over a byte buffer, with a bit-granular cursor.
/// Port of the reference `EcLibDirect::read_bits`.
pub struct BitReader<'a> {
data: &'a [u8],
pos: usize, // in bits
}
impl<'a> BitReader<'a> {
pub fn new(data: &'a [u8]) -> Self { BitReader { data, pos: 0 } }
pub fn at(data: &'a [u8], bit_pos: usize) -> Self { BitReader { data, pos: bit_pos } }
pub fn bit_pos(&self) -> usize { self.pos }
pub fn seek_bit(&mut self, bit_pos: usize) { self.pos = bit_pos; }
/// Advance to the next byte boundary (the reference flushes after the segment length).
pub fn align_to_byte(&mut self) { self.pos = (self.pos + 7) & !7; }
/// Read `n` bits MSB-first. Stops early at end-of-buffer (returning what was read),
/// mirroring the reference's `byte_pos < m_data_size` guard.
pub fn read(&mut self, n: u32) -> u64 {
let mut v = 0u64;
for _ in 0..n {
let byte = self.pos >> 3;
if byte >= self.data.len() { break; }
let bit = (self.data[byte] >> (7 - (self.pos & 7))) & 1;
v = (v << 1) | bit as u64;
self.pos += 1;
}
v
}
/// Read one symbol whose width is **derived from its maximum value**, per the
/// reference `HeaderCoder::calc_bits_count`: `bits = ceil(log2(max_value + 1))`.
///
/// This is the header's universal width rule — no field has a fixed width. Some are
/// runtime-dependent: a `max_symbol` decoded earlier in a segment sets the width of
/// the weights that follow it (e.g. 65535 ⇒ 16 bits, then 99 ⇒ 7 bits).
pub fn read_max(&mut self, max_value: u64) -> u64 {
self.read(bits_for_max(max_value))
}
/// Unsigned exp-Golomb, k=0: count leading zeros `n`, then read `n` bits;
/// value = `2^n − 1 + suffix`. Used for marker-segment payload lengths.
pub fn read_expgolomb_k0(&mut self) -> u64 {
let mut n = 0u32;
while self.read(1) == 0 {
n += 1;
if n >= 32 || (self.pos >> 3) >= self.data.len() { return 0; }
}
let suffix = if n > 0 { self.read(n) } else { 0 };
(1u64 << n) - 1 + suffix
}
}
/// Walk a codestream and return its marker segments in order.
///
/// Stops at EOC or at the first unrecognised marker. Each segment's payload length is
/// the exp-Golomb k0 value following the marker, after which the reader is byte-aligned.
pub fn parse_container(data: &[u8]) -> Vec<Segment> {
let mut out = Vec::new();
let mut pos = 0usize; // byte offset of the next marker
while pos + 1 < data.len() {
let mk = match Marker::from_u16(((data[pos] as u16) << 8) | data[pos + 1] as u16) {
Some(m) => m,
None => break,
};
if !mk.has_payload() {
out.push(Segment { marker: mk, start: pos + 2, len: 0 });
if mk == Marker::Eoc { break; }
pos += 2;
continue;
}
let mut br = BitReader::at(data, (pos + 2) * 8);
let len = br.read_expgolomb_k0() as usize;
br.align_to_byte();
let start = br.bit_pos() / 8;
if start + len > data.len() { break; }
out.push(Segment { marker: mk, start, len });
pos = start + len;
}
out
}
/// Find the first segment with `marker`.
pub fn find_segment(segs: &[Segment], marker: Marker) -> Option<Segment> {
segs.iter().copied().find(|s| s.marker == marker)
}
/// Per-component (luma / chroma) parameters from the picture header.
#[derive(Clone, Debug, Default)]
pub struct ComponentHeader {
pub num_threads_r: u32,
pub num_chs: u32,
pub use_cube_flags: bool,
pub rvs_enable: bool,
pub grfs_enable: bool,
/// GRFS channel-selection flags, `num_chs` of them (empty if `grfs_enable` is false).
pub grfs_channel_flags: Vec<bool>,
/// `synthesis_tile_enable[c]` — the synthesis tile manager's flag, coded in the
/// picture header right after this component's GRFS channel flags. (The
/// component's LSBS flag is not here: LSBS codes it into the tool header; see
/// [`parse_lsbs_flags`].)
pub synthesis_tile_enable: bool,
}
/// The decoded JPEG-AI picture header (PIH substream).
#[derive(Clone, Debug, Default)]
pub struct PictureHeader {
pub stream_profile_idc: u32,
pub decoder_profile_id: u32,
pub synthesis_transform_ids: Vec<u32>,
pub level_idc: u32,
pub img_width: u32,
pub img_height: u32,
pub diff_display_width: u32,
pub diff_display_height: u32,
pub bit_depth_idc: u32,
/// Luma/chroma spatial subsampling factors (1 = 4:4:4).
pub s_ver: u32,
pub s_hor: u32,
pub c_ver: u32,
pub c_hor: u32,
/// 0 = none, 1 = built-in YUV↔RGB, 2 = signalled 3×3 matrix (+ offset).
pub colour_transform_idx: u32,
pub colour_transform_matrix: Option<[u32; 9]>,
pub colour_transform_offset: Option<[u32; 3]>,
pub model_id: u32,
pub num_threads_z: u32,
/// `beta_displacement_log` (already de-biased by 2048).
pub beta_displacement_log: [i32; 2],
pub independent_beta_uv: bool,
pub region_partitioning: bool,
/// Two components: `[0]` = luma, `[1]` = chroma.
pub components: [ComponentHeader; 2],
pub gain_3d_enable: bool,
/// Bit position just past the last field (before byte-alignment padding).
pub end_bit: usize,
}
/// Parse the picture header (PIH payload). `start_bit` is the payload bit offset
/// (`Segment::start_bit`). A faithful port of the reference decode order:
/// prefix → `CCS_SGMM::decode_header` (`multi_threading_z`, quantizer β F-header,
/// `region_partitioning`, quantizer β O-header) → the per-component tool groups
/// (`multi_threading_r`, `num_chs`, `use_cube_flags`, `rvs_enable`, `grfs_enable` +
/// channel flags, `synthesis_tile_enable`) → `gain_3D_enable`.
///
/// Header-part placement follows the reference: the synthesis tile manager and
/// `gain_3D` are declared with `stream_header_part='pic_header'`, while LSBS keeps
/// the default `'tool_header'`, so `lsbs_enable_flag[0..1]` are the first two bits of
/// TON, not PIH fields ([`parse_lsbs_flags`]). The reference header trace records the
/// same order (`synthesis_tile_enable[0]` before `multi_threading_r[1]`,
/// `gain_3D_enable_flag` last) and `lsbs_enable_flag = 1` for both components.
///
/// `c_ver`/`c_hor` are coded only when the matching `s_ver`/`s_hor` is 1; otherwise
/// they are 2 and take no bits (reference `CodingEngine` header decode).
///
/// Branches not exercised by the 4:4:4 single-tile reference stream (region
/// sub-splits, `independent_beta_uv`'s second β, the signalled colour matrix,
/// multi-threading counts, `s_ver`/`s_hor` = 2) follow the reference syntax and are
/// decoded but untested against a real codestream.
// ---- tools header (TON) ----
/// One `decode_filters` group of the EFE **linear** filter header.
#[derive(Clone, Debug, Default)]
pub struct EflFilters {
/// `best_cand_idx − 1`; `-1` means the plane's filter is absent.
pub cand_u: i32,
pub cand_v: i32,
pub fl_u: u32,
pub fl_v: u32,
pub min_symbol: u32,
pub max_symbol: u32,
pub weights_u: Vec<u32>,
pub weights_u2: Vec<u32>,
pub weights_v: Vec<u32>,
pub weights_v2: Vec<u32>,
}
/// EFE linear filter header: the two plane means plus two filter groups.
#[derive(Clone, Debug, Default)]
pub struct EflHeader {
/// `B1[i] / 100` = the per-plane mean consumed by `jpegai_pipeline::efl_chroma`.
pub b1: [u32; 2],
pub filters_best2: EflFilters,
pub filters_best: EflFilters,
}
/// eICCI per-tile model selection.
#[derive(Clone, Debug, Default)]
pub struct IcciTile {
/// Per-plane use flags (Y, U, V).
pub use_yuv: [bool; 3],
pub use_short_list: bool,
/// Signalled model indices (into the active list) for the Y and the joint-UV paths.
pub model_idx_y: u32,
pub model_idx_uv: u32,
}
/// EFE nonlinear filter header (the luma-aided chroma filter's parameters).
#[derive(Clone, Debug, Default)]
pub struct EfnHeader {
pub min_symbol: u32,
pub max_symbol: u32,
pub mask1_enabled: bool,
pub mask2_enabled: bool,
pub nonlinear_u_enabled: bool,
pub nonlinear_v_enabled: bool,
pub tile_width: u32,
pub tile_height: u32,
/// Per-tile luma range — the `luma_min_q` / `luma_max_q` of `efn_filter`.
pub luma_min: Vec<u32>,
pub luma_max: Vec<u32>,
/// Per-plane filter weights (`A1`), already offset by `min_symbol` — the
/// `weights_q15` of `efn_filter`.
pub weights_u: Vec<u32>,
pub weights_v: Vec<u32>,
}
/// The decoded tools header (TON substream).
#[derive(Clone, Debug, Default)]
pub struct ToolsHeader {
pub efl_enabled: bool,
pub efl: Option<EflHeader>,
pub icci_enabled: bool,
pub icci_tile_manager_enabled: bool,
pub icci_tiles: Vec<IcciTile>,
pub efn_enabled: bool,
pub efn: Option<EfnHeader>,
pub lef_enabled: bool,
/// LEF's reference channel index.
pub lef_ch_idx: u32,
/// Bit position just past the last field (before byte-alignment padding).
pub end_bit: usize,
}
/// `cands[i][0]` — the spatial-tiling candidate filter counts of the EFE linear filter.
const EFL_CAND_FILTER_COUNT: [u32; 6] = [1, 2, 2, 3, 3, 4];
/// One `decode_filters` group. `scale_2x2` selects the 4:4:4 path, where a single
/// plane's coefficients are decoded and replicated rather than coded four times.
fn parse_efl_filters(br: &mut BitReader, scale_2x2: bool) -> EflFilters {
let mut f = EflFilters::default();
f.cand_u = br.read_max(15) as i32 - 1;
f.cand_v = br.read_max(15) as i32 - 1;
if f.cand_u > -1 { f.fl_u = br.read_max(9) as u32; }
if f.cand_v > -1 { f.fl_v = br.read_max(9) as u32; }
f.min_symbol = br.read_max((1 << 16) - 1) as u32;
f.max_symbol = br.read_max((1 << 16) - 1) as u32;
let group = |cand: i32, fl: u32, br: &mut BitReader| -> (Vec<u32>, Vec<u32>) {
if cand <= -1 { return (Vec::new(), Vec::new()); }
let taps = (fl * fl) as usize;
// shape[0]==4 is coded as a single plane under 4:4:4 and replicated
let n_main = if scale_2x2 { taps } else { EFL_CAND_FILTER_COUNT[cand as usize] as usize * taps };
let main = (0..n_main).map(|_| br.read_max(f.max_symbol as u64) as u32).collect();
let second = (0..taps).map(|_| br.read_max(f.max_symbol as u64) as u32).collect();
(main, second)
};
let (u, u2) = group(f.cand_u, f.fl_u, br);
f.weights_u = u;
f.weights_u2 = u2;
let (v, v2) = group(f.cand_v, f.fl_v, br);
f.weights_v = v;
f.weights_v2 = v2;
f
}
/// Parse the **post-filter chain** of the tools header (TON), in filters-factory order:
/// EFE-linear → ICCI → eICCI → EFE-nonlinear → LEF. Each tool is gated by its own enable
/// flag (the recursive header walk), and every field's width is derived from its
/// `max_symbol_value` — including the runtime-dependent ones, where a just-decoded
/// `max_symbol` sets the width of the weights that follow.
///
/// `start_bit` is the bit position of the **first filter's enable flag**, not the TON
/// payload start: `_stream_header_part` defaults to `"tool_header"`, so tools other than
/// the post-filters also emit into TON ahead of the filter chain (in the reference
/// stream two such bits precede EFE-linear's flag). Locating that prefix generally means
/// walking the whole tool tree; this function parses the chain from a given offset.
///
/// `models_count` is the size of the eICCI model list the stream selects from (it sets
/// the width of the signalled model indices); `num_tiles` is the eICCI tile count.
pub fn parse_tools_header(data: &[u8], start_bit: usize, scale_2x2: bool, models_count: u32, num_tiles: usize) -> ToolsHeader {
let mut br = BitReader::at(data, start_bit);
let mut t = ToolsHeader::default();
t.efl_enabled = br.read_max(1) == 1;
if t.efl_enabled {
let mut h = EflHeader::default();
h.b1[0] = br.read_max((1 << 15) - 1) as u32;
h.b1[1] = br.read_max((1 << 15) - 1) as u32;
h.filters_best2 = parse_efl_filters(&mut br, scale_2x2);
h.filters_best = parse_efl_filters(&mut br, scale_2x2);
t.efl = Some(h);
}
t.icci_enabled = br.read_max(1) == 1;
if t.icci_enabled {
t.icci_tile_manager_enabled = br.read_max(1) == 1;
for _ in 0..num_tiles.max(1) {
let mut tile = IcciTile::default();
for u in tile.use_yuv.iter_mut() { *u = br.read_max(1) == 1; }
if tile.use_yuv.iter().any(|&b| b) {
tile.use_short_list = br.read_max(1) == 1;
if tile.use_yuv[0] {
tile.model_idx_y = br.read_max(models_count as u64) as u32;
}
if tile.use_yuv[1] || tile.use_yuv[2] {
tile.model_idx_uv = br.read_max(models_count as u64) as u32;
}
}
t.icci_tiles.push(tile);
}
}
t.efn_enabled = br.read_max(1) == 1;
if t.efn_enabled {
let mut e = EfnHeader::default();
e.min_symbol = br.read_max((1 << 16) - 1) as u32;
e.max_symbol = br.read_max((1 << 16) - 1) as u32;
e.mask1_enabled = br.read_max(1) == 1;
e.mask2_enabled = br.read_max(1) == 1;
if e.mask1_enabled || e.mask2_enabled {
let _b_size = br.read_max((1 << 10) - 1);
let h = br.read_max((1 << 10) - 1) as usize;
let w = br.read_max((1 << 10) - 1) as usize;
// W5 masks: only the enabled one(s) carry h×w symbols (max 2 ⇒ 2 bits)
for enabled in [e.mask1_enabled, e.mask2_enabled] {
let n = if enabled { h * w } else { 0 };
for _ in 0..n { br.read_max(2); }
}
}
e.nonlinear_u_enabled = br.read_max(1) == 1;
e.nonlinear_v_enabled = br.read_max(1) == 1;
if e.nonlinear_u_enabled || e.nonlinear_v_enabled {
e.tile_width = br.read_max((1 << 16) - 1) as u32;
e.tile_height = br.read_max((1 << 16) - 1) as u32;
let n = br.read_max((1 << 16) - 1) as usize; // numTiles
e.luma_min = (0..n).map(|_| br.read_max((1 << 16) - 1) as u32).collect();
e.luma_max = (0..n).map(|_| br.read_max((1 << 16) - 1) as u32).collect();
// candNum + A1 are coded per ENABLED plane (U first, then V)
for (enabled, dst) in [(e.nonlinear_u_enabled, 0), (e.nonlinear_v_enabled, 1)] {
if !enabled { continue; }
let cand_num = br.read_max((1 << 16) - 1) as usize;
let w: Vec<u32> = (0..cand_num)
.map(|_| br.read_max(e.max_symbol as u64) as u32 + e.min_symbol)
.collect();
if dst == 0 { e.weights_u = w; } else { e.weights_v = w; }
}
}
t.efn = Some(e);
}
t.lef_enabled = br.read_max(1) == 1;
if t.lef_enabled {
t.lef_ch_idx = br.read_max(255) as u32;
}
t.end_bit = br.bit_pos();
t
}
/// `lsbs_enable_flag[0..1]` (luma, chroma): the first two bits of the tool header
/// (TON), ahead of the post-filter chain that [`parse_tools_header`] reads from
/// `ton_start_bit + 2`. LSBS codes its enable flag into the tool header (the
/// reference's default header part), one flag per component.
pub fn parse_lsbs_flags(data: &[u8], ton_start_bit: usize) -> [bool; 2] {
let mut br = BitReader::at(data, ton_start_bit);
[br.read(1) == 1, br.read(1) == 1]
}
pub fn parse_picture_header(data: &[u8], start_bit: usize) -> PictureHeader {
let mut br = BitReader::at(data, start_bit);
let mut h = PictureHeader::default();
h.stream_profile_idc = br.read(4) as u32;
h.decoder_profile_id = br.read(4) as u32;
let num_transforms = br.read(4) as u32 + 1;
h.synthesis_transform_ids = (0..num_transforms).map(|_| br.read(4) as u32).collect();
h.level_idc = br.read(8) as u32;
h.img_width = br.read(16) as u32 + 64;
h.img_height = br.read(16) as u32 + 64;
h.diff_display_width = br.read(6) as u32;
h.diff_display_height = br.read(6) as u32;
h.bit_depth_idc = br.read(3) as u32;
h.s_ver = br.read(1) as u32 + 1;
h.s_hor = br.read(1) as u32 + 1;
h.c_ver = if h.s_ver == 1 { br.read(1) as u32 + 1 } else { 2 };
h.c_hor = if h.s_hor == 1 { br.read(1) as u32 + 1 } else { 2 };
h.colour_transform_idx = br.read(2) as u32;
if h.colour_transform_idx == 2 {
let mut m = [0u32; 9];
for v in m.iter_mut() { *v = br.read(8) as u32; }
let mut o = [0u32; 3];
for v in o.iter_mut() { *v = br.read(8) as u32; }
h.colour_transform_matrix = Some(m);
h.colour_transform_offset = Some(o);
}
h.model_id = br.read(4) as u32;
// CCS_SGMM::decode_header
h.num_threads_z = if br.read(1) == 1 { 1 << (br.read(2) + 1) } else { 1 };
h.beta_displacement_log[0] = br.read(12) as i32 - 2048; // quantizer β F-header
h.region_partitioning = br.read(1) == 1;
if h.region_partitioning {
let _num_ver = br.read(7) + 1;
let _num_hor = br.read(7) + 1;
if br.read(1) == 0 {
let _hyper_overlap = br.read(2);
let _mcm_overlap = br.read(4);
}
}
h.independent_beta_uv = br.read(1) == 1; // quantizer β O-header
h.beta_displacement_log[1] = if h.independent_beta_uv {
br.read(12) as i32 - 2048
} else {
h.beta_displacement_log[0]
};
// per-component tool groups (0 = luma, 1 = chroma)
for c in 0..2 {
let comp = &mut h.components[c];
comp.num_threads_r = if br.read(1) == 1 { 1 << (br.read(2) + 1) } else { 1 };
comp.num_chs = br.read(8) as u32;
// Only THREE flags precede the channel flags; the fourth PIH flag TRAILS
// them. `res_var_scale`'s pair (rvs_enable, grfs_enable) sits immediately
// before the channel flags it gates, and the synthesis tile manager's flag
// follows the group. Reading four up-front shifts `grfs_channel_flags` one
// bit late and corrupts them (while still landing on the right end_bit,
// since the extra leading bit is cancelled by one fewer channel flag).
comp.use_cube_flags = br.read(1) == 1;
comp.rvs_enable = br.read(1) == 1;
comp.grfs_enable = br.read(1) == 1;
if comp.grfs_enable {
comp.grfs_channel_flags = (0..comp.num_chs).map(|_| br.read(1) == 1).collect();
}
comp.synthesis_tile_enable = br.read(1) == 1;
}
h.gain_3d_enable = br.read(1) == 1;
h.end_bit = br.bit_pos();
h
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bit_reader_is_msb_first() {
// 0b1011_0010, 0b0100_0000
let d = [0xB2u8, 0x40];
let mut br = BitReader::new(&d);
assert_eq!(br.read(1), 1);
assert_eq!(br.read(3), 0b011);
assert_eq!(br.read(4), 0b0010);
assert_eq!(br.bit_pos(), 8);
assert_eq!(br.read(2), 0b01, "continues MSB-first into the next byte");
}
#[test]
fn read_past_end_returns_available_bits() {
let d = [0xFFu8];
let mut br = BitReader::at(&d, 4);
// only 4 bits remain; the reference stops at the buffer end
assert_eq!(br.read(8), 0b1111);
}
#[test]
fn expgolomb_k0_round_values() {
// k0: '1'->0 ; '010'->1 ; '011'->2 ; '00100'->3 ; '00111'->6
let cases: [(&[u8], u64, u32); 3] = [
(&[0b1000_0000], 0, 1),
(&[0b0100_0000], 1, 3),
(&[0b0011_1000], 6, 5),
];
for (bytes, want, bits) in cases {
let mut br = BitReader::new(bytes);
assert_eq!(br.read_expgolomb_k0(), want, "value");
assert_eq!(br.bit_pos(), bits as usize, "consumed bits");
}
}
#[test]
fn align_to_byte_is_idempotent_on_boundary() {
let d = [0u8; 4];
let mut br = BitReader::at(&d, 8);
br.align_to_byte();
assert_eq!(br.bit_pos(), 8, "already aligned");
br.seek_bit(9);
br.align_to_byte();
assert_eq!(br.bit_pos(), 16);
}
/// MSB-first bit writer for synthetic headers.
fn pack(fields: &[(u64, u32)]) -> Vec<u8> {
let mut out = Vec::new();
let mut n = 0usize;
for &(v, w) in fields {
for i in (0..w).rev() {
if n % 8 == 0 { out.push(0); }
let bit = ((v >> i) & 1) as u8;
*out.last_mut().unwrap() |= bit << (7 - n % 8);
n += 1;
}
}
out.extend([0u8; 8]);
out
}
/// `c_ver`/`c_hor` take no bits when `s_ver`/`s_hor` is 2 (reference
/// `CodingEngine` header decode): the fields after them must not shift.
#[test]
fn chroma_factors_are_implicit_when_secondary_is_subsampled() {
let prefix = [(0, 4), (2, 4), (0, 4), (2, 4), (52, 8), (192, 16), (192, 16), (0, 6), (0, 6), (0, 3)];
// s_ver = 2, s_hor = 2 ⇒ no c_ver / c_hor bits; then colour_transform_idx = 1, model_id = 5
let mut f: Vec<(u64, u32)> = prefix.to_vec();
f.extend([(1, 1), (1, 1), (1, 2), (5, 4)]);
let h = parse_picture_header(&pack(&f), 0);
assert_eq!((h.s_ver, h.s_hor, h.c_ver, h.c_hor), (2, 2, 2, 2));
assert_eq!((h.colour_transform_idx, h.model_id), (1, 5), "fields after s/c stay aligned");
// s_ver = s_hor = 1 ⇒ c_ver / c_hor are coded (here 2 and 1)
let mut f: Vec<(u64, u32)> = prefix.to_vec();
f.extend([(0, 1), (0, 1), (1, 1), (0, 1), (1, 2), (5, 4)]);
let h = parse_picture_header(&pack(&f), 0);
assert_eq!((h.s_ver, h.s_hor, h.c_ver, h.c_hor), (1, 1, 2, 1));
assert_eq!((h.colour_transform_idx, h.model_id), (1, 5));
}
#[test]
fn parse_container_walks_markers_and_lengths() {
// SOC | PIH(len 2, payload "ab") | EOC
// expgolomb_k0(2) = '011' -> byte 0b0110_0000 after align
let d = [0xFF, 0x80, 0xFF, 0x82, 0b0110_0000, b'a', b'b', 0xFF, 0x81];
let segs = parse_container(&d);
assert_eq!(segs.len(), 3, "{segs:?}");
assert_eq!(segs[0].marker, Marker::Soc);
assert_eq!(segs[1].marker, Marker::Pih);
assert_eq!((segs[1].start, segs[1].len), (5, 2));
assert_eq!(&d[segs[1].start..segs[1].start + segs[1].len], b"ab");
assert_eq!(segs[2].marker, Marker::Eoc);
assert!(find_segment(&segs, Marker::Ton).is_none());
}
}