Skip to main content

alpha2_gpu/
acquisition.rs

1//! One device acquisition at a time in a process, for test harnesses.
2//!
3//! A test binary runs its tests on parallel threads, and each may make its
4//! own instance, enumerate adapters and open a device at the same moment.
5//! Vulkan loaders and drivers do not all survive that: the 1.4.321 loader can
6//! crash naming a new device's objects while another thread enumerates
7//! physical devices, and the NVIDIA EGL driver can abort enumerating GL
8//! adapters beside another enumeration. A host makes its device once, on one
9//! thread, so only tests meet this; the `serial-acquisition` feature, which
10//! the workspace's test builds turn on as a dev-dependency, makes those
11//! acquisitions take turns. Without the feature the guard is free.
12//!
13//! Turns cover making instances and devices, not dropping them or the exit
14//! of a thread that used them, and those race too. When a process's last
15//! instance goes, the loader unloads the drivers, and a driver can leave
16//! per-thread data whose destructor lives in what was unloaded. The NVIDIA
17//! 580 Vulkan driver's EGL library does, so a test thread that exits as
18//! another test drops the last instance (or makes the next one, which loads
19//! and unloads every driver to list extensions) dies with SIGSEGV. The
20//! loader's own dispatch has crashed the same way while another thread
21//! unloaded a layer. So the first turn of a process also
22//! makes an instance the process keeps until it exits, as a host keeps its
23//! one: every driver and layer the tests' instances load stays loaded, and
24//! their unloads become reference counts. A driver that offers no adapter is
25//! still loaded and unloaded around each instance; it holds nothing a thread
26//! keeps.
27
28/// While alive, keeps every other thread of the process out of the
29/// acquisitions that take [`serialize_acquisition`]: instance creation,
30/// adapter requests and enumeration, and device creation. The thread holding
31/// one may take another (an acquisition inside another's callback).
32#[must_use = "acquisitions are serialized only while the guard lives"]
33pub struct AcquisitionGuard {
34    #[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
35    _held: held::Held,
36}
37
38impl std::fmt::Debug for AcquisitionGuard {
39    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        formatter.write_str("AcquisitionGuard")
41    }
42}
43
44/// Waits until no other thread holds an [`AcquisitionGuard`] and returns one.
45///
46/// Callers that make a wgpu instance, a Vulkan instance or a device outside
47/// [`crate::GpuContext`]'s constructors (which take it themselves) hold it
48/// for as long as that takes. Without the `serial-acquisition` feature it
49/// returns at once.
50pub fn serialize_acquisition() -> AcquisitionGuard {
51    AcquisitionGuard {
52        #[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
53        _held: held::Held::take(),
54    }
55}
56
57#[cfg(all(feature = "serial-acquisition", not(target_arch = "wasm32")))]
58mod held {
59    use std::sync::{Condvar, Mutex, OnceLock, PoisonError};
60    use std::thread::ThreadId;
61
62    /// The thread holding the turn and how many guards it holds.
63    static TURN: Mutex<Option<(ThreadId, usize)>> = Mutex::new(None);
64    static FREED: Condvar = Condvar::new();
65    /// The instance the first turn makes, with the backends and flags every
66    /// instance of the process gets from the environment, kept until the
67    /// process exits so that the drivers and layers stay loaded.
68    static KEPT: OnceLock<wgpu::Instance> = OnceLock::new();
69
70    pub(super) struct Held;
71
72    impl Held {
73        pub(super) fn take() -> Self {
74            let me = std::thread::current().id();
75            let mut turn = TURN.lock().unwrap_or_else(PoisonError::into_inner);
76            loop {
77                match &mut *turn {
78                    None => {
79                        *turn = Some((me, 1));
80                        break;
81                    }
82                    Some((owner, count)) if *owner == me => {
83                        *count += 1;
84                        return Self;
85                    }
86                    Some(_) => turn = FREED.wait(turn).unwrap_or_else(PoisonError::into_inner),
87                }
88            }
89            drop(turn);
90            // Made inside the turn, as any instance is, and before the caller makes its own.
91            KEPT.get_or_init(|| {
92                wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env())
93            });
94            Self
95        }
96    }
97
98    impl Drop for Held {
99        fn drop(&mut self) {
100            let mut turn = TURN.lock().unwrap_or_else(PoisonError::into_inner);
101            if let Some((_, count)) = &mut *turn {
102                *count -= 1;
103                if *count == 0 {
104                    *turn = None;
105                    FREED.notify_all();
106                }
107            }
108        }
109    }
110}
111
112#[cfg(all(test, feature = "serial-acquisition", not(target_arch = "wasm32")))]
113mod tests {
114    use std::sync::Arc;
115    use std::sync::atomic::{AtomicUsize, Ordering};
116
117    use super::serialize_acquisition;
118
119    #[test]
120    fn one_thread_holds_the_turn_at_a_time_and_may_take_it_again() {
121        let inside = Arc::new(AtomicUsize::new(0));
122        let threads: Vec<_> = (0..8)
123            .map(|_| {
124                let inside = Arc::clone(&inside);
125                std::thread::spawn(move || {
126                    for _ in 0..50 {
127                        let _outer = serialize_acquisition();
128                        let _inner = serialize_acquisition();
129                        assert_eq!(inside.fetch_add(1, Ordering::SeqCst), 0);
130                        std::thread::yield_now();
131                        inside.fetch_sub(1, Ordering::SeqCst);
132                    }
133                })
134            })
135            .collect();
136        for thread in threads {
137            thread.join().expect("no two threads held the turn");
138        }
139    }
140}