use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum FeatureLevel {
Core,
Compatibility,
}
#[derive(Debug)]
pub struct NoDevice {
pub refused: Vec<RefusedAdapter>,
pub no_adapter: Option<wgpu::RequestAdapterError>,
}
#[derive(Debug)]
pub struct RefusedAdapter {
pub adapter: wgpu::AdapterInfo,
pub refusal: Refusal,
}
#[derive(Debug)]
pub enum Refusal {
Downlevel,
NotFallback,
SurfaceUnsupported,
Device(wgpu::RequestDeviceError),
}
pub async fn request_device(
instance: &wgpu::Instance,
options: &wgpu::RequestAdapterOptions<'_, '_>,
level: FeatureLevel,
) -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue), NoDevice> {
let found = Found::of(instance, options).await?;
let ranking = Ranking::of(&found.candidates, &found.choice, level);
let adapters = found.adapters;
let mut refused = Vec::new();
for &index in &ranking.order {
let adapter = &adapters[index];
match adapter.request_device(&device_descriptor(adapter)).await {
Ok((device, queue)) => return Ok((adapter.clone(), device, queue)),
Err(error) => refused.push(RefusedAdapter {
adapter: adapter.get_info(),
refusal: Refusal::Device(error),
}),
}
}
refused.extend(
ranking
.unqualified
.into_iter()
.map(|(index, refusal)| RefusedAdapter {
adapter: adapters[index].get_info(),
refusal,
}),
);
Err(NoDevice {
refused,
no_adapter: None,
})
}
struct Found {
adapters: Vec<wgpu::Adapter>,
candidates: Vec<Candidate>,
choice: Choice,
}
impl Found {
#[cfg(not(target_arch = "wasm32"))]
async fn of(
instance: &wgpu::Instance,
options: &wgpu::RequestAdapterOptions<'_, '_>,
) -> Result<Self, NoDevice> {
let adapters = instance.enumerate_adapters(wgpu::Backends::all()).await;
let candidates = adapters
.iter()
.map(|adapter| Candidate::of(adapter, options.compatible_surface))
.collect();
Ok(Self {
adapters,
candidates,
choice: Choice::from_options(options),
})
}
#[cfg(target_arch = "wasm32")]
async fn of(
instance: &wgpu::Instance,
options: &wgpu::RequestAdapterOptions<'_, '_>,
) -> Result<Self, NoDevice> {
let adapter = instance
.request_adapter(options)
.await
.map_err(|error| NoDevice {
refused: Vec::new(),
no_adapter: Some(error),
})?;
Ok(Self {
candidates: vec![Candidate::of(&adapter, None)],
adapters: vec![adapter],
choice: Choice::default(),
})
}
}
fn device_descriptor(adapter: &wgpu::Adapter) -> wgpu::DeviceDescriptor<'static> {
let wanted = wgpu::Features::TIMESTAMP_QUERY | wgpu::Features::MAPPABLE_PRIMARY_BUFFERS;
wgpu::DeviceDescriptor {
label: Some("valo"),
required_features: adapter.features() & wanted,
required_limits: adapter.limits(),
..Default::default()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Candidate {
device_type: wgpu::DeviceType,
webgpu_compliant: bool,
presents: bool,
}
impl Candidate {
fn of(adapter: &wgpu::Adapter, surface: Option<&wgpu::Surface<'_>>) -> Self {
Self {
device_type: adapter.get_info().device_type,
webgpu_compliant: adapter.get_downlevel_capabilities().is_webgpu_compliant(),
presents: surface.is_none_or(|surface| adapter.is_surface_supported(surface)),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Choice {
power_preference: wgpu::PowerPreference,
force_fallback_adapter: bool,
}
impl Choice {
#[cfg(not(target_arch = "wasm32"))]
fn from_options(options: &wgpu::RequestAdapterOptions<'_, '_>) -> Self {
Self {
power_preference: options.power_preference,
force_fallback_adapter: options.force_fallback_adapter,
}
}
fn rank(&self, device_type: wgpu::DeviceType) -> u8 {
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
use wgpu::PowerPreference::{HighPerformance, LowPower};
match (self.power_preference, device_type) {
(_, Cpu) => 4,
(HighPerformance, DiscreteGpu) | (LowPower, IntegratedGpu) => 0,
(HighPerformance, IntegratedGpu) | (LowPower, DiscreteGpu) => 1,
(HighPerformance | LowPower, VirtualGpu) => 2,
(HighPerformance | LowPower, Other) => 3,
_ => 0,
}
}
fn refusal(&self, candidate: &Candidate, level: FeatureLevel) -> Option<Refusal> {
if !candidate.presents {
Some(Refusal::SurfaceUnsupported)
} else if self.force_fallback_adapter && candidate.device_type != wgpu::DeviceType::Cpu {
Some(Refusal::NotFallback)
} else if level == FeatureLevel::Core && !candidate.webgpu_compliant {
Some(Refusal::Downlevel)
} else {
None
}
}
}
#[derive(Debug)]
struct Ranking {
order: Vec<usize>,
unqualified: Vec<(usize, Refusal)>,
}
impl Ranking {
fn of(candidates: &[Candidate], choice: &Choice, level: FeatureLevel) -> Self {
let mut order = Vec::new();
let mut unqualified = Vec::new();
for (index, candidate) in candidates.iter().enumerate() {
match choice.refusal(candidate, level) {
Some(refusal) => unqualified.push((index, refusal)),
None => order.push(index),
}
}
order.sort_by_key(|&index| choice.rank(candidates[index].device_type));
Self { order, unqualified }
}
}
#[cfg(test)]
pub(crate) fn test_device() -> Option<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let options = wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
..Default::default()
};
pollster::block_on(request_device(&instance, &options, FeatureLevel::Core)).ok()
}
impl fmt::Display for NoDevice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(error) = &self.no_adapter {
return write!(f, "no adapter: {error}");
}
if self.refused.is_empty() {
return write!(f, "no adapter found");
}
write!(f, "no adapter gave a device:")?;
for refused in &self.refused {
let adapter = &refused.adapter;
write!(f, " {} ({:?}): ", adapter.name, adapter.backend)?;
match &refused.refusal {
Refusal::Downlevel => write!(f, "not fully WebGPU-compliant;")?,
Refusal::NotFallback => write!(f, "not a software adapter;")?,
Refusal::SurfaceUnsupported => write!(f, "cannot present to the surface;")?,
Refusal::Device(error) => write!(f, "refused the device: {error};")?,
}
}
Ok(())
}
}
impl std::error::Error for NoDevice {}
#[cfg(test)]
mod tests {
use super::*;
use wgpu::DeviceType::{Cpu, DiscreteGpu, IntegratedGpu, Other, VirtualGpu};
fn adapter(device_type: wgpu::DeviceType, webgpu_compliant: bool) -> Candidate {
Candidate {
device_type,
webgpu_compliant,
presents: true,
}
}
fn choice(power_preference: wgpu::PowerPreference) -> Choice {
Choice {
power_preference,
force_fallback_adapter: false,
}
}
fn every_type() -> Vec<Candidate> {
[Cpu, Other, VirtualGpu, IntegratedGpu, DiscreteGpu]
.map(|device_type| adapter(device_type, true))
.to_vec()
}
fn types_in_order(candidates: &[Candidate], ranking: &Ranking) -> Vec<wgpu::DeviceType> {
ranking
.order
.iter()
.map(|&index| candidates[index].device_type)
.collect()
}
#[test]
fn high_performance_ranks_discrete_integrated_virtual_other_then_software() {
let candidates = every_type();
let ranking = Ranking::of(
&candidates,
&choice(wgpu::PowerPreference::HighPerformance),
FeatureLevel::Core,
);
assert_eq!(
types_in_order(&candidates, &ranking),
[DiscreteGpu, IntegratedGpu, VirtualGpu, Other, Cpu]
);
}
#[test]
fn low_power_ranks_integrated_first() {
let candidates = every_type();
let ranking = Ranking::of(
&candidates,
&choice(wgpu::PowerPreference::LowPower),
FeatureLevel::Core,
);
assert_eq!(
types_in_order(&candidates, &ranking),
[IntegratedGpu, DiscreteGpu, VirtualGpu, Other, Cpu]
);
}
#[test]
fn no_preference_keeps_the_platform_order_with_software_last() {
let candidates = every_type();
let ranking = Ranking::of(
&candidates,
&choice(wgpu::PowerPreference::None),
FeatureLevel::Core,
);
assert_eq!(
types_in_order(&candidates, &ranking),
[Other, VirtualGpu, IntegratedGpu, DiscreteGpu, Cpu]
);
}
#[test]
fn core_passes_over_a_downlevel_adapter() {
let candidates = vec![adapter(IntegratedGpu, false), adapter(Cpu, true)];
let preference = choice(wgpu::PowerPreference::HighPerformance);
let core = Ranking::of(&candidates, &preference, FeatureLevel::Core);
assert_eq!(core.order, [1]);
assert!(matches!(core.unqualified[..], [(0, Refusal::Downlevel)]));
let compatibility = Ranking::of(&candidates, &preference, FeatureLevel::Compatibility);
assert_eq!(compatibility.order, [0, 1]);
assert!(compatibility.unqualified.is_empty());
}
#[test]
fn forcing_the_fallback_admits_software_adapters_only() {
let candidates = every_type();
let forced = Choice {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: true,
};
let ranking = Ranking::of(&candidates, &forced, FeatureLevel::Core);
assert_eq!(types_in_order(&candidates, &ranking), [Cpu]);
assert_eq!(ranking.unqualified.len(), 4);
assert!(ranking
.unqualified
.iter()
.all(|(_, refusal)| matches!(refusal, Refusal::NotFallback)));
}
#[test]
fn an_adapter_that_cannot_present_does_not_qualify() {
let candidates = vec![
Candidate {
presents: false,
..adapter(DiscreteGpu, true)
},
adapter(IntegratedGpu, true),
];
let ranking = Ranking::of(
&candidates,
&choice(wgpu::PowerPreference::HighPerformance),
FeatureLevel::Compatibility,
);
assert_eq!(ranking.order, [1]);
assert!(matches!(
ranking.unqualified[..],
[(0, Refusal::SurfaceUnsupported)]
));
}
#[test]
fn core_opens_a_device_on_a_compliant_adapter() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let compliant = pollster::block_on(instance.enumerate_adapters(wgpu::Backends::all()))
.iter()
.any(|adapter| adapter.get_downlevel_capabilities().is_webgpu_compliant());
match test_device() {
Some((adapter, _, _)) => {
assert!(adapter.get_downlevel_capabilities().is_webgpu_compliant());
}
None => {
assert!(
!compliant,
"a compliant adapter is there but gave no device"
);
eprintln!("SKIP core_opens_a_device_on_a_compliant_adapter: no compliant adapter");
}
}
}
}