1#[cfg(feature = "cuda-runtime")]
4use j2k_cuda_j2k_engine::{
5 CudaClassicDecodeTableResources, CudaHtj2kDecodeTableResources, CudaHtj2kDecodeTables,
6 CudaHtj2kEncodeResources, J2kCudaEngine,
7};
8#[cfg(feature = "cuda-runtime")]
9use j2k_cuda_runtime::{CudaBufferPool, CudaContext, CudaContextDiagnostics};
10#[cfg(feature = "cuda-runtime")]
11use j2k_native::{ht_uvlc_table0, ht_uvlc_table1, ht_vlc_table0, ht_vlc_table1};
12#[cfg(feature = "cuda-runtime")]
13use std::num::NonZeroUsize;
14#[cfg(all(test, feature = "cuda-runtime"))]
15use std::sync::atomic::{AtomicUsize, Ordering};
16#[cfg(feature = "cuda-runtime")]
17use std::sync::Arc;
18
19#[cfg(feature = "cuda-runtime")]
20use crate::runtime::cuda_error;
21#[cfg(feature = "cuda-runtime")]
22use crate::Error;
23
24#[cfg(all(test, feature = "cuda-runtime"))]
25static HTJ2K_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
26#[cfg(all(test, feature = "cuda-runtime"))]
27static CLASSIC_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
28
29#[cfg(feature = "cuda-runtime")]
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct CudaDecodePoolSnapshot {
33 pub cached_buffers: usize,
35 pub cached_bytes: usize,
37 pub deferred_buffers: usize,
39 pub deferred_bytes: usize,
41 pub reuse_holds: usize,
43 pub peak_cached_bytes: usize,
45 pub peak_deferred_bytes: usize,
47}
48
49#[cfg(feature = "cuda-runtime")]
50impl CudaDecodePoolSnapshot {
51 #[must_use]
53 pub const fn retained_bytes(self) -> usize {
54 self.cached_bytes.saturating_add(self.deferred_bytes)
55 }
56
57 #[must_use]
59 pub const fn peak_retained_bytes_upper_bound(self) -> usize {
60 self.peak_cached_bytes
61 .saturating_add(self.peak_deferred_bytes)
62 }
63}
64
65#[cfg(feature = "cuda-runtime")]
66impl From<j2k_cuda_runtime::CudaBufferPoolDiagnostics> for CudaDecodePoolSnapshot {
67 fn from(value: j2k_cuda_runtime::CudaBufferPoolDiagnostics) -> Self {
68 Self {
69 cached_buffers: value.cached_buffers,
70 cached_bytes: value.cached_bytes,
71 deferred_buffers: value.deferred_buffers,
72 deferred_bytes: value.deferred_bytes,
73 reuse_holds: value.reuse_holds,
74 peak_cached_bytes: value.peak_cached_bytes,
75 peak_deferred_bytes: value.peak_deferred_bytes,
76 }
77 }
78}
79
80#[cfg(feature = "cuda-runtime")]
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub struct CudaDecodePoolDiagnostics {
84 pub decode: Option<CudaDecodePoolSnapshot>,
86 pub batch_decode: Option<CudaDecodePoolSnapshot>,
88}
89
90#[cfg(feature = "cuda-runtime")]
92#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
93pub struct CudaSessionDiagnostics {
94 pub runtime: Option<CudaContextDiagnostics>,
97 pub pools: CudaDecodePoolDiagnostics,
99}
100
101#[cfg(feature = "cuda-runtime")]
102impl CudaDecodePoolDiagnostics {
103 #[must_use]
105 pub fn retained_bytes(self) -> usize {
106 self.decode
107 .map_or(0, CudaDecodePoolSnapshot::retained_bytes)
108 .saturating_add(
109 self.batch_decode
110 .map_or(0, CudaDecodePoolSnapshot::retained_bytes),
111 )
112 }
113
114 #[must_use]
116 pub fn peak_retained_bytes_upper_bound(self) -> usize {
117 self.decode
118 .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound)
119 .saturating_add(
120 self.batch_decode
121 .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound),
122 )
123 }
124}
125
126#[cfg(feature = "cuda-runtime")]
127mod encode_resources;
128
129#[cfg(feature = "cuda-runtime")]
130use self::encode_resources::get_or_try_init_context_bound;
131
132#[derive(Clone, Default)]
134pub struct CudaSession {
135 submissions: u64,
136 #[cfg(feature = "cuda-runtime")]
137 context: Option<CudaContext>,
138 #[cfg(feature = "cuda-runtime")]
139 htj2k_decode_tables: Option<CudaHtj2kDecodeTableResources>,
140 #[cfg(feature = "cuda-runtime")]
141 classic_decode_tables: Option<CudaClassicDecodeTableResources>,
142 #[cfg(feature = "cuda-runtime")]
143 htj2k_encode_resources: Option<Arc<CudaHtj2kEncodeResources>>,
144 #[cfg(all(test, feature = "cuda-runtime"))]
145 htj2k_encode_resource_uploads: usize,
146 #[cfg(feature = "cuda-runtime")]
147 decode_buffer_pool: Option<CudaBufferPool>,
148 #[cfg(feature = "cuda-runtime")]
149 decode_batch_buffer_pool: Option<CudaBufferPool>,
150 #[cfg(feature = "cuda-runtime")]
151 htj2k_decode_chunk_limits: Option<j2k_core::HtGpuJobChunkLimits>,
152 #[cfg(all(test, feature = "cuda-runtime"))]
153 last_htj2k_decode_chunk_count: usize,
154}
155
156impl CudaSession {
157 #[cfg(feature = "cuda-runtime")]
159 #[doc(hidden)]
160 pub fn with_context(context: CudaContext) -> Self {
161 Self {
162 context: Some(context),
163 ..Self::default()
164 }
165 }
166
167 pub fn submissions(&self) -> u64 {
169 self.submissions
170 }
171
172 #[cfg(feature = "cuda-runtime")]
173 pub fn is_runtime_initialized(&self) -> bool {
175 self.context.is_some()
176 }
177
178 #[cfg(feature = "cuda-runtime")]
184 #[doc(hidden)]
185 pub fn context_for_device_interop(
186 &mut self,
187 device_ordinal: usize,
188 ) -> Result<CudaContext, Error> {
189 if let Some(context) = &self.context {
190 if context.device_ordinal() != device_ordinal {
191 return Err(Error::capability_rejected(
192 j2k_core::CapabilityRejection::context_mismatch(
193 "J2K CUDA interop device does not match the persistent session",
194 ),
195 ));
196 }
197 return Ok(context.clone());
198 }
199
200 let context = CudaContext::retain_primary(device_ordinal).map_err(cuda_error)?;
201 self.context = Some(context.clone());
202 Ok(context)
203 }
204
205 #[cfg(feature = "cuda-runtime")]
206 pub(crate) fn cuda_context(&mut self) -> Result<CudaContext, Error> {
207 if self.context.is_none() {
208 self.context = Some(CudaContext::system_default().map_err(cuda_error)?);
209 }
210 self.context.clone().ok_or(Error::CudaUnavailable)
211 }
212
213 #[cfg(feature = "cuda-runtime")]
214 pub(crate) fn htj2k_decode_table_resources(
215 &mut self,
216 ) -> Result<CudaHtj2kDecodeTableResources, Error> {
217 if let Some(tables) = &self.htj2k_decode_tables {
218 return Ok(tables.clone());
219 }
220
221 let context = self.cuda_context()?;
222 let tables = CudaHtj2kDecodeTables {
223 vlc_table0: ht_vlc_table0(),
224 vlc_table1: ht_vlc_table1(),
225 uvlc_table0: ht_uvlc_table0(),
226 uvlc_table1: ht_uvlc_table1(),
227 };
228 let resources = J2kCudaEngine::new(&context)
229 .upload_htj2k_decode_table_resources(tables)
230 .map_err(cuda_error)?;
231 #[cfg(test)]
232 HTJ2K_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
233 self.htj2k_decode_tables = Some(resources.clone());
234 Ok(resources)
235 }
236
237 #[cfg(feature = "cuda-runtime")]
238 pub(crate) fn classic_decode_table_resources(
239 &mut self,
240 ) -> Result<CudaClassicDecodeTableResources, Error> {
241 if let Some(tables) = &self.classic_decode_tables {
242 return Ok(tables.clone());
243 }
244 let context = self.cuda_context()?;
245 let tables = J2kCudaEngine::new(&context)
246 .upload_classic_decode_table_resources()
247 .map_err(cuda_error)?;
248 #[cfg(test)]
249 CLASSIC_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
250 self.classic_decode_tables = Some(tables.clone());
251 Ok(tables)
252 }
253
254 #[cfg(feature = "cuda-runtime")]
255 pub(crate) fn htj2k_encode_resources(
256 &mut self,
257 requested_context: &CudaContext,
258 ) -> Result<Arc<CudaHtj2kEncodeResources>, Error> {
259 let (resources, initialized) = get_or_try_init_context_bound(
260 &mut self.context,
261 &mut self.htj2k_encode_resources,
262 requested_context,
263 CudaContext::is_same_context,
264 || {
265 Error::capability_rejected(j2k_core::CapabilityRejection::context_mismatch(
266 "J2K CUDA encode tile belongs to a different context than the session",
267 ))
268 },
269 |context| {
270 J2kCudaEngine::new(context)
271 .upload_htj2k_encode_resources(crate::encode::cuda_htj2k_encode_tables())
272 .map_err(cuda_error)
273 },
274 )?;
275 #[cfg(test)]
276 if initialized {
277 self.htj2k_encode_resource_uploads =
278 self.htj2k_encode_resource_uploads.saturating_add(1);
279 }
280 #[cfg(not(test))]
281 let _ = initialized;
282 Ok(resources)
283 }
284
285 #[cfg(all(test, feature = "cuda-runtime"))]
286 pub(crate) fn htj2k_encode_resource_uploads_for_test(&self) -> usize {
287 self.htj2k_encode_resource_uploads
288 }
289
290 #[cfg(feature = "cuda-runtime")]
291 pub(crate) fn decode_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
292 if let Some(pool) = &self.decode_buffer_pool {
293 return Ok(pool.clone());
294 }
295 let context = self.cuda_context()?;
296 let pool = context.buffer_pool();
297 self.decode_buffer_pool = Some(pool.clone());
298 Ok(pool)
299 }
300
301 #[cfg(feature = "cuda-runtime")]
302 pub(crate) fn decode_batch_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
303 if let Some(pool) = &self.decode_batch_buffer_pool {
304 return Ok(pool.clone());
305 }
306 let context = self.cuda_context()?;
307 let pool = context.best_fit_buffer_pool();
308 self.decode_batch_buffer_pool = Some(pool.clone());
309 Ok(pool)
310 }
311
312 #[cfg(feature = "cuda-runtime")]
316 pub fn decode_pool_diagnostics(&self) -> Result<CudaDecodePoolDiagnostics, Error> {
317 Ok(CudaDecodePoolDiagnostics {
318 decode: self
319 .decode_buffer_pool
320 .as_ref()
321 .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
322 .transpose()
323 .map_err(cuda_error)?,
324 batch_decode: self
325 .decode_batch_buffer_pool
326 .as_ref()
327 .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
328 .transpose()
329 .map_err(cuda_error)?,
330 })
331 }
332
333 #[cfg(feature = "cuda-runtime")]
335 pub fn diagnostics(&self) -> Result<CudaSessionDiagnostics, Error> {
336 Ok(CudaSessionDiagnostics {
337 runtime: self
338 .context
339 .as_ref()
340 .map(CudaContext::diagnostics)
341 .transpose()
342 .map_err(cuda_error)?,
343 pools: self.decode_pool_diagnostics()?,
344 })
345 }
346
347 #[cfg(feature = "cuda-runtime")]
348 pub(crate) fn htj2k_decode_chunk_limits(&self) -> j2k_core::HtGpuJobChunkLimits {
349 if let Some(limits) = self.htj2k_decode_chunk_limits {
350 return limits;
351 }
352 let Some(max_jobs) = NonZeroUsize::new(65_536) else {
353 return j2k_core::HtGpuJobChunkLimits::new(NonZeroUsize::MIN, 0, 0);
354 };
355 j2k_core::HtGpuJobChunkLimits::new(
356 max_jobs,
357 64 * 1024 * 1024,
358 max_jobs
359 .get()
360 .saturating_mul(j2k_cuda_j2k_engine::htj2k_cleanup_multi_descriptor_bytes()),
361 )
362 }
363
364 #[cfg(all(test, feature = "cuda-runtime"))]
365 pub(crate) fn set_htj2k_decode_chunk_limits_for_test(
366 &mut self,
367 limits: j2k_core::HtGpuJobChunkLimits,
368 ) {
369 self.htj2k_decode_chunk_limits = Some(limits);
370 }
371
372 #[cfg(all(test, feature = "cuda-runtime"))]
373 pub(crate) fn record_htj2k_decode_chunk_count_for_test(&mut self, count: usize) {
374 self.last_htj2k_decode_chunk_count = count;
375 }
376
377 #[cfg(all(test, feature = "cuda-runtime"))]
378 pub(crate) fn last_htj2k_decode_chunk_count_for_test(&self) -> usize {
379 self.last_htj2k_decode_chunk_count
380 }
381}
382
383impl j2k_core::DeviceSubmitSession for CudaSession {
384 fn record_submit(&mut self) {
385 self.submissions = self.submissions.saturating_add(1);
386 }
387}
388
389#[doc(hidden)]
390impl j2k_core::AcceleratorSession for CudaSession {
391 fn backend_kind(&self) -> j2k_core::BackendKind {
392 j2k_core::BackendKind::Cuda
393 }
394
395 fn execution_stats(&self) -> j2k_core::ExecutionStats {
396 j2k_core::ExecutionStats {
397 submissions: self.submissions,
398 ..j2k_core::ExecutionStats::default()
399 }
400 }
401}
402
403#[cfg(all(test, feature = "cuda-runtime"))]
404pub(crate) fn reset_htj2k_decode_table_uploads_for_test() {
405 HTJ2K_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
406}
407
408#[cfg(all(test, feature = "cuda-runtime"))]
409pub(crate) fn htj2k_decode_table_uploads_for_test() -> usize {
410 HTJ2K_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
411}
412
413#[cfg(all(test, feature = "cuda-runtime"))]
414pub(crate) fn reset_classic_decode_table_uploads_for_test() {
415 CLASSIC_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
416}
417
418#[cfg(all(test, feature = "cuda-runtime"))]
419pub(crate) fn classic_decode_table_uploads_for_test() -> usize {
420 CLASSIC_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
421}
422
423impl std::fmt::Debug for CudaSession {
424 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
425 let mut debug = f.debug_struct("CudaSession");
426 debug.field("submissions", &self.submissions);
427 #[cfg(feature = "cuda-runtime")]
428 debug.field("runtime_initialized", &self.is_runtime_initialized());
429 #[cfg(feature = "cuda-runtime")]
430 debug.field(
431 "htj2k_decode_tables_cached",
432 &self.htj2k_decode_tables.is_some(),
433 );
434 #[cfg(feature = "cuda-runtime")]
435 debug.field(
436 "classic_decode_tables_cached",
437 &self.classic_decode_tables.is_some(),
438 );
439 #[cfg(feature = "cuda-runtime")]
440 debug.field(
441 "htj2k_encode_resources_cached",
442 &self.htj2k_encode_resources.is_some(),
443 );
444 #[cfg(feature = "cuda-runtime")]
445 debug.field(
446 "decode_buffer_pool_cached",
447 &self.decode_buffer_pool.is_some(),
448 );
449 #[cfg(feature = "cuda-runtime")]
450 debug.field(
451 "decode_batch_buffer_pool_cached",
452 &self.decode_batch_buffer_pool.is_some(),
453 );
454 debug.finish_non_exhaustive()
455 }
456}
457
458#[cfg(all(test, feature = "cuda-runtime"))]
459mod tests;