Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and stale roadmap entries have been removed from the public documentation set.

Staged workspace version: `0.13.2`.

- `AdapterErrorKind::Decode`, `BackendErrorKind::Decode` and
`BackendError::decode` classify GPU Tier-1 device decode failures. Metal FAIL
statuses now use the existing `Error::Decode` and `J2kError::Backend`
wrappers instead of `Error::MetalKernel`, so their display starts with
`backend failed (decode):`; the diagnostic keeps the classic or HT path,
status, detail and batch source index. CUDA keeps its typed source and job
attribution. Unsupported statuses, raw runtime failures and combined
cleanup or release failures keep their existing classes.
- Classic code-blocks with BYPASS, per-pass termination, context resets or
segmentation symbols use the packed-column passes, including raw segments.
Vertically causal blocks retain the per-coefficient passes. Clean and corrupt
Expand Down
5 changes: 4 additions & 1 deletion crates/j2k-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,17 @@ pub enum AdapterErrorKind {
Buffer,
/// Error is an unsupported backend, request, input, or host capability.
Unsupported,
/// Backend execution failed while decoding accepted input.
Decode,
}

/// Variant mapping supplied by GPU adapter error enums.
pub trait AdapterErrorParts {
/// Return the wrapped codec/fallback error, when this adapter error has one.
fn source_codec_error(&self) -> Option<&dyn CodecError>;

/// Return the adapter-local classification for non-codec variants.
/// Return the adapter-local classification, including backend decode
/// execution failures.
fn adapter_error_kind(&self) -> AdapterErrorKind;
}

Expand Down
88 changes: 88 additions & 0 deletions crates/j2k-cuda/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ use j2k_native::DecodeError as NativeDecodeError;

pub use native_source::NativeBackendError;

#[cfg(feature = "cuda-runtime")]
// Mirrors STATUS_FAILED in classic decode and HT_STATUS_FAIL in HT decode.
const TIER1_STATUS_FAIL: u32 = 1;

/// Error returned by the CUDA JPEG 2000 adapter.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
Expand Down Expand Up @@ -205,6 +209,15 @@ impl AdapterErrorParts for Error {
| Self::HostAllocationTooLarge { .. }
| Self::HtJobChunkPlan(_) => AdapterErrorKind::Other,
#[cfg(feature = "cuda-runtime")]
Self::CudaTier1JobFailed {
source:
j2k_cuda_runtime::CudaError::KernelJobStatus {
code: TIER1_STATUS_FAIL,
..
},
..
} => AdapterErrorKind::Decode,
#[cfg(feature = "cuda-runtime")]
Self::CudaRuntime { .. }
| Self::CudaTier1JobFailed { .. }
| Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
Expand Down Expand Up @@ -254,6 +267,8 @@ pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
#[cfg(test)]
mod tests {
use j2k_core::CodecError;
#[cfg(feature = "cuda-runtime")]
use j2k_core::{AdapterErrorKind, AdapterErrorParts};
use j2k_native::{
DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
Expand Down Expand Up @@ -395,4 +410,77 @@ mod tests {
assert!(rendered.contains("synchronization failure"));
assert!(!combined.is_unsupported());
}

#[cfg(feature = "cuda-runtime")]
#[test]
fn tier1_job_failure_has_decode_class_without_reclassifying_runtime_or_cleanup() {
let source = CudaError::KernelJobStatus {
kernel: "test_tier1",
job_index: 3,
code: 1,
detail: 5,
};
let error = Error::CudaTier1JobFailed {
source_index: 7,
original_job_index: 3,
source,
};
assert_eq!(error.adapter_error_kind(), AdapterErrorKind::Decode);
assert!(matches!(
&error,
Error::CudaTier1JobFailed {
source_index: 7,
original_job_index: 3,
source: CudaError::KernelJobStatus {
code: 1,
detail: 5,
..
}
}
));

let raw_runtime = Error::CudaRuntime {
source: CudaError::KernelJobStatus {
kernel: "test_tier1",
job_index: 3,
code: 1,
detail: 5,
},
};
assert_eq!(raw_runtime.adapter_error_kind(), AdapterErrorKind::Other);
let cleanup = combine_cuda_cleanup_errors(error, runtime_error("cleanup failed"));
assert_eq!(cleanup.adapter_error_kind(), AdapterErrorKind::Other);

let unsupported = Error::CudaTier1JobFailed {
source_index: 7,
original_job_index: 3,
source: CudaError::KernelJobStatus {
kernel: "test_tier1",
job_index: 3,
code: 2,
detail: 9,
},
};
assert_eq!(unsupported.adapter_error_kind(), AdapterErrorKind::Other);

let release_failure = Error::CudaTier1JobFailed {
source_index: 7,
original_job_index: 3,
source: CudaError::ResourceReleaseFailed {
primary: Box::new(CudaError::KernelJobStatus {
kernel: "test_tier1",
job_index: 3,
code: 1,
detail: 5,
}),
release: Box::new(CudaError::StatePoisoned {
message: "release failed".to_string(),
}),
},
};
assert_eq!(
release_failure.adapter_error_kind(),
AdapterErrorKind::Other
);
}
}
115 changes: 96 additions & 19 deletions crates/j2k-metal/src/engine/direct_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::metal_types::Buffer;

use crate::error::adapter_decode_error;
use crate::{buffer_pool::PooledBuffer, Error, MetalDirectFallbackReason};

use super::abi::{
Expand Down Expand Up @@ -227,13 +228,15 @@ fn decode_classic_status_error_for_source(
reason: MetalDirectFallbackReason::UnsupportedRuntimeInput,
};
}
let kind = match status.code {
J2K_CLASSIC_STATUS_FAIL => "decode failure",
_ => "unexpected classic kernel status",
};
if status.code == J2K_CLASSIC_STATUS_FAIL {
return Error::Decode(adapter_decode_error(format!(
"classic J2K Metal kernel decode failure{source} (code={}, detail={})",
status.code, status.detail
)));
}
Error::MetalKernel {
message: format!(
"classic J2K Metal kernel {kind}{source} (detail={})",
"classic J2K Metal kernel unexpected classic kernel status{source} (detail={})",
status.detail
),
}
Expand Down Expand Up @@ -267,38 +270,109 @@ fn decode_ht_status_error_for_source(status: J2kHtStatus, source_index: Option<u
reason: MetalDirectFallbackReason::UnsupportedRuntimeInput,
};
}
let kind = match status.code {
J2K_HT_STATUS_FAIL => "decode failure",
_ => "unexpected HT kernel status",
};
if status.code == J2K_HT_STATUS_FAIL {
return Error::Decode(adapter_decode_error(format!(
"HTJ2K Metal kernel decode failure{source} (code={}, detail={})",
status.code, status.detail
)));
}
Error::MetalKernel {
message: format!(
"HTJ2K Metal kernel {kind}{source} (detail={})",
"HTJ2K Metal kernel unexpected HT kernel status{source} (detail={})",
status.detail
),
}
}

