Skip to main content

oximedia_gpu/
device.rs

1//! GPU device management and enumeration
2
3use crate::{GpuError, Result};
4use std::sync::Arc;
5use wgpu::{
6    Adapter, Device, DeviceDescriptor, Features, Instance, Limits, PowerPreference, Queue,
7    RequestAdapterOptions,
8};
9
10/// Information about a GPU device
11#[derive(Debug, Clone)]
12pub struct GpuDeviceInfo {
13    /// Device name
14    pub name: String,
15    /// Vendor ID
16    pub vendor: u32,
17    /// Device ID
18    pub device: u32,
19    /// Device type (discrete, integrated, virtual, cpu, unknown)
20    pub device_type: String,
21    /// Backend being used (Vulkan, Metal, DX12, etc.)
22    pub backend: String,
23}
24
25/// GPU device wrapper
26///
27/// This structure manages the WGPU device and queue, providing a safe
28/// interface for GPU operations.
29pub struct GpuDevice {
30    device: Arc<Device>,
31    queue: Arc<Queue>,
32    info: GpuDeviceInfo,
33    #[allow(dead_code)]
34    adapter: Adapter,
35    /// When true, this device was created via the fallback (software) adapter.
36    /// GPU-dependent operations will return `GpuError::NotSupported`.
37    pub is_fallback: bool,
38}
39
40impl GpuDevice {
41    /// Create a new GPU device
42    ///
43    /// # Arguments
44    ///
45    /// * `device_index` - Optional device index for multi-GPU selection
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if no suitable adapter is found or device request fails.
50    pub fn new(device_index: Option<usize>) -> Result<Self> {
51        let instance = Self::create_instance();
52        let adapter = pollster::block_on(Self::select_adapter(&instance, device_index))?;
53
54        let info = Self::adapter_info(&adapter);
55
56        let (device, queue) = pollster::block_on(Self::request_device(&adapter))?;
57
58        Ok(Self {
59            device: Arc::new(device),
60            queue: Arc::new(queue),
61            info,
62            adapter,
63            is_fallback: false,
64        })
65    }
66
67    /// Create a CPU-only fallback device using the wgpu software (Vulkan Portability /
68    /// force_fallback_adapter) path.
69    ///
70    /// Returns `Err(GpuError::NoAdapter)` only when there is genuinely no wgpu
71    /// adapter available on the system (e.g., a truly headless environment with
72    /// no software renderer).  In all other cases, the returned device will be
73    /// functional, albeit potentially CPU-backed.
74    ///
75    /// # Errors
76    ///
77    /// Returns `GpuError::NoAdapter` if no adapter can be obtained through any
78    /// backend on this system.
79    pub fn new_fallback() -> Result<Self> {
80        let instance = Self::create_instance();
81
82        // Attempt software / CPU adapter.  The `force_fallback_adapter` flag tells
83        // wgpu to prefer the software (Mesa lavapipe / wgpu-hal-null) renderer.
84        let maybe_adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
85            power_preference: PowerPreference::None,
86            compatible_surface: None,
87            force_fallback_adapter: true,
88        }));
89
90        if let Ok(adapter) = maybe_adapter {
91            let info = Self::adapter_info(&adapter);
92            if let Ok((device, queue)) = pollster::block_on(Self::request_device(&adapter)) {
93                return Ok(Self {
94                    device: Arc::new(device),
95                    queue: Arc::new(queue),
96                    info,
97                    adapter,
98                    is_fallback: true,
99                });
100            }
101        }
102
103        // Primary path failed — try every available strategy via make_null_device.
104        Self::make_null_device().ok_or(GpuError::NoAdapter)
105    }
106
107    /// Build a minimal "null device" that holds dummy wgpu objects.
108    ///
109    /// This is used only when both the primary and fallback GPU paths fail.
110    /// Returns `None` when no wgpu adapter can be obtained on the current
111    /// system (e.g., headless CI without any GPU or software renderer).
112    /// Callers that receive `None` must handle the absence of a real device.
113    fn make_null_device() -> Option<Self> {
114        let null_info = GpuDeviceInfo {
115            name: "CPU Null Device".to_string(),
116            vendor: 0,
117            device: 0,
118            device_type: "cpu".to_string(),
119            backend: "Null".to_string(),
120        };
121
122        // Helper: try to build a GpuDevice from an adapter, returning None on
123        // any failure rather than panicking.
124        fn try_adapter(adapter: Adapter, info: GpuDeviceInfo) -> Option<GpuDevice> {
125            match pollster::block_on(GpuDevice::request_device(&adapter)) {
126                Ok((device, queue)) => Some(GpuDevice {
127                    device: Arc::new(device),
128                    queue: Arc::new(queue),
129                    info,
130                    adapter,
131                    is_fallback: true,
132                }),
133                Err(_) => None,
134            }
135        }
136
137        // Attempt 1: GL / software backend with force_fallback.
138        let mut gl_desc = wgpu::InstanceDescriptor::new_without_display_handle();
139        gl_desc.backends = wgpu::Backends::GL;
140        let gl_instance = wgpu::Instance::new(gl_desc);
141        if let Ok(adapter) =
142            pollster::block_on(gl_instance.request_adapter(&RequestAdapterOptions {
143                power_preference: PowerPreference::None,
144                compatible_surface: None,
145                force_fallback_adapter: true,
146            }))
147        {
148            let info = Self::adapter_info(&adapter);
149            if let Some(dev) = try_adapter(adapter, info) {
150                return Some(dev);
151            }
152        }
153
154        // Attempt 2: enumerate all adapters from the GL instance and try the first.
155        let adapters = pollster::block_on(gl_instance.enumerate_adapters(wgpu::Backends::all()));
156        for adapter in adapters {
157            let info = Self::adapter_info(&adapter);
158            if let Some(dev) = try_adapter(adapter, info) {
159                return Some(dev);
160            }
161        }
162
163        // Attempt 3: default instance with force_fallback.
164        let default_instance =
165            wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
166        if let Ok(adapter) =
167            pollster::block_on(default_instance.request_adapter(&RequestAdapterOptions {
168                power_preference: PowerPreference::None,
169                compatible_surface: None,
170                force_fallback_adapter: true,
171            }))
172        {
173            let info = Self::adapter_info(&adapter);
174            if let Some(dev) = try_adapter(adapter, info) {
175                return Some(dev);
176            }
177        }
178
179        // Attempt 4: enumerate all adapters from the default instance.
180        let adapters =
181            pollster::block_on(default_instance.enumerate_adapters(wgpu::Backends::all()));
182        for adapter in adapters {
183            let info = Self::adapter_info(&adapter);
184            if let Some(dev) = try_adapter(adapter, info) {
185                return Some(dev);
186            }
187        }
188
189        // No adapter found at all on this system — caller handles None.
190        let _ = null_info;
191        None
192    }
193
194    /// List all available GPU devices
195    pub fn list_devices() -> Result<Vec<GpuDeviceInfo>> {
196        let instance = Self::create_instance();
197
198        #[cfg(not(target_arch = "wasm32"))]
199        {
200            let adapters = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()));
201            Ok(adapters.iter().map(Self::adapter_info).collect())
202        }
203
204        #[cfg(target_arch = "wasm32")]
205        {
206            // On wasm, enumerate_adapters is not available; request a single adapter instead
207            let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
208                power_preference: PowerPreference::HighPerformance,
209                compatible_surface: None,
210                force_fallback_adapter: false,
211            }));
212            match adapter {
213                Ok(a) => Ok(vec![Self::adapter_info(&a)]),
214                Err(_) => Ok(Vec::new()),
215            }
216        }
217    }
218
219    /// Get device information
220    #[must_use]
221    pub fn info(&self) -> &GpuDeviceInfo {
222        &self.info
223    }
224
225    /// Get the WGPU device
226    #[must_use]
227    pub fn device(&self) -> &Arc<Device> {
228        &self.device
229    }
230
231    /// Get the WGPU queue
232    #[must_use]
233    pub fn queue(&self) -> &Arc<Queue> {
234        &self.queue
235    }
236
237    /// Wait for all GPU operations to complete
238    pub fn wait(&self) {
239        let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
240    }
241
242    fn create_instance() -> Instance {
243        Instance::new(wgpu::InstanceDescriptor::new_without_display_handle())
244    }
245
246    async fn select_adapter(instance: &Instance, device_index: Option<usize>) -> Result<Adapter> {
247        if let Some(index) = device_index {
248            #[cfg(not(target_arch = "wasm32"))]
249            {
250                let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
251                return adapters.into_iter().nth(index).ok_or(GpuError::NoAdapter);
252            }
253
254            #[cfg(target_arch = "wasm32")]
255            {
256                // On wasm, enumerate_adapters is not available; only index 0 is supported
257                if index != 0 {
258                    return Err(GpuError::NoAdapter);
259                }
260                return instance
261                    .request_adapter(&RequestAdapterOptions {
262                        power_preference: PowerPreference::HighPerformance,
263                        compatible_surface: None,
264                        force_fallback_adapter: false,
265                    })
266                    .await
267                    .map_err(|_| GpuError::NoAdapter);
268            }
269        } else {
270            // Select high-performance adapter by default
271            instance
272                .request_adapter(&RequestAdapterOptions {
273                    power_preference: PowerPreference::HighPerformance,
274                    compatible_surface: None,
275                    force_fallback_adapter: false,
276                })
277                .await
278                .map_err(|_| GpuError::NoAdapter)
279        }
280    }
281
282    async fn request_device(adapter: &Adapter) -> Result<(Device, Queue)> {
283        adapter
284            .request_device(&DeviceDescriptor {
285                label: Some("OxiMedia GPU Device"),
286                required_features: Features::empty(),
287                required_limits: Limits::default(),
288                memory_hints: wgpu::MemoryHints::default(),
289                experimental_features: wgpu::ExperimentalFeatures::disabled(),
290                trace: wgpu::Trace::Off,
291            })
292            .await
293            .map_err(|e| GpuError::DeviceRequest(e.to_string()))
294    }
295
296    fn adapter_info(adapter: &Adapter) -> GpuDeviceInfo {
297        let info = adapter.get_info();
298
299        let device_type = match info.device_type {
300            wgpu::DeviceType::DiscreteGpu => "discrete",
301            wgpu::DeviceType::IntegratedGpu => "integrated",
302            wgpu::DeviceType::VirtualGpu => "virtual",
303            wgpu::DeviceType::Cpu => "cpu",
304            wgpu::DeviceType::Other => "unknown",
305        };
306
307        let backend = match info.backend {
308            wgpu::Backend::Vulkan => "Vulkan",
309            wgpu::Backend::Metal => "Metal",
310            wgpu::Backend::Dx12 => "DirectX 12",
311            wgpu::Backend::Gl => "OpenGL",
312            wgpu::Backend::BrowserWebGpu => "WebGPU",
313            _ => "Unknown",
314        };
315
316        GpuDeviceInfo {
317            name: info.name,
318            vendor: info.vendor,
319            device: info.device,
320            device_type: device_type.to_string(),
321            backend: backend.to_string(),
322        }
323    }
324}
325
326impl std::fmt::Debug for GpuDevice {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        f.debug_struct("GpuDevice")
329            .field("info", &self.info)
330            .field("is_fallback", &self.is_fallback)
331            .finish()
332    }
333}