use serde::{Deserialize, Serialize};
use crate::backend::{RenderBackend, RenderFamily};
#[derive(Debug, Clone, Copy)]
pub struct PerfDefaults {
pub fps_limit: u32,
pub msaa_samples: u8,
}
pub fn detect_backend(info: &wgpu::AdapterInfo) -> RenderBackend {
match info.device_type {
wgpu::DeviceType::DiscreteGpu => RenderBackend::VelloGpu,
wgpu::DeviceType::IntegratedGpu => RenderBackend::VelloGpu,
wgpu::DeviceType::VirtualGpu => RenderBackend::VelloCpu,
wgpu::DeviceType::Cpu => RenderBackend::TinySkia,
_ => RenderBackend::VelloGpu,
}
}
pub fn detect_backend_urx(info: &wgpu::AdapterInfo) -> RenderBackend {
match info.device_type {
wgpu::DeviceType::DiscreteGpu => RenderBackend::UrxWgpu,
wgpu::DeviceType::IntegratedGpu => RenderBackend::UrxWgpu,
wgpu::DeviceType::VirtualGpu => RenderBackend::UrxCpu,
wgpu::DeviceType::Cpu => RenderBackend::UrxCpu,
_ => RenderBackend::UrxWgpu,
}
}
pub fn detect_backend_for_family(info: &wgpu::AdapterInfo, family: RenderFamily) -> RenderBackend {
match family {
RenderFamily::Vello => detect_backend(info),
RenderFamily::Urx => detect_backend_urx(info),
}
}
pub fn no_adapter_backend_for_family(family: RenderFamily) -> RenderBackend {
match family {
RenderFamily::Vello => RenderBackend::TinySkia,
RenderFamily::Urx => RenderBackend::UrxCpu,
}
}
fn parse_render_family_env(value: Option<&str>) -> Result<Option<RenderFamily>, &str> {
match value {
None => Ok(None),
Some(v) => match v.to_ascii_lowercase().as_str() {
"vello" => Ok(Some(RenderFamily::Vello)),
"urx" => Ok(Some(RenderFamily::Urx)),
_ => Err(v),
},
}
}
fn warn_invalid_render_family_env_once(value: &str) {
static WARNED: std::sync::Once = std::sync::Once::new();
WARNED.call_once(|| {
eprintln!(
"[uzor-render-hub] UZOR_RENDER_FAMILY={value:?} is not \"vello\" or \"urx\" (case-insensitive) — \
ignoring it and falling back to the app builder's own render_family (or RenderFamily::Vello if \
that's unset too). This warning prints at most once per process."
);
});
}
pub fn resolve_render_family(env_value: Option<&str>, builder_family: Option<RenderFamily>) -> RenderFamily {
match parse_render_family_env(env_value) {
Ok(Some(family)) => family,
Ok(None) => builder_family.unwrap_or_default(),
Err(invalid) => {
warn_invalid_render_family_env_once(invalid);
builder_family.unwrap_or_default()
}
}
}
pub fn resolve_render_family_from_process_env(builder_family: Option<RenderFamily>) -> RenderFamily {
let raw = std::env::var("UZOR_RENDER_FAMILY").ok();
resolve_render_family(raw.as_deref(), builder_family)
}
pub fn default_perf(backend: RenderBackend) -> PerfDefaults {
let (fps_limit, msaa_samples) = match backend {
RenderBackend::VelloGpu => (120u32, 0u8),
RenderBackend::VelloCpu => (30, 0),
RenderBackend::TinySkia => (90, 0),
RenderBackend::InstancedWgpu => (90, 0),
RenderBackend::VelloHybrid => (90, 0),
RenderBackend::Canvas2d => (60, 0),
RenderBackend::UrxCpu => (30, 0),
RenderBackend::UrxWgpu => (90, 0),
RenderBackend::UrxHybrid => (90, 0),
RenderBackend::UrxWgpuFull => (120, 0),
};
PerfDefaults { fps_limit, msaa_samples }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecommendedBackend {
VelloGpu,
VelloCpu,
TinySkia,
}
#[derive(Debug, Clone)]
pub struct GpuInfo {
pub name: String,
pub driver: String,
pub device_type: wgpu::DeviceType,
pub recommended: RecommendedBackend,
pub backend: RenderBackend,
}
pub fn detect(info: &wgpu::AdapterInfo) -> GpuInfo {
let backend = detect_backend(info);
let recommended = match info.device_type {
wgpu::DeviceType::DiscreteGpu => RecommendedBackend::VelloGpu,
wgpu::DeviceType::IntegratedGpu => RecommendedBackend::VelloGpu,
wgpu::DeviceType::VirtualGpu => RecommendedBackend::VelloCpu,
wgpu::DeviceType::Cpu => RecommendedBackend::TinySkia,
_ => RecommendedBackend::VelloGpu,
};
GpuInfo {
name: info.name.clone(),
driver: info.driver.clone(),
device_type: info.device_type,
recommended,
backend,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn info(device_type: wgpu::DeviceType) -> wgpu::AdapterInfo {
wgpu::AdapterInfo {
name: "test-adapter".to_string(),
vendor: 0,
device: 0,
device_type,
device_pci_bus_id: String::new(),
driver: String::new(),
driver_info: String::new(),
backend: wgpu::Backend::Noop,
subgroup_min_size: 0,
subgroup_max_size: 0,
transient_saves_memory: false,
}
}
#[test]
fn discrete_and_integrated_gpu_map_to_urx_wgpu() {
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::DiscreteGpu)), RenderBackend::UrxWgpu);
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::IntegratedGpu)), RenderBackend::UrxWgpu);
}
#[test]
fn virtual_gpu_and_cpu_map_to_urx_cpu() {
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::VirtualGpu)), RenderBackend::UrxCpu);
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::Cpu)), RenderBackend::UrxCpu);
}
#[test]
fn unknown_device_type_falls_through_to_urx_wgpu() {
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::Other)), RenderBackend::UrxWgpu);
}
#[test]
fn never_returns_a_non_urx_backend() {
for dt in [
wgpu::DeviceType::DiscreteGpu,
wgpu::DeviceType::IntegratedGpu,
wgpu::DeviceType::VirtualGpu,
wgpu::DeviceType::Cpu,
wgpu::DeviceType::Other,
] {
let backend = detect_backend_urx(&info(dt));
assert!(
matches!(backend, RenderBackend::UrxCpu | RenderBackend::UrxWgpu),
"device_type {dt:?} produced non-URX backend {backend:?}"
);
}
}
#[test]
fn env_none_and_builder_none_falls_back_to_default_vello() {
assert_eq!(resolve_render_family(None, None), RenderFamily::Vello);
}
#[test]
fn env_none_falls_back_to_builder_family() {
assert_eq!(resolve_render_family(None, Some(RenderFamily::Urx)), RenderFamily::Urx);
assert_eq!(resolve_render_family(None, Some(RenderFamily::Vello)), RenderFamily::Vello);
}
#[test]
fn valid_env_value_beats_builder_family() {
assert_eq!(resolve_render_family(Some("urx"), Some(RenderFamily::Vello)), RenderFamily::Urx);
assert_eq!(resolve_render_family(Some("vello"), Some(RenderFamily::Urx)), RenderFamily::Vello);
}
#[test]
fn env_value_parsing_is_case_insensitive() {
assert_eq!(resolve_render_family(Some("URX"), None), RenderFamily::Urx);
assert_eq!(resolve_render_family(Some("Vello"), None), RenderFamily::Vello);
assert_eq!(resolve_render_family(Some("uRx"), None), RenderFamily::Urx);
}
#[test]
fn invalid_env_value_falls_back_to_builder_family_not_default() {
assert_eq!(resolve_render_family(Some("uwu"), Some(RenderFamily::Urx)), RenderFamily::Urx);
assert_eq!(resolve_render_family(Some("bogus"), None), RenderFamily::Vello);
}
#[test]
fn parse_render_family_env_is_pure_and_distinguishes_unset_from_invalid() {
assert_eq!(parse_render_family_env(None), Ok(None));
assert_eq!(parse_render_family_env(Some("vello")), Ok(Some(RenderFamily::Vello)));
assert_eq!(parse_render_family_env(Some("urx")), Ok(Some(RenderFamily::Urx)));
assert_eq!(parse_render_family_env(Some("nope")), Err("nope"));
}
#[test]
fn detect_backend_for_family_dispatches_to_the_right_tree_on_discrete_gpu() {
let i = info(wgpu::DeviceType::DiscreteGpu);
assert_eq!(detect_backend_for_family(&i, RenderFamily::Urx), RenderBackend::UrxWgpu);
assert_eq!(detect_backend_for_family(&i, RenderFamily::Vello), RenderBackend::VelloGpu);
}
#[test]
fn no_adapter_backend_for_family_matches_each_familys_own_cpu_fallback() {
assert_eq!(no_adapter_backend_for_family(RenderFamily::Urx), RenderBackend::UrxCpu);
assert_eq!(no_adapter_backend_for_family(RenderFamily::Vello), RenderBackend::TinySkia);
}
#[test]
fn urx_cpu_device_type_and_urx_no_adapter_fallback_agree() {
assert_eq!(detect_backend_urx(&info(wgpu::DeviceType::Cpu)), RenderBackend::UrxCpu);
assert_eq!(no_adapter_backend_for_family(RenderFamily::Urx), RenderBackend::UrxCpu);
}
}