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}