1use crate::components::{IndirectLighting, PostProcessConfig};
9use crate::ecs::Component;
10use crate::gfx::render_types::PostProcessTunables;
11use crate::math::exp2;
12
13const EXPOSURE_EV_LIMIT: f32 = 16.0;
16
17pub trait PostProcessResolve {
21 fn resolve(&self) -> PostProcessTunables;
27
28 fn ambient_intensity(&self) -> f32;
32
33 fn reflection_blur_divisor(&self) -> u32;
36
37 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings>;
40
41 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings>;
44
45 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings>;
50
51 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings>;
54
55 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings>;
58}
59
60impl PostProcessResolve for PostProcessConfig {
61 fn resolve(&self) -> PostProcessTunables {
62 let ev = self
63 .exposure_ev
64 .clamp(-EXPOSURE_EV_LIMIT, EXPOSURE_EV_LIMIT);
65 PostProcessTunables {
66 bloom_intensity: self.bloom_intensity.max(0.0),
67 bloom_threshold: self.bloom_threshold.max(0.0),
68 bloom_knee: self.bloom_knee.max(0.0),
69 exposure: exp2(ev),
70 vignette: self.vignette_strength.clamp(0.0, 1.0),
71 lut_strength: self.lut_strength.clamp(0.0, 1.0),
72 fxaa: self.aa_mode.fxaa_flag(),
73 }
74 }
75
76 fn ambient_intensity(&self) -> f32 {
77 self.ambient_intensity.clamp(0.0, 16.0)
78 }
79
80 fn reflection_blur_divisor(&self) -> u32 {
81 self.reflection_blur_resolution.scale_divisor()
82 }
83
84 fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings> {
85 self.ssao
86 .then(|| crate::gfx::ssao::SsaoSettings::resolve(self.ssao_radius, self.ssao_intensity))
87 }
88
89 fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings> {
90 self.ssr.then(|| {
91 crate::gfx::ssr::SsrSettings::resolve(self.ssr_intensity, self.ssr_max_distance)
92 })
93 }
94
95 fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings> {
96 self.ray_traced_reflections.then(|| {
97 crate::gfx::rt_reflections::RtReflectionSettings::resolve(
98 self.ssr_intensity,
99 self.ssr_max_distance,
100 )
101 })
102 }
103
104 fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings> {
105 (self.indirect_lighting == IndirectLighting::Ssgi).then(|| {
106 crate::gfx::ssgi::SsgiSettings::resolve(
107 self.ssgi_intensity,
108 self.ssgi_max_distance,
109 self.ssgi_rays,
110 self.ssgi_steps,
111 self.ssgi_resolution.scale_divisor(),
112 )
113 })
114 }
115
116 fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings> {
117 self.auto_exposure.then(|| {
118 crate::gfx::auto_exposure::AutoExposureSettings::resolve(
125 self.auto_exposure_min_ev,
126 self.auto_exposure_max_ev,
127 self.auto_exposure_speed,
128 self.hdr_display,
129 )
130 })
131 }
132}
133
134impl Component for PostProcessConfig {
135 const NAME: &'static str = "PostProcessConfig";
136
137 fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
138 Ok(crate::blob::decode_exact(bytes)?)
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::components::{
146 AaMode, ReflectionBlurResolution, SsgiResolution, UpscaleQuality, UpscalerBackend,
147 };
148 use alloc::format;
149
150 #[test]
151 fn default_resolves_to_neutral_params() {
152 let p = PostProcessConfig::default().resolve();
153 assert_eq!(p.bloom_intensity, 0.6);
154 assert_eq!(p.bloom_threshold, 1.0);
155 assert_eq!(p.bloom_knee, 0.5);
156 assert_eq!(p.exposure, 1.0);
158 assert_eq!(p.vignette, 0.0);
159 assert_eq!(p.lut_strength, 1.0);
161 assert_eq!(p, PostProcessTunables::DEFAULT);
163 }
164
165 #[test]
166 fn exposure_ev_resolves_to_power_of_two_multiplier() {
167 let cfg = PostProcessConfig {
168 exposure_ev: 2.0,
169 ..Default::default()
170 };
171 assert_eq!(cfg.resolve().exposure, 4.0);
172
173 let cfg = PostProcessConfig {
174 exposure_ev: -1.0,
175 ..Default::default()
176 };
177 assert_eq!(cfg.resolve().exposure, 0.5);
178 }
179
180 #[test]
181 fn exposure_ev_is_clamped_to_a_finite_multiplier() {
182 let cfg = PostProcessConfig {
183 exposure_ev: 1.0e9,
184 ..Default::default()
185 };
186 let exposure = cfg.resolve().exposure;
187 assert!(exposure.is_finite());
188 assert_eq!(exposure, EXPOSURE_EV_LIMIT.exp2());
189 }
190
191 #[test]
192 fn negative_and_overrange_inputs_are_clamped() {
193 let cfg = PostProcessConfig {
194 bloom_intensity: -3.0,
195 bloom_threshold: -1.0,
196 bloom_knee: -0.2,
197 vignette_strength: 5.0,
198 lut_strength: -2.0,
199 ..Default::default()
200 };
201 let p = cfg.resolve();
202 assert_eq!(p.bloom_intensity, 0.0);
203 assert_eq!(p.bloom_threshold, 0.0);
204 assert_eq!(p.bloom_knee, 0.0);
205 assert_eq!(p.vignette, 1.0);
206 assert_eq!(p.lut_strength, 0.0);
207 }
208
209 #[test]
210 fn lut_strength_is_clamped_to_unit_range() {
211 let cfg = PostProcessConfig {
212 lut_strength: 3.0,
213 ..Default::default()
214 };
215 assert_eq!(cfg.resolve().lut_strength, 1.0);
216 }
217
218 #[test]
219 fn aa_mode_defaults_to_fxaa_and_round_trips_through_args() {
220 assert_eq!(PostProcessConfig::default().aa_mode, AaMode::Fxaa);
221 let cfg = PostProcessConfig {
222 aa_mode: AaMode::Taa,
223 ..Default::default()
224 };
225 assert_eq!(cfg.clone().aa_mode, AaMode::Taa);
226 }
227
228 #[test]
229 fn aa_mode_gates_taa_and_fxaa() {
230 assert!(!AaMode::Off.taa_enabled());
231 assert!(!AaMode::Fxaa.taa_enabled());
232 assert!(AaMode::Taa.taa_enabled());
233 let off = PostProcessConfig {
235 aa_mode: AaMode::Off,
236 ..Default::default()
237 };
238 assert_eq!(off.resolve().fxaa, 0.0);
239 assert_eq!(PostProcessConfig::default().resolve().fxaa, 1.0);
240 }
241
242 #[test]
243 fn ssao_defaults_off_with_neutral_tunables() {
244 let cfg = PostProcessConfig::default();
245 assert!(!cfg.ssao);
246 assert_eq!(cfg.ssao_radius, 0.5);
247 assert_eq!(cfg.ssao_intensity, 1.0);
248 assert!(cfg.ssao_settings().is_none());
250 }
251
252 #[test]
253 fn ssao_settings_resolve_and_clamp_when_enabled() {
254 let cfg = PostProcessConfig {
255 ssao: true,
256 ssao_radius: -1.0,
257 ssao_intensity: 99.0,
258 ..Default::default()
259 };
260 let s = cfg.ssao_settings().expect("ssao on");
261 assert!(s.radius > 0.0);
262 assert_eq!(s.intensity, 4.0);
263 }
264
265 #[test]
266 fn ssao_deserialises_from_jsonl_args() {
267 let cfg: PostProcessConfig =
268 serde_json::from_str(r#"{"ssao":true,"ssao_radius":0.6}"#).expect("parse");
269 assert!(cfg.ssao);
270 assert_eq!(cfg.ssao_radius, 0.6);
271 assert_eq!(cfg.ssao_intensity, 1.0);
273 }
274
275 #[test]
276 fn ssr_defaults_off_with_neutral_tunables() {
277 let cfg = PostProcessConfig::default();
278 assert!(!cfg.ssr);
279 assert_eq!(cfg.ssr_intensity, 0.7);
280 assert_eq!(cfg.ssr_max_distance, 40.0);
281 assert!(cfg.ssr_settings().is_none());
283 }
284
285 #[test]
286 fn ssr_settings_resolve_and_clamp_when_enabled() {
287 let cfg = PostProcessConfig {
288 ssr: true,
289 ssr_intensity: 9.0,
290 ssr_max_distance: 1.0e6,
291 ..Default::default()
292 };
293 let s = cfg.ssr_settings().expect("ssr on");
294 assert_eq!(s.intensity, 1.0);
295 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
296 }
297
298 #[test]
299 fn ssr_deserialises_from_jsonl_args() {
300 let cfg: PostProcessConfig =
301 serde_json::from_str(r#"{"ssr":true,"ssr_intensity":0.5}"#).expect("parse");
302 assert!(cfg.ssr);
303 assert_eq!(cfg.ssr_intensity, 0.5);
304 assert_eq!(cfg.ssr_max_distance, 40.0);
306 }
307
308 #[test]
309 fn rt_reflections_default_off_and_resolve_to_none() {
310 let cfg = PostProcessConfig::default();
311 assert!(!cfg.ray_traced_reflections);
312 assert!(cfg.rt_reflection_settings().is_none());
314 }
315
316 #[test]
317 fn rt_reflection_settings_reuse_ssr_tunables_when_enabled() {
318 let cfg = PostProcessConfig {
319 ray_traced_reflections: true,
320 ssr_intensity: 9.0,
321 ssr_max_distance: 1.0e6,
322 ..Default::default()
323 };
324 let s = cfg.rt_reflection_settings().expect("rt on");
325 assert_eq!(s.intensity, 1.0);
327 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
328 }
329
330 #[test]
331 fn rt_reflections_deserialise_from_jsonl_args() {
332 let cfg: PostProcessConfig =
333 serde_json::from_str(r#"{"ray_traced_reflections":true,"ssr_intensity":0.5}"#)
334 .expect("parse");
335 assert!(cfg.ray_traced_reflections);
336 assert!(cfg.rt_reflection_settings().is_some());
337 let cfg: PostProcessConfig =
339 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
340 assert!(!cfg.ray_traced_reflections);
341 assert!(cfg.rt_reflection_settings().is_none());
342 }
343
344 #[test]
345 fn ambient_intensity_defaults_neutral_and_clamps() {
346 assert_eq!(PostProcessConfig::default().ambient_intensity(), 1.0);
348 let hot = PostProcessConfig {
350 ambient_intensity: 100.0,
351 ..Default::default()
352 };
353 assert_eq!(hot.ambient_intensity(), 16.0);
354 let neg = PostProcessConfig {
355 ambient_intensity: -2.0,
356 ..Default::default()
357 };
358 assert_eq!(neg.ambient_intensity(), 0.0);
359 let cfg: PostProcessConfig =
361 serde_json::from_str(r#"{"ambient_intensity":3.5}"#).expect("parse");
362 assert_eq!(cfg.ambient_intensity(), 3.5);
363 }
364
365 #[test]
366 fn ssgi_defaults_to_ibl_with_neutral_tunables() {
367 let cfg = PostProcessConfig::default();
368 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
369 assert_eq!(cfg.ssgi_intensity, 0.5);
370 assert_eq!(cfg.ssgi_max_distance, 8.0);
371 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
374 assert_eq!(cfg.ssgi_rays, 8);
375 assert_eq!(cfg.ssgi_steps, 12);
376 assert!(cfg.ssgi_settings().is_none());
378 }
379
380 #[test]
381 fn ssgi_resolution_maps_to_a_per_axis_divisor() {
382 assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
383 assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
384 assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
385 assert_eq!(SsgiResolution::default(), SsgiResolution::Half);
386 }
387
388 #[test]
389 fn ssgi_resolution_and_counts_flow_into_settings() {
390 let cfg = PostProcessConfig {
391 indirect_lighting: IndirectLighting::Ssgi,
392 ssgi_resolution: SsgiResolution::Quarter,
393 ssgi_rays: 4,
394 ssgi_steps: 20,
395 ..Default::default()
396 };
397 let s = cfg.ssgi_settings().expect("ssgi on");
398 assert_eq!(s.rays, 4);
399 assert_eq!(s.steps, 20);
400 assert_eq!(s.gi_scale, 4);
401 }
402
403 #[test]
404 fn ssgi_resolution_and_counts_deserialise_from_jsonl_args() {
405 let cfg: PostProcessConfig = serde_json::from_str(
406 r#"{"indirect_lighting":"ssgi","ssgi_resolution":"full","ssgi_rays":16,"ssgi_steps":8}"#,
407 )
408 .expect("parse");
409 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Full);
410 assert_eq!(cfg.ssgi_rays, 16);
411 assert_eq!(cfg.ssgi_steps, 8);
412 let cfg: PostProcessConfig =
414 serde_json::from_str(r#"{"indirect_lighting":"ssgi"}"#).expect("parse");
415 assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
416 assert_eq!(cfg.ssgi_rays, 8);
417 assert_eq!(cfg.ssgi_steps, 12);
418 }
419
420 #[test]
421 fn reflection_blur_resolution_defaults_to_half() {
422 let cfg = PostProcessConfig::default();
423 assert_eq!(
424 cfg.reflection_blur_resolution,
425 ReflectionBlurResolution::Half
426 );
427 assert_eq!(cfg.reflection_blur_divisor(), 2);
428 }
429
430 #[test]
431 fn reflection_blur_resolution_maps_to_a_per_axis_divisor() {
432 assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
433 assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
434 assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
435 assert_eq!(
436 ReflectionBlurResolution::default(),
437 ReflectionBlurResolution::Half
438 );
439 }
440
441 #[test]
442 fn reflection_blur_resolution_deserialises_from_jsonl_args() {
443 let cfg: PostProcessConfig =
444 serde_json::from_str(r#"{"ssr":true,"reflection_blur_resolution":"quarter"}"#)
445 .expect("parse");
446 assert_eq!(
447 cfg.reflection_blur_resolution,
448 ReflectionBlurResolution::Quarter
449 );
450 assert_eq!(cfg.reflection_blur_divisor(), 4);
451 let cfg: PostProcessConfig = serde_json::from_str(r#"{"ssr":true}"#).expect("parse");
453 assert_eq!(
454 cfg.reflection_blur_resolution,
455 ReflectionBlurResolution::Half
456 );
457 assert_eq!(cfg.reflection_blur_divisor(), 2);
458 }
459
460 #[test]
461 fn ssgi_settings_resolve_and_clamp_when_enabled() {
462 let cfg = PostProcessConfig {
463 indirect_lighting: IndirectLighting::Ssgi,
464 ssgi_intensity: 99.0,
465 ssgi_max_distance: 1.0e6,
466 ..Default::default()
467 };
468 let s = cfg.ssgi_settings().expect("ssgi on");
469 assert_eq!(s.intensity, 4.0);
470 assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
471 }
472
473 #[test]
474 fn ssgi_deserialises_from_jsonl_args() {
475 let cfg: PostProcessConfig =
476 serde_json::from_str(r#"{"indirect_lighting":"ssgi","ssgi_intensity":0.8}"#)
477 .expect("parse");
478 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ssgi);
479 assert_eq!(cfg.ssgi_intensity, 0.8);
480 assert_eq!(cfg.ssgi_max_distance, 8.0);
482 let cfg: PostProcessConfig =
484 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
485 assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
486 assert!(cfg.ssgi_settings().is_none());
487 }
488
489 #[test]
490 fn auto_exposure_defaults_off_with_neutral_tunables() {
491 let cfg = PostProcessConfig::default();
492 assert!(!cfg.auto_exposure);
493 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
494 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
495 assert_eq!(cfg.auto_exposure_speed, 1.5);
496 assert!(cfg.auto_exposure_settings().is_none());
497 }
498
499 #[test]
500 fn auto_exposure_settings_resolve_when_enabled() {
501 let cfg = PostProcessConfig {
502 auto_exposure: true,
503 auto_exposure_min_ev: -4.0,
504 auto_exposure_max_ev: 6.0,
505 auto_exposure_speed: 2.0,
506 ..Default::default()
507 };
508 let s = cfg.auto_exposure_settings().expect("auto-exposure on");
509 assert_eq!(s.min_ev, -4.0);
510 assert_eq!(s.max_ev, 6.0);
511 assert_eq!(s.speed, 2.0);
512 }
513
514 #[test]
515 fn auto_exposure_deserialises_from_jsonl_args() {
516 let cfg: PostProcessConfig =
517 serde_json::from_str(r#"{"auto_exposure":true,"auto_exposure_speed":3.0}"#)
518 .expect("parse");
519 assert!(cfg.auto_exposure);
520 assert_eq!(cfg.auto_exposure_speed, 3.0);
521 assert_eq!(cfg.auto_exposure_min_ev, -8.0);
523 assert_eq!(cfg.auto_exposure_max_ev, 8.0);
524 }
525
526 #[test]
527 fn aa_mode_deserialises_from_jsonl_args() {
528 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"taa"}"#).expect("parse");
529 assert_eq!(cfg.aa_mode, AaMode::Taa);
530 let cfg: PostProcessConfig =
532 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
533 assert_eq!(cfg.aa_mode, AaMode::Fxaa);
534 let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"off"}"#).expect("parse");
536 assert_eq!(cfg.aa_mode, AaMode::Off);
537 }
538
539 #[test]
540 fn hdr_display_defaults_off() {
541 assert!(!PostProcessConfig::default().hdr_display);
542 }
543
544 #[test]
545 fn hdr_display_round_trips_through_args_and_jsonl() {
546 let cfg = PostProcessConfig {
547 hdr_display: true,
548 ..Default::default()
549 };
550 assert!(cfg.clone().hdr_display);
551
552 let cfg: PostProcessConfig =
553 serde_json::from_str(r#"{"hdr_display":true}"#).expect("parse");
554 assert!(cfg.hdr_display);
555 }
556
557 #[test]
558 fn temporal_upscaling_defaults_off_with_quality_preset() {
559 let cfg = PostProcessConfig::default();
560 assert!(!cfg.temporal_upscaling);
561 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
562 }
563
564 #[test]
565 fn upscale_quality_scales_are_monotonic() {
566 let q = UpscaleQuality::Quality.scale();
569 let b = UpscaleQuality::Balanced.scale();
570 let p = UpscaleQuality::Performance.scale();
571 let u = UpscaleQuality::UltraPerformance.scale();
572 assert!(q > b && b > p && p > u);
573 assert!(u > 0.0);
574 }
575
576 #[test]
577 fn occlusion_two_pass_defaults_off_and_round_trips() {
578 assert!(!PostProcessConfig::default().occlusion_two_pass);
579 let cfg = PostProcessConfig {
580 occlusion_two_pass: true,
581 ..Default::default()
582 };
583 assert!(cfg.clone().occlusion_two_pass);
584 let cfg: PostProcessConfig =
586 serde_json::from_str(r#"{"occlusion_two_pass":true}"#).expect("parse");
587 assert!(cfg.occlusion_two_pass);
588 let cfg: PostProcessConfig =
589 serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
590 assert!(!cfg.occlusion_two_pass);
591 }
592
593 #[test]
594 fn upscale_backend_defaults_to_auto() {
595 assert_eq!(
596 PostProcessConfig::default().upscale_backend,
597 UpscalerBackend::Auto
598 );
599 assert_eq!(UpscalerBackend::default(), UpscalerBackend::Auto);
600 }
601
602 #[test]
603 fn upscale_backend_round_trips_via_snake_case_json() {
604 for (s, want) in [
605 ("auto", UpscalerBackend::Auto),
606 ("fsr3", UpscalerBackend::Fsr3),
607 ("dlss", UpscalerBackend::Dlss),
608 ("xess", UpscalerBackend::Xess),
609 ] {
610 let json = format!(r#"{{"temporal_upscaling":true,"upscale_backend":"{s}"}}"#);
611 let cfg: PostProcessConfig = serde_json::from_str(&json).expect("parse");
612 assert_eq!(cfg.upscale_backend, want, "for {s}");
613 }
614 let cfg: PostProcessConfig =
616 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
617 assert_eq!(cfg.upscale_backend, UpscalerBackend::Auto);
618 }
619
620 #[test]
621 fn upscale_backend_round_trips_through_args() {
622 let cfg = PostProcessConfig {
623 upscale_backend: UpscalerBackend::Xess,
624 ..Default::default()
625 };
626 assert_eq!(cfg.clone().upscale_backend, UpscalerBackend::Xess);
627 }
628
629 #[test]
630 fn upscale_quality_round_trips_via_snake_case_json() {
631 let cfg: PostProcessConfig =
632 serde_json::from_str(r#"{"temporal_upscaling":true,"upscale_quality":"performance"}"#)
633 .expect("parse");
634 assert!(cfg.temporal_upscaling);
635 assert_eq!(cfg.upscale_quality, UpscaleQuality::Performance);
636 let cfg: PostProcessConfig =
638 serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
639 assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
640 }
641}