Skip to main content

ironaccelerator_webgpu/
drv.rs

1//! Host-bound WebGPU adapter registration.
2//!
3//! WebGPU has no synchronous enumeration. `navigator.gpu.requestAdapter()` and
4//! `GPUAdapter.requestDevice()` both return promises, and [`Backend`] is a
5//! synchronous trait — so this crate cannot negotiate an adapter itself
6//! without blocking, which is not permitted on a browser's main thread.
7//!
8//! The host therefore does the async negotiation and registers the result with
9//! [`bind_adapter`]. Everything downstream — [`enumerate`], the [`Backend`]
10//! impl, `Runtime::devices()` — reads what was bound. Until a host binds, the
11//! backend honestly reports unavailable.
12//!
13//! [`Backend`]: ironaccelerator_core::Backend
14
15use std::sync::Mutex;
16
17/// What a host learned about its adapter, in WebGPU's own vocabulary.
18///
19/// The fields mirror `GPUAdapterInfo`, `GPUSupportedLimits`, and the two
20/// optional `GPUFeatureName`s that matter for compute. WebGPU deliberately
21/// does not expose PCI vendor/device IDs — `vendor` and `architecture` are
22/// lowercase strings such as `"nvidia"` / `"ampere"`, and may be empty when
23/// the browser chooses to withhold them for fingerprinting reasons.
24#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct AdapterInfo {
26    /// `GPUAdapterInfo.vendor`, e.g. `"nvidia"`, `"amd"`, `"intel"`, `"apple"`.
27    pub vendor: String,
28    /// `GPUAdapterInfo.architecture`, e.g. `"ampere"`, `"rdna-3"`.
29    pub architecture: String,
30    /// `GPUAdapterInfo.device` — often empty in browsers.
31    pub device: String,
32    /// `GPUAdapterInfo.description` — often empty in browsers.
33    pub description: String,
34    /// `GPUAdapter.isFallbackAdapter` — a software rasteriser.
35    pub is_fallback: bool,
36    /// The `"shader-f16"` feature was granted on the device.
37    pub shader_f16: bool,
38    /// The `"subgroups"` feature was granted on the device.
39    pub subgroups: bool,
40    /// `GPUSupportedLimits.maxBufferSize`.
41    pub max_buffer_size: u64,
42    /// `GPUSupportedLimits.maxStorageBufferBindingSize`.
43    pub max_storage_buffer_binding_size: u64,
44    /// `GPUSupportedLimits.maxComputeWorkgroupSizeX`.
45    pub max_compute_workgroup_size_x: u32,
46    /// `GPUSupportedLimits.maxComputeInvocationsPerWorkgroup`.
47    pub max_compute_invocations_per_workgroup: u32,
48}
49
50static BOUND: Mutex<Option<AdapterInfo>> = Mutex::new(None);
51
52/// Test-only: a single lock shared by every test in this crate that touches the
53/// process-global [`BOUND`] binding. The `drv` and `backend` test modules run
54/// in one binary across parallel threads and both mutate `BOUND`; without a
55/// *shared* guard each module would serialise only against itself and the two
56/// would race. Every such test takes this lock first.
57#[cfg(test)]
58pub(crate) static TEST_BINDING_LOCK: Mutex<()> = Mutex::new(());
59
60/// Register the adapter this host negotiated. Replaces any previous binding.
61///
62/// Call after `requestAdapter()` / `requestDevice()` resolve. The `GPUDevice`
63/// itself stays with the host — this crate records what the adapter *is* so it
64/// appears in the device survey, and does not take ownership of the handle.
65pub fn bind_adapter(info: AdapterInfo) {
66    *BOUND.lock().unwrap_or_else(|e| e.into_inner()) = Some(info);
67}
68
69/// Drop the current binding. After this the backend reports unavailable again.
70pub fn unbind_adapter() {
71    *BOUND.lock().unwrap_or_else(|e| e.into_inner()) = None;
72}
73
74/// The currently bound adapter, if any.
75pub fn bound_adapter() -> Option<AdapterInfo> {
76    BOUND.lock().unwrap_or_else(|e| e.into_inner()).clone()
77}
78
79/// Whether a host has bound an adapter.
80pub fn is_available() -> bool {
81    BOUND
82        .lock()
83        .unwrap_or_else(|e| e.into_inner())
84        .as_ref()
85        .is_some_and(|a| !a.is_fallback)
86}
87
88/// The bound adapter as a one-element list, or empty. WebGPU exposes exactly
89/// one adapter per `requestAdapter` call; there is no multi-device survey.
90pub fn enumerate() -> Vec<AdapterInfo> {
91    match bound_adapter() {
92        Some(a) if !a.is_fallback => vec![a],
93        _ => Vec::new(),
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    /// Serialises the tests below against every other test in the crate that
102    /// touches the process-wide binding, via the shared [`TEST_BINDING_LOCK`].
103    fn with_clean_binding<T>(f: impl FnOnce() -> T) -> T {
104        let _g = TEST_BINDING_LOCK.lock().unwrap_or_else(|e| e.into_inner());
105        unbind_adapter();
106        let out = f();
107        unbind_adapter();
108        out
109    }
110
111    fn sample() -> AdapterInfo {
112        AdapterInfo {
113            vendor: "nvidia".into(),
114            architecture: "ampere".into(),
115            shader_f16: true,
116            subgroups: true,
117            max_buffer_size: 1 << 31,
118            ..Default::default()
119        }
120    }
121
122    #[test]
123    fn unbound_is_unavailable_and_enumerates_empty() {
124        with_clean_binding(|| {
125            assert!(!is_available());
126            assert!(enumerate().is_empty());
127        });
128    }
129
130    #[test]
131    fn bound_adapter_round_trips() {
132        with_clean_binding(|| {
133            bind_adapter(sample());
134            assert!(is_available());
135            assert_eq!(enumerate(), vec![sample()]);
136            assert_eq!(bound_adapter(), Some(sample()));
137        });
138    }
139
140    #[test]
141    fn fallback_adapter_is_not_offered() {
142        with_clean_binding(|| {
143            bind_adapter(AdapterInfo {
144                is_fallback: true,
145                ..sample()
146            });
147            assert!(!is_available(), "software fallback must not be offered");
148            assert!(enumerate().is_empty());
149        });
150    }
151
152    #[test]
153    fn unbind_clears() {
154        with_clean_binding(|| {
155            bind_adapter(sample());
156            unbind_adapter();
157            assert!(!is_available());
158            assert_eq!(bound_adapter(), None);
159        });
160    }
161}