Files
benilla-octo/crates/benilla-assets/src/spatial_cache.rs
T
Sam SungandClaude Opus 5.5 d69d09dfce Comments in benilla-assets: one line of fact each, no history, no record numbers (#95)
Part of #41: the comments of `benilla-assets`, Rust and WGSL, rewritten to the rule in `docs/METHOD.md`: what the code does and the reference fact behind it, in a line. 3,872 comment lines become 1,568.

- The citations that were only a private record number go first, mechanically, each edit shown to be a pure removal.
- The rewrite corrects the comments that stated wrong facts: the alpha-key cutoff now cites the reference's per-blend-mode table (`0x85ad20`), the texture-filter note names the subsystems that ignore the global setting, a test doc said an exhausted variation roll returns the last variation (it returns the first), and about twenty more found on the way (renamed or deleted items, a shader flag the doc misread, docs sitting on the wrong function). "Deviation:" now marks only a choice with a reason; an unbuilt behaviour says so plainly.
- Verified: every file strips to identical code, and `cargo fmt --check` is clean. A second reader compared every file old against new, restored the facts the rewrite had dropped, and checked each new citation against the reference.

String literals are untouched; they are #92.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 16:27:58 +07:00

242 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The spatial dedup cache: a `HashMap` whose entries remember where they were last used from, so
//! a sweep can drop the art of a place you have left.
use std::collections::HashMap;
use std::hash::Hash;
/// The dwell floor: nothing is dropped within this many wall seconds of its last use, however far
/// away. A free-fly camera at 500 yd/s crosses the radius in seconds, and distance alone would
/// drop art it rebuilds on the way back; at gameplay speed the floor never binds.
const MIN_DWELL_SECS: f32 = 30.0;
/// Horizontal distance between two WoW-space points; height is ignored, as the streamer's window
/// is a tile square.
fn plan_dist(a: [f32; 3], b: [f32; 3]) -> f32 {
let (dx, dy) = (a[0] - b[0], a[1] - b[1]);
(dx * dx + dy * dy).sqrt()
}
/// A dedup cache that a sweep can empty of the art of places left behind. A lookup is
/// [`Self::fetch`], which counts as a use; a `None` stamp means used since the last sweep, so a
/// hit costs no stamping.
pub struct SpatialCache<K, V> {
map: HashMap<K, (V, Option<Stamp>)>,
}
/// The view focus and the time of the first sweep after an entry's last use.
#[derive(Clone, Copy)]
struct Stamp {
at: [f32; 3],
t: f32,
}
impl<K, V> Default for SpatialCache<K, V> {
fn default() -> Self {
Self {
map: HashMap::new(),
}
}
}
impl<K: Eq + Hash, V> SpatialCache<K, V> {
/// Live entry count.
pub fn len(&self) -> usize {
self.map.len()
}
/// Whether the cache holds nothing.
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
/// Drop everything: the map-change teardown.
pub fn clear(&mut self) {
self.map.clear();
}
/// Fetch a value, counting the hit as a use. It clones out so the miss path can insert in the
/// same expression.
pub fn fetch(&mut self, key: &K) -> Option<V>
where
V: Clone,
{
let slot = self.map.get_mut(key)?;
slot.1 = None;
Some(slot.0.clone())
}
/// Install a freshly-built value, counted as a use.
pub fn insert(&mut self, key: K, value: V) {
self.map.insert(key, (value, None));
}
/// Get or build in place, counted as a use, for a value too big to clone out.
pub fn or_insert_with(&mut self, key: K, build: impl FnOnce() -> V) -> &mut V {
let slot = self.map.entry(key).or_insert_with(|| (build(), None));
slot.1 = None;
&mut slot.0
}
/// Stamp everything used since the last sweep at `focus` and `now`, then drop every entry both
/// more than `radius` yd away and idle for [`MIN_DWELL_SECS`]; returns how many were dropped.
pub fn scope(&mut self, focus: [f32; 3], radius: f32, now: f32) -> usize {
let before = self.map.len();
self.map.retain(|_, (_, stamp)| match *stamp {
None => {
*stamp = Some(Stamp { at: focus, t: now });
true
}
Some(s) => plan_dist(s.at, focus) <= radius || now - s.t < MIN_DWELL_SECS,
});
before - self.map.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::*;
/// When the art is used and stamped.
const NOW: f32 = 1000.0;
/// A sweep just past the dwell floor, the earliest that distance can expire anything.
const LATER: f32 = NOW + MIN_DWELL_SECS + 1.0;
const HOME: [f32; 3] = [0.0, 0.0, 0.0];
const FAR: [f32; 3] = [4000.0, 0.0, 0.0];
/// Five ADT tiles; any radius exercises the same code.
const R: f32 = 2667.0;
#[test]
fn the_first_sweep_stamps_rather_than_judges() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
// No stamp yet, so it cannot be too far.
assert_eq!(c.scope(FAR, R, NOW), 0);
assert_eq!(c.len(), 1);
// Now it is stamped at FAR, so a later sweep from HOME drops it.
assert_eq!(c.scope(HOME, R, LATER), 1);
assert!(c.fetch(&1).is_none());
}
#[test]
fn art_you_walked_away_from_is_dropped() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
c.scope(HOME, R, NOW); // stamp at home
assert_eq!(
c.scope([R * 0.9, 0.0, 0.0], R, LATER),
0,
"inside the radius"
);
assert_eq!(c.scope(FAR, R, LATER), 1, "beyond it");
}
#[test]
fn a_use_restarts_the_grace() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
c.scope(HOME, R, NOW);
for step in 1..20 {
let at = [step as f32 * R * 0.5, 0.0, 0.0];
assert_eq!(c.fetch(&1), Some(10));
// Swept well past the dwell floor every step, so what keeps it alive is the use.
assert_eq!(
c.scope(at, R, NOW + step as f32 * (MIN_DWELL_SECS + 1.0)),
0,
"re-used at step {step}"
);
}
}
#[test]
fn or_insert_with_builds_once_and_counts_as_a_use() {
let mut c: SpatialCache<u32, Vec<u32>> = SpatialCache::default();
let mut builds = 0;
for _ in 0..3 {
let v = c.or_insert_with(7, || {
builds += 1;
vec![1, 2, 3]
});
assert_eq!(v.len(), 3);
}
assert_eq!(builds, 1);
assert_eq!(
c.scope(FAR, R, LATER),
0,
"the last use restarted the grace"
);
}
#[test]
fn a_fast_camera_cannot_outrun_the_dwell_floor() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
c.scope(HOME, R, NOW);
// 500 yd/s (FlyCam 100 × the Ctrl boost 5) for five seconds: far past the radius already.
for s in 1..=5 {
let at = [500.0 * s as f32, 0.0, 0.0];
assert_eq!(
c.scope(at, R, NOW + s as f32),
0,
"dropped {s} s out — the free-fly thrash"
);
}
// Back inside the radius, the same entry: no rebuild.
assert_eq!(c.scope(HOME, R, NOW + 6.0), 0);
assert_eq!(c.fetch(&1), Some(10));
// Away past the floor, it does expire.
c.scope(HOME, R, NOW + 7.0);
assert_eq!(c.scope(FAR, R, NOW + 7.0 + MIN_DWELL_SECS), 1);
}
#[test]
fn altitude_is_not_distance() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
c.scope(HOME, R, NOW);
assert_eq!(c.scope([0.0, 0.0, 10_000.0], R, LATER), 0);
}
/// Eviction frees memory only if dropping the last handle frees the asset.
#[test]
fn dropping_the_last_handle_frees_the_asset() {
let mut app = App::new();
app.add_plugins((
bevy::app::TaskPoolPlugin::default(),
bevy::asset::AssetPlugin::default(),
))
.init_asset::<Image>();
let mut cache: SpatialCache<u32, Handle<Image>> = SpatialCache::default();
let id = {
let mut images = app.world_mut().resource_mut::<Assets<Image>>();
let handle = images.add(Image::default());
let id = handle.id();
cache.insert(1, handle);
id
};
app.update();
assert!(
app.world().resource::<Assets<Image>>().contains(id),
"held by the cache"
);
cache.scope(HOME, R, NOW); // stamp it here…
assert_eq!(cache.scope(FAR, R, LATER), 1, "…then walk away");
// The drop is processed by `Assets::track_assets` on a later frame, not at the drop itself.
for _ in 0..3 {
app.update();
}
assert!(
!app.world().resource::<Assets<Image>>().contains(id),
"the image outlived its last handle — eviction would free nothing"
);
}
#[test]
fn an_unswept_cache_never_expires() {
let mut c: SpatialCache<u32, u32> = SpatialCache::default();
c.insert(1, 10);
assert_eq!(c.fetch(&1), Some(10));
assert_eq!(c.len(), 1);
}
}