use std::sync::Arc;
use std::time::Duration;
use web_time::Instant;
use crate::diagnostic::DiagnosticHook;
use crate::{
Capabilities, CapabilityTier, GpuDiagnostic, GpuDiagnosticHandler, GpuError, IntermediateFormat,
};
#[derive(Clone, Debug)]
pub struct GpuContext {
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: Arc<LockedQueue>,
capabilities: Arc<Capabilities>,
diagnostics: Arc<DiagnosticHook>,
}
#[derive(Debug)]
struct LockedQueue {
queue: wgpu::Queue,
lock: QueueLockSlot,
}
#[derive(Debug)]
struct QueueLockSlot {
lock: Option<Arc<dyn ExternalQueueLock>>,
held: bool,
}
impl LockedQueue {
#[cfg_attr(target_arch = "wasm32", allow(clippy::arc_with_non_send_sync))]
fn shared(queue: wgpu::Queue, lock: Option<Arc<dyn ExternalQueueLock>>) -> Arc<Self> {
Arc::new(Self {
queue,
lock: QueueLockSlot { lock, held: false },
})
}
fn locked<T>(&self, call: impl FnOnce(&wgpu::Queue) -> T) -> T {
let guard = self.lock.lock.as_ref().map(|lock| {
lock.lock();
QueueGuard(lock.as_ref())
});
let result = call(&self.queue);
drop(guard);
result
}
}
impl Drop for LockedQueue {
fn drop(&mut self) {
if let Some(lock) = &self.lock.lock {
lock.lock();
self.lock.held = true;
}
}
}
impl Drop for QueueLockSlot {
fn drop(&mut self) {
if self.held
&& let Some(lock) = &self.lock
{
lock.unlock();
}
}
}
pub trait ExternalQueueLock: std::fmt::Debug + Send + Sync + 'static {
fn lock(&self);
fn unlock(&self);
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub struct SubmitTimings {
pub queue_lock_wait: Duration,
pub queue_submit: Duration,
}
struct QueueGuard<'a>(&'a dyn ExternalQueueLock);
impl Drop for QueueGuard<'_> {
fn drop(&mut self) {
self.0.unlock();
}
}
impl GpuContext {
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn from_parts(adapter: wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Self {
Self::from_parts_with_queue_lock(adapter, device, queue, None)
}
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn from_parts_with_queue_lock(
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
queue_lock: Option<Arc<dyn ExternalQueueLock>>,
) -> Self {
let capabilities = Arc::new(Capabilities::from_adapter(&adapter));
let diagnostics = Arc::new(DiagnosticHook::new());
install_device_callbacks(&device, &diagnostics);
Self {
adapter,
device,
queue: LockedQueue::shared(queue, queue_lock),
capabilities,
diagnostics,
}
}
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub const fn device(&self) -> &wgpu::Device {
&self.device
}
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn queue(&self) -> &wgpu::Queue {
&self.queue.queue
}
pub fn on_submitted_work_done(&self, callback: impl FnOnce() + Send + 'static) {
#[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
self.queue
.locked(|queue| queue.on_submitted_work_done(callback));
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn map_async(
&self,
slice: &wgpu::BufferSlice<'_>,
mode: wgpu::MapMode,
callback: impl FnOnce(Result<(), wgpu::BufferAsyncError>) + wgpu::WasmNotSend + 'static,
) {
#[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
self.queue.locked(|_| slice.map_async(mode, callback));
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn present(&self, texture: wgpu::SurfaceTexture) {
#[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
self.queue.locked(|queue| queue.present(texture));
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn submit<I>(&self, command_buffers: I) -> wgpu::SubmissionIndex
where
I: IntoIterator<Item = wgpu::CommandBuffer>,
{
self.submit_measured(command_buffers, false).0
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn submit_timed<I>(&self, command_buffers: I) -> (wgpu::SubmissionIndex, SubmitTimings)
where
I: IntoIterator<Item = wgpu::CommandBuffer>,
{
self.submit_measured(command_buffers, true)
}
fn submit_measured<I>(
&self,
command_buffers: I,
measure: bool,
) -> (wgpu::SubmissionIndex, SubmitTimings)
where
I: IntoIterator<Item = wgpu::CommandBuffer>,
{
let mut timings = SubmitTimings::default();
let started = measure.then(Instant::now);
let index = self.queue.locked(|queue| {
let acquired = started.map(|started| {
let now = Instant::now();
timings.queue_lock_wait = now.duration_since(started);
now
});
#[expect(clippy::disallowed_methods, reason = "this is the locked wrapper")]
let index = queue.submit(command_buffers);
if let Some(acquired) = acquired {
timings.queue_submit = acquired.elapsed();
}
index
});
(index, timings)
}
pub async fn request() -> Result<Self, GpuError> {
let _turn = crate::serialize_acquisition();
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
Self::request_compatible(&instance, None).await
}
pub async fn request_high_performance() -> Result<Self, GpuError> {
Self::request_high_performance_without(wgpu::Features::empty()).await
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn request_high_performance_without(
withheld: wgpu::Features,
) -> Result<Self, GpuError> {
let _turn = crate::serialize_acquisition();
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let preference =
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
Self::request_compatible_with_preference(&instance, None, preference, withheld).await
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn request_high_performance_on(backends: wgpu::Backends) -> Result<Self, GpuError> {
let _turn = crate::serialize_acquisition();
let instance = Self::instance_on(backends);
let preference =
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
Self::request_compatible_with_preference(
&instance,
None,
preference,
wgpu::Features::empty(),
)
.await
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn request_high_performance_on_accepted<T, E>(
backends: wgpu::Backends,
mut accept: impl FnMut(&Self) -> Result<T, E>,
) -> Result<AcceptedDevice<T, E>, GpuError> {
let _turn = crate::serialize_acquisition();
let instance = Self::instance_on(backends);
let preference =
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
let preferred = Self::request_compatible_with_preference(
&instance,
None,
preference,
wgpu::Features::empty(),
)
.await?;
let mut refusals = AdapterRefusals(Vec::new());
match accept(&preferred) {
Ok(value) => {
return Ok(AcceptedDevice {
context: preferred,
accepted: Some(value),
refusals,
});
}
Err(error) => refusals.0.push((preferred.adapter.get_info().name, error)),
}
let tried = preferred.adapter.get_info();
let mut others: Vec<wgpu::Adapter> = instance
.enumerate_adapters(wgpu::Backends::all())
.await
.into_iter()
.filter(|other| {
let info = other.get_info();
info != tried && info.device_type != wgpu::DeviceType::Cpu
})
.collect();
others.sort_by_key(|other| fallback_rank(other.get_info().device_type, preference));
for other in others {
let name = other.get_info().name;
let Ok(context) = Self::open_device(other, wgpu::Features::empty()).await else {
continue;
};
match accept(&context) {
Ok(value) => {
return Ok(AcceptedDevice {
context,
accepted: Some(value),
refusals,
});
}
Err(error) => refusals.0.push((name, error)),
}
}
Ok(AcceptedDevice {
context: preferred,
accepted: None,
refusals,
})
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn adapters_by_preference_on(
backends: wgpu::Backends,
) -> Vec<(usize, wgpu::Adapter)> {
let _turn = crate::serialize_acquisition();
let instance = Self::instance_on(backends);
let preference =
wgpu::PowerPreference::from_env().unwrap_or(wgpu::PowerPreference::HighPerformance);
let preferred = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: preference,
..Default::default()
})
.await
.ok()
.map(|adapter| adapter.get_info());
let mut adapters: Vec<(usize, wgpu::Adapter)> = instance
.enumerate_adapters(wgpu::Backends::all())
.await
.into_iter()
.enumerate()
.collect();
adapters.sort_by_key(|(_, adapter)| {
let info = adapter.get_info();
if info.device_type == wgpu::DeviceType::Cpu {
(2, u8::MAX)
} else if preferred.as_ref() == Some(&info) {
(0, 0)
} else {
(1, fallback_rank(info.device_type, preference))
}
});
adapters
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn open_adapter(adapter: wgpu::Adapter) -> Result<Self, GpuError> {
let _turn = crate::serialize_acquisition();
Self::open_device(adapter, wgpu::Features::empty()).await
}
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub const fn adapter(&self) -> &wgpu::Adapter {
&self.adapter
}
fn instance_on(backends: wgpu::Backends) -> wgpu::Instance {
let mut descriptor = wgpu::InstanceDescriptor::new_without_display_handle();
descriptor.backends = backends;
wgpu::Instance::new(descriptor.with_env())
}
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub async fn request_compatible(
instance: &wgpu::Instance,
compatible_surface: Option<&wgpu::Surface<'_>>,
) -> Result<Self, GpuError> {
let preference = wgpu::PowerPreference::from_env().unwrap_or_default();
Self::request_compatible_with_preference(
instance,
compatible_surface,
preference,
wgpu::Features::empty(),
)
.await
}
async fn request_compatible_with_preference(
instance: &wgpu::Instance,
compatible_surface: Option<&wgpu::Surface<'_>>,
power_preference: wgpu::PowerPreference,
withheld: wgpu::Features,
) -> Result<Self, GpuError> {
let _turn = crate::serialize_acquisition();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
compatible_surface,
power_preference,
..Default::default()
})
.await
.map_err(GpuError::adapter)?;
#[cfg(not(target_arch = "wasm32"))]
let preferred = adapter.get_info();
let error = match Self::open_device(adapter, withheld).await {
Ok(context) => return Ok(context),
Err(error) => error,
};
#[cfg(not(target_arch = "wasm32"))]
{
let mut others: Vec<wgpu::Adapter> = instance
.enumerate_adapters(wgpu::Backends::all())
.await
.into_iter()
.filter(|other| other.get_info() != preferred)
.filter(|other| {
compatible_surface.is_none_or(|surface| other.is_surface_supported(surface))
})
.collect();
others
.sort_by_key(|other| fallback_rank(other.get_info().device_type, power_preference));
for other in others {
if let Ok(context) = Self::open_device(other, withheld).await {
return Ok(context);
}
}
}
Err(error)
}
async fn open_device(
adapter: wgpu::Adapter,
withheld: wgpu::Features,
) -> Result<Self, GpuError> {
let capabilities = Arc::new(Capabilities::from_adapter(&adapter));
let optional_features = wgpu::Features::TIMESTAMP_QUERY
| wgpu::Features::TEXTURE_FORMAT_16BIT_NORM
| if cfg!(target_os = "windows") {
wgpu::Features::TEXTURE_FORMAT_NV12 | wgpu::Features::TEXTURE_FORMAT_P010
} else {
wgpu::Features::empty()
};
let required_features = adapter.features() & optional_features & !withheld;
let required_limits =
wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits());
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
required_features,
required_limits,
..wgpu::DeviceDescriptor::default()
})
.await
.map_err(GpuError::device)?;
let diagnostics = Arc::new(DiagnosticHook::new());
install_device_callbacks(&device, &diagnostics);
Ok(Self {
adapter,
device,
queue: LockedQueue::shared(queue, None),
capabilities,
diagnostics,
})
}
pub fn set_diagnostic_handler(&self, handler: impl Fn(GpuDiagnostic) + Send + Sync + 'static) {
let handler: GpuDiagnosticHandler = Arc::new(handler);
self.diagnostics.set(Some(handler));
}
pub fn clear_diagnostic_handler(&self) {
self.diagnostics.set(None);
}
#[must_use]
pub fn capabilities(&self) -> &Capabilities {
&self.capabilities
}
#[must_use]
pub fn with_rgba8_intermediate(&self) -> Self {
let mut capabilities = Capabilities::clone(&self.capabilities);
capabilities.intermediate_format = IntermediateFormat::Rgba8Unorm;
Self {
capabilities: Arc::new(capabilities),
..self.clone()
}
}
#[must_use]
pub fn with_tier(&self, tier: CapabilityTier) -> Self {
let mut capabilities = Capabilities::clone(&self.capabilities);
capabilities.tier = tier;
Self {
capabilities: Arc::new(capabilities),
..self.clone()
}
}
#[must_use]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub fn surface_config(
&self,
surface: &wgpu::Surface<'_>,
width: u32,
height: u32,
) -> Option<wgpu::SurfaceConfiguration> {
let mut config = surface.get_default_config(&self.adapter, width, height)?;
if let Some(format) = surface
.get_capabilities(&self.adapter)
.formats
.into_iter()
.find(|format| !format.is_srgb())
{
config.format = format;
}
Some(config)
}
}
#[derive(Debug)]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub struct AcceptedDevice<T, E> {
pub context: GpuContext,
pub accepted: Option<T>,
pub refusals: AdapterRefusals<E>,
}
#[derive(Debug)]
#[cfg_attr(not(feature = "wgpu-interop"), doc(hidden))]
pub struct AdapterRefusals<E>(pub Vec<(String, E)>);
impl<E: std::fmt::Display> std::fmt::Display for AdapterRefusals<E> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (index, (adapter, error)) in self.0.iter().enumerate() {
if index > 0 {
formatter.write_str("; ")?;
}
write!(formatter, "{adapter}: {error}")?;
}
Ok(())
}
}
impl<E: std::fmt::Display + std::fmt::Debug> std::error::Error for AdapterRefusals<E> {}
#[cfg(not(target_arch = "wasm32"))]
fn fallback_rank(device_type: wgpu::DeviceType, preference: wgpu::PowerPreference) -> u8 {
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
let order: &[wgpu::DeviceType] = match preference {
wgpu::PowerPreference::HighPerformance => {
&[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
}
wgpu::PowerPreference::LowPower => &[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu],
wgpu::PowerPreference::None => return 0,
};
order
.iter()
.position(|kind| *kind == device_type)
.and_then(|rank| u8::try_from(rank).ok())
.unwrap_or(u8::MAX)
}
#[cfg(not(target_arch = "wasm32"))]
fn install_device_callbacks(device: &wgpu::Device, diagnostics: &Arc<DiagnosticHook>) {
let uncaptured = Arc::clone(diagnostics);
device.on_uncaptured_error(Arc::new(move |error| {
uncaptured.emit(GpuDiagnostic::uncaptured(&error));
}));
let lost = Arc::clone(diagnostics);
device.set_device_lost_callback(move |reason, message| {
lost.emit(GpuDiagnostic::DeviceLost {
reason: format!("{reason:?}"),
message,
});
});
}
#[cfg(target_arch = "wasm32")]
fn install_device_callbacks(_device: &wgpu::Device, _diagnostics: &Arc<DiagnosticHook>) {}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::fallback_rank;
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
use wgpu::PowerPreference;
#[test]
fn fallback_adapters_follow_the_preference_and_a_cpu_comes_last() {
let ordered = |preference| {
let mut kinds = vec![Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu];
kinds.sort_by_key(|kind| fallback_rank(*kind, preference));
kinds
};
assert_eq!(
ordered(PowerPreference::HighPerformance),
[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
);
assert_eq!(
ordered(PowerPreference::LowPower),
[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu]
);
assert_eq!(
ordered(PowerPreference::None),
[Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu]
);
}
}