gpu-handle-types 0.2.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
//
// Verifies typed `GpuResource` handles.
//
// Covers: invariant rejection at `try_from_raw`, `Send + Sync + Clone`
// auto-derive on the enum, the `Arc<dyn ResourceKeepAlive>` trait
// object, and the drop-counter behaviour for Arc-wrapped handles.

use std::ffi::c_void;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

use gpu_handle_types::{
    CudaBufferHandle, CudaPtr2D, D3D11Texture, D3D12BufferHandle, D3D12Resource, GpuResource, InvalidHandleError,
    KeepAlive, MetalBufferHandle, MetalTextureHandle, OpenClMem, PixelFormat, ResourceKeepAlive, VkBufferHandle,
    VkImage,
};

// ─────────────────────────────────────────────────────────────────────────────
// Static assertions — pin the auto-trait propagation contract
// ─────────────────────────────────────────────────────────────────────────────

// `Clone` holds on every target; `Send + Sync` only off wasm. On wasm
// `GpuResource` is `!Send + !Sync`: the `WgpuTexture`
// variant's `Arc<dyn ResourceKeepAlive>` keep-alive is thread-affine on
// both wasm targets (empty `MaybeSendSync` bound), so the inverse assert is
// gated on `wgpu` to match the in-crate guard in `gpu_resource.rs`.
static_assertions::assert_impl_all!(GpuResource: Clone);
#[cfg(not(target_family = "wasm"))]
static_assertions::assert_impl_all!(GpuResource: Send, Sync);
#[cfg(all(target_family = "wasm", feature = "web", feature = "wgpu"))]
static_assertions::assert_not_impl_any!(GpuResource: Send, Sync);
static_assertions::assert_impl_all!(VkImage: Send, Sync, Clone);
static_assertions::assert_impl_all!(D3D11Texture: Send, Sync, Clone);
static_assertions::assert_impl_all!(D3D12Resource: Send, Sync, Clone);
static_assertions::assert_impl_all!(CudaPtr2D: Send, Sync, Clone);
static_assertions::assert_impl_all!(OpenClMem: Send, Sync, Clone);
static_assertions::assert_impl_all!(MetalTextureHandle: Send, Sync, Clone);

// `Arc<dyn ResourceKeepAlive>` (the trait object) is `Send + Sync` off
// wasm; `Clone` on every target. On wasm `ResourceKeepAlive: MaybeSendSync`
// is the empty marker, so the trait object drops its `Send + Sync`
// supertrait bound and the `Arc` is `!Send + !Sync` — independent of the
// `web` / `wgpu` features.
static_assertions::assert_impl_all!(Arc<dyn ResourceKeepAlive>: Clone);
#[cfg(not(target_family = "wasm"))]
static_assertions::assert_impl_all!(Arc<dyn ResourceKeepAlive>: Send, Sync);
#[cfg(target_family = "wasm")]
static_assertions::assert_not_impl_any!(Arc<dyn ResourceKeepAlive>: Send, Sync);

// ─────────────────────────────────────────────────────────────────────────────
// `try_from_raw` invariant rejection
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn vk_image_rejects_null_handle() {
    // SAFETY: image = 0 is the rejected case; no UB.
    let r = unsafe {
        VkImage::try_from_raw(
            0,                     // image (null)
            0x1234 as *mut c_void, // device
            core::ptr::null_mut(), // instance
            core::ptr::null_mut(), // phys_dev
            None,                  // uuid
            0,                     // row_pitch
            None,                  // keep_alive
        )
    };
    assert!(matches!(r, Err(InvalidHandleError::NullHandle(_))));
}

#[test]
fn vk_image_accepts_null_device_for_trusted_import() {
    // VkImage::try_from_raw deliberately permits `device == null` so
    // "trusted import" call sites that recover the device implicitly
    // (e.g. the wgpu-vulkan backend that knows the device because it
    // owns the wgpu adapter) can still construct the handle.
    let r = unsafe {
        VkImage::try_from_raw(
            0xABCD,
            core::ptr::null_mut(),
            core::ptr::null_mut(),
            core::ptr::null_mut(),
            None,
            0,
            None,
        )
    };
    let h = r.expect("null device is permitted on the trusted-import path");
    assert_eq!(h.image(), 0xABCD);
    assert!(h.device().is_null());
}

