Skip to main content

j2k_transcode_cuda/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use core::fmt;
4
5use j2k_transcode::TranscodeStageError;
6
7/// Stable message returned when the CUDA runtime is unavailable (feature not
8/// compiled, no device, or the transcode kernels were not built).
9pub const CUDA_UNAVAILABLE: &str = "CUDA is unavailable on this host";
10
11/// Error returned by the CUDA transcode accelerator.
12#[derive(Debug)]
13pub enum CudaTranscodeError {
14    /// CUDA is unavailable on this host or the kernels were not built.
15    CudaUnavailable,
16    /// The request is outside the current CUDA implementation.
17    UnsupportedJob(&'static str),
18    /// A validated CUDA result violated an internal kernel contract.
19    Kernel(&'static str),
20    /// A codec-owned host allocation exceeds the transcode safety limit.
21    HostAllocationTooLarge {
22        /// Requested byte count, saturated when size arithmetic overflowed.
23        requested: usize,
24        /// Maximum permitted byte count.
25        cap: usize,
26        /// Logical allocation purpose.
27        what: &'static str,
28    },
29    /// A bounded codec-owned host allocation could not be reserved.
30    HostAllocationFailed {
31        /// Requested byte count.
32        requested: usize,
33        /// Logical allocation purpose.
34        what: &'static str,
35    },
36    /// CUDA runtime or kernel execution failed with the backend diagnostic
37    /// retained as an error source.
38    Runtime(CudaRuntimeFailure),
39}
40
41impl CudaTranscodeError {
42    /// Whether Auto mode may recover from this error by using the scalar
43    /// fallback (`Ok(None)`). Hard kernel and allocation failures propagate.
44    #[cfg(feature = "cuda-runtime")]
45    pub(crate) fn is_recoverable(&self) -> bool {
46        matches!(self, Self::CudaUnavailable | Self::UnsupportedJob(_))
47            || matches!(self, Self::Runtime(failure) if failure.is_unavailable())
48    }
49
50    #[cfg(feature = "cuda-runtime")]
51    pub(crate) fn runtime(operation: &'static str, source: j2k_cuda_runtime::CudaError) -> Self {
52        let unavailable = source.is_unavailable();
53        Self::Runtime(CudaRuntimeFailure::new(operation, unavailable, source))
54    }
55}
56
57/// Diagnostic retained when the CUDA runtime rejects a transcode operation.
58///
59/// The runtime dependency is optional, so this feature-independent wrapper
60/// retains a boxed concrete source without changing the public error shape
61/// between feature configurations.
62#[derive(Debug)]
63pub struct CudaRuntimeFailure {
64    operation: &'static str,
65    unavailable: bool,
66    source: Box<dyn std::error::Error + Send + Sync + 'static>,
67}
68
69impl CudaRuntimeFailure {
70    #[cfg(any(feature = "cuda-runtime", test))]
71    fn new(
72        operation: &'static str,
73        unavailable: bool,
74        source: impl std::error::Error + Send + Sync + 'static,
75    ) -> Self {
76        Self {
77            operation,
78            unavailable,
79            source: Box::new(source),
80        }
81    }
82
83    /// Logical operation that failed.
84    #[must_use]
85    pub const fn operation(&self) -> &'static str {
86        self.operation
87    }
88
89    /// Whether the CUDA runtime classified this failure as device/driver
90    /// unavailability rather than an execution failure.
91    #[must_use]
92    pub const fn is_unavailable(&self) -> bool {
93        self.unavailable
94    }
95}
96
97impl fmt::Display for CudaRuntimeFailure {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}: {}", self.operation, self.source)
100    }
101}
102
103impl fmt::Display for CudaTranscodeError {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        match self {
106            Self::CudaUnavailable => f.write_str(CUDA_UNAVAILABLE),
107            Self::UnsupportedJob(reason) | Self::Kernel(reason) => f.write_str(reason),
108            Self::HostAllocationTooLarge {
109                requested,
110                cap,
111                what,
112            } => write!(
113                f,
114                "CUDA transcode host allocation for {what} is too large: requested {requested} bytes, cap {cap} bytes"
115            ),
116            Self::HostAllocationFailed { requested, what } => write!(
117                f,
118                "CUDA transcode host allocation failed for {what}: {requested} bytes"
119            ),
120            Self::Runtime(failure) => failure.fmt(f),
121        }
122    }
123}
124
125impl From<CudaTranscodeError> for TranscodeStageError {
126    fn from(error: CudaTranscodeError) -> Self {
127        match error {
128            CudaTranscodeError::CudaUnavailable => Self::DeviceUnavailable,
129            CudaTranscodeError::UnsupportedJob(reason) => Self::Unsupported(reason),
130            CudaTranscodeError::Kernel(reason) => Self::backend(
131                "cuda",
132                "validate CUDA transcode result",
133                CudaTranscodeError::Kernel(reason),
134            ),
135            CudaTranscodeError::HostAllocationTooLarge { requested, cap, .. } => {
136                Self::MemoryCapExceeded { requested, cap }
137            }
138            CudaTranscodeError::HostAllocationFailed { requested, .. } => {
139                Self::HostAllocationFailed { bytes: requested }
140            }
141            CudaTranscodeError::Runtime(failure) => {
142                let operation = failure.operation();
143                Self::backend("cuda", operation, CudaTranscodeError::Runtime(failure))
144            }
145        }
146    }
147}
148
149impl std::error::Error for CudaRuntimeFailure {
150    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
151        Some(self.source.as_ref())
152    }
153}
154
155impl std::error::Error for CudaTranscodeError {
156    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157        match self {
158            Self::Runtime(failure) => Some(failure),
159            Self::CudaUnavailable
160            | Self::UnsupportedJob(_)
161            | Self::Kernel(_)
162            | Self::HostAllocationTooLarge { .. }
163            | Self::HostAllocationFailed { .. } => None,
164        }
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::{CudaRuntimeFailure, CudaTranscodeError};
171    use core::fmt;
172    use j2k_transcode::TranscodeStageError;
173
174    #[derive(Debug)]
175    struct TestRuntimeError;
176
177    impl fmt::Display for TestRuntimeError {
178        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179            formatter.write_str("driver rejected launch")
180        }
181    }
182
183    impl std::error::Error for TestRuntimeError {}
184
185    #[test]
186    fn runtime_failure_retains_operation_detail_and_error_source() {
187        let error = CudaTranscodeError::Runtime(CudaRuntimeFailure::new(
188            "CUDA test dispatch",
189            false,
190            TestRuntimeError,
191        ));
192        assert_eq!(
193            error.to_string(),
194            "CUDA test dispatch: driver rejected launch"
195        );
196        let stage = TranscodeStageError::from(error);
197        let TranscodeStageError::Backend {
198            backend,
199            operation,
200            source,
201        } = &stage
202        else {
203            panic!("runtime execution failures must remain backend failures");
204        };
205        assert_eq!(*backend, "cuda");
206        assert_eq!(*operation, "CUDA test dispatch");
207        let cuda_error = source
208            .downcast_ref::<CudaTranscodeError>()
209            .expect("stage source must retain the CUDA adapter error");
210        let runtime_failure = std::error::Error::source(cuda_error)
211            .expect("CUDA adapter error must retain its runtime wrapper");
212        let concrete = std::error::Error::source(runtime_failure)
213            .expect("runtime wrapper must retain the concrete runtime source");
214        assert!(concrete.downcast_ref::<TestRuntimeError>().is_some());
215    }
216
217    #[test]
218    fn allocation_failures_preserve_typed_stage_classification() {
219        assert!(matches!(
220            TranscodeStageError::from(CudaTranscodeError::HostAllocationTooLarge {
221                requested: 17,
222                cap: 16,
223                what: "test",
224            }),
225            TranscodeStageError::MemoryCapExceeded {
226                requested: 17,
227                cap: 16,
228            }
229        ));
230        assert!(matches!(
231            TranscodeStageError::from(CudaTranscodeError::HostAllocationFailed {
232                requested: 32,
233                what: "test",
234            }),
235            TranscodeStageError::HostAllocationFailed { bytes: 32 }
236        ));
237    }
238
239    #[test]
240    fn kernel_contract_failure_is_a_concrete_backend_source() {
241        let stage = TranscodeStageError::from(CudaTranscodeError::Kernel("bad kernel output"));
242        let TranscodeStageError::Backend {
243            backend,
244            operation,
245            source,
246        } = &stage
247        else {
248            panic!("kernel contract failures must remain backend failures");
249        };
250        assert_eq!(*backend, "cuda");
251        assert_eq!(*operation, "validate CUDA transcode result");
252        assert!(matches!(
253            source.downcast_ref::<CudaTranscodeError>(),
254            Some(CudaTranscodeError::Kernel("bad kernel output"))
255        ));
256    }
257
258    #[cfg(feature = "cuda-runtime")]
259    #[test]
260    fn unavailable_runtime_failure_retains_diagnostic_and_allows_auto_recovery() {
261        let error = CudaTranscodeError::runtime(
262            "CUDA context initialization",
263            j2k_cuda_runtime::CudaError::Unavailable {
264                message: "no compatible device".to_string(),
265            },
266        );
267
268        assert!(error.is_recoverable());
269        let CudaTranscodeError::Runtime(failure) = error else {
270            panic!("runtime constructor must return the runtime variant");
271        };
272        assert!(failure.is_unavailable());
273        assert!(matches!(
274            std::error::Error::source(&failure)
275                .and_then(|source| source.downcast_ref::<j2k_cuda_runtime::CudaError>()),
276            Some(j2k_cuda_runtime::CudaError::Unavailable { message })
277                if message == "no compatible device"
278        ));
279    }
280}