1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Persistent CUDA batch decoder facade.
#[cfg(not(feature = "cuda-runtime"))]
use super::IndexedBatchError;
#[cfg(feature = "cuda-runtime")]
use super::{
decode_warnings, group_pixel_format, native_color_inputs, native_decode_settings,
native_referenced_classic_plan, native_referenced_htj2k_plan, validate_layout,
CudaExternalBatchGroup, PixelFormat, PreparedBatchGroup, SubmittedCudaCodecBatch,
SubmittedCudaExternalBatch,
};
use super::{
prepare_batch, prepare_batch_from_images, BatchDecodeOptions, BatchDecoder,
BatchInfrastructureError, CudaBatchDecodeResult, CudaBatchError, CudaSession, EncodedImage,
Error, PreparedBatch, PreparedImage,
};
/// Persistent CUDA batch decoder that reuses one [`CudaSession`].
#[derive(Clone, Debug, Default)]
pub struct CudaBatchDecoder {
pub(super) session: CudaSession,
pub(super) options: BatchDecodeOptions,
}
impl CudaBatchDecoder {
/// Create a decoder with a lazily initialized CUDA session and strict
/// shared batch options.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Create a decoder with explicit shared preparation options.
#[must_use]
pub fn with_options(options: BatchDecodeOptions) -> Self {
Self {
session: CudaSession::default(),
options,
}
}
/// Create a decoder around an existing CUDA session.
#[must_use]
pub fn with_session(session: CudaSession) -> Self {
Self {
session,
options: BatchDecodeOptions::default(),
}
}
/// Create a decoder around an existing session and preparation policy.
#[must_use]
pub const fn with_session_and_options(
session: CudaSession,
options: BatchDecodeOptions,
) -> Self {
Self { session, options }
}
/// Shared preparation options used by [`Self::prepare`] and
/// [`Self::decode_batch`].
#[must_use]
pub const fn options(&self) -> BatchDecodeOptions {
self.options
}
/// Borrow the persistent session for diagnostics.
#[must_use]
pub const fn session(&self) -> &CudaSession {
&self.session
}
/// Snapshot the persistent session's two private decode buffer pools.
#[cfg(feature = "cuda-runtime")]
pub fn decode_pool_diagnostics(&self) -> Result<crate::CudaDecodePoolDiagnostics, Error> {
self.session.decode_pool_diagnostics()
}
/// Snapshot CUDA transfer/event counters and retained decode-pool memory.
#[cfg(feature = "cuda-runtime")]
pub fn diagnostics(&self) -> Result<crate::CudaSessionDiagnostics, Error> {
self.session.diagnostics()
}
/// Mutably borrow the persistent session for advanced configuration.
#[must_use]
pub fn session_mut(&mut self) -> &mut CudaSession {
&mut self.session
}
/// Inspect and group owned inputs without copying their compressed bytes.
pub fn prepare(
&self,
inputs: Vec<EncodedImage>,
) -> Result<PreparedBatch, BatchInfrastructureError> {
prepare_batch(inputs, self.options)
}
/// Regroup caller-supplied prepared images without reparsing codestream bytes.
///
/// Returned source indices are positions in `images`; each image retains
/// its original [`PreparedImage::source_index`] for provenance.
pub fn prepare_prepared_images(
&self,
images: Vec<PreparedImage>,
) -> Result<PreparedBatch, BatchInfrastructureError> {
prepare_batch_from_images(images, self.options)
}
/// Prepare and strictly decode one owned batch to CUDA-resident groups.
pub fn decode_batch(
&mut self,
inputs: Vec<EncodedImage>,
) -> Result<CudaBatchDecodeResult, CudaBatchError> {
let prepared = self.prepare(inputs)?;
self.decode_prepared(&prepared)
}
/// Regroup and decode prepared images without reparsing their encoded bytes.
pub fn decode_prepared_images(
&mut self,
images: Vec<PreparedImage>,
) -> Result<CudaBatchDecodeResult, CudaBatchError> {
let prepared = self.prepare_prepared_images(images)?;
self.decode_prepared(&prepared)
}
/// Strictly decode a reusable shared codec batch to CUDA-resident groups.
///
/// Explicit CUDA execution never falls back to CPU-decoded pixels. A CUDA
/// execution error discards the entire affected dense group.
pub fn decode_prepared(
&mut self,
prepared: &PreparedBatch,
) -> Result<CudaBatchDecodeResult, CudaBatchError> {
#[cfg(feature = "cuda-runtime")]
{
self.submit_prepared(prepared)?.wait()
}
#[cfg(not(feature = "cuda-runtime"))]
{
if let Some(group) = prepared.groups().first() {
return Err(CudaBatchError::group(group, Error::CudaUnavailable));
}
let mut errors = Vec::new();
errors
.try_reserve_exact(prepared.errors().len())
.map_err(|_| BatchInfrastructureError::HostAllocationFailed {
what: "CUDA stub indexed errors",
bytes: prepared
.errors()
.len()
.saturating_mul(core::mem::size_of::<IndexedBatchError>()),
})?;
errors.extend_from_slice(prepared.errors());
Ok(CudaBatchDecodeResult {
groups: Vec::new(),
errors,
group_errors: Vec::new(),
})
}
}
/// Decode one prepared exact-native Gray/RGB/RGBA group directly into a
/// validated caller-owned CUDA allocation range.
///
/// The destination must belong to this decoder's CUDA context and cover
/// the tightly concatenated group output. Decoded pixels are never staged
/// through host memory or copied through an intermediate device output.
///
/// # Safety
///
/// The destination allocation must remain live until this method returns
/// success. If CUDA completion cannot be proven and an error is returned,
/// the caller must quarantine the allocation rather than free or reuse it.
#[cfg(feature = "cuda-runtime")]
pub unsafe fn decode_batch_into(
&mut self,
group: &PreparedBatchGroup,
destination: &mut j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'_>,
) -> Result<CudaExternalBatchGroup, CudaBatchError> {
// SAFETY: this synchronous convenience immediately waits while the
// caller's external view and exclusive managed-owner borrow are live.
unsafe { self.submit_batch_into(group, destination) }?.wait()
}
/// Submit one prepared exact-native Gray/RGB/RGBA group into CUDA storage
/// without a host completion wait.
///
/// Callers integrating another CUDA runtime must use
/// [`j2k_cuda_runtime::CudaContext::with_primary_stream_ordering`] around
/// this submission, then retain the returned value alongside the tensor so
/// codec-internal resources outlive the ordered final store.
///
/// # Safety
///
/// The destination allocation must remain live and may only be consumed on
/// a CUDA stream ordered after codec completion until this value is waited
/// or dropped. No unordered host or device access may overlap the decode.
/// Stream completion alone does not validate entropy status: callers must
/// not expose the destination as decoded pixels until
/// [`SubmittedCudaExternalBatch::wait`] succeeds. If CUDA completion
/// cannot be proven, the external allocation must be quarantined rather
/// than freed or reused.
#[cfg(feature = "cuda-runtime")]
#[expect(
clippy::too_many_lines,
reason = "external submission keeps routing, safety-critical destination ownership, and fallible metadata capture in one boundary"
)]
pub unsafe fn submit_batch_into(
&mut self,
group: &PreparedBatchGroup,
destination: &mut j2k_cuda_runtime::CudaExternalDeviceBufferViewMut<'_>,
) -> Result<SubmittedCudaExternalBatch, CudaBatchError> {
let fmt = group_pixel_format(group.info())
.and_then(|fmt| {
validate_layout(group.info())?;
Ok(fmt)
})
.map_err(|source| CudaBatchError::group(group, source))?;
let pending = if matches!(
fmt,
PixelFormat::Rgb8
| PixelFormat::Rgb16
| PixelFormat::RgbI16
| PixelFormat::Rgba8
| PixelFormat::Rgba16
| PixelFormat::RgbaI16
) {
let inputs = native_color_inputs(group)
.map_err(|source| CudaBatchError::group(group, source))?;
SubmittedCudaCodecBatch::Color(
crate::decoder::submit_native_color_resident_prepared_batch_into(
&inputs,
&mut self.session,
fmt,
group.info().layout,
destination,
)
.map_err(|source| CudaBatchError::group(group, source))?,
)
} else if matches!(
fmt,
PixelFormat::Gray8 | PixelFormat::Gray16 | PixelFormat::GrayI16
) {
let inputs = group
.images()
.iter()
.zip(group.source_indices().iter().copied())
.map(|(image, source_index)| {
let referenced_plan = image
.htj2k_plan()
.map(native_referenced_htj2k_plan)
.transpose()?;
let referenced_classic_plan = image
.classic_plan()
.map(native_referenced_classic_plan)
.transpose()?;
Ok(crate::decoder::grayscale_batch::GrayscaleBatchInput {
source_index,
bytes: image.bytes().as_ref(),
device_plan: Some(image.plan()),
referenced_plan,
referenced_classic_plan,
})
})
.collect::<Result<Vec<_>, Error>>()
.map_err(|source| CudaBatchError::group(group, source))?;
SubmittedCudaCodecBatch::Grayscale(
crate::decoder::grayscale_batch::submit_grayscale_cuda_resident_prepared_batch_into(
&inputs,
native_decode_settings(group.options().settings),
&mut self.session,
fmt,
destination,
)
.map_err(|source| CudaBatchError::group(group, source))?,
)
} else {
return Err(CudaBatchError::group(
group,
Error::UnsupportedCudaRequest {
reason:
"direct external CUDA batch decode requires exact Gray, RGB, or RGBA output",
},
));
};
let mut metadata_budget =
crate::allocation::HostPhaseBudget::new("CUDA external batch result metadata");
let source_indices = metadata_budget
.try_clone_slice(group.source_indices())
.map_err(|source| CudaBatchError::group(group, source))?;
let decoded_rects = metadata_budget
.try_collect_exact(
group
.images()
.iter()
.map(|image| image.plan().output_rect()),
)
.map_err(|source| CudaBatchError::group(group, source))?;
let warnings = decode_warnings(group.images())
.map_err(|source| CudaBatchError::group(group, source))?;
let ranges = metadata_budget
.try_clone_slice(pending.ranges())
.map_err(|source| CudaBatchError::group(group, source))?;
let external_group = CudaExternalBatchGroup {
info: group.info().clone(),
source_indices,
decoded_rects,
warnings,
ranges,
};
Ok(SubmittedCudaExternalBatch {
group: external_group,
pending,
})
}
}
impl BatchDecoder for CudaBatchDecoder {
type Output = CudaBatchDecodeResult;
type Error = CudaBatchError;
fn decode_prepared(&mut self, prepared: &PreparedBatch) -> Result<Self::Output, Self::Error> {
CudaBatchDecoder::decode_prepared(self, prepared)
}
fn options(&self) -> BatchDecodeOptions {
self.options
}
}