use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::gfx::chunk_coord::ChunkCoord;
const EVICT_HYSTERESIS: i32 = 2;
const DETAIL_HYSTERESIS: i32 = 1;
const BYTE_BUDGET_LOW_PCT: u64 = 75;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ChunkState {
Pending,
Resident,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum ChunkDetail {
Near,
Far,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
struct Slot {
state: ChunkState,
detail: ChunkDetail,
bytes: u64,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct ChunkPlan {
pub to_load: Vec<(ChunkCoord, ChunkDetail)>,
pub to_evict: Vec<ChunkCoord>,
}
pub struct ChunkWindow {
states: BTreeMap<ChunkCoord, Slot>,
near_radius: i32,
far_radius: i32,
load_budget: usize,
byte_budget: Option<u64>,
shrink: i32,
}
impl ChunkWindow {
pub fn new(near_radius: i32, far_radius: i32, load_budget: usize) -> Self {
let near_radius = near_radius.max(0);
let far_radius = far_radius.max(near_radius);
Self {
states: BTreeMap::new(),
near_radius,
far_radius,
load_budget: load_budget.max(1),
byte_budget: None,
shrink: 0,
}
}
pub fn set_byte_budget(&mut self, budget: Option<u64>) {
self.byte_budget = budget;
}
pub fn byte_budget(&self) -> Option<u64> {
self.byte_budget
}
fn target_detail(
&self,
c: ChunkCoord,
camera: ChunkCoord,
current: Option<ChunkDetail>,
near_radius: i32,
) -> ChunkDetail {
let d = c.chebyshev_distance(camera);
if d <= near_radius {
ChunkDetail::Near
} else if matches!(current, Some(ChunkDetail::Near)) && d <= near_radius + DETAIL_HYSTERESIS
{
ChunkDetail::Near
} else {
ChunkDetail::Far
}
}
fn effective_radii(&self) -> (i32, i32) {
let far_span = self.far_radius - self.near_radius;
if self.shrink <= far_span {
(self.near_radius, self.far_radius - self.shrink)
} else {
let near = (self.near_radius - (self.shrink - far_span)).max(0);
(near, near)
}
}
fn adjust_shrink(&mut self) {
let Some(budget) = self.byte_budget else {
self.shrink = 0;
return;
};
let resident = self.resident_bytes();
if resident > budget {
self.shrink = (self.shrink + 1).min(self.far_radius);
return;
}
if self.shrink == 0 {
return;
}
let pending = self
.states
.values()
.filter(|slot| slot.state == ChunkState::Pending)
.count();
if pending == 0 && resident.saturating_mul(100) < budget.saturating_mul(BYTE_BUDGET_LOW_PCT)
{
self.shrink -= 1;
}
}
pub fn plan(&mut self, camera: ChunkCoord) -> ChunkPlan {
self.adjust_shrink();
let (near_radius, far_radius) = self.effective_radii();
let evict_radius = far_radius + EVICT_HYSTERESIS;
let mut to_evict: Vec<ChunkCoord> = Vec::new();
let gone: Vec<ChunkCoord> = self
.states
.keys()
.copied()
.filter(|c| c.chebyshev_distance(camera) > evict_radius)
.collect();
for c in &gone {
self.states.remove(c);
}
to_evict.extend_from_slice(&gone);
let redetail: Vec<ChunkCoord> = self
.states
.iter()
.filter(|(c, slot)| {
self.target_detail(**c, camera, Some(slot.detail), near_radius) != slot.detail
})
.map(|(c, _)| *c)
.collect();
for c in &redetail {
self.states.remove(c);
}
to_evict.extend_from_slice(&redetail);
let mut candidates: Vec<ChunkCoord> = Vec::new();
for dz in -far_radius..=far_radius {
for dx in -far_radius..=far_radius {
let c = camera.offset(dx, dz);
if !self.states.contains_key(&c) {
candidates.push(c);
}
}
}
candidates.sort_unstable_by(|a, b| {
a.sq_distance(camera)
.cmp(&b.sq_distance(camera))
.then(a.cmp(b))
});
candidates.truncate(self.load_budget);
let mut to_load = Vec::with_capacity(candidates.len());
for &c in &candidates {
let detail = self.target_detail(c, camera, None, near_radius);
self.states.insert(
c,
Slot {
state: ChunkState::Pending,
detail,
bytes: 0,
},
);
to_load.push((c, detail));
}
to_evict.sort_unstable();
ChunkPlan { to_load, to_evict }
}
pub fn mark_resident(&mut self, coord: ChunkCoord, bytes: u64) {
if let Some(slot) = self.states.get_mut(&coord) {
slot.state = ChunkState::Resident;
slot.bytes = bytes;
}
}
pub fn resident_bytes(&self) -> u64 {
self.states
.values()
.filter(|slot| slot.state == ChunkState::Resident)
.map(|slot| slot.bytes)
.sum()
}
pub fn forget(&mut self, coord: ChunkCoord) {
self.states.remove(&coord);
}
pub fn is_tracked(&self, coord: ChunkCoord) -> bool {
self.states.contains_key(&coord)
}
pub fn counts(&self) -> (usize, usize) {
let mut resident = 0;
let mut pending = 0;
for slot in self.states.values() {
match slot.state {
ChunkState::Resident => resident += 1,
ChunkState::Pending => pending += 1,
}
}
(resident, pending)
}
pub fn counts_by_detail(&self) -> (usize, usize) {
let mut near = 0;
let mut far = 0;
for slot in self.states.values() {
if slot.state == ChunkState::Resident {
match slot.detail {
ChunkDetail::Near => near += 1,
ChunkDetail::Far => far += 1,
}
}
}
(near, far)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn cc(x: i32, z: i32) -> ChunkCoord {
ChunkCoord::new(x, z)
}
fn load_coords(plan: &ChunkPlan) -> Vec<ChunkCoord> {
plan.to_load.iter().map(|(c, _)| *c).collect()
}
fn fill(w: &mut ChunkWindow, camera: ChunkCoord, bytes: u64) {
for (c, _) in w.plan(camera).to_load {
w.mark_resident(c, bytes);
}
}
fn settle(w: &mut ChunkWindow, camera: ChunkCoord, bytes: u64) {
let mut quiet = 0;
for _ in 0..200 {
let plan = w.plan(camera);
for (c, _) in &plan.to_load {
w.mark_resident(*c, bytes);
}
if plan.to_load.is_empty() && plan.to_evict.is_empty() {
quiet += 1;
if quiet >= 4 {
return;
}
} else {
quiet = 0;
}
}
panic!("byte-budget clamp did not converge (oscillating?)");
}
#[test]
fn plan_loads_nearest_in_window_chunks_within_budget() {
let mut w = ChunkWindow::new(2, 2, 4);
let plan = w.plan(cc(0, 0));
assert!(plan.to_evict.is_empty());
assert_eq!(plan.to_load.len(), 4);
assert_eq!(plan.to_load[0], (cc(0, 0), ChunkDetail::Near));
for (c, detail) in &plan.to_load {
assert!(c.chebyshev_distance(cc(0, 0)) <= 2);
assert_eq!(*detail, ChunkDetail::Near);
}
}
#[test]
fn plan_does_not_redispatch_tracked_chunks() {
let mut w = ChunkWindow::new(3, 3, 100);
let first = w.plan(cc(0, 0));
assert_eq!(first.to_load.len(), 49);
let second = w.plan(cc(0, 0));
assert!(second.to_load.is_empty());
assert!(second.to_evict.is_empty());
}
#[test]
fn plan_evicts_chunks_past_the_hysteresis_band() {
let mut w = ChunkWindow::new(2, 2, 100);
w.plan(cc(0, 0)); let plan = w.plan(cc(6, 0));
assert!(plan.to_evict.contains(&cc(0, 0)));
}
#[test]
fn evicted_chunk_can_be_reloaded_after_returning() {
let mut w = ChunkWindow::new(1, 1, 100);
w.plan(cc(0, 0));
w.plan(cc(20, 0)); assert!(!w.is_tracked(cc(0, 0)));
let plan = w.plan(cc(0, 0));
assert!(load_coords(&plan).contains(&cc(0, 0)));
}
#[test]
fn mark_resident_promotes_a_pending_chunk() {
let mut w = ChunkWindow::new(0, 0, 1);
let plan = w.plan(cc(0, 0));
assert_eq!(plan.to_load, vec![(cc(0, 0), ChunkDetail::Near)]);
assert_eq!(w.counts(), (0, 1));
w.mark_resident(cc(0, 0), 0);
assert_eq!(w.counts(), (1, 0));
}
#[test]
fn mark_resident_of_an_untracked_chunk_is_a_noop() {
let mut w = ChunkWindow::new(0, 0, 1);
w.mark_resident(cc(9, 9), 0); assert_eq!(w.counts(), (0, 0));
assert!(!w.is_tracked(cc(9, 9)));
}
#[test]
fn forget_lets_a_chunk_be_redispatched() {
let mut w = ChunkWindow::new(0, 0, 1);
w.plan(cc(0, 0));
assert!(w.is_tracked(cc(0, 0)));
w.forget(cc(0, 0));
assert!(!w.is_tracked(cc(0, 0)));
let plan = w.plan(cc(0, 0));
assert_eq!(plan.to_load, vec![(cc(0, 0), ChunkDetail::Near)]);
}
#[test]
fn zero_radius_and_budget_are_floored() {
let mut w = ChunkWindow::new(-5, -5, 0);
let plan = w.plan(cc(0, 0));
assert_eq!(plan.to_load, vec![(cc(0, 0), ChunkDetail::Near)]);
}
#[test]
fn far_band_chunks_load_as_impostors() {
let mut w = ChunkWindow::new(1, 3, 100);
let plan = w.plan(cc(0, 0));
assert_eq!(plan.to_load.len(), 49);
for (c, detail) in &plan.to_load {
let d = c.chebyshev_distance(cc(0, 0));
let expected = if d <= 1 {
ChunkDetail::Near
} else {
ChunkDetail::Far
};
assert_eq!(*detail, expected, "chunk {:?} at distance {}", c, d);
}
}
#[test]
fn crossing_the_boundary_redetails_a_chunk() {
let mut w = ChunkWindow::new(1, 3, 100);
let plan = w.plan(cc(0, 0));
let crossing = cc(2, 0); assert!(plan.to_load.contains(&(crossing, ChunkDetail::Far)));
for (c, _) in plan.to_load.clone() {
w.mark_resident(c, 0);
}
let (near0, far0) = w.counts_by_detail();
assert!(near0 > 0 && far0 > 0);
let plan = w.plan(cc(1, 0));
assert!(plan.to_evict.contains(&crossing));
assert!(plan.to_load.contains(&(crossing, ChunkDetail::Near)));
}
#[test]
fn detail_hysteresis_holds_a_full_chunk_through_the_band() {
let mut w = ChunkWindow::new(2, 5, 200);
for (c, _) in w.plan(cc(0, 0)).to_load {
w.mark_resident(c, 0);
}
let plan = w.plan(cc(3, 0));
assert!(!plan.to_evict.contains(&cc(0, 0)));
let plan = w.plan(cc(4, 0));
assert!(plan.to_evict.contains(&cc(0, 0)));
assert!(plan.to_load.contains(&(cc(0, 0), ChunkDetail::Far)));
}
#[test]
fn equal_radii_disable_the_far_band() {
let mut w = ChunkWindow::new(3, 3, 100);
let plan = w.plan(cc(0, 0));
assert!(plan.to_load.iter().all(|(_, d)| *d == ChunkDetail::Near));
assert_eq!(w.counts_by_detail().1, 0); }
#[test]
fn resident_bytes_counts_only_resident_chunks() {
let mut w = ChunkWindow::new(1, 1, 100); w.plan(cc(0, 0)); assert_eq!(w.resident_bytes(), 0); w.mark_resident(cc(0, 0), 500);
w.mark_resident(cc(1, 0), 250);
assert_eq!(w.resident_bytes(), 750);
assert_eq!(w.counts(), (2, 7));
}
#[test]
fn byte_budget_accessor_reflects_set_and_clear() {
let mut w = ChunkWindow::new(1, 1, 4);
assert_eq!(w.byte_budget(), None);
w.set_byte_budget(Some(4096));
assert_eq!(w.byte_budget(), Some(4096));
w.set_byte_budget(None);
assert_eq!(w.byte_budget(), None);
}
#[test]
fn no_byte_budget_never_shrinks_the_window() {
let mut w = ChunkWindow::new(1, 3, 1000);
fill(&mut w, cc(0, 0), 10_000_000);
assert_eq!(w.counts().0, 49); for _ in 0..4 {
let plan = w.plan(cc(0, 0));
assert!(plan.to_load.is_empty());
assert!(plan.to_evict.is_empty());
}
assert_eq!(w.counts().0, 49);
}
#[test]
fn byte_budget_evicts_the_far_band_before_the_near_band() {
let mut w = ChunkWindow::new(2, 6, 1000);
fill(&mut w, cc(0, 0), 100);
let (near_full, far_full) = w.counts_by_detail();
assert_eq!(near_full, 25); assert!(far_full > 0);
w.set_byte_budget(Some(9000));
settle(&mut w, cc(0, 0), 100);
let (near_after, far_after) = w.counts_by_detail();
assert_eq!(near_after, 25, "full-detail core must survive");
assert!(far_after < far_full, "impostor band must shrink");
assert!(w.resident_bytes() <= 9000);
assert!(w.is_tracked(cc(0, 0)), "camera chunk is never evicted");
assert!(!w.is_tracked(cc(6, 0)), "outermost ring evicted");
}
#[test]
fn tighter_byte_budget_shrinks_the_window_further() {
let build = |budget: u64| {
let mut w = ChunkWindow::new(2, 6, 1000);
fill(&mut w, cc(0, 0), 100);
w.set_byte_budget(Some(budget));
settle(&mut w, cc(0, 0), 100);
w
};
let loose = build(9000);
let tight = build(6000);
assert!(loose.resident_bytes() <= 9000);
assert!(tight.resident_bytes() <= 6000);
assert!(
tight.counts().0 < loose.counts().0,
"tighter budget must shrink further: tight {} vs loose {}",
tight.counts().0,
loose.counts().0
);
assert!(loose.is_tracked(cc(0, 0)) && tight.is_tracked(cc(0, 0)));
}
#[test]
fn byte_budget_clamp_settles_without_oscillating() {
let mut w = ChunkWindow::new(1, 5, 1000);
fill(&mut w, cc(0, 0), 100);
w.set_byte_budget(Some(5000));
settle(&mut w, cc(0, 0), 100); for _ in 0..8 {
let plan = w.plan(cc(0, 0));
assert!(plan.to_load.is_empty(), "regrew: {:?}", plan.to_load);
assert!(plan.to_evict.is_empty(), "evicted: {:?}", plan.to_evict);
}
assert!(w.resident_bytes() <= 5000);
}
}