From ef4797e7fa8181c6f068e03bfc08047e5a91ce28 Mon Sep 17 00:00:00 2001 From: Benjamin Saunders Date: Sat, 7 Mar 2026 16:45:23 -0800 Subject: [PATCH] Unvendor lru-slab --- Cargo.lock | 1 + client/Cargo.toml | 1 + client/src/graphics/voxels/mod.rs | 17 +- common/src/lib.rs | 2 - common/src/lru_slab.rs | 306 ------------------------------ common/src/node.rs | 5 +- 6 files changed, 12 insertions(+), 320 deletions(-) delete mode 100644 common/src/lru_slab.rs diff --git a/Cargo.lock b/Cargo.lock index 365bffca..f33dffc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ dependencies = [ "hecs", "lahar", "libm", + "lru-slab", "memoffset", "metrics", "nalgebra", diff --git a/client/Cargo.toml b/client/Cargo.toml index 03d9c9aa..094908f7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -39,6 +39,7 @@ gltf = { version = "1.0.0", default-features = false, features = ["utils"] } metrics = "0.24.0" hdrhistogram = { version = "7", default-features = false } save = { path = "../save" } +lru-slab = "0.1.2" [features] default = ["use-repo-assets"] diff --git a/client/src/graphics/voxels/mod.rs b/client/src/graphics/voxels/mod.rs index 992f3b58..b5cb758c 100644 --- a/client/src/graphics/voxels/mod.rs +++ b/client/src/graphics/voxels/mod.rs @@ -7,6 +7,7 @@ mod tests; use std::{sync::Arc, time::Instant}; use ash::{Device, vk}; +use lru_slab::LruSlab; use metrics::histogram; use tracing::warn; @@ -15,10 +16,8 @@ use crate::{ graphics::{Base, Frustum}, }; use common::{ - LruSlab, dodeca::{self, Vertex}, graph::NodeId, - lru_slab::SlotId, math::{MIsometry, MPoint}, node::{Chunk, ChunkId, VoxelData}, }; @@ -135,7 +134,7 @@ impl Voxels { self.states.get_mut(slot).refcount += 1; frame.drawn.push(slot); // Transfer transform - frame.surface.transforms_mut()[slot.0 as usize] = + frame.surface.transforms_mut()[slot as usize] = na::Matrix4::from(*node_transform) * vertex.chunk_to_node(); } if let (None, &VoxelData::Dense(ref data)) = (&surface, voxels) { @@ -183,9 +182,9 @@ impl Voxels { let node_is_odd = sim.graph.depth(node) & 1 != 0; extractions.push(ExtractTask { index: scratch_slot, - indirect_offset: self.surfaces.indirect_offset(slot.0), - face_offset: self.surfaces.face_offset(slot.0), - draw_id: slot.0, + indirect_offset: self.surfaces.indirect_offset(slot), + face_offset: self.surfaces.face_offset(slot), + draw_id: slot, reverse_winding: vertex.parity() ^ node_is_odd, }); } @@ -224,8 +223,8 @@ impl Voxels { ) { return; } - for chunk in &frame.drawn { - self.draw.draw(device, cmd, &self.surfaces, chunk.0); + for &chunk in &frame.drawn { + self.draw.draw(device, cmd, &self.surfaces, chunk); } histogram!("frame.cpu.voxels.draw").record(started.elapsed()); } @@ -245,7 +244,7 @@ pub struct Frame { surface: surface::Frame, /// Scratch slots completed in this frame extracted: Vec, - drawn: Vec, + drawn: Vec, } impl Frame { diff --git a/common/src/lib.rs b/common/src/lib.rs index 6e3cf06e..b107dd3e 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -21,7 +21,6 @@ pub mod graph; pub mod graph_collision; mod graph_entities; pub mod graph_ray_casting; -pub mod lru_slab; mod margins; pub mod math; pub mod node; @@ -35,7 +34,6 @@ pub mod worldgen; pub use chunks::Chunks; pub use graph_entities::GraphEntities; -pub use lru_slab::LruSlab; pub use sim_config::{SimConfig, SimConfigRaw}; // Stable IDs made of 8 random bytes for easy persistent references diff --git a/common/src/lru_slab.rs b/common/src/lru_slab.rs deleted file mode 100644 index ff2576b9..00000000 --- a/common/src/lru_slab.rs +++ /dev/null @@ -1,306 +0,0 @@ -/// A random-access table that maintains an LRU list in constant time -pub struct LruSlab { - slots: Box<[Slot]>, - /// Most recently used - head: SlotId, - /// Least recently used - tail: SlotId, - /// First unused - free: SlotId, - /// Number of occupied slots - len: u32, -} - -impl LruSlab { - pub fn new() -> Self { - Self::with_capacity(0) - } - - pub fn with_capacity(capacity: u32) -> Self { - assert!(capacity != u32::MAX, "capacity too large"); - Self { - slots: (0..capacity) - .map(|n| Slot { - value: None, - prev: SlotId::NONE, - next: if n + 1 == capacity { - SlotId::NONE - } else { - SlotId(n + 1) - }, - }) - .collect::>() - .into(), - head: SlotId::NONE, - tail: SlotId::NONE, - free: if capacity == 0 { - SlotId::NONE - } else { - SlotId(0) - }, - len: 0, - } - } - - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - pub fn len(&self) -> u32 { - self.len - } - - pub fn capacity(&self) -> u32 { - self.slots.len() as u32 - } - - /// Inserts a value, returning the slot it was stored in - /// - /// The returned slot is marked as the most recently used. - pub fn insert(&mut self, value: T) -> SlotId { - let id = match self.alloc() { - Some(id) => id, - None => { - let len = self.capacity(); - let cap = 2 * len.max(2); - self.slots = self - .slots - .iter_mut() - .map(|x| Slot { - value: x.value.take(), - next: x.next, - prev: x.prev, - }) - .chain((len..cap).map(|n| Slot { - value: None, - prev: SlotId::NONE, - next: if n + 1 == cap { - SlotId::NONE - } else { - SlotId(n + 1) - }, - })) - .collect::>() - .into_boxed_slice(); - self.free = SlotId(len + 1); - SlotId(len) - } - }; - let idx = id.0 as usize; - - debug_assert!(self.slots[idx].value.is_none(), "corrupt free list"); - self.slots[idx].value = Some(value); - self.link_at_head(id); - self.len += 1; - - id - } - - /// Get the least recently used slot, if any - pub fn lru(&self) -> Option { - if self.tail == SlotId::NONE { - debug_assert_eq!(self.head, SlotId::NONE); - None - } else { - Some(self.tail) - } - } - - pub fn remove(&mut self, slot: SlotId) -> T { - self.unlink(slot); - self.slots[slot.0 as usize].next = self.free; - self.slots[slot.0 as usize].prev = SlotId::NONE; - self.free = slot; - self.len -= 1; - self.slots[slot.0 as usize] - .value - .take() - .expect("removing empty slot") - } - - /// Mark `slot` as the most recently used and access it uniquely - pub fn get_mut(&mut self, slot: SlotId) -> &mut T { - self.freshen(slot); - self.peek_mut(slot) - } - - /// Access `slot` without marking it as most recently used - pub fn peek(&self, slot: SlotId) -> &T { - self.slots[slot.0 as usize].value.as_ref().unwrap() - } - - /// Access `slot` uniquely without marking it as most recently used - pub fn peek_mut(&mut self, slot: SlotId) -> &mut T { - self.slots[slot.0 as usize].value.as_mut().unwrap() - } - - /// Walks the container from most to least recently used - pub fn iter(&self) -> Iter<'_, T> { - Iter { - slots: &self.slots[..], - head: self.head, - tail: self.tail, - len: self.len, - } - } - - /// Remove a slot from the freelist - fn alloc(&mut self) -> Option { - if self.free == SlotId::NONE { - return None; - } - let slot = self.free; - self.free = self.slots[slot.0 as usize].next; - Some(slot) - } - - /// Mark `slot` as the most recently used - fn freshen(&mut self, slot: SlotId) { - if self.slots[slot.0 as usize].prev == SlotId::NONE { - // This is already the freshest slot, so we don't need to do anything - debug_assert_eq!(self.head, slot, "corrupt lru list"); - return; - } - - self.unlink(slot); - self.link_at_head(slot); - } - - /// Add a link to the head of the list - fn link_at_head(&mut self, slot: SlotId) { - let idx = slot.0 as usize; - if self.head == SlotId::NONE { - // List was empty - self.slots[idx].next = SlotId::NONE; - self.tail = slot; - } else { - self.slots[idx].next = self.head; - self.slots[self.head.0 as usize].prev = slot; - } - self.slots[idx].prev = SlotId::NONE; - self.head = slot; - } - - /// Remove a link from anywhere in the list - fn unlink(&mut self, slot: SlotId) { - let idx = slot.0 as usize; - if self.slots[idx].prev != SlotId::NONE { - self.slots[self.slots[idx].prev.0 as usize].next = self.slots[idx].next; - } else { - self.head = self.slots[idx].next; - } - if self.slots[idx].next != SlotId::NONE { - self.slots[self.slots[idx].next.0 as usize].prev = self.slots[idx].prev; - } else { - // This was the tail - self.tail = self.slots[idx].prev; - } - } -} - -impl Default for LruSlab { - fn default() -> Self { - Self::new() - } -} - -struct Slot { - value: Option, - /// Next slot in the LRU or free list - next: SlotId, - /// Previous slot in the LRU list; NONE when free - prev: SlotId, -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct SlotId(pub u32); - -impl SlotId { - const NONE: Self = SlotId(u32::MAX); -} - -pub struct Iter<'a, T> { - slots: &'a [Slot], - head: SlotId, - tail: SlotId, - len: u32, -} - -impl<'a, T> Iterator for Iter<'a, T> { - type Item = &'a T; - fn next(&mut self) -> Option<&'a T> { - if self.len == 0 { - return None; - } - let idx = self.head.0 as usize; - let result = self.slots[idx].value.as_ref().expect("corrupt LRU list"); - self.head = self.slots[idx].next; - self.len -= 1; - Some(result) - } - - fn size_hint(&self) -> (usize, Option) { - (self.len as usize, Some(self.len as usize)) - } -} - -impl<'a, T> DoubleEndedIterator for Iter<'a, T> { - fn next_back(&mut self) -> Option<&'a T> { - if self.len == 0 { - return None; - } - let idx = self.tail.0 as usize; - let result = self.slots[idx].value.as_ref().expect("corrupt LRU list"); - self.tail = self.slots[idx].prev; - self.len -= 1; - Some(result) - } -} - -impl ExactSizeIterator for Iter<'_, T> { - fn len(&self) -> usize { - self.len as usize - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn lru_order() { - let mut cache = LruSlab::new(); - let b = cache.insert('b'); - assert_eq!(cache.iter().collect::(), "b"); - let _a = cache.insert('a'); - assert_eq!(cache.iter().collect::(), "ab"); - let d = cache.insert('d'); - assert_eq!(cache.iter().collect::(), "dab"); - let c = cache.insert('c'); - assert_eq!(cache.iter().collect::(), "cdab"); - let e = cache.insert('e'); - assert_eq!(cache.iter().collect::(), "ecdab"); - - cache.get_mut(b); - cache.get_mut(c); - cache.get_mut(d); - cache.get_mut(e); - - assert_eq!(cache.remove(cache.lru().unwrap()), 'a'); - assert_eq!(cache.remove(cache.lru().unwrap()), 'b'); - assert_eq!(cache.remove(cache.lru().unwrap()), 'c'); - assert_eq!(cache.remove(cache.lru().unwrap()), 'd'); - assert_eq!(cache.remove(cache.lru().unwrap()), 'e'); - assert!(cache.lru().is_none()); - } - - #[test] - fn slot_reuse() { - let mut cache = LruSlab::new(); - let a = cache.insert('a'); - cache.remove(a); - let a_prime = cache.insert('a'); - assert_eq!(a, a_prime); - assert_eq!(cache.len(), 1); - } -} diff --git a/common/src/node.rs b/common/src/node.rs index 36df04ce..e6de01c0 100644 --- a/common/src/node.rs +++ b/common/src/node.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; use crate::collision_math::Ray; use crate::dodeca::Vertex; use crate::graph::{Graph, NodeId}; -use crate::lru_slab::SlotId; use crate::proto::{BlockUpdate, Position, SerializedVoxelData}; use crate::voxel_math::{ChunkDirection, CoordAxis, CoordSign, Coords}; use crate::world::Material; @@ -266,12 +265,12 @@ pub enum Chunk { /// A reference to the "mesh" used to render the chunk. Set to `None` if /// this mesh needs to be computed or recomputed. - surface: Option, + surface: Option, /// An outdated (but valid) reference to the "mesh" used to render the /// chunk. This is used to allow the mesh to still be rendered while it /// is being recomputed. - old_surface: Option, + old_surface: Option, }, }