Skip to main content

nucleation/selection/
visited.rs

1//! Sparse-dense block-coordinate set used as the flood's visited marker.
2//!
3//! Ported from RedstoneTools' `That.kt#BlockSet`: a `HashMap` keyed by chunk
4//! coordinate, mapping to a fixed 16×16×16 bitset (4096 bits = 64 u64).
5//! Sparse where the world is sparse, dense where the flood actually walks.
6
7use std::collections::HashMap;
8
9const CHUNK_BITS: u32 = 4;
10const CHUNK_SIDE: i32 = 1 << CHUNK_BITS; // 16
11const CHUNK_MASK: i32 = CHUNK_SIDE - 1; // 15
12const CHUNK_VOLUME: usize = 1 << (CHUNK_BITS * 3); // 4096
13const WORDS_PER_CHUNK: usize = CHUNK_VOLUME / 64; // 64
14
15type ChunkKey = (i32, i32, i32);
16
17/// A sparse 3D bitset over `i32` block coordinates.
18///
19/// Memory: one 512-byte chunk per touched 16×16×16 area plus the hashmap entry.
20/// A flood that visits N blocks touches at most ⌈N/4096⌉ + boundary chunks.
21#[derive(Default)]
22pub struct VisitedSet {
23    chunks: HashMap<ChunkKey, [u64; WORDS_PER_CHUNK]>,
24}
25
26impl VisitedSet {
27    pub fn new() -> Self {
28        Self {
29            chunks: HashMap::new(),
30        }
31    }
32
33    /// Approximate live memory footprint, in bytes. Useful for diagnostics.
34    pub fn approx_bytes(&self) -> usize {
35        // each entry: key (12B) + bitset (512B); ignore hashmap bookkeeping
36        self.chunks.len() * (std::mem::size_of::<ChunkKey>() + WORDS_PER_CHUNK * 8)
37    }
38
39    /// Number of chunks touched. For diagnostics / tuning.
40    pub fn chunk_count(&self) -> usize {
41        self.chunks.len()
42    }
43
44    /// True if `(x, y, z)` has been inserted.
45    #[inline]
46    pub fn contains(&self, x: i32, y: i32, z: i32) -> bool {
47        let key = chunk_key(x, y, z);
48        match self.chunks.get(&key) {
49            Some(bits) => {
50                let (word, bit) = index_in_chunk(x, y, z);
51                (bits[word] >> bit) & 1 != 0
52            }
53            None => false,
54        }
55    }
56
57    /// Insert `(x, y, z)`. Returns `true` if the position was not already set.
58    #[inline]
59    pub fn insert(&mut self, x: i32, y: i32, z: i32) -> bool {
60        let key = chunk_key(x, y, z);
61        let chunk = self
62            .chunks
63            .entry(key)
64            .or_insert_with(|| [0u64; WORDS_PER_CHUNK]);
65        let (word, bit) = index_in_chunk(x, y, z);
66        let mask = 1u64 << bit;
67        let was = chunk[word] & mask != 0;
68        chunk[word] |= mask;
69        !was
70    }
71}
72
73#[inline]
74fn chunk_key(x: i32, y: i32, z: i32) -> ChunkKey {
75    // arithmetic shift gives correct floor-div behaviour for negatives in
76    // two's complement, which is what we want for chunk coordinates.
77    (x >> CHUNK_BITS, y >> CHUNK_BITS, z >> CHUNK_BITS)
78}
79
80#[inline]
81fn index_in_chunk(x: i32, y: i32, z: i32) -> (usize, u32) {
82    let lx = (x & CHUNK_MASK) as u32;
83    let ly = (y & CHUNK_MASK) as u32;
84    let lz = (z & CHUNK_MASK) as u32;
85    // 12-bit linear index inside the 16³ chunk: y high, z mid, x low.
86    // Layout doesn't matter for correctness as long as it's a bijection;
87    // chosen so that x-major scans stay in the same 64-bit word.
88    let linear = (ly << 8) | (lz << 4) | lx;
89    let word = (linear >> 6) as usize; // /64
90    let bit = linear & 63;
91    (word, bit)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn insert_returns_true_only_first_time() {
100        let mut s = VisitedSet::new();
101        assert!(s.insert(0, 0, 0));
102        assert!(!s.insert(0, 0, 0));
103        assert!(s.insert(1, 0, 0));
104    }
105
106    #[test]
107    fn contains_matches_insert() {
108        let mut s = VisitedSet::new();
109        for &p in &[(0, 0, 0), (5, 6, 7), (-1, -2, -3), (1023, 0, -512)] {
110            assert!(!s.contains(p.0, p.1, p.2));
111            s.insert(p.0, p.1, p.2);
112            assert!(s.contains(p.0, p.1, p.2));
113        }
114        // a neighbour should still be unset
115        assert!(!s.contains(1, 0, 0));
116    }
117
118    #[test]
119    fn negative_coordinates_share_correct_chunk() {
120        // (-1, -1, -1) lives in chunk (-1, -1, -1), local (15, 15, 15)
121        let mut s = VisitedSet::new();
122        s.insert(-1, -1, -1);
123        assert!(s.contains(-1, -1, -1));
124        // and (-16, -16, -16) is the corner of chunk (-1, -1, -1) too
125        s.insert(-16, -16, -16);
126        assert!(s.contains(-16, -16, -16));
127        // both are in the same chunk
128        assert_eq!(s.chunk_count(), 1);
129        // (-17, ...) crosses the boundary
130        s.insert(-17, -1, -1);
131        assert_eq!(s.chunk_count(), 2);
132    }
133
134    #[test]
135    fn distinct_chunks_dont_alias() {
136        // Two positions with the same `lx,ly,lz` but in different chunks must
137        // not collide.
138        let mut s = VisitedSet::new();
139        s.insert(0, 0, 0);
140        assert!(!s.contains(16, 0, 0));
141        assert!(!s.contains(0, 16, 0));
142        assert!(!s.contains(0, 0, 16));
143    }
144
145    #[test]
146    fn fills_a_chunk_completely() {
147        let mut s = VisitedSet::new();
148        for y in 0..16 {
149            for z in 0..16 {
150                for x in 0..16 {
151                    assert!(s.insert(x, y, z));
152                }
153            }
154        }
155        assert_eq!(s.chunk_count(), 1);
156        for y in 0..16 {
157            for z in 0..16 {
158                for x in 0..16 {
159                    assert!(s.contains(x, y, z));
160                }
161            }
162        }
163        // re-insert is a no-op
164        for y in 0..16 {
165            for z in 0..16 {
166                for x in 0..16 {
167                    assert!(!s.insert(x, y, z));
168                }
169            }
170        }
171    }
172}