use super::collect::MAX_CASCADES;
use super::{
AntiAliasing, MAX_LIGHT_SHADOW_ATLAS, MAX_LIGHT_SHADOW_RESOLUTION, MAX_LIGHT_SHADOW_VIEWS, MAX_RENDER_SCALE,
MAX_SHADOW_RESOLUTION, MIN_LIGHT_SHADOW_ATLAS, MIN_LIGHT_SHADOW_RESOLUTION, MIN_RENDER_SCALE,
MIN_SHADOW_RESOLUTION,
};
use crate::Ctx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum QualityLevel {
Low = 0,
Medium = 1,
High = 2,
Ultra = 3,
Custom = 4,
}
impl QualityLevel {
#[must_use]
pub fn quality(self) -> Option<GraphicsQuality> {
match self {
Self::Low => Some(GraphicsQuality::low()),
Self::Medium => Some(GraphicsQuality::medium()),
Self::High => Some(GraphicsQuality::high()),
Self::Ultra => Some(GraphicsQuality::ultra()),
Self::Custom => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GraphicsQuality {
pub render_scale: f32,
pub sharpen: f32,
pub shadows: bool,
pub shadow_distance: f32,
pub shadow_cascades: u32,
pub shadow_resolution: u32,
pub shadow_refresh: u32,
pub shadow_filter: u32,
pub light_shadow_views: u32,
pub light_shadow_resolution: u32,
pub light_shadow_atlas: u32,
pub ambient_occlusion: bool,
pub ao_half_resolution: bool,
pub ao_temporal: bool,
pub ao_slices: u32,
pub ao_steps: u32,
pub ssr: f32,
pub gi: bool,
pub anti_aliasing: AntiAliasing,
}
impl Default for GraphicsQuality {
fn default() -> Self {
Self {
render_scale: 1.0,
sharpen: 0.0,
shadows: true,
shadow_distance: 60.0,
shadow_cascades: 4,
shadow_resolution: 2048,
shadow_refresh: 2,
shadow_filter: 3,
light_shadow_views: 12,
light_shadow_resolution: 1024,
light_shadow_atlas: 4096,
ambient_occlusion: false,
ao_half_resolution: false,
ao_temporal: false,
ao_slices: 3,
ao_steps: 6,
ssr: 0.0,
gi: false,
anti_aliasing: AntiAliasing::None,
}
}
}
impl GraphicsQuality {
#[must_use]
pub fn low() -> Self {
Self {
render_scale: 0.67,
sharpen: 0.5,
shadows: true,
shadow_distance: 30.0,
shadow_cascades: 2,
shadow_resolution: 1024,
shadow_refresh: 4,
shadow_filter: 1,
light_shadow_views: 0,
light_shadow_resolution: 512,
light_shadow_atlas: 1024,
ambient_occlusion: false,
ao_half_resolution: false,
ao_temporal: false,
ao_slices: 2,
ao_steps: 4,
ssr: 0.0,
gi: false,
anti_aliasing: AntiAliasing::None,
}
}
#[must_use]
pub fn medium() -> Self {
Self {
render_scale: 1.0,
sharpen: 0.0,
shadows: true,
shadow_distance: 50.0,
shadow_cascades: 3,
shadow_resolution: 2048,
shadow_refresh: 2,
shadow_filter: 2,
light_shadow_views: 4,
light_shadow_resolution: 512,
light_shadow_atlas: 2048,
ambient_occlusion: false,
ao_half_resolution: false,
ao_temporal: false,
ao_slices: 2,
ao_steps: 4,
ssr: 0.0,
gi: false,
anti_aliasing: AntiAliasing::Fxaa,
}
}
#[must_use]
pub fn high() -> Self {
Self {
render_scale: 1.0,
sharpen: 0.0,
shadows: true,
shadow_distance: 80.0,
shadow_cascades: 4,
shadow_resolution: 2048,
shadow_refresh: 2,
shadow_filter: 3,
light_shadow_views: 8,
light_shadow_resolution: 1024,
light_shadow_atlas: 4096,
ambient_occlusion: true,
ao_half_resolution: true,
ao_temporal: true,
ao_slices: 2,
ao_steps: 4,
ssr: 1.0,
gi: false,
anti_aliasing: AntiAliasing::Smaa,
}
}
#[must_use]
pub fn ultra() -> Self {
Self {
render_scale: 1.0,
sharpen: 0.0,
shadows: true,
shadow_distance: 120.0,
shadow_cascades: 4,
shadow_resolution: 4096,
shadow_refresh: 1,
shadow_filter: 3,
light_shadow_views: MAX_LIGHT_SHADOW_VIEWS as u32,
light_shadow_resolution: 2048,
light_shadow_atlas: 8192,
ambient_occlusion: true,
ao_half_resolution: true,
ao_temporal: true,
ao_slices: 4,
ao_steps: 8,
ssr: 1.0,
gi: true,
anti_aliasing: AntiAliasing::Smaa,
}
}
#[must_use]
pub fn level(&self) -> QualityLevel {
let me = self.clamped();
[
QualityLevel::Low,
QualityLevel::Medium,
QualityLevel::High,
QualityLevel::Ultra,
]
.into_iter()
.find(|l| l.quality() == Some(me))
.unwrap_or(QualityLevel::Custom)
}
#[must_use]
pub fn clamped(&self) -> Self {
let d = Self::default();
let float = |v: f32, lo: f32, hi: f32, fallback: f32| if v.is_nan() { fallback } else { v.clamp(lo, hi) };
Self {
render_scale: float(self.render_scale, MIN_RENDER_SCALE, MAX_RENDER_SCALE, d.render_scale),
sharpen: float(self.sharpen, 0.0, 1.0, d.sharpen),
shadows: self.shadows,
shadow_distance: float(self.shadow_distance, 0.0, f32::MAX, d.shadow_distance),
shadow_cascades: self.shadow_cascades.clamp(1, MAX_CASCADES as u32),
shadow_resolution: self
.shadow_resolution
.clamp(MIN_SHADOW_RESOLUTION, MAX_SHADOW_RESOLUTION),
shadow_refresh: self.shadow_refresh.clamp(1, super::collect::MAX_SHADOW_REFRESH),
shadow_filter: self.shadow_filter.clamp(1, 3),
light_shadow_views: self.light_shadow_views.min(MAX_LIGHT_SHADOW_VIEWS as u32),
light_shadow_resolution: self
.light_shadow_resolution
.clamp(MIN_LIGHT_SHADOW_RESOLUTION, MAX_LIGHT_SHADOW_RESOLUTION),
light_shadow_atlas: super::shadow_atlas::floor_pow2(
self.light_shadow_atlas
.clamp(MIN_LIGHT_SHADOW_ATLAS, MAX_LIGHT_SHADOW_ATLAS),
),
ambient_occlusion: self.ambient_occlusion,
ao_half_resolution: self.ao_half_resolution,
ao_temporal: self.ao_temporal,
ao_slices: self.ao_slices.clamp(1, 8),
ao_steps: self.ao_steps.clamp(1, 16),
ssr: float(self.ssr, 0.0, 1.0, d.ssr),
gi: self.gi,
anti_aliasing: self.anti_aliasing,
}
}
}
impl Ctx {
#[must_use]
pub fn graphics_quality(&self) -> GraphicsQuality {
GraphicsQuality {
render_scale: self.post.render_scale,
sharpen: self.post.sharpen,
shadows: self.lighting.shadows,
shadow_distance: self.lighting.shadow_distance,
shadow_cascades: self.lighting.shadow_cascades,
shadow_resolution: self.lighting.shadow_resolution,
shadow_refresh: self.lighting.shadow_refresh,
shadow_filter: self.lighting.shadow_filter,
light_shadow_views: self.lighting.light_shadow_views,
light_shadow_resolution: self.lighting.light_shadow_resolution,
light_shadow_atlas: self.lighting.light_shadow_atlas,
ambient_occlusion: self.post.ambient_occlusion.enabled,
ao_half_resolution: self.post.ao_half_resolution,
ao_temporal: self.post.ao_temporal,
ao_slices: self.post.ambient_occlusion.slices,
ao_steps: self.post.ambient_occlusion.steps,
ssr: self.post.ssr,
gi: self.lighting.gi.enabled,
anti_aliasing: self.post.anti_aliasing,
}
.clamped()
}
pub fn set_graphics_quality(&mut self, quality: GraphicsQuality) {
let q = quality.clamped();
self.post.render_scale = q.render_scale;
self.post.sharpen = q.sharpen;
self.post.anti_aliasing = q.anti_aliasing;
self.post.ambient_occlusion.enabled = q.ambient_occlusion;
self.post.ao_half_resolution = q.ao_half_resolution;
self.post.ao_temporal = q.ao_temporal;
self.post.ambient_occlusion.slices = q.ao_slices;
self.post.ambient_occlusion.steps = q.ao_steps;
self.post.ssr = q.ssr;
self.lighting.gi.enabled = q.gi;
self.lighting.shadows = q.shadows;
self.lighting.shadow_distance = q.shadow_distance;
self.lighting.shadow_cascades = q.shadow_cascades;
self.lighting.shadow_resolution = q.shadow_resolution;
self.lighting.shadow_refresh = q.shadow_refresh;
self.lighting.shadow_filter = q.shadow_filter;
self.lighting.light_shadow_views = q.light_shadow_views;
self.lighting.light_shadow_resolution = q.light_shadow_resolution;
self.lighting.light_shadow_atlas = q.light_shadow_atlas;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presets_get_heavier_from_low_to_ultra() {
let p = [
GraphicsQuality::low(),
GraphicsQuality::medium(),
GraphicsQuality::high(),
GraphicsQuality::ultra(),
];
for w in p.windows(2) {
let (a, b) = (w[0], w[1]);
assert!(a.render_scale <= b.render_scale);
assert!(a.shadow_distance < b.shadow_distance);
assert!(a.shadow_cascades <= b.shadow_cascades);
assert!(a.shadow_resolution <= b.shadow_resolution);
assert!(
a.shadow_refresh >= b.shadow_refresh,
"heavier redraws far shadows more often"
);
assert!(a.shadow_filter <= b.shadow_filter, "heavier filters softer");
assert!(a.light_shadow_views < b.light_shadow_views);
assert!(a.light_shadow_resolution <= b.light_shadow_resolution);
assert!(a.light_shadow_atlas < b.light_shadow_atlas);
assert!(a.ambient_occlusion <= b.ambient_occlusion);
assert!(a.ao_slices * a.ao_steps <= b.ao_slices * b.ao_steps);
assert!(a.ssr <= b.ssr);
assert!(a.gi <= b.gi);
assert!(a.anti_aliasing as u32 <= b.anti_aliasing as u32);
}
}
#[test]
fn presets_are_valid_and_distinct() {
for l in [
QualityLevel::Low,
QualityLevel::Medium,
QualityLevel::High,
QualityLevel::Ultra,
] {
let q = l.quality().unwrap();
assert_eq!(q, q.clamped(), "{l:?} is inside every range");
assert_eq!(q.level(), l);
}
assert_eq!(GraphicsQuality::default().level(), QualityLevel::Custom);
assert_eq!(GraphicsQuality::default(), GraphicsQuality::default().clamped());
}
#[test]
fn the_documented_values() {
let low = GraphicsQuality::low();
assert_eq!((low.render_scale, low.sharpen, low.shadow_distance), (0.67, 0.5, 30.0));
assert_eq!((low.shadow_cascades, low.shadow_resolution), (2, 1024));
assert_eq!(low.shadow_refresh, 4);
assert_eq!(GraphicsQuality::ultra().shadow_refresh, 1);
assert_eq!(
(
low.light_shadow_views,
low.light_shadow_resolution,
low.light_shadow_atlas
),
(0, 512, 1024)
);
assert_eq!((low.ambient_occlusion, low.anti_aliasing), (false, AntiAliasing::None));
let medium = GraphicsQuality::medium();
assert_eq!(
(medium.shadow_distance, medium.shadow_cascades, medium.shadow_resolution),
(50.0, 3, 2048)
);
assert_eq!(
(
medium.light_shadow_views,
medium.light_shadow_resolution,
medium.light_shadow_atlas
),
(4, 512, 2048)
);
assert_eq!(
(medium.ambient_occlusion, medium.anti_aliasing),
(false, AntiAliasing::Fxaa)
);
let high = GraphicsQuality::high();
assert_eq!(
(high.shadow_distance, high.shadow_cascades, high.shadow_resolution),
(80.0, 4, 2048)
);
assert_eq!(
(
high.light_shadow_views,
high.light_shadow_resolution,
high.light_shadow_atlas
),
(8, 1024, 4096)
);
assert_eq!((high.ambient_occlusion, high.ao_slices, high.ao_steps), (true, 2, 4));
assert!(high.ao_half_resolution && high.ao_temporal);
assert_eq!(high.anti_aliasing, AntiAliasing::Smaa);
assert_eq!((low.ssr, medium.ssr, high.ssr), (0.0, 0.0, 1.0));
let ultra = GraphicsQuality::ultra();
assert_eq!(
(ultra.shadow_distance, ultra.shadow_cascades, ultra.shadow_resolution),
(120.0, 4, 4096)
);
assert_eq!(
(
ultra.light_shadow_views,
ultra.light_shadow_resolution,
ultra.light_shadow_atlas
),
(24, 2048, 8192)
);
assert_eq!((ultra.ao_slices, ultra.ao_steps, ultra.ssr), (4, 8, 1.0));
assert!(ultra.gi && !high.gi);
}
#[test]
fn clamping_fixes_out_of_range_and_nan_values() {
let q = GraphicsQuality {
render_scale: 0.0,
sharpen: f32::NAN,
shadow_distance: -5.0,
shadow_cascades: 9,
shadow_resolution: 1 << 20,
light_shadow_views: 99,
light_shadow_resolution: 1,
light_shadow_atlas: 5000,
ao_slices: 0,
ao_steps: 99,
ssr: 7.0,
..Default::default()
}
.clamped();
assert_eq!(q.render_scale, MIN_RENDER_SCALE);
assert_eq!(q.sharpen, 0.0);
assert_eq!(q.shadow_distance, 0.0);
assert_eq!(q.shadow_cascades, 4);
assert_eq!(q.shadow_resolution, 4096);
assert_eq!(q.light_shadow_views, 24);
assert_eq!(q.light_shadow_resolution, 128);
assert_eq!(q.light_shadow_atlas, 4096);
assert_eq!((q.ao_slices, q.ao_steps), (1, 16));
assert_eq!(q.ssr, 1.0);
}
#[test]
fn applying_a_preset_and_reading_it_back_round_trips() {
crate::Game::new("quality", 64, 64).run_headless(0, |ctx| {
assert_eq!(ctx.graphics_quality(), GraphicsQuality::default());
for l in [
QualityLevel::Ultra,
QualityLevel::Low,
QualityLevel::High,
QualityLevel::Medium,
] {
let q = l.quality().unwrap();
ctx.set_graphics_quality(q);
assert_eq!(ctx.graphics_quality(), q);
assert_eq!(ctx.graphics_quality().level(), l);
assert_eq!(ctx.post.render_scale, q.render_scale);
assert_eq!(ctx.lighting.shadow_resolution, q.shadow_resolution);
assert_eq!(ctx.post.ambient_occlusion.enabled, q.ambient_occlusion);
assert_eq!(ctx.post.ao_half_resolution, q.ao_half_resolution);
assert_eq!(ctx.post.ao_temporal, q.ao_temporal);
assert_eq!(ctx.post.ssr, q.ssr);
assert_eq!(ctx.lighting.gi.enabled, q.gi);
}
});
}
#[test]
fn tweaks_after_a_preset_stick_and_make_it_custom() {
crate::Game::new("quality", 64, 64).run_headless(0, |ctx| {
ctx.post.ambient_occlusion.radius = 3.5;
ctx.set_graphics_quality(GraphicsQuality::high());
assert_eq!(
ctx.post.ambient_occlusion.radius, 3.5,
"settings outside the struct stay"
);
ctx.lighting.shadow_cascades = 2;
assert_eq!(ctx.graphics_quality().level(), QualityLevel::Custom);
ctx.set_graphics_quality(GraphicsQuality {
render_scale: 5.0,
..GraphicsQuality::high()
});
assert_eq!(ctx.post.render_scale, MAX_RENDER_SCALE, "clamped when applied");
});
}
}