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    /// Return exact Part 15 magnitude bounds for the most recent successful
574    /// preencoded batch, in flattened output-component order.
575    fn last_htj2k97_required_magnitude_bounds(&self) -> &[u8] {
576        &[]
577    }
578}
579
580/// Accelerator that always uses the scalar CPU fallback.
581#[derive(Debug, Default, Clone, Copy)]
582pub struct CpuOnlyDctToWaveletStageAccelerator;
583
584#[doc(hidden)]
585impl DctToWaveletStageAccelerator for CpuOnlyDctToWaveletStageAccelerator {}
586
587/// CPU/Rayon accelerator for the exact reversible integer 5/3 first level.
588///
589/// This backend keeps j2k's scalar ISLOW IDCT semantics as the oracle:
590/// each 8x8 block is decoded with `j2k-jpeg`, level-shifted to signed
591/// component samples, then transformed with reversible integer 5/3 lifting.
592#[derive(Debug, Default, Clone)]
593pub struct RayonReversibleDwt53Accelerator {
594    attempts: usize,
595    dispatches: usize,
596    batch_attempts: usize,
597    batch_dispatches: usize,
598}
599
600impl RayonReversibleDwt53Accelerator {
601    /// Number of reversible 5/3 jobs offered to this accelerator.
602    #[must_use]
603    pub const fn reversible_dwt53_attempts(&self) -> usize {
604        self.attempts
605    }
606
607    /// Number of reversible 5/3 jobs handled by this accelerator.
608    #[must_use]
609    pub const fn reversible_dwt53_dispatches(&self) -> usize {
610        self.dispatches
611    }
612
613    /// Number of reversible 5/3 batches offered to this accelerator.
614    #[must_use]
615    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
616        self.batch_attempts
617    }
618
619    /// Number of reversible 5/3 batches handled by this accelerator.
620    #[must_use]
621    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
622        self.batch_dispatches
623    }
624}
625
626#[doc(hidden)]
627impl DctToWaveletStageAccelerator for RayonReversibleDwt53Accelerator {
628    fn dct_grid_to_reversible_dwt53(
629        &mut self,
630        job: DctGridToReversibleDwt53Job<'_>,
631    ) -> Result<Option<ReversibleDwt53FirstLevel>, TranscodeStageError> {
632        self.attempts = self.attempts.saturating_add(1);
633        let output = reversible_dwt53_first_level_rayon(job)?;
634        self.dispatches = self.dispatches.saturating_add(1);
635        Ok(Some(output))
636    }
637
638    fn dct_grid_to_reversible_dwt53_batch(
639        &mut self,
640        jobs: &[DctGridToReversibleDwt53Job<'_>],
641    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
642        self.batch_attempts = self.batch_attempts.saturating_add(1);
643        validate_reversible_batch_workspace(jobs)?;
644        let mut output = try_vec_with_capacity(jobs.len()).map_err(TranscodeStageError::from)?;
645        for job in jobs {
646            output.push(reversible_dwt53_first_level_rayon(*job)?);
647        }
648        self.batch_dispatches = self.batch_dispatches.saturating_add(1);
649        Ok(Some(output))
650    }
651}
652
653/// Decode the job's dequantized DCT blocks into j2k's signed integer
654/// component sample blocks.
655///
656/// This is source-visible so hybrid GPU backends can keep JPEG parsing and
657/// exact IDCT on CPU while offloading the reversible 5/3 projection.
658#[doc(hidden)]
659pub fn idct_blocks_to_signed_samples_rayon(
660    blocks: &[[i16; 64]],
661) -> Result<Vec<[i32; 64]>, TranscodeStageError> {
662    let mut output = try_vec_filled(blocks.len(), [0i32; 64]).map_err(TranscodeStageError::from)?;
663    output
664        .par_iter_mut()
665        .zip(blocks.par_iter())
666        .for_each(|(output, block)| {
667            let decoded = idct_islow_block(block);
668            *output = decoded.map(|sample| i32::from(sample) - 128);
669        });
670    Ok(output)
671}
672
673/// Compute one exact reversible integer 5/3 level from already decoded
674/// block-local signed samples.
675pub(crate) fn reversible_dwt53_first_level_from_block_samples(
676    block_samples: &[[i32; 64]],
677    block_cols: usize,
678    block_rows: usize,
679    width: usize,
680    height: usize,
681) -> Result<ReversibleDwt53FirstLevel, TranscodeStageError> {
682    validate_reversible_grid(block_samples.len(), block_cols, block_rows, width, height)?;
683    validate_reversible_output_workspace(width, height)?;
684
685    let low_width = width.div_ceil(2);
686    let low_height = height.div_ceil(2);
687    let high_width = width / 2;
688    let high_height = height / 2;
689
690    let low_row_count = checked_stage_product(width, low_height)?;
691    let mut low_rows = try_vec_filled(low_row_count, 0i32).map_err(TranscodeStageError::from)?;
692    low_rows
693        .par_chunks_mut(width)
694        .enumerate()
695        .for_each(|(output_y, row)| {
696            for (x, sample) in row.iter_mut().enumerate() {
697                *sample =
698                    vertical_low_53_i32_at(block_samples, block_cols, width, height, x, output_y);
699            }
700            reversible_lift_53_i32(row);
701        });
702    let high_row_count = checked_stage_product(width, high_height)?;
703    let mut high_rows = try_vec_filled(high_row_count, 0i32).map_err(TranscodeStageError::from)?;
704    high_rows
705        .par_chunks_mut(width)
706        .enumerate()
707        .for_each(|(output_y, row)| {
708            for (x, sample) in row.iter_mut().enumerate() {
709                *sample =
710                    vertical_high_53_i32_at(block_samples, block_cols, width, height, x, output_y);
711            }
712            reversible_lift_53_i32(row);
713        });
714
715    let mut ll = try_vec_with_capacity(checked_stage_product(low_width, low_height)?)
716        .map_err(TranscodeStageError::from)?;
717    let mut hl = try_vec_with_capacity(checked_stage_product(high_width, low_height)?)
718        .map_err(TranscodeStageError::from)?;
719    for row in low_rows.chunks_exact(width) {
720        ll.extend(row.iter().step_by(2).copied());
721        hl.extend(row.iter().skip(1).step_by(2).copied());
722    }
723
724    let mut lh = try_vec_with_capacity(checked_stage_product(low_width, high_height)?)
725        .map_err(TranscodeStageError::from)?;
726    let mut hh = try_vec_with_capacity(checked_stage_product(high_width, high_height)?)
727        .map_err(TranscodeStageError::from)?;
728    for row in high_rows.chunks_exact(width) {
729        lh.extend(row.iter().step_by(2).copied());
730        hh.extend(row.iter().skip(1).step_by(2).copied());
731    }
732
733    Ok(ReversibleDwt53FirstLevel {
734        ll,
735        hl,
736        lh,
737        hh,
738        low_width,
739        low_height,
740        high_width,
741        high_height,
742    })
743}
744
745fn reversible_dwt53_first_level_rayon(
746    job: DctGridToReversibleDwt53Job<'_>,
747) -> Result<ReversibleDwt53FirstLevel, TranscodeStageError> {
748    validate_reversible_grid(
749        job.dequantized_blocks.len(),
750        job.block_cols,
751        job.block_rows,
752        job.width,
753        job.height,
754    )?;
755    validate_reversible_job_workspace(job)?;
756    let block_samples = idct_blocks_to_signed_samples_rayon(job.dequantized_blocks)?;
757    reversible_dwt53_first_level_from_block_samples(
758        &block_samples,
759        job.block_cols,
760        job.block_rows,
761        job.width,
762        job.height,
763    )
764}
765
766fn validate_reversible_output_workspace(
767    width: usize,
768    height: usize,
769) -> Result<(), TranscodeStageError> {
770    let sample_count = checked_stage_product(width, height)?;
771    let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
772    let band_bytes = checked_allocation_bytes::<i32>(sample_count)?;
773    checked_add_allocation_bytes(row_bytes, band_bytes)
774        .map(|_| ())
775        .map_err(TranscodeStageError::from)
776}
777
778fn validate_reversible_job_workspace(
779    job: DctGridToReversibleDwt53Job<'_>,
780) -> Result<(), TranscodeStageError> {
781    let block_bytes = checked_allocation_bytes::<[i32; 64]>(job.dequantized_blocks.len())?;
782    let sample_count = checked_stage_product(job.width, job.height)?;
783    let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
784    let band_bytes = checked_allocation_bytes::<i32>(sample_count)?;
785    let workspace = checked_add_allocation_bytes(block_bytes, row_bytes)?;
786    checked_add_allocation_bytes(workspace, band_bytes)?;
787    Ok(())
788}
789
790fn validate_reversible_batch_workspace(
791    jobs: &[DctGridToReversibleDwt53Job<'_>],
792) -> Result<(), TranscodeStageError> {
793    let mut retained_output_bytes = 0usize;
794    let mut max_transient_bytes = 0usize;
795    for job in jobs {
796        validate_reversible_grid(
797            job.dequantized_blocks.len(),
798            job.block_cols,
799            job.block_rows,
800            job.width,
801            job.height,
802        )?;
803        let sample_count = checked_stage_product(job.width, job.height)?;
804        let output_bytes = checked_allocation_bytes::<i32>(sample_count)?;
805        retained_output_bytes = checked_add_allocation_bytes(retained_output_bytes, output_bytes)?;
806        let block_bytes = checked_allocation_bytes::<[i32; 64]>(job.dequantized_blocks.len())?;
807        let row_bytes = checked_allocation_bytes::<i32>(sample_count)?;
808        max_transient_bytes =
809            max_transient_bytes.max(checked_add_allocation_bytes(block_bytes, row_bytes)?);
810    }
811    checked_add_allocation_bytes(retained_output_bytes, max_transient_bytes)?;
812    Ok(())
813}
814
815fn checked_stage_product(left: usize, right: usize) -> Result<usize, TranscodeStageError> {
816    left.checked_mul(right)
817        .ok_or(TranscodeStageError::MemoryCapExceeded {
818            requested: usize::MAX,
819            cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
820        })
821}
822
823fn validate_reversible_grid(
824    block_count: usize,
825    block_cols: usize,
826    block_rows: usize,
827    width: usize,
828    height: usize,
829) -> Result<(), TranscodeStageError> {
830    validate_dct_block_grid(block_count, block_cols, block_rows, width, height)
831        .map_err(|_| TranscodeStageError::Unsupported(REVERSIBLE_DWT53_UNSUPPORTED_GRID))
832}
833
834fn vertical_low_53_i32_at(
835    block_samples: &[[i32; 64]],
836    block_cols: usize,
837    width: usize,
838    height: usize,
839    x: usize,
840    low_idx: usize,
841) -> i32 {
842    reversible_lift_53_low_at(height, low_idx, |y| {
843        component_sample_i32(block_samples, block_cols, width, height, x, y)
844    })
845}
846
847fn vertical_high_53_i32_at(
848    block_samples: &[[i32; 64]],
849    block_cols: usize,
850    width: usize,
851    height: usize,
852    x: usize,
853    high_idx: usize,
854) -> i32 {
855    reversible_lift_53_high_at(height, high_idx, |y| {
856        component_sample_i32(block_samples, block_cols, width, height, x, y)
857    })
858}
859
860fn component_sample_i32(
861    block_samples: &[[i32; 64]],
862    block_cols: usize,
863    width: usize,
864    height: usize,
865    x: usize,
866    y: usize,
867) -> i32 {
868    debug_assert!(x < width);
869    debug_assert!(y < height);
870    let block_x = x / 8;
871    let block_y = y / 8;
872    let block_idx = block_y * block_cols + block_x;
873    let local_idx = (y % 8) * 8 + (x % 8);
874    block_samples[block_idx][local_idx]
875}
876
877#[cfg(test)]
878mod allocation_tests {
879    use super::{
880        idct_blocks_to_signed_samples_rayon, validate_reversible_grid,
881        validate_reversible_output_workspace, TranscodeStageError,
882        REVERSIBLE_DWT53_UNSUPPORTED_GRID,
883    };
884
885    #[test]
886    fn malformed_reversible_grid_is_explicitly_unsupported() {
887        assert!(matches!(
888            validate_reversible_grid(0, 1, 1, 8, 8),
889            Err(TranscodeStageError::Unsupported(
890                REVERSIBLE_DWT53_UNSUPPORTED_GRID
891            ))
892        ));
893    }
894
895    #[test]
896    fn reversible_workspace_overflow_is_typed() {
897        assert!(matches!(
898            validate_reversible_output_workspace(usize::MAX, 2),
899            Err(TranscodeStageError::MemoryCapExceeded {
900                requested: usize::MAX,
901                ..
902            })
903        ));
904    }
905
906    #[test]
907    fn fallible_parallel_idct_preserves_signed_samples() {
908        let blocks = [[0i16; 64]; 2];
909        let samples = idct_blocks_to_signed_samples_rayon(&blocks)
910            .expect("two block outputs fit the host cap");
911        assert_eq!(samples, [[0i32; 64]; 2]);
912    }
913}
914
915#[cfg(test)]
916mod ground_truth_tests {
917    //! Independent ground truth for the reversible integer 5/3.
918    //!
919    //! The CUDA 5/3 kernel is parity-tested against the lifting in this module,
920    //! so a boundary/indexing/band-split bug here would be faithfully copied by
921    //! the kernel and pass parity. Validate the lifting against the canonical
922    //! JPEG2000 reversible 5/3 (ISO/IEC 15444-1 Annex F.3.8.1) evaluated per
923    //! output index from a whole-sample-symmetrically extended signal — a
924    //! structurally different implementation than the in-place two-pass loops.
925
926    use super::{
927        reversible_dwt53_first_level_from_block_samples, reversible_lift_53_i32,
928        ReversibleDwt53FirstLevel,
929    };
930
931    fn floor2(a: i32, b: i32) -> i32 {
932        a.div_euclid(b)
933    }
934
935    /// Whole-sample symmetric reflection (mirror about 0 and `n - 1`, endpoints
936    /// not repeated) — the boundary extension the lifting realizes at the edges.
937    fn ws_reflect(i: isize, n: usize) -> usize {
938        if n == 1 {
939            return 0;
940        }
941        let n = isize::try_from(n).unwrap();
942        let period = 2 * (n - 1);
943        let mut k = i.rem_euclid(period);
944        if k >= n {
945            k = period - k;
946        }
947        usize::try_from(k).unwrap()
948    }
949
950    /// Canonical forward 5/3: `(low, high)` where `low[m]` is the even/approx
951    /// coefficient and `high[m]` the odd/detail coefficient. Every index is read
952    /// through whole-sample symmetric extension of the original signal, so the
953    /// detail-boundary behavior follows automatically (no special cases).
954    fn ref_53_forward(signal: &[i32]) -> (Vec<i32>, Vec<i32>) {
955        let n = signal.len();
956        if n < 2 {
957            return (signal.to_vec(), Vec::new());
958        }
959        let sig = |i: isize| signal[ws_reflect(i, n)];
960        let detail = |m: isize| {
961            let c = 2 * m + 1;
962            sig(c) - floor2(sig(c - 1) + sig(c + 1), 2)
963        };
964        let low: Vec<i32> = (0..n.div_ceil(2))
965            .map(|m| {
966                let mi = isize::try_from(m).unwrap();
967                sig(2 * mi) + floor2(detail(mi - 1) + detail(mi) + 2, 4)
968            })
969            .collect();
970        let high: Vec<i32> = (0..n / 2)
971            .map(|m| detail(isize::try_from(m).unwrap()))
972            .collect();
973        (low, high)
974    }
975
976    /// Separable 2D reference matching the oracle's vertical-then-horizontal
977    /// order (integer floor lifting is NOT order-independent, so order matters).
978    fn ref_53_2d(plane: &[i32], width: usize, height: usize) -> ReversibleDwt53FirstLevel {
979        let low_width = width.div_ceil(2);
980        let high_width = width / 2;
981        let low_height = height.div_ceil(2);
982        let high_height = height / 2;
983
984        let mut v_low = vec![0i32; width * low_height];
985        let mut v_high = vec![0i32; width * high_height];
986        for x in 0..width {
987            let column: Vec<i32> = (0..height).map(|y| plane[y * width + x]).collect();
988            let (lo, hi) = ref_53_forward(&column);
989            for (oy, &value) in lo.iter().enumerate() {
990                v_low[oy * width + x] = value;
991            }
992            for (oy, &value) in hi.iter().enumerate() {
993                v_high[oy * width + x] = value;
994            }
995        }
996
997        let horizontal = |source: &[i32], rows: usize| -> (Vec<i32>, Vec<i32>) {
998            let mut low = vec![0i32; low_width * rows];
999            let mut high = vec![0i32; high_width * rows];
1000            for oy in 0..rows {
1001                let (lo, hi) = ref_53_forward(&source[oy * width..oy * width + width]);
1002                low[oy * low_width..oy * low_width + low_width].copy_from_slice(&lo);
1003                high[oy * high_width..oy * high_width + high_width].copy_from_slice(&hi);
1004            }
1005            (low, high)
1006        };
1007
1008        let (ll, hl) = horizontal(&v_low, low_height);
1009        let (lh, hh) = horizontal(&v_high, high_height);
1010
1011        ReversibleDwt53FirstLevel {
1012            ll,
1013            hl,
1014            lh,
1015            hh,
1016            low_width,
1017            low_height,
1018            high_width,
1019            high_height,
1020        }
1021    }
1022
1023    /// Pack a flat `width x height` sample plane into the block-major
1024    /// `[[i32; 64]]` layout `reversible_dwt53_first_level_from_block_samples`
1025    /// consumes (local index `(y % 8) * 8 + (x % 8)`).
1026    fn pack_plane(plane: &[i32], width: usize, height: usize) -> (Vec<[i32; 64]>, usize, usize) {
1027        let block_cols = width.div_ceil(8);
1028        let block_rows = height.div_ceil(8);
1029        let mut blocks = vec![[0i32; 64]; block_cols * block_rows];
1030        for y in 0..height {
1031            for x in 0..width {
1032                let block = (y / 8) * block_cols + (x / 8);
1033                blocks[block][(y % 8) * 8 + (x % 8)] = plane[y * width + x];
1034            }
1035        }
1036        (blocks, block_cols, block_rows)
1037    }
1038
1039    fn next_sample(state: &mut u64) -> i32 {
1040        *state = state
1041            .wrapping_mul(6_364_136_223_846_793_005)
1042            .wrapping_add(1_442_695_040_888_963_407);
1043        ((*state >> 40) & 0x1ff) as i32 - 256
1044    }
1045
1046    #[test]
1047    fn reversible_lift_53_matches_canonical_formula_1d() {
1048        let mut state = 0x0a11_ce5e_ed00_d001u64;
1049        for n in [2usize, 3, 4, 5, 8, 9, 12, 15, 16, 23, 32, 33, 64, 65] {
1050            let signal: Vec<i32> = (0..n).map(|_| next_sample(&mut state)).collect();
1051            let mut lifted = signal.clone();
1052            reversible_lift_53_i32(&mut lifted);
1053            let lifted_low: Vec<i32> = lifted.iter().step_by(2).copied().collect();
1054            let lifted_high: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1055            let (low, high) = ref_53_forward(&signal);
1056            assert_eq!(lifted_low, low, "low band mismatch for n={n}");
1057            assert_eq!(lifted_high, high, "high band mismatch for n={n}");
1058        }
1059    }
1060
1061    #[test]
1062    fn reversible_lift_53_shared_helper_matches_canonical_formula_1d() {
1063        let mut state = 0x5a53_5a53_5a53_5a53u64;
1064        for n in [2usize, 3, 4, 5, 8, 9, 16, 17, 31, 32, 65] {
1065            let signal: Vec<i32> = (0..n).map(|_| next_sample(&mut state)).collect();
1066            let mut lifted = signal.clone();
1067            crate::reversible53::reversible_lift_53_i32(&mut lifted);
1068            let lifted_low: Vec<i32> = lifted.iter().step_by(2).copied().collect();
1069            let lifted_high: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1070            let (low, high) = ref_53_forward(&signal);
1071            assert_eq!(lifted_low, low, "low band mismatch for n={n}");
1072            assert_eq!(lifted_high, high, "high band mismatch for n={n}");
1073        }
1074    }
1075
1076    #[test]
1077    fn reversible_dwt53_2d_matches_canonical_separable() {
1078        let mut state = 0xfeed_5eed_d00d_face_u64;
1079        for (width, height) in [
1080            (8usize, 8usize),
1081            (16, 16),
1082            (24, 16),
1083            (15, 13),
1084            (16, 23),
1085            (9, 7),
1086            (32, 32),
1087        ] {
1088            let plane: Vec<i32> = (0..width * height)
1089                .map(|_| next_sample(&mut state))
1090                .collect();
1091            let (blocks, block_cols, block_rows) = pack_plane(&plane, width, height);
1092            let got = reversible_dwt53_first_level_from_block_samples(
1093                &blocks, block_cols, block_rows, width, height,
1094            )
1095            .expect("oracle accepts the packed grid");
1096            let want = ref_53_2d(&plane, width, height);
1097            assert_eq!(
1098                (
1099                    got.low_width,
1100                    got.low_height,
1101                    got.high_width,
1102                    got.high_height
1103                ),
1104                (
1105                    want.low_width,
1106                    want.low_height,
1107                    want.high_width,
1108                    want.high_height
1109                ),
1110                "band dimensions for {width}x{height}"
1111            );
1112            assert_eq!(got.ll, want.ll, "LL mismatch for {width}x{height}");
1113            assert_eq!(got.hl, want.hl, "HL mismatch for {width}x{height}");
1114            assert_eq!(got.lh, want.lh, "LH mismatch for {width}x{height}");
1115            assert_eq!(got.hh, want.hh, "HH mismatch for {width}x{height}");
1116        }
1117    }
1118
1119    #[test]
1120    fn reversible_lift_53_kills_dc_and_linear_detail() {
1121        // Constant -> low = constant, detail exactly zero.
1122        let mut constant = vec![7i32; 32];
1123        reversible_lift_53_i32(&mut constant);
1124        assert!(
1125            constant.iter().skip(1).step_by(2).all(|&v| v == 0),
1126            "constant produced nonzero detail"
1127        );
1128        assert!(
1129            constant.iter().step_by(2).all(|&v| v == 7),
1130            "constant low band drifted from 7"
1131        );
1132
1133        // Linear ramp -> interior detail exactly zero (two vanishing moments).
1134        let ramp: Vec<i32> = (0..40_i32).map(|k| 3 * k - 5).collect();
1135        let mut lifted = ramp;
1136        reversible_lift_53_i32(&mut lifted);
1137        let detail: Vec<i32> = lifted.iter().skip(1).step_by(2).copied().collect();
1138        for &value in &detail[1..detail.len() - 1] {
1139            assert_eq!(value, 0, "linear ramp produced interior detail {value}");
1140        }
1141    }
1142
1143    #[test]
1144    fn reversible_dwt53_2d_separates_horizontal_and_vertical_detail() {
1145        // Varies only along x -> no vertical detail (LH and HH vanish).
1146        let (width, height) = (16usize, 16usize);
1147        let varies_in_x: Vec<i32> = (0..width * height)
1148            .map(|i| 3 * i32::try_from(i % width).unwrap() - 7)
1149            .collect();
1150        let (blocks, bc, br) = pack_plane(&varies_in_x, width, height);
1151        let t = reversible_dwt53_first_level_from_block_samples(&blocks, bc, br, width, height)
1152            .expect("oracle accepts grid");
1153        assert!(
1154            t.lh.iter().all(|&v| v == 0),
1155            "x-only plane produced LH detail"
1156        );
1157        assert!(
1158            t.hh.iter().all(|&v| v == 0),
1159            "x-only plane produced HH detail"
1160        );
1161
1162        // Varies only along y -> no horizontal detail (HL and HH vanish).
1163        let varies_in_y: Vec<i32> = (0..width * height)
1164            .map(|i| 3 * i32::try_from(i / width).unwrap() - 7)
1165            .collect();
1166        let (blocks, bc, br) = pack_plane(&varies_in_y, width, height);
1167        let t = reversible_dwt53_first_level_from_block_samples(&blocks, bc, br, width, height)
1168            .expect("oracle accepts grid");
1169        assert!(
1170            t.hl.iter().all(|&v| v == 0),
1171            "y-only plane produced HL detail"
1172        );
1173        assert!(
1174            t.hh.iter().all(|&v| v == 0),
1175            "y-only plane produced HH detail"
1176        );
1177    }
1178}