1use crate::ecs::Component;
9use crate::gfx::render_types::PostProcessTunables;
10use crate::math::exp2;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
33#[serde(default)]
34pub struct PostProcessConfig {
35 pub bloom_intensity: f32,
37 pub bloom_threshold: f32,
40 pub bloom_knee: f32,
42 pub exposure_ev: f32,
45 pub vignette_strength: f32,
47 pub lut_strength: f32,
51 pub aa_mode: AaMode,
56 pub ssao: bool,
59 pub ssao_radius: f32,
62 pub ssao_intensity: f32,
65 pub ssr: bool,
68 pub ssr_intensity: f32,
71 pub ssr_max_distance: f32,
74 pub ray_traced_reflections: bool,
80 pub reflection_blur_resolution: ReflectionBlurResolution,
88 pub indirect_lighting: IndirectLighting,
93 pub ambient_intensity: f32,
102 pub ssgi_intensity: f32,
106 pub ssgi_max_distance: f32,
110 pub ssgi_resolution: SsgiResolution,
114 pub ssgi_rays: u32,
118 pub ssgi_steps: u32,
122 pub auto_exposure: bool,
126 pub auto_exposure_min_ev: f32,
129 pub auto_exposure_max_ev: f32,
131 pub auto_exposure_speed: f32,
134 pub hdr_display: bool,
138 pub hdr_pq: bool,
142 pub temporal_upscaling: bool,
147 pub upscale_quality: UpscaleQuality,
151 pub upscale_backend: UpscalerBackend,
157 pub occlusion_two_pass: bool,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
168#[serde(rename_all = "snake_case")]
169#[derive(Default)]
170pub enum UpscaleQuality {
171 #[default]
173 Quality,
174 Balanced,
176 Performance,
178 UltraPerformance,
180}
181
182impl UpscaleQuality {
183 pub fn scale(self) -> f32 {
186 match self {
187 UpscaleQuality::Quality => 2.0 / 3.0,
188 UpscaleQuality::Balanced => 0.587,
189 UpscaleQuality::Performance => 0.5,
190 UpscaleQuality::UltraPerformance => 1.0 / 3.0,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
202#[serde(rename_all = "snake_case")]
203#[derive(Default)]
204pub enum UpscalerBackend {
205 #[default]
207 Auto,
208 Fsr3,
210 Dlss,
212 Xess,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
222#[serde(rename_all = "snake_case")]
223#[derive(Default)]
224pub enum AaMode {
225 Off,
227 #[default]
229 Fxaa,
230 Taa,
232}
233
234impl AaMode {
235 pub fn taa_enabled(self) -> bool {
239 matches!(self, AaMode::Taa)
240 }
241
242 fn fxaa_enabled(self) -> bool {
246 !matches!(self, AaMode::Off)
247 }
248
249 pub fn fxaa_flag(self) -> f32 {
252 if self.fxaa_enabled() { 1.0 } else { 0.0 }
253 }
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
260#[serde(rename_all = "snake_case")]
261#[derive(Default)]
262pub enum IndirectLighting {
263 #[default]
265 Ibl,
266 Ssgi,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
278#[serde(rename_all = "snake_case")]
279#[derive(Default)]
280pub enum SsgiResolution {
281 Full,
283 #[default]
285 Half,
286 Quarter,
288}
289
290impl SsgiResolution {
291 pub fn scale_divisor(self) -> u32 {
293 match self {
294 SsgiResolution::Full => 1,
295 SsgiResolution::Half => 2,
296 SsgiResolution::Quarter => 4,
297 }
298 }
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
310#[serde(rename_all = "snake_case")]
311#[derive(Default)]
312pub enum ReflectionBlurResolution {
313 Full,
315 #[default]
317 Half,
318 Quarter,
320}
321
322impl ReflectionBlurResolution {
323 pub fn scale_divisor(self) -> u32 {
326 match self {
327 ReflectionBlurResolution::Full => 1,
328 ReflectionBlurResolution::Half => 2,
329 ReflectionBlurResolution::Quarter => 4,
330 }
331 }
332}
333
334pub const DEFAULT_SSGI_RAYS: u32 = 8;
339pub const DEFAULT_SSGI_STEPS: u32 = 12;
341
342impl Default for PostProcessConfig {
343 fn default() -> Self {
344 Self {
345 bloom_intensity: 0.6,
346 bloom_threshold: 1.0,
347 bloom_knee: 0.5,
348 exposure_ev: 0.0,
349 vignette_strength: 0.0,
350 lut_strength: 1.0,
351 aa_mode: AaMode::Fxaa,
352 ssao: false,
353 ssao_radius: 0.5,
354 ssao_intensity: 1.0,
355 ssr: false,
356 ssr_intensity: 0.7,
357 ssr_max_distance: 40.0,
358 ray_traced_reflections: false,
359 reflection_blur_resolution: ReflectionBlurResolution::default(),
360 indirect_lighting: IndirectLighting::Ibl,
361 ambient_intensity: 1.0,
362 ssgi_intensity: 0.5,
363 ssgi_max_distance: 8.0,
364 ssgi_resolution: SsgiResolution::default(),
365 ssgi_rays: DEFAULT_SSGI_RAYS,
366 ssgi_steps: DEFAULT_SSGI_STEPS,
367 auto_exposure: false,
368 auto_exposure_min_ev: -8.0,
369 auto_exposure_max_ev: 8.0,
370 auto_exposure_speed: 1.5,
371 hdr_display: false,
372 hdr_pq: false,
373 temporal_upscaling: false,
374 upscale_quality: UpscaleQuality::default(),
375 upscale_backend: UpscalerBackend::default(),
376 occlusion_two_pass: false,
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn defaults_leave_the_expensive_effects_off() {
387 let c = PostProcessConfig::default();
390 assert_eq!(c.aa_mode, AaMode::Fxaa);
391 assert_eq!(c.bloom_intensity, 0.6);
392 assert!(!c.ssao);
393 assert!(!c.ssr);
394 assert!(!c.ray_traced_reflections);
395 assert!(!c.auto_exposure);
396 assert!(!c.temporal_upscaling);
397 assert!(!c.hdr_display);
398 assert!(!c.occlusion_two_pass);
399 assert_eq!(c.indirect_lighting, IndirectLighting::Ibl);
400 assert_eq!(c.ssgi_rays, DEFAULT_SSGI_RAYS);
401 assert_eq!(c.ssgi_steps, DEFAULT_SSGI_STEPS);
402 }
403
404 #[test]
405 fn every_enum_default_matches_the_config_default() {
406 let c = PostProcessConfig::default();
407 assert_eq!(c.upscale_quality, UpscaleQuality::Quality);
408 assert_eq!(c.upscale_backend, UpscalerBackend::Auto);
409 assert_eq!(c.ssgi_resolution, SsgiResolution::Half);
410 assert_eq!(c.reflection_blur_resolution, ReflectionBlurResolution::Half);
411 assert_eq!(AaMode::default(), AaMode::Fxaa);
412 assert_eq!(IndirectLighting::default(), IndirectLighting::Ibl);
413 }
414
415 #[test]
416 fn upscale_quality_scales_the_render_resolution_down() {
417 assert_eq!(UpscaleQuality::Quality.scale(), 2.0 / 3.0);
419 assert_eq!(UpscaleQuality::Balanced.scale(), 0.587);
420 assert_eq!(UpscaleQuality::Performance.scale(), 0.5);
421 assert_eq!(UpscaleQuality::UltraPerformance.scale(), 1.0 / 3.0);
422 let tiers = [
423 UpscaleQuality::Quality,
424 UpscaleQuality::Balanced,
425 UpscaleQuality::Performance,
426 UpscaleQuality::UltraPerformance,
427 ];
428 assert!(tiers.windows(2).all(|w| w[0].scale() > w[1].scale()));
429 }
430
431 #[test]
432 fn fxaa_runs_for_every_mode_but_off_and_taa_only_for_taa() {
433 assert!(!AaMode::Off.taa_enabled());
436 assert!(!AaMode::Fxaa.taa_enabled());
437 assert!(AaMode::Taa.taa_enabled());
438
439 assert_eq!(AaMode::Off.fxaa_flag(), 0.0);
441 assert_eq!(AaMode::Fxaa.fxaa_flag(), 1.0);
442 assert_eq!(AaMode::Taa.fxaa_flag(), 1.0);
443 }
444
445 #[test]
446 fn half_and_quarter_resolutions_divide_the_target() {
447 assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
448 assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
449 assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
450 assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
451 assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
452 assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
453 }
454
455 #[test]
456 fn enum_names_parse_in_snake_case() {
457 let aa = |s: &str| serde_json::from_str::<AaMode>(s).unwrap();
458 assert_eq!(aa(r#""off""#), AaMode::Off);
459 assert_eq!(aa(r#""fxaa""#), AaMode::Fxaa);
460 assert_eq!(aa(r#""taa""#), AaMode::Taa);
461
462 let q = |s: &str| serde_json::from_str::<UpscaleQuality>(s).unwrap();
463 assert_eq!(q(r#""balanced""#), UpscaleQuality::Balanced);
464 assert_eq!(
465 q(r#""ultra_performance""#),
466 UpscaleQuality::UltraPerformance
467 );
468 assert_eq!(
469 serde_json::to_string(&UpscaleQuality::UltraPerformance).unwrap(),
470 r#""ultra_performance""#
471 );
472
473 let b = |s: &str| serde_json::from_str::<UpscalerBackend>(s).unwrap();
474 assert_eq!(b(r#""auto""#), UpscalerBackend::Auto);
475 assert_eq!(b(r#""fsr3""#), UpscalerBackend::Fsr3);
476 assert_eq!(b(r#""dlss""#), UpscalerBackend::Dlss);
477 assert_eq!(b(r#""xess""#), UpscalerBackend::Xess);
478
479 assert_eq!(
480 serde_json::from_str::<IndirectLighting>(r#""ssgi""#).unwrap(),
481 IndirectLighting::Ssgi
482 );
483 assert_eq!(
484 serde_json::from_str::<SsgiResolution>(r#""quarter""#).unwrap(),
485 SsgiResolution::Quarter
486 );
487 assert_eq!(
488 serde_json::from_str::<ReflectionBlurResolution>(r#""full""#).unwrap(),
489 ReflectionBlurResolution::Full
490 );
491 }
492
493 #[test]
494 fn an_authored_stack_round_trips_through_postcard() {
495 let c: PostProcessConfig = serde_json::from_str(
496 r#"{"aa_mode":"taa","ssao":true,"ssr":true,"indirect_lighting":"ssgi",
497 "ssgi_resolution":"quarter","temporal_upscaling":true,
498 "upscale_quality":"performance","upscale_backend":"dlss",
499 "auto_exposure":true,"hdr_display":true,"hdr_pq":true}"#,
500 )
501 .unwrap();
502 assert!(c.aa_mode.taa_enabled());
503 assert_eq!(c.ssgi_resolution.scale_divisor(), 4);
504 assert_eq!(c.bloom_intensity, 0.6);
506
507 let bytes = postcard::to_allocvec(&c).unwrap();
508 let back: PostProcessConfig = postcard::from_bytes(&bytes).unwrap();
509 assert_eq!(back.aa_mode, AaMode::Taa);
510 assert_eq!(back.upscale_backend, UpscalerBackend::Dlss);
511 assert_eq!(back.upscale_quality, UpscaleQuality::Performance);
512 assert_eq!(back.indirect_lighting, IndirectLighting::Ssgi);
513 assert!(back.hdr_pq);
514 }
515}
516
517const EXPOSURE_EV_LIMIT: f32 = 16.0;
520
521pub trait PostProcessResolve {
525 fn resolve(&self) -> PostProcessTunables;
531
532 fn ambient_intensity(&self) -> f32;
536
537 fn reflection_blur_divisor(&self) -> u32;
540
541 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings>;
544
545 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings>;
548
549 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings>;
554
555 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings>;
558
559 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings>;
562}
563
564impl PostProcessResolve for PostProcessConfig {
565 fn resolve(&self) -> PostProcessTunables {
566 let ev = self
567 .exposure_ev
568 .clamp(-EXPOSURE_EV_LIMIT, EXPOSURE_EV_LIMIT);
569 PostProcessTunables {
570 bloom_intensity: self.bloom_intensity.max(0.0),
571 bloom_threshold: self.bloom_threshold.max(0.0),
572 bloom_knee: self.bloom_knee.max(0.0),
573 exposure: exp2(ev),
574 vignette: self.vignette_strength.clamp(0.0, 1.0),
575 lut_strength: self.lut_strength.clamp(0.0, 1.0),
576 fxaa: self.aa_mode.fxaa_flag(),
577 }
578 }
579
580 fn ambient_intensity(&self) -> f32 {
581 self.ambient_intensity.clamp(0.0, 16.0)
582 }
583
584 fn reflection_blur_divisor(&self) -> u32 {
585 self.reflection_blur_resolution.scale_divisor()
586 }
587
588 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings> {
589 self.ssao
590 .then(|| crate::gfx::ssao::SsaoSettings::resolve(self.ssao_radius, self.ssao_intensity))
591 }
592
593 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings> {
594 self.ssr.then(|| {
595 crate::gfx::ssr::SsrSettings::resolve(self.ssr_intensity, self.ssr_max_distance)
596 })
597 }
598
599 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings> {
600 self.ray_traced_reflections.then(|| {
601 crate::gfx::rt_reflections::RtReflectionSettings::resolve(
602 self.ssr_intensity,
603 self.ssr_max_distance,
604 )
605 })
606 }
607
608 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings> {
609 (self.indirect_lighting == IndirectLighting::Ssgi).then(|| {
610 crate::gfx::ssgi::SsgiSettings::resolve(
611 self.ssgi_intensity,
612 self.ssgi_max_distance,
613 self.ssgi_rays,
614 self.ssgi_steps,
615 self.ssgi_resolution.scale_divisor(),
616 )
617 })
618 }
619
620 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings> {
621 self.auto_exposure.then(|| {
622 crate::gfx::auto_exposure::AutoExposureSettings::resolve(
629 self.auto_exposure_min_ev,
630 self.auto_exposure_max_ev,
631 self.auto_exposure_speed,
632 self.hdr_display,
633 )
634 })
635 }
636}
637
638impl Component for PostProcessConfig {
639 const NAME: &'static str = "PostProcessConfig";
640
641 fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
642 Ok(crate::blob::decode_exact(bytes)?)
643 }
644}
645
646#[cfg(test)]
647mod runtime_tests {
648 use super::*;
649 use crate::components::{
650 AaMode, ReflectionBlurResolution, SsgiResolution, UpscaleQuality, UpscalerBackend,
651 };
652 use alloc::format;
653
654 #[test]
655 fn default_resolves_to_neutral_params() {
656 let p = PostProcessConfig::default().resolve();
657 assert_eq!(p.bloom_intensity, 0.6);
658 assert_eq!(p.bloom_threshold, 1.0);
659 assert_eq!(p.bloom_knee, 0.5);
660 assert_eq!(p.exposure, 1.0);
662 assert_eq!(p.vignette, 0.0);
663 assert_eq!(p.lut_strength, 1.0);
665 assert_eq!(p, PostProcessTunables::DEFAULT);
667 }
668
669 #[test]
670 fn exposure_ev_resolves_to_power_of_two_multiplier() {
671 let cfg = PostProcessConfig {
672 exposure_ev: 2.0,
673 ..Default::default()
674 };
675 assert_eq!(cfg.resolve().exposure, 4.0);
676
677 let cfg = PostProcessConfig {
678 exposure_ev: -1.0,
679 ..Default::default()
680 };
681 assert_eq!(cfg.resolve().exposure, 0.5);
682 }
683
684 #[test]
685 fn exposure_ev_is_clamped_to_a_finite_multiplier() {
686 let cfg = PostProcessConfig {
687 exposure_ev: 1.0e9,
688 ..Default::default()
689 };
690 let exposure = cfg.resolve().exposure;
691 assert!(exposure.is_finite());
692 assert_eq!(exposure, EXPOSURE_EV_LIMIT.exp2());
693 }
694
695 #[test]
696 fn negative_and_overrange_inputs_are_clamped() {
697 let cfg = PostProcessConfig {
698 bloom_intensity: -3.0,
699 bloom_threshold: -1.0,
700 bloom_knee: -0.2,
701 vignette_strength: 5.0,
702 lut_strength: -2.0,
703 ..Default::default()
704 };
705 let p = cfg.resolve();
706 assert_eq!(p.bloom_intensity, 0.0);
707 assert_eq!(p.bloom_threshold, 0.0);
708 assert_eq!(p.bloom_knee, 0.0);
709 assert_eq!(p.vignette, 1.0);
710 assert_eq!(p.lut_strength, 0.0);
711 }
712
713 #[test]
714 fn lut_strength_is_clamped_to_unit_range() {
715 let cfg = PostProcessConfig {
716 lut_strength: 3.0,
717 ..Default::default()
718 };
719 assert_eq!(cfg.resolve().lut_strength, 1.0);
720 }
721
722 #[test]
723 fn aa_mode_defaults_to_fxaa_and_round_trips_through_args() {
724 assert_eq!(PostProcessConfig::default().aa_mode, AaMode::Fxaa);
725 let cfg = PostProcessConfig {
726 aa_mode: AaMode::Taa,
727 ..Default::default()
728 };
729 assert_eq!(cfg.clone().aa_mode, AaMode::Taa);
730 }
731
732 #[test]
733 fn aa_mode_gates_taa_and_fxaa() {
734 assert!(!AaMode::Off.taa_enabled());
735 assert!(!AaMode::Fxaa.taa_enabled());
736 assert!(AaMode::Taa.taa_enabled());
737 let off = PostProcessConfig {
739 aa_mode: AaMode::Off,
740 ..Default::default()
741 };
742 assert_eq!(off.resolve().fxaa, 0.0);
743 assert_eq!(PostProcessConfig::default().resolve().fxaa, 1.0);
744 }
745
746 #[test]
747 fn ssao_defaults_off_with_neutral_tunables() {
748 let cfg = PostProcessConfig::default();
749 assert!(!cfg.ssao);
750 assert_eq!(cfg.ssao_radius, 0.5);
751 assert_eq!(cfg.ssao_intensity, 1.0);
752 assert!(cfg.ssao_settings().is_none());
754 }
755
756 #[test]
757 fn ssao_settings_resolve_and_clamp_when_enabled() {
758 let cfg = PostProcessConfig {
759 ssao: true,
760 ssao_radius: -1.0,
761 ssao_intensity: 99.0,
762 ..Default::default()
763 };
764 let s = cfg.ssao_settings().expect("ssao on");
765 assert!(s.radius > 0.0);
766 assert_eq!(s.intensity, 4.0);
767 }
768
769 #[test]
770 fn ssao_deserialises_from_jsonl_args() {
771 let cfg: PostProcessConfig =
772 serde_json::from_str(r#"{"ssao":true,"ssao_radius":0.6}"#).expect("parse");
773 assert!(cfg.ssao);
774 assert_eq!(cfg.ssao_radius, 0.6);
775 assert_eq!(cfg.ssao_intensity, 1.0);
777 }
778
779 #[test]
780 fn ssr_defaults_off_with_neutral_tunables() {
781 let cfg = PostProcessConfig::default();
782 assert!(!cfg.ssr);
783 assert_eq!(cfg.ssr_intensity, 0.7);
784 assert_eq!(cfg.ssr_max_distance, 40.0);
785 assert!(cfg.ssr_settings().is_none());
787 }
788
789 #[test]
790 fn ssr_settings_resolve_and_clamp_when_enabled() {
791 let cfg = PostProcessConfig {
792 ssr: true,
793 ssr_intensity: 9.0,
794 ssr_max_distance: 1.0e6,
795 ..Default::default()
796 };
797 let s = cfg.ssr_settings().expect("ssr on");
798 assert_eq!(s.intensity, 1.0);
799 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
800 }
801
802 #[test]
803 fn ssr_deserialises_from_jsonl_args() {
804 let cfg: PostProcessConfig =
805 serde_json::from_str(r#"{"ssr":true,"ssr_intensity":0.5}"#).expect("parse");
806 assert!(cfg.ssr);
807 assert_eq!(cfg.ssr_intensity, 0.5);
808 assert_eq!(cfg.ssr_max_distance, 40.0);
810 }
811
812 #[test]
813 fn rt_reflections_default_off_and_resolve_to_none() {
814 let cfg = PostProcessConfig::default();
815 assert!(!cfg.ray_traced_reflections);
816 assert!(cfg.rt_reflection_settings().is_none());
818 }
819
820 #[test]
821 fn rt_reflection_settings_reuse_ssr_tunables_when_enabled() {
822 let cfg = PostProcessConfig {
823 ray_traced_reflections: true,
824 ssr_intensity: 9.0,
825 ssr_max_distance: 1.0e6,
826 ..Default::default()
827 };
828 let s = cfg.rt_reflection_settings().expect("rt on");
829 assert_eq!(s.intensity, 1.0);
831 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
832 }
833
834 #[test]
835 fn rt_reflections_deserialise_from_jsonl_args() {
836 let cfg: PostProcessConfig =
837 serde_json::from_str(r#"{"ray_traced_reflections":true,"ssr_intensity":0.5}"#)
838 .expect("parse");
839 assert!(cfg.ray_traced_reflections);
840 assert!(cfg.rt_reflection_settings().is_some());
841 let cfg: PostProcessConfig =
843 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
844 assert!(!cfg.ray_traced_reflections);
845 assert!(cfg.rt_reflection_settings().is_none());
846 }
847
848 #[test]
849 fn ambient_intensity_defaults_neutral_and_clamps() {
850 assert_eq!(PostProcessConfig::default().ambient_intensity(), 1.0);
852 let hot = PostProcessConfig {
854 ambient_intensity: 100.0,
855 ..Default::default()
856 };
857 assert_eq!(hot.ambient_intensity(), 16.0);
858 let neg = PostProcessConfig {
859 ambient_intensity: -2.0,
860 ..Default::default()
861 };
862 assert_eq!(neg.ambient_intensity(), 0.0);
863 let cfg: PostProcessConfig =
865 serde_json::from_str(r#"{"ambient_intensity":3.5}"#).expect("parse");
866 assert_eq!(cfg.ambient_intensity(), 3.5);
867 }
868
869 #[test]
870 fn ssgi_defaults_to_ibl_with_neutral_tunables() {
871 let cfg = PostProcessConfig::default();
872 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
873 assert_eq!(cfg.ssgi_intensity, 0.5);
874 assert_eq!(cfg.ssgi_max_distance, 8.0);
875 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
878 assert_eq!(cfg.ssgi_rays, 8);
879 assert_eq!(cfg.ssgi_steps, 12);
880 assert!(cfg.ssgi_settings().is_none());
882 }
883
884 #[test]
885 fn ssgi_resolution_maps_to_a_per_axis_divisor() {
886 assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
887 assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
888 assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
889 assert_eq!(SsgiResolution::default(), SsgiResolution::Half);
890 }
891
892 #[test]
893 fn ssgi_resolution_and_counts_flow_into_settings() {
894 let cfg = PostProcessConfig {
895 indirect_lighting: IndirectLighting::Ssgi,
896 ssgi_resolution: SsgiResolution::Quarter,
897 ssgi_rays: 4,
898 ssgi_steps: 20,
899 ..Default::default()
900 };
901 let s = cfg.ssgi_settings().expect("ssgi on");
902 assert_eq!(s.rays, 4);
903 assert_eq!(s.steps, 20);
904 assert_eq!(s.gi_scale, 4);
905 }
906
907 #[test]
908 fn ssgi_resolution_and_counts_deserialise_from_jsonl_args() {
909 let cfg: PostProcessConfig = serde_json::from_str(
910 r#"{"indirect_lighting":"ssgi","ssgi_resolution":"full","ssgi_rays":16,"ssgi_steps":8}"#,
911 )
912 .expect("parse");
913 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Full);
914 assert_eq!(cfg.ssgi_rays, 16);
915 assert_eq!(cfg.ssgi_steps, 8);
916 let cfg: PostProcessConfig =
918 serde_json::from_str(r#"{"indirect_lighting":"ssgi"}"#).expect("parse");
919 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
920 assert_eq!(cfg.ssgi_rays, 8);
921 assert_eq!(cfg.ssgi_steps, 12);
922 }
923
924 #[test]
925 fn reflection_blur_resolution_defaults_to_half() {
926 let cfg = PostProcessConfig::default();
927 assert_eq!(
928 cfg.reflection_blur_resolution,
929 ReflectionBlurResolution::Half
930 );
931 assert_eq!(cfg.reflection_blur_divisor(), 2);
932 }
933
934 #[test]
935 fn reflection_blur_resolution_maps_to_a_per_axis_divisor() {
936 assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
937 assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
938 assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
939 assert_eq!(
940 ReflectionBlurResolution::default(),
941 ReflectionBlurResolution::Half
942 );
943 }
944
945 #[test]
946 fn reflection_blur_resolution_deserialises_from_jsonl_args() {
947 let cfg: PostProcessConfig =
948 serde_json::from_str(r#"{"ssr":true,"reflection_blur_resolution":"quarter"}"#)
949 .expect("parse");
950 assert_eq!(
951 cfg.reflection_blur_resolution,
952 ReflectionBlurResolution::Quarter
953 );
954 assert_eq!(cfg.reflection_blur_divisor(), 4);
955 let cfg: PostProcessConfig = serde_json::from_str(r#"{"ssr":true}"#).expect("parse");
957 assert_eq!(
958 cfg.reflection_blur_resolution,
959 ReflectionBlurResolution::Half
960 );
961 assert_eq!(cfg.reflection_blur_divisor(), 2);
962 }
963
964 #[test]
965 fn ssgi_settings_resolve_and_clamp_when_enabled() {
966 let cfg = PostProcessConfig {
967 indirect_lighting: IndirectLighting::Ssgi,
968 ssgi_intensity: 99.0,
969 ssgi_max_distance: 1.0e6,
970 ..Default::default()
971 };
972 let s = cfg.ssgi_settings().expect("ssgi on");
973 assert_eq!(s.intensity, 4.0);
974 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
975 }
976
977 #[test]
978 fn ssgi_deserialises_from_jsonl_args() {
979 let cfg: PostProcessConfig =
980 serde_json::from_str(r#"{"indirect_lighting":"ssgi","ssgi_intensity":0.8}"#)
981 .expect("parse");
982 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ssgi);
983 assert_eq!(cfg.ssgi_intensity, 0.8);
984 assert_eq!(cfg.ssgi_max_distance, 8.0);
986 let cfg: PostProcessConfig =
988 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
989 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
990 assert!(cfg.ssgi_settings().is_none());
991 }
992
993 #[test]
994 fn auto_exposure_defaults_off_with_neutral_tunables() {
995 let cfg = PostProcessConfig::default();
996 assert!(!cfg.auto_exposure);
997 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
998 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
999 assert_eq!(cfg.auto_exposure_speed, 1.5);
1000 assert!(cfg.auto_exposure_settings().is_none());
1001 }
1002
1003 #[test]
1004 fn auto_exposure_settings_resolve_when_enabled() {
1005 let cfg = PostProcessConfig {
1006 auto_exposure: true,
1007 auto_exposure_min_ev: -4.0,
1008 auto_exposure_max_ev: 6.0,
1009 auto_exposure_speed: 2.0,
1010 ..Default::default()
1011 };
1012 let s = cfg.auto_exposure_settings().expect("auto-exposure on");
1013 assert_eq!(s.min_ev, -4.0);
1014 assert_eq!(s.max_ev, 6.0);
1015 assert_eq!(s.speed, 2.0);
1016 }
1017
1018 #[test]
1019 fn auto_exposure_deserialises_from_jsonl_args() {
1020 let cfg: PostProcessConfig =
1021 serde_json::from_str(r#"{"auto_exposure":true,"auto_exposure_speed":3.0}"#)
1022 .expect("parse");
1023 assert!(cfg.auto_exposure);
1024 assert_eq!(cfg.auto_exposure_speed, 3.0);
1025 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
1027 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
1028 }
1029
1030 #[test]
1031 fn aa_mode_deserialises_from_jsonl_args() {
1032 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"taa"}"#).expect("parse");
1033 assert_eq!(cfg.aa_mode, AaMode::Taa);
1034 let cfg: PostProcessConfig =
1036 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
1037 assert_eq!(cfg.aa_mode, AaMode::Fxaa);
1038 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"off"}"#).expect("parse");
1040 assert_eq!(cfg.aa_mode, AaMode::Off);
1041 }
1042
1043 #[test]
1044 fn hdr_display_defaults_off() {
1045 assert!(!PostProcessConfig::default().hdr_display);
1046 }
1047
1048 #[test]
1049 fn hdr_display_round_trips_through_args_and_jsonl() {
1050 let cfg = PostProcessConfig {
1051 hdr_display: true,
1052 ..Default::default()
1053 };
1054 assert!(cfg.clone().hdr_display);
1055
1056 let cfg: PostProcessConfig =
1057 serde_json::from_str(r#"{"hdr_display":true}"#).expect("parse");
1058 assert!(cfg.hdr_display);
1059 }
1060
1061 #[test]
1062 fn temporal_upscaling_defaults_off_with_quality_preset() {
1063 let cfg = PostProcessConfig::default();
1064 assert!(!cfg.temporal_upscaling);
1065 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
1066 }
1067
1068 #[test]
1069 fn upscale_quality_scales_are_monotonic() {
1070 let q = UpscaleQuality::Quality.scale();
1073 let b = UpscaleQuality::Balanced.scale();
1074 let p = UpscaleQuality::Performance.scale();
1075 let u = UpscaleQuality::UltraPerformance.scale();
1076 assert!(q > b && b > p && p > u);
1077 assert!(u > 0.0);
1078 }
1079
1080 #[test]
1081 fn occlusion_two_pass_defaults_off_and_round_trips() {
1082 assert!(!PostProcessConfig::default().occlusion_two_pass);
1083 let cfg = PostProcessConfig {
1084 occlusion_two_pass: true,
1085 ..Default::default()
1086 };
1087 assert!(cfg.clone().occlusion_two_pass);
1088 let cfg: PostProcessConfig =
1090 serde_json::from_str(r#"{"occlusion_two_pass":true}"#).expect("parse");
1091 assert!(cfg.occlusion_two_pass);
1092 let cfg: PostProcessConfig =
1093 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
1094 assert!(!cfg.occlusion_two_pass);
1095 }
1096
1097 #[test]
1098 fn upscale_backend_defaults_to_auto() {
1099 assert_eq!(
1100 PostProcessConfig::default().upscale_backend,
1101 UpscalerBackend::Auto
1102 );
1103 assert_eq!(UpscalerBackend::default(), UpscalerBackend::Auto);
1104 }
1105
1106 #[test]
1107 fn upscale_backend_round_trips_via_snake_case_json() {
1108 for (s, want) in [
1109 ("auto", UpscalerBackend::Auto),
1110 ("fsr3", UpscalerBackend::Fsr3),
1111 ("dlss", UpscalerBackend::Dlss),
1112 ("xess", UpscalerBackend::Xess),
1113 ] {
1114 let json = format!(r#"{{"temporal_upscaling":true,"upscale_backend":"{s}"}}"#);
1115 let cfg: PostProcessConfig = serde_json::from_str(&json).expect("parse");
1116 assert_eq!(cfg.upscale_backend, want, "for {s}");
1117 }
1118 let cfg: PostProcessConfig =
1120 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
1121 assert_eq!(cfg.upscale_backend, UpscalerBackend::Auto);
1122 }
1123
1124 #[test]
1125 fn upscale_backend_round_trips_through_args() {
1126 let cfg = PostProcessConfig {
1127 upscale_backend: UpscalerBackend::Xess,
1128 ..Default::default()
1129 };
1130 assert_eq!(cfg.clone().upscale_backend, UpscalerBackend::Xess);
1131 }
1132
1133 #[test]
1134 fn upscale_quality_round_trips_via_snake_case_json() {
1135 let cfg: PostProcessConfig =
1136 serde_json::from_str(r#"{"temporal_upscaling":true,"upscale_quality":"performance"}"#)
1137 .expect("parse");
1138 assert!(cfg.temporal_upscaling);
1139 assert_eq!(cfg.upscale_quality, UpscaleQuality::Performance);
1140 let cfg: PostProcessConfig =
1142 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
1143 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
1144 }
1145}