1mod native_source;
4
5use j2k::J2kError;
6use j2k_core::{
7 adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
8 adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest, BufferError,
9 CodecError,
10};
11use j2k_native::DecodeError as NativeDecodeError;
12
13pub use native_source::NativeBackendError;
14
15#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19 #[error(transparent)]
21 Decode(#[from] J2kError),
22 #[error("{context}: {source}")]
24 NativeDecode {
25 context: &'static str,
27 #[source]
29 source: NativeBackendError,
30 },
31 #[error(transparent)]
33 Buffer(#[from] BufferError),
34 #[error("host allocation failed for {what}: {bytes} bytes")]
36 HostAllocationFailed {
37 bytes: usize,
39 what: &'static str,
41 },
42 #[error(
44 "host allocation capacity for {what} is too large: requested {requested} bytes, cap {cap} bytes"
45 )]
46 HostAllocationTooLarge {
47 requested: usize,
49 cap: usize,
51 what: &'static str,
53 },
54 #[error(transparent)]
56 HtJobChunkPlan(#[from] j2k_core::HtGpuJobChunkPlanError),
57 #[error("backend request {request:?} is not supported by j2k-cuda")]
59 UnsupportedBackend {
60 request: BackendRequest,
62 },
63 #[error("unsupported CUDA request: {reason}")]
65 UnsupportedCudaRequest {
66 reason: &'static str,
68 },
69 #[error("CUDA is unavailable on this host")]
71 CudaUnavailable,
72 #[cfg(feature = "cuda-runtime")]
73 #[error("CUDA runtime error: {source}")]
75 CudaRuntime {
76 #[source]
78 source: j2k_cuda_runtime::CudaError,
79 },
80 #[cfg(feature = "cuda-runtime")]
81 #[error(
83 "CUDA tier-1 source {source_index} job {original_job_index} failed during device execution: {source}"
84 )]
85 CudaTier1JobFailed {
86 source_index: usize,
88 original_job_index: usize,
90 #[source]
92 source: j2k_cuda_runtime::CudaError,
93 },
94 #[cfg(feature = "cuda-runtime")]
95 #[error("CUDA operation failed ({primary}); CUDA cleanup also failed ({cleanup})")]
97 CudaCleanupFailed {
98 primary: Box<Error>,
100 cleanup: Box<Error>,
102 },
103}
104
105impl From<j2k_core::HostPhaseError> for Error {
106 fn from(error: j2k_core::HostPhaseError) -> Self {
107 match error {
108 j2k_core::HostPhaseError::AllocationFailed {
109 requested_bytes,
110 what,
111 } => Self::HostAllocationFailed {
112 bytes: requested_bytes,
113 what,
114 },
115 j2k_core::HostPhaseError::LimitExceeded {
116 requested_bytes,
117 cap_bytes,
118 what,
119 } => Self::HostAllocationTooLarge {
120 requested: requested_bytes,
121 cap: cap_bytes,
122 what,
123 },
124 }
125 }
126}
127
128impl Error {
129 pub(crate) const fn capability_rejected(rejection: j2k_core::CapabilityRejection) -> Self {
130 Self::UnsupportedCudaRequest {
131 reason: rejection.reason(),
132 }
133 }
134
135 #[cfg(feature = "cuda-runtime")]
138 #[doc(hidden)]
139 pub fn completion_is_uncertain(&self) -> bool {
140 match self {
141 Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
142 source.completion_is_uncertain()
143 }
144 Self::CudaCleanupFailed { primary, cleanup } => {
145 primary.completion_is_uncertain() || cleanup.completion_is_uncertain()
146 }
147 _ => false,
148 }
149 }
150
151 #[doc(hidden)]
153 #[must_use]
154 pub fn session_is_unusable(&self) -> bool {
155 match self {
156 Self::CudaUnavailable => true,
157 #[cfg(feature = "cuda-runtime")]
158 Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
159 source.session_is_unusable()
160 }
161 #[cfg(feature = "cuda-runtime")]
162 Self::CudaCleanupFailed { primary, cleanup } => {
163 primary.session_is_unusable() || cleanup.session_is_unusable()
164 }
165 _ => false,
166 }
167 }
168}
169
170#[cfg(feature = "cuda-runtime")]
171pub(crate) fn combine_cuda_cleanup_errors(primary_error: Error, cleanup_error: Error) -> Error {
172 Error::CudaCleanupFailed {
173 primary: Box::new(primary_error),
174 cleanup: Box::new(cleanup_error),
175 }
176}
177
178#[doc(hidden)]
179impl AdapterErrorParts for Error {
180 fn source_codec_error(&self) -> Option<&dyn CodecError> {
181 match self {
182 Self::Decode(inner) => Some(inner),
183 _ => None,
184 }
185 }
186
187 fn adapter_error_kind(&self) -> AdapterErrorKind {
188 match self {
189 Self::Buffer(_) => AdapterErrorKind::Buffer,
190 Self::UnsupportedBackend { .. }
191 | Self::UnsupportedCudaRequest { .. }
192 | Self::CudaUnavailable => AdapterErrorKind::Unsupported,
193 Self::Decode(_)
194 | Self::NativeDecode { .. }
195 | Self::HostAllocationFailed { .. }
196 | Self::HostAllocationTooLarge { .. }
197 | Self::HtJobChunkPlan(_) => AdapterErrorKind::Other,
198 #[cfg(feature = "cuda-runtime")]
199 Self::CudaRuntime { .. }
200 | Self::CudaTier1JobFailed { .. }
201 | Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
202 }
203 }
204}
205
206#[doc(hidden)]
207impl CodecError for Error {
208 fn is_truncated(&self) -> bool {
209 matches!(
210 self,
211 Self::NativeDecode { source, .. } if source.is_decode_truncated()
212 ) || adapter_error_is_truncated(self)
213 }
214
215 fn is_not_implemented(&self) -> bool {
216 adapter_error_is_not_implemented(self)
217 }
218
219 fn is_unsupported(&self) -> bool {
220 matches!(
221 self,
222 Self::NativeDecode { source, .. } if source.is_unsupported()
223 ) || adapter_error_is_unsupported(self)
224 }
225
226 fn is_buffer_error(&self) -> bool {
227 adapter_error_is_buffer_error(self)
228 }
229}
230
231#[cfg_attr(
232 not(any(test, feature = "cuda-runtime")),
233 expect(
234 dead_code,
235 reason = "native decode translation is used by CUDA decode and its tests"
236 )
237)]
238pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
239 Error::NativeDecode {
240 context: "native JPEG 2000 backend failed",
241 source: NativeBackendError::decode(error),
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use j2k_core::CodecError;
248 use j2k_native::{
249 DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
250 DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
251 };
252
253 #[cfg(feature = "cuda-runtime")]
254 use super::combine_cuda_cleanup_errors;
255 use super::{native_decode_error, Error, NativeBackendError};
256 #[cfg(feature = "cuda-runtime")]
257 use j2k_cuda_runtime::CudaError;
258
259 #[cfg(feature = "cuda-runtime")]
260 fn runtime_error(message: &str) -> Error {
261 Error::CudaRuntime {
262 source: CudaError::StatePoisoned {
263 message: message.to_string(),
264 },
265 }
266 }
267
268 #[test]
269 fn host_allocation_failure_is_an_operational_error_not_buffer_misuse() {
270 let error = Error::HostAllocationFailed {
271 bytes: 4096,
272 what: "test staging",
273 };
274 assert!(!error.is_buffer_error());
275 assert!(!error.is_unsupported());
276 assert!(error.to_string().contains("4096"));
277 }
278
279 #[test]
280 fn host_capacity_failure_preserves_actual_phase_budget() {
281 let error = Error::HostAllocationTooLarge {
282 requested: 17,
283 cap: 16,
284 what: "test phase",
285 };
286 assert!(!error.is_buffer_error());
287 assert!(!error.is_unsupported());
288 assert!(error.to_string().contains("17"));
289 assert!(error.to_string().contains("16"));
290 }
291
292 #[test]
293 fn native_decode_unsupported_error_keeps_codec_classification() {
294 let error = native_decode_error(NativeDecodeError::Decoding(
295 NativeDecodingError::UnsupportedFeature("test feature"),
296 ));
297
298 assert!(error.is_unsupported());
299 assert!(!error.is_truncated());
300 assert!(!error.is_not_implemented());
301 }
302
303 #[test]
304 fn native_decode_direct_plan_error_keeps_codec_classification() {
305 let error = native_decode_error(NativeDecodeError::Decoding(
306 NativeDecodingError::DirectPlanUnsupported(
307 NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
308 ),
309 ));
310
311 assert!(error.is_unsupported());
312 assert!(!error.is_truncated());
313 assert!(!error.is_not_implemented());
314 }
315
316 #[test]
317 fn native_decode_unexpected_eof_keeps_codec_classification() {
318 let error = native_decode_error(NativeDecodeError::Decoding(
319 NativeDecodingError::UnexpectedEof,
320 ));
321
322 assert!(error.is_truncated());
323 assert!(!error.is_unsupported());
324 }
325
326 #[test]
327 fn native_decode_resource_errors_preserve_typed_sources() {
328 let sources = [
329 NativeDecodeError::AllocationTooLarge {
330 what: "CUDA decode fixture",
331 requested: 9,
332 cap: 8,
333 },
334 NativeDecodeError::HostAllocationFailed {
335 what: "CUDA decode fixture",
336 bytes: 7,
337 },
338 ];
339
340 for source in sources {
341 let error = native_decode_error(source);
342 assert!(matches!(
343 &error,
344 Error::NativeDecode {
345 context: "native JPEG 2000 backend failed",
346 source: stored,
347 } if stored == &NativeBackendError::decode(source)
348 ));
349 let opaque = core::error::Error::source(&error).expect("opaque adapter source");
350 assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
351 let concrete = opaque.source().expect("concrete native decode source");
352 assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
353 assert!(!error.is_buffer_error());
354 assert!(!error.is_unsupported());
355 }
356 }
357
358 #[cfg(feature = "cuda-runtime")]
359 #[test]
360 fn cuda_cleanup_failure_preserves_both_runtime_diagnostics() {
361 let combined = combine_cuda_cleanup_errors(
362 runtime_error("primary launch failure"),
363 runtime_error("follow-up synchronization failure"),
364 );
365
366 assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
367 let rendered = combined.to_string();
368 assert!(rendered.contains("primary launch failure"));
369 assert!(rendered.contains("follow-up synchronization failure"));
370 assert!(!combined.is_unsupported());
371 }
372
373 #[cfg(feature = "cuda-runtime")]
374 #[test]
375 fn cuda_cleanup_failure_preserves_non_runtime_primary_and_blocks_fallback() {
376 let combined = combine_cuda_cleanup_errors(
377 Error::UnsupportedCudaRequest {
378 reason: "invalid prepared store",
379 },
380 runtime_error("synchronization failure"),
381 );
382
383 assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
384 let rendered = combined.to_string();
385 assert!(rendered.contains("invalid prepared store"));
386 assert!(rendered.contains("synchronization failure"));
387 assert!(!combined.is_unsupported());
388 }
389}