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 Some(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 gl_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
139            backends: wgpu::Backends::GL,
140            ..Default::default()
141        });
142        if let Some(adapter) =
143            pollster::block_on(gl_instance.request_adapter(&RequestAdapterOptions {
144                power_preference: PowerPreference::None,
145                compatible_surface: None,
146                force_fallback_adapter: true,
147            }))
148        {
149            let info = Self::adapter_info(&adapter);
150            if let Some(dev) = try_adapter(adapter, info) {
151                return Some(dev);
152            }
153        }
154
155        // Attempt 2: enumerate all adapters from the GL instance and try the first.
156        let adapters = gl_instance.enumerate_adapters(wgpu::Backends::all());
157        for adapter in adapters {
158            let info = Self::adapter_info(&adapter);
159            if let Some(dev) = try_adapter(adapter, info) {
160                return Some(dev);
161            }
162        }
163
164        // Attempt 3: default instance with force_fallback.
165        let default_instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
166        if let Some(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 = default_instance.enumerate_adapters(wgpu::Backends::all());
181        for adapter in adapters {
182            let info = Self::adapter_info(&adapter);
183            if let Some(dev) = try_adapter(adapter, info) {
184                return Some(dev);
185            }
186        }
187
188        // No adapter found at all on this system — caller handles None.
189        let _ = null_info;
190        None
191    }
192
193    /// List all available GPU devices
194    pub fn list_devices() -> Result<Vec<GpuDeviceInfo>> {
195        let instance = Self::create_instance();
196
197        #[cfg(not(target_arch = "wasm32"))]
198        {
199            let adapters = instance.enumerate_adapters(wgpu::Backends::all());
200            Ok(adapters.iter().map(Self::adapter_info).collect())
201        }
202
203        #[cfg(target_arch = "wasm32")]
204        {
205            // On wasm, enumerate_adapters is not available; request a single adapter instead
206            let adapter = pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
207                power_preference: PowerPreference::HighPerformance,
208                compatible_surface: None,
209                force_fallback_adapter: false,
210            }));
211            match adapter {
212                Some(a) => Ok(vec![Self::adapter_info(&a)]),
213                None => Ok(Vec::new()),
214            }
215        }
216    }
217
218    /// Get device information
219    #[must_use]
220    pub fn info(&self) -> &GpuDeviceInfo {
221        &self.info
222    }
223
224    /// Get the WGPU device
225    #[must_use]
226    pub fn device(&self) -> &Arc<Device> {
227        &self.device
228    }
229
230    /// Get the WGPU queue
231    #[must_use]
232    pub fn queue(&self) -> &Arc<Queue> {
233        &self.queue
234    }
235
236    /// Wait for all GPU operations to complete
237    pub fn wait(&self) {
238        self.device.poll(wgpu::Maintain::Wait);
239    }
240
241    fn create_instance() -> Instance {
242        Instance::new(&wgpu::InstanceDescriptor {
243            backends: wgpu::Backends::all(),
244            ..Default::default()
245        })
246    }
247
248    async fn select_adapter(instance: &Instance, device_index: Option<usize>) -> Result<Adapter> {
249        if let Some(index) = device_index {
250            #[cfg(not(target_arch = "wasm32"))]
251            {
252                let adapters = instance.enumerate_adapters(wgpu::Backends::all());
253                return adapters.into_iter().nth(index).ok_or(GpuError::NoAdapter);
254            }
255
256            #[cfg(target_arch = "wasm32")]
257            {
258                // On wasm, enumerate_adapters is not available; only index 0 is supported
259                if index != 0 {
260                    return Err(GpuError::NoAdapter);
261                }
262                return instance
263                    .request_adapter(&RequestAdapterOptions {
264                        power_preference: PowerPreference::HighPerformance,
265                        compatible_surface: None,
266                        force_fallback_adapter: false,
267                    })
268                    .await
269                    .ok_or(GpuError::NoAdapter);
270            }
271        } else {
272            // Select high-performance adapter by default
273            instance
274                .request_adapter(&RequestAdapterOptions {
275                    power_preference: PowerPreference::HighPerformance,
276                    compatible_surface: None,
277                    force_fallback_adapter: false,
278                })
279                .await
280                .ok_or(GpuError::NoAdapter)
281        }
282    }
283
284    async fn request_device(adapter: &Adapter) -> Result<(Device, Queue)> {
285        adapter
286            .request_device(
287                &DeviceDescriptor {
288                    label: Some("OxiMedia GPU Device"),
289                    required_features: Features::empty(),
290                    required_limits: Limits::default(),
291                    memory_hints: wgpu::MemoryHints::default(),
292                },
293                None,
294            )
295            .await
296            .map_err(|e| GpuError::DeviceRequest(e.to_string()))
297    }
298
299    fn adapter_info(adapter: &Adapter) -> GpuDeviceInfo {
300        let info = adapter.get_info();
301
302        let device_type = match info.device_type {
303            wgpu::DeviceType::DiscreteGpu => "discrete",
304            wgpu::DeviceType::IntegratedGpu => "integrated",
305            wgpu::DeviceType::VirtualGpu => "virtual",
306            wgpu::DeviceType::Cpu => "cpu",
307            wgpu::DeviceType::Other => "unknown",
308        };
309
310        let backend = match info.backend {
311            wgpu::Backend::Vulkan => "Vulkan",
312            wgpu::Backend::Metal => "Metal",
313            wgpu::Backend::Dx12 => "DirectX 12",
314            wgpu::Backend::Gl => "OpenGL",
315            wgpu::Backend::BrowserWebGpu => "WebGPU",
316            _ => "Unknown",
317        };
318
319        GpuDeviceInfo {
320            name: info.name,
321            vendor: info.vendor,
322            device: info.device,
323            device_type: device_type.to_string(),
324            backend: backend.to_string(),
325        }
326    }
327}
328
329impl std::fmt::Debug for GpuDevice {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        f.debug_struct("GpuDevice")
332            .field("info", &self.info)
333            .field("is_fallback", &self.is_fallback)
334            .finish()
335    }
336}