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

//! `GpuResource` — the single source of truth for GPU resource handles
//! crossing API boundaries. Used by decoders, encoders, interop layers and
//! renderers.
//!
//! The variant payloads are **typed newtypes**
//! (see [`handles`]). Each newtype owns its raw payload privately and
//! exposes a single `try_from_raw` constructor that asserts invariants
//! once. The enum here is a thin sum type over those newtypes.
//!
//! # Lifetime model
//!
//! Each newtype variant carries either:
//! - `Option<Arc<dyn ResourceKeepAlive>>` — borrowed raw handle, lifetime
//!   anchored by the producer's `Arc` if attached.
//! - An `Arc<...>` refcount wrapper — for OS-handle / refcounted-foreign
//!   payloads (`OwnedFd`, `OwnedHandle`, `IoSurfaceRetain`,
//!   `CvBufferRetain`, `AHbInner`). Clone bumps the Arc; last drop runs
//!   the wrapped destructor.
//!
//! Cloning a `GpuResource` is at most one atomic per attached Arc — no
//! driver round-trip, no `CFRetain` / `AHardwareBuffer_acquire` per
//! clone.
//!
//! # Thread safety
//!
//! `Send + Sync` is **auto-derived** on the enum (not a blanket
//! `unsafe impl`). Each newtype carries its own `unsafe impl Send + Sync`
//! documented on the type — see [`handles`]. If a future variant lands
//! a non-`Send` payload, the compiler refuses to derive on the enum,
//! forcing the author to either fix the payload or refactor consumers
//! explicitly.
//!
//! **wasm carve-out.** On `target_family = "wasm"` `GpuResource` is
//! `!Send + !Sync` on BOTH the non-atomics and threaded `atomics` targets.
//! The pin that holds on both is the `wgpu` `WgpuTexture` variant's
//! `Option<Arc<dyn ResourceKeepAlive>>` keep-alive: `ResourceKeepAlive`'s
//! [`crate::MaybeSendSync`] supertrait is the *empty* marker on wasm, so
//! that `Arc` trait object is `!Send + !Sync`, and the variant — unlike the
//! native newtypes — carries no `unsafe impl` to override it. This is
//! independent of the threading model and of whether a wgpu web backend is
//! even compiled: with no backend selected, `wgpu::Texture` is an
//! uninhabited, vacuously-`Send` dispatch enum, so it is the keep-alive
//! field, not `wgpu::Texture`'s own auto-traits, that pins the variant.
//!
//! The `atomics` target adds a second, independent pin: the
//! [`GpuResource::Web`] variant's `web_sys` payload is `!Send` there,
//! because wasm-bindgen gates `unsafe impl Send/Sync for JsValue` on
//! `cfg(not(target_feature = "atomics"))`. On the non-atomics target that
//! same payload *is* `Send + Sync` (wasm-bindgen's fragile impl) — a
//! property we deliberately do **not** rely on, which is exactly why the
//! keep-alive pin above is written to stand on its own. The `Web` variant
//! carries **no** `unsafe impl`, so it stays `!Send` on the atomics target;
//! do not add one "for symmetry".
//!
//! This carve-out is intentional and non-restrictive: one interop instance
//! per Worker, cross-Worker handoff via `postMessage` / `Transferable`s,
//! never Rust thread movement.

mod handles;

pub use handles::*;

