1use std::sync::Arc;
4
5use jxr_core::{DecodedImage, DecodedSamples, PlaneDescriptor, StorageKind};
6
7use crate::{
8 CudaBatchDestination, CudaBatchDestinationSubmission, CudaBatchSubmission, CudaDecodePlan,
9 CudaDestination, CudaDestinationSubmission, CudaError, CudaResidentBatch,
10 CudaResidentBatchSubmission, CudaSubmission, DenseCudaBatchLayout, ResidentCudaImage,
11 runtime::{BATCH_SCRATCH_BUDGET, CudaRuntime},
12};
13
14#[derive(Clone)]
16pub struct CudaDecoderSession {
17 runtime: Arc<CudaRuntime>,
18}
19
20impl core::fmt::Debug for CudaDecoderSession {
21 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
22 formatter
23 .debug_struct("CudaDecoderSession")
24 .field("device_ordinal", &self.runtime.context.ordinal())
25 .field("stream_count", &self.runtime.streams.len())
26 .finish_non_exhaustive()
27 }
28}
29
30impl CudaDecoderSession {
31 #[must_use]
35 pub fn is_available() -> bool {
36 unsafe { cudarc::driver::sys::is_culib_present() && cudarc::nvrtc::sys::is_culib_present() }
39 }
40
41 pub fn system_default() -> Result<Self, CudaError> {
43 Self::new(0)
44 }
45
46 pub fn new(device_ordinal: usize) -> Result<Self, CudaError> {
48 if !Self::is_available() {
49 return Err(CudaError::Unavailable);
50 }
51 Ok(Self {
52 runtime: CudaRuntime::build(device_ordinal)?,
53 })
54 }
55
56 #[must_use]
58 pub const fn is_usable(&self) -> bool {
59 true
60 }
61
62 #[must_use]
64 pub fn device_ordinal(&self) -> usize {
65 self.runtime.context.ordinal()
66 }
67
68 #[must_use]
70 pub fn stream_count(&self) -> usize {
71 self.runtime.streams.len()
72 }
73
74 pub fn buffer_pool_diagnostics(&self) -> Result<crate::CudaBufferPoolDiagnostics, CudaError> {
76 self.runtime.buffer_pool.diagnostics()
77 }
78
79 pub fn upload_cache_diagnostics(&self) -> Result<crate::CudaUploadCacheDiagnostics, CudaError> {
81 self.runtime.upload_cache.diagnostics()
82 }
83
84 pub fn allocate_destination(
86 &self,
87 layout: jxr_core::SurfaceLayout,
88 ) -> Result<CudaDestination, CudaError> {
89 let buffer = self.runtime.stream(0).alloc_zeros(layout.byte_len)?;
90 CudaDestination::from_device_buffer(buffer, layout)
91 }
92
93 pub fn allocate_batch_destination(
95 &self,
96 layout: DenseCudaBatchLayout,
97 ) -> Result<CudaBatchDestination, CudaError> {
98 let buffer = self.runtime.stream(0).alloc_zeros(layout.byte_len())?;
99 CudaBatchDestination::from_device_buffer(buffer, layout)
100 }
101
102 pub fn submit(&self, plan: &CudaDecodePlan) -> Result<CudaSubmission, CudaError> {
104 let encoded = crate::encode::encode_owned(&self.runtime, plan, 0)?;
105 Ok(CudaSubmission::submitted(
106 encoded,
107 plan.decode_report(false),
108 ))
109 }
110
111 pub fn submit_batch(&self, plans: &[CudaDecodePlan]) -> Result<CudaBatchSubmission, CudaError> {
113 check_batch_scratch(plans)?;
114 let mut submissions = Vec::with_capacity(plans.len());
115 for (index, plan) in plans.iter().enumerate() {
116 let encoded = crate::encode::encode_owned(&self.runtime, plan, index)?;
117 submissions.push(CudaSubmission::submitted(
118 encoded,
119 plan.decode_report(false),
120 ));
121 }
122 Ok(CudaBatchSubmission::new(submissions))
123 }
124
125 pub fn submit_dense_batch(
127 &self,
128 plans: &[CudaDecodePlan],
129 ) -> Result<CudaResidentBatchSubmission, CudaError> {
130 let first = plans.first().ok_or(CudaError::InvalidPlan {
131 reason: "dense CUDA batch cannot be empty",
132 })?;
133 if plans.iter().any(|plan| plan.output() != first.output()) {
134 return Err(CudaError::InvalidPlan {
135 reason: "dense CUDA batch plans are not homogeneous",
136 });
137 }
138 check_batch_scratch(plans)?;
139 let layout = DenseCudaBatchLayout::new(first.output().clone(), plans.len())?;
140 let stream = self.runtime.stream(0);
141 let mut output = stream.alloc_zeros::<u8>(layout.byte_len())?;
142 let mut submissions = Vec::with_capacity(plans.len());
143 let mut reports = Vec::with_capacity(plans.len());
144 for (image, plan) in plans.iter().enumerate() {
145 let encoded = crate::encode::encode_into(
146 &self.runtime,
147 plan,
148 &mut output,
149 layout.image_offset(image)?,
150 0,
151 )?;
152 let report = plan.decode_report(false);
153 submissions.push(CudaSubmission::submitted(encoded, report.clone()));
154 reports.push(report);
155 }
156 Ok(CudaResidentBatchSubmission::new(
157 submissions,
158 output,
159 layout,
160 reports,
161 ))
162 }
163
164 pub fn submit_into(
166 &self,
167 plan: &CudaDecodePlan,
168 mut destination: CudaDestination,
169 ) -> Result<CudaDestinationSubmission, CudaError> {
170 destination.validate_plan(plan)?;
171 destination.validate_context(&self.runtime.context)?;
172 let encoded =
173 crate::encode::encode_into(&self.runtime, plan, destination.buffer_mut(), 0, 0)?;
174 let report = plan.decode_report(false);
175 Ok(CudaDestinationSubmission::new(
176 CudaSubmission::submitted(encoded, report.clone()),
177 destination,
178 report,
179 ))
180 }
181
182 pub fn submit_batch_into(
184 &self,
185 plans: &[CudaDecodePlan],
186 mut destination: CudaBatchDestination,
187 ) -> Result<CudaBatchDestinationSubmission, CudaError> {
188 destination.validate_plans(plans)?;
189 destination.validate_context(&self.runtime.context)?;
190 check_batch_scratch(plans)?;
191 let mut submissions = Vec::with_capacity(plans.len());
192 let mut reports = Vec::with_capacity(plans.len());
193 for (image, plan) in plans.iter().enumerate() {
194 let offset = destination.layout().image_offset(image)?;
195 let encoded = crate::encode::encode_into(
196 &self.runtime,
197 plan,
198 destination.buffer_mut(),
199 offset,
200 0,
201 )?;
202 let report = plan.decode_report(false);
203 submissions.push(CudaSubmission::submitted(encoded, report.clone()));
204 reports.push(report);
205 }
206 Ok(CudaBatchDestinationSubmission::new(
207 submissions,
208 destination,
209 reports,
210 ))
211 }
212
213 pub fn decode_to_host(&self, plan: &CudaDecodePlan) -> Result<DecodedImage, CudaError> {
215 let resident = self.submit(plan)?.wait()?;
216 let bytes = self.readback(&resident)?;
217 decoded_image(plan, bytes)
218 }
219
220 pub fn decode_batch_to_host(
222 &self,
223 plans: &[CudaDecodePlan],
224 ) -> Result<Vec<DecodedImage>, CudaError> {
225 self.submit_batch(plans)?
226 .wait()?
227 .into_iter()
228 .zip(plans)
229 .map(|(resident, plan)| {
230 let bytes = self.readback(&resident)?;
231 decoded_image(plan, bytes)
232 })
233 .collect()
234 }
235
236 pub fn readback(&self, image: &ResidentCudaImage) -> Result<Vec<u8>, CudaError> {
238 validate_image_context(image, &self.runtime)?;
239 let stream = image.buffer.stream();
240 let bytes = stream.clone_dtoh(&image.buffer)?;
241 stream.synchronize()?;
242 Ok(bytes)
243 }
244
245 pub fn readback_batch_image(
247 &self,
248 batch: &CudaResidentBatch,
249 image: usize,
250 ) -> Result<Vec<u8>, CudaError> {
251 if batch.buffer.context() != &self.runtime.context {
252 return Err(CudaError::InvalidDestination {
253 reason: "resident CUDA batch belongs to a different context",
254 });
255 }
256 let start = batch.layout().image_offset(image)?;
257 let end = start
258 .checked_add(batch.layout().image_stride_bytes())
259 .ok_or(CudaError::InvalidDestination {
260 reason: "resident CUDA batch image range overflows usize",
261 })?;
262 let view = batch.buffer.slice(start..end);
263 let stream = batch.buffer.stream();
264 let bytes = stream.clone_dtoh(&view)?;
265 stream.synchronize()?;
266 Ok(bytes)
267 }
268}
269
270fn validate_image_context(
271 image: &ResidentCudaImage,
272 runtime: &CudaRuntime,
273) -> Result<(), CudaError> {
274 if image.buffer.context() == &runtime.context {
275 Ok(())
276 } else {
277 Err(CudaError::InvalidDestination {
278 reason: "resident CUDA image belongs to a different context",
279 })
280 }
281}
282
283fn check_batch_scratch(plans: &[CudaDecodePlan]) -> Result<(), CudaError> {
284 let total = plans.iter().try_fold(0_usize, |total, plan| {
285 total
286 .checked_add(plan.scratch_bytes()?)
287 .ok_or(CudaError::ResourceLimit {
288 reason: "aggregate CUDA batch scratch overflows usize",
289 requested: usize::MAX,
290 maximum: BATCH_SCRATCH_BUDGET,
291 })
292 })?;
293 if total > BATCH_SCRATCH_BUDGET {
294 return Err(CudaError::ResourceLimit {
295 reason: "aggregate CUDA batch scratch exceeds the bounded session budget",
296 requested: total,
297 maximum: BATCH_SCRATCH_BUDGET,
298 });
299 }
300 Ok(())
301}
302
303fn decoded_image(plan: &CudaDecodePlan, bytes: Vec<u8>) -> Result<DecodedImage, CudaError> {
304 let info = plan.info().ok_or(CudaError::InvalidPlan {
305 reason: "metadata-only plan cannot produce a decoded image",
306 })?;
307 let decoded_region = plan.output_region().ok_or(CudaError::InvalidPlan {
308 reason: "metadata-only plan omits its output region",
309 })?;
310 let samples = decoded_samples(plan.output().format, bytes)?;
311 let decoded = DecodedImage {
312 info: info.clone(),
313 decoded_region,
314 format: plan.output().format,
315 planes: plan
316 .output()
317 .planes
318 .iter()
319 .map(|plane| PlaneDescriptor {
320 byte_offset: plane.byte_offset,
321 row_stride_bytes: plane.row_stride_bytes,
322 width: plane.width,
323 height: plane.height,
324 channels: plane.channels,
325 })
326 .collect(),
327 samples,
328 report: plan.decode_report(true),
329 };
330 decoded
331 .validate_layout()
332 .map_err(|_| CudaError::InvalidPlan {
333 reason: "CUDA host result violates the decoded-image layout",
334 })?;
335 Ok(decoded)
336}
337
338fn decoded_samples(
339 format: jxr_core::PixelFormat,
340 bytes: Vec<u8>,
341) -> Result<DecodedSamples, CudaError> {
342 match format.storage_kind() {
343 StorageKind::BitPacked => Ok(DecodedSamples::BitPacked(bytes)),
344 StorageKind::U8 => Ok(DecodedSamples::U8(bytes)),
345 StorageKind::U16 => Ok(DecodedSamples::U16(read_u16(&bytes)?)),
346 StorageKind::I16 => Ok(DecodedSamples::I16(
347 read_u16(&bytes)?
348 .into_iter()
349 .map(|value| i16::from_ne_bytes(value.to_ne_bytes()))
350 .collect(),
351 )),
352 StorageKind::I32 => Ok(DecodedSamples::I32(
353 read_u32(&bytes)?
354 .into_iter()
355 .map(|value| i32::from_ne_bytes(value.to_ne_bytes()))
356 .collect(),
357 )),
358 StorageKind::F16Bits => Ok(DecodedSamples::F16(read_u16(&bytes)?)),
359 StorageKind::F32 => Ok(DecodedSamples::F32(
360 read_u32(&bytes)?.into_iter().map(f32::from_bits).collect(),
361 )),
362 StorageKind::PackedU16 => match format {
363 jxr_core::PixelFormat::Rgb555 => Ok(DecodedSamples::Rgb555(read_u16(&bytes)?)),
364 jxr_core::PixelFormat::Rgb565 => Ok(DecodedSamples::Rgb565(read_u16(&bytes)?)),
365 _ => Err(CudaError::InvalidPlan {
366 reason: "unknown packed 16-bit output",
367 }),
368 },
369 StorageKind::PackedU32 => match format {
370 jxr_core::PixelFormat::Rgb101010 => Ok(DecodedSamples::Rgb101010(read_u32(&bytes)?)),
371 jxr_core::PixelFormat::Rgbe => Ok(DecodedSamples::Rgbe(read_u32(&bytes)?)),
372 _ => Err(CudaError::InvalidPlan {
373 reason: "unknown packed 32-bit output",
374 }),
375 },
376 }
377}
378
379fn read_u16(bytes: &[u8]) -> Result<Vec<u16>, CudaError> {
380 let (chunks, remainder) = bytes.as_chunks::<2>();
381 if !remainder.is_empty() {
382 return Err(CudaError::InvalidPlan {
383 reason: "16-bit CUDA host readback has a trailing byte",
384 });
385 }
386 Ok(chunks
387 .iter()
388 .map(|chunk| u16::from_ne_bytes(*chunk))
389 .collect())
390}
391
392fn read_u32(bytes: &[u8]) -> Result<Vec<u32>, CudaError> {
393 let (chunks, remainder) = bytes.as_chunks::<4>();
394 if !remainder.is_empty() {
395 return Err(CudaError::InvalidPlan {
396 reason: "32-bit CUDA host readback has trailing bytes",
397 });
398 }
399 Ok(chunks
400 .iter()
401 .map(|chunk| u32::from_ne_bytes(*chunk))
402 .collect())
403}