#[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()
}
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 mut candidates = instance.enumerate_adapters(pinned).await;
if candidates.is_empty() {
log::error!("[gpu] WGPU_BACKEND pinned {pinned:?} but no adapter exposes that backend");
return None;
}
candidates.sort_by_key(|adapter| adapter.get_info().device_type == wgpu::DeviceType::Cpu);
let adapter = candidates.into_iter().next()?;
let backend = adapter.get_info().backend;
return Some((adapter, GpuBackendTier::from_backend(backend)));
}
let ladder = ladder_for_target();
let last = ladder.len().saturating_sub(1);
for (index, (tier, _backends)) in ladder.iter().enumerate() {
let force_fallback_adapter = index == last;
match instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference,
compatible_surface,
force_fallback_adapter,
apply_limit_buckets: false,
})
.await
{
Ok(adapter) => {
if *tier != GpuBackendTier::Software {
return Some((adapter, *tier));
}
let backend = adapter.get_info().backend;
return Some((adapter, GpuBackendTier::from_backend(backend)));
}
Err(error) => {
log::debug!("[gpu] adapter request failed for tier {}: {error:?}", tier.label());
}
}
}
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 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,
GpuBackendTier::from_backend(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 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",
),
}
}
}