From 3f2066bb2096adc8faec5d17c6c3eba39a0dc9ee Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Wed, 25 Feb 2026 23:05:35 +0000 Subject: [PATCH 1/4] Implement floating-point predictor (Predictor 3) encoding Add support for encoding TIFF images with the floating-point predictor, which improves compression ratios for floating-point image data. The floating-point predictor works by: 1. Converting floats to big-endian bytes 2. Shuffling bytes so all first bytes are together, all second bytes, etc. 3. Applying byte-level horizontal differencing across the buffer This complements the existing decoding support for Predictor 3. Supported types: Gray32Float, Gray64Float, RGB32Float, RGB64Float, RGBA32Float, RGBA64Float, CMYK32Float, CMYK64Float. Co-Authored-By: Claude Opus 4.5 --- src/encoder/colortype.rs | 121 +++++++++++++++++++++++++++++++--- src/encoder/mod.rs | 19 ++++-- src/tags.rs | 4 +- tests/encode_images.rs | 137 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 267 insertions(+), 14 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index 7bb45d0..b8a09a3 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -1,5 +1,66 @@ use crate::tags::{PhotometricInterpretation, SampleFormat}; +/// Apply floating-point predictor encoding to a row of f32 samples. +/// +/// The floating-point predictor works by: +/// 1. Converting each f32 to big-endian bytes +/// 2. Shuffling bytes so all first bytes are together, all second bytes, etc. +/// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer +/// +/// The decoder reverses this by: +/// 1. Applying cumulative sum (wrapping_add) on the entire byte stream +/// 2. De-shuffling bytes back into f32 values +fn fp_predict_f32(row: &[f32], samples: usize, result: &mut Vec) { + let num_values = row.len(); + let byte_count = num_values * 4; + let start_len = result.len(); + + // Reserve space for the output + result.reserve(byte_count); + + // Step 1 & 2: Convert to big-endian and shuffle bytes + // All first bytes, then all second bytes, etc. + for byte_idx in 0..4 { + for &value in row { + let bytes = value.to_be_bytes(); + result.push(bytes[byte_idx]); + } + } + + // Step 3: Apply byte-level horizontal difference across the entire buffer + // We iterate backwards so that when we compute output[i] - output[i-samples], + // output[i-samples] still has its original value. + // The prediction spans across quarter boundaries, just like the decoder's cumulative sum. + let output = &mut result[start_len..]; + for i in (samples..byte_count).rev() { + output[i] = output[i].wrapping_sub(output[i - samples]); + } +} + +/// Apply floating-point predictor encoding to a row of f64 samples. +fn fp_predict_f64(row: &[f64], samples: usize, result: &mut Vec) { + let num_values = row.len(); + let byte_count = num_values * 8; + let start_len = result.len(); + + // Reserve space for the output + result.reserve(byte_count); + + // Step 1 & 2: Convert to big-endian and shuffle bytes + for byte_idx in 0..8 { + for &value in row { + let bytes = value.to_be_bytes(); + result.push(bytes[byte_idx]); + } + } + + // Step 3: Apply byte-level horizontal difference across the entire buffer + let output = &mut result[start_len..]; + for i in (samples..byte_count).rev() { + output[i] = output[i].wrapping_sub(output[i - samples]); + } +} + macro_rules! integer_horizontal_predict { () => { fn horizontal_predict(row: &[Self::Inner], result: &mut Vec) { @@ -23,6 +84,10 @@ macro_rules! integer_horizontal_predict { .map(|(prev, current)| current.wrapping_sub(*prev)), ); } + + fn floating_point_predict(_: &[Self::Inner], _: &mut Vec) { + unreachable!("floating-point predictor is only valid for floating-point sample types") + } }; } @@ -38,6 +103,10 @@ pub trait ColorType { const SAMPLE_FORMAT: &'static [SampleFormat]; fn horizontal_predict(row: &[Self::Inner], result: &mut Vec); + + /// Apply floating-point predictor encoding to a row of samples. + /// This is only implemented for floating-point types; integer types will panic. + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec); } pub struct Gray8; @@ -108,7 +177,11 @@ impl ColorType for Gray32Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -140,7 +213,11 @@ impl ColorType for Gray64Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -180,8 +257,13 @@ impl ColorType for RGB32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -201,8 +283,13 @@ impl ColorType for RGB64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 3]; + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -242,8 +329,13 @@ impl ColorType for RGBA32Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[32, 32, 32, 32]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -263,8 +355,13 @@ impl ColorType for RGBA64Float { const TIFF_VALUE: PhotometricInterpretation = PhotometricInterpretation::RGB; const BITS_PER_SAMPLE: &'static [u16] = &[64, 64, 64, 64]; const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; + fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -306,7 +403,11 @@ impl ColorType for CMYK32Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f32(row, Self::SAMPLE_FORMAT.len(), result) } } @@ -328,7 +429,11 @@ impl ColorType for CMYK64Float { const SAMPLE_FORMAT: &'static [SampleFormat] = &[SampleFormat::IEEEFP; 4]; fn horizontal_predict(_: &[Self::Inner], _: &mut Vec) { - unreachable!() + unreachable!("horizontal predictor is not valid for floating-point sample types") + } + + fn floating_point_predict(row: &[Self::Inner], result: &mut Vec) { + fp_predict_f64(row, Self::SAMPLE_FORMAT.len(), result) } } diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index b043a4d..6a1f6d1 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -33,9 +33,10 @@ use self::writer::*; /// Image data can be very unpredictable, and thus very hard to compress. Predictors are simple /// passes ran over the image data to prepare it for compression. This is mostly used for LZW /// compression, where using [Predictor::Horizontal] we see a 35% improvement in compression -/// ratio over the unpredicted compression ! +/// ratio over the unpredicted compression! /// -/// [Predictor::FloatingPoint] is currently not supported. +/// [Predictor::Horizontal] is for integer sample types. +/// [Predictor::FloatingPoint] is for floating-point sample types (f32, f64). pub type Predictor = crate::tags::Predictor; #[cfg(feature = "deflate")] pub type DeflateLevel = compression::DeflateLevel; @@ -635,11 +636,14 @@ pub struct ImageEncoder<'a, W: 'a + Write + Seek, C: ColorType, K: TiffKind> { impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, K> { fn sanity_check(compression: Compression, predictor: Predictor) -> TiffResult<()> { match (predictor, compression, T::SAMPLE_FORMAT[0]) { + // Horizontal predictor is not valid for floating-point types (Predictor::Horizontal, _, SampleFormat::IEEEFP | SampleFormat::Void) => { Err(TiffError::UsageError(UsageError::PredictorIncompatible)) } + // Floating-point predictor is only valid for IEEEFP sample format + (Predictor::FloatingPoint, _, SampleFormat::IEEEFP) => Ok(()), (Predictor::FloatingPoint, _, _) => { - Err(TiffError::UsageError(UsageError::PredictorUnavailable)) + Err(TiffError::UsageError(UsageError::PredictorIncompatible)) } _ => Ok(()), } @@ -773,7 +777,14 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, } self.encoder.write_data(row_result.as_slice())? } - _ => unimplemented!(), + Predictor::FloatingPoint => { + let byte_size = mem::size_of::(); + let mut row_result = Vec::with_capacity(value.len() * byte_size); + for row in value.chunks_exact(self.row_samples as usize) { + T::floating_point_predict(row, &mut row_result); + } + self.encoder.write_data(row_result.as_slice())? + } }; let byte_count = self.encoder.last_written() as usize; diff --git a/src/tags.rs b/src/tags.rs index ab1ffba..690c30c 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -357,7 +357,9 @@ pub enum Predictor(u16) { /// This means that instead of having in order `[r1, g1. b1, r2, g2 ...]` you will find /// `[r1, g1, b1, r2-r1, g2-g1, b2-b1, r3-r2, g3-g2, ...]` Horizontal = 2, - /// Not currently supported + /// Floating-point predictor for IEEE floating-point sample types. + /// + /// This predictor byte-shuffles the data and applies byte-level horizontal differencing. FloatingPoint = 3, } } diff --git a/tests/encode_images.rs b/tests/encode_images.rs index df0dac9..3fb9dee 100644 --- a/tests/encode_images.rs +++ b/tests/encode_images.rs @@ -1,7 +1,7 @@ extern crate tiff; use tiff::decoder::{ifd, Decoder, DecodingResult}; -use tiff::encoder::{colortype, Ifd, Ifd8, SRational, TiffEncoder}; +use tiff::encoder::{colortype, Ifd, Ifd8, Predictor, SRational, TiffEncoder}; use tiff::tags::Tag; use tiff::ColorType; @@ -379,6 +379,141 @@ fn test_ycbcr_u8_roundtrip() { test_u8_roundtrip::("tiled-jpeg-ycbcr.tif", ColorType::YCbCr(8)); } +/// Test that encoding with the floating-point predictor works correctly +/// and can be decoded back to the original data. +#[test] +fn test_gray_f32_floating_point_predictor() { + // Generate test data: a gradient of f32 values + let width = 32u32; + let height = 32u32; + let mut image_data: Vec = Vec::with_capacity((width * height) as usize); + for y in 0..height { + for x in 0..width { + // Create a gradient pattern + image_data.push((x as f32 + y as f32 * 0.1) / 100.0); + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(32)); + + let mut buffer = DecodingResult::F32(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F32(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (orig, decoded) in image_data.iter().zip(img_res.iter()) { + assert_eq!( + *orig, *decoded, + "Mismatch at value: orig={}, decoded={}", + orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + +/// Test floating-point predictor with RGB f32 data +#[test] +fn test_rgb_f32_floating_point_predictor() { + let width = 16u32; + let height = 16u32; + let mut image_data: Vec = Vec::with_capacity((width * height * 3) as usize); + for y in 0..height { + for x in 0..width { + // RGB pattern + image_data.push(x as f32 / width as f32); // R + image_data.push(y as f32 / height as f32); // G + image_data.push((x + y) as f32 / (width + height) as f32); // B + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::RGB(32)); + + let mut buffer = DecodingResult::F32(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F32(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (i, (orig, decoded)) in image_data.iter().zip(img_res.iter()).enumerate() { + assert_eq!( + *orig, *decoded, + "Mismatch at index {}: orig={}, decoded={}", + i, orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + +/// Test floating-point predictor with f64 data +#[test] +fn test_gray_f64_floating_point_predictor() { + let width = 16u32; + let height = 16u32; + let mut image_data: Vec = Vec::with_capacity((width * height) as usize); + for y in 0..height { + for x in 0..width { + image_data.push((x as f64 + y as f64 * 0.1) / 100.0); + } + } + + let mut file = Cursor::new(Vec::new()); + { + let mut tiff = TiffEncoder::new(&mut file) + .unwrap() + .with_predictor(Predictor::FloatingPoint); + tiff.write_image::(width, height, &image_data) + .unwrap(); + } + + file.seek(SeekFrom::Start(0)).unwrap(); + { + let mut decoder = Decoder::new(&mut file).unwrap(); + assert_eq!(decoder.colortype().unwrap(), ColorType::Gray(64)); + + let mut buffer = DecodingResult::F64(vec![]); + decoder.read_image_to_buffer(&mut buffer).unwrap(); + if let DecodingResult::F64(img_res) = buffer { + assert_eq!(image_data.len(), img_res.len()); + for (orig, decoded) in image_data.iter().zip(img_res.iter()) { + assert_eq!( + *orig, *decoded, + "Mismatch: orig={}, decoded={}", + orig, decoded + ); + } + } else { + panic!("Wrong data type"); + } + } +} + trait AssertDecode { fn assert_tag_u32(&mut self, tag: u16) -> u32; fn assert_tag_u32_vec(&mut self, tag: u16) -> Vec; From fdde3467fc66a5d25277a90c05c1682c6384efdc Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 26 Feb 2026 15:00:21 +0000 Subject: [PATCH 2/4] Apply clippy lint --- src/encoder/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/encoder/mod.rs b/src/encoder/mod.rs index 6a1f6d1..23bf645 100644 --- a/src/encoder/mod.rs +++ b/src/encoder/mod.rs @@ -778,8 +778,7 @@ impl<'a, W: 'a + Write + Seek, T: ColorType, K: TiffKind> ImageEncoder<'a, W, T, self.encoder.write_data(row_result.as_slice())? } Predictor::FloatingPoint => { - let byte_size = mem::size_of::(); - let mut row_result = Vec::with_capacity(value.len() * byte_size); + let mut row_result = Vec::with_capacity(std::mem::size_of_val(value)); for row in value.chunks_exact(self.row_samples as usize) { T::floating_point_predict(row, &mut row_result); } From 8b0da5939aa7b9def186e14f098f0d462a1830d1 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 26 Feb 2026 15:29:29 +0000 Subject: [PATCH 3/4] Deduplicate fp_predict_f32 and fp_predict_f64 with a declarative macro Replace the two near-identical floating-point predictor encoding functions with a single fp_predict! macro parameterized on the float type, using core::mem::size_of to derive the byte size. Co-Authored-By: Claude --- src/encoder/colortype.rs | 80 ++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 48 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index b8a09a3..b717669 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -1,65 +1,49 @@ use crate::tags::{PhotometricInterpretation, SampleFormat}; -/// Apply floating-point predictor encoding to a row of f32 samples. +/// Apply floating-point predictor encoding to a row of floating-point samples. /// /// The floating-point predictor works by: -/// 1. Converting each f32 to big-endian bytes +/// 1. Converting each value to big-endian bytes /// 2. Shuffling bytes so all first bytes are together, all second bytes, etc. /// 3. Applying byte-level horizontal difference (wrapping_sub) across the entire buffer /// /// The decoder reverses this by: /// 1. Applying cumulative sum (wrapping_add) on the entire byte stream -/// 2. De-shuffling bytes back into f32 values -fn fp_predict_f32(row: &[f32], samples: usize, result: &mut Vec) { - let num_values = row.len(); - let byte_count = num_values * 4; - let start_len = result.len(); +/// 2. De-shuffling bytes back into float values +macro_rules! fp_predict { + ($name:ident, $float_type:ty) => { + fn $name(row: &[$float_type], samples: usize, result: &mut Vec) { + const BYTE_SIZE: usize = core::mem::size_of::<$float_type>(); + let num_values = row.len(); + let byte_count = num_values * BYTE_SIZE; + let start_len = result.len(); - // Reserve space for the output - result.reserve(byte_count); + // Reserve space for the output + result.reserve(byte_count); - // Step 1 & 2: Convert to big-endian and shuffle bytes - // All first bytes, then all second bytes, etc. - for byte_idx in 0..4 { - for &value in row { - let bytes = value.to_be_bytes(); - result.push(bytes[byte_idx]); + // Step 1 & 2: Convert to big-endian and shuffle bytes + // All first bytes, then all second bytes, etc. + for byte_idx in 0..BYTE_SIZE { + for &value in row { + let bytes = value.to_be_bytes(); + result.push(bytes[byte_idx]); + } + } + + // Step 3: Apply byte-level horizontal difference across the entire buffer + // We iterate backwards so that when we compute output[i] - output[i-samples], + // output[i-samples] still has its original value. + // The prediction spans across quarter boundaries, just like the decoder's cumulative sum. + let output = &mut result[start_len..]; + for i in (samples..byte_count).rev() { + output[i] = output[i].wrapping_sub(output[i - samples]); + } } - } - - // Step 3: Apply byte-level horizontal difference across the entire buffer - // We iterate backwards so that when we compute output[i] - output[i-samples], - // output[i-samples] still has its original value. - // The prediction spans across quarter boundaries, just like the decoder's cumulative sum. - let output = &mut result[start_len..]; - for i in (samples..byte_count).rev() { - output[i] = output[i].wrapping_sub(output[i - samples]); - } + }; } -/// Apply floating-point predictor encoding to a row of f64 samples. -fn fp_predict_f64(row: &[f64], samples: usize, result: &mut Vec) { - let num_values = row.len(); - let byte_count = num_values * 8; - let start_len = result.len(); - - // Reserve space for the output - result.reserve(byte_count); - - // Step 1 & 2: Convert to big-endian and shuffle bytes - for byte_idx in 0..8 { - for &value in row { - let bytes = value.to_be_bytes(); - result.push(bytes[byte_idx]); - } - } - - // Step 3: Apply byte-level horizontal difference across the entire buffer - let output = &mut result[start_len..]; - for i in (samples..byte_count).rev() { - output[i] = output[i].wrapping_sub(output[i - samples]); - } -} +fp_predict!(fp_predict_f32, f32); +fp_predict!(fp_predict_f64, f64); macro_rules! integer_horizontal_predict { () => { From c74f23784c262b4bfa165a40e586bd9712c50f31 Mon Sep 17 00:00:00 2001 From: "Sergey \"Shnatsel\" Davidoff" Date: Thu, 26 Feb 2026 15:45:21 +0000 Subject: [PATCH 4/4] Vectorize byte-level horizontal differencing in fp_predict Process the difference step in reverse 16-byte chunks, reading both curr and prev into local arrays before writing. This makes all 16 subtractions independent, enabling autovectorization (confirmed with SSE2: movdqu + psubb). Co-Authored-By: Claude --- src/encoder/colortype.rs | 57 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/encoder/colortype.rs b/src/encoder/colortype.rs index b717669..ef4b9a8 100644 --- a/src/encoder/colortype.rs +++ b/src/encoder/colortype.rs @@ -30,12 +30,59 @@ macro_rules! fp_predict { } } - // Step 3: Apply byte-level horizontal difference across the entire buffer - // We iterate backwards so that when we compute output[i] - output[i-samples], - // output[i-samples] still has its original value. + // Step 3: Apply byte-level horizontal difference across the entire buffer. // The prediction spans across quarter boundaries, just like the decoder's cumulative sum. - let output = &mut result[start_len..]; - for i in (samples..byte_count).rev() { + // + // We process elements in reverse in batches of 16 to enable autovectorization. + // Reverse order ensures that when we compute output[i] - output[i-samples], + // the value at output[i-samples] hasn't been overwritten yet (critical when + // samples < 16, which is the common case: 1, 3, 4, 5). + // + // 8 bits * 16 = 128 bits, the width of a typical SIMD register. + // Even if the target has no vector instructions, this still benefits from + // instruction-level parallelism, since unlike in decoding, all subtractions + // within a chunk are truly independent of each other. + let output = &mut result[start_len..byte_count + start_len]; + let diff_len = byte_count - samples; + let remainder_len = diff_len % 16; + // Process full 16-byte chunks in reverse. + // We iterate from the end so that predecessor values (at lower indices) + // are still in their original state when we read them. + let mut chunk_end = byte_count; + while chunk_end >= samples + remainder_len + 16 { + let chunk_start = chunk_end - 16; + let prev_start = chunk_start - samples; + + // Read all values in the chunk and their predecessors first. + // This makes all subtractions below independent, enabling vectorization. + let curr: [u8; 16] = output[chunk_start..chunk_end].try_into().unwrap(); + let prev: [u8; 16] = output[prev_start..prev_start + 16].try_into().unwrap(); + + let chunk = &mut output[chunk_start..chunk_end]; + chunk[0] = curr[0].wrapping_sub(prev[0]); + chunk[1] = curr[1].wrapping_sub(prev[1]); + chunk[2] = curr[2].wrapping_sub(prev[2]); + chunk[3] = curr[3].wrapping_sub(prev[3]); + chunk[4] = curr[4].wrapping_sub(prev[4]); + chunk[5] = curr[5].wrapping_sub(prev[5]); + chunk[6] = curr[6].wrapping_sub(prev[6]); + chunk[7] = curr[7].wrapping_sub(prev[7]); + chunk[8] = curr[8].wrapping_sub(prev[8]); + chunk[9] = curr[9].wrapping_sub(prev[9]); + chunk[10] = curr[10].wrapping_sub(prev[10]); + chunk[11] = curr[11].wrapping_sub(prev[11]); + chunk[12] = curr[12].wrapping_sub(prev[12]); + chunk[13] = curr[13].wrapping_sub(prev[13]); + chunk[14] = curr[14].wrapping_sub(prev[14]); + chunk[15] = curr[15].wrapping_sub(prev[15]); + + chunk_end = chunk_start; + } + // Handle the leading remainder that doesn't fill a complete chunk of 16. + // This must be done last (after the reverse chunk loop) because these are + // the lowest indices, and the chunks above may read predecessor values + // from this range. + for i in (samples..samples + remainder_len).rev() { output[i] = output[i].wrapping_sub(output[i - samples]); } }