Skip to main content

flashraw_codecs/
ljpeg_par.rs

1//! Parallel decoding of a single lossless-JPEG scan.
2//!
3//! A scan without restart markers (every CR2, single-strip DNGs) is one long Huffman
4//! stream. [`crate::par_entropy`] decodes its differences on all threads by
5//! speculation; this module adapts lossless JPEG to it (unstuffed MSB stream, one
6//! difference per symbol, table per MCU position) and rebuilds samples:
7//! predictor-1 rows by a parallel prefix sum after chaining the first sample of every
8//! row, sRAW rows in parallel after chaining the first-column predictors, predictors
9//! 2-7 by a wavefront over bands of rows and blocks of columns. Whenever speculation cannot prove the result identical to the
10//! sequential decoder, the caller is told to decode sequentially.
11//!
12//! Scans with restart markers need no speculation: every restart interval is its own bit
13//! stream with reset predictors, so intervals are decoded one per task
14//! ([`decode_frame_restarts`]).
15
16use crate::ljpeg::LjpegHeader;
17use crate::par_entropy::{self, EndCheck, SymbolStream};
18use flashraw_core::bits::{BitPumpMsb, BitRead};
19use flashraw_core::huffman::HuffTable;
20use flashraw_core::{Error, Result, par};
21
22/// Frames smaller than this decode sequentially (speculation has a fixed cost).
23pub(crate) const PARALLEL_MIN_SAMPLES: usize = 4 << 20;
24/// [`PARALLEL_MIN_SAMPLES`] for lossless JPEG frames (CR2 incl. sRAW, DNG, Canon TIF):
25/// their unstuffing and speculation already pay off from about a million samples.
26pub(crate) const LJPEG_PARALLEL_MIN_SAMPLES: usize = 1 << 20;
27/// Sequential decoding tolerates this many bits consumed past the end of the scan
28/// (`past_end() <= 2` bytes at the last row).
29const END_SLACK_BITS: u64 = 16;
30
31/// The unstuffed scan of a lossless JPEG stream.
32struct Scan<'a> {
33    stream: &'a [u8],
34    tables: &'a [HuffTable],
35    period: usize,
36}
37
38impl SymbolStream for Scan<'_> {
39    type Pump<'p>
40        = BitPumpMsb<'p>
41    where
42        Self: 'p;
43    const WIDTH: usize = 1;
44    fn cycle(&self) -> usize {
45        self.tables.len()
46    }
47    fn period(&self) -> usize {
48        self.period
49    }
50    fn total_bits(&self) -> u64 {
51        self.stream.len() as u64 * 8
52    }
53    fn end_check(&self, pos: u64) -> EndCheck {
54        // `past_end` counts whole bytes consumed past the data, which is exactly this.
55        if pos <= self.total_bits() + END_SLACK_BITS { EndCheck::Valid } else { EndCheck::Invalid }
56    }
57    fn pump_at(&self, bit: u64) -> BitPumpMsb<'_> {
58        BitPumpMsb::at_bit(self.stream, bit)
59    }
60    fn position(p: &BitPumpMsb<'_>) -> u64 {
61        p.bit_position()
62    }
63    #[inline(always)]
64    fn decode(&self, p: &mut BitPumpMsb<'_>, phase: usize, out: &mut [u16]) -> bool {
65        let before = p.bit_position();
66        p.fill();
67        let mut err = false;
68        out[0] = self.tables[phase].decode_diff_buffered(p, &mut err) as u16;
69        if err && p.bit_position() == before {
70            p.skip_no_fill(1);
71        }
72        !err
73    }
74}
75
76/// Decode the whole frame of a restart-free scan into `frame` (`height` rows of
77/// [`LjpegHeader::row_len`] samples) using all worker threads.
78///
79/// Returns `Ok(false)` without a usable result when the stream does not qualify or
80/// cannot be decoded speculatively; the caller then uses [`crate::ljpeg::decode_rows`]
81/// (which also produces the exact error for corrupt data).
82pub fn decode_frame_parallel(data: &[u8], hdr: &LjpegHeader, frame: &mut [u16]) -> Result<bool> {
83    decode_frame_chunked(data, hdr, hdr.height as usize, frame, None)
84}
85
86/// [`decode_frame_parallel`] for the first `rows` rows only: data after them is not
87/// required to be valid.
88pub(crate) fn decode_frame_rows(data: &[u8], hdr: &LjpegHeader, rows: usize, frame: &mut [u16]) -> Result<bool> {
89    decode_frame_chunked(data, hdr, rows.min(hdr.height as usize), frame, None)
90}
91
92pub(crate) fn decode_frame_chunked(
93    data: &[u8],
94    hdr: &LjpegHeader,
95    height: usize,
96    frame: &mut [u16],
97    chunk_bytes: Option<usize>,
98) -> Result<bool> {
99    let clrs = hdr.clrs as usize;
100    let rl = hdr.row_len();
101    let total = rl.checked_mul(height).ok_or_else(|| Error::corrupt("lossless JPEG: frame too large"))?;
102    if hdr.restart_interval != 0 {
103        return decode_frame_restarts(data, hdr, height, frame);
104    }
105    if clrs == 0 || clrs > 6 || frame.len() < total {
106        return Ok(false);
107    }
108    let Some(scan) = data.get(hdr.scan_offset..) else { return Ok(false) };
109    if chunk_bytes.is_none() && par_entropy::default_chunk(scan.len()).is_none() {
110        // Too short to split (the unstuffed stream is shorter still).
111        return Ok(false);
112    }
113    let un = crate::ljpeg::unstuff_scan(scan, false);
114    if un.segments.len() != 1 {
115        return Ok(false);
116    }
117    let stream = un.segment(0);
118    let chunk = match chunk_bytes.or_else(|| par_entropy::default_chunk(stream.len())) {
119        Some(c) => c,
120        None => return Ok(false),
121    };
122    let src = Scan { stream, tables: &hdr.tables, period: phase_period(&hdr.table_ids) };
123    let Some(diffs) = par_entropy::decode(&src, total, chunk) else { return Ok(false) };
124
125    if hdr.predictor == 1 && (hdr.sraw != 0 || clrs > 4) {
126        // Predictor 1 with sRAW layouts: a row depends on the rows above only through the
127        // first-column predictors (`vpred`), updated by the first MCU's differences of
128        // positions not predicted within the row. Chain those, then rebuild rows in parallel.
129        let sraw = hdr.sraw as usize;
130        let init = 1u16 << (hdr.bits - 1);
131        let mut starts = vec![[init; 6]; height];
132        let mut v = [init; 6];
133        for (r, st) in starts.iter_mut().enumerate() {
134            *st = v;
135            for c in (0..clrs).filter(|&c| c == 0 || c > sraw) {
136                v[c] = v[c].wrapping_add(diffs.get(r * rl + c));
137            }
138        }
139        let rows_per_task = height.div_ceil(par::threads().max(1) * 4).max(1);
140        let failed = std::sync::atomic::AtomicBool::new(false);
141        par::for_each_row_chunk(&mut frame[..total], rl, rows_per_task, true, |r0, rows| {
142            let mut buf = vec![0u16; rl];
143            for (i, row) in rows.chunks_exact_mut(rl).enumerate() {
144                let r = r0 + i;
145                diffs.copy_to(r * rl, &mut buf);
146                let mut vpred = starts[r];
147                let mut k = 0usize;
148                let next = |_c: usize| {
149                    let d = buf[k] as i16 as i32;
150                    k += 1;
151                    Ok(d)
152                };
153                // Row 0 semantics: predictor 1 never reads the previous row.
154                if crate::ljpeg::reconstruct_row(hdr, next, row, &[], 0, clrs, &mut vpred).is_err() {
155                    failed.store(true, std::sync::atomic::Ordering::Relaxed);
156                }
157            }
158        });
159        return Ok(!failed.into_inner());
160    }
161    if hdr.sraw == 0 && (2..=7).contains(&hdr.predictor) && height > 1 {
162        // Two-dimensional predictors: differences go into the frame, then rows are rebuilt
163        // in place by a wavefront over bands of rows and blocks of columns.
164        let rows_per_task = height.div_ceil(par::threads().max(1) * 4).max(1);
165        par::for_each_row_chunk(&mut frame[..total], rl, rows_per_task, true, |r0, rows| {
166            diffs.copy_to(r0 * rl, rows);
167        });
168        rebuild_2d(hdr, &mut frame[..total], rl);
169        return Ok(true);
170    }
171    if hdr.predictor != 1 || hdr.sraw != 0 || clrs > 4 {
172        // Remaining predictor/sRAW combinations: rows depend on the previous row or on
173        // values decoded earlier in the row, so they are rebuilt in order (entropy decoding, the costly
174        // part, already ran in parallel).
175        let init = 1u16 << (hdr.bits - 1);
176        let mut vpred = [init; 6];
177        let mut buf = vec![0u16; rl];
178        for r in 0..height {
179            diffs.copy_to(r * rl, &mut buf);
180            let (done, rest) = frame.split_at_mut(r * rl);
181            let prev = if r > 0 { &done[(r - 1) * rl..] } else { &[][..] };
182            let mut k = 0usize;
183            let next = |_c: usize| {
184                // Differences are kept modulo 2^16, which is all reconstruction needs.
185                let d = buf[k] as i16 as i32;
186                k += 1;
187                Ok(d)
188            };
189            crate::ljpeg::reconstruct_row(hdr, next, &mut rest[..rl], prev, r, clrs, &mut vpred)?;
190        }
191        return Ok(true);
192    }
193    // Pass 3: chain the first sample of every row, then fill rows in parallel.
194
195    let init = 1u16 << (hdr.bits - 1);
196    let mut starts = vec![0u16; height * clrs];
197    let mut v = [init; 4];
198    for (r, s) in starts.chunks_exact_mut(clrs).enumerate() {
199        for c in 0..clrs {
200            v[c] = v[c].wrapping_add(diffs.get(r * rl + c));
201            s[c] = v[c];
202        }
203    }
204    let rows_per_task = height.div_ceil(par::threads().max(1) * 4).max(1);
205    par::for_each_row_chunk(&mut frame[..total], rl, rows_per_task, true, |r0, rows| {
206        for (i, row) in rows.chunks_exact_mut(rl).enumerate() {
207            let r = r0 + i;
208            diffs.copy_to(r * rl, row);
209            row[..clrs].copy_from_slice(&starts[r * clrs..(r + 1) * clrs]);
210            match clrs {
211                1 => prefix::<1>(row),
212                2 => prefix::<2>(row),
213                3 => prefix::<3>(row),
214                _ => prefix::<4>(row),
215            }
216        }
217    });
218    Ok(true)
219}
220
221/// Decode the first `rows` rows of a scan with restart markers, one restart interval per
222/// task. An interval starts a fresh bit stream with reset predictors, so with predictor 1
223/// (which never reads the row above) intervals are independent. Each one runs the
224/// sequential row decoder over its own segment with the same checks; any failure, or a
225/// layout outside that case, returns `Ok(false)` so the sequential decoder reports it.
226pub(crate) fn decode_frame_restarts(data: &[u8], hdr: &LjpegHeader, rows: usize, frame: &mut [u16]) -> Result<bool> {
227    let rl = hdr.row_len();
228    let rows = rows.min(hdr.height as usize);
229    let total = rl.checked_mul(rows).ok_or_else(|| Error::corrupt("lossless JPEG: frame too large"))?;
230    let fast = crate::ljpeg::fast_rows(hdr);
231    let restart = hdr.restart_interval as usize;
232    let clrs = hdr.clrs as usize;
233    if restart == 0 || rl == 0 || clrs == 0 || clrs > 6 || frame.len() < total || !(fast || hdr.predictor == 1) {
234        return Ok(false);
235    }
236    // Restarts are honoured at row starts only, exactly where the sequential decoder does.
237    let width = hdr.width as usize;
238    let starts: Vec<usize> = (0..rows).filter(|&r| (r * width) % restart == 0).collect();
239    if starts.len() < 2 {
240        return Ok(false);
241    }
242    let Some(scan) = data.get(hdr.scan_offset..) else { return Ok(false) };
243    let un = crate::ljpeg::unstuff_scan(scan, true);
244    if un.segments.len() < starts.len() {
245        return Ok(false);
246    }
247    let bounds: Vec<(usize, usize)> = starts
248        .iter()
249        .zip(starts.iter().skip(1).chain(std::iter::once(&rows)))
250        .map(|(&a, &b)| (a * rl, b * rl))
251        .collect();
252    let Some(parts) = par::split_disjoint(&mut frame[..total], &bounds) else { return Ok(false) };
253    let init = 1u16 << (hdr.bits - 1);
254    let ok = par::map(parts.into_iter().enumerate().collect(), true, |(k, part): (usize, &mut [u16])| {
255        let mut bits = BitPumpMsb::new(un.segment(k));
256        let mut vpred = [init; 6];
257        // Predictor 1 never uses the row above (only the generic decoder indexes it).
258        let zeros = if fast { Vec::new() } else { vec![0u16; rl] };
259        let mut rest = part;
260        let mut jrow = starts[k];
261        while !rest.is_empty() {
262            let (row, tail) = std::mem::take(&mut rest).split_at_mut(rl);
263            if crate::ljpeg::decode_row(hdr, fast, &mut bits, row, &zeros, jrow, &mut vpred).is_err() {
264                return false;
265            }
266            rest = tail;
267            jrow += 1;
268        }
269        true
270    });
271    Ok(ok.into_iter().all(|v| v))
272}
273
274/// Rebuild a frame of predictor 2-7 rows (no sRAW) in place from the differences it
275/// holds. Row 0 predicts from the left; later rows start from the sample above and
276/// then use the predictor. Sample `(r, i)` depends on `(r, i - clrs)`, `(r - 1, i)` and
277/// `(r - 1, i - clrs)` only, so the rows are cut into bands and the columns into
278/// blocks: block `j` of band `k` can be rebuilt once block `j - 1` of the same band and
279/// block `j` of the band above are done, which lets the bands run as a wavefront.
280fn rebuild_2d(hdr: &LjpegHeader, frame: &mut [u16], rl: usize) {
281    match hdr.predictor {
282        2 => rebuild_2d_psv::<2>(hdr, frame, rl),
283        3 => rebuild_2d_psv::<3>(hdr, frame, rl),
284        4 => rebuild_2d_psv::<4>(hdr, frame, rl),
285        5 => rebuild_2d_psv::<5>(hdr, frame, rl),
286        6 => rebuild_2d_psv::<6>(hdr, frame, rl),
287        _ => rebuild_2d_psv::<7>(hdr, frame, rl),
288    }
289}
290
291fn rebuild_2d_psv<const PSV: u32>(hdr: &LjpegHeader, frame: &mut [u16], rl: usize) {
292    let clrs = hdr.clrs as usize;
293    let init = 1u16 << (hdr.bits - 1);
294    let (row0, rest) = frame.split_at_mut(rl);
295    for c in 0..clrs {
296        row0[c] = init.wrapping_add(row0[c]);
297    }
298    for i in clrs..rl {
299        row0[i] = row0[i - clrs].wrapping_add(row0[i]);
300    }
301    // Columns `a..b` of a band's rows from the row above it (`above`, columns `lo..b`).
302    let geo = par::Wavefront { row_len: rl, halo: 1, margin: clrs, block: block_width(rl, clrs) };
303    par::wavefront(rest, row0, geo, true, |_, band, above, lo, a, b| {
304        let mut prev: &[u16] = above;
305        for row in band.chunks_exact_mut(rl) {
306            let cur = &mut row[lo..b];
307            rebuild_window::<PSV>(cur, prev, a - lo, clrs);
308            prev = cur;
309        }
310    });
311}
312
313/// Wavefront block width: about four blocks per thread, at least 64 MCUs.
314fn block_width(rl: usize, clrs: usize) -> usize {
315    (rl / clrs).div_ceil(4 * par::threads().max(1)).max(64) * clrs
316}
317
318/// Rebuild `cur[start..]` from `prev` (the same columns of the row above); both start
319/// `start` samples before the first one rebuilt, which is column 0 when `start == 0`.
320#[inline(always)]
321fn rebuild_window<const PSV: u32>(cur: &mut [u16], prev: &[u16], start: usize, clrs: usize) {
322    let n = cur.len().min(prev.len());
323    let (cur, prev) = (&mut cur[..n], &prev[..n]);
324    let mut i = start;
325    if start == 0 {
326        // Column 0 predicts from the sample above.
327        while i < clrs.min(n) {
328            cur[i] = prev[i].wrapping_add(cur[i]);
329            i += 1;
330        }
331    }
332    if i >= n {
333        return;
334    }
335    match PSV {
336        2 => {
337            for (c, &p) in cur[i..].iter_mut().zip(&prev[i..]) {
338                *c = p.wrapping_add(*c);
339            }
340        }
341        3 => {
342            for (c, &p) in cur[i..].iter_mut().zip(&prev[i - clrs..]) {
343                *c = p.wrapping_add(*c);
344            }
345        }
346        _ => {
347            for i in i..n {
348                let (ra, rb, rc) = (cur[i - clrs] as i32, prev[i] as i32, prev[i - clrs] as i32);
349                let pred = match PSV {
350                    4 => ra + rb - rc,
351                    5 => ra + ((rb - rc) >> 1),
352                    6 => rb + ((ra - rc) >> 1),
353                    _ => (ra + rb) >> 1,
354                };
355                cur[i] = (pred as u16).wrapping_add(cur[i]);
356            }
357        }
358    }
359}
360
361/// Smallest `p` with `ids[i] == ids[i % p]` (table sequence repeats every `p` symbols).
362fn phase_period(ids: &[u8]) -> usize {
363    let n = ids.len().max(1);
364    (1..=n).find(|&p| n % p == 0 && (0..n).all(|i| ids[i] == ids[i % p])).unwrap_or(n)
365}
366
367#[inline(always)]
368fn prefix<const N: usize>(row: &mut [u16]) {
369    let mut p: [u16; N] = std::array::from_fn(|c| row[c]);
370    for px in row[N..].chunks_exact_mut(N) {
371        for c in 0..N {
372            p[c] = p[c].wrapping_add(px[c]);
373            px[c] = p[c];
374        }
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::ljpeg::{self, LjpegOptions, encoder::encode};
382
383    fn pseudo(n: usize, bits: u32, seed: u32) -> Vec<u16> {
384        let mut x = seed;
385        let mut v = 1u32 << (bits - 1);
386        (0..n)
387            .map(|_| {
388                x = x.wrapping_mul(1103515245).wrapping_add(12345);
389                // Smooth-ish data with occasional jumps: realistic code-length mix.
390                let step = ((x >> 16) & 0xff) as i32 - 128;
391                let step = if (x >> 8) & 31 == 0 { step * 40 } else { step / 8 };
392                v = (v as i32 + step).clamp(0, (1 << bits) - 1) as u32;
393                v as u16
394            })
395            .collect()
396    }
397
398    fn sequential(data: &[u8], hdr: &LjpegHeader) -> Vec<u16> {
399        let mut out = Vec::new();
400        ljpeg::decode_rows(data, hdr, |_, r| {
401            out.extend_from_slice(r);
402            Ok(())
403        })
404        .unwrap();
405        out
406    }
407
408    #[test]
409    fn matches_sequential_for_many_chunkings() {
410        let cases = [(1usize, 12u32, 1u32), (2, 14, 1), (3, 12, 1), (4, 14, 1), (2, 14, 6), (1, 12, 7)];
411        let cases = cases.into_iter().chain((2..=5).map(|p| (2, 14, p)));
412        for (comps, bits, psv) in cases {
413            let (w, h) = (97usize, 61usize);
414            let samples = pseudo(w * h * comps, bits, comps as u32 * 7 + psv);
415            let enc = encode(w, h, comps, bits, psv, &samples);
416            let hdr = LjpegHeader::parse(&enc, LjpegOptions::default()).unwrap();
417            let want = sequential(&enc, &hdr);
418            assert_eq!(want, samples);
419            for chunk in [7usize, 64, 333, 1000, 5000, 1 << 20] {
420                let mut frame = vec![0u16; want.len()];
421                let ok = decode_frame_chunked(&enc, &hdr, hdr.height as usize, &mut frame, Some(chunk)).unwrap();
422                assert!(ok, "comps {comps} psv {psv} chunk {chunk}: no synchronisation");
423                assert_eq!(frame, want, "comps {comps} psv {psv} chunk {chunk}");
424            }
425        }
426    }
427
428    #[test]
429    fn truncated_stream_is_left_to_the_sequential_decoder() {
430        let (w, h) = (64usize, 40usize);
431        let samples = pseudo(w * h * 2, 14, 3);
432        let enc = encode(w, h, 2, 14, 1, &samples);
433        let hdr = LjpegHeader::parse(&enc, LjpegOptions::default()).unwrap();
434        let cut = &enc[..hdr.scan_offset + (enc.len() - hdr.scan_offset) / 2];
435        let mut frame = vec![0u16; w * h * 2];
436        assert!(!decode_frame_chunked(cut, &hdr, hdr.height as usize, &mut frame, Some(100)).unwrap());
437    }
438
439    #[test]
440    fn wavefront_rebuild_matches_sequential() {
441        // Wide enough for many column blocks, tall enough for one band per thread.
442        for (w, h) in [(700usize, 61usize), (300, 2), (129, 5)] {
443            for comps in 1..=4 {
444                for psv in 2..=7 {
445                    let samples = pseudo(w * h * comps, 16, comps as u32 * 3 + psv);
446                    let enc = encode(w, h, comps, 16, psv, &samples);
447                    let hdr = LjpegHeader::parse(&enc, LjpegOptions::default()).unwrap();
448                    let want = sequential(&enc, &hdr);
449                    assert_eq!(want, samples);
450                    let mut frame = vec![0u16; want.len()];
451                    let ok = decode_frame_chunked(&enc, &hdr, h, &mut frame, Some(500)).unwrap();
452                    assert!(ok, "{w}x{h} comps {comps} psv {psv}");
453                    assert_eq!(frame, want, "{w}x{h} comps {comps} psv {psv}");
454                }
455            }
456        }
457    }
458
459    #[test]
460    fn restart_intervals_match_sequential() {
461        let (w, h) = (53usize, 61usize);
462        for comps in 1..=4 {
463            for psv in 1..=7u32 {
464                // One row, three rows, and an interval that does not divide the height.
465                for rows_per_interval in [1usize, 3, 8] {
466                    let samples = pseudo(w * h * comps, 14, comps as u32 * 13 + psv);
467                    let enc =
468                        ljpeg::encoder::encode_with_restarts(w, h, comps, 14, psv, &samples, w * rows_per_interval);
469                    let hdr = LjpegHeader::parse(&enc, LjpegOptions::default()).unwrap();
470                    assert_eq!(hdr.restart_interval as usize, w * rows_per_interval);
471                    let want = sequential(&enc, &hdr);
472                    assert_eq!(want, samples, "comps {comps} psv {psv} interval {rows_per_interval}");
473                    let mut frame = vec![0u16; want.len()];
474                    let ok = decode_frame_restarts(&enc, &hdr, h, &mut frame).unwrap();
475                    // Only predictor 1 rows are independent of the interval before.
476                    assert_eq!(ok, psv == 1, "comps {comps} psv {psv}");
477                    if ok {
478                        assert_eq!(frame, want, "comps {comps} interval {rows_per_interval}");
479                        // A row limit decodes only the intervals it needs.
480                        let mut part = vec![0u16; 20 * w * comps];
481                        assert!(decode_frame_restarts(&enc, &hdr, 20, &mut part).unwrap());
482                        assert_eq!(part[..], want[..part.len()]);
483                    }
484                    let mut via_rows = Vec::new();
485                    ljpeg::decode_rows_par(&enc, &hdr, true, |_, r| {
486                        via_rows.extend_from_slice(r);
487                        Ok(())
488                    })
489                    .unwrap();
490                    assert_eq!(via_rows, want);
491                }
492            }
493        }
494    }
495
496    #[test]
497    fn damaged_restart_interval_is_left_to_the_sequential_decoder() {
498        let (w, h) = (64usize, 48usize);
499        let samples = pseudo(w * h * 2, 12, 5);
500        let enc = ljpeg::encoder::encode_with_restarts(w, h, 2, 12, 1, &samples, w * 4);
501        let hdr = LjpegHeader::parse(&enc, LjpegOptions::default()).unwrap();
502        let rst: Vec<usize> = (hdr.scan_offset..enc.len() - 1)
503            .filter(|&i| enc[i] == 0xFF && (0xD0..=0xD7).contains(&enc[i + 1]))
504            .collect();
505        assert_eq!(rst.len(), h / 4 - 1);
506        // Drop the middle of one interval: it runs short and the frame must fall back.
507        let mut cut = enc[..(rst[3] + rst[4]) / 2].to_vec();
508        cut.extend_from_slice(&enc[rst[4]..]);
509        let mut frame = vec![0u16; w * h * 2];
510        assert!(!decode_frame_restarts(&cut, &hdr, h, &mut frame).unwrap());
511        assert!(ljpeg::decode_rows_par(&cut, &hdr, true, |_, _| Ok(())).is_err());
512        // A missing marker (fewer segments than intervals) falls back too.
513        let mut merged = enc[..rst[2]].to_vec();
514        merged.extend_from_slice(&enc[rst[2] + 2..]);
515        assert!(!decode_frame_restarts(&merged, &hdr, h, &mut frame).unwrap());
516        let seq = ljpeg::decode_rows(&merged, &hdr, |_, _| Ok(()));
517        let par = ljpeg::decode_rows_par(&merged, &hdr, true, |_, _| Ok(()));
518        assert_eq!(seq.is_err(), par.is_err());
519    }
520
521    #[test]
522    fn phase_periods() {
523        assert_eq!(phase_period(&[0]), 1);
524        assert_eq!(phase_period(&[0, 0]), 1);
525        assert_eq!(phase_period(&[0, 1]), 2);
526        assert_eq!(phase_period(&[0, 1, 0, 1]), 2);
527        assert_eq!(phase_period(&[0, 1, 1]), 3);
528    }
529}