1use crate::ecs::Component;
9use crate::gfx::render_types::PostProcessTunables;
10use crate::math::exp2;
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
42#[serde(default)]
43pub struct PostProcessConfig {
44 pub bloom_intensity: f32,
46 pub bloom_threshold: f32,
49 pub bloom_knee: f32,
51 pub exposure_ev: f32,
54 pub vignette_strength: f32,
56 pub lut_strength: f32,
60 pub aa_mode: AaMode,
66 pub ssao: bool,
70 pub ssao_radius: f32,
73 pub ssao_intensity: f32,
76 pub ssr: bool,
80 pub ssr_intensity: f32,
83 pub ssr_max_distance: f32,
86 pub ray_traced_reflections: bool,
93 pub reflection_blur_resolution: ReflectionBlurResolution,
101 pub indirect_lighting: IndirectLighting,
107 pub ambient_intensity: f32,
116 pub ssgi_intensity: f32,
120 pub ssgi_max_distance: f32,
124 pub ssgi_resolution: SsgiResolution,
128 pub ssgi_rays: u32,
132 pub ssgi_steps: u32,
136 pub auto_exposure: bool,
140 pub auto_exposure_min_ev: f32,
143 pub auto_exposure_max_ev: f32,
145 pub auto_exposure_speed: f32,
148 pub hdr_display: bool,
152 pub hdr_pq: bool,
156 pub temporal_upscaling: bool,
161 pub upscale_quality: UpscaleQuality,
165 pub upscale_backend: UpscalerBackend,
171 pub occlusion_two_pass: bool,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
182#[serde(rename_all = "snake_case")]
183#[derive(Default)]
184pub enum UpscaleQuality {
185 #[default]
187 Quality,
188 Balanced,
190 Performance,
192 UltraPerformance,
194}
195
196impl UpscaleQuality {
197 pub fn scale(self) -> f32 {
200 match self {
201 UpscaleQuality::Quality => 2.0 / 3.0,
202 UpscaleQuality::Balanced => 0.587,
203 UpscaleQuality::Performance => 0.5,
204 UpscaleQuality::UltraPerformance => 1.0 / 3.0,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
216#[serde(rename_all = "snake_case")]
217#[derive(Default)]
218pub enum UpscalerBackend {
219 #[default]
221 Auto,
222 Fsr3,
224 Dlss,
226 Xess,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
236#[serde(rename_all = "snake_case")]
237#[derive(Default)]
238pub enum AaMode {
239 Off,
241 #[default]
243 Fxaa,
244 Taa,
246}
247
248impl AaMode {
249 pub fn taa_enabled(self) -> bool {
253 matches!(self, AaMode::Taa)
254 }
255
256 fn fxaa_enabled(self) -> bool {
260 !matches!(self, AaMode::Off)
261 }
262
263 pub fn fxaa_flag(self) -> f32 {
266 if self.fxaa_enabled() { 1.0 } else { 0.0 }
267 }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
274#[serde(rename_all = "snake_case")]
275#[derive(Default)]
276pub enum IndirectLighting {
277 #[default]
279 Ibl,
280 Ssgi,
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
292#[serde(rename_all = "snake_case")]
293#[derive(Default)]
294pub enum SsgiResolution {
295 Full,
297 #[default]
299 Half,
300 Quarter,
302}
303
304impl SsgiResolution {
305 pub fn scale_divisor(self) -> u32 {
307 match self {
308 SsgiResolution::Full => 1,
309 SsgiResolution::Half => 2,
310 SsgiResolution::Quarter => 4,
311 }
312 }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
324#[serde(rename_all = "snake_case")]
325#[derive(Default)]
326pub enum ReflectionBlurResolution {
327 Full,
329 #[default]
331 Half,
332 Quarter,
334}
335
336impl ReflectionBlurResolution {
337 pub fn scale_divisor(self) -> u32 {
340 match self {
341 ReflectionBlurResolution::Full => 1,
342 ReflectionBlurResolution::Half => 2,
343 ReflectionBlurResolution::Quarter => 4,
344 }
345 }
346}
347
348pub const DEFAULT_SSGI_RAYS: u32 = 8;
353pub const DEFAULT_SSGI_STEPS: u32 = 12;
355
356impl Default for PostProcessConfig {
357 fn default() -> Self {
358 Self {
359 bloom_intensity: 0.6,
360 bloom_threshold: 1.0,
361 bloom_knee: 0.5,
362 exposure_ev: 0.0,
363 vignette_strength: 0.0,
364 lut_strength: 1.0,
365 aa_mode: AaMode::Taa,
366 ssao: true,
367 ssao_radius: 0.5,
368 ssao_intensity: 1.0,
369 ssr: true,
370 ssr_intensity: 0.7,
371 ssr_max_distance: 40.0,
372 ray_traced_reflections: true,
373 reflection_blur_resolution: ReflectionBlurResolution::default(),
374 indirect_lighting: IndirectLighting::Ssgi,
375 ambient_intensity: 1.0,
376 ssgi_intensity: 0.5,
377 ssgi_max_distance: 8.0,
378 ssgi_resolution: SsgiResolution::default(),
379 ssgi_rays: DEFAULT_SSGI_RAYS,
380 ssgi_steps: DEFAULT_SSGI_STEPS,
381 auto_exposure: false,
382 auto_exposure_min_ev: -8.0,
383 auto_exposure_max_ev: 8.0,
384 auto_exposure_speed: 1.5,
385 hdr_display: false,
386 hdr_pq: false,
387 temporal_upscaling: false,
388 upscale_quality: UpscaleQuality::default(),
389 upscale_backend: UpscalerBackend::default(),
390 occlusion_two_pass: true,
391 }
392 }
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn defaults_author_the_capable_hardware_look() {
401 let c = PostProcessConfig::default();
405 assert_eq!(c.aa_mode, AaMode::Taa);
406 assert_eq!(c.bloom_intensity, 0.6);
407 assert!(c.ssao);
408 assert!(c.ssr);
409 assert!(c.ray_traced_reflections);
410 assert!(c.occlusion_two_pass);
411 assert_eq!(c.indirect_lighting, IndirectLighting::Ssgi);
412 assert_eq!(c.ssgi_rays, DEFAULT_SSGI_RAYS);
413 assert_eq!(c.ssgi_steps, DEFAULT_SSGI_STEPS);
414 }
415
416 #[test]
417 fn look_and_display_choices_stay_off_by_default() {
418 let c = PostProcessConfig::default();
422 assert!(!c.auto_exposure);
423 assert!(!c.temporal_upscaling);
424 assert!(!c.hdr_display);
425 assert!(!c.hdr_pq);
426 assert_eq!(c.vignette_strength, 0.0);
427 }
428
429 #[test]
430 fn enum_defaults_are_the_cheap_variants() {
431 let c = PostProcessConfig::default();
432 assert_eq!(c.upscale_quality, UpscaleQuality::Quality);
433 assert_eq!(c.upscale_backend, UpscalerBackend::Auto);
434 assert_eq!(c.ssgi_resolution, SsgiResolution::Half);
435 assert_eq!(c.reflection_blur_resolution, ReflectionBlurResolution::Half);
436 assert_eq!(AaMode::default(), AaMode::Fxaa);
437 assert_eq!(IndirectLighting::default(), IndirectLighting::Ibl);
438 assert_ne!(c.aa_mode, AaMode::default());
442 assert_ne!(c.indirect_lighting, IndirectLighting::default());
443 }
444
445 #[test]
446 fn upscale_quality_scales_the_render_resolution_down() {
447 assert_eq!(UpscaleQuality::Quality.scale(), 2.0 / 3.0);
449 assert_eq!(UpscaleQuality::Balanced.scale(), 0.587);
450 assert_eq!(UpscaleQuality::Performance.scale(), 0.5);
451 assert_eq!(UpscaleQuality::UltraPerformance.scale(), 1.0 / 3.0);
452 let tiers = [
453 UpscaleQuality::Quality,
454 UpscaleQuality::Balanced,
455 UpscaleQuality::Performance,
456 UpscaleQuality::UltraPerformance,
457 ];
458 assert!(tiers.windows(2).all(|w| w[0].scale() > w[1].scale()));
459 }
460
461 #[test]
462 fn fxaa_runs_for_every_mode_but_off_and_taa_only_for_taa() {
463 assert!(!AaMode::Off.taa_enabled());
466 assert!(!AaMode::Fxaa.taa_enabled());
467 assert!(AaMode::Taa.taa_enabled());
468
469 assert_eq!(AaMode::Off.fxaa_flag(), 0.0);
471 assert_eq!(AaMode::Fxaa.fxaa_flag(), 1.0);
472 assert_eq!(AaMode::Taa.fxaa_flag(), 1.0);
473 }
474
475 #[test]
476 fn half_and_quarter_resolutions_divide_the_target() {
477 assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
478 assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
479 assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
480 assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
481 assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
482 assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
483 }
484
485 #[test]
486 fn enum_names_parse_in_snake_case() {
487 let aa = |s: &str| serde_json::from_str::<AaMode>(s).unwrap();
488 assert_eq!(aa(r#""off""#), AaMode::Off);
489 assert_eq!(aa(r#""fxaa""#), AaMode::Fxaa);
490 assert_eq!(aa(r#""taa""#), AaMode::Taa);
491
492 let q = |s: &str| serde_json::from_str::<UpscaleQuality>(s).unwrap();
493 assert_eq!(q(r#""balanced""#), UpscaleQuality::Balanced);
494 assert_eq!(
495 q(r#""ultra_performance""#),
496 UpscaleQuality::UltraPerformance
497 );
498 assert_eq!(
499 serde_json::to_string(&UpscaleQuality::UltraPerformance).unwrap(),
500 r#""ultra_performance""#
501 );
502
503 let b = |s: &str| serde_json::from_str::<UpscalerBackend>(s).unwrap();
504 assert_eq!(b(r#""auto""#), UpscalerBackend::Auto);
505 assert_eq!(b(r#""fsr3""#), UpscalerBackend::Fsr3);
506 assert_eq!(b(r#""dlss""#), UpscalerBackend::Dlss);
507 assert_eq!(b(r#""xess""#), UpscalerBackend::Xess);
508
509 assert_eq!(
510 serde_json::from_str::<IndirectLighting>(r#""ssgi""#).unwrap(),
511 IndirectLighting::Ssgi
512 );
513 assert_eq!(
514 serde_json::from_str::<SsgiResolution>(r#""quarter""#).unwrap(),
515 SsgiResolution::Quarter
516 );
517 assert_eq!(
518 serde_json::from_str::<ReflectionBlurResolution>(r#""full""#).unwrap(),
519 ReflectionBlurResolution::Full
520 );
521 }
522
523 #[test]
524 fn an_authored_stack_round_trips_through_postcard() {
525 let c: PostProcessConfig = serde_json::from_str(
526 r#"{"aa_mode":"taa","ssao":true,"ssr":true,"indirect_lighting":"ssgi",
527 "ssgi_resolution":"quarter","temporal_upscaling":true,
528 "upscale_quality":"performance","upscale_backend":"dlss",
529 "auto_exposure":true,"hdr_display":true,"hdr_pq":true}"#,
530 )
531 .unwrap();
532 assert!(c.aa_mode.taa_enabled());
533 assert_eq!(c.ssgi_resolution.scale_divisor(), 4);
534 assert_eq!(c.bloom_intensity, 0.6);
536
537 let bytes = postcard::to_allocvec(&c).unwrap();
538 let back: PostProcessConfig = postcard::from_bytes(&bytes).unwrap();
539 assert_eq!(back.aa_mode, AaMode::Taa);
540 assert_eq!(back.upscale_backend, UpscalerBackend::Dlss);
541 assert_eq!(back.upscale_quality, UpscaleQuality::Performance);
542 assert_eq!(back.indirect_lighting, IndirectLighting::Ssgi);
543 assert!(back.hdr_pq);
544 }
545}
546
547const EXPOSURE_EV_LIMIT: f32 = 16.0;
550
551pub trait PostProcessResolve {
555 fn resolve(&self) -> PostProcessTunables;
561
562 fn ambient_intensity(&self) -> f32;
566
567 fn reflection_blur_divisor(&self) -> u32;
570
571 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings>;
574
575 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings>;
578
579 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings>;
584
585 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings>;
588
589 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings>;
592}
593
594impl PostProcessResolve for PostProcessConfig {
595 fn resolve(&self) -> PostProcessTunables {
596 let ev = self
597 .exposure_ev
598 .clamp(-EXPOSURE_EV_LIMIT, EXPOSURE_EV_LIMIT);
599 PostProcessTunables {
600 bloom_intensity: self.bloom_intensity.max(0.0),
601 bloom_threshold: self.bloom_threshold.max(0.0),
602 bloom_knee: self.bloom_knee.max(0.0),
603 exposure: exp2(ev),
604 vignette: self.vignette_strength.clamp(0.0, 1.0),
605 lut_strength: self.lut_strength.clamp(0.0, 1.0),
606 fxaa: self.aa_mode.fxaa_flag(),
607 }
608 }
609
610 fn ambient_intensity(&self) -> f32 {
611 self.ambient_intensity.clamp(0.0, 16.0)
612 }
613
614 fn reflection_blur_divisor(&self) -> u32 {
615 self.reflection_blur_resolution.scale_divisor()
616 }
617
618 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings> {
619 self.ssao
620 .then(|| crate::gfx::ssao::SsaoSettings::resolve(self.ssao_radius, self.ssao_intensity))
621 }
622
623 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings> {
624 self.ssr.then(|| {
625 crate::gfx::ssr::SsrSettings::resolve(self.ssr_intensity, self.ssr_max_distance)
626 })
627 }
628
629 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings> {
630 self.ray_traced_reflections.then(|| {
631 crate::gfx::rt_reflections::RtReflectionSettings::resolve(
632 self.ssr_intensity,
633 self.ssr_max_distance,
634 )
635 })
636 }
637
638 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings> {
639 (self.indirect_lighting == IndirectLighting::Ssgi).then(|| {
640 crate::gfx::ssgi::SsgiSettings::resolve(
641 self.ssgi_intensity,
642 self.ssgi_max_distance,
643 self.ssgi_rays,
644 self.ssgi_steps,
645 self.ssgi_resolution.scale_divisor(),
646 )
647 })
648 }
649
650 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings> {
651 self.auto_exposure.then(|| {
652 crate::gfx::auto_exposure::AutoExposureSettings::resolve(
659 self.auto_exposure_min_ev,
660 self.auto_exposure_max_ev,
661 self.auto_exposure_speed,
662 self.hdr_display,
663 )
664 })
665 }
666}
667
668impl Component for PostProcessConfig {
669 const NAME: &'static str = "PostProcessConfig";
670
671 fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
672 Ok(crate::blob::decode_exact(bytes)?)
673 }
674}
675
676#[cfg(test)]
677mod runtime_tests {
678 use super::*;
679 use crate::components::{
680 AaMode, ReflectionBlurResolution, SsgiResolution, UpscaleQuality, UpscalerBackend,
681 };
682 use alloc::format;
683
684 #[test]
685 fn default_resolves_to_neutral_params() {
686 let p = PostProcessConfig::default().resolve();
687 assert_eq!(p.bloom_intensity, 0.6);
688 assert_eq!(p.bloom_threshold, 1.0);
689 assert_eq!(p.bloom_knee, 0.5);
690 assert_eq!(p.exposure, 1.0);
692 assert_eq!(p.vignette, 0.0);
693 assert_eq!(p.lut_strength, 1.0);
695 assert_eq!(p, PostProcessTunables::DEFAULT);
697 }
698
699 #[test]
700 fn exposure_ev_resolves_to_power_of_two_multiplier() {
701 let cfg = PostProcessConfig {
702 exposure_ev: 2.0,
703 ..Default::default()
704 };
705 assert_eq!(cfg.resolve().exposure, 4.0);
706
707 let cfg = PostProcessConfig {
708 exposure_ev: -1.0,
709 ..Default::default()
710 };
711 assert_eq!(cfg.resolve().exposure, 0.5);
712 }
713
714 #[test]
715 fn exposure_ev_is_clamped_to_a_finite_multiplier() {
716 let cfg = PostProcessConfig {
717 exposure_ev: 1.0e9,
718 ..Default::default()
719 };
720 let exposure = cfg.resolve().exposure;
721 assert!(exposure.is_finite());
722 assert_eq!(exposure, EXPOSURE_EV_LIMIT.exp2());
723 }
724
725 #[test]
726 fn negative_and_overrange_inputs_are_clamped() {
727 let cfg = PostProcessConfig {
728 bloom_intensity: -3.0,
729 bloom_threshold: -1.0,
730 bloom_knee: -0.2,
731 vignette_strength: 5.0,
732 lut_strength: -2.0,
733 ..Default::default()
734 };
735 let p = cfg.resolve();
736 assert_eq!(p.bloom_intensity, 0.0);
737 assert_eq!(p.bloom_threshold, 0.0);
738 assert_eq!(p.bloom_knee, 0.0);
739 assert_eq!(p.vignette, 1.0);
740 assert_eq!(p.lut_strength, 0.0);
741 }
742
743 #[test]
744 fn lut_strength_is_clamped_to_unit_range() {
745 let cfg = PostProcessConfig {
746 lut_strength: 3.0,
747 ..Default::default()
748 };
749 assert_eq!(cfg.resolve().lut_strength, 1.0);
750 }
751
752 #[test]
753 fn aa_mode_defaults_to_taa_and_round_trips_through_args() {
754 assert_eq!(PostProcessConfig::default().aa_mode, AaMode::Taa);
755 let cfg = PostProcessConfig {
756 aa_mode: AaMode::Fxaa,
757 ..Default::default()
758 };
759 assert_eq!(cfg.clone().aa_mode, AaMode::Fxaa);
760 }
761
762 #[test]
763 fn aa_mode_gates_taa_and_fxaa() {
764 assert!(!AaMode::Off.taa_enabled());
765 assert!(!AaMode::Fxaa.taa_enabled());
766 assert!(AaMode::Taa.taa_enabled());
767 let off = PostProcessConfig {
769 aa_mode: AaMode::Off,
770 ..Default::default()
771 };
772 assert_eq!(off.resolve().fxaa, 0.0);
773 assert_eq!(PostProcessConfig::default().resolve().fxaa, 1.0);
774 }
775
776 #[test]
777 fn ssao_defaults_on_with_neutral_tunables() {
778 let cfg = PostProcessConfig::default();
779 assert!(cfg.ssao);
780 assert_eq!(cfg.ssao_radius, 0.5);
781 assert_eq!(cfg.ssao_intensity, 1.0);
782 assert!(cfg.ssao_settings().is_some());
783 let off = PostProcessConfig {
785 ssao: false,
786 ..Default::default()
787 };
788 assert!(off.ssao_settings().is_none());
789 }
790
791 #[test]
792 fn ssao_settings_resolve_and_clamp_when_enabled() {
793 let cfg = PostProcessConfig {
794 ssao: true,
795 ssao_radius: -1.0,
796 ssao_intensity: 99.0,
797 ..Default::default()
798 };
799 let s = cfg.ssao_settings().expect("ssao on");
800 assert!(s.radius > 0.0);
801 assert_eq!(s.intensity, 4.0);
802 }
803
804 #[test]
805 fn ssao_deserialises_from_jsonl_args() {
806 let cfg: PostProcessConfig =
807 serde_json::from_str(r#"{"ssao":true,"ssao_radius":0.6}"#).expect("parse");
808 assert!(cfg.ssao);
809 assert_eq!(cfg.ssao_radius, 0.6);
810 assert_eq!(cfg.ssao_intensity, 1.0);
812 }
813
814 #[test]
815 fn ssr_defaults_on_with_neutral_tunables() {
816 let cfg = PostProcessConfig::default();
817 assert!(cfg.ssr);
818 assert_eq!(cfg.ssr_intensity, 0.7);
819 assert_eq!(cfg.ssr_max_distance, 40.0);
820 assert!(cfg.ssr_settings().is_some());
821 let off = PostProcessConfig {
823 ssr: false,
824 ..Default::default()
825 };
826 assert!(off.ssr_settings().is_none());
827 }
828
829 #[test]
830 fn ssr_settings_resolve_and_clamp_when_enabled() {
831 let cfg = PostProcessConfig {
832 ssr: true,
833 ssr_intensity: 9.0,
834 ssr_max_distance: 1.0e6,
835 ..Default::default()
836 };
837 let s = cfg.ssr_settings().expect("ssr on");
838 assert_eq!(s.intensity, 1.0);
839 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
840 }
841
842 #[test]
843 fn ssr_deserialises_from_jsonl_args() {
844 let cfg: PostProcessConfig =
845 serde_json::from_str(r#"{"ssr":true,"ssr_intensity":0.5}"#).expect("parse");
846 assert!(cfg.ssr);
847 assert_eq!(cfg.ssr_intensity, 0.5);
848 assert_eq!(cfg.ssr_max_distance, 40.0);
850 }
851
852 #[test]
853 fn rt_reflections_default_on_and_resolve_to_settings() {
854 let cfg = PostProcessConfig::default();
855 assert!(cfg.ray_traced_reflections);
856 assert!(cfg.rt_reflection_settings().is_some());
857 let off = PostProcessConfig {
859 ray_traced_reflections: false,
860 ..Default::default()
861 };
862 assert!(off.rt_reflection_settings().is_none());
863 }
864
865 #[test]
866 fn rt_reflection_settings_reuse_ssr_tunables_when_enabled() {
867 let cfg = PostProcessConfig {
868 ray_traced_reflections: true,
869 ssr_intensity: 9.0,
870 ssr_max_distance: 1.0e6,
871 ..Default::default()
872 };
873 let s = cfg.rt_reflection_settings().expect("rt on");
874 assert_eq!(s.intensity, 1.0);
876 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
877 }
878
879 #[test]
880 fn rt_reflections_deserialise_from_jsonl_args() {
881 let cfg: PostProcessConfig =
882 serde_json::from_str(r#"{"ray_traced_reflections":true,"ssr_intensity":0.5}"#)
883 .expect("parse");
884 assert!(cfg.ray_traced_reflections);
885 assert!(cfg.rt_reflection_settings().is_some());
886 let cfg: PostProcessConfig =
889 serde_json::from_str(r#"{"ray_traced_reflections":false}"#).expect("parse");
890 assert!(!cfg.ray_traced_reflections);
891 assert!(cfg.rt_reflection_settings().is_none());
892 }
893
894 #[test]
895 fn ambient_intensity_defaults_neutral_and_clamps() {
896 assert_eq!(PostProcessConfig::default().ambient_intensity(), 1.0);
898 let hot = PostProcessConfig {
900 ambient_intensity: 100.0,
901 ..Default::default()
902 };
903 assert_eq!(hot.ambient_intensity(), 16.0);
904 let neg = PostProcessConfig {
905 ambient_intensity: -2.0,
906 ..Default::default()
907 };
908 assert_eq!(neg.ambient_intensity(), 0.0);
909 let cfg: PostProcessConfig =
911 serde_json::from_str(r#"{"ambient_intensity":3.5}"#).expect("parse");
912 assert_eq!(cfg.ambient_intensity(), 3.5);
913 }
914
915 #[test]
916 fn ssgi_defaults_on_with_neutral_tunables() {
917 let cfg = PostProcessConfig::default();
918 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ssgi);
919 assert_eq!(cfg.ssgi_intensity, 0.5);
920 assert_eq!(cfg.ssgi_max_distance, 8.0);
921 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
924 assert_eq!(cfg.ssgi_rays, 8);
925 assert_eq!(cfg.ssgi_steps, 12);
926 assert!(cfg.ssgi_settings().is_some());
927 let ibl = PostProcessConfig {
929 indirect_lighting: IndirectLighting::Ibl,
930 ..Default::default()
931 };
932 assert!(ibl.ssgi_settings().is_none());
933 }
934
935 #[test]
936 fn ssgi_resolution_maps_to_a_per_axis_divisor() {
937 assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
938 assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
939 assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
940 assert_eq!(SsgiResolution::default(), SsgiResolution::Half);
941 }
942
943 #[test]
944 fn ssgi_resolution_and_counts_flow_into_settings() {
945 let cfg = PostProcessConfig {
946 indirect_lighting: IndirectLighting::Ssgi,
947 ssgi_resolution: SsgiResolution::Quarter,
948 ssgi_rays: 4,
949 ssgi_steps: 20,
950 ..Default::default()
951 };
952 let s = cfg.ssgi_settings().expect("ssgi on");
953 assert_eq!(s.rays, 4);
954 assert_eq!(s.steps, 20);
955 assert_eq!(s.gi_scale, 4);
956 }
957
958 #[test]
959 fn ssgi_resolution_and_counts_deserialise_from_jsonl_args() {
960 let cfg: PostProcessConfig = serde_json::from_str(
961 r#"{"indirect_lighting":"ssgi","ssgi_resolution":"full","ssgi_rays":16,"ssgi_steps":8}"#,
962 )
963 .expect("parse");
964 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Full);
965 assert_eq!(cfg.ssgi_rays, 16);
966 assert_eq!(cfg.ssgi_steps, 8);
967 let cfg: PostProcessConfig =
969 serde_json::from_str(r#"{"indirect_lighting":"ssgi"}"#).expect("parse");
970 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
971 assert_eq!(cfg.ssgi_rays, 8);
972 assert_eq!(cfg.ssgi_steps, 12);
973 }
974
975 #[test]
976 fn reflection_blur_resolution_defaults_to_half() {
977 let cfg = PostProcessConfig::default();
978 assert_eq!(
979 cfg.reflection_blur_resolution,
980 ReflectionBlurResolution::Half
981 );
982 assert_eq!(cfg.reflection_blur_divisor(), 2);
983 }
984
985 #[test]
986 fn reflection_blur_resolution_maps_to_a_per_axis_divisor() {
987 assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
988 assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
989 assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
990 assert_eq!(
991 ReflectionBlurResolution::default(),
992 ReflectionBlurResolution::Half
993 );
994 }
995
996 #[test]
997 fn reflection_blur_resolution_deserialises_from_jsonl_args() {
998 let cfg: PostProcessConfig =
999 serde_json::from_str(r#"{"ssr":true,"reflection_blur_resolution":"quarter"}"#)
1000 .expect("parse");
1001 assert_eq!(
1002 cfg.reflection_blur_resolution,
1003 ReflectionBlurResolution::Quarter
1004 );
1005 assert_eq!(cfg.reflection_blur_divisor(), 4);
1006 let cfg: PostProcessConfig = serde_json::from_str(r#"{"ssr":true}"#).expect("parse");
1008 assert_eq!(
1009 cfg.reflection_blur_resolution,
1010 ReflectionBlurResolution::Half
1011 );
1012 assert_eq!(cfg.reflection_blur_divisor(), 2);
1013 }
1014
1015 #[test]
1016 fn ssgi_settings_resolve_and_clamp_when_enabled() {
1017 let cfg = PostProcessConfig {
1018 indirect_lighting: IndirectLighting::Ssgi,
1019 ssgi_intensity: 99.0,
1020 ssgi_max_distance: 1.0e6,
1021 ..Default::default()
1022 };
1023 let s = cfg.ssgi_settings().expect("ssgi on");
1024 assert_eq!(s.intensity, 4.0);
1025 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
1026 }
1027
1028 #[test]
1029 fn ssgi_deserialises_from_jsonl_args() {
1030 let cfg: PostProcessConfig =
1031 serde_json::from_str(r#"{"indirect_lighting":"ssgi","ssgi_intensity":0.8}"#)
1032 .expect("parse");
1033 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ssgi);
1034 assert_eq!(cfg.ssgi_intensity, 0.8);
1035 assert_eq!(cfg.ssgi_max_distance, 8.0);
1037 let cfg: PostProcessConfig =
1040 serde_json::from_str(r#"{"indirect_lighting":"ibl"}"#).expect("parse");
1041 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
1042 assert!(cfg.ssgi_settings().is_none());
1043 }
1044
1045 #[test]
1046 fn auto_exposure_defaults_off_with_neutral_tunables() {
1047 let cfg = PostProcessConfig::default();
1048 assert!(!cfg.auto_exposure);
1049 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
1050 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
1051 assert_eq!(cfg.auto_exposure_speed, 1.5);
1052 assert!(cfg.auto_exposure_settings().is_none());
1053 }
1054
1055 #[test]
1056 fn auto_exposure_settings_resolve_when_enabled() {
1057 let cfg = PostProcessConfig {
1058 auto_exposure: true,
1059 auto_exposure_min_ev: -4.0,
1060 auto_exposure_max_ev: 6.0,
1061 auto_exposure_speed: 2.0,
1062 ..Default::default()
1063 };
1064 let s = cfg.auto_exposure_settings().expect("auto-exposure on");
1065 assert_eq!(s.min_ev, -4.0);
1066 assert_eq!(s.max_ev, 6.0);
1067 assert_eq!(s.speed, 2.0);
1068 }
1069
1070 #[test]
1071 fn auto_exposure_deserialises_from_jsonl_args() {
1072 let cfg: PostProcessConfig =
1073 serde_json::from_str(r#"{"auto_exposure":true,"auto_exposure_speed":3.0}"#)
1074 .expect("parse");
1075 assert!(cfg.auto_exposure);
1076 assert_eq!(cfg.auto_exposure_speed, 3.0);
1077 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
1079 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
1080 }
1081
1082 #[test]
1083 fn aa_mode_deserialises_from_jsonl_args() {
1084 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"taa"}"#).expect("parse");
1085 assert_eq!(cfg.aa_mode, AaMode::Taa);
1086 let cfg: PostProcessConfig =
1088 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
1089 assert_eq!(cfg.aa_mode, AaMode::Taa);
1090 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"off"}"#).expect("parse");
1092 assert_eq!(cfg.aa_mode, AaMode::Off);
1093 }
1094
1095 #[test]
1096 fn hdr_display_defaults_off() {
1097 assert!(!PostProcessConfig::default().hdr_display);
1098 }
1099
1100 #[test]
1101 fn hdr_display_round_trips_through_args_and_jsonl() {
1102 let cfg = PostProcessConfig {
1103 hdr_display: true,
1104 ..Default::default()
1105 };
1106 assert!(cfg.clone().hdr_display);
1107
1108 let cfg: PostProcessConfig =
1109 serde_json::from_str(r#"{"hdr_display":true}"#).expect("parse");
1110 assert!(cfg.hdr_display);
1111 }
1112
1113 #[test]
1114 fn temporal_upscaling_defaults_off_with_quality_preset() {
1115 let cfg = PostProcessConfig::default();
1116 assert!(!cfg.temporal_upscaling);
1117 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
1118 }
1119
1120 #[test]
1121 fn upscale_quality_scales_are_monotonic() {
1122 let q = UpscaleQuality::Quality.scale();
1125 let b = UpscaleQuality::Balanced.scale();
1126 let p = UpscaleQuality::Performance.scale();
1127 let u = UpscaleQuality::UltraPerformance.scale();
1128 assert!(q > b && b > p && p > u);
1129 assert!(u > 0.0);
1130 }
1131
1132 #[test]
1133 fn occlusion_two_pass_defaults_on_and_round_trips() {
1134 assert!(PostProcessConfig::default().occlusion_two_pass);
1135 let cfg = PostProcessConfig {
1136 occlusion_two_pass: false,
1137 ..Default::default()
1138 };
1139 assert!(!cfg.clone().occlusion_two_pass);
1140 let cfg: PostProcessConfig =
1142 serde_json::from_str(r#"{"occlusion_two_pass":false}"#).expect("parse");
1143 assert!(!cfg.occlusion_two_pass);
1144 let cfg: PostProcessConfig =
1145 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
1146 assert!(cfg.occlusion_two_pass);
1147 }
1148
1149 #[test]
1150 fn upscale_backend_defaults_to_auto() {
1151 assert_eq!(
1152 PostProcessConfig::default().upscale_backend,
1153 UpscalerBackend::Auto
1154 );
1155 assert_eq!(UpscalerBackend::default(), UpscalerBackend::Auto);
1156 }
1157
1158 #[test]
1159 fn upscale_backend_round_trips_via_snake_case_json() {
1160 for (s, want) in [
1161 ("auto", UpscalerBackend::Auto),
1162 ("fsr3", UpscalerBackend::Fsr3),
1163 ("dlss", UpscalerBackend::Dlss),
1164 ("xess", UpscalerBackend::Xess),
1165 ] {
1166 let json = format!(r#"{{"temporal_upscaling":true,"upscale_backend":"{s}"}}"#);
1167 let cfg: PostProcessConfig = serde_json::from_str(&json).expect("parse");
1168 assert_eq!(cfg.upscale_backend, want, "for {s}");
1169 }
1170 let cfg: PostProcessConfig =
1172 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
1173 assert_eq!(cfg.upscale_backend, UpscalerBackend::Auto);
1174 }
1175
1176 #[test]
1177 fn upscale_backend_round_trips_through_args() {
1178 let cfg = PostProcessConfig {
1179 upscale_backend: UpscalerBackend::Xess,
1180 ..Default::default()
1181 };
1182 assert_eq!(cfg.clone().upscale_backend, UpscalerBackend::Xess);
1183 }
1184
1185 #[test]
1186 fn upscale_quality_round_trips_via_snake_case_json() {
1187 let cfg: PostProcessConfig =
1188 serde_json::from_str(r#"{"temporal_upscaling":true,"upscale_quality":"performance"}"#)
1189 .expect("parse");
1190 assert!(cfg.temporal_upscaling);
1191 assert_eq!(cfg.upscale_quality, UpscaleQuality::Performance);
1192 let cfg: PostProcessConfig =
1194 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
1195 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
1196 }
1197}