use serde::{Deserialize, Serialize};
use crate::components::{
AaMode, ReflectionBlurResolution, ShadowUpdate, SsgiResolution, UpscaleQuality,
};
use crate::gfx::backend::{GpuProfile, GpuTier};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum QualityPreset {
Auto,
Low,
Medium,
High,
Ultra,
Custom,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct QualityCeiling {
pub aa_mode: AaMode,
pub ssao: bool,
pub ssr: bool,
pub ray_traced_reflections: bool,
pub ssgi: bool,
pub auto_exposure: bool,
pub(crate) min_upscale: UpscaleQuality,
pub(crate) ssgi_resolution: SsgiResolution,
pub(crate) ssgi_rays: u32,
pub(crate) ssgi_steps: u32,
pub(crate) reflection_blur_resolution: ReflectionBlurResolution,
pub shadow_map_size: u32,
pub(crate) allow_every_frame_shadows: bool,
pub(crate) anisotropy: u32,
pub shadow_distance: u32,
pub shadow_cascades: u32,
pub(crate) planar_reflection_planes: u32,
}
pub(crate) fn coarser_ssgi_resolution(a: SsgiResolution, b: SsgiResolution) -> SsgiResolution {
if a.scale_divisor() >= b.scale_divisor() {
a
} else {
b
}
}
pub(crate) fn clamp_aa_mode(authored: AaMode, cap: AaMode) -> AaMode {
use crate::gfx::settings::aa_mode_index;
if aa_mode_index(authored) <= aa_mode_index(cap) {
authored
} else {
cap
}
}
pub(crate) fn coarser_reflection_blur(
a: ReflectionBlurResolution,
b: ReflectionBlurResolution,
) -> ReflectionBlurResolution {
if a.scale_divisor() >= b.scale_divisor() {
a
} else {
b
}
}
const SSGI_RES_MAX: SsgiResolution = SsgiResolution::Full;
const SSGI_RAYS_MAX: u32 = 32;
const SSGI_STEPS_MAX: u32 = 64;
const REFLECTION_BLUR_MAX: ReflectionBlurResolution = ReflectionBlurResolution::Full;
const SHADOW_SIZE_MAX: u32 = u32::MAX;
const ANISO_MAX: u32 = 16;
const SHADOW_DIST_MAX: u32 = u32::MAX;
const SHADOW_CASCADES_MAX: u32 = 4;
const PLANAR_PLANES_MAX: u32 = crate::gfx::planar_reflection::MAX_PLANAR_PLANES as u32;
pub(crate) fn clamp_shadow_update(
authored: ShadowUpdate,
ceiling: &QualityCeiling,
) -> ShadowUpdate {
if ceiling.allow_every_frame_shadows {
authored
} else {
ShadowUpdate::Hybrid
}
}
const NONE: QualityCeiling = QualityCeiling {
aa_mode: AaMode::Taa,
ssao: true,
ssr: true,
ray_traced_reflections: true,
ssgi: true,
auto_exposure: true,
min_upscale: UpscaleQuality::Quality,
ssgi_resolution: SSGI_RES_MAX,
ssgi_rays: SSGI_RAYS_MAX,
ssgi_steps: SSGI_STEPS_MAX,
reflection_blur_resolution: REFLECTION_BLUR_MAX,
shadow_map_size: SHADOW_SIZE_MAX,
allow_every_frame_shadows: true,
anisotropy: ANISO_MAX,
shadow_distance: SHADOW_DIST_MAX,
shadow_cascades: SHADOW_CASCADES_MAX,
planar_reflection_planes: PLANAR_PLANES_MAX,
};
const LOW: QualityCeiling = QualityCeiling {
aa_mode: AaMode::Fxaa,
ssao: false,
ssr: false,
ray_traced_reflections: false,
ssgi: false,
auto_exposure: true,
min_upscale: UpscaleQuality::Performance,
ssgi_resolution: SsgiResolution::Quarter,
ssgi_rays: 4,
ssgi_steps: 8,
reflection_blur_resolution: ReflectionBlurResolution::Quarter,
shadow_map_size: 1024,
allow_every_frame_shadows: false,
anisotropy: 4,
shadow_distance: 40,
shadow_cascades: 2,
planar_reflection_planes: 2,
};
const MEDIUM: QualityCeiling = QualityCeiling {
aa_mode: AaMode::Taa,
ssao: true,
ssr: false,
ray_traced_reflections: false,
ssgi: false,
auto_exposure: true,
min_upscale: UpscaleQuality::Balanced,
ssgi_resolution: SsgiResolution::Half,
ssgi_rays: 8,
ssgi_steps: 12,
reflection_blur_resolution: ReflectionBlurResolution::Half,
shadow_map_size: 2048,
allow_every_frame_shadows: false,
anisotropy: 8,
shadow_distance: 80,
shadow_cascades: 3,
planar_reflection_planes: 3,
};
const HIGH: QualityCeiling = QualityCeiling {
aa_mode: AaMode::Taa,
ssao: true,
ssr: true,
ray_traced_reflections: false,
ssgi: true,
auto_exposure: true,
min_upscale: UpscaleQuality::Quality,
ssgi_resolution: SsgiResolution::Half,
ssgi_rays: 8,
ssgi_steps: 12,
reflection_blur_resolution: ReflectionBlurResolution::Half,
shadow_map_size: 4096,
allow_every_frame_shadows: false,
anisotropy: 16,
shadow_distance: 160,
shadow_cascades: 4,
planar_reflection_planes: PLANAR_PLANES_MAX,
};
const ULTRA: QualityCeiling = QualityCeiling {
aa_mode: AaMode::Taa,
ssao: true,
ssr: true,
ray_traced_reflections: true,
ssgi: true,
auto_exposure: true,
min_upscale: UpscaleQuality::Quality,
ssgi_resolution: SSGI_RES_MAX,
ssgi_rays: SSGI_RAYS_MAX,
ssgi_steps: SSGI_STEPS_MAX,
reflection_blur_resolution: REFLECTION_BLUR_MAX,
shadow_map_size: SHADOW_SIZE_MAX,
allow_every_frame_shadows: true,
anisotropy: ANISO_MAX,
shadow_distance: SHADOW_DIST_MAX,
shadow_cascades: SHADOW_CASCADES_MAX,
planar_reflection_planes: PLANAR_PLANES_MAX,
};
pub(crate) fn resolve_ceiling(preset: QualityPreset, profile: &GpuProfile) -> QualityCeiling {
match preset {
QualityPreset::Custom => NONE,
QualityPreset::Low => LOW,
QualityPreset::Medium => MEDIUM,
QualityPreset::High => HIGH,
QualityPreset::Ultra => ULTRA,
QualityPreset::Auto => auto_tier(profile).map_or(NONE, |(_, ceiling)| ceiling),
}
}
pub(crate) fn more_aggressive_upscale(a: UpscaleQuality, b: UpscaleQuality) -> UpscaleQuality {
use crate::gfx::settings::render_scale_index;
if render_scale_index(a) >= render_scale_index(b) {
a
} else {
b
}
}
impl QualityPreset {
pub const ALL: [QualityPreset; 6] = [
Self::Auto,
Self::Low,
Self::Medium,
Self::High,
Self::Ultra,
Self::Custom,
];
pub(crate) fn name(self) -> &'static str {
match self {
Self::Auto => "Auto",
Self::Low => "Low",
Self::Medium => "Medium",
Self::High => "High",
Self::Ultra => "Ultra",
Self::Custom => "Custom",
}
}
}
pub(crate) fn preset_index(preset: QualityPreset) -> usize {
QualityPreset::ALL
.iter()
.position(|&p| p == preset)
.unwrap_or(0)
}
pub(crate) fn preset_at(index: usize) -> QualityPreset {
QualityPreset::ALL
.get(index)
.copied()
.unwrap_or(QualityPreset::Auto)
}
pub(crate) fn auto_resolved_name(profile: &GpuProfile) -> Option<&'static str> {
auto_tier(profile).map(|(name, _)| name)
}
fn auto_tier(profile: &GpuProfile) -> Option<(&'static str, QualityCeiling)> {
match profile.tier {
GpuTier::Unknown => Some(("Medium", MEDIUM)),
GpuTier::Integrated => Some(("Low", LOW)),
GpuTier::EntryDiscrete => Some(("Medium", MEDIUM)),
GpuTier::MidDiscrete => Some(("High", HIGH)),
GpuTier::HighDiscrete => Some(("Ultra", ULTRA)),
}
}
pub(crate) fn preset_label(preset: QualityPreset, profile: &GpuProfile) -> String {
match preset {
QualityPreset::Auto => match auto_resolved_name(profile) {
Some(tier) => format!("Auto ({tier})"),
None => "Auto".to_string(),
},
other => other.name().to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn profile_with_tier(tier: GpuTier) -> GpuProfile {
GpuProfile {
tier,
..GpuProfile::UNKNOWN
}
}
#[test]
fn only_custom_imposes_no_ceiling() {
assert_eq!(
resolve_ceiling(
QualityPreset::Custom,
&profile_with_tier(GpuTier::Integrated)
),
NONE
);
assert_eq!(
resolve_ceiling(QualityPreset::Auto, &profile_with_tier(GpuTier::Unknown)),
MEDIUM
);
}
#[test]
fn auto_maps_tier_to_named_ceiling() {
assert_eq!(
resolve_ceiling(QualityPreset::Auto, &profile_with_tier(GpuTier::Integrated)),
LOW
);
assert_eq!(
resolve_ceiling(
QualityPreset::Auto,
&profile_with_tier(GpuTier::EntryDiscrete)
),
MEDIUM
);
assert_eq!(
resolve_ceiling(
QualityPreset::Auto,
&profile_with_tier(GpuTier::MidDiscrete)
),
HIGH
);
assert_eq!(
resolve_ceiling(
QualityPreset::Auto,
&profile_with_tier(GpuTier::HighDiscrete)
),
ULTRA
);
}
#[test]
fn named_presets_resolve_independent_of_hardware() {
let weak = profile_with_tier(GpuTier::Integrated);
assert_eq!(resolve_ceiling(QualityPreset::Low, &weak), LOW);
assert_eq!(resolve_ceiling(QualityPreset::Ultra, &weak), ULTRA);
}
#[test]
fn ceilings_are_monotonic_in_tier() {
let order = [LOW, MEDIUM, HIGH, ULTRA];
for pair in order.windows(2) {
let (lo, hi) = (pair[0], pair[1]);
for (lo_on, hi_on) in [
(lo.ssao, hi.ssao),
(lo.ssr, hi.ssr),
(lo.ray_traced_reflections, hi.ray_traced_reflections),
(lo.ssgi, hi.ssgi),
(lo.auto_exposure, hi.auto_exposure),
] {
assert!(!lo_on || hi_on, "a higher tier dropped a feature");
}
assert!(
crate::gfx::settings::aa_mode_index(lo.aa_mode)
<= crate::gfx::settings::aa_mode_index(hi.aa_mode),
"a higher tier capped AA at a cheaper mode"
);
assert_eq!(
more_aggressive_upscale(lo.min_upscale, hi.min_upscale),
lo.min_upscale
);
assert!(lo.ssgi_rays <= hi.ssgi_rays, "ssgi_rays cap dropped");
assert!(lo.ssgi_steps <= hi.ssgi_steps, "ssgi_steps cap dropped");
assert_eq!(
coarser_ssgi_resolution(lo.ssgi_resolution, hi.ssgi_resolution),
lo.ssgi_resolution,
"a higher tier permitted a coarser SSGI gather"
);
assert_eq!(
coarser_reflection_blur(
lo.reflection_blur_resolution,
hi.reflection_blur_resolution
),
lo.reflection_blur_resolution,
"a higher tier permitted a coarser reflection blur"
);
assert!(
lo.shadow_map_size <= hi.shadow_map_size,
"shadow_map_size cap dropped"
);
assert!(
!lo.allow_every_frame_shadows || hi.allow_every_frame_shadows,
"a higher tier forbade the EveryFrame shadow cadence"
);
assert!(lo.anisotropy <= hi.anisotropy, "anisotropy cap dropped");
assert!(
lo.shadow_distance <= hi.shadow_distance,
"shadow_distance cap dropped"
);
assert!(
lo.shadow_cascades <= hi.shadow_cascades,
"shadow_cascades cap dropped"
);
assert!(
lo.planar_reflection_planes <= hi.planar_reflection_planes,
"planar plane budget cap dropped"
);
}
}
#[test]
fn tier_caps_bound_the_shadow_and_texture_knobs() {
use crate::components::ShadowUpdate;
let none = resolve_ceiling(QualityPreset::Custom, &GpuProfile::UNKNOWN);
assert_eq!(none.shadow_map_size, SHADOW_SIZE_MAX);
assert_eq!(none.anisotropy, ANISO_MAX);
assert_eq!(none.shadow_distance, SHADOW_DIST_MAX);
assert_eq!(none.shadow_cascades, SHADOW_CASCADES_MAX);
assert_eq!(
clamp_shadow_update(ShadowUpdate::EveryFrame, &none),
ShadowUpdate::EveryFrame
);
let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
assert_eq!(low.shadow_map_size, 1024);
assert_eq!(low.anisotropy, 4);
assert_eq!(low.shadow_distance, 40);
assert_eq!(low.shadow_cascades, 2);
assert_eq!(
clamp_shadow_update(ShadowUpdate::EveryFrame, &low),
ShadowUpdate::Hybrid
);
assert_eq!(
clamp_shadow_update(ShadowUpdate::Hybrid, &none),
ShadowUpdate::Hybrid
);
let medium = resolve_ceiling(QualityPreset::Medium, &GpuProfile::UNKNOWN);
assert_eq!(medium.shadow_cascades, 3);
}
#[test]
fn planar_plane_budget_scales_with_tier_within_capacity() {
let none = resolve_ceiling(QualityPreset::Custom, &GpuProfile::UNKNOWN);
assert_eq!(none.planar_reflection_planes, PLANAR_PLANES_MAX);
let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
assert_eq!(ultra.planar_reflection_planes, PLANAR_PLANES_MAX);
let high = resolve_ceiling(QualityPreset::High, &GpuProfile::UNKNOWN);
assert_eq!(high.planar_reflection_planes, PLANAR_PLANES_MAX);
for preset in [QualityPreset::Low, QualityPreset::Medium] {
let c = resolve_ceiling(preset, &GpuProfile::UNKNOWN);
assert!(
c.planar_reflection_planes >= 1,
"{preset:?} dropped all planes"
);
assert!(
c.planar_reflection_planes < PLANAR_PLANES_MAX,
"{preset:?} did not reduce the budget"
);
}
}
#[test]
fn ssgi_caps_clamp_down_only() {
assert_eq!(NONE.ssgi_rays, 32);
assert_eq!(NONE.ssgi_steps, 64);
assert_eq!(NONE.ssgi_resolution, SsgiResolution::Full);
assert_eq!(
coarser_ssgi_resolution(SsgiResolution::Full, SsgiResolution::Quarter),
SsgiResolution::Quarter
);
assert_eq!(
coarser_ssgi_resolution(SsgiResolution::Half, SsgiResolution::Half),
SsgiResolution::Half
);
let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
assert_eq!(ultra.ssgi_rays, 32);
let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
assert_eq!(low.ssgi_rays, 4);
assert_eq!(low.ssgi_resolution, SsgiResolution::Quarter);
}
#[test]
fn low_disables_the_expensive_effects() {
let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
assert!(!low.ssr);
assert!(!low.ssgi);
assert!(!low.ray_traced_reflections);
assert!(!low.ssao);
assert_eq!(low.aa_mode, AaMode::Fxaa);
assert!(low.auto_exposure);
}
#[test]
fn aa_mode_clamps_down_only() {
assert_eq!(NONE.aa_mode, AaMode::Taa);
assert_eq!(clamp_aa_mode(AaMode::Taa, NONE.aa_mode), AaMode::Taa);
assert_eq!(clamp_aa_mode(AaMode::Taa, AaMode::Fxaa), AaMode::Fxaa);
assert_eq!(clamp_aa_mode(AaMode::Fxaa, AaMode::Taa), AaMode::Fxaa);
assert_eq!(clamp_aa_mode(AaMode::Fxaa, AaMode::Off), AaMode::Off);
assert_eq!(clamp_aa_mode(AaMode::Off, AaMode::Taa), AaMode::Off);
let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
assert_eq!(ultra.aa_mode, AaMode::Taa);
let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
assert_eq!(low.aa_mode, AaMode::Fxaa);
}
#[test]
fn preset_index_and_at_round_trip() {
for p in QualityPreset::ALL {
assert_eq!(preset_at(preset_index(p)), p);
}
assert_eq!(preset_at(0), QualityPreset::Auto);
assert_eq!(preset_at(99), QualityPreset::Auto);
}
#[test]
fn auto_label_annotates_the_resolved_tier() {
assert_eq!(
preset_label(
QualityPreset::Auto,
&profile_with_tier(GpuTier::MidDiscrete)
),
"Auto (High)"
);
assert_eq!(
preset_label(QualityPreset::Auto, &profile_with_tier(GpuTier::Integrated)),
"Auto (Low)"
);
assert_eq!(
preset_label(QualityPreset::Auto, &profile_with_tier(GpuTier::Unknown)),
"Auto (Medium)"
);
assert_eq!(
preset_label(
QualityPreset::Ultra,
&profile_with_tier(GpuTier::Integrated)
),
"Ultra"
);
for tier in [
GpuTier::Integrated,
GpuTier::EntryDiscrete,
GpuTier::MidDiscrete,
GpuTier::HighDiscrete,
] {
let profile = profile_with_tier(tier);
let suffix = auto_resolved_name(&profile).unwrap();
assert_eq!(
preset_label(QualityPreset::Auto, &profile),
format!("Auto ({suffix})")
);
}
}
#[test]
fn more_aggressive_picks_the_lower_resolution() {
assert_eq!(
more_aggressive_upscale(UpscaleQuality::Quality, UpscaleQuality::Performance),
UpscaleQuality::Performance
);
assert_eq!(
more_aggressive_upscale(UpscaleQuality::UltraPerformance, UpscaleQuality::Balanced),
UpscaleQuality::UltraPerformance
);
assert_eq!(
more_aggressive_upscale(UpscaleQuality::Balanced, UpscaleQuality::Quality),
UpscaleQuality::Balanced
);
}
}