Skip to main content

steel_core/worldgen/carver/
mask.rs

1//! Per-chunk bitset marking which block positions have already been visited
2//! by a carver.
3//!
4//! Mirrors vanilla's `net.minecraft.world.level.chunk.CarvingMask`. Used by
5//! `WorldCarver` to avoid repeatedly processing the same position when
6//! multiple carver steps overlap.
7
8use std::cell::RefCell;
9use std::mem;
10
11thread_local! {
12    static CARVING_MASK_WORDS: RefCell<Vec<u64>> = const { RefCell::new(Vec::new()) };
13}
14
15/// A `16 × height × 16` bitset of local block positions in a chunk.
16#[derive(Debug, Clone)]
17pub struct CarvingMask {
18    min_y: i32,
19    height: i32,
20    bits: Vec<u64>,
21}
22
23impl CarvingMask {
24    /// Creates an empty mask covering the inclusive range `[min_y, max_y]`.
25    ///
26    /// # Panics
27    ///
28    /// Panics if `min_y` is greater than `max_y`.
29    #[must_use]
30    pub fn new(min_y: i32, max_y: i32) -> Self {
31        assert!(min_y <= max_y, "carving mask range must not be empty");
32        let height = max_y - min_y + 1;
33        let total_bits = (256 * height) as usize;
34        let lanes = total_bits.div_ceil(64);
35        let mut bits = CARVING_MASK_WORDS.with(|cache| mem::take(&mut *cache.borrow_mut()));
36        bits.resize(lanes, 0);
37        Self {
38            min_y,
39            height,
40            bits,
41        }
42    }
43
44    /// Creates the range used by `NoiseBasedChunkGenerator.generateCarvers`.
45    ///
46    /// The bottom block and the top seven blocks are protected for normal
47    /// generation, matching vanilla's non-upgrading chunk path.
48    #[must_use]
49    pub fn for_worldgen_context(min_y: i32, height: i32) -> Self {
50        Self::new(min_y + 1, min_y + height - 8)
51    }
52
53    /// Vanilla's `getIndex`: `y - min_y + (z + (x << 4)) * height`.
54    #[inline]
55    const fn index(&self, x: i32, y: i32, z: i32) -> usize {
56        let column = ((z & 15) + ((x & 15) << 4)) as usize;
57        (y - self.min_y) as usize + column * self.height as usize
58    }
59
60    /// Marks `(x, y, z)` as carved.
61    #[inline]
62    pub fn set(&mut self, x: i32, y: i32, z: i32) {
63        let idx = self.index(x, y, z);
64        let lane = idx / 64;
65        let bit = idx % 64;
66        self.bits[lane] |= 1u64 << bit;
67    }
68
69    /// Marks `(x, y, z)` as carved if it was not already marked.
70    ///
71    /// Returns `true` when this call set the bit, or `false` when a previous
72    /// carver step had already visited the position.
73    #[inline]
74    pub fn set_if_unset(&mut self, x: i32, y: i32, z: i32) -> bool {
75        let idx = self.index(x, y, z);
76        let lane = idx / 64;
77        let bit = 1u64 << (idx % 64);
78        if self.bits[lane] & bit != 0 {
79            return false;
80        }
81        self.bits[lane] |= bit;
82        true
83    }
84
85    /// Returns whether `(x, y, z)` has been carved.
86    #[inline]
87    #[must_use]
88    pub fn get(&self, x: i32, y: i32, z: i32) -> bool {
89        let idx = self.index(x, y, z);
90        let lane = idx / 64;
91        let bit = idx % 64;
92        (self.bits[lane] >> bit) & 1 != 0
93    }
94
95    /// Returns whether no carver marked any position.
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.bits.iter().all(|&word| word == 0)
99    }
100
101    /// Visits the marked vertical ranges in vanilla's bitset order.
102    pub fn visit(&self, mut visitor: impl FnMut(i32, i32, i32, i32)) {
103        let Some(mut start_index) = self.next_set_bit(0) else {
104            return;
105        };
106
107        loop {
108            let end_index = self.next_clear_bit(start_index) - 1;
109            self.visit_segment(&mut visitor, start_index, end_index);
110
111            let Some(next_start) = self.next_set_bit(end_index + 1) else {
112                return;
113            };
114            start_index = next_start;
115        }
116    }
117
118    fn visit_segment(
119        &self,
120        visitor: &mut impl FnMut(i32, i32, i32, i32),
121        start_index: usize,
122        end_index: usize,
123    ) {
124        let height = self.height as usize;
125        for column in start_index / height..=end_index / height {
126            let column_base = column * height;
127            let bottom_y = (start_index.saturating_sub(column_base)).min(height - 1) as i32;
128            let top_y = (end_index.saturating_sub(column_base)).min(height - 1) as i32;
129            visitor(
130                ((column >> 4) & 15) as i32,
131                (column & 15) as i32,
132                self.min_y + bottom_y,
133                self.min_y + top_y,
134            );
135        }
136    }
137
138    fn next_set_bit(&self, from: usize) -> Option<usize> {
139        let mut lane = from / 64;
140        if lane >= self.bits.len() {
141            return None;
142        }
143        let mut word = self.bits[lane] & (u64::MAX << (from % 64));
144        loop {
145            if word != 0 {
146                return Some(lane * 64 + word.trailing_zeros() as usize);
147            }
148            lane += 1;
149            word = *self.bits.get(lane)?;
150        }
151    }
152
153    fn next_clear_bit(&self, from: usize) -> usize {
154        let mut lane = from / 64;
155        if lane >= self.bits.len() {
156            return from;
157        }
158        let mut word = !self.bits[lane] & (u64::MAX << (from % 64));
159        loop {
160            if word != 0 {
161                return lane * 64 + word.trailing_zeros() as usize;
162            }
163            lane += 1;
164            let Some(next_word) = self.bits.get(lane) else {
165                return self.bits.len() * 64;
166            };
167            word = !next_word;
168        }
169    }
170
171    /// Y range bound at construction.
172    #[must_use]
173    pub const fn min_y(&self) -> i32 {
174        self.min_y
175    }
176
177    /// Inclusive upper Y bound.
178    #[must_use]
179    pub const fn max_y(&self) -> i32 {
180        self.min_y + self.height - 1
181    }
182}
183
184impl Drop for CarvingMask {
185    fn drop(&mut self) {
186        CARVING_MASK_WORDS.with(|cache| {
187            let mut cached = cache.borrow_mut();
188            if self.bits.capacity() > cached.capacity() {
189                self.bits.clear();
190                mem::swap(&mut self.bits, &mut *cached);
191            }
192        });
193    }
194}
195
196#[cfg(test)]
197mod test {
198    use steel_worldgen::density_functions::{
199        nether::NetherNoiseSettings, overworld::OverworldNoiseSettings,
200    };
201
202    use super::*;
203
204    const OVERWORLD_CARVER_MIN_Y: i32 = OverworldNoiseSettings::MIN_Y + 1;
205    const OVERWORLD_CARVER_MAX_Y: i32 =
206        OverworldNoiseSettings::MIN_Y + OverworldNoiseSettings::HEIGHT - 8;
207    const OVERWORLD_CARVER_HEIGHT: usize = OverworldNoiseSettings::HEIGHT as usize - 8;
208
209    #[test]
210    fn set_and_get_roundtrip() {
211        let mut mask = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
212        assert!(!mask.get(5, 10, 7));
213        mask.set(5, 10, 7);
214        assert!(mask.get(5, 10, 7));
215        // Neighbors untouched
216        assert!(!mask.get(4, 10, 7));
217        assert!(!mask.get(5, 11, 7));
218        assert!(!mask.get(5, 10, 8));
219    }
220
221    #[test]
222    fn reused_mask_is_clear_across_height_ranges() {
223        let mut first = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
224        first.set(5, 10, 7);
225        drop(first);
226
227        let mut nether = CarvingMask::for_worldgen_context(
228            NetherNoiseSettings::MIN_Y,
229            NetherNoiseSettings::HEIGHT,
230        );
231        assert!(nether.is_empty());
232        nether.set(5, 10, 7);
233        drop(nether);
234
235        let reused = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
236        assert!(reused.is_empty());
237        assert!(!reused.get(5, 10, 7));
238    }
239
240    #[test]
241    fn set_if_unset_reports_first_visit() {
242        let mut mask = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
243        assert!(mask.set_if_unset(5, 10, 7));
244        assert!(!mask.set_if_unset(5, 10, 7));
245        assert!(mask.get(5, 10, 7));
246    }
247
248    #[test]
249    fn indexing_matches_vanilla_layout() {
250        let mask = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
251        // x=0, z=0, y=min_y → index 0
252        assert_eq!(mask.index(0, OVERWORLD_CARVER_MIN_Y, 0), 0);
253        // x=15, z=0, y=min_y → 240 columns × mask height.
254        assert_eq!(
255            mask.index(15, OVERWORLD_CARVER_MIN_Y, 0),
256            240 * OVERWORLD_CARVER_HEIGHT
257        );
258        // x=0, z=1, y=min_y → one column.
259        assert_eq!(
260            mask.index(0, OVERWORLD_CARVER_MIN_Y, 1),
261            OVERWORLD_CARVER_HEIGHT
262        );
263        // x=0, z=0, y=min_y+1 → next row in the column.
264        assert_eq!(mask.index(0, OVERWORLD_CARVER_MIN_Y + 1, 0), 1);
265    }
266
267    #[test]
268    fn x_and_z_are_masked_to_chunk_local() {
269        let mut mask = CarvingMask::new(OVERWORLD_CARVER_MIN_Y, OVERWORLD_CARVER_MAX_Y);
270        // Chunk-local: 17 → 1, 18 → 2
271        mask.set(17, 0, 18);
272        assert!(mask.get(1, 0, 2));
273        assert!(mask.get(17, 0, 18));
274    }
275
276    #[test]
277    fn worldgen_context_excludes_protected_edges() {
278        let mask = CarvingMask::for_worldgen_context(
279            OverworldNoiseSettings::MIN_Y,
280            OverworldNoiseSettings::HEIGHT,
281        );
282        assert_eq!(mask.min_y(), OverworldNoiseSettings::MIN_Y + 1);
283        assert_eq!(mask.max_y(), OVERWORLD_CARVER_MAX_Y);
284
285        let nether = CarvingMask::for_worldgen_context(
286            NetherNoiseSettings::MIN_Y,
287            NetherNoiseSettings::HEIGHT,
288        );
289        assert_eq!(nether.min_y(), NetherNoiseSettings::MIN_Y + 1);
290        assert_eq!(
291            nether.max_y(),
292            NetherNoiseSettings::MIN_Y + NetherNoiseSettings::HEIGHT - 8
293        );
294    }
295
296    #[test]
297    fn visit_splits_ranges_at_clear_bits_and_columns() {
298        let mut mask = CarvingMask::new(-4, 3);
299        mask.set(0, 3, 0);
300        mask.set(0, -4, 1);
301        mask.set(1, -3, 2);
302        mask.set(1, -2, 2);
303        mask.set(1, 0, 2);
304
305        let mut visited = Vec::new();
306        mask.visit(|x, z, bottom, top| visited.push((x, z, bottom, top)));
307
308        assert_eq!(
309            visited,
310            vec![(0, 0, 3, 3), (0, 1, -4, -4), (1, 2, -3, -2), (1, 2, 0, 0)]
311        );
312    }
313}