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