Skip to content

[wgpu-types]: Add cross-platform Mutex and RwLock #7830

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
21 changes: 21 additions & 0 deletions wgpu-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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 }
Expand Down
1 change: 1 addition & 0 deletions wgpu-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod env;
mod features;
pub mod instance;
pub mod math;
pub mod sync;
mod transfers;

pub use counters::*;
Expand Down
237 changes: 237 additions & 0 deletions wgpu-types/src/sync.rs
Original file line number Diff line number Diff line change
@@ -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<T>(mut f: impl FnMut() -> Option<T>) -> 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<T>(implementation::Mutex<T>);

/// 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<T> Mutex<T> {
/// Create a new [`Mutex`].
pub fn new(value: T) -> Mutex<T> {
Mutex(implementation::Mutex::new(value))
}

/// Lock the provided [`Mutex`], allowing reading and/or writing.
pub fn lock(&self) -> MutexGuard<T> {
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<T> ops::Deref for MutexGuard<'_, T> {
type Target = T;

fn deref(&self) -> &Self::Target {
self.0.deref()
}
}

impl<T> ops::DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.0.deref_mut()
}
}

impl<T: fmt::Debug> fmt::Debug for Mutex<T> {
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<T>(implementation::RwLock<T>);

/// 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<T>,
}

impl<T> RwLock<T> {
/// Create a new [`RwLock`].
pub fn new(value: T) -> RwLock<T> {
RwLock(implementation::RwLock::new(value))
}

/// Read from the provided [`RwLock`].
pub fn read(&self) -> RwLockReadGuard<T> {
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<T> {
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<T: fmt::Debug> fmt::Debug for RwLock<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl<T> ops::Deref for RwLockReadGuard<'_, T> {
type Target = T;

fn deref(&self) -> &Self::Target {
self.guard.deref()
}
}

impl<T> ops::Deref for RwLockWriteGuard<'_, T> {
type Target = T;

fn deref(&self) -> &Self::Target {
self.guard.deref()
}
}

impl<T> ops::DerefMut for RwLockWriteGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.guard.deref_mut()
}
}
Loading