1#[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
99pub 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 for queued in &mut self.queued {
190 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}