From 81f921e0ca37424d6bd1f7b5429ad9aecffb46bd Mon Sep 17 00:00:00 2001 From: GF Date: Sat, 26 Sep 2026 02:51:30 -0400 Subject: [PATCH 1/2] perf: read entropy bits a word at a time and decode VLCs by table PacketBitReader::read_bits consumed one bit per loop iteration with a bounds-checked byte index, and the three prefix-code decoders (entropy VLCs, CBPHP, and YUV DC/LP patterns) read one bit at a time while linearly searching their code tables and recomputing the maximum code length on every call. Reads of up to 57 bits now take one big-endian 64-bit window load, with the end-of-buffer padding and error construction moved into cold out-of-line paths so the hot path inlines. Every normative prefix table is compiled into an eight-bit lookup; construction is const, so an ambiguous or oversized table fails compilation. Error behavior is unchanged: truncated packets report UnexpectedEnd at the bit where the serial decoder stopped, and unmatched full-length prefixes report InvalidVlc at the code start. Exhaustive tests compare the lookup decoders with the serial search for every 16-bit input, offset, and truncation, and the reader with a bit-serial reference for every width and offset. --- Cargo.lock | 14 +- crates/jxr-core/Cargo.toml | 2 +- crates/jxr-cuda/Cargo.toml | 4 +- crates/jxr-image/Cargo.toml | 4 +- crates/jxr-metal/Cargo.toml | 4 +- crates/jxr-mpsgraph/Cargo.toml | 8 +- crates/jxr-native/Cargo.toml | 4 +- crates/jxr-native/src/entropy/bit_reader.rs | 171 +++++++++++++-- crates/jxr-native/src/entropy/coefficients.rs | 20 +- crates/jxr-native/src/entropy/mod.rs | 3 + crates/jxr-native/src/entropy/vlc.rs | 207 +++++++++++++----- crates/jxr-native/src/tile_decode/cbphp.rs | 59 ++--- .../jxr-native/src/tile_decode/yuv/syntax.rs | 45 ++-- crates/jxr-test-support/Cargo.toml | 4 +- crates/jxr/Cargo.toml | 10 +- 15 files changed, 400 insertions(+), 159 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15ac94f..e531a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "jxr" -version = "0.1.2" +version = "0.2.0" dependencies = [ "jxr-core", "jxr-cuda", @@ -158,14 +158,14 @@ dependencies = [ [[package]] name = "jxr-core" -version = "0.1.1" +version = "0.2.0" dependencies = [ "j2k-core", ] [[package]] name = "jxr-cuda" -version = "0.1.1" +version = "0.2.0" dependencies = [ "cudarc", "jxr-core", @@ -175,7 +175,7 @@ dependencies = [ [[package]] name = "jxr-image" -version = "0.1.1" +version = "0.2.0" dependencies = [ "image", "jxr", @@ -188,7 +188,7 @@ version = "0.1.0" [[package]] name = "jxr-metal" -version = "0.1.2" +version = "0.2.0" dependencies = [ "j2k-core", "j2k-metal-support", @@ -202,7 +202,7 @@ dependencies = [ [[package]] name = "jxr-mpsgraph" -version = "0.1.1" +version = "0.2.0" dependencies = [ "j2k-metal-support", "j2k-mpsgraph-support", @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "jxr-native" -version = "0.1.1" +version = "0.2.0" dependencies = [ "fearless_simd", "jxr-core", diff --git a/crates/jxr-core/Cargo.toml b/crates/jxr-core/Cargo.toml index f2c3c8f..858cc9a 100644 --- a/crates/jxr-core/Cargo.toml +++ b/crates/jxr-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-core" description = "Shared JPEG XR decoder contracts" -version = "0.1.1" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/jxr-cuda/Cargo.toml b/crates/jxr-cuda/Cargo.toml index 3f94554..95b57ad 100644 --- a/crates/jxr-cuda/Cargo.toml +++ b/crates/jxr-cuda/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-cuda" description = "CUDA reconstruction backend for the JXR decoder" -version = "0.1.1" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -10,7 +10,7 @@ homepage.workspace = true [dependencies] cudarc.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.1.1" } +jxr-core = { path = "../jxr-core", version = "=0.2.0" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } thiserror.workspace = true diff --git a/crates/jxr-image/Cargo.toml b/crates/jxr-image/Cargo.toml index bcddb2c..506360c 100644 --- a/crates/jxr-image/Cargo.toml +++ b/crates/jxr-image/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-image" description = "Lossless ownership adapter from JPEG XR decode results to image::DynamicImage" -version = "0.1.1" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] image.workspace = true -jxr = { path = "../jxr", version = "=0.1.2" } +jxr = { path = "../jxr", version = "=0.2.0" } thiserror.workspace = true [lints] diff --git a/crates/jxr-metal/Cargo.toml b/crates/jxr-metal/Cargo.toml index 00a61ac..2e5bbf4 100644 --- a/crates/jxr-metal/Cargo.toml +++ b/crates/jxr-metal/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-metal" description = "Metal execution boundary for the JXR decoder" -version = "0.1.2" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -11,7 +11,7 @@ homepage.workspace = true [dependencies] j2k-core.workspace = true j2k-metal-support.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.1.1" } +jxr-core = { path = "../jxr-core", version = "=0.2.0" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } thiserror.workspace = true diff --git a/crates/jxr-mpsgraph/Cargo.toml b/crates/jxr-mpsgraph/Cargo.toml index d70879c..697b71e 100644 --- a/crates/jxr-mpsgraph/Cargo.toml +++ b/crates/jxr-mpsgraph/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-mpsgraph" description = "Direct MPSGraph tensor adapter for Metal-resident JPEG XR batches" -version = "0.1.1" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -13,9 +13,9 @@ readme = "README.md" [dependencies] j2k-metal-support.workspace = true -jxr = { path = "../jxr", version = "=0.1.2", features = ["metal"] } -jxr-core = { path = "../jxr-core", version = "=0.1.1" } -jxr-metal = { path = "../jxr-metal", version = "=0.1.2" } +jxr = { path = "../jxr", version = "=0.2.0", features = ["metal"] } +jxr-core = { path = "../jxr-core", version = "=0.2.0" } +jxr-metal = { path = "../jxr-metal", version = "=0.2.0" } thiserror.workspace = true [target.'cfg(all(target_arch = "aarch64", target_os = "macos"))'.dependencies] diff --git a/crates/jxr-native/Cargo.toml b/crates/jxr-native/Cargo.toml index 391c716..bdda3b5 100644 --- a/crates/jxr-native/Cargo.toml +++ b/crates/jxr-native/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-native" description = "Safe Rust JPEG XR parser, CPU decoder, and Annex-A writer" -version = "0.1.1" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -10,7 +10,7 @@ homepage.workspace = true [dependencies] fearless_simd.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.1.1" } +jxr-core = { path = "../jxr-core", version = "=0.2.0" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } rayon.workspace = true diff --git a/crates/jxr-native/src/entropy/bit_reader.rs b/crates/jxr-native/src/entropy/bit_reader.rs index 6902f32..44ee34d 100644 --- a/crates/jxr-native/src/entropy/bit_reader.rs +++ b/crates/jxr-native/src/entropy/bit_reader.rs @@ -50,41 +50,115 @@ impl<'a> PacketBitReader<'a> { } /// Reads one bit. + #[inline] pub fn read_bit(&mut self) -> Result { Ok(self.read_bits(1)? != 0) } /// Reads up to 64 bits, most-significant bit first. + #[inline] pub fn read_bits(&mut self, count: u8) -> Result { + // A byte-aligned 64-bit window always holds 57 bits after the in-byte + // offset, so every entropy-syntax read needs exactly one load. + if count > 57 { + return self.read_wide_bits(count); + } + let end = self.checked_end(count)?; + let value = match count { + 0 => 0, + _ => self.window() >> (64 - count), + }; + self.bit_position = end; + Ok(value) + } + + /// Returns the next eight bits without consuming them, zero-filled past the packet end. + #[inline] + pub(crate) fn peek_byte(&self) -> u8 { + let remaining = self.bits_remaining(); + let mut window = self.window(); + if remaining < 8 { + window &= !(u64::MAX >> remaining); + } + window.to_be_bytes()[0] + } + + /// Advances past bits already inspected with [`Self::peek_byte`]. + #[inline] + pub(crate) fn skip_bits(&mut self, count: u8) -> Result<(), EntropyError> { + self.bit_position = self.checked_end(count)?; + Ok(()) + } + + /// Builds the error a bit-serial reader reports after consuming `consumed` more bits. + #[cold] + pub(crate) fn unexpected_end_after(&self, consumed: usize) -> EntropyError { + EntropyError::UnexpectedEnd { + bit_position: self.bit_position.saturating_add(consumed), + requested_bits: 1, + bit_length: self.bit_length, + } + } + + #[inline(never)] + fn read_wide_bits(&mut self, count: u8) -> Result { if count > 64 { return Err(EntropyError::InvalidParameter { parameter: "bit count", value: i64::from(count), }); } - let Some(end) = self.bit_position.checked_add(usize::from(count)) else { - return Err(EntropyError::UnexpectedEnd { - bit_position: self.bit_position, - requested_bits: count, - bit_length: self.bit_length, - }); - }; - if end > self.bit_length { - return Err(EntropyError::UnexpectedEnd { - bit_position: self.bit_position, - requested_bits: count, - bit_length: self.bit_length, - }); + let end = self.checked_end(count)?; + let high_bits = count - 32; + let high = self.window() >> (64 - high_bits); + self.bit_position += usize::from(high_bits); + let low = self.window() >> 32; + self.bit_position = end; + Ok((high << 32) | low) + } + + #[inline] + fn checked_end(&self, count: u8) -> Result { + match self.bit_position.checked_add(usize::from(count)) { + Some(end) if end <= self.bit_length => Ok(end), + _ => Err(self.end_error(count)), } + } - let mut value = 0_u64; - while self.bit_position < end { - let byte = self.bytes[self.bit_position / 8]; - let shift = 7 - (self.bit_position % 8); - value = (value << 1) | u64::from((byte >> shift) & 1); - self.bit_position += 1; + #[cold] + #[inline(never)] + fn end_error(&self, count: u8) -> EntropyError { + EntropyError::UnexpectedEnd { + bit_position: self.bit_position, + requested_bits: count, + bit_length: self.bit_length, } - Ok(value) + } + + /// Returns 64 bits starting at `bit_position`, most-significant-bit aligned. + /// + /// Bytes beyond the backing slice read as zero. Bits between the packet + /// bit length and the end of its final byte are returned unchanged, so + /// callers must bound their use by `bits_remaining`. + #[inline] + fn window(&self) -> u64 { + let byte = self.bit_position / 8; + let word = match self.bytes.get(byte..byte + 8) { + Some(chunk) => { + u64::from_be_bytes(chunk.try_into().expect("slice is exactly eight bytes")) + } + None => Self::padded_word(self.bytes, byte), + }; + word << (self.bit_position % 8) + } + + #[cold] + #[inline(never)] + fn padded_word(bytes: &[u8], byte: usize) -> u64 { + let mut padded = [0_u8; 8]; + let tail = bytes.get(byte..).unwrap_or_default(); + padded[..tail.len()].copy_from_slice(tail); + u64::from_be_bytes(padded) } } @@ -125,4 +199,61 @@ mod tests { assert_eq!(reader.read_bits(0).unwrap(), 0); assert_eq!(reader.bit_position(), 0); } + + /// Bit-serial reference for the word-at-a-time reader. + fn reference_bits(bytes: &[u8], start: usize, count: usize) -> u64 { + (start..start + count).fold(0, |value, position| { + (value << 1) | u64::from((bytes[position / 8] >> (7 - position % 8)) & 1) + }) + } + + #[test] + fn every_width_and_offset_matches_bit_serial_reads() { + let bytes: Vec = (0_u8..24) + .map(|index| index.wrapping_mul(157) ^ 0xa5) + .collect(); + let bit_length = bytes.len() * 8 - 3; + for start in 0..bit_length { + for count in 0..=64_u8 { + let mut reader = PacketBitReader::with_bit_length(&bytes, bit_length).unwrap(); + if start > 0 { + reader.read_bits(u8::try_from(start % 64).unwrap()).unwrap(); + for _ in 0..start / 64 { + reader.read_bits(64).unwrap(); + } + } + assert_eq!(reader.bit_position(), start); + let result = reader.read_bits(count); + if start + usize::from(count) > bit_length { + assert_eq!( + result, + Err(EntropyError::UnexpectedEnd { + bit_position: start, + requested_bits: count, + bit_length, + }) + ); + assert_eq!(reader.bit_position(), start); + } else { + assert_eq!( + result.unwrap(), + reference_bits(&bytes, start, usize::from(count)), + "start {start}, count {count}" + ); + assert_eq!(reader.bit_position(), start + usize::from(count)); + } + } + } + } + + #[test] + fn peek_zero_fills_past_the_bounded_packet() { + let mut reader = PacketBitReader::with_bit_length(&[0xff, 0xff], 11).unwrap(); + assert_eq!(reader.peek_byte(), 0xff); + reader.read_bits(5).unwrap(); + assert_eq!(reader.peek_byte(), 0b1111_1100); + reader.read_bits(6).unwrap(); + assert_eq!(reader.peek_byte(), 0); + assert_eq!(reader.bit_position(), 11); + } } diff --git a/crates/jxr-native/src/entropy/coefficients.rs b/crates/jxr-native/src/entropy/coefficients.rs index 11a5456..a98d953 100644 --- a/crates/jxr-native/src/entropy/coefficients.rs +++ b/crates/jxr-native/src/entropy/coefficients.rs @@ -245,7 +245,7 @@ fn decode_abs_level( let index = vlc::decode( reader, "ABS_LEVEL_INDEX", - vlc::ABS_LEVEL[state.table_index()], + &vlc::ABS_LEVEL[state.table_index()], )?; observe_abs_level(state, index); if index < 6 { @@ -279,7 +279,11 @@ fn decode_first_index( reader: &mut PacketBitReader<'_>, state: &mut AdaptiveVlc, ) -> Result { - let symbol = vlc::decode(reader, "FIRST_INDEX", vlc::FIRST_INDEX[state.table_index()])?; + let symbol = vlc::decode( + reader, + "FIRST_INDEX", + &vlc::FIRST_INDEX[state.table_index()], + )?; observe_first_index(state, symbol); Ok(symbol) } @@ -291,11 +295,11 @@ fn decode_index( ) -> Result { match location.cmp(&15) { core::cmp::Ordering::Less => { - let symbol = vlc::decode(reader, "INDEX_A", vlc::INDEX_A[state.table_index()])?; + let symbol = vlc::decode(reader, "INDEX_A", &vlc::INDEX_A[state.table_index()])?; observe_index(state, symbol); Ok(symbol) } - core::cmp::Ordering::Equal => vlc::decode(reader, "INDEX_B", vlc::INDEX_B), + core::cmp::Ordering::Equal => vlc::decode(reader, "INDEX_B", &vlc::INDEX_B), core::cmp::Ordering::Greater => Ok(u8::from(reader.read_bit()?)), } } @@ -310,14 +314,14 @@ fn decode_run(reader: &mut PacketBitReader<'_>, max_run: u8) -> Result Ok(1), - 2 => vlc::decode(reader, "RUN_VALUE", vlc::RUN_VALUE_2), - 3 => vlc::decode(reader, "RUN_VALUE", vlc::RUN_VALUE_3), - 4 => vlc::decode(reader, "RUN_VALUE", vlc::RUN_VALUE_4), + 2 => vlc::decode(reader, "RUN_VALUE", &vlc::RUN_VALUE_2), + 3 => vlc::decode(reader, "RUN_VALUE", &vlc::RUN_VALUE_3), + 4 => vlc::decode(reader, "RUN_VALUE", &vlc::RUN_VALUE_4), _ => unreachable!(), }; } - let run_index = vlc::decode(reader, "RUN_INDEX", vlc::RUN_INDEX)?; + let run_index = vlc::decode(reader, "RUN_INDEX", &vlc::RUN_INDEX)?; let table_index = i16::from(run_index) + 5 * i16::from(RUN_BIN[usize::from(max_run)]); let table_index = usize::try_from(table_index).map_err(|_| EntropyError::InvalidParameter { parameter: "run table index", diff --git a/crates/jxr-native/src/entropy/mod.rs b/crates/jxr-native/src/entropy/mod.rs index 81a18a8..78ee768 100644 --- a/crates/jxr-native/src/entropy/mod.rs +++ b/crates/jxr-native/src/entropy/mod.rs @@ -20,3 +20,6 @@ pub use refinement::{ decode_flex, decode_flex_block, decode_lp_refinement, decode_lp_refinement_at, }; pub use scan::{AdaptiveHpScan, AdaptiveLpScan, HpScanDirection}; +#[cfg(test)] +pub(crate) use vlc::tests::assert_matches_serial; +pub(crate) use vlc::{PrefixTable, decode as decode_prefix}; diff --git a/crates/jxr-native/src/entropy/vlc.rs b/crates/jxr-native/src/entropy/vlc.rs index cab3d6f..3beee51 100644 --- a/crates/jxr-native/src/entropy/vlc.rs +++ b/crates/jxr-native/src/entropy/vlc.rs @@ -2,45 +2,79 @@ use super::{EntropyError, PacketBitReader}; -#[derive(Clone, Copy)] -pub(super) struct VlcCode { - bits: u16, - length: u8, - symbol: u8, +/// One normative prefix code: `(code bits, code length, symbol)`. +pub(crate) type VlcCode = (u16, u8, u8); + +/// Longest code in any T.832 prefix table used by the tile syntax. +const LOOKUP_BITS: u8 = 8; + +/// A prefix-code table resolved through one eight-bit lookup. +/// +/// Each entry stores `(length << 8) | symbol`; zero marks a prefix that no +/// code matches. Construction is `const`, so an ambiguous or oversized table +/// fails compilation instead of decoding. +pub(crate) struct PrefixTable { + max_length: u8, + lookup: [u16; 1 << LOOKUP_BITS], } -const fn c(bits: u16, length: u8, symbol: u8) -> VlcCode { - VlcCode { - bits, - length, - symbol, +impl PrefixTable { + pub(crate) const fn new(codes: &[VlcCode]) -> Self { + let mut lookup = [0_u16; 1 << LOOKUP_BITS]; + let mut max_length = 0; + let mut index = 0; + while index < codes.len() { + let (bits, length, symbol) = codes[index]; + assert!(length > 0 && length <= LOOKUP_BITS, "prefix code length"); + assert!(bits >> length == 0, "prefix code bits exceed length"); + if length > max_length { + max_length = length; + } + let span = 1_usize << (LOOKUP_BITS - length); + let first = (bits as usize) << (LOOKUP_BITS - length); + let mut slot = first; + while slot < first + span { + assert!(lookup[slot] == 0, "prefix table is not prefix-free"); + lookup[slot] = ((length as u16) << 8) | symbol as u16; + slot += 1; + } + index += 1; + } + Self { max_length, lookup } } } -pub(super) fn decode( +/// Decodes one prefix-coded symbol with bit-serial error semantics. +/// +/// A truncated packet reports `UnexpectedEnd` at the bit where a serial +/// decoder would stop, and an unmatched full-length prefix reports +/// `InvalidVlc` at the code start. Neither error consumes input. +pub(crate) fn decode( reader: &mut PacketBitReader<'_>, syntax: &'static str, - codes: &[VlcCode], + table: &PrefixTable, ) -> Result { - let start = reader.bit_position(); - let max_length = codes.iter().map(|code| code.length).max().unwrap_or(0); - let mut prefix = 0_u16; - for length in 1..=max_length { - prefix = (prefix << 1) | u16::from(reader.read_bit()?); - if let Some(code) = codes - .iter() - .find(|code| code.length == length && code.bits == prefix) - { - return Ok(code.symbol); - } + let remaining = reader.bits_remaining(); + let entry = table.lookup[usize::from(reader.peek_byte())]; + let [length, symbol] = entry.to_be_bytes(); + if entry != 0 && usize::from(length) <= remaining { + reader.skip_bits(length)?; + return Ok(symbol); + } + if remaining < usize::from(table.max_length) { + return Err(reader.unexpected_end_after(remaining)); } Err(EntropyError::InvalidVlc { syntax, - bit_position: start, + bit_position: reader.bit_position(), }) } -pub(super) const ABS_LEVEL: [&[VlcCode]; 2] = [ +const fn c(bits: u16, length: u8, symbol: u8) -> VlcCode { + (bits, length, symbol) +} + +const ABS_LEVEL_CODES: [&[VlcCode]; 2] = [ &[ c(0b01, 2, 0), c(0b10, 2, 1), @@ -61,11 +95,11 @@ pub(super) const ABS_LEVEL: [&[VlcCode]; 2] = [ ], ]; -pub(super) const RUN_VALUE_2: &[VlcCode] = &[c(0b1, 1, 1), c(0b0, 1, 2)]; -pub(super) const RUN_VALUE_3: &[VlcCode] = &[c(0b1, 1, 1), c(0b01, 2, 2), c(0b00, 2, 3)]; -pub(super) const RUN_VALUE_4: &[VlcCode] = +const RUN_VALUE_2_CODES: &[VlcCode] = &[c(0b1, 1, 1), c(0b0, 1, 2)]; +const RUN_VALUE_3_CODES: &[VlcCode] = &[c(0b1, 1, 1), c(0b01, 2, 2), c(0b00, 2, 3)]; +const RUN_VALUE_4_CODES: &[VlcCode] = &[c(0b1, 1, 1), c(0b01, 2, 2), c(0b001, 3, 3), c(0b000, 3, 4)]; -pub(super) const RUN_INDEX: &[VlcCode] = &[ +const RUN_INDEX_CODES: &[VlcCode] = &[ c(0b1, 1, 0), c(0b01, 2, 1), c(0b001, 3, 2), @@ -73,7 +107,7 @@ pub(super) const RUN_INDEX: &[VlcCode] = &[ c(0b0001, 4, 4), ]; -pub(super) const INDEX_A: [&[VlcCode]; 4] = [ +const INDEX_A_CODES: [&[VlcCode]; 4] = [ &[ c(0b1, 1, 0), c(0b00000, 5, 1), @@ -108,10 +142,9 @@ pub(super) const INDEX_A: [&[VlcCode]; 4] = [ ], ]; -pub(super) const INDEX_B: &[VlcCode] = - &[c(0b0, 1, 0), c(0b10, 2, 2), c(0b110, 3, 1), c(0b111, 3, 3)]; +const INDEX_B_CODES: &[VlcCode] = &[c(0b0, 1, 0), c(0b10, 2, 2), c(0b110, 3, 1), c(0b111, 3, 3)]; -pub(super) const FIRST_INDEX: [&[VlcCode]; 5] = [ +const FIRST_INDEX_CODES: [&[VlcCode]; 5] = [ &[ c(0b00001, 5, 0), c(0b00_0001, 6, 1), @@ -184,37 +217,100 @@ pub(super) const FIRST_INDEX: [&[VlcCode]; 5] = [ ], ]; +pub(super) static ABS_LEVEL: [PrefixTable; 2] = [ + PrefixTable::new(ABS_LEVEL_CODES[0]), + PrefixTable::new(ABS_LEVEL_CODES[1]), +]; +pub(super) static RUN_VALUE_2: PrefixTable = PrefixTable::new(RUN_VALUE_2_CODES); +pub(super) static RUN_VALUE_3: PrefixTable = PrefixTable::new(RUN_VALUE_3_CODES); +pub(super) static RUN_VALUE_4: PrefixTable = PrefixTable::new(RUN_VALUE_4_CODES); +pub(super) static RUN_INDEX: PrefixTable = PrefixTable::new(RUN_INDEX_CODES); +pub(super) static INDEX_A: [PrefixTable; 4] = [ + PrefixTable::new(INDEX_A_CODES[0]), + PrefixTable::new(INDEX_A_CODES[1]), + PrefixTable::new(INDEX_A_CODES[2]), + PrefixTable::new(INDEX_A_CODES[3]), +]; +pub(super) static INDEX_B: PrefixTable = PrefixTable::new(INDEX_B_CODES); +pub(super) static FIRST_INDEX: [PrefixTable; 5] = [ + PrefixTable::new(FIRST_INDEX_CODES[0]), + PrefixTable::new(FIRST_INDEX_CODES[1]), + PrefixTable::new(FIRST_INDEX_CODES[2]), + PrefixTable::new(FIRST_INDEX_CODES[3]), + PrefixTable::new(FIRST_INDEX_CODES[4]), +]; + #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; - fn assert_decodes_table(syntax: &'static str, table: &[VlcCode]) { - for code in table { - let shifted = code.bits << (8 - code.length); - let bytes = [u8::try_from(shifted).unwrap()]; - let mut reader = - PacketBitReader::with_bit_length(&bytes, usize::from(code.length)).unwrap(); - assert_eq!(decode(&mut reader, syntax, table).unwrap(), code.symbol); - assert_eq!(reader.bits_remaining(), 0); + /// The normative bit-serial search that the lookup tables replace. + pub(crate) fn serial_decode( + reader: &mut PacketBitReader<'_>, + syntax: &'static str, + codes: &[VlcCode], + ) -> Result { + let start = reader.bit_position(); + let max_length = codes.iter().map(|code| code.1).max().unwrap_or(0); + let mut prefix = 0_u16; + for length in 1..=max_length { + prefix = (prefix << 1) | u16::from(reader.read_bit()?); + if let Some(code) = codes + .iter() + .find(|code| code.1 == length && code.0 == prefix) + { + return Ok(code.2); + } + } + Err(EntropyError::InvalidVlc { + syntax, + bit_position: start, + }) + } + + /// Compares table and serial decoding for every 16-bit input, offset, and truncation. + pub(crate) fn assert_matches_serial(syntax: &'static str, codes: &[VlcCode]) { + let table = PrefixTable::new(codes); + for pattern in 0_u16..=u16::MAX { + let bytes = pattern.to_be_bytes(); + for offset in [0_usize, 3] { + for bit_length in offset..=16 { + let mut serial = PacketBitReader::with_bit_length(&bytes, bit_length).unwrap(); + let mut lookup = serial.clone(); + if offset > 0 { + serial.read_bits(3).unwrap(); + lookup.read_bits(3).unwrap(); + } + let expected = serial_decode(&mut serial, syntax, codes); + let actual = decode(&mut lookup, syntax, &table); + assert_eq!( + actual, expected, + "{syntax} {pattern:016b} @{offset}/{bit_length}" + ); + if expected.is_ok() { + assert_eq!(lookup.bit_position(), serial.bit_position()); + } + } + } } } #[test] - fn decodes_every_normative_vlc_entry() { - for table in ABS_LEVEL { - assert_decodes_table("ABS_LEVEL_INDEX", table); + fn lookup_tables_match_serial_prefix_search() { + for codes in ABS_LEVEL_CODES { + assert_matches_serial("ABS_LEVEL_INDEX", codes); } - for table in INDEX_A { - assert_decodes_table("INDEX_A", table); + for codes in INDEX_A_CODES { + assert_matches_serial("INDEX_A", codes); } - for table in FIRST_INDEX { - assert_decodes_table("FIRST_INDEX", table); + for codes in FIRST_INDEX_CODES { + assert_matches_serial("FIRST_INDEX", codes); } - assert_decodes_table("RUN_VALUE", RUN_VALUE_2); - assert_decodes_table("RUN_VALUE", RUN_VALUE_3); - assert_decodes_table("RUN_VALUE", RUN_VALUE_4); - assert_decodes_table("RUN_INDEX", RUN_INDEX); - assert_decodes_table("INDEX_B", INDEX_B); + assert_matches_serial("RUN_VALUE", RUN_VALUE_2_CODES); + assert_matches_serial("RUN_VALUE", RUN_VALUE_3_CODES); + assert_matches_serial("RUN_VALUE", RUN_VALUE_4_CODES); + assert_matches_serial("RUN_INDEX", RUN_INDEX_CODES); + assert_matches_serial("INDEX_B", INDEX_B_CODES); } #[test] @@ -230,9 +326,10 @@ mod tests { let bytes = [u8::try_from(bits << (8 - length)).unwrap()]; let mut reader = PacketBitReader::with_bit_length(&bytes, usize::from(length)).unwrap(); assert_eq!( - decode(&mut reader, "FIRST_INDEX", FIRST_INDEX[table]).unwrap(), + decode(&mut reader, "FIRST_INDEX", &FIRST_INDEX[table]).unwrap(), expected ); + assert_eq!(reader.bits_remaining(), 0); } } } diff --git a/crates/jxr-native/src/tile_decode/cbphp.rs b/crates/jxr-native/src/tile_decode/cbphp.rs index 6929b7f..d76635b 100644 --- a/crates/jxr-native/src/tile_decode/cbphp.rs +++ b/crates/jxr-native/src/tile_decode/cbphp.rs @@ -2,11 +2,11 @@ use jxr_core::ChromaSampling; -use crate::entropy::PacketBitReader; +use crate::entropy::{PacketBitReader, PrefixTable, decode_prefix}; use super::TileDecodeError; -const NUM_CODES: [[(u16, u8, u8); 5]; 2] = [ +const NUM_CODE_LISTS: [[(u16, u8, u8); 5]; 2] = [ [ (0b1, 1, 0), (0b01, 2, 1), @@ -22,7 +22,7 @@ const NUM_CODES: [[(u16, u8, u8); 5]; 2] = [ (0b011, 3, 4), ], ]; -const NUM_CODES_YUV: [[(u16, u8, u8); 9]; 2] = [ +const NUM_CODE_LISTS_YUV: [[(u16, u8, u8); 9]; 2] = [ [ (0b010, 3, 0), (0b00000, 5, 1), @@ -46,10 +46,10 @@ const NUM_CODES_YUV: [[(u16, u8, u8); 9]; 2] = [ (0b000_0001, 7, 8), ], ]; -const CHROMA_CODES: [(u16, u8, u8); 3] = [(0b1, 1, 0), (0b01, 2, 1), (0b00, 2, 2)]; -const CHROMA_BLOCK_CODES: [(u16, u8, u8); 4] = +const CHROMA_CODE_LIST: [(u16, u8, u8); 3] = [(0b1, 1, 0), (0b01, 2, 1), (0b00, 2, 2)]; +const CHROMA_BLOCK_CODE_LIST: [(u16, u8, u8); 4] = [(0b1, 1, 0), (0b01, 2, 1), (0b000, 3, 2), (0b001, 3, 3)]; -const REF_TWO: [(u16, u8, u8); 6] = [ +const REF_TWO_CODE_LIST: [(u16, u8, u8); 6] = [ (0b00, 2, 3), (0b01, 2, 5), (0b100, 3, 6), @@ -57,6 +57,17 @@ const REF_TWO: [(u16, u8, u8); 6] = [ (0b110, 3, 10), (0b111, 3, 12), ]; +static NUM_CODES: [PrefixTable; 2] = [ + PrefixTable::new(&NUM_CODE_LISTS[0]), + PrefixTable::new(&NUM_CODE_LISTS[1]), +]; +static NUM_CODES_YUV: [PrefixTable; 2] = [ + PrefixTable::new(&NUM_CODE_LISTS_YUV[0]), + PrefixTable::new(&NUM_CODE_LISTS_YUV[1]), +]; +static CHROMA_CODES: PrefixTable = PrefixTable::new(&CHROMA_CODE_LIST); +static CHROMA_BLOCK_CODES: PrefixTable = PrefixTable::new(&CHROMA_BLOCK_CODE_LIST); +static REF_TWO: PrefixTable = PrefixTable::new(&REF_TWO_CODE_LIST); const DELTA: [i8; 5] = [0, -1, 0, 1, 1]; const DELTA_YUV: [i8; 9] = [2, 2, 1, 1, -1, -2, -2, -2, -3]; const BLOCK_OUTPUT: [u16; 16] = [0, 15, 3, 12, 1, 2, 4, 8, 5, 6, 9, 10, 7, 11, 13, 14]; @@ -364,34 +375,32 @@ fn refine(reader: &mut PacketBitReader<'_>, count: u8) -> Result( +fn read_code( reader: &mut PacketBitReader<'_>, - table: &[(u16, u8, u8); N], + table: &PrefixTable, syntax: &'static str, ) -> Result { - let start = reader.bit_position(); - let max_length = table.iter().map(|entry| entry.1).max().unwrap_or(0); - let mut bits = 0_u16; - for length in 1..=max_length { - bits = (bits << 1) | u16::from(reader.read_bit()?); - if let Some(entry) = table - .iter() - .find(|entry| entry.1 == length && entry.0 == bits) - { - return Ok(entry.2); - } - } - Err(crate::entropy::EntropyError::InvalidVlc { - syntax, - bit_position: start, - } - .into()) + Ok(decode_prefix(reader, syntax, table)?) } #[cfg(test)] mod tests { use super::*; + #[test] + fn cbphp_prefix_tables_match_serial_search() { + use crate::entropy::assert_matches_serial; + for codes in &NUM_CODE_LISTS { + assert_matches_serial("NUM_CBPHP", codes); + } + for codes in &NUM_CODE_LISTS_YUV { + assert_matches_serial("NUM_BLKCBPHP", codes); + } + assert_matches_serial("CHR_CBPHP", &CHROMA_CODE_LIST); + assert_matches_serial("NUM_CH_BLK", &CHROMA_BLOCK_CODE_LIST); + assert_matches_serial("REF_CBPHP1", &REF_TWO_CODE_LIST); + } + #[test] fn initial_zero_residual_predicts_normative_checker_pattern() { let mut prediction = PredictionState::new(); diff --git a/crates/jxr-native/src/tile_decode/yuv/syntax.rs b/crates/jxr-native/src/tile_decode/yuv/syntax.rs index b19c04f..5e50425 100644 --- a/crates/jxr-native/src/tile_decode/yuv/syntax.rs +++ b/crates/jxr-native/src/tile_decode/yuv/syntax.rs @@ -3,8 +3,9 @@ use jxr_core::{ChromaSampling, PredictionMode}; use crate::entropy::{ - ColourModel, ComponentClass, FrequencyBand, PacketBitReader, TileEntropyState, decode_ac_block, - decode_dc_coefficient, decode_lp_refinement, decode_lp_refinement_at, + ColourModel, ComponentClass, FrequencyBand, PacketBitReader, PrefixTable, TileEntropyState, + decode_ac_block, decode_dc_coefficient, decode_lp_refinement, decode_lp_refinement_at, + decode_prefix, }; use super::super::{ @@ -15,7 +16,7 @@ use super::super::{ }, }; -const DC_PATTERN: [(u16, u8, u8); 8] = [ +const DC_PATTERN_CODES: [(u16, u8, u8); 8] = [ (0b10, 2, 0), (0b001, 3, 1), (0b00001, 5, 2), @@ -25,7 +26,7 @@ const DC_PATTERN: [(u16, u8, u8); 8] = [ (0b00000, 5, 6), (0b011, 3, 7), ]; -const LP_PATTERN: [(u16, u8, u8); 8] = [ +const LP_PATTERN_CODES: [(u16, u8, u8); 8] = [ (0b0, 1, 0), (0b100, 3, 1), (0b1010, 4, 2), @@ -35,9 +36,13 @@ const LP_PATTERN: [(u16, u8, u8); 8] = [ (0b1110, 4, 6), (0b1111, 4, 7), ]; -const LP_PATTERN_SUBSAMPLED: [(u16, u8, u8); 4] = +const LP_PATTERN_SUBSAMPLED_CODES: [(u16, u8, u8); 4] = [(0b0, 1, 0), (0b10, 2, 1), (0b110, 3, 2), (0b111, 3, 3)]; +static DC_PATTERN: PrefixTable = PrefixTable::new(&DC_PATTERN_CODES); +static LP_PATTERN: PrefixTable = PrefixTable::new(&LP_PATTERN_CODES); +static LP_PATTERN_SUBSAMPLED: PrefixTable = PrefixTable::new(&LP_PATTERN_SUBSAMPLED_CODES); + pub(in crate::tile_decode) struct CbplpState { sampling: ChromaSampling, zero: i8, @@ -421,34 +426,26 @@ fn predict_subsampled_chroma( Ok(()) } -fn read_code( +fn read_code( reader: &mut PacketBitReader<'_>, - table: &[(u16, u8, u8); N], + table: &PrefixTable, syntax: &'static str, ) -> Result { - let start = reader.bit_position(); - let max = table.iter().map(|entry| entry.1).max().unwrap_or(0); - let mut bits = 0_u16; - for length in 1..=max { - bits = (bits << 1) | u16::from(reader.read_bit()?); - if let Some(entry) = table - .iter() - .find(|entry| entry.1 == length && entry.0 == bits) - { - return Ok(entry.2); - } - } - Err(crate::entropy::EntropyError::InvalidVlc { - syntax, - bit_position: start, - } - .into()) + Ok(decode_prefix(reader, syntax, table)?) } #[cfg(test)] mod tests { use super::*; + #[test] + fn yuv_pattern_prefix_tables_match_serial_search() { + use crate::entropy::assert_matches_serial; + assert_matches_serial("VAL_DC_YUV", &DC_PATTERN_CODES); + assert_matches_serial("CBPLP_YUV1", &LP_PATTERN_CODES); + assert_matches_serial("CBPLP_YUV1", &LP_PATTERN_SUBSAMPLED_CODES); + } + #[test] fn fixed_length_cbplp_does_not_apply_vlc_inversion() { let mut reader = PacketBitReader::with_bit_length(&[0b1110_0000], 3).unwrap(); diff --git a/crates/jxr-test-support/Cargo.toml b/crates/jxr-test-support/Cargo.toml index d32600e..8bb5e37 100644 --- a/crates/jxr-test-support/Cargo.toml +++ b/crates/jxr-test-support/Cargo.toml @@ -14,8 +14,8 @@ metal = ["jxr/metal"] cuda = ["jxr/cuda"] [dependencies] -jxr = { path = "../jxr", version = "=0.1.2" } -jxr-native = { path = "../jxr-native", version = "=0.1.1" } +jxr = { path = "../jxr", version = "=0.2.0" } +jxr-native = { path = "../jxr-native", version = "=0.2.0" } rayon.workspace = true thiserror.workspace = true diff --git a/crates/jxr/Cargo.toml b/crates/jxr/Cargo.toml index 2675083..659e851 100644 --- a/crates/jxr/Cargo.toml +++ b/crates/jxr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr" description = "JPEG XR decoder and Annex-A writer with optional Metal and CUDA reconstruction" -version = "0.1.2" +version = "0.2.0" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -18,10 +18,10 @@ metal = ["dep:jxr-metal"] cuda = ["dep:jxr-cuda"] [dependencies] -jxr-core = { path = "../jxr-core", version = "=0.1.1" } -jxr-native = { path = "../jxr-native", version = "=0.1.1" } -jxr-metal = { path = "../jxr-metal", version = "=0.1.2", optional = true } -jxr-cuda = { path = "../jxr-cuda", version = "=0.1.1", optional = true } +jxr-core = { path = "../jxr-core", version = "=0.2.0" } +jxr-native = { path = "../jxr-native", version = "=0.2.0" } +jxr-metal = { path = "../jxr-metal", version = "=0.2.0", optional = true } +jxr-cuda = { path = "../jxr-cuda", version = "=0.2.0", optional = true } rayon.workspace = true thiserror.workspace = true From 20818a4e8ba3e54f00c3e584be730452558941c9 Mon Sep 17 00:00:00 2001 From: GF Date: Sat, 26 Sep 2026 03:13:53 -0400 Subject: [PATCH 2/2] build: restore crate versions changed outside this PR The previous commit unintentionally included workspace crate version bumps to 0.2.0 and the matching lockfile entries. Version changes belong to a release PR, so this restores every manifest and Cargo.lock to main. --- Cargo.lock | 14 +++++++------- crates/jxr-core/Cargo.toml | 2 +- crates/jxr-cuda/Cargo.toml | 4 ++-- crates/jxr-image/Cargo.toml | 4 ++-- crates/jxr-metal/Cargo.toml | 4 ++-- crates/jxr-mpsgraph/Cargo.toml | 8 ++++---- crates/jxr-native/Cargo.toml | 4 ++-- crates/jxr-test-support/Cargo.toml | 4 ++-- crates/jxr/Cargo.toml | 10 +++++----- 9 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e531a00..15ac94f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -146,7 +146,7 @@ dependencies = [ [[package]] name = "jxr" -version = "0.2.0" +version = "0.1.2" dependencies = [ "jxr-core", "jxr-cuda", @@ -158,14 +158,14 @@ dependencies = [ [[package]] name = "jxr-core" -version = "0.2.0" +version = "0.1.1" dependencies = [ "j2k-core", ] [[package]] name = "jxr-cuda" -version = "0.2.0" +version = "0.1.1" dependencies = [ "cudarc", "jxr-core", @@ -175,7 +175,7 @@ dependencies = [ [[package]] name = "jxr-image" -version = "0.2.0" +version = "0.1.1" dependencies = [ "image", "jxr", @@ -188,7 +188,7 @@ version = "0.1.0" [[package]] name = "jxr-metal" -version = "0.2.0" +version = "0.1.2" dependencies = [ "j2k-core", "j2k-metal-support", @@ -202,7 +202,7 @@ dependencies = [ [[package]] name = "jxr-mpsgraph" -version = "0.2.0" +version = "0.1.1" dependencies = [ "j2k-metal-support", "j2k-mpsgraph-support", @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "jxr-native" -version = "0.2.0" +version = "0.1.1" dependencies = [ "fearless_simd", "jxr-core", diff --git a/crates/jxr-core/Cargo.toml b/crates/jxr-core/Cargo.toml index 858cc9a..f2c3c8f 100644 --- a/crates/jxr-core/Cargo.toml +++ b/crates/jxr-core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-core" description = "Shared JPEG XR decoder contracts" -version = "0.2.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true diff --git a/crates/jxr-cuda/Cargo.toml b/crates/jxr-cuda/Cargo.toml index 95b57ad..3f94554 100644 --- a/crates/jxr-cuda/Cargo.toml +++ b/crates/jxr-cuda/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-cuda" description = "CUDA reconstruction backend for the JXR decoder" -version = "0.2.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -10,7 +10,7 @@ homepage.workspace = true [dependencies] cudarc.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.2.0" } +jxr-core = { path = "../jxr-core", version = "=0.1.1" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } thiserror.workspace = true diff --git a/crates/jxr-image/Cargo.toml b/crates/jxr-image/Cargo.toml index 506360c..bcddb2c 100644 --- a/crates/jxr-image/Cargo.toml +++ b/crates/jxr-image/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-image" description = "Lossless ownership adapter from JPEG XR decode results to image::DynamicImage" -version = "0.2.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] image.workspace = true -jxr = { path = "../jxr", version = "=0.2.0" } +jxr = { path = "../jxr", version = "=0.1.2" } thiserror.workspace = true [lints] diff --git a/crates/jxr-metal/Cargo.toml b/crates/jxr-metal/Cargo.toml index 2e5bbf4..00a61ac 100644 --- a/crates/jxr-metal/Cargo.toml +++ b/crates/jxr-metal/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-metal" description = "Metal execution boundary for the JXR decoder" -version = "0.2.0" +version = "0.1.2" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -11,7 +11,7 @@ homepage.workspace = true [dependencies] j2k-core.workspace = true j2k-metal-support.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.2.0" } +jxr-core = { path = "../jxr-core", version = "=0.1.1" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } thiserror.workspace = true diff --git a/crates/jxr-mpsgraph/Cargo.toml b/crates/jxr-mpsgraph/Cargo.toml index 697b71e..d70879c 100644 --- a/crates/jxr-mpsgraph/Cargo.toml +++ b/crates/jxr-mpsgraph/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-mpsgraph" description = "Direct MPSGraph tensor adapter for Metal-resident JPEG XR batches" -version = "0.2.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -13,9 +13,9 @@ readme = "README.md" [dependencies] j2k-metal-support.workspace = true -jxr = { path = "../jxr", version = "=0.2.0", features = ["metal"] } -jxr-core = { path = "../jxr-core", version = "=0.2.0" } -jxr-metal = { path = "../jxr-metal", version = "=0.2.0" } +jxr = { path = "../jxr", version = "=0.1.2", features = ["metal"] } +jxr-core = { path = "../jxr-core", version = "=0.1.1" } +jxr-metal = { path = "../jxr-metal", version = "=0.1.2" } thiserror.workspace = true [target.'cfg(all(target_arch = "aarch64", target_os = "macos"))'.dependencies] diff --git a/crates/jxr-native/Cargo.toml b/crates/jxr-native/Cargo.toml index bdda3b5..391c716 100644 --- a/crates/jxr-native/Cargo.toml +++ b/crates/jxr-native/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr-native" description = "Safe Rust JPEG XR parser, CPU decoder, and Annex-A writer" -version = "0.2.0" +version = "0.1.1" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -10,7 +10,7 @@ homepage.workspace = true [dependencies] fearless_simd.workspace = true -jxr-core = { path = "../jxr-core", version = "=0.2.0" } +jxr-core = { path = "../jxr-core", version = "=0.1.1" } jxr-math = { path = "../jxr-math", version = "=0.1.0" } rayon.workspace = true diff --git a/crates/jxr-test-support/Cargo.toml b/crates/jxr-test-support/Cargo.toml index 8bb5e37..d32600e 100644 --- a/crates/jxr-test-support/Cargo.toml +++ b/crates/jxr-test-support/Cargo.toml @@ -14,8 +14,8 @@ metal = ["jxr/metal"] cuda = ["jxr/cuda"] [dependencies] -jxr = { path = "../jxr", version = "=0.2.0" } -jxr-native = { path = "../jxr-native", version = "=0.2.0" } +jxr = { path = "../jxr", version = "=0.1.2" } +jxr-native = { path = "../jxr-native", version = "=0.1.1" } rayon.workspace = true thiserror.workspace = true diff --git a/crates/jxr/Cargo.toml b/crates/jxr/Cargo.toml index 659e851..2675083 100644 --- a/crates/jxr/Cargo.toml +++ b/crates/jxr/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "jxr" description = "JPEG XR decoder and Annex-A writer with optional Metal and CUDA reconstruction" -version = "0.2.0" +version = "0.1.2" edition.workspace = true rust-version.workspace = true license.workspace = true @@ -18,10 +18,10 @@ metal = ["dep:jxr-metal"] cuda = ["dep:jxr-cuda"] [dependencies] -jxr-core = { path = "../jxr-core", version = "=0.2.0" } -jxr-native = { path = "../jxr-native", version = "=0.2.0" } -jxr-metal = { path = "../jxr-metal", version = "=0.2.0", optional = true } -jxr-cuda = { path = "../jxr-cuda", version = "=0.2.0", optional = true } +jxr-core = { path = "../jxr-core", version = "=0.1.1" } +jxr-native = { path = "../jxr-native", version = "=0.1.1" } +jxr-metal = { path = "../jxr-metal", version = "=0.1.2", optional = true } +jxr-cuda = { path = "../jxr-cuda", version = "=0.1.1", optional = true } rayon.workspace = true thiserror.workspace = true