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
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]>
611 lines
25 KiB
Rust
611 lines
25 KiB
Rust
//! 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());
|
||
}
|
||
}
|