Skip to main content

j2k_cuda/
session.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#[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/// Stable retention snapshot for one internal CUDA decode buffer pool.
30#[cfg(feature = "cuda-runtime")]
31#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
32pub struct CudaDecodePoolSnapshot {
33    /// Completed device buffers immediately available for reuse.
34    pub cached_buffers: usize,
35    /// Completed device-allocation bytes immediately available for reuse.
36    pub cached_bytes: usize,
37    /// Buffers held until queued work establishes completion.
38    pub deferred_buffers: usize,
39    /// Device-allocation bytes held until queued work establishes completion.
40    pub deferred_bytes: usize,
41    /// Active completion guards preventing deferred-buffer reuse.
42    pub reuse_holds: usize,
43    /// Highest completed allocation-byte total observed by this pool.
44    pub peak_cached_bytes: usize,
45    /// Highest deferred allocation-byte total observed by this pool.
46    pub peak_deferred_bytes: usize,
47}
48
49#[cfg(feature = "cuda-runtime")]
50impl CudaDecodePoolSnapshot {
51    /// Device bytes currently retained either for reuse or pending completion.
52    #[must_use]
53    pub const fn retained_bytes(self) -> usize {
54        self.cached_bytes.saturating_add(self.deferred_bytes)
55    }
56
57    /// Conservative upper bound obtained by adding the independent pool peaks.
58    #[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/// Diagnostics for the two private pools retained by one CUDA codec session.
81#[cfg(feature = "cuda-runtime")]
82#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
83pub struct CudaDecodePoolDiagnostics {
84    /// General single-image/component decode pool, when initialized.
85    pub decode: Option<CudaDecodePoolSnapshot>,
86    /// Best-fit dense batch decode pool, when initialized.
87    pub batch_decode: Option<CudaDecodePoolSnapshot>,
88}
89
90/// Combined runtime work counters and retained decode-pool state for one session.
91#[cfg(feature = "cuda-runtime")]
92#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
93pub struct CudaSessionDiagnostics {
94    /// Context-level transfer, launch, allocation, and synchronization counters,
95    /// when CUDA was initialized.
96    pub runtime: Option<CudaContextDiagnostics>,
97    /// Private decode-buffer pool state.
98    pub pools: CudaDecodePoolDiagnostics,
99}
100
101#[cfg(feature = "cuda-runtime")]
102impl CudaDecodePoolDiagnostics {
103    /// Total device bytes currently retained by both initialized decode pools.
104    #[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    /// Conservative sum of the independent high-water bounds for both pools.
115    #[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/// Mutable CUDA adapter session reused across submissions.
133#[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    /// Create a session bound to an existing CUDA context.
158    #[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    /// Number of submissions recorded by this session.
168    pub fn submissions(&self) -> u64 {
169        self.submissions
170    }
171
172    #[cfg(feature = "cuda-runtime")]
173    /// True when a CUDA runtime context has been initialized.
174    pub fn is_runtime_initialized(&self) -> bool {
175        self.context.is_some()
176    }
177
178    /// Return the session-owned context for a framework-owned destination.
179    ///
180    /// The first call retains the requested device's primary context. Later
181    /// calls reject a different device ordinal, so adapters cannot construct a
182    /// destination view against a context other than the one this session owns.
183    #[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    /// Snapshot only the two device-buffer pools retained for decode work.
313    ///
314    /// Calling this method does not initialize a CUDA context or either pool.
315    #[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    /// Snapshot transfer/event counters and decode-pool retention without initializing CUDA.
334    #[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;