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};
78
79fn device_descriptor(
80    required_features: wgpu::Features,
81    limits: wgpu::Limits,
82) -> wgpu::DeviceDescriptor<'static> {
83    wgpu::DeviceDescriptor {
84        label: Some("rlx-wgpu device"),
85        required_features,
86        required_limits: limits,
87        memory_hints: wgpu::MemoryHints::Performance,
88        experimental_features: unsafe { wgpu::ExperimentalFeatures::enabled() },
89        trace: wgpu::Trace::Off,
90    }
91}
92
93impl WgpuDevice {
94    /// Native: block on adapter + device discovery via `pollster`.
95    #[cfg(not(target_arch = "wasm32"))]
96    fn new_with_backends(backends: wgpu::Backends) -> Option<Self> {
97        let instance = make_instance(backends);
98        let adapter = pollster::block_on(instance.request_adapter(&ADAPTER_OPTIONS)).ok()?;
99
100        let info = adapter.get_info();
101        let limits = adapter.limits();
102        let required_features = required_features_for(&adapter);
103
104        let (device, queue) = match pollster::block_on(
105            adapter.request_device(&device_descriptor(required_features, limits)),
106        ) {
107            Ok(p) => p,
108            Err(e) => {
109                eprintln!("rlx-wgpu request_device failed: {e}");
110                return None;
111            }
112        };
113
114        Some(Self {
115            instance,
116            adapter,
117            device,
118            queue,
119            name: info.name,
120            backend: info.backend,
121        })
122    }
123
124    /// WASM: the browser cannot block its event loop, so adapter + device
125    /// discovery is genuinely async — callers `.await` [`init_wgpu_device`]
126    /// once at startup, after which [`wgpu_device`] returns synchronously.
127    #[cfg(target_arch = "wasm32")]
128    async fn new_with_backends_async(backends: wgpu::Backends) -> Option<Self> {
129        let instance = make_instance(backends);
130        let adapter = instance.request_adapter(&ADAPTER_OPTIONS).await.ok()?;
131
132        let info = adapter.get_info();
133        let limits = adapter.limits();
134        let required_features = required_features_for(&adapter);
135
136        let (device, queue) = adapter
137            .request_device(&device_descriptor(required_features, limits))
138            .await
139            .ok()?;
140
141        Some(Self {
142            instance,
143            adapter,
144            device,
145            queue,
146            name: info.name,
147            backend: info.backend,
148        })
149    }
150
151    #[cfg(not(target_arch = "wasm32"))]
152    fn new_default() -> Option<Self> {
153        Self::new_with_backends(default_backends())
154    }
155}
156
157#[cfg(not(target_arch = "wasm32"))]
158fn default_backends() -> wgpu::Backends {
159    if let Some(b) = wgpu::Backends::from_env() {
160        return b;
161    }
162    #[cfg(target_os = "windows")]
163    {
164        // Native DX12 first on Windows MSVC; Vulkan remains as fallback.
165        wgpu::Backends::DX12 | wgpu::Backends::VULKAN
166    }
167    #[cfg(target_os = "linux")]
168    {
169        // WSL Ubuntu + native Linux: prefer Vulkan (NVIDIA passthrough).
170        wgpu::Backends::VULKAN
171    }
172    #[cfg(target_vendor = "apple")]
173    {
174        // Every Apple platform (macOS, iOS, tvOS, visionOS): native Metal,
175        // plus MoltenVK-translated Vulkan where the user installed it.
176        wgpu::Backends::METAL | wgpu::Backends::VULKAN
177    }
178    #[cfg(not(any(target_os = "windows", target_os = "linux", target_vendor = "apple")))]
179    {
180        wgpu::Backends::all()
181    }
182}
183
184// SAFETY: wgpu's Device + Queue are documented thread-safe.
185unsafe impl Send for WgpuDevice {}
186unsafe impl Sync for WgpuDevice {}
187
188#[cfg(not(target_arch = "wasm32"))]
189fn default_device() -> Option<&'static WgpuDevice> {
190    static DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
191    DEVICE.get_or_init(WgpuDevice::new_default).as_ref()
192}
193
194#[cfg(not(target_arch = "wasm32"))]
195fn vulkan_device() -> Option<&'static WgpuDevice> {
196    static DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
197    DEVICE
198        .get_or_init(|| WgpuDevice::new_with_backends(wgpu::Backends::VULKAN))
199        .as_ref()
200}
201
202/// Prefer the Vulkan-only wgpu instance for [`Device::Vulkan`] sessions.
203/// Call before the first [`wgpu_device`] use in that process (or use
204/// `Device::Vulkan` via the runtime registry, which calls this).
205#[cfg(not(target_arch = "wasm32"))]
206pub fn select_vulkan_backend() {
207    BACKEND_PREF.store(PREF_VULKAN, Ordering::SeqCst);
208}
209
210/// No Vulkan path in the browser — the only wasm adapter is WebGPU.
211#[cfg(target_arch = "wasm32")]
212pub fn select_vulkan_backend() {}
213
214/// True when a Vulkan adapter is reachable (MoltenVK on macOS, native on Linux/Windows).
215#[cfg(not(target_arch = "wasm32"))]
216pub fn is_vulkan_available() -> bool {
217    vulkan_device().is_some()
218}
219
220/// No Vulkan in the browser.
221#[cfg(target_arch = "wasm32")]
222pub fn is_vulkan_available() -> bool {
223    false
224}
225
226/// Get or initialize the global wgpu device singleton. Returns None
227/// on systems with no compatible adapter.
228#[cfg(not(target_arch = "wasm32"))]
229pub fn wgpu_device() -> Option<&'static WgpuDevice> {
230    if BACKEND_PREF.load(Ordering::SeqCst) == PREF_VULKAN {
231        vulkan_device()
232    } else {
233        default_device()
234    }
235}
236
237// ── WASM device singleton ──────────────────────────────────────────────
238//
239// The browser cannot block on adapter/device discovery, so the singleton
240// is populated asynchronously by [`init_wgpu_device`] (called once at
241// startup from JS via `wasm-bindgen-futures`). After that completes,
242// [`wgpu_device`] returns the cached device synchronously, so the rest of
243// the (synchronous) backend is unchanged.
244#[cfg(target_arch = "wasm32")]
245static WASM_DEVICE: OnceLock<Option<WgpuDevice>> = OnceLock::new();
246
247/// Initialize the WebGPU device. Idempotent; returns `true` when a device
248/// is available. Must be awaited before any [`wgpu_device`] call on wasm.
249#[cfg(target_arch = "wasm32")]
250pub async fn init_wgpu_device() -> bool {
251    if let Some(slot) = WASM_DEVICE.get() {
252        return slot.is_some();
253    }
254    let dev = WgpuDevice::new_with_backends_async(wgpu::Backends::BROWSER_WEBGPU).await;
255    let available = dev.is_some();
256    let _ = WASM_DEVICE.set(dev);
257    available
258}
259
260/// Get the wgpu device. On wasm this returns `None` until
261/// [`init_wgpu_device`] has been awaited.
262#[cfg(target_arch = "wasm32")]
263pub fn wgpu_device() -> Option<&'static WgpuDevice> {
264    WASM_DEVICE.get().and_then(|d| d.as_ref())
265}
266
267/// Adapter name for calibration cache keys.
268pub fn adapter_name() -> Option<String> {
269    wgpu_device().map(|d| d.name.clone())
270}
271
272/// True on Vulkan or DX12 — discrete GPU cooperative-matrix backends.
273pub fn coop_discrete_backend() -> bool {
274    wgpu_device()
275        .map(|d| matches!(d.backend, wgpu::Backend::Vulkan | wgpu::Backend::Dx12))
276        .unwrap_or(false)
277}
278
279/// True when the adapter reports 8×8×8 f32 cooperative-matrix support
280/// (required for `matmul_coop_f32_portable` on Vulkan/DX12).
281pub fn coop_f32_8x8_supported() -> bool {
282    let dev = match wgpu_device() {
283        Some(d) => d,
284        None => return false,
285    };
286    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
287        p.m_size == 8
288            && p.n_size == 8
289            && p.k_size == 8
290            && p.ab_type == wgpu::CooperativeScalarType::F32
291            && p.cr_type == wgpu::CooperativeScalarType::F32
292    })
293}
294
295/// True when the adapter reports 16×16×16 f16 cooperative-matrix support
296/// (NVIDIA / AMD tensor-core path on Vulkan/DX12).
297pub fn coop_f16_16x16_supported() -> bool {
298    let dev = match wgpu_device() {
299        Some(d) => d,
300        None => return false,
301    };
302    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
303        p.m_size == 16
304            && p.n_size == 16
305            && p.k_size == 16
306            && p.ab_type == wgpu::CooperativeScalarType::F16
307            && (p.cr_type == wgpu::CooperativeScalarType::F16
308                || p.cr_type == wgpu::CooperativeScalarType::F32)
309    })
310}
311
312/// True when f16 operands with f32 accumulator are available (preferred on RTX).
313pub fn coop_f16_16x16_f32_acc_supported() -> bool {
314    let dev = match wgpu_device() {
315        Some(d) => d,
316        None => return false,
317    };
318    dev.adapter.cooperative_matrix_properties().iter().any(|p| {
319        p.m_size == 16
320            && p.n_size == 16
321            && p.k_size == 16
322            && p.ab_type == wgpu::CooperativeScalarType::F16
323            && p.cr_type == wgpu::CooperativeScalarType::F32
324    })
325}