gpu-handle-types 0.1.0

Typed, owned native GPU resource handles (Vulkan, D3D11/12, Metal, OpenGL, CUDA, OpenCL, DMA-BUF, IOSurface, AHardwareBuffer, WebGPU, ...), cross-API sync points and video pixel formats, for passing GPU resources between libraries.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

use std::borrow::Cow;

use crate::{BackendKind, DeviceId};

/// Domain error for every leaf operation in `gpu-handle-types`.
///
/// Downstream crates keep their own domain errors with `#[from]` into this
/// one — this crate has zero backend-library knowledge (no ffmpeg, no wgpu,
/// no ocl error `From` impls).
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
    #[error("io: {0}")]
    Io(#[from] std::io::Error),

    #[error("out of memory")]
    OutOfMemory,

    #[error("out of gpu memory: need {required_bytes}, have {available_bytes} ({backend:?})")]
    OutOfGpuMemory { required_bytes: u64, available_bytes: u64, backend: BackendKind },

    #[error("not supported: {0}")]
    NotSupported(Cow<'static, str>),

    #[error("invalid argument: {0}")]
    InvalidArgument(Cow<'static, str>),

    /// An operation was invoked while the object is in a state that
    /// cannot service it — an encoder that was already finished, a
    /// flush issued while a deferred-submit frame is still awaiting its
    /// submit notification, etc. Distinct from [`Error::InvalidArgument`]
    /// (the *argument* is fine; the *lifecycle position* is wrong) and
    /// classified as [`ErrorKind::Configuration`] (the caller fixes the
    /// call order and retries; the same call from the same state fails
    /// identically). Carries a `&'static str` because every state-machine
    /// transition message is a compile-time literal — no formatting.
    #[error("invalid state: {0}")]
    InvalidState(&'static str),

    /// A deliberate, caller-initiated cancellation — an interrupt-token
    /// cancel aborting a blocking read, a dropped wait future, an SDK job
    /// abort. Not a failure: the operation stopped because it was asked
    /// to. Classified as [`ErrorKind::Cancelled`] so upper layers can
    /// void it (no retry, no error state, no error-level logging) instead
    /// of surfacing it as a fault.
    #[error("cancelled")]
    Cancelled,

    #[error("timeout")]
    Timeout,

    /// A non-blocking operation has no result available **yet** — the caller
    /// should retry later, nothing has failed. This is the leaf form of the
    /// "temporary unavailability" signal a poll/try surface needs: e.g. a
    /// browser WebCodecs decoder's synchronous next-frame call returns it
    /// when a frame is in flight but not yet delivered (an async poll
    /// surface uses `Poll::Pending` instead and never produces this).
    /// Distinct from [`Error::Timeout`] (a *wait* elapsed) and from
    /// `Ok(None)`/EOF (the stream genuinely ended).
    /// Classified [`ErrorKind::Recoverable`].
    #[error("would block")]
    WouldBlock,

    /// The underlying GPU / accelerator device is in an unrecoverable
    /// state (Vulkan `VK_ERROR_DEVICE_LOST`, DXGI `DXGI_ERROR_DEVICE_REMOVED`,
    /// CUDA `CUDA_ERROR_LAUNCH_FAILED` after a TDR, OpenCL
    /// `CL_INVALID_COMMAND_QUEUE` after a fault, …). Distinct from
    /// [`Error::Timeout`] (which is recoverable — the wait simply hadn't
    /// completed yet) and from [`Error::NotSupported`] (which means the
    /// platform never supported the operation). Callers MUST tear down
    /// any in-flight work bound to the device and rebuild — same-device
    /// retries will fail identically.
    #[error("device lost ({backend:?})")]
    DeviceLost { backend: BackendKind },

    #[error("unknown param: {backend:?}/{key}")]
    UnknownParam { backend: BackendKind, key: String },

    #[error("device mismatch: expected {expected:?}, got {actual:?}")]
    DeviceMismatch { expected: DeviceId, actual: DeviceId },

    #[error("no cpu shader provided")]
    NoCpuShader,

    #[error("explicit sync is invalid in auto mode")]
    ExplicitSyncInAutoMode,

    #[error("user kernel panic: {backtrace}")]
    UserKernelPanic { backtrace: String },

    /// Shader compilation failed. The string carries the backend-specific
    /// diagnostic (naga / NVRTC / clBuildProgram output).
    #[error("shader compile error: {0}")]
    ShaderCompile(String),

    /// Bound output / input format does not match what the pipeline /
    /// shader requires.
    #[error("format mismatch: {0}")]
    FormatMismatch(String),

    /// An operation was handed an `ExternalQueue` whose backend does not
    /// match the backend of the processor receiving it.
    #[error("queue backend mismatch: expected {expected:?}, got {actual:?}")]
    QueueBackendMismatch { expected: BackendKind, actual: BackendKind },

    /// Unrecoverable internal invariant violation that the caller cannot
    /// undo by re-configuring — e.g. saturation of a finite counter the
    /// crate cannot recycle. Distinct from [`Error::DeviceLost`] (which is
    /// driver-level) and from [`Error::NotSupported`] (which can be fixed by
    /// rebuilding with different features). Always classified as
    /// [`ErrorKind::Fatal`].
    #[error("fatal: {0}")]
    Fatal(&'static str),

    /// A **fatal backend error that carries its full diagnostic** — the SDK /
    /// backend message (and its `source()` chain), preserved rather than
    /// collapsed to a static category string. Downstream crates lower their
    /// own fatal domain errors here (a decoder or SDK fault that is not a
    /// device-loss) so the exact SDK message propagates all the way to the
    /// caller / log instead of a generic "backend fatal error". Boxed to keep
    /// the leaf enum
    /// pointer-sized. Classified as [`ErrorKind::Fatal`] — same abort contract
    /// as [`Error::Fatal`], but with the detail retained.
    #[error("fatal backend: {0}")]
    FatalBackend(Box<dyn std::error::Error + Send + Sync + 'static>),

    /// Decoder / encoder / resampler / muxer backend error raised by a
    /// downstream media crate. Boxed so the leaf-crate [`Error`] stays a small
    /// pointer-sized enum, and so a backend can layer its own error enum
    /// underneath without pulling SDK-specific `From` impls into the leaf
    /// crate.
    ///
    /// Classified as [`ErrorKind::Recoverable`] by default. Backends
    /// that have richer classification of their own should expose a
    /// `kind()` accessor through their own type and consume the leaf
    /// variant only at the outermost boundary.
    #[error("backend: {0}")]
    Backend(Box<dyn std::error::Error + Send + Sync + 'static>),
}

/// Classification of an [`Error`]. Callers branch on `.kind()` for
/// retry / abort / reconfigure decisions without pattern-matching every
/// variant.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ErrorKind {
    /// A deliberate, caller-initiated cancellation (an interrupt-token
    /// abort of a blocking read, a dropped wait future, an SDK job
    /// abort). Not a failure: the operation stopped because it was asked
    /// to. Callers void it — no retry, no error state, no error-level
    /// logging; whatever superseded the cancelled work carries on.
    Cancelled,
    /// Safe to retry or ignore — typically a transient resource shortage.
    Recoverable,
    /// Caller supplied something the backend cannot do. Fix the config
    /// and retry; same input will fail the same way.
    Configuration,
    /// Backend is in an unrecoverable state. Abort the pipeline.
    Fatal,
}

impl Error {
    /// Classify this error. See [`ErrorKind`].
    pub fn kind(&self) -> ErrorKind {
        match self {
            Error::Cancelled => ErrorKind::Cancelled,

            Error::Timeout
            | Error::WouldBlock
            | Error::OutOfMemory
            | Error::OutOfGpuMemory { .. }
            | Error::Backend(_) => ErrorKind::Recoverable,

            Error::NotSupported(_)
            | Error::InvalidArgument(_)
            | Error::InvalidState(_)
            | Error::UnknownParam { .. }
            | Error::DeviceMismatch { .. }
            | Error::NoCpuShader
            | Error::ExplicitSyncInAutoMode
            | Error::ShaderCompile(_)
            | Error::FormatMismatch(_)
            | Error::QueueBackendMismatch { .. } => ErrorKind::Configuration,

            Error::Io(_)
            | Error::UserKernelPanic { .. }
            | Error::DeviceLost { .. }
            | Error::Fatal(_)
            | Error::FatalBackend(_) => ErrorKind::Fatal,
        }
    }
}

static_assertions::assert_impl_all!(Error: Send, Sync);