diff --git a/Cargo.lock b/Cargo.lock index bc57905953..b904d76b44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5100,10 +5100,13 @@ version = "25.0.0" dependencies = [ "bitflags 2.9.1", "bytemuck", + "cfg-if", "js-sys", "log", + "parking_lot", "serde", "serde_json", + "spin", "thiserror 2.0.12", "web-sys", ] diff --git a/Cargo.toml b/Cargo.toml index e9f0254aed..c50d237286 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,8 @@ serde_json = "1.0.118" serde = { version = "1.0.219", default-features = false } shell-words = "1" smallvec = "1.9" +# NOTE: `crossbeam-deque` currently relies on this version of spin +spin = { version = "0.9.8", default-features = false } spirv = "0.3" static_assertions = "1.1" strum = { version = "0.27", default-features = false, features = ["derive"] } diff --git a/wgpu-types/Cargo.toml b/wgpu-types/Cargo.toml index 921f319592..d78e81469b 100644 --- a/wgpu-types/Cargo.toml +++ b/wgpu-types/Cargo.toml @@ -48,6 +48,20 @@ trace = ["std"] # Enable web-specific dependencies for wasm. web = ["dep:js-sys", "dep:web-sys"] +# Enables the `parking_lot` set of locking primitives. +# This is the recommended implementation and will be used in preference to +# any other implementation. +# Will fallback to a `RefCell` based implementation which is `!Sync` when no +# alternative feature is enabled. +parking_lot = ["dep:parking_lot"] + +# Enables the `spin` set of locking primitives. +# This is generally only useful for `no_std` targets, and will be unused if +# either `std` or `parking_lot` are available. +# Will fallback to a `RefCell` based implementation which is `!Sync` when no +# alternative feature is enabled. +spin = ["dep:spin"] + [dependencies] bitflags = { workspace = true, features = ["serde"] } bytemuck = { workspace = true, features = ["derive"] } @@ -57,6 +71,13 @@ serde = { workspace = true, default-features = false, features = [ "alloc", "derive", ], optional = true } +cfg-if.workspace = true +spin = { workspace = true, features = [ + "rwlock", + "mutex", + "spin_mutex", +], optional = true } +parking_lot = { workspace = true, optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { workspace = true, optional = true, default-features = false } diff --git a/wgpu-types/src/lib.rs b/wgpu-types/src/lib.rs index 3ba3bbe268..4852596669 100644 --- a/wgpu-types/src/lib.rs +++ b/wgpu-types/src/lib.rs @@ -39,6 +39,7 @@ mod env; mod features; pub mod instance; pub mod math; +pub mod sync; mod transfers; pub use counters::*; diff --git a/wgpu-types/src/sync.rs b/wgpu-types/src/sync.rs new file mode 100644 index 0000000000..4c788ae709 --- /dev/null +++ b/wgpu-types/src/sync.rs @@ -0,0 +1,237 @@ +//! Provides [`Mutex`] and [`RwLock`] types with an appropriate implementation chosen +//! from: +//! +//! 1. [`parking_lot`] (default) +//! 2. [`std`] +//! 3. [`spin`] +//! 4. [`RefCell`](core::cell::RefCell) (fallback) +//! +//! These are ordered by priority. +//! For example if `parking_lot` and `std` are both enabled, `parking_lot` will +//! be used as the implementation. +//! +//! Generally you should use `parking_lot` for the optimal performance, at the +//! expense of reduced target compatibility. +//! In contrast, `spin` provides the best compatibility (e.g., `no_std`) in exchange +//! for potentially worse performance. +//! If no implementation is chosen, [`RefCell`](core::cell::RefCell) will be used +//! as a fallback. +//! Note that the fallback implementation is _not_ [`Sync`] and will [spin](core::hint::spin_loop) +//! when a lock is contested. +//! +//! [`parking_lot`]: https://docs.rs/parking_lot/ +//! [`std`]: https://docs.rs/std/ +//! [`spin`]: https://docs.rs/std/ + +use core::{fmt, ops}; + +cfg_if::cfg_if! { + if #[cfg(feature = "parking_lot")] { + use parking_lot as implementation; + } else if #[cfg(feature = "std")] { + use std::sync as implementation; + } else if #[cfg(feature = "spin")] { + use spin as implementation; + } else { + mod implementation { + pub(super) use core::cell::RefCell as Mutex; + pub(super) use core::cell::RefMut as MutexGuard; + + pub(super) use core::cell::RefCell as RwLock; + pub(super) use core::cell::Ref as RwLockReadGuard; + pub(super) use core::cell::RefMut as RwLockWriteGuard; + + /// Repeatedly invoke `f` until [`Option::Some`] is returned. + /// This method [spins](core::hint::spin_loop), busy-waiting the current + /// thread. + pub(super) fn spin_unwrap(mut f: impl FnMut() -> Option) -> T { + 'spin: loop { + match (f)() { + Some(value) => break 'spin value, + None => core::hint::spin_loop(), + } + } + } + } + } +} + +/// A plain wrapper around [`implementation::Mutex`]. +/// +/// This is just like [`implementation::Mutex`], but slight inconsistencies +/// between the different implementation APIs are smoothed-over. +pub struct Mutex(implementation::Mutex); + +/// A guard produced by locking [`Mutex`]. +/// +/// This is just a wrapper around a [`implementation::MutexGuard`]. +pub struct MutexGuard<'a, T>(implementation::MutexGuard<'a, T>); + +impl Mutex { + /// Create a new [`Mutex`]. + pub fn new(value: T) -> Mutex { + Mutex(implementation::Mutex::new(value)) + } + + /// Lock the provided [`Mutex`], allowing reading and/or writing. + pub fn lock(&self) -> MutexGuard { + cfg_if::cfg_if! { + if #[cfg(feature = "parking_lot")] { + let lock = self.0.lock(); + } else if #[cfg(feature = "std")] { + let lock = self.0.lock().unwrap_or_else(std::sync::PoisonError::into_inner); + } else if #[cfg(feature = "spin")] { + let lock = self.0.lock(); + } else { + let lock = implementation::spin_unwrap(|| self.0.try_borrow_mut().ok()); + } + } + + MutexGuard(lock) + } + + /// Consume the provided [`Mutex`], returning the inner value. + pub fn into_inner(self) -> T { + let inner = self.0.into_inner(); + + #[cfg(all(feature = "std", not(feature = "parking_lot")))] + let inner = inner.unwrap_or_else(std::sync::PoisonError::into_inner); + + inner + } +} + +impl ops::Deref for MutexGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl ops::DerefMut for MutexGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// A plain wrapper around [`implementation::RwLock`]. +/// +/// This is just like [`implementation::RwLock`], but slight inconsistencies +/// between the different implementation APIs are smoothed-over. +pub struct RwLock(implementation::RwLock); + +/// A read guard produced by locking [`RwLock`] as a reader. +/// +/// This is just a wrapper around a [`implementation::RwLockReadGuard`]. +pub struct RwLockReadGuard<'a, T> { + guard: implementation::RwLockReadGuard<'a, T>, +} + +/// A write guard produced by locking [`RwLock`] as a writer. +/// +/// This is just a wrapper around a [`implementation::RwLockWriteGuard`]. +pub struct RwLockWriteGuard<'a, T> { + guard: implementation::RwLockWriteGuard<'a, T>, + /// Allows for a safe `downgrade` method without `parking_lot` + #[cfg(not(feature = "parking_lot"))] + lock: &'a RwLock, +} + +impl RwLock { + /// Create a new [`RwLock`]. + pub fn new(value: T) -> RwLock { + RwLock(implementation::RwLock::new(value)) + } + + /// Read from the provided [`RwLock`]. + pub fn read(&self) -> RwLockReadGuard { + cfg_if::cfg_if! { + if #[cfg(feature = "parking_lot")] { + let guard = self.0.read(); + } else if #[cfg(feature = "std")] { + let guard = self.0.read().unwrap_or_else(std::sync::PoisonError::into_inner); + } else if #[cfg(feature = "spin")] { + let guard = self.0.read(); + } else { + let guard = implementation::spin_unwrap(|| self.0.try_borrow().ok()); + } + } + + RwLockReadGuard { guard } + } + + /// Write to the provided [`RwLock`]. + pub fn write(&self) -> RwLockWriteGuard { + cfg_if::cfg_if! { + if #[cfg(feature = "parking_lot")] { + let guard = self.0.write(); + } else if #[cfg(feature = "std")] { + let guard = self.0.write().unwrap_or_else(std::sync::PoisonError::into_inner); + } else if #[cfg(feature = "spin")] { + let guard = self.0.write(); + } else { + let guard = implementation::spin_unwrap(|| self.0.try_borrow_mut().ok()); + } + } + + RwLockWriteGuard { + guard, + #[cfg(not(feature = "parking_lot"))] + lock: self, + } + } +} + +impl<'a, T> RwLockWriteGuard<'a, T> { + /// Downgrade a [write guard](RwLockWriteGuard) into a [read guard](RwLockReadGuard). + pub fn downgrade(this: Self) -> RwLockReadGuard<'a, T> { + cfg_if::cfg_if! { + if #[cfg(feature = "parking_lot")] { + RwLockReadGuard { guard: implementation::RwLockWriteGuard::downgrade(this.guard) } + } else { + let RwLockWriteGuard { guard, lock } = this; + + // FIXME(https://github.com/rust-lang/rust/issues/128203): Replace with `RwLockWriteGuard::downgrade` once stable. + // This implementation allows for a different thread to "steal" the lock in-between the drop and the read. + // Ideally, `downgrade` should hold the lock the entire time, maintaining uninterrupted custody. + drop(guard); + lock.read() + } + } + } +} + +impl fmt::Debug for RwLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl ops::Deref for RwLockReadGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.guard.deref() + } +} + +impl ops::Deref for RwLockWriteGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.guard.deref() + } +} + +impl ops::DerefMut for RwLockWriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.guard.deref_mut() + } +}