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));