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}