Skip to main content

flashraw_codecs/
packed.rs

1//! Unpacking of uncompressed, bit-packed sensor data.
2//!
3//! Every kernel has a scalar reference implementation (`*_scalar`) that is the
4//! specification. Fast paths (word-at-a-time scalar, SSE4.1, NEON, WASM SIMD128)
5//! are tested bit-for-bit against it (see tests and `tests/simd_equivalence.rs`).
6
7use flashraw_core::bits::{BitPumpLsb, BitPumpMsb, BitPumpMsb16, BitPumpMsb32, BitRead};
8use flashraw_core::{Error, PackOrder, Result, par};
9
10/// Description of a packed raster.
11#[derive(Debug, Clone, Copy)]
12pub struct PackedLayout {
13    pub width: usize,
14    pub height: usize,
15    /// Bits per sample (1..=16).
16    pub bits: u32,
17    pub order: PackOrder,
18    /// Bytes from one row start to the next (>= ceil(width * bits / 8)).
19    pub row_stride: usize,
20}
21
22impl PackedLayout {
23    /// Layout with rows tightly packed (no row padding).
24    pub fn tight(width: usize, height: usize, bits: u32, order: PackOrder) -> Self {
25        PackedLayout { width, height, bits, order, row_stride: (width * bits as usize).div_ceil(8) }
26    }
27    pub fn row_bytes(&self) -> usize {
28        (self.width * self.bits as usize).div_ceil(8)
29    }
30}
31
32/// Unpack `src` into `dst` rows of `dst_stride` samples. A source too short for every
33/// row is an error unless `lenient` (recovering truncated data): then complete rows and
34/// the complete samples of a partial final row are unpacked, and the remaining samples
35/// are left untouched (callers pass zeroed buffers).
36pub fn unpack(
37    src: &[u8],
38    layout: PackedLayout,
39    dst: &mut [u16],
40    dst_stride: usize,
41    parallel: bool,
42    simd: bool,
43    lenient: bool,
44) -> Result<()> {
45    let PackedLayout { width, height, bits, row_stride, .. } = layout;
46    if bits == 0 || bits > 16 {
47        return Err(Error::corrupt(format!("unsupported packed bit depth {bits}")));
48    }
49    if dst_stride < width || dst.len() < dst_stride * height.saturating_sub(1) + width.min(dst.len()) {
50        return Err(Error::InvalidArgument("destination too small for packed unpack".into()));
51    }
52    let row_bytes = layout.row_bytes();
53    let avail_rows =
54        src.len().saturating_sub(row_bytes).checked_div(row_stride).map_or(height, |n| (n + 1).min(height));
55    let avail_rows = if src.len() < row_bytes { 0 } else { avail_rows };
56    if avail_rows < height && !lenient {
57        return Err(Error::corrupt(format!("truncated packed data: {avail_rows}/{height} rows present")));
58    }
59    let n = (dst_stride * height).min(dst.len());
60    let dst = &mut dst[..n];
61    let body = |first: usize, chunk: &mut [u16]| {
62        for (i, row) in chunk.chunks_mut(dst_stride).enumerate() {
63            let y = first + i;
64            if y >= avail_rows {
65                // Recovering: the complete samples of a partial row (rows below have no data).
66                if let Some(s) = src.get(y * row_stride..).filter(|s| y == avail_rows && !s.is_empty()) {
67                    unpack_row_partial(s, &mut row[..width], bits, layout.order, simd);
68                }
69                break;
70            }
71            let s = &src[y * row_stride..];
72            let s = &s[..row_bytes.min(s.len())];
73            unpack_row(s, &mut row[..width], bits, layout.order, simd);
74        }
75    };
76    // Unpacking is memory-bound at every bit depth (see par::memory_bound_workers). Plain
77    // 16-bit copies on macOS take one worker: there threads that write fresh pages at the same
78    // time slow each other down (Apple M1 Ultra, 90 MB Nikon D850 frame: 3.5 ms on one thread,
79    // 5.7 to 13.7 ms on 2 to 20).
80    let workers = if bits == 16 && cfg!(target_os = "macos") { 1 } else { par::memory_bound_workers() };
81    par::for_each_row_chunk_bounded(dst, dst_stride, workers, parallel, body);
82    Ok(())
83}
84
85/// Unpack one row of `dst.len()` samples.
86pub fn unpack_row(src: &[u8], dst: &mut [u16], bits: u32, order: PackOrder, simd: bool) {
87    match (bits, order) {
88        (16, PackOrder::LsbBytes) => unpack_u16le(src, dst),
89        (16, PackOrder::MsbBytes) => unpack_u16be(src, dst),
90        (8, PackOrder::MsbBytes | PackOrder::LsbBytes) => unpack_u8(src, dst),
91        (12, PackOrder::MsbBytes) => unpack12_msb(src, dst, simd),
92        (12, PackOrder::LsbBytes) => unpack12_lsb(src, dst, simd),
93        (14, PackOrder::MsbBytes) => unpack14_msb(src, dst),
94        (14, PackOrder::LsbBytes) => unpack14_lsb(src, dst),
95        (10, PackOrder::MsbBytes) => unpack10_msb(src, dst),
96        (10, PackOrder::LsbBytes) => unpack10_lsb(src, dst),
97        _ => unpack_row_scalar(src, dst, bits, order),
98    }
99}
100
101/// Samples of `bits` bits whose bits lie entirely within the first `len` bytes of a
102/// row packed in `order` (word orders need whole words).
103pub fn complete_samples(len: usize, bits: u32, order: PackOrder) -> usize {
104    let usable = match order {
105        PackOrder::Msb16 => len / 2 * 2,
106        PackOrder::Msb32 => len / 4 * 4,
107        PackOrder::MsbBytes | PackOrder::LsbBytes => len,
108    };
109    if bits == 0 { 0 } else { usable * 8 / bits as usize }
110}
111
112/// [`unpack_row`] for a row cut short by truncated input: samples not entirely within
113/// `src` are set to zero instead of being decoded from padding. Returns the number of
114/// complete samples.
115pub fn unpack_row_partial(src: &[u8], dst: &mut [u16], bits: u32, order: PackOrder, simd: bool) -> usize {
116    let n = complete_samples(src.len(), bits, order).min(dst.len());
117    let (done, rest) = dst.split_at_mut(n);
118    unpack_row(src, done, bits, order, simd);
119    rest.fill(0);
120    n
121}
122
123/// Reference implementation for every packing (bit pump based).
124pub fn unpack_row_scalar(src: &[u8], dst: &mut [u16], bits: u32, order: PackOrder) {
125    match order {
126        PackOrder::MsbBytes => {
127            let mut p = BitPumpMsb::new(src);
128            dst.iter_mut().for_each(|d| *d = p.get(bits) as u16);
129        }
130        PackOrder::LsbBytes => {
131            let mut p = BitPumpLsb::new(src);
132            dst.iter_mut().for_each(|d| *d = p.get(bits) as u16);
133        }
134        PackOrder::Msb16 => {
135            let mut p = BitPumpMsb16::new(src);
136            dst.iter_mut().for_each(|d| *d = p.get(bits) as u16);
137        }
138        PackOrder::Msb32 => {
139            let mut p = BitPumpMsb32::new(src);
140            dst.iter_mut().for_each(|d| *d = p.get(bits) as u16);
141        }
142    }
143}
144
145fn unpack_u16le(src: &[u8], dst: &mut [u16]) {
146    let n = dst.len().min(src.len() / 2);
147    #[cfg(target_endian = "little")]
148    {
149        // Little-endian samples on a little-endian machine: a plain copy (memcpy speed).
150        // SAFETY: `dst[..n]` viewed as its `2 * n` bytes; every byte pattern is a valid `u16`.
151        let bytes = unsafe { std::slice::from_raw_parts_mut(dst.as_mut_ptr().cast::<u8>(), n * 2) };
152        bytes.copy_from_slice(&src[..n * 2]);
153    }
154    #[cfg(not(target_endian = "little"))]
155    for (d, s) in dst[..n].iter_mut().zip(src.chunks_exact(2)) {
156        *d = u16::from_le_bytes([s[0], s[1]]);
157    }
158    tail(src, dst, n, 16, PackOrder::LsbBytes);
159}
160
161fn unpack_u16be(src: &[u8], dst: &mut [u16]) {
162    let n = dst.len().min(src.len() / 2);
163    for (d, s) in dst[..n].iter_mut().zip(src.chunks_exact(2)) {
164        *d = u16::from_be_bytes([s[0], s[1]]);
165    }
166    tail(src, dst, n, 16, PackOrder::MsbBytes);
167}
168
169fn unpack_u8(src: &[u8], dst: &mut [u16]) {
170    let n = dst.len().min(src.len());
171    for (d, &s) in dst[..n].iter_mut().zip(src) {
172        *d = s as u16;
173    }
174    // Samples beyond the data read as zero, as with the bit pump.
175    dst[n..].iter_mut().for_each(|d| *d = 0);
176}
177
178/// Fill any samples not covered by complete groups with the scalar reference.
179#[inline]
180fn tail(src: &[u8], dst: &mut [u16], done: usize, bits: u32, order: PackOrder) {
181    if done < dst.len() {
182        let byte = done * bits as usize / 8; // groups are byte aligned
183        unpack_row_scalar(src.get(byte..).unwrap_or(&[]), &mut dst[done..], bits, order);
184    }
185}
186
187fn unpack12_msb(src: &[u8], dst: &mut [u16], simd: bool) {
188    let mut done = 0;
189    if simd {
190        done = simd12::unpack(src, dst, true);
191    }
192    let groups = ((dst.len() - done) / 2).min(src.len().saturating_sub(done / 2 * 3) / 3);
193    for g in 0..groups {
194        let s = &src[(done / 2 + g) * 3..];
195        let (b0, b1, b2) = (s[0] as u16, s[1] as u16, s[2] as u16);
196        dst[done + 2 * g] = (b0 << 4) | (b1 >> 4);
197        dst[done + 2 * g + 1] = ((b1 & 0xF) << 8) | b2;
198    }
199    tail(src, dst, done + groups * 2, 12, PackOrder::MsbBytes);
200}
201
202fn unpack12_lsb(src: &[u8], dst: &mut [u16], simd: bool) {
203    let mut done = 0;
204    if simd {
205        done = simd12::unpack(src, dst, false);
206    }
207    let groups = ((dst.len() - done) / 2).min(src.len().saturating_sub(done / 2 * 3) / 3);
208    for g in 0..groups {
209        let s = &src[(done / 2 + g) * 3..];
210        let (b0, b1, b2) = (s[0] as u16, s[1] as u16, s[2] as u16);
211        dst[done + 2 * g] = b0 | ((b1 & 0xF) << 8);
212        dst[done + 2 * g + 1] = (b1 >> 4) | (b2 << 4);
213    }
214    tail(src, dst, done + groups * 2, 12, PackOrder::LsbBytes);
215}
216
217macro_rules! word_unpack {
218    ($name:ident, $bits:expr, $bytes:expr, $n:expr, $be:expr, $order:expr) => {
219        fn $name(src: &[u8], dst: &mut [u16]) {
220            // $n samples per $bytes bytes; read 8 bytes at a time (requires 8 readable bytes).
221            let groups = (dst.len() / $n).min(src.len().saturating_sub(8 - $bytes) / $bytes);
222            for g in 0..groups {
223                let s = &src[g * $bytes..g * $bytes + 8];
224                let w = if $be {
225                    u64::from_be_bytes(s.try_into().unwrap())
226                } else {
227                    u64::from_le_bytes(s.try_into().unwrap())
228                };
229                let m = (1u64 << $bits) - 1;
230                for k in 0..$n {
231                    dst[g * $n + k] =
232                        if $be { (w >> (64 - $bits * (k + 1))) & m } else { (w >> ($bits * k)) & m } as u16;
233                }
234            }
235            tail(src, dst, groups * $n, $bits, $order);
236        }
237    };
238}
239
240word_unpack!(unpack14_msb, 14, 7, 4, true, PackOrder::MsbBytes);
241word_unpack!(unpack14_lsb, 14, 7, 4, false, PackOrder::LsbBytes);
242word_unpack!(unpack10_msb, 10, 5, 4, true, PackOrder::MsbBytes);
243word_unpack!(unpack10_lsb, 10, 5, 4, false, PackOrder::LsbBytes);
244
245/// 12-bit SIMD kernels: 12 source bytes → 8 samples per 128-bit vector.
246#[allow(dead_code)] // tables/helpers unused on targets without a SIMD kernel
247mod simd12 {
248    /// Shuffle placing (hi, lo) byte pairs for MSB packing into little-endian u16 lanes.
249    pub(super) const SHUF_MSB: [u8; 16] = [1, 0, 2, 1, 4, 3, 5, 4, 7, 6, 8, 7, 10, 9, 11, 10];
250    /// Shuffle for LSB packing: lane = bytes (lo, hi) in natural order.
251    pub(super) const SHUF_LSB: [u8; 16] = [0, 1, 1, 2, 3, 4, 4, 5, 6, 7, 7, 8, 9, 10, 10, 11];
252
253    /// Returns the number of samples written (a multiple of 8).
254    #[allow(unreachable_code)]
255    pub fn unpack(src: &[u8], dst: &mut [u16], msb: bool) -> usize {
256        #[cfg(target_arch = "x86_64")]
257        {
258            if std::arch::is_x86_feature_detected!("sse4.1") && std::arch::is_x86_feature_detected!("ssse3") {
259                // SAFETY: the required CPU features were detected at runtime.
260                return unsafe { x86::unpack(src, dst, msb) };
261            }
262            return 0;
263        }
264        #[cfg(target_arch = "aarch64")]
265        {
266            // SAFETY: NEON is mandatory on aarch64.
267            return unsafe { neon::unpack(src, dst, msb) };
268        }
269        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
270        {
271            return wasm::unpack(src, dst, msb);
272        }
273        let _ = (src, dst, msb);
274        0
275    }
276
277    /// Number of 8-sample groups that can be processed reading 16 bytes per 12 consumed.
278    #[inline]
279    pub(super) fn groups(src: &[u8], dst: &[u16]) -> usize {
280        (dst.len() / 8).min(src.len().saturating_sub(4) / 12)
281    }
282
283    #[cfg(target_arch = "x86_64")]
284    mod x86 {
285        use std::arch::x86_64::*;
286
287        /// # Safety
288        /// Caller must ensure SSSE3 and SSE4.1 are available.
289        #[target_feature(enable = "ssse3,sse4.1")]
290        pub unsafe fn unpack(src: &[u8], dst: &mut [u16], msb: bool) -> usize {
291            let n = super::groups(src, dst);
292            let shuf = if msb { super::SHUF_MSB } else { super::SHUF_LSB };
293            // SAFETY: (whole block) `groups` guarantees for each g < n that bytes
294            // [12g, 12g+16) are in `src` and samples [8g, 8g+8) are in `dst`;
295            // unaligned load/store intrinsics are used.
296            unsafe {
297                let shuf = _mm_loadu_si128(shuf.as_ptr() as *const __m128i);
298                let mask = _mm_set1_epi16(0x0FFF);
299                for g in 0..n {
300                    let v = _mm_loadu_si128(src.as_ptr().add(g * 12) as *const __m128i);
301                    let v = _mm_shuffle_epi8(v, shuf);
302                    let shifted = _mm_srli_epi16(v, 4);
303                    let masked = _mm_and_si128(v, mask);
304                    // MSB: even lanes shifted, odd lanes masked. LSB: the reverse.
305                    let out = if msb {
306                        _mm_blend_epi16(masked, shifted, 0b0101_0101)
307                    } else {
308                        _mm_blend_epi16(masked, shifted, 0b1010_1010)
309                    };
310                    _mm_storeu_si128(dst.as_mut_ptr().add(g * 8) as *mut __m128i, out);
311                }
312            }
313            n * 8
314        }
315    }
316
317    #[cfg(target_arch = "aarch64")]
318    mod neon {
319        use std::arch::aarch64::*;
320
321        /// # Safety
322        /// NEON must be available (always true on aarch64).
323        #[target_feature(enable = "neon")]
324        pub unsafe fn unpack(src: &[u8], dst: &mut [u16], msb: bool) -> usize {
325            let n = super::groups(src, dst);
326            let shuf = if msb { super::SHUF_MSB } else { super::SHUF_LSB };
327            let even_sel: [u16; 8] = if msb {
328                [0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0]
329            } else {
330                [0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF]
331            };
332            // SAFETY: bounds guaranteed by `groups` as in the x86 kernel.
333            unsafe {
334                let tbl = vld1q_u8(shuf.as_ptr());
335                let sel = vld1q_u16(even_sel.as_ptr());
336                let mask = vdupq_n_u16(0x0FFF);
337                for g in 0..n {
338                    let v = vld1q_u8(src.as_ptr().add(g * 12));
339                    let v = vreinterpretq_u16_u8(vqtbl1q_u8(v, tbl));
340                    let shifted = vshrq_n_u16(v, 4);
341                    let masked = vandq_u16(v, mask);
342                    let out = vbslq_u16(sel, shifted, masked);
343                    vst1q_u16(dst.as_mut_ptr().add(g * 8), out);
344                }
345            }
346            n * 8
347        }
348    }
349
350    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
351    mod wasm {
352        use core::arch::wasm32::*;
353
354        pub fn unpack(src: &[u8], dst: &mut [u16], msb: bool) -> usize {
355            let n = super::groups(src, dst);
356            let shuf = if msb { super::SHUF_MSB } else { super::SHUF_LSB };
357            let sel = if msb {
358                u16x8(0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0)
359            } else {
360                u16x8(0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF, 0, 0xFFFF)
361            };
362            // SAFETY: bounds guaranteed by `groups`; v128 loads/stores are unaligned.
363            unsafe {
364                let tbl = v128_load(shuf.as_ptr() as *const v128);
365                let mask = u16x8_splat(0x0FFF);
366                for g in 0..n {
367                    let v = v128_load(src.as_ptr().add(g * 12) as *const v128);
368                    let v = i8x16_swizzle(v, tbl);
369                    let shifted = u16x8_shr(v, 4);
370                    let masked = v128_and(v, mask);
371                    v128_store(dst.as_mut_ptr().add(g * 8) as *mut v128, v128_bitselect(shifted, masked, sel));
372                }
373            }
374            n * 8
375        }
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use super::*;
382
383    fn pseudo(n: usize, seed: u64) -> Vec<u8> {
384        let mut x = seed | 1;
385        (0..n)
386            .map(|_| {
387                x ^= x << 13;
388                x ^= x >> 7;
389                x ^= x << 17;
390                x as u8
391            })
392            .collect()
393    }
394
395    #[test]
396    fn fast_paths_match_reference() {
397        for bits in 1..=16u32 {
398            for order in [PackOrder::MsbBytes, PackOrder::LsbBytes, PackOrder::Msb16, PackOrder::Msb32] {
399                for width in [1usize, 7, 8, 15, 16, 17, 63, 64, 333] {
400                    let src = pseudo((width * bits as usize).div_ceil(8) + 3, bits as u64 * 1000 + width as u64);
401                    let mut a = vec![0u16; width];
402                    let mut b = vec![0u16; width];
403                    let mut c = vec![0u16; width];
404                    unpack_row_scalar(&src, &mut a, bits, order);
405                    unpack_row(&src, &mut b, bits, order, true);
406                    unpack_row(&src, &mut c, bits, order, false);
407                    assert_eq!(a, b, "bits {bits} {order:?} width {width} simd");
408                    assert_eq!(a, c, "bits {bits} {order:?} width {width} nosimd");
409                }
410            }
411        }
412    }
413
414    #[test]
415    fn short_sources_match_reference() {
416        // Regression (fuzzing): partial final samples must be zero-padded like the scalar reference.
417        for bits in 1..=16u32 {
418            for order in [PackOrder::MsbBytes, PackOrder::LsbBytes, PackOrder::Msb16, PackOrder::Msb32] {
419                for len in 0..12usize {
420                    let src = pseudo(len, bits as u64 + len as u64);
421                    let mut a = vec![0xAAAAu16; 31];
422                    let mut b = vec![0x5555u16; 31];
423                    unpack_row_scalar(&src, &mut a, bits, order);
424                    unpack_row(&src, &mut b, bits, order, true);
425                    assert_eq!(a, b, "bits {bits} {order:?} len {len}");
426                }
427            }
428        }
429    }
430
431    #[test]
432    fn exact_length_sources() {
433        // Source buffers with no slack after the last group (SIMD/word paths must not over-read).
434        for bits in [10u32, 12, 14, 16] {
435            for width in [8usize, 16, 24, 32, 40, 48] {
436                let src = pseudo(width * bits as usize / 8, 9);
437                for order in [PackOrder::MsbBytes, PackOrder::LsbBytes] {
438                    let mut a = vec![0u16; width];
439                    let mut b = vec![0u16; width];
440                    unpack_row_scalar(&src, &mut a, bits, order);
441                    unpack_row(&src, &mut b, bits, order, true);
442                    assert_eq!(a, b);
443                }
444            }
445        }
446    }
447
448    #[test]
449    fn whole_image_with_stride_and_truncation() {
450        let (w, h) = (100usize, 50usize);
451        let layout = PackedLayout { width: w, height: h, bits: 12, order: PackOrder::MsbBytes, row_stride: 160 };
452        let src = pseudo(160 * h, 5);
453        let mut dst = vec![0u16; w * h];
454        unpack(&src, layout, &mut dst, w, true, true, false).unwrap();
455        for y in 0..h {
456            let mut r = vec![0u16; w];
457            unpack_row_scalar(&src[y * 160..], &mut r, 12, PackOrder::MsbBytes);
458            assert_eq!(&dst[y * w..(y + 1) * w], &r[..]);
459        }
460        assert!(unpack(&src[..160 * 10], layout, &mut dst, w, true, true, false).is_err());
461        assert!(unpack(&src[..160 * 10], layout, &mut dst, w, true, true, true).is_ok());
462        // Recovering a partial row: its complete samples are kept, the rest are zero.
463        let mut part = vec![0u16; w * h];
464        unpack(&src[..160 * 10 + 7], layout, &mut part, w, true, true, true).unwrap();
465        assert_eq!(&part[..w * 10], &dst[..w * 10]);
466        assert_eq!(&part[w * 10..w * 10 + 4], &dst[w * 10..w * 10 + 4]);
467        assert!(part[w * 10 + 4..].iter().all(|&v| v == 0));
468    }
469
470    #[test]
471    fn partial_rows_zero_incomplete_samples() {
472        let src = pseudo(16, 3);
473        for (bits, order, len, complete) in [
474            (12, PackOrder::MsbBytes, 5, 3),
475            (14, PackOrder::LsbBytes, 7, 4),
476            (14, PackOrder::MsbBytes, 6, 3),
477            (16, PackOrder::Msb16, 5, 2),
478            (12, PackOrder::Msb32, 7, 2),
479        ] {
480            let mut full = vec![0u16; 8];
481            unpack_row_scalar(&src, &mut full, bits, order);
482            let mut part = vec![0xFFFFu16; 8];
483            assert_eq!(unpack_row_partial(&src[..len], &mut part, bits, order, true), complete);
484            assert_eq!(&part[..complete], &full[..complete], "{bits} {order:?}");
485            assert!(part[complete..].iter().all(|&v| v == 0), "{bits} {order:?}");
486        }
487    }
488}