From 11774c227ae970be7564781e91f2037d5927236d Mon Sep 17 00:00:00 2001
From: BitSyndicate <contact@bitsyndicate.de>
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<A> = Option<(Box<[Option<NonZeroUsize>; SPARSE_PAGESIZE], A>, usize)>;
+
+pub struct SparseSet<T, PackedAlloc = Global, SparseAlloc = Global>
+where
+    PackedAlloc: Allocator,
+    SparseAlloc: Allocator,
+{
+    sparse: Vec<SparsePage<SparseAlloc>, SparseAlloc>,
+    dense: Vec<T, PackedAlloc>,
+    dense_to_id: Vec<usize, SparseAlloc>,
+}
+
+impl<T> SparseSet<T> {
+    pub const fn new() -> Self {
+        Self {
+            sparse: Vec::new(),
+            dense: Vec::new(),
+            dense_to_id: Vec::new(),
+        }
+    }
+}
+
+impl<T, PackedAlloc, SparseAlloc> SparseSet<T, PackedAlloc, SparseAlloc>
+where
+    PackedAlloc: Allocator,
+    SparseAlloc: Allocator + Clone,
+{
+    pub fn insert(&mut self, id: usize, value: T) -> Option<T> {
+        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<usize>) {
+        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<usize> {
+        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<T> {
+        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<T, PackedAlloc> SparseSet<T, PackedAlloc>
+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::<u32>::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::<u32>::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;
 
-- 
2.47.2


From 6166925d39121885c9de70e55d732a90984a7342 Mon Sep 17 00:00:00 2001
From: BitSyndicate <contact@bitsyndicate.de>
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
     }
 
-- 
2.47.2