/// Zero-copy GPU resource handle.
///
/// `#[non_exhaustive]` — future versions may add variants; existing matchers stay
/// valid.
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum GpuResource {
    // ── D3D11 / D3D12 ─────────────────────────────────────────────────
    D3D11Texture(D3D11Texture),
    D3D12Resource(D3D12Resource),
    D3D12BufferHandle(D3D12BufferHandle),

    // ── Vulkan ────────────────────────────────────────────────────────
    VkImage(VkImage),
    VkBufferHandle(VkBufferHandle),
    /// `VK_KHR_external_memory_fd` opaque-fd export (Linux / Android).
    #[cfg(any(unix, target_os = "wasi", target_os = "hermit"))]
    VkOpaqueFd(VkOpaqueFd),
    /// `VK_KHR_external_memory_win32` opaque-NT-handle export (Windows).
    #[cfg(windows)]
    VkOpaqueWin32(VkOpaqueWin32),

    // ── CUDA ──────────────────────────────────────────────────────────
    CudaPtr2D(CudaPtr2D),
    CudaBufferHandle(CudaBufferHandle),
    /// Opaque CUDA `CUsurfObject` bound to a `CUarray` — a `surf2Dwrite`
    /// kernel write target (typically a foreign-API texture imported as
    /// a CUDA array). Gated on the `wgpu` feature because the handle
    /// carries a `wgpu::TextureFormat`; see [`CudaSurface`].
    #[cfg(feature = "wgpu")]
    CudaSurface(CudaSurface),

    // ── OpenCL ────────────────────────────────────────────────────────
    OpenClMem(OpenClMem),

    // ── Metal / Apple ─────────────────────────────────────────────────
    MetalTextureHandle(MetalTextureHandle),
    MetalBufferHandle(MetalBufferHandle),
    #[cfg(target_vendor = "apple")]
    IoSurface(IoSurface),
    #[cfg(target_vendor = "apple")]
    VideoToolboxFrame(VideoToolboxFrame),

    // ── OpenGL ────────────────────────────────────────────────────────
    GlTextureHandle(GlTextureHandle),
    GlBufferHandle(GlBufferHandle),

    // ── Linux ─────────────────────────────────────────────────────────
    /// Bare Linux DMA-BUF descriptor.
    #[cfg(any(target_os = "linux", target_os = "android"))]
    DmaBufHandle(DmaBufHandle),
    /// VAAPI surface — kept as an importer path.
    #[cfg(target_os = "linux")]
    VaapiSurface(VaapiSurface),

    // ── Android ───────────────────────────────────────────────────────
    #[cfg(target_os = "android")]
    AHardwareBufferHandle(AHardwareBufferHandle),
    #[cfg(target_os = "android")]
    MediaCodecFrame(MediaCodecFrame),
    #[cfg(target_os = "android")]
    AImageFrame(AImageFrame),
    #[cfg(target_os = "android")]
    AndroidNativeWindowHandle(AndroidNativeWindowHandle),
    #[cfg(target_os = "android")]
    AndroidSurfaceControlHandle(AndroidSurfaceControlHandle),

    // ── Windows NT handles ────────────────────────────────────────────
    #[cfg(windows)]
    NtHandle(NtHandle),
    #[cfg(windows)]
    KmtToken(KmtToken),

    // ── CPU ───────────────────────────────────────────────────────────
    CpuBytes(CpuBytes),
    CpuPlanes(CpuPlaneSet),
    CpuSharedSlot(CpuSharedSlot),

    // ── Native wgpu (same-device passthrough) ─────────────────────────
    /// A `wgpu::Texture` already on the consumer's own wgpu device — the
    /// degenerate "nothing to import" case. The interop import path
    /// returns the texture verbatim (no foreign-handle import, no copy).
    /// Gated on the `wgpu` feature; see [`WgpuTexture`].
    #[cfg(feature = "wgpu")]
    WgpuTexture(WgpuTexture),

    // ── Web (wasm) ────────────────────────────────────────────────────
    /// Browser GPU / media handle (`GPUTexture`, `WebGLTexture`,
    /// `<video>`, `VideoFrame`, …). wasm-only. See the module
    /// `# Thread safety` note and [`crate::web::WebGpuResource`].
    #[cfg(all(target_family = "wasm", feature = "web"))]
    Web(crate::web::WebGpuResource),
}

// Compile-time enforcement of the wasm thread-affinity carve-out.
// On wasm `GpuResource` is `!Send + !Sync`:
//   - with the `wgpu` feature (always on for the interop stack) the
//     `WgpuTexture` variant's `Option<Arc<dyn ResourceKeepAlive>>`
//     keep-alive is `!Send + !Sync` on BOTH wasm targets — its
//     `MaybeSendSync` bound is the empty marker on wasm — and the variant
//     has no `unsafe impl` to force it `Send`. This is the pin on the
//     *non-atomics* target (where the `Web` payload below is still `Send`),
//     which is why the assertion is gated on `wgpu`.
//   - on the threaded (`atomics`) target the `Web(WebGpuResource)`
//     variant's `web_sys` payload is additionally `!Send` (wasm-bindgen
//     gates `Send for JsValue` on non-atomics).
// The guarantee therefore holds independent of the wasm threading model,
// which is why nothing here relies on wgpu's
// `fragile-send-sync-non-atomic-wasm`.
#[cfg(all(target_family = "wasm", feature = "web", feature = "wgpu"))]
static_assertions::assert_not_impl_any!(GpuResource: Send, Sync);

