#[cfg(feature = "gpu-wgpu")]
use crate::compat::format;
use crate::compat::MiniToString;
use crate::compat::String;
#[cfg(feature = "gpu-wgpu")]
use crate::compat::{lock, Mutex, OnceLock};
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuType {
Discrete,
Integrated,
Cpu,
}
impl GpuType {
#[cfg(feature = "gpu-wgpu")]
pub fn detect_primary() -> Option<Self> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: crate::gpu::backend_ladder::instance_backends(),
flags: wgpu::InstanceFlags::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::default(),
display: None,
});
let adapter =
match pollster::block_on(crate::gpu::backend_ladder::select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::HighPerformance,
None,
)) {
Some((adapter, _tier)) => adapter,
None => return None,
};
let info = adapter.get_info();
Some(GpuType::from(GpuDeviceType::from(info.device_type)))
}
#[cfg(not(feature = "gpu-wgpu"))]
pub fn detect_primary() -> Option<Self> {
None
}
pub fn description(&self) -> &'static str {
match self {
GpuType::Discrete => "Discrete GPU",
GpuType::Integrated => "Integrated GPU",
GpuType::Cpu => "CPU Software Rendering",
}
}
}
impl From<GpuDeviceType> for GpuType {
fn from(device_type: GpuDeviceType) -> Self {
match device_type {
GpuDeviceType::DiscreteGpu => GpuType::Discrete,
GpuDeviceType::IntegratedGpu => GpuType::Integrated,
GpuDeviceType::VirtualGpu => GpuType::Integrated,
GpuDeviceType::Other => GpuType::Integrated,
GpuDeviceType::Cpu => GpuType::Cpu,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuDeviceType {
DiscreteGpu,
IntegratedGpu,
VirtualGpu,
Other,
Cpu,
}
impl GpuDeviceType {
pub fn priority(&self) -> u8 {
match self {
Self::DiscreteGpu => 5,
Self::IntegratedGpu => 3,
Self::VirtualGpu => 2,
Self::Other => 2,
Self::Cpu => 1,
}
}
}
impl PartialOrd for GpuDeviceType {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for GpuDeviceType {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.priority().cmp(&other.priority()).then_with(|| self.rank().cmp(&other.rank()))
}
}
impl GpuDeviceType {
fn rank(&self) -> u8 {
match self {
Self::DiscreteGpu => 4,
Self::IntegratedGpu => 3,
Self::VirtualGpu => 2,
Self::Other => 1,
Self::Cpu => 0,
}
}
}
impl GpuDeviceType {
pub fn is_hardware_gpu(&self) -> bool {
!matches!(self, GpuDeviceType::Cpu)
}
pub fn is_discrete(&self) -> bool {
matches!(self, GpuDeviceType::DiscreteGpu)
}
pub fn is_integrated(&self) -> bool {
matches!(self, GpuDeviceType::IntegratedGpu)
}
pub fn is_cpu(&self) -> bool {
matches!(self, GpuDeviceType::Cpu)
}
pub fn description(&self) -> &'static str {
match self {
GpuDeviceType::DiscreteGpu => "Discrete GPU",
GpuDeviceType::IntegratedGpu => "Integrated GPU",
GpuDeviceType::VirtualGpu => "Virtual GPU",
GpuDeviceType::Other => "Other GPU",
GpuDeviceType::Cpu => "CPU Software Rendering",
}
}
pub fn performance_tier(&self) -> u8 {
match self {
GpuDeviceType::DiscreteGpu => 5,
GpuDeviceType::IntegratedGpu => 3,
GpuDeviceType::VirtualGpu => 2,
GpuDeviceType::Other => 2,
GpuDeviceType::Cpu => 1,
}
}
}
impl fmt::Display for GpuDeviceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.description())
}
}
#[cfg(feature = "gpu-wgpu")]
impl From<wgpu::DeviceType> for GpuDeviceType {
fn from(device_type: wgpu::DeviceType) -> Self {
match device_type {
wgpu::DeviceType::DiscreteGpu => GpuDeviceType::DiscreteGpu,
wgpu::DeviceType::IntegratedGpu => GpuDeviceType::IntegratedGpu,
wgpu::DeviceType::VirtualGpu => GpuDeviceType::VirtualGpu,
wgpu::DeviceType::Other => GpuDeviceType::Other,
wgpu::DeviceType::Cpu => GpuDeviceType::Cpu,
}
}
}
#[derive(Debug, Clone)]
pub struct AdapterInfo {
pub device_type: GpuDeviceType,
pub vendor: String,
pub name: String,
pub backend: String,
pub driver: String,
pub driver_version: u64,
pub is_selected: bool,
}
impl AdapterInfo {
#[cfg(feature = "gpu-wgpu")]
pub fn from_wgpu(info: &wgpu::AdapterInfo) -> Self {
Self {
device_type: info.device_type.into(),
vendor: format!("{:04x}", info.vendor),
name: info.name.clone(),
backend: format!("{:?}", info.backend),
driver: info.driver.clone(),
driver_version: 0, is_selected: false,
}
}
pub fn cpu_fallback() -> Self {
Self {
device_type: GpuDeviceType::Cpu,
vendor: "CPU".to_string(),
name: "Software Renderer".to_string(),
backend: "CPU".to_string(),
driver: "Software".to_string(),
driver_version: 0,
is_selected: true,
}
}
pub fn supports_high_quality(&self) -> bool {
self.device_type.performance_tier() >= 3
}
#[cfg(feature = "gpu")]
pub fn backend_tier(&self) -> crate::gpu::backend_ladder::GpuBackendTier {
use crate::gpu::backend_ladder::GpuBackendTier;
if self.device_type.is_cpu() {
return GpuBackendTier::Software;
}
match self.backend.as_str() {
"Vulkan" | "Metal" | "Dx12" | "BrowserWebGpu" => GpuBackendTier::Primary,
"Gl" => GpuBackendTier::OpenGlEs,
_ => GpuBackendTier::Software,
}
}
#[cfg(feature = "gpu")]
pub fn is_degraded_backend(&self) -> bool {
self.backend_tier().is_degraded()
}
pub fn is_suitable_for_quality(&self, quality: crate::quality::QualityLevel) -> bool {
match quality {
crate::quality::QualityLevel::High => self.device_type.performance_tier() >= 4,
crate::quality::QualityLevel::Medium => self.device_type.performance_tier() >= 2,
crate::quality::QualityLevel::Low => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AdapterSelectionStrategy {
PreferPerformance,
PreferPowerEfficiency,
ForceDiscrete,
ForceIntegrated,
ForceCpu,
#[default]
Auto,
}
#[cfg(feature = "gpu-wgpu")]
#[derive(Debug, Clone, Copy, PartialEq)]
struct SelectionCacheKey {
strategy: AdapterSelectionStrategy,
allow_fallback: bool,
pinned: Option<wgpu::Backends>,
}
#[cfg(feature = "gpu-wgpu")]
#[derive(Debug, Clone)]
struct CachedSelection {
key: SelectionCacheKey,
result: Result<AdapterInfo, AdapterSelectionError>,
}
#[cfg(feature = "gpu-wgpu")]
fn selection_cache() -> &'static Mutex<Option<CachedSelection>> {
static CACHE: OnceLock<Mutex<Option<CachedSelection>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(None))
}
pub struct AdapterSelector {
strategy: AdapterSelectionStrategy,
allow_fallback: bool,
}
impl AdapterSelector {
pub fn new() -> Self {
Self { strategy: AdapterSelectionStrategy::Auto, allow_fallback: true }
}
pub fn with_strategy(strategy: AdapterSelectionStrategy) -> Self {
Self { strategy, allow_fallback: true }
}
pub fn allow_fallback(mut self, allow: bool) -> Self {
self.allow_fallback = allow;
self
}
#[cfg(feature = "gpu-wgpu")]
pub async fn enumerate_adapters(&self) -> Vec<AdapterInfo> {
let backends = crate::gpu::backend_ladder::instance_backends();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
flags: wgpu::InstanceFlags::default(),
memory_budget_thresholds: wgpu::MemoryBudgetThresholds::default(),
backend_options: wgpu::BackendOptions::default(),
display: None,
});
let adapters = instance.enumerate_adapters(backends).await;
adapters
.into_iter()
.map(|adapter| {
let info = adapter.get_info();
AdapterInfo::from_wgpu(&info)
})
.collect()
}
#[cfg(feature = "gpu-wgpu")]
pub async fn select_adapter_with_fallback(
&self,
compatible_surface: Option<&wgpu::Surface<'static>>,
) -> Result<AdapterInfo, AdapterSelectionError> {
let key = SelectionCacheKey {
strategy: self.strategy,
allow_fallback: self.allow_fallback,
pinned: crate::gpu::backend_ladder::backends_from_env(),
};
if compatible_surface.is_none() {
let cached = lock(selection_cache()).clone();
if let Some(cached) = cached {
if cached.key == key {
return cached.result;
}
}
}
let result = self.select_adapter_probing(key.pinned, compatible_surface).await;
if compatible_surface.is_none() {
*lock(selection_cache()) = Some(CachedSelection { key, result: result.clone() });
}
result
}
#[cfg(feature = "gpu-wgpu")]
async fn select_adapter_probing(
&self,
pinned: Option<wgpu::Backends>,
compatible_surface: Option<&wgpu::Surface<'static>>,
) -> Result<AdapterInfo, AdapterSelectionError> {
use crate::gpu::backend_ladder::{instance_backends, select_adapter_with_gl_fallback};
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,
});
match self.strategy {
AdapterSelectionStrategy::Auto | AdapterSelectionStrategy::PreferPerformance => {
match select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::HighPerformance,
compatible_surface,
)
.await
{
Some((adapter, tier)) => self.accept_ladder_result(adapter, tier),
None => self.no_adapter(pinned),
}
}
AdapterSelectionStrategy::PreferPowerEfficiency => {
match select_adapter_with_gl_fallback(
&instance,
wgpu::PowerPreference::LowPower,
compatible_surface,
)
.await
{
Some((adapter, tier)) => self.accept_ladder_result(adapter, tier),
None => self.no_adapter(pinned),
}
}
AdapterSelectionStrategy::ForceDiscrete => {
match self
.find_device_type(
&instance,
wgpu::DeviceType::DiscreteGpu,
pinned,
compatible_surface,
)
.await
{
Some(adapter) => Ok(publish_adapter_with_tier(&adapter)),
None => Err(match pinned {
Some(pinned) => AdapterSelectionError::RequestFailed(format!(
"WGPU_BACKEND pinned {pinned:?} but no discrete GPU exposes that backend"
)),
None => AdapterSelectionError::DiscreteGpuNotFound,
}),
}
}
AdapterSelectionStrategy::ForceIntegrated => {
match self
.find_device_type(
&instance,
wgpu::DeviceType::IntegratedGpu,
pinned,
compatible_surface,
)
.await
{
Some(adapter) => Ok(publish_adapter_with_tier(&adapter)),
None => Err(match pinned {
Some(pinned) => AdapterSelectionError::RequestFailed(format!(
"WGPU_BACKEND pinned {pinned:?} but no integrated GPU exposes that backend"
)),
None => AdapterSelectionError::IntegratedGpuNotFound,
}),
}
}
AdapterSelectionStrategy::ForceCpu => {
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::LowPower,
compatible_surface,
force_fallback_adapter: true,
apply_limit_buckets: false,
})
.await
.map_err(|e| AdapterSelectionError::RequestFailed(e.to_string()))?;
Ok(publish_adapter(&adapter, crate::gpu::backend_ladder::GpuBackendTier::Software))
}
}
}
#[cfg(feature = "gpu-wgpu")]
fn accept_ladder_result(
&self,
adapter: wgpu::Adapter,
tier: crate::gpu::backend_ladder::GpuBackendTier,
) -> Result<AdapterInfo, AdapterSelectionError> {
if !self.allow_fallback && tier == crate::gpu::backend_ladder::GpuBackendTier::Software {
log::warn!(
"[gpu] only the CPU software adapter is available, but fallback is disabled"
);
return Err(AdapterSelectionError::NoAdapterFound);
}
Ok(publish_adapter(&adapter, tier))
}
#[cfg(feature = "gpu-wgpu")]
async fn find_device_type(
&self,
instance: &wgpu::Instance,
wanted: wgpu::DeviceType,
pinned: Option<wgpu::Backends>,
compatible_surface: Option<&wgpu::Surface<'static>>,
) -> Option<wgpu::Adapter> {
let backends = pinned.unwrap_or_else(crate::gpu::backend_ladder::instance_backends);
instance.enumerate_adapters(backends).await.into_iter().find(|adapter| {
adapter.get_info().device_type == wanted
&& compatible_surface.is_none_or(|surface| adapter.is_surface_supported(surface))
})
}
#[cfg(feature = "gpu-wgpu")]
fn no_adapter(
&self,
pinned: Option<wgpu::Backends>,
) -> Result<AdapterInfo, AdapterSelectionError> {
if let Some(pinned) = pinned {
log::error!(
"[gpu] WGPU_BACKEND pinned {pinned:?} but this host provides no adapter for it"
);
return Err(AdapterSelectionError::RequestFailed(format!(
"WGPU_BACKEND pinned {pinned:?} but this host provides no adapter for that backend"
)));
}
if self.allow_fallback {
log::warn!("[gpu] no adapter on any backend tier; using the CPU software fallback");
crate::gpu::backend_ladder::publish_shared_selection(
crate::gpu::backend_ladder::GpuBackendSelection {
tier: crate::gpu::backend_ladder::GpuBackendTier::Software,
backend: wgpu::Backend::Noop,
},
);
Ok(AdapterInfo::cpu_fallback())
} else {
Err(AdapterSelectionError::NoAdapterFound)
}
}
pub fn strategy(&self) -> AdapterSelectionStrategy {
self.strategy
}
pub fn set_strategy(&mut self, strategy: AdapterSelectionStrategy) {
self.strategy = strategy;
}
}
#[cfg(feature = "gpu-wgpu")]
fn publish_adapter(
adapter: &wgpu::Adapter,
tier: crate::gpu::backend_ladder::GpuBackendTier,
) -> AdapterInfo {
let info = adapter.get_info();
let adapter_info = AdapterInfo::from_wgpu(&info);
crate::gpu::backend_ladder::publish_shared_selection(
crate::gpu::backend_ladder::GpuBackendSelection { tier, backend: info.backend },
);
adapter_info
}
#[cfg(feature = "gpu-wgpu")]
fn publish_adapter_with_tier(adapter: &wgpu::Adapter) -> AdapterInfo {
let info = adapter.get_info();
let adapter_info = AdapterInfo::from_wgpu(&info);
crate::gpu::backend_ladder::publish_shared_selection(
crate::gpu::backend_ladder::GpuBackendSelection {
tier: adapter_info.backend_tier(),
backend: info.backend,
},
);
adapter_info
}
crate::impl_default_via_new!(AdapterSelector);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AdapterSelectionError {
NoAdapterFound,
DiscreteGpuNotFound,
IntegratedGpuNotFound,
RequestFailed(String),
}
impl fmt::Display for AdapterSelectionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoAdapterFound => write!(f, "No GPU adapter found"),
Self::DiscreteGpuNotFound => write!(f, "Discrete GPU not found"),
Self::IntegratedGpuNotFound => write!(f, "Integrated GPU not found"),
Self::RequestFailed(msg) => write!(f, "Adapter request failed: {msg}"),
}
}
}
#[cfg(not(alloc_frugal))]
impl core::error::Error for AdapterSelectionError {}
#[cfg(target_arch = "wasm32")]
pub fn detect_browser_forced_integrated_gpu() -> bool {
true }
#[cfg(not(target_arch = "wasm32"))]
pub fn detect_browser_forced_integrated_gpu() -> bool {
false }
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::*;
#[test]
fn test_gpu_device_type_priority() {
assert!(GpuDeviceType::DiscreteGpu > GpuDeviceType::IntegratedGpu);
assert!(GpuDeviceType::IntegratedGpu > GpuDeviceType::Cpu);
assert!(GpuDeviceType::DiscreteGpu > GpuDeviceType::Cpu);
}
#[test]
fn test_gpu_device_type_checks() {
assert!(GpuDeviceType::DiscreteGpu.is_discrete());
assert!(GpuDeviceType::IntegratedGpu.is_integrated());
assert!(GpuDeviceType::Cpu.is_cpu());
assert!(!GpuDeviceType::Cpu.is_hardware_gpu());
assert!(GpuDeviceType::DiscreteGpu.is_hardware_gpu());
}
#[test]
fn test_performance_tier() {
assert_eq!(GpuDeviceType::DiscreteGpu.performance_tier(), 5);
assert_eq!(GpuDeviceType::IntegratedGpu.performance_tier(), 3);
assert_eq!(GpuDeviceType::Cpu.performance_tier(), 1);
}
#[test]
fn test_adapter_info_cpu_fallback() {
let info = AdapterInfo::cpu_fallback();
assert!(info.device_type.is_cpu());
assert!(info.is_selected);
assert!(!info.supports_high_quality());
}
#[test]
fn test_adapter_selection_error_display() {
let err = AdapterSelectionError::NoAdapterFound;
assert_eq!(err.to_string(), "No GPU adapter found");
}
#[test]
fn test_gpu_type_from_device_type() {
assert!(matches!(GpuType::from(GpuDeviceType::DiscreteGpu), GpuType::Discrete));
assert!(matches!(GpuType::from(GpuDeviceType::IntegratedGpu), GpuType::Integrated));
assert!(matches!(GpuType::from(GpuDeviceType::Cpu), GpuType::Cpu));
}
#[test]
fn test_gpu_type_description() {
assert_eq!(GpuType::Discrete.description(), "Discrete GPU");
assert_eq!(GpuType::Integrated.description(), "Integrated GPU");
assert_eq!(GpuType::Cpu.description(), "CPU Software Rendering");
}
#[test]
fn gpu_device_type_ord_is_consistent_with_eq() {
use core::cmp::Ordering;
const ALL: [GpuDeviceType; 5] = [
GpuDeviceType::DiscreteGpu,
GpuDeviceType::IntegratedGpu,
GpuDeviceType::VirtualGpu,
GpuDeviceType::Other,
GpuDeviceType::Cpu,
];
for &a in &ALL {
for &b in &ALL {
let eq = a == b;
let cmp_equal = a.cmp(&b) == Ordering::Equal;
assert_eq!(
cmp_equal, eq,
"cmp({a:?}, {b:?}) == Equal is {cmp_equal} but == is {eq}"
);
assert_eq!(
a.cmp(&b),
b.cmp(&a).reverse(),
"cmp must be antisymmetric ({a:?},{b:?})"
);
}
}
assert!(GpuDeviceType::DiscreteGpu > GpuDeviceType::IntegratedGpu);
assert!(GpuDeviceType::IntegratedGpu > GpuDeviceType::VirtualGpu);
assert!(GpuDeviceType::VirtualGpu > GpuDeviceType::Cpu);
}
#[cfg(feature = "gpu")]
#[test]
fn adapter_info_backend_tier_covers_every_backend_label() {
use crate::gpu::backend_ladder::GpuBackendTier;
let with_backend = |backend: &str| AdapterInfo {
device_type: GpuDeviceType::IntegratedGpu,
vendor: "0000".to_string(),
name: "test".to_string(),
backend: backend.to_string(),
driver: String::new(),
driver_version: 0,
is_selected: true,
};
for primary in ["Vulkan", "Metal", "Dx12", "BrowserWebGpu"] {
assert_eq!(
with_backend(primary).backend_tier(),
GpuBackendTier::Primary,
"{primary} must be the primary tier"
);
assert!(!with_backend(primary).is_degraded_backend());
}
assert_eq!(with_backend("Gl").backend_tier(), GpuBackendTier::OpenGlEs);
assert!(with_backend("Gl").is_degraded_backend());
assert_eq!(AdapterInfo::cpu_fallback().backend_tier(), GpuBackendTier::Software);
assert!(AdapterInfo::cpu_fallback().is_degraded_backend());
assert_eq!(with_backend("Noop").backend_tier(), GpuBackendTier::Software);
for backend in ["Vulkan", "Metal", "Dx12", "BrowserWebGpu", "Gl", "Noop", "CPU"] {
let info = AdapterInfo { device_type: GpuDeviceType::Cpu, ..with_backend(backend) };
assert_eq!(
info.backend_tier(),
GpuBackendTier::Software,
"a CPU adapter on {backend} is still the software rung"
);
assert!(info.is_degraded_backend());
}
}
}
pub struct GpuAdapter;
impl GpuAdapter {
pub fn detect_primary_gpu_type() -> Option<GpuType> {
GpuType::detect_primary()
}
pub fn can_detect_gpu_memory() -> bool {
crate::platform::platform_facts().total_memory_mb().is_some()
}
}