From 7808f34cb2c8bd8b0a7a630d8eee377cbc093e53 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Fri, 25 Sep 2026 17:02:15 +0100 Subject: [PATCH] MONKEY (fix-daylight): gate the district window split; per-slot terrain torch keys Review B4: the Stormwind window split changed the default image with no switch. New cvar daylightWindowSplit (default 1 = merged behaviour, a future High-only preset member), bridged to daylight::set_window_split. Review B5: each settled exterior torch slot is keyed by the resident tiles whose footprint reaches its sphere (terrain_torch_generation_near), not a global stamp that re-rendered every slot on any stream event. Co-Authored-By: Claude Opus 5.5 --- crates/benilla-app/src/cvars.rs | 6 ++++ crates/benilla-app/src/dynamic_interior.rs | 2 ++ crates/benilla-app/src/torch_shadow.rs | 9 +++++- crates/benilla-app/src/video.rs | 7 ++++ crates/benilla-world/src/lighting/daylight.rs | 12 +++++++ crates/benilla-world/src/lighting/mod.rs | 1 + crates/benilla-world/src/terrain_stream.rs | 4 ++- .../src/terrain_stream/torch_terrain.rs | 32 +++++++++++++++++++ 8 files changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/benilla-app/src/cvars.rs b/crates/benilla-app/src/cvars.rs index c32d78c5..51fde75e 100644 --- a/crates/benilla-app/src/cvars.rs +++ b/crates/benilla-app/src/cvars.rs @@ -1188,6 +1188,12 @@ pub(crate) const REGISTERED: &[Registered] = &[ "0.0", "benilla's own: daylight floor for rooms inside a building, 0..1 (0 = none, the old look)", ), + // MONKEY (fix-daylight): the district window split, gateable (default on = merged behaviour). + ours( + "daylightWindowSplit", + "1", + "benilla's own: split city window batches into window-sized daylight apertures (applies to newly loaded buildings)", + ), // MONKEY (bake floor): the share of an interior batch's own MOCV bake that survives the live- // fixture lane. The lane throws the bake away and lets the fixtures decide, which leaves a room // no fixture reaches (the Lion's Pride Inn's east vestibule: MOLR 0, no claims, one faded diff --git a/crates/benilla-app/src/dynamic_interior.rs b/crates/benilla-app/src/dynamic_interior.rs index edb6aa9e..31baea90 100644 --- a/crates/benilla-app/src/dynamic_interior.rs +++ b/crates/benilla-app/src/dynamic_interior.rs @@ -83,6 +83,8 @@ fn bridge( if moon.0 != video.moon_shadow_strength { moon.0 = video.moon_shadow_strength; } + // MONKEY (fix-daylight): `daylightWindowSplit` (an atomic store; seeds read it at load). + benilla_world::lighting::set_window_split(video.daylight_window_split); let want = DynamicInteriors { enabled: video.interior_light, ambient: video.interior_ambient, diff --git a/crates/benilla-app/src/torch_shadow.rs b/crates/benilla-app/src/torch_shadow.rs index 621cc18a..3e2c7475 100644 --- a/crates/benilla-app/src/torch_shadow.rs +++ b/crates/benilla-app/src/torch_shadow.rs @@ -1244,7 +1244,14 @@ fn update_torch_shadows( // fixture's room already occludes the ground, and a moving slot regathers every frame. let with_terrain = terrain_gen != 0 && slot.exterior && !moving; if with_terrain { - terrain_gen.hash(&mut hash); + // MONKEY (fix-daylight): only the tiles near THIS slot key it; the global stamp + // re-rendered every exterior slot on any stream event anywhere. + if let (Some(t), Some(a)) = (&ents.terrain, &ents.adt_tiles) { + (benilla_world::terrain_stream::terrain_torch_generation_near( + t, a, slot.pos, TORCH_RANGE, + ) ^ 0x7e44_a1d0) + .hash(&mut hash); + } } let key = hash.finish(); let dirty = slot.geometry_key != Some(key); diff --git a/crates/benilla-app/src/video.rs b/crates/benilla-app/src/video.rs index 709c56d0..dce6c5a2 100644 --- a/crates/benilla-app/src/video.rs +++ b/crates/benilla-app/src/video.rs @@ -394,6 +394,10 @@ pub(crate) struct VideoConfig { /// record table flags as enclosed. `0` restores the pre-feature look exactly; the night look is /// unaffected at any value (the term is scaled by the sun's own day envelope). pub(crate) interior_daylight: f32, + /// MONKEY (fix-daylight): split a district's oversized window batch into window-sized + /// daylight apertures (`daylightWindowSplit`, default on = the merged behaviour; a future + /// High-only preset member). Applies to WMOs loaded after a change. + pub(crate) daylight_window_split: bool, /// MONKEY (bake floor): the share of a WMO interior batch's OWN MOCV bake every interior-lane /// fragment keeps whether or not a fixture reaches it (`interiorBakeFloor`, 0..1, default /// **0.12**). Bridged to [`benilla_world::lighting::DynamicInteriors::bake_floor`], packed @@ -472,6 +476,7 @@ impl Default for VideoConfig { // MONKEY (enclosed day floor): calibrated so the Goldshire inn's entry floor reads // ~50 % of the sunlit threshold beside it — see `lighting::DAYLIGHT_LANE_SCALE`. interior_daylight: 0.0, + daylight_window_split: true, // MONKEY (bake floor): an eighth of the authored bake — measured to lift the inn's // black door band from 0.019 to 0.108 x tex while moving candle-lit surfaces by // under 10 % (see `lighting::DynamicInteriors::bake_floor`). @@ -659,6 +664,8 @@ pub(crate) fn on_cvar( // MONKEY (enclosed day floor): 0 IS meaningful here (it restores the pre-feature look // exactly), unlike the two dim dials above whose 0 would be a broken-looking world. "interiordaylight" => cfg.interior_daylight = v.clamp(0.0, 1.0), + // MONKEY (fix-daylight) + "daylightwindowsplit" => cfg.daylight_window_split = ev.flag(), // MONKEY (bake floor): 0 IS meaningful here too (it restores the pre-feature look exactly). // The upper clamp matters more than usual: the packer multiplies this by `interiorGain` // (up to 1.5) and rides the product in a lane fraction that must stay under 0.5 after diff --git a/crates/benilla-world/src/lighting/daylight.rs b/crates/benilla-world/src/lighting/daylight.rs index 2fed0fbf..7738445c 100644 --- a/crates/benilla-world/src/lighting/daylight.rs +++ b/crates/benilla-world/src/lighting/daylight.rs @@ -802,6 +802,15 @@ where /// authors ALL its stained-glass windows as ONE EXT batch per wall — `g135 b11` spans /// 15 x 44 x 20 yd (diag 51), `g146 b10` 30 x 44 x 10 yd (diag 54) — so [`APERTURE_MAX_DIAG`] /// refused the batch whole and the nave took no daylight through a single window. Its windows are +/// MONKEY (fix-daylight): the district window split's gate (default on: the merged behaviour). +static WINDOW_SPLIT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); + +/// MONKEY (fix-daylight): `daylightWindowSplit`. Read when a placement's seeds are built, so a +/// change applies to WMOs loaded afterwards. +pub fn set_window_split(on: bool) { + WINDOW_SPLIT.store(on, std::sync::atomic::Ordering::Relaxed); +} + /// separate panes between pillars, so the batch's vertices fall into separate clusters at the /// plan linkage of [`plan_clusters`], and each window-sized cluster is an aperture. /// @@ -812,6 +821,9 @@ fn window_boxes(positions: &[[f32; 3]], district: bool) -> Vec<([f32; 3], [f32; let Some((lo, hi)) = bounds(positions.iter().copied()) else { return Vec::new(); }; + // MONKEY (fix-daylight): the split changes the default image (Stormwind 25 -> 33 apertures), + // so it has its own gate, the `daylightWindowSplit` cvar ([`set_window_split`]). + let district = district && WINDOW_SPLIT.load(std::sync::atomic::Ordering::Relaxed); if !district || diagonal(lo, hi) <= APERTURE_MAX_DIAG { return vec![(lo, hi)]; } diff --git a/crates/benilla-world/src/lighting/mod.rs b/crates/benilla-world/src/lighting/mod.rs index a4d9b863..74d72add 100644 --- a/crates/benilla-world/src/lighting/mod.rs +++ b/crates/benilla-world/src/lighting/mod.rs @@ -27,6 +27,7 @@ pub use blob::LightBlob; // calls now -- daylight seeds and interior<->interior doorway seeds share the per-placement budget, // so neither can be ranked without the other. `BleedFixture`/`BleedSeed` are the doorway lane's own // two types; the fixture itself still wears `DaylightFixture`. +pub use daylight::set_window_split; // MONKEY (fix-daylight) pub use daylight::{ daylight_claims, daylight_intensity, daylight_lane, daylight_point_light, daylight_reach, daylight_rooms, daylight_seeds, daylight_target, bleed_seeds, placement_openings, BleedFixture, diff --git a/crates/benilla-world/src/terrain_stream.rs b/crates/benilla-world/src/terrain_stream.rs index c6d3e353..1103086c 100644 --- a/crates/benilla-world/src/terrain_stream.rs +++ b/crates/benilla-world/src/terrain_stream.rs @@ -36,7 +36,9 @@ mod weld; pub(crate) mod window; // MONKEY (daylight: terrain torch casters): resident MCNK chunks as torch cube-map casters. mod torch_terrain; -pub use torch_terrain::{append_terrain_torch_triangles, terrain_torch_generation}; +pub use torch_terrain::{ + append_terrain_torch_triangles, terrain_torch_generation, terrain_torch_generation_near, +}; use collider::{finish_colliders, impassable_wall_data, terrain_collider_data}; use furnish::furnish_tile_cells; diff --git a/crates/benilla-world/src/terrain_stream/torch_terrain.rs b/crates/benilla-world/src/terrain_stream/torch_terrain.rs index b58e2aa4..ed6231ba 100644 --- a/crates/benilla-world/src/terrain_stream/torch_terrain.rs +++ b/crates/benilla-world/src/terrain_stream/torch_terrain.rs @@ -102,6 +102,38 @@ pub fn terrain_torch_generation(streamer: &TerrainStreamer, adt_tiles: &Assets, + center: Vec3, + reach: f32, +) -> u64 { + use std::hash::{Hash, Hasher}; + let c = Vec3::from(benilla_assets::coords::bevy_to_wow(center)); + let t = benilla_formats::TILE_SIZE; + let offset = 32.0 * t; + let mut sum = 0u64; + for (key, ts) in &streamer.tiles { + let (tx, ty) = (key.0 as f32, key.1 as f32); + let (x_hi, y_hi) = (offset - ty * t, offset - tx * t); + let lo = Vec3::new(x_hi - t, y_hi - t, c.z); + let hi = Vec3::new(x_hi, y_hi, c.z); + if box_sphere_d2(lo, hi, c) > reach * reach { + continue; + } + let mut h = std::collections::hash_map::DefaultHasher::new(); + key.hash(&mut h); + ts.handle.id().hash(&mut h); + adt_tiles.contains(&ts.handle).hash(&mut h); + sum = sum.wrapping_add(h.finish()); + } + sum +} + #[cfg(test)] mod tests { use super::*;