Skip to main content

steel_core/java_block_pos_set/
mod.rs

1//! Shared Java `HashSet<BlockPos>` bucket and tree-bin ordering.
2//!
3//! Distinct hashes follow `OpenJDK` 25's linked tree-bin order. `BlockPos` inherits
4//! `Comparable<Vec3i>`, so equal-hash ties use JVM identities. Steel preserves its
5//! existing deterministic policies: coordinate ordering for worldgen, and insertion
6//! indices for explosions (matching increasing JVM identities).
7
8use std::iter::from_fn;
9use std::mem;
10use std::vec::IntoIter;
11
12use rustc_hash::FxHashMap;
13use steel_utils::BlockPos;
14
15mod removal;
16#[cfg(test)]
17mod tests;
18mod tree;
19#[cfg(test)]
20mod worldgen_tests;
21
22const EMPTY: u32 = u32::MAX;
23const TREEIFY_THRESHOLD: usize = 8;
24const UNTREEIFY_THRESHOLD: usize = 6;
25const MIN_TREEIFY_CAPACITY: usize = 64;
26
27#[derive(Default)]
28pub(crate) enum EqualHashOrder {
29    #[default]
30    Coordinates,
31    Insertion,
32}
33
34/// Java bucket ordering with a deterministic policy for equal-hash tree entries.
35#[derive(Default)]
36pub(crate) struct JavaBlockPosSet {
37    buckets: Vec<Bucket>,
38    entries: Vec<Entry>,
39    // Allocate tree links only for entries that have actually entered a tree bin.
40    trees: FxHashMap<u32, tree::Links>,
41    len: usize,
42    equal_hash_order: EqualHashOrder,
43}
44
45#[derive(Clone, Copy)]
46struct Bucket {
47    head: u32,
48    // EMPTY marks a tree bin; empty list bins use a dummy tail and an EMPTY head.
49    tail: u32,
50}
51
52impl Bucket {
53    const EMPTY: Self = Self {
54        head: EMPTY,
55        tail: 0,
56    };
57
58    const fn is_tree(self) -> bool {
59        self.tail == EMPTY
60    }
61}
62
63struct Entry {
64    pos: BlockPos,
65    next: u32,
66}
67
68impl JavaBlockPosSet {
69    pub(crate) fn new(equal_hash_order: EqualHashOrder) -> Self {
70        Self {
71            equal_hash_order,
72            ..Self::default()
73        }
74    }
75
76    #[inline]
77    pub(crate) fn insert(&mut self, pos: BlockPos) -> bool {
78        if self.buckets.is_empty() {
79            self.buckets.resize(16, Bucket::EMPTY);
80            self.entries.reserve(16);
81        }
82        let hash = spread(pos);
83        let index = hash as u32 as usize & (self.buckets.len() - 1);
84        let bucket = self.buckets[index];
85        let mut bin_len = 0;
86        if bucket.is_tree() {
87            if self.find_in_tree(self.tree_root(bucket.head), hash, pos) {
88                return false;
89            }
90        } else {
91            let mut current = bucket.head;
92            while current != EMPTY {
93                let entry = &self.entries[current as usize];
94                if entry.pos == pos {
95                    return false;
96                }
97                current = entry.next;
98                bin_len += 1;
99            }
100        }
101
102        let Ok(entry_index) = u32::try_from(self.entries.len()) else {
103            panic!("JavaBlockPosSet entry arena exceeded its u32 index space");
104        };
105        assert_ne!(
106            entry_index, EMPTY,
107            "JavaBlockPosSet entry arena exhausted its u32 index space"
108        );
109        self.entries.push(Entry { pos, next: EMPTY });
110        self.len += 1;
111        if bucket.is_tree() {
112            self.insert_tree(index, entry_index);
113        } else {
114            self.append(index, entry_index);
115            // Java treeifies when adding the ninth bin entry.
116            if bin_len >= TREEIFY_THRESHOLD {
117                if self.buckets.len() < MIN_TREEIFY_CAPACITY {
118                    self.resize();
119                } else {
120                    self.treeify(index);
121                }
122            }
123        }
124        if self.len > self.buckets.len() * 3 / 4 {
125            self.resize();
126        }
127        true
128    }
129
130    pub(crate) fn contains(&self, pos: BlockPos) -> bool {
131        if self.buckets.is_empty() {
132            return false;
133        }
134        let hash = spread(pos);
135        let bucket = self.buckets[hash as u32 as usize & (self.buckets.len() - 1)];
136        if bucket.is_tree() {
137            return self.find_in_tree(self.tree_root(bucket.head), hash, pos);
138        }
139        let mut current = bucket.head;
140        while current != EMPTY {
141            let entry = &self.entries[current as usize];
142            if entry.pos == pos {
143                return true;
144            }
145            current = entry.next;
146        }
147        false
148    }
149
150    pub(crate) const fn is_empty(&self) -> bool {
151        self.len == 0
152    }
153
154    pub(crate) fn java_order(&self) -> impl Iterator<Item = &BlockPos> {
155        self.buckets.iter().flat_map(move |bucket| {
156            let mut current = bucket.head;
157            from_fn(move || {
158                if current == EMPTY {
159                    return None;
160                }
161                let entry = &self.entries[current as usize];
162                current = entry.next;
163                Some(&entry.pos)
164            })
165        })
166    }
167
168    pub(crate) fn java_ordered_positions(&self) -> Vec<BlockPos> {
169        self.java_order().copied().collect()
170    }
171
172    pub(crate) fn pop_java_ordered_position(&mut self) -> Option<BlockPos> {
173        let index = self
174            .buckets
175            .iter()
176            .position(|bucket| bucket.head != EMPTY)?;
177        let bucket = self.buckets[index];
178        let node = bucket.head;
179        let entry = &self.entries[node as usize];
180        let pos = entry.pos;
181        self.buckets[index].head = entry.next;
182        self.len -= 1;
183        if self.buckets[index].head == EMPTY {
184            self.buckets[index].tail = 0;
185        } else if bucket.is_tree() {
186            let next = self.buckets[index].head;
187            self.links_mut(next).prev = EMPTY;
188            // Iterator removal preserves linked order rather than moving the tree root.
189            self.remove_from_tree(node);
190        }
191        self.trees.remove(&node);
192        Some(pos)
193    }
194
195    #[cfg(test)]
196    pub(crate) const fn bucket_count(&self) -> usize {
197        self.buckets.len()
198    }
199
200    fn append(&mut self, bucket_index: usize, entry_index: u32) {
201        debug_assert!(!self.buckets[bucket_index].is_tree());
202        let bucket = &mut self.buckets[bucket_index];
203        self.entries[entry_index as usize].next = EMPTY;
204        if bucket.head == EMPTY {
205            bucket.head = entry_index;
206        } else {
207            self.entries[bucket.tail as usize].next = entry_index;
208        }
209        bucket.tail = entry_index;
210    }
211
212    fn resize(&mut self) {
213        let old_capacity = self.buckets.len();
214        // HashMap stops growing at MAXIMUM_CAPACITY.
215        if old_capacity >= 1 << 30 {
216            return;
217        }
218        let old_buckets = mem::replace(&mut self.buckets, vec![Bucket::EMPTY; old_capacity * 2]);
219        for (index, bucket) in old_buckets.into_iter().enumerate() {
220            if bucket.is_tree() {
221                self.split_tree(index, bucket.head, old_capacity);
222                continue;
223            }
224            let mut current = bucket.head;
225            while current != EMPTY {
226                let next = self.entries[current as usize].next;
227                let target = spread(self.entries[current as usize].pos) as u32 as usize
228                    & (self.buckets.len() - 1);
229                self.append(target, current);
230                current = next;
231            }
232        }
233    }
234}
235
236impl IntoIterator for JavaBlockPosSet {
237    type Item = BlockPos;
238    type IntoIter = IntoIter<BlockPos>;
239
240    fn into_iter(self) -> Self::IntoIter {
241        let mut ordered = Vec::with_capacity(self.len);
242        for bucket in self.buckets {
243            let mut current = bucket.head;
244            while current != EMPTY {
245                let entry = &self.entries[current as usize];
246                ordered.push(entry.pos);
247                current = entry.next;
248            }
249        }
250        ordered.into_iter()
251    }
252}
253
254const fn spread(pos: BlockPos) -> i32 {
255    let hash = pos.java_hash_code() as u32;
256    (hash ^ (hash >> 16)) as i32
257}