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
//
// Per-device serialization of wgpu's error-scope stack.
//
// `wgpu::Device::push_error_scope` / `pop_error_scope` operate on a
// **per-device, strictly-LIFO** stack. When two threads share ONE
// `wgpu::Device` and each brackets an allocation in its own
// `push → create → pop` triple, the calls interleave and wgpu aborts with
// "Mismatched pop_error_scope call: error scopes must be popped in reverse
// order". The hazard is per *device*, not per *pool* / per *call site*:
// every `push/pop` pair anywhere in the process that targets a given device
// must be mutually exclusive with every other pair on that same device.
//
// This module owns the one process-global registry that makes that true.
// `with_error_scope` keys a `Mutex<()>` on the `wgpu::Device`'s identity —
// wgpu's handle `Hash`, which every clone of one device shares (a
// `wgpu::Device` is itself a shared handle, so keying on an outer `Arc`'s
// address would split one device across two locks) — while independent
// devices get independent locks, so an N-device pipeline still allocates
// on each device concurrently.
//
// wgpu keeps the stack thread-local only with its `std` feature. Without it
// (a `default-features = false` build, which is how this crate depends on
// wgpu) one stack is shared by all threads, so the hazard is real for any
// two threads touching the device.
//
// Every scoped allocation on a device that can be shared must route through
// here: a frame pool or transient allocator that a shared render device
// drives from every render thread, an interop layer's output pools. It
// lives in this low-level crate for exactly that reason — one registry
// every such caller can reach. Any error-scope call site that can run on a
// SHARED device must route through here too.

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

use std::collections::HashMap;
use std::hash::{BuildHasher, BuildHasherDefault, DefaultHasher};
use std::sync::{Arc, Mutex, OnceLock};

/// Process-global registry: device-identity → that device's error-scope
/// lock. The outer `Mutex` guards only the map (held for a hash lookup /
/// insert, never across GPU work); the inner `Arc<Mutex<()>>` is the
/// per-device lock held across the `push → create → pop` triple.
fn registry() -> &'static Mutex<HashMap<u64, Arc<Mutex<()>>>> {
    static REG: OnceLock<Mutex<HashMap<u64, Arc<Mutex<()>>>>> = OnceLock::new();
    REG.get_or_init(|| Mutex::new(HashMap::new()))
}

/// The error-scope lock for `device`, creating it on first use. Keyed on the
/// device's identity hash: every clone of one `wgpu::Device` maps to the same
/// lock. A 64-bit collision between two distinct devices would only make
/// them share a lock — extra serialization, never a missed one.
fn lock_for(device: &wgpu::Device) -> Arc<Mutex<()>> {
    let key = BuildHasherDefault::<DefaultHasher>::default().hash_one(device);
    let mut map = registry().lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    Arc::clone(map.entry(key).or_insert_with(|| Arc::new(Mutex::new(()))))
}

/// Run `f` (an allocation already bracketed by `push_error_scope` /
/// `pop_error_scope` on `device`) while holding `device`'s process-global
/// error-scope lock, so the push/pop triple cannot interleave with a
/// concurrent triple on the same device.
///
/// The lock guards a `()` with no invariant a panic can corrupt, so a
/// poisoned lock is recovered (the next caller proceeds) rather than
/// propagated. The lock is held only for the microsecond-scale push/create/pop
/// window — never across blocking GPU work — so peers on the *same* device
/// serialize their allocations (already serial in the driver) while peers on
/// *other* devices are unaffected.
pub fn with_error_scope<R>(device: &wgpu::Device, f: impl FnOnce() -> R) -> R {
    let lock = lock_for(device);
    let _guard = lock.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
    f()
}