Skip to main content

alpha2_gpu/
acquisition.rs

1//! One device acquisition at a time in a process, for test harnesses.
2//!
3//! Parallel test threads that create instances, enumerate adapters and open devices at once can
4//! crash some Vulkan loaders and the NVIDIA EGL driver. Hosts acquire once on one thread, so the
5//! `serial-acquisition` feature (enabled by test dev-dependencies) serializes acquisitions only
6//! in tests. Without the feature the guard is free.
7//!
8//! Unloading drivers also races: a driver can leave per-thread data whose destructor lives in
9//! the unloaded library, so a thread exiting while the last instance drops can crash. The first
10//! turn therefore creates an instance on the first product backend that lives until process
11//! exit, keeping drivers and layers loaded. The instance builder keeps one more per backend set
12//! not yet covered, so GL/EGL libraries load only in processes that use GL.
13
14/// While alive, keeps other threads out of acquisitions guarded by [`serialize_acquisition`].
15/// The holding thread may re-enter.
16#[must_use = "acquisitions are serialized only while the guard lives"]
17pub struct AcquisitionGuard {
18    #[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
19    _held: held::Held,
20}
21
22impl std::fmt::Debug for AcquisitionGuard {
23    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        formatter.write_str("AcquisitionGuard")
25    }
26}
27
28/// Waits until no other thread holds an [`AcquisitionGuard`] and returns one.
29///
30/// Hold it while creating a wgpu instance, Vulkan instance or device outside
31/// [`crate::GpuContext`]'s constructors. Without the `serial-acquisition` feature it returns at
32/// once.
33pub fn serialize_acquisition() -> AcquisitionGuard {
34    AcquisitionGuard {
35        #[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
36        _held: held::Held::take(),
37    }
38}
39
40#[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
41mod held {
42    use std::sync::{Condvar, Mutex, PoisonError};
43    use std::thread::ThreadId;
44
45    static TURN: Mutex<Option<(ThreadId, usize)>> = Mutex::new(None);
46    static FREED: Condvar = Condvar::new();
47
48    pub(super) struct Held;
49
50    impl Held {
51        pub(super) fn take() -> Self {
52            let me = std::thread::current().id();
53            let mut turn = TURN.lock().unwrap_or_else(PoisonError::into_inner);
54            loop {
55                match &mut *turn {
56                    None => {
57                        *turn = Some((me, 1));
58                        break;
59                    }
60                    Some((owner, count)) if *owner == me => {
61                        *count += 1;
62                        return Self;
63                    }
64                    Some(_) => turn = FREED.wait(turn).unwrap_or_else(PoisonError::into_inner),
65                }
66            }
67            drop(turn);
68            // Keep a process-lifetime instance, created before the caller's own.
69            crate::instance::keep(crate::product_backends()[0]);
70            Self
71        }
72    }
73
74    impl Drop for Held {
75        fn drop(&mut self) {
76            let mut turn = TURN.lock().unwrap_or_else(PoisonError::into_inner);
77            if let Some((_, count)) = &mut *turn {
78                *count -= 1;
79                if *count == 0 {
80                    *turn = None;
81                    FREED.notify_all();
82                }
83            }
84        }
85    }
86}
87
88#[cfg(all(test, feature = "serial-acquisition", not(target_arch = "wasm32")))]
89mod tests {
90    use std::sync::Arc;
91    use std::sync::atomic::{AtomicUsize, Ordering};
92
93    use super::serialize_acquisition;
94
95    #[test]
96    fn one_thread_holds_the_turn_at_a_time_and_may_take_it_again() {
97        let inside = Arc::new(AtomicUsize::new(0));
98        let threads: Vec<_> = (0..8)
99            .map(|_| {
100                let inside = Arc::clone(&inside);
101                std::thread::spawn(move || {
102                    for _ in 0..50 {
103                        let _outer = serialize_acquisition();
104                        let _inner = serialize_acquisition();
105                        assert_eq!(inside.fetch_add(1, Ordering::SeqCst), 0);
106                        std::thread::yield_now();
107                        inside.fetch_sub(1, Ordering::SeqCst);
108                    }
109                })
110            })
111            .collect();
112        for thread in threads {
113            thread.join().expect("no two threads held the turn");
114        }
115    }
116}