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_runtime::{
5    CudaBufferPool, CudaClassicDecodeTableResources, CudaContext, CudaContextDiagnostics,
6    CudaHtj2kDecodeTableResources, CudaHtj2kDecodeTables, CudaHtj2kEncodeResources,
7};
8#[cfg(feature = "cuda-runtime")]
9use j2k_native::{ht_uvlc_table0, ht_uvlc_table1, ht_vlc_table0, ht_vlc_table1};
10#[cfg(feature = "cuda-runtime")]
11use std::num::NonZeroUsize;
12#[cfg(all(test, feature = "cuda-runtime"))]
13use std::sync::atomic::{AtomicUsize, Ordering};
14#[cfg(feature = "cuda-runtime")]
15use std::sync::Arc;
16
17#[cfg(feature = "cuda-runtime")]
18use crate::runtime::cuda_error;
19#[cfg(feature = "cuda-runtime")]
20use crate::Error;
21
22#[cfg(all(test, feature = "cuda-runtime"))]
23static HTJ2K_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
24#[cfg(all(test, feature = "cuda-runtime"))]
25static CLASSIC_DECODE_TABLE_UPLOADS: AtomicUsize = AtomicUsize::new(0);
26
27/// Stable retention snapshot for one internal CUDA decode buffer pool.
28#[cfg(feature = "cuda-runtime")]
29#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
30pub struct CudaDecodePoolSnapshot {
31    /// Completed device buffers immediately available for reuse.
32    pub cached_buffers: usize,
33    /// Completed device-allocation bytes immediately available for reuse.
34    pub cached_bytes: usize,
35    /// Buffers held until queued work establishes completion.
36    pub deferred_buffers: usize,
37    /// Device-allocation bytes held until queued work establishes completion.
38    pub deferred_bytes: usize,
39    /// Active completion guards preventing deferred-buffer reuse.
40    pub reuse_holds: usize,
41    /// Highest completed allocation-byte total observed by this pool.
42    pub peak_cached_bytes: usize,
43    /// Highest deferred allocation-byte total observed by this pool.
44    pub peak_deferred_bytes: usize,
45}
46
47#[cfg(feature = "cuda-runtime")]
48impl CudaDecodePoolSnapshot {
49    /// Device bytes currently retained either for reuse or pending completion.
50    #[must_use]
51    pub const fn retained_bytes(self) -> usize {
52        self.cached_bytes.saturating_add(self.deferred_bytes)
53    }
54
55    /// Conservative upper bound obtained by adding the independent pool peaks.
56    #[must_use]
57    pub const fn peak_retained_bytes_upper_bound(self) -> usize {
58        self.peak_cached_bytes
59            .saturating_add(self.peak_deferred_bytes)
60    }
61}
62
63#[cfg(feature = "cuda-runtime")]
64impl From<j2k_cuda_runtime::CudaBufferPoolDiagnostics> for CudaDecodePoolSnapshot {
65    fn from(value: j2k_cuda_runtime::CudaBufferPoolDiagnostics) -> Self {
66        Self {
67            cached_buffers: value.cached_buffers,
68            cached_bytes: value.cached_bytes,
69            deferred_buffers: value.deferred_buffers,
70            deferred_bytes: value.deferred_bytes,
71            reuse_holds: value.reuse_holds,
72            peak_cached_bytes: value.peak_cached_bytes,
73            peak_deferred_bytes: value.peak_deferred_bytes,
74        }
75    }
76}
77
78/// Diagnostics for the two private pools retained by one CUDA codec session.
79#[cfg(feature = "cuda-runtime")]
80#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
81pub struct CudaDecodePoolDiagnostics {
82    /// General single-image/component decode pool, when initialized.
83    pub decode: Option<CudaDecodePoolSnapshot>,
84    /// Best-fit dense batch decode pool, when initialized.
85    pub batch_decode: Option<CudaDecodePoolSnapshot>,
86}
87
88/// Combined runtime work counters and retained decode-pool state for one session.
89#[cfg(feature = "cuda-runtime")]
90#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
91pub struct CudaSessionDiagnostics {
92    /// Context-level transfer, launch, allocation, and synchronization counters,
93    /// when CUDA was initialized.
94    pub runtime: Option<CudaContextDiagnostics>,
95    /// Private decode-buffer pool state.
96    pub pools: CudaDecodePoolDiagnostics,
97}
98
99#[cfg(feature = "cuda-runtime")]
100impl CudaDecodePoolDiagnostics {
101    /// Total device bytes currently retained by both initialized decode pools.
102    #[must_use]
103    pub fn retained_bytes(self) -> usize {
104        self.decode
105            .map_or(0, CudaDecodePoolSnapshot::retained_bytes)
106            .saturating_add(
107                self.batch_decode
108                    .map_or(0, CudaDecodePoolSnapshot::retained_bytes),
109            )
110    }
111
112    /// Conservative sum of the independent high-water bounds for both pools.
113    #[must_use]
114    pub fn peak_retained_bytes_upper_bound(self) -> usize {
115        self.decode
116            .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound)
117            .saturating_add(
118                self.batch_decode
119                    .map_or(0, CudaDecodePoolSnapshot::peak_retained_bytes_upper_bound),
120            )
121    }
122}
123
124#[cfg(feature = "cuda-runtime")]
125mod encode_resources;
126
127#[cfg(feature = "cuda-runtime")]
128use self::encode_resources::get_or_try_init_context_bound;
129
130/// Mutable CUDA adapter session reused across submissions.
131#[derive(Clone, Default)]
132pub struct CudaSession {
133    submissions: u64,
134    #[cfg(feature = "cuda-runtime")]
135    context: Option<CudaContext>,
136    #[cfg(feature = "cuda-runtime")]
137    htj2k_decode_tables: Option<CudaHtj2kDecodeTableResources>,
138    #[cfg(feature = "cuda-runtime")]
139    classic_decode_tables: Option<CudaClassicDecodeTableResources>,
140    #[cfg(feature = "cuda-runtime")]
141    htj2k_encode_resources: Option<Arc<CudaHtj2kEncodeResources>>,
142    #[cfg(all(test, feature = "cuda-runtime"))]
143    htj2k_encode_resource_uploads: usize,
144    #[cfg(feature = "cuda-runtime")]
145    decode_buffer_pool: Option<CudaBufferPool>,
146    #[cfg(feature = "cuda-runtime")]
147    decode_batch_buffer_pool: Option<CudaBufferPool>,
148    #[cfg(feature = "cuda-runtime")]
149    htj2k_decode_chunk_limits: Option<j2k_core::HtGpuJobChunkLimits>,
150    #[cfg(all(test, feature = "cuda-runtime"))]
151    last_htj2k_decode_chunk_count: usize,
152}
153
154impl CudaSession {
155    /// Create a session bound to an existing CUDA context.
156    #[cfg(feature = "cuda-runtime")]
157    #[doc(hidden)]
158    pub fn with_context(context: CudaContext) -> Self {
159        Self {
160            context: Some(context),
161            ..Self::default()
162        }
163    }
164
165    /// Number of submissions recorded by this session.
166    pub fn submissions(&self) -> u64 {
167        self.submissions
168    }
169
170    #[cfg(feature = "cuda-runtime")]
171    /// True when a CUDA runtime context has been initialized.
172    pub fn is_runtime_initialized(&self) -> bool {
173        self.context.is_some()
174    }
175
176    /// Return the session-owned context for a framework-owned destination.
177    ///
178    /// The first call retains the requested device's primary context. Later
179    /// calls reject a different device ordinal, so adapters cannot construct a
180    /// destination view against a context other than the one this session owns.
181    #[cfg(feature = "cuda-runtime")]
182    #[doc(hidden)]
183    pub fn context_for_device_interop(
184        &mut self,
185        device_ordinal: usize,
186    ) -> Result<CudaContext, Error> {
187        if let Some(context) = &self.context {
188            if context.device_ordinal() != device_ordinal {
189                return Err(Error::UnsupportedCudaRequest {
190                    reason: "J2K CUDA interop device does not match the persistent session",
191                });
192            }
193            return Ok(context.clone());
194        }
195
196        let context = CudaContext::retain_primary(device_ordinal).map_err(cuda_error)?;
197        self.context = Some(context.clone());
198        Ok(context)
199    }
200
201    #[cfg(feature = "cuda-runtime")]
202    pub(crate) fn cuda_context(&mut self) -> Result<CudaContext, Error> {
203        if self.context.is_none() {
204            self.context = Some(CudaContext::system_default().map_err(cuda_error)?);
205        }
206        self.context.clone().ok_or(Error::CudaUnavailable)
207    }
208
209    #[cfg(feature = "cuda-runtime")]
210    pub(crate) fn htj2k_decode_table_resources(
211        &mut self,
212    ) -> Result<CudaHtj2kDecodeTableResources, Error> {
213        if let Some(tables) = &self.htj2k_decode_tables {
214            return Ok(tables.clone());
215        }
216
217        let context = self.cuda_context()?;
218        let tables = CudaHtj2kDecodeTables {
219            vlc_table0: ht_vlc_table0(),
220            vlc_table1: ht_vlc_table1(),
221            uvlc_table0: ht_uvlc_table0(),
222            uvlc_table1: ht_uvlc_table1(),
223        };
224        let resources = context
225            .upload_htj2k_decode_table_resources(tables)
226            .map_err(cuda_error)?;
227        #[cfg(test)]
228        HTJ2K_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
229        self.htj2k_decode_tables = Some(resources.clone());
230        Ok(resources)
231    }
232
233    #[cfg(feature = "cuda-runtime")]
234    pub(crate) fn classic_decode_table_resources(
235        &mut self,
236    ) -> Result<CudaClassicDecodeTableResources, Error> {
237        if let Some(tables) = &self.classic_decode_tables {
238            return Ok(tables.clone());
239        }
240        let context = self.cuda_context()?;
241        let tables = context
242            .upload_classic_decode_table_resources()
243            .map_err(cuda_error)?;
244        #[cfg(test)]
245        CLASSIC_DECODE_TABLE_UPLOADS.fetch_add(1, Ordering::Relaxed);
246        self.classic_decode_tables = Some(tables.clone());
247        Ok(tables)
248    }
249
250    #[cfg(feature = "cuda-runtime")]
251    pub(crate) fn htj2k_encode_resources(
252        &mut self,
253        requested_context: &CudaContext,
254    ) -> Result<Arc<CudaHtj2kEncodeResources>, Error> {
255        let (resources, initialized) = get_or_try_init_context_bound(
256            &mut self.context,
257            &mut self.htj2k_encode_resources,
258            requested_context,
259            CudaContext::is_same_context,
260            || Error::UnsupportedCudaRequest {
261                reason: "J2K CUDA encode tile belongs to a different context than the session",
262            },
263            |context| {
264                context
265                    .upload_htj2k_encode_resources(crate::encode::cuda_htj2k_encode_tables())
266                    .map_err(cuda_error)
267            },
268        )?;
269        #[cfg(test)]
270        if initialized {
271            self.htj2k_encode_resource_uploads =
272                self.htj2k_encode_resource_uploads.saturating_add(1);
273        }
274        #[cfg(not(test))]
275        let _ = initialized;
276        Ok(resources)
277    }
278
279    #[cfg(all(test, feature = "cuda-runtime"))]
280    pub(crate) fn htj2k_encode_resource_uploads_for_test(&self) -> usize {
281        self.htj2k_encode_resource_uploads
282    }
283
284    #[cfg(feature = "cuda-runtime")]
285    pub(crate) fn decode_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
286        if let Some(pool) = &self.decode_buffer_pool {
287            return Ok(pool.clone());
288        }
289        let context = self.cuda_context()?;
290        let pool = context.buffer_pool();
291        self.decode_buffer_pool = Some(pool.clone());
292        Ok(pool)
293    }
294
295    #[cfg(feature = "cuda-runtime")]
296    pub(crate) fn decode_batch_buffer_pool(&mut self) -> Result<CudaBufferPool, Error> {
297        if let Some(pool) = &self.decode_batch_buffer_pool {
298            return Ok(pool.clone());
299        }
300        let context = self.cuda_context()?;
301        let pool = context.best_fit_buffer_pool();
302        self.decode_batch_buffer_pool = Some(pool.clone());
303        Ok(pool)
304    }
305
306    /// Snapshot only the two device-buffer pools retained for decode work.
307    ///
308    /// Calling this method does not initialize a CUDA context or either pool.
309    #[cfg(feature = "cuda-runtime")]
310    pub fn decode_pool_diagnostics(&self) -> Result<CudaDecodePoolDiagnostics, Error> {
311        Ok(CudaDecodePoolDiagnostics {
312            decode: self
313                .decode_buffer_pool
314                .as_ref()
315                .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
316                .transpose()
317                .map_err(cuda_error)?,
318            batch_decode: self
319                .decode_batch_buffer_pool
320                .as_ref()
321                .map(|pool| pool.diagnostics().map(CudaDecodePoolSnapshot::from))
322                .transpose()
323                .map_err(cuda_error)?,
324        })
325    }
326
327    /// Snapshot transfer/event counters and decode-pool retention without initializing CUDA.
328    #[cfg(feature = "cuda-runtime")]
329    pub fn diagnostics(&self) -> Result<CudaSessionDiagnostics, Error> {
330        Ok(CudaSessionDiagnostics {
331            runtime: self
332                .context
333                .as_ref()
334                .map(CudaContext::diagnostics)
335                .transpose()
336                .map_err(cuda_error)?,
337            pools: self.decode_pool_diagnostics()?,
338        })
339    }
340
341    #[cfg(feature = "cuda-runtime")]
342    pub(crate) fn htj2k_decode_chunk_limits(&self) -> j2k_core::HtGpuJobChunkLimits {
343        if let Some(limits) = self.htj2k_decode_chunk_limits {
344            return limits;
345        }
346        let Some(max_jobs) = NonZeroUsize::new(65_536) else {
347            return j2k_core::HtGpuJobChunkLimits::new(NonZeroUsize::MIN, 0, 0);
348        };
349        j2k_core::HtGpuJobChunkLimits::new(
350            max_jobs,
351            64 * 1024 * 1024,
352            max_jobs
353                .get()
354                .saturating_mul(j2k_cuda_runtime::htj2k_cleanup_multi_descriptor_bytes()),
355        )
356    }
357
358    #[cfg(all(test, feature = "cuda-runtime"))]
359    pub(crate) fn set_htj2k_decode_chunk_limits_for_test(
360        &mut self,
361        limits: j2k_core::HtGpuJobChunkLimits,
362    ) {
363        self.htj2k_decode_chunk_limits = Some(limits);
364    }
365
366    #[cfg(all(test, feature = "cuda-runtime"))]
367    pub(crate) fn record_htj2k_decode_chunk_count_for_test(&mut self, count: usize) {
368        self.last_htj2k_decode_chunk_count = count;
369    }
370
371    #[cfg(all(test, feature = "cuda-runtime"))]
372    pub(crate) fn last_htj2k_decode_chunk_count_for_test(&self) -> usize {
373        self.last_htj2k_decode_chunk_count
374    }
375}
376
377impl j2k_core::DeviceSubmitSession for CudaSession {
378    fn record_submit(&mut self) {
379        self.submissions = self.submissions.saturating_add(1);
380    }
381}
382
383#[doc(hidden)]
384impl j2k_core::AcceleratorSession for CudaSession {
385    fn backend_kind(&self) -> j2k_core::BackendKind {
386        j2k_core::BackendKind::Cuda
387    }
388
389    fn execution_stats(&self) -> j2k_core::ExecutionStats {
390        j2k_core::ExecutionStats {
391            submissions: self.submissions,
392            ..j2k_core::ExecutionStats::default()
393        }
394    }
395}
396
397#[cfg(all(test, feature = "cuda-runtime"))]
398pub(crate) fn reset_htj2k_decode_table_uploads_for_test() {
399    HTJ2K_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
400}
401
402#[cfg(all(test, feature = "cuda-runtime"))]
403pub(crate) fn htj2k_decode_table_uploads_for_test() -> usize {
404    HTJ2K_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
405}
406
407#[cfg(all(test, feature = "cuda-runtime"))]
408pub(crate) fn reset_classic_decode_table_uploads_for_test() {
409    CLASSIC_DECODE_TABLE_UPLOADS.store(0, Ordering::Relaxed);
410}
411
412#[cfg(all(test, feature = "cuda-runtime"))]
413pub(crate) fn classic_decode_table_uploads_for_test() -> usize {
414    CLASSIC_DECODE_TABLE_UPLOADS.load(Ordering::Relaxed)
415}
416
417impl std::fmt::Debug for CudaSession {
418    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
419        let mut debug = f.debug_struct("CudaSession");
420        debug.field("submissions", &self.submissions);
421        #[cfg(feature = "cuda-runtime")]
422        debug.field("runtime_initialized", &self.is_runtime_initialized());
423        #[cfg(feature = "cuda-runtime")]
424        debug.field(
425            "htj2k_decode_tables_cached",
426            &self.htj2k_decode_tables.is_some(),
427        );
428        #[cfg(feature = "cuda-runtime")]
429        debug.field(
430            "classic_decode_tables_cached",
431            &self.classic_decode_tables.is_some(),
432        );
433        #[cfg(feature = "cuda-runtime")]
434        debug.field(
435            "htj2k_encode_resources_cached",
436            &self.htj2k_encode_resources.is_some(),
437        );
438        #[cfg(feature = "cuda-runtime")]
439        debug.field(
440            "decode_buffer_pool_cached",
441            &self.decode_buffer_pool.is_some(),
442        );
443        #[cfg(feature = "cuda-runtime")]
444        debug.field(
445            "decode_batch_buffer_pool_cached",
446            &self.decode_batch_buffer_pool.is_some(),
447        );
448        debug.finish_non_exhaustive()
449    }
450}
451
452#[cfg(all(test, feature = "cuda-runtime"))]
453mod tests;