Skip to main content

ironaccelerator_webgpu/
backend.rs

1//! WebGPU `Backend` impl over the host-bound adapter.
2
3use ironaccelerator_core::{
4    Backend, BackendKind, Capability, CapabilityFlags, ComputeTier, DeviceDescriptor, DeviceId,
5    Error, Result, Vendor,
6};
7
8use crate::drv::AdapterInfo;
9
10/// WebGPU backend handle; zero-sized, see [`WEBGPU_BACKEND`].
11pub struct WebGpuBackend;
12/// Process-wide WebGPU backend instance.
13pub static WEBGPU_BACKEND: WebGpuBackend = WebGpuBackend;
14
15impl Backend for WebGpuBackend {
16    fn kind(&self) -> BackendKind {
17        BackendKind::WebGpu
18    }
19
20    fn is_available(&self) -> bool {
21        crate::drv::is_available()
22    }
23
24    fn enumerate(&self) -> Result<Vec<DeviceDescriptor>> {
25        Ok(crate::drv::enumerate()
26            .into_iter()
27            .enumerate()
28            .map(|(i, info)| describe(i as u32, info))
29            .collect())
30    }
31
32    fn capabilities(&self, device: u32) -> Result<CapabilityFlags> {
33        if device != 0 {
34            return Err(Error::InvalidArgument(
35                "webgpu exposes a single bound adapter at ordinal 0",
36            ));
37        }
38        crate::drv::bound_adapter()
39            .map(|a| flags_for(&a))
40            .ok_or(Error::BackendUnavailable("webgpu: no adapter bound"))
41    }
42}
43
44/// Translate the granted WebGPU feature set into the common flag space.
45///
46/// WebGPU's compute surface is small by design, so this stays minimal: FP32 is
47/// guaranteed, FP16 only with `"shader-f16"`, and nothing above that is
48/// expressible. There is no INT8 dot product, no matrix engine, and no
49/// query for either.
50fn flags_for(a: &AdapterInfo) -> CapabilityFlags {
51    let mut flags = CapabilityFlags::FP32;
52    if a.shader_f16 {
53        flags |= CapabilityFlags::FP16;
54    }
55    flags
56}
57
58fn describe(ordinal: u32, a: AdapterInfo) -> DeviceDescriptor {
59    // WebGPU reports a lowercase vendor string, not a PCI ID.
60    let vendor = match a.vendor.as_str() {
61        "nvidia" => Vendor::Nvidia,
62        "amd" => Vendor::Amd,
63        "intel" => Vendor::Intel,
64        "apple" => Vendor::Apple,
65        "qualcomm" => Vendor::Qualcomm,
66        _ => Vendor::Other,
67    };
68
69    // The browser tells us nothing about discrete-vs-integrated or silicon
70    // class, and guessing from the vendor string would be fiction.
71    let tier = ComputeTier::Baseline;
72
73    let name = if !a.description.is_empty() {
74        a.description.clone()
75    } else if !a.device.is_empty() {
76        a.device.clone()
77    } else if !a.vendor.is_empty() {
78        a.vendor.clone()
79    } else {
80        "webgpu adapter".to_string()
81    };
82
83    let arch = if a.architecture.is_empty() {
84        "webgpu".to_string()
85    } else {
86        format!("webgpu-{}", a.architecture)
87    };
88
89    DeviceDescriptor {
90        id: DeviceId {
91            backend: BackendKind::WebGpu,
92            ordinal,
93        },
94        vendor,
95        name,
96        arch,
97        // Not a memory size: WebGPU never reports one. This is the largest
98        // single allocation the adapter will honour, which is the only
99        // capacity figure available.
100        total_memory_bytes: a.max_buffer_size,
101        multiprocessor_count: 0,
102        clock_khz: 0,
103        capability: Capability {
104            flags: flags_for(&a),
105            tier,
106            fp16_tflops: None,
107            fp8_tflops: None,
108            mem_bandwidth_gbs: None,
109        },
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::drv::{bind_adapter, unbind_adapter, TEST_BINDING_LOCK};
117
118    #[test]
119    fn unbound_backend_reports_unavailable() {
120        let _g = TEST_BINDING_LOCK.lock().unwrap_or_else(|e| e.into_inner());
121        unbind_adapter();
122        assert!(!WEBGPU_BACKEND.is_available());
123        assert!(WEBGPU_BACKEND.enumerate().unwrap().is_empty());
124        assert!(WEBGPU_BACKEND.capabilities(0).is_err());
125    }
126
127    #[test]
128    fn bound_adapter_is_described_and_capable() {
129        let _g = TEST_BINDING_LOCK.lock().unwrap_or_else(|e| e.into_inner());
130        bind_adapter(AdapterInfo {
131            vendor: "amd".into(),
132            architecture: "rdna-3".into(),
133            shader_f16: true,
134            max_buffer_size: 4096,
135            ..Default::default()
136        });
137
138        let devices = WEBGPU_BACKEND.enumerate().unwrap();
139        assert_eq!(devices.len(), 1);
140        assert_eq!(devices[0].vendor, Vendor::Amd);
141        assert_eq!(devices[0].arch, "webgpu-rdna-3");
142        assert_eq!(devices[0].name, "amd");
143        assert_eq!(devices[0].total_memory_bytes, 4096);
144
145        let flags = WEBGPU_BACKEND.capabilities(0).unwrap();
146        assert_eq!(flags, CapabilityFlags::FP32 | CapabilityFlags::FP16);
147        assert_eq!(flags, devices[0].capability.flags);
148
149        assert!(
150            WEBGPU_BACKEND.capabilities(1).is_err(),
151            "only ordinal 0 exists"
152        );
153        unbind_adapter();
154    }
155
156    #[test]
157    fn without_shader_f16_only_fp32() {
158        let _g = TEST_BINDING_LOCK.lock().unwrap_or_else(|e| e.into_inner());
159        bind_adapter(AdapterInfo {
160            vendor: "intel".into(),
161            ..Default::default()
162        });
163        assert_eq!(
164            WEBGPU_BACKEND.capabilities(0).unwrap(),
165            CapabilityFlags::FP32
166        );
167        unbind_adapter();
168    }
169}