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::*;