use core::fmt;
use j2k_metal_support::MetalSupportError;
use j2k_transcode::TranscodeStageError;
use crate::{weights::SparseWeightRowsError, METAL_UNAVAILABLE};
#[derive(Debug)]
pub enum MetalTranscodeError {
MetalUnavailable,
UnsupportedJob(&'static str),
Runtime(MetalRuntimeFailure),
MetalSupport {
operation: &'static str,
source: MetalSupportError,
},
Kernel(&'static str),
HostAllocationTooLarge {
requested: usize,
cap: usize,
what: &'static str,
},
HostAllocationFailed {
requested: usize,
what: &'static str,
},
DeviceAllocationTooLarge {
requested: usize,
cap: usize,
what: &'static str,
},
DeviceAllocationFailed {
requested: usize,
what: &'static str,
},
}
impl MetalTranscodeError {
#[cfg(any(test, target_os = "macos"))]
pub(crate) const fn is_recoverable(&self) -> bool {
matches!(self, Self::MetalUnavailable | Self::UnsupportedJob(_))
}
#[cfg(target_os = "macos")]
pub(crate) fn runtime(
operation: &'static str,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::Runtime(MetalRuntimeFailure {
operation,
source: Box::new(source),
})
}
#[cfg(any(test, target_os = "macos"))]
pub(crate) const fn support(operation: &'static str, source: MetalSupportError) -> Self {
Self::MetalSupport { operation, source }
}
}
#[derive(Debug)]
pub struct MetalRuntimeFailure {
operation: &'static str,
source: Box<dyn std::error::Error + Send + Sync + 'static>,
}
impl MetalRuntimeFailure {
#[must_use]
pub const fn operation(&self) -> &'static str {
self.operation
}
#[must_use]
pub fn source_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
self.source.as_ref()
}
}
impl fmt::Display for MetalRuntimeFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.operation, self.source)
}
}
impl fmt::Display for MetalTranscodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MetalUnavailable => f.write_str(METAL_UNAVAILABLE),
Self::UnsupportedJob(reason) | Self::Kernel(reason) => f.write_str(reason),
Self::Runtime(failure) => failure.fmt(f),
Self::MetalSupport { operation, source } => {
write!(f, "{operation}: {source}")
}
Self::HostAllocationTooLarge {
requested,
cap,
what,
} => write!(
f,
"Metal transcode host allocation for {what} is too large: requested {requested} bytes, cap {cap} bytes"
),
Self::HostAllocationFailed { requested, what } => write!(
f,
"Metal transcode host allocation failed for {what}: {requested} bytes"
),
Self::DeviceAllocationTooLarge {
requested,
cap,
what,
} => write!(
f,
"Metal transcode device allocation for {what} is too large: requested {requested} bytes, cap {cap} bytes"
),
Self::DeviceAllocationFailed { requested, what } => write!(
f,
"Metal transcode device allocation failed for {what}: {requested} bytes"
),
}
}
}
impl From<MetalTranscodeError> for TranscodeStageError {
fn from(error: MetalTranscodeError) -> Self {
match error {
MetalTranscodeError::MetalUnavailable => Self::DeviceUnavailable,
MetalTranscodeError::UnsupportedJob(reason) => Self::Unsupported(reason),
MetalTranscodeError::Runtime(failure) => {
let operation = failure.operation();
Self::backend("metal", operation, failure)
}
MetalTranscodeError::MetalSupport { operation, source } => {
Self::backend("metal", operation, source)
}
MetalTranscodeError::Kernel(reason) => Self::backend(
"metal",
"kernel execution",
MetalTranscodeError::Kernel(reason),
),
MetalTranscodeError::HostAllocationTooLarge { requested, cap, .. } => {
Self::MemoryCapExceeded { requested, cap }
}
MetalTranscodeError::HostAllocationFailed { requested, .. } => {
Self::HostAllocationFailed { bytes: requested }
}
MetalTranscodeError::DeviceAllocationTooLarge {
requested,
cap,
what,
} => Self::DeviceMemoryCapExceeded {
backend: "metal",
what,
requested,
cap,
},
MetalTranscodeError::DeviceAllocationFailed { requested, what } => {
Self::DeviceAllocationFailed {
backend: "metal",
what,
requested,
}
}
}
}
}
impl From<SparseWeightRowsError> for MetalTranscodeError {
fn from(error: SparseWeightRowsError) -> Self {
match error {
SparseWeightRowsError::SizeOverflow => Self::HostAllocationTooLarge {
requested: usize::MAX,
cap: j2k_core::DEFAULT_MAX_HOST_ALLOCATION_BYTES,
what: "projection weights",
},
SparseWeightRowsError::AllocationTooLarge { requested, cap } => {
Self::HostAllocationTooLarge {
requested,
cap,
what: "projection weights",
}
}
SparseWeightRowsError::HostAllocationFailed { requested } => {
Self::HostAllocationFailed {
requested,
what: "projection weights",
}
}
}
}
}
impl std::error::Error for MetalRuntimeFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(self.source.as_ref())
}
}
impl std::error::Error for MetalTranscodeError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Runtime(failure) => Some(failure),
Self::MetalSupport { source, .. } => Some(source),
Self::MetalUnavailable
| Self::UnsupportedJob(_)
| Self::Kernel(_)
| Self::HostAllocationTooLarge { .. }
| Self::HostAllocationFailed { .. }
| Self::DeviceAllocationTooLarge { .. }
| Self::DeviceAllocationFailed { .. } => None,
}
}
}
#[cfg(test)]
mod tests {
use super::{MetalTranscodeError, TranscodeStageError};
use j2k_metal_support::MetalSupportError;
#[test]
fn host_cap_failure_preserves_typed_stage_error() {
let stage = TranscodeStageError::from(MetalTranscodeError::HostAllocationTooLarge {
requested: 65,
cap: 64,
what: "test host output",
});
assert!(matches!(
stage,
TranscodeStageError::MemoryCapExceeded {
requested: 65,
cap: 64,
}
));
}
#[test]
fn host_allocator_failure_preserves_typed_stage_error() {
let stage = TranscodeStageError::from(MetalTranscodeError::HostAllocationFailed {
requested: 4096,
what: "test host output",
});
assert!(matches!(
stage,
TranscodeStageError::HostAllocationFailed { bytes: 4096 }
));
}
#[test]
fn device_allocation_failures_preserve_stage_resource_categories() {
let cap = TranscodeStageError::from(MetalTranscodeError::DeviceAllocationTooLarge {
requested: 65,
cap: 64,
what: "test device output",
});
assert!(matches!(
cap,
TranscodeStageError::DeviceMemoryCapExceeded {
backend: "metal",
what: "test device output",
requested: 65,
cap: 64,
}
));
let allocation = TranscodeStageError::from(MetalTranscodeError::DeviceAllocationFailed {
requested: 4096,
what: "test device output",
});
assert!(matches!(
allocation,
TranscodeStageError::DeviceAllocationFailed {
backend: "metal",
what: "test device output",
requested: 4096,
}
));
}
#[test]
fn metal_support_failure_keeps_typed_source_and_is_not_recoverable() {
let source = MetalSupportError::CommandBufferUnavailable;
let error = MetalTranscodeError::support("test command buffer", source.clone());
assert!(!error.is_recoverable());
assert!(matches!(
&error,
MetalTranscodeError::MetalSupport {
operation: "test command buffer",
source: stored,
} if stored == &source
));
let chained = std::error::Error::source(&error).expect("typed Metal support source");
assert!(chained.downcast_ref::<MetalSupportError>().is_some());
let stage = TranscodeStageError::from(error);
assert!(matches!(
&stage,
TranscodeStageError::Backend {
backend: "metal",
operation: "test command buffer",
..
}
));
let chained = std::error::Error::source(&stage).expect("typed stage backend source");
assert_eq!(chained.downcast_ref::<MetalSupportError>(), Some(&source));
}
}