Skip to main content

steel_worldgen/noise/
corner_column_store.rs

1//! share boundary corner columns with neighboring chunks
2//!
3//! quart-aligned corners agree on cached and raw paths, so reuse is exact
4//! entries leave once all sharing chunks have used them; a size cap handles stragglers
5
6use rustc_hash::{FxBuildHasher, FxHashMap};
7use std::hash::BuildHasher;
8use steel_utils::locks::SyncMutex;
9
10const SHARD_COUNT: usize = 64;
11/// Upper bound on stored values across all shards (~32 MiB of `f32`).
12const MAX_STORED_VALUES: usize = 8 * 1024 * 1024;
13
14struct Column {
15    values: Box<[f32]>,
16    /// Chunks sharing this column that have not yet filled it.
17    remaining: u8,
18}
19
20#[derive(Default)]
21struct Shard {
22    columns: FxHashMap<(i32, i32), Column>,
23    stored_values: usize,
24}
25
26/// Generator-wide store of boundary corner columns, keyed by cell column.
27pub struct CornerColumnStore {
28    shards: Box<[SyncMutex<Shard>]>,
29}
30
31impl Default for CornerColumnStore {
32    fn default() -> Self {
33        Self {
34            shards: (0..SHARD_COUNT)
35                .map(|_| SyncMutex::new(Shard::default()))
36                .collect(),
37        }
38    }
39}
40
41impl CornerColumnStore {
42    fn shard(&self, key: (i32, i32)) -> &SyncMutex<Shard> {
43        &self.shards[FxBuildHasher.hash_one(key) as usize % SHARD_COUNT]
44    }
45
46    /// read a shared column and count this chunk's use; false on a miss
47    pub fn consume(&self, key: (i32, i32), read: impl FnOnce(&[f32])) -> bool {
48        let mut shard = self.shard(key).lock();
49        let Some(column) = shard.columns.get_mut(&key) else {
50            return false;
51        };
52
53        read(&column.values);
54        column.remaining -= 1;
55        if column.remaining == 0 {
56            shard.remove(key);
57        }
58
59        true
60    }
61
62    /// keep a column for the other chunk a race just counts this chunk use
63    pub fn produce(&self, key: (i32, i32), users: u8, values: impl FnOnce() -> Box<[f32]>) {
64        debug_assert!(users > 1, "only shared columns are stored");
65        let mut shard = self.shard(key).lock();
66        if let Some(column) = shard.columns.get_mut(&key) {
67            column.remaining -= 1;
68            if column.remaining == 0 {
69                shard.remove(key);
70            }
71
72            return;
73        }
74
75        let values = values();
76        if shard.stored_values + values.len() > MAX_STORED_VALUES / SHARD_COUNT {
77            shard.columns.clear();
78            shard.stored_values = 0;
79        }
80
81        shard.stored_values += values.len();
82        shard.columns.insert(
83            key,
84            Column {
85                values,
86                remaining: users - 1,
87            },
88        );
89    }
90}
91
92impl Shard {
93    fn remove(&mut self, key: (i32, i32)) {
94        if let Some(column) = self.columns.remove(&key) {
95            self.stored_values -= column.values.len();
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::CornerColumnStore;
103
104    fn read(store: &CornerColumnStore, key: (i32, i32)) -> Option<Vec<f32>> {
105        let mut out = None;
106        store.consume(key, |values| out = Some(values.to_vec()));
107        out
108    }
109
110    #[test]
111    fn last_user_removes_the_column() {
112        let store = CornerColumnStore::default();
113        assert_eq!(read(&store, (0, 0)), None);
114
115        store.produce((0, 0), 2, || vec![1.0, 2.0].into());
116        assert_eq!(read(&store, (0, 0)), Some(vec![1.0, 2.0]));
117        assert_eq!(read(&store, (0, 0)), None);
118
119        // Chunk corner: shared by four chunks.
120        store.produce((4, 4), 4, || vec![3.0].into());
121        for _ in 0..3 {
122            assert_eq!(read(&store, (4, 4)), Some(vec![3.0]));
123        }
124        assert_eq!(read(&store, (4, 4)), None);
125    }
126
127    #[test]
128    fn concurrent_producers_count_as_uses() {
129        let store = CornerColumnStore::default();
130        // Both sharing chunks missed and computed the column themselves.
131        store.produce((0, 8), 2, || vec![1.0].into());
132        store.produce((0, 8), 2, || unreachable!("already stored"));
133        assert_eq!(read(&store, (0, 8)), None);
134    }
135}