Skip to main content

steel_worldgen/noise/
preliminary_surface_store.rs

1//! share preliminary surface levels across chunks
2//!
3//! quart-aligned lookups agree on cached and raw paths, so neighbors can reuse them
4
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use crate::density::DimensionNoises;
8use crate::noise::aquifer::preliminary_surface_level;
9
10/// 512 quarts per axis, enough for a 128-chunk span without slot collisions
11const AXIS_BITS: u32 = 9;
12const AXIS_MASK: i32 = (1 << AXIS_BITS) - 1;
13
14/// Packed slot layout: `valid (1) | quart_x (24) | quart_z (24) | level (15)`.
15const COORD_BITS: u32 = 24;
16const LEVEL_BITS: u32 = 15;
17const COORD_MASK: u64 = (1 << COORD_BITS) - 1;
18const LEVEL_MASK: u64 = (1 << LEVEL_BITS) - 1;
19const VALID: u64 = 1 << (u64::BITS - 1);
20const COORD_MIN: i32 = -(1 << (COORD_BITS - 1));
21const COORD_MAX: i32 = (1 << (COORD_BITS - 1)) - 1;
22const LEVEL_MIN: i32 = -(1 << (LEVEL_BITS - 1));
23const LEVEL_MAX: i32 = (1 << (LEVEL_BITS - 1)) - 1;
24
25/// shared surface levels, one atomic word per entry
26/// reads see a complete entry or miss; collisions just mean recomputing
27pub struct PreliminarySurfaceStore {
28    slots: Box<[AtomicU64]>,
29}
30
31impl Default for PreliminarySurfaceStore {
32    fn default() -> Self {
33        Self {
34            slots: (0..1usize << (2 * AXIS_BITS))
35                .map(|_| AtomicU64::new(0))
36                .collect(),
37        }
38    }
39}
40
41impl PreliminarySurfaceStore {
42    /// Preliminary surface level of the quart column containing block
43    /// `(x, z)`, evaluated through `cache` on a miss.
44    #[inline]
45    pub fn level<N: DimensionNoises>(
46        &self,
47        noises: &N,
48        cache: &mut N::ColumnCache,
49        x: i32,
50        z: i32,
51    ) -> i32 {
52        let quart_x = x >> 2;
53        let quart_z = z >> 2;
54        let Some(tag) = Self::tag(quart_x, quart_z) else {
55            return preliminary_surface_level(noises, cache, x, z);
56        };
57
58        let slot =
59            &self.slots[(((quart_x & AXIS_MASK) << AXIS_BITS) | (quart_z & AXIS_MASK)) as usize];
60
61        let packed = slot.load(Ordering::Relaxed);
62        if packed & !LEVEL_MASK == tag {
63            return unpack_level(packed);
64        }
65
66        let level = preliminary_surface_level(noises, cache, x, z);
67        if let Some(level_bits) = pack_level(level) {
68            slot.store(tag | level_bits, Ordering::Relaxed);
69        }
70
71        level
72    }
73
74    /// Valid bit plus both coordinates, or `None` if a coordinate does not fit.
75    #[inline]
76    fn tag(quart_x: i32, quart_z: i32) -> Option<u64> {
77        if !(COORD_MIN..=COORD_MAX).contains(&quart_x)
78            || !(COORD_MIN..=COORD_MAX).contains(&quart_z)
79        {
80            return None;
81        }
82
83        Some(
84            VALID
85                | ((quart_x as u64 & COORD_MASK) << (COORD_BITS + LEVEL_BITS))
86                | ((quart_z as u64 & COORD_MASK) << LEVEL_BITS),
87        )
88    }
89}
90
91/// pack the level if it fits
92#[inline]
93fn pack_level(level: i32) -> Option<u64> {
94    (LEVEL_MIN..=LEVEL_MAX)
95        .contains(&level)
96        .then_some(level as u64 & LEVEL_MASK)
97}
98
99/// unpack the level, keeping its sign
100#[inline]
101const fn unpack_level(packed: u64) -> i32 {
102    (((packed & LEVEL_MASK) as i32) << (i32::BITS - LEVEL_BITS)) >> (i32::BITS - LEVEL_BITS)
103}
104
105#[cfg(test)]
106mod tests {
107    use steel_utils::random::{Random, xoroshiro::Xoroshiro};
108
109    use super::{
110        AXIS_BITS, LEVEL_MAX, LEVEL_MIN, PreliminarySurfaceStore, pack_level, unpack_level,
111    };
112    use crate::density_functions::overworld::{OverworldColumnCache, OverworldNoises};
113    use crate::noise::aquifer::preliminary_surface_level;
114    use crate::noise_parameters::get_noise_parameters;
115
116    #[test]
117    fn packed_levels_round_trip_through_colliding_slots() {
118        let seed = 1;
119        let splitter = Xoroshiro::from_seed(seed).next_positional();
120        let noises = OverworldNoises::create(seed, &splitter, &get_noise_parameters());
121        let store = PreliminarySurfaceStore::default();
122        let mut cache = OverworldColumnCache::default();
123
124        // Same slot, `1 << AXIS_BITS` quarts apart on each axis.
125        let wrap = 4 << AXIS_BITS;
126        let mut columns = vec![(0, 0), (wrap, 0), (0, -wrap), (-wrap, wrap)];
127        // Negative and unaligned block coordinates.
128        for x in (-3_000..3_000).step_by(373) {
129            for z in (-3_000..3_000).step_by(419) {
130                columns.push((x, z));
131            }
132        }
133
134        // second pass hits unless another column took the slot
135        for _ in 0..2 {
136            for &(x, z) in &columns {
137                let expected = preliminary_surface_level(&noises, &mut cache, x, z);
138                assert_eq!(
139                    store.level(&noises, &mut cache, x, z),
140                    expected,
141                    "column ({x}, {z})"
142                );
143            }
144        }
145    }
146
147    #[test]
148    fn levels_keep_sign_and_reject_out_of_range() {
149        for level in [LEVEL_MIN, -64, -1, 0, 63, 320, LEVEL_MAX] {
150            let packed = pack_level(level).expect("in range");
151            assert_eq!(unpack_level(packed), level);
152        }
153        assert_eq!(pack_level(LEVEL_MIN - 1), None);
154        assert_eq!(pack_level(LEVEL_MAX + 1), None);
155    }
156}