#[cfg(test)]
mod ht_source_tests {
use super::{decode_ht_status_error_for_source, Error, J2kHtStatus, J2K_HT_STATUS_FAIL};
mod tier1_decode_status_tests {
use j2k::{BackendErrorKind, J2kError};
use j2k_core::{AdapterErrorKind, AdapterErrorParts};

use super::{
decode_classic_status_error_for_source, decode_ht_status_error_for_source, Error,
J2kClassicStatus, J2kHtStatus, J2K_CLASSIC_STATUS_FAIL, J2K_CLASSIC_STATUS_UNSUPPORTED,
J2K_HT_STATUS_FAIL,
};

fn assert_decode_failure(error: Error, coding: &str, detail: u32, source_index: usize) {
assert_eq!(error.adapter_error_kind(), AdapterErrorKind::Decode);
let Error::Decode(J2kError::Backend(backend)) = error else {
panic!("Tier-1 status failure must keep the existing facade decode wrapper");
};
assert_eq!(backend.kind(), BackendErrorKind::Decode);
assert!(backend.message().contains(coding));
assert!(backend
.message()
.contains(&format!("source {source_index}")));
assert!(backend.message().contains("code=1"));
assert!(backend.message().contains(&format!("detail={detail}")));
}

#[test]
fn classic_status_failure_keeps_decode_class_and_source_attribution() {
assert_decode_failure(
decode_classic_status_error_for_source(
J2kClassicStatus {
code: J2K_CLASSIC_STATUS_FAIL,
detail: 10,
..J2kClassicStatus::default()
},
Some(7),
),
"classic",
10,
7,
);
}

#[test]
fn ht_status_failure_keeps_decode_class_and_source_attribution() {
assert_decode_failure(
decode_ht_status_error_for_source(
J2kHtStatus {
code: J2K_HT_STATUS_FAIL,
detail: 5,
..J2kHtStatus::default()
},
Some(9),
),
"HTJ2K",
5,
9,
);
}

#[test]
fn ht_status_failure_names_the_responsible_batch_source() {
let error = decode_ht_status_error_for_source(
fn unsupported_and_unknown_statuses_keep_their_existing_classes() {
let unsupported = decode_classic_status_error_for_source(
J2kClassicStatus {
code: J2K_CLASSIC_STATUS_UNSUPPORTED,
detail: 3,
..J2kClassicStatus::default()
},
Some(4),
);
assert!(matches!(unsupported, Error::MetalDirectFallback { .. }));
assert_eq!(
unsupported.adapter_error_kind(),
AdapterErrorKind::Unsupported
);

let unknown = decode_ht_status_error_for_source(
J2kHtStatus {
code: J2K_HT_STATUS_FAIL,
code: u32::MAX,
detail: 17,
..J2kHtStatus::default()
},
Some(9),
);

assert!(matches!(
error,
Error::MetalKernel { message }
unknown,
Error::MetalKernel { ref message }
if message.contains("source 9") && message.contains("detail=17")
));
assert_eq!(unknown.adapter_error_kind(), AdapterErrorKind::Other);
}
}

Expand Down Expand Up @@ -336,6 +410,7 @@ mod mct_status_tests {
#[cfg(test)]
mod retirement_tests {
use core::mem::size_of;
use j2k::{BackendErrorKind, J2kError};

use super::{
retire_direct_status_checks, DirectStatusCheck, DirectStatusRetirementMode, Error,
Expand Down Expand Up @@ -392,7 +467,9 @@ mod retirement_tests {
.expect_err("first status must fail validation");
assert!(matches!(
error,
Error::MetalKernel { message } if message.contains("detail=41")
Error::Decode(J2kError::Backend(backend))
if backend.kind() == BackendErrorKind::Decode
&& backend.message().contains("detail=41")
));
assert_eq!(
runtime
Expand Down
29 changes: 25 additions & 4 deletions crates/j2k-metal/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

mod native_source;

use j2k::J2kError;
#[cfg(target_os = "macos")]
use j2k::{BackendError, BackendErrorKind};
use j2k::BackendError;
use j2k::{BackendErrorKind, J2kError};
use j2k_core::{
adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest,
Expand All @@ -19,7 +19,7 @@ pub use native_source::NativeBackendError;
#[derive(Debug, thiserror::Error)]
/// Errors returned by the Metal J2K backend.
pub enum Error {
/// Error returned by the CPU or native J2K decoder.
/// Error returned while decoding through the shared facade contract.
#[error(transparent)]
Decode(#[from] J2kError),
/// Native decoder failure produced while preparing Metal-resident work.
Expand Down Expand Up @@ -221,6 +221,9 @@ impl AdapterErrorParts for Error {
fn adapter_error_kind(&self) -> AdapterErrorKind {
match self {
Self::Buffer(_) => AdapterErrorKind::Buffer,
Self::Decode(J2kError::Backend(error)) if error.kind() == BackendErrorKind::Decode => {
AdapterErrorKind::Decode
}
Self::UnsupportedBackend { .. }
| Self::UnsupportedMetalRequest { .. }
| Self::MetalUnavailable
Expand Down Expand Up @@ -313,9 +316,15 @@ pub(crate) fn adapter_backend_error(message: impl Into<String>) -> J2kError {
J2kError::Backend(BackendError::new(BackendErrorKind::Other, message))
}

#[cfg(target_os = "macos")]
pub(crate) fn adapter_decode_error(message: impl Into<String>) -> J2kError {
J2kError::Backend(BackendError::decode(message))
}

#[cfg(test)]
mod tests {
use j2k_core::CodecError;
use j2k::{BackendError, BackendErrorKind, J2kError};
use j2k_core::{AdapterErrorKind, AdapterErrorParts, CodecError};
use j2k_metal_support::MetalSupportError;
use j2k_native::{
DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
Expand Down Expand Up @@ -458,6 +467,18 @@ mod tests {
assert!(error.is_unsupported());
}

#[test]
fn facade_decode_class_is_selective_for_backend_decode_failures() {
let decode = Error::Decode(J2kError::Backend(BackendError::decode("Tier-1 failed")));
assert_eq!(decode.adapter_error_kind(), AdapterErrorKind::Decode);

let other = Error::Decode(J2kError::Backend(BackendError::new(
BackendErrorKind::Other,
"device setup failed",
)));
assert_eq!(other.adapter_error_kind(), AdapterErrorKind::Other);
}

#[test]
fn persistent_session_classification_is_owned_by_metal_error() {
assert!(Error::MetalUnavailable.session_is_unusable());
Expand Down
Loading
Loading