makepad/libs/render/src/quality_tier.rs

123 lines
5.9 KiB
Rust

//! Graphics quality tiers: what a scene spends on its look, from an
//! integrated laptop GPU to a desktop card.
//!
//! A tier sets the static budgets that cost GPU time in an outdoor scene:
//! where trees turn into impostors (a mesh tree costs far more than its
//! stand-in), how far and how thickly the grass grows, the sun cascades'
//! resolution and reach, screen-space occlusion and the render scale cap.
//! The adaptive governor (thermometer.rs) still trims decoration on top of
//! the tier when the frame time runs over; the tier is where it starts.
//!
//! Auto picks the tier once from the GPU the backend reports
//! ([`GraphicsTier::for_gpu`]); the host shows the choice as a direct
//! control and the user's pick overrides it.
/// One of the quality tiers, cheapest first.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum GraphicsTier {
Low,
Medium,
High,
Ultra,
}
/// What a tier lets a scene spend.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TierBudget {
/// Multiplier on every model's impostor distance.
pub impostor_scale: f32,
/// Multiplier on the grass field's radius.
pub grass_radius: f32,
/// Fraction of the grass blades kept (sparser rings closer in).
pub grass_density: f32,
/// One sun cascade's edge, texels, and the detail range, metres.
pub csm_resolution: usize,
pub csm_range: f32,
/// Screen-space ambient occlusion.
pub ssao: bool,
/// The largest render scale the host's automatic scale may use.
pub render_scale: f32,
/// How far a level terrain tile blends its paint layers (the splat
/// lane's per-pixel layer walk, a texture read per candidate layer);
/// beyond it the heaviest layer alone.
pub terrain_blend_reach: f32,
}
impl GraphicsTier {
pub const ALL: [GraphicsTier; 4] = [GraphicsTier::Low, GraphicsTier::Medium, GraphicsTier::High, GraphicsTier::Ultra];
pub fn label(self) -> &'static str {
match self {
GraphicsTier::Low => "Low",
GraphicsTier::Medium => "Medium",
GraphicsTier::High => "High",
GraphicsTier::Ultra => "Ultra",
}
}
pub fn budget(self) -> TierBudget {
match self {
GraphicsTier::Low => TierBudget { impostor_scale: 0.8, grass_radius: 0.55, grass_density: 0.5, csm_resolution: 1024, csm_range: 40.0, ssao: false, render_scale: 0.75, terrain_blend_reach: 150.0 },
GraphicsTier::Medium => TierBudget { impostor_scale: 1.25, grass_radius: 0.8, grass_density: 0.75, csm_resolution: 1536, csm_range: 60.0, ssao: false, render_scale: 1.0, terrain_blend_reach: 450.0 },
GraphicsTier::High => TierBudget { impostor_scale: 1.6, grass_radius: 1.0, grass_density: 1.0, csm_resolution: 2048, csm_range: 80.0, ssao: true, render_scale: 1.0, terrain_blend_reach: f32::MAX },
GraphicsTier::Ultra => TierBudget { impostor_scale: 2.4, grass_radius: 1.4, grass_density: 1.0, csm_resolution: 4096, csm_range: 120.0, ssao: true, render_scale: 1.0, terrain_blend_reach: f32::MAX },
}
}
/// The tier Auto starts at for a GPU, from what the backend reports:
/// phone, headset and software rasterisers Low, integrated parts
/// Medium at most, discrete desktop cards High. The governor takes it
/// down further when the frame time says so.
/// `discrete`: the backend's word on the device kind (None: unknown).
pub fn for_gpu(vendor: &str, renderer: &str, discrete: Option<bool>) -> GraphicsTier {
let (v, r) = (vendor.to_ascii_lowercase(), renderer.to_ascii_lowercase());
let any = |keys: &[&str]| keys.iter().any(|k| v.contains(k) || r.contains(k));
if any(&["llvmpipe", "softpipe", "swiftshader", "software", "adreno", "mali", "powervr", "qualcomm", "videocore"]) {
return GraphicsTier::Low;
}
if any(&["apple"]) {
return if any(&["max", "ultra"]) { GraphicsTier::High } else { GraphicsTier::Medium };
}
match discrete {
Some(true) => return GraphicsTier::High,
Some(false) => return if any(&["intel"]) { GraphicsTier::Low } else { GraphicsTier::Medium },
None => {}
}
if any(&["intel"]) {
return if any(&["arc a", "arc b"]) { GraphicsTier::High } else { GraphicsTier::Low };
}
if any(&["nvidia", "geforce", "quadro", "rtx", "radeon rx", "radeon pro"]) {
return GraphicsTier::High;
}
// An AMD APU names its graphics without a card model.
if any(&["radeon", "amd"]) {
return GraphicsTier::Medium;
}
GraphicsTier::Medium
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_starts_low_on_weak_parts_and_high_on_desktop_cards() {
assert_eq!(GraphicsTier::for_gpu("Intel", "Intel(R) UHD Graphics 620", None), GraphicsTier::Low);
assert_eq!(GraphicsTier::for_gpu("Mesa", "llvmpipe (LLVM 17.0.6, 256 bits)", None), GraphicsTier::Low);
assert_eq!(GraphicsTier::for_gpu("apple", "Apple M4", Some(false)), GraphicsTier::Medium);
assert_eq!(GraphicsTier::for_gpu("apple", "Apple M3 Max", Some(false)), GraphicsTier::High);
assert_eq!(GraphicsTier::for_gpu("nvidia", "NVIDIA GeForce RTX 4090", Some(true)), GraphicsTier::High);
assert_eq!(GraphicsTier::for_gpu("amd", "AMD Radeon Graphics (RADV REMBRANDT)", Some(false)), GraphicsTier::Medium);
assert_eq!(GraphicsTier::for_gpu("intel", "Intel(R) Arc(tm) A770 Graphics (DG2)", Some(true)), GraphicsTier::High);
assert_eq!(GraphicsTier::for_gpu("intel", "Intel(R) Graphics (RPL-P)", Some(false)), GraphicsTier::Low);
}
#[test]
fn tiers_spend_more_as_they_rise() {
for w in GraphicsTier::ALL.windows(2) {
let (a, b) = (w[0].budget(), w[1].budget());
assert!(a.impostor_scale < b.impostor_scale && a.grass_radius <= b.grass_radius && a.csm_resolution < b.csm_resolution, "{:?} -> {:?}", w[0], w[1]);
}
}
}