feat(chunk): improve chunk generation speed ~1.27x (#2847)

* feat(noise): Add benchmark for noise generation

* feat(noise): fix extra noise libm calls

* feat(noise): cache density fill

* feat(aquifer): add skip sampling for aquifer
This commit is contained in:
Bjorn Beishline
2026-08-09 03:04:28 -07:00
committed by GitHub
parent 3150bd13de
commit ff57b6e282
12 changed files with 327 additions and 224 deletions

1
Cargo.lock generated
View File

@@ -3424,6 +3424,7 @@ dependencies = [
"base64 0.23.1",
"bytes",
"colored",
"criterion",
"crypto-bigint 0.7.5",
"ecdsa 0.17.0",
"md5",

View File

@@ -32,9 +32,16 @@ sha2.workspace = true
crypto-bigint.workspace = true
ureq.workspace = true
[dev-dependencies]
criterion = { workspace = true, features = ["html_reports"] }
[features]
default = []
codegen = ["dep:syn", "dep:quote", "dep:proc-macro2"]
[[bench]]
name = "perlin"
harness = false
[lints]
workspace = true

View File

@@ -0,0 +1,54 @@
use criterion::{Criterion, criterion_group, criterion_main};
use pumpkin_util::noise::perlin::{OctavePerlinNoiseSampler, PerlinNoiseSampler};
use pumpkin_util::random::{RandomImpl, xoroshiro128::Xoroshiro};
use std::hint::black_box;
fn make_coords(count: usize) -> Vec<(f64, f64, f64)> {
let mut rand = Xoroshiro::from_seed(0x5EED_C0DE_1234_5678);
(0..count)
.map(|_| {
(
(rand.next_f64() - 0.5) * 200_000.0,
(rand.next_f64() - 0.5) * 4_000.0,
(rand.next_f64() - 0.5) * 200_000.0,
)
})
.collect()
}
fn bench_perlin_sample(c: &mut Criterion) {
let mut rand = Xoroshiro::from_seed(1);
let sampler = PerlinNoiseSampler::new(&mut rand);
let coords = make_coords(4096);
c.bench_function("perlin_sample_no_fade", |b| {
b.iter(|| {
let mut acc = 0.0f64;
for &(x, y, z) in &coords {
acc += black_box(sampler.sample_no_fade(black_box(x), y, z, 0.0, 0.0));
}
black_box(acc)
});
});
}
fn bench_octave_perlin_sample(c: &mut Criterion) {
let mut rand = Xoroshiro::from_seed(1);
let (start, amplitudes) =
OctavePerlinNoiseSampler::calculate_amplitudes(&(-4..=2).collect::<Vec<i32>>());
let sampler = OctavePerlinNoiseSampler::new(&mut rand, start, &amplitudes, false);
let coords = make_coords(4096);
c.bench_function("octave_perlin_sample", |b| {
b.iter(|| {
let mut acc = 0.0f64;
for &(x, y, z) in &coords {
acc += black_box(sampler.sample(black_box(x), y, z));
}
black_box(acc)
});
});
}
criterion_group!(benches, bench_perlin_sample, bench_octave_perlin_sample);
criterion_main!(benches);

View File

@@ -108,6 +108,10 @@ impl Gradient {
#[inline]
#[must_use]
pub const fn dot(&self, x: f64, y: f64, z: f64) -> f64 {
self.z.mul_add(z, self.x.mul_add(x, self.y * y))
// When using mul_add without target-feature=+fma, you get a huge performance cost
// because it lowers into a libm call 16x per Perlin sample.
//
// This improves performance by something crazy like 15%
self.x * x + self.y * y + self.z * z
}
}

View File

@@ -1035,4 +1035,63 @@ mod tests {
// assert_eq!(y, *expected_iter.next().unwrap());
// }
// }
/// Hashes raw f64 outputs from `PerlinNoiseSampler`/`OctavePerlinNoiseSampler`.
/// Performance changes to this file must not change any float value by a single
/// ULP. This test just helps confirm that there really hasn't been a change.
#[test]
fn perlin_and_octave_fingerprint_is_stable() {
fn fnv1a_hash_f64(values: impl Iterator<Item = f64>) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
let mut hash = FNV_OFFSET;
for value in values {
for byte in value.to_bits().to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
}
hash
}
let mut coord_rand = Xoroshiro::from_seed(0x5EED_C0DE_1234_5678);
let coords: Vec<(f64, f64, f64)> = (0..2000)
.map(|_| {
(
(coord_rand.next_f64() - 0.5) * 200_000.0,
(coord_rand.next_f64() - 0.5) * 4_000.0,
(coord_rand.next_f64() - 0.5) * 200_000.0,
)
})
.collect();
let mut results = Vec::new();
for seed in [1u64, 2, 42, 12345, 987_654_321] {
let mut rand = Xoroshiro::from_seed(seed);
let sampler = PerlinNoiseSampler::new(&mut rand);
for &(x, y, z) in &coords {
results.push(sampler.sample_no_fade(x, y, z, 0.0, 0.0));
results.push(sampler.sample_no_fade(x, y, z, 0.5, 100.0));
}
}
for seed in [7u64, 99, 555] {
for legacy in [false, true] {
let mut rand = Xoroshiro::from_seed(seed);
let (start, amplitudes) =
OctavePerlinNoiseSampler::calculate_amplitudes(&(-4..=2).collect::<Vec<i32>>());
let sampler = OctavePerlinNoiseSampler::new(&mut rand, start, &amplitudes, legacy);
for &(x, y, z) in coords.iter().take(500) {
results.push(sampler.sample(x, y, z));
}
}
}
let hash = fnv1a_hash_f64(results.into_iter());
assert_eq!(
hash, 0x2c36_e2de_2f21_9be7,
"Perlin/OctavePerlin fingerprint changed"
);
}
}

View File

@@ -18,8 +18,10 @@ fn bench_noise_router_creation(c: &mut Criterion) {
let proto_routers = ProtoNoiseRouters::generate(base_routers, &random_config);
let proto_noise_router = proto_routers.noise;
// Production overworld cell geometry (384 block world height / 8 = 48 vertical
// cells). the previous vertical_cell_count of 4 under-measured by ~12x.
let builder_options =
ChunkNoiseFunctionBuilderOptions::new(4, 8, 4, 4, 0, 0, 3, vec![], vec![], None);
ChunkNoiseFunctionBuilderOptions::new(4, 8, 48, 4, 0, 0, 4, vec![], vec![], None);
// Benchmarking
c.bench_function("noise_router_creation_with_pooling", |b| {

View File

@@ -176,6 +176,18 @@ macro_rules! local_y {
};
}
macro_rules! from_grid_xz {
($grid:expr, $offset:expr) => {
($grid << 4) + $offset
};
}
macro_rules! from_grid_y {
($grid:expr, $offset:expr) => {
$grid * 12 + $offset
};
}
pub struct WorldAquiferSampler {
fluid_level_sampler: StandardChunkFluidLevelSampler,
start_x: i32,
@@ -185,6 +197,11 @@ pub struct WorldAquiferSampler {
size_z: usize,
levels: Box<[Option<FluidLevel>]>,
packed_positions: Box<[i64]>,
surface_level_sample_min_x: i32,
surface_level_sample_min_z: i32,
surface_level_sample_max_x: i32,
surface_level_sample_max_z: i32,
skip_sampling_above_y: Option<i32>,
}
impl WorldAquiferSampler {
@@ -225,6 +242,11 @@ impl WorldAquiferSampler {
let end_z = local_xz!(chunk_pos::end_block_z(chunk_z)) + 1;
let size_z = (end_z - start_z) as usize + 1;
let surface_level_sample_min_x = from_grid_xz!(start_x, 0);
let surface_level_sample_min_z = from_grid_xz!(start_z, 0);
let surface_level_sample_max_x = from_grid_xz!(end_x, 9);
let surface_level_sample_max_z = from_grid_xz!(end_z, 9);
let cache_size = size_x * size_y * size_z;
let mut packed_positions = vec![0; cache_size];
@@ -258,9 +280,48 @@ impl WorldAquiferSampler {
size_z,
levels: vec![None; cache_size as usize].into(),
packed_positions: packed_positions.into(),
surface_level_sample_min_x,
surface_level_sample_min_z,
surface_level_sample_max_x,
surface_level_sample_max_z,
skip_sampling_above_y: None,
}
}
fn skip_sampling_above_y(
cache: &mut Option<i32>,
surface_level_sample_min_x: i32,
surface_level_sample_min_z: i32,
surface_level_sample_max_x: i32,
surface_level_sample_max_z: i32,
height_estimator: &mut SurfaceHeightEstimateSampler,
) -> i32 {
if let Some(y) = *cache {
return y;
}
let mut max_surface_level = i32::MIN;
let mut z = surface_level_sample_min_z;
while z <= surface_level_sample_max_z {
let mut x = surface_level_sample_min_x;
while x <= surface_level_sample_max_x {
let level = height_estimator.estimate_height(x, z);
if level > max_surface_level {
max_surface_level = level;
}
x += 4;
}
z += 4;
}
let adjusted_surface_level = max_surface_level + 8;
let skip_sampling_above_grid_y = local_y!(adjusted_surface_level + 12) + 1;
let y = from_grid_y!(skip_sampling_above_grid_y, 11) - 1;
*cache = Some(y);
y
}
fn checked_packed_position_index(&self, x: i32, y: i32, z: i32) -> Option<usize> {
let local_x = usize::try_from(x - self.start_x).ok()?;
let local_y = usize::try_from(y - self.start_y).ok()?;
@@ -584,6 +645,19 @@ impl WorldAquiferSampler {
let fluid_level = self
.fluid_level_sampler
.get_fluid_level(sample_x, sample_y, sample_z);
let skip_sampling_above_y = Self::skip_sampling_above_y(
&mut self.skip_sampling_above_y,
self.surface_level_sample_min_x,
self.surface_level_sample_min_z,
self.surface_level_sample_max_x,
self.surface_level_sample_max_z,
height_estimator,
);
if sample_y > skip_sampling_above_y {
return (Some(fluid_level.get_block(sample_y).default_state), false);
}
if fluid_level.get_block(sample_y) == &LAVA_BLOCK {
return (Some(LAVA_BLOCK.default_state), false);
}
@@ -606,20 +680,21 @@ impl WorldAquiferSampler {
let dy = block_pos::unpack_y(packed) - sample_y;
let dz = block_pos::unpack_z(packed) - sample_z;
let h = dx * dx + dy * dy + dz * dz;
if nearest[3].1 > h {
nearest[3] = (packed, h);
}
if nearest[2].1 > h {
nearest[3] = nearest[2];
nearest[2] = (packed, h);
}
if nearest[1].1 > h {
nearest[2] = nearest[1];
nearest[1] = (packed, h);
}
if nearest[0].1 > h {
nearest[1] = nearest[0];
nearest[0] = (packed, h);
if nearest[2].1 > h {
nearest[3] = nearest[2];
nearest[2] = (packed, h);
}
if nearest[1].1 > h {
nearest[2] = nearest[1];
nearest[1] = (packed, h);
}
if nearest[0].1 > h {
nearest[1] = nearest[0];
nearest[0] = (packed, h);
}
}
}};
}

View File

@@ -5,7 +5,7 @@ use super::{
chunk_noise_router::{ChunkNoiseFunctionComponent, MutableChunkNoiseFunctionComponentImpl},
density_function::{IndexToNoisePos, NoiseFunctionComponentRange},
};
use pumpkin_util::math::{lerp, lerp3, vector3::Vector3};
use pumpkin_util::math::{lerp, lerp2, lerp3, vector3::Vector3};
use crate::generation::{biome_coords, positions::chunk_pos};
@@ -369,10 +369,46 @@ impl MutableChunkNoiseFunctionComponentImpl for DensityInterpolator {
sample_options: &mut ChunkNoiseFunctionSampleOptions,
) {
if sample_options.populating_caches {
let mut cached_xy_delta: Option<(f64, f64)> = None;
let mut cached_a = 0.0;
let mut cached_b = 0.0;
array.iter_mut().enumerate().for_each(|(index, value)| {
let pos = mapper.at(index, Some(sample_options));
let result = self.sample(component_stack, &pos, sample_options);
*value = result;
let SampleAction::CellCaches(WrapperData {
x_delta,
y_delta,
z_delta,
..
}) = &sample_options.action
else {
*value = self.sample(component_stack, &pos, sample_options);
return;
};
let (x_delta, y_delta, z_delta) = (*x_delta, *y_delta, *z_delta);
if cached_xy_delta != Some((x_delta, y_delta)) {
cached_a = lerp2(
x_delta,
y_delta,
self.first_pass[0],
self.first_pass[4],
self.first_pass[2],
self.first_pass[6],
);
cached_b = lerp2(
x_delta,
y_delta,
self.first_pass[1],
self.first_pass[5],
self.first_pass[3],
self.first_pass[7],
);
cached_xy_delta = Some((x_delta, y_delta));
}
*value = lerp(z_delta, cached_a, cached_b);
});
} else {
ChunkNoiseFunctionComponent::fill_from_stack(
@@ -721,19 +757,6 @@ impl CellCache {
max_value,
}
}
/// Clones this instance, creating a new struct taking ownership of the cache and replacing the
/// original with a dummy
fn take_cache_clone(&mut self) -> Self {
let mut cache: Box<[f64]> = Box::new([]);
mem::swap(&mut cache, &mut self.cache);
Self {
input_index: self.input_index,
cache,
min_value: self.min_value,
max_value: self.max_value,
}
}
}
pub enum ChunkSpecificNoiseFunctionComponent {

View File

@@ -1,5 +1,3 @@
use std::cell::RefCell;
use pumpkin_data::noise_router::WrapperType;
use pumpkin_util::math::vector3::Vector3;
@@ -68,109 +66,10 @@ pub trait MutableChunkNoiseFunctionComponentImpl {
pub enum ChunkNoiseFunctionComponent<'a> {
Independent(&'a IndependentProtoNoiseFunctionComponent),
Dependent(&'a DependentProtoNoiseFunctionComponent),
// NOTE: The box here is intentional: we want to bring down the size to keep the component stack
// smaller
Chunk(ChunkSpecificNoiseFunctionComponent),
PassThrough(PassThrough),
//Panic(String),
}
/*
impl ChunkNoiseFunctionComponent<'_> {
pub fn display_test(&self, stack: &[ChunkNoiseFunctionComponent<'_>]) -> String {
match self {
Self::Independent(independent) => match independent {
IndependentProtoNoiseFunctionComponent::ClampedYGradient(_) => {
"ClampedYGradient".into()
}
IndependentProtoNoiseFunctionComponent::InterpolatedNoise(_) => {
"InterpolatedNoise".into()
}
IndependentProtoNoiseFunctionComponent::EndIsland(_) => "EndIsland".into(),
IndependentProtoNoiseFunctionComponent::Constant(_) => "Constant".into(),
IndependentProtoNoiseFunctionComponent::Noise(_) => "Noise".into(),
IndependentProtoNoiseFunctionComponent::ShiftA(_) => "ShiftA".into(),
IndependentProtoNoiseFunctionComponent::ShiftB(_) => "ShiftB".into(),
},
Self::Dependent(dependent) => match dependent {
DependentProtoNoiseFunctionComponent::Spline(spine) => {
let a = stack[spine.spline.input_index].display_test(stack);
format!("Spline({})", a)
}
DependentProtoNoiseFunctionComponent::Unary(x) => {
let a = stack[x.input_index].display_test(stack);
format!("Unary({})", a)
}
DependentProtoNoiseFunctionComponent::ShiftedNoise(x) => {
let a = stack[x.input_x_index].display_test(stack);
let b = stack[x.input_y_index].display_test(stack);
let c = stack[x.input_z_index].display_test(stack);
format!("ShiftedNoise({}, {}, {})", a, b, c)
}
DependentProtoNoiseFunctionComponent::Linear(x) => {
let a = stack[x.input_index].display_test(stack);
format!("Linear({})", a)
}
DependentProtoNoiseFunctionComponent::Binary(x) => {
let a = stack[x.input1_index].display_test(stack);
let b = stack[x.input2_index].display_test(stack);
format!("Binary({}, {})", a, b)
}
DependentProtoNoiseFunctionComponent::IntervalSelect(x) => {
let a = stack[x.input_index].display_test(stack);
format!("IntervalSelect({})", a)
}
DependentProtoNoiseFunctionComponent::Clamp(x) => {
let a = stack[x.input_index].display_test(stack);
format!("Clamp({})", a)
}
DependentProtoNoiseFunctionComponent::RangeChoice(x) => {
let when_in = stack[x.when_in_index].display_test(stack);
let when_out = stack[x.when_out_index].display_test(stack);
format!("RangeChoice({}, {})", when_in, when_out)
}
DependentProtoNoiseFunctionComponent::FindTopSurface(_) => {
format!("FindTopSurface")
}
},
Self::Chunk(chunk) => match &**chunk {
ChunkSpecificNoiseFunctionComponent::CellCache(x) => {
let input = &stack[x.input_index];
let input_display = input.display_test(stack);
format!("CellCache({})", input_display)
}
ChunkSpecificNoiseFunctionComponent::Cache2D(x) => {
let input = &stack[x.input_index];
let input_display = input.display_test(stack);
format!("Cache2D({})", input_display)
}
ChunkSpecificNoiseFunctionComponent::DensityInterpolator(x) => {
let input = &stack[x.input_index];
let input_display = input.display_test(stack);
format!("DensityInterpolator({})", input_display)
}
ChunkSpecificNoiseFunctionComponent::FlatCache(x) => {
let input = &stack[x.input_index];
let input_display = input.display_test(stack);
format!("FlatCache({})", input_display)
}
ChunkSpecificNoiseFunctionComponent::CacheOnce(x) => {
let input = &stack[x.input_index];
let input_display = input.display_test(stack);
format!("CacheOnce({})", input_display)
}
},
Self::PassThrough(x) => {
let input = &stack[x.input_index()];
let input_display = input.display_test(stack);
format!("PassThrough({})", input_display)
}
Self::Panic(_) => unreachable!(),
}
}
}
*/
impl NoiseFunctionComponentRange for ChunkNoiseFunctionComponent<'_> {
#[inline]
fn min(&self) -> f64 {
@@ -179,7 +78,6 @@ impl NoiseFunctionComponentRange for ChunkNoiseFunctionComponent<'_> {
Self::Dependent(dependent) => dependent.min(),
Self::Chunk(chunk) => chunk.min(),
Self::PassThrough(pass_through) => pass_through.min(),
//Self::Panic(message) => panic!("{}", message),
}
}
@@ -190,7 +88,6 @@ impl NoiseFunctionComponentRange for ChunkNoiseFunctionComponent<'_> {
Self::Dependent(dependent) => dependent.max(),
Self::Chunk(chunk) => chunk.max(),
Self::PassThrough(pass_through) => pass_through.max(),
//Self::Panic(message) => panic!("{}", message),
}
}
}
@@ -212,7 +109,6 @@ impl MutableChunkNoiseFunctionComponentImpl for ChunkNoiseFunctionComponent<'_>
pos,
sample_options,
),
//Self::Panic(message) => panic!("{}", message),
}
}
@@ -240,10 +136,6 @@ impl MutableChunkNoiseFunctionComponentImpl for ChunkNoiseFunctionComponent<'_>
}
}
thread_local! {
static TOPO_FILL_BUFFERS: RefCell<Vec<Vec<f64>>> = const { RefCell::new(Vec::new()) };
}
impl ChunkNoiseFunctionComponent<'_> {
#[inline]
pub fn sample_from_stack(

View File

@@ -27,23 +27,6 @@ impl SplineValue {
}
}
#[inline]
pub(crate) fn sample_with_buffers(
&self,
buffers: &[Vec<f64>],
elem_idx: usize,
pos: &Vector3<i32>,
component_stack: &mut [ChunkNoiseFunctionComponent],
sample_options: &ChunkNoiseFunctionSampleOptions,
) -> f32 {
match self {
Self::Fixed(fixed) => *fixed,
Self::Spline(spline) => {
spline.sample_with_buffers(buffers, elem_idx, pos, component_stack, sample_options)
}
}
}
#[inline]
fn calculate_min_and_max(&self, component_stack: &[ProtoNoiseFunctionComponent]) -> (f32, f32) {
match self {
@@ -217,75 +200,6 @@ impl Spline {
cubic_part + linear_part
}
pub(crate) fn sample_with_buffers(
&self,
buffers: &[Vec<f64>],
elem_idx: usize,
pos: &Vector3<i32>,
component_stack: &mut [ChunkNoiseFunctionComponent],
sample_options: &ChunkNoiseFunctionSampleOptions,
) -> f32 {
let location = buffers[self.input_index][elem_idx] as f32;
let n = self.points.len();
let index_greater_than_x = self.points.partition_point(|p| location >= p.location);
if index_greater_than_x == 0 {
let point = &self.points[0];
let val = point.value.sample_with_buffers(
buffers,
elem_idx,
pos,
component_stack,
sample_options,
);
return point.sample_outside_range(location, val);
}
if index_greater_than_x == n {
let point = &self.points[n - 1];
let val = point.value.sample_with_buffers(
buffers,
elem_idx,
pos,
component_stack,
sample_options,
);
return point.sample_outside_range(location, val);
}
let lower_point = &self.points[index_greater_than_x - 1];
let upper_point = &self.points[index_greater_than_x];
let lower_value = lower_point.value.sample_with_buffers(
buffers,
elem_idx,
pos,
component_stack,
sample_options,
);
let upper_value = upper_point.value.sample_with_buffers(
buffers,
elem_idx,
pos,
component_stack,
sample_options,
);
let dist = upper_point.location - lower_point.location;
let x_scale = (location - lower_point.location) / dist;
let delta = upper_value - lower_value;
let extrapolated_lower = lower_point.derivative * dist - delta;
let extrapolated_upper = -upper_point.derivative * dist + delta;
let cubic_part =
(x_scale * (1.0 - x_scale)) * lerp(x_scale, extrapolated_lower, extrapolated_upper);
let linear_part = lerp(x_scale, lower_value, upper_value);
cubic_part + linear_part
}
}
pub struct SplineFunction {

View File

@@ -3,6 +3,8 @@ pub mod chunk_noise_router;
pub mod density_function;
pub mod find_top_surface;
pub mod multi_noise_sampler;
#[cfg(test)]
mod parity_fingerprint_test;
pub mod proto_noise_router;
pub mod static_router;
pub mod surface_height_sampler;

View File

@@ -0,0 +1,70 @@
use pumpkin_data::noise_router::OVERWORLD_BASE_NOISE_ROUTER;
use pumpkin_util::math::vector3::Vector3;
use crate::generation::GlobalRandomConfig;
use crate::generation::noise::router::chunk_density_function::{
ChunkNoiseFunctionBuilderOptions, ChunkNoiseFunctionSampleOptions, SampleAction,
};
use crate::generation::noise::router::chunk_noise_router::ChunkNoiseRouter;
use crate::generation::noise::router::proto_noise_router::ProtoNoiseRouters;
fn fnv1a_hash_f64(values: impl Iterator<Item = f64>) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
let mut hash = FNV_OFFSET;
for value in values {
for byte in value.to_bits().to_le_bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(FNV_PRIME);
}
}
hash
}
/// This just detects if a mass set of hashes is the same as it was previously, should reliably detect regressions.
#[test]
fn overworld_density_fingerprint_is_stable() {
let mut results = Vec::new();
for seed in [0u64, 1, 42, 1_779_920_288_596_261_407] {
let random_config = GlobalRandomConfig::new(seed, false);
let proto_routers =
ProtoNoiseRouters::generate(&OVERWORLD_BASE_NOISE_ROUTER, &random_config);
let builder_options = ChunkNoiseFunctionBuilderOptions::new(
4,
8,
48,
4,
0,
0,
4,
Vec::new(),
Vec::new(),
None,
);
let mut router = ChunkNoiseRouter::generate(&proto_routers.noise, &builder_options);
let options =
ChunkNoiseFunctionSampleOptions::new(false, SampleAction::SkipCellCaches, 0, 0, 0);
for x in (-64..64).step_by(11) {
for y in (-64..320).step_by(19) {
for z in (-64..64).step_by(13) {
let pos = Vector3::new(x, y, z);
results.push(router.final_density(&pos, &options));
results.push(router.vein_toggle(&pos, &options));
results.push(router.vein_ridged(&pos, &options));
results.push(router.vein_gap(&pos, &options));
}
}
}
}
let hash = fnv1a_hash_f64(results.into_iter());
assert_eq!(
hash, 0x31d6_54d7_22dd_0134,
"Overworld density fingerprint changed"
);
}