Skip to main content

oxicuda_webgpu/
device.rs

1//! WebGPU device wrapper — owns the wgpu instance, adapter, device, and queue.
2
3use wgpu;
4
5use crate::error::{WebGpuError, WebGpuResult};
6
7/// A fully initialised WebGPU device together with its submit queue.
8///
9/// Created via [`WebGpuDevice::new`] which blocks the calling thread using
10/// [`pollster`] until the async device request completes.
11pub struct WebGpuDevice {
12    /// The wgpu instance used to enumerate adapters.
13    /// Kept alive to ensure the adapter and device remain valid.
14    #[allow(dead_code)]
15    pub(crate) instance: wgpu::Instance,
16    /// The selected GPU adapter.
17    /// Kept alive to ensure the device remains valid.
18    #[allow(dead_code)]
19    pub(crate) adapter: wgpu::Adapter,
20    /// The logical device (command encoder, buffer allocator, …).
21    pub(crate) device: wgpu::Device,
22    /// The queue for submitting command buffers.
23    pub(crate) queue: wgpu::Queue,
24    /// Human-readable adapter name for diagnostics.
25    pub adapter_name: String,
26    /// Whether the `SHADER_F16` feature was successfully enabled on the device.
27    /// Gates the FP16 GEMM path, whose WGSL declares `enable f16;`.
28    pub(crate) supports_f16: bool,
29}
30
31impl WebGpuDevice {
32    /// Create a WebGPU device by selecting the highest-performance adapter.
33    ///
34    /// Blocks the calling thread until the device is ready.
35    pub fn new() -> WebGpuResult<Self> {
36        pollster::block_on(Self::new_async())
37    }
38
39    async fn new_async() -> WebGpuResult<Self> {
40        // wgpu 29: `InstanceDescriptor` does not impl `Default`; use the
41        // provided constructor instead.
42        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
43
44        let adapter = instance
45            .request_adapter(&wgpu::RequestAdapterOptions {
46                power_preference: wgpu::PowerPreference::HighPerformance,
47                compatible_surface: None,
48                force_fallback_adapter: false,
49            })
50            .await
51            .map_err(|e| WebGpuError::DeviceRequest(e.to_string()))?;
52
53        let adapter_name = adapter.get_info().name.clone();
54
55        // Enable FP16 shader support when the adapter advertises it, so the
56        // `gemm_f16` path (whose WGSL declares `enable f16;`) validates instead
57        // of being rejected for a missing capability.  When the adapter lacks
58        // it we simply do not request it, and `gemm_f16` returns a typed
59        // `Unsupported` error rather than emitting an invalid module.
60        let supports_f16 = adapter.features().contains(wgpu::Features::SHADER_F16);
61        let required_features = if supports_f16 {
62            wgpu::Features::SHADER_F16
63        } else {
64            wgpu::Features::empty()
65        };
66
67        // `DeviceDescriptor` does implement `Default` in wgpu-types 29 so we
68        // can use struct-update syntax.
69        let (device, queue) = adapter
70            .request_device(&wgpu::DeviceDescriptor {
71                label: Some("oxicuda-webgpu"),
72                required_features,
73                required_limits: wgpu::Limits::default(),
74                memory_hints: wgpu::MemoryHints::default(),
75                ..Default::default()
76            })
77            .await
78            .map_err(|e| WebGpuError::DeviceRequest(e.to_string()))?;
79
80        Ok(Self {
81            instance,
82            adapter,
83            device,
84            queue,
85            adapter_name,
86            supports_f16,
87        })
88    }
89}
90
91impl std::fmt::Debug for WebGpuDevice {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "WebGpuDevice({})", self.adapter_name)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    /// Confirm that WebGpuDevice::new() does not panic — it may return Ok or Err
102    /// depending on whether a GPU is available in the test environment.
103    #[test]
104    fn webgpu_device_new_graceful() {
105        match WebGpuDevice::new() {
106            Ok(dev) => {
107                assert!(!dev.adapter_name.is_empty());
108                // Debug impl should not panic.
109                let _ = format!("{dev:?}");
110            }
111            Err(WebGpuError::NoAdapter) => {
112                // Expected on headless CI without a GPU.
113            }
114            Err(e) => {
115                // Any other error is also acceptable; we just must not panic.
116                let _ = format!("device init error (non-fatal): {e}");
117            }
118        }
119    }
120}