Skip to main content

j2k_cuda/encode/
facade.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use core::{cell::Cell, marker::PhantomData};
4
5use j2k::{EncodeBackendPreference, EncodedJ2k, J2kLosslessEncodeOptions, J2kLosslessSamples};
6use j2k_core::BackendKind;
7
8use super::CudaEncodeStageAccelerator;
9
10/// Reason an `Auto` lossless encode completed on the CPU.
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum CudaEncodeFallbackReason {
14    /// CUDA support was not compiled or no usable CUDA runtime/device was found.
15    DeviceUnavailable,
16    /// CUDA was available, but it did not implement every stage required by the request.
17    DeviceRouteIncomplete,
18}
19
20/// Opaque result from [`CudaLosslessEncoder`].
21///
22/// The requested backend is retained separately from the backend that actually
23/// satisfied the request. A CPU result is a fallback only when the request used
24/// [`EncodeBackendPreference::Auto`].
25#[derive(Clone, Debug, Eq, PartialEq)]
26pub struct CudaLosslessEncodeResult {
27    requested_backend: EncodeBackendPreference,
28    fallback_reason: Option<CudaEncodeFallbackReason>,
29    encoded: EncodedJ2k,
30}
31
32impl CudaLosslessEncodeResult {
33    fn new(
34        requested_backend: EncodeBackendPreference,
35        device_unavailable: bool,
36        encoded: EncodedJ2k,
37    ) -> Self {
38        let fallback_reason = if requested_backend == EncodeBackendPreference::Auto
39            && encoded.backend == BackendKind::Cpu
40        {
41            Some(if device_unavailable {
42                CudaEncodeFallbackReason::DeviceUnavailable
43            } else {
44                CudaEncodeFallbackReason::DeviceRouteIncomplete
45            })
46        } else {
47            None
48        };
49        Self {
50            requested_backend,
51            fallback_reason,
52            encoded,
53        }
54    }
55
56    /// Backend preference supplied for this encode job.
57    #[must_use]
58    pub const fn requested_backend(&self) -> EncodeBackendPreference {
59        self.requested_backend
60    }
61
62    /// Backend that satisfied the encode contract.
63    #[must_use]
64    pub const fn actual_backend(&self) -> BackendKind {
65        self.encoded.backend
66    }
67
68    /// Why an `Auto` request completed on the CPU, if it did.
69    #[must_use]
70    pub const fn fallback_reason(&self) -> Option<CudaEncodeFallbackReason> {
71        self.fallback_reason
72    }
73
74    /// Encode-stage dispatches observed while producing the codestream.
75    #[must_use]
76    pub const fn dispatch_report(&self) -> j2k::J2kEncodeDispatchReport {
77        self.encoded.dispatch_report
78    }
79
80    /// Borrow the encoded codestream and its image metadata.
81    #[must_use]
82    pub const fn encoded(&self) -> &EncodedJ2k {
83        &self.encoded
84    }
85
86    /// Consume the route report and return the encoded codestream and metadata.
87    #[must_use]
88    pub fn into_encoded(self) -> EncodedJ2k {
89        self.encoded
90    }
91}
92
93/// Reusable CUDA-aware lossless JPEG 2000 encoder.
94///
95/// [`Self::encode`] honors each job's [`EncodeBackendPreference`]:
96///
97/// - `CpuOnly` does not initialize or submit CUDA work.
98/// - `Auto` may use CUDA stages and returns a CPU result when CUDA is unavailable
99///   or the device route does not cover every required stage.
100/// - `RequireDevice` returns an error unless CUDA satisfies every required stage.
101///
102/// `Auto` does not retry on the CPU after a CUDA execution error. Such errors
103/// can indicate uncertain device state and are returned to the caller. The
104/// encoder discards its cached accelerator state after any error, so a later
105/// job can safely retry initialization or select `CpuOnly`.
106///
107/// The encoder is `Send` but intentionally not `Sync`; encoding requires
108/// exclusive `&mut self` access. Move one encoder to each worker, or put it
109/// behind a mutex when jobs must share it.
110///
111/// ```compile_fail
112/// fn require_sync<T: Sync>() {}
113/// require_sync::<j2k_cuda::CudaLosslessEncoder>();
114/// ```
115#[derive(Debug)]
116pub struct CudaLosslessEncoder {
117    accelerator: CudaEncodeStageAccelerator,
118    not_sync: PhantomData<Cell<()>>,
119}
120
121impl Default for CudaLosslessEncoder {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl CudaLosslessEncoder {
128    /// Create a reusable encoder with lazily initialized CUDA state.
129    #[must_use]
130    pub fn new() -> Self {
131        Self {
132            accelerator: CudaEncodeStageAccelerator::default(),
133            not_sync: PhantomData,
134        }
135    }
136
137    /// Encode one job according to its backend preference.
138    ///
139    /// Device unavailability and incomplete device-stage coverage are recoverable
140    /// only for `Auto`. Input, allocation, validation, and CUDA execution errors
141    /// are returned without a second CPU encode attempt.
142    pub fn encode(
143        &mut self,
144        samples: J2kLosslessSamples<'_>,
145        options: &J2kLosslessEncodeOptions,
146    ) -> Result<CudaLosslessEncodeResult, crate::Error> {
147        self.encode_with_options(samples, *options)
148    }
149
150    /// Encode one job with a strict CUDA contract.
151    ///
152    /// This method ignores the job's stored backend preference and behaves as
153    /// [`EncodeBackendPreference::RequireDevice`]. It exists alongside
154    /// [`Self::encode`] so CUDA-specific callers can opt into fail-closed routing
155    /// without changing reusable per-job option templates.
156    pub fn encode_strict_cuda(
157        &mut self,
158        samples: J2kLosslessSamples<'_>,
159        options: &J2kLosslessEncodeOptions,
160    ) -> Result<CudaLosslessEncodeResult, crate::Error> {
161        self.encode_with_options(
162            samples,
163            options.with_backend(EncodeBackendPreference::RequireDevice),
164        )
165    }
166
167    fn encode_with_options(
168        &mut self,
169        samples: J2kLosslessSamples<'_>,
170        options: J2kLosslessEncodeOptions,
171    ) -> Result<CudaLosslessEncodeResult, crate::Error> {
172        self.accelerator.begin_encode_attempt();
173        let requested_backend = options.backend;
174        let encoded = if requested_backend == EncodeBackendPreference::CpuOnly {
175            j2k::encode_j2k_lossless(samples, &options)
176        } else {
177            j2k::encode_j2k_lossless_with_accelerator(
178                samples,
179                &options,
180                BackendKind::Cuda,
181                &mut self.accelerator,
182            )
183        };
184
185        match encoded {
186            Ok(encoded) => Ok(CudaLosslessEncodeResult::new(
187                requested_backend,
188                self.accelerator.device_unavailable_observed(),
189                encoded,
190            )),
191            Err(error) => {
192                self.accelerator = CudaEncodeStageAccelerator::default();
193                Err(error.into())
194            }
195        }
196    }
197}