Skip to main content

concinnity_engine/gfx/
quality_preset.rs

1// src/gfx/quality_preset.rs
2//
3// The master "Graphics Quality" preset and how it resolves to a performance
4// ceiling over a world's authored look. The preset is persisted in
5// GraphicsSettings; at init it produces a QualityCeiling that clamps the
6// perf-relevant Tier-A settings DOWN where the chosen tier (or detected GPU,
7// under Auto) cannot honor them. A ceiling never turns a feature on -- it only
8// reduces -- so a world authored conservatively is never "upgraded", and an
9// explicit per-row user override always wins over the ceiling (applied by the
10// caller). This keeps the per-field `None = use the world's value` contract: the
11// only thing persisted is the one preset marker, not a bake of every field.
12
13use serde::{Deserialize, Serialize};
14
15use crate::components::{
16    AaMode, ReflectionBlurResolution, ShadowUpdate, SsgiResolution, UpscaleQuality,
17};
18use crate::gfx::backend::{GpuProfile, GpuTier};
19
20/// Persisted master graphics-quality choice. `Auto` resolves from the detected
21/// GPU tier each launch; a named tier (Low..Ultra) is a fixed ceiling; `Custom`
22/// imposes no ceiling (the user's per-row overrides drive). In GraphicsSettings
23/// a `None` (never persisted) means "never configured": the first launch seeds
24/// `Auto` and saves once.
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub enum QualityPreset {
27    /// Resolve the ceiling from the GPU tier detected at launch.
28    Auto,
29    /// Fixed low-tier ceiling.
30    Low,
31    /// Fixed mid-tier ceiling.
32    Medium,
33    /// Fixed high-tier ceiling.
34    High,
35    /// Fixed top-tier ceiling; the only one that permits ray-traced reflections.
36    Ultra,
37    /// No ceiling: the world's authored look and the per-row overrides stand.
38    Custom,
39}
40
41// A ceiling on the perf-relevant Tier-A settings: which feature toggles are
42// permitted (`false` forces the feature off), and the least aggressive render
43// scale allowed. A ceiling only reduces quality; it cannot enable a feature the
44// world did not author.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub(crate) struct QualityCeiling {
47    // The most aggressive anti-aliasing mode the tier permits. Clamps a world's
48    // authored mode DOWN by cost rank (Off < FXAA < TAA): a lower tier can force
49    // TAA to FXAA (skipping the velocity pre-pass) but never enables a mode the
50    // world did not author. The no-ceiling value is `Taa` (the maximum), so an
51    // authored mode always stands under it.
52    pub aa_mode: AaMode,
53    pub ssao: bool,
54    pub ssr: bool,
55    pub ray_traced_reflections: bool,
56    pub ssgi: bool,
57    pub auto_exposure: bool,
58    // The minimum upscaling the ceiling forces: the effective render scale is the
59    // more aggressive (lower internal resolution) of the world's choice and this.
60    // `Quality` (the least aggressive) means "no forced upscaling" -- the world's
61    // choice stands.
62    pub(crate) min_upscale: UpscaleQuality,
63    // Caps on the SSGI gather sub-quality (they only bite where `ssgi` is
64    // permitted): the finest gather resolution, and the most rays / ray-march
65    // steps per pixel. Each clamps DOWN: the effective value is the coarser
66    // resolution / smaller count of the world's choice and the cap. The
67    // no-ceiling values are the engine maxima (`Full`, 32, 64), so a world's
68    // authored value always stands under them.
69    pub(crate) ssgi_resolution: SsgiResolution,
70    pub(crate) ssgi_rays: u32,
71    pub(crate) ssgi_steps: u32,
72    // Cap on the roughness-aware reflection blur resolution (only bites where
73    // `ssr` or `ray_traced_reflections` is permitted): the finest blur the tier
74    // allows, clamping the world's choice coarser. The no-ceiling value is `Full`
75    // (finest), so a world's authored value always stands under it.
76    pub(crate) reflection_blur_resolution: ReflectionBlurResolution,
77    // Cap on the shadow-map cascade resolution in texels (restart-required): the
78    // effective size is the smaller of the world's choice and this cap. The
79    // no-ceiling value is `u32::MAX`, so a world's authored size always stands.
80    pub shadow_map_size: u32,
81    // Whether the tier permits the `EveryFrame` shadow re-render cadence (live).
82    // When false the cadence is clamped to the cheaper `Hybrid`; the no-ceiling
83    // value is `true`, so a world's authored cadence always stands.
84    pub(crate) allow_every_frame_shadows: bool,
85    // Cap on the scene sampler's max anisotropic-filtering degree
86    // (restart-required): the effective degree is the smaller of the world's
87    // choice and this cap. The no-ceiling value is `ANISO_MAX` (16, the GPU
88    // maximum), so a world's authored degree always stands.
89    pub(crate) anisotropy: u32,
90    // Cap on the shadow distance in world units (live): the effective distance is
91    // the smaller of the world's choice and this cap. The no-ceiling value is
92    // `u32::MAX`, so a world's authored distance always stands. A lower tier
93    // shadows a shorter distance (cheaper, and sharper per texel).
94    pub shadow_distance: u32,
95    // Cap on the shadow cascade count (live): the effective count is the smaller
96    // of the world's choice and this cap. The no-ceiling value is `4` (the
97    // maximum), so a world's authored count always stands. A lower tier renders
98    // fewer cascades (one shadow-map render saved per dropped cascade).
99    pub shadow_cascades: u32,
100    // Cap on the number of distinct planar reflection planes that render a mirror
101    // pass each frame (restart-required -- the mirror targets are allocated at
102    // backend init). There is no world-authored value: the engine baseline is the
103    // capacity maximum (`PLANAR_PLANES_MAX`), so this cap is the effective budget
104    // directly. The no-ceiling value is the maximum, so a full-budget world stands.
105    // A lower tier renders fewer full render-res MSAA mirror passes (saving VRAM +
106    // GPU cost); reflectors past the budget fall back to the box-projected probe
107    // cube.
108    pub(crate) planar_reflection_planes: u32,
109}
110
111// The coarser (higher render-resolution divisor) of two SSGI resolutions, the
112// resolution analogue of `more_aggressive_upscale`. Used to clamp a world's
113// gather resolution under a ceiling without ever making it finer.
114pub(crate) fn coarser_ssgi_resolution(a: SsgiResolution, b: SsgiResolution) -> SsgiResolution {
115    if a.scale_divisor() >= b.scale_divisor() {
116        a
117    } else {
118        b
119    }
120}
121
122// The authored anti-aliasing mode clamped under a ceiling: the cheaper of the
123// world's mode and the cap, by the ascending-cost rank `aa_mode_index` defines
124// (Off < FXAA < TAA). Never makes AA more aggressive than the world authored.
125pub(crate) fn clamp_aa_mode(authored: AaMode, cap: AaMode) -> AaMode {
126    use crate::gfx::settings::aa_mode_index;
127    if aa_mode_index(authored) <= aa_mode_index(cap) {
128        authored
129    } else {
130        cap
131    }
132}
133
134// The coarser of two reflection-blur resolutions (the SSGI helper's sibling for
135// the reflection blur enum).
136pub(crate) fn coarser_reflection_blur(
137    a: ReflectionBlurResolution,
138    b: ReflectionBlurResolution,
139) -> ReflectionBlurResolution {
140    if a.scale_divisor() >= b.scale_divisor() {
141        a
142    } else {
143        b
144    }
145}
146
147// No ceiling: everything permitted, no forced upscaling. The resolved ceiling
148// for `Custom`, and for `Auto` on hardware we could not classify (clamping an
149// unknown GPU on a guess would risk needlessly degrading a capable one; the
150// world's authored look is the best signal we have there).
151// The engine maxima for the SSGI sub-quality caps, used wherever a tier imposes
152// no SSGI ceiling: `Full` gather resolution, and the upper clamp bounds the
153// gather honours (rays <= 32, steps <= 64). A world's authored value always
154// stands under these.
155const SSGI_RES_MAX: SsgiResolution = SsgiResolution::Full;
156const SSGI_RAYS_MAX: u32 = 32;
157const SSGI_STEPS_MAX: u32 = 64;
158// `Full` (finest) is the no-cap reflection-blur resolution: a world's choice
159// always stands coarser-or-equal under it.
160const REFLECTION_BLUR_MAX: ReflectionBlurResolution = ReflectionBlurResolution::Full;
161// `u32::MAX` is the no-cap shadow-map resolution: a world's authored size always
162// stands smaller-or-equal under it.
163const SHADOW_SIZE_MAX: u32 = u32::MAX;
164// `16` is the no-cap anisotropy degree -- the maximum every backend's API
165// guarantees -- so a world's authored degree always stands smaller-or-equal
166// under it.
167const ANISO_MAX: u32 = 16;
168// `u32::MAX` is the no-cap shadow distance: a world's authored distance always
169// stands smaller-or-equal under it.
170const SHADOW_DIST_MAX: u32 = u32::MAX;
171// `4` is the no-cap shadow cascade count (the engine maximum = NUM_SHADOW_CASCADES):
172// a world's authored count always stands smaller-or-equal under it.
173const SHADOW_CASCADES_MAX: u32 = 4;
174// The no-cap planar reflection plane budget: the engine capacity ceiling every
175// backend sizes its mirror allocations to. Sourced from the single
176// `gfx::planar_reflection` constant so a capacity bump flows here automatically.
177const PLANAR_PLANES_MAX: u32 = crate::gfx::planar_reflection::MAX_PLANAR_PLANES as u32;
178
179// The world's shadow re-render cadence clamped under the ceiling: a tier that
180// disallows `EveryFrame` forces the cheaper `Hybrid`; otherwise the authored
181// cadence stands. Never raises (`Hybrid` -> `EveryFrame`).
182pub(crate) fn clamp_shadow_update(
183    authored: ShadowUpdate,
184    ceiling: &QualityCeiling,
185) -> ShadowUpdate {
186    if ceiling.allow_every_frame_shadows {
187        authored
188    } else {
189        ShadowUpdate::Hybrid
190    }
191}
192
193const NONE: QualityCeiling = QualityCeiling {
194    aa_mode: AaMode::Taa,
195    ssao: true,
196    ssr: true,
197    ray_traced_reflections: true,
198    ssgi: true,
199    auto_exposure: true,
200    min_upscale: UpscaleQuality::Quality,
201    ssgi_resolution: SSGI_RES_MAX,
202    ssgi_rays: SSGI_RAYS_MAX,
203    ssgi_steps: SSGI_STEPS_MAX,
204    reflection_blur_resolution: REFLECTION_BLUR_MAX,
205    shadow_map_size: SHADOW_SIZE_MAX,
206    allow_every_frame_shadows: true,
207    anisotropy: ANISO_MAX,
208    shadow_distance: SHADOW_DIST_MAX,
209    shadow_cascades: SHADOW_CASCADES_MAX,
210    planar_reflection_planes: PLANAR_PLANES_MAX,
211};
212const LOW: QualityCeiling = QualityCeiling {
213    aa_mode: AaMode::Fxaa,
214    ssao: false,
215    ssr: false,
216    ray_traced_reflections: false,
217    ssgi: false,
218    auto_exposure: true,
219    min_upscale: UpscaleQuality::Performance,
220    ssgi_resolution: SsgiResolution::Quarter,
221    ssgi_rays: 4,
222    ssgi_steps: 8,
223    reflection_blur_resolution: ReflectionBlurResolution::Quarter,
224    shadow_map_size: 1024,
225    allow_every_frame_shadows: false,
226    anisotropy: 4,
227    shadow_distance: 40,
228    shadow_cascades: 2,
229    // Integrated / weakest tier: keep only the two most impactful mirror planes
230    // (e.g. a floor plus one wall); further reflectors take the probe cube.
231    planar_reflection_planes: 2,
232};
233const MEDIUM: QualityCeiling = QualityCeiling {
234    aa_mode: AaMode::Taa,
235    ssao: true,
236    ssr: false,
237    ray_traced_reflections: false,
238    ssgi: false,
239    auto_exposure: true,
240    min_upscale: UpscaleQuality::Balanced,
241    ssgi_resolution: SsgiResolution::Half,
242    ssgi_rays: 8,
243    ssgi_steps: 12,
244    reflection_blur_resolution: ReflectionBlurResolution::Half,
245    shadow_map_size: 2048,
246    allow_every_frame_shadows: false,
247    anisotropy: 8,
248    shadow_distance: 80,
249    shadow_cascades: 3,
250    // Entry discrete: one more mirror plane than Low before the probe fallback.
251    planar_reflection_planes: 3,
252};
253const HIGH: QualityCeiling = QualityCeiling {
254    aa_mode: AaMode::Taa,
255    ssao: true,
256    ssr: true,
257    ray_traced_reflections: false,
258    ssgi: true,
259    auto_exposure: true,
260    min_upscale: UpscaleQuality::Quality,
261    ssgi_resolution: SsgiResolution::Half,
262    ssgi_rays: 8,
263    ssgi_steps: 12,
264    reflection_blur_resolution: ReflectionBlurResolution::Half,
265    shadow_map_size: 4096,
266    allow_every_frame_shadows: false,
267    anisotropy: 16,
268    shadow_distance: 160,
269    shadow_cascades: 4,
270    // Mid discrete and up run the full mirror-plane budget, matching the flat
271    // pre-scaling default, so a capable GPU is never downgraded.
272    planar_reflection_planes: PLANAR_PLANES_MAX,
273};
274const ULTRA: QualityCeiling = QualityCeiling {
275    aa_mode: AaMode::Taa,
276    ssao: true,
277    ssr: true,
278    ray_traced_reflections: true,
279    ssgi: true,
280    auto_exposure: true,
281    min_upscale: UpscaleQuality::Quality,
282    ssgi_resolution: SSGI_RES_MAX,
283    ssgi_rays: SSGI_RAYS_MAX,
284    ssgi_steps: SSGI_STEPS_MAX,
285    reflection_blur_resolution: REFLECTION_BLUR_MAX,
286    shadow_map_size: SHADOW_SIZE_MAX,
287    allow_every_frame_shadows: true,
288    anisotropy: ANISO_MAX,
289    shadow_distance: SHADOW_DIST_MAX,
290    shadow_cascades: SHADOW_CASCADES_MAX,
291    planar_reflection_planes: PLANAR_PLANES_MAX,
292};
293
294// The active ceiling for the persisted preset and detected GPU. `Auto` maps the
295// GPU tier to a named tier (Unknown -> no ceiling); `Custom` imposes no ceiling;
296// a named tier is fixed.
297pub(crate) fn resolve_ceiling(preset: QualityPreset, profile: &GpuProfile) -> QualityCeiling {
298    match preset {
299        QualityPreset::Custom => NONE,
300        QualityPreset::Low => LOW,
301        QualityPreset::Medium => MEDIUM,
302        QualityPreset::High => HIGH,
303        QualityPreset::Ultra => ULTRA,
304        QualityPreset::Auto => auto_tier(profile).map_or(NONE, |(_, ceiling)| ceiling),
305    }
306}
307
308// The more aggressive (lower internal resolution) of two upscale qualities,
309// ordered by `settings::render_scale_index` (Quality < Balanced < Performance <
310// UltraPerformance). Used to clamp a world's render scale under a ceiling's
311// `min_upscale` without ever raising it.
312pub(crate) fn more_aggressive_upscale(a: UpscaleQuality, b: UpscaleQuality) -> UpscaleQuality {
313    use crate::gfx::settings::render_scale_index;
314    if render_scale_index(a) >= render_scale_index(b) {
315        a
316    } else {
317        b
318    }
319}
320
321impl QualityPreset {
322    /// The presets in menu-cycle order. The settings-menu master row cycles
323    /// through these; `GRAPHICS_QUALITY_OPTIONS` in `gfx::settings` holds the
324    /// matching display labels in the same order (locked by a test there).
325    pub const ALL: [QualityPreset; 6] = [
326        Self::Auto,
327        Self::Low,
328        Self::Medium,
329        Self::High,
330        Self::Ultra,
331        Self::Custom,
332    ];
333
334    // The display name for this preset (the bare label, without an `Auto`
335    // tier suffix; see `preset_label`).
336    pub(crate) fn name(self) -> &'static str {
337        match self {
338            Self::Auto => "Auto",
339            Self::Low => "Low",
340            Self::Medium => "Medium",
341            Self::High => "High",
342            Self::Ultra => "Ultra",
343            Self::Custom => "Custom",
344        }
345    }
346}
347
348// The cycle index of a preset, and the preset at an index, over `ALL`. The
349// master settings row cycles indices; these convert to and from the live
350// `QualityPreset`. An out-of-range index falls back to `Auto`.
351pub(crate) fn preset_index(preset: QualityPreset) -> usize {
352    QualityPreset::ALL
353        .iter()
354        .position(|&p| p == preset)
355        .unwrap_or(0)
356}
357pub(crate) fn preset_at(index: usize) -> QualityPreset {
358    QualityPreset::ALL
359        .get(index)
360        .copied()
361        .unwrap_or(QualityPreset::Auto)
362}
363
364// The named tier `Auto` resolves to on this GPU, for the menu label (e.g.
365// "Auto (High)"). `None` when the GPU is unclassified, where `Auto` imposes no
366// ceiling and the bare "Auto" reads correctly.
367pub(crate) fn auto_resolved_name(profile: &GpuProfile) -> Option<&'static str> {
368    auto_tier(profile).map(|(name, _)| name)
369}
370
371// The named tier `Auto` resolves to on this GPU, as (label, ceiling). `None`
372// for an unclassified GPU, where `Auto` imposes no ceiling. One table, so the
373// menu label can never disagree with the ceiling actually applied.
374fn auto_tier(profile: &GpuProfile) -> Option<(&'static str, QualityCeiling)> {
375    match profile.tier {
376        GpuTier::Unknown => None,
377        GpuTier::Integrated => Some(("Low", LOW)),
378        GpuTier::EntryDiscrete => Some(("Medium", MEDIUM)),
379        GpuTier::MidDiscrete => Some(("High", HIGH)),
380        GpuTier::HighDiscrete => Some(("Ultra", ULTRA)),
381    }
382}
383
384// The master row's display text for a preset: a named tier shows its own name,
385// while `Auto` annotates the tier it resolved to on the detected GPU (e.g.
386// "Auto (High)") so the user can see what the auto-config chose.
387pub(crate) fn preset_label(preset: QualityPreset, profile: &GpuProfile) -> String {
388    match preset {
389        QualityPreset::Auto => match auto_resolved_name(profile) {
390            Some(tier) => format!("Auto ({tier})"),
391            None => "Auto".to_string(),
392        },
393        other => other.name().to_string(),
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    fn profile_with_tier(tier: GpuTier) -> GpuProfile {
402        GpuProfile {
403            tier,
404            ..GpuProfile::UNKNOWN
405        }
406    }
407
408    #[test]
409    fn custom_and_unknown_impose_no_ceiling() {
410        // Custom never clamps, regardless of hardware.
411        assert_eq!(
412            resolve_ceiling(
413                QualityPreset::Custom,
414                &profile_with_tier(GpuTier::Integrated)
415            ),
416            NONE
417        );
418        // Auto on an unclassified GPU does not clamp on a guess.
419        assert_eq!(
420            resolve_ceiling(QualityPreset::Auto, &profile_with_tier(GpuTier::Unknown)),
421            NONE
422        );
423    }
424
425    #[test]
426    fn auto_maps_tier_to_named_ceiling() {
427        assert_eq!(
428            resolve_ceiling(QualityPreset::Auto, &profile_with_tier(GpuTier::Integrated)),
429            LOW
430        );
431        assert_eq!(
432            resolve_ceiling(
433                QualityPreset::Auto,
434                &profile_with_tier(GpuTier::EntryDiscrete)
435            ),
436            MEDIUM
437        );
438        assert_eq!(
439            resolve_ceiling(
440                QualityPreset::Auto,
441                &profile_with_tier(GpuTier::MidDiscrete)
442            ),
443            HIGH
444        );
445        assert_eq!(
446            resolve_ceiling(
447                QualityPreset::Auto,
448                &profile_with_tier(GpuTier::HighDiscrete)
449            ),
450            ULTRA
451        );
452    }
453
454    #[test]
455    fn named_presets_resolve_independent_of_hardware() {
456        // A named preset ignores the GPU tier.
457        let weak = profile_with_tier(GpuTier::Integrated);
458        assert_eq!(resolve_ceiling(QualityPreset::Low, &weak), LOW);
459        assert_eq!(resolve_ceiling(QualityPreset::Ultra, &weak), ULTRA);
460    }
461
462    #[test]
463    fn ceilings_are_monotonic_in_tier() {
464        // Each tier permits a superset of the next-lower tier's features, so a
465        // higher tier never disables something a lower tier allows.
466        let order = [LOW, MEDIUM, HIGH, ULTRA];
467        for pair in order.windows(2) {
468            let (lo, hi) = (pair[0], pair[1]);
469            for (lo_on, hi_on) in [
470                (lo.ssao, hi.ssao),
471                (lo.ssr, hi.ssr),
472                (lo.ray_traced_reflections, hi.ray_traced_reflections),
473                (lo.ssgi, hi.ssgi),
474                (lo.auto_exposure, hi.auto_exposure),
475            ] {
476                assert!(!lo_on || hi_on, "a higher tier dropped a feature");
477            }
478            // The AA-mode cap rises (or holds) with the tier: a higher tier
479            // never permits a less aggressive (cheaper) anti-aliasing mode.
480            assert!(
481                crate::gfx::settings::aa_mode_index(lo.aa_mode)
482                    <= crate::gfx::settings::aa_mode_index(hi.aa_mode),
483                "a higher tier capped AA at a cheaper mode"
484            );
485            // And never forces more aggressive upscaling than a lower tier.
486            assert_eq!(
487                more_aggressive_upscale(lo.min_upscale, hi.min_upscale),
488                lo.min_upscale
489            );
490            // The SSGI sub-quality caps rise (or hold) with the tier too: a
491            // higher tier never permits fewer rays / steps or a coarser gather.
492            assert!(lo.ssgi_rays <= hi.ssgi_rays, "ssgi_rays cap dropped");
493            assert!(lo.ssgi_steps <= hi.ssgi_steps, "ssgi_steps cap dropped");
494            assert_eq!(
495                coarser_ssgi_resolution(lo.ssgi_resolution, hi.ssgi_resolution),
496                lo.ssgi_resolution,
497                "a higher tier permitted a coarser SSGI gather"
498            );
499            assert_eq!(
500                coarser_reflection_blur(
501                    lo.reflection_blur_resolution,
502                    hi.reflection_blur_resolution
503                ),
504                lo.reflection_blur_resolution,
505                "a higher tier permitted a coarser reflection blur"
506            );
507            // The shadow caps rise (or hold) with the tier: a higher tier never
508            // permits a smaller shadow map or forbids a cadence a lower tier
509            // allowed.
510            assert!(
511                lo.shadow_map_size <= hi.shadow_map_size,
512                "shadow_map_size cap dropped"
513            );
514            assert!(
515                !lo.allow_every_frame_shadows || hi.allow_every_frame_shadows,
516                "a higher tier forbade the EveryFrame shadow cadence"
517            );
518            // The anisotropy cap rises (or holds) with the tier too.
519            assert!(lo.anisotropy <= hi.anisotropy, "anisotropy cap dropped");
520            // The shadow-distance cap rises (or holds) with the tier too.
521            assert!(
522                lo.shadow_distance <= hi.shadow_distance,
523                "shadow_distance cap dropped"
524            );
525            // The shadow cascade-count cap rises (or holds) with the tier too.
526            assert!(
527                lo.shadow_cascades <= hi.shadow_cascades,
528                "shadow_cascades cap dropped"
529            );
530            // The planar reflection plane budget rises (or holds) with the tier too.
531            assert!(
532                lo.planar_reflection_planes <= hi.planar_reflection_planes,
533                "planar plane budget cap dropped"
534            );
535        }
536    }
537
538    // Callers clamp with `authored.min(ceiling.field)`, so what each tier
539    // actually promises is the cap value itself.
540    #[test]
541    fn tier_caps_bound_the_shadow_and_texture_knobs() {
542        use crate::components::ShadowUpdate;
543        // No ceiling (Custom / Ultra) leaves a world's authored values alone.
544        let none = resolve_ceiling(QualityPreset::Custom, &GpuProfile::UNKNOWN);
545        assert_eq!(none.shadow_map_size, SHADOW_SIZE_MAX);
546        assert_eq!(none.anisotropy, ANISO_MAX);
547        assert_eq!(none.shadow_distance, SHADOW_DIST_MAX);
548        assert_eq!(none.shadow_cascades, SHADOW_CASCADES_MAX);
549        assert_eq!(
550            clamp_shadow_update(ShadowUpdate::EveryFrame, &none),
551            ShadowUpdate::EveryFrame
552        );
553
554        // Low caps every knob hard and forces the cheaper Hybrid cadence.
555        let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
556        assert_eq!(low.shadow_map_size, 1024);
557        assert_eq!(low.anisotropy, 4);
558        assert_eq!(low.shadow_distance, 40);
559        assert_eq!(low.shadow_cascades, 2);
560        assert_eq!(
561            clamp_shadow_update(ShadowUpdate::EveryFrame, &low),
562            ShadowUpdate::Hybrid
563        );
564        // The cadence clamp never raises Hybrid to EveryFrame.
565        assert_eq!(
566            clamp_shadow_update(ShadowUpdate::Hybrid, &none),
567            ShadowUpdate::Hybrid
568        );
569
570        let medium = resolve_ceiling(QualityPreset::Medium, &GpuProfile::UNKNOWN);
571        assert_eq!(medium.shadow_cascades, 3);
572    }
573
574    #[test]
575    fn planar_plane_budget_scales_with_tier_within_capacity() {
576        // No ceiling (Custom / Ultra) permits the full engine capacity, matching
577        // the flat pre-scaling default so capable GPUs are never downgraded.
578        let none = resolve_ceiling(QualityPreset::Custom, &GpuProfile::UNKNOWN);
579        assert_eq!(none.planar_reflection_planes, PLANAR_PLANES_MAX);
580        let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
581        assert_eq!(ultra.planar_reflection_planes, PLANAR_PLANES_MAX);
582        // High (mid discrete under Auto) also runs the full budget.
583        let high = resolve_ceiling(QualityPreset::High, &GpuProfile::UNKNOWN);
584        assert_eq!(high.planar_reflection_planes, PLANAR_PLANES_MAX);
585        // Lower tiers reduce the budget but keep at least one sharp mirror plane,
586        // and never exceed the capacity every backend's allocation is sized to.
587        for preset in [QualityPreset::Low, QualityPreset::Medium] {
588            let c = resolve_ceiling(preset, &GpuProfile::UNKNOWN);
589            assert!(
590                c.planar_reflection_planes >= 1,
591                "{preset:?} dropped all planes"
592            );
593            assert!(
594                c.planar_reflection_planes < PLANAR_PLANES_MAX,
595                "{preset:?} did not reduce the budget"
596            );
597        }
598    }
599
600    #[test]
601    fn ssgi_caps_clamp_down_only() {
602        // The no-ceiling values are the engine maxima, so any authored value
603        // stands under them.
604        assert_eq!(NONE.ssgi_rays, 32);
605        assert_eq!(NONE.ssgi_steps, 64);
606        assert_eq!(NONE.ssgi_resolution, SsgiResolution::Full);
607        // The coarser-resolution helper picks the higher divisor (lower quality),
608        // and an equal input is returned as-is.
609        assert_eq!(
610            coarser_ssgi_resolution(SsgiResolution::Full, SsgiResolution::Quarter),
611            SsgiResolution::Quarter
612        );
613        assert_eq!(
614            coarser_ssgi_resolution(SsgiResolution::Half, SsgiResolution::Half),
615            SsgiResolution::Half
616        );
617        // Ultra imposes the maxima (no clamp); Low caps hard.
618        let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
619        assert_eq!(ultra.ssgi_rays, 32);
620        let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
621        assert_eq!(low.ssgi_rays, 4);
622        assert_eq!(low.ssgi_resolution, SsgiResolution::Quarter);
623    }
624
625    #[test]
626    fn low_disables_the_expensive_effects() {
627        // Resolve through the public path so the assertions are on a runtime
628        // value, not the `const LOW` directly.
629        let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
630        assert!(!low.ssr);
631        assert!(!low.ssgi);
632        assert!(!low.ray_traced_reflections);
633        assert!(!low.ssao);
634        // Low caps anti-aliasing at FXAA: it keeps edges smooth nearly for free
635        // but skips TAA's velocity pre-pass and history buffer. Auto-exposure is
636        // cheap enough to keep on.
637        assert_eq!(low.aa_mode, AaMode::Fxaa);
638        assert!(low.auto_exposure);
639    }
640
641    #[test]
642    fn aa_mode_clamps_down_only() {
643        // The no-ceiling cap is TAA (the maximum), so any authored mode stands.
644        assert_eq!(NONE.aa_mode, AaMode::Taa);
645        assert_eq!(clamp_aa_mode(AaMode::Taa, NONE.aa_mode), AaMode::Taa);
646        // A lower cap forces a more expensive authored mode down to it, but
647        // never raises a cheaper authored mode up.
648        assert_eq!(clamp_aa_mode(AaMode::Taa, AaMode::Fxaa), AaMode::Fxaa);
649        assert_eq!(clamp_aa_mode(AaMode::Fxaa, AaMode::Taa), AaMode::Fxaa);
650        assert_eq!(clamp_aa_mode(AaMode::Fxaa, AaMode::Off), AaMode::Off);
651        assert_eq!(clamp_aa_mode(AaMode::Off, AaMode::Taa), AaMode::Off);
652        // Ultra imposes the maximum (no clamp); Low caps at FXAA.
653        let ultra = resolve_ceiling(QualityPreset::Ultra, &GpuProfile::UNKNOWN);
654        assert_eq!(ultra.aa_mode, AaMode::Taa);
655        let low = resolve_ceiling(QualityPreset::Low, &GpuProfile::UNKNOWN);
656        assert_eq!(low.aa_mode, AaMode::Fxaa);
657    }
658
659    #[test]
660    fn preset_index_and_at_round_trip() {
661        for p in QualityPreset::ALL {
662            assert_eq!(preset_at(preset_index(p)), p);
663        }
664        // Auto leads the cycle order; an out-of-range index falls back to Auto.
665        assert_eq!(preset_at(0), QualityPreset::Auto);
666        assert_eq!(preset_at(99), QualityPreset::Auto);
667    }
668
669    #[test]
670    fn auto_label_annotates_the_resolved_tier() {
671        // Auto shows the tier it resolved to, so the user sees the auto choice.
672        assert_eq!(
673            preset_label(
674                QualityPreset::Auto,
675                &profile_with_tier(GpuTier::MidDiscrete)
676            ),
677            "Auto (High)"
678        );
679        assert_eq!(
680            preset_label(QualityPreset::Auto, &profile_with_tier(GpuTier::Integrated)),
681            "Auto (Low)"
682        );
683        // An unclassified GPU imposes no ceiling, so bare "Auto" is honest.
684        assert_eq!(
685            preset_label(QualityPreset::Auto, &profile_with_tier(GpuTier::Unknown)),
686            "Auto"
687        );
688        // A named preset is just its own name, hardware-independent.
689        assert_eq!(
690            preset_label(
691                QualityPreset::Ultra,
692                &profile_with_tier(GpuTier::Integrated)
693            ),
694            "Ultra"
695        );
696        // The Auto suffix tracks the resolved ceiling.
697        for tier in [
698            GpuTier::Integrated,
699            GpuTier::EntryDiscrete,
700            GpuTier::MidDiscrete,
701            GpuTier::HighDiscrete,
702        ] {
703            let profile = profile_with_tier(tier);
704            let suffix = auto_resolved_name(&profile).unwrap();
705            assert_eq!(
706                preset_label(QualityPreset::Auto, &profile),
707                format!("Auto ({suffix})")
708            );
709        }
710    }
711
712    #[test]
713    fn more_aggressive_picks_the_lower_resolution() {
714        // Higher index = more aggressive (lower internal resolution).
715        assert_eq!(
716            more_aggressive_upscale(UpscaleQuality::Quality, UpscaleQuality::Performance),
717            UpscaleQuality::Performance
718        );
719        assert_eq!(
720            more_aggressive_upscale(UpscaleQuality::UltraPerformance, UpscaleQuality::Balanced),
721            UpscaleQuality::UltraPerformance
722        );
723        // Equal inputs return that quality; a ceiling of Quality never raises.
724        assert_eq!(
725            more_aggressive_upscale(UpscaleQuality::Balanced, UpscaleQuality::Quality),
726            UpscaleQuality::Balanced
727        );
728    }
729}