#[test]
fn vk_image_accepts_valid_inputs() {
    // SAFETY: handles are bogus but pass the non-null check, which is
    // all `try_from_raw` validates.
    let r = unsafe {
        VkImage::try_from_raw(
            0xDEAD_BEEF,
            0x1000 as *mut c_void,
            0x2000 as *mut c_void,
            0x3000 as *mut c_void,
            Some([1u8; 16]),
            64,
            None,
        )
    };
    let h = r.expect("valid inputs accepted");
    assert_eq!(h.image(), 0xDEAD_BEEF);
    assert_eq!(h.row_pitch(), 64);
    assert_eq!(h.uuid(), Some([1u8; 16]));
}

#[test]
fn vk_buffer_rejects_zero_size() {
    let r = unsafe { VkBufferHandle::try_from_raw(0xAA, std::ptr::dangling_mut::<c_void>(), None, 0, None) };
    assert!(matches!(r, Err(InvalidHandleError::ZeroSize(_))));
}

#[test]
fn d3d12_buffer_rejects_zero_size() {
    let r = unsafe { D3D12BufferHandle::try_from_raw(std::ptr::dangling_mut::<c_void>(), None, 0, None) };
    assert!(matches!(r, Err(InvalidHandleError::ZeroSize(_))));
}

#[test]
fn cuda_ptr_rejects_null() {
    let r = unsafe { CudaPtr2D::try_from_raw(0, 64, None, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullHandle(_))));
}

#[test]
fn cuda_buffer_rejects_zero_size() {
    let r = unsafe { CudaBufferHandle::try_from_raw(0xAA, 0, None, None) };
    assert!(matches!(r, Err(InvalidHandleError::ZeroSize(_))));
}

#[test]
fn cuda_ptr_plane_row_pitch_falls_back_to_row_pitch() {
    // No per-plane pitches attached ⇒ every plane reports `row_pitch`
    // (the NVDEC / cuMemAllocPitch single-allocation contract).
    let h = unsafe { CudaPtr2D::try_from_raw(0xAA, 256, None, None) }.expect("non-null");
    assert_eq!(h.plane_row_pitch(0), 256);
    assert_eq!(h.plane_row_pitch(1), 256);
    assert_eq!(h.plane_row_pitch(2), 256);
    assert_eq!(h.plane_row_pitch(3), 256);
    // Out-of-range index clamps to plane 0.
    assert_eq!(h.plane_row_pitch(4), 256);
    assert_eq!(h.plane_row_pitches(), None);
}

#[test]
fn cuda_ptr_plane_row_pitch_honours_explicit_per_plane() {
    // A triplanar source whose U/V planes carry their own narrower
    // device pitch — the case a per-plane consumer would silently
    // row-drift on if it scaled luma pitch proportionally.
    let h = unsafe { CudaPtr2D::try_from_raw(0xAA, 1024, None, None) }
        .expect("non-null")
        .with_plane_row_pitches([512, 512, 0]);
    assert_eq!(h.plane_row_pitch(0), 1024, "plane 0 always uses row_pitch");
    assert_eq!(h.plane_row_pitch(1), 512);
    assert_eq!(h.plane_row_pitch(2), 512);
    // A zero tail entry means "unset" for that plane ⇒ row_pitch fallback.
    assert_eq!(h.plane_row_pitch(3), 1024);
    assert_eq!(h.plane_row_pitches(), Some([512, 512, 0]));
}

#[test]
fn opencl_rejects_null_pointers() {
    let r = unsafe { OpenClMem::try_from_raw(core::ptr::null_mut(), std::ptr::dangling_mut::<c_void>(), 0, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullPointer(_))));
}

#[test]
fn metal_handles_reject_null() {
    let r = unsafe { MetalTextureHandle::try_from_raw(core::ptr::null_mut(), None, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullPointer(_))));

    let r = unsafe { MetalBufferHandle::try_from_raw(core::ptr::null_mut(), 0, None, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullPointer(_))));
}

