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