From 6d2804d28bcf02005ede8bfff45905e1fe07b0c2 Mon Sep 17 00:00:00 2001 From: pkuzic Date: Sun, 4 Oct 2026 01:16:06 +0100 Subject: [PATCH] MONKEY (gfx): keep slow carried lights on the live shadow path Match motion detection to the exact-position geometry key so small hand sway uses the existing stable-mesh moving path. Cover 30, 60, 144 and 240 fps plus settling. Verified Windows play build, 4314 app tests passing, and pixel-identical inn capture. Co-Authored-By: GPT-6 Astra (Codex) --- crates/benilla-app/src/torch_shadow.rs | 40 +++++++++++++++++++------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/crates/benilla-app/src/torch_shadow.rs b/crates/benilla-app/src/torch_shadow.rs index 3e2c7475..74a8b837 100644 --- a/crates/benilla-app/src/torch_shadow.rs +++ b/crates/benilla-app/src/torch_shadow.rs @@ -172,13 +172,6 @@ const TORCH_MOVING_MAX: usize = TorchShadowViews::MAX_TORCH_MOVING; /// handed back to the cached path. Long enough to cover a walk cycle's pauses and a pet's /// stop-turn-start, short enough that a brazier dropped on the ground is cached within a second. const TORCH_MOVING_SETTLE: f64 = 0.75; -/// MONKEY (moving fixture): how far (yd²) a fixture must shift between two frames to count as -/// having MOVED at all. A hand-held flame on an idling NPC jitters by millimetres with the breathe -/// animation; at 60 fps this threshold is ~1.2 yd/s, so an idle never registers and a walk always -/// does. It is deliberately NOT [`TORCH_STALE_DRIFT_SQ`]: that one asks "is the cached map still -/// valid" (a question about the map), this one asks "is the fixture in motion" (a question about -/// the fixture), and a fixture creeping 0.09 yd a frame would answer the first "yes, forever". -const TORCH_MOVING_EPS_SQ: f32 = 0.0004; /// MONKEY (moving fixture): the fade rate (weight per second) for a moving slot that could NOT get /// the moving budget — 0.15 s to nothing, five times the normal [`TORCH_FADE_RATE`]. The normal /// rate exists to keep a *correct* shadow on screen while it dissolves; this one is for a shadow @@ -281,6 +274,15 @@ struct TorchSlot { } impl TorchSlot { + // MONKEY (gfx): the geometry key hashes the exact position. Even sub-centimetre hand sway + // therefore needs the live path's stable mesh handle, or each upload drops the ready bit. + fn monkey_track_motion(&mut self, now: f64) { + if self.pos != self.last_pos { + self.still_since = now; + } + self.last_pos = self.pos; + } + /// MONKEY (moving fixture): has the fixture left the position its cached map was baked from? /// This is [`map_publishable`]'s complement with one extra condition — a slot that has NEVER /// built is not "drifted", it is simply new, and belongs to the promotion path rather than to @@ -967,10 +969,7 @@ fn update_torch_shadows( // `score`) and the weight ramp below (which needs to know who fades fast), so every slot's // verdict is made from fresh positions and acted on in the same frame it is made. for slot in lane.slots.iter_mut() { - if slot.pos.distance_squared(slot.last_pos) > TORCH_MOVING_EPS_SQ { - slot.still_since = now; - } - slot.last_pos = slot.pos; + slot.monkey_track_motion(now); } let (live, dropped) = moving_budget( lane.slots @@ -1814,6 +1813,25 @@ mod tests { } // MONKEY (moving fixture): WHO may be re-rendered every frame, and for HOW LONG after it stops. + #[test] + fn monkey_slow_hand_sway_keeps_the_live_shadow_path_at_every_frame_rate() { + for fps in [30, 60, 144, 240] { + let owner = Some(LightOwner::Instance(Entity::PLACEHOLDER)); + let mut slot = moving_slot(owner, Vec3::ZERO, Vec3::ZERO, 0.0); + for frame in 1..=fps * 2 { + let now = 10.0 + f64::from(frame) / f64::from(fps); + slot.pos.x = (frame as f32 / fps as f32 * 3.0).sin() * 0.04; + slot.monkey_track_motion(now); + assert!(slot.moving(now), "{fps} fps, frame {frame}"); + slot.built_at = Some(slot.pos); + } + slot.monkey_track_motion(12.5); + assert!(slot.moving(12.5)); + slot.monkey_track_motion(13.0); + assert!(!slot.moving(13.0), "a stationary fixture returns to the cache"); + } + } + #[test] fn a_fixture_is_moving_only_while_it_is_actually_moving() { let pet = Some(LightOwner::Instance(Entity::PLACEHOLDER));