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
//
// `SyncPoint::DeferredWgpu` + `DeferredWgpuSlot` verification.
// Asserts the four contract points the OnceLock-backed
// slot promises:
//
// 1. `slot.get()` returns `None` before the producer commits.
// 2. `slot.set(idx)` succeeds exactly once; a second `set` returns
//    `Error::InvalidArgument` *without* overwriting the committed value.
// 3. `make_deferred_wgpu_sync_point(slot, device)` produces a
//    `SyncPoint::DeferredWgpu` whose `.backend()` is `Wgpu` and whose
//    `.is_signaled()` short-circuits cleanly on `Duration::ZERO`.
// 4. After commit, `wait_blocking` resolves on the recorded
//    `SubmissionIndex` rather than draining the whole device.
//
// Skips with a printed message when no wgpu adapter is available. Tests
// that depend on a real `SubmissionIndex` (`set`, `wait_blocking` after
// commit) need a real submit; tests that exercise only the `OnceLock`
// shape (`get` before set, double-set guard) need only the slot itself.

#![cfg(feature = "wgpu")]

use std::sync::Arc;
use std::time::Duration;

use gpu_handle_types::{BackendKind, DeferredWgpuSlot, Error, SyncPoint, make_deferred_wgpu_sync_point};

/// Open a fresh `(device, queue)` pair, inlined here so the leaf-crate
/// test has no upward dependency.
// The `DeviceDescriptor` keeps a `..Default::default()` tail so it also
// builds against wgpu revisions that add descriptor fields.
#[allow(clippy::needless_update)]
fn try_open_wgpu_device() -> Option<(wgpu::Device, wgpu::Queue)> {
    let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
        backends: wgpu::Backends::all(),
        ..wgpu::InstanceDescriptor::new_without_display_handle()
    });
    let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
        power_preference: wgpu::PowerPreference::None,
        force_fallback_adapter: false,
        compatible_surface: None,
        apply_limit_buckets: false,
    }))
    .ok()?;
    let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
        label: Some("sync_deferred_wgpu.test.device"),
        required_features: wgpu::Features::empty(),
        required_limits: wgpu::Limits::default(),
        memory_hints: wgpu::MemoryHints::default(),
        trace: wgpu::Trace::Off,
        experimental_features: wgpu::ExperimentalFeatures::disabled(),
        ..Default::default()
    }))
    .ok()?;
    Some((device, queue))
}

/// Submit an empty encoder and return the `SubmissionIndex` — the
/// cheapest way to get a real, valid index for slot-set tests.
fn submit_empty(device: &wgpu::Device, queue: &wgpu::Queue) -> wgpu::SubmissionIndex {
    let encoder =
        device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("sync_deferred_wgpu.test.empty") });
    queue.submit([encoder.finish()])
}

#[test]
fn slot_get_returns_none_before_set() {
    let slot = DeferredWgpuSlot::new();
    assert!(slot.get().is_none(), "fresh slot must be uncommitted");
}

#[test]
fn slot_double_set_rejects_second_call() {
    let Some((device, queue)) = try_open_wgpu_device() else {
        eprintln!("no wgpu adapter, skipping");
        return;
    };
    let slot = DeferredWgpuSlot::new();
    let first = submit_empty(&device, &queue);
    let second = submit_empty(&device, &queue);

    // First set succeeds.
    slot.set(first).expect("first set must succeed");
    assert!(slot.get().is_some(), "committed slot must surface Some");

    // Second set must fail without overwriting.
    let err = slot.set(second).expect_err("second set must fail");
    match err {
        Error::InvalidArgument(_) => {}
        other => panic!("expected InvalidArgument, got {other:?}"),
    }

    // get() still resolves — the OnceLock value is the *first* commit.
    assert!(slot.get().is_some(), "rejected second set must not erase the first");
}

#[test]
fn sync_point_backend_is_wgpu() {
    let Some((device, _queue)) = try_open_wgpu_device() else {
        eprintln!("no wgpu adapter, skipping");
        return;
    };
    let slot = Arc::new(DeferredWgpuSlot::new());
    let sp = make_deferred_wgpu_sync_point(slot, device);
    assert_eq!(sp.backend(), BackendKind::Wgpu);
}

#[test]
fn is_signaled_short_circuits_before_commit() {
    // Before commit, `is_signaled` polls with `Duration::ZERO`. The
    // result is allowed to be either Ok(true) (queue happens to be idle)
    // or Ok(false) (timed out without a recorded index); the contract
    // is that it returns *cleanly without erroring*.
    let Some((device, _queue)) = try_open_wgpu_device() else {
        eprintln!("no wgpu adapter, skipping");
        return;
    };
    let slot = Arc::new(DeferredWgpuSlot::new());
    let sp = make_deferred_wgpu_sync_point(slot, device);

    let waiter = sp.waiter().expect("DeferredWgpu carries a waiter");
    let _signaled = waiter.is_signaled().expect("is_signaled must not error");
}

#[test]
fn wait_after_commit_resolves_quickly() {
    let Some((device, queue)) = try_open_wgpu_device() else {
        eprintln!("no wgpu adapter, skipping");
        return;
    };
    let slot = Arc::new(DeferredWgpuSlot::new());
    let sp = make_deferred_wgpu_sync_point(slot.clone(), device.clone());

    // Submit, then commit the slot. A short, finite wait must succeed —
    // wgpu's `device.poll(Wait { submission_index: Some(idx), .. })`
    // returns as soon as that index is drained.
    let idx = submit_empty(&device, &queue);
    slot.set(idx).expect("commit");

    sp.wait_with_timeout(Duration::from_secs(5)).expect("post-commit wait must resolve within 5 s");
}

#[test]
fn sync_point_clone_shares_committed_slot() {
    // The producer-side `slot.set(idx)` is visible to every clone of
    // the SyncPoint — that's the whole point of `Arc<DeferredWgpuSlot>`.
    let Some((device, queue)) = try_open_wgpu_device() else {
        eprintln!("no wgpu adapter, skipping");
        return;
    };
    let slot = Arc::new(DeferredWgpuSlot::new());
    let sp_a = make_deferred_wgpu_sync_point(slot.clone(), device.clone());
    let sp_b = sp_a.clone();

    let idx = submit_empty(&device, &queue);
    slot.set(idx).expect("commit through the original slot Arc");

    // Both clones see the commit.
    sp_a.wait_with_timeout(Duration::from_secs(5)).expect("clone A sees the commit");
    sp_b.wait_with_timeout(Duration::from_secs(5)).expect("clone B sees the commit through the shared Arc");
}

#[test]
fn sync_point_is_clone() {
    // Compile-only — proves `SyncPoint::DeferredWgpu` clones cheaply via
    // its shared-handle fields without forcing the value-typed `SubmissionIndex`
    // through a `Clone` of its own (it carries `Clone + Send + Sync` per
    // the defensive `assert_impl_all!` next to `DeferredWgpuSlot` in sync.rs).
    fn _assert_clone<T: Clone>() {}
    _assert_clone::<SyncPoint>();
}