use crate::components::{IndirectLighting, PostProcessConfig};
use crate::ecs::Component;
use crate::gfx::render_types::PostProcessTunables;
use crate::math::exp2;
const EXPOSURE_EV_LIMIT: f32 = 16.0;
pub trait PostProcessResolve {
fn resolve(&self) -> PostProcessTunables;
fn ambient_intensity(&self) -> f32;
fn reflection_blur_divisor(&self) -> u32;
fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings>;
fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings>;
fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings>;
fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings>;
fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings>;
}
impl PostProcessResolve for PostProcessConfig {
fn resolve(&self) -> PostProcessTunables {
let ev = self
.exposure_ev
.clamp(-EXPOSURE_EV_LIMIT, EXPOSURE_EV_LIMIT);
PostProcessTunables {
bloom_intensity: self.bloom_intensity.max(0.0),
bloom_threshold: self.bloom_threshold.max(0.0),
bloom_knee: self.bloom_knee.max(0.0),
exposure: exp2(ev),
vignette: self.vignette_strength.clamp(0.0, 1.0),
lut_strength: self.lut_strength.clamp(0.0, 1.0),
fxaa: self.aa_mode.fxaa_flag(),
}
}
fn ambient_intensity(&self) -> f32 {
self.ambient_intensity.clamp(0.0, 16.0)
}
fn reflection_blur_divisor(&self) -> u32 {
self.reflection_blur_resolution.scale_divisor()
}
fn ssao_settings(&self) -> Option<crate::gfx::ssao::SsaoSettings> {
self.ssao
.then(|| crate::gfx::ssao::SsaoSettings::resolve(self.ssao_radius, self.ssao_intensity))
}
fn ssr_settings(&self) -> Option<crate::gfx::ssr::SsrSettings> {
self.ssr.then(|| {
crate::gfx::ssr::SsrSettings::resolve(self.ssr_intensity, self.ssr_max_distance)
})
}
fn rt_reflection_settings(&self) -> Option<crate::gfx::rt_reflections::RtReflectionSettings> {
self.ray_traced_reflections.then(|| {
crate::gfx::rt_reflections::RtReflectionSettings::resolve(
self.ssr_intensity,
self.ssr_max_distance,
)
})
}
fn ssgi_settings(&self) -> Option<crate::gfx::ssgi::SsgiSettings> {
(self.indirect_lighting == IndirectLighting::Ssgi).then(|| {
crate::gfx::ssgi::SsgiSettings::resolve(
self.ssgi_intensity,
self.ssgi_max_distance,
self.ssgi_rays,
self.ssgi_steps,
self.ssgi_resolution.scale_divisor(),
)
})
}
fn auto_exposure_settings(&self) -> Option<crate::gfx::auto_exposure::AutoExposureSettings> {
self.auto_exposure.then(|| {
crate::gfx::auto_exposure::AutoExposureSettings::resolve(
self.auto_exposure_min_ev,
self.auto_exposure_max_ev,
self.auto_exposure_speed,
self.hdr_display,
)
})
}
}
impl Component for PostProcessConfig {
const NAME: &'static str = "PostProcessConfig";
fn from_baked(bytes: &[u8]) -> Result<Self, crate::result::CnResult> {
Ok(crate::blob::decode_exact(bytes)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::{
AaMode, ReflectionBlurResolution, SsgiResolution, UpscaleQuality, UpscalerBackend,
};
use alloc::format;
#[test]
fn default_resolves_to_neutral_params() {
let p = PostProcessConfig::default().resolve();
assert_eq!(p.bloom_intensity, 0.6);
assert_eq!(p.bloom_threshold, 1.0);
assert_eq!(p.bloom_knee, 0.5);
assert_eq!(p.exposure, 1.0);
assert_eq!(p.vignette, 0.0);
assert_eq!(p.lut_strength, 1.0);
assert_eq!(p, PostProcessTunables::DEFAULT);
}
#[test]
fn exposure_ev_resolves_to_power_of_two_multiplier() {
let cfg = PostProcessConfig {
exposure_ev: 2.0,
..Default::default()
};
assert_eq!(cfg.resolve().exposure, 4.0);
let cfg = PostProcessConfig {
exposure_ev: -1.0,
..Default::default()
};
assert_eq!(cfg.resolve().exposure, 0.5);
}
#[test]
fn exposure_ev_is_clamped_to_a_finite_multiplier() {
let cfg = PostProcessConfig {
exposure_ev: 1.0e9,
..Default::default()
};
let exposure = cfg.resolve().exposure;
assert!(exposure.is_finite());
assert_eq!(exposure, EXPOSURE_EV_LIMIT.exp2());
}
#[test]
fn negative_and_overrange_inputs_are_clamped() {
let cfg = PostProcessConfig {
bloom_intensity: -3.0,
bloom_threshold: -1.0,
bloom_knee: -0.2,
vignette_strength: 5.0,
lut_strength: -2.0,
..Default::default()
};
let p = cfg.resolve();
assert_eq!(p.bloom_intensity, 0.0);
assert_eq!(p.bloom_threshold, 0.0);
assert_eq!(p.bloom_knee, 0.0);
assert_eq!(p.vignette, 1.0);
assert_eq!(p.lut_strength, 0.0);
}
#[test]
fn lut_strength_is_clamped_to_unit_range() {
let cfg = PostProcessConfig {
lut_strength: 3.0,
..Default::default()
};
assert_eq!(cfg.resolve().lut_strength, 1.0);
}
#[test]
fn aa_mode_defaults_to_fxaa_and_round_trips_through_args() {
assert_eq!(PostProcessConfig::default().aa_mode, AaMode::Fxaa);
let cfg = PostProcessConfig {
aa_mode: AaMode::Taa,
..Default::default()
};
assert_eq!(cfg.clone().aa_mode, AaMode::Taa);
}
#[test]
fn aa_mode_gates_taa_and_fxaa() {
assert!(!AaMode::Off.taa_enabled());
assert!(!AaMode::Fxaa.taa_enabled());
assert!(AaMode::Taa.taa_enabled());
let off = PostProcessConfig {
aa_mode: AaMode::Off,
..Default::default()
};
assert_eq!(off.resolve().fxaa, 0.0);
assert_eq!(PostProcessConfig::default().resolve().fxaa, 1.0);
}
#[test]
fn ssao_defaults_off_with_neutral_tunables() {
let cfg = PostProcessConfig::default();
assert!(!cfg.ssao);
assert_eq!(cfg.ssao_radius, 0.5);
assert_eq!(cfg.ssao_intensity, 1.0);
assert!(cfg.ssao_settings().is_none());
}
#[test]
fn ssao_settings_resolve_and_clamp_when_enabled() {
let cfg = PostProcessConfig {
ssao: true,
ssao_radius: -1.0,
ssao_intensity: 99.0,
..Default::default()
};
let s = cfg.ssao_settings().expect("ssao on");
assert!(s.radius > 0.0);
assert_eq!(s.intensity, 4.0);
}
#[test]
fn ssao_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"ssao":true,"ssao_radius":0.6}"#).expect("parse");
assert!(cfg.ssao);
assert_eq!(cfg.ssao_radius, 0.6);
assert_eq!(cfg.ssao_intensity, 1.0);
}
#[test]
fn ssr_defaults_off_with_neutral_tunables() {
let cfg = PostProcessConfig::default();
assert!(!cfg.ssr);
assert_eq!(cfg.ssr_intensity, 0.7);
assert_eq!(cfg.ssr_max_distance, 40.0);
assert!(cfg.ssr_settings().is_none());
}
#[test]
fn ssr_settings_resolve_and_clamp_when_enabled() {
let cfg = PostProcessConfig {
ssr: true,
ssr_intensity: 9.0,
ssr_max_distance: 1.0e6,
..Default::default()
};
let s = cfg.ssr_settings().expect("ssr on");
assert_eq!(s.intensity, 1.0);
assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
}
#[test]
fn ssr_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"ssr":true,"ssr_intensity":0.5}"#).expect("parse");
assert!(cfg.ssr);
assert_eq!(cfg.ssr_intensity, 0.5);
assert_eq!(cfg.ssr_max_distance, 40.0);
}
#[test]
fn rt_reflections_default_off_and_resolve_to_none() {
let cfg = PostProcessConfig::default();
assert!(!cfg.ray_traced_reflections);
assert!(cfg.rt_reflection_settings().is_none());
}
#[test]
fn rt_reflection_settings_reuse_ssr_tunables_when_enabled() {
let cfg = PostProcessConfig {
ray_traced_reflections: true,
ssr_intensity: 9.0,
ssr_max_distance: 1.0e6,
..Default::default()
};
let s = cfg.rt_reflection_settings().expect("rt on");
assert_eq!(s.intensity, 1.0);
assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
}
#[test]
fn rt_reflections_deserialise_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"ray_traced_reflections":true,"ssr_intensity":0.5}"#)
.expect("parse");
assert!(cfg.ray_traced_reflections);
assert!(cfg.rt_reflection_settings().is_some());
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
assert!(!cfg.ray_traced_reflections);
assert!(cfg.rt_reflection_settings().is_none());
}
#[test]
fn ambient_intensity_defaults_neutral_and_clamps() {
assert_eq!(PostProcessConfig::default().ambient_intensity(), 1.0);
let hot = PostProcessConfig {
ambient_intensity: 100.0,
..Default::default()
};
assert_eq!(hot.ambient_intensity(), 16.0);
let neg = PostProcessConfig {
ambient_intensity: -2.0,
..Default::default()
};
assert_eq!(neg.ambient_intensity(), 0.0);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"ambient_intensity":3.5}"#).expect("parse");
assert_eq!(cfg.ambient_intensity(), 3.5);
}
#[test]
fn ssgi_defaults_to_ibl_with_neutral_tunables() {
let cfg = PostProcessConfig::default();
assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
assert_eq!(cfg.ssgi_intensity, 0.5);
assert_eq!(cfg.ssgi_max_distance, 8.0);
assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
assert_eq!(cfg.ssgi_rays, 8);
assert_eq!(cfg.ssgi_steps, 12);
assert!(cfg.ssgi_settings().is_none());
}
#[test]
fn ssgi_resolution_maps_to_a_per_axis_divisor() {
assert_eq!(SsgiResolution::Full.scale_divisor(), 1);
assert_eq!(SsgiResolution::Half.scale_divisor(), 2);
assert_eq!(SsgiResolution::Quarter.scale_divisor(), 4);
assert_eq!(SsgiResolution::default(), SsgiResolution::Half);
}
#[test]
fn ssgi_resolution_and_counts_flow_into_settings() {
let cfg = PostProcessConfig {
indirect_lighting: IndirectLighting::Ssgi,
ssgi_resolution: SsgiResolution::Quarter,
ssgi_rays: 4,
ssgi_steps: 20,
..Default::default()
};
let s = cfg.ssgi_settings().expect("ssgi on");
assert_eq!(s.rays, 4);
assert_eq!(s.steps, 20);
assert_eq!(s.gi_scale, 4);
}
#[test]
fn ssgi_resolution_and_counts_deserialise_from_jsonl_args() {
let cfg: PostProcessConfig = serde_json::from_str(
r#"{"indirect_lighting":"ssgi","ssgi_resolution":"full","ssgi_rays":16,"ssgi_steps":8}"#,
)
.expect("parse");
assert_eq!(cfg.ssgi_resolution, SsgiResolution::Full);
assert_eq!(cfg.ssgi_rays, 16);
assert_eq!(cfg.ssgi_steps, 8);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"indirect_lighting":"ssgi"}"#).expect("parse");
assert_eq!(cfg.ssgi_resolution, SsgiResolution::Half);
assert_eq!(cfg.ssgi_rays, 8);
assert_eq!(cfg.ssgi_steps, 12);
}
#[test]
fn reflection_blur_resolution_defaults_to_half() {
let cfg = PostProcessConfig::default();
assert_eq!(
cfg.reflection_blur_resolution,
ReflectionBlurResolution::Half
);
assert_eq!(cfg.reflection_blur_divisor(), 2);
}
#[test]
fn reflection_blur_resolution_maps_to_a_per_axis_divisor() {
assert_eq!(ReflectionBlurResolution::Full.scale_divisor(), 1);
assert_eq!(ReflectionBlurResolution::Half.scale_divisor(), 2);
assert_eq!(ReflectionBlurResolution::Quarter.scale_divisor(), 4);
assert_eq!(
ReflectionBlurResolution::default(),
ReflectionBlurResolution::Half
);
}
#[test]
fn reflection_blur_resolution_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"ssr":true,"reflection_blur_resolution":"quarter"}"#)
.expect("parse");
assert_eq!(
cfg.reflection_blur_resolution,
ReflectionBlurResolution::Quarter
);
assert_eq!(cfg.reflection_blur_divisor(), 4);
let cfg: PostProcessConfig = serde_json::from_str(r#"{"ssr":true}"#).expect("parse");
assert_eq!(
cfg.reflection_blur_resolution,
ReflectionBlurResolution::Half
);
assert_eq!(cfg.reflection_blur_divisor(), 2);
}
#[test]
fn ssgi_settings_resolve_and_clamp_when_enabled() {
let cfg = PostProcessConfig {
indirect_lighting: IndirectLighting::Ssgi,
ssgi_intensity: 99.0,
ssgi_max_distance: 1.0e6,
..Default::default()
};
let s = cfg.ssgi_settings().expect("ssgi on");
assert_eq!(s.intensity, 4.0);
assert!(s.max_distance > 0.0 && s.max_distance.is_finite());
}
#[test]
fn ssgi_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"indirect_lighting":"ssgi","ssgi_intensity":0.8}"#)
.expect("parse");
assert_eq!(cfg.indirect_lighting, IndirectLighting::Ssgi);
assert_eq!(cfg.ssgi_intensity, 0.8);
assert_eq!(cfg.ssgi_max_distance, 8.0);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
assert_eq!(cfg.indirect_lighting, IndirectLighting::Ibl);
assert!(cfg.ssgi_settings().is_none());
}
#[test]
fn auto_exposure_defaults_off_with_neutral_tunables() {
let cfg = PostProcessConfig::default();
assert!(!cfg.auto_exposure);
assert_eq!(cfg.auto_exposure_min_ev, -8.0);
assert_eq!(cfg.auto_exposure_max_ev, 8.0);
assert_eq!(cfg.auto_exposure_speed, 1.5);
assert!(cfg.auto_exposure_settings().is_none());
}
#[test]
fn auto_exposure_settings_resolve_when_enabled() {
let cfg = PostProcessConfig {
auto_exposure: true,
auto_exposure_min_ev: -4.0,
auto_exposure_max_ev: 6.0,
auto_exposure_speed: 2.0,
..Default::default()
};
let s = cfg.auto_exposure_settings().expect("auto-exposure on");
assert_eq!(s.min_ev, -4.0);
assert_eq!(s.max_ev, 6.0);
assert_eq!(s.speed, 2.0);
}
#[test]
fn auto_exposure_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"auto_exposure":true,"auto_exposure_speed":3.0}"#)
.expect("parse");
assert!(cfg.auto_exposure);
assert_eq!(cfg.auto_exposure_speed, 3.0);
assert_eq!(cfg.auto_exposure_min_ev, -8.0);
assert_eq!(cfg.auto_exposure_max_ev, 8.0);
}
#[test]
fn aa_mode_deserialises_from_jsonl_args() {
let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"taa"}"#).expect("parse");
assert_eq!(cfg.aa_mode, AaMode::Taa);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
assert_eq!(cfg.aa_mode, AaMode::Fxaa);
let cfg: PostProcessConfig = serde_json::from_str(r#"{"aa_mode":"off"}"#).expect("parse");
assert_eq!(cfg.aa_mode, AaMode::Off);
}
#[test]
fn hdr_display_defaults_off() {
assert!(!PostProcessConfig::default().hdr_display);
}
#[test]
fn hdr_display_round_trips_through_args_and_jsonl() {
let cfg = PostProcessConfig {
hdr_display: true,
..Default::default()
};
assert!(cfg.clone().hdr_display);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"hdr_display":true}"#).expect("parse");
assert!(cfg.hdr_display);
}
#[test]
fn temporal_upscaling_defaults_off_with_quality_preset() {
let cfg = PostProcessConfig::default();
assert!(!cfg.temporal_upscaling);
assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
}
#[test]
fn upscale_quality_scales_are_monotonic() {
let q = UpscaleQuality::Quality.scale();
let b = UpscaleQuality::Balanced.scale();
let p = UpscaleQuality::Performance.scale();
let u = UpscaleQuality::UltraPerformance.scale();
assert!(q > b && b > p && p > u);
assert!(u > 0.0);
}
#[test]
fn occlusion_two_pass_defaults_off_and_round_trips() {
assert!(!PostProcessConfig::default().occlusion_two_pass);
let cfg = PostProcessConfig {
occlusion_two_pass: true,
..Default::default()
};
assert!(cfg.clone().occlusion_two_pass);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"occlusion_two_pass":true}"#).expect("parse");
assert!(cfg.occlusion_two_pass);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"bloom_intensity":0.5}"#).expect("parse");
assert!(!cfg.occlusion_two_pass);
}
#[test]
fn upscale_backend_defaults_to_auto() {
assert_eq!(
PostProcessConfig::default().upscale_backend,
UpscalerBackend::Auto
);
assert_eq!(UpscalerBackend::default(), UpscalerBackend::Auto);
}
#[test]
fn upscale_backend_round_trips_via_snake_case_json() {
for (s, want) in [
("auto", UpscalerBackend::Auto),
("fsr3", UpscalerBackend::Fsr3),
("dlss", UpscalerBackend::Dlss),
("xess", UpscalerBackend::Xess),
] {
let json = format!(r#"{{"temporal_upscaling":true,"upscale_backend":"{s}"}}"#);
let cfg: PostProcessConfig = serde_json::from_str(&json).expect("parse");
assert_eq!(cfg.upscale_backend, want, "for {s}");
}
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
assert_eq!(cfg.upscale_backend, UpscalerBackend::Auto);
}
#[test]
fn upscale_backend_round_trips_through_args() {
let cfg = PostProcessConfig {
upscale_backend: UpscalerBackend::Xess,
..Default::default()
};
assert_eq!(cfg.clone().upscale_backend, UpscalerBackend::Xess);
}
#[test]
fn upscale_quality_round_trips_via_snake_case_json() {
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"temporal_upscaling":true,"upscale_quality":"performance"}"#)
.expect("parse");
assert!(cfg.temporal_upscaling);
assert_eq!(cfg.upscale_quality, UpscaleQuality::Performance);
let cfg: PostProcessConfig =
serde_json::from_str(r#"{"temporal_upscaling":true}"#).expect("parse");
assert_eq!(cfg.upscale_quality, UpscaleQuality::Quality);
}
}