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/// Register the adapter this host negotiated. Replaces any previous binding.
53///
54/// Call after `requestAdapter()` / `requestDevice()` resolve. The `GPUDevice`
55/// itself stays with the host — this crate records what the adapter *is* so it
56/// appears in the device survey, and does not take ownership of the handle.
57pub fn bind_adapter(info: AdapterInfo) {
58    *BOUND.lock().unwrap_or_else(|e| e.into_inner()) = Some(info);
59}
60
61/// Drop the current binding. After this the backend reports unavailable again.
62pub fn unbind_adapter() {
63    *BOUND.lock().unwrap_or_else(|e| e.into_inner()) = None;
64}
65
66/// The currently bound adapter, if any.
67pub fn bound_adapter() -> Option<AdapterInfo> {
68    BOUND.lock().unwrap_or_else(|e| e.into_inner()).clone()
69}
70
71/// Whether a host has bound an adapter.
72pub fn is_available() -> bool {
73    BOUND
74        .lock()
75        .unwrap_or_else(|e| e.into_inner())
76        .as_ref()
77        .is_some_and(|a| !a.is_fallback)
78}
79
80/// The bound adapter as a one-element list, or empty. WebGPU exposes exactly
81/// one adapter per `requestAdapter` call; there is no multi-device survey.
82pub fn enumerate() -> Vec<AdapterInfo> {
83    match bound_adapter() {
84        Some(a) if !a.is_fallback => vec![a],
85        _ => Vec::new(),
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    /// Serialises the tests below: they share one process-wide binding.
94    fn with_clean_binding<T>(f: impl FnOnce() -> T) -> T {
95        static GUARD: Mutex<()> = Mutex::new(());
96        let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner());
97        unbind_adapter();
98        let out = f();
99        unbind_adapter();
100        out
101    }
102
103    fn sample() -> AdapterInfo {
104        AdapterInfo {
105            vendor: "nvidia".into(),
106            architecture: "ampere".into(),
107            shader_f16: true,
108            subgroups: true,
109            max_buffer_size: 1 << 31,
110            ..Default::default()
111        }
112    }
113
114    #[test]
115    fn unbound_is_unavailable_and_enumerates_empty() {
116        with_clean_binding(|| {
117            assert!(!is_available());
118            assert!(enumerate().is_empty());
119        });
120    }
121
122    #[test]
123    fn bound_adapter_round_trips() {
124        with_clean_binding(|| {
125            bind_adapter(sample());
126            assert!(is_available());
127            assert_eq!(enumerate(), vec![sample()]);
128            assert_eq!(bound_adapter(), Some(sample()));
129        });
130    }
131
132    #[test]
133    fn fallback_adapter_is_not_offered() {
134        with_clean_binding(|| {
135            bind_adapter(AdapterInfo {
136                is_fallback: true,
137                ..sample()
138            });
139            assert!(!is_available(), "software fallback must not be offered");
140            assert!(enumerate().is_empty());
141        });
142    }
143
144    #[test]
145    fn unbind_clears() {
146        with_clean_binding(|| {
147            bind_adapter(sample());
148            unbind_adapter();
149            assert!(!is_available());
150            assert_eq!(bound_adapter(), None);
151        });
152    }
153}