Worldgen: build the rotation table without cloning every block state six times

StateProviderBindings::rotations' table (a coral feature's decode builds
it, while the world's decoration is prepared) cloned each of the
registry's ~30k states once per direction and looked each clone up by
hash, under a BTreeMap keyed by cloned block names: ~230 ms of the world
data preload, on the path to the world's open. A state with no full axis
or facing family now rotates to itself without a clone, as does a
rotation that sets the value the state already has; the map borrows its
keys. The table is the same.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
VaspyyyandClaude Opus 5.5 committed 2026-11-02 07:34:03 +01:00
1 parent 435730a405
commit cbaf3287f6
1 file changed
+120 -18
@@ -551,37 +551,52 @@ pub(crate) const MAX_TAG_BLOCKS: usize = 1 << 20;
/// families. A five-way facing or two-axis property is a different property and
/// is intentionally left unchanged. Unrelated properties retain their values.
fn rotation_table(registry: &BlockRegistry) -> Result<Rotations, String> {
let mut values = BTreeMap::<(String, String), BTreeSet<String>>::new();
let mut values = BTreeMap::<(&str, &str), BTreeSet<&str>>::new();
for index in 0..registry.len() {
let state = registry.state(BlockStateId(index as u32)).unwrap();
for name in ["axis", "facing"] {
if let Some(value) = state.properties.get(name) {
values
.entry((state.name.clone(), name.into()))
.entry((state.name.as_str(), name))
.or_default()
.insert(value.clone());
.insert(value.as_str());
}
}
}
let axis = ["x", "y", "z"].map(str::to_owned).into_iter().collect();
let facing = DIRECTIONS.map(str::to_owned).into_iter().collect();
let horizontal = DIRECTIONS[2..].iter().map(|s| (*s).to_owned()).collect();
let axis: BTreeSet<&str> = ["x", "y", "z"].into_iter().collect();
let facing: BTreeSet<&str> = DIRECTIONS.into_iter().collect();
let horizontal: BTreeSet<&str> = DIRECTIONS[2..].iter().copied().collect();
let mut result = Vec::with_capacity(registry.len());
for index in 0..registry.len() {
let state = registry.state(BlockStateId(index as u32)).unwrap();
let full_axis = values.get(&(state.name.clone(), "axis".into())) == Some(&axis);
let directions = values.get(&(state.name.clone(), "facing".into()));
let mut rotated = [BlockStateId(index as u32); 6];
let id = BlockStateId(index as u32);
let state = registry.state(id).unwrap();
let full_axis = values.get(&(state.name.as_str(), "axis")) == Some(&axis);
let directions = values.get(&(state.name.as_str(), "facing"));
let mut rotated = [id; 6];
// Most states have neither family: every rotation is the state itself.
if !full_axis && directions != Some(&facing) && directions != Some(&horizontal) {
result.push(rotated);
continue;
}
for (direction, name) in DIRECTIONS.iter().enumerate() {
let mut value = state.clone();
if full_axis {
value.properties.insert(
"axis".into(),
["y", "y", "z", "z", "x", "x"][direction].into(),
);
let axis_value = full_axis.then_some(["y", "y", "z", "z", "x", "x"][direction]);
let facing_value = (directions == Some(&facing)
|| (direction >= 2 && directions == Some(&horizontal)))
.then_some(*name);
// A rotation that sets what the state already has is the state.
let unchanged = axis_value
.is_none_or(|v| state.properties.get("axis").map(String::as_str) == Some(v))
&& facing_value
.is_none_or(|v| state.properties.get("facing").map(String::as_str) == Some(v));
if unchanged {
continue;
}
if directions == Some(&facing) || (direction >= 2 && directions == Some(&horizontal)) {
value.properties.insert("facing".into(), (*name).into());
let mut value = state.clone();
if let Some(v) = axis_value {
value.properties.insert("axis".into(), v.into());
}
if let Some(v) = facing_value {
value.properties.insert("facing".into(), v.into());
}
rotated[direction] = registry
.id(&value)
@@ -591,3 +606,90 @@ fn rotation_table(registry: &BlockRegistry) -> Result<Rotations, String> {
}
Ok(Arc::new(result))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nbt::Tag;
/// air, stone, oak_log axis x/y/z, furnace facing the four horizontal
/// ways, observer facing all six with `powered` false then true, and a
/// hopper's five-way facing.
fn registry() -> BlockRegistry {
let state = |id: &str, props: &[(&str, &str)]| {
let mut entries = vec![("id", Tag::String(id.into()))];
if !props.is_empty() {
entries.push((
"properties",
Tag::compound(
props
.iter()
.map(|(k, v)| (*k, Tag::String((*v).into())))
.collect::<Vec<_>>(),
),
));
}
Tag::compound(entries)
};
let mut states = vec![state("minecraft:air", &[]), state("minecraft:stone", &[])];
for axis in ["x", "y", "z"] {
states.push(state("minecraft:oak_log", &[("axis", axis)]));
}
for facing in ["north", "south", "west", "east"] {
states.push(state("minecraft:furnace", &[("facing", facing)]));
}
for powered in ["false", "true"] {
for facing in DIRECTIONS {
states.push(state(
"minecraft:observer",
&[("facing", facing), ("powered", powered)],
));
}
}
for facing in ["down", "north", "south", "west", "east"] {
states.push(state("minecraft:hopper", &[("facing", facing)]));
}
BlockRegistry::from_nbt(&Tag::compound([
("DataVersion", Tag::Int(crate::DATA_VERSION)),
("states", Tag::List(states)),
(
"defaults",
Tag::compound([
("minecraft:air", Tag::Int(0)),
("minecraft:stone", Tag::Int(1)),
("minecraft:oak_log", Tag::Int(3)),
("minecraft:furnace", Tag::Int(5)),
("minecraft:observer", Tag::Int(9)),
("minecraft:hopper", Tag::Int(21)),
]),
),
]))
.unwrap()
}
/// Rotations follow the direction (down, up, north, south, west, east):
/// a full axis turns, horizontal facing turns only for the four sides,
/// full facing turns all six ways keeping its other properties, and a
/// five-way facing or a block without these properties stays as it is.
#[test]
fn rotations_turn_axis_and_facing_families_only() {
let registry = registry();
let table = rotation_table(&registry).unwrap();
let ids = |state: u32| table[state as usize].map(|id| id.0);
assert_eq!(ids(1), [1; 6], "stone");
assert_eq!(ids(3), [3, 3, 4, 4, 2, 2], "oak_log axis=y");
assert_eq!(ids(2), [3, 3, 4, 4, 2, 2], "oak_log axis=x");
assert_eq!(ids(6), [6, 6, 5, 6, 7, 8], "furnace facing=south");
assert_eq!(
ids(16),
[15, 16, 17, 18, 19, 20],
"observer powered=true facing=up"
);
assert_eq!(
ids(9),
[9, 10, 11, 12, 13, 14],
"observer powered=false facing=down"
);
assert_eq!(ids(22), [22; 6], "hopper facing=north");
}
}