/// Up-to-four-plane CPU buffer set. `count` ∈ `1..=4`; unused slots are
/// `None`. Zero heap allocation — the `[Option<CpuPlane>; 4]` is inline.
#[derive(Debug, Copy, Clone)]
pub struct CpuPlaneSet {
    pub format: crate::PixelFormat,
    pub count: u8,
    pub planes: [Option<CpuPlane>; 4],
}

#[derive(Debug, Copy, Clone)]
pub struct CpuPlane {
    pub data: *mut u8,
    pub size: usize,
    /// Row pitch in bytes.
    pub row_pitch: u32,
}

// SAFETY: caller-asserted contract on the byte ranges. Same shape as
// the typed-newtype payloads in `handles`.
unsafe impl Send for CpuPlaneSet {}
unsafe impl Sync for CpuPlaneSet {}
unsafe impl Send for CpuPlane {}
unsafe impl Sync for CpuPlane {}

// ─────────────────────────────────────────────────────────────────────────────
// GL bind targets / formats
// ─────────────────────────────────────────────────────────────────────────────

/// GL bind target the texture was allocated with. Maps 1:1 onto the
/// matching `GL_TEXTURE_*` enum value.
///
/// `GL_TEXTURE_EXTERNAL_OES` is intentionally absent — those are
/// surfaced through the Android `MediaCodec` / `AImage` paths or the
/// Linux EGL_EXT_image_external bridge, not directly as a
/// `GpuResource::GlTextureHandle`. Importers take the OES target through
/// their own import description instead.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlTextureTarget {
    /// `GL_TEXTURE_2D`.
    Texture2D,
    /// `GL_TEXTURE_2D_ARRAY`.
    Texture2DArray,
    /// `GL_TEXTURE_3D`.
    Texture3D,
    /// `GL_TEXTURE_CUBE_MAP`.
    CubeMap,
    /// `GL_TEXTURE_RECTANGLE` (desktop GL only).
    Rectangle,
    /// `GL_RENDERBUFFER`.
    Renderbuffer,
}

impl GlTextureTarget {
    /// The matching `GL_TEXTURE_*` / `GL_RENDERBUFFER` enum value.
    pub const fn to_gl_enum(self) -> u32 {
        match self {
            Self::Texture2D => 0x0DE1,
            Self::Texture2DArray => 0x8C1A,
            Self::Texture3D => 0x806F,
            Self::CubeMap => 0x8513,
            Self::Rectangle => 0x84F5,
            Self::Renderbuffer => 0x8D41,
        }
    }
}

/// GL buffer bind target. Non-exhaustive — covers the targets a video /
/// VFX caller realistically allocates exportable buffers under.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlBufferTarget {
    /// `GL_ARRAY_BUFFER` — vertex attribute storage.
    ArrayBuffer,
    /// `GL_ELEMENT_ARRAY_BUFFER` — index storage.
    ElementArrayBuffer,
    /// `GL_PIXEL_UNPACK_BUFFER` — async upload PBO.
    PixelUnpackBuffer,
    /// `GL_PIXEL_PACK_BUFFER` — async download PBO.
    PixelPackBuffer,
    /// `GL_SHADER_STORAGE_BUFFER` — SSBO (read-write storage).
    ShaderStorageBuffer,
    /// `GL_UNIFORM_BUFFER` — UBO.
    UniformBuffer,
    /// `GL_TRANSFORM_FEEDBACK_BUFFER` — transform-feedback output.
    TransformFeedbackBuffer,
    /// `GL_COPY_READ_BUFFER` / `GL_COPY_WRITE_BUFFER` — generic
    /// copy-only binding. Useful when the caller doesn't know the final
    /// use up-front.
    CopyReadBuffer,
    CopyWriteBuffer,
}

