Skip to main content

j2k_cuda/
decoder.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[cfg(feature = "cuda-runtime")]
4use std::sync::Arc;
5
6use j2k::{
7    DeviceDecodePlan, DeviceDecodeRequest, J2kDecodeWarning, J2kDecoder as CpuDecoder,
8    J2kScratchPool as CpuJ2kScratchPool, J2kView,
9};
10#[cfg(feature = "cuda-runtime")]
11use j2k_core::BackendKind;
12use j2k_core::{
13    checked_surface_len, submit_ready_device, BackendRequest, CpuBackedImageDecode, DecodeOutcome,
14    Downscale, ImageCodec, ImageDecodeDevice, ImageDecodeSubmit, PixelFormat, ReadySubmission,
15    Rect, DEFAULT_MAX_HOST_ALLOCATION_BYTES,
16};
17#[cfg(feature = "cuda-runtime")]
18use j2k_cuda_runtime::{
19    CudaBufferPool, CudaBufferPoolTakeTrace, CudaClassicCodeBlockJob, CudaClassicDecodeTarget,
20    CudaClassicSegment, CudaContext, CudaDeviceBuffer, CudaError, CudaExecutionStats,
21    CudaHtj2kCleanupTarget, CudaHtj2kCodeBlockJob, CudaHtj2kDecodeResources,
22    CudaHtj2kDecodeTableResources, CudaHtj2kDequantizeTarget, CudaJ2kIdwtJob, CudaJ2kIdwtTarget,
23    CudaJ2kRect, CudaJ2kStoreGray16Job, CudaJ2kStoreGray8Job, CudaPooledDeviceBuffer,
24    CudaQueuedExecution, CudaQueuedHtj2kCleanup,
25};
26#[cfg(feature = "cuda-runtime")]
27use j2k_native::{DecodeSettings, DecoderContext as NativeDecoderContext, Image as NativeImage};
28
29#[cfg(feature = "cuda-runtime")]
30use crate::error::{combine_cuda_cleanup_errors, native_decode_error};
31#[cfg(feature = "cuda-runtime")]
32use crate::runtime::cuda_error;
33use crate::runtime::{validate_surface_request, wrap_cpu_staged_cuda_surface, wrap_surface};
34#[cfg(feature = "cuda-runtime")]
35use crate::surface::{cuda_range_storage, Storage};
36#[cfg(feature = "cuda-runtime")]
37use crate::{
38    profile, CudaHtj2kBandId, CudaHtj2kDecodePlan, CudaHtj2kDecodeProfileDetail, CudaHtj2kIdwtStep,
39    CudaHtj2kStoreStep, CudaHtj2kTransform, CudaSurfaceStats, SurfaceResidency,
40};
41use crate::{CudaHtj2kProfileReport, CudaSession, Error, Surface};
42
43#[cfg(feature = "cuda-runtime")]
44const CUDA_HTJ2K_KERNELS_NOT_READY: &str =
45    "strict CUDA HTJ2K resident codestream decode kernels are not available in this build";
46#[cfg(feature = "cuda-runtime")]
47const CUDA_HTJ2K_OUTPUT_FORMAT_UNSUPPORTED: &str =
48    "strict CUDA HTJ2K resident decode currently accepts Gray8, Gray16, GrayI16, Rgb8, Rgba8, Rgb16, and Rgba16 output";
49#[cfg(feature = "cuda-runtime")]
50const CUDA_HTJ2K_PLAN_INVARIANT_FAILED: &str =
51    "strict CUDA HTJ2K resident decode plan has invalid internal ranges";
52#[cfg(feature = "cuda-runtime")]
53const CUDA_HTJ2K_STORE_UNSUPPORTED: &str =
54    "strict CUDA HTJ2K resident decode requires a single grayscale store step";
55#[cfg(feature = "cuda-runtime")]
56const CUDA_HTJ2K_BATCH_PAYLOAD_TOO_LARGE: &str =
57    "strict CUDA HTJ2K resident batch decode payload is too large";
58#[cfg(feature = "cuda-runtime")]
59const CUDA_IDWT_TRACE_ENV_VAR: &str = "J2K_CUDA_IDWT_TRACE";
60
61mod api;
62#[cfg(feature = "cuda-runtime")]
63mod color_batch;
64#[path = "decoder/profile.rs"]
65mod decode_profile;
66#[cfg(feature = "cuda-runtime")]
67pub(crate) mod grayscale_batch;
68#[cfg(feature = "cuda-runtime")]
69mod pending_completion;
70#[cfg(feature = "cuda-runtime")]
71mod plan;
72#[cfg(feature = "cuda-runtime")]
73mod resident;
74
75#[cfg(feature = "cuda-runtime")]
76pub(crate) use self::color_batch::native_batch::{
77    submit_native_color_resident_prepared_batch, submit_native_color_resident_prepared_batch_into,
78    NativeColorBatchInput, NativeColorOwnedBatch, SubmittedNativeColorExternalBatch,
79    SubmittedNativeColorResidentBatch,
80};
81#[cfg(all(test, feature = "cuda-runtime"))]
82pub(crate) use self::color_batch::{
83    testing_cuda_htj2k_batch_decode_calls, testing_reset_cuda_htj2k_batch_decode_calls,
84};
85#[cfg(feature = "cuda-runtime")]
86use self::decode_profile::CudaDecodeStageTimings;
87#[cfg(all(test, feature = "cuda-runtime"))]
88use self::decode_profile::{format_cuda_idwt_batch_host_trace_row, CudaIdwtBatchHostTraceRow};
89#[cfg(all(test, feature = "cuda-runtime"))]
90use self::plan::build_cuda_htj2k_color_plans_from_bytes_with_profile;
91#[cfg(all(test, feature = "cuda-runtime"))]
92use self::resident::{
93    can_batch_color_idwt, cuda_code_block_job_from_plan_block,
94    htj2k_batched_cleanup_dequant_dispatches, htj2k_batched_cleanup_dispatches,
95};
96#[cfg(all(test, feature = "cuda-runtime"))]
97use self::resident::{htj2k_batched_dequant_dispatches, split_htj2k_subband_decode_dispatches};
98
99/// CUDA-facing JPEG 2000 decoder wrapper.
100pub struct J2kDecoder<'a> {
101    #[cfg_attr(
102        not(feature = "cuda-runtime"),
103        expect(
104            dead_code,
105            reason = "raw codestream bytes are consumed only by CUDA decode routes"
106        )
107    )]
108    bytes: &'a [u8],
109    inner: CpuDecoder<'a>,
110    transfer_syntax: Option<j2k_core::CompressedTransferSyntax>,
111    payload_kind: Option<j2k_core::CompressedPayloadKind>,
112    pool: CpuJ2kScratchPool,
113}
114
115#[cfg(feature = "cuda-runtime")]
116struct CudaCoefficientBand {
117    band_id: CudaHtj2kBandId,
118    buffer: CudaPooledDeviceBuffer,
119}
120
121#[cfg(feature = "cuda-runtime")]
122struct CudaPendingDequantBand {
123    band_index: usize,
124    jobs: Vec<CudaHtj2kCodeBlockJob>,
125    output_words: usize,
126}
127
128#[cfg(feature = "cuda-runtime")]
129struct CudaPendingClassicBand {
130    band_index: usize,
131    jobs: Vec<CudaClassicCodeBlockJob>,
132    segments: Vec<CudaClassicSegment>,
133    output_words: usize,
134}
135
136#[cfg(feature = "cuda-runtime")]
137struct CudaComponentDecodeWork {
138    bands: Vec<CudaCoefficientBand>,
139    pending_classic_bands: Vec<CudaPendingClassicBand>,
140    pending_dequant_bands: Vec<CudaPendingDequantBand>,
141    store: CudaHtj2kStoreStep,
142    dispatches: usize,
143    decode_dispatches: usize,
144    timings: CudaDecodeStageTimings,
145}
146
147#[cfg(feature = "cuda-runtime")]
148struct CudaQueuedIdwtBatch {
149    context: CudaContext,
150    queued: Vec<CudaQueuedExecution>,
151    kernel_dispatches: usize,
152    decode_dispatches: usize,
153}
154
155#[cfg(feature = "cuda-runtime")]
156impl CudaQueuedIdwtBatch {
157    fn merge(mut self, mut next: Self) -> Result<Self, Error> {
158        if !self.context.is_same_context(&next.context) {
159            return Err(Error::UnsupportedCudaRequest {
160                reason: CUDA_HTJ2K_PLAN_INVARIANT_FAILED,
161            });
162        }
163        self.queued
164            .try_reserve_exact(next.queued.len())
165            .map_err(|_| {
166                crate::allocation::host_allocation_error::<CudaQueuedExecution>(
167                    self.queued.len().saturating_add(next.queued.len()),
168                    "j2k CUDA independent IDWT completion guards",
169                )
170            })?;
171        self.queued.append(&mut next.queued);
172        self.kernel_dispatches = self
173            .kernel_dispatches
174            .saturating_add(next.kernel_dispatches);
175        self.decode_dispatches = self
176            .decode_dispatches
177            .saturating_add(next.decode_dispatches);
178        Ok(self)
179    }
180
181    fn resources_pending(&self) -> bool {
182        self.kernel_dispatches != 0 && !self.queued.is_empty()
183    }
184
185    fn release_after_completion(&mut self) -> Result<(), Error> {
186        // The decode pools are private to an exclusively borrowed session.
187        // MCT/store helpers use completion-synchronizing runtime launches, so
188        // reaching this path proves every IDWT reference has completed.
189        for queued in &mut self.queued {
190            // SAFETY: `CudaSession` owns this pool privately behind an
191            // exclusive mutable borrow. Callers reach this method only after
192            // context synchronization or a completion-synchronizing MCT/store
193            // dispatch on the same context.
194            unsafe { queued.release_pool_reuse_after_completion() }.map_err(cuda_error)?;
195        }
196        self.queued.clear();
197        Ok(())
198    }
199
200    fn synchronize_and_release(&mut self) -> Result<(), Error> {
201        if self.resources_pending() {
202            self.context.synchronize().map_err(cuda_error)?;
203        }
204        self.release_after_completion()
205    }
206
207    fn finish(mut self) -> Result<(), Error> {
208        self.synchronize_and_release()
209    }
210
211    fn resolve_optional_after_completed_work<T>(
212        pending: Option<Self>,
213        result: Result<(T, bool), Error>,
214    ) -> Result<T, Error> {
215        let Some(mut pending) = pending else {
216            return result.map(|(output, _completion_established)| output);
217        };
218        match result {
219            Ok((output, completion_established)) => {
220                if pending.resources_pending() && !completion_established {
221                    pending.synchronize_and_release()?;
222                } else {
223                    pending.release_after_completion()?;
224                }
225                Ok(output)
226            }
227            Err(error) => match pending.synchronize_and_release() {
228                Ok(()) => Err(error),
229                Err(cleanup_error) => Err(combine_cuda_cleanup_errors(error, cleanup_error)),
230            },
231        }
232    }
233}
234
235#[cfg(feature = "cuda-runtime")]
236struct CudaDecodedComponent {
237    buffer: CudaPooledDeviceBuffer,
238    store: CudaHtj2kStoreStep,
239    dispatches: usize,
240    decode_dispatches: usize,
241    timings: CudaDecodeStageTimings,
242}
243
244#[cfg(feature = "cuda-runtime")]
245struct CudaHtj2kColorDecodePlans {
246    output_index: usize,
247    dimensions: (u32, u32),
248    mct_dimensions: (u32, u32),
249    bit_depths: [u8; 4],
250    mct: bool,
251    transform: CudaHtj2kTransform,
252    payload: Vec<u8>,
253    components: Vec<CudaHtj2kDecodePlan>,
254    report: CudaHtj2kProfileReport,
255}
256
257#[cfg(feature = "cuda-runtime")]
258impl CudaHtj2kColorDecodePlans {
259    const fn rgb_bit_depths(&self) -> [u8; 3] {
260        [self.bit_depths[0], self.bit_depths[1], self.bit_depths[2]]
261    }
262}
263
264#[cfg(all(test, feature = "cuda-runtime"))]
265mod tests {
266    use super::{
267        build_cuda_htj2k_color_plans_from_bytes_with_profile, can_batch_color_idwt,
268        cuda_code_block_job_from_plan_block, htj2k_batched_cleanup_dequant_dispatches,
269        htj2k_batched_cleanup_dispatches, htj2k_batched_dequant_dispatches, CudaDecodeStageTimings,
270    };
271    use j2k_core::PixelFormat;
272    use j2k_native::{encode_htj2k, DecoderContext as NativeDecoderContext, EncodeOptions};
273
274    use crate::CudaHtj2kCodeBlock;
275
276    #[test]
277    fn cuda_runtime_code_block_job_preserves_plan_output_stride() {
278        let block = CudaHtj2kCodeBlock {
279            subband_index: 0,
280            payload_offset: 13,
281            payload_len: 5,
282            cleanup_length: 5,
283            refinement_length: 0,
284            output_x: 3,
285            output_y: 2,
286            width: 4,
287            height: 5,
288            output_stride: 99,
289            missing_bit_planes: 1,
290            number_of_coding_passes: 1,
291            num_bitplanes: 8,
292            stripe_causal: 0,
293            dequantization_step: 1.0,
294        };
295
296        let job = cuda_code_block_job_from_plan_block(&block, 64)
297            .expect("valid CUDA code-block runtime job");
298
299        assert_eq!(job.output_offset, 131);
300        assert_eq!(job.output_stride, 99);
301    }
302
303    #[test]
304    fn batched_cleanup_and_dequant_dispatch_helpers_count_one_shared_dispatch() {
305        assert_eq!(htj2k_batched_cleanup_dispatches(0), 0);
306        assert_eq!(htj2k_batched_cleanup_dispatches(1), 1);
307        assert_eq!(htj2k_batched_cleanup_dispatches(3), 1);
308        assert_eq!(htj2k_batched_dequant_dispatches(0), 0);
309        assert_eq!(htj2k_batched_dequant_dispatches(1), 1);
310        assert_eq!(htj2k_batched_dequant_dispatches(3), 1);
311        assert_eq!(htj2k_batched_cleanup_dequant_dispatches(0, true), (0, 0));
312        assert_eq!(htj2k_batched_cleanup_dequant_dispatches(1, true), (1, 0));
313        assert_eq!(htj2k_batched_cleanup_dequant_dispatches(3, true), (1, 0));
314        assert_eq!(htj2k_batched_cleanup_dequant_dispatches(1, false), (1, 1));
315        assert_eq!(htj2k_batched_cleanup_dequant_dispatches(3, false), (1, 1));
316    }
317
318    #[test]
319    fn profiled_cuda_batch_decode_api_accepts_empty_batch() {
320        let mut session = crate::CudaSession::default();
321        let inputs: [&[u8]; 0] = [];
322
323        let (surfaces, report) =
324            crate::J2kDecoder::decode_batch_to_device_with_session_and_profile(
325                &inputs,
326                PixelFormat::Rgb8,
327                &mut session,
328            )
329            .expect("empty CUDA batch decode");
330
331        assert!(surfaces.is_empty());
332        assert_eq!(report.block_count, 0);
333        assert_eq!(report.payload_bytes, 0);
334    }
335
336    #[test]
337    fn cuda_batch_decode_two_color_images_matches_single_when_runtime_required() {
338        let pixels_a: Vec<u8> = (0u16..16 * 16 * 3)
339            .map(|idx| u8::try_from((idx * 7 + idx / 5) & 0xff).expect("masked byte"))
340            .collect();
341        let pixels_b: Vec<u8> = (0u16..16 * 16 * 3)
342            .map(|idx| u8::try_from((idx * 11 + 23) & 0xff).expect("masked byte"))
343            .collect();
344        let options = EncodeOptions {
345            reversible: true,
346            num_decomposition_levels: 1,
347            ..EncodeOptions::default()
348        };
349        let codestream_a =
350            encode_htj2k(&pixels_a, 16, 16, 3, 8, false, &options).expect("encode fixture A");
351        let codestream_b =
352            encode_htj2k(&pixels_b, 16, 16, 3, 8, false, &options).expect("encode fixture B");
353        let inputs = [codestream_a.as_slice(), codestream_b.as_slice()];
354        let mut batch_session = crate::CudaSession::default();
355
356        let batch = crate::J2kDecoder::decode_batch_to_device_with_session_and_profile(
357            &inputs,
358            PixelFormat::Rgb8,
359            &mut batch_session,
360        );
361        let (surfaces, report) = match batch {
362            Ok(result) => result,
363            Err(crate::Error::CudaUnavailable | crate::Error::CudaRuntime { .. })
364                if !cuda_runtime_gate() =>
365            {
366                return;
367            }
368            Err(error) => panic!("batch CUDA decode failed: {error}"),
369        };
370
371        assert_eq!(surfaces.len(), 2);
372        assert_eq!(report.detail.ht_dispatch_count, 1);
373        assert_eq!(report.detail.dequant_dispatch_count, 0);
374        assert_eq!(report.detail.store_dispatch_count, 1);
375        let batch_pixels_tight =
376            crate::Surface::download_batch_tight(&surfaces).expect("download tight CUDA batch");
377        assert_eq!(batch_pixels_tight.len(), surfaces.len() * 16 * 16 * 3);
378        for (index, codestream) in inputs.iter().enumerate() {
379            let mut single_session = crate::CudaSession::default();
380            let mut decoder = crate::J2kDecoder::new(codestream).expect("single decoder");
381            let single = decoder
382                .decode_to_device_with_session(PixelFormat::Rgb8, &mut single_session)
383                .expect("single CUDA decode");
384            let mut single_pixels = vec![0u8; 16 * 16 * 3];
385            let mut batch_pixels = vec![0u8; 16 * 16 * 3];
386            single
387                .download_into(&mut single_pixels, 16 * 3)
388                .expect("download single decode");
389            surfaces[index]
390                .download_into(&mut batch_pixels, 16 * 3)
391                .expect("download batch decode");
392            assert_eq!(batch_pixels, single_pixels);
393            assert_eq!(
394                &batch_pixels_tight[index * 16 * 16 * 3..(index + 1) * 16 * 16 * 3],
395                single_pixels.as_slice()
396            );
397        }
398    }
399
400    #[test]
401    fn cuda_batch_decode_mixed_idwt_shapes_avoids_fused_batch_store_without_idwt_batch() {
402        let codestream_a = rgb8_htj2k_fixture(32, 32, 1, 7);
403        let codestream_b = rgb8_htj2k_fixture(32, 32, 2, 19);
404        let inputs = [codestream_a.as_slice(), codestream_b.as_slice()];
405        let mut batch_session = crate::CudaSession::default();
406
407        let result = crate::J2kDecoder::decode_batch_to_device_with_session(
408            &inputs,
409            PixelFormat::Rgb8,
410            &mut batch_session,
411        );
412        let surfaces = match result {
413            Ok(surfaces) => surfaces,
414            Err(crate::Error::CudaUnavailable | crate::Error::CudaRuntime { .. })
415                if !cuda_runtime_gate() =>
416            {
417                return;
418            }
419            Err(crate::Error::UnsupportedCudaRequest { .. }) => return,
420            Err(error) => panic!("mixed-shape batch CUDA decode failed: {error}"),
421        };
422
423        assert_eq!(surfaces.len(), inputs.len());
424        for (index, codestream) in inputs.iter().enumerate() {
425            let mut single_session = crate::CudaSession::default();
426            let mut decoder = crate::J2kDecoder::new(codestream).expect("single decoder");
427            let single = decoder
428                .decode_to_device_with_session(PixelFormat::Rgb8, &mut single_session)
429                .expect("single CUDA decode");
430            let mut single_pixels = vec![0u8; 32 * 32 * 3];
431            let mut batch_pixels = vec![0u8; 32 * 32 * 3];
432            single
433                .download_into(&mut single_pixels, 32 * 3)
434                .expect("download single decode");
435            surfaces[index]
436                .download_into(&mut batch_pixels, 32 * 3)
437                .expect("download mixed-shape batch decode");
438            assert_eq!(batch_pixels, single_pixels);
439        }
440    }
441
442    #[test]
443    fn decode_stage_timings_report_status_download_detail() {
444        let mut report = crate::CudaHtj2kProfileReport::default();
445        let timings = CudaDecodeStageTimings {
446            h2d: 17,
447            table_upload: 7,
448            job_upload: 10,
449            status_d2h: 5,
450            classic_tier1: 11,
451            ..CudaDecodeStageTimings::default()
452        };
453
454        timings.add_to_report(&mut report);
455
456        assert_eq!(report.h2d_us, 17);
457        assert_eq!(report.detail.table_upload_us, 7);
458        assert_eq!(report.detail.job_upload_us, 10);
459        assert_eq!(report.detail.status_d2h_us, 5);
460        assert_eq!(report.classic_tier1_us, 11);
461    }
462
463    fn cuda_runtime_gate() -> bool {
464        j2k_test_support::cuda_runtime_gate(module_path!())
465    }
466
467    fn rgb8_htj2k_fixture(width: u32, height: u32, levels: u8, seed: u16) -> Vec<u8> {
468        let mut pixels = Vec::with_capacity(width as usize * height as usize * 3);
469        for idx in 0..width * height {
470            let seed = u32::from(seed);
471            pixels.push(u8::try_from((idx * seed + idx / 3) & 0xff).expect("red"));
472            pixels.push(u8::try_from((idx * (seed + 11) + 7) & 0xff).expect("green"));
473            pixels.push(u8::try_from((idx * (seed + 23) + 19) & 0xff).expect("blue"));
474        }
475        let options = EncodeOptions {
476            reversible: true,
477            num_decomposition_levels: levels,
478            ..EncodeOptions::default()
479        };
480        encode_htj2k(&pixels, width, height, 3, 8, false, &options)
481            .expect("encode RGB HTJ2K fixture")
482    }
483
484    #[test]
485    fn color_plan_flattens_one_shared_payload_for_component_decode() {
486        let pixels: Vec<u8> = (0u16..4 * 4 * 3)
487            .map(|idx| u8::try_from((idx * 13 + idx / 3) & 0xff).expect("masked byte"))
488            .collect();
489        let options = EncodeOptions {
490            reversible: true,
491            num_decomposition_levels: 1,
492            ..EncodeOptions::default()
493        };
494        let codestream =
495            encode_htj2k(&pixels, 4, 4, 3, 8, false, &options).expect("encode HTJ2K RGB fixture");
496        let mut decoder = crate::J2kDecoder::new(&codestream).expect("decoder");
497
498        let color = decoder
499            .build_cuda_htj2k_color_plans_with_profile(PixelFormat::Rgb8)
500            .expect("CUDA color plans");
501
502        assert_eq!(color.components.len(), 3);
503        assert!(!color.payload.is_empty());
504        assert_eq!(color.report.payload_bytes, color.payload.len());
505        for component in &color.components {
506            assert!(component.payload().is_empty());
507            for block in component.code_blocks() {
508                let start = usize::try_from(block.payload_offset).expect("payload offset");
509                let end = start + block.payload_len as usize;
510                assert!(end <= color.payload.len());
511            }
512        }
513    }
514
515    #[test]
516    fn byte_color_plan_builder_matches_decoder_color_plan() {
517        let pixels: Vec<u8> = (0u16..8 * 8 * 3)
518            .map(|idx| u8::try_from((idx * 19 + idx / 5) & 0xff).expect("masked byte"))
519            .collect();
520        let options = EncodeOptions {
521            reversible: true,
522            num_decomposition_levels: 1,
523            ..EncodeOptions::default()
524        };
525        let codestream =
526            encode_htj2k(&pixels, 8, 8, 3, 8, false, &options).expect("encode HTJ2K RGB fixture");
527        let mut decoder = crate::J2kDecoder::new(&codestream).expect("decoder");
528        let decoder_plan = decoder
529            .build_cuda_htj2k_color_plans_with_profile(PixelFormat::Rgb8)
530            .expect("decoder CUDA color plans");
531        let mut native_context = NativeDecoderContext::default();
532        let byte_plan = build_cuda_htj2k_color_plans_from_bytes_with_profile(
533            &codestream,
534            PixelFormat::Rgb8,
535            &mut native_context,
536        )
537        .expect("byte CUDA color plans");
538
539        assert_eq!(byte_plan.dimensions, decoder_plan.dimensions);
540        assert_eq!(byte_plan.mct_dimensions, decoder_plan.mct_dimensions);
541        assert_eq!(byte_plan.bit_depths, decoder_plan.bit_depths);
542        assert_eq!(byte_plan.mct, decoder_plan.mct);
543        assert_eq!(byte_plan.components.len(), decoder_plan.components.len());
544        assert_eq!(byte_plan.payload.len(), decoder_plan.payload.len());
545        assert_eq!(
546            byte_plan
547                .components
548                .iter()
549                .map(|component| component.code_blocks().len())
550                .collect::<Vec<_>>(),
551            decoder_plan
552                .components
553                .iter()
554                .map(|component| component.code_blocks().len())
555                .collect::<Vec<_>>()
556        );
557    }
558
559    #[test]
560    fn multi_image_color_components_can_share_one_idwt_batch() {
561        let pixels: Vec<u8> = (0u16..16 * 16 * 3)
562            .map(|idx| u8::try_from((idx * 17 + idx / 7) & 0xff).expect("masked byte"))
563            .collect();
564        let options = EncodeOptions {
565            reversible: true,
566            num_decomposition_levels: 1,
567            ..EncodeOptions::default()
568        };
569        let codestream =
570            encode_htj2k(&pixels, 16, 16, 3, 8, false, &options).expect("encode HTJ2K RGB fixture");
571        let mut first = crate::J2kDecoder::new(&codestream).expect("first decoder");
572        let mut second = crate::J2kDecoder::new(&codestream).expect("second decoder");
573        let first = first
574            .build_cuda_htj2k_color_plans_with_profile(PixelFormat::Rgb8)
575            .expect("first CUDA color plans");
576        let second = second
577            .build_cuda_htj2k_color_plans_with_profile(PixelFormat::Rgb8)
578            .expect("second CUDA color plans");
579        let components = first
580            .components
581            .iter()
582            .chain(second.components.iter())
583            .collect::<Vec<_>>();
584
585        assert_eq!(components.len(), 6);
586        assert!(can_batch_color_idwt(&components));
587    }
588
589    #[test]
590    fn batched_color_idwt_defers_completion_to_store_sync() {
591        let source = include_str!("decoder.rs");
592
593        assert!(
594            !source.contains(
595                "if !collect_stage_timings {\n        context.synchronize().map_err(cuda_error)?;\n    }"
596            ),
597            "batched color IDWT should keep queued resources live and let the following store synchronize"
598        );
599    }
600
601    #[test]
602    fn batched_color_idwt_preflights_each_output_before_pool_take() {
603        let source = include_str!("decoder/resident/idwt.rs");
604        let function = source
605            .split("fn enqueue_color_component_idwt_batches")
606            .nth(1)
607            .expect("batched IDWT enqueue function");
608        let preflight = function
609            .find("j2k_inverse_dwt_single_output_bytes")
610            .expect("runtime IDWT output preflight");
611        for pool_take in [
612            "pool.take_with_trace(output_bytes)",
613            "pool.take(output_bytes)",
614        ] {
615            let pool_take = function.find(pool_take).expect("IDWT output pool take");
616            assert!(
617                preflight < pool_take,
618                "IDWT job semantics and launch geometry must be validated before output allocation"
619            );
620        }
621    }
622}
623
624#[cfg(all(test, feature = "cuda-runtime"))]
625#[path = "htj2k_plan_tests.rs"]
626mod htj2k_plan_tests;
627
628#[cfg(all(test, feature = "cuda-runtime"))]
629mod dispatch_tests {
630    use super::{
631        format_cuda_idwt_batch_host_trace_row, htj2k_batched_dequant_dispatches,
632        split_htj2k_subband_decode_dispatches, CudaIdwtBatchHostTraceRow,
633    };
634
635    #[test]
636    fn htj2k_decode_dispatch_split_separates_ht_and_dequant_counts() {
637        assert_eq!(split_htj2k_subband_decode_dispatches(0), (0, 0));
638        assert_eq!(split_htj2k_subband_decode_dispatches(1), (1, 0));
639        assert_eq!(split_htj2k_subband_decode_dispatches(2), (1, 1));
640        assert_eq!(split_htj2k_subband_decode_dispatches(3), (2, 1));
641    }
642
643    #[test]
644    fn htj2k_batched_dequant_dispatch_count_is_one_for_any_non_empty_batch() {
645        assert_eq!(htj2k_batched_dequant_dispatches(0), 0);
646        assert_eq!(htj2k_batched_dequant_dispatches(1), 1);
647        assert_eq!(htj2k_batched_dequant_dispatches(48), 1);
648    }
649
650    #[test]
651    fn cuda_idwt_batch_host_trace_row_reports_host_split() {
652        let row = CudaIdwtBatchHostTraceRow {
653            component_count: 327,
654            step_count: 5,
655            output_alloc_us: 11,
656            target_build_us: 22,
657            enqueue_us: 33,
658            output_take_count: 1635,
659            output_pool_reuse_count: 1600,
660            output_pool_alloc_count: 35,
661            output_pool_scanned_count: 2400,
662            output_pool_max_free_count: 1700,
663            output_requested_bytes: 28,
664        };
665
666        assert_eq!(
667            format_cuda_idwt_batch_host_trace_row(row).expect("bounded host trace row"),
668            "j2k_profile codec=j2k op=cuda_idwt_batch_host path=decode component_count=327 step_count=5 output_alloc_us=11 target_build_us=22 enqueue_us=33 output_take_count=1635 output_pool_reuse_count=1600 output_pool_alloc_count=35 output_pool_scanned_count=2400 output_pool_max_free_count=1700 output_requested_bytes=28"
669        );
670    }
671}