Skip to main content

j2k_cuda/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3mod 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/// Error returned by the CUDA JPEG 2000 adapter.
16#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19    /// CPU JPEG 2000 decode failed.
20    #[error(transparent)]
21    Decode(#[from] J2kError),
22    /// Native decoder failure produced while preparing CUDA-resident work.
23    #[error("{context}: {source}")]
24    NativeDecode {
25        /// Stable adapter operation context.
26        context: &'static str,
27        /// Concrete native decoder failure.
28        #[source]
29        source: NativeBackendError,
30    },
31    /// Caller-owned output buffers were invalid.
32    #[error(transparent)]
33    Buffer(#[from] BufferError),
34    /// A host-side allocation needed by the adapter could not be reserved.
35    #[error("host allocation failed for {what}: {bytes} bytes")]
36    HostAllocationFailed {
37        /// Requested allocation size in bytes.
38        bytes: usize,
39        /// Logical allocation purpose.
40        what: &'static str,
41    },
42    /// Allocator-reported host capacity exceeds the codec phase budget.
43    #[error(
44        "host allocation capacity for {what} is too large: requested {requested} bytes, cap {cap} bytes"
45    )]
46    HostAllocationTooLarge {
47        /// Aggregate allocator-reported byte capacity.
48        requested: usize,
49        /// Maximum permitted simultaneously live host bytes.
50        cap: usize,
51        /// Logical phase or owner graph.
52        what: &'static str,
53    },
54    /// Bounded HTJ2K GPU job planning rejected one source job.
55    #[error(transparent)]
56    HtJobChunkPlan(#[from] j2k_core::HtGpuJobChunkPlanError),
57    /// Backend request is unsupported by this adapter.
58    #[error("backend request {request:?} is not supported by j2k-cuda")]
59    UnsupportedBackend {
60        /// Requested backend.
61        request: BackendRequest,
62    },
63    /// CUDA request is unsupported by the strict CUDA adapter contract.
64    #[error("unsupported CUDA request: {reason}")]
65    UnsupportedCudaRequest {
66        /// Human-readable rejection reason.
67        reason: &'static str,
68    },
69    /// CUDA runtime or device is unavailable.
70    #[error("CUDA is unavailable on this host")]
71    CudaUnavailable,
72    #[cfg(feature = "cuda-runtime")]
73    /// CUDA runtime returned an error.
74    #[error("CUDA runtime error: {source}")]
75    CudaRuntime {
76        /// Typed runtime failure, including nested completion or resource-release errors.
77        #[source]
78        source: j2k_cuda_runtime::CudaError,
79    },
80    #[cfg(feature = "cuda-runtime")]
81    /// A classic JPEG 2000 or HTJ2K tier-1 descriptor failed during GPU execution.
82    #[error(
83        "CUDA tier-1 source {source_index} job {original_job_index} failed during device execution: {source}"
84    )]
85    CudaTier1JobFailed {
86        /// Original caller input index that owns the failed job.
87        source_index: usize,
88        /// Stable job index before pass-bucket ordering and chunk splitting.
89        original_job_index: usize,
90        /// Indexed CUDA kernel failure.
91        #[source]
92        source: j2k_cuda_runtime::CudaError,
93    },
94    #[cfg(feature = "cuda-runtime")]
95    /// A CUDA operation failed and the required resource cleanup also failed.
96    #[error("CUDA operation failed ({primary}); CUDA cleanup also failed ({cleanup})")]
97    CudaCleanupFailed {
98        /// Error returned by the original operation.
99        primary: Box<Error>,
100        /// Error returned while synchronizing or releasing queued resources.
101        cleanup: Box<Error>,
102    },
103}
104
105impl Error {
106    /// Whether this error can leave submitted CUDA work referencing an
107    /// external allocation whose completion was not established.
108    #[cfg(feature = "cuda-runtime")]
109    #[doc(hidden)]
110    pub fn completion_is_uncertain(&self) -> bool {
111        match self {
112            Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
113                source.completion_is_uncertain()
114            }
115            Self::CudaCleanupFailed { primary, cleanup } => {
116                primary.completion_is_uncertain() || cleanup.completion_is_uncertain()
117            }
118            _ => false,
119        }
120    }
121
122    /// Whether the persistent CUDA session cannot safely execute later groups.
123    #[doc(hidden)]
124    #[must_use]
125    pub fn session_is_unusable(&self) -> bool {
126        match self {
127            Self::CudaUnavailable => true,
128            #[cfg(feature = "cuda-runtime")]
129            Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
130                source.session_is_unusable()
131            }
132            #[cfg(feature = "cuda-runtime")]
133            Self::CudaCleanupFailed { primary, cleanup } => {
134                primary.session_is_unusable() || cleanup.session_is_unusable()
135            }
136            _ => false,
137        }
138    }
139}
140
141#[cfg(feature = "cuda-runtime")]
142pub(crate) fn combine_cuda_cleanup_errors(primary_error: Error, cleanup_error: Error) -> Error {
143    Error::CudaCleanupFailed {
144        primary: Box::new(primary_error),
145        cleanup: Box::new(cleanup_error),
146    }
147}
148
149#[doc(hidden)]
150impl AdapterErrorParts for Error {
151    fn source_codec_error(&self) -> Option<&dyn CodecError> {
152        match self {
153            Self::Decode(inner) => Some(inner),
154            _ => None,
155        }
156    }
157
158    fn adapter_error_kind(&self) -> AdapterErrorKind {
159        match self {
160            Self::Buffer(_) => AdapterErrorKind::Buffer,
161            Self::UnsupportedBackend { .. }
162            | Self::UnsupportedCudaRequest { .. }
163            | Self::CudaUnavailable => AdapterErrorKind::Unsupported,
164            Self::Decode(_)
165            | Self::NativeDecode { .. }
166            | Self::HostAllocationFailed { .. }
167            | Self::HostAllocationTooLarge { .. }
168            | Self::HtJobChunkPlan(_) => AdapterErrorKind::Other,
169            #[cfg(feature = "cuda-runtime")]
170            Self::CudaRuntime { .. }
171            | Self::CudaTier1JobFailed { .. }
172            | Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
173        }
174    }
175}
176
177#[doc(hidden)]
178impl CodecError for Error {
179    fn is_truncated(&self) -> bool {
180        matches!(
181            self,
182            Self::NativeDecode { source, .. } if source.is_decode_truncated()
183        ) || adapter_error_is_truncated(self)
184    }
185
186    fn is_not_implemented(&self) -> bool {
187        adapter_error_is_not_implemented(self)
188    }
189
190    fn is_unsupported(&self) -> bool {
191        matches!(
192            self,
193            Self::NativeDecode { source, .. } if source.is_unsupported()
194        ) || adapter_error_is_unsupported(self)
195    }
196
197    fn is_buffer_error(&self) -> bool {
198        adapter_error_is_buffer_error(self)
199    }
200}
201
202#[cfg_attr(
203    not(any(test, feature = "cuda-runtime")),
204    expect(
205        dead_code,
206        reason = "native decode translation is used by CUDA decode and its tests"
207    )
208)]
209pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
210    Error::NativeDecode {
211        context: "native JPEG 2000 backend failed",
212        source: NativeBackendError::decode(error),
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use j2k_core::CodecError;
219    use j2k_native::{
220        DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
221        DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
222    };
223
224    #[cfg(feature = "cuda-runtime")]
225    use super::combine_cuda_cleanup_errors;
226    use super::{native_decode_error, Error, NativeBackendError};
227    #[cfg(feature = "cuda-runtime")]
228    use j2k_cuda_runtime::CudaError;
229
230    #[cfg(feature = "cuda-runtime")]
231    fn runtime_error(message: &str) -> Error {
232        Error::CudaRuntime {
233            source: CudaError::StatePoisoned {
234                message: message.to_string(),
235            },
236        }
237    }
238
239    #[test]
240    fn host_allocation_failure_is_an_operational_error_not_buffer_misuse() {
241        let error = Error::HostAllocationFailed {
242            bytes: 4096,
243            what: "test staging",
244        };
245        assert!(!error.is_buffer_error());
246        assert!(!error.is_unsupported());
247        assert!(error.to_string().contains("4096"));
248    }
249
250    #[test]
251    fn host_capacity_failure_preserves_actual_phase_budget() {
252        let error = Error::HostAllocationTooLarge {
253            requested: 17,
254            cap: 16,
255            what: "test phase",
256        };
257        assert!(!error.is_buffer_error());
258        assert!(!error.is_unsupported());
259        assert!(error.to_string().contains("17"));
260        assert!(error.to_string().contains("16"));
261    }
262
263    #[test]
264    fn native_decode_unsupported_error_keeps_codec_classification() {
265        let error = native_decode_error(NativeDecodeError::Decoding(
266            NativeDecodingError::UnsupportedFeature("test feature"),
267        ));
268
269        assert!(error.is_unsupported());
270        assert!(!error.is_truncated());
271        assert!(!error.is_not_implemented());
272    }
273
274    #[test]
275    fn native_decode_direct_plan_error_keeps_codec_classification() {
276        let error = native_decode_error(NativeDecodeError::Decoding(
277            NativeDecodingError::DirectPlanUnsupported(
278                NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
279            ),
280        ));
281
282        assert!(error.is_unsupported());
283        assert!(!error.is_truncated());
284        assert!(!error.is_not_implemented());
285    }
286
287    #[test]
288    fn native_decode_unexpected_eof_keeps_codec_classification() {
289        let error = native_decode_error(NativeDecodeError::Decoding(
290            NativeDecodingError::UnexpectedEof,
291        ));
292
293        assert!(error.is_truncated());
294        assert!(!error.is_unsupported());
295    }
296
297    #[test]
298    fn native_decode_resource_errors_preserve_typed_sources() {
299        let sources = [
300            NativeDecodeError::AllocationTooLarge {
301                what: "CUDA decode fixture",
302                requested: 9,
303                cap: 8,
304            },
305            NativeDecodeError::HostAllocationFailed {
306                what: "CUDA decode fixture",
307                bytes: 7,
308            },
309        ];
310
311        for source in sources {
312            let error = native_decode_error(source);
313            assert!(matches!(
314                &error,
315                Error::NativeDecode {
316                    context: "native JPEG 2000 backend failed",
317                    source: stored,
318                } if stored == &NativeBackendError::decode(source)
319            ));
320            let opaque = core::error::Error::source(&error).expect("opaque adapter source");
321            assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
322            let concrete = opaque.source().expect("concrete native decode source");
323            assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
324            assert!(!error.is_buffer_error());
325            assert!(!error.is_unsupported());
326        }
327    }
328
329    #[cfg(feature = "cuda-runtime")]
330    #[test]
331    fn cuda_cleanup_failure_preserves_both_runtime_diagnostics() {
332        let combined = combine_cuda_cleanup_errors(
333            runtime_error("primary launch failure"),
334            runtime_error("follow-up synchronization failure"),
335        );
336
337        assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
338        let rendered = combined.to_string();
339        assert!(rendered.contains("primary launch failure"));
340        assert!(rendered.contains("follow-up synchronization failure"));
341        assert!(!combined.is_unsupported());
342    }
343
344    #[cfg(feature = "cuda-runtime")]
345    #[test]
346    fn cuda_cleanup_failure_preserves_non_runtime_primary_and_blocks_fallback() {
347        let combined = combine_cuda_cleanup_errors(
348            Error::UnsupportedCudaRequest {
349                reason: "invalid prepared store",
350            },
351            runtime_error("synchronization failure"),
352        );
353
354        assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
355        let rendered = combined.to_string();
356        assert!(rendered.contains("invalid prepared store"));
357        assert!(rendered.contains("synchronization failure"));
358        assert!(!combined.is_unsupported());
359    }
360}