impl GlBufferTarget {
    /// The matching `GL_*_BUFFER` enum value.
    pub const fn to_gl_enum(self) -> u32 {
        match self {
            Self::ArrayBuffer => 0x8892,
            Self::ElementArrayBuffer => 0x8893,
            Self::PixelUnpackBuffer => 0x88EC,
            Self::PixelPackBuffer => 0x88EB,
            Self::ShaderStorageBuffer => 0x90D2,
            Self::UniformBuffer => 0x8A11,
            Self::TransformFeedbackBuffer => 0x8C8E,
            Self::CopyReadBuffer => 0x8F36,
            Self::CopyWriteBuffer => 0x8F37,
        }
    }
}

/// GL sized internal-format enum the texture was created with — only the
/// subset that a video pipeline ever surfaces.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum GlInternalFormat {
    Unspecified,
    R8,
    R8Snorm,
    R8Ui,
    R8I,
    R16,
    R16F,
    R16Ui,
    R16I,
    R32F,
    Rg8,
    Rg8Snorm,
    Rg16,
    Rg16F,
    Rgba8,
    Srgb8Alpha8,
    Rgba8Snorm,
    Rgb10A2,
    Rgba16,
    Rgba16F,
    Rgba32F,
    R11fG11fB10f,
    Bgra8,
    Depth16,
    Depth24Stencil8,
}

impl GlInternalFormat {
    /// The matching `GL_*` enum value, or `0` for `Unspecified`.
    pub const fn to_gl_enum(self) -> u32 {
        match self {
            Self::Unspecified => 0,
            Self::R8 => 0x8229,
            Self::R8Snorm => 0x8F94,
            Self::R8Ui => 0x8232,
            Self::R8I => 0x8231,
            Self::R16 => 0x822A,
            Self::R16F => 0x822D,
            Self::R16Ui => 0x8234,
            Self::R16I => 0x8233,
            Self::R32F => 0x822E,
            Self::Rg8 => 0x822B,
            Self::Rg8Snorm => 0x8F95,
            Self::Rg16 => 0x822C,
            Self::Rg16F => 0x822F,
            Self::Rgba8 => 0x8058,
            Self::Srgb8Alpha8 => 0x8C43,
            Self::Rgba8Snorm => 0x8F97,
            Self::Rgb10A2 => 0x8059,
            Self::Rgba16 => 0x805B,
            Self::Rgba16F => 0x881A,
            Self::Rgba32F => 0x8814,
            Self::R11fG11fB10f => 0x8C3A,
            Self::Bgra8 => 0x93A1, // GL_BGRA8_EXT
            Self::Depth16 => 0x81A5,
            Self::Depth24Stencil8 => 0x88F0,
        }
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Convenience constructors
// ─────────────────────────────────────────────────────────────────────────────

impl GpuResource {
    /// Convenience constructor for the common case — single GL context,
    /// `GL_TEXTURE_2D`, format left `Unspecified` for the importer to
    /// take from its own import description. No keep-alive (caller owns
    /// the texture name).
    pub fn opengl_texture_simple(name: u32) -> Self {
        // SAFETY: caller-supplied name in current context; the `0`
        // sentinel is the only failure mode `try_from_raw` rejects.
        let h = unsafe {
            GlTextureHandle::try_from_raw(
                name,
                GlTextureTarget::Texture2D,
                GlInternalFormat::Unspecified,
                core::ptr::null_mut(),
                None,
                None,
            )
        }
        .expect("opengl_texture_simple: name must be non-zero");
        Self::GlTextureHandle(h)
    }

    /// Convenience constructor for the common case — single GL context,
    /// `GL_ARRAY_BUFFER`. No keep-alive (caller owns the buffer name).
    pub fn opengl_buffer_simple(name: u32, size: u64) -> Self {
        // SAFETY: caller-supplied name in current context; the `0`
        // sentinel is the only failure mode `try_from_raw` rejects.
        let h = unsafe {
            GlBufferHandle::try_from_raw(name, GlBufferTarget::ArrayBuffer, size, core::ptr::null_mut(), None, None)
        }
        .expect("opengl_buffer_simple: name and size must be non-zero");
        Self::GlBufferHandle(h)
    }
}