From ff57b6e282f83c68990ae965bf1ddd18ce79c1fc Mon Sep 17 00:00:00 2001 From: Bjorn Beishline <75190918+BjornTheProgrammer@users.noreply.github.com> Date: Sun, 9 Aug 2026 03:04:28 -0700 Subject: [PATCH] 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 --- Cargo.lock | 1 + crates/pumpkin-util/Cargo.toml | 7 ++ crates/pumpkin-util/benches/perlin.rs | 54 +++++++++ crates/pumpkin-util/src/noise/mod.rs | 6 +- crates/pumpkin-util/src/noise/perlin.rs | 59 ++++++++++ crates/pumpkin-world/benches/noise_router.rs | 4 +- .../src/generation/noise/aquifer_sampler.rs | 99 ++++++++++++++-- .../noise/router/chunk_density_function.rs | 55 ++++++--- .../noise/router/chunk_noise_router.rs | 108 ------------------ .../noise/router/density_function/spline.rs | 86 -------------- .../src/generation/noise/router/mod.rs | 2 + .../noise/router/parity_fingerprint_test.rs | 70 ++++++++++++ 12 files changed, 327 insertions(+), 224 deletions(-) create mode 100644 crates/pumpkin-util/benches/perlin.rs create mode 100644 crates/pumpkin-world/src/generation/noise/router/parity_fingerprint_test.rs diff --git a/Cargo.lock b/Cargo.lock index 35693f13d..cb9ea4a7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3424,6 +3424,7 @@ dependencies = [ "base64 0.23.1", "bytes", "colored", + "criterion", "crypto-bigint 0.7.5", "ecdsa 0.17.0", "md5", diff --git a/crates/pumpkin-util/Cargo.toml b/crates/pumpkin-util/Cargo.toml index 69f8e49c3..a44a794ee 100644 --- a/crates/pumpkin-util/Cargo.toml +++ b/crates/pumpkin-util/Cargo.toml @@ -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 diff --git a/crates/pumpkin-util/benches/perlin.rs b/crates/pumpkin-util/benches/perlin.rs new file mode 100644 index 000000000..21759acc0 --- /dev/null +++ b/crates/pumpkin-util/benches/perlin.rs @@ -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::>()); + let sampler = OctavePerlinNoiseSampler::new(&mut rand, start, &litudes, 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); diff --git a/crates/pumpkin-util/src/noise/mod.rs b/crates/pumpkin-util/src/noise/mod.rs index 486eb8a3d..4f0b060f3 100644 --- a/crates/pumpkin-util/src/noise/mod.rs +++ b/crates/pumpkin-util/src/noise/mod.rs @@ -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 } } diff --git a/crates/pumpkin-util/src/noise/perlin.rs b/crates/pumpkin-util/src/noise/perlin.rs index 966f34dd3..4198d1371 100644 --- a/crates/pumpkin-util/src/noise/perlin.rs +++ b/crates/pumpkin-util/src/noise/perlin.rs @@ -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) -> 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::>()); + let sampler = OctavePerlinNoiseSampler::new(&mut rand, start, &litudes, 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" + ); + } } diff --git a/crates/pumpkin-world/benches/noise_router.rs b/crates/pumpkin-world/benches/noise_router.rs index f7a801d36..a11521641 100644 --- a/crates/pumpkin-world/benches/noise_router.rs +++ b/crates/pumpkin-world/benches/noise_router.rs @@ -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| { diff --git a/crates/pumpkin-world/src/generation/noise/aquifer_sampler.rs b/crates/pumpkin-world/src/generation/noise/aquifer_sampler.rs index e8a891661..4af9fa2b2 100644 --- a/crates/pumpkin-world/src/generation/noise/aquifer_sampler.rs +++ b/crates/pumpkin-world/src/generation/noise/aquifer_sampler.rs @@ -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]>, 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, } 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, + 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 { 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); + } } }}; } diff --git a/crates/pumpkin-world/src/generation/noise/router/chunk_density_function.rs b/crates/pumpkin-world/src/generation/noise/router/chunk_density_function.rs index babd8c7b6..6bd67467c 100644 --- a/crates/pumpkin-world/src/generation/noise/router/chunk_density_function.rs +++ b/crates/pumpkin-world/src/generation/noise/router/chunk_density_function.rs @@ -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 { diff --git a/crates/pumpkin-world/src/generation/noise/router/chunk_noise_router.rs b/crates/pumpkin-world/src/generation/noise/router/chunk_noise_router.rs index e41ed9a3e..d8b2af1bc 100644 --- a/crates/pumpkin-world/src/generation/noise/router/chunk_noise_router.rs +++ b/crates/pumpkin-world/src/generation/noise/router/chunk_noise_router.rs @@ -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>> = const { RefCell::new(Vec::new()) }; -} - impl ChunkNoiseFunctionComponent<'_> { #[inline] pub fn sample_from_stack( diff --git a/crates/pumpkin-world/src/generation/noise/router/density_function/spline.rs b/crates/pumpkin-world/src/generation/noise/router/density_function/spline.rs index 26cf05382..1e5f4cb20 100644 --- a/crates/pumpkin-world/src/generation/noise/router/density_function/spline.rs +++ b/crates/pumpkin-world/src/generation/noise/router/density_function/spline.rs @@ -27,23 +27,6 @@ impl SplineValue { } } - #[inline] - pub(crate) fn sample_with_buffers( - &self, - buffers: &[Vec], - elem_idx: usize, - pos: &Vector3, - 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], - elem_idx: usize, - pos: &Vector3, - 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 { diff --git a/crates/pumpkin-world/src/generation/noise/router/mod.rs b/crates/pumpkin-world/src/generation/noise/router/mod.rs index dc5514519..6be063910 100644 --- a/crates/pumpkin-world/src/generation/noise/router/mod.rs +++ b/crates/pumpkin-world/src/generation/noise/router/mod.rs @@ -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; diff --git a/crates/pumpkin-world/src/generation/noise/router/parity_fingerprint_test.rs b/crates/pumpkin-world/src/generation/noise/router/parity_fingerprint_test.rs new file mode 100644 index 000000000..ed47efa87 --- /dev/null +++ b/crates/pumpkin-world/src/generation/noise/router/parity_fingerprint_test.rs @@ -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) -> 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" + ); +}