Skip to main content

oxillama_gpu/
context.rs

1//! GPU device and queue initialisation.
2//!
3//! [`GpuContext`] holds an initialised wgpu device+queue pair.  When the
4//! `gpu` feature is disabled the struct is zero-size and `try_init` always
5//! returns `None`, so all call-sites compile without GPU hardware or the
6//! feature flag.
7
8/// Information about an available GPU device.
9#[derive(Debug, Clone)]
10pub struct GpuDeviceInfo {
11    /// Human-readable device name.
12    pub name: String,
13    /// Backend type (Vulkan, Metal, DX12, etc.)
14    pub backend: String,
15    /// Device type (discrete, integrated, software, etc.)
16    pub device_type: String,
17}
18
19/// An initialised GPU device and queue.
20///
21/// Construct via [`GpuContext::try_init`].  Returns `None` if no compatible
22/// adapter is available (headless CI, no GPU hardware, feature disabled).
23///
24/// The `_private` field is always present (unconditionally) so that external
25/// code cannot construct a `GpuContext` with struct-literal syntax even when
26/// the `gpu` feature is disabled (which would otherwise leave an empty struct
27/// that can be trivially constructed).
28pub struct GpuContext {
29    #[cfg(feature = "gpu")]
30    pub(crate) device: wgpu::Device,
31    #[cfg(feature = "gpu")]
32    pub(crate) queue: wgpu::Queue,
33    /// Prevents external struct-literal construction.
34    _private: (),
35}
36
37impl GpuContext {
38    /// Try to initialise a GPU context.
39    ///
40    /// Returns `None` when:
41    /// - The `gpu` feature is not enabled.
42    /// - No compatible wgpu adapter exists on the current host.
43    /// - The device-request step fails (e.g. out-of-resources).
44    pub fn try_init() -> Option<Self> {
45        #[cfg(feature = "gpu")]
46        {
47            pollster::block_on(Self::try_init_async())
48        }
49        #[cfg(not(feature = "gpu"))]
50        {
51            None
52        }
53    }
54
55    /// Async GPU initialisation used by `try_init`.
56    #[cfg(feature = "gpu")]
57    async fn try_init_async() -> Option<Self> {
58        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
59            backends: wgpu::Backends::all(),
60            ..wgpu::InstanceDescriptor::new_without_display_handle()
61        });
62
63        let adapter = instance
64            .request_adapter(&wgpu::RequestAdapterOptions {
65                power_preference: wgpu::PowerPreference::HighPerformance,
66                compatible_surface: None,
67                force_fallback_adapter: false,
68            })
69            .await
70            .ok()?;
71
72        let (device, queue) = adapter
73            .request_device(&wgpu::DeviceDescriptor::default())
74            .await
75            .ok()?;
76
77        Some(GpuContext {
78            device,
79            queue,
80            _private: (),
81        })
82    }
83
84    /// Enumerate available GPU adapters and return info about each.
85    pub fn enumerate_devices() -> Vec<GpuDeviceInfo> {
86        #[cfg(feature = "gpu")]
87        {
88            let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
89                backends: wgpu::Backends::all(),
90                ..wgpu::InstanceDescriptor::new_without_display_handle()
91            });
92
93            pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()))
94                .into_iter()
95                .map(|adapter| {
96                    let info = adapter.get_info();
97                    GpuDeviceInfo {
98                        name: info.name,
99                        backend: format!("{:?}", info.backend),
100                        device_type: format!("{:?}", info.device_type),
101                    }
102                })
103                .collect()
104        }
105        #[cfg(not(feature = "gpu"))]
106        {
107            Vec::new()
108        }
109    }
110
111    /// Try to initialise with a specific adapter selected by name substring
112    /// match (case-insensitive).
113    pub fn try_init_with_name(name_pattern: &str) -> Option<Self> {
114        #[cfg(feature = "gpu")]
115        {
116            pollster::block_on(Self::try_init_with_name_async(name_pattern))
117        }
118        #[cfg(not(feature = "gpu"))]
119        {
120            let _ = name_pattern;
121            None
122        }
123    }
124
125    /// Async helper for `try_init_with_name`.
126    #[cfg(feature = "gpu")]
127    async fn try_init_with_name_async(name_pattern: &str) -> Option<Self> {
128        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
129            backends: wgpu::Backends::all(),
130            ..wgpu::InstanceDescriptor::new_without_display_handle()
131        });
132
133        let pattern_lower = name_pattern.to_lowercase();
134        let adapter = instance
135            .enumerate_adapters(wgpu::Backends::all())
136            .await
137            .into_iter()
138            .find(|a| a.get_info().name.to_lowercase().contains(&pattern_lower))?;
139
140        let (device, queue) = adapter
141            .request_device(&wgpu::DeviceDescriptor::default())
142            .await
143            .ok()?;
144
145        Some(GpuContext {
146            device,
147            queue,
148            _private: (),
149        })
150    }
151
152    /// Try to initialise with a specific adapter by index.
153    pub fn try_init_with_index(index: usize) -> Option<Self> {
154        #[cfg(feature = "gpu")]
155        {
156            pollster::block_on(Self::try_init_with_index_async(index))
157        }
158        #[cfg(not(feature = "gpu"))]
159        {
160            let _ = index;
161            None
162        }
163    }
164
165    /// Async helper for `try_init_with_index`.
166    #[cfg(feature = "gpu")]
167    async fn try_init_with_index_async(index: usize) -> Option<Self> {
168        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
169            backends: wgpu::Backends::all(),
170            ..wgpu::InstanceDescriptor::new_without_display_handle()
171        });
172
173        let adapters: Vec<_> = instance
174            .enumerate_adapters(wgpu::Backends::all())
175            .await
176            .into_iter()
177            .collect();
178
179        let adapter = adapters.into_iter().nth(index)?;
180
181        let (device, queue) = adapter
182            .request_device(&wgpu::DeviceDescriptor::default())
183            .await
184            .ok()?;
185
186        Some(GpuContext {
187            device,
188            queue,
189            _private: (),
190        })
191    }
192}