Skip to main content

rlx_wgpu/
device.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! wgpu device discovery + capabilities.
17//!
18//! [`wgpu_device`] returns a process-global singleton. [`select_vulkan_backend`]
19//! routes subsequent calls to a Vulkan-only instance (for [`Device::Vulkan`]).
20
21use std::sync::OnceLock;
22#[cfg(not(target_arch = "wasm32"))]
23use std::sync::atomic::{AtomicU8, Ordering};
24
25#[cfg(not(target_arch = "wasm32"))]
26const PREF_DEFAULT: u8 = 0;
27#[cfg(not(target_arch = "wasm32"))]
28const PREF_VULKAN: u8 = 1;
29
30#[cfg(not(target_arch = "wasm32"))]
31static BACKEND_PREF: AtomicU8 = AtomicU8::new(PREF_DEFAULT);
32
33/// Detected wgpu adapter + device. We hold them together because
34/// every command submission needs both the device (for encoding) and
35/// the queue (for committing).
36pub struct WgpuDevice {
37    pub instance: wgpu::Instance,
38    pub adapter: wgpu::Adapter,
39    pub device: wgpu::Device,
40    pub queue: wgpu::Queue,
41    pub name: String,
42    pub backend: wgpu::Backend,
43}
44
45/// Pick the subset of adapter features we want to enable on the device.
46/// Shared by the native (blocking) and wasm (async) constructors so the
47/// capability policy stays in one place.
48fn required_features_for(adapter: &wgpu::Adapter) -> wgpu::Features {
49    let adapter_feats = adapter.features();
50    let mut required_features = wgpu::Features::empty();
51    if adapter_feats.contains(wgpu::Features::SHADER_F16) {
52        required_features |= wgpu::Features::SHADER_F16;
53    }
54    if adapter_feats.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX) {
55        required_features |= wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX;
56    }
57    if adapter_feats.contains(wgpu::Features::SUBGROUP) {
58        required_features |= wgpu::Features::SUBGROUP;
59    }
60    required_features
61}
62
63fn make_instance(backends: wgpu::Backends) -> wgpu::Instance {
64    wgpu::Instance::new(wgpu::InstanceDescriptor {
65        backends,
66        flags: wgpu::InstanceFlags::default(),
67        backend_options: wgpu::BackendOptions::default(),
68        memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
69        display: None,
70    })
71}
72
73const ADAPTER_OPTIONS: wgpu::RequestAdapterOptions = wgpu::RequestAdapterOptions {
74    power_preference: wgpu::PowerPreference::HighPerformance,
75    compatible_surface: None,
76    force_fallback_adapter: false,
77    apply_limit_buckets: false,
78};
79
80fn device_descriptor(
81    required_features: wgpu::Features,
82    limits: wgpu::Limits,
83) -> wgpu::DeviceDescriptor<'static> {
84    wgpu::DeviceDescriptor {
85        label: Some("rlx-wgpu device"),
86        required_features,
87        required_limits: limits,
88        memory_hints: wgpu::MemoryHints::Performance,
89        experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
90        trace: wgpu::Trace::Off,
91    }
92}
93
94impl WgpuDevice {
95    /// Native: block on adapter + device discovery via `pollster`.
96    #[cfg(not(target_arch = "wasm32"))]
97    fn new_with_backends(backends: wgpu::Backends) -> Option<Self> {
98        let instance = make_instance(backends);
99        let adapter = pollster::block_on(instance.request_adapter(&ADAPTER_OPTIONS)).ok()?;
100
101        let info = adapter.get_info();
102        let limits = adapter.limits();
103        let required_features = required_features_for(&adapter);
104
105        let (device, queue) = match pollster::block_on(
106            adapter.request_device(&device_descriptor(required_features, limits)),
107        ) {
108            Ok(p) => p,
109            Err(e) => {
110                eprintln!("rlx-wgpu request_device failed: {e}");
111                return None;
112            }
113        };
114
115        Some(Self {
116            instance,
117            adapter,
118            device,
119            queue,
120            name: info.name,
121            backend: info.backend,
122        })
123    }
124
125    /// WASM: the browser cannot block its event loop, so adapter + device
126    /// discovery is genuinely async — callers `.await` [`init_wgpu_device`]
127    /// once at startup, after which [`wgpu_device`] returns synchronously.
128    #[cfg(target_arch = "wasm32")]
129    async fn new_with_backends_async(backends: wgpu::Backends) -> Option<Self> {
130        let instance = make_instance(backends);
131        let adapter = instance.request_adapter(&ADAPTER_OPTIONS).await.ok()?;
132
133        let info = adapter.get_info();
134        let limits = adapter.limits();
135        let required_features = required_features_for(&adapter);
136
137        let (device, queue) = adapter
138            .request_device(&device_descriptor(required_features, limits))
139            .await
140            .ok()?;
141
142        Some(Self {
143            instance,
144            adapter,
145            device,
146            queue,
147            name: info.name,
148            backend: info.backend,
149        })
150    }
151
152    #[cfg(not(target_arch = "wasm32"))]
153    fn new_default() -> Option<Self> {
154        Self::new_with_backends(default_backends())
155    }
156}
157
158#[cfg(not(target_arch = "wasm32"))]
159fn default_backends() -> wgpu::Backends {
160    if let Some(b) = wgpu::Backends::from_env() {
161        return b;
162    }
163    #[cfg(target_os = "windows")]
164    {
165        // Native DX12 first on Windows MSVC; Vulkan remains as fallback.
166        wgpu::Backends::DX12 | wgpu::Backends::VULKAN
167    }
168    #[cfg(target_os = "linux")]
169    {
170        // WSL Ubuntu + native Linux: prefer Vulkan (NVIDIA passthrough).
171        wgpu::Backends::VULKAN
172    }
173    #[cfg(target_vendor = "apple")]
174    {
175        // Every Apple platform (macOS, iOS, tvOS, visionOS): native Metal,
176        // plus MoltenVK-translated Vulkan where the user installed it.
177        wgpu::Backends::METAL | wgpu::Backends::VULKAN
178    }
179    #[cfg(not(any(target_os = "windows", target_os = "linux", target_vendor = "apple")))]
180    {
181        wgpu::Backends::all()
182    }
183}
184
185// SAFETY: wgpu's Device + Queue are documented thread-safe.
186unsafe impl Send for WgpuDevice {}
187unsafe impl Sync for WgpuDevice {}
188
189#[cfg(not(target_arch = "wasm32"))]
190fn default_device() -> Option<&'static WgpuDevice> {
191    static DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
192    DEVICE.get_or_init(WgpuDevice::new_default).as_ref()
193}
194
195#[cfg(not(target_arch = "wasm32"))]
196fn vulkan_device() -> Option<&'static WgpuDevice> {
197    static DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
198    DEVICE
199        .get_or_init(|| WgpuDevice::new_with_backends(wgpu::Backends::VULKAN))
200        .as_ref()
201}
202
203/// Prefer the Vulkan-only wgpu instance for [`Device::Vulkan`] sessions.
204/// Call before the first [`wgpu_device`] use in that process (or use
205/// `Device::Vulkan` via the runtime registry, which calls this).
206#[cfg(not(target_arch = "wasm32"))]
207pub fn select_vulkan_backend() {
208    BACKEND_PREF.store(PREF_VULKAN, Ordering::SeqCst);
209}
210
211/// No Vulkan path in the browser — the only wasm adapter is WebGPU.
212#[cfg(target_arch = "wasm32")]
213pub fn select_vulkan_backend() {}
214
215/// True when a Vulkan adapter is reachable (MoltenVK on macOS, native on Linux/Windows).
216#[cfg(not(target_arch = "wasm32"))]
217pub fn is_vulkan_available() -> bool {
218    vulkan_device().is_some()
219}
220
221/// No Vulkan in the browser.
222#[cfg(target_arch = "wasm32")]
223pub fn is_vulkan_available() -> bool {
224    false
225}
226
227/// Get or initialize the global wgpu device singleton. Returns None
228/// on systems with no compatible adapter.
229#[cfg(not(target_arch = "wasm32"))]
230pub fn wgpu_device() -> Option<&'static WgpuDevice> {
231    if BACKEND_PREF.load(Ordering::SeqCst) == PREF_VULKAN {
232        vulkan_device()
233    } else {
234        default_device()
235    }
236}
237
238// ── WASM device singleton ──────────────────────────────────────────────
239//
240// The browser cannot block on adapter/device discovery, so the singleton
241// is populated asynchronously by [`init_wgpu_device`] (called once at
242// startup from JS via `wasm-bindgen-futures`). After that completes,
243// [`wgpu_device`] returns the cached device synchronously, so the rest of
244// the (synchronous) backend is unchanged.
245#[cfg(target_arch = "wasm32")]
246static WASM_DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
247
248/// Initialize the WebGPU device. Idempotent; returns `true` when a device
249/// is available. Must be awaited before any [`wgpu_device`] call on wasm.
250#[cfg(target_arch = "wasm32")]
251pub async fn init_wgpu_device() -> bool {
252    if let Some(slot) = WASM_DEVICE.get() {
253        return slot.is_some();
254    }
255    let dev = WgpuDevice::new_with_backends_async(wgpu::Backends::BROWSER_WEBGPU).await;
256    let available = dev.is_some();
257    let _ = WASM_DEVICE.set(dev);
258    available
259}
260
261/// Get the wgpu device. On wasm this returns `None` until
262/// [`init_wgpu_device`] has been awaited.
263#[cfg(target_arch = "wasm32")]
264pub fn wgpu_device() -> Option<&'static WgpuDevice> {
265    WASM_DEVICE.get().and_then(|d| d.as_ref())
266}
267
268/// Adapter name for calibration cache keys.
269pub fn adapter_name() -> Option<String> {
270    wgpu_device().map(|d| d.name.clone())
271}
272
273/// True on Vulkan or DX12 — discrete GPU cooperative-matrix backends.
274pub fn coop_discrete_backend() -> bool {
275    wgpu_device()
276        .map(|d| matches!(d.backend, wgpu::Backend::Vulkan | wgpu::Backend::Dx12))
277        .unwrap_or(false)
278}
279
280/// True when the adapter reports 8×8×8 f32 cooperative-matrix support
281/// (required for `matmul_coop_f32_portable` on Vulkan/DX12).
282pub fn coop_f32_8x8_supported() -> bool {
283    let dev = match wgpu_device() {
284        Some(d) => d,
285        None => return false,
286    };
287    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
288        p.m_size == 8
289            && p.n_size == 8
290            && p.k_size == 8
291            && p.ab_type == wgpu::CooperativeScalarType::F32
292            && p.cr_type == wgpu::CooperativeScalarType::F32
293    })
294}
295
296/// True when the adapter reports 16×16×16 f16 cooperative-matrix support
297/// (NVIDIA / AMD tensor-core path on Vulkan/DX12).
298pub fn coop_f16_16x16_supported() -> bool {
299    let dev = match wgpu_device() {
300        Some(d) => d,
301        None => return false,
302    };
303    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
304        p.m_size == 16
305            && p.n_size == 16
306            && p.k_size == 16
307            && p.ab_type == wgpu::CooperativeScalarType::F16
308            && (p.cr_type == wgpu::CooperativeScalarType::F16
309                || p.cr_type == wgpu::CooperativeScalarType::F32)
310    })
311}
312
313/// True when f16 operands with f32 accumulator are available (preferred on RTX).
314pub fn coop_f16_16x16_f32_acc_supported() -> bool {
315    let dev = match wgpu_device() {
316        Some(d) => d,
317        None => return false,
318    };
319    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
320        p.m_size == 16
321            && p.n_size == 16
322            && p.k_size == 16
323            && p.ab_type == wgpu::CooperativeScalarType::F16
324            && p.cr_type == wgpu::CooperativeScalarType::F32
325    })
326}