From 0e0f9331f9d14f423bbd9af6fa410e5bd1714d5f Mon Sep 17 00:00:00 2001 From: BitSyndicate Date: Sun, 20 Apr 2025 23:54:16 +0200 Subject: [PATCH 1/2] feat(ecs): add rudimentary sparse set impl --- Cargo.lock | 1 + Cargo.toml | 1 + src/collections/mod.rs | 3 + src/collections/sparse_set.rs | 273 ++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 5 files changed, 279 insertions(+) create mode 100644 src/collections/mod.rs create mode 100644 src/collections/sparse_set.rs diff --git a/Cargo.lock b/Cargo.lock index 8cc3820..fb809d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3685,6 +3685,7 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" name = "zenyx" version = "0.1.0" dependencies = [ + "allocator-api2", "bytemuck", "cgmath", "image", diff --git a/Cargo.toml b/Cargo.toml index 7c423c4..732a166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } vulkano = "0.35.1" wgpu = { version = "25.0.0", features = ["spirv"] } zlog.workspace = true +allocator-api2 = "0.2.21" [target.aarch64-linux-android.dependencies] winit = { version = "0.30.9", features = ["android-native-activity"] } diff --git a/src/collections/mod.rs b/src/collections/mod.rs new file mode 100644 index 0000000..fa842ed --- /dev/null +++ b/src/collections/mod.rs @@ -0,0 +1,3 @@ +mod sparse_set; + +pub use sparse_set::SparseSet; diff --git a/src/collections/sparse_set.rs b/src/collections/sparse_set.rs new file mode 100644 index 0000000..f5c1c6e --- /dev/null +++ b/src/collections/sparse_set.rs @@ -0,0 +1,273 @@ +use core::{num::NonZeroUsize, usize}; + +use allocator_api2::{ + alloc::{Allocator, Global}, + boxed::Box, + vec::Vec, +}; +use bytemuck::Contiguous; + +const SPARSE_PAGESIZE: usize = (1 << 10) * 4; +type SparsePage = Option<(Box<[Option; SPARSE_PAGESIZE], A>, usize)>; + +pub struct SparseSet +where + PackedAlloc: Allocator, + SparseAlloc: Allocator, +{ + sparse: Vec, SparseAlloc>, + dense: Vec, + dense_to_id: Vec, +} + +impl SparseSet { + pub const fn new() -> Self { + Self { + sparse: Vec::new(), + dense: Vec::new(), + dense_to_id: Vec::new(), + } + } +} + +impl SparseSet +where + PackedAlloc: Allocator, + SparseAlloc: Allocator + Clone, +{ + pub fn insert(&mut self, id: usize, value: T) -> Option { + match self.get_dense_idx(id) { + Some(idx) => { + let previous = core::mem::replace(&mut self.dense[idx], value); + self.dense_to_id[idx] = id; + Some(previous) + } + None => { + self.increase_page_usage_count(id); + self.set_dense_idx(id, Some(self.dense.len())); + self.dense.push(value); + self.dense_to_id.push(id); + None + } + } + } + + pub fn get(&self, id: usize) -> Option<&T> { + self.dense.get(self.get_dense_idx(id)?) + } + + pub fn get_mut(&mut self, id: usize) -> Option<&mut T> { + let idx = self.get_dense_idx(id)?; + self.dense.get_mut(idx) + } + + fn set_dense_idx(&mut self, id: usize, idx: Option) { + let page = id / SPARSE_PAGESIZE; + let sparse_index = id % SPARSE_PAGESIZE; + + if page >= self.sparse.len() { + self.sparse.resize(page + 1, None); + } + + if self.sparse[page].is_none() { + self.sparse[page] = Some(( + Box::new_in([None; 4096], self.sparse.allocator().clone()), + 1, + )) + } + + match &mut self.sparse[page] { + Some(page) => { + page.0[sparse_index] = idx.map(|i| NonZeroUsize::new(i + 1).unwrap()); + } + None => unreachable!("wtf, failed to init sparse page 5 lines above??"), + } + } + + pub fn get_dense_idx(&self, id: usize) -> Option { + let page = id / SPARSE_PAGESIZE; + let sparse_index = id % SPARSE_PAGESIZE; + let page = self.sparse.get(page)?.as_ref()?; + page.0[sparse_index].map(|idx| idx.into_integer() - 1) + } + + fn reduce_page_usage_count(&mut self, id: usize) { + let page = id / SPARSE_PAGESIZE; + let Some(usage) = &mut self.sparse[page] else { + return; + }; + usage.1 -= 1; + let usage = usage.1; + if usage == 0 { + self.sparse[page] = None; + } + } + + fn increase_page_usage_count(&mut self, id: usize) { + let page = id / SPARSE_PAGESIZE; + if page >= self.sparse.len() { + return; + } + let Some(usage) = &mut self.sparse[page] else { + return; + }; + usage.1 += 1; + } + + pub fn remove(&mut self, id: usize) -> Option { + let index = self.get_dense_idx(id)?; + if self.dense.is_empty() { + return None; + } + + self.set_dense_idx(*self.dense_to_id.last().unwrap(), Some(index)); + self.set_dense_idx(id, None); + self.reduce_page_usage_count(id); + + let previous = self.dense.swap_remove(index); + self.dense_to_id.swap_remove(index); + Some(previous) + } + + pub fn is_emtpy(&self) -> bool { + self.len() == 0 + } + + pub fn len(&self) -> usize { + self.dense.len() + } + + pub fn contains(&self, id: usize) -> bool { + self.get_dense_idx(id).is_some() + } + + pub fn keys(&self) -> &[usize] { + &self.dense_to_id + } + + pub fn values(&self) -> &[T] { + &self.dense + } + + pub fn new_in(packed_alloc: PackedAlloc, sparse_alloc: SparseAlloc) -> Self { + Self { + dense: Vec::new_in(packed_alloc), + sparse: Vec::new_in(sparse_alloc.clone()), + dense_to_id: Vec::new_in(sparse_alloc), + } + } +} + +impl SparseSet +where + PackedAlloc: Allocator, +{ + pub const fn new_in_packed(packed_alloc: PackedAlloc) -> Self { + Self { + sparse: Vec::new(), + dense: Vec::new_in(packed_alloc), + dense_to_id: Vec::new(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn insert() { + let mut sparse_set = SparseSet::::new(); + sparse_set.insert(10, 1); + assert_eq!(sparse_set.keys(), &[10]); + assert_eq!(sparse_set.values(), &[1]); + + assert_eq!( + sparse_set.sparse[0].as_ref().unwrap().0[10].unwrap(), + NonZeroUsize::new(1).unwrap() + ); + assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 1); + assert_eq!(sparse_set.insert(10, 2).unwrap(), 1); + assert_eq!(sparse_set.values(), &[2]); + assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 1); + + sparse_set.insert(11, 4); + assert_eq!(sparse_set.keys(), &[10, 11]); + assert_eq!(sparse_set.values(), &[2, 4]); + assert_eq!( + sparse_set.sparse[0].as_ref().unwrap().0[11].unwrap(), + NonZeroUsize::new(2).unwrap() + ); + assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 2); + + sparse_set.insert(5000, 3); + assert_eq!(sparse_set.keys(), &[10, 11, 5000]); + assert_eq!(sparse_set.values(), &[2, 4, 3]); + assert_eq!( + sparse_set.sparse[5000 / SPARSE_PAGESIZE] + .as_ref() + .unwrap() + .0[5000 % SPARSE_PAGESIZE] + .unwrap(), + NonZeroUsize::new(3).unwrap() + ); + assert_eq!( + sparse_set.sparse[5000 / SPARSE_PAGESIZE] + .as_ref() + .unwrap() + .1, + 1 + ); + + assert_eq!(*sparse_set.get(10).unwrap(), 2); + assert_eq!(*sparse_set.get(11).unwrap(), 4); + assert_eq!(*sparse_set.get(5000).unwrap(), 3); + } + + #[test] + fn remove() { + let mut sparse_set = SparseSet::::new(); + sparse_set.insert(10, 1); + sparse_set.insert(11, 2); + sparse_set.insert(12, 2); + sparse_set.insert(SPARSE_PAGESIZE, 1); + sparse_set.insert(SPARSE_PAGESIZE + 1, 2); + sparse_set.insert(SPARSE_PAGESIZE + 2, 3); + + assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 2).unwrap(), 3); + assert_eq!(sparse_set.sparse[1].as_ref().unwrap().1, 2); + assert_eq!(sparse_set.keys(), [10, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]); + assert_eq!(sparse_set.values(), [1, 2, 2, 1, 2]); + + assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 1).unwrap(), 2); + assert_eq!(sparse_set.sparse[1].as_ref().unwrap().1, 1); + assert_eq!(sparse_set.keys(), [10, 11, 12, SPARSE_PAGESIZE]); + assert_eq!(sparse_set.values(), [1, 2, 2, 1]); + + assert_eq!(sparse_set.remove(SPARSE_PAGESIZE).unwrap(), 1); + assert!(sparse_set.sparse[1].is_none()); + assert_eq!(sparse_set.keys(), [10, 11, 12]); + assert_eq!(sparse_set.values(), [1, 2, 2]); + + + sparse_set.insert(SPARSE_PAGESIZE, 1); + sparse_set.insert(SPARSE_PAGESIZE + 1, 2); + sparse_set.insert(SPARSE_PAGESIZE + 2, 3); + + assert_eq!(sparse_set.remove(10).unwrap(), 1); + assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 2); + // swap-remove + assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]); + assert_eq!(sparse_set.values(), [3, 2, 2, 1, 2]); + + assert_eq!(sparse_set.remove(11).unwrap(), 2); + assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 1); + assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, 12, SPARSE_PAGESIZE]); + assert_eq!(sparse_set.values(), [3, 2, 2, 1]); + + assert_eq!(sparse_set.remove(12).unwrap(), 2); + assert!(sparse_set.sparse[0].is_none()); + assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, SPARSE_PAGESIZE]); + assert_eq!(sparse_set.values(), [3, 2, 1]); + } +} diff --git a/src/main.rs b/src/main.rs index 54e5339..d588364 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ use zlog::LogLevel; use zlog::config::LoggerConfig; pub mod camera; +pub mod collections; pub mod model; pub mod texture; From 869f9e8f07ad0a54624f0e975b40550cb94fdc31 Mon Sep 17 00:00:00 2001 From: BitSyndicate Date: Mon, 21 Apr 2025 00:05:33 +0200 Subject: [PATCH 2/2] chore: fix typo in function name --- src/collections/sparse_set.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/collections/sparse_set.rs b/src/collections/sparse_set.rs index f5c1c6e..5e0ec84 100644 --- a/src/collections/sparse_set.rs +++ b/src/collections/sparse_set.rs @@ -129,7 +129,7 @@ where Some(previous) } - pub fn is_emtpy(&self) -> bool { + pub fn is_empty(&self) -> bool { self.len() == 0 }