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