diff --git a/crates/benilla-app/src/volumetric_fog.rs b/crates/benilla-app/src/volumetric_fog.rs index b5753771..2d608273 100644 --- a/crates/benilla-app/src/volumetric_fog.rs +++ b/crates/benilla-app/src/volumetric_fog.rs @@ -5,11 +5,9 @@ use crate::{ shadow_core::{ShadowSet, ShadowSun}, video::VideoConfig, - world_shadow::WorldLane, }; use benilla_world::{ lighting::{ResolvedPointLight, ResolvedPointLights, WorldTime, WowLighting}, - static_gx::StaticGx, view::WorldCamera, weather::WeatherState, wmo_portal::CameraInteriorClaim, @@ -119,7 +117,8 @@ impl Plugin for VolumetricFogPlugin { shaft_gain, }) .add_plugins(ExtractComponentPlugin::::default()) - .add_systems(Last, refresh_streamed_shadows.before(ShadowSet::Lanes)) + // MONKEY (followups): the streamed-caster refresh moved to `world_shadow.rs`; it no longer + // depends on this cvar. .add_systems(Last, update_fog.after(ShadowSet::Lanes)); // Keep headless policy tests independent of the renderer. if !app.is_plugin_added::() { @@ -149,27 +148,6 @@ impl Plugin for VolumetricFogPlugin { } } -// The legacy caster cache otherwise misses streamed trees at a stationary camera. -// Off must leave even this old cache behaviour untouched for the exact baseline. -fn refresh_streamed_shadows( - video: Res, - override_value: Res, - gx: Option>, - mut lane: ResMut, - mut seen: Local>, -) { - if override_value.fog.unwrap_or(video.volumetric_fog) == 0 || !video.world_shadows { - *seen = None; - return; - } - let Some(gx) = gx else { return }; - let generation = gx.torch_residency_generation(); - if *seen != Some(generation) { - lane.invalidate_static(); - *seen = Some(generation); - } -} - fn density(minute: f32, weather: f32, indoors: bool) -> f32 { let dawn = (1.0 - ((minute - 390.0) / 90.0).abs()).clamp(0.0, 1.0); let dawn = dawn * dawn * (3.0 - 2.0 * dawn); @@ -704,28 +682,6 @@ fn fragment(@builtin(position) pixel: vec4) -> @location(0) vec4 { mod tests { use super::*; #[test] - fn shadow_cache_refresh_is_gated_and_quiet_when_unchanged() { - let mut app = App::new(); - app.init_resource::() - .init_resource::() - .init_resource::() - .insert_resource(FogOverride { - fog: Some(0), - lamp: None, - shaft_gain: 2.0, - }) - .add_systems(Update, refresh_streamed_shadows); - app.world_mut().clear_trackers(); - app.world_mut().run_schedule(Update); - assert!(!app.world().resource_ref::().is_changed()); - app.world_mut().resource_mut::().fog = Some(1); - app.world_mut().run_schedule(Update); - assert!(app.world().resource_ref::().is_changed()); - app.world_mut().clear_trackers(); - app.world_mut().run_schedule(Update); - assert!(!app.world().resource_ref::().is_changed()); - } - #[test] fn dawn_weather_and_rooms_control_density() { let noon = density(720.0, 0.0, false); assert!(density(390.0, 0.0, false) > noon * 2.0); diff --git a/crates/benilla-app/src/world_shadow.rs b/crates/benilla-app/src/world_shadow.rs index 8e787dce..e04aa5a4 100644 --- a/crates/benilla-app/src/world_shadow.rs +++ b/crates/benilla-app/src/world_shadow.rs @@ -49,10 +49,34 @@ impl Plugin for WorldShadowPlugin { fn build(&self, app: &mut App) { app.add_plugins(MaterialPlugin::::default()) .init_resource::() + // MONKEY (followups): refresh the static caster cache when streamed geometry arrives. + // This used to live in the volumetric-fog plugin and so only ran with that cvar on. + .add_systems(Last, refresh_streamed_shadows.before(ShadowSet::Lanes)) .add_systems(Last, update_world_shadows.in_set(ShadowSet::Lanes)); } } +/// MONKEY (followups): the static caster rebuilds on 16 yd of camera drift, so a standing camera +/// never picks up trees and doodads that stream in later. Invalidate it whenever the retained +/// scene's residency changes, while world shadows are on (Classic has them off, so it is inert). +fn refresh_streamed_shadows( + video: Res, + gx: Option>, + mut lane: ResMut, + mut seen: Local>, +) { + if !video.world_shadows { + *seen = None; + return; + } + let Some(gx) = gx else { return }; + let generation = gx.torch_residency_generation(); + if *seen != Some(generation) { + lane.invalidate_static(); + *seen = Some(generation); + } +} + /// The alpha-tested twin of [`ShadowCasterMaterial`]: same invisible-in-forward proxy, but its /// prepass fragment samples a leaf sheet and discards transparent texels so cutout FOLIAGE casts a /// leaf-shaped silhouette instead of the solid box a positions-only proxy throws. One instance per @@ -106,7 +130,7 @@ struct CutoutCaster { /// The world lane's retained casters. #[derive(Resource, Default)] -// MONKEY (volumetric fog): allow the fog plugin to invalidate a stale streamed caster cache. +// MONKEY (volumetric fog, followups): the streamed-residency refresh invalidates the cache. pub(crate) struct WorldLane { /// The cached static caster — the retained `static_gx` world (trees + buildings), rebuilt on drift. static_caster: Option, @@ -123,8 +147,7 @@ pub(crate) struct WorldLane { env_rate: RebuildRate, } -// MONKEY (volumetric fog): retain ownership of the cache here; the fog plugin decides -// when streamed geometry needs a fresh map, without changing the legacy Off path. +// MONKEY (followups): `refresh_streamed_shadows` decides when streamed geometry needs a fresh map. impl WorldLane { pub(crate) fn invalidate_static(&mut self) { self.static_rebuilt_at = None; @@ -359,3 +382,30 @@ fn teardown( // The mesh the gate was pacing is gone — re-arm so a re-enable builds on its first frame. lane.env_rate.reset(); } + +// MONKEY (followups): the refresh must not depend on volumetric fog. +#[cfg(test)] +mod streamed_refresh_tests { + use super::*; + + #[test] + fn shadow_cache_refresh_runs_without_volumetric_fog_and_is_quiet_when_unchanged() { + let mut app = App::new(); + let mut video = VideoConfig::default(); + video.volumetric_fog = 0; + video.world_shadows = false; + app.insert_resource(video) + .init_resource::() + .init_resource::() + .add_systems(Update, refresh_streamed_shadows); + app.world_mut().clear_trackers(); + app.world_mut().run_schedule(Update); + assert!(!app.world().resource_ref::().is_changed()); + app.world_mut().resource_mut::().world_shadows = true; + app.world_mut().run_schedule(Update); + assert!(app.world().resource_ref::().is_changed()); + app.world_mut().clear_trackers(); + app.world_mut().run_schedule(Update); + assert!(!app.world().resource_ref::().is_changed()); + } +}