Skip to main content

j2k_transcode_cuda/
lib.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! CUDA acceleration for coefficient-domain JPEG to HTJ2K transcode stages.
4//!
5//! Mirrors `j2k-transcode-metal`: it implements
6//! [`DctToWaveletStageAccelerator`] for direct DCT-grid to one-level 5/3 and 9/7
7//! wavelet projections (and the fused 9/7 HTJ2K code-block path), so JPEG can be
8//! transcoded to HTJ2K without an IDCT->pixels->DWT spatial round-trip. The CPU
9//! scalar code in `j2k-transcode` remains the oracle and fallback; this
10//! crate never reimplements it.
11//!
12//! The actual GPU kernels live in `j2k-cuda-runtime` CUDA Oxide projects and
13//! are loaded through the runtime's CUDA Driver API host layer. The GPU path is
14//! gated behind the `cuda-runtime` feature; without it this accelerator behaves
15//! like Metal's non-macOS path (Explicit -> typed `Err`, Auto -> `Ok(None)`
16//! scalar fallback).
17
18#[cfg(feature = "cuda-runtime")]
19mod cuda;
20mod error;
21
22pub use error::{CudaRuntimeFailure, CudaTranscodeError, CUDA_UNAVAILABLE};
23
24use j2k_transcode::{
25    DctGridI16ToHtj2k97CodeBlockBatch, DctGridI16ToHtj2k97CodeBlockJob, DctGridToDwt53Job,
26    DctGridToDwt97Job, DctGridToHtj2k97CodeBlockJob, DctGridToReversibleDwt53Job,
27    DctToWaveletStageAccelerator, DctToWaveletStageCounterEvent as CounterEvent,
28    DctToWaveletStageCounters, Dwt53TwoDimensional, Dwt97BatchStageTimings, Dwt97TwoDimensional,
29    Htj2k97CodeBlockOptions, PreencodedHtj2k97CompactBatch, PreencodedHtj2k97CompactBatchGroups,
30    PreencodedHtj2k97Component, PrequantizedHtj2k97Component, ReversibleDwt53FirstLevel,
31    TranscodeStageDispatchMode, TranscodeStageError,
32};
33
34/// Default minimum component sample count before Auto mode offers a single
35/// transform job to CUDA. Batch thresholds below intentionally match Metal's
36/// same-geometry batch gates so WSI tile-component queues are offered
37/// consistently across GPU adapters.
38const DEFAULT_AUTO_MIN_SAMPLES: usize = 224 * 224;
39const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS: usize = 32;
40const DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
41const DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS: usize = 32;
42const DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES: usize = 224 * 224 * 32;
43const DISABLE_COMPACT_PREENCODED_ENV: &str = "J2K_CUDA_DISABLE_COMPACT_PREENCODED";
44
45/// Optional CUDA accelerator for `j2k-transcode` transform stages.
46#[derive(Debug, Clone)]
47pub struct CudaDctToWaveletStageAccelerator {
48    mode: TranscodeStageDispatchMode,
49    min_auto_samples: usize,
50    min_auto_reversible_batch_jobs: usize,
51    min_auto_reversible_batch_samples: usize,
52    min_auto_dwt97_batch_jobs: usize,
53    min_auto_dwt97_batch_samples: usize,
54    counters: DctToWaveletStageCounters,
55    last_dwt97_batch_stage_timings: Option<Dwt97BatchStageTimings>,
56    resident_ht_encode: bool,
57    #[cfg(feature = "cuda-runtime")]
58    session: Option<cuda::CudaTranscodeSession>,
59}
60
61impl CudaDctToWaveletStageAccelerator {
62    /// Create an accelerator that treats unavailable/unsupported CUDA dispatch
63    /// as an error (no silent scalar fallback).
64    #[must_use]
65    pub const fn new_explicit() -> Self {
66        Self::with_mode(TranscodeStageDispatchMode::Explicit, 0)
67    }
68
69    /// Create an explicit accelerator that keeps 9/7 code-block coefficients
70    /// resident and HT-encodes them on the same CUDA context before CPU
71    /// packetization.
72    #[must_use]
73    pub const fn new_explicit_resident_ht_encode() -> Self {
74        let mut accelerator = Self::with_mode(TranscodeStageDispatchMode::Explicit, 0);
75        accelerator.resident_ht_encode = true;
76        accelerator
77    }
78
79    /// Create an accelerator that falls back to the scalar oracle for small or
80    /// unsupported jobs.
81    #[must_use]
82    pub const fn for_auto() -> Self {
83        let mut accelerator =
84            Self::with_mode(TranscodeStageDispatchMode::Auto, DEFAULT_AUTO_MIN_SAMPLES);
85        accelerator.min_auto_reversible_batch_jobs = DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_JOBS;
86        accelerator.min_auto_reversible_batch_samples = DEFAULT_AUTO_REVERSIBLE_BATCH_MIN_SAMPLES;
87        accelerator.min_auto_dwt97_batch_jobs = DEFAULT_AUTO_DWT97_BATCH_MIN_JOBS;
88        accelerator.min_auto_dwt97_batch_samples = DEFAULT_AUTO_DWT97_BATCH_MIN_SAMPLES;
89        accelerator
90    }
91
92    const fn with_mode(mode: TranscodeStageDispatchMode, min_auto_samples: usize) -> Self {
93        Self {
94            mode,
95            min_auto_samples,
96            min_auto_reversible_batch_jobs: 0,
97            min_auto_reversible_batch_samples: 0,
98            min_auto_dwt97_batch_jobs: 0,
99            min_auto_dwt97_batch_samples: 0,
100            counters: DctToWaveletStageCounters::new(),
101            last_dwt97_batch_stage_timings: None,
102            resident_ht_encode: false,
103            #[cfg(feature = "cuda-runtime")]
104            session: None,
105        }
106    }
107
108    #[cfg(feature = "cuda-runtime")]
109    fn cuda_session(&mut self) -> &mut cuda::CudaTranscodeSession {
110        self.session
111            .get_or_insert_with(cuda::CudaTranscodeSession::default)
112    }
113
114    /// Override the reversible 5/3 batch thresholds used before Auto mode
115    /// dispatches a batch to CUDA.
116    #[must_use]
117    pub const fn with_auto_reversible_batch_thresholds(
118        mut self,
119        min_jobs: usize,
120        min_samples: usize,
121    ) -> Self {
122        self.min_auto_reversible_batch_jobs = min_jobs;
123        self.min_auto_reversible_batch_samples = min_samples;
124        self
125    }
126
127    /// Override the 9/7 batch thresholds used before Auto mode dispatches a
128    /// same-geometry batch to CUDA.
129    #[must_use]
130    pub const fn with_auto_dwt97_batch_thresholds(
131        mut self,
132        min_jobs: usize,
133        min_samples: usize,
134    ) -> Self {
135        self.min_auto_dwt97_batch_jobs = min_jobs;
136        self.min_auto_dwt97_batch_samples = min_samples;
137        self
138    }
139
140    /// Number of reversible 5/3 jobs offered to this accelerator.
141    #[must_use]
142    pub const fn reversible_dwt53_attempts(&self) -> usize {
143        self.counters.reversible_dwt53_attempts()
144    }
145
146    /// Number of reversible 5/3 jobs handled on the GPU.
147    #[must_use]
148    pub const fn reversible_dwt53_dispatches(&self) -> usize {
149        self.counters.reversible_dwt53_dispatches()
150    }
151
152    /// Number of reversible 5/3 batches offered to this accelerator.
153    #[must_use]
154    pub const fn reversible_dwt53_batch_attempts(&self) -> usize {
155        self.counters.reversible_dwt53_batch_attempts()
156    }
157
158    /// Number of reversible 5/3 batches handled on the GPU.
159    #[must_use]
160    pub const fn reversible_dwt53_batch_dispatches(&self) -> usize {
161        self.counters.reversible_dwt53_batch_dispatches()
162    }
163
164    /// Number of float 5/3 jobs offered to this accelerator.
165    #[must_use]
166    pub const fn dwt53_attempts(&self) -> usize {
167        self.counters.dwt53_attempts()
168    }
169
170    /// Number of float 5/3 jobs handled on the GPU.
171    #[must_use]
172    pub const fn dwt53_dispatches(&self) -> usize {
173        self.counters.dwt53_dispatches()
174    }
175
176    /// Number of 9/7 jobs offered to this accelerator.
177    #[must_use]
178    pub const fn dwt97_attempts(&self) -> usize {
179        self.counters.dwt97_attempts()
180    }
181
182    /// Number of 9/7 jobs handled on the GPU.
183    #[must_use]
184    pub const fn dwt97_dispatches(&self) -> usize {
185        self.counters.dwt97_dispatches()
186    }
187
188    /// Number of 9/7 batches offered to this accelerator.
189    #[must_use]
190    pub const fn dwt97_batch_attempts(&self) -> usize {
191        self.counters.dwt97_batch_attempts()
192    }
193
194    /// Number of 9/7 batches handled on the GPU.
195    #[must_use]
196    pub const fn dwt97_batch_dispatches(&self) -> usize {
197        self.counters.dwt97_batch_dispatches()
198    }
199
200    /// Number of prequantized 9/7 HTJ2K code-block batches offered.
201    #[must_use]
202    pub const fn htj2k97_codeblock_batch_attempts(&self) -> usize {
203        self.counters.htj2k97_codeblock_batch_attempts()
204    }
205
206    /// Number of prequantized 9/7 HTJ2K code-block batches handled on the GPU.
207    #[must_use]
208    pub const fn htj2k97_codeblock_batch_dispatches(&self) -> usize {
209        self.counters.htj2k97_codeblock_batch_dispatches()
210    }
211
212    /// Map a CUDA dispatch error to the trait outcome for the current mode:
213    /// Auto recovers from recoverable errors with `Ok(None)`; Explicit and hard
214    /// kernel failures propagate as `Err`.
215    #[cfg(feature = "cuda-runtime")]
216    fn recover<T>(&self, error: CudaTranscodeError) -> Result<Option<T>, TranscodeStageError> {
217        self.mode.recover(error, CudaTranscodeError::is_recoverable)
218    }
219}
220
221fn reversible_batch_total_samples(jobs: &[DctGridToReversibleDwt53Job<'_>]) -> usize {
222    jobs.iter().fold(0usize, |total, job| {
223        total.saturating_add(job.width.saturating_mul(job.height))
224    })
225}
226
227fn dwt97_batch_total_samples(jobs: &[DctGridToDwt97Job<'_>]) -> usize {
228    jobs.iter().fold(0usize, |total, job| {
229        total.saturating_add(job.width.saturating_mul(job.height))
230    })
231}
232
233fn htj2k97_codeblock_batch_total_samples(jobs: &[DctGridToHtj2k97CodeBlockJob<'_>]) -> usize {
234    jobs.iter().fold(0usize, |total, job| {
235        total.saturating_add(job.width.saturating_mul(job.height))
236    })
237}
238
239fn htj2k97_i16_codeblock_batch_total_samples(
240    jobs: &[DctGridI16ToHtj2k97CodeBlockJob<'_>],
241) -> usize {
242    jobs.iter().fold(0usize, |total, job| {
243        total.saturating_add(job.width.saturating_mul(job.height))
244    })
245}
246
247fn htj2k97_i16_codeblock_batch_group_total_samples(
248    groups: &[DctGridI16ToHtj2k97CodeBlockBatch<'_, '_>],
249) -> usize {
250    groups.iter().fold(0usize, |total, group| {
251        total.saturating_add(htj2k97_i16_codeblock_batch_total_samples(group.jobs))
252    })
253}
254
255impl Default for CudaDctToWaveletStageAccelerator {
256    fn default() -> Self {
257        Self::for_auto()
258    }
259}
260
261#[doc(hidden)]
262impl DctToWaveletStageAccelerator for CudaDctToWaveletStageAccelerator {
263    fn supports_dwt97_batch(&self) -> bool {
264        true
265    }
266
267    // The fused DCT->9/7->prequantized-codeblock path runs the staged 9/7
268    // kernels followed by per-subband deadzone quantization into code-block-major
269    // layout, mirroring the local Metal backend.
270    fn supports_htj2k97_codeblock_batch(&self) -> bool {
271        true
272    }
273
274    fn supports_htj2k97_i16_preencoded_batch(&self) -> bool {
275        self.resident_ht_encode
276    }
277
278    fn supports_htj2k97_compact_preencoded_batch(&self) -> bool {
279        self.resident_ht_encode && std::env::var_os(DISABLE_COMPACT_PREENCODED_ENV).is_none()
280    }
281
282    fn dct_grid_to_reversible_dwt53(
283        &mut self,
284        job: DctGridToReversibleDwt53Job<'_>,
285    ) -> Result<Option<ReversibleDwt53FirstLevel>, TranscodeStageError> {
286        self.counters
287            .record(CounterEvent::ReversibleDwt53Attempt, 1);
288
289        if self.mode.is_auto() && job.width.saturating_mul(job.height) < self.min_auto_samples {
290            return Ok(None);
291        }
292
293        #[cfg(not(feature = "cuda-runtime"))]
294        {
295            let _ = job;
296            self.mode.unavailable()
297        }
298
299        #[cfg(feature = "cuda-runtime")]
300        {
301            match cuda::dispatch_reversible_dwt53(self.cuda_session(), job) {
302                Ok(output) => {
303                    self.counters
304                        .record(CounterEvent::ReversibleDwt53Dispatch, 1);
305                    Ok(Some(output))
306                }
307                Err(error) => self.recover(error),
308            }
309        }
310    }
311
312    fn dct_grid_to_reversible_dwt53_batch(
313        &mut self,
314        jobs: &[DctGridToReversibleDwt53Job<'_>],
315    ) -> Result<Option<Vec<ReversibleDwt53FirstLevel>>, TranscodeStageError> {
316        self.counters
317            .record(CounterEvent::ReversibleDwt53BatchAttempt, 1);
318
319        if jobs.is_empty() {
320            return Ok(Some(Vec::new()));
321        }
322        if self.mode.is_auto()
323            && (jobs.len() < self.min_auto_reversible_batch_jobs
324                || reversible_batch_total_samples(jobs) < self.min_auto_reversible_batch_samples)
325        {
326            return Ok(None);
327        }
328
329        #[cfg(not(feature = "cuda-runtime"))]
330        {
331            let _ = jobs;
332            self.mode.unavailable()
333        }
334
335        #[cfg(feature = "cuda-runtime")]
336        {
337            match cuda::dispatch_reversible_dwt53_batch(self.cuda_session(), jobs) {
338                Ok(output) => {
339                    self.counters
340                        .record(CounterEvent::ReversibleDwt53BatchDispatch, 1);
341                    Ok(Some(output))
342                }
343                Err(error) => self.recover(error),
344            }
345        }
346    }
347
348    fn dct_grid_to_dwt53(
349        &mut self,
350        job: DctGridToDwt53Job<'_>,
351    ) -> Result<Option<Dwt53TwoDimensional<f64>>, TranscodeStageError> {
352        self.counters.record(CounterEvent::Dwt53Attempt, 1);
353
354        if self.mode.is_auto() && job.width.saturating_mul(job.height) < self.min_auto_samples {
355            return Ok(None);
356        }
357
358        #[cfg(not(feature = "cuda-runtime"))]
359        {
360            let _ = job;
361            self.mode.unavailable()
362        }
363
364        #[cfg(feature = "cuda-runtime")]
365        {
366            match cuda::dispatch_dwt53(job) {
367                Ok(output) => {
368                    self.counters.record(CounterEvent::Dwt53Dispatch, 1);
369                    Ok(Some(output))
370                }
371                Err(error) => self.recover(error),
372            }
373        }
374    }
375
376    fn dct_grid_to_dwt97(
377        &mut self,
378        job: DctGridToDwt97Job<'_>,
379    ) -> Result<Option<Dwt97TwoDimensional<f64>>, TranscodeStageError> {
380        self.counters.record(CounterEvent::Dwt97Attempt, 1);
381
382        if self.mode.is_auto() && job.width.saturating_mul(job.height) < self.min_auto_samples {
383            return Ok(None);
384        }
385
386        #[cfg(not(feature = "cuda-runtime"))]
387        {
388            let _ = job;
389            self.mode.unavailable()
390        }
391
392        #[cfg(feature = "cuda-runtime")]
393        {
394            match cuda::dispatch_dwt97(self.cuda_session(), job) {
395                Ok(output) => {
396                    self.counters.record(CounterEvent::Dwt97Dispatch, 1);
397                    Ok(Some(output))
398                }
399                Err(error) => self.recover(error),
400            }
401        }
402    }
403
404    fn dct_grid_to_dwt97_batch(
405        &mut self,
406        jobs: &[DctGridToDwt97Job<'_>],
407    ) -> Result<Option<Vec<Dwt97TwoDimensional<f64>>>, TranscodeStageError> {
408        self.counters.record(CounterEvent::Dwt97BatchAttempt, 1);
409        self.last_dwt97_batch_stage_timings = None;
410
411        if jobs.is_empty() {
412            return Ok(Some(Vec::new()));
413        }
414        if self.mode.is_auto()
415            && (jobs.len() < self.min_auto_dwt97_batch_jobs
416                || dwt97_batch_total_samples(jobs) < self.min_auto_dwt97_batch_samples)
417        {
418            return Ok(None);
419        }
420
421        #[cfg(not(feature = "cuda-runtime"))]
422        {
423            let _ = jobs;
424            self.mode.unavailable()
425        }
426
427        #[cfg(feature = "cuda-runtime")]
428        {
429            match cuda::dispatch_dwt97_batch(self.cuda_session(), jobs) {
430                Ok((output, timings)) => {
431                    self.counters.record(CounterEvent::Dwt97BatchDispatch, 1);
432                    self.last_dwt97_batch_stage_timings = Some(timings);
433                    Ok(Some(output))
434                }
435                Err(error) => self.recover(error),
436            }
437        }
438    }
439
440    fn dct_grid_to_htj2k97_codeblock_batch(
441        &mut self,
442        jobs: &[DctGridToHtj2k97CodeBlockJob<'_>],
443        options: Htj2k97CodeBlockOptions,
444    ) -> Result<Option<Vec<PrequantizedHtj2k97Component>>, TranscodeStageError> {
445        // The code-block path is a staged 9/7 batch plus quantization, so it
446        // counts as both a 9/7 batch and a code-block batch (matching Metal).
447        self.counters.record(CounterEvent::Dwt97BatchAttempt, 1);
448        self.counters
449            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, 1);
450        self.last_dwt97_batch_stage_timings = None;
451
452        if jobs.is_empty() {
453            return Ok(Some(Vec::new()));
454        }
455        if self.mode.is_auto()
456            && (jobs.len() < self.min_auto_dwt97_batch_jobs
457                || htj2k97_codeblock_batch_total_samples(jobs) < self.min_auto_dwt97_batch_samples)
458        {
459            return Ok(None);
460        }
461
462        #[cfg(not(feature = "cuda-runtime"))]
463        {
464            let _ = (jobs, options);
465            self.mode.unavailable()
466        }
467
468        #[cfg(feature = "cuda-runtime")]
469        {
470            match cuda::dispatch_htj2k97_codeblock_batch(self.cuda_session(), jobs, options) {
471                Ok((output, timings)) => {
472                    self.counters.record(CounterEvent::Dwt97BatchDispatch, 1);
473                    self.counters
474                        .record(CounterEvent::Htj2k97CodeblockBatchDispatch, 1);
475                    self.last_dwt97_batch_stage_timings = Some(timings);
476                    Ok(Some(output))
477                }
478                Err(error) => self.recover(error),
479            }
480        }
481    }
482
483    fn dct_grid_to_htj2k97_preencoded_batch(
484        &mut self,
485        jobs: &[DctGridToHtj2k97CodeBlockJob<'_>],
486        options: Htj2k97CodeBlockOptions,
487    ) -> Result<Option<Vec<PreencodedHtj2k97Component>>, TranscodeStageError> {
488        if !self.resident_ht_encode {
489            return Ok(None);
490        }
491
492        self.counters.record(CounterEvent::Dwt97BatchAttempt, 1);
493        self.counters
494            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, 1);
495        self.last_dwt97_batch_stage_timings = None;
496
497        if jobs.is_empty() {
498            return Ok(Some(Vec::new()));
499        }
500        if self.mode.is_auto()
501            && (jobs.len() < self.min_auto_dwt97_batch_jobs
502                || htj2k97_codeblock_batch_total_samples(jobs) < self.min_auto_dwt97_batch_samples)
503        {
504            return Ok(None);
505        }
506
507        #[cfg(not(feature = "cuda-runtime"))]
508        {
509            let _ = (jobs, options);
510            self.mode.unavailable()
511        }
512
513        #[cfg(feature = "cuda-runtime")]
514        {
515            match cuda::dispatch_htj2k97_preencoded_batch(self.cuda_session(), jobs, options) {
516                Ok((output, timings)) => {
517                    self.counters.record(CounterEvent::Dwt97BatchDispatch, 1);
518                    self.counters
519                        .record(CounterEvent::Htj2k97CodeblockBatchDispatch, 1);
520                    self.last_dwt97_batch_stage_timings = Some(timings);
521                    Ok(Some(output))
522                }
523                Err(error) => self.recover(error),
524            }
525        }
526    }
527
528    fn dct_grid_i16_to_htj2k97_preencoded_batch(
529        &mut self,
530        jobs: &[DctGridI16ToHtj2k97CodeBlockJob<'_>],
531        options: Htj2k97CodeBlockOptions,
532    ) -> Result<Option<Vec<PreencodedHtj2k97Component>>, TranscodeStageError> {
533        if !self.resident_ht_encode {
534            return Ok(None);
535        }
536
537        self.counters.record(CounterEvent::Dwt97BatchAttempt, 1);
538        self.counters
539            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, 1);
540        self.last_dwt97_batch_stage_timings = None;
541
542        if jobs.is_empty() {
543            return Ok(Some(Vec::new()));
544        }
545        if self.mode.is_auto()
546            && (jobs.len() < self.min_auto_dwt97_batch_jobs
547                || htj2k97_i16_codeblock_batch_total_samples(jobs)
548                    < self.min_auto_dwt97_batch_samples)
549        {
550            return Ok(None);
551        }
552
553        #[cfg(not(feature = "cuda-runtime"))]
554        {
555            let _ = (jobs, options);
556            self.mode.unavailable()
557        }
558
559        #[cfg(feature = "cuda-runtime")]
560        {
561            match cuda::dispatch_htj2k97_preencoded_i16_batch(self.cuda_session(), jobs, options) {
562                Ok((output, timings)) => {
563                    self.counters.record(CounterEvent::Dwt97BatchDispatch, 1);
564                    self.counters
565                        .record(CounterEvent::Htj2k97CodeblockBatchDispatch, 1);
566                    self.last_dwt97_batch_stage_timings = Some(timings);
567                    Ok(Some(output))
568                }
569                Err(error) => self.recover(error),
570            }
571        }
572    }
573
574    fn dct_grid_i16_to_htj2k97_compact_preencoded_batch(
575        &mut self,
576        jobs: &[DctGridI16ToHtj2k97CodeBlockJob<'_>],
577        options: Htj2k97CodeBlockOptions,
578    ) -> Result<Option<PreencodedHtj2k97CompactBatch>, TranscodeStageError> {
579        if !self.resident_ht_encode {
580            return Ok(None);
581        }
582
583        self.counters.record(CounterEvent::Dwt97BatchAttempt, 1);
584        self.counters
585            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, 1);
586        self.last_dwt97_batch_stage_timings = None;
587
588        if jobs.is_empty() {
589            return Ok(Some(PreencodedHtj2k97CompactBatch {
590                payload: Vec::new(),
591                components: Vec::new(),
592            }));
593        }
594        if self.mode.is_auto()
595            && (jobs.len() < self.min_auto_dwt97_batch_jobs
596                || htj2k97_i16_codeblock_batch_total_samples(jobs)
597                    < self.min_auto_dwt97_batch_samples)
598        {
599            return Ok(None);
600        }
601
602        #[cfg(not(feature = "cuda-runtime"))]
603        {
604            let _ = (jobs, options);
605            self.mode.unavailable()
606        }
607
608        #[cfg(feature = "cuda-runtime")]
609        {
610            match cuda::dispatch_htj2k97_compact_preencoded_i16_batch(
611                self.cuda_session(),
612                jobs,
613                options,
614            ) {
615                Ok((output, timings)) => {
616                    self.counters.record(CounterEvent::Dwt97BatchDispatch, 1);
617                    self.counters
618                        .record(CounterEvent::Htj2k97CodeblockBatchDispatch, 1);
619                    self.last_dwt97_batch_stage_timings = Some(timings);
620                    Ok(Some(output))
621                }
622                Err(error) => self.recover(error),
623            }
624        }
625    }
626
627    fn dct_grid_i16_to_htj2k97_preencoded_batch_groups(
628        &mut self,
629        groups: &[DctGridI16ToHtj2k97CodeBlockBatch<'_, '_>],
630        options: Htj2k97CodeBlockOptions,
631    ) -> Result<Option<Vec<Vec<PreencodedHtj2k97Component>>>, TranscodeStageError> {
632        if !self.resident_ht_encode {
633            return Ok(None);
634        }
635
636        self.counters
637            .record(CounterEvent::Dwt97BatchAttempt, groups.len());
638        self.counters
639            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, groups.len());
640        self.last_dwt97_batch_stage_timings = None;
641
642        if groups.is_empty() {
643            return Ok(Some(Vec::new()));
644        }
645        let total_jobs = groups.iter().map(|group| group.jobs.len()).sum::<usize>();
646        if self.mode.is_auto()
647            && (total_jobs < self.min_auto_dwt97_batch_jobs
648                || htj2k97_i16_codeblock_batch_group_total_samples(groups)
649                    < self.min_auto_dwt97_batch_samples)
650        {
651            return Ok(None);
652        }
653
654        #[cfg(not(feature = "cuda-runtime"))]
655        {
656            let _ = (groups, options);
657            self.mode.unavailable()
658        }
659
660        #[cfg(feature = "cuda-runtime")]
661        {
662            match cuda::dispatch_htj2k97_preencoded_i16_batch_groups(
663                self.cuda_session(),
664                groups,
665                options,
666            ) {
667                Ok((output, timings)) => {
668                    self.counters
669                        .record(CounterEvent::Dwt97BatchDispatch, groups.len());
670                    self.counters.record(
671                        CounterEvent::Htj2k97CodeblockBatchDispatch,
672                        timings.ht_codeblock_dispatches,
673                    );
674                    self.last_dwt97_batch_stage_timings = Some(timings);
675                    Ok(Some(output))
676                }
677                Err(error) => self.recover(error),
678            }
679        }
680    }
681
682    fn dct_grid_i16_to_htj2k97_compact_preencoded_batch_groups(
683        &mut self,
684        groups: &[DctGridI16ToHtj2k97CodeBlockBatch<'_, '_>],
685        options: Htj2k97CodeBlockOptions,
686    ) -> Result<Option<PreencodedHtj2k97CompactBatchGroups>, TranscodeStageError> {
687        if !self.resident_ht_encode {
688            return Ok(None);
689        }
690
691        self.counters
692            .record(CounterEvent::Dwt97BatchAttempt, groups.len());
693        self.counters
694            .record(CounterEvent::Htj2k97CodeblockBatchAttempt, groups.len());
695        self.last_dwt97_batch_stage_timings = None;
696
697        if groups.is_empty() {
698            return Ok(Some(PreencodedHtj2k97CompactBatchGroups {
699                payload: Vec::new(),
700                groups: Vec::new(),
701            }));
702        }
703        let total_jobs = groups.iter().map(|group| group.jobs.len()).sum::<usize>();
704        if self.mode.is_auto()
705            && (total_jobs < self.min_auto_dwt97_batch_jobs
706                || htj2k97_i16_codeblock_batch_group_total_samples(groups)
707                    < self.min_auto_dwt97_batch_samples)
708        {
709            return Ok(None);
710        }
711
712        #[cfg(not(feature = "cuda-runtime"))]
713        {
714            let _ = (groups, options);
715            self.mode.unavailable()
716        }
717
718        #[cfg(feature = "cuda-runtime")]
719        {
720            match cuda::dispatch_htj2k97_compact_preencoded_i16_batch_groups(
721                self.cuda_session(),
722                groups,
723                options,
724            ) {
725                Ok((output, timings)) => {
726                    self.counters
727                        .record(CounterEvent::Dwt97BatchDispatch, groups.len());
728                    self.counters.record(
729                        CounterEvent::Htj2k97CodeblockBatchDispatch,
730                        timings.ht_codeblock_dispatches,
731                    );
732                    self.last_dwt97_batch_stage_timings = Some(timings);
733                    Ok(Some(output))
734                }
735                Err(error) => self.recover(error),
736            }
737        }
738    }
739
740    fn last_dwt97_batch_stage_timings(&self) -> Option<Dwt97BatchStageTimings> {
741        self.last_dwt97_batch_stage_timings
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use std::sync::Mutex;
749
750    static ENV_LOCK: Mutex<()> = Mutex::new(());
751
752    fn test_htj2k97_codeblock_options() -> Htj2k97CodeBlockOptions {
753        Htj2k97CodeBlockOptions {
754            bit_depth: 8,
755            guard_bits: 2,
756            code_block_width_exp: 4,
757            code_block_height_exp: 4,
758            irreversible_quantization_scale: 1.0,
759            irreversible_quantization_subband_scales:
760                j2k_transcode::IrreversibleQuantizationSubbandScales::default(),
761        }
762    }
763
764    #[test]
765    fn explicit_mode_without_cuda_runtime_errors_on_reversible_job() {
766        // Without the cuda-runtime feature, Explicit mode must surface a typed
767        // error rather than silently using the scalar fallback.
768        let mut accelerator = CudaDctToWaveletStageAccelerator::new_explicit();
769        let blocks: Vec<[i16; 64]> = vec![[0i16; 64]];
770        let job = DctGridToReversibleDwt53Job {
771            dequantized_blocks: &blocks,
772            block_cols: 1,
773            block_rows: 1,
774            width: 8,
775            height: 8,
776        };
777        let result = accelerator.dct_grid_to_reversible_dwt53(job);
778        #[cfg(not(feature = "cuda-runtime"))]
779        assert!(matches!(
780            result,
781            Err(TranscodeStageError::DeviceUnavailable)
782        ));
783        let _ = result;
784        assert_eq!(accelerator.reversible_dwt53_attempts(), 1);
785    }
786
787    #[test]
788    fn auto_mode_falls_back_to_scalar_for_small_jobs() {
789        // Auto mode returns Ok(None) for sub-threshold jobs so the transcode
790        // pipeline uses its scalar oracle.
791        let mut accelerator = CudaDctToWaveletStageAccelerator::for_auto();
792        let blocks: Vec<[i16; 64]> = vec![[0i16; 64]];
793        let job = DctGridToReversibleDwt53Job {
794            dequantized_blocks: &blocks,
795            block_cols: 1,
796            block_rows: 1,
797            width: 8,
798            height: 8,
799        };
800        assert!(matches!(
801            accelerator.dct_grid_to_reversible_dwt53(job),
802            Ok(None)
803        ));
804    }
805
806    #[test]
807    fn empty_batches_return_empty_without_dispatch() {
808        let mut accelerator = CudaDctToWaveletStageAccelerator::new_explicit();
809        assert!(matches!(
810            accelerator.dct_grid_to_reversible_dwt53_batch(&[]),
811            Ok(Some(values)) if values.is_empty()
812        ));
813        assert!(matches!(
814            accelerator.dct_grid_to_dwt97_batch(&[]),
815            Ok(Some(values)) if values.is_empty()
816        ));
817    }
818
819    #[test]
820    fn compact_preencoded_support_obeys_cuda_env_gate() {
821        let _guard = ENV_LOCK.lock().expect("env lock");
822        let previous = std::env::var_os(DISABLE_COMPACT_PREENCODED_ENV);
823        std::env::remove_var(DISABLE_COMPACT_PREENCODED_ENV);
824        let accelerator = CudaDctToWaveletStageAccelerator::new_explicit_resident_ht_encode();
825        assert!(accelerator.supports_htj2k97_i16_preencoded_batch());
826        assert!(accelerator.supports_htj2k97_compact_preencoded_batch());
827
828        std::env::set_var(DISABLE_COMPACT_PREENCODED_ENV, "1");
829        let accelerator = CudaDctToWaveletStageAccelerator::new_explicit_resident_ht_encode();
830        assert!(accelerator.supports_htj2k97_i16_preencoded_batch());
831        assert!(!accelerator.supports_htj2k97_compact_preencoded_batch());
832
833        if let Some(previous) = previous {
834            std::env::set_var(DISABLE_COMPACT_PREENCODED_ENV, previous);
835        } else {
836            std::env::remove_var(DISABLE_COMPACT_PREENCODED_ENV);
837        }
838    }
839
840    #[test]
841    fn auto_mode_declines_under_amortized_reversible_batches() {
842        let mut accelerator = CudaDctToWaveletStageAccelerator::for_auto()
843            .with_auto_reversible_batch_thresholds(2, 224 * 224 * 2);
844        let blocks = vec![[0i16; 64]; 256 * 256 / 64];
845        let job = DctGridToReversibleDwt53Job {
846            dequantized_blocks: &blocks,
847            block_cols: 32,
848            block_rows: 32,
849            width: 256,
850            height: 256,
851        };
852
853        assert!(matches!(
854            accelerator.dct_grid_to_reversible_dwt53_batch(&[job]),
855            Ok(None)
856        ));
857        assert_eq!(accelerator.reversible_dwt53_batch_attempts(), 1);
858        assert_eq!(accelerator.reversible_dwt53_batch_dispatches(), 0);
859    }
860
861    #[test]
862    fn auto_mode_declines_under_amortized_dwt97_batches() {
863        let mut accelerator = CudaDctToWaveletStageAccelerator::for_auto()
864            .with_auto_dwt97_batch_thresholds(2, 224 * 224 * 2);
865        let blocks = vec![[[0.0f64; 8]; 8]; 256 * 256 / 64];
866        let job = DctGridToDwt97Job {
867            blocks: &blocks,
868            block_cols: 32,
869            block_rows: 32,
870            width: 256,
871            height: 256,
872        };
873
874        assert!(matches!(
875            accelerator.dct_grid_to_dwt97_batch(&[job]),
876            Ok(None)
877        ));
878        assert_eq!(accelerator.dwt97_batch_attempts(), 1);
879        assert_eq!(accelerator.dwt97_batch_dispatches(), 0);
880    }
881
882    #[test]
883    fn auto_mode_declines_under_amortized_htj2k97_codeblock_batches() {
884        let mut accelerator = CudaDctToWaveletStageAccelerator::for_auto()
885            .with_auto_dwt97_batch_thresholds(2, 224 * 224 * 2);
886        let blocks = vec![[[0.0f64; 8]; 8]; 256 * 256 / 64];
887        let job = DctGridToHtj2k97CodeBlockJob {
888            blocks: &blocks,
889            block_cols: 32,
890            block_rows: 32,
891            width: 256,
892            height: 256,
893            x_rsiz: 1,
894            y_rsiz: 1,
895        };
896
897        let result = accelerator
898            .dct_grid_to_htj2k97_codeblock_batch(&[job], test_htj2k97_codeblock_options());
899        assert!(matches!(result, Ok(None)));
900        assert_eq!(accelerator.dwt97_batch_attempts(), 1);
901        assert_eq!(accelerator.dwt97_batch_dispatches(), 0);
902        assert_eq!(accelerator.htj2k97_codeblock_batch_attempts(), 1);
903        assert_eq!(accelerator.htj2k97_codeblock_batch_dispatches(), 0);
904    }
905}