1use std::sync::Arc;
2use std::time::Duration;
3
4use web_time::Instant;
5
6use crate::diagnostic::DiagnosticHook;
7use crate::{
8 Capabilities, CapabilityTier, GpuDiagnostic, GpuDiagnosticHandler, GpuError, IntermediateFormat,
9};
10
11#[derive(Clone, Debug)]
31pub struct GpuContext {
32 adapter: wgpu::Adapter,
33 device: wgpu::Device,
34 queue: Arc<LockedQueue>,
35 capabilities: Arc<Capabilities>,
37 diagnostics: Arc<DiagnosticHook>,
38}
39
40#[derive(Debug)]
42struct LockedQueue {
43 queue: wgpu::Queue,
45 lock: QueueLockSlot,
46}
47
48#[derive(Debug)]
51struct QueueLockSlot {
52 lock: Option<Arc<dyn ExternalQueueLock>>,
53 held: bool,
54}
55
56impl LockedQueue {
57 #[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
61 fn shared(queue: wgpu::Queue, lock: Option<Arc<dyn ExternalQueueLock>>) -> Arc<Self> {
62 Arc::new(Self {
63 queue,
64 lock: QueueLockSlot { lock, held: false },
65 })
66 }
67
68 fn locked<T>(&self, call: impl FnOnce(&wgpu::Queue) -> T) -> T {
70 let guard = self.lock.lock.as_ref().map(|lock| {
71 lock.lock();
72 QueueGuard(lock.as_ref())
73 });
74 let result = call(&self.queue);
75 drop(guard);
76 result
77 }
78}
79
80impl Drop for LockedQueue {
81 fn drop(&mut self) {
82 if let Some(lock) = &self.lock.lock {
85 lock.lock();
86 self.lock.held = true;
87 }
88 }
89}
90
91impl Drop for QueueLockSlot {
92 fn drop(&mut self) {
93 if self.held
94 && let Some(lock) = &self.lock
95 {
96 lock.unlock();
97 }
98 }
99}
100
101pub trait ExternalQueueLock: std::fmt::Debug + Send + Sync + 'static {
110 fn lock(&self);
112 fn unlock(&self);
114}
115
116#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
118#[non_exhaustive]
119#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
120pub struct SubmitTimings {
121 pub queue_lock_wait: Duration,
123 pub queue_submit: Duration,
125}
126
127struct QueueGuard<'a>(&'a dyn ExternalQueueLock);
128
129impl Drop for QueueGuard<'_> {
130 fn drop(&mut self) {
131 self.0.unlock();
132 }
133}
134
135impl GpuContext {
136 #[must_use]
142 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
143 pub fn from_parts(adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Self {
144 Self::from_parts_with_queue_lock(adapter, device, queue, None)
145 }
146
147 #[must_use]
154 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
155 pub fn from_parts_with_queue_lock(
156 adapter: wgpu::Adapter,
157 device: wgpu::Device,
158 queue: wgpu::Queue,
159 queue_lock: Option<Arc<dyn ExternalQueueLock>>,
160 ) -> Self {
161 let capabilities = Arc::new(Capabilities::from_adapter(&adapter));
162 let diagnostics = Arc::new(DiagnosticHook::new());
163 install_device_callbacks(&device, &diagnostics);
164 Self {
165 adapter,
166 device,
167 queue: LockedQueue::shared(queue, queue_lock),
168 capabilities,
169 diagnostics,
170 }
171 }
172
173 #[must_use]
175 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
176 pub const fn device(&self) -> &wgpu::Device {
177 &self.device
178 }
179
180 #[must_use]
187 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
188 pub fn queue(&self) -> &wgpu::Queue {
189 &self.queue.queue
190 }
191
192 pub fn on_submitted_work_done(&self, callback: impl FnOnce() + Send + 'static) {
198 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
199 self.queue
200 .locked(|queue| queue.on_submitted_work_done(callback));
201 }
202
203 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
207 pub fn map_async(
208 &self,
209 slice: &wgpu::BufferSlice<'_>,
210 mode: wgpu::MapMode,
211 callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) + wgpu::WasmNotSend + 'static,
212 ) {
213 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
214 self.queue.locked(|_| slice.map_async(mode, callback));
215 }
216
217 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
219 pub fn present(&self, texture: wgpu::SurfaceTexture) {
220 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
221 self.queue.locked(|queue| queue.present(texture));
222 }
223
224 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
229 pub fn submit<I>(&self, command_buffers: I) -> wgpu::SubmissionIndex
230 where
231 I: IntoIterator<Item = wgpu::CommandBuffer>,
232 {
233 self.submit_measured(command_buffers, false).0
234 }
235
236 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
243 pub fn submit_timed<I>(&self, command_buffers: I) -> (wgpu::SubmissionIndex, SubmitTimings)
244 where
245 I: IntoIterator<Item = wgpu::CommandBuffer>,
246 {
247 self.submit_measured(command_buffers, true)
248 }
249
250 fn submit_measured<I>(
251 &self,
252 command_buffers: I,
253 measure: bool,
254 ) -> (wgpu::SubmissionIndex, SubmitTimings)
255 where
256 I: IntoIterator<Item = wgpu::CommandBuffer>,
257 {
258 let mut timings = SubmitTimings::default();
259 let started = measure.then(Instant::now);
260 let index = self.queue.locked(|queue| {
261 let acquired = started.map(|started| {
262 let now = Instant::now();
263 timings.queue_lock_wait = now.duration_since(started);
264 now
265 });
266 #[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
267 let index = queue.submit(command_buffers);
268 if let Some(acquired) = acquired {
269 timings.queue_submit = acquired.elapsed();
270 }
271 index
272 });
273 (index, timings)
274 }
275
276 pub async fn request() -> Result<Self, GpuError> {
281 let _turn = crate::serialize_acquisition();
282 let instance =
283 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
284 Self::request_compatible(&instance, None).await
285 }
286
287 pub async fn request_high_performance() -> Result<Self, GpuError> {
294 Self::request_high_performance_without(wgpu::Features::empty()).await
295 }
296
297 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
310 pub async fn request_high_performance_without(
311 withheld: wgpu::Features,
312 ) -> Result<Self, GpuError> {
313 let _turn = crate::serialize_acquisition();
314 let instance =
315 wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
316 let preference =
317 wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
318 Self::request_compatible_with_preference(&instance, None, preference, withheld).await
319 }
320
321 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
330 pub async fn request_high_performance_on(backends: wgpu::Backends) -> Result<Self, GpuError> {
331 let _turn = crate::serialize_acquisition();
332 let instance = Self::instance_on(backends);
333 let preference =
334 wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
335 Self::request_compatible_with_preference(
336 &instance,
337 None,
338 preference,
339 wgpu::Features::empty(),
340 )
341 .await
342 }
343
344 #[cfg(not(target_arch = "wasm32"))]
359 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
360 pub async fn request_high_performance_on_accepted<T, E>(
361 backends: wgpu::Backends,
362 mut accept: impl FnMut(&Self) -> Result<T, E>,
363 ) -> Result<AcceptedDevice<T, E>, GpuError> {
364 let _turn = crate::serialize_acquisition();
365 let instance = Self::instance_on(backends);
366 let preference =
367 wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
368 let preferred = Self::request_compatible_with_preference(
369 &instance,
370 None,
371 preference,
372 wgpu::Features::empty(),
373 )
374 .await?;
375 let mut refusals = AdapterRefusals(Vec::new());
376 match accept(&preferred) {
377 Ok(value) => {
378 return Ok(AcceptedDevice {
379 context: preferred,
380 accepted: Some(value),
381 refusals,
382 });
383 }
384 Err(error) => refusals.0.push((preferred.adapter.get_info().name, error)),
385 }
386 let tried = preferred.adapter.get_info();
387 let mut others: Vec<wgpu::Adapter> = instance
388 .enumerate_adapters(wgpu::Backends::all())
389 .await
390 .into_iter()
391 .filter(|other| {
392 let info = other.get_info();
393 info != tried && info.device_type != wgpu::DeviceType::Cpu
394 })
395 .collect();
396 others.sort_by_key(|other| fallback_rank(other.get_info().device_type, preference));
397 for other in others {
398 let name = other.get_info().name;
399 let Ok(context) = Self::open_device(other, wgpu::Features::empty()).await else {
400 continue;
401 };
402 match accept(&context) {
403 Ok(value) => {
404 return Ok(AcceptedDevice {
405 context,
406 accepted: Some(value),
407 refusals,
408 });
409 }
410 Err(error) => refusals.0.push((name, error)),
411 }
412 }
413 Ok(AcceptedDevice {
414 context: preferred,
415 accepted: None,
416 refusals,
417 })
418 }
419
420 #[cfg(not(target_arch = "wasm32"))]
431 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
432 pub async fn adapters_by_preference_on(
433 backends: wgpu::Backends,
434 ) -> Vec<(usize, wgpu::Adapter)> {
435 let _turn = crate::serialize_acquisition();
436 let instance = Self::instance_on(backends);
437 let preference =
438 wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
439 let preferred = instance
440 .request_adapter(&wgpu::RequestAdapterOptions {
441 power_preference: preference,
442 ..Default::default()
443 })
444 .await
445 .ok()
446 .map(|adapter| adapter.get_info());
447 let mut adapters: Vec<(usize, wgpu::Adapter)> = instance
448 .enumerate_adapters(wgpu::Backends::all())
449 .await
450 .into_iter()
451 .enumerate()
452 .collect();
453 adapters.sort_by_key(|(_, adapter)| {
455 let info = adapter.get_info();
456 if info.device_type == wgpu::DeviceType::Cpu {
457 (2, u8::MAX)
458 } else if preferred.as_ref() == Some(&info) {
459 (0, 0)
460 } else {
461 (1, fallback_rank(info.device_type, preference))
462 }
463 });
464 adapters
465 }
466
467 #[cfg(not(target_arch = "wasm32"))]
473 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
474 pub async fn open_adapter(adapter: wgpu::Adapter) -> Result<Self, GpuError> {
475 let _turn = crate::serialize_acquisition();
476 Self::open_device(adapter, wgpu::Features::empty()).await
477 }
478
479 #[must_use]
481 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
482 pub const fn adapter(&self) -> &wgpu::Adapter {
483 &self.adapter
484 }
485
486 fn instance_on(backends: wgpu::Backends) -> wgpu::Instance {
488 let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
489 descriptor.backends = backends;
490 wgpu::Instance::new(descriptor.with_env())
491 }
492
493 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
501 pub async fn request_compatible(
502 instance: &wgpu::Instance,
503 compatible_surface: Option<&wgpu::Surface<'_>>,
504 ) -> Result<Self, GpuError> {
505 let preference = wgpu::PowerPreference::from_env().unwrap_or_default();
506 Self::request_compatible_with_preference(
507 instance,
508 compatible_surface,
509 preference,
510 wgpu::Features::empty(),
511 )
512 .await
513 }
514
515 async fn request_compatible_with_preference(
527 instance: &wgpu::Instance,
528 compatible_surface: Option<&wgpu::Surface<'_>>,
529 power_preference: wgpu::PowerPreference,
530 withheld: wgpu::Features,
531 ) -> Result<Self, GpuError> {
532 let _turn = crate::serialize_acquisition();
533 let adapter = instance
534 .request_adapter(&wgpu::RequestAdapterOptions {
535 compatible_surface,
536 power_preference,
537 ..Default::default()
538 })
539 .await
540 .map_err(GpuError::adapter)?;
541 #[cfg(not(target_arch = "wasm32"))]
542 let preferred = adapter.get_info();
543 let error = match Self::open_device(adapter, withheld).await {
544 Ok(context) => return Ok(context),
545 Err(error) => error,
546 };
547 #[cfg(not(target_arch = "wasm32"))]
548 {
549 let mut others: Vec<wgpu::Adapter> = instance
550 .enumerate_adapters(wgpu::Backends::all())
551 .await
552 .into_iter()
553 .filter(|other| other.get_info() != preferred)
554 .filter(|other| {
555 compatible_surface.is_none_or(|surface| other.is_surface_supported(surface))
556 })
557 .collect();
558 others
559 .sort_by_key(|other| fallback_rank(other.get_info().device_type, power_preference));
560 for other in others {
561 if let Ok(context) = Self::open_device(other, withheld).await {
562 return Ok(context);
563 }
564 }
565 }
566 Err(error)
567 }
568
569 async fn open_device(
570 adapter: wgpu::Adapter,
571 withheld: wgpu::Features,
572 ) -> Result<Self, GpuError> {
573 let capabilities = Arc::new(Capabilities::from_adapter(&adapter));
574 let optional_features = wgpu::Features::TIMESTAMP_QUERY
577 | wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
578 | if cfg!(target_os = "windows") {
579 wgpu::Features::TEXTURE_FORMAT_NV12 | wgpu::Features::TEXTURE_FORMAT_P010
580 } else {
581 wgpu::Features::empty()
582 };
583 let required_features = adapter.features() & optional_features & !withheld;
584 let required_limits =
585 wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits());
586 let (device, queue) = adapter
587 .request_device(&wgpu::DeviceDescriptor {
588 required_features,
589 required_limits,
590 ..wgpu::DeviceDescriptor::default()
591 })
592 .await
593 .map_err(GpuError::device)?;
594 let diagnostics = Arc::new(DiagnosticHook::new());
595 install_device_callbacks(&device, &diagnostics);
596 Ok(Self {
597 adapter,
598 device,
599 queue: LockedQueue::shared(queue, None),
600 capabilities,
601 diagnostics,
602 })
603 }
604
605 pub fn set_diagnostic_handler(&self, handler: impl Fn(GpuDiagnostic) + Send + Sync + 'static) {
614 let handler: GpuDiagnosticHandler = Arc::new(handler);
615 self.diagnostics.set(Some(handler));
616 }
617
618 pub fn clear_diagnostic_handler(&self) {
620 self.diagnostics.set(None);
621 }
622
623 #[must_use]
625 pub fn capabilities(&self) -> &Capabilities {
626 &self.capabilities
627 }
628
629 #[must_use]
638 pub fn with_rgba8_intermediate(&self) -> Self {
639 let mut capabilities = Capabilities::clone(&self.capabilities);
640 capabilities.intermediate_format = IntermediateFormat::Rgba8Unorm;
641 Self {
642 capabilities: Arc::new(capabilities),
643 ..self.clone()
644 }
645 }
646
647 #[must_use]
656 pub fn with_tier(&self, tier: CapabilityTier) -> Self {
657 let mut capabilities = Capabilities::clone(&self.capabilities);
658 capabilities.tier = tier;
659 Self {
660 capabilities: Arc::new(capabilities),
661 ..self.clone()
662 }
663 }
664
665 #[must_use]
673 #[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
674 pub fn surface_config(
675 &self,
676 surface: &wgpu::Surface<'_>,
677 width: u32,
678 height: u32,
679 ) -> Option<wgpu::SurfaceConfiguration> {
680 let mut config = surface.get_default_config(&self.adapter, width, height)?;
681 if let Some(format) = surface
682 .get_capabilities(&self.adapter)
683 .formats
684 .into_iter()
685 .find(|format| !format.is_srgb())
686 {
687 config.format = format;
688 }
689 Some(config)
690 }
691}
692
693#[derive(Debug)]
697#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
698pub struct AcceptedDevice<T, E> {
699 pub context: GpuContext,
701 pub accepted: Option<T>,
703 pub refusals: AdapterRefusals<E>,
705}
706
707#[derive(Debug)]
710#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
711pub struct AdapterRefusals<E>(pub Vec<(String, E)>);
712
713impl<E: std::fmt::Display> std::fmt::Display for AdapterRefusals<E> {
714 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 for (index, (adapter, error)) in self.0.iter().enumerate() {
716 if index > 0 {
717 formatter.write_str("; ")?;
718 }
719 write!(formatter, "{adapter}: {error}")?;
720 }
721 Ok(())
722 }
723}
724
725impl<E: std::fmt::Display + std::fmt::Debug> std::error::Error for AdapterRefusals<E> {}
726
727#[cfg(not(target_arch = "wasm32"))]
731fn fallback_rank(device_type: wgpu::DeviceType, preference: wgpu::PowerPreference) -> u8 {
732 use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
733 let order: &[wgpu::DeviceType] = match preference {
734 wgpu::PowerPreference::HighPerformance => {
735 &[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
736 }
737 wgpu::PowerPreference::LowPower => &[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu],
738 wgpu::PowerPreference::None => return 0,
739 };
740 order
741 .iter()
742 .position(|kind| *kind == device_type)
743 .and_then(|rank| u8::try_from(rank).ok())
744 .unwrap_or(u8::MAX)
745}
746
747#[cfg(not(target_arch = "wasm32"))]
750fn install_device_callbacks(device: &wgpu::Device, diagnostics: &Arc<DiagnosticHook>) {
751 let uncaptured = Arc::clone(diagnostics);
752 device.on_uncaptured_error(Arc::new(move |error| {
753 uncaptured.emit(GpuDiagnostic::uncaptured(&error));
754 }));
755 let lost = Arc::clone(diagnostics);
756 device.set_device_lost_callback(move |reason, message| {
757 lost.emit(GpuDiagnostic::DeviceLost {
758 reason: format!("{reason:?}"),
759 message,
760 });
761 });
762}
763
764#[cfg(target_arch = "wasm32")]
765fn install_device_callbacks(_device: &wgpu::Device, _diagnostics: &Arc<DiagnosticHook>) {}
766
767#[cfg(all(test, not(target_arch = "wasm32")))]
768mod tests {
769 use super::fallback_rank;
770 use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
771 use wgpu::PowerPreference;
772
773 #[test]
774 fn fallback_adapters_follow_the_preference_and_a_cpu_comes_last() {
775 let ordered = |preference| {
776 let mut kinds = vec![Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu];
777 kinds.sort_by_key(|kind| fallback_rank(*kind, preference));
778 kinds
779 };
780 assert_eq!(
781 ordered(PowerPreference::HighPerformance),
782 [DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
783 );
784 assert_eq!(
785 ordered(PowerPreference::LowPower),
786 [IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu]
787 );
788 assert_eq!(
789 ordered(PowerPreference::None),
790 [Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu]
791 );
792 }
793}