use glam::Vec3;
use super::{SparseVoxelOctree, SvoNode};
use crate::compiled::AabbPacked;
#[derive(Clone)]
pub struct SvoChunk {
pub chunk_id: u32,
pub nodes: Vec<SvoNode>,
pub bounds: AabbPacked,
pub start_depth: u32,
}
impl SvoChunk {
pub const fn new(
chunk_id: u32,
nodes: Vec<SvoNode>,
bounds: AabbPacked,
start_depth: u32,
) -> Self {
Self {
chunk_id,
nodes,
bounds,
start_depth,
}
}
pub fn memory_bytes(&self) -> usize {
self.nodes.len() * std::mem::size_of::<SvoNode>()
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.chunk_id.to_le_bytes());
bytes.extend_from_slice(&(self.nodes.len() as u32).to_le_bytes());
bytes.extend_from_slice(&self.start_depth.to_le_bytes());
bytes.extend_from_slice(&self.bounds.min_x.to_le_bytes());
bytes.extend_from_slice(&self.bounds.min_y.to_le_bytes());
bytes.extend_from_slice(&self.bounds.min_z.to_le_bytes());
bytes.extend_from_slice(&self.bounds.max_x.to_le_bytes());
bytes.extend_from_slice(&self.bounds.max_y.to_le_bytes());
bytes.extend_from_slice(&self.bounds.max_z.to_le_bytes());
for node in &self.nodes {
bytes.extend_from_slice(&node.distance.to_le_bytes());
bytes.extend_from_slice(&node.nx.to_le_bytes());
bytes.extend_from_slice(&node.ny.to_le_bytes());
bytes.extend_from_slice(&node.nz.to_le_bytes());
bytes.push(node.child_mask);
bytes.push(node.is_leaf);
bytes.extend_from_slice(&node.material_id.to_le_bytes());
bytes.extend_from_slice(&node.first_child.to_le_bytes());
bytes.extend_from_slice(&node._pad[0].to_le_bytes());
bytes.extend_from_slice(&node._pad[1].to_le_bytes());
}
bytes
}
pub fn from_bytes(data: &[u8]) -> Option<Self> {
if data.len() < 36 {
return None;
}
let chunk_id = u32::from_le_bytes(data[0..4].try_into().ok()?);
let node_count = u32::from_le_bytes(data[4..8].try_into().ok()?) as usize;
let start_depth = u32::from_le_bytes(data[8..12].try_into().ok()?);
let min_x = f32::from_le_bytes(data[12..16].try_into().ok()?);
let min_y = f32::from_le_bytes(data[16..20].try_into().ok()?);
let min_z = f32::from_le_bytes(data[20..24].try_into().ok()?);
let max_x = f32::from_le_bytes(data[24..28].try_into().ok()?);
let max_y = f32::from_le_bytes(data[28..32].try_into().ok()?);
let max_z = f32::from_le_bytes(data[32..36].try_into().ok()?);
let bounds = AabbPacked::new(
Vec3::new(min_x, min_y, min_z),
Vec3::new(max_x, max_y, max_z),
);
let node_data = &data[36..];
let node_byte_size = 32; if node_data.len() < node_count * node_byte_size {
return None;
}
let mut nodes = Vec::with_capacity(node_count);
for i in 0..node_count {
let offset = i * node_byte_size;
let d = &node_data[offset..];
let distance = f32::from_le_bytes(d[0..4].try_into().ok()?);
let nx = f32::from_le_bytes(d[4..8].try_into().ok()?);
let ny = f32::from_le_bytes(d[8..12].try_into().ok()?);
let nz = f32::from_le_bytes(d[12..16].try_into().ok()?);
let child_mask = d[16];
let is_leaf = d[17];
let material_id = u16::from_le_bytes(d[18..20].try_into().ok()?);
let first_child = u32::from_le_bytes(d[20..24].try_into().ok()?);
let pad0 = f32::from_le_bytes(d[24..28].try_into().ok()?);
let pad1 = f32::from_le_bytes(d[28..32].try_into().ok()?);
nodes.push(SvoNode {
distance,
nx,
ny,
nz,
child_mask,
is_leaf,
material_id,
first_child,
_pad: [pad0, pad1],
});
}
Some(Self {
chunk_id,
nodes,
bounds,
start_depth,
})
}
}
struct CacheEntry {
chunk: SvoChunk,
last_access: u64,
}
pub struct SvoStreamingCache {
capacity: usize,
entries: std::collections::HashMap<u32, CacheEntry>,
access_counter: u64,
memory_used: usize,
max_memory: usize,
hits: u64,
misses: u64,
}
impl SvoStreamingCache {
pub fn new(capacity: usize) -> Self {
Self {
capacity,
entries: std::collections::HashMap::new(),
access_counter: 0,
memory_used: 0,
max_memory: 0,
hits: 0,
misses: 0,
}
}
pub fn with_memory_budget(max_memory_bytes: usize) -> Self {
Self {
capacity: usize::MAX,
entries: std::collections::HashMap::new(),
access_counter: 0,
memory_used: 0,
max_memory: max_memory_bytes,
hits: 0,
misses: 0,
}
}
pub fn get(&mut self, chunk_id: u32) -> Option<&SvoChunk> {
self.access_counter += 1;
if let Some(entry) = self.entries.get_mut(&chunk_id) {
entry.last_access = self.access_counter;
self.hits += 1;
Some(&entry.chunk)
} else {
self.misses += 1;
None
}
}
pub fn insert(&mut self, chunk: SvoChunk) {
let chunk_id = chunk.chunk_id;
let chunk_mem = chunk.memory_bytes();
while self.needs_eviction(chunk_mem) {
if !self.evict_lru() {
break; }
}
self.access_counter += 1;
self.memory_used += chunk_mem;
self.entries.insert(
chunk_id,
CacheEntry {
chunk,
last_access: self.access_counter,
},
);
}
fn needs_eviction(&self, additional_bytes: usize) -> bool {
if self.entries.len() >= self.capacity {
return true;
}
if self.max_memory > 0 && self.memory_used + additional_bytes > self.max_memory {
return true;
}
false
}
fn evict_lru(&mut self) -> bool {
if self.entries.is_empty() {
return false;
}
let lru_id = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_access)
.map(|(&id, _)| id);
if let Some(id) = lru_id {
if let Some(entry) = self.entries.remove(&id) {
self.memory_used = self.memory_used.saturating_sub(entry.chunk.memory_bytes());
}
true
} else {
false
}
}
pub fn remove(&mut self, chunk_id: u32) -> Option<SvoChunk> {
if let Some(entry) = self.entries.remove(&chunk_id) {
self.memory_used = self.memory_used.saturating_sub(entry.chunk.memory_bytes());
Some(entry.chunk)
} else {
None
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.memory_used = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub const fn memory_used(&self) -> usize {
self.memory_used
}
pub fn hit_rate(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
pub fn split_into_chunks(svo: &SparseVoxelOctree, split_depth: u32) -> Vec<SvoChunk> {
if svo.nodes.is_empty() {
return Vec::new();
}
let mut chunks = Vec::new();
let mut chunk_id = 0u32;
struct BfsEntry {
node_idx: usize,
depth: u32,
center: Vec3,
half_size: Vec3,
}
let center = svo.bounds.center();
let half_size = svo.bounds.half_size();
let mut queue: std::collections::VecDeque<BfsEntry> = std::collections::VecDeque::new();
queue.push_back(BfsEntry {
node_idx: 0,
depth: 0,
center,
half_size,
});
let mut root_nodes = Vec::new();
while let Some(entry) = queue.pop_front() {
if entry.node_idx >= svo.nodes.len() {
continue;
}
let node = &svo.nodes[entry.node_idx];
if entry.depth < split_depth && node.is_leaf == 0 {
root_nodes.push(*node);
for octant in 0..8u8 {
if let Some(child_idx) = node.child_index(octant) {
let child_c = super::child_center(entry.center, entry.half_size, octant);
queue.push_back(BfsEntry {
node_idx: child_idx as usize,
depth: entry.depth + 1,
center: child_c,
half_size: entry.half_size * 0.5,
});
}
}
} else {
let subtree = extract_subtree(svo, entry.node_idx);
let bounds = AabbPacked::new(
entry.center - entry.half_size,
entry.center + entry.half_size,
);
chunks.push(SvoChunk::new(chunk_id, subtree, bounds, entry.depth));
chunk_id += 1;
}
}
if !root_nodes.is_empty() {
chunks.insert(
0,
SvoChunk::new(
u32::MAX, root_nodes,
svo.bounds,
0,
),
);
}
chunks
}
fn extract_subtree(svo: &SparseVoxelOctree, root_idx: usize) -> Vec<SvoNode> {
if root_idx >= svo.nodes.len() {
return Vec::new();
}
let mut result = Vec::new();
let mut queue: std::collections::VecDeque<usize> = std::collections::VecDeque::new();
let mut old_to_new: std::collections::HashMap<usize, u32> = std::collections::HashMap::new();
queue.push_back(root_idx);
old_to_new.insert(root_idx, 0);
let mut next_new_idx = 1u32;
let mut bfs_order: Vec<usize> = Vec::new();
let mut temp_queue = queue.clone();
while let Some(idx) = temp_queue.pop_front() {
bfs_order.push(idx);
if idx < svo.nodes.len() {
let node = &svo.nodes[idx];
if node.is_leaf == 0 {
for octant in 0..8u8 {
if let Some(child_idx) = node.child_index(octant) {
let ci = child_idx as usize;
if let std::collections::hash_map::Entry::Vacant(e) = old_to_new.entry(ci) {
e.insert(next_new_idx);
next_new_idx += 1;
temp_queue.push_back(ci);
}
}
}
}
}
}
for &old_idx in &bfs_order {
if old_idx >= svo.nodes.len() {
continue;
}
let mut node = svo.nodes[old_idx];
if node.is_leaf == 0 {
let old_first = node.first_child as usize;
if let Some(&new_first) = old_to_new.get(&old_first) {
node.first_child = new_first;
}
}
result.push(node);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::svo::SvoBuildConfig;
use crate::types::SdfNode;
fn make_test_svo() -> SparseVoxelOctree {
let sphere = SdfNode::sphere(1.0);
let config = SvoBuildConfig {
max_depth: 4,
bounds_min: Vec3::splat(-2.0),
bounds_max: Vec3::splat(2.0),
use_compiled: false,
..Default::default()
};
super::super::build::build_svo(&sphere, &config)
}
#[test]
fn test_cache_basic() {
let mut cache = SvoStreamingCache::new(4);
let chunk = SvoChunk::new(0, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk);
assert_eq!(cache.len(), 1);
assert!(cache.get(0).is_some());
assert!(cache.get(1).is_none());
}
#[test]
fn test_cache_eviction() {
let mut cache = SvoStreamingCache::new(2);
for i in 0..3 {
let chunk = SvoChunk::new(i, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk);
}
assert_eq!(cache.len(), 2);
assert!(cache.get(0).is_none(), "Chunk 0 should have been evicted");
assert!(cache.get(1).is_some());
assert!(cache.get(2).is_some());
}
#[test]
fn test_cache_lru_order() {
let mut cache = SvoStreamingCache::new(2);
let chunk0 = SvoChunk::new(0, vec![SvoNode::default()], AabbPacked::empty(), 0);
let chunk1 = SvoChunk::new(1, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk0);
cache.insert(chunk1);
cache.get(0);
let chunk2 = SvoChunk::new(2, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk2);
assert!(
cache.get(0).is_some(),
"Chunk 0 was recently accessed, should still be cached"
);
assert!(cache.get(1).is_none(), "Chunk 1 should have been evicted");
assert!(cache.get(2).is_some());
}
#[test]
fn test_cache_hit_rate() {
let mut cache = SvoStreamingCache::new(4);
let chunk = SvoChunk::new(0, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk);
cache.get(0); cache.get(0); cache.get(1);
assert!((cache.hit_rate() - 2.0 / 3.0).abs() < 0.01);
}
#[test]
fn test_chunk_serialization() {
let nodes = vec![
SvoNode::leaf(1.5, Vec3::new(0.0, 1.0, 0.0)),
SvoNode::interior(0.5, 0b00110101, 42),
];
let chunk = SvoChunk::new(7, nodes, AabbPacked::new(Vec3::ZERO, Vec3::ONE), 3);
let bytes = chunk.to_bytes();
let restored = SvoChunk::from_bytes(&bytes).expect("Should deserialize");
assert_eq!(restored.chunk_id, 7);
assert_eq!(restored.start_depth, 3);
assert_eq!(restored.nodes.len(), 2);
assert_eq!(restored.nodes[0].distance, 1.5);
assert_eq!(restored.nodes[0].ny, 1.0);
assert_eq!(restored.nodes[1].child_mask, 0b00110101);
assert_eq!(restored.nodes[1].first_child, 42);
}
#[test]
fn test_split_into_chunks() {
let svo = make_test_svo();
let chunks = split_into_chunks(&svo, 2);
assert!(chunks.len() > 1, "Should produce multiple chunks");
let total_chunk_nodes: usize = chunks.iter().map(|c| c.nodes.len()).sum();
assert!(total_chunk_nodes > 0);
}
#[test]
fn test_cache_memory_budget() {
let mut cache = SvoStreamingCache::with_memory_budget(128);
for i in 0..10 {
let chunk = SvoChunk::new(i, vec![SvoNode::default()], AabbPacked::empty(), 0);
cache.insert(chunk);
}
assert!(
cache.memory_used() <= 128 + 32,
"Memory should be near budget"
);
}
#[test]
fn test_cache_clear() {
let mut cache = SvoStreamingCache::new(4);
for i in 0..3 {
cache.insert(SvoChunk::new(
i,
vec![SvoNode::default()],
AabbPacked::empty(),
0,
));
}
cache.clear();
assert_eq!(cache.len(), 0);
assert_eq!(cache.memory_used(), 0);
}
}