1use burn_core::tensor::backend::Backend;
4use burn_cuda::{Cuda, CudaDevice};
5use j2k::{BatchDecodeOptions, EncodedImage, PreparedBatch, PreparedImage};
6use j2k_cuda::{
7 CudaBatchDecodeResult, CudaBatchDecoder as CodecDecoder, SubmittedCudaResidentBatch,
8};
9
10use crate::{BurnBatchDecode, BurnBatchGroupError, BurnDecodeError};
11
12#[must_use = "submitted CUDA upload batches must be waited or dropped"]
15pub struct SubmittedCudaBurnBatch {
16 pending: SubmittedCudaResidentBatch,
17 device: CudaDevice,
18}
19
20impl core::fmt::Debug for SubmittedCudaBurnBatch {
21 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22 f.debug_struct("SubmittedCudaBurnBatch")
23 .field("pending", &self.pending)
24 .field("device", &self.device)
25 .finish()
26 }
27}
28
29impl SubmittedCudaBurnBatch {
30 #[must_use]
32 pub fn len(&self) -> usize {
33 self.pending.pending_group_count()
34 }
35
36 #[must_use]
38 pub fn is_empty(&self) -> bool {
39 self.len() == 0
40 }
41
42 pub fn wait(self) -> Result<BurnBatchDecode<Cuda>, BurnDecodeError> {
45 materialize(self.pending.wait()?, &self.device)
46 }
47}
48
49#[derive(Debug)]
55pub struct CudaBurnDecoder {
56 codec: CodecDecoder,
57 device: CudaDevice,
58}
59
60impl CudaBurnDecoder {
61 #[must_use]
63 pub fn new(device: CudaDevice, options: BatchDecodeOptions) -> Self {
64 Self {
65 codec: CodecDecoder::with_options(options),
66 device,
67 }
68 }
69
70 #[must_use]
72 pub const fn codec(&self) -> &CodecDecoder {
73 &self.codec
74 }
75
76 #[must_use]
78 pub const fn device(&self) -> &CudaDevice {
79 &self.device
80 }
81
82 pub fn prepare(&self, inputs: Vec<EncodedImage>) -> Result<PreparedBatch, BurnDecodeError> {
84 Ok(self.codec.prepare(inputs)?)
85 }
86
87 pub fn prepare_prepared_images(
89 &self,
90 images: Vec<PreparedImage>,
91 ) -> Result<PreparedBatch, BurnDecodeError> {
92 Ok(self.codec.prepare_prepared_images(images)?)
93 }
94
95 pub fn decode(
97 &mut self,
98 inputs: Vec<EncodedImage>,
99 ) -> Result<BurnBatchDecode<Cuda>, BurnDecodeError> {
100 self.submit(inputs)?.wait()
101 }
102
103 pub fn decode_prepared_images(
105 &mut self,
106 images: Vec<PreparedImage>,
107 ) -> Result<BurnBatchDecode<Cuda>, BurnDecodeError> {
108 let prepared = self.prepare_prepared_images(images)?;
109 self.decode_prepared(&prepared)
110 }
111
112 pub fn decode_prepared(
114 &mut self,
115 prepared: &PreparedBatch,
116 ) -> Result<BurnBatchDecode<Cuda>, BurnDecodeError> {
117 self.submit_prepared(prepared)?.wait()
118 }
119
120 pub fn submit(
123 &mut self,
124 inputs: Vec<EncodedImage>,
125 ) -> Result<SubmittedCudaBurnBatch, BurnDecodeError> {
126 let prepared = self.prepare(inputs)?;
127 self.submit_prepared(&prepared)
128 }
129
130 pub fn submit_prepared(
132 &mut self,
133 prepared: &PreparedBatch,
134 ) -> Result<SubmittedCudaBurnBatch, BurnDecodeError> {
135 ensure_dtypes::<Cuda>(prepared, &self.device)?;
136 Ok(SubmittedCudaBurnBatch {
137 pending: self.codec.submit_prepared(prepared)?,
138 device: self.device.clone(),
139 })
140 }
141}
142
143fn ensure_dtypes<B: Backend>(
144 prepared: &PreparedBatch,
145 device: &B::Device,
146) -> Result<(), BurnDecodeError> {
147 for group in prepared.groups() {
148 let dtype = crate::batch_contract::dtype(group.info().sample_type)?;
149 if !B::supports_dtype(device, dtype) {
150 return Err(BurnDecodeError::UnsupportedDType { dtype });
151 }
152 }
153 Ok(())
154}
155
156fn materialize(
157 decoded: CudaBatchDecodeResult,
158 device: &CudaDevice,
159) -> Result<BurnBatchDecode<Cuda>, BurnDecodeError> {
160 let (codec_groups, errors, codec_group_errors) = decoded.into_parts();
161 let mut groups = Vec::new();
162 groups
163 .try_reserve_exact(codec_groups.len())
164 .map_err(|_| BurnDecodeError::SizeOverflow)?;
165 for group in codec_groups {
166 let (info, source_indices, decoded_rects, warnings, _surfaces, dense) = group.into_parts();
167 let mut bytes = vec![0; crate::staging::byte_len(source_indices.len(), &info)?];
168 dense.buffer().copy_to_host(&mut bytes).map_err(|source| {
169 BurnDecodeError::AcceleratorInterop {
170 backend: "CUDA staged readback",
171 message: source.to_string(),
172 }
173 })?;
174 groups.push(crate::staging::materialize(
175 info,
176 source_indices,
177 decoded_rects,
178 warnings,
179 bytes,
180 device,
181 )?);
182 }
183 let group_errors = codec_group_errors
184 .into_iter()
185 .map(|error| {
186 let (source_indices, source) = error.into_parts();
187 cuda_group_error(source_indices, source)
188 })
189 .collect();
190 Ok(BurnBatchDecode {
191 groups,
192 errors,
193 group_errors,
194 })
195}
196
197fn cuda_group_error(source_indices: Vec<usize>, source: j2k_cuda::Error) -> BurnBatchGroupError {
198 let adapter_source = BurnDecodeError::Cuda(j2k_cuda::CudaBatchError::GroupExecution {
199 source_indices: source_indices.clone(),
200 source: Box::new(source),
201 });
202 BurnBatchGroupError::new(source_indices, adapter_source)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::cuda_group_error;
208 use crate::BurnDecodeError;
209
210 #[test]
211 fn cuda_group_failure_preserves_the_existing_typed_error_chain() {
212 let error = cuda_group_error(
213 vec![2, 4],
214 j2k_cuda::Error::UnsupportedCudaRequest {
215 reason: "test rejection",
216 },
217 );
218
219 assert_eq!(error.source_indices(), [2, 4]);
220 let BurnDecodeError::Cuda(j2k_cuda::CudaBatchError::GroupExecution {
221 source_indices,
222 source,
223 ..
224 }) = error.source()
225 else {
226 panic!("CUDA codec failure must use the patch-compatible batch error variant");
227 };
228 assert_eq!(source_indices, &[2, 4]);
229 assert!(matches!(
230 source.as_ref(),
231 j2k_cuda::Error::UnsupportedCudaRequest {
232 reason: "test rejection"
233 }
234 ));
235 }
236}