#[test]
fn d3d_resources_reject_null() {
    let r = unsafe { D3D11Texture::try_from_raw(core::ptr::null_mut(), None, 0, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullPointer(_))));

    let r = unsafe { D3D12Resource::try_from_raw(core::ptr::null_mut(), None, 0, None) };
    assert!(matches!(r, Err(InvalidHandleError::NullPointer(_))));
}

// ─────────────────────────────────────────────────────────────────────────────
// Keep-alive trait dispatch + drop ordering
// ─────────────────────────────────────────────────────────────────────────────

#[derive(Debug)]
struct DropCounter {
    counter: Arc<AtomicUsize>,
}

impl Drop for DropCounter {
    fn drop(&mut self) {
        self.counter.fetch_add(1, Ordering::SeqCst);
    }
}

// Explicit ResourceKeepAlive impl — no blanket impl on the trait
// (concrete anchor types override `mark_consumed`, so every type opts
// in by writing one line).
impl gpu_handle_types::ResourceKeepAlive for DropCounter {}

#[test]
fn keep_alive_drop_runs_once_on_last_clone() {
    let counter = Arc::new(AtomicUsize::new(0));
    let dc = Arc::new(DropCounter { counter: counter.clone() });
    let keep: KeepAlive = dc.clone();

    // SAFETY: bogus handle, but try_from_raw only checks for non-null.
    let h = unsafe {
        VkImage::try_from_raw(
            0x1,
            std::ptr::dangling_mut::<c_void>(),
            core::ptr::null_mut(),
            core::ptr::null_mut(),
            None,
            0,
            Some(keep),
        )
    }
    .unwrap();

    let h2 = h.clone();
    let h3 = h.clone();
    assert_eq!(counter.load(Ordering::SeqCst), 0, "no drop while clones live");

    drop(h);
    drop(h2);
    assert_eq!(counter.load(Ordering::SeqCst), 0, "no drop while any clone is alive");

    drop(h3);
    drop(dc); // release the original
    assert_eq!(counter.load(Ordering::SeqCst), 1, "exactly one drop after last clone");
}

#[test]
fn keep_alive_trait_object_dyn_dispatch() {
    // Ensure `Arc<dyn ResourceKeepAlive>` can be constructed from any
    // type that explicitly `impl ResourceKeepAlive for X {}`. The payload
    // field is sample data — only the type's `Send + Sync + Debug +
    // 'static` bounds + the explicit impl matter for the dyn-dispatch
    // coercion under test.
    #[derive(Debug)]
    struct CustomMarker(#[allow(dead_code)] u32);
    impl gpu_handle_types::ResourceKeepAlive for CustomMarker {}

    let m: KeepAlive = Arc::new(CustomMarker(42));
    let _: KeepAlive = m.clone();
}

// ─────────────────────────────────────────────────────────────────────────────
// GpuResource enum smoke tests
// ─────────────────────────────────────────────────────────────────────────────

#[test]
fn gpu_resource_clone_propagates_through_enum() {
    let bytes = vec![0u8; 1024].into_boxed_slice();
    let ptr = bytes.as_ptr() as *mut u8;
    let size = bytes.len();
    // Leak the buffer so the pointer stays valid (test-only).
    Box::leak(bytes);

    // SAFETY: pointer is leaked above; valid for the rest of the test.
    let cpu = unsafe { gpu_handle_types::CpuBytes::try_from_raw(ptr, size, 64, PixelFormat::RgbaU8, None).unwrap() };
    let r = GpuResource::CpuBytes(cpu);

    let r2 = r.clone();
    let _r3 = r2.clone();

    // Smoke — clone doesn't panic, types compose.
    let _ = format!("{:?}", r);
}

#[test]
fn opengl_simple_constructors_work() {
    let tex = GpuResource::opengl_texture_simple(42);
    let buf = GpuResource::opengl_buffer_simple(7, 1024);

    match tex {
        GpuResource::GlTextureHandle(h) => assert_eq!(h.name(), 42),
        _ => panic!("wrong variant"),
    }
    match buf {
        GpuResource::GlBufferHandle(h) => assert_eq!(h.name(), 7),
        _ => panic!("wrong variant"),
    }
}

#[test]
#[should_panic(expected = "must be non-zero")]
fn opengl_simple_panics_on_zero_name() {
    GpuResource::opengl_texture_simple(0);
}