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}