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
//
// `SyncPoint::wait()` async surface.
//
// Coverage matrix:
//
// 1. Trivial variants (`Cpu`, `Noop`) — `wait().await` resolves
//    immediately with `Ok(())`; `is_signaled()` reports `true`.
// 2. A custom `SyncWaiter` that flips from "in flight" to "signalled"
//    after a configurable number of `is_signaled` probes — verifies
//    the default `wait_async` impl on `SyncWaiter` drives the probe
//    loop to completion across `await` points.
// 3. A custom `SyncWaiter` that never signals — verifies
//    `wait_with_timeout_async` returns `Error::Timeout` once the
//    deadline elapses.
// 4. Repeated `is_signaled()` calls don't observe spurious
//    state transitions (probe idempotency on a settled waiter).

use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use gpu_handle_types::{BackendKind, Error, SyncPoint, SyncWaiter};

// ---------------------------------------------------------------------
// Trivial variants
// ---------------------------------------------------------------------

#[test]
fn cpu_wait_blocking_immediate() {
    SyncPoint::Cpu.wait_blocking().expect("Cpu wait must succeed immediately");
    SyncPoint::Noop.wait_blocking().expect("Noop wait must succeed immediately");
}

#[test]
fn cpu_is_signaled_true() {
    assert!(SyncPoint::Cpu.is_signaled().expect("Cpu is_signaled never errors"));
    assert!(SyncPoint::Noop.is_signaled().expect("Noop is_signaled never errors"));
}

#[test]
fn cpu_wait_async_immediate() {
    pollster::block_on(async {
        SyncPoint::Cpu.wait().await.expect("Cpu async wait must succeed immediately");
        SyncPoint::Noop.wait().await.expect("Noop async wait must succeed immediately");
    });
}

// ---------------------------------------------------------------------
// Custom waiter — drives the default wait_async probe loop
// ---------------------------------------------------------------------

/// Reports "not signalled" for `flip_after` probes, then "signalled"
/// thereafter. Drives the default `wait_async` impl through both its
/// fast-path yield-spin and the waiter-thread-fallback `is_signaled` loop.
struct CountdownWaiter {
    flip_after: usize,
    probes: AtomicUsize,
}

impl SyncWaiter for CountdownWaiter {
    fn wait(&self, _timeout: Duration) -> Result<(), Error> {
        // Force the async default impl to take the `is_signaled` path
        // by reporting `Timeout` from the blocking entry point until
        // we've crossed the threshold.
        let n = self.probes.fetch_add(1, Ordering::SeqCst);
        if n >= self.flip_after { Ok(()) } else { Err(Error::Timeout) }
    }

    fn is_signaled(&self) -> Result<bool, Error> {
        let n = self.probes.fetch_add(1, Ordering::SeqCst);
        Ok(n >= self.flip_after)
    }

    fn backend(&self) -> BackendKind {
        BackendKind::Cpu
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

fn sync_point_with_waiter(w: Arc<dyn SyncWaiter>) -> SyncPoint {
    // `Cuda` variant is the cheapest carrier — the sync wait dispatch
    // routes through `.waiter()`, not the discriminant, so the variant
    // is invisible to the test.
    SyncPoint::Cuda { event: core::ptr::null_mut(), value: None, waiter: w }
}

#[test]
fn custom_waiter_drives_to_completion_async() {
    let w: Arc<dyn SyncWaiter> = Arc::new(CountdownWaiter { flip_after: 4, probes: AtomicUsize::new(0) });
    let sp = sync_point_with_waiter(w);

    pollster::block_on(async {
        sp.wait_with_timeout_async(Duration::from_secs(5)).await.expect("countdown waiter must signal within 5 s");
    });
}

// ---------------------------------------------------------------------
// Timeout — never-signalling waiter
// ---------------------------------------------------------------------

struct NeverWaiter;
impl SyncWaiter for NeverWaiter {
    fn wait(&self, _timeout: Duration) -> Result<(), Error> {
        Err(Error::Timeout)
    }
    fn is_signaled(&self) -> Result<bool, Error> {
        Ok(false)
    }
    fn backend(&self) -> BackendKind {
        BackendKind::Cpu
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[test]
fn timeout_surfaces_on_unsignalled_waiter_async() {
    let sp = sync_point_with_waiter(Arc::new(NeverWaiter));
    let result = pollster::block_on(sp.wait_with_timeout_async(Duration::from_millis(50)));
    match result {
        Err(Error::Timeout) => {}
        other => panic!("expected Error::Timeout, got {other:?}"),
    }
}

#[test]
fn timeout_surfaces_on_unsignalled_waiter_blocking() {
    let sp = sync_point_with_waiter(Arc::new(NeverWaiter));
    let result = sp.wait_with_timeout(Duration::from_millis(50));
    match result {
        Err(Error::Timeout) => {}
        other => panic!("expected Error::Timeout, got {other:?}"),
    }
}

// ---------------------------------------------------------------------
// Idempotency on a settled waiter
// ---------------------------------------------------------------------

struct AlwaysSignalledWaiter;
impl SyncWaiter for AlwaysSignalledWaiter {
    fn wait(&self, _timeout: Duration) -> Result<(), Error> {
        Ok(())
    }
    fn is_signaled(&self) -> Result<bool, Error> {
        Ok(true)
    }
    fn backend(&self) -> BackendKind {
        BackendKind::Cpu
    }
    fn as_any(&self) -> &dyn Any {
        self
    }
}

#[test]
fn repeated_is_signaled_is_stable() {
    let sp = sync_point_with_waiter(Arc::new(AlwaysSignalledWaiter));
    for _ in 0..16 {
        assert!(sp.is_signaled().expect("settled waiter never errors"));
    }
}