Skip to main content

j2k_transcode/
accelerator_contracts.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// j2k-coverage: shared-accelerator-host
3
4// Optional acceleration hooks for coefficient-domain transform stages.
5//
6// These hooks are intentionally narrow: accelerated backends may replace the
7// direct DCT-grid to one-level wavelet projection, while the scalar path
8// remains the default oracle and fallback.
9
10use crate::allocation::{
11    checked_add_allocation_bytes, checked_allocation_bytes, try_vec_filled, try_vec_with_capacity,
12};
13use crate::dct_grid::validate_dct_block_grid;
14use crate::reversible53::{
15    reversible_lift_53_high_at, reversible_lift_53_i32, reversible_lift_53_low_at,
16};
17use crate::{
18    DctGridToReversibleDwt53Job, Dwt53TwoDimensional, Dwt97BatchStageTimings, Dwt97TwoDimensional,
19    ReversibleDwt53FirstLevel, TranscodeStageError,
20};
21pub use j2k::{
22    EncodedHtJ2kCodeBlock, IrreversibleQuantizationSubbandScales, J2kSubBandType,
23    PreencodedHtj2k97CodeBlock, PreencodedHtj2k97CompactCodeBlock,
24    PreencodedHtj2k97CompactComponent, PreencodedHtj2k97CompactImage,
25    PreencodedHtj2k97CompactResolution, PreencodedHtj2k97CompactSubband,
26    PreencodedHtj2k97Component, PreencodedHtj2k97Resolution, PreencodedHtj2k97Subband,
27    PrequantizedHtj2k97CodeBlock, PrequantizedHtj2k97Component, PrequantizedHtj2k97Image,
28    PrequantizedHtj2k97Resolution, PrequantizedHtj2k97Subband,
29};
30use j2k_jpeg::transcode::idct_islow_block;
31use rayon::prelude::{
32    IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator,
33    ParallelSliceMut,
34};
35
36const REVERSIBLE_DWT53_UNSUPPORTED_GRID: &str =
37    "reversible DCT 5/3 job has unsupported grid geometry";
38
39/// Direct DCT-grid to one-level 5/3 projection job.
40#[derive(Debug, Clone, Copy)]
41pub struct DctGridToDwt53Job<'a> {
42    /// Natural-order, dequantized 8x8 DCT blocks.
43    pub blocks: &'a [[[f64; 8]; 8]],
44    /// Number of DCT block columns in `blocks`.
45    pub block_cols: usize,
46    /// Number of DCT block rows in `blocks`.
47    pub block_rows: usize,
48    /// Logical component width in samples.
49    pub width: usize,
50    /// Logical component height in samples.
51    pub height: usize,
52}
53
54/// Direct DCT-grid to one-level 9/7 transform job.
55#[derive(Debug, Clone, Copy)]
56pub struct DctGridToDwt97Job<'a> {
57    /// Natural-order, dequantized 8x8 DCT blocks.
58    pub blocks: &'a [[[f64; 8]; 8]],
59    /// Number of DCT block columns in `blocks`.
60    pub block_cols: usize,
61    /// Number of DCT block rows in `blocks`.
62    pub block_rows: usize,
63    /// Logical component width in samples.
64    pub width: usize,
65    /// Logical component height in samples.
66    pub height: usize,
67}
68
69/// Direct DCT-grid to prequantized one-level 9/7 HTJ2K code-block job.
70#[derive(Debug, Clone, Copy)]
71pub struct DctGridToHtj2k97CodeBlockJob<'a> {
72    /// Natural-order, dequantized 8x8 DCT blocks.
73    pub blocks: &'a [[[f64; 8]; 8]],
74    /// Number of DCT block columns in `blocks`.
75    pub block_cols: usize,
76    /// Number of DCT block rows in `blocks`.
77    pub block_rows: usize,
78    /// Logical component width in samples.
79    pub width: usize,
80    /// Logical component height in samples.
81    pub height: usize,
82    /// Horizontal SIZ sampling factor (`XRsiz`).
83    pub x_rsiz: u8,
84    /// Vertical SIZ sampling factor (`YRsiz`).
85    pub y_rsiz: u8,
86}
87
88/// Direct dequantized i16 DCT-grid to one-level 9/7 HTJ2K code-block job.
89///
90/// This is for accelerators that consume the JPEG coefficient extraction
91/// output directly and do not need the generic f64 block representation.
92#[derive(Debug, Clone, Copy)]
93pub struct DctGridI16ToHtj2k97CodeBlockJob<'a> {
94    /// Natural-order, dequantized 8x8 DCT blocks.
95    pub dequantized_blocks: &'a [[i16; 64]],
96    /// Number of DCT block columns in `dequantized_blocks`.
97    pub block_cols: usize,
98    /// Number of DCT block rows in `dequantized_blocks`.
99    pub block_rows: usize,
100    /// Logical component width in samples.
101    pub width: usize,
102    /// Logical component height in samples.
103    pub height: usize,
104    /// Horizontal SIZ sampling factor (`XRsiz`).
105    pub x_rsiz: u8,
106    /// Vertical SIZ sampling factor (`YRsiz`).
107    pub y_rsiz: u8,
108}
109
110/// One same-geometry i16 DCT-grid HTJ2K preencode batch.
111#[derive(Debug, Clone, Copy)]
112pub struct DctGridI16ToHtj2k97CodeBlockBatch<'a, 'j> {
113    /// Jobs in this same-geometry batch.
114    pub jobs: &'j [DctGridI16ToHtj2k97CodeBlockJob<'a>],
115}
116
117/// Compact preencoded HTJ2K components backed by one payload buffer.
118#[derive(Debug)]
119pub struct PreencodedHtj2k97CompactBatch {
120    /// Contiguous encoded code-block payload bytes for every component.
121    pub payload: Vec<u8>,
122    /// Compact components in the same order as the submitted jobs.
123    pub components: Vec<PreencodedHtj2k97CompactComponent>,
124}
125
126/// Compact preencoded HTJ2K grouped-batch output backed by one payload buffer.
127#[derive(Debug)]
128pub struct PreencodedHtj2k97CompactBatchGroups {
129    /// Contiguous encoded code-block payload bytes for every returned group.
130    pub payload: Vec<u8>,
131    /// Compact components grouped in the same order as submitted batches.
132    pub groups: Vec<Vec<PreencodedHtj2k97CompactComponent>>,
133}
134
135/// Encode parameters needed to quantize 9/7 output directly into HTJ2K
136/// code-block coefficient layout.
137#[derive(Debug, Clone, Copy, PartialEq)]
138pub struct Htj2k97CodeBlockOptions {
139    /// Component precision in bits.
140    pub bit_depth: u8,
141    /// JPEG 2000 guard bits used for QCD and code-block bitplane counts.
142    pub guard_bits: u8,
143    /// Code-block width exponent minus two.
144    pub code_block_width_exp: u8,
145    /// Code-block height exponent minus two.
146    pub code_block_height_exp: u8,
147    /// Multiplier applied to irreversible 9/7 scalar quantization step sizes.
148    pub irreversible_quantization_scale: f32,
149    /// Per-subband multipliers applied on top of
150    /// [`irreversible_quantization_scale`](Self::irreversible_quantization_scale).
151    pub irreversible_quantization_subband_scales: IrreversibleQuantizationSubbandScales,
152}
153
154/// Counter row recorded by DCT-to-wavelet stage accelerators.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum DctToWaveletStageCounterEvent {
157    /// One reversible integer 5/3 job was offered to the accelerator.
158    ReversibleDwt53Attempt,
159    /// One reversible integer 5/3 job was handled by the accelerator.
160    ReversibleDwt53Dispatch,
161    /// One reversible integer 5/3 batch was offered to the accelerator.
162    ReversibleDwt53BatchAttempt,
163    /// One reversible integer 5/3 batch was handled by the accelerator.
164    ReversibleDwt53BatchDispatch,
165    /// One 5/3 projection job was offered to the accelerator.
166    Dwt53Attempt,
167    /// One 5/3 projection job was handled by the accelerator.
168    Dwt53Dispatch,
169    /// One 9/7 transform job was offered to the accelerator.
170    Dwt97Attempt,
171    /// One 9/7 transform job was handled by the accelerator.
172    Dwt97Dispatch,
173    /// One same-geometry 9/7 transform batch was offered to the accelerator.
174    Dwt97BatchAttempt,
175    /// One same-geometry 9/7 transform batch was handled by the accelerator.
176    Dwt97BatchDispatch,
177    /// One 9/7 code-block-ready batch was offered to the accelerator.
178    Htj2k97CodeblockBatchAttempt,
179    /// One 9/7 code-block-ready batch was handled by the accelerator.
180    Htj2k97CodeblockBatchDispatch,
181}
182
183/// Shared offered/handled counters for DCT-to-wavelet stage accelerators.
184#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
185pub struct DctToWaveletStageCounters {
186    reversible_dwt53_attempts: usize,
187    reversible_dwt53_dispatches: usize,
188    reversible_dwt53_batch_attempts: usize,
189    reversible_dwt53_batch_dispatches: usize,
190    dwt53_attempts: usize,
191    dwt53_dispatches: usize,
192    dwt97_attempts: usize,
193    dwt97_dispatches: usize,
194    dwt97_batch_attempts: usize,
195    dwt97_batch_dispatches: usize,
196    htj2k97_codeblock_batch_attempts: usize,
197    htj2k97_codeblock_batch_dispatches: usize,
198}
199
200impl DctToWaveletStageCounters {
201    /// Create an empty counter set.
202    #[must_use]
203    pub const fn new() -> Self {
204        Self {
205            reversible_dwt53_attempts: 0,
206            reversible_dwt53_dispatches: 0,
207            reversible_dwt53_batch_attempts: 0,
208            reversible_dwt53_batch_dispatches: 0,
209            dwt53_attempts: 0,
210            dwt53_dispatches: 0,
211            dwt97_attempts: 0,
212            dwt97_dispatches: 0,
213            dwt97_batch_attempts: 0,
214            dwt97_batch_dispatches: 0,
215            htj2k97_codeblock_batch_attempts: 0,
216            htj2k97_codeblock_batch_dispatches: 0,
217        }
218    }
219
220    /// Number of reversible integer 5/3 jobs offered to this accelerator.
221    #[must_use]
222    pub const fn reversible_dwt53_attempts(&self) -> usize {
223        self.reversible_dwt53_attempts
224    }
225
226    /// Number of reversible integer 5/3 jobs handled by this accelerator.
227    #[must_use]
228    pub const fn reversible_dwt53_dispatches(&self) -> usize {
229        self.reversible_dwt53_dispatches
230    }
231
232    /// Number of reversible integer 5/3 batches offered to this accelerator.
233    #[must_use]
234    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
235        self.reversible_dwt53_batch_attempts
236    }
237
238    /// Number of reversible integer 5/3 batches handled by this accelerator.
239    #[must_use]
240    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
241        self.reversible_dwt53_batch_dispatches
242    }
243
244    /// Number of 5/3 projection jobs offered to this accelerator.
245    #[must_use]
246    pub const fn dwt53_attempts(&self) -> usize {
247        self.dwt53_attempts
248    }
249
250    /// Number of 5/3 projection jobs handled by this accelerator.
251    #[must_use]
252    pub const fn dwt53_dispatches(&self) -> usize {
253        self.dwt53_dispatches
254    }
255
256    /// Number of 9/7 transform jobs offered to this accelerator.
257    #[must_use]
258    pub const fn dwt97_attempts(&self) -> usize {
259        self.dwt97_attempts
260    }
261
262    /// Number of 9/7 transform jobs handled by this accelerator.
263    #[must_use]
264    pub const fn dwt97_dispatches(&self) -> usize {
265        self.dwt97_dispatches
266    }
267
268    /// Number of 9/7 transform batches offered to this accelerator.
269    #[must_use]
270    pub const fn dwt97_batch_attempts(&self) -> usize {
271        self.dwt97_batch_attempts
272    }
273
274    /// Number of 9/7 transform batches handled by this accelerator.
275    #[must_use]
276    pub const fn dwt97_batch_dispatches(&self) -> usize {
277        self.dwt97_batch_dispatches
278    }
279
280    /// Number of 9/7 code-block-ready batches offered to this accelerator.
281    #[must_use]
282    pub const fn htj2k97_codeblock_batch_attempts(&self) -> usize {
283        self.htj2k97_codeblock_batch_attempts
284    }
285
286    /// Number of 9/7 code-block-ready batches handled by this accelerator.
287    #[must_use]
288    pub const fn htj2k97_codeblock_batch_dispatches(&self) -> usize {
289        self.htj2k97_codeblock_batch_dispatches
290    }
291
292    /// Record one or more accelerator counter events.
293    pub fn record(&mut self, event: DctToWaveletStageCounterEvent, count: usize) {
294        match event {
295            DctToWaveletStageCounterEvent::ReversibleDwt53Attempt => {
296                self.reversible_dwt53_attempts =
297                    self.reversible_dwt53_attempts.saturating_add(count);
298            }
299            DctToWaveletStageCounterEvent::ReversibleDwt53Dispatch => {
300                self.reversible_dwt53_dispatches =
301                    self.reversible_dwt53_dispatches.saturating_add(count);
302            }
303            DctToWaveletStageCounterEvent::ReversibleDwt53BatchAttempt => {
304                self.reversible_dwt53_batch_attempts =
305                    self.reversible_dwt53_batch_attempts.saturating_add(count);
306            }
307            DctToWaveletStageCounterEvent::ReversibleDwt53BatchDispatch => {
308                self.reversible_dwt53_batch_dispatches =
309                    self.reversible_dwt53_batch_dispatches.saturating_add(count);
310            }
311            DctToWaveletStageCounterEvent::Dwt53Attempt => {
312                self.dwt53_attempts = self.dwt53_attempts.saturating_add(count);
313            }
314            DctToWaveletStageCounterEvent::Dwt53Dispatch => {
315                self.dwt53_dispatches = self.dwt53_dispatches.saturating_add(count);
316            }
317            DctToWaveletStageCounterEvent::Dwt97Attempt => {
318                self.dwt97_attempts = self.dwt97_attempts.saturating_add(count);
319            }
320            DctToWaveletStageCounterEvent::Dwt97Dispatch => {
321                self.dwt97_dispatches = self.dwt97_dispatches.saturating_add(count);
322            }
323            DctToWaveletStageCounterEvent::Dwt97BatchAttempt => {
324                self.dwt97_batch_attempts = self.dwt97_batch_attempts.saturating_add(count);
325            }
326            DctToWaveletStageCounterEvent::Dwt97BatchDispatch => {
327                self.dwt97_batch_dispatches = self.dwt97_batch_dispatches.saturating_add(count);
328            }
329            DctToWaveletStageCounterEvent::Htj2k97CodeblockBatchAttempt => {
330                self.htj2k97_codeblock_batch_attempts =
331                    self.htj2k97_codeblock_batch_attempts.saturating_add(count);
332            }
333            DctToWaveletStageCounterEvent::Htj2k97CodeblockBatchDispatch => {
334                self.htj2k97_codeblock_batch_dispatches = self
335                    .htj2k97_codeblock_batch_dispatches
336                    .saturating_add(count);
337            }
338        }
339    }
340}
341
342/// Dispatch policy for optional transcode-stage accelerators.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum TranscodeStageDispatchMode {
345    /// Treat unavailable or unsupported backend dispatch as an error.
346    Explicit,
347    /// Decline unavailable or unsupported backend dispatch with `Ok(None)` so
348    /// callers can use the scalar fallback.
349    Auto,
350}
351
352impl TranscodeStageDispatchMode {
353    /// Whether this mode allows scalar fallback for recoverable backend
354    /// declines.
355    #[must_use]
356    pub const fn is_auto(self) -> bool {
357        matches!(self, Self::Auto)
358    }
359
360    /// Outcome for a job that the backend cannot serve because it is
361    /// unavailable on the current host.
362    #[doc(hidden)]
363    pub const fn unavailable<T>(self) -> Result<Option<T>, TranscodeStageError> {
364        match self {
365            Self::Explicit => Err(TranscodeStageError::DeviceUnavailable),
366            Self::Auto => Ok(None),
367        }
368    }
369
370    /// Convert a backend dispatch error into the trait outcome for this mode.
371    ///
372    /// Auto mode recovers from backend-declared recoverable errors with
373    /// `Ok(None)`; Explicit mode and hard errors propagate as
374    /// [`TranscodeStageError`].
375    #[doc(hidden)]
376    pub fn recover<T, E>(
377        self,
378        error: E,
379        is_recoverable: impl FnOnce(&E) -> bool,
380    ) -> Result<Option<T>, TranscodeStageError>
381    where
382        E: Into<TranscodeStageError>,
383    {
384        if self.is_auto() && is_recoverable(&error) {
385            Ok(None)
386        } else {
387            Err(error.into())
388        }
389    }
390}
391
392/// Optional backend for SIMD, GPU, or other accelerated transform stages.
393pub trait DctToWaveletStageAccelerator {
394    /// Whether this accelerator wants same-geometry 9/7 batch jobs offered.
395    ///
396    /// The default is false so CPU-only fallback paths do not pay the memory
397    /// cost of materializing batch-owned float DCT blocks before immediately
398    /// falling back.
399    fn supports_dwt97_batch(&self) -> bool {
400        false
401    }
402
403    /// Whether this accelerator wants same-geometry 9/7 batches offered as
404    /// prequantized HTJ2K code-block jobs before the float-band hook.
405    fn supports_htj2k97_codeblock_batch(&self) -> bool {
406        false
407    }
408
409    /// Whether this accelerator wants same-geometry 9/7 preencoded HTJ2K
410    /// batches offered with dequantized i16 DCT blocks before materializing the
411    /// generic f64 block representation.
412    fn supports_htj2k97_i16_preencoded_batch(&self) -> bool {
413        false
414    }
415
416    /// Whether this accelerator wants the compact i16 preencoded HTJ2K batch
417    /// hook offered before the owned preencoded hook.
418    fn supports_htj2k97_compact_preencoded_batch(&self) -> bool {
419        self.supports_htj2k97_i16_preencoded_batch()
420    }
421
422    /// Optionally compute the direct DCT-grid to one-level reversible integer
423    /// 5/3 projection.
424    ///
425    /// Return `Ok(Some(output))` when the backend handled the job bit-exactly
426    /// relative to j2k's scalar integer oracle. Return `Ok(None)` to use
427    /// the scalar fallback.
428    fn dct_grid_to_reversible_dwt53(
429        &mut self,
430        _job: DctGridToReversibleDwt53Job<'_>,
431    ) -> Result<Option<ReversibleDwt53FirstLevel>, TranscodeStageError> {
432        Ok(None)
433    }
434
435    /// Optionally compute a same-geometry batch of direct DCT-grid to
436    /// one-level reversible integer 5/3 projections.
437    ///
438    /// Backends should return outputs in the same order as `jobs`. Return
439    /// `Ok(None)` to use the scalar per-component fallback.
440    fn dct_grid_to_reversible_dwt53_batch(
441        &mut self,
442        _jobs: &[DctGridToReversibleDwt53Job<'_>],
443    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
444        Ok(None)
445    }
446
447    /// Optionally compute the direct DCT-grid to one-level 5/3 projection.
448    ///
449    /// Return `Ok(Some(output))` when the backend handled the job. Return
450    /// `Ok(None)` to use the scalar fallback.
451    fn dct_grid_to_dwt53(
452        &mut self,
453        _job: DctGridToDwt53Job<'_>,
454    ) -> Result<Option<Dwt53TwoDimensional<f64>>, TranscodeStageError> {
455        Ok(None)
456    }
457
458    /// Optionally compute the direct DCT-grid to one-level 9/7 transform.
459    ///
460    /// Return `Ok(Some(output))` when the backend handled the job. Return
461    /// `Ok(None)` to use the scalar fallback.
462    fn dct_grid_to_dwt97(
463        &mut self,
464        _job: DctGridToDwt97Job<'_>,
465    ) -> Result<Option<Dwt97TwoDimensional<f64>>, TranscodeStageError> {
466        Ok(None)
467    }
468
469    /// Optionally compute a same-geometry batch of direct DCT-grid to
470    /// one-level 9/7 transforms.
471    ///
472    /// Backends should return outputs in the same order as `jobs`. Return
473    /// `Ok(None)` to use the scalar per-component fallback.
474    fn dct_grid_to_dwt97_batch(
475        &mut self,
476        _jobs: &[DctGridToDwt97Job<'_>],
477    ) -> Result<Option<Vec<Dwt97TwoDimensional<f64>>>, TranscodeStageError> {
478        Ok(None)
479    }
480
481    /// Optionally compute same-geometry DCT-grid 9/7 jobs directly into
482    /// prequantized HTJ2K code-block components.
483    ///
484    /// Backends should return one component per input job in the same order as
485    /// `jobs`. Return `Ok(None)` to use the float-band path.
486    fn dct_grid_to_htj2k97_codeblock_batch(
487        &mut self,
488        _jobs: &[DctGridToHtj2k97CodeBlockJob<'_>],
489        _options: Htj2k97CodeBlockOptions,
490    ) -> Result<Option<Vec<PrequantizedHtj2k97Component>>, TranscodeStageError> {
491        Ok(None)
492    }
493
494    /// Optionally compute same-geometry DCT-grid 9/7 jobs directly into
495    /// preencoded HTJ2K code-block payloads.
496    ///
497    /// Backends should return one component per input job in the same order as
498    /// `jobs`. Return `Ok(None)` to use the prequantized or float-band path.
499    fn dct_grid_to_htj2k97_preencoded_batch(
500        &mut self,
501        _jobs: &[DctGridToHtj2k97CodeBlockJob<'_>],
502        _options: Htj2k97CodeBlockOptions,
503    ) -> Result<Option<Vec<PreencodedHtj2k97Component>>, TranscodeStageError> {
504        Ok(None)
505    }
506
507    /// Optionally compute same-geometry dequantized i16 DCT-grid 9/7 jobs
508    /// directly into preencoded HTJ2K code-block payloads.
509    ///
510    /// Backends should return one component per input job in the same order as
511    /// `jobs`. Return `Ok(None)` to use the generic f64 preencoded path.
512    fn dct_grid_i16_to_htj2k97_preencoded_batch(
513        &mut self,
514        _jobs: &[DctGridI16ToHtj2k97CodeBlockJob<'_>],
515        _options: Htj2k97CodeBlockOptions,
516    ) -> Result<Option<Vec<PreencodedHtj2k97Component>>, TranscodeStageError> {
517        Ok(None)
518    }
519
520    /// Optionally compute same-geometry dequantized i16 DCT-grid 9/7 jobs into
521    /// compact preencoded HTJ2K code-block payloads.
522    ///
523    /// Backends should return one component per input job in the same order as
524    /// `jobs`, with all component ranges pointing into the returned payload.
525    /// Return `Ok(None)` to use the owned preencoded path.
526    fn dct_grid_i16_to_htj2k97_compact_preencoded_batch(
527        &mut self,
528        _jobs: &[DctGridI16ToHtj2k97CodeBlockJob<'_>],
529        _options: Htj2k97CodeBlockOptions,
530    ) -> Result<Option<PreencodedHtj2k97CompactBatch>, TranscodeStageError> {
531        Ok(None)
532    }
533
534    /// Optionally compute multiple same-geometry dequantized i16 DCT-grid
535    /// batches directly into preencoded HTJ2K code-block payloads.
536    ///
537    /// Each input batch is internally same-geometry, but different batches may
538    /// have different component dimensions. Backends should return one output
539    /// vector per input batch, in order. Return `Ok(None)` to use the per-group
540    /// fallback hooks.
541    fn dct_grid_i16_to_htj2k97_preencoded_batch_groups(
542        &mut self,
543        _groups: &[DctGridI16ToHtj2k97CodeBlockBatch<'_, '_>],
544        _options: Htj2k97CodeBlockOptions,
545    ) -> Result<Option<Vec<Vec<PreencodedHtj2k97Component>>>, TranscodeStageError> {
546        Ok(None)
547    }
548
549    /// Optionally compute multiple same-geometry dequantized i16 DCT-grid 9/7
550    /// batches into compact preencoded HTJ2K code-block payloads.
551    ///
552    /// Each returned item corresponds to one input batch and contains one
553    /// component per job in that batch. Return `Ok(None)` to use the owned
554    /// preencoded grouped hook.
555    fn dct_grid_i16_to_htj2k97_compact_preencoded_batch_groups(
556        &mut self,
557        _groups: &[DctGridI16ToHtj2k97CodeBlockBatch<'_, '_>],
558        _options: Htj2k97CodeBlockOptions,
559    ) -> Result<Option<PreencodedHtj2k97CompactBatchGroups>, TranscodeStageError> {
560        Ok(None)
561    }
562
563    /// Return backend stage timings for the most recent 9/7 batch dispatch.
564    fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
565        None
566    }
567}
568
569/// Accelerator that always uses the scalar CPU fallback.
570#[derive(Debug, Default, Clone, Copy)]
571pub struct CpuOnlyDctToWaveletStageAccelerator;
572
573#[doc(hidden)]
574impl DctToWaveletStageAccelerator for CpuOnlyDctToWaveletStageAccelerator {}
575
576/// CPU/Rayon accelerator for the exact reversible integer 5/3 first level.
577///
578/// This backend keeps j2k's scalar ISLOW IDCT semantics as the oracle:
579/// each 8x8 block is decoded with `j2k-jpeg`, level-shifted to signed
580/// component samples, then transformed with reversible integer 5/3 lifting.
581#[derive(Debug, Default, Clone)]
582pub struct RayonReversibleDwt53Accelerator {
583    attempts: usize,
584    dispatches: usize,
585    batch_attempts: usize,
586    batch_dispatches: usize,
587}
588
589impl RayonReversibleDwt53Accelerator {
590    /// Number of reversible 5/3 jobs offered to this accelerator.
591    #[must_use]
592    pub const fn reversible_dwt53_attempts(&self) -> usize {
593        self.attempts
594    }
595
596    /// Number of reversible 5/3 jobs handled by this accelerator.
597    #[must_use]
598    pub const fn reversible_dwt53_dispatches(&self) -> usize {
599        self.dispatches
600    }
601
602    /// Number of reversible 5/3 batches offered to this accelerator.
603    #[must_use]
604    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
605        self.batch_attempts
606    }
607
608    /// Number of reversible 5/3 batches handled by this accelerator.
609    #[must_use]
610    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
611        self.batch_dispatches
612    }
613}
614
615#[doc(hidden)]
616impl DctToWaveletStageAccelerator for RayonReversibleDwt53Accelerator {
617    fn dct_grid_to_reversible_dwt53(
618        &mut self,
619        job: DctGridToReversibleDwt53Job<'_>,
620    ) -> Result<Option<ReversibleDwt53FirstLevel>, TranscodeStageError> {
621        self.attempts = self.attempts.saturating_add(1);
622        let output = reversible_dwt53_first_level_rayon(job)?;
623        self.dispatches = self.dispatches.saturating_add(1);
624        Ok(Some(output))
625    }
626
627    fn dct_grid_to_reversible_dwt53_batch(
628        &mut self,
629        jobs: &[DctGridToReversibleDwt53Job<'_>],
630    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
631        self.batch_attempts = self.batch_attempts.saturating_add(1);
632        validate_reversible_batch_workspace(jobs)?;
633        let mut output = try_vec_with_capacity(jobs.len()).map_err(TranscodeStageError::from)?;
634        for job in jobs {
635            output.push(reversible_dwt53_first_level_rayon(*job)?);
636        }
637        self.batch_dispatches = self.batch_dispatches.saturating_add(1);
638        Ok(Some(output))
639    }
640}
641
642/// Decode the job's dequantized DCT blocks into j2k's signed integer
643/// component sample blocks.
644///
645/// This is source-visible so hybrid GPU backends can keep JPEG parsing and
646/// exact IDCT on CPU while offloading the reversible 5/3 projection.
647#[doc(hidden)]
648pub fn idct_blocks_to_signed_samples_rayon(
649    blocks: &[[i16; 64]],
650) -> Result<Vec<[i32; 64]>, TranscodeStageError> {
651    let mut output = try_vec_filled(blocks.len(), [0i32; 64]).map_err(TranscodeStageError::from)?;
652    output
653        .par_iter_mut()
654        .zip(blocks.par_iter())
655        .for_each(|(output, block)| {
656            let decoded = idct_islow_block(block);
657            *output = decoded.map(|sample| i32::from(sample) - 128);
658        });
659    Ok(output)
660}
661
662/// Compute one exact reversible integer 5/3 level from already decoded
663/// block-local signed samples.
664pub(crate) fn reversible_dwt53_first_level_from_block_samples(
665    block_samples: &[[i32; 64]],
666    block_cols: usize,
667    block_rows: usize,
668    width: usize,
669    height: usize,
670) -> Result<ReversibleDwt53FirstLevel, TranscodeStageError> {
671    validate_reversible_grid(block_samples.len(), block_cols, block_rows, width, height)?;
672    validate_reversible_output_workspace(width, height)?;
673
674    let low_width = width.div_ceil(2);
675    let low_height = height.div_ceil(2);
676    let high_width = width / 2;
677    let high_height = height / 2;
678
679    let low_row_count = checked_stage_product(width, low_height)?;
680    let mut low_rows = try_vec_filled(low_row_count, 0i32).map_err(TranscodeStageError::from)?;
681    low_rows
682        .par_chunks_mut(width)
683        .enumerate()
684        .for_each(|(output_y, row)| {
685            for (x, sample) in row.iter_mut().enumerate() {
686                *sample =
687                    vertical_low_53_i32_at(block_samples, block_cols, width, height, x, output_y);
688            }
689            reversible_lift_53_i32(row);
690        });
691    let high_row_count = checked_stage_product(width, high_height)?;
692    let mut high_rows = try_vec_filled(high_row_count, 0i32).map_err(TranscodeStageError::from)?;
693    high_rows
694        .par_chunks_mut(width)
695        .enumerate()
696        .for_each(|(output_y, row)| {
697            for (x, sample) in row.iter_mut().enumerate() {
698                *sample =
699                    vertical_high_53_i32_at(block_samples, block_cols, width, height, x, output_y);
700            }
701            reversible_lift_53_i32(row);
702        });
703
704    let mut ll = try_vec_with_capacity(checked_stage_product(low_width, low_height)?)
705        .map_err(TranscodeStageError::from)?;
706    let mut hl = try_vec_with_capacity(checked_stage_product(high_width, low_height)?)
707        .map_err(TranscodeStageError::from)?;
708    for row in low_rows.chunks_exact(width) {
709        ll.extend(row.iter().step_by(2).copied());
710        hl.extend(row.iter().skip(1).step_by(2).copied());
711    }
712
713    let mut lh = try_vec_with_capacity(checked_stage_product(low_width, high_height)?)
714        .map_err(TranscodeStageError::from)?;
715    let mut hh = try_vec_with_capacity(checked_stage_product(high_width, high_height)?)
716        .map_err(TranscodeStageError::from)?;
717    for row in high_rows.chunks_exact(width) {
718        lh.extend(row.iter().step_by(2).copied());
719        hh.extend(row.iter().skip(1).step_by(2).copied());
720    }
721
722    Ok(ReversibleDwt53FirstLevel {
723        ll,
724        hl,
725        lh,
726        hh,
727        low_width,
728        low_height,
729        high_width,
730        high_height,
731    })
732}
733
734fn reversible_dwt53_first_level_rayon(
735    job: DctGridToReversibleDwt53Job<'_>,
736) -> Result<ReversibleDwt53FirstLevel, TranscodeStageError> {
737    validate_reversible_grid(
738        job.dequantized_blocks.len(),
739        job.block_cols,
740        job.block_rows,
741        job.width,
742        job.height,
743    )?;
744    validate_reversible_job_workspace(job)?;
745    let block_samples = idct_blocks_to_signed_samples_rayon(job.dequantized_blocks)?;
746    reversible_dwt53_first_level_from_block_samples(
747        &block_samples,
748        job.block_cols,
749        job.block_rows,
750        job.width,
751        job.height,
752    )
753}
754
755fn validate_reversible_output_workspace(
756    width: usize,
757    height: usize,
758) -> Result<(), TranscodeStageError> {
759    let sample_count = checked_stage_product(width, height)?;
760    let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
761    let band_bytes = checked_allocation_bytes::<i32>(sample_count)?;
762    checked_add_allocation_bytes(row_bytes, band_bytes)
763        .map(|_| ())
764        .map_err(TranscodeStageError::from)
765}
766
767fn validate_reversible_job_workspace(
768    job: DctGridToReversibleDwt53Job<'_>,
769) -> Result<(), TranscodeStageError> {
770    let block_bytes = checked_allocation_bytes::<[i32; 64]>(job.dequantized_blocks.len())?;
771    let sample_count = checked_stage_product(job.width, job.height)?;
772    let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
773    let band_bytes = checked_allocation_bytes::<i32>(sample_count)?;
774    let workspace = checked_add_allocation_bytes(block_bytes, row_bytes)?;
775    checked_add_allocation_bytes(workspace, band_bytes)?;
776    Ok(())
777}
778
779fn validate_reversible_batch_workspace(
780    jobs: &[DctGridToReversibleDwt53Job<'_>],
781) -> Result<(), TranscodeStageError> {
782    let mut retained_output_bytes = 0usize;
783    let mut max_transient_bytes = 0usize;
784    for job in jobs {
785        validate_reversible_grid(
786            job.dequantized_blocks.len(),
787            job.block_cols,
788            job.block_rows,
789            job.width,
790            job.height,
791        )?;
792        let sample_count = checked_stage_product(job.width, job.height)?;
793        let output_bytes = checked_allocation_bytes::<i32>(sample_count)?;
794        retained_output_bytes = checked_add_allocation_bytes(retained_output_bytes, output_bytes)?;
795        let block_bytes = checked_allocation_bytes::<[i32; 64]>(job.dequantized_blocks.len())?;
796        let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
797        max_transient_bytes =
798            max_transient_bytes.max(checked_add_allocation_bytes(block_bytes, row_bytes)?);
799    }
800    checked_add_allocation_bytes(retained_output_bytes, max_transient_bytes)?;
801    Ok(())
802}
803
804fn checked_stage_product(left: usize, right: usize) -> Result<usize, TranscodeStageError> {
805    left.checked_mul(right)
806        .ok_or(TranscodeStageError::MemoryCapExceeded {
807            requested: usize::MAX,
808            cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
809        })
810}
811
812fn validate_reversible_grid(
813    block_count: usize,
814    block_cols: usize,
815    block_rows: usize,
816    width: usize,
817    height: usize,
818) -> Result<(), TranscodeStageError> {
819    validate_dct_block_grid(block_count, block_cols, block_rows, width, height)
820        .map_err(|_| TranscodeStageError::Unsupported(REVERSIBLE_DWT53_UNSUPPORTED_GRID))
821}
822
823fn vertical_low_53_i32_at(
824    block_samples: &[[i32; 64]],
825    block_cols: usize,
826    width: usize,
827    height: usize,
828    x: usize,
829    low_idx: usize,
830) -> i32 {
831    reversible_lift_53_low_at(height, low_idx, |y| {
832        component_sample_i32(block_samples, block_cols, width, height, x, y)
833    })
834}
835
836fn vertical_high_53_i32_at(
837    block_samples: &[[i32; 64]],
838    block_cols: usize,
839    width: usize,
840    height: usize,
841    x: usize,
842    high_idx: usize,
843) -> i32 {
844    reversible_lift_53_high_at(height, high_idx, |y| {
845        component_sample_i32(block_samples, block_cols, width, height, x, y)
846    })
847}
848
849fn component_sample_i32(
850    block_samples: &[[i32; 64]],
851    block_cols: usize,
852    width: usize,
853    height: usize,
854    x: usize,
855    y: usize,
856) -> i32 {
857    debug_assert!(x < width);
858    debug_assert!(y < height);
859    let block_x = x / 8;
860    let block_y = y / 8;
861    let block_idx = block_y * block_cols + block_x;
862    let local_idx = (y % 8) * 8 + (x % 8);
863    block_samples[block_idx][local_idx]
864}
865
866#[cfg(test)]
867mod allocation_tests {
868    use super::{
869        idct_blocks_to_signed_samples_rayon, validate_reversible_grid,
870        validate_reversible_output_workspace, TranscodeStageError,
871        REVERSIBLE_DWT53_UNSUPPORTED_GRID,
872    };
873
874    #[test]
875    fn malformed_reversible_grid_is_explicitly_unsupported() {
876        assert!(matches!(
877            validate_reversible_grid(0, 1, 1, 8, 8),
878            Err(TranscodeStageError::Unsupported(
879                REVERSIBLE_DWT53_UNSUPPORTED_GRID
880            ))
881        ));
882    }
883
884    #[test]
885    fn reversible_workspace_overflow_is_typed() {
886        assert!(matches!(
887            validate_reversible_output_workspace(usize::MAX, 2),
888            Err(TranscodeStageError::MemoryCapExceeded {
889                requested: usize::MAX,
890                ..
891            })
892        ));
893    }
894
895    #[test]
896    fn fallible_parallel_idct_preserves_signed_samples() {
897        let blocks = [[0i16; 64]; 2];
898        let samples = idct_blocks_to_signed_samples_rayon(&blocks)
899            .expect("two block outputs fit the host cap");
900        assert_eq!(samples, [[0i32; 64]; 2]);
901    }
902}
903
904#[cfg(test)]
905mod ground_truth_tests {
906    //! Independent ground truth for the reversible integer 5/3.
907    //!
908    //! The CUDA 5/3 kernel is parity-tested against the lifting in this module,
909    //! so a boundary/indexing/band-split bug here would be faithfully copied by
910    //! the kernel and pass parity. Validate the lifting against the canonical
911    //! JPEG2000 reversible 5/3 (ISO/IEC 15444-1 Annex F.3.8.1) evaluated per
912    //! output index from a whole-sample-symmetrically extended signal — a
913    //! structurally different implementation than the in-place two-pass loops.
914
915    use super::{
916        reversible_dwt53_first_level_from_block_samples, reversible_lift_53_i32,
917        ReversibleDwt53FirstLevel,
918    };
919
920    fn floor2(a: i32, b: i32) -> i32 {
921        a.div_euclid(b)
922    }
923
924    /// Whole-sample symmetric reflection (mirror about 0 and `n - 1`, endpoints
925    /// not repeated) — the boundary extension the lifting realizes at the edges.
926    fn ws_reflect(i: isize, n: usize) -> usize {
927        if n == 1 {
928            return 0;
929        }
930        let n = isize::try_from(n).unwrap();
931        let period = 2 * (n - 1);
932        let mut k = i.rem_euclid(period);
933        if k >= n {
934            k = period - k;
935        }
936        usize::try_from(k).unwrap()
937    }
938
939    /// Canonical forward 5/3: `(low, high)` where `low[m]` is the even/approx
940    /// coefficient and `high[m]` the odd/detail coefficient. Every index is read
941    /// through whole-sample symmetric extension of the original signal, so the
942    /// detail-boundary behavior follows automatically (no special cases).
943    fn ref_53_forward(signal: &[i32]) -> (Vec<i32>, Vec<i32>) {
944        let n = signal.len();
945        if n < 2 {
946            return (signal.to_vec(), Vec::new());
947        }
948        let sig = |i: isize| signal[ws_reflect(i, n)];
949        let detail = |m: isize| {
950            let c = 2 * m + 1;
951            sig(c) - floor2(sig(c - 1) + sig(c + 1), 2)
952        };
953        let low: Vec<i32> = (0..n.div_ceil(2))
954            .map(|m| {
955                let mi = isize::try_from(m).unwrap();
956                sig(2 * mi) + floor2(detail(mi - 1) + detail(mi) + 2, 4)
957            })
958            .collect();
959        let high: Vec<i32> = (0..n / 2)
960            .map(|m| detail(isize::try_from(m).unwrap()))
961            .collect();
962        (low, high)
963    }
964
965    /// Separable 2D reference matching the oracle's vertical-then-horizontal
966    /// order (integer floor lifting is NOT order-independent, so order matters).
967    fn ref_53_2d(plane: &[i32], width: usize, height: usize) -> ReversibleDwt53FirstLevel {
968        let low_width = width.div_ceil(2);
969        let high_width = width / 2;
970        let low_height = height.div_ceil(2);
971        let high_height = height / 2;
972
973        let mut v_low = vec![0i32; width * low_height];
974        let mut v_high = vec![0i32; width * high_height];
975        for x in 0..width {
976            let column: Vec<i32> = (0..height).map(|y| plane[y * width + x]).collect();
977            let (lo, hi) = ref_53_forward(&column);
978            for (oy, &value) in lo.iter().enumerate() {
979                v_low[oy * width + x] = value;
980            }
981            for (oy, &value) in hi.iter().enumerate() {
982                v_high[oy * width + x] = value;
983            }
984        }
985
986        let horizontal = |source: &[i32], rows: usize| -> (Vec<i32>, Vec<i32>) {
987            let mut low = vec![0i32; low_width * rows];
988            let mut high = vec![0i32; high_width * rows];
989            for oy in 0..rows {
990                let (lo, hi) = ref_53_forward(&source[oy * width..oy * width + width]);
991                low[oy * low_width..oy * low_width + low_width].copy_from_slice(&lo);
992                high[oy * high_width..oy * high_width + high_width].copy_from_slice(&hi);
993            }
994            (low, high)
995        };
996
997        let (ll, hl) = horizontal(&v_low, low_height);
998        let (lh, hh) = horizontal(&v_high, high_height);
999
1000        ReversibleDwt53FirstLevel {
1001            ll,
1002            hl,
1003            lh,
1004            hh,
1005            low_width,
1006            low_height,
1007            high_width,
1008            high_height,
1009        }
1010    }
1011
1012    /// Pack a flat `width x height` sample plane into the block-major
1013    /// `[[i32; 64]]` layout `reversible_dwt53_first_level_from_block_samples`
1014    /// consumes (local index `(y % 8) * 8 + (x % 8)`).
1015    fn pack_plane(plane: &[i32], width: usize, height: usize) -> (Vec<[i32; 64]>, usize, usize) {
1016        let block_cols = width.div_ceil(8);
1017        let block_rows = height.div_ceil(8);
1018        let mut blocks = vec![[0i32; 64]; block_cols * block_rows];
1019        for y in 0..height {
1020            for x in 0..width {
1021                let block = (y / 8) * block_cols + (x / 8);
1022                blocks[block][(y % 8) * 8 + (x % 8)] = plane[y * width + x];
1023            }
1024        }
1025        (blocks, block_cols, block_rows)
1026    }
1027
1028    fn next_sample(state: &mut u64) -> i32 {
1029        *state = state
1030            .wrapping_mul(6_364_136_223_846_793_005)
1031            .wrapping_add(1_442_695_040_888_963_407);
1032        ((*state >> 40) & 0x1ff) as i32 - 256
1033    }
1034
1035    #[test]
1036    fn reversible_lift_53_matches_canonical_formula_1d() {
1037        let mut state = 0x0a11_ce5e_ed00_d001u64;
1038        for n in [2usize, 3, 4, 5, 8, 9, 12, 15, 16, 23, 32, 33, 64, 65] {
1039            let signal: Vec<i32> = (0..n).map(|_| next_sample(&mut state)).collect();
1040            let mut lifted = signal.clone();
1041            reversible_lift_53_i32(&mut lifted);
1042            let lifted_low: Vec<i32> = lifted.iter().step_by(2).copied().collect();
1043            let lifted_high: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1044            let (low, high) = ref_53_forward(&signal);
1045            assert_eq!(lifted_low, low, "low band mismatch for n={n}");
1046            assert_eq!(lifted_high, high, "high band mismatch for n={n}");
1047        }
1048    }
1049
1050    #[test]
1051    fn reversible_lift_53_shared_helper_matches_canonical_formula_1d() {
1052        let mut state = 0x5a53_5a53_5a53_5a53u64;
1053        for n in [2usize, 3, 4, 5, 8, 9, 16, 17, 31, 32, 65] {
1054            let signal: Vec<i32> = (0..n).map(|_| next_sample(&mut state)).collect();
1055            let mut lifted = signal.clone();
1056            crate::reversible53::reversible_lift_53_i32(&mut lifted);
1057            let lifted_low: Vec<i32> = lifted.iter().step_by(2).copied().collect();
1058            let lifted_high: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1059            let (low, high) = ref_53_forward(&signal);
1060            assert_eq!(lifted_low, low, "low band mismatch for n={n}");
1061            assert_eq!(lifted_high, high, "high band mismatch for n={n}");
1062        }
1063    }
1064
1065    #[test]
1066    fn reversible_dwt53_2d_matches_canonical_separable() {
1067        let mut state = 0xfeed_5eed_d00d_face_u64;
1068        for (width, height) in [
1069            (8usize, 8usize),
1070            (16, 16),
1071            (24, 16),
1072            (15, 13),
1073            (16, 23),
1074            (9, 7),
1075            (32, 32),
1076        ] {
1077            let plane: Vec<i32> = (0..width * height)
1078                .map(|_| next_sample(&mut state))
1079                .collect();
1080            let (blocks, block_cols, block_rows) = pack_plane(&plane, width, height);
1081            let got = reversible_dwt53_first_level_from_block_samples(
1082                &blocks, block_cols, block_rows, width, height,
1083            )
1084            .expect("oracle accepts the packed grid");
1085            let want = ref_53_2d(&plane, width, height);
1086            assert_eq!(
1087                (
1088                    got.low_width,
1089                    got.low_height,
1090                    got.high_width,
1091                    got.high_height
1092                ),
1093                (
1094                    want.low_width,
1095                    want.low_height,
1096                    want.high_width,
1097                    want.high_height
1098                ),
1099                "band dimensions for {width}x{height}"
1100            );
1101            assert_eq!(got.ll, want.ll, "LL mismatch for {width}x{height}");
1102            assert_eq!(got.hl, want.hl, "HL mismatch for {width}x{height}");
1103            assert_eq!(got.lh, want.lh, "LH mismatch for {width}x{height}");
1104            assert_eq!(got.hh, want.hh, "HH mismatch for {width}x{height}");
1105        }
1106    }
1107
1108    #[test]
1109    fn reversible_lift_53_kills_dc_and_linear_detail() {
1110        // Constant -> low = constant, detail exactly zero.
1111        let mut constant = vec![7i32; 32];
1112        reversible_lift_53_i32(&mut constant);
1113        assert!(
1114            constant.iter().skip(1).step_by(2).all(|&v| v == 0),
1115            "constant produced nonzero detail"
1116        );
1117        assert!(
1118            constant.iter().step_by(2).all(|&v| v == 7),
1119            "constant low band drifted from 7"
1120        );
1121
1122        // Linear ramp -> interior detail exactly zero (two vanishing moments).
1123        let ramp: Vec<i32> = (0..40_i32).map(|k| 3 * k - 5).collect();
1124        let mut lifted = ramp;
1125        reversible_lift_53_i32(&mut lifted);
1126        let detail: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1127        for &value in &detail[1..detail.len() - 1] {
1128            assert_eq!(value, 0, "linear ramp produced interior detail {value}");
1129        }
1130    }
1131
1132    #[test]
1133    fn reversible_dwt53_2d_separates_horizontal_and_vertical_detail() {
1134        // Varies only along x -> no vertical detail (LH and HH vanish).
1135        let (width, height) = (16usize, 16usize);
1136        let varies_in_x: Vec<i32> = (0..width * height)
1137            .map(|i| 3 * i32::try_from(i % width).unwrap() - 7)
1138            .collect();
1139        let (blocks, bc, br) = pack_plane(&varies_in_x, width, height);
1140        let t = reversible_dwt53_first_level_from_block_samples(&blocks, bc, br, width, height)
1141            .expect("oracle accepts grid");
1142        assert!(
1143            t.lh.iter().all(|&v| v == 0),
1144            "x-only plane produced LH detail"
1145        );
1146        assert!(
1147            t.hh.iter().all(|&v| v == 0),
1148            "x-only plane produced HH detail"
1149        );
1150
1151        // Varies only along y -> no horizontal detail (HL and HH vanish).
1152        let varies_in_y: Vec<i32> = (0..width * height)
1153            .map(|i| 3 * i32::try_from(i / width).unwrap() - 7)
1154            .collect();
1155        let (blocks, bc, br) = pack_plane(&varies_in_y, width, height);
1156        let t = reversible_dwt53_first_level_from_block_samples(&blocks, bc, br, width, height)
1157            .expect("oracle accepts grid");
1158        assert!(
1159            t.hl.iter().all(|&v| v == 0),
1160            "y-only plane produced HL detail"
1161        );
1162        assert!(
1163            t.hh.iter().all(|&v| v == 0),
1164            "y-only plane produced HH detail"
1165        );
1166    }
1167}