Skip to main content

j2k_cuda/batch/
external.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Caller-owned CUDA destination submission and completion.
4
5use super::{BatchGroupInfo, CudaBatchError, Error, J2kDecodeWarning, Rect};
6
7/// Metadata for a homogeneous group decoded directly into caller-owned CUDA
8/// storage.
9#[cfg(feature = "cuda-runtime")]
10#[derive(Debug)]
11pub struct CudaExternalBatchGroup {
12    pub(super) info: BatchGroupInfo,
13    pub(super) source_indices: Vec<usize>,
14    pub(super) decoded_rects: Vec<Rect>,
15    pub(super) warnings: Vec<Vec<J2kDecodeWarning>>,
16    pub(super) ranges: Vec<j2k_cuda_runtime::CudaDeviceBufferRange>,
17}
18
19/// Asynchronously submitted CUDA external-destination batch.
20///
21/// The destination ranges are metadata-only until the caller stream has been
22/// ordered after codec completion or [`Self::wait`] succeeds. Dropping this
23/// value waits before releasing codec-internal resources.
24#[cfg(feature = "cuda-runtime")]
25#[must_use = "submitted CUDA decode must be retained or waited"]
26pub struct SubmittedCudaExternalBatch {
27    pub(super) group: CudaExternalBatchGroup,
28    pub(super) pending: SubmittedCudaCodecBatch,
29}
30
31#[cfg(feature = "cuda-runtime")]
32pub(super) enum SubmittedCudaCodecBatch {
33    Grayscale(crate::decoder::grayscale_batch::SubmittedGrayscaleExternalBatch),
34    Color(crate::decoder::SubmittedNativeColorExternalBatch),
35}
36
37#[cfg(feature = "cuda-runtime")]
38impl SubmittedCudaCodecBatch {
39    pub(super) fn ranges(&self) -> &[j2k_cuda_runtime::CudaDeviceBufferRange] {
40        match self {
41            Self::Grayscale(pending) => pending.ranges(),
42            Self::Color(pending) => pending.ranges(),
43        }
44    }
45
46    fn is_complete(&self) -> Result<bool, Error> {
47        match self {
48            Self::Grayscale(pending) => pending.is_complete(),
49            Self::Color(pending) => pending.is_complete(),
50        }
51    }
52
53    fn finish(
54        self,
55    ) -> Result<
56        (
57            Vec<j2k_cuda_runtime::CudaDeviceBufferRange>,
58            crate::CudaHtj2kProfileReport,
59        ),
60        Error,
61    > {
62        match self {
63            Self::Grayscale(pending) => pending.finish(),
64            Self::Color(pending) => pending.finish(),
65        }
66    }
67}
68
69/// Result of a nonblocking attempt to retire an external CUDA batch.
70#[cfg(feature = "cuda-runtime")]
71#[derive(Debug)]
72#[must_use = "pending CUDA work must remain retained until it completes"]
73#[expect(
74    clippy::large_enum_variant,
75    reason = "boxing would allocate on every incomplete retirement poll in the throughput path"
76)]
77pub enum CudaExternalBatchTryFinish {
78    /// GPU work is still in flight; retain this completion owner.
79    Pending(SubmittedCudaExternalBatch),
80    /// GPU work completed and codec status validation succeeded.
81    Complete(CudaExternalBatchGroup),
82}
83
84#[cfg(feature = "cuda-runtime")]
85impl core::fmt::Debug for SubmittedCudaExternalBatch {
86    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
87        f.debug_struct("SubmittedCudaExternalBatch")
88            .field("group", &self.group)
89            .field("pending", &true)
90            .finish()
91    }
92}
93
94#[cfg(feature = "cuda-runtime")]
95impl SubmittedCudaExternalBatch {
96    /// Metadata and destination ranges for the submitted group.
97    #[must_use]
98    pub fn group(&self) -> &CudaExternalBatchGroup {
99        &self.group
100    }
101
102    /// Query final-store completion without waiting on the host.
103    pub fn is_complete(&self) -> Result<bool, CudaBatchError> {
104        let source_indices = self.group().source_indices.clone();
105        self.pending
106            .is_complete()
107            .map_err(|source| CudaBatchError::GroupExecution {
108                source_indices,
109                source: Box::new(source),
110            })
111    }
112
113    /// Retire completed work without waiting, or return the still-pending
114    /// completion owner unchanged.
115    pub fn try_finish(self) -> Result<CudaExternalBatchTryFinish, CudaBatchError> {
116        if self.is_complete()? {
117            self.wait().map(CudaExternalBatchTryFinish::Complete)
118        } else {
119            Ok(CudaExternalBatchTryFinish::Pending(self))
120        }
121    }
122
123    /// Wait for final-store completion and validate entropy-kernel status.
124    pub fn wait(self) -> Result<CudaExternalBatchGroup, CudaBatchError> {
125        let Self { mut group, pending } = self;
126        let source_indices = group.source_indices.clone();
127        let (ranges, _report) =
128            pending
129                .finish()
130                .map_err(|source| CudaBatchError::GroupExecution {
131                    source_indices,
132                    source: Box::new(source),
133                })?;
134        group.ranges = ranges;
135        Ok(group)
136    }
137}
138
139#[cfg(feature = "cuda-runtime")]
140impl CudaExternalBatchGroup {
141    /// Shared decoded dimensions, type, color, transform, route, and layout.
142    #[must_use]
143    pub const fn info(&self) -> &BatchGroupInfo {
144        &self.info
145    }
146
147    /// Original input indices in destination batch order.
148    #[must_use]
149    pub fn source_indices(&self) -> &[usize] {
150        &self.source_indices
151    }
152
153    /// Actual decoded rectangle for each image.
154    #[must_use]
155    pub fn decoded_rects(&self) -> &[Rect] {
156        &self.decoded_rects
157    }
158
159    /// Non-fatal codec warnings for each image.
160    #[must_use]
161    pub fn warnings(&self) -> &[Vec<J2kDecodeWarning>] {
162        &self.warnings
163    }
164
165    /// Validated byte ranges written inside the caller allocation.
166    #[must_use]
167    pub fn ranges(&self) -> &[j2k_cuda_runtime::CudaDeviceBufferRange] {
168        &self.ranges
169    }
170}