#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GpuBackendTier {
Primary,
OpenGlEs,
Software,
}
impl GpuBackendTier {
pub fn label(self) -> &'static str {
match self {
Self::Primary => "primary (Vulkan/Metal/DX12/WebGPU)",
Self::OpenGlEs => "OpenGL ES (GL backend)",
Self::Software => "software (CPU fallback)",
}
}
pub fn is_degraded(self) -> bool {
self != Self::Primary
}
pub fn from_backend(backend: wgpu::Backend) -> Self {
match backend {
wgpu::Backend::Vulkan | wgpu::Backend::Metal | wgpu::Backend::Dx12 => Self::Primary,
wgpu::Backend::Gl => Self::OpenGlEs,
wgpu::Backend::BrowserWebGpu => Self::Primary,
wgpu::Backend::Noop => Self::Software,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GpuBackendSelection {
pub tier: GpuBackendTier,
pub backend: wgpu::Backend,
}
impl GpuBackendSelection {
pub fn is_degraded(&self) -> bool {
self.tier.is_degraded()
}
}
pub const BACKEND_LADDER: &[(GpuBackendTier, wgpu::Backends)] = &[
(GpuBackendTier::Primary, wgpu::Backends::PRIMARY),
(GpuBackendTier::OpenGlEs, wgpu::Backends::GL),
(GpuBackendTier::Software, wgpu::Backends::empty()),
];
pub fn ladder_for_target() -> &'static [(GpuBackendTier, wgpu::Backends)] {
#[cfg(target_arch = "wasm32")]
{
&[(GpuBackendTier::Primary, wgpu::Backends::BROWSER_WEBGPU)]
}
#[cfg(not(target_arch = "wasm32"))]
{
BACKEND_LADDER
}
}
pub fn instance_backends() -> wgpu::Backends {
let from_ladder = ladder_for_target()
.iter()
.fold(wgpu::Backends::empty(), |acc, (_, backends)| acc | *backends);
match backends_from_env() {
Some(pinned) => from_ladder | pinned,
None => from_ladder,
}
}
pub fn backends_from_env() -> Option<wgpu::Backends> {
wgpu::Backends::from_env()
}
fn tier_of_adapter(device_type: wgpu::DeviceType, backend: wgpu::Backend) -> GpuBackendTier {
if device_type == wgpu::DeviceType::Cpu {
GpuBackendTier::Software
} else {
GpuBackendTier::from_backend(backend)
}
}
fn rank_adapter(device_type: wgpu::DeviceType, power_preference: wgpu::PowerPreference) -> u8 {
match device_type {
wgpu::DeviceType::Cpu => 4,
wgpu::DeviceType::DiscreteGpu => match power_preference {
wgpu::PowerPreference::LowPower => 1,
_ => 0,
},
wgpu::DeviceType::IntegratedGpu => match power_preference {
wgpu::PowerPreference::HighPerformance => 1,
_ => 0,
},
wgpu::DeviceType::VirtualGpu => 2,
wgpu::DeviceType::Other => 3,
}
}
fn best_candidate_index(
candidates: &[(wgpu::DeviceType, bool)],
power_preference: wgpu::PowerPreference,
) -> Option<usize> {
let mut best: Option<(usize, u8)> = None;
for (index, &(device_type, surface_ok)) in candidates.iter().enumerate() {
if !surface_ok {
continue;
}
let rank = rank_adapter(device_type, power_preference);
match best {
Some((_, best_rank)) if best_rank <= rank => {}
_ => best = Some((index, rank)),
}
}
best.map(|(index, _)| index)
}
async fn best_adapter_for_backends(
instance: &wgpu::Instance,
backends: wgpu::Backends,
power_preference: wgpu::PowerPreference,
compatible_surface: Option<&wgpu::Surface<'_>>,
) -> Option<wgpu::Adapter> {
let mut candidates = instance.enumerate_adapters(backends).await;
let meta: Vec<(wgpu::DeviceType, bool)> = candidates
.iter()
.map(|adapter| {
let info = adapter.get_info();
let surface_ok = match compatible_surface {
Some(surface) => adapter.is_surface_supported(surface),
None => true,
};
(info.device_type, surface_ok)
})
.collect();
let index = best_candidate_index(&meta, power_preference)?;
Some(candidates.swap_remove(index))
}
pub async fn select_adapter_with_gl_fallback(
instance: &wgpu::Instance,
power_preference: wgpu::PowerPreference,
compatible_surface: Option<&wgpu::Surface<'_>>,
) -> Option<(wgpu::Adapter, GpuBackendTier)> {
if let Some(pinned) = backends_from_env() {
let candidates = instance.enumerate_adapters(pinned).await;
let meta: Vec<(wgpu::DeviceType, bool)> = candidates
.iter()
.map(|adapter| {
let info = adapter.get_info();
let surface_ok = match compatible_surface {
Some(surface) => adapter.is_surface_supported(surface),
None => true,
};
(info.device_type, surface_ok)
})
.collect();
match best_candidate_index(&meta, power_preference) {
Some(index) => {
let adapter = &candidates[index];
let backend = adapter.get_info().backend;
let mut candidates = candidates;
let adapter = candidates.swap_remove(index);
return Some((adapter, GpuBackendTier::from_backend(backend)));
}
None => {
log::error!(
"[gpu] WGPU_BACKEND pinned {pinned:?} but no surface-compatible adapter \
exposes that backend"
);
return None;
}
}
}
let ladder = ladder_for_target();
for (tier, backends) in ladder.iter() {
let adapter = if backends.is_empty() {
match instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface,
force_fallback_adapter: true,
apply_limit_buckets: false,
})
.await
{
Ok(adapter) => adapter,
Err(error) => {
log::debug!(
"[gpu] fallback adapter request failed for tier {}: {error:?}",
tier.label(),
);
continue;
}
}
} else {
match best_adapter_for_backends(
instance,
*backends,
power_preference,
compatible_surface,
)
.await
{
Some(adapter) => adapter,
None => {
log::debug!("[gpu] no adapter on tier {}", tier.label());
continue;
}
}
};
let info = adapter.get_info();
return Some((adapter, tier_of_adapter(info.device_type, info.backend)));
}
log::warn!("[gpu] no adapter available on any tier of the degradation ladder");
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ladder_is_ordered_from_primary_to_software() {
let tiers: Vec<GpuBackendTier> = BACKEND_LADDER.iter().map(|(tier, _)| *tier).collect();
assert_eq!(
tiers,
vec![GpuBackendTier::Primary, GpuBackendTier::OpenGlEs, GpuBackendTier::Software]
);
assert!(GpuBackendTier::Primary < GpuBackendTier::OpenGlEs);
assert!(GpuBackendTier::OpenGlEs < GpuBackendTier::Software);
}
#[test]
fn opengl_es_rung_requests_the_gl_backend() {
let (_, backends) =
BACKEND_LADDER.iter().find(|(t, _)| *t == GpuBackendTier::OpenGlEs).expect("GL rung");
assert_eq!(*backends, wgpu::Backends::GL);
assert!(backends.contains(wgpu::Backends::GL));
}
#[test]
fn primary_rung_excludes_gl() {
let (_, backends) = BACKEND_LADDER
.iter()
.find(|(t, _)| *t == GpuBackendTier::Primary)
.expect("primary rung");
assert_eq!(*backends, wgpu::Backends::PRIMARY);
assert!(!backends.contains(wgpu::Backends::GL));
}
#[test]
fn backend_maps_to_expected_tier() {
assert_eq!(GpuBackendTier::from_backend(wgpu::Backend::Vulkan), GpuBackendTier::Primary);
assert_eq!(GpuBackendTier::from_backend(wgpu::Backend::Metal), GpuBackendTier::Primary);
assert_eq!(GpuBackendTier::from_backend(wgpu::Backend::Dx12), GpuBackendTier::Primary);
assert_eq!(GpuBackendTier::from_backend(wgpu::Backend::Gl), GpuBackendTier::OpenGlEs);
assert_eq!(GpuBackendTier::from_backend(wgpu::Backend::Noop), GpuBackendTier::Software);
}
#[test]
fn degradation_is_reported_for_non_primary_tiers() {
assert!(!GpuBackendTier::Primary.is_degraded());
assert!(GpuBackendTier::OpenGlEs.is_degraded());
assert!(GpuBackendTier::Software.is_degraded());
let gl = GpuBackendSelection { tier: GpuBackendTier::OpenGlEs, backend: wgpu::Backend::Gl };
assert!(gl.is_degraded());
}
#[test]
fn cpu_adapter_maps_to_software_regardless_of_backend() {
assert_eq!(
tier_of_adapter(wgpu::DeviceType::Cpu, wgpu::Backend::Vulkan),
GpuBackendTier::Software,
);
assert_eq!(
tier_of_adapter(wgpu::DeviceType::Cpu, wgpu::Backend::Gl),
GpuBackendTier::Software,
);
assert_eq!(
tier_of_adapter(wgpu::DeviceType::Cpu, wgpu::Backend::Noop),
GpuBackendTier::Software,
);
assert_eq!(
tier_of_adapter(wgpu::DeviceType::DiscreteGpu, wgpu::Backend::Metal),
GpuBackendTier::Primary,
);
assert_eq!(
tier_of_adapter(wgpu::DeviceType::IntegratedGpu, wgpu::Backend::Gl),
GpuBackendTier::OpenGlEs,
);
}
#[test]
fn adapter_ranking_honours_power_preference() {
assert!(
rank_adapter(wgpu::DeviceType::DiscreteGpu, wgpu::PowerPreference::HighPerformance)
< rank_adapter(
wgpu::DeviceType::IntegratedGpu,
wgpu::PowerPreference::HighPerformance
)
);
assert!(
rank_adapter(wgpu::DeviceType::IntegratedGpu, wgpu::PowerPreference::LowPower)
< rank_adapter(wgpu::DeviceType::DiscreteGpu, wgpu::PowerPreference::LowPower)
);
assert!(
rank_adapter(wgpu::DeviceType::DiscreteGpu, wgpu::PowerPreference::HighPerformance)
< rank_adapter(wgpu::DeviceType::Cpu, wgpu::PowerPreference::HighPerformance)
);
assert!(
rank_adapter(wgpu::DeviceType::IntegratedGpu, wgpu::PowerPreference::LowPower)
< rank_adapter(wgpu::DeviceType::Cpu, wgpu::PowerPreference::LowPower)
);
}
#[test]
fn ladder_selects_a_real_adapter_and_reports_its_true_tier() {
let ladder = ladder_for_target();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: ladder.iter().fold(wgpu::Backends::empty(), |acc, (_, b)| acc | *b),
flags: wgpu::InstanceFlags::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::default(),
display: None,
});
let selected = block_on_ladder(select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::HighPerformance,
None,
));
let Some((adapter, tier)) = selected else {
return;
};
let info = adapter.get_info();
assert_eq!(
tier,
tier_of_adapter(info.device_type, info.backend),
"reported tier {:?} disagrees with adapter backend {:?}",
tier,
info.backend,
);
}
fn block_on_ladder<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
let mut future = Box::pin(future);
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[test]
fn instance_backends_keeps_the_whole_ladder_reachable() {
let union = instance_backends();
for (tier, backends) in ladder_for_target() {
if backends.is_empty() {
continue;
}
assert!(
union.contains(*backends),
"instance backends {union:?} cannot reach tier {} ({backends:?})",
tier.label(),
);
}
}
#[test]
fn candidate_selection_honours_surface_and_power_preference() {
use wgpu::DeviceType;
use wgpu::PowerPreference;
let candidates = [
(DeviceType::DiscreteGpu, false),
(DeviceType::IntegratedGpu, true),
(DeviceType::Cpu, true),
];
assert_eq!(
best_candidate_index(&candidates, PowerPreference::HighPerformance),
Some(1),
"a surface-incompatible discrete adapter must not be selected"
);
let candidates = [(DeviceType::DiscreteGpu, true), (DeviceType::IntegratedGpu, true)];
assert_eq!(
best_candidate_index(&candidates, PowerPreference::HighPerformance),
Some(0),
"high performance prefers the discrete adapter"
);
assert_eq!(
best_candidate_index(&candidates, PowerPreference::LowPower),
Some(1),
"low power prefers the integrated adapter"
);
let candidates = [(DeviceType::Cpu, true), (DeviceType::IntegratedGpu, true)];
assert_eq!(best_candidate_index(&candidates, PowerPreference::HighPerformance), Some(1));
let candidates = [(DeviceType::DiscreteGpu, false)];
assert_eq!(best_candidate_index(&candidates, PowerPreference::HighPerformance), None);
assert_eq!(best_candidate_index(&[], PowerPreference::HighPerformance), None);
}
#[test]
fn pinned_backend_that_is_unavailable_yields_none_without_substitution() {
let Some(pinned) = backends_from_env() else {
return; };
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: instance_backends(),
flags: wgpu::InstanceFlags::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::default(),
display: None,
});
let available = block_on_ladder(instance.enumerate_adapters(pinned));
let selected = block_on_ladder(select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::HighPerformance,
None,
));
match selected {
Some((adapter, tier)) => {
let info = adapter.get_info();
assert_eq!(
tier,
GpuBackendTier::from_backend(info.backend),
"reported tier must describe the selected adapter",
);
assert!(
pinned.contains(wgpu::Backends::from(info.backend)),
"selected backend {:?} is outside the pinned set {pinned:?}",
info.backend,
);
}
None => assert!(
available.is_empty(),
"missing adapter while {pinned:?} still exposes candidates",
),
}
}
}