1use core::fmt;
4
5use j2k_transcode::TranscodeStageError;
6
7pub const CUDA_UNAVAILABLE: &str = "CUDA is unavailable on this host";
10
11#[derive(Debug)]
13pub enum CudaTranscodeError {
14 CudaUnavailable,
16 UnsupportedJob(&'static str),
18 Kernel(&'static str),
20 HostAllocationTooLarge {
22 requested: usize,
24 cap: usize,
26 what: &'static str,
28 },
29 HostAllocationFailed {
31 requested: usize,
33 what: &'static str,
35 },
36 Runtime(CudaRuntimeFailure),
39}
40
41impl CudaTranscodeError {
42 #[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#[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 #[must_use]
85 pub const fn operation(&self) -> &'static str {
86 self.operation
87 }
88
89 #[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}