1use crate::{BindingKind, BindingSpec, ComputePipeline};
2use std::collections::HashMap;
3use std::fmt::{self, Display, Formatter};
4use std::sync::{Arc, Mutex};
5use wgpu::{
6 Adapter, AdapterInfo, Backend, Backends, Device, DeviceLostReason, DeviceType, Features,
7 Instance, InstanceDescriptor, Limits, PowerPreference, Queue,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum LimitsPolicy {
13 Minimum,
15 Adapter,
17}
18
19pub struct GpuRequest {
21 pub backends: Backends,
22 pub power_preference: PowerPreference,
23 pub device_name: Option<String>,
25 pub required_features: Features,
27 pub optional_features: Features,
29 pub limits: LimitsPolicy,
30}
31
32impl Default for GpuRequest {
33 fn default() -> Self {
34 Self {
35 backends: GpuRequest::NATIVE_BACKENDS,
36 power_preference: PowerPreference::HighPerformance,
37 device_name: None,
38 required_features: Features::empty(),
39 optional_features: GpuRequest::PROFILING_FEATURES,
40 limits: LimitsPolicy::Adapter,
41 }
42 }
43}
44
45impl GpuRequest {
46 #[cfg(feature = "profile")]
49 pub const PROFILING_FEATURES: Features =
50 Features::TIMESTAMP_QUERY.union(Features::TIMESTAMP_QUERY_INSIDE_ENCODERS);
51 #[cfg(not(feature = "profile"))]
52 pub const PROFILING_FEATURES: Features = Features::empty();
53
54 pub const NATIVE_BACKENDS: Backends = Backends::DX12
56 .union(Backends::METAL)
57 .union(Backends::VULKAN);
58
59 pub fn adapter_named(name: impl Into<String>) -> Self {
60 Self {
61 device_name: Some(name.into()),
62 ..Self::default()
63 }
64 }
65
66 pub fn minimum_limits() -> Self {
68 Self {
69 limits: LimitsPolicy::Minimum,
70 ..Self::default()
71 }
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub enum GpuUnavailable {
78 NoAdapter {
79 backends: Backends,
80 },
81 DeviceNotFound {
82 requested: String,
83 available: Vec<String>,
84 },
85 MissingFeatures {
86 missing: Features,
87 available: Features,
88 },
89 DeviceRejected {
90 message: String,
91 },
92 InsufficientLimits {
93 adapter: String,
94 },
95}
96
97impl Display for GpuUnavailable {
98 fn fmt(&self, out: &mut Formatter<'_>) -> fmt::Result {
99 match self {
100 Self::NoAdapter { backends } => {
101 write!(
102 out,
103 "no adapter exposed by the enabled backends {backends:?}"
104 )
105 }
106 Self::DeviceNotFound {
107 requested,
108 available,
109 } => write!(
110 out,
111 "no adapter name contains {requested:?}; available: {available:?}"
112 ),
113 Self::MissingFeatures { missing, available } => write!(
114 out,
115 "adapter lacks required features {missing:?} (offers {available:?})"
116 ),
117 Self::DeviceRejected { message } => write!(out, "device request rejected: {message}"),
118 Self::InsufficientLimits { adapter } => write!(
119 out,
120 "adapter {adapter:?} cannot satisfy the storage-buffer limits dynamis requires"
121 ),
122 }
123 }
124}
125
126impl std::error::Error for GpuUnavailable {}
127
128#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct DeviceLost {
131 pub reason: DeviceLostReason,
132 pub message: String,
133}
134
135impl Display for DeviceLost {
136 fn fmt(&self, out: &mut Formatter<'_>) -> fmt::Result {
137 write!(out, "{:?}: {}", self.reason, self.message)
138 }
139}
140
141#[derive(Default)]
142struct Health {
143 lost: Mutex<Option<DeviceLost>>,
144}
145
146pub struct GpuContext {
147 adapter: Adapter,
148 device: Device,
149 queue: Queue,
150 features: Features,
151 limits: Limits,
152 timestamp_period_ns: f32,
153 health: Arc<Health>,
154 pipelines: Arc<Mutex<HashMap<PipelineKey, Arc<ComputePipeline>>>>,
155}
156
157#[derive(PartialEq, Eq, Hash, Clone)]
158struct PipelineKey {
159 label: String,
160 shader: String,
161 entry: String,
162 workgroup_size: u32,
163 bindings: Vec<(u32, BindingKind)>,
164}
165
166impl PipelineKey {
167 fn of(
168 label: &str,
169 shader: &str,
170 entry: &str,
171 groups: &[&[BindingSpec]],
172 workgroup_size: u32,
173 ) -> Self {
174 let bindings = groups
175 .iter()
176 .flat_map(|group| group.iter())
177 .map(|spec| (spec.binding, spec.kind))
178 .collect();
179 Self {
180 label: label.to_owned(),
181 shader: shader.to_owned(),
182 entry: entry.to_owned(),
183 workgroup_size,
184 bindings,
185 }
186 }
187}
188
189impl GpuContext {
190 pub const MINIMUM_LIMITS: Limits = Limits {
192 max_storage_buffers_per_shader_stage: 16,
193 max_storage_textures_per_shader_stage: 0,
194 max_storage_buffer_binding_size: 16 * 1024 * 1024,
195 max_buffer_size: 64 * 1024 * 1024,
196 max_compute_workgroup_size_x: 64,
197 max_compute_workgroup_size_y: 64,
198 max_compute_workgroup_size_z: 64,
199 max_compute_workgroups_per_dimension: 65_535,
200 ..Limits::defaults()
201 };
202
203 pub async fn open(request: &GpuRequest) -> Result<Self, GpuUnavailable> {
205 let adapter = select_adapter(request).await?;
206 let available = adapter.features();
207 let missing = request.required_features.difference(available);
208 if !missing.is_empty() {
209 return Err(GpuUnavailable::MissingFeatures { missing, available });
210 }
211 let features = request.required_features | (request.optional_features & available);
212 let limits = match request.limits {
213 LimitsPolicy::Adapter => adapter.limits(),
214 LimitsPolicy::Minimum => Self::MINIMUM_LIMITS,
215 };
216 if !Self::MINIMUM_LIMITS.check_limits(&adapter.limits()) {
217 return Err(GpuUnavailable::InsufficientLimits {
218 adapter: adapter.get_info().name,
219 });
220 }
221 let (device, queue) = adapter
222 .request_device(&wgpu::DeviceDescriptor {
223 label: Some("dynamis device"),
224 required_features: features,
225 required_limits: limits.clone(),
226 memory_hints: wgpu::MemoryHints::Performance,
227 ..Default::default()
228 })
229 .await
230 .map_err(|error| GpuUnavailable::DeviceRejected {
231 message: error.to_string(),
232 })?;
233 let health = Arc::new(Health::default());
234 let callback = health.clone();
235 device.set_device_lost_callback(move |reason, message| {
236 *callback.lost.lock().unwrap() = Some(DeviceLost { reason, message });
237 });
238 let timestamp_period_ns = queue.get_timestamp_period();
239 Ok(Self {
240 adapter,
241 device,
242 queue,
243 features,
244 limits,
245 timestamp_period_ns,
246 health,
247 pipelines: Arc::new(Mutex::new(HashMap::new())),
248 })
249 }
250
251 pub async fn new() -> Self {
253 match Self::open(&GpuRequest::default()).await {
254 Ok(context) => context,
255 Err(error) => panic!("{error}"),
256 }
257 }
258
259 pub async fn available_adapters(backends: Backends) -> Vec<AdapterInfo> {
261 let instance = create_instance(backends);
262 instance
263 .enumerate_adapters(backends)
264 .await
265 .iter()
266 .map(|adapter| adapter.get_info())
267 .collect()
268 }
269
270 pub fn device(&self) -> &Device {
271 &self.device
272 }
273
274 pub fn queue(&self) -> &Queue {
275 &self.queue
276 }
277
278 pub fn adapter_info(&self) -> AdapterInfo {
279 self.adapter.get_info()
280 }
281
282 pub fn features(&self) -> Features {
283 self.features
284 }
285
286 pub fn limits(&self) -> &Limits {
287 &self.limits
288 }
289
290 pub fn supports(&self, features: Features) -> bool {
291 self.features.contains(features)
292 }
293
294 pub fn timestamp_period_ns(&self) -> f32 {
296 self.timestamp_period_ns
297 }
298
299 #[cfg(feature = "profile")]
301 pub fn supports_pass_timing(&self) -> bool {
302 self.supports(GpuRequest::PROFILING_FEATURES)
303 }
304
305 pub fn device_lost(&self) -> Option<DeviceLost> {
306 self.health.lost.lock().unwrap().clone()
307 }
308
309 pub fn assert_alive(&self) {
310 if let Some(lost) = self.device_lost() {
311 panic!("gpu device lost: {lost}");
312 }
313 }
314
315 pub fn compute_pipeline(
316 &self,
317 label: &str,
318 shader: &str,
319 entry: &str,
320 groups: &[&[BindingSpec]],
321 workgroup_size: u32,
322 ) -> ComputePipeline {
323 let key = PipelineKey::of(label, shader, entry, groups, workgroup_size);
324 let mut cache = self.pipelines.lock().unwrap();
325 if let Some(pipeline) = cache.get(&key) {
326 return (**pipeline).clone();
327 }
328 let pipeline = Arc::new(ComputePipeline::new(
329 &self.device,
330 label,
331 shader,
332 entry,
333 groups,
334 ));
335 cache.insert(key, pipeline.clone());
336 (*pipeline).clone()
337 }
338}
339
340impl Clone for GpuContext {
341 fn clone(&self) -> Self {
342 Self {
343 adapter: self.adapter.clone(),
344 device: self.device.clone(),
345 queue: self.queue.clone(),
346 features: self.features,
347 limits: self.limits.clone(),
348 timestamp_period_ns: self.timestamp_period_ns,
349 health: self.health.clone(),
350 pipelines: self.pipelines.clone(),
351 }
352 }
353}
354
355fn create_instance(backends: Backends) -> Instance {
356 Instance::new(InstanceDescriptor {
357 backends,
358 ..InstanceDescriptor::new_without_display_handle()
359 })
360}
361
362async fn select_adapter(request: &GpuRequest) -> Result<Adapter, GpuUnavailable> {
363 let instance = create_instance(request.backends);
364 let adapters = instance.enumerate_adapters(request.backends).await;
365 if adapters.is_empty() {
366 return Err(GpuUnavailable::NoAdapter {
367 backends: request.backends,
368 });
369 }
370 let candidates = match &request.device_name {
371 Some(needle) => {
372 let wanted = needle.to_lowercase();
373 let matched = adapters
374 .iter()
375 .filter(|adapter| adapter.get_info().name.to_lowercase().contains(&wanted))
376 .cloned()
377 .collect::<Vec<_>>();
378 if matched.is_empty() {
379 return Err(GpuUnavailable::DeviceNotFound {
380 requested: needle.clone(),
381 available: adapters
382 .iter()
383 .map(|adapter| adapter.get_info().name)
384 .collect(),
385 });
386 }
387 matched
388 }
389 None => adapters,
390 };
391 Ok(prefer(candidates, request.power_preference))
392}
393
394fn prefer(candidates: Vec<Adapter>, power: PowerPreference) -> Adapter {
395 let mut candidates = candidates;
396 let rank = |adapter: &Adapter| {
397 let info = adapter.get_info();
398 let device = match power {
399 PowerPreference::HighPerformance => match info.device_type {
400 DeviceType::DiscreteGpu => 0u8,
401 DeviceType::VirtualGpu => 1,
402 DeviceType::IntegratedGpu => 2,
403 _ => 3,
404 },
405 PowerPreference::LowPower => match info.device_type {
406 DeviceType::IntegratedGpu => 0u8,
407 DeviceType::VirtualGpu => 1,
408 DeviceType::DiscreteGpu => 2,
409 _ => 3,
410 },
411 PowerPreference::None => 0u8,
412 };
413 (native_backend_rank(info.backend), device)
414 };
415 candidates.sort_by_key(|adapter| rank(adapter));
416 candidates
417 .into_iter()
418 .next()
419 .unwrap_or_else(|| panic!("adapter candidates must be non-empty"))
420}
421
422fn native_backend_rank(backend: Backend) -> u8 {
423 #[cfg(target_os = "windows")]
424 const PRIMARY: Backend = Backend::Dx12;
425 #[cfg(any(target_os = "macos", target_os = "ios", target_os = "visionos"))]
426 const PRIMARY: Backend = Backend::Metal;
427 #[cfg(not(any(
428 target_os = "windows",
429 target_os = "macos",
430 target_os = "ios",
431 target_os = "visionos"
432 )))]
433 const PRIMARY: Backend = Backend::Vulkan;
434
435 u8::from(backend != PRIMARY)
436}